外观
响应与 Hook
Constant 定义可在组件间传递的错误和扩展契约。错误转换为 HTTP 响应、扩展何时执行,都由上层组件或应用编排。
业务消息与异常
当多个业务方法需要报告同一种错误时,用 ResponseMessage 定义业务码和消息模板,再用 BusinessException 携带本次参数。下面先打印异常内容以便观察;实际业务方法需要中止处理时使用 throw error。
以下片段可放入已接入 Constant 的 Java 程序的 main 中,import 放在类定义前:
java
import com.own.constant.entity.ResponseMessage;
import com.own.constant.exception.BusinessException;
var missing = new ResponseMessage("DEMO_ITEM_MISSING", "条目 %s 不存在");
var error = new BusinessException(missing, "A-1");
System.out.println(error.getCode()); // DEMO_ITEM_MISSING
System.out.println(error.getMessage()); // 条目 A-1 不存在示例输出业务码 DEMO_ITEM_MISSING 和消息“条目 A-1 不存在”。在 Web 应用中,异常到 HTTP 响应的转换由 Base 或应用异常处理器完成。
BusinessException 是运行期异常,BusinessSimpleException 继承它。两者的所有构造入口都会查询响应消息覆盖,格式参数必须与最终模板兼容。BusinessSimpleException 在 Constant 内不改变捕获或 HTTP 行为;Base 对它采用不同的日志处理。
消息字段与异常构造方式
ResponseMessage 位于 com.own.constant.entity,通过 code()、message()、description() 读取三个只读字段。二参数构造器将 description 设为 message;三参数构造器可分别传入。该类不是 record,也没有按字段生成 equals/hashCode。
两种异常均支持以下构造方式:
| 参数 | 处理 |
|---|---|
String message | 默认业务码 E0001 |
String code, String message | 指定业务码和消息 |
String code, String message, Object... args | 先取得消息模板,再调用 String.format |
| ResponseMessage response | 从消息对象取得码和文本 |
| ResponseMessage response, Object... args | 取得消息模板后格式化 |
withThrowable(error) 返回当前异常并保存自定义 error 字段。它没有设置标准 cause,读取原异常应使用 getError();只跟踪 getCause() 的日志或监控可能看不到原错误。
内置消息位于 com.own.constant.response.em,以 public static final ResponseMessage 字段提供,包括 ResponseMessageEnum 的通用、工具、登录、表单等分组,以及 ResponseMessageEnumForCommon/ForAliPay/ForAliYun/ForBaidu。这些是消息数据,不是第三方服务调用实现;应用自定义码还应避免与内置码冲突。
响应消息覆盖
ResponseMessageCacheUtil 位于 com.own.constant.response.util,用于按 code 覆盖消息:
java
import com.own.constant.entity.ResponseMessage;
import com.own.constant.response.util.ResponseMessageCacheUtil;
ResponseMessageCacheUtil.update("DEMO_ITEM_MISSING",
new ResponseMessage("DEMO_ITEM_MISSING", "暂未找到条目 %s"));注册后,新构造的同 code 异常使用覆盖模板。get(code, fallback) 只取消息文本;get(response) 则返回覆盖后的整个消息对象。因此更新时保持 Map key 与对象 code 一致,并传入非 null 对象。delete(code) 移除覆盖,后续调用重新使用兜底消息;不会修改已经创建的异常。
该缓存是进程内静态 HashMap,没有 TTL、持久化或并发保护,不接入 Store。适合受控初始化后只读,不应作为多线程动态配置接口。
按模块收集消息
ResponseMessageResolveUtil.resolve(moduleName, clazz) 从类自身声明的字段收集 ResponseMessage,可用于构建错误码目录。传入类应为可访问的公开类,字段为非 null 的 public static ResponseMessage;不会递归扫描嵌套类或继承字段。
| 查询方法 | 结果 |
|---|---|
listByModule(moduleName) | 最新一次该模块的列表;未知模块为空列表 |
list() | 全量累计列表 |
getByCode(code) | 最后写入该 code 的对象;未知为 null |
它与 ResponseMessageCacheUtil 是两套独立静态集合,收集消息不会自动覆盖异常文案。重复 resolve 会向全量列表继续追加;返回的命中列表是内部可变引用,不要修改。反射访问失败会静默结束本次解析,非静态或 null 字段还可能造成异常;只在初始化阶段显式收集一次。
组织扩展 Hook
Hook 是应用提供的处理步骤,基类负责整理这些步骤,应用决定何时调用。先按需要选择组织方式:
| 你的需求 | 选择 | 示例 |
|---|---|---|
| 所有步骤依次处理同一输入 | List | 按顺序执行校验、转换、记录 |
| 按类型找到一组步骤 | Group | 按文件类型找到多个检查器 |
| 按类型选出一个实现 | Map | 按导出格式选择一个转换器 |
Map 不会强制类型唯一:遇到重复类型只记录错误并保留组内首项。需要唯一实现时,应用必须检查冲突;不能把加载日志当作启动校验。
下例选择 List,让序号 10 的步骤先于序号 20 执行。
基类、访问器与类型契约
基础契约均位于 com.own.constant.base:BaseSequenceHook.sequence() 默认 0;BaseTypeHook<T> 在此基础上增加 type() 和 types(),两者默认 null。
| 基类 | 提供给子类的访问器 | 选择方式 |
|---|---|---|
| AbstractBusinessListHook<HOOK> | getHookList() | 全部实例按 sequence 升序排列 |
| AbstractBusinessGroupHook<TYPE, HOOK> | getHookGroupMap() | 同时收集 type 与 types,按类型分组并在组内排序 |
| AbstractBusinessMapHook<TYPE, HOOK> | getHookMap() | 每个类型保留组内排在最前的实例 |
构造器接收非 null 的 Hook 列表,Hook 元素也应非 null。访问器为 protected final,返回不可修改的列表或 Map,但不会冻结 Hook 对象本身。name() 用于加载日志,在父类构造期间就可能调用,返回固定名称,不要依赖尚未初始化的子类字段。
以下完整示例演示有序调用,可作为 example.HookExample 运行:
java
package example;
import com.own.constant.base.AbstractBusinessListHook;
import com.own.constant.base.BaseSequenceHook;
import java.util.List;
public class HookExample {
interface Step extends BaseSequenceHook {
void execute(String value);
}
record PrintStep(int sequence, String label) implements Step {
@Override
public void execute(String value) {
System.out.println(label + ":" + value);
}
}
static final class Dispatcher extends AbstractBusinessListHook<Step> {
Dispatcher(List<Step> steps) {
super(steps);
}
@Override
protected String name() {
return "demo";
}
void execute(String value) {
getHookList().forEach(step -> step.execute(value));
}
}
public static void main(String[] args) {
new Dispatcher(List.of(new PrintStep(20, "second"), new PrintStep(10, "first")))
.execute("ready");
}
}业务输出依次为 first:ready、second:ready,日志实现可另输出 Hook 加载日志。这个示例在调用方同步执行,某项抛异常会中断后续执行;基类没有事务、重试或异常隔离。
想改变执行顺序,修改 sequence 值即可,数值小的先执行。需要按类型选择时,再切换到 Group 或 Map,并为步骤实现 BaseTypeHook<T>。
分组基类通过 HashSet 按 equals/hashCode 去重,再按 sequence 排序,因此相同 sequence 的组内优先级不能依赖传入顺序。Group 和 Map 中需要确定的优先级时,应给出不同的顺序值。
type() 为 null 时不经单类型入口注册;types() 可同时声明多个类型。未知类型查询返回 null,调用方要决定是返回空集合、报错还是使用默认实现。仅将 Hook 注册成 Spring Bean,不会让这些基类自动收集或执行;应由具体调度器注入列表并触发。
元组与枚举契约
com.own.constant.entity.tuple.Tuple 到 Tuple9 是 record。可使用 Tuple.of(value) 或最多九个参数的工厂,单值访问器为 value(),多值为 value1() 至 value9();多值类型也各有自己的 of。允许 null,record 只保证字段引用不变,字段中的集合仍可能可变。
java
import com.own.constant.entity.tuple.Tuple;
var result = Tuple.of("A-1", 3);
System.out.println(result.value1()); // A-1BaseEnum<K,V> 的 code() / desc() 默认 null,枚举实现应覆盖;buildInvalidMessage(name) 只拼接“未查询到有效的…信息”,不会查库或抛异常。
函数契约 SerializableConsumer<T> 用于进程内事件,CustomFunction 的去重与冲突合并函数见集合处理。