解析Spring中的ResponseBody和RequestBody

spring,restful,前后端分离这些关键词都是大家耳熟能详的关键词了,一般spring常常需要与前端、第三方使用JSON,XML等形式进行交互,你也一定不会对@RequestBody和@ResponseBody这两个注解感到陌生。

@ResponseBody的使用

由于@ResponseBody和@RequestBody的内部实现是同样的原理(封装请求和封装响应),所以本文以@ResponseBody为主要入手点,理解清楚任何一者,都可以同时掌握另一者。

如果想要从spring获得一个json形式返回值,操作起来是非常容易的。首先定义一个实体类:

public class Book {
    private Integer id;
    private String bookName;
}

接着定义一个后端端点:

@RestController
public class BookController {

    @GetMapping(value = "/book/{bookId}")
    public Book getBook(@PathVariable("bookId") Integer bookId) {
        return new Book(bookId, "book" + bookId);
    }

}

在RestController中,相当于给所有的xxxMapping端点都添加了@ResponseBody注解,不返回视图,只返回数据。使用http工具访问这个后端端点 localhost:8080/book/2,便可以得到如下的响应:

{
    "id": 2,
    "bookName": "book2"
}

这是一个最简单的返回JSON对象的使用示例了,相信这样的代码很多人在项目中都写过。

添加XML解析

如果我们需要将Book对象以XML的形式返回,该如何操作呢?这也很简单,给Book对象添加@XmlRootElement注解,让spring内部能够解析XML对象。

@XmlRootElement
public class Book {
    private Integer id;
    private String bookName;
}

在我们未对web层的BookController做任何改动之前,尝试访问 localhost:8080/book/2时,会发现得到的结果仍然是前面的JSON对象。这也能够理解,因为Book对象如今既可以被解析为XML,也可以被解析为JSON,我们隐隐察觉这背后有一定的解析顺序关系,但不着急,先看看如何让RestController返回XML解析结果。

方法1 http客户端指定接收的返回结果类型

http协议中,可以给请求头添加Accept属性,笔者常用的http客户端是idea自带的Test RESTful Web Service以及chrome的插件Postman。简单的调试,前者基本可以满足我们大多数的需求,而这里为了给大家更直观的体验,笔者使用了Postman。以code形式展示:

GET /book/2 HTTP/1.1
Host: localhost:8080
Accept: application/xml

响应内容如下:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<book>
    <bookName>book2</bookName>
    <id>2</id>
</book>

方法2 在RestController后端端点中指定返回类型

修改后的RestController如下所示

@RestController
public class BookController {

    @GetMapping(value = "/book/{bookId}", produces = {"application/xml"})
    public Book getBook(@PathVariable("bookId") Integer bookId) {
        return new Book(bookId, "book" + bookId);
    }

}

此时即使将请求中的 Accept:application/xml去除,依旧可以返回上述的XML结果。

通常情况下,我们的服务端返回的形式一般是固定的,即限定了是JSON,XML中的一种,不建议依赖于客户端添加Accept的信息,而是在服务端限定produces类型。

详解Accpect与produces

Accpect包含在http协议的请求头中,其本身代表着客户端发起请求时,期望返回的响应结果的媒体类型。如果服务端可能返回多个媒体类型,则可以通过Accpect指定具体的类型。

produces是Spring为我们提供的注解参数,代表着服务端能够支持返回的媒体类型,我们注意到produces后跟随的是一个数组类型,也就意味着服务端支持多种媒体类型的响应。

在上一节中,我们未显示指定produces值时,其实就隐式的表明,支持XML形式,JSON形式的媒体类型响应。从实验结果,我们也可以看出,当请求未指定Accpect,响应未指定produces时,具体采用何种形式返回是有Spring控制的。在接口交互时,最良好的对接方式,当然是客户端指定Accpect,服务端指定produces,这样可以避免模棱两可的请求响应,避免出现意想不到的对接结果。

详解ContentType与consumes

恰恰和Accpect&produces相反,这两个参数是与用于限制请求的。理解了前两者的含义,这两个参数可以举一反三理解清楚。

ContentType包含在http协议的请求头中,其本身代表着客户端发起请求时,告知服务端自己的请求媒体类型是什么。

consumes是Spring为我们提供的注解参数,代表着服务端能够支持处理的请求媒体类型,同样是一个数组,意味着服务端支持多种媒体类型的请求。一般而言,consumes与produces对请求响应媒体类型起到的限制作用,我们给他一个专有名词:窄化。

http请求响应媒体类型一览

上面描述的4个属性:Accpect与produces,ContentType与consumes究竟有哪些类型与之对应呢?我只将常用的一些列举了出来:

媒体类型

含义

text/html

HTML格式

text/plain

纯文本格式

text/xml, application/xml

XML数据格式

application/json

JSON数据格式

image/gif

gif图片格式

image/png

png图片格式

application/octet-stream

二进制流数据

application/ x-www-form-urlencoded

form表单数据

multipart/form-data

含文件的form表单

其中有几个类型值得一说,web开发中我们常用的提交表单操作,其默认的媒体类型就是application/ x-www-form-urlencoded,而当表单中包含文件时,大家估计都踩过坑,需要将enctype=multipart/form-data设置在form参数中。text/html也就是常见的网页了,json与xml常用于数据交互,其他不再赘述。

而在JAVA中,提供了MediaType这样的抽象,来与http的媒体类型进行对应。‘/’之前的名词,如text,application被称为类型(type),‘/’之后被称为子类型(subType)。

详解HttpMessageConverter

我们想要搞懂Spring到底如何完成众多实体类等复杂类型的数据转换以及与媒体类型的对应,就必须要搞懂HttpMessageConverter这个顶级接口:

public interface HttpMessageConverter<T> {
    boolean canRead(Class<?> var1, MediaType var2);

    boolean canWrite(Class<?> var1, MediaType var2);

    List<MediaType> getSupportedMediaTypes();

    T read(Class<? extends T> var1, HttpInputMessage var2) throws IOException, HttpMessageNotReadableException;

    void write(T var1, MediaType var2, HttpOutputMessage var3) throws IOException, HttpMessageNotWritableException;
}

大致能看出Spring的处理思路。下面的流程图可以更好方便我们的理解:

对于添加了@RequestBody和@ResponseBody注解的后端端点,都会经历由HttpMessageConverter进行的数据转换的过程。而在Spring启动之初,就已经有一些默认的转换器被注册了。通过在 RequestResponseBodyMethodProcessor 中打断点,我们可以获取到一个converters列表:

源码方面不做过多的解读,有兴趣的朋友可以研究一下 RequestResponseBodyMethodProcessor 中的handleReturnValue方法,包含了转换的核心实现。

自定义HttpMessageConverter

前面已经提及了消息转换器是通过判断媒体类型来调用响应的转换类的,不禁引发了我们的思考,如果我们遇到了不常用的MediaType,或者自定义的MediaType,又想要使用Spring的@RequestBody,@ResponseBody注解,该如何添加代码呢?下面我们通过自定义一个HttpMessageConverter来了解Spring内部的转换过程。

先定义我们的需求,自定一个MediaType:application/toString,当返回一个带有@ResponseBody注解的实体类时,将该实体类的ToString作为响应内容。

1 首先重写Book的ToString方法,方便后期效果展示

@Override
public String toString() {
    return "~~~Book{" +
            "id=" + id +
            ", bookName='" + bookName + '\'' +
            "}~~~";
}

2 编写自定义的消息转换器

public class ToStringHttpMessageConverter extends AbstractHttpMessageConverter<Object> {

    public ToStringHttpMessageConverter() {
        super(new MediaType("application", "toString", Charset.forName("UTF-8")));// <1>
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return true;
    }

    //从请求体封装数据 对应RequestBody 用String接收
    @Override
    protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        return StreamUtils.copyToString(inputMessage.getBody(), Charset.forName("UTF-8"));
    }

    //从响应体封装数据 对应ResponseBody
    @Override
    protected void writeInternal(Object o, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        String result = o.toString();//<2>
        outputMessage.getBody().write(result.getBytes());
    }
}

<1> 此处指定了支持的媒体类型

<2> 调用类的ToString方法,将结果写入到输出流中

3 配置自定义的消息转换器

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter{

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

4 配置后端端点,指定生产类型

@RestController
public class BookController {

    @GetMapping(value = "/book/{bookId}",produces = {"application/toString","application/json","application/xml"})
    public Book getBook(@PathVariable("bookId") Integer bookId) {
        return new Book(bookId, "book" + bookId);
    }
}

此处只是为了演示,添加了三个生产类型,我们的后端端点可以支持输出三种类型,而具体输出哪一者,则依赖客户端的Accept指定。

5 客户端请求

GET /book/2 HTTP/1.1
Host: localhost:8080
Accept: application/toString

响应结果如下:

~~~Book{id=2, bookName='book2'}~~~

此时,你可以任意指定Accept的类型,即可获得不同形式的Book返回结果,可以是application/toString,application/json,application/xml,都会对应各自的HttpMessageConverter。

原文发布于微信公众号 - Kirito的技术分享(cnkirito)

原文发表时间:2017-09-07

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Android相关

Gradle For Android(7)--创建Task以及Plugin

到目前为止,我们已经看到了很多Gradle构建的属性,并且知道了怎么去执行Tasks。这一章,会更多的了解这些属性,并且创建我们自己的Task。一旦知道如何自定...

1482
来自专栏程序猿DD

【译】Spring 官方教程:创建批处理服务

原文:Creating a Batch Service 译者:Mr.lzc 校对:lexburner 本指南将引导你完成创建基本的批处理驱动解决方案的过程。 你...

5407
来自专栏杨建荣的学习笔记

一些“简单”的linux命令(r2笔记46天)

有些linux命令看起来极其简单,只包含2个字符,但确有很强的功能性。看起来还是有些陌生的命令,不过在工作中别忘记它们的存在。 ab 这条命令式做为性能测试所...

3038
来自专栏CodeSheep的技术分享

初探Kotlin+SpringBoot联合编程

Kotlin是一门最近比较流行的静态类型编程语言,而且和Groovy、Scala一样同属Java系。Kotlin具有的很多静态语言特性诸如:类型判断、多范式、扩...

76314
来自专栏Java技术栈

Java 必看的 Spring 知识汇总!

2233
来自专栏程序猿DD

Spring Boot 2.0 新特性(一):配置绑定 2.0 全解析

在Spring Boot 2.0中推出了Relaxed Binding 2.0,对原有的属性绑定功能做了非常多的改进以帮助我们更容易的在Spring应用中加载和...

3884
来自专栏机器学习从入门到成神

临界区、互斥量、信号量

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sinat_35512245/articl...

1972
来自专栏极客编程

用Java为Hyperledger Fabric(超级账本)开发区块链智能合约链代码之部署与运行示例代码

您已经定义并启动了本地区块链网络,而且已构建 Java shim 客户端 JAR 并安装到本地 Maven 存储库中,现在已准备好在之前下载的 Hyperled...

2121
来自专栏菩提树下的杨过

JAVA CDI 学习(1) - @Inject基本用法

CDI(Contexts and Dependency Injection 上下文依赖注入),是JAVA官方提供的依赖注入实现,可用于Dynamic Web M...

2032
来自专栏美团技术团队

这个Spring高危漏洞,你修补了吗?

前言 2009年9月Spring 3.0 RC1发布后,Spring就引入了SpEL(Spring Expression Language)。对于开发者而言,引...

1.3K11

扫码关注云+社区

领取腾讯云代金券