SpringBoot 使用 beforeBodyWrite 实现统一的接口返回类型

通常我们在 Spring Boot 的项目中,会使用一个类来作为统一的接口返回,比如这样:

import lombok.Data;

@Data
public class Result<T> {
    private int code;
    private String message;
    private T data;

    public static <T> Result<T> success(T t) {
        Result<T> result = new Result<>();
        result.code = 200;
        result.message = "ok";
        result.data = t;
        return result;
    }

    public static Result<Object> failed(String message) {
        Result<Object> result = new Result<>();
        result.code = 500;
        result.message = message;
        return result;
    }
}

但是我们又不想在每一个接口都写一遍Result.success(obj)这样的代码,比如这样:

@GetMapping("test")
public Result<String> test() {
    return Result.success("ok");
}

这个时候,我们就可以使用org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice#beforeBodyWrite来实现统一的接口返回类型。

使用方法也特别简单,如下所示:

@RestControllerAdvice
@Slf4j
public class WebAdvice implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof Result) {
            return body;
        }

        return Result.success(body);
    }
}

这段代码的逻辑也特别简单,就是判断接口的返回值是不是Result类型,不是的话就用Result包装后进行返回,这样就达到了统一的接口返回类型。


当我们测试普通的引用类型时,是没有什么问题的。

但是当我们要返回的值类型为String类型时,就会发生异常:

java.lang.ClassCastException: com.xx.Result cannot be cast to java.lang.String
	at org.springframework.http.converter.StringHttpMessageConverter.getContentLength(StringHttpMessageConverter.java:44) ~[spring-web-5.1.16.RELEASE.jar:5.1.16.RELEASE]
	at org.springframework.http.converter.AbstractHttpMessageConverter.addDefaultHeaders(AbstractHttpMessageConverter.java:260) ~[spring-web-5.1.16.RELEASE.jar:5.1.16.RELEASE]
	at org.springframework.http.converter.AbstractHttpMessageConverter.write(AbstractHttpMessageConverter.java:211) ~[spring-web-5.1.16.RELEASE.jar:5.1.16.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:294) ~[spring-webmvc-5.1.16.RELEASE.jar:5.1.16.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:181) ~[spring-webmvc-5.1.16.RELEASE.jar:5.1.16.RELEASE]
	at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:82) ~[spring-web-5.1.16.RELEASE.jar:5.1.16.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:123) ~[spring-webmvc-5.1.16.RELEASE.jar:5.1.16.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:893) ~[spring-webmvc-5.1.16.RELEASE.jar:5.1.16.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:798) ~[spring-webmvc-5.1.16.RELEASE.jar:5.1.16.RELEASE]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.1.16.RELEASE.jar:5.1.16.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040) ~[spring-webmvc-5.1.16.RELEASE.jar:5.1.16.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943) ~[spring-webmvc-5.1.16.RELEASE.jar:5.1.16.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) [spring-webmvc-5.1.16.RELEASE.jar:5.1.16.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) [spring-webmvc-5.1.16.RELEASE.jar:5.1.16.RELEASE]

现在我们通过源码来分析这个问题是怎么产生的

首先,我们要知道
浏览器传给服务器和服务器返回给浏览器的数据分别是输入流和输出流
然后 spring 框架有个HttpMessageConverter类,就是专门用来处理流和接口的参数类型或返回值类型之间的转换的。方法也很好理解,就是判断能不能进行读操作,能的话就进行读操作;能不能进行写操作,能的话就进行写操作。
在这里插入图片描述

现在,我们启动一个项目,跟踪断点进行排查。
首先我们要找到发生异常的地址,从刚才控制台打印的异常信息,我们可以找到这个地方,并打上一个断点
在这里插入图片描述
然后,我们通过该断点找到调用这个write的方法
在这里插入图片描述
我们可以发现这个converter是一个StringHttpMessageConverter类型,然后我们进入到addDefaultHeaders方法
在这里插入图片描述
然后,当代码执行到这个方法的 260 行时,就会发生刚才我们说的那个ClassCastException异常。
在这里插入图片描述
最后,我们终于找到了发生异常的原因,因为 260 的代码会执行getContentLength(t, headers.getContentType())这个方法,而这个方法会去执行StringHttpMessageConvertergetContentLength方法,如下图所示:
在这里插入图片描述
但是这个时候 t的类型已经被我们用beforeBodyWrite方法转为Result类型了,所以就发生了类型转换异常的错误。


我们再来说说这个异常为啥会发生。

首先,spring 框架默认给我们注入了一些转换器,如下图所示:
在这里插入图片描述
然后,我们通过源码就能发现,这个转换的执行过程是:
1.遍历所有的转换器,判断当前转换器能不能使用
2.如果可以使用,才调用我们写的beforeBodyWrite进行处理
在这里插入图片描述
最后,我们就发现了原来是因为处理过后的body已经从String类型转为Result类型,然后在调用实现类即StringHttpMessageConverter#getContentLength(String str, @Nullable MediaType contentType)方法时,第一个参数str发生了类型转换错误的异常。


最后,我们来说说怎么解决这个问题

1.第一种方式

就是我们在我们自己写的beforeBodyWrite的方法中,将返回值用 Result 包装过后再将Result 转为 String 类型进行返回。

@Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
    								Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof String) {
            ObjectMapper om = new ObjectMapper();
            return om.writeValueAsString(Result.success(body));
        }
        if (body instanceof Result) {
            return body;
        }

        return Result.success(body);
    }
2.第二种方式,处理 spring 框架提供的转换器

如下代码中的方式,任选其一即可。

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    	  // 第一种方式是将 json 处理的转换器放到第一位,使得先让 json 转换器处理返回值,这样 String转换器就处理不了了。
//        converters.add(0, new MappingJackson2HttpMessageConverter());
		  // 第二种就是把String类型的转换器去掉,不使用String类型的转换器
//        converters.removeIf(httpMessageConverter -> httpMessageConverter.getClass() == StringHttpMessageConverter.class);
    }
}
  • 38
    点赞
  • 60
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值