理论

Representational state transfer

Representational state transfer (REST) or RESTful web services are a way of providing interoperability between computer systems on the Internet. REST-compliant Web services allow requesting systems to access and manipulate textual representations of Web resources using a uniform and predefined set of stateless operations. Other forms of Web services exist, which expose their own arbitrary sets of operations such as WSDL and SOAP.[1]

  • 特性
    • 性能
    • 伸缩性
    • 简化统一接口
    • 组件的可修改性
    • 通讯的可视性
    • 组件移植性
    • 可靠性
  • 约束
    • 客户端计算能力薄弱,服务端计算能力较强
    • 无状态
      • 如果需要持久化状态
        • redis
        • 数据库
    • 缓存
    • 分层系统
    • 统一接口
    • 自描述消息
  • 类型
    • GET
    • PUT
      • 幂等
    • POST
      • 非幂等
    • DELETE
      • 幂等
    • PATCH
      • 不常用

幂等

初始状态:0

修改状态:1 * N

最终状态:1

非幂等

初始状态:1

修改状态:1+1=2

N次修改:1+N=N+1

最终状态:N+1

幂等/非幂等 依赖于服务端实现,这种方式是一种契约。

  • 服务端核心接口
    • 定义相关
      • @Controller
      • RestController
    • 映射相关
      • @RequestMapping
      • @PathVariable
    • 方法相关
      • RequestMethod

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.bai.restonwebwebmvc.domain;

public class Person {
private long id;
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public long getId() {

return id;
}

public void setId(long id) {
this.id = id;
}
}
  • 创建Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.bai.restonwebwebmvc.controller;

import com.bai.restonwebwebmvc.domain.Person;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
* {@link } {@link org.springframework.web.bind.annotation.RestController}
*/
@RestController
public class PersonRestController {

@GetMapping("/person/{id}")
//required = false,参数不强制传输
public Person person(@PathVariable Long id, @RequestParam(required = false) String name ){
Person person=new Person();
person.setId(id);
person.setName(name);
return person;
}
}
  • 启动程序(xxxApplication)
    • 如果IDE用的是IntelliJ IDEA,可以通过Tools->Test RESTful Web Service进行访问http://localhost:8080/person/1?name=2bai
    • 返回:{"id":1,"name":"2bai"}

自描述消息

Accept:

text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8

  • 第一优先顺序
    • text/html
    • application/xhtml+xml
    • application/xml
  • 第二优先顺序
    • image/webp
    • image/apng
  • q=0.9
    • 权重
    • 浏览器默认设置
    • 可以自行修改

按照xml格式返回

  • 添加pom依赖
1
2
3
4
5
6
<!--依赖包是通过org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport类中jackson2XmlPresent的className在search.maven.org中搜索得来-->
<!--search.maven.org -> search -> Advanced Search -> By Classname-->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
  • 再次访问,会以XML格式返回。

问题及解答

Q:为什么第一次是JSON,后来添加了XML依赖后,又变成了XML

A:Spring Boot应用默认没有增加XML处理器实现,所以最后采用轮询的方式去逐一尝试是否可读(canWrite(POJO)),如果返回true,说明可以序列化改POJO对象,那么Jackson2恰好能处理,那么Jackson输出了。可以查阅源码org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler#sendInternal

Q:当Accept请求头未被指定时,为什么还是JSON

A:依赖于messageConverters的插入顺序。

Q:converters中的顺序可以修改吗?

A:可以。参考下面实现的扩展之调整顺序。

源码导读

@EnableWebMvc

org.springframework.web.servlet.config.annotation.EnableWebMvc

1
2
3
4
5
6
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {
}

org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration

1
2
3
4
@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
//ignore
}

org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
//List集合:先来先服务
//所有的HTTP自描述消息处理器均在messageConverters里
//这个集合会传递到RequestMappingHandlerAdapter类里
//最终控制写出。
protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
stringConverter.setWriteAcceptCharset(false);
//开始按顺序添加
//debug得出下列顺序,重复添加的为Spring Boot的一个bug
//0 = {ByteArrayHttpMessageConverter@6489}
//1 = {StringHttpMessageConverter@6490}
//2 = {StringHttpMessageConverter@6491}
//3 = {ResourceHttpMessageConverter@6492}
//4 = {ResourceRegionHttpMessageConverter@6493}
//5 = {SourceHttpMessageConverter@6494}
//6 = {AllEncompassingFormHttpMessageConverter@6495}
//7 = {MappingJackson2HttpMessageConverter@6166}
//8 = {MappingJackson2HttpMessageConverter@6240}
//9 = {MappingJackson2XmlHttpMessageConverter@6247}
//10 = {MappingJackson2XmlHttpMessageConverter@6258}

messageConverters.add(new ByteArrayHttpMessageConverter());
messageConverters.add(stringConverter);
messageConverters.add(new ResourceHttpMessageConverter());
messageConverters.add(new ResourceRegionHttpMessageConverter());
messageConverters.add(new SourceHttpMessageConverter());
messageConverters.add(new AllEncompassingFormHttpMessageConverter());
if (romePresent) {
messageConverters.add(new AtomFeedHttpMessageConverter());
messageConverters.add(new RssChannelHttpMessageConverter());
}

Jackson2ObjectMapperBuilder builder;
//xml
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());
}
//json
if (jackson2Present) {
builder = Jackson2ObjectMapperBuilder.json();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
//以application/json为例,Spring Boot中默认使用Jackson2序列化方式
//媒体类型application/json,处理类为MappingJackson2HttpMessageConverter
//提供两类方法:
//1.读read*:通过HTTP请求内容转化成对应的Bean
//2.写write*:通过Bean序列化成对应文本内容作为响应内容
messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build()));
} else if (gsonPresent) {
messageConverters.add(new GsonHttpMessageConverter());
} else if (jsonbPresent) {
messageConverters.add(new JsonbHttpMessageConverter());
}

if (jackson2SmilePresent) {
builder = Jackson2ObjectMapperBuilder.smile();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}

messageConverters.add(new MappingJackson2SmileHttpMessageConverter(builder.build()));
}

if (jackson2CborPresent) {
builder = Jackson2ObjectMapperBuilder.cbor();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}

messageConverters.add(new MappingJackson2CborHttpMessageConverter(builder.build()));
}

}

org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//protected 包内可见/继承类可见
//final 不允许修改
protected final List<HttpMessageConverter<?>> getMessageConverters() {
//初始化时如果为空
if (this.messageConverters == null) {
//创建一个并加以扩展
this.messageConverters = new ArrayList<>();
configureMessageConverters(this.messageConverters);
//如果没有扩展
if (this.messageConverters.isEmpty()) {
//添加默认
addDefaultHttpMessageConverters(this.messageConverters);
}
extendMessageConverters(this.messageConverters);
}
return this.messageConverters;
}

org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
//内容协调管理器
//此类中有一个方法:resolveMediaTypes
adapter.setContentNegotiationManager(mvcContentNegotiationManager());
adapter.setMessageConverters(getMessageConverters());
adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer());
adapter.setCustomArgumentResolvers(getArgumentResolvers());
adapter.setCustomReturnValueHandlers(getReturnValueHandlers());

if (jackson2Present) {
adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice()));
adapter.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice()));
}

AsyncSupportConfigurer configurer = new AsyncSupportConfigurer();
configureAsyncSupport(configurer);
if (configurer.getTaskExecutor() != null) {
adapter.setTaskExecutor(configurer.getTaskExecutor());
}
if (configurer.getTimeout() != null) {
adapter.setAsyncRequestTimeout(configurer.getTimeout());
}
adapter.setCallableInterceptors(configurer.getCallableInterceptors());
adapter.setDeferredResultInterceptors(configurer.getDeferredResultInterceptors());

return adapter;
}

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter#MappingJackson2HttpMessageConverter

1
2
3
public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
super(objectMapper, new MediaType[]{MediaType.APPLICATION_JSON, new MediaType("application", "*+json")});
}

扩展自描述消息

org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#getMessageConverters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected final List<HttpMessageConverter<?>> getMessageConverters() {
if (this.messageConverters == null) {
this.messageConverters = new ArrayList<>();
//扩展点
//有没有自定义配置
configureMessageConverters(this.messageConverters);
//如果没有
if (this.messageConverters.isEmpty()) {
//加载默认配置
addDefaultHttpMessageConverters(this.messageConverters);
}
extendMessageConverters(this.messageConverters);
}
return this.messageConverters;
}

一次失败的试验

  • 创建类,继承WebMvcConfigurer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.bai.restonwebwebmvc.config;

import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new MappingJackson2HttpMessageConverter());
//converters:[org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@317a118b]
System.err.println("converters:"+converters);
}
}
  • 但是,经debug发现,org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#getMessageConverters方法内,判断this.messageConverters.isEmpty()为true,还是加载了默认配置,这可能是个bug。其实扩展已经加载成功了,可能在某一地方又清空了。

这次是成功的

添加扩展
  • 把默认实现的方法configureMessageConverters换为extendMessageConverters
  • debug发现,在加载默认配置后,自定义配置已经加载成功。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.bai.restonwebwebmvc.config;


import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

private Object object;

@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
//加载自定义配置
converters.add(new MappingJackson2XmlHttpMessageConverter());
}
}
调整顺序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.bai.restonwebwebmvc.config;


import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

private Object object;

@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
//将自定义配置添加到最前
converters.set(0,new MappingJackson2XmlHttpMessageConverter());
}
}

代码示例-完整版

  • POJO类—Person
  • 扩展处理类—PropertiesPersonHttpMessageConverter
  • 配置类—WebMvcConfig
  • Controller—PersonRestController

反序列化:JSON -> MappingJackson2HttpMessageConverter把文本转化为Person对象

序列化 Person对象 -> PropertiesPersonHttpMessageConverter#write->Properties文本

  • 如果请求中accept中和produces不一样,controller是不能执行的,会返回HTTP 状态码406
  • 并不一定需要POJO,这么做是简化操作
    • JSON -> 反序列化 -> Map -> Properties
    • 增加了操作的复杂度

Person

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.bai.restonwebwebmvc.domain;

public class Person {
private long id;
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public long getId() {

return id;
}

public void setId(long id) {
this.id = id;
}
}

PropertiesPersonHttpMessageConverter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package com.bai.restonwebwebmvc.http.message;

import com.bai.restonwebwebmvc.domain.Person;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import java.io.*;
import java.nio.charset.Charset;
import java.util.Properties;


/**
*
* @author 2bai
*/
public class PropertiesPersonHttpMessageConverter extends AbstractHttpMessageConverter<Person> {

public PropertiesPersonHttpMessageConverter() {
//设置支持的自定义媒体类型
super(MediaType.valueOf("application/properties+person"));
setDefaultCharset(Charset.forName("UTF-8"));
}

/**
* 是否支持当前POJO类型
* @param clazz
* @return
*/
@Override
protected boolean supports(Class<?> clazz) {
//必须是Person的子类
return clazz.isAssignableFrom(Person.class);
}

/**
* 将HTTP请求中的内容,并将内容转化成响应的POJO对象
* properties -> Person
* @param clazz
* @param inputMessage
* @return
* @throws IOException
* @throws HttpMessageNotReadableException
*/
@Override
protected Person readInternal(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
/**
* person.id=1
* person.name=2bai
*/
//请求体
InputStream inputStream=inputMessage.getBody();
Properties properties=new Properties();

//将请求中的内容转化成properties
properties.load(new InputStreamReader(inputStream,getDefaultCharset()));

//将properties内容转化到Person字段中
Person person=new Person();
person.setId(Long.valueOf(properties.getProperty("person.id")));
person.setName(properties.getProperty("person.name"));
return person;
}

/**
* 将POJO内容序列化成文本内容,最终输出到响应中
* @param person
* @param outputMessage
* @throws IOException
* @throws HttpMessageNotWritableException
*/
@Override
protected void writeInternal(Person person, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
OutputStream outputStream=outputMessage.getBody();
Properties properties=new Properties();
properties.setProperty("person.id",String.valueOf(person.getId()));
properties.setProperty("person.name",person.getName());
properties.store(new OutputStreamWriter(outputStream,getDefaultCharset()),"Written By Application Server");
}
}

WebMvcConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.bai.restonwebwebmvc.config;


import com.bai.restonwebwebmvc.http.message.PropertiesPersonHttpMessageConverter;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

/**
* @author 2bai
* @date 2017.11.28
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new PropertiesPersonHttpMessageConverter());
}
}

PersonRestController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package com.bai.restonwebwebmvc.controller;

import com.bai.restonwebwebmvc.domain.Person;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;

/**
* {@link } {@link org.springframework.web.bind.annotation.RestController}
* @author 2bai
* @date 2017.11.28
*/
@RestController
public class PersonRestController {

/**
* required = false,参数不强制传输
* @param id
* @param name
* @return
*/
@GetMapping("/person/{id}")
public Person person(@PathVariable Long id, @RequestParam(required = false) String name ){
Person person=new Person();
person.setId(id);
person.setName(name);
return person;
}

/**
* value : 路径
* consumes :请求类型 Content-Type
* produces :响应类型 Accept
* input :
* {
"id": 1,
"name": "2bai"
}
output:
#Written By Application Server
#Tue Nov 28 16:22:48 CST 2017
person.name=2bai
person.id=1

* @param person
* @return
*/
@PostMapping(value="/person/json/to/properties",
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE,
produces = "application/properties+person"
)
public Person personJsonToProperties(@RequestBody Person person){
//@RequestBody读取的是JSON格式
//响应的格式是proerties

return person;
}
/**
* value : 路径
* consumes :请求类型 Content-Type
* produces :响应类型 Accept
* input :
person.name=2bai
person.id=1
output:
{
"id": 1,
"name": "2bai"
}
* @param person
* @return
*/
@PostMapping(value="/person/properties/to/json",
consumes = "application/properties+person",
produces = MediaType.APPLICATION_JSON_UTF8_VALUE
)
public Person personPropertiesToJson(@RequestBody Person person){
//@RequestBody读取的是proerties
//响应的格式是JSON格式
return person;
}
}

其它

  • HttpMessageConverter执行逻辑
    • 读操作
      • 尝试是否能读取,canRead方法去尝试,如果返回true,下一步执行read
    • 写操作
      • 尝试是否能写入,canWrite方法去尝试,如果返回true,下一步执行write