alamide的笔记库「 87篇笔记 」「 小破站已建 0 天啦 🐶 」


SpringMVC HttpMessageConverter

2023-05-16, by alamide

SpringWeb 使用 HttpMessageConverter 来转化 HTTP 请求的 Request 和 Response.

The spring-web module contains the HttpMessageConverter contract for reading and writing the body of HTTP requests and responses through InputStream and OutputStream. HttpMessageConverter instances are used on the client side (for example, in the RestTemplate) and on the server side (for example, in Spring MVC REST controllers).

1.框架提供的一些转化器

框架为主要的数据类型(MIME)提供一些具体的实现类。

</tr>
MessageConverter Description
StringHttpMessageConverter Request: Content-Type: text/*; Response: Content-Type: text/plain
FormHttpMessageConverter Content-Type: application/x-www-form-urlencoded; 读取表单中的数据
MappingJackson2HttpMessageConverter Request: 将 RequestBody 的 Json 转换为对象;Response: 将对象转换为 Json 字符串
MappingJackson2XmlHttpMessageConverter Request: 将 RequestBody 的 xml 转换为对象;Response: 将对象转换为 xml 字符串

2.Request Converter

标记为 @RequestBody 的 Controller Method Parameter

2.1 StringHttpMessageConverter

可以将请求体中的内容转换为 String,Request 的 Content-Type: text/*,只要请求的类型为 text,都可以转换为字符串

POST /message/string HTTP/1.1
Accept: */*
Content-Length: 23
Content-Type: text/plain
Host: localhost:8080

Payload: username=alamide&age=18
@PostMapping("/string")
public void stringMessage(@RequestBody String content){
    //username=alamide&age=18
    log.info(content);
}

上面的请求指定了 Content-Type: text/plain, 所以只能转换为 String 或 byte[],即只有 StringHttpMessageConverter 和 ByteArrayHttpMessageConverter 支持,不能转换为 UserInfo。所有 text/* 都可以转换为 String 。

2.2 FormHttpMessageConverter

可以将 Form 表单提交的 Content-Type: application/x-www-form-urlencoded 数据转换为 MultiValueMap<String, String>,

POST /message/form HTTP/1.1
Accept: */*
Content-Length: 23
Content-Type: application/x-www-form-urlencoded
Host: localhost:8080

Payload: username=alamide&age=18
@PostMapping("/form")
public void form(@RequestBody MultiValueMap<String, String> multiValueMap) {
    //{username=[alamide], age=[20]}
    log.info(multiValueMap.toString());
}

2.3 MappingJackson2HttpMessageConverter

将请求体中的 Json 数据转换为被 @RequestBody 标记的参数对象

POST /message/json HTTP/1.1
Accept: */*
Content-Length: 31
Content-Type: application/json
Host: localhost:8080

Payload: {
  "username": "alamide",
  "age": 18
}
@PostMapping("/json")
public void json(@RequestBody UserInfo userInfo) {
    //UserInfo(username=alamide, age=18)
    log.info(userInfo.toString());
}

2.4 MappingJackson2XmlHttpMessageConverter

将请求体中的 Xml 数据转换为被 @RequestBody 标记的参数对象

POST /message/xml HTTP/1.1
Accept: */*
Content-Length: 79
Content-Type: application/xml
Host: localhost:8080

Payload: 
<Pet>
    <ownerId>1</ownerId>
    <petId>2</petId>
    <name>tom</name>
</Pet>
@PostMapping("/xml")
public void xmlDe(@RequestBody Pet pet) {
    //Pet(ownerId=1, petId=2, name=tom)
    log.info(pet.toString());
}

2.5 HttpMessageConverter 工作原理

HttpMessageConverter 是在 WebMvcConfigurationSupport 中注册的

public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
     static {
        ...
        ClassLoader classLoader = WebMvcConfigurationSupport.class.getClassLoader();
        //查看类路径下是否有目标类
        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);
        gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
        ...
    }
     protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
        messageConverters.add(new ByteArrayHttpMessageConverter());
        messageConverters.add(new StringHttpMessageConverter());
        messageConverters.add(new ResourceHttpMessageConverter());
        messageConverters.add(new ResourceRegionHttpMessageConverter());
        messageConverters.add(new AllEncompassingFormHttpMessageConverter());
        ...
        //如果目标类存在,则注册
        if (jackson2XmlPresent) {
            builder = Jackson2ObjectMapperBuilder.xml();
            if (this.applicationContext != null) {
                builder.applicationContext(this.applicationContext);
            }

            messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
        } else if (jaxb2Present) {
            messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
        }
        ...
        //如果目标类存在,则注册
        if (jackson2Present) {
            builder = Jackson2ObjectMapperBuilder.json();
            if (this.applicationContext != null) {
                builder.applicationContext(this.applicationContext);
            }

            messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build()));
        } else if (gsonPresent) {
            messageConverters.add(new GsonHttpMessageConverter());
        } else if (jsonbPresent) {
            messageConverters.add(new JsonbHttpMessageConverter());
        }
     }
}

Debug 调试可以看到默认注册 7 个转换器 converts

ByteArrayHttpMessageConverter: Content-Typ: application/octet-stream, */*; ParameterType: byte[]

StringHttpMessageConverter: Content-Typ: text/plain, */*;ParameterType: String

MappingJackson2XmlHttpMessageConverter: Content-Typ: application/xml, text/xml, application/*+xml;ParameterType: objectMapper.canDeserialize

MappingJackson2HttpMessageConverter: Content-Typ: application/json, application/*+json;ParameterType: objectMapper.canDeserialize

请求体匹配时,按照这个顺序依次匹配,匹配的过程为

public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
    @Nullable
    @SuppressWarnings({ "unchecked", "rawtypes" })
    protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
            Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
        for (HttpMessageConverter<?> converter : this.messageConverters) {
            Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
            GenericHttpMessageConverter<?> genericConverter =
                    (converter instanceof GenericHttpMessageConverter ghmc ? ghmc : null);
            //查看是否支持类型转换
            if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
                    (targetClass != null && converter.canRead(targetClass, contentType))) {
                if (message.hasBody()) {
                    HttpInputMessage msgToUse =
                            getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
                    body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
                            ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
                    body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
                }
                else {
                    body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
                }
                break;
            }
        }
        
    }
}

3.Response Converter

标记为 @ResponseBody 的 Controller Method ReturnValue

3.1 StringHttpMessageConverter

将返回的数据以 String 写回响应流

@ResponseBody
@PostMapping("/string")
public String stringMessage(@RequestBody String content) {
    return content;
}

执行的流程,messageConverters 还是上图的那几个

public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
		implements HandlerMethodReturnValueHandler {
    protected List<MediaType> getProducibleMediaTypes(
            HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType) {
        Set<MediaType> mediaTypes =
                (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
        if (!CollectionUtils.isEmpty(mediaTypes)) {
            return new ArrayList<>(mediaTypes);
        }
        Set<MediaType> result = new LinkedHashSet<>();
        //查询支持返回数据类型的转换器 
        //StringHttpMessageConverter
        //MappingJackson2HttpMessageConverter
        //MappingJackson2XmlHttpMessageConverter
        for (HttpMessageConverter<?> converter : this.messageConverters) {
            if (converter instanceof GenericHttpMessageConverter<?> ghmc && targetType != null) {
                if (ghmc.canWrite(targetType, valueClass, null)) {
                    result.addAll(converter.getSupportedMediaTypes(valueClass));
                }
            }
            else if (converter.canWrite(valueClass, null)) {
                result.addAll(converter.getSupportedMediaTypes(valueClass));
            }
        }
        //最终 result 的类型为 下图
        return (result.isEmpty() ? Collections.singletonList(MediaType.ALL) : new ArrayList<>(result));
    }
}

response-convert

public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
		implements HandlerMethodReturnValueHandler {
    protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
        ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
        throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
        ...
        MediaType contentType = outputMessage.getHeaders().getContentType();
        boolean isContentTypePreset = contentType != null && contentType.isConcrete();
        //response 是否已经设置了 Content-Type
        if (isContentTypePreset) {
            if (logger.isDebugEnabled()) {
                logger.debug("Found 'Content-Type:" + contentType + "' in response");
            }
            selectedMediaType = contentType;
        }
        //没有设置的话,则从请求头中读取 Accept 请求头
        else {
            HttpServletRequest request = inputMessage.getServletRequest();
            List<MediaType> acceptableTypes;
            try {
                //读取 Accept 请求头信息
                acceptableTypes = getAcceptableMediaTypes(request);
            }
            catch (HttpMediaTypeNotAcceptableException ex) {
                ...
            }
            //获取 Convert 中支持返回类型的所有 MediaType,具体查找方法见上面代码块
            List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
            ...

            List<MediaType> compatibleMediaTypes = new ArrayList<>();
            //依据 Accept 筛选最终支持的 MediaType
            determineCompatibleMediaTypes(acceptableTypes, producibleTypes, compatibleMediaTypes);
            ...
            //重排 MediaType
            MimeTypeUtils.sortBySpecificity(compatibleMediaTypes);

            for (MediaType mediaType : compatibleMediaTypes) {
                //选择第一个符合条件的 MediaType
                if (mediaType.isConcrete()) {
                    selectedMediaType = mediaType;
                    break;
                }
                else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
                    selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
                    break;
                }
            }
            ...
        }
        if (selectedMediaType != null) {
            selectedMediaType = selectedMediaType.removeQualityValue();
            for (HttpMessageConverter<?> converter : this.messageConverters) {
                GenericHttpMessageConverter genericConverter =
                        (converter instanceof GenericHttpMessageConverter ghmc ? ghmc : null);
                //选择第一个合适的 Converter,本次请求第一个符合的是 StringHttpMessageConverter
                if (genericConverter != null ?
                        ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
                        converter.canWrite(valueType, selectedMediaType)) {
                    ...
                    //向响应流中写入数据,并这只 Reponse 的 Content-Type
                    ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
                    ...
                    ...
                    return;
                }
            }
        }
        ...
    }
}

最终选出 StringHttpMessageConverter ,向客户端写数据,并且 Response 的 Content-Type 为 text/plain

3.2 Content-Type 和 Accept 的灵活使用

可以依据 Accept 灵活的返回数据,依据 Content-Type 序列化请求体,下面的这个请求,可以对应四种不同类型的请求

@PostMapping("/multi")
public UserInfo json(@RequestBody UserInfo userInfo) {
    return userInfo;
}

Request:

POST /message/multi HTTP/1.1
Accept: application/json
Content-Type: application/json
Host: localhost:8080

Payload: {
  "username": "alamide",
  "age": 18
}

Response:

Content-Type: application/json

ResponseBody:
{
    "username": "alamide",
    "age": 18
}

Request:

POST /message/multi HTTP/1.1
Accept: application/xml
Content-Type: application/json
Host: localhost:8080

Payload: {
  "username": "alamide",
  "age": 18
}

Response:

Content-Type: application/xml

ResponseBody:
<UserInfo>
    <username>alamide</username>
    <age>18</age>
</UserInfo>

Request:

POST /message/multi HTTP/1.1
Accept: application/xml
Content-Type: application/xml
Host: localhost:8080

Payload:
<UserInfo>
    <username>alamide</username>
    <age>18</age>
</UserInfo>

Response:

Content-Type: application/xml

ResponseBody:
<UserInfo>
    <username>alamide</username>
    <age>18</age>
</UserInfo>

Request:

POST /message/multi HTTP/1.1
Accept: application/json
Content-Type: application/xml
Host: localhost:8080

Payload:
<UserInfo>
    <username>alamide</username>
    <age>18</age>
</UserInfo>

Response:

Content-Type: application/json

ResponseBody:
{
    "username": "alamide",
    "age": 18
}

4.自定义 HttpMessageConverter

定义一个可以解析特定格式请求的 Converter

public class DiyHttpMessageConverter implements HttpMessageConverter<Pet> {
    @Override
    public boolean canRead(Class<?> clazz, MediaType mediaType) {
        return clazz.isAssignableFrom(Pet.class) && MediaType.valueOf("text/pet").equalsTypeAndSubtype(mediaType);
    }

    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        return false;
    }

    @Override
    public List<MediaType> getSupportedMediaTypes() {
        return Collections.singletonList(MediaType.valueOf("text/pet"));
    }

    @Override
    public Pet read(Class<? extends Pet> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        final String data = StreamUtils.copyToString(inputMessage.getBody(), StandardCharsets.UTF_8);
        //ownerId=1;petId=2;name=tom;
        final String[] strings = data.split(";");
        Pet pet = null;
        try {
            pet = clazz.getDeclaredConstructor().newInstance();
            pet.setOwnerId(Long.valueOf(strings[0].split("=")[1]));
            pet.setPetId(Long.valueOf(strings[1].split("=")[1]));
            pet.setName(strings[2].split("=")[1]);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        return pet;
    }

    @Override
    public void write(Pet pet, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {

    }
}

//扩展系统的转换器
@Configuration
@ComponentScan(basePackages = {"com.alamide.web"})
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    //扩展
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new DiyHttpMessageConverter());
    }

    //替换
    // @Override
    // public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    //     WebMvcConfigurer.super.configureMessageConverters(converters);
    // }

}

@PostMapping("/pet")
public Pet pet(@RequestBody Pet pet) {
    return pet;
}

Request:

POST /message/pet HTTP/1.1
Accept: application/json
Content-Type: text/pet
Host: localhost:8080

Payload:`
ownerId=1;petId=2;name=tom;

Response:

Content-Type: application/json

ResponseBody:
{
    "ownerId": 1,
    "petId": 2,
    "name": "tom"
}

5.小总结

SpringMVC 可以依据请求的 Content-Type 和 Accept 来灵活的转换数据

Tags: Java - SpringMVC
~ belongs to alamide@163.com