1. 项目背景与需求分析
在企业级应用开发中,与外部系统对接是常见需求。最近在基于JeecgBoot框架开发的项目中,我们遇到了一个典型问题:需要同时对接多个客户系统,而每个客户对异常响应的格式要求各不相同。有的要求标准JSON格式,有的要求XML格式,甚至还有客户要求使用自定义的分隔符文本格式。
传统的统一异常处理机制通常只能返回固定格式的响应,这在多协议对接场景下显得力不从心。我们需要在保持JeecgBoot原有异常处理机制的基础上,实现动态适配不同响应格式的能力。具体需求包括:
- 根据请求来源自动识别目标平台
- 对同一异常能返回不同格式的响应内容
- 保持代码的可维护性和扩展性
- 不破坏原有异常处理流程
2. 技术方案设计
2.1 整体架构设计
我们采用分层设计的思路,将功能拆解为三个核心模块:
- 协议标识层:负责识别和标记请求所属平台
- 上下文管理层:线程安全的存储和传递平台标识
- 响应适配层:根据平台标识生成对应格式的响应
java复制// 架构示意图(伪代码)
class PlatformIdentifierInterceptor {
// 识别平台并设置上下文
}
class RequestContextHolder {
// 线程安全的上下文存储
}
class CustomExceptionHandler {
// 多格式响应适配
}
2.2 关键技术选型
- ThreadLocal:用于实现线程隔离的上下文存储,确保多线程环境下不会出现数据混乱
- Spring拦截器:轻量级的请求预处理机制,适合用于平台标识识别
- 枚举类型:定义标准化的平台标识和对应配置
- @RestControllerAdvice:Spring提供的统一异常处理机制
提示:选择ThreadLocal而非Session或Request属性,是因为它更轻量且不会引入额外的依赖关系,特别适合这种仅需在单次请求过程中传递数据的场景。
3. 详细实现步骤
3.1 定义协议枚举和上下文工具
首先创建平台枚举类,明确定义各平台的标识码和对应的Content-Type:
java复制public enum PlatformEnum {
CUSTOMER_A_JSON("customerA", "application/json"),
CUSTOMER_B_XML("customerB", "application/xml"),
CUSTOMER_C_CUSTOM("customerC", "application/custom"),
DEFAULT("default", "application/json");
private final String code;
private final String contentType;
// 构造方法和getter
// 根据code查找枚举的静态方法
}
上下文工具类使用ThreadLocal实现线程安全的存储:
java复制public class RequestContextHolderUtil {
private static final ThreadLocal<PlatformEnum> context = new ThreadLocal<>();
public static void setPlatform(PlatformEnum platform) {
context.set(platform);
}
public static PlatformEnum getPlatform() {
return context.get() == null ? PlatformEnum.DEFAULT : context.get();
}
public static void clear() {
context.remove();
}
}
3.2 实现平台识别拦截器
创建Spring拦截器,从请求头或参数中提取平台标识:
java复制@Component
public class PlatformIdentifyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) {
String platformCode = request.getHeader("X-Platform-Code");
if (StringUtils.isEmpty(platformCode)) {
platformCode = request.getParameter("platformCode");
}
PlatformEnum platform = PlatformEnum.getByCode(platformCode);
RequestContextHolderUtil.setPlatform(platform);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
RequestContextHolderUtil.clear();
}
}
注册拦截器到Spring MVC:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private PlatformIdentifyInterceptor interceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor)
.addPathPatterns("/api/third/**");
}
}
3.3 改造统一异常处理器
扩展JeecgBoot的全局异常处理器,支持多格式响应:
java复制@RestControllerAdvice
public class CustomGlobalExceptionHandler {
@ExceptionHandler(JeecgBootException.class)
public Object handleJeecgBootException(JeecgBootException e, HttpServletResponse response) {
return buildResponse(e.getCode(), e.getMessage(), response);
}
private Object buildResponse(int code, String msg, HttpServletResponse response) {
PlatformEnum platform = RequestContextHolderUtil.getPlatform();
response.setContentType(platform.getContentType() + ";charset=UTF-8");
switch (platform) {
case CUSTOMER_A_JSON:
return buildJsonResponse(code, msg);
case CUSTOMER_B_XML:
return buildXmlResponse(code, msg);
case CUSTOMER_C_CUSTOM:
return buildCustomResponse(code, msg);
default:
return Result.error(code, msg);
}
}
private Map<String, Object> buildJsonResponse(int code, String msg) {
Map<String, Object> map = new HashMap<>();
map.put("retCode", code);
map.put("retMsg", msg);
map.put("timestamp", System.currentTimeMillis());
return map;
}
private String buildXmlResponse(int code, String msg) {
return String.format("<?xml version=\"1.0\"?><response><code>%d</code><message>%s</message></response>",
code, msg);
}
private String buildCustomResponse(int code, String msg) {
return code + "|" + msg + "|" + System.currentTimeMillis();
}
}
4. 高级功能与优化
4.1 响应模板配置化
将响应模板移到配置文件中,提高灵活性:
yaml复制response-templates:
customerA:
contentType: application/json
template: '{"retCode":%d,"retMsg":"%s","timestamp":%d}'
customerB:
contentType: application/xml
template: '<?xml version="1.0"?><response><code>%d</code><message>%s</message></response>'
通过@ConfigurationProperties读取配置:
java复制@ConfigurationProperties(prefix = "response-templates")
public class ResponseTemplateConfig {
private Map<String, Template> templates;
// getter/setter
public static class Template {
private String contentType;
private String template;
// getter/setter
}
}
4.2 协议自动协商
支持根据Accept头自动选择响应格式:
java复制private PlatformEnum negotiateContentType(HttpServletRequest request) {
String acceptHeader = request.getHeader("Accept");
// 解析acceptHeader并匹配支持的格式
// 实现内容协商逻辑
}
4.3 性能优化建议
- 缓存响应构建器:对频繁使用的响应格式,可以预构建StringBuilder模板
- 异步日志记录:异常日志记录采用异步方式,减少对主流程的影响
- 响应压缩:对大体积的响应启用GZIP压缩
5. 常见问题与解决方案
5.1 线程污染问题
现象:偶尔会出现响应格式错乱的情况
原因:线程池复用导致ThreadLocal未及时清理
解决方案:
- 确保拦截器的afterCompletion方法被调用
- 添加Filter进行二次清理保证
java复制public class ContextCleanFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
try {
chain.doFilter(request, response);
} finally {
RequestContextHolderUtil.clear();
}
}
}
5.2 格式扩展问题
需求:新增一种二进制协议支持
步骤:
- 在PlatformEnum中添加新枚举
- 实现对应的响应构建方法
- 注册对应的MessageConverter
java复制// 在枚举中添加
CUSTOMER_D_PROTOBUF("customerD", "application/x-protobuf");
// 在处理器中添加case分支
case CUSTOMER_D_PROTOBUF:
return buildProtobufResponse(code, msg);
5.3 测试验证方法
验证多格式响应的测试策略:
- 单元测试:针对每个响应构建方法
- 集成测试:模拟不同header的请求
- 压力测试:验证线程安全性
示例测试用例:
java复制@Test
public void testXmlResponse() {
RequestContextHolderUtil.setPlatform(PlatformEnum.CUSTOMER_B_XML);
String response = handler.buildXmlResponse(404, "Not Found");
assertTrue(response.contains("<code>404</code>"));
}
6. 实际应用案例
6.1 金融系统对接
在与某银行系统对接时,对方要求异常响应必须包含以下字段:
json复制{
"responseCode": "数字",
"responseDesc": "字符串",
"responseTime": "yyyy-MM-dd HH:mm:ss"
}
我们只需新增一个枚举值和对应的响应构建方法:
java复制BANK_SPECIAL_JSON("bank", "application/json");
private Object buildBankJsonResponse(int code, String msg) {
Map<String, Object> map = new HashMap<>();
map.put("responseCode", code);
map.put("responseDesc", msg);
map.put("responseTime", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
return map;
}
6.2 政府系统对接
某政务系统要求XML响应必须遵循特定的Schema:
xml复制<ns2:response xmlns:ns2="http://gov.example.com/schema">
<resultCode>1001</resultCode>
<resultDesc>操作失败</resultDesc>
</ns2:response>
实现方案:
java复制private String buildGovXmlResponse(int code, String msg) {
return String.format(
"<ns2:response xmlns:ns2=\"http://gov.example.com/schema\">" +
"<resultCode>%d</resultCode><resultDesc>%s</resultDesc></ns2:response>",
code, msg);
}
7. 性能考量与最佳实践
-
线程上下文管理
- 确保每次请求后清理ThreadLocal
- 考虑使用RequestContextFilter作为兜底
-
响应构建优化
- 对复杂XML使用JAXB预编译
- JSON序列化选用高性能库如Jackson
-
异常处理边界
- 只对业务异常做格式适配
- 系统级异常保持原始处理
-
监控与告警
- 记录各平台的异常发生情况
- 设置异常率阈值告警
java复制// 监控示例
@ExceptionHandler(Exception.class)
public Object handleException(Exception e, HttpServletResponse response) {
metrics.increment("exception.count");
return buildResponse(500, "System Error", response);
}
8. 扩展思考
8.1 前端适配方案
对于需要在前端展示不同格式错误信息的场景,可以:
- 在axios拦截器中统一处理响应
- 根据Content-Type选择解析方式
- 转换为标准格式供UI组件使用
javascript复制axios.interceptors.response.use(response => {
const contentType = response.headers['content-type'];
if(contentType.includes('xml')) {
return parseXml(response.data);
}
return response.data;
}, error => {
// 统一错误处理
});
8.2 协议升级策略
- 版本控制:通过header区分协议版本
- 灰度发布:逐步切换新格式
- 兼容模式:支持新旧格式并行
java复制String version = request.getHeader("X-API-Version");
if("2.0".equals(version)) {
return buildV2Response(code, msg);
}
8.3 安全增强措施
- 平台标识验证:防止伪造header
- 响应签名:确保数据完整性
- 敏感信息过滤:异常消息脱敏
java复制private void validatePlatformCode(String code) {
if(!validCodes.contains(code)) {
throw new SecurityException("Invalid platform code");
}
}
在实际项目中,这种灵活的多协议异常处理机制大大提升了系统的对接能力。一个典型的应用场景是我们需要同时对接三个不同的支付渠道,每个渠道都有自己独特的错误响应规范。通过这套方案,我们仅用两天时间就完成了所有对接工作,而传统方式可能需要为每个渠道单独开发异常处理逻辑。