什么是反射以及反射机制优缺点
java反射机制是java语言的核心特性之一,允许程序在运行时动态获取类的内部信息并操作其成员。其核心在于绕过编译时的静态检查,实现动态加载和类结构探知的能力。
反射的优点:
1、动态性与灵活性
1.1) 运行时类操作
反射可以在运行时动态加载类、创建对象、调用方法、访问字段,无需在编译时确定具体类型。比如下面的例子
我们先定义一个Student类,类中成员变量包括如下信息
package com.test.demo;
public class Student {
private String name;
private int age;
public String address;
public Student(String name, int age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
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;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
private void eat(String name) {
System.out.println(name + "在吃饭");
}
private void eat(String name, int age) {
System.out.println("姓名:"name + ", 年龄:" + age + ", 正在吃饭";
}
}
下面是获取字节码Class对象的三种方式
//方法一:通过全类名我们就可以获取这个类的Class对象,这个在编译阶段就可以拿到Class对象
Class<?> clazz = Class.forName("com.test.demo.Student");
//另外获取Class对象还有另外两种方法
//方法二: 直接通过类名.class来获取,这个在加载阶段可以拿到Class对象
Class<?> clazz2 = Student.class;
//方法三:先实例化Student的一个实例对象,然后通过实例对象的getClass()方法获取Class对象,这个是在运行阶段
Student stu = new Student();
Class<?> clazz3 = stu.getClass();
下面是通过反射获取构造方法的例子,如下:
//拿到clazz之后,就可以通过clazz.getDeclaredConstructor()获取类的无参构造方法,获取构造方法有好几个方式:
//clazz.getDeclaredConstructors()可以获取类中所有的构造方法(包括public修饰的也包括其它类型修饰的,比如protected、private等);
//clazz.getConstructors()可以获取类中所有public修饰的构造方法。
//我们还可以通过指定参数类型来获取指定的一个构造方法,比如clazz.getDeclaredConstructor(String.class)这个就是获取只有一个String类型入参的构造方法
//在拿到构造方法后,我们就可以通过构造方法获取类的一个实例,如下所示
Constructor<?> constructor = clazz.getDeclaredConstructor();
Object student = constructor.newInstance();
//那如果我们获取到的构造方法是个private修饰的构造方法的话,我们要创建实例对象还得加一步,就是设置构造方法可以被访问,代码如下所示
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);
constructor.setAccessible(true);
Object student = constructor.newInstance();
下面是通过反射获取字段和修改字段的例子,如下:
//接下来就是获取类的成员变量,如下所示,这个是获取所有用public修饰的成员变量
Field[] fields = clazz.getFields();
//如果要获取全部修饰符修饰的成员变量(public、protected、private等),那么可以通过如下方法获取,如下所示
Field[] fields = clazz.getDeclaredFields();
//如果只想获取某个指定的成员变量,那么可以通过指定字段名称来获取,比如我们的Student类中有个用private修饰的name成员变量,那么可以通过如下方式获取
Field name = clazz.getDeclaredField("name");
//如果我们想要获取这个name成员变量的值,需要先实例化一个Student对象
Student student = new Student("zhangsan", 23, "河北保定");
//然后通过name.get(obj)的方式获取成员变量name的值
Object value = name.get(student);
//打印成员变量
System.out.println(value);
//如果我们运行上面打印value的动作发现报异常了,说是没有权限访问私有成员变量的值。那么我们如何获取这个成员变量的值呢,跟上面构造方法的处理一样,我们给Field对象设置访问权限即可,代码如下
Student student = new Student("zhangsan", 23, "河北邯郸");
name.setAccessible(true);
Object value = name.get(student);
System.out.println(value);
下面是通过反射获取成员方法和调用成员方法的例子,如下:
//接下来就是获取成员方法
//获取成员方法与获取成员变量稍有不同,通过clazz.getMethods()获取的不仅包含当前类的成员方法,还包含了父类的成员方法
Method[] methods = clazz.getMethods();
//那如果我们只想要当前类的成员方法,那么使用getDeclaredMethods方法来获取
Method[] methods = clazz.getDeclaredMethods();
//如果只想获取单个成员方法,那么我们可以通过成员方法名+参数类型的方式来获取,比如我们上面Student类中有两个私有的eat方法,两个eat方法参数不同,假如我们想要获取那个只有一个String类型参数的方法,那么就可以通过如下方式获取
Method eatMethod = clazz.getDeclaredMethod("eat", String.class);
//如果想获取有两个参数的eat方法,我们可以通过如下方式获取
Method eatMethod = clazz.getDeclaredMethod("eat", String.class, int.class);
//拿到method之后,我们还可以获取方法的参数
Parameter[] parameters = eatMethod.getParameters();
//还可以获取方法抛出的异常
Class<?>[] exceptionTypes = eatMethod.getExceptionTypes();
//最重要的是方法的执行,也就是invoke
//invoke主要包含两个入参以及返回值处理。第一个参数是obj对象,也就是我们实例化出来的实例对象,第二个参数是调用方法传递的参数。对于返回值要根据方法具体的情况来定,如果有返回值那么我们可以用一个Object对象来接收返回值,如果没有返回值,则我们不用接收即可。假如我们想要给有两个参数的eat方法设置新的参数值,然后执行一下eat方法查看打印的信息(由于eat方法是私有的,因此要执行的话必须先给eatMethod设置访问权限eatMethod.setAccessible(true)才可以)这样就可以打印出我们指定参数的执行日志了
Student student = new Student("zhangsan", 23, "河北邯郸");
Method eatMethod = clazz.getDeclaredMethod("eat", String.class, int.class); eatMethod.setAccessible(true);
eatMethod.invoke(student, "lisi", 20);
1.2)支持框架和库的开发
反射是许多框架,比如Spring、Hibernate、Junit等的核心机制,实现依赖注入、动态代理、注解解析等。
依赖注入的核心原理:
依赖注入的核心目标:将对象的创建和依赖关系的管理从代码中解耦。
通过反射实现依赖注入的关键步骤:
1)扫描类路径:找到需要被管理的类(如标记了特定注解的类)
2)实例化对象:通过反射创建对象。
3)递归解析依赖:检查对象的字段或构造方法,自动注入依赖的其他对象。
4)管理单例:确保同一类只被实例化一次(可选)
下面用最简单的代码来手写一个依赖注入的案例
步骤一:定义注解
Component注解上面@Target(ElementType.TYPE)的意思是它作用于接口、类、枚举、注解。而Autowired注解上面@Target(ElementType.FIELD)的意思是它作用于字段
@Retention包含如下几种类型
1)RetentionPolicy.SOURCE 这种类型的Annotations只在源代码级别保留,当java文件编译成class文件的时候注解被遗弃。
2)RetentionPolicy.CLASS 这种类型的注解会被保留到class文件,但jvm加载class文件的时候被遗弃,这是默认的生命周期。
3)RetentionPolicy.RUNNING 这种类型的注解不仅被保留到class文件中,jvm加载class文件之后依然保留。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Component {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Autowired {
}
步骤二:下面我们模拟实现一个容器
public class DIContainer {
//存储单例对象的容器(key是类名 value是实例)
private final Map<String, Object> singletonMap = new ConcurrentHashMap<>();
//扫描指定包并初始化所有Component类
public void scan(String basePackage) throws Exception {
//通过类路径扫描获取所有类(此处简化,实际需要实现类扫描逻辑)
Set<Class<?>> classes = findClass(basePackage);
for (Class<?> clazz : classes) {
if (clazz.isAnnotationPresent(Component.class)) {
//创建实例并注入依赖
Object instance = createInstance(clazz);
singletonMap.put(clazz.getName(), instance);
}
}
}
//创建对象并注入依赖
private Object createInstance(Class<?> clazz) throws Exception {
//1.通过无参构造方法创建实例
Object instance = clazz.getDeclaredConstructor().newInstance();
//2.注入字段依赖
for (Field field : clazz.getDeclaredFields()) {
//检查字段上是否添加了@Autowired注解
if (field.isAnnotationPresent(Autowired.class)) {
//获取字段类型对应的实现类(此处简化,实际需处理接口和实现类映射)
Class<?> fieldType = field.getType();
Object dependency = getOrCreateBean(fieldType);
//突破private限制并注入值
field.setAccessible(true);
field.set(instance, dependency);
}
}
return instance;
}
//获取或创建Bean(支持单例)
private Object getOrCreateBean(Class<?> type) throws Exception {
//查找已存在的实例
Object bean = singletonMap.get(type.getName());
if (bean != null) {
return bean;
}
//创建新实例并递归注入依赖
bean = createInstance(type);
singletonMap.put(type.getName(), bean);
return bean;
}
//获取Bean
public <T> getBean(Class<?> type) {
return (T) singletonMap.get(type.getName));
}
//模拟类扫描(实际需使用类加载遍历包路径)
private Set<Class<?>> findClasses(String basePackage) {
//示例直接返回预设类型(实际需实现扫描逻辑)
Set<Class<?>> classes = new HashSet<>();
classes.add(UserService.class);
classes.add(UserRepository.class);
return classes;
}
}
步骤三:定义被管理的类
@Component
public class UserService {
@Autowired
private UserRepository userRepository;
public void saveUser(String user) {
userRepository.save(user);
}
}
@Component
public class UserRepository {
public void save(String user) {
System.out.println("保存用户:" + user);
}
}
步骤四:使用容器
public class Test1 {
public static void main(String[] args) throws Exception {
DIContainer container = new DIContainer();
container.scan("com.example");
UserService userService = container.getBean(UserService.class);
userService.saveUser("zhangsan");
}
}
关键点解析:
1)类扫描与实例化
通过反射查找所有标记了@Component的类,并递归创建他们的实例。
2)依赖注入
检查@Autowired注解的字段,动态注入已创建的依赖对象。
3)单例管理
使用Map缓存实例,确保每个类只有一个实例。
4)简化假设
假设@Autowired字段的类型有且仅有一个实现类。
实际框架(如Spring)会处理接口与实现类的映射、构造方法注入、循环依赖等问题。
反射在依赖注入中的作用
1)动态创建对象
通过clazz.newInstance()或构造方法反射创建对象。
2)访问私有字段
通过field.setAccessible(true)突破访问权限,注入依赖。
3)递归解析依赖
检查字段类型,递归创建并注入依赖的Bean
2、突破访问权限
反射可以绕过修饰符比如private、protected的限制,强制访问或修改类的私有属性和方法。
反射是实现动态代理的基础,支持面向切面编程(AOP)。
3、通用工具开发
3.1) 通用序列化和反序列化
反射可以用于实现通用的JSON、XML序列化工具,如Jackson、Gson
3.2) 调试和监控
通过反射获取类的元数据,用于调试工具或性能监控。
反射的缺点如下:
1、性能开销
运行效率低:反射操作需要JVM在运行时动态解析类信息,涉及方法调用、字段访问的权限检查等,性能显著低于直接代码调用。
JIT优化受限:反射调用的代码难以被JIT编译器优化,尤其是在高频调用时可能成为性能瓶颈。
2、安全隐患
破坏封装性:反射可以强制访问私有成员,破坏类的封装性,可能导致数据不一致或安全漏洞。
权限管理绕过:通过反射调用setAccessible(true)可以绕过安全管理器的检查(SecurityManager),威胁系统安全。
3、代码可维护性差
可读性差:反射代码通常冗长且难以理解,IDE难以提供代码提示和重构支持。
编译时检查缺失:反射调用的方法或字段在编译时无法检查是否存在或类型是否匹配,错误只能在运行时暴露。
4、兼容性问题
版本敏感性:反射代码依赖于类的内部(如方法名、参数列表、字段名),如果类发生变更(如方法重命名),反射代码会直接失效。
5、反射破坏单例
本来我们通过双重校验模式获取单例对象只会拿到一个对象实例,但是通过反射的话,就可以调用私有方法,然后通过私有方法创建新的实例对象,这样就破坏了单例模式。