前言
在 Springboot 中根本不需要做什么(只需要@RestController),只要返回值是一个对象,都会将这个对象转换成 json 字符串。而如果要对返回的对象进行封装,例如加一个 message 就需要写一个工具类并且每个方法都进行一次调用。
为了简化开发,可以利用 ControllerAdvice 来对返回的数据自动进行封装。
原理
自动封装
- 首先根据需求手写数据封装类及工具类
- 自定义注解,用来标记需要进行数据封装的接口
- 使用 ControllerAdvice 在数据返回给前端前进行封装
判断是否需要封装
只有在进入控制器前能获取到request请求即将进入的方法
- 获取控制器及方法,利用反射判断是否加了自定义注解
- 如果加了注解,向 request 的 attribute 中写入一条标记,表示需要封装
- 否则直接进入控制器
一图流
封装类
这里只封装了3条数据:错误代码,错误信息,以及要返回的数据
@Data // lombok
public class ResultVO {
private Object data;
private Integer code;
private String message;
}
工具类
public class CommonUtil {
public static ResultVO ajaxReturn(Object data, Integer code, String message) throws JsonProcessingException {
return new ResultVO(data, code, message);
}
// 方法重载
public static ResultVO ajaxReturn(Object data) {
return new ResultVO(data, 200, "成功");
}
// 方法重载
public static ResultVO ajaxReturn(Integer code,String message){
return new ResultVO(null, code, message);
}
// 格式化date对象 返回格式: '2021-05-09 12:30:59'
public static String getTime(Date time) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return simpleDateFormat.format(time);
}
}
自定义注解
通过自定义的注解标记需要封装的接口
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 可以标记类,也可以标记方法
@Target({ElementType.TYPE, ElementType.METHOD})
// 类名当然是任意取的
public @interface ResponseResult {
}
ControllerAdvice
@Slf4j // lombok
@ControllerAdvice
public class ResponseResultHandler implements ResponseBodyAdvice<Object> {
// 自定义的标记字段
public static final String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";
/**
* 从request中的attribute判断是否被标记
* 标记的过程通过拦截器实现
* supports方法的返回值将决定是否执行下面的beforeBodyWrite方法
*/
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
ServletRequestAttributes sra = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
HttpServletRequest request = sra.getRequest();
ResponseResult responseResult = (ResponseResult) request.getAttribute(RESPONSE_RESULT_ANN);
return responseResult != null;
}
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if (o instanceof ResultVO) {
// 如果返回的数据已经被封装,直接返回
return o;
} else if (o instanceof LinkedHashMap) {
// 当服务器内部错误(500)时,springboot返回的数据是一个 LinkedHashMap
// 需要从中拿出错误信息进行封装
LinkedHashMap r = (LinkedHashMap) o;
if (r.containsKey("status") && r.containsKey("error")) {
log.error(r.toString());
HttpServletResponse httpServletResponse = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getResponse();
httpServletResponse.setStatus(500);
return CommonUtils.ajaxReturn((int)r.get("status"), r.get("error").toString());
}
}
// 其余常规的数据返回,直接调用工具类封装
return CommonUtils.ajaxReturn(o);
}
}
拦截器
@Slf4j //lombok
@Component
public class ResponseResultInterceptor implements HandlerInterceptor {
// 自定义的标记字段
public static final String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";
/**
* preHandle,将会在request请求进入控制器前执行
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
final HandlerMethod handlerMethod = (HandlerMethod) handler;
// 获取将要进入的控制器以及具体方法
final Class<?> clazz = handlerMethod.getBeanType();
final Method method = handlerMethod.getMethod();
// 如果控制器被标记或方法被标记,写入attribute
if (clazz.isAnnotationPresent(ResponseResult.class)) {
request.setAttribute(RESPONSE_RESULT_ANN, clazz.getAnnotation(ResponseResult.class));
} else if (method.isAnnotationPresent(ResponseResult.class)) {
request.setAttribute(RESPONSE_RESULT_ANN, method.getAnnotation(ResponseResult.class));
}
}
return true;
}
}
配置类
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
ResponseResultInterceptor responseResultInterceptor;
/**
* 关闭默认的消息转换器
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.removeIf(httpMessageConverter -> httpMessageConverter.getClass() == StringHttpMessageConverter.class);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 使用自定义注解返回数据
registry.addInterceptor(responseResultInterceptor);
}
}
异常枚举
lombok 不能为枚举类添加方法
public enum Error {
/**
* 根据业务需求添加错误信息
*/
SUCCESS(200, "成功"),
NO_LOGIN(400, "无此权限"),
PRAM_ERROR(400, "参数异常"),
FILE_UPLOAD_FAILED(400, "文件上传失败"),
UNKNOWN_ERROR(500, "未知错误,请联系管理员");
// 下面是枚举类的基本属性与方法
private int code;
private String msg;
Error(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
public void setCode(int code) {
this.code = code;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
自定义异常及捕获
自定义异常
@Getter
@Setter
public class CommonException extends RuntimeException {
private Integer code;
public CommonException(Integer code, String msg) {
super(msg);
this.code = code;
}
// JDK自带Error类,导包时需要注意选择自己的枚举类
public CommonException(Error err) {
super(err.getMsg());
this.code = err.getCode();
}
}
自定义异常捕获
@ControllerAdvice
public class CommonExceptionHandler {
@ExceptionHandler(value = CommonException.class)
// 用来转换为json数据
@ResponseBody
public ResultVO error(CommonException e) {
return CommonUtil.ajaxReturn(e.getCode(), e.getMessage());
}
}
前端传入参数异常捕获
@Slf4j
@ControllerAdvice
public class HttpMessageNotReadableExceptionHandler {
@ExceptionHandler(value = HttpMessageNotReadableException.class)
@ResponseBody
public ResultVO error(HttpMessageNotReadableException e) {
log.error("前端传入数据异常");
return CommonUtil.ajaxReturn(400, "参数异常");
}
}
有了自定义异常及捕获,可以在业务逻辑中直接抛出枚举类中相应的异常信息,同时也会被转换为 json 数据
实际应用
- 控制器返回数据
@RestController
@RequestMapping("/api/user")
@ResponseResult // 自定义注解
public class UserController{
@Autowired
UserService userService;
@PostMapping("/select/inf")
public CustomerInfDTO selectUserInf(){
// 直接返回从业务逻辑层返回的数据,不用进行封装
CustomerInfDTO data = userService.selectUserInf (customerId);
return data;
}
}
- 抛出错误异常返回数据
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
OrderCartMapper cartMapper;
@Override
public void addToOrderCart(OrderCart data) {
int result;
try {
// 好吧顶级的处理异常比实际业务代码多
result = cartMapper.addToOrderCart(data);
} catch (DataIntegrityViolationException e) {
String cause = e.getCause().toString();
// 外键约束
if (cause.contains(FOREIGN_KEY)) {
if (cause.contains(CUSTOMER_ID)) {
// 这情况确实不太可能,刚加购物车就封号?说不定呢
throw new CommonException(NO_USER);
} else if (cause.contains(PRODUCT_ID)) {
// 这还是必须有的,刚准备加购物车,商家下架商品了
throw new CommonException(NO_PRODUCT);
}
}
// 其他的暂时想不到了,等遇到了再说
log.error(e.getMessage());
throw new CommonException(UNKNOWN_ERROR);
} catch (Exception e) {
log.error(e.getMessage());
throw new CommonException(UNKNOWN_ERROR);
}
if (0 == result) {
// 说不定数据库抽风了
throw new CommonException(FAILED_TO_ADD_TO_CART);
}
}
}