Spring Boot Starter 设计:实现统一返回与异常处理
1. 需求背景
在基于 Spring Boot 开发企业级应用时,为了减少基础工程搭建的步骤,确保 API 的一致性和健壮性至关重要。如果没有统一的返回和异常规范,开发人员往往需要编写大量重复的代码,即便通过复制粘贴,也会耗费时间和精力。API 的一致性和健壮性不仅能减少前后端的工作量,还能保证项目的规范性,提升系统的可维护性。 为此,我们设计了一个自定义的 Spring Boot Starter,专门用于 Web 工程的基础配置。本文将介绍该 Starter 的一部分功能,后续文章将继续介绍 Web 工程的其他部分功能。 主要功能 统一的返回格式:实现 ResponseBodyAdvice 接口,确保所有 API 响应都遵循一致的格式,便于前端解析和处理。 统一异常处理:通过 @RestControllerAdvice 集中处理各种异常,避免因未捕获的异常导致系统崩溃或返回不友好的错误信息。
2. 方案设计
2.1 实现 ResponseBodyAdvice 接口
ResponseBodyAdvice 实现类 + @RestControllerAdvice 注解, 相当于一个返回结果集的切面,supports 方法表示是否需要增强,beforeBodyWrite 表示增强方法。
2.2 @RestControllerAdvice + ExceptionHandler 实现统一返回
编写一个类,然后标注 @RestControllerAdvice 注解,当系统需要处理对应类型的错误,编写处理方法,方法上标注两个注解 @ExceptionHandler(value =异常类型),当出现对应类型的错误时,系统会调用该方法并执行相关逻辑,最终通过 @ResponseBody 注解返回 JSON 数据出去。
3. 实现步骤
3.1. 定义 RestfulResponse 返回实体类
统一格式包含以下字段:
- code:返回码
- message:消息
- data :数据载体
@Data
public class RestfulResponse implements Serializable {
private Integer code;
private String message;
private Object data;
private RestfulResponse(){}
public static RestfulResponse of(Object data){
RestfulResponse response = new RestfulResponse();
response.code = HttpConstants.SUCCESS_CODE;
response.message = HttpConstants.SUCCESSFUL_MESSAGE;
response.data = data;
return response;
}
public static RestfulResponse fail(Integer code,String message){
RestfulResponse response = new RestfulResponse();
response.code = code;
response.message = message;
return response;
}
public static RestfulResponse fail(ResponseBaseEnums responseBaseEnums) {
RestfulResponse response = new RestfulResponse();
response.code = responseBaseEnums.getCode();
response.message = responseBaseEnums.getMessage();
return response;
}
public static RestfulResponse fail(String message){
RestfulResponse response = new RestfulResponse();
response.code = HttpConstants.ERROR_CODE;
response.message = message;
return response;
}
public static RestfulResponse fail(){
RestfulResponse response = new RestfulResponse();
response.code = HttpConstants.ERROR_CODE;
response.message = HttpConstants.FAILED_MESSAGE;
return response;
}
}
3.2 编写需要处理结果的注解
@JsonResponse 注解继承 RestController, 所以到时候需要统一格式返回在类上加@JsonResponse 即可, 同时不需要的话,在类上或者方法上 ignore 设置为 true 即可
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@RestController
public @interface JsonResponse {
/**
* 是否忽略
* @return
*/
boolean ignore() default false;
}
3.3. 编写返回结果处理器
@ControllerAdvice
public class JsonResponseHandler implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> converterType) {
Method method = methodParameter.getMethod();
Class<?> clazz = Objects.requireNonNull(method, "method is null").getDeclaringClass();
// 看看类上有没有JsonResponse
JsonResponse annotation = clazz.getAnnotation(JsonResponse.class);
// 看看方法上是否有注解
JsonResponse methodAnnotation = method.getAnnotation(JsonResponse.class);
if (methodAnnotation != null) {
annotation = methodAnnotation;
}
if (method.getAnnotatedReturnType().getType().getTypeName().equalsIgnoreCase(FileSystemResource.class.getName())) {
return false;
}
return annotation != null && !annotation.ignore();
}
@Override
@SneakyThrows
public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aclazz, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof String && !MediaType.APPLICATION_XML_VALUE.equals(mediaType.toString())) {
ObjectMapper om = new ObjectMapper();
response.getHeaders().set("Content-Type", "application/json");
RestfulResponse restfulResponse = RestfulResponse.of(body);
return om.writeValueAsString(restfulResponse);
}
if (Objects.isNull(body) && MediaType.TEXT_HTML_VALUE.equals(mediaType.toString())) {
ObjectMapper om = new ObjectMapper();
response.getHeaders().set("Content-Type", "application/json");
RestfulResponse restfulResponse = RestfulResponse.of(null);
return om.writeValueAsString(restfulResponse);
}
return RestfulResponse.of(body);
}
}
3.4. 定义全局异常处理器
使用 @RestControllerAdvice 来集中处理全局异常,确保所有未被捕获的异常都能被优雅地处理。
@RestControllerAdvice
public class ExceptionAdvanceHandler {
@ExceptionHandler(value = NullPointerException.class)
public RestfulResponse nullPointerException(NullPointerException e) {
return RestfulResponse.fail(OpenCode.NULL_POINT_ERROR.getCode(),e.getMessage());
}
@ExceptionHandler(value = IllegalArgumentException.class)
public RestfulResponse handleIllegalArgumentException(IllegalArgumentException e) {
return RestfulResponse.fail(OpenCode.PARAMS_ERROR.getCode(),e.getMessage());
}
@ExceptionHandler(value = NoHandlerFoundException.class)
public RestfulResponse noHandlerFoundException(NoHandlerFoundException e) {
return RestfulResponse.fail(OpenCode.NO_HANDLER_EXCEPTION.getCode(),e.getMessage());
}
@ExceptionHandler(value = BizException.class)
public RestfulResponse baseException(BizException e) {
return RestfulResponse.fail(e.getCode(),e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public RestfulResponse bindException(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
Set<String> errSet = bindingResult.getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.toSet());
String errorMessage = String.join(", ", errSet);
return RestfulResponse.fail(OpenCode.PARAMS_VALID_ERROR.getCode(),errorMessage);
}
/**
* 参数绑定错误
*
* @param ex
* @return
*/
@ExceptionHandler(value = BindException.class)
public RestfulResponse exception(BindException ex) {
String defaultMessage = Objects.requireNonNull(ex.getBindingResult().getFieldError()).getDefaultMessage();
return RestfulResponse.fail(defaultMessage);
}
/**
* HttpRequestMethodNotSupportedException
* @param e
* @return
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public RestfulResponse httpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
return RestfulResponse.fail(OpenCode.METHOD_NOT_SUPPORT.getCode(),e.getMessage());
}
@ExceptionHandler(value = Exception.class)
public RestfulResponse exception(Exception e) {
return RestfulResponse.fail(OpenCode.SERVER_ERROR.getCode(),e.getMessage());
}
}
3.5. 配置自动装配
3.5.1. 自动装配类
```java
@Configuration
@Import(value = {ExceptionAdvanceHandler.class, JsonResponseHandler.class})
public class WebConfig {
}
3.5.2. resources 下 META-INF 创建 spring.factories,写以下内容
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.ssn.kit.interceptor.WebConfig
4. 组件测试
4.1 引入 starter
<dependency>
<groupId>com.ssn.kit</groupId>
<artifactId>corp-kit-web</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
4.2 编写测试 controller
@JsonResponse
@RequestMapping("test")
public class TestController {
@GetMapping("string")
public String test() {
return "test";
}
@GetMapping("obj")
public UserInfo userInfo() {
UserInfo info = new UserInfo();
info.setId("1");
info.setName("test");
return info;
}
@GetMapping("obj2")
@JsonResponse(ignore = true)
public UserInfo userInfo2() {
UserInfo info = new UserInfo();
info.setId("1");
info.setName("test");
return info;
}
@GetMapping("obj3")
@JsonResponse(ignore = true)
public UserInfo userInfo3() {
UserInfo info = null;
if (info == null) {
throw new BizException(BizCode.USER_NOT_FOUND);
}
return info;
}
}
api 调试
- 测试接口返回 string
http://localhost:8090/test/string
{ "code": 0, "message": "SUCCESS", "data": "test" }
- 测试返回对象
http://localhost:8090/test/obj
{ "code": 0, "message": "SUCCESS", "data": { "id": "1", "name": "test" } }
- 不包装返回
http://localhost:8090/test/obj2
{ "id": "1", "name": "test" }
- 异常捕获
http://localhost:8090/test/obj3
{ "code": 40000, "message": "找不到用户信息", "data": null }
5. 结论
通过上述实现,我们可以在 Spring Boot 应用中轻松实现统一返回格式和统一异常处理。这不仅提高了 API 的一致性和健壮性,还简化了开发和维护工作.