Java反序列化基础
2019-07-14 16:21:04

[toc]

JAVA的序列化和反序列化

Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。把字节序列恢复为对象的过程称为对象的反序列化。

  • 序列化就是把对象的状态信息转换为字节序列(即可以存储或传输的形式)过程
  • 反序列化即逆过程,由字节流还原成对象

位置: Java.io.ObjectOutputStream   java.io.ObjectInputStream 序列化:  ObjectOutputStream –> writeObject()该方法对参数指定的obj对象进行序列化,把字节序列写到一个目标输出流中,按Java的标准约定是给文件一个.ser扩展名。 反序列化: ObjectInputStream –> readObject() 该方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。 注:实现SerializableExternalizable接口的类的对象才能被序列化。 并不是一个实现了序列化接口的类的所有字段及属性,都是可以序列化的:

  • 如果该类有父类,则分两种情况来考虑:如果该父类已经实现了可序列化接口,则其父类的相应字段及属性的处理和该类相同;如果该类的父类没有实现可序列化接口,则该类的父类所有的字段属性将不会序列化,并且反序列化时会调用父类的默认构造函数来初始化父类的属性,而子类却不调用默认构造函数,而是直接从流中恢复属性的值。
  • 如果该类的某个属性标识为static类型的,则该属性不能序列化。
  • 如果该类的某个属性采用transient关键字标识,则该属性不能序列化。

Demo

一般方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.text.MessageFormat;
import java.io.Serializable;

class Person implements Serializable {

/**
* 序列化ID
*/
private static final long serialVersionUID = -5809782578272943999L;


private int age;
private String name;
private String sex;

public int getAge() {
return age;
}

public String getName() {
return name;
}

public String getSex() {
return sex;
}

public void setAge(int age) {
this.age = age;
}

public void setName(String name) {
this.name = name;
}

public void setSex(String sex) {
this.sex = sex;
}
}

/**
* ClassName: SerializeAndDeserialize
* Description: 测试对象的序列化和反序列
*/
public class SerializeDeserialize_readObject {

public static void main(String[] args) throws Exception {
SerializePerson();//序列化Person对象
Person p = DeserializePerson();//反序列Perons对象
System.out.println(MessageFormat.format("name={0},age={1},sex={2}",
p.getName(), p.getAge(), p.getSex()));
}

/**
* MethodName: SerializePerson
* Description: 序列化Person对象
*/
private static void SerializePerson() throws FileNotFoundException,
IOException {
Person person = new Person();
person.setName("ssooking");
person.setAge(20);
person.setSex("男");
// ObjectOutputStream 对象输出流,将Person对象存储到Person.txt文件中,完成对Person对象的序列化操作
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(
new File("Person.txt")));
oo.writeObject(person);
System.out.println("Person对象序列化成功!");
oo.close();
}

/**
* MethodName: DeserializePerson
* Description: 反序列Perons对象
*/
private static Person DeserializePerson() throws Exception, IOException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("Person.txt")));
/*
FileInputStream fis = new FileInputStream("Person.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
*/
Person person = (Person) ois.readObject();
System.out.println("Person对象反序列化成功!");
return person;
}

}

自定义序列化的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class SeriDemo1 implements Serializable {
private String name;
transient private String password;
// 瞬态,不可序列化状态,该字段的生命周期仅存于调用者的内存中
public SeriDemo1() {
}
public SeriDemo1(String name, String password) {
this.name = name;
this.password = password;
}
//模拟对密码进行加密
private String change(String password) {
return password + "minna";
}
//写入
private void writeObject(ObjectOutputStream outStream) throws IOException {
outStream.defaultWriteObject();
outStream.writeObject(change(password));
}
//读取
private void readObject(ObjectInputStream inStream) throws IOException,
ClassNotFoundException {
inStream.defaultReadObject();
String strPassowrd = (String) inStream.readObject();
//模拟对密码解密
password = strPassowrd.substring(0, strPassowrd.length() - 5);
}
//返回一个“以文本方式表示”此对象的字符串
public String toString() {
return "SeriDemo1 [name=" + name + ", password=" + password + "]";
}
//静态的main
public static void main(String[] args) throws Exception {
SeriDemo1 demo = new SeriDemo1("haom", "0123");
ByteArrayOutputStream buf = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(buf);
out.writeObject(demo);
ObjectInputStream in = new ObjectInputStream(new
ByteArrayInputStream(buf.toByteArray()));
demo = (SeriDemo1) in.readObject();
System.out.println(demo);
}
}

JMX

 JMX(Java Management Extensions)是一个为应用程序植入管理功能的框架。JMX是一套标准的代理和服务,实际上,用户可以在任何Java应用程序中使用这些代理和服务实现管理,JMX让程序有被管理的功能。 JMX分为三层,分别负责处理不同的事务:1、Instrumentation 层: Instrumentation层主要包括了一系列的接口定义和描述如何开发MBean的规范。通常JMX所管理的资源有一个或多个MBean组成,因此这个资源可以是任何由Java语言开发的组件,或是一个JavaWrapper包装的其他语言开发的资源。2、Agent 层: Agent用来管理相应的资源,并且为远端用户提供访问的接口。Agent层构建在Intrumentation层之上,并且使用管理Instrumentation层内部的组件。通常Agent由一个MBeanServer组成。另外Agent还提供一个或多个Adapter或Connector以供外界的访问3、Distributed 层: Distributed层关心Agent如何被远端用户访问的细节。它定义了一系列用来访问Agent的接口和组件,包括Adapter和Connector的描述。 JMX的连接方式:

  • RMI Connector 用JAVA的RMI功能来实现,可以在本地调用接口对象,这个不多说,因为只能是JAVA客户端使用,实现不了异构系统。
  • JMXMP Connector 就是使用协议在通讯了,Java也有实现这种连接的API,其他语言肯定也有的,这个能实现异构系统调用,是RMC模式。
  • Jolokia是一个JMX-HTTP桥梁,它提供了一种访问JMX bean的替代方法,可以在管理HTTP服务器上使用/jolokia进行访问。

http://www.voidcn.com/article/p-roccfzao-bdv.htmlhttp://www.blogjava.net/mlh123caoer/archive/2014/01/22/142456.htmlhttp://www.paraller.com/2017/05/22/JMX%E7%9A%84%E7%90%86%E8%A7%A3%E4%B8%8E%E5%AE%9E%E9%99%85%E7%94%A8%E9%80%94/

RMI

RMI(Remote Method Invocation)为远程方法调用,是允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。 这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。 RMI目前使用Java远程消息交换协议JRMP(Java Remote Messaging Protocol)进行通信。由于JRMP是专为Java对象制定的,Java RMI具有Java的”Write Once,Run Anywhere”的优点,是分布式应用系统的百分之百纯Java解决方案。用Java RMI开发的应用系统可以部署在任何支持JRE(Java Run Environment Java,运行环境)的平台上。但由于JRMP是专为Java对象制定的,因此,RMI对于用非Java语言开发的应用系统的支持不足。不能与用非Java语言书写的对象进行通信。image.pngimage.pngJava RMI 简单示例

  1. 定义一个远程方法接口,这个接口需要继承Remote接口,这个接口中的方法必须声明RemoteException异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* IHello.java */
package mytest;
/*
* 在Java中,只要一个类extends了java.rmi.Remote接口,即可成为存在于服务器端的远程对象,
* 供客户端访问并提供一定的服务。JavaDoc描述:Remote 接口用于标识其方法可以从非本地虚拟机上
* 调用的接口。任何远程对象都必须直接或间接实现此接口。只有在“远程接口”
* (扩展 java.rmi.Remote 的接口)中指定的这些方法才可被远程调用。
*/
import java.rmi.Remote;

public interface IHello extends Remote {
    /* extends了Remote接口的类或者其他接口中的方法若是声明抛出了RemoteException异常,
    * 则表明该方法可被客户端远程访问调用。
    */
public String sayHello(String name) throws java.rmi.RemoteException;
}
  1. 创建远程方法接口实现类,需要继承UnicastRemoteObject类,并显示声明无参构造函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* HelloImpl.java */
package mytest;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

/*
* 远程对象必须实现java.rmi.server.UniCastRemoteObject类,这样才能保证客户端访问获得远程对象时,
* 该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为“存根”,
* 而服务器端本身已存在的远程对象则称之为“骨架”。其实此时的存根是客户端的一个代理,用于与服务器端的通信,
* 而骨架也可认为是服务器端的一个代理,用于接收客户端的请求之后调用远程方法来响应客户端的请求。
*/

/* java.rmi.server.UnicastRemoteObject构造函数中将生成stub和skeleton */
public class HelloImpl extends UnicastRemoteObject implements IHello {
// 这个实现必须有一个显式的构造函数,并且要抛出一个RemoteException异常
protected HelloImpl() throws RemoteException {
super();
}

private static final long serialVersionUID = 4077329331699640331L;
public String sayHello(String name) throws RemoteException {
return "Hello " + name + " ^_^ ";
}
}
  1. 创建服务器程序,在RMIREGISTRY注册表中注册远程对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* HelloServer.java */
package mytest;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

/* 注册远程对象,向客户端提供远程对象服务
* 远程对象是在远程服务上创建的,你无法确切地知道远程服务器上的对象的名称
* 但是,将远程对象注册到RMI Service之后,客户端就可以通过RMI Service请求
* 到该远程服务对象的stub了,利用stub代理就可以访问远程服务对象了
*/

public class HelloServer {
public static void main(String[] args) {
try {
IHello hello = new HelloImpl(); /* 生成stub和skeleton,并返回stub代理引用 */
/* 本地创建并启动RMI Service,被创建的Registry服务将在指定的端口上侦听到来的请求
* 实际上,RMI Service本身也是一个RMI应用,我们也可以从远端获取Registry:
* public interface Registry extends Remote;
* public static Registry getRegistry(String host, int port) throws RemoteException;
*/
LocateRegistry.createRegistry(1099);
/* 将stub代理绑定到Registry服务的URL上 */
java.rmi.Naming.rebind("rmi://localhost:1099/hello", hello);
System.out.print("Ready");
} catch (Exception e) {
e.printStackTrace();
}
}
}
  1. 客户端调用远程对象方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* Hello_RMI_Client.java */
package mytest;
import java.rmi.Naming;

/* 客户端向服务端请求远程对象服务 */
public class Hello_RMI_Client {
public static void main(String[] args) {
try {
/* 从RMI Registry中请求stub
             * 如果RMI Service就在本地机器上,URL就是:rmi://localhost:1099/hello
             * 否则,URL就是:rmi://RMIService_IP:1099/hello
             */
IHello hello = (IHello) Naming.lookup("rmi://localhost:1099/hello");
/* 通过stub调用远程接口实现 */
System.out.println(hello.sayHello("Smi1e"));
} catch (Exception e) {
e.printStackTrace();
}
}
}

RMI通信传输的数据是经过序列化的数据image.pngimage.png

JNDI

JNDI的全称是Java Naming Directory Interface,即java命名和目录接口,它允许java程序通过一个名字来访问真正的java对象。JNDI提供了API来访问命名目录服务,它独立于命名目录服务器。  JNDI中的命名(Naming),就是将Java对象以某个名称的形式绑定(binding)到一个容器环境(Context)中,以后调用容器环境(Context)的查找(lookup)方法又可以查找出某个名称所绑定的Java对象。JNDI中的目录(Directory)与文件系统中的目录概念有很大的不同,JNDI中的目录(Directory)是指将一个对象的所有属性信息保存到一个容器环境中。JNDI的目录(Directory)原理与JNDI的命名(Naming)原理非常相似,主要的区别在于目录容器环境中保存的是对象的属性信息,而不是对象本身,所以,目录提供的是对属性的各种操作。事实上,JNDI的目录(Directory)与命名(Naming)往往是结合在一起使用的,JNDI API中提供的代表目录容器环境的类为DirContextDirContextContext的子类,显然它除了能完成目录相关的操作外,也能完成所有的命名(Naming)操作。DirContext是对Context的扩展,它在Context的基础上增加了对目录属性的操作功能,可以在其中绑定对象的属性信息和查找对象的属性信息。 JNDI最根本的目的是java应用通过一个名字获取其他JVM中的数据 JNDI demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import javax.naming.*;
import java.util.*;
public class NamingServiceTest{
public static void main(String[] args) throws NamingException{
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.fscontext.RefFSContextFactory");
env.put(Context.PROVIDER_URL,"file:/e:/java");
/*
* INITIAL_CONTEXT_FACTORY:实际实例化的工厂类,由该工厂创建一个Context对象
* PROVIDER_URL:命名服务的提供者
* InitialContext对象的构造需要以上两个属性值,如果没指定的话,使用System.getProperty()来获取,
* 如果获取不到,就抛出异常
* */
Context ctx = new InitialContext(env);

Object file = ctx.lookup("ABCD.java");
System.out.println("abcd.java文件被绑定到了:"+file);

Object dir = ctx.lookup("books");
System.out.println("books文件夹被绑定到了:"+dir);

ctx.close();
}
}

abcd.java文件被绑定到了:e:\java\ABCD.java
books文件夹被绑定到了:com.sun.jndi.fscontext.RefFSContext@665753

JNDI RMI Server demo RMIServer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package mytest;

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.util.Properties;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class RMIServer {
public static void main(String[] args) {
try {
//注册RMI服务器端口
LocateRegistry.createRegistry(8080);
//建立RMI服务端接口实现对象
IHello server = new HelloImpl();
//设置JNDI属性
Properties properties = new Properties();
//RMI的JNDI工厂类
properties.setProperty(Context.INITIAL_CONTEXT_FACTORY , "com.sun.jndi.rmi.registry.RegistryContextFactory");
//RMI服务端的访问地址
properties.setProperty(Context.PROVIDER_URL, "rmi://localhost:8080");
//根据JNDI属性,创建上下文
InitialContext ctx = new InitialContext(properties);
//将服务端接口实现对象与JNDI命名绑定,这个地方写的并不是很规范
//如果在J2EE开发中,规范的写法是,绑定的名字要以java:comp/env/开头
ctx.bind("Hello123", server);
System.out.println("RMI与JNDI集成服务启动.等待客户端调用...");
} catch (RemoteException e) {
e.printStackTrace();
} catch (NamingException e) {
e.printStackTrace();
}
}
}

RMIClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package mytest;

import java.rmi.RemoteException;
import java.util.Properties;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class JNDIClient {
public static void main(String[] args) {
//设置JNDI属性
Properties properties = new Properties();
//RMI的JNDI工厂类
properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
//RMI服务端的访问地址
properties.setProperty(Context.PROVIDER_URL, "rmi://localhost:8080");
try {
InitialContext ctx = new InitialContext(properties);
IHello remote = (IHello) ctx.lookup("Hello123");
System.out.println(remote.sayHello("123"));
} catch (RemoteException e) {
e.printStackTrace();
} catch (NamingException e) {
e.printStackTrace();
}
}
}

java反射机制

反射 (Reflection) 是 Java 的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。 通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。程序中一般的对象的类型都是在编译期就确定下来的,而 Java 反射机制可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。所以我们可以通过反射机制直接创建对象,即使这个对象的类型在编译期是未知的 Java 反射主要提供以下功能:

  • 在运行时判断任意一个对象所属的类;
  • 在运行时构造任意一个类的对象
  • 在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法);
  • 在运行时调用任意一个对象的方法

反射的基本运用(反射相关的类一般都在 java.lang.relfect 包里)

  1. 获得 Class 对象
  • Class.forName()如果你知道某个类的名字,想获取到这个类,就可以使用forName获取。
  • Test.class 如果已经加载了某个类,只是想获取到它的java.lang.Class对象,那么直接调用它的class属性即可。这个方法其实不属于反射。
  • obj.getClass() 如果上下文中存在某个类的实例obj,可以直接通过obj.getClass()来获取他的类。
  • ClassLoader.getSystemClassLoader().loadClass("java.lang.Runtime") 类似的利用类加载机制,也可以获取 Class 对象 (转自p牛小密圈)
  1. 判断是否为某个类的实例: System.out.println(test instanceof TestClass);
  2. 创建实例:
  • 使用Class对象的newInstance()方法来创建Class对象对应类的实例。 (class.newInstance()只能调用这个class的无参构造函数)
1
2
Class<?> c = String.class;
Object str = c.newInstance();
  • 先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建实例。这种方法接收的参数是构造函数列表类型,因为构造函数也支持重载,所以必须用参数列表类型才能唯一确定一个构造函数。
1
2
3
4
5
6
7
//获取String所对应的Class对象
Class<?> c = String.class;
//获取String类带一个String参数的构造器
Constructor constructor = c.getConstructor(String.class);
//根据构造器创建实例
Object obj = constructor.newInstance("23333");
System.out.println(obj);
  1. 获取方法

获取某个Class对象的方法集合,主要有以下几个方法:

  • getDeclaredMethods 方法返回类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。
  • getMethods 方法返回某个类的所有公用(public)方法,包括其继承类的公用方法。
  • getMethod 方法返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应Class的对象。(方法名+参数的class类型。实际上,调用Class对象的getMethod()方法时,内部会循环遍历所有Method,然后根据方法名和参数类型匹配唯一的Method返回。)
1
2
3
4
5
6
7
8
9
10
public Method getMethod(String name, Class<?>... parameterTypes)

class methodClass {
public final int fuck = 3;
public int add(int a,int b) {
return a+b;
}
}
Class<?> c = methodClass.class;
Method method = c.getMethod("add", int.class, int.class);
  1. 获取构造器信息

获取类构造器的用法与上述获取方法的用法类似。主要是通过Class类的getConstructor方法得到Constructor类的一个实例,而Constructor类有一个newInstance方法可以创建一个对象实例:

1
public T newInstance(Object ... initargs)

此方法可以根据传入的参数来调用对应的Constructor创建对象实例。

  1. 获取类的成员变量(字段)信息
  • getFiled:访问公有的成员变量
  • getDeclaredField:所有已声明的成员变量,但不能得到其父类的成员变量
  1. 调用方法

当我们从类中获取了一个方法后,我们就可以用 invoke() 方法来调用这个方法。invoke 方法的原型为:

1
2
3
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class test1 {
public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Class<?> klass = methodClass.class;
//创建methodClass的实例
Object obj = klass.newInstance();
//获取methodClass类的add方法
Method method = klass.getMethod("add",int.class,int.class);
//调用method对应的方法 => add(1,4)
Object result = method.invoke(obj,1,4);
System.out.println(result);
}
}
class methodClass {
public final int fuck = 3;
public int add(int a,int b) {
return a+b;
}
public int sub(int a,int b) {
return a+b;
}
}

由于反射会额外消耗一定的系统资源,因此如果不需要动态地创建一个对象,那么就不需要用反射。另外,反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。 反射执行系统命令

1
2
3
4
5
6
7
8
import java.lang.reflect.Method;
//Runtime.getRuntime().exec("open -a Calculator");
public class ExecTest {
public static void main(String[] args) throws Exception{
Method method= Runtime.class.getMethod("exec", String.class);
Object result = method.invoke(Runtime.getRuntime(), "open -a Calculator");
}
}

Referer

java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一 Java反序列化漏洞分析 JAVA序列化和反序列化,以及漏洞补救 浅显易懂的JAVA反序列化入门 Java学习笔记(十六)——Java RMI java RMI原理详解JNDI详解 Java之JNDI详解 深入解析Java反射(1)

Prev
2019-07-14 16:21:04
Next