技术前言****
在Java中我们经常听到一个神秘的名词——反射,java.lang.reflect反射库提供了一系列丰富而精巧的工具集,可以用来编写能够动态操作Java代码的程序。
反射被广泛用于框架、库和工具的设计和实现中,支持用户界面生成器、对象关系映射器以及其他很多需要动态查询能力的开发工具,但是很多开发者对其仍然感到困惑。本文将详细介绍一下Java反射的概念、原理、优缺点、如何用代码编写、有哪些应用场景与项目实践,帮助大家彻底理解和应用这个技术。
什么是反射?****
Java反射是指在运行时能够动态地获取类的信息,动态地调用对象的方法和访问对象的属性,具有分析类的和执行类中方法的能力。通过反射可以获取任意一个类的所有属性和方法,还可以调用这些方法和属性,它允许我们在编译时并不知道类的具体信息的情况下,通过运行时的分析和操作来获取和利用类的信息。
简单来说,反射可以动态引入类、动态调用实例的成员变量、成员方法等。
反射的原理**
反射的原理主要基于Java的类加载机制和字节码。要想解剖一个类,必须先要获取到该类的字节码文件对象。当Java程序加载类时,会将类的信息存储在内存中,通过反射,我们可以通过类加载器获取类的Class对象,然后利用Class对象获取类的详细信息,包括构造函数、方法和字段等。通过这些信息,我们可以创建对象的实例、调用对象的方法以及访问和修改对象的属性。
如何获取 Class 对象?****
-
Object中的getClass()方法可以返回一个Class类型的实例
Employee e;
Class c = e.getClass();
-
使用静态方法forName()传入类的路径获取
Class c = Class.forName("com.xhl.demo.User")
-
如果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));
}
}
运行结果:
代码解析****
代码中,我们用普通的创建对象获取类信息的方法(正射),与动态获取类的技术反射作对比,可以看到使用反射需要按照一定的步骤去写代码:
- 获取反射类的 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();
- 获取要调用的方法的 Method 对象,,通过 invoke() 方法执行
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));
总结以下常用的API:
- java.lang.Class
- java.lang.reflect.Method
- java.lang.reflect.Field
- java.lang.reflect.Constructor
至此,我们就学会了基础的反射的使用,然而要理解整个反射机制的话还需要去理解JVM的类加载机制,大家可以通过搜索引擎找一篇适合自己、通俗易懂的文章阅读。
反射的优缺点****
- 优点:可以动态创建和使用对象,使用灵活,为各种框架提供开箱即用的功能提供了便利
- 缺点:
- 反射调用的性能开销较高,执行速度慢
- 容易破坏封装性和类型安全性
反射的应用场景***\
在我们平时在代码的时候比如应用业务程序,一般是用不到反射机制的,主要是给开发工具的程序员使用的。
- 开发框架:像Spring、Spring Boot、Hibernate等框架里都大量使用反射机制,
- 比如通过配置文件来加载不同的对象,依赖注入、 ORM 映射。
- 动态代理:动态代理主要通过java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口来实现也就是说底层技术是反射,在面向切面编程中,需要拦截特定的方法,就会选择动态代理的方式。
- 调试和测试:通过反射,可以在调试和测试过程中获取类的信息、调用方法并检查对象的状态。这对于编写通用的测试框架或进行单元测试非常有帮助。
项目最佳实践****
在定制化代码生成的项目里,我们在使用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
执行代码效果如下:
描述已自动生成]()
发现程序强制让我们输入密码,至此需求完成!
总结****
本文详细介绍了Java反射的概念、原理、优缺点、代码示例、使用场景以及项目实践,通过反射,我们可以在运行时动态地获取类的信息、调用对象的方法以及访问和修改对象的属性。反射在许多领域中都发挥着重要作用,如框架和库的设计、动态配置和扩展、调试和测试等。然而,反射也需要谨慎使用,以避免性能问题和破坏封装性。在学习反射的时候可以来看看,希望对大家有用~