跳至主要內容

SpringREST

soulballad微服务SpringCloud NetfilxSpringCloud约 1449 字大约 5 分钟

基本概念

REST= RESTful = Representational State Transfer,is one way of providing interoperability between computer systems on the Internet

https://en.wikipedia.org/wiki/Representational_state_transfer

SpringMVC

Rest 在 springMVC 中使用

SpringMVC 使用 REST

  1. 实体类

    public class Person {
    
        private Long id;
        private String name;
    
    	// get&set
    }
    
  2. RestController

    @RestController
    public class PersonRestController {
    
        @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;
        }
    }
    
  3. 浏览器访问

    1569989945986

  4. 可以看到当前 content-type 类型是 application/json

    1569990951355

修改媒体类型

springmvc 中有一个 @EnableWebMvc 注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}

上面有一个 DelegatingWebMvcConfiguration 的配置类

1569990214667

它继承自 WebMvcConfigurationSupport,在这个类里面加载各种类型

1569990297041

其中有一个 getDefaultMediaTypes 的方法

1569990342024

这个方法里面保存了可以使用的类型,如果要使用 xml,需要 jaxb2Present || jackson2XmlPresentjackson2XmlPresent 根据静态代码块中判断,需要 com.fasterxml.jackson.dataformat.xml.XmlMapper

1569990480171

在 pom.xml 中加入 jackso-dataformat-xml 的依赖

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

然后重新访问浏览器,发现响应信息变成了 xml 格式

1569990739023

1569990782173

ACCEPT 说明:

Accept:

text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3

q 表示权重,从左到右权重依次降低

第一优先顺序:text/html -> application/xhtml+xml -> application/xml

第二优先顺序:image/webp -> image/apng

第三优先顺序:signed-exchange

默认媒体类型源码分析

默认媒体类型是 application/json

org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#requestMappingHandlerAdapter->getMessageConverters)->addDefaultHttpMessageConverters->MappingJackson2HttpMessageConverter

1569992749793

所有的 HTTP 自描述消息处理器均在 messageConverters(类型:HttpMessageConverter),这个集合会传递到 RequestMappingHandlerAdapter,最终控制写出。

messageConverters,其中包含很多自描述消息类型的处理,比如 JSON、XML、TEXT等等

以 application/json 为例,Spring Boot 中默认使用 Jackson2 序列化方式,其中媒体类型:application/json,它的处理类 MappingJackson2HttpMessageConverter,提供两类方法:

  1. 读read* :通过 HTTP 请求内容转化成对应的 Bean
  2. 写write*: 通过 Bean 序列化成对应文本内容作为响应内容

媒体类型问题

  1. 问题:为什么第一次是JSON,后来增加了了 XML 依赖,又变成了 XML 内容输出?

    回答:Spring Boot 应用默认没有增加 XML 处理器(HttpMessageConverter)实现,所以最后采用轮训的方式去逐一尝试是否可以 canWrite(POJO) ,如果返回 true,说明可以序列化该 POJO 对象,那么 Jackson2 恰好能处理,那么Jackson 输出了。

    1. WebMvcConfiguration 中配置了 MessageConverter

      1570001314648

    2. 这些配置来自于 addDefaultHttpMessageConverters 方法

      1570001299468

    3. 每个messageConverter都要判断 canWrite,最后得到结果

      1570001357570

    4. 这里的结果再和 acceptTypes 判断,当前 accept = "*/*"

      1570001436391

    5. 然后根据权重进行排序,上图中

    6. 如果 selectedMediaType!=null,需要根据 messageConverters 重新判断 canWrite,并将最终结果write输出

      1570001690973

    7. selectedMediaType 来源,先判断 contentType,然后判断 MediaType,当前为 application/json

    1570001881915

    1. 所以最终以 application/json 的方式进行输出
  2. 问题:当 Accept 请求头未被指定时,为什么还是 JSON 来处理?

    回答:这个依赖于 messageConverters 的插入顺序。

  3. 问题:优先级是默认的是吧 可以修改吗?

    回答:是可以调整的,通过 extendMessageConverters 方法调整

    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
    
        public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    		
            System.err.println("converters: " + converters);
            // 扩展 messageConverter,在后面添加一个jackson
            converters.add(new MappingJackson2HttpMessageConverter());
            // 扩展 messageConverter,在首位添加一个jackson
            converters.set(0, new MappingJackson2HttpMessageConverter());
        }
    }
    

扩展媒体类型

Person

JSON 格式(application/json)

{
	"id":1,
	"name":"zhagnsan"
}

XML 格式(application/xml)

<Person>
    <id>1</id>
    <name>zhagnsan</name>
</Person>

Properties 格式(application/properties+person)

(需要扩展)

person.id = 1
person.name = zhagnsan

步骤:

  1. 实现 AbstractHttpMessageConverter 抽象类

    1. supports 方法:是否支持当前POJO类型
    2. readInternal 方法:读取 HTTP 请求中的内容,并且转化成相应的POJO对象(通过 Properties 内容转化成 JSON)
    3. writeInternal 方法:将 POJO 的内容序列化成文本内容(Properties格式),最终输出到 HTTP 响应中(通过 JSON 内容转化成 Properties )
    public class PropertiesPersonHttpMessageConverter extends AbstractHttpMessageConverter<Person> {
    
        public PropertiesPersonHttpMessageConverter() {
            super(MediaType.valueOf("application/properties+person"));
            setDefaultCharset(StandardCharsets.UTF_8);
        }
    
        @Override
        protected boolean supports(Class<?> clazz) {
            return clazz.isAssignableFrom(Person.class);
        }
    
        /**
         * 讲请求内容中 Properties 内容转化成 Person 对象
         *
         * @param clazz        Class
         * @param inputMessage 输入消息
         * @return
         * @throws IOException
         * @throws HttpMessageNotReadableException
         */
        @Override
        protected Person readInternal(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
            Person person = new Person();
            InputStream inputStream = inputMessage.getBody();
            Properties props = new Properties();
    
            // 将请求中的内容转化成Properties
            props.load(new InputStreamReader(inputStream, getDefaultCharset()));
            // 将properties 内容转化到 Person 对象字段中
            person.setId(Long.valueOf(props.getProperty("person.id")));
            person.setName(props.getProperty("person.name"));
            return person;
        }
    
        /**
         * 响应信息中的Person对象转化成 Properties
         *
         * @param person        Person
         * @param outputMessage 输出消息
         * @throws IOException
         * @throws HttpMessageNotWritableException
         */
        @Override
        protected void writeInternal(Person person, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
            OutputStream outputStream = outputMessage.getBody();
            Properties props = new Properties();
    
            props.setProperty("person.id", String.valueOf(person.getId()));
            props.setProperty("person.name", person.getName());
    
            props.store(new OutputStreamWriter(outputStream, getDefaultCharset()), "Written by web server");
        }
    }
    
  2. 启用自定义 MessageConverter

    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
    
        public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    		// 扩展 MessageConverter,添加 PropertiesPersonHttpMessageConverter
            converters.add(new PropertiesPersonHttpMessageConverter());
        }
    }
    
  3. 使用 rest 请求指定 mediaType

    • @RequestMappng 中的 consumes 对应 请求头 “Content-Type”
    • @RequestMappng 中的 produces 对应 请求头 “Accept”
    @PostMapping(value = "/person/json/to/properties", 
                 consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, 
                 produces = "application/properties+person")
    public Person personJsonToProperties(@RequestBody Person person) {
        // 请求的内容是 JSON
        // 响应的内容是 Properties
        return person;
    }
    
    @PostMapping(value = "/person/properties/to/json", 
                 consumes = "application/properties+person", 
                 produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public Person personPropertiesToJson(@RequestBody Person person) {
        // 请求的内容是 Properties
        // 响应的内容是 JSON
        return person;
    }
    
  4. 使用 postman 测试

    • 请求类型是 application/json;charset=UTF-8,响应类型是 application/properties+person

      1570006077489

    • 请求类型是 application/properties+person,响应类型是 application/json;charset=UTF-8

      1570006211526

HttpMessageConverter 执行逻辑:

  • 读操作:尝试是否能读取,canRead 方法去尝试,如果返回 true 下一步执行 read
  • 写操作:尝试是否能写入,canWrite 方法去尝试,如果返回 true 下一步执行 write
上次编辑于:
贡献者: soulballad