模板清单与依赖
这份附录把第 2 章的代码整理成可以整段抄走的模板,全部只依赖 JDK,不需要 Spring AOP、Byte Buddy 或 CGLIB:三个运行时注解、三个拦截器、仓储代理工厂、record 读取与 MethodHandles 替代、模块开放。
所有 Java 类在 /tmp/vol3-d/templates/ 下用 Temurin 25.0.5 编译通过,并由 TemplateDemo 实际运行;module-info 片段需要按实际模块名替换目标模块后再编译。模板里的 TransactionManager 是接口,libraryd 卷二已有 ThreadLocalTransactionManager 的 JDBC 实现;本附录只依赖接口,不重复实现提交与回滚。
注解定义
package com.plume.library.domain;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Audit { String action(); }
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Retry {
int maxAttempts() default 3;
long backoffMillis() default 50;
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Transactional { }
三个注解都必须是 RUNTIME,否则代理运行时读不到;@Target(METHOD) 把使用范围限制在方法上,避免有人标到类上却期待生效。方法注解不会从接口继承到实现,也不会从实现回传到接口,这正是下一节要处理的问题。
注解查找:接口 Method 上为什么看不到
JDK 动态代理的 InvocationHandler.invoke 拿到的是接口方法的 Method 对象。如果注解写在 JdbcBookRepository 的实现方法上,直接在 method.getAnnotation(...) 里找会返回 null,事务和审计就静默失效。
泛型擦除还会让签名与注解对不上。Repository<T, ID>.save(T) 在接口字节码里擦除为 save(Object);如果 BookRepository 不重新声明 save(Book),代理收到的参数类型就是 Object。实现类里虽然会生成桥方法 save(Object),javac 也可能把注解复制到桥上,但这属于编译器实现细节,不能当作契约。模板的做法是:
BookRepository把四个泛型方法按具体类型重新声明一遍。- 注解查找先按「方法名 + 参数类型」定位实现类方法,找不到再回退到接口方法。
package com.plume.library.persistence;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
public final class Annotations {
private Annotations() {
}
public static <A extends Annotation> A find(
Class<?> targetType, Method interfaceMethod, Class<A> annotationType) {
try {
Method impl = targetType.getMethod(
interfaceMethod.getName(), interfaceMethod.getParameterTypes());
A found = impl.getAnnotation(annotationType);
if (found != null) {
return found;
}
} catch (NoSuchMethodException ignored) {
// 接口方法可能由父接口声明,回退到接口方法本身。
}
return interfaceMethod.getAnnotation(annotationType);
}
}
public interface BookRepository extends Repository<Book, String> {
Optional<Book> findById(String isbn);
List<Book> findAll();
void save(Book entity);
void deleteById(String isbn);
List<Book> findByAuthor(String author);
}
重新声明不是多余代码,它让代理、反射和 getMethod 都看到具体参数类型。代价是接口变长,收益是注解和代理行为可预测。
AuditInvocationHandler
package com.plume.library.persistence;
import com.plume.library.domain.Audit;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public final class AuditInvocationHandler implements InvocationHandler {
@FunctionalInterface
public interface AuditSink {
void record(String action, String method, long elapsedNanos, boolean success);
}
private final Object target;
private final AuditSink sink;
private final InvocationHandler next;
public AuditInvocationHandler(Object target, AuditSink sink, InvocationHandler next) {
this.target = target;
this.sink = sink;
this.next = next;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Audit audit = Annotations.find(target.getClass(), method, Audit.class);
if (audit == null) {
return next.invoke(proxy, method, args);
}
long start = System.nanoTime();
boolean success = false;
try {
Object result = next.invoke(proxy, method, args);
success = true;
return result;
} finally {
sink.record(audit.action(), method.getName(), System.nanoTime() - start, success);
}
}
}
审计必须放在 finally:失败调用同样要留痕,否则只看成功日志会误判「没有异常」。success 只在正常返回前置真,Error 和异常都会留下 false。AuditSink 用函数式接口注入,生产环境接 SLF4J 或审计表,测试里可以收集到列表中断言。
libraryd 的 LibraryService.borrow、returnBook 用 @Audit(action = "loan.borrow") 这类动作名,审计记录包含会员、ISBN 和耗时;不要把密码、令牌等敏感参数拼进 action。
TransactionalInvocationHandler
TransactionManager 的契约是「把一段工作放进事务,返回结果」,卷二已有实现:
package com.plume.library.persistence;
import java.util.function.Supplier;
public interface TransactionManager {
<T> T inTransaction(Supplier<T> work);
}
package com.plume.library.persistence;
import com.plume.library.domain.Transactional;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.UndeclaredThrowableException;
public final class TransactionalInvocationHandler implements InvocationHandler {
private final Object target;
private final TransactionManager transactionManager;
private final InvocationHandler next;
public TransactionalInvocationHandler(
Object target, TransactionManager transactionManager, InvocationHandler next) {
this.target = target;
this.transactionManager = transactionManager;
this.next = next;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Annotations.find(target.getClass(), method, Transactional.class) == null) {
return next.invoke(proxy, method, args);
}
return transactionManager.inTransaction(() -> {
try {
return next.invoke(proxy, method, args);
} catch (RuntimeException | Error e) {
throw e;
} catch (Throwable t) {
throw new UndeclaredThrowableException(t);
}
});
}
}
Supplier<T> 不能抛受检异常,所以受检异常要包装成 UndeclaredThrowableException。仓储接口的方法都是 Optional、List、void,不会触发这条路径;如果以后给接口加受检异常,要么改 TransactionManager 的函数式接口,要么在代理里做受检异常还原。
void 方法返回 null 没有问题,代理会把 null 当返回值丢弃。基本类型返回值要避免:事务回调返回 null 时自动拆箱会抛 NullPointerException。
RetryInvocationHandler
package com.plume.library.persistence;
import com.plume.library.domain.Retry;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public final class RetryInvocationHandler implements InvocationHandler {
private final Object target;
private final InvocationHandler next;
public RetryInvocationHandler(Object target, InvocationHandler next) {
this.target = target;
this.next = next;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Retry retry = Annotations.find(target.getClass(), method, Retry.class);
if (retry == null) {
return next.invoke(proxy, method, args);
}
Throwable last = null;
for (int attempt = 1; attempt <= retry.maxAttempts(); attempt++) {
try {
return next.invoke(proxy, method, args);
} catch (Throwable t) {
last = t;
if (attempt == retry.maxAttempts() || !isTransient(t)) {
throw t;
}
try {
Thread.sleep(retry.backoffMillis() * attempt);
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw t;
}
}
}
throw new IllegalStateException("retry loop exited without result", last);
}
private static boolean isTransient(Throwable t) {
return t instanceof RuntimeException;
}
}
这个模板故意只重试 RuntimeException,因为「所有异常都重试」会把参数校验错误、BOOK_NOT_FOUND 也重试三遍,既慢又掩盖问题。生产代码应该按 SQLState 或异常类型白名单判断,例如只重试死锁 40P01、序列化失败 40001、连接瞬断。
链顺序是 Audit → Retry → Transactional → Target。重试放在事务外层,每次重试都开一个新事务;如果顺序反过来,失败事务被标记回滚后再重试,仍然在同一个事务里,重试没有意义。这也要求被重试的方法本身幂等,或者业务能接受重复执行。
代理工厂
package com.plume.library.persistence;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public final class RepositoryProxyFactory {
private RepositoryProxyFactory() {
}
@SuppressWarnings("unchecked")
public static <T> T create(
Class<T> repositoryInterface,
T target,
TransactionManager transactionManager,
AuditInvocationHandler.AuditSink auditSink) {
if (!repositoryInterface.isInterface()) {
throw new IllegalArgumentException(
"dynamic proxy target must be an interface: " + repositoryInterface.getName());
}
InvocationHandler chain = new TargetInvocationHandler(target);
chain = new TransactionalInvocationHandler(target, transactionManager, chain);
chain = new RetryInvocationHandler(target, chain);
chain = new AuditInvocationHandler(target, auditSink, chain);
Object proxy = Proxy.newProxyInstance(
repositoryInterface.getClassLoader(),
new Class<?>[] {repositoryInterface},
chain);
return (T) proxy;
}
}
TargetInvocationHandler 是链尾,负责真正调用实现并拆掉 InvocationTargetException:
public final class TargetInvocationHandler implements InvocationHandler {
private final Object target;
public TargetInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
return method.invoke(target, args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
}
}
使用方式:
BookRepository repository = RepositoryProxyFactory.create(
BookRepository.class, new JdbcBookRepository(dataSource), transactionManager, auditSink);
repository.save(book);
libraryd 的 JdbcBookRepository、JdbcLoanRepository 保持普通类,装配层决定是否套代理。测试里可以传真实的 JdbcBookRepository 加内存事务管理器,也可以传一个假实现验证审计和重试,不需要启动数据库。
编译与运行
export JAVA_HOME=~/.jdks/jdk-25.0.5+8/Contents/Home
export PATH="$JAVA_HOME/bin:$PATH"
javac --release 25 -d out $(find src -name '*.java')
java -cp out com.plume.library.demo.TemplateDemo
本机输出(耗时数值随机器和 JIT 变化):
audit action=book.save method=save cost=9.554ms success=true
findById=Book[isbn=978-7-115-54736-1, title=Java 高级编程, author=Leeting Yan, year=2026]
audit action=book.find_by_author method=findByAuthor cost=1.100ms success=true
findByAuthor=[Book[isbn=978-7-115-54736-1, title=Java 高级编程, author=Leeting Yan, year=2026]]
tx active after call=false
retry result=[Book[isbn=978-7-115-54736-1, title=Java 高级编程, author=Leeting Yan, year=2026]]
retry attempts=3
retry attempts=3 说明前两次抛出的 IllegalStateException 被重试,第三次成功。tx active after call=false 说明事务标记在调用结束后清理干净,没有泄漏到线程后续请求。
反射读取 record 组件
package com.plume.library.domain;
import java.lang.reflect.Method;
import java.lang.reflect.RecordComponent;
import java.util.LinkedHashMap;
import java.util.Map;
public final class RecordReader {
private RecordReader() {
}
public static Map<String, Object> read(Object value) throws ReflectiveOperationException {
Class<?> type = value.getClass();
if (!type.isRecord()) {
throw new IllegalArgumentException(type.getName() + " is not a record");
}
Map<String, Object> result = new LinkedHashMap<>();
for (RecordComponent component : type.getRecordComponents()) {
Method accessor = component.getAccessor();
result.put(component.getName(), accessor.invoke(value));
}
return result;
}
}
getRecordComponents() 只对 record 返回非空数组,顺序与声明顺序一致;getAccessor() 返回公开的访问器方法,invoke 不需要 setAccessible(true)。本机运行:
record reflection={isbn=978-7-115-54736-1, title=Java 高级编程, author=Leeting Yan, year=2026}
libraryd 用这个模板做通用的 DTO 转换、审计字段提取和测试断言。注意 RecordComponent.getType() 是擦除后的类型,泛型组件要用 getGenericType();嵌套 record 只读取第一层,递归要自己控制深度和循环引用。
MethodHandles 替代
package com.plume.library.domain;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.RecordComponent;
import java.util.LinkedHashMap;
import java.util.Map;
public final class MethodHandleRecordReader {
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
private MethodHandleRecordReader() {
}
public static Map<String, Object> read(Object value) throws Throwable {
Class<?> type = value.getClass();
if (!type.isRecord()) {
throw new IllegalArgumentException(type.getName() + " is not a record");
}
Map<String, Object> result = new LinkedHashMap<>();
for (RecordComponent component : type.getRecordComponents()) {
MethodHandle accessor = LOOKUP.unreflect(component.getAccessor());
result.put(component.getName(), accessor.invoke(value));
}
return result;
}
}
本机输出与反射版本一致:
record methodhandle={isbn=978-7-115-54736-1, title=Java 高级编程, author=Leeting Yan, year=2026}
MethodHandle.invoke 是签名多态方法,调用点会被 JVM 特化;invokeExact 要求参数和返回类型完全匹配。它不会自动做反射那套访问检查和参数装箱,但是否更快必须用 JMH 测,不能凭「MethodHandle 更底层」下结论。需要频繁调用同一个访问器时,把 MethodHandle 缓存在 static final 或 ConcurrentHashMap 里,不要每次循环都 unreflect。
InaccessibleObjectException 与模块开放
JDK 9 之后,反射访问 JDK 内部 API 默认被强封装。下面这段代码在 JDK 25 上不带参数运行:
Field elementData = ArrayList.class.getDeclaredField("elementData");
elementData.setAccessible(true);
本机输出:
elementData exception=java.lang.reflect.InaccessibleObjectException
elementData message=Unable to make field transient java.lang.Object[] java.util.ArrayList.elementData accessible: module java.base does not "opens java.util" to unnamed module @610455d6
处理方式有三种,按优先级排列:
- 不用内部字段。 先找公开 API;
ArrayList的容量不是业务需要,通常说明思路错了。 - 命令行开放。 仅限本地排查或测试:
java --add-opens java.base/java.util=ALL-UNNAMED -cp out com.plume.library.demo.TemplateDemo
本机加上参数后输出变为 elementData accessible=true。
- 模块描述符开放。 如果是自己的模块给框架反射,在
module-info.java里写opens,不要要求用户加--add-opens:
module com.plume.library.domain {
exports com.plume.library.domain;
opens com.plume.library.domain to com.fasterxml.jackson.databind;
}
opens 只开放反射访问,不导出编译期依赖;exports 才是给其他模块 requires 用的。限定目标模块比无限制 opens com.plume.library.domain; 安全,但目标模块改名时要同步改这里。
小结
- 动态代理的
Method来自接口,注解查找必须回到实现类;泛型方法要在子接口重新声明具体类型。 AuditInvocationHandler用finally记录成功和失败,审计动作名不要包含敏感参数。TransactionalInvocationHandler把工作交给TransactionManager,受检异常和基本类型返回值是两个边界。RetryInvocationHandler只重试瞬时错误,且必须位于事务外层,保证每次重试都有新事务。- record 组件可以通过
getRecordComponents()读取,MethodHandles.lookup().unreflect是等价替代。 InaccessibleObjectException优先用公开 API 规避,其次命令行--add-opens,最后才是模块opens。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。