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); // trueSystem.out.println(c2 == c3); // true三种方式拿到的是同一个 Class 对象:类只被类加载器加载一次,缓存在方法区,之后一直复用(同一类加载器下全局唯一)。
选用场景:
- 编译期就知道类 →
类名.class(最安全) - 手里已有对象 →
getClass() - 运行期才决定类名(比如配置文件里写的)→
Class.forName("全限定名"),需捕获ClassNotFoundException
反射操作类的三种成分
反射能在运行期拿到并操作类的三类成分,对应 java.lang.reflect 包下的三个类:
| 成分 | 类 | 操作 |
|---|---|---|
| 构造器 | Constructor | newInstance(参数...) 创建对象 |
| 成员变量 | Field | set(对象, 值) / get(对象) 读写字段 |
| 成员方法 | Method | invoke(对象, 参数...) 调用方法 |
暴力反射 setAccessible(true):取消 Java 语言级的访问控制检查,private 成员也能访问——「破解封装」。
注意点:
newInstance()无参重载只能调无参构造器(没有无参构造会抛InstantiationException);private 构造器必须先setAccessible(true)再 newinvoke()返回方法的返回值(Object 类型),void 方法返回 null
getDeclaredXxx 与 getXxx 的区别
| getDeclaredXxx | getXxx | |
|---|---|---|
| 权限 | 全权限(含 private) | 只能拿 public |
| 范围 | 只看当前类(不含父类) | 向上追溯父类;getMethods() 还包含 Object 继承来的方法 |
实际框架正是这样用的:Spring 的 @Autowired 给私有字段注入依赖,用的是 getDeclaredField() 加 setAccessible(true);MyBatis 给私有字段找 setter,用的是 getDeclaredMethods()。
反射绕过泛型检查:泛型擦除的证据
ArrayList<String> name = new ArrayList<>(); // 先放几个字符串 hello/world/javaClass 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
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!





















