关于java:长篇图解java反射机制及其应用场景

55次阅读

共计 8280 个字符,预计需要花费 21 分钟才能阅读完成。

一、什么是 java 反射?

在 java 的面向对象编程过程中,通常咱们须要先晓得一个 Class 类,而后 new 类名() 形式来获取该类的对象。也就是说咱们须要在写代码的时候(编译期或者编译期之前)就晓得咱们要实例化哪一个类,运行哪一个办法,这种通常被称为 动态的类加载

然而在有些场景下,咱们当时是不晓得咱们的代码的具体行为的。比方,咱们定义一个服务工作工作流,每一个服务工作都是对应的一个类的一个办法。

  • 服务工作 B 执行哪一个类的哪一个办法,是由服务工作 A 的执行后果决定的
  • 服务工作 C 执行哪一个类的哪一个办法,是由服务工作 A 和 B 的执行后果决定的
  • 并且用户不心愿服务工作的性能在代码中写死,心愿通过配置的形式执行不同的程序

面对这个状况,咱们就不能用代码 new 类名() 来实现了,因为你不晓得用户具体要怎么做配置,这一秒他心愿服务工作 A 执行 Xxxx 类的 x 办法,下一秒他可能心愿执行 Yyyy 类的 y 办法。当然你也能够说提需要嘛,用户改一次需要,我改一次代码。这种形式也能需要,但对于用户和程序员集体而言都是苦楚,那么有没有一种办法 在运行期动静的改变程序的调用行为的办法 呢?这就是要为大家介绍的“java 反射机制”。

那么 java 的反射机制可能做那些事呢?大略是这样几种:

  • 在程序运行期动静的依据 package 名. 类名 实例化类对象
  • 在程序运行期动静获取类对象的信息,包含对象的老本变量和办法
  • 在程序运行期动静应用对象的成员变量属性
  • 在程序运行期动静调用对象的办法(公有办法也能够调用)

二、Hello World

咱们定义一个类叫做 Student

package com.zimug.java.reflection;

public class Student {
    public String nickName;
    private Integer age;

    public void dinner(){System.out.println("吃晚餐!");
    }

    private void sleep(int minutes){System.out.println("睡" + minutes + "分钟");
    }
}

如果不必反射的形式,我置信只有学过 java 的敌人必定会调用 dinner 办法

Student student = new Student();
student.dinner();

如果是反射的形式咱们该怎么调用呢?

// 获取 Student 类信息
Class cls = Class.forName("com.zimug.java.reflection.Student");
// 对象实例化
Object obj = cls.getDeclaredConstructor().newInstance();
// 依据办法名获取并执行办法
Method dinnerMethod = cls.getDeclaredMethod("dinner");
dinnerMethod.invoke(obj);  // 打印:吃晚餐!

通过下面的代码咱们看到,com.zimug.java.reflection.Student 类名和 dinner 办法名是字符串。既然是字符串咱们就能够通过配置文件,或数据库、或什么其余的灵便配置办法来执行这段程序了。这就是反射最根底的应用形式。

三、类加载与反射关系

java 的类加载机制还是挺简单的,咱们这里为了不混同重点,只为大家介绍和“反射”有关系的一部分内容。

java 执行编译的时候将 java 文件编译成字节码 class 文件, 类加载器在类加载阶段将 class 文件加载到内存,并实例化一个 java.lang.Class 的对象。比方:对于 Student 类在加载阶段

  • 在内存 (办法区或叫代码区) 中实例化一个 Class 对象,留神是 Class 对象不是 Student 对象
  • 一个 Class 类(字节码文件)对应一个 Class 对象
  • 该 Class 对象保留了 Student 类的根底信息,比方这个 Student 类有几个字段(Filed)?有几个构造方法(Constructor)?有几个办法(Method)?有哪些注解(Annotation)?等信息。

有了下面的对于 Student 类的根本信息对象(java.lang.Class 对象), 在运行期就能够依据这些信息来实例化 Student 类的对象。

  • 在运行期你能够间接 new 一个 Student 对象
  • 也能够应用反射的办法结构一个 Student 对象

然而无论你 new 多少个 Student 对象,不管你反射构建多少个 Student 对象,保留 Student 类信息的 java.lang.Class 对象都只有一个。上面的代码能够证实。

Class cls = Class.forName("com.zimug.java.reflection.Student");
Class cls2 = new Student().getClass();

System.out.println(cls == cls2); // 比拟 Class 对象的地址,输入后果是 true

四、操作反射的 java 类

理解了下面的这些根底信息,咱们就能够更深刻学习反射类相干的类和办法了:

  • java.lang.Class: 代表一个类
  • java.lang.reflect.Constructor: 代表类的构造方法
  • java.lang.reflect.Method: 代表类的一般办法
  • java.lang.reflect.Field: 代表类的成员变量
  • Java.lang.reflect.Modifier: 修饰符,办法的修饰符,成员变量的修饰符。
  • java.lang.annotation.Annotation:在类、成员变量、构造方法、一般办法上都能够加注解

4.1. 获取 Class 对象的三种办法

Class.forName()办法获取 Class 对象

/**
* Class.forName 办法获取 Class 对象,这也是反射中最罕用的获取对象的办法,因为字符串传参加强了配置实现的灵活性
*/
Class cls = Class.forName("com.zimug.java.reflection.Student");

类名.class获取 Class 对象

/**
* ` 类名.class` 的形式获取 Class 对象
*/
Class clz = User.class;

类对象.getClass()形式获取 Class 对象

/**
* ` 类对象.getClass()` 形式获取 Class 对象
*/
User user = new User();
Class clazz = user.getClass();

尽管有三种办法能够获取某个类的 Class 对象,然而只有第一种能够被称为“反射”。

4.2. 获取 Class 类对象的根本信息

Class cls = Class.forName("com.zimug.java.reflection.Student");

// 获取类的包名 + 类名
System.out.println(cls.getName());          //com.zimug.java.reflection.Student
// 获取类的父类
Class cls = Class.forName("com.zimug.java.reflection.Student");
// 这个类型是不是一个注解?System.out.println(cls.isAnnotation());     //false
// 这个类型是不是一个枚举?System.out.println(cls.isEnum());      //false
// 这个类型是不是根底数据类型?System.out.println(cls.isPrimitive()); //false

Class 类对象信息中简直包含了所有的你想晓得的对于这个类型定义的信息,更多的办法就不一一列举了。还能够通过上面的办法

  • 获取 Class 类对象代表的类实现了哪些接口:getInterfaces()
  • 获取 Class 类对象代表的类应用了哪些注解:getAnnotations()

4.3. 取得 Class 对象的成员变量

联合上文中的 Student 类的定义了解上面的代码

Class cls = Class.forName("com.zimug.java.reflection.Student");

Field[] fields = cls.getFields();
for (Field field : fields) {System.out.println(field.getName());      //nickName
}

fields = cls.getDeclaredFields();
for (Field field : fields) {System.out.println(field.getName());      //nickName 换行  age
}
  • getFields()办法获取类的非公有的成员变量,数组,蕴含从父类继承的成员变量
  • getDeclaredFields 办法获取所有的成员变量,数组,然而不蕴含从父类继承而来的成员变量

4.4. 获取 Class 对象的办法

  • getMethods() : 获取 Class 对象代表的类的所有的非公有办法,数组,蕴含从父类继承而来的办法
  • getDeclaredMethods() : 获取 Class 对象代表的类定义的所有的办法,数组,然而不蕴含从父类继承而来的办法
  • getMethod(methodName): 获取 Class 对象代表的类的指定办法名的非公有办法
  • getDeclaredMethod(methodName): 获取 Class 对象代表的类的指定办法名的办法
        Class cls = Class.forName("com.zimug.java.reflection.Student");

        Method[] methods = cls.getMethods();
        System.out.println("Student 对象的非公有办法");
        for (Method m : methods) {System.out.print(m.getName() + ",");
        }
        System.out.println("end");


        Method[] allMethods = cls.getDeclaredMethods();
        System.out.println("Student 对象的所有办法");
        for (Method m : allMethods) {System.out.print(m.getName() + ",");
        }
        System.out.println("end");


        Method dinnerMethod = cls.getMethod("dinner");
        System.out.println("dinner 办法的参数个数" + dinnerMethod.getParameterCount());

        Method sleepMethod = cls.getDeclaredMethod("sleep",int.class);
        System.out.println("sleep 办法的参数个数" + sleepMethod.getParameterCount());
        System.out.println("sleep 办法的参数对象数组" + Arrays.toString(sleepMethod.getParameters()));
        System.out.println("sleep 办法的参数返回值类型" + sleepMethod.getReturnType());

下面代码的执行后果如下:

Student 对象的非公有办法
dinner,wait,wait,wait,equals,toString,hashCode,getClass,notify,notifyAll,  end

Student 对象的所有办法
dinner,sleep,  end

dinner 办法的参数个数 0
sleep 办法的参数个数 1
sleep 办法的参数对象数组[int arg0]
sleep 办法的参数返回值类型 void

能够看到 getMethods 获取的办法中蕴含 Object 父类中定义的办法,然而不蕴含本类中定义的公有办法 sleep。另外咱们还能够获取办法的参数及返回值信息:

  • 获取参数相干的属性:

    • 获取办法参数个数:getParameterCount()
    • 获取办法参数数组对象:getParameters(),返回值是 java.lang.reflect.Parameter 数组
  • 获取返回值相干的属性

    • 获取办法返回值的数据类型:getReturnType()

4.5. 办法的调用

理论在上文中曾经演示了办法的调用,如下 invoke 调用 dinner 办法

Method dinnerMethod = cls.getDeclaredMethod("dinner");
dinnerMethod.invoke(obj);  // 打印:吃晚餐!

dinner 办法是无参的那么有参数的办法怎么调用? 看看 invoke 办法定义,第一个参数是 Method 对象,无论前面 Object... args 有多少参数就依照办法定义顺次传参就能够了。

public Object invoke(Object obj, Object... args)

4.6. 创立类的对象(实例化对象)

// 获取 Student 类信息
Class cls = Class.forName("com.zimug.java.reflection.Student");
// 对象实例化
Student student = (Student)cls.getDeclaredConstructor().newInstance();
// 上面的这种办法是曾经 Deprecated 了,不倡议应用。然而在比拟旧的 JDK 版本中依然是惟一的形式。//Student student = (Student)cls.newInstance();

五、反射的罕用场景

  • 通过配置信息调用类的办法
  • 联合注解实现非凡性能
  • 按需加载 jar 包或 class

5.1. 通过配置信息调用类的办法

将上文的 hello world 中的代码封装一下,你晓得类名 className 和办法名 methodName 是不是就能够调用办法了?至于你将 className 和 methodName 配置到文件,还是 nacos,还是数据库,本人决定吧!

public void invokeClassMethod(String className,String methodName) throws ClassNotFoundException, 
            NoSuchMethodException, 
            InvocationTargetException, 
            InstantiationException, 
            IllegalAccessException {
        // 获取类信息
        Class cls = Class.forName(className);
        // 对象实例化
        Object obj = cls.getDeclaredConstructor().newInstance();
        // 依据办法名获取并执行办法
        Method dinnerMethod = cls.getDeclaredMethod(methodName);
        dinnerMethod.invoke(obj);
}

5.2. 联合注解实现非凡性能

大家如果学习过 mybatis plus 都应该学习过这样的一个注解 TableName,这个注解示意以后的实例类 Student 对应的数据库中的哪一张表。如下问代码所示,Student 所示该类对应的是 t_student 这张表。

@TableName("t_student")
public class Student {
    public String nickName;
    private Integer age;
}

上面咱们自定义 TableName 这个注解

@Target(ElementType.TYPE)  // 示意 TableName 可作用于类、接口或 enum Class, 或 interface
@Retention(RetentionPolicy.RUNTIME) // 示意运行时由 JVM 加载
public @interface TableName {String value() ;   // 则应用 @TableName 注解的时候:@TableName(”t_student”);
}

有了这个注解,咱们就能够扫描某个门路下的 java 文件,至于类注解的扫描咱们就不必本人开发了,引入上面的 maven 坐标就能够

<dependency>
    <groupId>org.reflections</groupId>
    <artifactId>reflections</artifactId>
    <version>0.9.10</version>
</dependency>

看上面代码:先扫描包,从包中获取标注了 TableName 注解的类,再对该类打印注解 value 信息

// 要扫描的包
String packageName = "com.zimug.java.reflection";
Reflections f = new Reflections(packageName);
// 获取扫描到的标记注解的汇合
Set<Class<?>> set = f.getTypesAnnotatedWith(TableName.class);
for (Class<?> c : set) {
// 循环获取标记的注解
TableName annotation = c.getAnnotation(TableName.class);
// 打印注解中的内容
System.out.println(c.getName() + "类,TableName 注解 value=" + annotation.value());

输入后果是:

com.zimug.java.reflection.Student 类,TableName 注解 value=t_student

有的敌人会问这有什么用?这有大用处了。有了类定义与数据库表的对应关系,你还能通过反射获取类的成员变量,之后你是不是就能够依据表明 t_student 和字段名 nickName,age 构建增删改查的 SQL 了?全都构建结束,是不是就是一个根底得 Mybatis plus 了?

反射和注解联合应用,能够演化出许许多多的利用场景,特地是在架构优化方面,期待你去察觉啊!

5.3. 按需加载 jar 包或 class

在某些场景下,咱们可能不心愿 JVM 的加载器一次性的把所有的 jar 包装载到 JVM 虚拟机中,因为这样会影响我的项目的启动和初始化效率,并且占用较多的内存。咱们心愿按需加载,须要用到哪些 jar,依照程序动静运行的需要取加载这些 jar。

// 按门路加载 jar 包
File file = new File("D:/com/zimug/commons-lang3.jar");
URL url = file.toURI().toURL();
// 创立类加载器
ClassLoader classLoader = new URLClassLoader(new URL[]{url});

Class cls = classLoader.loadClass("org.apache.commons.lang3.StringUtils");

同样的把.class 文件放在一个门路下,咱们也是能够动静加载到的

//java 的.class 文件所在门路
File file = new File("D:/com/zimug");
URL url = file.toURI().toURL();
// 创立类加载器
ClassLoader classLoader = new URLClassLoader(new URL[]{url});
// 加载指定类,package 全门路
Class<?> cls = classLoader.loadClass("com.zimug.java.reflection.Student");

类的动静加载能不能让你想到些什么?是不是能够实现代码批改,不须要重新启动容器?对的,就是这个原理,因为一个类的 Class 对象只有一个,所以不论你从新加载多少次,都是应用最初一次加载的 class 对象(上文讲过哦)。

六、反射的优缺点

  • 长处:自在,应用灵便,不受类的拜访权限限度。能够依据指定类名、办法名来实现办法调用,非常适合实现业务的灵便配置。
  • 毛病:

    • 也正因为反射不受类的拜访权限限度,其安全性低,很大部分的 java 平安问题都是反射导致的。
    • 绝对于失常的对象的拜访调用,反射因为存在类和办法的实例化过程,性能也绝对较低
    • 毁坏 java 类封装性,类的信息暗藏性和边界被毁坏
      欢送关注我的布告号:字母哥杂谈,回复 003 赠送作者专栏《docker 修炼之道》的 PDF 版本,30 余篇精品 docker 文章。字母哥博客:zimug.com

正文完
 0