java 反射,注解等
Java 高级主题
本文档总结了Java中一些高级且深入的主题,如反射、注解、动态代理等。
1. 反射与注解 (Reflection & Annotation)
这两个主题紧密相连:注解为代码提供元数据,反射则是在运行时读取和使用这些元数据的主要手段。
Part 1: 反射 (Reflection) - “程序的自我审视”
1. 什么是反射?
一句话概括:反射是在程序运行时,动态地获取任意类的信息、并能动态地调用其方法和操作其属性的能力。
通常情况下,我们写的代码在编译时就已经确定了要调用哪个类、哪个方法。而反射打破了这种静态性,它允许程序在“运行”的过程中,才去决定要操作哪个对象。
可以把它想象成 Java 程序的“说明书”和“万能工具箱”。通过反射,程序可以在运行时:
- 审视自己:对于任意一个类,都能知道它所有的属性和方法。
- 操作自己:对于任意一个对象,都能调用它的任意方法、修改它的任意属性(包括私有的!)。
2. 反射的核心API
反射功能主要由 java.lang.reflect 包下的几个核心类提供:
Class<T>: 反射的入口。JVM中每个加载的类都有一个与之对应的Class对象。通过它,可以获取类的所有信息。- 获取方式:
String.class,object.getClass(),Class.forName("java.lang.String")。
- 获取方式:
Field: 代表类的成员变量(属性)。- 可以获取字段的类型、名称,并且可以读取和设置特定对象上该字段的值。
Method: 代表类的方法。- 可以获取方法的名称、参数、返回类型,并且可以调用特定对象上的该方法。
Constructor<T>: 代表类的构造函数。- 可以获取构造函数的参数,并且可以用它来创建新实例。
关键方法对比:
getFields()vsgetDeclaredFields(): 前者只能获取public的字段(包括父类的),后者可以获取所有在本类中声明的字段(不包括父类的)。Method和Constructor的get...与getDeclared...系列方法同理。field.setAccessible(true): 这是反射的“魔法棒”,用于关闭访问权限检查,使得我们可以访问和修改private成员。
3. 反射的应用场景(重点)
你平时可能没有直接写很多反射代码,但你用的框架无时无刻不在使用它。
- Spring/SpringBoot 框架:
- 依赖注入 (DI):当你写下
@Autowired时,Spring 容器会启动并扫描你的类。它通过反射找到所有标记了@Autowired的字段,然后通过反射调用field.set()方法,将对应的 Bean 实例注入进去,这一切都无需你手动new和set。 - AOP (面向切面编程):通过反射和动态代理技术,在不修改你源码的情况下,为你调用的方法前后(或周围)增加日志、事务、安全等通用逻辑。
- 依赖注入 (DI):当你写下
- MyBatis/Hibernate (ORM 框架):
- 当你执行一条 SQL 查询后,框架会得到一个
ResultSet。ORM 框架会通过反射创建你的实体类对象(如User),然后根据ResultSet中的列名(如 “user_name”),通过反射找到User类中对应的setUserName方法或userName字段,并调用它来填充数据。
- 当你执行一条 SQL 查询后,框架会得到一个
- 动态代理 (Dynamic Proxy):
- 这是反射的一个非常重要的应用。它允许你在不实现任何接口的情况下,创建一个代理对象来“伪装”成另一个对象,从而拦截对原始对象方法的调用。RPC 框架(如 Dubbo)的底层就大量用到了动态代理。
- 通用工具库/序列化:
- 如 Jackson/Gson,它们将 JSON 字符串转换为 Java 对象时,就是通过反射读取 JSON 的 key,找到 Java 类中同名的字段,然后通过反射为其设值。
- 我们之前讨论的深拷贝,使用这类库实现也是基于反射。
- 单元测试 (JUnit):
- 当你运行测试时,JUnit 会通过反射找到所有被
@Test注解标记的方法,然后逐一通过反射调用它们来执行测试用例。
- 当你运行测试时,JUnit 会通过反射找到所有被
Part 2: 注解 (Annotation) - “代码的标签”
1. 什么是注解?
注解(也叫元数据),是插入到代码中的一种特殊“标签”或“注释”。它本身不会对代码的执行逻辑产生任何影响。
它的核心价值在于为其他程序(编译器、框架、工具)提供额外信息。注解就像一个贴在代码上的便签,上面写着“这里需要事务”、“这个字段需要自动注入”、“这个方法是个测试用例”等等。
2. 注解的原理(底层实现)
这部分比较深入,我们分两步看:
-
编译期:当你写下一个注解,比如
@Override,编译器javac会检查这个注解。在编译时,一个注解本质上被当作一个特殊的接口来处理。当编译器将你的代码编译成.class文件时,它会把注解的信息(以及它的属性值)存储在.class文件的一个叫做“运行时可见注解属性表”的结构里。 -
运行期:
- 当 JVM 加载这个
.class文件时,它会读取这些注解信息,并保存在内存中。 - 这是最关键的一步:当你通过反射调用
field.getAnnotation(MyAnnotation.class)时,JVM 并不会返回一个普通的MyAnnotation对象。相反,JVM 使用动态代理技术,在运行时动态地创建了一个实现了MyAnnotation接口的代理对象 ($Proxy...)。 - 这个代理对象内部有一个
InvocationHandler,它连接着 JVM 内部存储的、关于这个字段的注解信息。 - 当你调用注解的方法(如
myAnnotation.value())时,实际上是调用了这个代理对象的方法。代理对象会通过InvocationHandler从 JVM 的内部数据结构中,找到value属性对应的值,然后返回给你。
- 当 JVM 加载这个
简单来说,你拿到的注解对象,是一个运行时生成的、专门用来读取注解属性的代理对象。
3. 注解的作用域 (@Retention)
@Retention 是一个元注解(用来修饰其他注解的注解),它决定了注解的“生命周期”。
RetentionPolicy.SOURCE: 仅存在于源码中,编译时被丢弃。- 用途:主要用于给编译器提供信息,或者在编译期进行代码生成。
- 例子:
@SuppressWarnings(告诉编译器忽略警告)、Lombok 的@Data、@Getter等(Lombok 的注解处理器在编译期根据这些注解生成get/set方法,然后注解本身就被丢弃了)。
RetentionPolicy.CLASS: 存在于.class文件中,但运行时不可见。这是默认值。- 用途:在字节码层面进行操作,比如字节码增强。在常规开发中用得较少。
RetentionPolicy.RUNTIME: 存在于.class文件中,并且在运行时可以通过反射获取。- 用途:这是最常用的类型,绝大部分框架(Spring, MyBatis, JUnit)都依赖它在运行时获取信息并执行相应逻辑。
- 例子:
@Autowired,@Service,@Test,@Column等。
4. 其他重要的元注解
@Target: 指定你的注解可以被用在什么地方(类、方法、字段、参数等)。@Documented: 指定该注解信息是否会被包含在 Javadoc 中。@Inherited: 允许子类继承父类的注解。
2. 动态代理 (Dynamic Proxy) - “无中生有”的艺术
在反射部分,我们提到了它的一个重要应用是动态代理。现在我们来深入挖掘它。
1. 代理模式是什么?
在聊动态代理前,先要理解代理模式 (Proxy Pattern)。它的核心思想是:为其他对象提供一种代理以控制对这个对象的访问。
代理就像一个“中介”,客户端不直接访问目标对象,而是通过访问中介,中介再把请求转给目标对象。这样做的好处是,中介可以在请求传递前后,添加一些额外的处理逻辑,比如权限校验、日志记录、性能监控、事务管理等,而目标对象完全不知道这些“加强”逻辑的存在,从而保持了其业务逻辑的纯粹性。
代理模式分为静态代理和动态代理:
- 静态代理:代理类是在编译期就创建好的 (
.java文件是手写的)。一个代理类通常只为一个目标类服务。缺点是如果目标类增多,或接口方法改变,代理类也需要手动维护,很繁琐。 - 动态代理:代理类是在程序运行时动态生成的。我们无需手写任何代理类的代码,它是在内存中“无中生有”的。这是所有现代框架的基石。
2. Java 中的两大动态代理技术:JDK 动态代理与 CGLIB
Java 生态中实现动态代理主要有两种主流方式。
方式一:JDK 动态代理
这是 Java 官方提供的、内置在 JDK 中的动态代理实现。
- 核心组件:
java.lang.reflect.Proxy:用于创建代理对象的工厂类。java.lang.reflect.InvocationHandler:一个接口,你需要实现它来定义代理的“拦截逻辑”。所有对代理对象方法的调用,最终都会被转发到这个Handler的invoke方法上。
- 工作原理:
- 前提:目标类必须实现一个或多个接口。JDK 代理是面向接口的代理。
Proxy.newProxyInstance()方法接收三个参数:类加载器、目标类实现的接口数组、以及你的InvocationHandler实例。- 该方法会在运行时动态地创建一个新的类(通常叫
$Proxy0),这个类实现了你传入的所有接口。 - 当你调用代理对象的任何方法时,这个调用会被 JRE 转发到你的
InvocationHandler的invoke方法中。 - 在
invoke方法里,你可以决定要做什么:比如在调用原始方法 (method.invoke(target, args)) 之前打印日志,在调用之后提交事务,或者甚至完全不调用原始方法。
- 关键限制:目标类必须实现接口。它无法代理一个没有实现任何接口的普通类。
方式二:CGLIB (Code Generation Library)
CGLIB 是一个强大的、高性能的代码生成库,被广泛用于弥补 JDK 动态代理的不足。Spring 框架内部就集成了 CGLIB。
- 核心组件:
net.sf.cglib.proxy.Enhancer:类似于 JDK 的Proxy类,是创建代理对象的入口。net.sf.cglib.proxy.MethodInterceptor:类似于 JDK 的InvocationHandler,你需要实现它来定义方法拦截逻辑。
- 工作原理:
- 前提:目标类不能是
final的。 - CGLIB 的
Enhancer会在运行时动态地为目标类创建一个子类。 - 这个子类会重写目标类中所有非
final的方法。 - 当你调用代理对象(即这个子类实例)的方法时,调用会被拦截,并转发到你的
MethodInterceptor的intercept方法中。 - 在
intercept方法里,你可以执行增强逻辑,并选择性地通过methodProxy.invokeSuper(obj, args)来调用原始的父类方法。
- 前提:目标类不能是
- 核心优势:不需要实现接口,可以直接代理普通的类。
3. JDK 动态代理 vs. CGLIB (面试必考对比)
| 特性 | JDK 动态代理 | CGLIB |
|---|---|---|
| 实现基础 | 面向接口,利用反射。 | 面向继承,利用字节码技术(ASM)。 |
| 核心要求 | 目标类必须实现接口。 | 目标类不能是 final 类。 |
| 代理范围 | 只能代理接口中定义的方法。 | 可以代理目标类中所有非 final 的方法。 |
| 依赖 | JDK 内置,无需额外依赖。 | 第三方库,需要引入 cglib 依赖。 |
| 性能 | <p>对象创建速度快,方法调用因涉及反射稍慢。</p><p>(但在现代高版本JDK中,性能已被极大优化,有时甚至反超CGLIB)。</p> | <p>对象创建速度慢(因为要生成子类字节码)。</p><p>方法调用速度通常更快(因为它直接调用方法,而非反射)。</p> |
4. 实际应用:Spring AOP 如何选择?
这是将理论与实践结合的经典面试题。
- Spring 框架会智能地在这两者之间进行选择:
- 如果目标 Bean 实现了接口,Spring AOP 默认使用 JDK 动态代理。
- 如果目标 Bean 没有实现任何接口,Spring AOP 会自动切换到 CGLIB 来创建子类代理。
- 你也可以通过配置 (
spring.aop.proxy-target-class=true) 来强制 Spring AOP 全部使用 CGLIB 代理,即使目标 Bean 实现了接口。