Spring统一返回类型中关于String的问题

23 篇文章 0 订阅


1. 问题铺垫

首先设置了以下代码统一处理返回类型

@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    @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) {
        return Result.success(body);
    }
}

其中Result是:
image.png
有一个接口是这样的
image.png
此时访问:
image.png
看到日志:
image.png
提到Result不能被映射到String

2. 解决方法

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
    if(body instanceof String){
        try {
            if("SUCCESS".equals(body)){
                response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
                return objectMapper.writeValueAsString(Result.success(body));
            }else {
                return objectMapper.writeValueAsString(Result.paramError());
            }
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }
    return Result.success(body);
}

就是要对String的返回类型进行特殊处理,转成JSON字符串

3. 问题分析

在Spring MVC中,HttpMessageConverter 接口定义了在HTTP请求的发送和接受的过程中,如何将请求消息体中的数据转化为java对象,以及如何将java对象装换为响应消息体中的数据类型
image.png
Spring MVC会默认注册一些自带的HttpMessageConverter(从先后顺序排序分别为:

  1. ByteArrayHttpMessageConverter
  2. StringHttpMessageConverter
  3. AllEncompassingFormHttpMessageConverter
private final List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
//...

private void initMessageConverters() {
    if (!this.messageConverters.isEmpty()) {
        return;
    }
    this.messageConverters.add(new ByteArrayHttpMessageConverter());
    this.messageConverters.add(new StringHttpMessageConverter());

    this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
}

Spring MVC 允许开发者通过配置来注册自定义的 HttpMessageConverter 实现,AllEncompassingFormHttpMessageConverter会根据项目依赖的情况,添加对应的HttpMessageConverter

public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConverter {

    private static final boolean jaxb2Present;

    private static final boolean jackson2Present;

    private static final boolean jackson2XmlPresent;

    private static final boolean jackson2SmilePresent;

    private static final boolean gsonPresent;

    private static final boolean jsonbPresent;

    private static final boolean kotlinSerializationCborPresent;

    private static final boolean kotlinSerializationJsonPresent;

    private static final boolean kotlinSerializationProtobufPresent;

    static {
        ClassLoader classLoader = AllEncompassingFormHttpMessageConverter.class.getClassLoader();
        jaxb2Present = ClassUtils.isPresent("jakarta.xml.bind.Binder", classLoader);
        jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
        ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
        jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
        jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
        gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
        jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader);
        kotlinSerializationCborPresent = ClassUtils.isPresent("kotlinx.serialization.cbor.Cbor", classLoader);
        kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader);
        kotlinSerializationProtobufPresent = ClassUtils.isPresent("kotlinx.serialization.protobuf.ProtoBuf", classLoader);
    }


    public AllEncompassingFormHttpMessageConverter() {

        if (jaxb2Present && !jackson2XmlPresent) {
            addPartConverter(new Jaxb2RootElementHttpMessageConverter());
        }

        if (kotlinSerializationJsonPresent) {
            addPartConverter(new KotlinSerializationJsonHttpMessageConverter());
        }
        if (jackson2Present) {
            addPartConverter(new MappingJackson2HttpMessageConverter());
        }
        else if (gsonPresent) {
            addPartConverter(new GsonHttpMessageConverter());
        }
        else if (jsonbPresent) {
            addPartConverter(new JsonbHttpMessageConverter());
        }

        if (jackson2XmlPresent) {
            addPartConverter(new MappingJackson2XmlHttpMessageConverter());
        }

        if (jackson2SmilePresent) {
            addPartConverter(new MappingJackson2SmileHttpMessageConverter());
        }

        if (kotlinSerializationCborPresent) {
            addPartConverter(new KotlinSerializationCborHttpMessageConverter());
        }

        if (kotlinSerializationProtobufPresent) {
            addPartConverter(new KotlinSerializationProtobufHttpMessageConverter());
        }
    }

}

当我们在依赖中引入jackson包(Spring自动引入)后,容器会将MappingJackson2HttpMessageConverter 自动添加到messageConverters末尾,用于处理json数据
处理数据数据时,Spring会根据返回的数据类型,从messageConverters链中选择合适的HttpMessageConverter
当Controller返回一个非字符串类型时,使用的是MappingJackson2XmlHttpMessageConverter写入对象
当返回的数据是字符串时,StringHttpMessageConverte会先被遍历到,并且认为StringHttpMessageConverte可以处理字符串的返回
image.png

核心问题就在这里:

protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
                                              ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

    //...代码省略
    if (selectedMediaType != null) {
       selectedMediaType = selectedMediaType.removeQualityValue();

        //遍历

        //GenericHttpMessageConverter用于处理复杂的数据类型,
        //此时converter是StringHttpMessageConverte,用于处理简单的字符串数据
        //因此converter instanceof GenericHttpMessageConverter = false
       for (HttpMessageConverter<?> converter : this.messageConverters) {
          GenericHttpMessageConverter genericConverter =
                (converter instanceof GenericHttpMessageConverter ghmc ? ghmc : null);

           //在这里判断当前converter是否可以处理当前数据,此时如果数据是String,
           //StringHttpMessageConverte可以直接处理
          if (genericConverter != null ?
                ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
                converter.canWrite(valueType, selectedMediaType)) {

              //调用getAdvice().beforeBodyWrite, 执⾏之后, 
              //body转换成了我们自定义的Result类型的
              
             body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
                   (Class<? extends HttpMessageConverter<?>>) converter.getClass(),
                   inputMessage, outputMessage);
             if (body != null) {
                Object theBody = body;
                LogFormatUtils.traceDebug(logger, traceOn ->
                      "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
                addContentDispositionHeader(inputMessage, outputMessage);
                if (genericConverter != null) {
                   genericConverter.write(body, targetType, selectedMediaType, outputMessage);
                }
                else {

                    //走的是这里
                   ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
                }
             }
             else {
                if (logger.isDebugEnabled()) {
                   logger.debug("Nothing to write: null body");
                }
             }
             return;
          }
       }
    }
    //...代码省略
}

当执行到((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage) ,
注意:此时的converter实例是StringHttpMessageConverte
接着会调用write方法,是在:AbstractHttpMessageConverter
image.png
但是! 关键来了:由于上面的converter实例是StringHttpMessageConverte,而StringHttpMessageConverteAbstractHttpMessageConverter的子类,重写了addDefaultHeader*方法,因此此时调用的是
StringHttpMessageConvert.addDefaultHeaders!!!

如果这里不理解没关系,我们举个类似的例子:
image.png
执行结果:image.png

回到正题,此时执行的是StringHttpMessageConvert.addDefaultHeaders:

image.png


但是由于

image.png

在这里调用的时候,t是我们最开始封装的Result类型,Result -> String,就会抛出Result cannot be cast to class java.lang.String异常

4 解决方法解释

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
    if(body instanceof String){
        try {
            if("SUCCESS".equals(body)){
                response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
                return objectMapper.writeValueAsString(Result.success(body));
            }else {
                return objectMapper.writeValueAsString(Result.paramError());
            }
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }
    return Result.success(body);
}

此时我们对String的返回类型进行了特判,转化成JSON字符串,此时就是以String类型去处理,而不会转成Result,自然就不会发生类型匹配异常

  • 14
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值