Java 泛型、反射、动态代理、SPI、枚举、注解、Lambda、equals/hashCode 和表达式引擎,看起来像一组互不相干的知识点。实际上,它们共同回答了 Java 工程中的几个核心问题:
如何在编译期保证类型安全?
如何在运行时读取类型信息并调用未知代码?
如何在不修改主程序的情况下扩展实现?
如何减少硬编码分支,让业务规则可组合、可配置、可治理?
如何保证对象在集合、缓存、代理和持久化场景中的身份语义正确?
本文以 Java 21+ 的工程实践为主要背景,基础语义同时适用于 Java 8 及之后版本。部分内容结合 Java SE 25 规范说明。文中的代码以解释机制为主,生产落地时还需要结合项目的异常体系、日志规范、监控体系和安全边界进行完善。
一、先建立整体认知:这些机制如何连接 flowchart LR
A[编译期类型系统] --> A1[泛型]
A --> A2[Lambda 目标类型]
A --> A3[注解处理器]
B[运行时元编程] --> B1[反射]
B --> B2[动态代理]
B --> B3[运行时注解]
C[扩展与解耦] --> C1[SPI / ServiceLoader]
C --> C2[策略模式]
C --> C3[表达式引擎]
D[领域建模] --> D1[枚举]
D --> D2[equals / hashCode]
D --> D3[规则对象]
A1 --> B1
B1 --> B2
B1 --> B3
C1 --> B1
A2 --> C2
C2 --> C3
D1 --> C2
D2 --> C3
可以把这些技术分成四层:
层次
代表机制
主要解决的问题
编译期抽象
泛型、Lambda、注解处理器
类型安全、减少重复代码、生成代码
运行时元编程
反射、动态代理、运行时注解
动态发现、动态调用、横切增强
扩展机制
SPI、策略模式、表达式引擎
插件化、业务规则动态化
对象语义
枚举、equals/hashCode
状态建模、对象身份、集合正确性
下面逐一展开。
第一部分:Java 泛型 二、泛型到底解决了什么问题 泛型最直接的作用是把原本只能在运行时发现的类型错误,尽可能提前到编译期。
没有泛型时,集合只能保存 Object:
1 2 3 4 5 6 List values = new ArrayList (); values.add("Java" ); values.add(42 );String first = (String) values.get(0 );String second = (String) values.get(1 );
使用泛型后:
1 2 3 4 5 List<String> values = new ArrayList <>(); values.add("Java" );String first = values.getFirst();
泛型带来的价值不只是“少写一次强制类型转换”,而是:
编译期类型检查;
API 的输入输出关系更清晰;
算法可以与具体类型解耦;
IDE 能提供更准确的补全与重构;
框架可以表达容器、转换器、仓储、事件等抽象。
三、泛型类、泛型接口与泛型方法 3.1 泛型类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public final class Result <T> { private final boolean success; private final T data; private final String message; private Result (boolean success, T data, String message) { this .success = success; this .data = data; this .message = message; } public static <T> Result<T> success (T data) { return new Result <>(true , data, null ); } public static <T> Result<T> failure (String message) { return new Result <>(false , null , message); } public boolean isSuccess () { return success; } public T getData () { return data; } public String getMessage () { return message; } }
这里的 T 是类型参数。实例化时,类型参数被具体类型替换:
1 2 Result<String> stringResult = Result.success("ok" ); Result<Long> idResult = Result.success(1001L );
3.2 泛型接口 1 2 3 4 5 @FunctionalInterface public interface Converter <S, T> { T convert (S source) ; }
实现时可以固定类型:
1 2 3 4 5 6 7 8 public final class UserToUserDTOConverter implements Converter <User, UserDTO> { @Override public UserDTO convert (User source) { return new UserDTO (source.id(), source.name()); } }
也可以继续保留类型参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public final class LoggingConverter <S, T> implements Converter <S, T> { private final Converter<S, T> delegate; public LoggingConverter (Converter<S, T> delegate) { this .delegate = delegate; } @Override public T convert (S source) { System.out.println("convert source: " + source); return delegate.convert(source); } }
3.3 泛型方法 泛型方法的类型参数声明在返回值之前:
1 2 3 4 5 6 public static <T> T first (List<T> values) { if (values == null || values.isEmpty()) { throw new IllegalArgumentException ("values must not be empty" ); } return values.getFirst(); }
注意下面两段代码的区别:
1 2 3 4 5 public final class Box <T> { public T get () { return null ; } }
get() 使用的是类级别的 T。
1 2 3 4 5 public final class Boxes { public static <T> T getDefault (T value) { return value; } }
getDefault() 自己声明了方法级别的 T。
四、类型上界、下界与交叉类型 4.1 上界:extends 1 2 3 4 5 6 7 public static double sum (List<? extends Number> numbers) { double total = 0 ; for (Number number : numbers) { total += number.doubleValue(); } return total; }
? extends Number 表示某个未知类型,但它一定是 Number 或其子类型。
因此以下调用都合法:
1 2 sum(List.of(1 , 2 , 3 )); sum(List.of(1.2 , 2.3 , 3.4 ));
4.2 类型参数上界 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static <T extends Comparable <? super T>> T max ( Collection<? extends T> values) { if (values == null || values.isEmpty()) { throw new IllegalArgumentException ("values must not be empty" ); } Iterator<? extends T > iterator = values.iterator(); T max = iterator.next(); while (iterator.hasNext()) { T candidate = iterator.next(); if (candidate.compareTo(max) > 0 ) { max = candidate; } } return max; }
T extends Comparable<? super T> 比 T extends Comparable<T> 更灵活。它允许 T 与其父类型定义的比较规则兼容。
4.3 交叉类型 一个类型参数可以有多个边界:
1 2 3 4 5 6 public static <T extends AutoCloseable & Runnable> void runAndClose (T resource) throws Exception { try (resource) { resource.run(); } }
规则是:
类上界最多一个,并且必须写在最前面;
接口上界可以有多个;
使用 & 连接。
五、为什么 List<Integer> 不是 List<Number> Java 泛型默认是不变的 。
即使:
也不能推出:
1 List<Integer> extends List <Number>
假设下面的赋值被允许:
1 2 3 List<Integer> integers = new ArrayList <>(); List<Number> numbers = integers; numbers.add(3.14 );
此时 integers 中就出现了 Double,破坏了原有类型约束。
因此 Java 必须禁止这种赋值。
数组则是协变的:
1 2 3 Integer[] integers = new Integer [10 ]; Number[] numbers = integers; numbers[0 ] = 3.14 ;
数组把错误推迟到了运行时;泛型选择在编译期阻止错误。
六、通配符与 PECS 原则 PECS 是泛型 API 设计中最常用的规则:
Producer Extends :生产数据时使用 extends;
Consumer Super :消费数据时使用 super。
6.1 生产者使用 extends 1 2 3 4 5 6 7 8 public static double totalPrice ( List<? extends Product> products) { return products.stream() .map(Product::price) .mapToDouble(BigDecimal::doubleValue) .sum(); }
从 products 中读取的数据至少可以当成 Product 使用,但通常不能安全地向里面添加具体对象:
1 2 3 4 5 6 List<? extends Number > values = new ArrayList <Integer>();Number number = values.getFirst(); values.add(null );
原因在于编译器不知道 ? 到底是 Integer、Long 还是其他子类型。
6.2 消费者使用 super 1 2 3 4 public static void addDefaults (List<? super Integer> target) { target.add(1 ); target.add(2 ); }
下面都可以调用:
1 2 3 4 5 6 7 List<Integer> integers = new ArrayList <>(); List<Number> numbers = new ArrayList <>(); List<Object> objects = new ArrayList <>(); addDefaults(integers); addDefaults(numbers); addDefaults(objects);
从 List<? super Integer> 读取时,只能稳妥地当成 Object:
1 Object value = objects.getFirst();
6.3 一个标准的复制方法 1 2 3 4 5 6 public static <T> void copy ( List<? super T> destination, List<? extends T> source) { destination.addAll(source); }
source 生产 T;
destination 消费 T。
七、通配符捕获 有时方法拿到 List<?> 后,需要对其中元素进行操作。编译器会为未知通配符创建一个临时的“捕获类型”。
下面的代码不能直接编译:
1 2 3 public static void swapFirstTwo (List<?> list) { }
可以通过辅助泛型方法捕获通配符:
1 2 3 4 5 6 7 8 9 public static void swapFirstTwo (List<?> list) { swapFirstTwoCaptured(list); }private static <T> void swapFirstTwoCaptured (List<T> list) { T first = list.get(0 ); list.set(0 , list.get(1 )); list.set(1 , first); }
这不是“绕过类型安全”,而是向编译器说明:列表中的未知类型虽然不知道是什么,但两次操作使用的是同一个类型。
八、类型擦除:泛型在 JVM 中如何存在 Java 泛型主要通过类型擦除 实现。编译器会:
使用类型参数的上界替换类型参数;
必要时插入强制类型转换;
为保持多态生成桥接方法;
不为 List<String> 和 List<Integer> 创建两套类。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 public final class Box <T> { private T value; public T get () { return value; } public void set (T value) { this .value = value; } }
擦除后可以近似理解为:
1 2 3 4 5 6 7 8 9 10 11 12 public final class Box { private Object value; public Object get () { return value; } public void set (Object value) { this .value = value; } }
调用:
1 2 Box<String> box = new Box <>();String value = box.get();
编译器会近似转换为:
1 String value = (String) box.get();
8.1 有界类型参数的擦除 1 2 3 public final class NumberBox <T extends Number > { private T value; }
这里 T 擦除为左侧第一个上界 Number,而不是 Object。
8.2 桥接方法 1 2 3 4 5 6 7 8 9 10 11 public interface Node <T> { void setData (T data) ; }public final class StringNode implements Node <String> { @Override public void setData (String data) { System.out.println(data); } }
擦除后,接口方法近似变成:
1 void setData (Object data) ;
但子类实际写的是:
1 void setData (String data) ;
为了保持覆盖关系,编译器会生成类似下面的桥接方法:
1 2 3 public void setData (Object data) { setData((String) data); }
通过反射查看方法时,可能看到 isBridge() 或 isSynthetic() 为 true 的方法。框架扫描方法时必须考虑桥接方法,否则容易重复注册或拿错泛型方法。
九、可具体化类型与不可具体化类型 运行时能够完整表示的类型称为可具体化类型,例如:
String;
int;
List<?>;
原始类型 List;
String[]。
运行时不能完整保留全部类型参数的类型称为不可具体化类型,例如:
List<String>;
Map<String, Long>;
类型变量 T;
List<T>。
因此下面的写法不合法:
只能写:
1 2 3 if (value instanceof List<?> list) { }
十、泛型的常见限制 10.1 不能直接 new T() 1 2 3 4 5 6 7 public final class Factory <T> { public T create () { return null ; } }
因为擦除后 JVM 不知道 T 的实际构造器。
可传入工厂:
1 2 3 4 5 6 7 8 9 10 11 12 public final class Factory <T> { private final Supplier<T> supplier; public Factory (Supplier<T> supplier) { this .supplier = supplier; } public T create () { return supplier.get(); } }
调用:
1 2 Factory<ArrayList<String>> factory = new Factory <>(ArrayList::new );
10.2 不能使用 T.class
应显式传入类型令牌:
1 2 3 4 5 6 7 8 9 10 11 12 13 public final class JsonReader <T> { private final Class<T> type; public JsonReader (Class<T> type) { this .type = type; } public T read (String json) { return null ; } }
10.3 不能创建泛型数组
数组在运行时检查实际元素类型,而泛型类型参数在运行时被擦除,这两套机制无法直接安全组合。
10.4 静态字段不能直接使用类级别类型参数 1 2 3 public final class Box <T> { }
静态字段属于类,而不是某个具体的 Box<String> 或 Box<Long> 实例。
十一、堆污染与泛型可变参数 堆污染是指:一个参数化类型变量引用了不属于该参数化类型的对象。
1 2 3 4 5 6 7 8 List<String> strings = new ArrayList <>();@SuppressWarnings({"rawtypes", "unchecked"}) List raw = strings; raw.add(42 );String value = strings.getFirst();
泛型可变参数也可能产生堆污染:
1 2 3 4 5 6 7 8 @SafeVarargs public static <T> List<T> flatten (List<? extends T>... lists) { List<T> result = new ArrayList <>(); for (List<? extends T > list : lists) { result.addAll(list); } return result; }
@SafeVarargs 不是“关闭警告就万事大吉”。只有当方法确实不会:
修改可变参数数组;
把不兼容对象写入数组;
暴露数组给不受控代码;
才应该标注。
十二、运行时如何读取泛型信息 虽然实例上的具体类型参数通常被擦除,但类文件的签名属性仍可能保存声明层面的泛型信息,反射 API 可以读取这些信息。
java.lang.reflect.Type 的常见实现包括:
类型
含义
示例
Class<?>
普通类或数组类
String.class
ParameterizedType
参数化类型
List<String>
TypeVariable<?>
类型变量
T
WildcardType
通配符
? extends Number
GenericArrayType
泛型数组
T[]
示例:
1 2 3 public final class UserRepository implements Repository <User, Long> { }
读取接口泛型:
1 2 3 4 5 6 7 8 9 10 11 Type[] interfaces = UserRepository.class.getGenericInterfaces();ParameterizedType repositoryType = (ParameterizedType) interfaces[0 ]; Type[] arguments = repositoryType.getActualTypeArguments(); System.out.println(arguments[0 ]); System.out.println(arguments[1 ]);
12.1 Type Token 对于 List<User> 这类嵌套参数类型,可以通过匿名子类捕获声明信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public abstract class TypeRef <T> { private final Type type; protected TypeRef () { Type superType = getClass().getGenericSuperclass(); if (!(superType instanceof ParameterizedType parameterizedType)) { throw new IllegalStateException ( "TypeRef must be created with generic type information" ); } this .type = parameterizedType.getActualTypeArguments()[0 ]; } public Type getType () { return type; } }
使用:
1 2 3 4 TypeRef<List<User>> typeRef = new TypeRef <>() {}; System.out.println(typeRef.getType());
Jackson 的 TypeReference、Guava 的 TypeToken 等工具采用了类似思想。
十三、泛型 API 设计建议
对外 API 不要暴露原始类型 :避免 List、Map、Class。
输入尽量宽,输出尽量具体 :输入可考虑 Collection<? extends T>。
优先使用 PECS,而不是到处写裸 T 。
避免通配符出现在返回类型中 :调用者会很难继续操作。
不要为了“看起来高级”滥用多层泛型 。
框架内部缓存反射结果时,应以完整 Type 而不只是 Class<?> 为键 。
遇到 unchecked 警告先查原因,不要立即加 @SuppressWarnings 。
公共泛型组件要测试子类、父类、通配符和原始类型污染场景 。
第二部分:Java 反射 十四、反射的本质 反射允许程序在运行时:
获取类、字段、方法、构造器的信息;
读取注解与泛型签名;
动态创建对象;
动态读取或写入字段;
动态调用方法;
根据字符串、配置或扫描结果装配程序。
正常调用是“代码在编译期就知道要调用谁”:
1 userService.createUser(command);
反射调用则是“运行时才确定调用谁”:
1 2 3 4 Method method = target.getClass() .getMethod("createUser" , CreateUserCommand.class);Object result = method.invoke(target, command);
反射并不是魔法。它依赖 JVM 已加载的类元数据,并受 Java 访问控制、模块边界和类型规则约束。
十五、获取 Class 对象的方式 1 2 3 4 5 6 7 8 9 10 Class<String> c1 = String.class;String value = "Java" ; Class<?> c2 = value.getClass(); Class<?> c3 = Class.forName("java.lang.String" ); Class<Integer> c4 = int .class; Class<String[]> c5 = String[].class;
几种方式的差异:
SomeType.class:编译期可检查,通常最推荐;
object.getClass():已经有对象时使用;
Class.forName(...):类型名来自配置、插件或外部输入时使用;
基本类型和数组也都有对应的 Class 对象。
Class.forName(String) 通常会触发类初始化。如果只想加载而不初始化,可以使用:
1 2 3 4 Class<?> type = Class.forName( "com.example.Plugin" , false , Thread.currentThread().getContextClassLoader());
十六、getXxx 与 getDeclaredXxx 的区别 以方法为例:
1 2 type.getMethods(); type.getDeclaredMethods();
API
是否包含继承成员
是否只返回 public
getMethods()
是
是
getDeclaredMethods()
否
否
getFields()
是
是
getDeclaredFields()
否
否
getConstructors()
不涉及继承
是
getDeclaredConstructors()
不涉及继承
否
框架扫描时经常需要沿继承层次向上遍历,而不能只调用一次 getDeclaredMethods()。
十七、读取类结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public static void inspect (Class<?> type) { System.out.println("name: " + type.getName()); System.out.println("simpleName: " + type.getSimpleName()); System.out.println("module: " + type.getModule().getName()); System.out.println("package: " + type.getPackageName()); System.out.println("record: " + type.isRecord()); System.out.println("enum: " + type.isEnum()); System.out.println("interface: " + type.isInterface()); System.out.println("sealed: " + type.isSealed()); for (Field field : type.getDeclaredFields()) { System.out.printf( "field: %s %s%n" , field.getType().getTypeName(), field.getName()); } for (Method method : type.getDeclaredMethods()) { System.out.printf( "method: %s %s(%s)%n" , method.getReturnType().getTypeName(), method.getName(), Arrays.toString(method.getParameterTypes())); } }
Java 的 Core Reflection 更接近 JVM 模型,而不完全等同于源码模型。编译器可能生成:
桥接方法;
合成字段;
合成方法;
捕获外部实例的字段;
Lambda 相关实现结构;
record 组件对应的方法。
框架代码通常应检查:
1 2 3 method.isSynthetic(); method.isBridge(); field.isSynthetic();
否则可能把编译器生成成员当成业务成员。
十八、动态创建对象 1 2 3 4 5 6 7 8 9 10 11 Constructor<User> constructor = User.class.getDeclaredConstructor( Long.class, String.class);if (!constructor.trySetAccessible()) { throw new IllegalStateException ( "constructor is not accessible" ); }User user = constructor.newInstance(1L , "Mario" );
不建议再使用已经废弃的:
原因包括:
只能调用无参构造器;
异常语义差;
无法清晰表达访问控制。
十九、动态调用方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Method method = UserService.class.getDeclaredMethod( "findById" , Long.class);if (!method.trySetAccessible()) { throw new IllegalStateException ( "method is not accessible" ); }try { Object result = method.invoke(userService, 1001L ); User user = (User) result; } catch (InvocationTargetException ex) { Throwable target = ex.getTargetException(); throw new IllegalStateException ( "target method failed" , target); }
Method.invoke 抛出的 InvocationTargetException 只是包装器。真正的业务异常在:
或:
如果框架不解包,日志中会出现一层又一层“调用目标异常”,定位问题非常痛苦。
二十、读写字段 1 2 3 4 5 6 7 8 9 Field field = User.class.getDeclaredField("name" );if (!field.trySetAccessible()) { throw new IllegalStateException ( "field is not accessible" ); }Object oldValue = field.get(user); field.set(user, "SuperMario" );
生产代码要谨慎修改字段,尤其是:
final 字段;
record 组件对应字段;
JPA 管理实体;
并发对象;
具备类不变式的领域对象。
反射绕过 setter,不等于绕过业务约束后仍然正确。
二十一、反射与模块系统 Java 9 之后,访问控制不再只有 public/protected/package/private,还受到模块的 exports 和 opens 约束。
exports:允许其他模块正常访问公开类型;
opens:允许其他模块进行深反射;
open module:模块中的包默认对深反射开放;
--add-opens:启动时临时开放模块包。
下面的代码不一定成功:
1 field.setAccessible(true );
失败时可能抛出:
1 java.lang.reflect.InaccessibleObjectException
更稳妥的方式是:
1 2 3 4 if (!field.trySetAccessible()) { }
不要把 --add-opens ...=ALL-UNNAMED 当成永久解决方案。它更像兼容旧框架的过渡措施。
二十二、反射读取注解 1 2 3 4 5 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Audit { String action () ; }
读取:
1 2 3 4 5 6 7 for (Method method : type.getDeclaredMethods()) { Audit audit = method.getAnnotation(Audit.class); if (audit != null ) { System.out.println( method.getName() + " -> " + audit.action()); } }
只有保留策略为 RUNTIME 的注解,才能通过运行时反射读取。
二十三、反射读取泛型 1 2 3 4 5 6 7 8 9 10 11 Method method = UserService.class .getDeclaredMethod("findAll" );Type returnType = method.getGenericReturnType();if (returnType instanceof ParameterizedType parameterizedType) { System.out.println(parameterizedType.getRawType()); System.out.println( Arrays.toString( parameterizedType.getActualTypeArguments())); }
不要只调用:
它只能得到擦除后的 Class<?>,例如 List.class,拿不到 List<User> 中的 User。
二十四、反射性能应该如何理解 “反射一定很慢”是过度简化的说法。
反射相对直接调用通常有额外成本,包括:
成员查找;
访问检查;
参数数组创建;
装箱和拆箱;
动态类型检查;
异常包装;
JIT 内联难度。
但现代 JDK 已不断优化反射。JDK 18 起,核心反射在实现层面改为基于 Method Handle。真正影响工程性能的,通常不是“用了反射”这三个字,而是:
是否在热路径上反复扫描类;
是否每次都重新查找 Method、Field;
是否创建大量临时数组和对象;
是否有更高成本的序列化、数据库或网络操作;
是否做过基准测试,而不是凭感觉判断。
24.1 缓存元数据 错误做法:
1 2 3 4 5 6 7 8 9 public Object invoke (Object target, String methodName) throws Exception { Method method = target.getClass() .getDeclaredMethod(methodName); method.setAccessible(true ); return method.invoke(target); }
每次都查找成员。
更合理的做法是按类和签名缓存:
1 2 3 4 5 public record MethodKey ( Class<?> type, String name, List<Class<?>> parameterTypes) { }
1 2 private final ConcurrentMap<MethodKey, Method> cache = new ConcurrentHashMap <>();
但缓存还要考虑:
类加载器泄漏;
热部署;
动态模块卸载;
弱引用或 ClassValue;
桥接方法与重载签名。
24.2 ClassValue 按 Class<?> 缓存元数据时,ClassValue 通常比全局 ConcurrentHashMap<Class<?>, ...> 更适合类卸载场景:
1 2 3 4 5 6 7 private static final ClassValue<List<Method>> METHODS = new ClassValue <>() { @Override protected List<Method> computeValue (Class<?> type) { return List.of(type.getDeclaredMethods()); } };
二十五、Method Handle 与 Var Handle 当调用点比较固定、处于高频路径,或者需要更精细的动态调用控制时,可以考虑:
MethodHandle:类型化的方法调用句柄;
VarHandle:字段、数组元素等变量访问句柄。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 MethodHandles.Lookup lookup = MethodHandles.lookup();MethodHandle handle = lookup.findVirtual( String.class, "substring" , MethodType.methodType( String.class, int .class, int .class));String result = (String) handle.invokeExact( "reflection" , 0 , 7 );
它比普通反射更底层,也更复杂。不要为了“性能感”把所有业务反射都改成 Method Handle。先用 JMH 测量真实热点。
二十六、反射适合与不适合的场景 适合:
IoC 容器;
ORM;
JSON 序列化;
RPC 框架;
测试框架;
插件扫描;
注解驱动框架;
对象映射工具;
调试器、诊断工具。
不适合:
可以明确写成普通调用的核心业务代码;
极高频且可静态绑定的计算路径;
依赖大量私有字段修改来维持的设计;
对模块边界毫无规划的“万能工具类”;
从不可信输入直接拼接类名、方法名并执行。
反射是基础设施能力,不应该成为业务代码逃避建模的快捷键。
第三部分:Java 动态代理 二十七、代理模式解决的不是“少写一层类”,而是分离关注点 假设系统中存在一个订单服务:
1 2 3 public interface OrderService { OrderResult createOrder (CreateOrderCommand command) ; }
业务真正关心的是“创建订单”。但在调用前后,往往还要处理:
参数校验;
权限判断;
事务控制;
日志记录;
指标采集;
链路追踪;
重试、熔断和限流;
缓存;
异常转换。
如果把这些逻辑直接写进每个业务方法,业务代码会迅速被横切逻辑淹没。代理模式的核心价值,是让调用者仍然面向原接口编程,同时把通用能力放到代理对象中。
sequenceDiagram
participant C as 调用方
participant P as 代理对象
participant H as InvocationHandler
participant T as 目标对象
C->>P: createOrder(command)
P->>H: invoke(proxy, method, args)
H->>H: 日志、鉴权、计时、事务等
H->>T: method.invoke(target, args)
T-->>H: 返回结果或抛出异常
H->>H: 指标、异常转换、资源清理
H-->>P: 返回结果
P-->>C: OrderResult
代理有三类常见实现:
方式
生成时机
被代理对象要求
典型技术
静态代理
编译前手写
通常实现同一接口
装饰器、手写包装类
JDK 动态代理
运行时
必须基于接口
java.lang.reflect.Proxy
子类代理
运行时生成子类
类和方法不能被 final 阻断
CGLIB、Byte Buddy
静态代理并没有过时。代理类型少、行为稳定、追求可读性时,手写代理往往最直接。动态代理的优势在于:可以用同一套拦截逻辑批量增强大量接口或类 。
二十八、JDK 动态代理的完整实现 先定义接口和实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public interface PaymentService { PaymentResult pay (PaymentCommand command) ; default String providerName () { return "default-provider" ; } }public final class DefaultPaymentService implements PaymentService { @Override public PaymentResult pay (PaymentCommand command) { if (command.amount().signum() <= 0 ) { throw new IllegalArgumentException ("amount must be positive" ); } return new PaymentResult (command.orderId(), "SUCCESS" ); } }public record PaymentCommand (String orderId, BigDecimal amount) {}public record PaymentResult (String orderId, String status) {}
编写一个可复用的处理器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 import java.lang.reflect.InvocationHandler;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.util.Arrays;import java.util.Objects;public final class MetricsInvocationHandler implements InvocationHandler { private final Object target; public MetricsInvocationHandler (Object target) { this .target = Objects.requireNonNull(target); } @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { if (method.getDeclaringClass() == Object.class) { return invokeObjectMethod(proxy, method, args); } long start = System.nanoTime(); try { System.out.printf("invoke %s, args=%s%n" , method.getName(), Arrays.toString(args)); return method.invoke(target, args); } catch (InvocationTargetException ex) { throw ex.getTargetException(); } finally { long costNanos = System.nanoTime() - start; System.out.printf("method=%s, cost=%dns%n" , method.getName(), costNanos); } } private Object invokeObjectMethod (Object proxy, Method method, Object[] args) { return switch (method.getName()) { case "toString" -> "Proxy(" + target + ")" ; case "hashCode" -> System.identityHashCode(proxy); case "equals" -> proxy == args[0 ]; default -> throw new IllegalStateException ( "Unexpected Object method: " + method); }; } @SuppressWarnings("unchecked") public static <T> T create (Class<T> interfaceType, T target) { Objects.requireNonNull(interfaceType); Objects.requireNonNull(target); if (!interfaceType.isInterface()) { throw new IllegalArgumentException ("JDK proxy requires an interface" ); } if (!interfaceType.isInstance(target)) { throw new IllegalArgumentException ("target does not implement interface" ); } return (T) Proxy.newProxyInstance( interfaceType.getClassLoader(), new Class <?>[]{interfaceType}, new MetricsInvocationHandler (target) ); } }
调用:
1 2 3 4 5 6 PaymentService target = new DefaultPaymentService ();PaymentService proxy = MetricsInvocationHandler.create(PaymentService.class, target);PaymentResult result = proxy.pay( new PaymentCommand ("O-1001" , new BigDecimal ("99.90" )) );
这里发生了三件事:
JVM 在运行时生成一个实现 PaymentService 的代理类;
代理对象接收到接口方法调用后,将调用编码为 Method + Object[];
调用统一转发给 InvocationHandler#invoke。
因此,调用方看到的是 PaymentService,处理器看到的是通用的方法元数据。
二十九、动态代理中最容易踩的坑 29.1 InvocationTargetException 不应直接泄漏 Method#invoke 抛出的目标异常会被包装成 InvocationTargetException。如果不拆包,调用者会收到反射基础设施异常,而不是原始业务异常,异常体系和事务回滚判断都可能受到影响。
推荐:
1 2 3 4 5 try { return method.invoke(target, args); } catch (InvocationTargetException ex) { throw ex.getTargetException(); }
29.2 equals、hashCode、toString 仍会进入处理器 不要在 toString 分支中再次调用 proxy.toString(),否则会无限递归。代理对象究竟采用“身份相等”还是“委托目标对象相等”,必须明确选择,不能碰运气。
29.3 接口声明的受检异常限制返回路径 如果处理器抛出接口方法没有声明的受检异常,调用者可能收到 UndeclaredThrowableException。通用代理层应该建立异常转换规则:
运行时业务异常原样抛出;
受检异常转换为项目统一异常;
基础设施异常保留 cause;
不要用一个 RuntimeException("failed") 吞掉上下文。
29.4 类加载器选错会出现“明明同名却无法转换” 同一个二进制类名由不同 ClassLoader 加载后,在 JVM 看来是不同类型。插件系统、应用服务器和容器环境尤其容易出现这一问题。通常应优先选择:
接口自身的类加载器;
框架明确传入的类加载器;
插件场景下经过设计的父子加载器;
谨慎使用线程上下文类加载器。
29.5 自调用绕过代理 Spring AOP 中常见:
1 2 3 4 5 6 7 8 9 10 11 12 @Service public class OrderService { public void create () { this .save(); } @Transactional public void save () { } }
代理通常拦截的是“外部对象调用代理对象”的边界。this.save() 是目标对象内部调用,不会重新经过代理,因此事务、缓存、重试等增强可能失效。
解决思路不是机械地“获取当前代理”,而是优先重新划分服务边界:把需要独立增强的方法放入另一个 Bean。
三十、接口默认方法如何处理 现代 JDK 的 InvocationHandler 提供了调用接口默认方法的能力。一个通用处理器可以根据策略决定:默认方法由接口实现,还是转发给目标对象。
1 2 3 4 5 6 7 @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { if (method.isDefault()) { return InvocationHandler.invokeDefault(proxy, method, args); } return method.invoke(target, args); }
但要注意:目标类可能也覆写了默认方法。选择接口默认实现还是目标实现属于框架语义,不能随意切换。
三十一、JDK 动态代理、CGLIB 与 Byte Buddy 如何选择
维度
JDK 动态代理
CGLIB
Byte Buddy
代理基础
接口
继承目标类
生成/改写字节码
是否依赖接口
是
否
否,可按多种方式工作
final 类/方法
接口调用不受此限制
无法覆写 final
取决于增强方式
复杂度
低
中
中到高
典型场景
RPC Stub、接口型 AOP
Spring 类代理
Java Agent、Mock、框架字节码增强
工程建议:
公共扩展边界优先设计接口;
仅需要方法拦截时,JDK 代理足够清晰;
无接口的遗留类才考虑子类代理;
需要 Java Agent、类重定义、复杂字节码生成时再使用 Byte Buddy;
不要把“代理方式”暴露为业务层应该感知的概念。
第四部分:Java SPI 与插件化扩展 三十二、API 与 SPI 的区别 API 和 SPI 都以接口形式出现,但依赖方向不同:
API :平台提供能力,业务代码调用平台;
SPI :平台定义扩展契约,第三方实现契约,平台反向发现实现。
flowchart LR
subgraph API调用
A[业务代码] --> B[平台 API]
end
subgraph SPI扩展
C[平台核心] --> D[SPI 接口]
E[实现 A] -. implements .-> D
F[实现 B] -. implements .-> D
C --> G[ServiceLoader]
G --> E
G --> F
end
SPI 解决的是:主程序编译时不依赖具体实现,但运行时可以发现和装配实现。
典型例子包括:
JDBC 驱动;
日志实现发现;
序列化器扩展;
RPC 协议或负载均衡扩展;
文件解析器;
规则执行器;
支付渠道插件;
数据库方言。
三十三、使用 ServiceLoader 实现一个完整 SPI 33.1 定义服务接口 1 2 3 4 5 6 7 8 package com.example.notification.api;public interface NotificationProvider { String channel () ; void send (String receiver, String content) ; }
接口应该稳定、最小、面向能力,而不是泄漏某个实现的内部对象。
33.2 编写实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.example.notification.email;import com.example.notification.api.NotificationProvider;public final class EmailNotificationProvider implements NotificationProvider { public EmailNotificationProvider () { } @Override public String channel () { return "email" ; } @Override public void send (String receiver, String content) { System.out.printf("send email to %s: %s%n" , receiver, content); } }
33.3 声明提供者配置文件 在实现模块中创建:
1 2 3 4 src/main/resources/ └── META-INF/ └── services/ └── com.example.notification.api.NotificationProvider
文件内容是实现类的二进制名称,一行一个:
1 com.example.notification.email.EmailNotificationProvider
最常见错误包括:
文件名不是接口的全限定名;
内容写成简单类名;
Maven 打包时资源文件未进入 JAR;
多模块合并 fat JAR 时覆盖了同名 SPI 文件;
实现类不可访问或无法实例化。
33.4 加载实现 1 2 3 4 5 6 ServiceLoader<NotificationProvider> loader = ServiceLoader.load(NotificationProvider.class);for (NotificationProvider provider : loader) { System.out.println("found: " + provider.channel()); }
建立索引:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public final class NotificationRegistry { private final Map<String, NotificationProvider> providers; public NotificationRegistry (ClassLoader classLoader) { Map<String, NotificationProvider> map = new LinkedHashMap <>(); ServiceLoader.load(NotificationProvider.class, classLoader) .forEach(provider -> { NotificationProvider old = map.putIfAbsent( provider.channel(), provider); if (old != null ) { throw new IllegalStateException ( "duplicate channel: " + provider.channel()); } }); this .providers = Map.copyOf(map); } public NotificationProvider required (String channel) { NotificationProvider provider = providers.get(channel); if (provider == null ) { throw new IllegalArgumentException ( "unsupported channel: " + channel); } return provider; } }
注意这里显式校验了重复扩展点。如果直接 findFirst(),最终使用哪个实现可能取决于类路径顺序,故障会像薛定谔的猫一样偶尔出现。
三十四、JPMS 模块系统中的 SPI Java 模块系统下,不必仅依赖 META-INF/services,可以在 module-info.java 中声明:
服务 API 模块:
1 2 3 module com.example.notification.api { exports com.example.notification.api; }
提供者模块:
1 2 3 4 5 6 module com.example.notification.email { requires com.example.notification.api; provides com.example.notification.api.NotificationProvider with com.example.notification.email.EmailNotificationProvider; }
消费者模块:
1 2 3 4 5 module com.example.notification.app { requires com.example.notification.api; uses com.example.notification.api.NotificationProvider; }
调用代码不变:
1 2 ServiceLoader<NotificationProvider> loader = ServiceLoader.load(NotificationProvider.class);
模块描述符让“谁使用服务、谁提供服务”成为显式依赖,适合边界清晰的模块化应用。
三十五、ServiceLoader 的运行特征 35.1 惰性加载 ServiceLoader 通常在迭代时发现并实例化提供者,而不是 load 时一次性完成。因此异常可能推迟到迭代阶段出现。
可以使用 provider 流先检查类型元数据:
1 2 3 4 5 List<Class<?>> providerTypes = ServiceLoader .load(NotificationProvider.class) .stream() .map(ServiceLoader.Provider::type) .toList();
在真正需要时再调用:
1 NotificationProvider provider = providerDescriptor.get();
35.2 内部缓存 加载器会缓存已经加载的提供者。类路径发生变化后,可调用:
但 reload() 不等于完整的热插拔。若旧实例仍被业务对象、线程或缓存引用,旧类加载器仍无法卸载。
35.3 线程安全 不要把同一个 ServiceLoader 实例当作天然线程安全的共享注册中心。更稳妥的方式是:
启动阶段单线程完成发现;
做冲突校验和健康检查;
复制为不可变 Map;
运行期只读取注册表。
35.4 顺序不是业务优先级协议 配置文件中的顺序、类路径顺序和模块解析顺序都不应该成为隐含优先级。需要优先级时,应显式定义:
1 2 3 public interface OrderedProvider { int order () ; }
然后排序,并对相同业务键冲突进行明确处理。
35.5 ServiceConfigurationError 提供者配置错误、类无法加载、类型不兼容、构造失败时,可能抛出 ServiceConfigurationError。它继承自 Error,不能只用 catch (Exception) 假装兜底。
框架启动阶段可以捕获并转换为带上下文的启动失败,但不要在运行期静默忽略:扩展点没装上却继续启动,通常比直接失败更危险。
三十六、SPI、Spring Bean 与插件框架的边界
方案
优点
局限
适合场景
ServiceLoader
JDK 原生、依赖少、标准化
条件装配、生命周期管理较弱
SDK、基础库、驱动、轻量插件
Spring Bean
DI、配置、条件装配、生命周期完整
依赖 Spring 容器
单体或微服务内部策略扩展
Spring Boot 自动配置
开箱即用、条件化、starter 生态
强框架绑定
企业组件、Starter
PF4J 等插件框架
独立类加载器、插件生命周期
复杂度更高
需要安装/卸载插件的平台
自研扩展框架
可定制路由、隔离和治理
容易重复造轮子
需求明显超出标准方案时
SPI 是发现机制,不等于完整插件平台。一个成熟插件平台还要处理:
插件版本与兼容性;
依赖隔离;
类加载器泄漏;
安装、启停和卸载;
配置与密钥;
资源配额;
健康检查;
安全审核;
灰度和回滚。
第五部分:Java 枚举与状态建模 三十七、枚举不是一组整数常量 早期代码常用:
1 2 3 public static final int CREATED = 0 ;public static final int PAID = 1 ;public static final int CANCELLED = 2 ;
这种写法的问题是:
任意整数都能传入;
常量缺少类型边界;
行为只能散落在 if/else 中;
调试和日志可读性差;
重构时无法得到完整编译期保护。
枚举本质上是一种特殊类:
每个枚举常量都是该枚举类型的唯一实例;
可以有字段、构造器、方法和接口实现;
隐式继承 java.lang.Enum;
不能再继承其他类;
枚举构造器不能由业务代码调用;
JVM 和序列化机制对枚举实例身份有特殊保障。
三十八、给枚举增加稳定业务编码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public enum OrderStatus { CREATED("CREATED" , "待支付" ), PAID("PAID" , "已支付" ), SHIPPED("SHIPPED" , "已发货" ), CANCELLED("CANCELLED" , "已取消" ); private final String code; private final String description; OrderStatus(String code, String description) { this .code = code; this .description = description; } public String code () { return code; } public String description () { return description; } public static OrderStatus fromCode (String code) { return Arrays.stream(values()) .filter(status -> status.code.equals(code)) .findFirst() .orElseThrow(() -> new IllegalArgumentException ( "unknown order status: " + code)); } }
枚举常量名称、数据库编码和展示文案是三个不同概念:
常量名称服务于 Java 源码;
稳定编码服务于接口和持久化;
展示文案服务于国际化和 UI。
不要用 ordinal() 作为数据库值。插入、删除或调整枚举常量顺序后,序号会变化,历史数据就会“集体失忆”。
三十九、用枚举封装策略 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public enum DiscountType { NONE { @Override public BigDecimal apply (BigDecimal amount) { return amount; } }, PERCENTAGE { @Override public BigDecimal apply (BigDecimal amount) { return amount.multiply(new BigDecimal ("0.90" )); } }, FIXED { @Override public BigDecimal apply (BigDecimal amount) { return amount.subtract(new BigDecimal ("20" )) .max(BigDecimal.ZERO); } }; public abstract BigDecimal apply (BigDecimal amount) ; }
优点是行为与类型聚合,调用方无需再写分支:
1 BigDecimal payable = discountType.apply(originalAmount);
但枚举策略适合集合稳定、实现简单 的策略。如果支付渠道、规则插件或算法会频繁增加并由不同模块提供,用 SPI 或 Spring 策略注册表更合适。否则每新增一种策略都要修改核心枚举,违反开放封闭原则。
四十、用枚举实现状态机 状态不是一个孤立值,还包含“允许从哪里来、可以到哪里去”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public enum OrderStatus { CREATED, PAID, SHIPPED, CANCELLED; private static final Map<OrderStatus, Set<OrderStatus>> TRANSITIONS = Map.of( CREATED, EnumSet.of(PAID, CANCELLED), PAID, EnumSet.of(SHIPPED, CANCELLED), SHIPPED, EnumSet.noneOf(OrderStatus.class), CANCELLED, EnumSet.noneOf(OrderStatus.class) ); public boolean canTransitTo (OrderStatus target) { return TRANSITIONS.getOrDefault( this , EnumSet.noneOf(OrderStatus.class) ).contains(target); } public void checkTransitTo (OrderStatus target) { if (!canTransitTo(target)) { throw new IllegalStateException ( "illegal transition: " + this + " -> " + target); } } }
状态流转服务中使用:
1 2 3 4 public void changeStatus (Order order, OrderStatus target) { order.status().checkTransitTo(target); repository.updateStatus(order.id(), order.status(), target); }
数据库更新仍应带旧状态条件,避免并发覆盖:
1 2 3 4 UPDATE ordersSET status = :targetWHERE id = :id AND status = :expected;
内存校验负责清晰错误,数据库条件负责并发正确性,两者不能互相替代。
四十一、EnumSet 与 EnumMap 41.1 EnumSet 当集合元素是枚举时,优先考虑 EnumSet:
1 2 EnumSet<OrderStatus> terminalStatuses = EnumSet.of(OrderStatus.SHIPPED, OrderStatus.CANCELLED);
它通常基于位向量实现,比通用 HashSet 更紧凑,也更准确地表达“枚举值集合”。
常用方法:
1 2 3 4 EnumSet.allOf(OrderStatus.class); EnumSet.noneOf(OrderStatus.class); EnumSet.complementOf(terminalStatuses); EnumSet.range(OrderStatus.CREATED, OrderStatus.SHIPPED);
41.2 EnumMap 枚举作为键时:
1 2 3 EnumMap<OrderStatus, String> labels = new EnumMap <>(OrderStatus.class); labels.put(OrderStatus.CREATED, "等待用户支付" ); labels.put(OrderStatus.PAID, "等待仓库发货" );
EnumMap 按枚举声明顺序管理键,比 HashMap<Enum, ...> 更适合状态映射和处理器映射。
四十二、枚举持久化与序列化建议 数据库 优先存储稳定字符串编码:
1 CREATED / PAID / SHIPPED / CANCELLED
如果必须使用数值编码,也要显式定义字段:
不要依赖 ordinal()。
JSON 明确输入输出协议。以 Jackson 为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public enum OrderStatus { CREATED("CREATED" ), PAID("PAID" ); private final String code; OrderStatus(String code) { this .code = code; } @JsonValue public String code () { return code; } @JsonCreator public static OrderStatus fromCode (String code) { return Arrays.stream(values()) .filter(value -> value.code.equalsIgnoreCase(code)) .findFirst() .orElseThrow(); } }
还要确定未知值策略:
直接拒绝,适合强一致内部协议;
映射为 UNKNOWN,适合前后端版本可能错位的协议;
保留原始字符串,适合开放扩展协议。
四十三、枚举单例是否可靠 1 2 3 4 5 6 public enum GlobalRegistry { INSTANCE; private final ConcurrentMap<String, Object> values = new ConcurrentHashMap <>(); }
枚举单例能抵抗普通反射重复构造,并天然处理序列化身份问题。但这并不意味着它适合所有单例:
难以替换和测试;
生命周期固定为类加载器生命周期;
不便于依赖注入;
插件类加载器下可能存在多份“单例”;
全局可变状态仍然是全局可变状态,披上枚举外套也不会突然高尚。
业务系统通常优先让 IoC 容器管理实例;枚举单例适合无外部依赖、生命周期简单的基础设施对象。
第六部分:从 if/else 到可维护的规则设计 四十四、不是所有 if/else 都需要优化 以下代码没有问题:
1 2 3 if (user == null ) { throw new IllegalArgumentException ("user must not be null" ); }
问题不在语法,而在分支承担了过多变化:
1 2 3 4 5 6 7 8 9 if (type == 1 ) { } else if (type == 2 ) { } else if (type == 3 ) { } else if (...) { }
优化前先判断条件属于哪一类:
条件类型
例子
更合适的手段
前置校验
参数为空、权限不足
卫语句
有限稳定分类
枚举状态映射
switch 表达式
类型分派
不同消息类型
模式匹配 switch / 多态
算法可替换
不同计费策略
策略模式
多处理器依次尝试
风控链、解析链
责任链
条件组合
搜索条件、资格判断
Predicate / Specification
规则频繁变化
营销、风控、路由
表达式/规则引擎
四十五、第一步通常是卫语句 嵌套代码:
1 2 3 4 5 6 7 8 9 public void submit (Order order) { if (order != null ) { if (order.items() != null && !order.items().isEmpty()) { if (order.customerId() != null ) { doSubmit(order); } } } }
改为卫语句:
1 2 3 4 5 6 7 8 9 10 11 12 public void submit (Order order) { Objects.requireNonNull(order, "order" ); if (order.items() == null || order.items().isEmpty()) { throw new IllegalArgumentException ("order items must not be empty" ); } if (order.customerId() == null ) { throw new IllegalArgumentException ("customerId must not be null" ); } doSubmit(order); }
卫语句的收益:
异常路径提前退出;
主流程保持在较低缩进层级;
每个失败原因清晰;
测试用例容易与校验规则一一对应。
四十六、使用 switch 表达式表达有限映射 传统写法:
1 2 3 4 5 6 7 8 9 10 11 BigDecimal fee;switch (level) { case VIP: fee = BigDecimal.ZERO; break ; case NORMAL: fee = new BigDecimal ("10" ); break ; default : throw new IllegalArgumentException ("unsupported level" ); }
现代写法:
1 2 3 4 5 BigDecimal fee = switch (level) { case VIP -> BigDecimal.ZERO; case NORMAL -> new BigDecimal ("10" ); case BLACKLISTED -> throw new IllegalStateException ("service denied" ); };
switch 表达式比“声明变量 + 分支赋值”更明确:整个结构就是一个值计算过程,编译器也更容易检查穷尽性。
复杂分支可以使用 yield:
1 2 3 4 5 6 7 8 9 BigDecimal fee = switch (level) { case VIP -> BigDecimal.ZERO; case NORMAL -> { BigDecimal base = new BigDecimal ("10" ); BigDecimal adjusted = regionPolicy.adjust(base); yield adjusted; } case BLACKLISTED -> throw new IllegalStateException ("service denied" ); };
四十七、密封类型与模式匹配 switch 当分支实际上是在判断不同类型时,不要堆叠 instanceof:
1 2 3 4 sealed interface Message permits TextMessage, ImageMessage, FileMessage {}record TextMessage (String text) implements Message {}record ImageMessage (byte [] data) implements Message {}record FileMessage (Path path) implements Message {}
分派:
1 2 3 4 5 6 7 8 9 10 11 12 13 int payloadSize (Message message) { return switch (message) { case TextMessage text -> text.text().getBytes(StandardCharsets.UTF_8).length; case ImageMessage image -> image.data().length; case FileMessage file -> { try { yield Math.toIntExact(Files.size(file.path())); } catch (IOException ex) { throw new UncheckedIOException (ex); } } }; }
密封层次明确列出了允许的实现,编译器可以检查分支是否完整。适合:
AST 节点;
领域命令或事件;
协议消息;
结果类型;
有限且由核心模块控制的类型集合。
如果实现集合开放给第三方插件,不应使用封闭的密封层次替代 SPI。
四十八、Map 分派适合简单的一一映射 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private static final Map<Operator, BinaryOperator<BigDecimal>> OPERATIONS = Map.of( Operator.ADD, BigDecimal::add, Operator.SUBTRACT, BigDecimal::subtract, Operator.MULTIPLY, BigDecimal::multiply, Operator.DIVIDE, (left, right) -> left.divide(right, 2 , RoundingMode.HALF_UP) );public BigDecimal calculate ( Operator operator, BigDecimal left, BigDecimal right) { BinaryOperator<BigDecimal> operation = OPERATIONS.get(operator); if (operation == null ) { throw new IllegalArgumentException ("unsupported operator: " + operator); } return operation.apply(left, right); }
它适合无状态、逻辑短小的分派。若每个分支需要多项依赖、复杂校验、事务或生命周期,就不要把大型 Lambda 硬塞进 Map,应升级为策略对象。
四十九、策略模式:让变化成为独立对象 1 2 3 4 5 6 public interface PaymentStrategy { PaymentChannel channel () ; PaymentResult pay (PaymentCommand command) ; }
实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Component public final class CardPaymentStrategy implements PaymentStrategy { private final CardGateway cardGateway; public CardPaymentStrategy (CardGateway cardGateway) { this .cardGateway = cardGateway; } @Override public PaymentChannel channel () { return PaymentChannel.CARD; } @Override public PaymentResult pay (PaymentCommand command) { return cardGateway.charge(command); } }
注册表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Component public final class PaymentStrategyRegistry { private final Map<PaymentChannel, PaymentStrategy> strategies; public PaymentStrategyRegistry (List<PaymentStrategy> strategies) { EnumMap<PaymentChannel, PaymentStrategy> map = new EnumMap <>(PaymentChannel.class); for (PaymentStrategy strategy : strategies) { PaymentStrategy old = map.putIfAbsent( strategy.channel(), strategy); if (old != null ) { throw new IllegalStateException ( "duplicate payment strategy: " + strategy.channel()); } } this .strategies = Collections.unmodifiableMap(map); } public PaymentStrategy required (PaymentChannel channel) { return Optional.ofNullable(strategies.get(channel)) .orElseThrow(() -> new IllegalArgumentException ( "unsupported payment channel: " + channel)); } }
调用方:
1 return registry.required(command.channel()).pay(command);
这并不是“消灭分支”,而是把一个不断增长的中心分支改造成可独立开发、测试和装配的策略集合。
五十、责任链:让多个处理器按顺序协作 1 2 3 4 5 6 7 8 9 10 11 12 13 public interface RiskRule { RiskDecision evaluate (RiskContext context) ; }public record RiskDecision (boolean passed, String reason) { public static RiskDecision pass () { return new RiskDecision (true , "PASS" ); } public static RiskDecision reject (String reason) { return new RiskDecision (false , reason); } }
执行器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public final class RiskRuleChain { private final List<RiskRule> rules; public RiskRuleChain (List<RiskRule> rules) { this .rules = List.copyOf(rules); } public RiskDecision evaluate (RiskContext context) { for (RiskRule rule : rules) { RiskDecision decision = rule.evaluate(context); if (!decision.passed()) { return decision; } } return RiskDecision.pass(); } }
责任链适合:
任一规则失败即终止;
处理器可以按顺序追加;
每个处理器只负责一个判断;
需要记录每一步结果。
不要把所有规则异常都当作“不通过”。系统故障、规则配置错误和正常拒绝必须有不同语义。
五十一、Predicate 与 Specification 组合条件 简单条件可组合成 Predicate:
1 2 3 4 5 Predicate<User> adult = user -> user.age() >= 18 ; Predicate<User> active = User::active; Predicate<User> eligible = adult.and(active);boolean allowed = eligible.test(user);
领域中可以定义更可读的 Specification:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @FunctionalInterface public interface Specification <T> { boolean isSatisfiedBy (T candidate) ; default Specification<T> and (Specification<T> other) { return candidate -> this .isSatisfiedBy(candidate) && other.isSatisfiedBy(candidate); } default Specification<T> or (Specification<T> other) { return candidate -> this .isSatisfiedBy(candidate) || other.isSatisfiedBy(candidate); } default Specification<T> not () { return candidate -> !this .isSatisfiedBy(candidate); } }
当需要动态查询时,Specification 还可以进一步转化为 SQL 条件或 JPA Criteria,但“内存布尔判断”和“数据库查询表达式”不应混成一个难以测试的万能接口。
五十二、什么时候应该引入表达式或规则引擎 满足以下多个条件时才值得考虑:
规则经常变化,发布应用的成本过高;
规则由运营、风控、财务或产品人员维护;
同一份规则需要在多个流程中复用;
需要灰度、生效时间、版本、审计和回滚;
条件组合较多,但核心动作仍可受控;
业务希望先试运行或影子验证。
反过来,只有三五个稳定分支时,引入规则引擎会把 20 行代码升级成一套平台。架构不是俄罗斯套娃,层数多不代表成熟。
第七部分:在 Java 源码中规范添加许可信息 五十三、先区分版权声明、许可证与 NOTICE 很多项目把三类信息混在一个头部注释里:
版权声明(Copyright Notice) 回答“谁对这份作品主张版权”:
1 Copyright 2024-2026 Example Corporation
版权归属和年份属于法律事实,不应该让格式化工具凭空猜测。代码被员工、外包、合作方或开源社区共同贡献时,归属需要遵循组织政策和贡献协议。
许可证(License) 回答“别人可以在什么条件下使用、修改和分发代码”。常见形式:
Apache License 2.0;
MIT License;
BSD-2-Clause / BSD-3-Clause;
GPL / LGPL / AGPL;
MPL;
企业内部专有许可。
NOTICE 用于保留某些许可证、上游项目或组织要求的归属说明。NOTICE 不是所有许可证都要求,也不能替代 LICENSE。
一个开源仓库通常至少要考虑:
1 2 3 4 5 LICENSE NOTICE # 仅在适用时 README.md THIRD-PARTY-NOTICES # 依赖许可归属汇总,按组织要求设置 源码文件头部标识
本节是工程治理说明,不构成法律意见。许可证选择、版权归属和第三方依赖合规,应由组织法务或开源办公室确认。
五十四、完整许可证头与 SPDX 标识 54.1 完整头部 Apache-2.0 风格的 Java 源文件通常会在 package 之前放置许可说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.example;
注意:不要把 Apache Software Foundation 的专用声明原封不动复制到非 ASF 项目中。你的项目应该使用与自身版权主体和发布方式相匹配的模板。
54.2 SPDX 短标识 更紧凑、机器可读的方式:
同时保留版权声明:
复杂许可关系可以使用 SPDX 表达式:
其中:
OR 表示使用者可任选一种许可证;
AND 表示必须同时满足多种许可证;
WITH 用于许可证例外;
括号用于明确组合优先级。
不能因为 SPDX 行很短,就随意改动已有文件的版权声明。许可标识描述许可证,版权声明描述权利主体,两者不是同一件事。
五十五、许可头应该放在哪里 Java 文件通常放在 package 语句之前:
1 2 package com.example.order;
特殊文件要考虑固定首行:
Shell 的 shebang:#!/usr/bin/env bash;
XML 声明:<?xml version="1.0"?>;
YAML 文档标记;
生成器要求的特殊注释;
编译器或工具识别的文件头。
这类文件的许可头可能需要放在固定首行之后。自动化工具要配置跳过规则,不能粗暴地在第 1 个字符前插入注释。
五十六、使用 Spotless 自动添加和校验许可头 在项目根目录创建 config/license-header.txt:
1 2 3 4 /* * Copyright $YEAR Example Corporation * SPDX-License-Identifier: Apache-2.0 */
Maven 配置示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 <properties > <spotless-maven-plugin.version > 3.6.0</spotless-maven-plugin.version > </properties > <build > <plugins > <plugin > <groupId > com.diffplug.spotless</groupId > <artifactId > spotless-maven-plugin</artifactId > <version > ${spotless-maven-plugin.version}</version > <configuration > <java > <target > <include > src/main/java/**/*.java</include > <include > src/test/java/**/*.java</include > </target > <licenseHeader > <file > ${project.basedir}/config/license-header.txt</file > </licenseHeader > <removeUnusedImports /> <trimTrailingWhitespace /> <endWithNewline /> </java > <formats > <format > <includes > <include > *.xml</include > <include > *.yml</include > <include > *.yaml</include > <include > *.md</include > </includes > <trimTrailingWhitespace /> <endWithNewline /> </format > </formats > </configuration > <executions > <execution > <id > spotless-check</id > <phase > verify</phase > <goals > <goal > check</goal > </goals > </execution > </executions > </plugin > </plugins > </build >
截至 2026 年 7 月,Spotless Maven Plugin 最新版本为 3.6.0,运行 Maven 时需要 JRE 17 或更高版本。若项目仍使用旧 JRE,应根据官方兼容说明选择旧版插件,而不是只修改项目源码级别。
本地执行:
1 2 3 4 5 mvn spotless:check mvn spotless:apply
年份策略 $YEAR 可以由工具填充,但企业项目要先明确规则:
新文件使用创建年份;
老文件是否使用 2022-2026;
是否根据 Git 历史补齐;
每年是否自动更新结束年份;
迁移文件是否保留原作者声明。
“把所有年份统一改成今年”看似整齐,实际上可能改坏历史事实。
多模块项目 头部模板可放在父工程:
1 <file > ${maven.multiModuleProjectDirectory}/config/license-header.txt</file >
但要验证 Maven 版本和执行环境是否支持对应属性。发布到外部的组件、内部闭源模块和生成代码可能使用不同规则,不能一套 include 覆盖宇宙。
五十七、使用 Apache RAT 做合规扫描 Spotless 更擅长“格式化和注入”,Apache RAT 更擅长“扫描哪些文件缺少认可的许可信息”。
Maven 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <plugin > <groupId > org.apache.rat</groupId > <artifactId > apache-rat-plugin</artifactId > <version > 0.18</version > <configuration > <excludes > <exclude > **/target/**</exclude > <exclude > **/.idea/**</exclude > <exclude > **/*.iml</exclude > <exclude > **/node_modules/**</exclude > <exclude > **/generated-sources/**</exclude > <exclude > **/*.json</exclude > <exclude > **/*.lock</exclude > </excludes > </configuration > <executions > <execution > <id > license-audit</id > <phase > verify</phase > <goals > <goal > check</goal > </goals > </execution > </executions > </plugin >
截至 2026 年 7 月,Apache RAT 当前版本为 0.18。该版本运行要求已提升到 JDK 17,官方建议使用 JDK 21 或更高版本。
执行:
排除项必须有理由:
二进制文件无法放文本头;
生成文件由模板决定;
锁文件修改会破坏工具行为;
第三方原样引入文件不能擅自改头;
测试快照可能要求字节级一致。
不要为了让 CI 变绿,最后写出一个 **/* 排除规则。那不是合规,是让扫描器闭嘴。
五十八、推荐的 CI 流程 flowchart LR
A[开发者提交] --> B[spotless:check]
B -->|通过| C[apache-rat:check]
B -->|失败| X[提示执行 spotless:apply]
C -->|通过| D[依赖许可证扫描]
C -->|失败| Y[检查缺失头部或排除原因]
D --> E[构建与测试]
E --> F[生成 SBOM / 发布制品]
推荐把以下检查分开:
源码头部检查 :Spotless / RAT;
依赖许可证检查 :检查第三方组件许可;
SBOM :生成组件清单;
制品内容检查 :确认 LICENSE、NOTICE、第三方声明进入发布包;
例外审批 :对特殊许可证、来源不明文件和复制代码留存审核记录。
五十九、许可治理常见错误
只添加头部,不在仓库根目录放完整许可证;
使用与项目实际许可证不一致的模板;
删除上游文件原有版权声明;
把第三方源码改成自己公司的版权;
使用 ordinal 一样的“魔法年份生成器”修改全部历史;
将生成目录、依赖目录全部纳入自动改写;
只扫描 Java,忽略脚本、配置、前端和文档;
认为开源许可证等于“没有限制”;
发布二进制时忘记携带许可证和 NOTICE;
在公司闭源项目中随意粘贴强 copyleft 代码。
第八部分:Java 表达式引擎与规则动态化 六十、表达式引擎究竟在做什么 表达式引擎接收一段文本,将其中的变量、运算符、属性访问和函数调用解析为可执行结构,再在给定上下文中求值。
flowchart LR
A[规则文本] --> B[词法分析]
B --> C[语法分析]
C --> D[AST / 中间表示]
D --> E[语义检查]
E --> F[编译或解释计划]
F --> G[缓存]
H[变量上下文] --> I[执行器]
G --> I
J[允许的函数/对象] --> I
I --> K[结果]
I --> L[轨迹、耗时、异常]
例如:
1 user.level == 'VIP' && order.amount >= 100 && !user.blacklisted
传统代码把规则固定在 Java 中:
1 2 3 boolean matched = user.level() == UserLevel.VIP && order.amount().compareTo(new BigDecimal ("100" )) >= 0 && !user.blacklisted();
表达式引擎把变化部分变成数据,但它不会自动提供完整的规则治理。生产系统仍需补齐:
规则定义与变量契约;
语法检查;
安全白名单;
超时和资源限制;
编译缓存;
版本管理;
审核发布;
灰度和回滚;
结果解释;
指标与审计。
六十一、表达式、脚本、规则引擎的边界
类型
特征
示例用途
表达式
通常返回单值,控制结构有限
条件、金额、路由键
脚本
多语句、变量、函数、流程控制
数据转换、复杂计算
规则引擎
多条规则、冲突解决、事实匹配
风控、资格判断、规则编排
决策表/DMN
表格或模型化决策
业务人员可维护的决策矩阵
不要因为一个库能执行多行脚本,就默认允许业务用户编写任意流程。表达能力越强,安全面、测试成本和治理成本越大。
六十二、选型时必须比较的维度
语法成本 :Java 风格、EL 风格还是自定义 DSL;
执行模型 :解释、字节码编译或混合;
性能 :解析成本、执行成本、缓存能力;
类型语义 :数字精度、空值、类型转换;
安全边界 :类、方法、构造器、反射和 IO 是否可访问;
超时能力 :能否中断长循环或高成本表达式;
可观测性 :错误位置、执行轨迹、变量依赖;
扩展能力 :自定义函数、操作符、属性访问器;
并发模型 :引擎、表达式对象、上下文是否线程安全;
生态整合 :是否与 Spring、规则平台或现有技术栈匹配;
版本与维护 :发布活跃度、JDK 兼容性、迁移成本;
授权许可 :是否符合企业使用和分发策略。
六十三、SpEL:Spring 生态中的表达式语言 SpEL 常见于:
@Value;
Spring Security 表达式;
@Cacheable 的 key、condition、unless;
Spring Integration;
Spring Batch;
自定义注解驱动能力。
基础示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ExpressionParser parser = new SpelExpressionParser ();Expression expression = parser.parseExpression( "amount >= 100 and level == 'VIP'" );SimpleEvaluationContext context = SimpleEvaluationContext .forReadOnlyDataBinding() .build();record PricingFacts (BigDecimal amount, String level) {}PricingFacts root = new PricingFacts ( new BigDecimal ("128.00" ), "VIP" );Boolean matched = expression.getValue(context, root, Boolean.class);
SimpleEvaluationContext 有意限制了部分 SpEL 能力,例如类型引用、构造器和 Bean 引用,更适合数据绑定和受限表达式。StandardEvaluationContext 能力更完整,也意味着暴露面更大。
使用变量而不是暴露整个容器 1 2 3 4 5 6 7 StandardEvaluationContext context = new StandardEvaluationContext (); context.setVariable("amount" , new BigDecimal ("128.00" )); context.setVariable("vip" , true );Boolean result = parser .parseExpression("#vip && #amount >= 100" ) .getValue(context, Boolean.class);
不建议把完整 ApplicationContext、Repository、HTTP 客户端或系统工具类直接暴露给可配置表达式。
缓存解析结果 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public final class SpelRuleEngine { private final ExpressionParser parser = new SpelExpressionParser (); private final ConcurrentMap<String, Expression> cache = new ConcurrentHashMap <>(); public boolean evaluate (String ruleId, long version, String source, Object root) { String cacheKey = ruleId + ":" + version; Expression expression = cache.computeIfAbsent( cacheKey, ignored -> parser.parseExpression(source)); SimpleEvaluationContext context = SimpleEvaluationContext .forReadOnlyDataBinding() .build(); return Boolean.TRUE.equals( expression.getValue(context, root, Boolean.class) ); } }
缓存键必须含规则版本,不能只用 ruleId。否则发布新规则后,节点可能继续执行旧 AST。
六十四、AviatorScript:偏高性能的 JVM 脚本与表达式方案 Maven 依赖:
1 2 3 4 5 <dependency > <groupId > com.googlecode.aviator</groupId > <artifactId > aviator</artifactId > <version > ${aviator.version}</version > </dependency >
基础执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 AviatorEvaluatorInstance evaluator = AviatorEvaluator.newInstance(); evaluator.enableSandboxMode(); evaluator.setOption(Options.EVAL_TIMEOUT_MS, 100L );String source = "vip && amount >= 100" ;Expression expression = evaluator.compile(source, true ); Map<String, Object> env = new HashMap <>(); env.put("vip" , true ); env.put("amount" , 128 );Object result = expression.execute(env);
AviatorScript 可以把脚本编译为 JVM 字节码执行,并提供表达式缓存、自定义函数、变量分析、执行超时等能力。
自定义函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public final class IsAdultFunction extends AbstractFunction { @Override public String getName () { return "isAdult" ; } @Override public AviatorObject call ( Map<String, Object> env, AviatorObject arg) { Number age = (Number) arg.getValue(env); return AviatorBoolean.valueOf(age.intValue() >= 18 ); } }
注册:
1 evaluator.addFunction(new IsAdultFunction ());
执行:
1 2 3 4 Object result = evaluator.execute( "isAdult(age)" , Map.of("age" , 20 ) );
自定义函数应该:
纯函数优先;
明确参数和返回类型;
不暴露数据库连接、文件系统或任意 HTTP;
设置执行耗时指标;
对异常做语义化转换;
有独立单元测试。
沙箱和超时不是万能保险 超时检测可能不是纳秒级准确中断;沙箱也只负责引擎已覆盖的访问路径。对不可信脚本,仍需考虑:
独立进程或容器隔离;
CPU、内存和线程配额;
输出大小限制;
禁止网络和文件系统;
函数白名单;
脚本长度和 AST 复杂度限制;
取消传播;
审计日志。
六十五、QLExpress4:面向业务规则和 DSL 的表达式引擎 截至 2026 年 7 月,QLExpress 官方主线文档介绍的是 QLExpress4。它基于 ANTLR4 重写解析引擎,强调默认安全、解释执行、JSON、函数式编程、表达式追踪、缓存和业务 DSL 扩展能力。
依赖示例:
1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > qlexpress4</artifactId > <version > 4.1.2</version > </dependency >
基础执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Express4Runner runner = new Express4Runner ( InitOptions.DEFAULT_OPTIONS ); Map<String, Object> context = new HashMap <>(); context.put("a" , 1 ); context.put("b" , 2 ); context.put("c" , 3 );QLResult result = runner.execute( "a + b * c" , context, QLOptions.DEFAULT_OPTIONS ); System.out.println(result.getResult());
自定义函数 1 2 3 4 5 6 7 8 9 10 11 12 runner.addVarArgsFunction( "join" , params -> Arrays.stream(params) .map(Object::toString) .collect(Collectors.joining("," )) );Object value = runner.execute( "join(1, 2, 3)" , Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS ).getResult();
语法检查 1 2 3 4 5 6 try { runner.check("a + b *" ); } catch (QLSyntaxException ex) { System.err.printf("line=%d, column=%d, message=%s%n" , ex.getLineNo(), ex.getColNo(), ex.getMessage()); }
规则平台应该在发布前做语法检查,而不是等第一笔真实订单触发规则后才发现少了一个括号。
提取外部变量 1 2 3 Set<String> variables = runner.getOutVarNames( "subtotal = price * quantity; subtotal - discount" );
这项能力可用于:
校验变量是否在契约中声明;
自动生成规则输入文档;
找出无效或拼写错误变量;
分析规则影响范围;
构建规则依赖图。
安全策略 QLExpress4 默认强调限制脚本与应用代码交互。只有明确需要时才开放 Java 能力,并且应该采用白名单而不是“先全开、以后再补安全”。
规则表达式通常只需要:
读取上下文变量;
数字、字符串和集合运算;
调用少量经过审核的领域函数;
返回布尔值、数字或结构化结果。
它通常不需要创建线程、读文件、发网络请求或加载任意类。
六十六、Apache Commons JEXL Maven 依赖:
1 2 3 4 5 <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-jexl3</artifactId > <version > 3.7.0</version > </dependency >
基础示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 JexlEngine jexl = new JexlBuilder () .strict(true ) .safe(false ) .cache(512 ) .permissions(JexlPermissions.SECURE) .create();JexlExpression expression = jexl.createExpression( "vip && amount >= threshold" );MapContext context = new MapContext (); context.set("vip" , true ); context.set("amount" , new BigDecimal ("128.00" )); context.set("threshold" , new BigDecimal ("100.00" ));Object result = expression.evaluate(context);
JEXL 3.7.0 将默认权限收紧为更安全的集合,并调整了若干默认语言特性。升级时不要只改版本号,需要运行规则兼容性测试。
权限白名单 1 2 3 4 5 6 7 8 9 10 JexlPermissions permissions = JexlPermissions.NONE.compose( "java.lang { +String{} }" , "java.math { +BigDecimal{} }" , "com.example.rules.api.*" );JexlEngine engine = new JexlBuilder () .permissions(permissions) .strict(true ) .create();
即使使用 SECURE 或自定义权限,也不能断言“不可信代码绝对安全”。安全需要引擎配置、输入限制、运行资源隔离和业务函数设计共同完成。
六十七、主流 Java 表达式方案对比
引擎
语法/定位
优势
注意点
典型场景
SpEL
Spring EL
Spring 集成极强、属性访问方便
完整上下文能力较强,需限制
注解条件、Spring 应用内部表达式
AviatorScript
JVM 脚本/表达式
字节码执行、函数丰富、支持超时与沙箱
需理解编译缓存和函数暴露
高频计算、规则表达式、脚本
QLExpress4
业务 DSL/脚本
默认安全、追踪、JSON、自定义语法
4.x 与旧版 API 有迁移成本
营销、流程、计费、规则平台
Commons JEXL
通用 EL/脚本
API 简洁、权限可配置、Apache 生态
版本升级需关注默认安全变化
嵌入式动态表达式、模板能力
MVEL
Java 风格表达式
历史应用广、语法灵活
新项目需评估维护和安全需求
遗留规则系统、Drools 相关生态
JavaScript 引擎
通用脚本
表达能力强、人才储备广
隔离和资源控制成本高
复杂脚本、跨语言规则
选型不能只看一张微基准图。真实总成本还包括:规则编译、缓存命中、上下文构造、函数 IO、日志、审计和故障恢复。
六十八、设计统一的表达式引擎门面 不要让业务代码直接依赖某个引擎的 Expression、Context 或异常类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public interface ExpressionEngine { String name () ; CompiledExpression compile ( RuleIdentity identity, String source, ExpressionOptions options ) ; }public interface CompiledExpression { Object evaluate (Map<String, Object> variables) ; Set<String> requiredVariables () ; }public record RuleIdentity ( String tenantId, String ruleCode, long version ) {}public record ExpressionOptions ( Duration timeout, Class<?> expectedType ) {}
泛型类型安全门面:
1 2 3 public interface TypedExpression <I, O> { O evaluate (I input) ; }
适配器负责:
将输入对象转换为受控变量;
调用具体引擎;
校验输出类型;
统一超时和异常;
记录版本、耗时和结果摘要;
屏蔽引擎升级差异。
六十九、表达式缓存如何设计 错误做法 1 cache.put(ruleCode, compiledExpression);
多租户、灰度和规则升级时会发生覆盖。
推荐缓存键 1 2 3 4 5 6 7 8 public record ExpressionCacheKey ( String engine, String tenantId, String ruleCode, long version, String sourceHash, String functionSetVersion ) {}
缓存值可以包含:
1 2 3 4 5 6 public record CompiledRule ( Object executable, Set<String> variables, Instant compiledAt, String sourceHash ) {}
还要考虑:
最大条目数;
空闲和写入过期;
单个租户配额;
编译失败是否缓存;
发布后主动预热;
回滚版本是否仍在缓存;
集群节点一致性;
引擎升级后的缓存失效。
七十、规则安全清单 输入层
限制表达式长度;
限制嵌套深度和集合字面量大小;
拒绝不可见控制字符;
校验变量名;
限制规则数量和发布频率。
语法层
禁止构造器、反射、类加载;
禁止线程、进程、文件和网络;
禁止无限循环或递归;
限制方法调用;
采用函数和操作符白名单。
运行层
执行超时;
CPU 和内存预算;
输出大小限制;
并发隔离;
熔断和降级;
取消传播;
慢规则监控。
数据层
只提供必要字段;
脱敏敏感信息;
不暴露实体对象的全部方法;
不把 Repository、DataSource、客户端对象放入上下文;
明确租户边界。
治理层
双人审核或审批流;
版本不可变;
生效时间;
灰度范围;
一键回滚;
操作审计;
历史输入回放;
影子执行对比。
七十一、规则发布的推荐流程 flowchart TD
A[编辑草稿] --> B[语法检查]
B --> C[变量契约检查]
C --> D[安全扫描]
D --> E[单元样例]
E --> F[历史数据回放]
F --> G[人工审核]
G --> H[生成不可变版本]
H --> I[预编译/预热]
I --> J[影子执行]
J --> K{差异可接受?}
K -->|否| L[阻断并修订]
K -->|是| M[灰度发布]
M --> N[全量生效]
N --> O[监控与审计]
O --> P[保留快速回滚版本]
数据库模型可包含:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 rule_definition - id - tenant_id - rule_code - engine - input_schema - output_type rule_version - rule_id - version - source - source_hash - status - created_by - reviewed_by - created_at - effective_from - effective_to rule_release - version_id - environment - gray_scope - released_at - rolled_back_from
“直接 UPDATE 当前规则文本”会破坏审计、回滚和历史复现。规则版本应不可变,新修改创建新版本。
七十二、表达式引擎不适合什么
需要强事务编排的核心流程;
大量数据库读写和远程调用;
复杂循环、递归和算法;
需要完整 IDE 重构能力的稳定逻辑;
对极端低延迟要求严格且规则几乎不变的热点;
无法建立安全边界的不可信脚本;
团队没有规则审计和回滚能力的场景。
把业务代码变成字符串,会失去一部分编译器、IDE 和静态分析保护。只有动态性带来的收益高于这些损失,表达式引擎才值得引入。
第九部分:Java 注解与元数据驱动设计 七十三、注解本身不会执行任何逻辑 注解是一种结构化元数据:
1 2 3 4 @Transactional public void createOrder () { }
@Transactional 不会自己打开事务。真正工作的是:
编译器、注解处理器或框架读取注解;
框架根据注解创建代理或生成代码;
调用发生时执行事务拦截器;
拦截器控制提交和回滚。
因此,一个注解机制至少包含两部分:
声明端 :注解类型及其属性;
解释端 :编译器插件、注解处理器、反射代码、代理或框架扩展。
只有注解,没有解释器,就像在门上贴了“自动门”三个字——门依然不会自动开。
七十四、定义注解 1 2 3 4 5 6 7 8 9 10 public @interface Audit { String action () ; AuditLevel level () default AuditLevel.INFO; boolean recordResult () default false ; String[] tags() default {}; }
注解元素可使用的类型受到限制,常见包括:
基本类型;
String;
Class;
枚举;
注解;
上述类型的一维数组。
不能把任意领域对象直接作为注解属性:
注解属性也不能是 null。需要表达“未配置”时,通常使用空字符串、特殊枚举或空数组,但应避免让默认值产生歧义。
七十五、元注解 75.1 @Target 限定注解可出现的位置:
1 @Target({ElementType.TYPE, ElementType.METHOD})
常见目标:
ElementType
位置
TYPE
类、接口、枚举、注解类型
FIELD
字段
METHOD
方法
PARAMETER
参数
CONSTRUCTOR
构造器
LOCAL_VARIABLE
局部变量
ANNOTATION_TYPE
注解类型
PACKAGE
包声明
MODULE
模块声明
TYPE_PARAMETER
类型参数声明
TYPE_USE
任何类型使用位置
RECORD_COMPONENT
Record 组件
TYPE_USE 支持更细粒度的类型注解:
1 List<@NonNull String> names;
它常用于静态空值分析、类型检查框架和代码质量工具。
75.2 @Retention 1 @Retention(RetentionPolicy.RUNTIME)
策略
是否进入 .class
运行时反射可见
适合场景
SOURCE
否
否
编译检查、代码生成提示
CLASS
是
否
字节码工具、默认策略
RUNTIME
是
是
运行时框架、反射、代理
如果处理器通过反射读取注解,却忘记声明 RUNTIME,运行时永远读不到。
75.3 @Documented 让注解出现在生成的 Javadoc 中:
1 2 @Documented public @interface PublicApi {}
75.4 @Inherited 它只影响类级注解沿父类继承 的反射查询:
1 2 3 4 @Inherited @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface FeatureFlag {}
限制:
不会从接口继承;
不作用于方法;
不作用于字段;
不等于所有框架都采用相同查找规则。
Spring 等框架可能实现自己的合并注解查找语义,不能把框架行为简单等同于 JDK @Inherited。
75.5 @Repeatable 1 2 3 4 5 6 7 8 9 10 11 12 @Repeatable(AccessRules.class) @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AccessRule { String role () ; }@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AccessRules { AccessRule[] value(); }
使用:
1 2 3 @AccessRule(role = "ADMIN") @AccessRule(role = "AUDITOR") public void exportReport () {}
读取:
1 AccessRule[] rules = method.getAnnotationsByType(AccessRule.class);
不要只调用 getAnnotation(AccessRule.class),否则重复注解处理不完整。
七十六、设计一个运行时审计注解 完整定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Documented @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Audit { String action () ; String resource () default "" ; boolean includeArguments () default false ; boolean includeResult () default false ; }
拦截器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public final class AuditInvocationHandler implements InvocationHandler { private final Object target; private final AuditSink sink; public AuditInvocationHandler (Object target, AuditSink sink) { this .target = Objects.requireNonNull(target); this .sink = Objects.requireNonNull(sink); } @Override public Object invoke (Object proxy, Method interfaceMethod, Object[] args) throws Throwable { Method targetMethod = target.getClass().getMethod( interfaceMethod.getName(), interfaceMethod.getParameterTypes() ); Audit audit = targetMethod.getAnnotation(Audit.class); if (audit == null ) { return invokeTarget(targetMethod, args); } Instant start = Instant.now(); try { Object result = invokeTarget(targetMethod, args); sink.success(new AuditEvent ( audit.action(), audit.resource(), audit.includeArguments() ? safeArguments(args) : null , audit.includeResult() ? safeResult(result) : null , start, Instant.now() )); return result; } catch (Throwable ex) { sink.failure(audit.action(), audit.resource(), start, ex); throw ex; } } private Object invokeTarget (Method method, Object[] args) throws Throwable { try { return method.invoke(target, args); } catch (InvocationTargetException ex) { throw ex.getTargetException(); } } }
这里还需要解决几个生产问题:
接口方法和实现方法的注解查找;
继承和桥接方法;
参数名是否由 -parameters 保留;
密码、Token、手机号等敏感字段脱敏;
大对象和二进制参数截断;
审计写入失败是否影响主流程;
异步审计是否保证顺序与可靠性;
审计事件是否包含 traceId、操作者和租户。
七十七、注解组合与语义化注解 与其在业务代码中重复一组框架注解,可以创建组合注解:
1 2 3 4 5 6 7 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Service @Transactional(readOnly = true) public @interface ReadOnlyApplicationService { }
使用:
1 2 3 4 @ReadOnlyApplicationService public class CustomerQueryService { }
组合注解适合表达组织级语义:
@InternalApi;
@IdempotentCommand;
@ReadOnlyApplicationService;
@SensitiveOperation;
@RuleExtension;
@FeatureEndpoint。
不要创建只为“少写两行”的无意义别名。好的组合注解应该建立稳定语义和统一治理规则。
七十八、编译期注解处理器 运行时反射不是注解的唯一处理方式。javax.annotation.processing/javax.lang.model 可以在编译期读取源码模型并生成代码。
处理器骨架:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 @SupportedAnnotationTypes("com.example.GenerateMapper") @SupportedSourceVersion(SourceVersion.RELEASE_21) public final class MapperProcessor extends AbstractProcessor { @Override public boolean process ( Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { for (Element element : roundEnv.getElementsAnnotatedWith( GenerateMapper.class)) { generateMapper((TypeElement) element); } return true ; } private void generateMapper (TypeElement sourceType) { String packageName = processingEnv.getElementUtils() .getPackageOf(sourceType) .getQualifiedName() .toString(); String sourceName = sourceType.getSimpleName().toString(); String generatedName = sourceName + "GeneratedMapper" ; String qualifiedName = packageName + "." + generatedName; try { JavaFileObject file = processingEnv.getFiler() .createSourceFile(qualifiedName, sourceType); try (Writer writer = file.openWriter()) { writer.write("package " + packageName + ";\n\n" ); writer.write("public final class " + generatedName + " {\n" ); writer.write(" private " + generatedName + "() {}\n" ); writer.write("}\n" ); } } catch (IOException ex) { processingEnv.getMessager().printMessage( Diagnostic.Kind.ERROR, "Failed to generate mapper: " + ex.getMessage(), sourceType ); } } }
编译期处理的优势:
生成代码可被编译器检查;
运行期无需扫描和反射;
启动速度和原生镜像兼容性通常更好;
配置错误可以在构建期失败;
IDE 可以导航到生成代码。
代价:
处理器开发复杂;
增量编译与多轮处理需谨慎;
调试体验不如普通代码;
生成代码目录和发布流程要规范;
API 变更可能影响大量生成器。
MapStruct、Lombok、Dagger 等都展示了不同的编译期元编程路径。
七十九、运行时注解与编译期注解如何选择
需求
推荐方式
方法调用时做事务、鉴权、审计
运行时注解 + 代理/拦截器
根据类结构生成 Mapper、Builder、元模型
编译期注解处理器
只做代码规范检查
编译器插件/静态分析
启动时扫描插件
运行时注解或 SPI
原生镜像、快速启动要求高
尽量编译期生成
需要动态配置改变行为
注解只做静态声明,动态部分放配置/规则系统
八十、注解驱动设计的常见问题 隐藏控制流 1 2 @Magic public void execute () {}
如果开发者无法从名称、文档和 IDE 提示理解 @Magic 做了什么,注解会让控制流变得隐形。应提供:
清晰名称;
Javadoc;
适用范围;
默认行为;
异常语义;
与代理、自调用和事务的关系。
属性过多 1 2 3 4 5 6 7 8 9 10 @Workflow( retry = true, async = true, timeout = 3000, transactional = true, lock = true, cache = true, audit = true, // 继续膨胀…… )
这意味着一个注解承担了多个相互独立的能力,应拆分成可组合注解或显式配置对象。
默认值过于危险 安全、事务、重试、幂等相关注解应采用保守默认值。不要让“忘记写一个属性”导致开放权限或无限重试。
只标注接口或只标注实现 不同代理和扫描方式读取的位置不同。框架设计时必须规定注解应该放在接口、实现、类还是方法,并编写继承/桥接/覆盖场景测试。
第十部分:正确理解 equals 与 hashCode 八十一、==、equals 与对象身份 对基本类型,== 比较值;对引用类型,== 比较是否指向同一个对象。
1 2 3 4 5 String a = new String ("java" );String b = new String ("java" ); System.out.println(a == b); System.out.println(a.equals(b));
Object#equals 的默认实现本质上是身份相等。值对象需要覆写它,表达“哪些字段相同就代表同一个逻辑值”。
八十二、equals 合约 对于非空引用,equals 应满足:
自反性 :x.equals(x) 为 true;
对称性 :x.equals(y) 与 y.equals(x) 结果一致;
传递性 :若 x.equals(y) 且 y.equals(z),则 x.equals(z);
一致性 :相关字段未变化时,多次调用结果一致;
非空性 :x.equals(null) 为 false。
这些不是学术装饰。集合、缓存、ORM、去重、测试断言和分布式键都依赖这些性质。
八十三、hashCode 合约 最重要的规则:
1 若 a.equals(b) 为 true,则 a.hashCode() 必须等于 b.hashCode()。
反过来不成立:哈希值相同的对象可以不相等,这叫哈希冲突。
如果只覆写 equals 不覆写 hashCode:
1 2 3 4 Set<UserId> ids = new HashSet <>(); ids.add(new UserId (100L ));boolean exists = ids.contains(new UserId (100L ));
即使两个 UserId 逻辑相等,也可能被分配到不同桶,contains 返回错误结果。
八十四、值对象的标准实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public final class CustomerId { private final long value; public CustomerId (long value) { if (value <= 0 ) { throw new IllegalArgumentException ("value must be positive" ); } this .value = value; } public long value () { return value; } @Override public boolean equals (Object obj) { if (this == obj) { return true ; } if (!(obj instanceof CustomerId other)) { return false ; } return value == other.value; } @Override public int hashCode () { return Long.hashCode(value); } @Override public String toString () { return Long.toString(value); } }
Java Record 天然适合简单值对象:
1 2 3 4 5 6 7 public record CustomerId (long value) { public CustomerId { if (value <= 0 ) { throw new IllegalArgumentException ("value must be positive" ); } } }
Record 自动基于组件生成 equals、hashCode 和 toString。不过如果组件包含数组,数组仍按引用语义参与 Record 的自动 equals,需要特别处理。
八十五、getClass() 还是 instanceof 两种常见模板:
1 2 3 if (obj == null || getClass() != obj.getClass()) { return false ; }
以及:
1 2 3 if (!(obj instanceof CustomerId other)) { return false ; }
getClass()要求双方运行时类型完全一致,适合:
最终类;
不希望子类与父类相等;
实体层次中相等性需要严格类型边界。
instanceof允许子类型参与比较,适合:
不可变值类型;
子类不会增加影响相等性的状态;
类型设计明确保证对称性和传递性。
继承与相等性容易发生冲突。例如父类只比较坐标,子类再增加颜色,就很难同时保证父子对象之间的对称性和传递性。更稳妥的设计通常是:
值对象使用 final;
优先组合而非继承;
实体相等策略显式规定;
避免跨类型相等。
八十六、可变对象不能轻易做 HashMap 的键 1 2 3 4 5 public final class MutableKey { private String value; }
1 2 3 4 5 6 7 MutableKey key = new MutableKey ("A" ); Map<MutableKey, String> map = new HashMap <>(); map.put(key, "value" ); key.setValue("B" ); System.out.println(map.get(key));
对象放入 Map 后位于根据旧哈希值计算的桶中;字段变化后,新哈希值指向另一个桶。对象仍在 Map 里,却像“搬家没改地址”一样找不到。
作为哈希键的对象应该:
不可变;
或至少相等性相关字段在作为键期间不变;
哈希值计算稳定;
字段语义明确。
八十七、实体对象的相等性为什么复杂 以 JPA 实体为例:
1 2 3 4 5 6 @Entity public class Customer { @Id @GeneratedValue private Long id; }
新建但尚未持久化的两个实体都可能 id == null。若只按 ID 比较,就可能把所有新实体视为相等;若按全部可变字段比较,持久化后字段变化又会破坏 Set。
常见策略:
业务自然键 例如不可变、唯一的订单号:
1 2 3 4 5 @Override public boolean equals (Object obj) { return obj instanceof Order other && orderNo.equals(other.orderNo); }
前提是自然键:
创建时就存在;
不可变;
真正唯一;
不会因业务规则变化失效。
数据库 ID 只在 ID 非空时比较:
1 2 3 4 5 6 7 8 9 10 @Override public boolean equals (Object obj) { if (this == obj) { return true ; } if (!(obj instanceof Customer other)) { return false ; } return id != null && id.equals(other.id); }
这种方式还要处理代理类型和 hashCode 稳定性,不能机械复制。Hibernate 代理可能是实体子类,严格 getClass() 会导致代理与实体不相等。项目应采用与 ORM 官方建议、聚合设计和集合使用方式一致的统一模板。
最安全的原则是:不要随意把生命周期复杂的可变实体放进 HashSet 或作为 HashMap 键 。
八十八、数组、BigDecimal 和浮点数的特殊情况 数组 数组没有按元素覆写 equals:
1 2 3 4 5 byte [] a = {1 , 2 };byte [] b = {1 , 2 }; System.out.println(a.equals(b)); System.out.println(Arrays.equals(a, b));
对象包含数组时:
1 2 3 4 5 6 7 8 9 10 @Override public boolean equals (Object obj) { return obj instanceof Payload other && Arrays.equals(bytes, other.bytes); }@Override public int hashCode () { return Arrays.hashCode(bytes); }
并且应做防御性复制,避免外部修改数组。
BigDecimal1 2 new BigDecimal ("1.0" ).equals(new BigDecimal ("1.00" )); new BigDecimal ("1.0" ).compareTo(new BigDecimal ("1.00" ));
equals 同时考虑数值和 scale;compareTo 比较数值大小。因此:
金额相等的业务判断常用 compareTo(...) == 0;
用 BigDecimal 做 Map 键时要先统一 scale 或接受精确表示差异;
不要让 compareTo 和 equals 的差异悄悄影响排序集合与哈希集合。
浮点数 double 存在 NaN、正负零和精度问题。领域金额不要使用 double。确实需要浮点值对象时,使用 Double.compare 和 Double.hashCode 保持一致模板。
八十九、代理对象与 equals/hashCode 动态代理、AOP 代理和 ORM 代理让运行时类型发生变化。代理处理器必须决定:
代理只与自身相等;
代理与目标对象相等;
两个包装相同目标的代理是否相等;
hashCode 使用代理身份还是目标值;
toString 是否触发远程调用或懒加载。
RPC 客户端代理通常采用身份语义;值对象不应依赖代理;ORM 实体则要遵循框架和领域共同约定。
尤其不要让 equals 发起远程请求、数据库查询或昂贵计算。集合可能高频调用它,届时一个 HashSet.contains 就能变成分布式系统冒险游戏。
九十、Lombok 的 @EqualsAndHashCode Lombok 能减少模板代码:
1 2 3 4 5 6 @Value @EqualsAndHashCode public class Money { BigDecimal amount; Currency currency; }
但要审核生成语义:
是否包含父类字段;
是否排除可变字段;
是否包含懒加载关联;
是否适合 JPA 实体;
BigDecimal scale 是否符合业务;
数组是否按期望处理;
是否引入双向关联递归。
“自动生成”只减少敲键盘,不替你决定对象身份。
九十一、如何测试相等性合约 至少覆盖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test void shouldRespectEqualsContract () { CustomerId a = new CustomerId (1 ); CustomerId b = new CustomerId (1 ); CustomerId c = new CustomerId (1 ); CustomerId other = new CustomerId (2 ); assertEquals(a, a); assertEquals(a, b); assertEquals(b, a); assertEquals(b, c); assertEquals(a, c); assertNotEquals(a, other); assertNotEquals(a, null ); assertNotEquals(a, "1" ); assertEquals(a.hashCode(), b.hashCode()); }
还应测试:
放入 HashSet 后能否用等价实例查到;
用作 HashMap 键;
序列化前后;
代理对象;
空值字段;
继承边界;
数组和集合字段;
JPA 持久化前后。
第十一部分:Java Lambda 表达式 九十二、Lambda 是函数式接口实例的紧凑表达 1 Runnable task = () -> System.out.println("run" );
它不是“一个没有类的自由函数”。Lambda 必须出现在目标类型上下文中,目标类型通常是函数式接口。
1 2 3 4 @FunctionalInterface public interface Calculator { int calculate (int left, int right) ; }
1 Calculator add = (left, right) -> left + right;
函数式接口只有一个抽象方法,但可以有:
default 方法;
static 方法;
与 Object 公共方法签名对应的方法。
@FunctionalInterface 不是必须的,但建议添加,它让编译器帮助维护接口契约。
九十三、Lambda 语法 无参数:
1 Supplier<UUID> supplier = () -> UUID.randomUUID();
一个参数:
1 Function<String, Integer> length = text -> text.length();
多个参数:
1 BinaryOperator<BigDecimal> add = (left, right) -> left.add(right);
代码块:
1 2 3 4 Function<String, String> normalize = text -> { String trimmed = text.trim(); return trimmed.toLowerCase(Locale.ROOT); };
显式参数类型:
1 2 3 BiFunction<BigDecimal, BigDecimal, BigDecimal> divide = (BigDecimal left, BigDecimal right) -> left.divide(right, 2 , RoundingMode.HALF_UP);
使用 var 时所有参数必须一致使用:
1 2 BiFunction<String, String, String> join = (@Deprecated var left, var right) -> left + right;
使用 var 的主要价值是允许在 Lambda 参数上添加注解,而不是让本已清楚的代码再多写三个字符。
九十四、JDK 常用函数式接口
接口
方法
语义
Supplier<T>
T get()
提供一个值
Consumer<T>
void accept(T)
消费一个值
Function<T,R>
R apply(T)
转换
Predicate<T>
boolean test(T)
判断
UnaryOperator<T>
T apply(T)
同类型一元运算
BinaryOperator<T>
T apply(T,T)
同类型二元运算
BiFunction<T,U,R>
R apply(T,U)
两输入一输出
BiConsumer<T,U>
void accept(T,U)
消费两个值
原始类型特化接口可以避免装箱:
IntPredicate;
IntFunction<R>;
ToLongFunction<T>;
LongConsumer;
DoubleBinaryOperator。
高频数值循环中,Function<Integer, Integer> 会产生装箱成本,优先使用 IntUnaryOperator。
九十五、目标类型与类型推断 同一个 Lambda 可以对应不同接口:
1 2 Callable<String> callable = () -> "done" ; Supplier<String> supplier = () -> "done" ;
Lambda 本身的参数和返回类型由目标类型推断。没有目标类型时,编译器无法判断:
重载可能产生歧义:
1 2 3 4 5 void submit (Callable<String> task) {}void submit (Supplier<String> task) {} submit((Supplier<String>) () -> "done" );
API 设计中不要创建仅靠相似函数式接口区分的重载,否则调用方会频繁强制转换。
九十六、变量捕获与 effectively final 1 2 3 int threshold = 100 ; Predicate<Order> largeOrder = order -> order.amount().compareTo(BigDecimal.valueOf(threshold)) >= 0 ;
局部变量必须是 final 或 effectively final:赋值后不再改变。
原因可以从生命周期理解:局部变量位于当前方法栈帧,而 Lambda 可能在方法返回后继续存在。实现会捕获变量值,而不是让异步代码悬挂引用一个已消失的栈槽。
对象引用不能重新赋值,但对象内部仍可能可变:
1 2 List<String> names = new ArrayList <>();Runnable task = () -> names.add("Mario" );
这能编译,不代表线程安全。names 引用 effectively final,ArrayList 本身仍可变且非线程安全。
九十七、Lambda 中的 this Lambda 不创建新的 this 语义:
1 2 3 4 5 6 7 public class Worker { private final String name = "worker" ; public Runnable task () { return () -> System.out.println(this .name); } }
这里的 this 是外层 Worker 实例。
匿名内部类不同:
1 2 3 4 5 6 7 8 return new Runnable () { private final String name = "anonymous" ; @Override public void run () { System.out.println(this .name); } };
匿名类中的 this 指向匿名类实例。这是 Lambda 和匿名内部类不能完全互换的重要差异。
九十八、方法引用 静态方法 1 Function<String, Integer> parse = Integer::parseInt;
等价于:
1 text -> Integer.parseInt(text)
特定对象的实例方法 1 2 String prefix = "order-" ; Predicate<String> startsWith = prefix::startsWith;
任意对象的实例方法 1 Function<String, String> trim = String::trim;
构造器引用 1 2 Supplier<ArrayList<String>> listFactory = ArrayList::new ; Function<String, CustomerId> idFactory = CustomerId::new ;
方法引用应该提升可读性。如果需要读者在多个重载间做脑内类型推理,显式 Lambda 反而更清楚。
九十九、函数组合 1 2 3 Function<String, String> trim = String::trim; Function<String, String> lower = value -> value.toLowerCase(Locale.ROOT); Function<String, String> normalize = trim.andThen(lower);
Predicate:
1 2 3 4 5 Predicate<Order> paid = order -> order.status() == OrderStatus.PAID; Predicate<Order> large = order -> order.amount() .compareTo(new BigDecimal ("1000" )) >= 0 ; Predicate<Order> target = paid.and(large);
Consumer:
1 2 3 Consumer<Order> log = order -> logger.info("order={}" , order.id()); Consumer<Order> publish = eventPublisher::publish; Consumer<Order> pipeline = log.andThen(publish);
组合前要关注副作用:第二个 Consumer 失败时,第一个已经执行。需要事务、补偿或可靠事件时,不要把关键流程简单拼成 Consumer 链。
一百、受检异常如何处理 标准函数式接口通常不声明业务受检异常:
1 Function<Path, String> reader = path -> Files.readString(path);
可在边界转换:
1 2 3 4 5 6 7 Function<Path, String> reader = path -> { try { return Files.readString(path); } catch (IOException ex) { throw new UncheckedIOException (ex); } };
或定义专用接口:
1 2 3 4 @FunctionalInterface public interface ThrowingFunction <T, R, E extends Exception > { R apply (T value) throws E; }
不要创建一个会捕获所有异常并返回 null 的“万能 Lambda 工具”。异常被吞掉后,Stream 流水线只会产出一串神秘空值。
一百零一、Stream 中的 Lambda 是惰性执行的 1 2 3 4 5 6 Stream<String> stream = names.stream() .filter(name -> { System.out.println("filter " + name); return !name.isBlank(); }) .map(String::trim);
没有终止操作时,过滤和转换不会执行:
1 List<String> result = stream.toList();
这意味着:
调试日志出现的时间可能比定义流水线晚;
Lambda 不应依赖定义时的瞬时上下文;
Stream 不能重复消费;
打开资源的 Stream 必须关闭;
并行流会改变线程和执行顺序。
避免在 map、filter 中修改外部集合:
1 2 List<String> output = new ArrayList <>(); names.parallelStream().forEach(output::add);
应该使用收集器:
1 2 3 List<String> output = names.parallelStream() .map(String::trim) .toList();
一百零二、Lambda 在 JVM 中如何实现 Java 编译器通常不会为每个 Lambda 固定生成一个类似匿名内部类的 .class 文件。调用点通常使用 invokedynamic,由 Lambda 元工厂在运行时链接目标实现。
概念流程:
flowchart LR
A[Lambda 源码] --> B[javac]
B --> C[invokedynamic 调用点]
C --> D[LambdaMetafactory]
D --> E[链接函数式接口实例]
E --> F[调用目标方法]
无捕获 Lambda 和有捕获 Lambda 的实例化策略可能不同,JVM 也可以优化对象分配。因此不要依赖:
1 2 3 4 Predicate<String> a = String::isBlank; Predicate<String> b = String::isBlank;
Lambda 的对象身份、实现类名称和 toString 都不是稳定业务协议。
一百零三、Lambda 序列化 函数式接口继承 Serializable 后,Lambda 可能被序列化:
1 2 3 4 @FunctionalInterface public interface SerializablePredicate <T> extends Predicate <T>, Serializable { }
但不建议把 Lambda 序列化形式作为长期存储或跨版本协议:
捕获的对象也必须可序列化;
代码重构可能破坏反序列化;
生成形式不是稳定业务模型;
反序列化不可信数据存在安全风险;
不同编译器/JDK 的细节不应成为协议依赖。
需要持久化规则时,保存显式规则模型、表达式文本或稳定 DSL,而不是序列化 Lambda。
一百零四、Lambda 最佳实践
Lambda 保持短小,复杂逻辑提取为有名称的方法;
尽量使用无副作用函数;
避免捕获可变共享状态;
不用 Stream 强行重写所有循环;
热点数值计算使用原始类型特化接口;
避免相似函数式接口重载;
异常转换保留 cause 和业务上下文;
方法引用只有在更清晰时才使用;
不依赖 Lambda 实例身份和类名;
对并行流显式评估线程安全、顺序和阻塞调用。
第十二部分:综合案例——构建可扩展的规则执行框架 一百零五、案例目标 现在用一个小型规则执行框架,把前面的知识连接起来。目标:
使用泛型 定义类型安全的规则接口;
使用注解 描述规则元数据;
使用枚举 表达规则类型与状态;
使用 SPI 发现不同规则提供者;
使用反射 读取规则注解;
使用动态代理 统一增加指标和异常处理;
使用 Lambda 快速定义简单规则;
使用表达式引擎 承载动态规则;
使用 equals/hashCode 正确实现规则缓存键;
用策略注册表替代持续增长的 if/else。
flowchart TD
A[规则调用方] --> B[RuleExecutor]
B --> C[RuleRegistry]
C --> D[Java Lambda Rule]
C --> E[Expression Rule]
C --> F[SPI Plugin Rule]
G[ServiceLoader] --> F
H[反射读取 @RuleDefinition] --> C
I[动态代理] --> D
I --> E
I --> F
J[RuleKey record] --> K[编译缓存]
E --> K
I --> L[耗时/异常/审计]
一百零六、用泛型定义规则契约 1 2 3 4 public interface Rule <I, O> { O evaluate (I input, RuleContext context) ; }
上下文只放稳定的横切信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 public record RuleContext ( String tenantId, String traceId, Instant evaluationTime, Map<String, Object> attributes ) { public RuleContext { Objects.requireNonNull(tenantId); Objects.requireNonNull(traceId); Objects.requireNonNull(evaluationTime); attributes = Map.copyOf(attributes); } }
为什么不直接写成:
1 Object evaluate (Map<String, Object> input) ;
因为全 Object/Map 设计会把错误推迟到运行时:
输入字段名靠字符串约定;
类型转换散落在规则中;
IDE 无法重构;
调用方不知道输出类型;
错误只能在真实流量触发。
泛型接口让 Java 规则保持静态类型安全;表达式适配器再负责动态输入与类型世界之间的边界转换。
一百零七、用注解声明规则元数据 1 2 3 4 5 6 7 8 9 10 11 12 13 @Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface RuleDefinition { String code () ; RuleKind kind () ; String description () default "" ; int order () default 0 ; }
枚举:
1 2 3 4 5 6 7 8 9 10 11 12 13 public enum RuleKind { VALIDATION, ROUTING, CALCULATION, RISK }public enum RuleStatus { DRAFT, ACTIVE, DISABLED, ARCHIVED }
Java 规则实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @RuleDefinition( code = "ORDER_AMOUNT_POSITIVE", kind = RuleKind.VALIDATION, description = "订单金额必须大于零", order = 10 ) public final class PositiveAmountRule implements Rule <OrderInput, RuleDecision> { @Override public RuleDecision evaluate ( OrderInput input, RuleContext context) { if (input.amount().signum() <= 0 ) { return RuleDecision.reject( "ORDER_AMOUNT_INVALID" , "order amount must be positive" ); } return RuleDecision.pass(); } }
输入与结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public record OrderInput ( String orderId, BigDecimal amount, String customerLevel, boolean blacklisted ) {}public record RuleDecision ( boolean passed, String code, String message ) { public static RuleDecision pass () { return new RuleDecision (true , "PASS" , "passed" ); } public static RuleDecision reject (String code, String message) { return new RuleDecision (false , code, message); } }
一百零八、使用 Record 实现可靠的规则键 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public record RuleKey ( String tenantId, String ruleCode, long version ) { public RuleKey { if (tenantId == null || tenantId.isBlank()) { throw new IllegalArgumentException ("tenantId must not be blank" ); } if (ruleCode == null || ruleCode.isBlank()) { throw new IllegalArgumentException ("ruleCode must not be blank" ); } if (version <= 0 ) { throw new IllegalArgumentException ("version must be positive" ); } } }
Record 自动生成一致的 equals/hashCode,适合做:
1 ConcurrentMap<RuleKey, CompiledRule> cache = new ConcurrentHashMap <>();
规则版本是键的一部分。发布新版本不会覆盖旧版本,可支持:
灰度;
历史回放;
快速回滚;
集群并存;
审计复现。
一百零九、使用 Lambda 创建简单规则 定义工厂:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public final class Rules { private Rules () {} public static <I> Rule<I, RuleDecision> fromPredicate ( Predicate<? super I> predicate, String rejectCode, String rejectMessage) { Objects.requireNonNull(predicate); return (input, context) -> predicate.test(input) ? RuleDecision.pass() : RuleDecision.reject(rejectCode, rejectMessage); } }
创建规则:
1 2 3 4 5 Rule<OrderInput, RuleDecision> notBlacklisted = Rules.fromPredicate( input -> !input.blacklisted(), "CUSTOMER_BLACKLISTED" , "customer is blacklisted" );
这里的 Predicate<? super I> 使用了 PECS 思想:Predicate 消费输入,因此接受 I 的父类型 Predicate。
Lambda 规则适合:
逻辑短小;
无依赖或依赖很少;
不需要复杂生命周期;
不需要独立元数据扫描。
复杂规则仍应使用命名类,便于测试、监控和排障。
一百一十、定义 SPI 扩展点 规则提供者:
1 2 3 4 5 6 public interface RuleProvider { String providerName () ; Collection<RuleDescriptor<?, ?>> provide(); }
描述器保留类型令牌:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public record RuleDescriptor <I, O>( String code, RuleKind kind, Class<I> inputType, Class<O> outputType, Rule<I, O> rule, int order ) { public RuleDescriptor { Objects.requireNonNull(code); Objects.requireNonNull(kind); Objects.requireNonNull(inputType); Objects.requireNonNull(outputType); Objects.requireNonNull(rule); } }
插件实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public final class RiskRuleProvider implements RuleProvider { @Override public String providerName () { return "risk-plugin" ; } @Override public Collection<RuleDescriptor<?, ?>> provide() { Rule<OrderInput, RuleDecision> rule = Rules.fromPredicate( input -> !input.blacklisted(), "CUSTOMER_BLACKLISTED" , "customer is blacklisted" ); return List.of(new RuleDescriptor <>( "NOT_BLACKLISTED" , RuleKind.RISK, OrderInput.class, RuleDecision.class, rule, 20 )); } }
SPI 文件:
1 META-INF/services/com.example.rules.api.RuleProvider
内容:
1 com.example.rules.risk.RiskRuleProvider
加载:
1 2 3 4 5 List<RuleProvider> providers = ServiceLoader .load(RuleProvider.class) .stream() .map(ServiceLoader.Provider::get) .toList();
一百一十一、使用反射读取 Java 规则注解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 public final class AnnotatedRuleFactory { public RuleDescriptor<?, ?> create(Rule<?, ?> rule) { Class<?> type = rule.getClass(); RuleDefinition definition = type.getAnnotation(RuleDefinition.class); if (definition == null ) { throw new IllegalArgumentException ( "missing @RuleDefinition: " + type.getName()); } RuleTypes types = resolveRuleTypes(type); return createDescriptor(definition, types, rule); } private RuleTypes resolveRuleTypes (Class<?> implementationType) { for (Type type : implementationType.getGenericInterfaces()) { if (type instanceof ParameterizedType parameterized && parameterized.getRawType() == Rule.class) { Type input = parameterized.getActualTypeArguments()[0 ]; Type output = parameterized.getActualTypeArguments()[1 ]; if (input instanceof Class<?> inputClass && output instanceof Class<?> outputClass) { return new RuleTypes (inputClass, outputClass); } } } throw new IllegalArgumentException ( "cannot resolve Rule<I,O> types from " + implementationType.getName()); } @SuppressWarnings({"rawtypes", "unchecked"}) private RuleDescriptor<?, ?> createDescriptor( RuleDefinition definition, RuleTypes types, Rule<?, ?> rule) { return new RuleDescriptor ( definition.code(), definition.kind(), types.inputType(), types.outputType(), rule, definition.order() ); } private record RuleTypes ( Class<?> inputType, Class<?> outputType ) {} }
这个简化实现只处理“实现类直接声明 Rule<具体类, 具体类>”的情况。生产框架还要解析:
间接接口继承;
抽象基类;
类型变量替换;
通配符;
代理类;
桥接方法;
Kotlin/Scala 生成类型;
AOT 元数据。
因此框架层的泛型解析不要重复手写,优先评估 Spring ResolvableType、Guava TypeToken 或经过验证的内部工具。
一百一十二、使用动态代理增加可观测性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public final class RuleProxyFactory { private final RuleMetrics metrics; public RuleProxyFactory (RuleMetrics metrics) { this .metrics = metrics; } @SuppressWarnings("unchecked") public <I, O> Rule<I, O> wrap (String ruleCode, Rule<I, O> target) { return (Rule<I, O>) Proxy.newProxyInstance( Rule.class.getClassLoader(), new Class <?>[]{Rule.class}, (proxy, method, args) -> { if (method.getDeclaringClass() == Object.class) { return switch (method.getName()) { case "toString" -> "ObservedRule(" + ruleCode + ")" ; case "hashCode" -> System.identityHashCode(proxy); case "equals" -> proxy == args[0 ]; default -> throw new IllegalStateException (); }; } long start = System.nanoTime(); try { Object result = method.invoke(target, args); metrics.success(ruleCode, System.nanoTime() - start); return result; } catch (InvocationTargetException ex) { Throwable cause = ex.getTargetException(); metrics.failure(ruleCode, System.nanoTime() - start, cause); throw cause; } } ); } }
指标至少应区分:
规则代码和版本;
租户;
成功、正常拒绝、执行错误、超时;
编译耗时与执行耗时;
缓存命中;
引擎类型;
输入规模分桶。
不要把用户 ID、订单号等高基数值放进 Metrics 标签,否则时序数据库会先于规则引擎倒下。
一百一十三、把表达式引擎适配为泛型规则 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public final class ExpressionBooleanRule <I> implements Rule <I, RuleDecision> { private final TypedExpression<I, Boolean> expression; private final String rejectCode; private final String rejectMessage; public ExpressionBooleanRule ( TypedExpression<I, Boolean> expression, String rejectCode, String rejectMessage) { this .expression = Objects.requireNonNull(expression); this .rejectCode = Objects.requireNonNull(rejectCode); this .rejectMessage = Objects.requireNonNull(rejectMessage); } @Override public RuleDecision evaluate (I input, RuleContext context) { Boolean matched = expression.evaluate(input); return Boolean.TRUE.equals(matched) ? RuleDecision.pass() : RuleDecision.reject(rejectCode, rejectMessage); } }
SpEL 适配器示意:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public final class SpelTypedExpression <I> implements TypedExpression <I, Boolean> { private final Expression expression; private final SimpleEvaluationContext context; public SpelTypedExpression (String source) { this .expression = new SpelExpressionParser ().parseExpression(source); this .context = SimpleEvaluationContext .forReadOnlyDataBinding() .build(); } @Override public Boolean evaluate (I input) { return expression.getValue(context, input, Boolean.class); } }
配置规则:
1 amount >= 100 && customerLevel == 'VIP' && !blacklisted
适配后,执行器并不关心它是 Java 类、Lambda、SPI 插件还是表达式规则:
1 RuleDecision decision = rule.evaluate(input, context);
这就是抽象层真正应该提供的价值:统一调用协议,而不是把每个引擎 API 扩散到业务代码。
一百一十四、注册表替代中心化 if/else 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public final class RuleRegistry { private final Map<String, RuleDescriptor<?, ?>> descriptors; public RuleRegistry (Collection<RuleDescriptor<?, ?>> descriptors) { Map<String, RuleDescriptor<?, ?>> map = new HashMap <>(); for (RuleDescriptor<?, ?> descriptor : descriptors) { RuleDescriptor<?, ?> old = map.putIfAbsent( descriptor.code(), descriptor); if (old != null ) { throw new IllegalStateException ( "duplicate rule code: " + descriptor.code()); } } this .descriptors = Map.copyOf(map); } public RuleDescriptor<?, ?> required(String code) { RuleDescriptor<?, ?> descriptor = descriptors.get(code); if (descriptor == null ) { throw new IllegalArgumentException ("unknown rule: " + code); } return descriptor; } }
类型安全执行器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public final class RuleExecutor { private final RuleRegistry registry; public RuleExecutor (RuleRegistry registry) { this .registry = registry; } public <I, O> O execute ( String code, Class<I> inputType, Class<O> outputType, I input, RuleContext context) { RuleDescriptor<?, ?> raw = registry.required(code); if (raw.inputType() != inputType || raw.outputType() != outputType) { throw new IllegalArgumentException ( "rule type mismatch, code=" + code + ", expected=" + inputType.getName() + " -> " + outputType.getName() + ", actual=" + raw.inputType().getName() + " -> " + raw.outputType().getName() ); } return invokeChecked(raw, input, context, outputType); } @SuppressWarnings("unchecked") private static <I, O> O invokeChecked ( RuleDescriptor<?, ?> raw, I input, RuleContext context, Class<O> outputType) { Rule<I, O> rule = (Rule<I, O>) raw.rule(); O result = rule.evaluate(input, context); return outputType.cast(result); } }
由于 Java 类型擦除,注册表边界仍有一次受控转换。但转换前后都校验了 Class<I> 和 Class<O>,把不安全操作集中在基础设施内部,而不是扩散到每个调用方。
一百一十五、执行规则链 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public final class ValidationRuleChain <I> { private final List<Rule<I, RuleDecision>> rules; public ValidationRuleChain (List<Rule<I, RuleDecision>> rules) { this .rules = List.copyOf(rules); } public RuleDecision evaluate (I input, RuleContext context) { for (Rule<I, RuleDecision> rule : rules) { RuleDecision decision = rule.evaluate(input, context); if (!decision.passed()) { return decision; } } return RuleDecision.pass(); } }
如果业务需要收集所有失败:
1 2 3 4 5 6 public List<RuleDecision> evaluateAll (I input, RuleContext context) { return rules.stream() .map(rule -> rule.evaluate(input, context)) .filter(decision -> !decision.passed()) .toList(); }
两种模式的语义不同:
Fail-fast:成本低,适合顺序校验;
Collect-all:反馈完整,适合表单或批量检查;
Score/Weight:适合风险评分;
First-match:适合路由;
Best-match:适合定价或推荐。
规则执行模式应建模为显式枚举或策略,而不是藏在循环细节中。
一百一十六、启动阶段的组装流程 sequenceDiagram
participant App as Application
participant SPI as ServiceLoader
participant RF as Reflection Factory
participant PF as Proxy Factory
participant Reg as RuleRegistry
App->>SPI: load RuleProvider
SPI-->>App: providers
App->>RF: parse @RuleDefinition and generic types
RF-->>App: descriptors
App->>PF: wrap each rule
PF-->>App: observed rules
App->>Reg: validate duplicates and build immutable registry
Reg-->>App: ready
启动时应一次性完成:
SPI 配置有效性;
注解完整性;
规则代码唯一性;
输入输出类型;
表达式语法与变量契约;
函数白名单;
依赖健康检查;
代理创建;
注册表冻结。
能在启动或发布阶段失败的问题,不要拖到运行期随机失败。
一百一十七、测试策略 单条 Java 规则 1 2 3 4 5 6 7 8 9 10 11 12 @Test void shouldRejectNonPositiveAmount () { PositiveAmountRule rule = new PositiveAmountRule (); RuleDecision decision = rule.evaluate( new OrderInput ("O-1" , BigDecimal.ZERO, "NORMAL" , false ), testContext() ); assertFalse(decision.passed()); assertEquals("ORDER_AMOUNT_INVALID" , decision.code()); }
表达式规则契约测试 同一组测试数据应在规则编辑、审核和发布阶段复用:
1 2 3 4 5 record RuleCase ( String name, OrderInput input, boolean expected ) {}
SPI 打包测试 不要只在 IDE classpath 中测试。应构建真实插件 JAR,再由隔离类加载器验证:
META-INF/services 是否存在;
shaded JAR 是否合并服务文件;
依赖是否缺失;
重复 provider 是否被检测;
模块描述符是否正确。
代理测试 验证:
原异常是否正确拆包;
成功、失败、超时指标;
equals/hashCode/toString;
泛型桥接方法;
默认方法;
并发调用。
缓存测试 验证:
不同租户不串规则;
新旧版本并存;
发布后正确失效;
并发只编译一次或允许可控重复编译;
失败编译不会污染长期缓存;
引擎升级后旧缓存不可复用。
一百一十八、生产化还需要什么 这个示例展示的是机制组合,不是完整规则平台。生产落地至少还需要:
规则仓储与不可变版本;
发布审批;
输入 Schema;
多租户隔离;
加密和脱敏;
规则依赖图;
超时和资源隔离;
灰度、影子执行和回滚;
历史流量回放;
决策解释;
指标、日志和审计;
插件签名与来源验证;
兼容性测试;
SBOM 和许可证治理。
每种机制都只解决一部分问题:
泛型负责类型边界;
反射负责运行时读取;
代理负责调用增强;
SPI 负责发现;
枚举负责有限状态;
Lambda 负责轻量行为;
表达式负责动态规则;
equals/hashCode 负责键与身份;
注解负责元数据;
设计模式负责组织变化。
把它们组合起来的关键不是“技术全家桶”,而是每种机制只待在自己最擅长的边界内。
第十三部分:知识关系、学习顺序与工程检查表 一百一十九、知识关系总表
机制
发生阶段
最核心能力
主要风险
泛型
编译期为主
类型安全、抽象复用
擦除、堆污染、错误通配符设计
反射
运行时
读取和调用未知结构
封装破坏、模块边界、性能和安全
动态代理
运行时
统一拦截方法调用
自调用、异常包装、代理语义
SPI
启动/运行时
发现第三方实现
类加载、冲突、配置与隔离
枚举
编译期+运行时
有限值与状态行为
使用 ordinal、过度承载开放扩展
条件优化
设计期
管理变化和分支复杂度
为模式而模式、过度抽象
许可治理
构建/发布期
代码归属与使用条件可追踪
错误模板、删改上游声明、漏扫制品
表达式引擎
运行时
动态规则求值
注入、逃逸、超时、版本失控
注解
编译期或运行时
声明结构化元数据
隐藏控制流、保留策略错误
equals/hashCode
运行时
对象相等与哈希集合语义
可变键、继承、代理、字段选择错误
Lambda
编译期+运行时链接
行为作为值、函数组合
副作用、捕获状态、异常和滥用 Stream
一百二十、推荐学习顺序 flowchart LR
A[对象与接口] --> B[equals/hashCode]
B --> C[枚举]
C --> D[泛型]
D --> E[函数式接口与 Lambda]
E --> F[注解]
F --> G[反射]
G --> H[动态代理]
H --> I[SPI 与类加载]
I --> J[策略/责任链]
J --> K[表达式引擎]
K --> L[规则平台治理]
建议不是死背 API,而是按三个问题学习每个机制:
它解决什么变化?
它的边界和失败模式是什么?
在框架源码中它如何与其他机制组合?
一百二十一、代码审查清单 泛型
是否为了省事使用裸类型;
通配符方向是否符合 PECS;
是否存在不受控强制转换;
泛型数组和可变参数是否产生堆污染;
运行时是否错误依赖被擦除的类型。
反射与代理
元数据是否缓存;
是否正确处理模块边界;
InvocationTargetException 是否拆包;
Object 方法是否有明确语义;
是否出现代理自调用;
是否可用普通接口调用替代反射。
SPI
服务文件是否进入最终 JAR;
是否检测重复实现;
是否显式定义优先级;
类加载器是否正确;
插件是否需要真正的生命周期与隔离。
枚举与分支
是否错误持久化 ordinal();
未知枚举值如何处理;
策略集合是否真的稳定;
if/else 是校验、映射、策略还是动态规则;
是否为消灭一个简单分支引入过度设计。
许可
顶层 LICENSE 是否与文件头一致;
是否保留第三方版权;
发布包是否携带必要声明;
自动化排除是否有合理理由;
依赖许可证是否单独扫描。
表达式引擎
是否使用白名单;
是否限制超时、长度和复杂度;
是否缓存已解析表达式;
缓存键是否包含租户与版本;
规则是否可审计、灰度和回滚;
是否暴露了 Repository、网络或文件系统;
错误与正常“不匹配”是否区分。
注解
Target 和 Retention 是否正确;
注解解释器在哪里;
默认值是否安全;
组合注解是否表达真实业务语义;
接口、实现和代理上的查找规则是否测试。
equals/hashCode
两者是否同时覆写;
使用了哪些字段;
字段是否可变;
继承是否破坏合约;
数组、BigDecimal、浮点数是否正确处理;
ORM/代理场景是否验证;
是否通过 HashSet/HashMap 行为测试。
Lambda
是否捕获可变共享状态;
是否隐藏复杂业务逻辑;
是否吞掉异常;
Stream 是否存在副作用;
并行流是否真的安全且有收益;
是否依赖 Lambda 实例身份或序列化细节。
总结 Java 的成熟之处,不在于每个机制都复杂,而在于这些机制可以按层次组合:
用泛型建立编译期边界;
用枚举和 equals/hashCode 建立稳定对象语义;
用 Lambda 和策略组织行为;
用注解声明元数据;
用反射和代理解释元数据并增强调用;
用 SPI 让实现可发现、可替换;
用表达式引擎承载真正需要动态变化的规则;
用构建工具和许可证治理保证代码可以被正确发布和复用。
最重要的不是把所有技术都塞进项目,而是知道什么时候不该使用它们:
普通调用能解决,就不要反射;
稳定分支能看懂,就不要规则引擎;
一个简单类能表达,就不要做万能注解;
开放扩展才用 SPI,内部策略优先交给容器;
对象身份没想清楚,就不要让 IDE 自动生成 equals/hashCode 后直接提交;
动态性没有治理能力支撑,就只是把编译错误延期成线上事故。
技术机制负责提供可能性,工程设计负责约束可能性。后者往往更重要。
参考资料 选题来源文章
下列链接为本文各主题的选题与延伸阅读入口。本文按主题重新组织并结合 Java 官方规范、开源项目文档与工程实践独立撰写,不是对原文的逐段转载。
Java 泛型
Java 反射
Java 动态代理
Java SPI
Java 枚举
if/else 优化
在源码中添加许可信息
表达式引擎(一)
表达式引擎(二)
表达式引擎(三)
Java 注解
Java 的 equals 与 hashCode
Java Lambda 表达式
Java 官方规范与 API
表达式引擎
许可证与构建治理