视频加载失败

Java 进阶 Day05:反射注解与动态代理

1962 字
10 分钟
Java 进阶 Day05:反射注解与动态代理

反射入门:获取 Class 对象的三种方式#

Class c1 = Student.class; // 方式1:类名.class(编译期就确定)
Class c2 = Class.forName("reflectdemo1.Student"); // 方式2:全限定名(运行期动态)
Class c3 = s.getClass(); // 方式3:对象.getClass()
System.out.println(c1 == c2); // true
System.out.println(c2 == c3); // true

三种方式拿到的是同一个 Class 对象:类只被类加载器加载一次,缓存在方法区,之后一直复用(同一类加载器下全局唯一)。

选用场景:

  • 编译期就知道类 → 类名.class(最安全)
  • 手里已有对象 → getClass()
  • 运行期才决定类名(比如配置文件里写的)→ Class.forName("全限定名"),需捕获 ClassNotFoundException

反射操作类的三种成分#

反射能在运行期拿到并操作类的三类成分,对应 java.lang.reflect 包下的三个类:

成分类操作
构造器ConstructornewInstance(参数...) 创建对象
成员变量Fieldset(对象, 值) / get(对象) 读写字段
成员方法Methodinvoke(对象, 参数...) 调用方法

暴力反射 setAccessible(true):取消 Java 语言级的访问控制检查,private 成员也能访问——「破解封装」。

注意点:

  • newInstance() 无参重载只能调无参构造器(没有无参构造会抛 InstantiationException);private 构造器必须先 setAccessible(true) 再 new
  • invoke() 返回方法的返回值(Object 类型),void 方法返回 null

getDeclaredXxx 与 getXxx 的区别#

getDeclaredXxxgetXxx
权限全权限(含 private)只能拿 public
范围只看当前类(不含父类)向上追溯父类;getMethods() 还包含 Object 继承来的方法

实际框架正是这样用的:Spring 的 @Autowired 给私有字段注入依赖,用的是 getDeclaredField() 加 setAccessible(true);MyBatis 给私有字段找 setter,用的是 getDeclaredMethods()。

反射绕过泛型检查:泛型擦除的证据#

ArrayList<String> name = new ArrayList<>(); // 先放几个字符串 hello/world/java
Class c1 = name.getClass();
Method add = c1.getDeclaredMethod("add", Object.class);
add.invoke(name, 9.9); // 反射拿到的是 add(Object) —— 塞 Double 不报错
add.invoke(name, true);
System.out.println(name); // [hello, world, java, 9.9, true] ← 真塞进去了

为什么能绕过:JVM 里根本不存在泛型。List<String> 的类型检查只在编译期有效,生成字节码时 add 已被擦除成 add(Object)(泛型擦除),反射拿到的就是擦除后的原始签名,传什么都能进去。

记忆点:泛型是编译期语法糖,运行期擦除成 Object。这段代码就是泛型擦除最直观的现场演示。

用反射写通用框架代码#

一个保存任意对象字段的框架方法:

public static void saveObject(Object obj) throws Exception {
Class<?> clazz = obj.getClass(); // 1. 拿到 Class 对象
Field[] fields = clazz.getDeclaredFields(); // 2. 拿到全部字段
for (Field field : fields) {
field.setAccessible(true); // 3. 私有也能访问
String fieldName = field.getName();
Object fieldValue = field.get(obj); // 4. 取字段名 + 值
System.out.println(fieldName + "=" + fieldValue);
}
}

为什么必须用反射:框架要接收「任意」对象——编译期连有哪些类都不知道,只有运行期拿到对象才第一次知道它有多少字段、字段叫什么。反射是唯一能在运行期反查类结构的手段。

Dog / Student / Teacher 三个完全不同的类,同一个 saveObject 通吃——这就是框架「通用」的本质。

自定义注解#

public @interface A { // @interface 定义注解
String name(); // 属性写法像方法声明
int age() default 18; // default 设置默认值
String[] address(); // 数组属性
}
public @interface B { String value(); }
@B("delete") // 只有 value 一个属性时,可省略 value=
public class AnnotationDemo1 { }
  • 注解编译后其实是个接口:自动继承 java.lang.annotation.Annotation,属性就是抽象方法,返回类型即属性类型
  • 属性类型有限制:8 种基本类型 / String / Class / 枚举 / 注解,以及它们的数组
  • value 特殊属性:注解只有一个属性且名字叫 value 时,可用 @B("delete") 省略 value=;属性多于一个、或名字不叫 value,就不能省

元注解:@Target 与 @Retention#

元注解是标在注解定义上的注解,用来约束注解本身:

@Target({ElementType.METHOD, ElementType.TYPE}) // 能贴在哪:方法、类……
@Retention(RetentionPolicy.RUNTIME) // 活到什么时候:运行时
public @interface Mytest2 {
String value();
String[] address();
double price() default 100;
}

@Target 指定注解能贴的代码元素:TYPE(类/接口)、METHOD(方法)、FIELD(字段)、PARAMETER(参数)、CONSTRUCTOR、ANNOTATION_TYPE(注解上的注解) 等。

@Retention 三档生命周期:

档位生命周期典型例子
SOURCE只保留到源码@Override(编译期检查,class 文件里没有)
CLASS保留进 class 字节码Lombok、编译期处理器
RUNTIME运行期可反射读取@Test、@Autowired

陷阱:不写 @Retention 默认是 CLASS——想用反射解析注解必须显式声明 RUNTIME,否则 isAnnotationPresent 永远返回 false。

解析注解:模拟 JUnit#

// 读注解:判断 → 获取 → 使用
if (c1.isAnnotationPresent(Mytest2.class)) { // 1. 判断有没有该注解
Mytest2 m = c1.getDeclaredAnnotation(Mytest2.class); // 2. 拿注解对象(泛型方法,不用强转)
System.out.println(m.value()); // 3. 读属性
System.out.println(Arrays.toString(m.address()));
}
// 模拟 JUnit:有 @Mytest 标记的方法才执行
for (Method method : c.getMethods()) {
if (method.isAnnotationPresent(Mytest.class)) { // 有标记
method.invoke(ad); // 就执行
}
}

模拟 JUnit 的运行结果:test2、test3 执行(有 @Mytest 标记),test1、test4 跳过。

原理一句话:注解本身是元数据,什么都不干;真正干活的是反射——把注解读出来,按规则调用方法。注解负责标记,反射负责驱动。现在用的 @Test 底层就是这么跑的,Spring 的注解体系也是同一套。

动态代理#

Proxy.newProxyInstance 三个参数加 InvocationHandler:

StarService proxy = (StarService) Proxy.newProxyInstance(
ProxyUtil.class.getClassLoader(), // 参数1:类加载器(加载动态生成的代理类)
s.getClass().getInterfaces(), // 参数2:代理类要实现的接口
new InvocationHandler() { // 参数3:调用处理程序
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("sing".equals(method.getName())) System.out.println("准备话筒,收钱20w");
else if ("dance".equals(method.getName())) System.out.println("准备场地,收钱100w");
return method.invoke(s, args); // 真正干活:反射调真实对象
}
});

调用流程:proxy.sing("小幸运") → 代理对象内部拦截 → 转交 InvocationHandler.invoke() → 反射执行真实 Star.sing()。

无侵入增强#

真实 Star 类的源代码一个标点都不用改,就多了「准备话筒/收钱」的逻辑——符合开闭原则,代理逻辑可复用。对比直接改 Star.sing() 加打印:那是侵入式,把经纪人逻辑写死在明星身上,换需求还得动源码。

为什么 JDK 动态代理必须实现接口#

newProxyInstance 在运行期动态生成代理类 $Proxy0,它已经 extends Proxy——Java 单继承,没法再继承业务类。唯一能规定「代理替真实对象干哪些活」的途径就是接口:$Proxy0 implements StarService。

好处:代理对象和真实对象实现同一接口,调用方只认接口,天然多态(面向接口编程的落地)。

反向考点:类没实现接口,JDK 动态代理就用不了,换 CGLib(基于继承生成子类代理)——这也是 Spring AOP 默认策略的由来。

重点速记#

  • 获取 Class 三方式:类名.class / Class.forName("全限定名") / 对象.getClass(),同一类加载器下全局唯一
  • 反射三成分:Constructor.newInstance() / Field.set·get() / Method.invoke();setAccessible(true) 取消访问检查(暴力反射)
  • getDeclaredXxx 全权限只看本类 vs getXxx 只拿 public 且向上追溯含 Object
  • 泛型擦除:编译期语法糖,运行期擦成 Object;反射拿到 add(Object) 就能绕过 List<String> 类型检查
  • 只有反射能在运行期反查任意对象的字段结构,框架底层因此离不开反射
  • 自定义注解:@interface + 属性(default 默认值)+ value 特殊属性可省略;编译后本质是接口
  • 元注解:@Target 管位置(TYPE/METHOD/FIELD…);@Retention 管生命周期(SOURCE/CLASS/RUNTIME,反射解析必须 RUNTIME)
  • 注解解析:isAnnotationPresent 判断 + getDeclaredAnnotation 获取;模拟 JUnit = 注解标记 + 反射驱动
  • 动态代理:Proxy.newProxyInstance(类加载器, 接口数组, InvocationHandler);invoke 里加增强逻辑,method.invoke 调真实对象
  • 无侵入增强符合开闭原则;JDK 代理必须实现接口(单继承限制),没接口用 CGLib

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
Java 进阶 Day05:反射注解与动态代理
https://ryuki.icu/posts/java-advanced-day05/
作者
RK
发布于
2026-08-09
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
RK
Long may the sunshine
公告
Long may the sunshine
分类
标签
最新动态
站点天气
天气加载中…
站点统计
文章
27
分类
6
标签
59
总字数
39,139
篝火已燃
0 天
最后活动
0 天前
每日一签