SpringBoot统一封装controller层返回的结果

一 前言

目前前后端分离的项目中,我们在controller层会统一格式封装结果给前端。如果我们在每个方法中手动封装Result,无疑是增加了额外的工作量。
手动封装Result
那么有没有一种方式我们只返回它相应的数据。对于结果可以自动帮我们封装,且还可以对某个方法或者某个类下所有方法封装或者不封装。
答案是肯定的。利用@RestControllerAdviceResponseBodyAdviceResult返回对象统一拦截处理。

二 @RestControllerAdvice注解和 ResponseBodyAdvice接口说明

@RestControllerAdvice:是一个组合注解,包含@ControllerAdvice@ResponseBody

  • @ControllerAdvice 捕获controller层中的方法做进一步加强。@ControllerAdvice三种使用场景
  • @ResponseBody 将controller的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到response对象的body区,通常用来返回JSON数据或者是XML
    @responseBody的使用

ResponseBodyAdvice :允许在@ResponseBody或ResponseEntity控制器方法执行之后,但在使用HttpMessageConverter编写body之前定制响应。

简单理解:ResponseBodyAdvice接口是在controller层方法执行之后,在response返回给前端数据之前对reponse的数据进行处理,可以对数据进行统一的处理,从而可以使返回数据格式一致。

三 具体实现

源码地址

3.1 统一返回数据格式代码

3.1.1 ResponseResult

@RestControllerAdvice
public class ResponseResult implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        if (returnType.getDeclaringClass().isAnnotationPresent(ResponseNotIntercept.class)) {
            //若在类中加了@ResponseNotIntercept 则该类中的方法不用做统一的拦截
            return false;
        }
        if (returnType.getMethod().isAnnotationPresent(ResponseNotIntercept.class)) {
            //若方法上加了@ResponseNotIntercept 则该方法不用做统一的拦截
            return false;
        }
        return true;
    }
    
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof Result) {
            // 提供一定的灵活度,如果body已经被包装了,就不进行包装
            return body;
        }
        if (body instanceof String) {
            //解决返回值为字符串时,不能正常包装
            return JSON.toJSONString(Result.success(body));
        }
        return Result.success(body);
    }
}

说明: 实现ResponseBodyAdvice接口 需要重写supports,beforeBodyWrite方法;
supports: 是否支持给定的控制器方法返回类型和选定的HttpMessageConverter类型;若不支持则就不会对数据进行做统一处理,就像上面代码,若加了@ResponseNotIntercept注解,则不会进行拦截(@ResponseNotIntercept是自己自定义的一个注解)

  • 参数:
    returnType:返回类型;
    converterType:选择的转换器类型
  • 返回:若返回结果为true,则调用beforeBodyWrite方法

beforeBodyWrite: 在选择HttpMessageConverter之后以及在调用其write方法之前调用。

  • 参数:
    body:你传入的数据;
    returnType:controller层方法返回的类型;
    selectedContentType :通过内容协商选择的内容类型;
    selectedConverterType:选择要写入响应的转换器类型;
    request/reponse:当前请求和响应;
  • 返回:传入的数据或修改的(可能是新的)实例。

3.1.2 ResponseNotIntercept

自定义不被拦截注解,灵活控制对某个方法不封装Result

/**
 * 返回放行注解
 * 在类和方法上使用此注解表示不会在ResponseResult类中进一步封装返回值,直接返回原生值
 *
 * @author xlwang55
 */
@Target({ElementType.METHOD, ElementType.TYPE})  //可以在字段、方法
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseNotIntercept {
    String value() default "";
}

3.1.3 CommonExceptionHandler

统一异常拦截,系统中出现的异常一致会往上抛 最终抛到controller层,被CommonExceptionHandler拦截,此类中针对不同的异常做了不同的返回消息体的处理,如果是未识别的异常统一封装为系统异常。使用原理可以学习下面连接
SpringBoot项目异常的统一处理


import com.wxl52d41.exception.BusinessException;
import com.wxl52d41.exception.ForbiddenException;
import com.wxl52d41.result.Result;
import com.wxl52d41.result.ResultEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

import javax.servlet.ServletException;
import javax.validation.ConstraintViolationException;
import java.util.stream.Collectors;

/**
 * 统一拦截异常
 *
 * @author xlwang
 */
@RestControllerAdvice
@Slf4j
public class CommonExceptionHandler {

    /**
     * 捕获 自定 异常
     */
    @ExceptionHandler({BusinessException.class})
    public Result<?> handleBusinessException(BusinessException ex) {
        log.error(ex.getMessage(), ex);
        return Result.failed(ex.getCode(),ex.getMessage());
    }

    /**
     * 参数缺失异常
     * 说明:参数为必填时,若入参中无此参数则会报MissingServletRequestParameterException
     */
    @ExceptionHandler({MissingServletRequestParameterException.class})
    public Result<?> handleMissingServletRequestParameterException(MissingServletRequestParameterException ex) {
        log.error(ex.getMessage(), ex);
        return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), ex.getMessage());
    }

    /**
     * 参数值校验异常
     * {@code @PathVariable} 和 {@code @RequestParam} 参数值校验不通过时抛出的异常处理
     */
    @ExceptionHandler({ConstraintViolationException.class})
    public Result<?> handleConstraintViolationException(ConstraintViolationException ex) {
        log.error(ex.getMessage(), ex);
        return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), ex.getMessage());
    }

    /**
     * 参数值类型异常
     * 说明: 定义Integer类型,输入的为String,会出现 MethodArgumentTypeMismatchException异常
     */
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public Result<?> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException ex) {
        log.error(ex.getMessage(), ex);
        String message = "参数:" + ex.getName() + " 类型错误";
        return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), message);
    }

    /**
     * {@code @RequestBody} 参数校验不通过时抛出的异常处理
     */
    @ExceptionHandler({MethodArgumentNotValidException.class})
    public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        log.error(ex.getMessage(), ex);
        String msg = ex.getBindingResult().getFieldErrors().stream()
                .map(f -> f == null ? "null" : f.getField() + ": " + f.getDefaultMessage())
                .collect(Collectors.joining(", "));
        return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), msg);
    }

    @ExceptionHandler(BindException.class)
    public Result<?> handleBindException(BindException ex) {
        log.error(ex.getMessage(), ex);
        return Result.failed(HttpStatus.BAD_REQUEST.value(),
                ex.getAllErrors().stream()
                        .map(ObjectError::getDefaultMessage)
                        .collect(Collectors.joining("; "))
        );
    }

    /**
     * 捕获 {@code ForbiddenException} 异常
     */
    @ExceptionHandler({ForbiddenException.class})
    public Result<?> handleForbiddenException(ForbiddenException ex) {
        log.error(ex.getMessage(), ex);
        return Result.failed(ResultEnum.FORBIDDEN);
    }


    /**
     * 顶级异常捕获并统一处理,当其他异常无法处理时候选择使用
     */
    @ExceptionHandler({Exception.class})
    public Result<?> handle(Exception ex) {
        log.error(ex.getMessage(), ex);
        return Result.failed(ResultEnum.COMMON_FAILED);
    }

    /**
     * 处理已知的系统异常
     */
    @ExceptionHandler({ServletException.class})
    public Result<?> handle1(Exception ex) {
        log.error(ex.getMessage(), ex);
        return Result.failed(ex.getMessage());
    }

}

3.2 统一返回对象Result

3.2 .1Result

/**
 * 统一返回数据结构
 * @author xlwang
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
    private Integer status;
    private String message;
    private T data;

    public static <T> Result<T> success(T data) {
        return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data);
    }

    public static <T> Result<T> success(String message, T data) {
        return new Result<>(ResultEnum.SUCCESS.getCode(), message, data);
    }

    public static Result<?> failed() {
        return new Result<>(ResultEnum.COMMON_FAILED.getCode(), ResultEnum.COMMON_FAILED.getMessage(), null);
    }

    public static Result<?> failed(String message) {
        return new Result<>(ResultEnum.COMMON_FAILED.getCode(), message, null);
    }

    public static Result<?> failed(IResult errorResult) {
        return new Result<>(errorResult.getCode(), errorResult.getMessage(), null);
    }

    public static Result<?> failed(Integer code, String message) {
        return new Result<>(code, message, null);
    }

    public static <T> Result<T> instance(Integer code, String message, T data) {
        Result<T> result = new Result<>();
        result.setStatus(code);
        result.setMessage(message);
        result.setData(data);
        return result;
    }
}

3.2 .2 IResult接口

**
 * 定义返回数据结构
 * @author xlwang
 */
public interface IResult {
    /**
     * 获取状态码
     *
     * @return 状态码
     */
    Integer getCode();

    /**
     * 获取消息体
     *
     * @return 消息体
     */
    String getMessage();
}

3.2 .3ResultEnum枚举

/**
 * 常用结果的枚举
 */
public enum ResultEnum implements IResult {
    SUCCESS(200, "成功"),
    VALIDATE_FAILED(400, "参数错误"),
    COMMON_FAILED(500, "系统错误"),
    FORBIDDEN(2004, "没有权限访问资源");

    private Integer code;
    private String message;

    //省略get、set方法和构造方法

    @Override
    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    @Override
    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    ResultEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

}

3.3 controller层方法测试

/**
 * 用来测试统一返回的功能
 */
@RestController
@RequestMapping("/CommentRest")
public class CommentRestController {

    /**
     *  数值返回值测试,是否能正常封装返回体
     */
    @GetMapping("getId")
    public Integer getId() {
        return 1;
    }
    /**
     * 对象返回值测试,是否能正常封装返回体
     */
    @GetMapping("getOne")
    public TestVO getOne() {
        TestVO testVO = new TestVO("1","测试标题","无内容","小明");
        return testVO;
    }
    /**
     * 字符串返回值测试
     */
    @DeleteMapping("delete")
    public String delete() {
        return "删除成功";
    }

    /**
     * 无返回值测试
     */
    @PutMapping("save")
    @ResponseNotIntercept
    public void save() {
        System.out.println("无返回值 = ");
    }
}

3.3.1 getId() 数值返回值测试

在这里插入图片描述

3.3.2 getOne()对象返回值测试结果

在这里插入图片描述

3.3.2 save()无返回值测试结果

由于添加了自定义的@ResponseNotIntercept注解,测试时不会封装返回值
无返回值测试结果
我们将注解注释后再测试一次
在这里插入图片描述

3.3.2 delete()字符串返回值测试结果(注意此处为重点)

上述测试都很顺利,当返回的值是字符串时发现报错了,并不是预期的字符串。
在这里插入图片描述
控制台报错信息

2022-11-01 19:44:26.962 ERROR 36416 --- [nio-9098-exec-2] c.w.advice.ControllerExceptionHandler    : com.wxl52d41.result.Result cannot be cast to java.lang.String

java.lang.ClassCastException: com.wxl52d41.result.Result cannot be cast to java.lang.String
	at org.springframework.http.converter.StringHttpMessageConverter.addDefaultHeaders(StringHttpMessageConverter.java:44) ~[spring-web-5.3.22.jar:5.3.22]

通过错误日志分析说,Result 这个类不能够转换成String。我明明返回的是字符串怎么就是报这个错了呢,其他怎么就没有问题。一顿百度,最终debug发现了端倪。
SpringBoot统一返回处理出现cannot be cast to java.lang.String异常
解决方案添加如下代码
在这里插入图片描述
再次测试,成功返回在这里插入图片描述

  • 11
    点赞
  • 59
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值