跳转到正文

响应与 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.baseBaseSequenceHook.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:readysecond: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.TupleTuple9 是 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-1

BaseEnum<K,V>code() / desc() 默认 null,枚举实现应覆盖;buildInvalidMessage(name) 只拼接“未查询到有效的…信息”,不会查库或抛异常。

函数契约 SerializableConsumer<T> 用于进程内事件CustomFunction 的去重与冲突合并函数见集合处理