Java-web学习之路-反射机制

0x01 序言

  绝不咕咕咕,第二篇聊一下java下一个比较独特的东西 反射机制 ,这东西以前为了理解,可是搞了好久呢。

0x02 反射机制

一、何为反射

  反射 (Reflection) 是 Java 的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性,Oracle官网对这个特征的描述是这样的

Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions.
The API accommodates applications that need access to either the public members of a target object (based on its runtime class) or the members declared by a given class. It also allows programs to suppress default reflective access control.

  简单来说通过反射机制,我们可以java程序运行过程中获取到每一个类型的成员和成员的信息。
  首先 JVMjava虚拟机这个是大家众所周知的一个东西,并且 java 代码是运行在 JVM 之上。java 之所以能够拥有跨平台的特性,正是依赖于这个 JVM ,下图是一个 java 运行过程中的内存加载模型。

  下面可以看个例子方便理解,我们先创建两个类,分别是Reflection1和Reflection2。

1
2
3
4
5
6
//Reflection1.java
public class Reflection1{
public void doReflection1(){
System.out.println("Reflection1");
}
}

1
2
3
4
5
6
//Reflection2.java
public class Reflection2{
public void doReflection2(){
System.out.println("Reflection2");
}
}

正常我们要调用这两个类需要这样写。

1
2
3
4
5
6
public class ReflectionTest{
public static void main(String[] args){
//new Reflection1().doReflection1();
new Reflection2().doReflection2();
}
}


  首先你的这个代码会被编译为 .class 的文件,并且被类加载器加载进 jvm 的内存中,你的类 Reflection2 加载到方法区中,创建了 Reflection2 类的 class对象 到堆中,注意这个不是 new 出来的对象,而是类的类型对象,每个类只有一个 class对象 ,作为方法区类的数据结构的接口。 jvm 创建对象前,会先检查类是否加载,寻找类对应的 class对象 ,若加载好,则为你的对象分配内存,初始化也就是代码: new Reflection2
  而假设现在又有一个 Reflection1 的类,也就是我们代码中注释的部分,如果我们要对它进行调用,首先我们需要去掉这一行代码前面的两个反斜杠,然后在 javac Reflection1.java 对其进行编译,最后使用 java Reflection1 进行运行,得到我们想要的结果。
这样的过程是相当的麻烦,而为了解决这个需求,反射 (Reflection) 特性应需而生。
  我们先看个例子,如何在利用反射机制实现我们刚刚的需求,首先我们先创建一个 Reflection.txt 文件

1
2
class = Reflection1
method = doReflection1

  然后我们看一下实现代码

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
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Properties;

public class ReflectionTest2 {
@SuppressWarnings({"rowtypes","unchecked"})
public static void main(String[] args)
throws FileNotFoundException, IOException, ClassNotFoundException, NoSuchMethodException, SecurityException,
InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
File ConfigFile= new File("/Users/l1nk3r/Desktop/java reflect/Reflection.txt");
Properties ReflectionConfig = new Properties();
ReflectionConfig.load(new FileInputStream(ConfigFile));
String className = (String)ReflectionConfig.get("class");
String methodName = (String)ReflectionConfig.get("method");
//根据类,名称创建类对象
Class clazz = Class.forName(className);
//根据方法名称获取方法
Method m = clazz.getMethod(methodName);
//获取构造器
Constructor c = clazz.getConstructor();
//根据构造器实例化出相关对象
Object service = c.newInstance();
//调用对象的指定方法
m.invoke(service);
}
}

  那么如果我们需要调用 Reflection1 里的 doReflection1 ,只需要修改 Reflection.txt 文件,同理需要调用 Reflection2 里的 doReflection2 ,也是修改这个文件,最后下图是效果。

  所以上面例子中如果 Reflection.txt 文件没有进行相关类和方法的设置,那么对于程序运行来说是不知道运行对象是谁。所以java的反射是可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。
  Java 反射主要提供以下功能:

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

重点:是运行时而不是编译时

二、反射机制的使用

  首先从第一章节的样例代码中,我们看到了反射机制有几个类,他们分别是 classConstructorMethod 三个类,除此之外还有类没有出现在我们上面的例子中,它是 Field ,简单介绍一下这几个类的作用。

1
2
3
4
Class                ----类对象
Constructor ----类的构造器对象
Field ----类的属性对象
Method ----类的方法对象

  这可太真实了,java真实一门万物皆对象的语言(小声bb。而java反射机制的相关对象的类和方法均在一般都在 java.lang.relfect 包里。

1.获取类对象的办法

  在反射机制中,有三种方式可以获取到class对象,他们分别如下:

  1. 使用 Class 类的 forName 静态方法。
  2. 调用运行时类本身的 .class 属性
  3. 通过类的对象 .getClass()

下面可以看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class GetClassDemo {
public static void main(String[] args) throws ClassNotFoundException {
//以String类为例:
String str="hello world";
//方式一:通过Class类的静态方法forName
Class<?> clazz1 =Class.forName("java.lang.String");
//方式二:通过类的class属性
Class<?> clazz2 =String.class;
//方式三 :通过对象的getClass()方法
Class<?> clazz3 =str.getClass();
System.out.println(clazz1.getName());
System.out.println(clazz2.getName());
System.out.println(clazz3.getName());
}
}

  我们知道java下字符串常量是 java.lang.String 这个类实例的引用。我们在上面的例子中通过三种方式获取到相关类对象的名字。

2.类的构造器对象的方法

获取类构造器的方法有以下几种:

1
2
3
4
5
6
7
8
//根据指定参数获得public构造器
Constructor getConstructor(Class[] params);
//获得public的所有构造器
Constructor[] getConstructors();
//根据指定参数获得public和非public的构造器
Constructor getDeclaredConstructor(Class[] params);
//获取所有public和非public的构造器
Constructor[] getDeclaredConstructors();

  下面看个例子,例子中创建一个 UserInfo 的方法,并且设置四个类的构造器,其中三个是 public 属性,另一个是 private 属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//UserInfo.java
package com.l1nk3r.reflect;
public class UserInfo{

public UserInfo(){}

public UserInfo(int userId){
this.userId = userId;
}
private UserInfo(String name){
this.name = name;
}

public UserInfo(int userId, String name, int age) {
super();
this.userId = userId;
this.name = name;
this.age = age;
}
}

  我们看一下上面这几种方法最后的结果分别是:
  使用 getConstructors ,返回结果如下所示

  使用 getDeclaredConstructors ,返回结果如下所示,两者一对比,大家应该就明白了这两个方法差别在哪。

3.创建实例

  通过反射来生成对象主要有两种方式,一种是使用 Class 对象的 newInstance() 方法来创建Class对象对应类的实例;

  另一种是先通过 Class 对象获取指定的 Constructor 对象,再调用 Constructor 对象的 newInstance() 方法来创建实例。

  我们可以看到两个方法区别在于,第二个方法可以用指定的构造器构造类的实例,实际用起来也比较好用。

4.获取类声明的方法

  获取类声明的方法有以下这几种:

  • Field getField(String name);获得指定名字的公共属性
  • Field[] getFields();获得全部公共属性(包括继承父类,实现接口)
  • Field getDclaredField(String name);获得指定名字的属性
  • Field[] getDeclaredFields();获得当前类的全部属性

  下面可以看个例子,这里将name设置为public,stuid、sex、type设置为private,brith不进行设置。

1
2
3
4
5
6
7
8
//Student.java
public class Student {

private int stuid=1;
public String name="l1nk3r";
private String sex="男";
private String type="test";
Date birth;

  利用下面的代码运行结果一目了然,由于getField的方法要求必须为public声明,因此调用sex出现了报错

  我们将代码注释之后即可看到上面几种方式的分别实现效果。

5.获取类的方法对象方法

  • Method getMethod(String name, Class[] params) – 使用特定的参数类型,获得命名的公共方法
  • Method[] getMethods() – 获得类的所有公共方法
  • Method getDeclaredMethod(String name, Class[] params) – 使用特写的参数类型,获得类声明的命名的方法
  • Method[] getDeclaredMethods() – 获得类声明的所有方法

下面可以看个例子,这里将getSex、setSex、getType均设置为private,其他设置为public。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void setName(String name) {
this.name = name;
}
private String getSex() {
return sex;
}
private void setSex(String sex) {
this.sex = sex;
}
private String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}

  利用下面的代码运行结果一目了然,由于getMethod的方法要求必须为public声明,因此调用getSex出现了报错。

  注释掉报错继续运行下去,这几个方法的用法结果一目了然,两个区别在于 getMethodsgetMethod 无法获取 private 方法,而 getDeclaredMethodsgetDeclaredMethod 可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.l1nk3r.reflect;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class MethodTest {
public static void main(String[] args) throws Exception {
Student s=new Student();
Class<?> c=s.getClass();
//获得方法
//Method f1=c.getMethod("getSex");
//System.out.println("getMethod(\"getSex\")方法获取到的:"+f1);
Method[] f2=c.getMethods();
for(int i=0;i<f2.length;i++) {
System.out.println("getMethods()方法获取到的:"+f2[i]);
}
Method f3=c.getDeclaredMethod("getSex");
System.out.println("getDeclaredMethod(\"getSex\");方法获取到的:"+f3);
Method[] f4=c.getDeclaredMethods();
for(int i=0;i<f4.length;i++) {
System.out.println("getDeclaredMethods()方法获取到的:"+f4[i]);
}

}
}

6.调用方法

  前面我们说了一堆如何获取类,对象,方法,注册实例,声明等,但是最后我在拿到了我们想要的数据之后,还是需要调用该方法,调用方法使用的是 invoke 方法。我们可以看个例子,在例子中首先通过运行 methodClass.class 属性获取类对象的方法,然后通过 newInstance 方式,实例化 methodClass 类,再通过 getMothod 方法获取 methodClass 类中的 add 方法,最后通过 invoke 方法执行 methodClass 类中的 add 方法,并执行 1+4 的运算,最后结果为 5

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
package com.l1nk3r.reflect;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class InvokeTest {
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;
}
}

0x03应用场景

  反射机制存在很多java下的框架里,例如struts、spring这种,开发者为了保证框架的通用性,所以需要根据配置文件加载不同的类或者对象,调用不同的方法。
  例如 spring 框架下可以通过 XML 配置模式装载 Bean ,它的过程如下:

  1. 将程序内所有 XML 或 Properties 配置文件加载入内存中
  2. Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息
  3. 使用反射机制,根据这个字符串获得某个类的Class实例
  4. 动态配置实例的属性

  在Struts框架下也用到了反射机制,在运用 Struts2 框架的开发中我们一般会在 struts.xml 里去配置 Action ,比如:

1
2
3
4
5
<action name="login" class="org.l1nk3r.test.action.SimpleLoginAction"
method="execute">
<result>/index.jsp</result>
<result name="error">login.jsp</result>
</action>

  配置文件与 Action 建立了一种映射关系,当 View 层发出请求时,请求会被 StrutsPrepareAndExecuteFilter 拦截,然后 StrutsPrepareAndExecuteFilter 会去动态地创建 Action 实例。比如我们请求 login.action,那么 StrutsPrepareAndExecuteFilter 就会去解析 struts.xml 文件,检索 actionnameloginAction ,并根据 class 属性创建 SimpleLoginAction 实例,并用 invoke 方法来调用 execute 方法,这个过程离不开反射。
  还有在java底下使用数据库的时候,会经常看到下述相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ConnectionJDBC {  
//驱动程序就是之前在classpath中配置的JDBC的驱动程序的JAR 包中
public static final String DBDRIVER = "com.mysql.jdbc.Driver";
//连接地址是由各个数据库生产商单独提供的,所以需要单独记住
public static final String DBURL = "jdbc:mysql://localhost:3306/test";
//连接数据库的用户名
public static final String DBUSER = "root";
//连接数据库的密码
public static final String DBPASS = "";


public static void main(String[] args) throws Exception {
Connection con = null; //表示数据库的连接对象
Class.forName(DBDRIVER); //1、使用CLASS 类加载驱动程序 ,反射机制的体现
con = DriverManager.getConnection(DBURL,DBUSER,DBPASS); //2、连接数据库
System.out.println(con);
con.close(); // 3、关闭数据库
}

  上面的代码就是用到了反射机制,首先通过 Class.forName 加载数据库的驱动程序 (通过反射加载,前提是引入相关了Jar包),然后通过 DriverManager 类进行数据库的连接,连接的时候要输入数据库的连接地址、用户名、密码,最后通过 Connection 接口接收连接。

0x04 弹个计算器

  之前介绍反序列化的时候,介绍过我们可以通过重写readObject方法,达到反序列化过程可以任意执行命令的目的,现在我们换个方法,通过反射的方法重写readObject,然后反序列化的时候执行命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//ReflectionCalcObject.java
package com.l1nk3r.reflect;
import java.io.*;
import java.lang.reflect.Method;

class ReflectionCalcObject implements Serializable{
public String name;
//重写readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException{
in.defaultReadObject();//调用原始的readOject方法

try {//通过反射方法执行命令;
Method method= java.lang.Runtime.class.getMethod("exec", String.class);
Object result = method.invoke(Runtime.getRuntime(), "open /Applications/Calculator.app/");
}
catch(Exception e) {
e.printStackTrace();
}
}
}

关键在于这两行代码

1
2
Method method= java.lang.Runtime.class.getMethod("exec", String.class);
Object result = method.invoke(Runtime.getRuntime(), "open /Applications/Calculator.app/");

  通过运行 java.lang.Runtime 这个类的 .class 属性,并使用 getMethod 方法来获取我们要执行命令的方法 exec ,最后我们通过 invoke 来实现注册这个方法,打开计算器。

0x05 相关地址

  本次实验代码均在这里

Reference

什么是Java内存模型
深入解析Java反射(1) - 基础