SpringBoot - 统一格式封装及高阶全局异常处理
Posted 小小工匠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot - 统一格式封装及高阶全局异常处理相关的知识,希望对你有一定的参考价值。
文章目录
Pre
Spring Boot2.x-11 使用@ControllerAdvice和@ExceptionHandler实现自定义全局异常
演进过程
我们搞个boot工程 ,来看下为什么以及如何来实现统一格式封装及高阶全局异常处理
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>
</dependencies>
版本V1
@RestController // 返回JSON
@RequestMapping("/v1")
public class ArtisanV1Controller
/**
* 返回字符串
*
* @return
*/
@GetMapping("/getString")
public String getStr()
return "OOOOOOOK";
/**
* 返回自定义对象
*
* @return
*/
@GetMapping("/getArtisan")
public Artisan getArt()
Artisan artisan = new Artisan();
artisan.setJob("ArtisanJob");
artisan.setAge(18);
return artisan;
/**
* 接口异常
*
* @return
*/
@GetMapping("/getMockError")
public int getMockError()
int i = 1 / 0;
return i;
分别测试下
这混乱的格式, 前端同学怎么想
版本2
Step1 约定统一返回格式
一个合格的标准的返回格式至少包含3部分:
-
status 状态值
由后端统一定义各种返回结果的状态码
-
message 描述
本次接口调用的结果描述
-
data 数据
本次接口返回的数据
如果需要可以加入其他节点,比如在返回对象中添加了接口调用时间 (timestamp: 接口调用时间)
Step2 开发统一返回对象
package com.artisan.resp;
import lombok.Data;
/**
* @author 小工匠
* @version 1.0
* @description: 公共结果
* @mark: show me the code , change the world
*/
@Data
public class ResponseData<T>
/**
* 结果状态 ,具体状态码参见ResponseCode
*/
private int status;
/**
* 响应消息
**/
private String message;
/**
* 响应数据
**/
private T data;
/**
* 接口请求时间
**/
private long timestamp;
/**
* 初始化,增加接口请求事件
*/
public ResponseData()
this.timestamp = System.currentTimeMillis();
/**
* 成功
*
* @param <T>
* @return
*/
public static <T> ResponseData<T> success()
ResponseData<T> resultData = new ResponseData<>();
resultData.setStatus(ResponseCode.RC100.getCode());
resultData.setMessage(ResponseCode.RC100.getMessage());
return resultData;
/**
* 成功
*
* @param message
* @param <T>
* @return
*/
public static <T> ResponseData<T> success(String message)
ResponseData<T> resultData = new ResponseData<>();
resultData.setStatus(ResponseCode.RC100.getCode());
resultData.setMessage(message);
return resultData;
/**
* 成功
*
* @param data
* @param <T>
* @return
*/
public static <T> ResponseData<T> success(T data)
ResponseData<T> resultData = new ResponseData<>();
resultData.setStatus(ResponseCode.RC100.getCode());
resultData.setMessage(ResponseCode.RC100.getMessage());
resultData.setData(data);
return resultData;
/**
* 失败
*
* @param message
* @param <T>
* @return
*/
public static <T> ResponseData<T> fail(String message)
ResponseData<T> resultData = new ResponseData<>();
resultData.setStatus(ResponseCode.RC999.getCode());
resultData.setMessage(message);
return resultData;
/**
* 失败
*
* @param code
* @param message
* @param <T>
* @return
*/
public static <T> ResponseData<T> fail(int code, String message)
ResponseData<T> resultData = new ResponseData<>();
resultData.setStatus(code);
resultData.setMessage(message);
return resultData;
/**
* 失败
*
* @param <T>
* @return
*/
public static <T> ResponseData<T> fail()
ResponseData<T> resultData = new ResponseData<>();
resultData.setStatus(ResponseCode.RC999.getCode());
resultData.setMessage(ResponseCode.RC999.getMessage());
return resultData;
Step3 约定接口状态码
package com.artisan.resp;
import lombok.Getter;
/**
* @author 小工匠
* @version 1.0
* @description: 状态码集合
* @mark: show me the code , change the world
*/
public enum ResponseCode
/**
* 操作成功
**/
RC100(100, "操作成功"),
/**
* 操作失败
**/
RC999(999, "操作失败"),
/**
* access_denied
**/
RC403(403, "无访问权限,请联系管理员授予权限"),
/**
* access_denied
**/
RC401(401, "匿名用户访问无权限资源时的异常"),
/**
* 服务异常
**/
RC500(500, "系统异常,请稍后重试"),
ILLEGAL_ARGUMENT(3001, "非法参数"),
INVALID_TOKEN(2001, "访问令牌不合法"),
ACCESS_DENIED(2003, "没有权限访问该资源"),
CLIENT_AUTHENTICATION_FAILED(1001, "客户端认证失败"),
USERNAME_OR_PASSWORD_NOTMATCH(1002, "用户名或密码错误");
/**
* 自定义状态码
**/
@Getter
private final int code;
/**
* 自定义描述
**/
@Getter
private final String message;
ResponseCode(int code, String message)
this.code = code;
this.message = message;
Step4 验证
package com.artisan.controller;
import com.artisan.entity.Artisan;
import com.artisan.resp.ResponseData;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author 小工匠
* @version 1.0
* @description: 版本2
* @mark: show me the code , change the world
*/
@RestController
@RequestMapping("/v2")
public class ArtisanV2Controller
@GetMapping("/getString")
public ResponseData<String> getStr()
return ResponseData.success("OOOOOOK");
@GetMapping("/getArtisan")
public ResponseData<Artisan> getArt()
Artisan artisan = new Artisan();
artisan.setJob("CodeMonkey");
artisan.setAge(18);
return ResponseData.success(artisan);
@GetMapping("/getMockError")
public ResponseData<Integer> getMockError()
int i = 1 / 0;
return ResponseData.success(i);
好像部分实现了统一格式返回,确实也是有很多项目在Controller层通过ResponseData.success()对返回结果进行包装后返回给前端。
但是这个抛异常的这么玩还是不行呀? ------------------------> 全局异常处理
Step5 完善全局异常处理 @RestControllerAdvice + @ExceptionHandler
package com.artisan.resp;
import com.artisan.exception.BaseException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
import java.util.stream.Collectors;
/**
* @author 小工匠
* @version 1.0
* @description: 全局异常处理
* @mark: show me the code , change the world
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler
/**
* 默认全局异常处理。
*
* @param e e
* @return ResponseData
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseData<String> exception(Exception e)
log.error("兜底异常信息 ex=", e.getMessage());
return ResponseData.fail(ResponseCode.RC500.getCode(), e.getMessage());
/**
* Assert异常
*/
@ExceptionHandler(IllegalArgumentException.class, IllegalStateException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseData<String> exception(IllegalArgumentException e)
return ResponseData.fail(ResponseCode.ILLEGAL_ARGUMENT.getCode(), e.getMessage());
/**
* 抓取自定义异常 BaseException
*/
@ExceptionHandler(BaseException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseData<String> exception(BaseException e)
return ResponseData.fail(e.getErrorCode(), e.getMessage());
-
@ExceptionHandler,统一处理某一类异常, 减少代码重复率和复杂度,比如要捕获自定义异常可以@ExceptionHandler(BusinessException.class)
-
@ResponseStatus指定客户端收到的http状态码
重新验证下
全局异常处理器的必要行
- 避免try…catch,由全局异常处理器统一捕获
- 自定义异常,只能通过全局异常处理器来处理
- Validator参数校验器的时候,参数校验不通过会抛出异常,无法用try…catch捕获,只能使用全局异常处理器。
- …
。
版本3 (ResponseBodyAdvice)
V2版本有缺陷么?
我们不难发现每写一个接口都需要调用ResponseData.success()对结果进行包装 ,程序猿懒啊, 能不写吗
Step1 自定义ResponseBodyAdvice接口实现类
ResponseBodyAdvice的作用一般是用于拦截Controller方法的返回值,统一处理返回值/响应体, 加解密,签名等
package com.artisan.resp.v3;
import com.artisan.resp.ResponseData;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
* @author 小工匠
* @version 1.0
* @description: 拦截Controller方法的返回值,统一处理返回值/响应体
* @mark: show me the code , change the world
*/
@RestControllerAdvice
public class CustomResponseAdvice implements ResponseBodyAdvice<Object>
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass)
return true;
@SneakyThrows
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse)
// 处理String类型
if (o instanceof String)
return objectMapper.writeValueAsString(ResponseData.success(o));
// 若是统一返回类型,则不用再此封装
// if (o instanceof ResponseData)
// return o;
//
return ResponseData.success(o);
-
@RestControllerAdvice,RestController的增强类,可用于实现全局异常处理器
-
有一个地方对String做了特殊处理,因为如果Controller直接返回String ,SpringBoot是直接返回,所以我们需要手动转换成json
接入@RestControllerAdvice后, Controller就正常写就可以了,不用统一格式去包装了,如下
@RestCont以上是关于SpringBoot - 统一格式封装及高阶全局异常处理的主要内容,如果未能解决你的问题,请参考以下文章