Spring Boot REST(二)源码分析

Spring 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html)

SpringBoot REST 系列相关的文章:

  1. SpringBoot REST(一)核心接口
  2. SpringBoot REST(二)源码分析

在上一篇文章中提到了 Spring Boot 中的 REST 的一些使用方法,@ResponseBody 默认返回一个 json,如果需要返回 xml 或者自定义返回媒体类型时怎么办呢?

@GetMapping("/v1/{user_id}")
public User user(@PathVariable("user_id") String userId) {
return new User(userId, "binarylei", "123456");
}

一、自定义媒体类型

1.1 媒体类型

首先要解释媒体类型这个概念,常见的媒体类型有 application/json、application/xml 等。

(1) 浏览器

浏览器即可以指定要发送的格式(Content-Type),也可以指定可以要接收的数据格式(Accept),如下表示发送 json 格式,接收 xml 格式:

Content-Type: application/xml
Accept: application/json

(2) 服务器

@RequestMapping 注解有两个参数可以匹配这种请求。下面这个只处理发送的请求是 xml 格式,返回 json 的请求。

@GetMapping(value = "/v3/xml/to/json",
consumes = "application/xml",
produces = "application/json")
public User propertiesToHJson(@RequestBody User user) {
return new User("1", "com/github/binarylei", "123456");
}

1.2 引入 application/xml 解析器

<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>

Spring Boot 默认只能处理 json 这种媒体类型,引入上述的 jar 包后就可以处理 xml 格式了。

1.3 自定义解析器

(1) PropertiesHttpMessageConverter

public class PropertiesHttpMessageConverter extends AbstractHttpMessageConverter<User> {
public PropertiesHttpMessageConverter() {
super(Charset.forName("utf-8"), MediaType.valueOf("application/properties"));
} @Override
protected boolean supports(Class clazz) {
return clazz == User.class;
} @Override
protected User readInternal(Class<? extends User> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
Properties properties = new Properties();
properties.load(inputMessage.getBody());
User user = new User();
user.setUserId(properties.getProperty("user.id"));
user.setUsername(properties.getProperty("user.name"));
return user;
} @Override
protected void writeInternal(User user, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
Properties properties = new Properties();
properties.setProperty("user.id", user.getUserId());
properties.setProperty("user.name", user.getUsername());
properties.setProperty("user.password", user.getPassword());
properties.store(outputMessage.getBody(), "write");
}
}

(2) 配置类 PropertiesWebMvcConfigurer

@Configuration
public class PropertiesWebMvcConfigurer implements WebMvcConfigurer {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new PropertiesHttpMessageConverter());
}
}

(3) rest 接口定义

@GetMapping(value = "/v3/properties/to/json",
consumes = "application/properties",
produces = "application/json")
public User propertiesToHJson(@RequestBody User user) {
return new User("1", "binarylei", "123456");
} @GetMapping(value = "/v3/json/to/properties",
consumes = "application/json",
produces = "application/properties")
public User jsonToProperties(@RequestBody User user) {
return new User("1", "binarylei", "123456");
}

(4) 测试

  1. 测试1:
请求地址:localhost:8080//v3/properties/to/json
请求头:Accept: application/properties, Content-Type: application/json
请求参数:user.id=1 user.name=binarylei
  1. 测试2:
请求地址:localhost:8080/v3/json/to/properties
请求头:Accept: application/json, Content-Type: application/properties
请求参数:{}

二、源码分析

@EnableWebMvc 注入了 DelegatingWebMvcConfiguration 组件,其类图结构如下:

2.1 默认 HttpMessageConverter 加载

在 WebMvcConfigurationSupport 类中定义了许多默认的 HttpMessageConverter,根据是否有相应的类加载来判断是否启动对应的 HttpMessageConverter。

// 类型转换器
private List<HttpMessageConverter<?>> messageConverters; protected final List<HttpMessageConverter<?>> getMessageConverters() {
if (this.messageConverters == null) {
this.messageConverters = new ArrayList<>();
configureMessageConverters(this.messageConverters); // (1)
if (this.messageConverters.isEmpty()) {
addDefaultHttpMessageConverters(this.messageConverters); // (2)
}
extendMessageConverters(this.messageConverters); // (3)
}
return this.messageConverters;
}

(1) 由子类 DelegatingWebMvcConfiguration 重写了 configureMessageConverters 方法,实际上是委托给了 WebMvcConfigurer 完成。

(2) 加载默认的 HttpMessageConverter

(3) 同 (1),也是由子类重写 extendMessageConverters

下面我们看一下 Spring Boot 默认加载了那些 HttpMessageConverter

boolean jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
boolean jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader); protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
stringHttpMessageConverter.setWriteAcceptCharset(false); // see SPR-7316 messageConverters.add(new ByteArrayHttpMessageConverter());
messageConverters.add(stringHttpMessageConverter);
messageConverters.add(new ResourceHttpMessageConverter());
messageConverters.add(new ResourceRegionHttpMessageConverter());
try {
messageConverters.add(new SourceHttpMessageConverter<>());
} catch (Throwable ex) {
// Ignore when no TransformerFactory implementation is available...
}
messageConverters.add(new AllEncompassingFormHttpMessageConverter()); // 省略...
if (jackson2XmlPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
}
if (jackson2Present) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build()));
}
// 省略...
}

可以看到除了 ByteArrayHttpMessageConverter 等是固定加载外,其余的都是通过判断是否有相应的类来决定是否启用。如果需要使用相应的解析器,只需要到相应的 jar 包添加到 pom.xml 中即可。

最终容器中加载了如下的 HttpMessageConverter 解析器:

0 = {ByteArrayHttpMessageConverter@5783}
1 = {StringHttpMessageConverter@5784}
2 = {ResourceHttpMessageConverter@5785}
3 = {ResourceRegionHttpMessageConverter@5786}
4 = {SourceHttpMessageConverter@5787}
5 = {AllEncompassingFormHttpMessageConverter@5788}
6 = {MappingJackson2XmlHttpMessageConverter@5789}
7 = {MappingJackson2HttpMessageConverter@5790}

2.2 HttpMessageConverter 执行过程

上文中提到 Spring Boot 启动时会在 messageConverters 集合中加载多个 HttpMessageConverter,到底执行那个呢?毫无疑问,执行肯定有三个过程:一是匹配对应的 HttpMessageConverter;二是执行 Handler;三是执行 HttpMessageConverter 响应结果。

HttpMessageConverter 的执行是在 AbstractMessageConverterMethodProcessor#writeWithMessageConverters 中执行的,这个方法很长,我们一点点来看。

2.2.1 匹配 HttpMessageConverter

MediaType selectedMediaType = null;
MediaType contentType = outputMessage.getHeaders().getContentType();
if (contentType != null && contentType.isConcrete()) {
selectedMediaType = contentType;
} else {
HttpServletRequest request = inputMessage.getServletRequest();
// 1. 获取客户端可接受的类型 Accept: application/jsion
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
// 2. 服务端可以生成的所有 MediaType 类型
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType); if (body != null && producibleTypes.isEmpty()) {
throw new HttpMessageNotWritableException(
"No converter found for return value of type: " + valueType);
} // 3. acceptableTypes 和 producibleTypes 比较,找出可用的 MediaType
List<MediaType> mediaTypesToUse = new ArrayList<>();
for (MediaType requestedType : acceptableTypes) {
for (MediaType producibleType : producibleTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
if (mediaTypesToUse.isEmpty()) {
if (body != null) {
throw new HttpMediaTypeNotAcceptableException(producibleTypes);
}
return;
}
MediaType.sortBySpecificityAndQuality(mediaTypesToUse); // 4. 如果有多个 MediaType 可用,选择一个可用的返回
for (MediaType mediaType : mediaTypesToUse) {
// 只要是非 */* 就直接返回
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
} else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}
}

客户端可以传两个请求头过来:

Accept: application/xml         // 客户端可接收的媒体类型
Content-Type: application/json // 客户端请求的媒体类型

2.2.2 执行 HttpMessageConverter

// 遍历 messageConverters,如果 converter 支持 selectedMediaType 则直接返回
for (HttpMessageConverter<?> converter : this.messageConverters) {
GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?
(GenericHttpMessageConverter<?>) converter : null);
// 1. canWrite 返回 true 则直接执行并结束循环
if (genericConverter != null ?
((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
converter.canWrite(valueType, selectedMediaType)) { // 2. 拿到 handler 的执行结果
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
inputMessage, outputMessage);
if (body != null) {
Object theBody = body;
addContentDispositionHeader(inputMessage, outputMessage); // 3. 执行对应的 genericConverter
if (genericConverter != null) {
genericConverter.write(body, targetType, selectedMediaType, outputMessage);
} else {
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
}
}
return;
}
}

核心的步骤 converter.write(body, selectedMediaType, outputMessage) 将 POJO 转换为 json 或 xml 后返回。

2.2.3 HttpMessageConverter

如果需要自定义 HttpMessageConverter,可以直接继承 AbstractHttpMessageConverter 类,重写 supports、readInternal、writeInternal 方法。


每天用心记录一点点。内容也许不重要,但习惯很重要!

Spring Boot REST(二)源码分析的更多相关文章

  1. spring boot 2.0 源码分析(四)

    在上一章的源码分析里,我们知道了spring boot 2.0中的环境是如何区分普通环境和web环境的,以及如何准备运行时环境和应用上下文的,今天我们继续分析一下run函数接下来又做了那些事情.先把r ...

  2. spring boot 2.0 源码分析(一)

    在学习spring boot 2.0源码之前,我们先利用spring initializr快速地创建一个基本的简单的示例: 1.先从创建示例中的main函数开始读起: package com.exam ...

  3. Spring Boot 自动配置 源码分析

    Spring Boot 最大的特点(亮点)就是自动配置 AutoConfiguration 下面,先说一下 @EnableAutoConfiguration ,然后再看源代码,到底自动配置是怎么配置的 ...

  4. spring boot 2.0 源码分析(二)

    在上一章学习了spring boot 2.0启动的大概流程以后,今天我们来深挖一下SpringApplication实例变量的run函数. 先把这段run函数的代码贴出来: /** * Run the ...

  5. spring boot 2.0 源码分析(三)

    通过上一章的源码分析,我们知道了spring boot里面的listeners到底是什么(META-INF/spring.factories定义的资源的实例),以及它是创建和启动的,今天我们继续深入分 ...

  6. spring boot 2.0 源码分析(五)

    在上一篇文章中我们详细分析了spring boot是如何准备上下文环境的,今天我们来看一下run函数剩余的内容.还是先把run函数贴出来: /** * Run the Spring applicati ...

  7. Spring中Bean命名源码分析

    Spring中Bean命名源码分析 一.案例代码 首先是demo的整体结构 其次是各个部分的代码,代码本身比较简单,不是我们关注的重点 配置类 /** * @Author Helius * @Crea ...

  8. Spring Cloud 学习 之 Spring Cloud Eureka(源码分析)

    Spring Cloud 学习 之 Spring Cloud Eureka(源码分析) Spring Boot版本:2.1.4.RELEASE Spring Cloud版本:Greenwich.SR1 ...

  9. Spring JPA实现逻辑源码分析总结

    1.SharedEntityManagerCreator: entitymanager的创建入口 该类被EntityManagerBeanDefinitionRegistrarPostProcesso ...

  10. 设计模式(二十一)——解释器模式(Spring 框架中SpelExpressionParser源码分析)

    1 四则运算问题 通过解释器模式来实现四则运算,如计算 a+b-c 的值,具体要求 1) 先输入表达式的形式,比如 a+b+c-d+e,  要求表达式的字母不能重复 2) 在分别输入 a ,b, c, ...

随机推荐

  1. Gimbal Lock

    [Gimbal Lock] 万向锁源于欧拉角的是有序处理的.U3D中的序列为: y->x->z.当旋转y时,local坐标系与世界坐标系重合,所以y等于永远按惯性坐标旋转.当x旋转+/-9 ...

  2. cdnbest如何查看站点操作日志(同步日志)

     1. 在区域列表点同步日志 2. 点击进入后,可以查看对哪个站点进行了操作,操作时间,ip,id都有记录 3. 想知道详细操作了什么内容把鼠标指向操作类型,就会弹出操作的信息

  3. NBU 还原LINUX ORACLE RAC数据库(CRM)

    CRM集群数据库恢复 linux centos 6.6 oracle 11.2.0.3 集群环境 1.53 oraclea 1.54 oracleb 在linux操作系统root用户下安装好NBUci ...

  4. w3af的使用

    编解码工具 配置身份 基本身份认证 明文用base64编码 NTML认证方式,微软特有的加密方式 form表单身份认证 这样基于表单的认证就配置好了 基于cookie认证 点击后面的感叹号,它会有co ...

  5. mybatis forEach使用

    1.集合的使用 <select id="getCitysByKeys" resultMap="city" parameterType="Arra ...

  6. cf-Global Round2-C. Ramesses and Corner Inversion(思维)

    题目链接:http://codeforces.com/contest/1119/problem/C 题意:给两个同型的由0.1组成的矩阵A.B,问A能否经过指定的操作变成B,指定操作为在矩阵A中选定一 ...

  7. Centos 7 下 Corosync + Pacemaker + psc 实现 httpd 服务高可用

    一.介绍 高可用,大家可能会想到比较简单的Keepalived,或者更早一点的 heartbeat,也可能会用到 Corosync+Pacemaker,那么他们之间有什么区别. Heartbeat到了 ...

  8. ubuntu 印象笔记

    1. 印象笔记剪裁,直接浏览器上百度搜索,下载浏览器插件,登陆即可.(不过,我遇到个问题,浏览器登陆的服务器貌似是国际版的,与pc上的服务器不同,也没有成功转换过来.两账户相互独立.) 2. 印象笔记 ...

  9. 树形DP(记忆化搜索) HYSBZ - 1509

    题目链接:https://vjudge.net/problem/HYSBZ-1509 我参考的证明的论文:8.陈瑜希<多角度思考 创造性思维>_百度文库  https://wenku.ba ...

  10. Adapter类 调用Activity中的函数

    在Adapter类中可以定义一个MainActivity变量,在初始化时,对其赋值,例如fragment的适配器中: private MainActivity context; private Lis ...