8.3 翻档案与贴标签:反射与注解


8.3 翻档案与贴标签:反射与注解

本节摘要:反射是运行时拆开类档案的能力——拿到 Class 对象即可枚举构造器、字段与方法,甚至调用私有成员(setAccessible 的特权与代价)。注解是档案上的标签纸:自身不执行任何逻辑,靠运行时反射被读取;自定义注解用元注解声明保留策略与使用位置。框架的"魔法"——依赖注入、对象映射、测试发现——几乎全部建立在这套"标签加反射"的组合上。本节完成一次反射拆解与一个自定义注解的完整闭环。

你写下的注解,是谁在替你干活

先描述一个每个 Java 工程师都见过、却少有人追问的场景:业务类上贴一个注解,启动时框架就自动把它注册成组件、把依赖注入进来;测试方法上贴一个注解,测试工具就自动找到并执行它。你从没写过那些"自动"的代码——它们从哪来?

答案是两件套。注解是标签纸:贴在类、方法、字段上,本身只是一串元数据,没有任何执行逻辑;反射是翻档案的特权:运行时拿到任何一个类的 Class 对象,就能枚举它的全部构造器、字段、方法——包括私有的——还能读出上面贴了哪些标签。框架的启动流程就是"扫描所有档案、读标签、按标签代劳"。

// 先看反射最基础的用法:拿 Class 对象的三种方式 public class ClassDemo { public static void main(String[] args) throws Exception { Class<?> c1 = String.class; // 方式一:类名点 class Class<?> c2 = "户籍".getClass(); // 方式二:对象调 getClass(第 2 章 Object 契约) Class<?> c3 = Class.forName("java.lang.String"); // 方式三:字符串类名加载(框架扫描用这种) System.out.println(c1 == c2); // 输出:true 同一个类只有一份 Class 对象 System.out.println(c2 == c3); // 输出:true System.out.println(c1.getName()); // 输出:java.lang.String } }

同一个类在运行时只有一份 Class 对象(第 5 章泛型擦除说"运行时只剩这一份"正是它)——Class 对象就是类的档案本体:类名、继承谁、实现哪些接口、有哪些成员,全记在这里。

拆开一份档案

拿到 Class 对象后,反射能翻出全部登记信息,还能绕过 new 直接造对象、绕过 private 直接调方法:

import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; class Ledger { private String owner = "未登记"; private int items = 0; private void addItem(String entry) { // 私有方法:正常途径外界不可见 items++; System.out.println(owner + " 记账:" + entry + " 当前 " + items + " 条"); } } public class ReflectDemo { public static void main(String[] args) throws Exception { Class<?> c = Ledger.class; // 一:造对象 —— 不写 new 也能实例化(无参构造器) Constructor<?> ctor = c.getDeclaredConstructor(); Object ledger = ctor.newInstance(); // 二:翻字段 —— 读到私有字段的值 Field owner = c.getDeclaredField("owner"); owner.setAccessible(true); // 特权:绕过 private 检查 System.out.println("私有字段原值:" + owner.get(ledger)); // 输出:私有字段原值:未登记 owner.set(ledger, "张三的账本"); System.out.println("改后:" + owner.get(ledger)); // 输出:改后:张三的账本 // 三:翻方法 —— 调用私有方法 Method add = c.getDeclaredMethod("addItem", String.class); add.setAccessible(true); add.invoke(ledger, "买印章"); // 输出:张三的账本 记账:买印章 当前 1 条 add.invoke(ledger, "买账册"); // 输出:张三的账本 记账:买账册 当前 2 条 } }

三步走完,反射的能力边界清楚了:getDeclared 系列能看到私有的,get 系列只看公开的;setAccessible(true) 是"特权开关",绕过访问检查——代价也要认清:破坏封装(第 1 章的锁被撬了)、绕过编译期类型检查(错误从编译期退回运行期)、性能低于直接调用(框架里用在启动或低频路径,不放在循环里烧)。

自定义注解:标签纸的完整闭环

注解的定义长得像接口,加个 @ 符号;注解上还能贴"元注解"——描述标签纸本身怎么用的标签。自定义一个可被反射读取的注解并完成闭环:

import java.lang.annotation.*; import java.lang.reflect.Method; @Retention(RetentionPolicy.RUNTIME) // 元注解一:保留到运行时 否则反射读不到(默认留在字节码前就丢) @Target(ElementType.METHOD) // 元注解二:只能贴在方法上 @interface Audit { // 注解只有一个属性时惯例用 value 命名 String value() default "普通操作"; // 属性带默认值 贴标签时可不填 int level() default 1; } class RegistryOps { @Audit(value = "新增登记", level = 3) // 贴标签 void register() { System.out.println("执行登记"); } @Audit("变更登记") // 只有 value 时可省属性名 void modify() { System.out.println("执行变更"); } void query() { System.out.println("执行查询"); } // 没贴标签 } public class AuditDemo { public static void main(String[] args) throws Exception { RegistryOps ops = new RegistryOps(); // 框架式流程:扫描方法 读标签 按标签决定附加行为 for (Method m : RegistryOps.class.getDeclaredMethods()) { Audit tag = m.getAnnotation(Audit.class); // 反射读标签:没贴返回 null if (tag != null) { System.out.println("[审计] " + tag.value() + " 级别 " + tag.level()); } m.invoke(ops); // 依次执行三个方法 } } }

运行输出:查询方法前没有审计行、登记与变更前各有一行带各自的标签值——注解本身没干任何事,是读注解的代码在干活。这就是框架魔法的全部底细:注解负责声明"我是什么",反射负责发现"谁是什么"然后代劳。依赖注入是"扫到组件标签就建对象并填充字段",对象映射是"扫到字段标签就按名字取值赋值",测试框架是"扫到测试标签就把方法排进执行列表"——同一套组合的不同应用。

完整案例:迷你审计器

背景:给一组业务方法加统一审计——凡贴了 Audit 标签的方法,调用前后自动记日志,业务代码本身一行日志都不写。操作:用反射代理调用,读标签决定是否包装:

import java.lang.reflect.Method; class Biz { @Audit(value = "开户", level = 2) void open() { System.out.println("开户逻辑"); } @Audit(value = "销户", level = 5) void close() { System.out.println("销户逻辑"); } void health() { System.out.println("健康检查"); } // 不需要审计 } public class MiniAudit { // 统一入口:按方法名找到方法 检查标签 决定是否包一层审计 static void call(Biz target, String methodName) throws Exception { Method m = Biz.class.getDeclaredMethod(methodName); Audit tag = m.getAnnotation(Audit.class); if (tag != null) { System.out.println("审计开始:" + tag.value() + " 级别 " + tag.level()); m.invoke(target); System.out.println("审计结束:" + tag.value()); } else { m.invoke(target); // 没标签 直接调用 } } public static void main(String[] args) throws Exception { Biz biz = new Biz(); call(biz, "open"); // 输出:审计开始:开户 级别 2 / 开户逻辑 / 审计结束:开户 call(biz, "close"); // 输出:审计开始:销户 级别 5 / 销户逻辑 / 审计结束:销户 call(biz, "health"); // 输出:健康检查 —— 无标签 无审计 } }

结果:业务方法里没写一行审计代码,审计行为完全由标签驱动、可按方法自由开关。解读:这就是"声明式编程"的雏形——把"做什么"(业务)与"怎么配套"(审计、事务、重试)分离,配套逻辑集中在一处读取标签统一执行;生产框架用动态代理把这套包装做得对调用方完全透明,思想与这个迷你版完全一致。变式:标签加在字段上,启动时反射扫描全部类、按字段标签做配置注入;标签加在类上,实现按标签路由到不同处理器的注册机制。注解保留策略三档——源码级(编译后丢,给编译器看的)、字节码级(进字节码但运行时读不到)、运行时级(反射可读)——要反射读就必须声明运行时级,忘了写 Retention 是自定义注解"读不到"的第一原因。

反射的代价与纪律

特权必有代价,反射的纪律三条。别撬锁:setAccessible 破坏封装,只用于框架与工具(测试、序列化、依赖注入),业务代码里出现它多半是设计问题——真正该做的是给类开公开入口。别进热路径:反射调用慢于直接调用,用在启动、配置、低频调度场景;高频循环里的反射是性能杀手。编译器帮不了你:方法名拼错从编译错误变成运行时异常(找不到方法异常),反射代码要有充分的测试覆盖。第 5 章泛型三禁令的绕行方案(传 Class 对象进来)、第 6 章序列化恢复对象、本节的注解读取——回头看,反射一直是那些"编译期做不到的事"的共同出口。

本节要点回顾

  • Class 对象是类的运行时档案:一个类只有一份;类名点 class、对象 getClass、按名字加载三种获取方式,框架扫描用第三种
  • 反射翻档案三步:getDeclaredConstructor 造对象、getDeclaredField 翻字段、getDeclaredMethod 调方法;getDeclared 系列连私有的都能看,setAccessible 是绕过封装的特权开关
  • 注解是标签纸不干活:执行逻辑全在读注解的代码里;自定义注解靠元注解声明保留策略与使用位置,反射读取必须用运行时级保留
  • 框架等于标签加反射:依赖注入、对象映射、测试发现是同一套组合的不同应用——声明式编程的地基
  • 三条纪律:不撬锁(业务代码别用 setAccessible)、不进热路径、拼错名字从编译期退到运行期要靠测试兜底

全书到此收束:登记手续、族谱与证书、工具房三间、差错登记科、特殊窗口——八个窗口串起 Java 面向对象机制与常用 API 的主干。回头看导读的知识地图,把每条连线上的机制讲给别人听一遍,这门课就真正毕业了。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U