反射底层原理

175 阅读7分钟

技术前言****

在Java中我们经常听到一个神秘的名词——反射,java.lang.reflect反射库提供了一系列丰富而精巧的工具集,可以用来编写能够动态操作Java代码的程序。

反射被广泛用于框架、库和工具的设计和实现中,支持用户界面生成器、对象关系映射器以及其他很多需要动态查询能力的开发工具,但是很多开发者对其仍然感到困惑。本文将详细介绍一下Java反射的概念、原理、优缺点、如何用代码编写、有哪些应用场景与项目实践,帮助大家彻底理解和应用这个技术。

什么是反射?****

Java反射是指在运行时能够动态地获取类的信息,动态地调用对象的方法和访问对象的属性,具有分析类的和执行类中方法的能力。通过反射可以获取任意一个类的所有属性和方法,还可以调用这些方法和属性,它允许我们在编译时并不知道类的具体信息的情况下,通过运行时的分析和操作来获取和利用类的信息。

  简单来说,反射可以动态引入类、动态调用实例的成员变量、成员方法等。

反射的原理**

反射的原理主要基于Java的类加载机制和字节码。要想解剖一个类,必须先要获取到该类的字节码文件对象。当Java程序加载类时,会将类的信息存储在内存中,通过反射,我们可以通过类加载器获取类的Class对象,然后利用Class对象获取类的详细信息,包括构造函数、方法和字段等。通过这些信息,我们可以创建对象的实例、调用对象的方法以及访问和修改对象的属性。

如何获取 Class 对象?****

  1. Object中的getClass()方法可以返回一个Class类型的实例

    Employee e;

    Class c = e.getClass();

  2. 使用静态方法forName()传入类的路径获取

    Class c = Class.forName("com.xhl.demo.User")

  3. 如果T是任意Java类型,T.class将代表匹配的类对象  

    Class c = int.class

  代码示例****

User ****

public class User {

public String name;

public int age;


public String getName() {

return name;

}


 public void setName(String name) {

this.name = name;

}

public int getAge() {

return age;

}


public void setAge(int age) {

this.age = age;

 }
}

ReflectDemo ****

public class ReflectDemo {


public static void main(String\[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {


User user = new User();

user.setName("杰尼龟");

System.out.println(user.getName());


Class c = Class.forName("com.xhl.maker.demo.reflect.User");

Object o = c.newInstance();

System.out.println(o);

Method method = c.getMethod("setName", String.class);


method.invoke(o, "小火龙");

Method method2 = c.getMethod("getName");


System.out.println(method2.invoke(o));


Field field = c.getField("name");

field.set(o, "皮卡丘");

System.out.println(field.get(o));


}


}

运行结果:

image.png

代码解析****

代码中,我们用普通的创建对象获取类信息的方法(正射),与动态获取类的技术反射作对比,可以看到使用反射需要按照一定的步骤去写代码:

  1. 获取反射类的 Class 对象

Class c = Class.forName("com.xhl.maker.demo.reflect.User");

在Java中虚拟机为每个类型(包括类、接口、数组以及基础类型)管理一个唯一的Class对象,Class对象是在JVM加载类的时候完成的。除了用类的全名获取 Class 对象,还有刚刚我们提到的另外两种方式也可以获取。

  2. 初始化反射类对象

Object o = c.newInstance();

我这里使用默认的无参构造函数创建实例,你也可以使用Constructor 对象初始化反射类对象如下:

  Constructor constructor = c.getConstructor();

Object object = constructor.newInstance();

 

  1. 获取要调用的方法的 Method 对象,,通过 invoke() 方法执行

Method method = c.getMethod("setName", String.class);

method.invoke(o, "小火龙");

Method method2 = c.getMethod("getName");

System.out.println(method2.invoke(o));

  1. 获取要用的字段并修改

Field field = c.getField("name");

field.set(o, "皮卡丘");

System.out.println(field.get(o));

总结以下常用的API:

  1. java.lang.Class
  2. java.lang.reflect.Method
  3. java.lang.reflect.Field
  4. java.lang.reflect.Constructor

至此,我们就学会了基础的反射的使用,然而要理解整个反射机制的话还需要去理解JVM的类加载机制,大家可以通过搜索引擎找一篇适合自己、通俗易懂的文章阅读。

反射的优缺点****

  1. 优点:可以动态创建和使用对象,使用灵活,为各种框架提供开箱即用的功能提供了便利
  2. 缺点:
  3. 反射调用的性能开销较高,执行速度慢
  4. 容易破坏封装性和类型安全性

反射的应用场景***\

 

在我们平时在代码的时候比如应用业务程序,一般是用不到反射机制的,主要是给开发工具的程序员使用的。

  1. 开发框架:像Spring、Spring Boot、Hibernate等框架里都大量使用反射机制,
  2. 比如通过配置文件来加载不同的对象,依赖注入、 ORM 映射。
  3. 动态代理:动态代理主要通过java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口来实现也就是说底层技术是反射,在面向切面编程中,需要拦截特定的方法,就会选择动态代理的方式。
  4. 调试和测试:通过反射,可以在调试和测试过程中获取类的信息、调用方法并检查对象的状态。这对于编写通用的测试框架或进行单元测试非常有帮助。

项目最佳实践****

在定制化代码生成的项目里,我们在使用Picocli命令行框架开发做交互式输入功能的开发的时候,如果需要实现强制交互式的功能,就要用到反射机制。

需求****

  我们要求用户必须输入某个选项(比如-p),而不能使用默认的空值,怎么办呢?

设计实现****

编写一段通用的校验程序,如果用户的输入命令中没有包含交互式选项,那么就自动为输入命令补充该选项即可,这样就能强制触发交互式输入,

利用反射自动读取必填的选项名称

具体编码实现****

我们想要实现的是,用户没有输入-p参数,我们也强制让用户输入密码的需求。

首先想到的硬编码实现,检查参数里面有没有"-p",如果没有就在args数组里添加上:

//检测 -p 选项是否存在,如果不存在则添加

boolean passwordOptionExists = false;

for (String arg : args) {

if (arg.equals("-p") || arg.equals("--password")) {

passwordOptionExists = true;

break;

  }

}

 

if (!passwordOptionExists) {

args = appendOption(args, "-p");

}   但是这段代码是有缺陷的,虽然很简单,但是不够灵活,如果未来有更多的选项需要强制性交互,那我们需要一个一个添加到代码里,去硬编码实现,这非常不优雅。因此可以使用Java的一个技术:反射机制,可以在程序运行的时候,动态获取类的属性。

首先引入一个反射包:import java.lang.reflect.Field;

然后给我们需要强制交互式的选项,打上required = true这个属性:  

@Option(names = {"-p", "--password"}, arity = "0..1", description = "Passphrase", interactive = true, echo = true, required = true)

String password;

最后就在主函数里编码实现逻辑:

private static String\[] appendOption(String\[] args, String option) {

String\[] newArgs = new String\[args.length + 1];

System.arraycopy(args, 0, newArgs, 0, args.length);

newArgs\[args.length] = option;

return newArgs;

}

public static void main(String[] args) {

Login login = new Login();

// 获取 Login 类中的所有字段

Field\[] fields = Login.class.getDeclaredFields();

List<String> requiredOptions = new ArrayList<>();



for (Field field : fields) {

// 检查字段是否具有 @Option 注解并且 required = true

if (field.isAnnotationPresent(Option.class)) {

Option option = field.getAnnotation(Option.class);

if (option.required()) {

requiredOptions.add(option.names()\[0]);

         }

  }

}



// 检查 args 数组中是否存在 requiredOptions 列表中的值

for (String requiredOption : requiredOptions) {

boolean optionExists = false;

for (String arg : args) {

if (arg.equals(requiredOption)) {

optionExists = true;

break;

         }

       }

    if (!optionExists) {

    args = appendOption(args, requiredOption);

    }
}
new CommandLine(new Login()).execute(args);
}

然后,我们只指定参数-u user123

image.png

执行代码效果如下:

image.png 描述已自动生成]()

发现程序强制让我们输入密码,至此需求完成!

总结****

本文详细介绍了Java反射的概念、原理、优缺点、代码示例、使用场景以及项目实践,通过反射,我们可以在运行时动态地获取类的信息、调用对象的方法以及访问和修改对象的属性。反射在许多领域中都发挥着重要作用,如框架和库的设计、动态配置和扩展、调试和测试等。然而,反射也需要谨慎使用,以避免性能问题和破坏封装性。在学习反射的时候可以来看看,希望对大家有用~