SpringBoot全局异常处理:如何优雅应对多系统多格式错误响应需求
引言部分
在微服务架构中,你是否曾为处理不同外部系统的异常响应而头痛?
A系统要求返回JSON格式的:
{code: 1001, message: "错误信息"}
B系统却需要XML格式的:
2001
错误描述
而C系统又有其特殊的格式要求...随着接入系统的增加,异常处理代码逐渐变得臃肿不堪,维护成本直线上升。
这种情况在企业级应用中极为常见,尤其是在集成多个第三方系统或提供多渠道API服务时。传统的做法是为每个系统编写定制化的异常处理逻辑,导致代码重复、难以维护且容易出错。
本文将探讨如何利用SpringBoot的能力构建一套灵活的全局异常处理框架,通过一次配置实现多种格式的错误响应,让你的代码更加优雅、可维护。
背景知识
Spring异常处理机制概述
SpringBoot提供了多种异常处理机制,主要包括:
- @ControllerAdvice/@RestControllerAdvice - 全局异常处理
- @ExceptionHandler - 针对特定异常的处理器
- HandlerExceptionResolver接口 - 自定义异常解析器
这些机制允许开发者将异常处理逻辑与业务代码分离,实现统一管理。
多系统集成的异常处理挑战
当需要对接多个外部系统时,异常处理面临以下挑战:
- 响应格式多样化 - JSON、XML、自定义文本等
- 错误码体系不统一 - 不同系统使用不同的错误码规范
- 字段命名差异 - code/errorCode, message/errorMessage/msg等
- 内容协商机制 - 根据请求决定响应格式
问题分析
传统异常处理方案的局限性
常见的SpringBoot异常处理方案通常采用以下几种方式:
- 单一@RestControllerAdvice + @ExceptionHandler
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity handleException(Exception ex) {
ErrorResponse errorResponse = new ErrorResponse(1001, ex.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
}
}
这种方式简单直接,但无法满足多种响应格式的需求。
- 多个@RestControllerAdvice按包路径区分
@RestControllerAdvice("「包名称,请自行替换」.system1")
public class System1ExceptionHandler {
// System1的异常处理
}
@RestControllerAdvice("「包名称,请自行替换」.system2")
public class System2ExceptionHandler {
// System2的异常处理
}
这种方式需要为每个系统创建独立的控制器和异常处理器,导致代码重复且难以维护。
- 使用内容协商但格式固定
@RestControllerAdvice
public class ContentNegotiationExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity> handleException(Exception ex, HttpServletRequest request) {
MediaType mediaType = MediaType.APPLICATION_JSON;
// 根据Accept头决定响应格式
if (request.getHeader("Accept").contains("application/xml")) {
mediaType = MediaType.APPLICATION_XML;
}
ErrorResponse error = new ErrorResponse(1001, ex.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.contentType(mediaType)
.body(error);
}
}
这种方式虽然支持不同的响应格式,但错误信息的结构是固定的,无法满足不同系统对字段名称和结构的要求。
关键挑战的技术本质
实现多系统异常处理的核心挑战在于:
- 异常类型与响应格式的解耦
- 响应内容与媒体类型的动态适配
- 错误码与业务场景的映射管理
- 请求来源识别与响应策略选择
解决方案详解
我们将设计一个灵活的全局异常处理框架,能够根据请求来源和内容类型,动态选择异常处理策略,生成符合目标系统要求的错误响应。
整体架构
核心组件说明
- ErrorResponse接口 - 定义错误响应的行为
- 具体ErrorResponse实现 - 不同格式的错误响应
- ErrorResponseFactory接口 - 定义错误响应创建工厂
- 具体Factory实现 - 针对不同系统的工厂实现
- ErrorResponseResolver - 根据请求选择合适的工厂
- GlobalExceptionHandler - 全局异常处理入口
关键代码实现
首先,定义核心接口和基础实现:
// 错误响应接口
public interface ErrorResponse {
ResponseEntity> toResponseEntity();
}
// JSON格式错误响应
@Data
@AllArgsConstructor
public class JsonErrorResponse implements ErrorResponse {
private int code;
private String message;
private Map data;
@Override
public ResponseEntity> toResponseEntity() {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.contentType(MediaType.APPLICATION_JSON)
.body(this);
}
}
// XML格式错误响应
@Data
@AllArgsConstructor
@JacksonXmlRootElement(localName = "error")
public class XmlErrorResponse implements ErrorResponse {
@JacksonXmlProperty(localName = "code")
private int code;
@JacksonXmlProperty(localName = "desc")
private String desc;
@Override
public ResponseEntity> toResponseEntity() {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.contentType(MediaType.APPLICATION_XML)
.body(this);
}
}
// 自定义格式错误响应
@Data
@AllArgsConstructor
public class CustomErrorResponse implements ErrorResponse {
private String errorCode;
private String errorMessage;
@Override
public ResponseEntity> toResponseEntity() {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.contentType(MediaType.APPLICATION_JSON)
.body(this);
}
}
接下来,实现错误响应工厂接口及其实现:
// 错误响应工厂接口
public interface ErrorResponseFactory {
ErrorResponse createErrorResponse(Exception ex, HttpServletRequest request);
boolean supports(HttpServletRequest request);
}
// 系统A的错误响应工厂
@Component
public class SystemAErrorResponseFactory implements ErrorResponseFactory {
@Override
public ErrorResponse createErrorResponse(Exception ex, HttpServletRequest request) {
int code = 1001; // 默认错误码
// 针对不同异常设置不同错误码
if (ex instanceof IllegalArgumentException) {
code = 1002;
} else if (ex instanceof ResourceNotFoundException) {
code = 1003;
}
return new JsonErrorResponse(code, ex.getMessage(), Collections.emptyMap());
}
@Override
public boolean supports(HttpServletRequest request) {
String systemId = request.getHeader("X-System-ID");
return "SystemA".equals(systemId);
}
}
// 系统B的错误响应工厂
@Component
public class SystemBErrorResponseFactory implements ErrorResponseFactory {
@Override
public ErrorResponse createErrorResponse(Exception ex, HttpServletRequest request) {
int code = 2001; // 系统B的默认错误码
// 针对不同异常设置不同错误码
if (ex instanceof IllegalArgumentException) {
code = 2002;
} else if (ex instanceof ResourceNotFoundException) {
code = 2003;
}
return new XmlErrorResponse(code, ex.getMessage());
}
@Override
public boolean supports(HttpServletRequest request) {
String systemId = request.getHeader("X-System-ID");
return "SystemB".equals(systemId);
}
}
然后,实现错误响应解析器:
@Component
public class ErrorResponseResolver {
private final List factories;
// 构造函数注入所有工厂实现
public ErrorResponseResolver(List factories) {
this.factories = factories;
}
public ResponseEntity> resolve(Exception ex, HttpServletRequest request) {
// 查找支持当前请求的工厂
for (ErrorResponseFactory factory : factories) {
if (factory.supports(request)) {
ErrorResponse errorResponse = factory.createErrorResponse(ex, request);
return errorResponse.toResponseEntity();
}
}
// 默认错误响应
JsonErrorResponse defaultResponse = new JsonErrorResponse(
9999,
"Unknown error: " + ex.getMessage(),
Collections.emptyMap()
);
return defaultResponse.toResponseEntity();
}
}
最后,实现全局异常处理器:
@RestControllerAdvice
public class GlobalExceptionHandler {
private final ErrorResponseResolver resolver;
public GlobalExceptionHandler(ErrorResponseResolver resolver) {
this.resolver = resolver;
}
@ExceptionHandler(Exception.class)
public ResponseEntity> handleException(Exception ex, HttpServletRequest request) {
return resolver.resolve(ex, request);
}
// 可以针对特定异常定义专门的处理方法
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity> handleResourceNotFoundException(
ResourceNotFoundException ex,
HttpServletRequest request) {
return resolver.resolve(ex, request);
}
}
实践案例
下面通过一个完整的示例展示我们的解决方案如何工作。
项目结构
src/
├── main/
│ ├── java/
│ │ └── 「包名称,请自行替换」/
│ │ ├── config/
│ │ │ └── WebConfig.java
│ │ ├── controller/
│ │ │ └── DemoController.java
│ │ ├── exception/
│ │ │ ├── GlobalExceptionHandler.java
│ │ │ ├── ErrorResponse.java
│ │ │ ├── JsonErrorResponse.java
│ │ │ ├── XmlErrorResponse.java
│ │ │ ├── CustomErrorResponse.java
│ │ │ ├── ErrorResponseFactory.java
│ │ │ ├── SystemAErrorResponseFactory.java
│ │ │ ├── SystemBErrorResponseFactory.java
│ │ │ ├── SystemCErrorResponseFactory.java
│ │ │ └── ErrorResponseResolver.java
│ │ └── Application.java
│ └── resources/
│ └── application.yml
└── test/
└── java/
└── 「包名称,请自行替换」/
└── controller/
└── DemoControllerTest.java
完整代码实现
Maven依赖:
org.springframework.boot
spring-boot-starter-web
com.fasterxml.jackson.dataformat
jackson-dataformat-xml
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
Web配置类:
package 「包名称,请自行替换」.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
// 配置内容协商
configurer
.favorParameter(true)
.parameterName("format")
.ignoreAcceptHeader(false)
.useRegisteredExtensionsOnly(false)
.defaultContentType(MediaType.APPLICATION_JSON)
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("xml", MediaType.APPLICATION_XML);
}
}
异常类:
package 「包名称,请自行替换」.exception;
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
public ResourceNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
测试控制器:
package 「包名称,请自行替换」.controller;
import 「包名称,请自行替换」.exception.ResourceNotFoundException;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class DemoController {
@GetMapping("/test/{id}")
public String test(@PathVariable String id) {
if ("error".equals(id)) {
throw new IllegalArgumentException("Invalid ID provided");
} else if ("notfound".equals(id)) {
throw new ResourceNotFoundException("Resource not found with ID: " + id);
}
return "Success with ID: " + id;
}
}
自定义系统C错误响应工厂:
package 「包名称,请自行替换」.exception;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@Component
public class SystemCErrorResponseFactory implements ErrorResponseFactory {
@Override
public ErrorResponse createErrorResponse(Exception ex, HttpServletRequest request) {
String errorCode = "ERR-GEN-001";
if (ex instanceof IllegalArgumentException) {
errorCode = "ERR-VAL-001";
} else if (ex instanceof ResourceNotFoundException) {
errorCode = "ERR-RES-001";
}
return new CustomErrorResponse(errorCode, ex.getMessage());
}
@Override
public boolean supports(HttpServletRequest request) {
String systemId = request.getHeader("X-System-ID");
return "SystemC".equals(systemId);
}
}
测试案例
package 「包名称,请自行替换」.controller;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
public class DemoControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testSystemAJsonError() throws Exception {
MvcResult result = mockMvc.perform(get("/api/test/error")
.header("X-System-ID", "SystemA")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.code").value(1002))
.andExpect(jsonPath("$.message").value("Invalid ID provided"))
.andReturn(); // 这里获取返回结果
// 打印响应内容
System.out.println("Response Body: " + result.getResponse().getContentAsString());
}
@Test
public void testSystemBXmlError() throws Exception {
MvcResult result = mockMvc.perform(get("/api/test/notfound")
.header("X-System-ID", "SystemB")
.accept(MediaType.APPLICATION_XML))
.andExpect(status().isBadRequest())
.andExpect(content().contentType(MediaType.APPLICATION_XML))
.andExpect(xpath("/error/code").string("2003"))
.andExpect(xpath("/error/desc").string("Resource not found with ID: notfound"))
.andReturn(); // 这里获取返回结果
// 打印响应内容
System.out.println("Response Body: " + result.getResponse().getContentAsString());
}
@Test
public void testSystemCCustomError() throws Exception {
MvcResult result = mockMvc.perform(get("/api/test/error")
.header("X-System-ID", "SystemC")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.errorCode").value("ERR-VAL-001"))
.andExpect(jsonPath("$.errorMessage").value("Invalid ID provided"))
.andReturn(); // 这里获取返回结果
System.out.println("Response Body: " + result.getResponse().getContentAsString());
}
}
运行效果
当向系统A发送请求时,得到JSON格式的错误响应:
{
"code": 1002,
"message": "Invalid ID provided",
"data": {}
}
当向系统B发送请求时,得到XML格式的错误响应:
2003
Resource not found with ID: notfound
当向系统C发送请求时,得到自定义格式的错误响应:
{
"errorCode": "ERR-VAL-001",
"errorMessage": "Invalid ID provided"
}
进阶优化
异常-错误码映射管理
为避免在代码中硬编码错误码,可以采用配置化方式管理异常与错误码的映射关系:
@Component
public class ErrorCodeMappingManager {
// 系统A的错误码映射
private final Map<Class extends exception>, Integer> systemAMappings;
// 系统B的错误码映射
private final Map<Class extends exception>, Integer> systemBMappings;
// 系统C的错误码映射
private final Map<Class extends exception>, String> systemCMappings;
public ErrorCodeMappingManager() {
// 初始化系统A的错误码映射
systemAMappings = new HashMap<>();
systemAMappings.put(IllegalArgumentException.class, 1002);
systemAMappings.put(ResourceNotFoundException.class, 1003);
systemAMappings.put(Exception.class, 1001); // 默认错误码
// 初始化系统B的错误码映射
systemBMappings = new HashMap<>();
systemBMappings.put(IllegalArgumentException.class, 2002);
systemBMappings.put(ResourceNotFoundException.class, 2003);
systemBMappings.put(Exception.class, 2001); // 默认错误码
// 初始化系统C的错误码映射
systemCMappings = new HashMap<>();
systemCMappings.put(IllegalArgumentException.class, "ERR-VAL-001");
systemCMappings.put(ResourceNotFoundException.class, "ERR-RES-001");
systemCMappings.put(Exception.class, "ERR-GEN-001"); // 默认错误码
}
public Integer getSystemAErrorCode(Exception ex) {
return findMostSpecificErrorCode(ex, systemAMappings);
}
public Integer getSystemBErrorCode(Exception ex) {
return findMostSpecificErrorCode(ex, systemBMappings);
}
public String getSystemCErrorCode(Exception ex) {
return findMostSpecificErrorCode(ex, systemCMappings);
}
// 查找最匹配的错误码
private T findMostSpecificErrorCode(Exception ex, Map<Class extends exception>, T> mappings) {
Class> exClass = ex.getClass();
while (exClass != null) {
if (mappings.containsKey(exClass)) {
return mappings.get(exClass);
}
exClass = exClass.getSuperclass();
}
return mappings.get(Exception.class); // 返回默认错误码
}
}
请求来源识别策略优化
除了使用请求头,还可以通过其他方式识别请求来源:
@Component
public class RequestSourceIdentifier {
// 识别请求来源的方法集合
private final List<Function> identifierFunctions;
public RequestSourceIdentifier() {
identifierFunctions = new ArrayList<>();
// 从请求头识别
identifierFunctions.add(request -> request.getHeader("X-System-ID"));
// 从请求参数识别
identifierFunctions.add(request -> request.getParameter("systemId"));
// 从请求路径识别
identifierFunctions.add(request -> {
String path = request.getRequestURI();
if (path.startsWith("/api/system-a")) {
return "SystemA";
} else if (path.startsWith("/api/system-b")) {
return "SystemB";
} else if (path.startsWith("/api/system-c")) {
return "SystemC";
}
return null;
});
// 从客户端IP识别(示例)
identifierFunctions.add(request -> {
String ip = request.getRemoteAddr();
// 根据IP范围判断系统来源
// ...
return null;
});
}
public String identifySource(HttpServletRequest request) {
for (Function function : identifierFunctions) {
String source = function.apply(request);
if (source != null && !source.isEmpty()) {
return source;
}
}
return "default"; // 默认来源
}
}
基于注解的异常处理
可以通过自定义注解简化异常与错误码的关联:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ErrorInfo {
String systemA() default "";
String systemB() default "";
String systemC() default "";
}
@ErrorInfo(systemA = "1002", systemB = "2002", systemC = "ERR-VAL-001")
public class ValidationException extends RuntimeException {
// ...
}
配合自定义工厂实现:
@Component
public class AnnotationBasedErrorResponseFactory implements ErrorResponseFactory {
private final String systemId;
public AnnotationBasedErrorResponseFactory(String systemId) {
this.systemId = systemId;
}
@Override
public ErrorResponse createErrorResponse(Exception ex, HttpServletRequest request) {
ErrorInfo errorInfo = ex.getClass().getAnnotation(ErrorInfo.class);
String errorCode = "9999"; // 默认错误码
if (errorInfo != null) {
switch (systemId) {
case "SystemA":
errorCode = errorInfo.systemA();
break;
case "SystemB":
errorCode = errorInfo.systemB();
break;
case "SystemC":
errorCode = errorInfo.systemC();
break;
}
}
// 根据系统ID创建不同格式的错误响应
// ...
}
@Override
public boolean supports(HttpServletRequest request) {
String requestSystemId = request.getHeader("X-System-ID");
return systemId.equals(requestSystemId);
}
}
总结与展望
本文介绍了一种灵活的SpringBoot全局异常处理框架,通过工厂模式和策略模式,实现了针对不同系统返回不同格式错误响应的需求。主要特点包括:
- 高度解耦 - 异常处理逻辑与业务代码完全分离
- 可扩展性强 - 轻松添加新的响应格式和系统适配
- 配置灵活 - 支持多种请求来源识别策略
- 易于维护 - 错误码集中管理,避免硬编码
未来可以考虑的优化方向:
- 基于配置文件的错误码管理 - 将错误码映射关系移至配置文件,支持动态修改
- 国际化支持 - 增加多语言错误消息支持
- 错误日志与监控集成 - 与日志系统和监控系统集成,提高可观测性
- 自定义序列化支持 - 增加更多自定义格式的序列化支持
这种方案特别适用于以下场景:
- 多渠道API服务 - 同一API需要服务多个客户端,每个客户端要求不同的错误响应格式
- 微服务网关 - 作为不同微服务的统一入口,需要适配不同的错误处理机制
- 系统集成项目 - 需要与多个第三方系统集成,每个系统有自己的错误处理规范
通过本文提供的框架,您可以大幅减少异常处理的重复代码,提高系统的可维护性和扩展性,让错误处理不再成为系统集成的痛点。
注意:本文仅供学习参考。
更多文章一键直达: