Springboot 自动封装返回数据


前言

在 Springboot 中根本不需要做什么(只需要@RestController),只要返回值是一个对象,都会将这个对象转换成 json 字符串。而如果要对返回的对象进行封装,例如加一个 message 就需要写一个工具类并且每个方法都进行一次调用。

为了简化开发,可以利用 ControllerAdvice 来对返回的数据自动进行封装。

原理

  • 自动封装

    1. 首先根据需求手写数据封装类工具类
    2. 自定义注解,用来标记需要进行数据封装的接口
    3. 使用 ControllerAdvice 在数据返回给前端前进行封装
  • 判断是否需要封装

    只有在进入控制器前能获取到request请求即将进入的方法

    1. 获取控制器及方法,利用反射判断是否加了自定义注解
    2. 如果加了注解,向 request 的 attribute 中写入一条标记,表示需要封装
    3. 否则直接进入控制器
  • 一图流

封装类

这里只封装了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 数据

实际应用

  1. 控制器返回数据
@RestController
@RequestMapping("/api/user")
@ResponseResult // 自定义注解
public class UserController{

    @Autowired
    UserService userService;

    @PostMapping("/select/inf")
    public CustomerInfDTO selectUserInf(){
        // 直接返回从业务逻辑层返回的数据,不用进行封装
        CustomerInfDTO data = userService.selectUserInf (customerId);
        return  data;
    }
}
  1. 抛出错误异常返回数据
@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);
        }
    }
}

文章作者: ❤纱雾
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 ❤纱雾 !
评论
  目录