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

文章目录

  • [1. 问题铺垫](#1. 问题铺垫)
  • [2. 解决方法](#2. 解决方法)
  • [3. 问题分析](#3. 问题分析)
  • [4 解决方法解释](#4 解决方法解释)

1. 问题铺垫

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

java 复制代码
@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是:

有一个接口是这样的

此时访问:

看到日志:

提到Result不能被映射到String

2. 解决方法

java 复制代码
@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对象装换为响应消息体中的数据类型

Spring MVC会默认注册一些自带的HttpMessageConverter(从先后顺序排序分别为:

  1. ByteArrayHttpMessageConverter
  2. StringHttpMessageConverter
  3. AllEncompassingFormHttpMessageConverter
java 复制代码
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

java 复制代码
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可以处理字符串的返回

核心问题就在这里:

java 复制代码
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

但是! 关键来了:由于上面的converter实例是 StringHttpMessageConverte,而 StringHttpMessageConverte AbstractHttpMessageConverter的子类,重写了addDefaultHeader*方法,因此此时调用的是
StringHttpMessageConvert.addDefaultHeaders!!!

如果这里不理解没关系,我们举个类似的例子:

执行结果:

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

但是由于

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

4 解决方法解释

java 复制代码
@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,自然就不会发生类型匹配异常

相关推荐
晴子呀6 分钟前
Spring底层原理大致脉络
java·后端·spring
只吹45°风13 分钟前
Java-ArrayList和LinkedList区别
java·arraylist·linkedlist·区别
阿华的代码王国20 分钟前
【JavaEE】多线程编程引入——认识Thread类
java·开发语言·数据结构·mysql·java-ee
黑蛋同志20 分钟前
array和linked list的区别
java
andrew_121926 分钟前
腾讯 IEG 游戏前沿技术 一面复盘
java·redis·sql·面试
andrew_121929 分钟前
腾讯 IEG 游戏前沿技术 二面复盘
后端·sql·面试
寻求出路的程序媛34 分钟前
JVM —— 类加载器的分类,双亲委派机制
java·jvm·面试
这孩子叫逆36 分钟前
35. MyBatis中的缓存失效机制是如何工作的?
java·spring·mybatis
骆晨学长37 分钟前
基于SpringBoot的校园失物招领系统
java·spring boot
汇匠源37 分钟前
零工市场小程序:保障灵活就业
java·小程序·零工市场