RESTful接⼝实现与测试


RESTful 接⼝实现与测试

1. RESTful 接⼝与 http 协议状态表述

⼀、RESTful ⻛格 API 的好处

API(Application Programming Interface),顾名思义:是⼀组编程接⼝规范,客户端与服务端通过请求响应进⾏数据通信。REST(Representational State Transfer)表述性状态传递,决定了接⼝的形式与规则。RESTful 是基于 http ⽅法的 API 设计⻛格,⽽不是⼀种新的技术。

  1. 看 Url 就知道要什么资源
  2. 看 http method 就知道针对资源⼲什么
  3. 看 http status code 就知道结果如何

对接⼝开发提供了⼀种可以⼴泛适⽤的规范,为前端后端交互减少了交流的成本,是约定⼤于配置的体现。

当然也不是所有的接⼝,都能⽤ REST 的形式来表述。要在实际⼯作中,灵活运⽤。 我们⽤ RESTful ⻛格的⽬的是为⼤家提供统⼀标准,避免不必要的沟通成本的浪费,形成⼀种 通⽤的⻛格。但也不是绝对适⽤任何场景。

⼆、RESTful API 的设计⻛格

2.1 RESTful 是⾯向资源的(名词)

REST 通过 URI 暴露资源时,会强调不要在 URI 中出现动词。⽐如:

不符合 REST 的接⼝ URI 符合 REST 接⼝ URI 功能
GET /api/getDogs/{id} GET /api/dogs/{id} 获取⼀个⼩狗
GET /api/getDogs GET /api/dogs 获取所有⼩狗
GET /api/addDogs POST /api/dogs 添加⼀个⼩狗
GET /api/editDogs/{id} PUT /api/dogs/{id} 修改⼀个⼩狗
GET /api/deleteDogs/{id} DELETE /api/dogs/{id} 删除⼀个⼩狗

2.2 ⽤ HTTP ⽅法体现对资源的操作(动词)

  • GET : 获取、读取资源
  • POST : 添加资源
  • PUT : 修改资源
  • DELETE : 删除资源

实际上,这四个动词实际上就对应着增删改查四个操作,利⽤了 HTTP 动词来表示对资源的操作。

2.3 HTTP 状态码

通过 HTTP 状态码体现动作的结果,不要⾃定义 200 OK 400 Bad Request 500 Internal Server Error

在 APP 与 API 的交互当中,其结果逃不出这三种状态:

  • 所有事情都按预期正确执⾏完毕 - 成功
  • APP 发⽣了⼀些错误 - 客户端错误(如:校验⽤户输⼊身份证,结果输⼊的是军官证, 就是客户端输⼊错误)
  • API 发⽣了⼀些错误 – 服务器端错误(各种编码 bug 或服务内部⾃⼰导致的异常)

这三种状态与上⾯的状态码是⼀⼀对应的。如果你觉得这三种状态,分类处理结果太宽泛, http-status code 还有很多。建议还是要遵循 KISS(Keep It Stupid and Simple) 原则,上⾯的三种状态码完全可以覆盖 99%以上的场景。这三个状态码⼤家都记得住,⽽且⾮常常⽤,多了就不⼀定了。

2.4 Get ⽅法和查询参数不应该改变数据

改变数据的事交给 POST、PUT、DELETE

2.5 使⽤复数名词

/dogs ⽽不是 /dog

2.6 复杂资源关系的表达

GET /cars/711/drivers/ 返回使⽤过编号 711 汽⻋的所有司机

GET /cars/711/drivers/4 返回使⽤过编号 711 汽⻋的 4 号司机

2.7 ⾼级⽤法:HATEOAS

HATEOAS:Hypermedia as the Engine of Application State 超媒体作为应⽤状态的引擎。 RESTful API 最好做到 HATEOAS ,即返回结果中提供链接,连向其他 API ⽅法,使得⽤户不查⽂档,也知道下⼀步应该做什么。⽐如,当⽤户向 api.example.com 的根⽬录发出请求,会得到这样⼀个⽂档。

{"link": { "rel": "collection https://www.example.com/zoos", "href": "https://api.example.com/zoos", "title": "List of zoos", "type": "application/vnd.yourformat+json" }}

上⾯代码表示,⽂档中有⼀个 link 属性,⽤户读取这个属性就知道下⼀步该调⽤什么 API 或者可以调⽤什么 API 了。

2.8 资源过滤、排序、选择和分⻚的表述

2.9 版本化你的 API

强制性增加 API 版本声明,不要发布⽆版本的 API。如:/api/v1/blog

⾯向扩展开放,⾯向修改关闭:也就是说⼀个版本的接⼝开发完成测试上线之后,我们⼀般不会对接⼝进⾏修改,如果有新的需求就开发新的接⼝进⾏功能扩展。这样做的⽬的是:当你的新接⼝上线后,不会影响使⽤⽼接⼝的⽤户。如果新接⼝⽬的是替换⽼接⼝,也不要在 v1 版本原接⼝上修改,⽽是开发 v2 版本接⼝,并声明 v1 接⼝废弃!

三、参考

关于 HTTP RESTful ⻛格 API 设计的更多例⼦,请⼤家参考:http://httpbin.org/

2. Spring 常⽤注解及基础讲解

⼀、HTTP 协议的四种传参⽅式

HTTP 协议组成 协议内容示例 对应 Spring 注解
path info 传参 /articles/12 (查询 id 为 12 的⽂章,12 是参数) @PathVariable
URL Query String 传参 /articles?id=12 @RequestParam
Body 传参 Content-Type: multipart/formdata @RequestParam
Body 传参 Content-Type: application/json,或其他⾃定义格式 @RequestBody
Headers 传参 @RequestHeader

⼆、常⽤注解回顾

2.1 @RequestBody 与@ResponseBody

注意并不要求 @RequestBody 与 @ResponseBody 成对使⽤。

public @ResponseBody  AjaxResponse saveArticle(@RequestBody ArticleVO article)

如上代码所示:

  • @RequestBody 修饰请求参数,注解⽤于接收 HTTP 的 body ,默认是使⽤ JSON 的格式
  • @ResponseBody 修饰返回值,注解⽤于在 HTTP 的 body 中携带响应数据,默认是使⽤ JSON 的 格式。如果不加该注解,spring 响应字符串类型,是跳转到模板⻚⾯或 jsp ⻚⾯的开发模式。说 ⽩了:加上这个注解你开发的是⼀个数据接⼝,不加这个注解你开发的是⼀个⻚⾯跳转控制器。

在使⽤ @ResponseBody 注解之后程序不会再⾛视图解析器,也就不再做 html 视图渲染,⽽是直接将对象以数据的形式(默认 JSON)返回给请求发送者。那么我们有⼀个问题:如果我们想接收或 XML 数据该怎么办?我们想响应 excel 的数据格式该怎么办?我们后⽂来回答这个问题。

2.2 @RequestMapping 注解

@RequestMapping 注解是所有常⽤注解中,最有看点的⼀个注解,⽤于标注 HTTP 服务端点。它的 很多属性对于丰富我们的应⽤开发⽅式⽅法,都有很重要的作⽤。如:

  • value:应⽤请求端点,最核⼼的属性,⽤于标志请求处理⽅法的唯⼀性;
  • method:HTTP 协议的 method 类型, 如:GET、POST、PUT、DELETE 等;
  • consumes:HTTP 协议请求内容的数据类型(Content-Type),例如 application/json, text/html;
  • produces: HTTP 协议响应内容的数据类型。下⽂会详细讲解。
  • params: HTTP 请求中必须包含某些参数值的时候,才允许被注解标注的⽅法处理请求。
  • headers: HTTP 请求中必须包含某些指定的 header 值,才允许被注解标注的⽅法处理请求。

@RequestMapping(value = “/article”, method = POST) @PostMapping(value = “/article”) 上⾯代码中两种写法起到的是⼀样的效果,也就是 PostMapping 等同于 @RequestMapping 的 method 等于 POST 。同理:@GetMapping、@PutMapping、@DeleteMapping 也都是简写的⽅式。

2.3 @RestController 与@Controller

@Controller 注解是开发中最常使⽤的注解,它的作⽤有两层含义:

  • ⼀是告诉 Spring,被该注解标注的类是⼀个 Spring 的 Bean,需要被注⼊到 Spring 的上下⽂环境中。
  • ⼆是该类⾥⾯所有被 RequestMapping 标注的注解都是 HTTP 服务端点。

@RestController 相当于 @Controller 和 @ResponseBody 结合。它有两层含义:

  • ⼀是作为 Controller 的作⽤,将控制器类注⼊到 Spring 上下⽂环境,该类 RequestMapping 标注⽅法为 HTTP 服务端点。
  • ⼆是作为 ResponseBody 的作⽤,请求响应默认使⽤的序列化⽅式是 JSON ,⽽不是跳转到 jsp 或模板⻚⾯。

2.4 @PathVariable 与@RequestParam

PathVariable ⽤于 URI 上的{参数},如下⽅法⽤于删除⼀篇⽂章,其中 id 为⽂章 id 。如:我们的请求 URL 为“/article/1”,那么将匹配 DeleteMapping 并且 PathVariable 接收参数 id=1。⽽ RequestParam ⽤于接收普通表单⽅式或者 ajax 模拟表单提交的参数数据。

@DeleteMapping("/article/{id}")
	public @ResponseBody AjaxResponse deleteArticle(@PathVariable Long id) {
}
@PostMapping("/article")
	public @ResponseBody AjaxResponse deleteArticle(@RequestParam Long id) {
}

三、接收复杂嵌套对象参数

有的同学可能还⽆法理解 RequestBody 注解存在的真正意义,表单数据提交⽤ RequestParam 就好 了,为什么还要搞出来⼀个 RequestBody 注解呢?

RequestBody 注解的真正意义在于能够使⽤对象或者嵌套对象接收前端数据

仔细看上⾯的代码,是⼀个 paramData 对象⾥⾯包含了⼀个 bestFriend 对象。这种数据结构使⽤ RequestParam 就⽆法接收了,RequestParam 只能接收平⾯的、⼀对⼀的参数。

像这种嵌套的数据结构的参数,就需要我们在 Java 服务端定义两个类,⼀个类是 ParamData ,⼀个 类是 BestFriend 。

public class ParamData {
    private String name;
    private int id;
    private String phone;
    private BestFriend bestFriend;

    public static class BestFriend {
        private String address;
        private String gender;
   }
}
  • 注意上⾯代码中省略了 GET、SET ⽅法等必要的 Java plain model 元素。
  • 注意成员变量名称⼀定要和 JSON 属性名称对应上。
  • 注意接收不同类型的参数,使⽤不同的成员变量类型。

完成以上动作,我们就可以使⽤ @RequestBody ParamData paramData ,⼀次性的接收以上所有的复杂嵌套对象参数了,参数对象的所有属性都将被赋值。

四、HTTP 数据转换的原理

现在使⽤ JSON 已经⽐较普遍了,其⽅便易⽤、表达能⼒强,是绝⼤部分接⼝应⽤数据的⾸选。 那么如何响应其他类型的数据?其中的判别原理⼜是什么?

  • 当⼀个 HTTP 请求到达时是⼀个 InputStream ,通过 HttpMessageConverter 转换为 Java 对象, 从⽽进⾏参数接收。
  • 当对⼀个 HTTP 请求进⾏响应时,我们⾸先输出的是⼀个 java 对象,然后由 HttpMessageConverter 转换为 OutputStream 输出。

当我们在 Spring Boot 应⽤中集成了 jackson 的类库之后,如下的⼀些 HttpMessageConverter 将会被加载。

实现类 功能说明
StringHttpMessageConverter 将请求信息转为字符串
FormHttpMessageConverter 将表单数据读取到 MultiValueMap 中
XmlAwareFormHttpMessageConverter 扩展与 FormHttpMessageConverter ,如果部分表单属性是 XML 数据,可⽤该转换器进⾏读取
ResourceHttpMessageConverter 读写 org.springframework.core.io.Resource 对象
BufferedImageHttpMessageConverter 读写 BufferedImage 对象
ByteArrayHttpMessageConverter 读写⼆进制数据
SourceHttpMessageConverter 读写 java.xml.transform.Source 类型的对象
MarshallingHttpMessageConverter 通过 Spring 的 org.springframework,xml.Marshaller 和 Unmarshaller 读写 XML 消息
Jaxb2RootElementHttpMessageConver ter 通过 JAXB2 读写 XML 消息,将请求消息转换为标注的 XmlRootElement 和 XmlType 连接的类中
MappingJacksonHttpMessageConverte r 利⽤ Jackson 开源包的 ObjectMapper 读写 JSON 数据
RssChannelHttpMessageConverter 读写 RSS 种⼦消息
AtomFeedHttpMessageConverter 和 RssChannelHttpMessageConverter 能够读写 RSS 种⼦消息

根据 HTTP 协议的 Accept 和 Content-Type 属性,以及参数数据类型来判别使⽤哪⼀种 HttpMessageConverter 。当使⽤ RequestBody 或 ResponseBody 时,再结合前端发送的 Accept 数据类型,会⾃动判定优先使⽤ MappingJacksonHttpMessageConverter 作为数据转换器。但是,不仅 JSON 可以表达对象数据类型,XML 也可以。如果我们希望使⽤ XML 格式该怎么告知 Spring 呢,那就要使⽤到 produces 属性了。

@GetMapping(value ="/demo",produces = MediaType.APPLICATION_XML_VALUE)

这⾥我们明确的告知了返回的数据类型是 xml ,就会使⽤ Jaxb2RootElementHttpMessageConverter 作为默认的数据转换器。当然实现 XML 数据响应⽐ JSON 还会更复杂⼀些,还需要结合 @XmlRootElement、@XmlElement 等注解实体类来使⽤,同理 consumes 属性你也就会使⽤了。

五、⾃定义 HttpMessageConverter

其实绝⼤多数的数据格式都不需要我们⾃定义 HttpMessageConverter ,都有第三⽅类库可以帮助 我们实现(包括下⽂代码中的 Excel 格式)。但有的时候,有些数据的输出格式并没有类似于 Jackson 这种类库帮助我们处理,需要我们⾃定义数据格式。该怎么做?

下⾯我们就以 Excel 数据格式为例,写⼀个⾃定义的 HTTP 类型转换器。 实现的效果就是,当我们返回 AjaxResponse 这种数据类型,就⾃动将 AjaxResponse 转成 Excel 数据响应给客户端。

<dependency>
   <groupId>org.apache.poi</groupId>
   <artifactId>poi-ooxml</artifactId>
   <version>3.9</version>
</dependency>
@Service
public class ResponseToXlsConverter extends
AbstractHttpMessageConverter<AjaxResponse> {
    private static final MediaType EXCEL_TYPE = MediaType.valueOf("application/vnd.ms-excel");
    ResponseToXlsConverter() {
        super(EXCEL_TYPE);
   }
    @Override
    protected AjaxResponse readInternal(final Class<? extends AjaxResponse> clazz,
                                final HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {
        return null;
   }
    //针对AjaxResponse类型返回值,使⽤下⾯的writeInternal⽅法进⾏消息类型转换
    @Override
    protected boolean supports(final Class<?> clazz) {
        return (AjaxResponse.class == clazz);
   }
    @Override
    protected void writeInternal(final AjaxResponse ajaxResponse, final HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException {
        final Workbook workbook = new HSSFWorkbook();
        final Sheet sheet = workbook.createSheet();
        final Row row = sheet.createRow(0);
        row.createCell(0).setCellValue(ajaxResponse.getMessage());
				row.createCell(1).setCellValue(ajaxResponse.getData().toString());
        workbook.write(outputMessage.getBody());
   }
}
  • 实现 AbstractHttpMessageConverter 接⼝
  • 指定该转换器是针对哪种数据格式的?如上⽂代码中的 “application/vnd.ms-excel”
  • 指定该转换器针对那些对象数据类型?如上⽂代码中的 supports 函数
  • 使⽤ writeInternal 对数据进⾏输出处理,上例中是输出为 Excel 格式。

3. 使⽤注解开发 RESTful 接⼝

⼀、 定义资源(对象)

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Article {
    private Long id;
    private String author;
    private String title;
    private String content;
    private Date createTime;
    private List<Reader> readerList;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Reader implements Serializable {
    private String name;
    private Integer age;
}

Data、Builder 等都是 lombok 提供给我们的注解,有利于我们简化代码。

  • @Builder 为我们提供了通过对象属性的链式赋值构建对象的⽅法。
  • @Data 注解帮我们定义了⼀系列常⽤⽅法,如:getters、setters、hashcode、equals 等。

⼆、统⼀规范接⼝响应的数据格式

下⾯这个类是⽤于统⼀数据响应接⼝标准的。它的作⽤是:统⼀所有开发⼈员响应前端请求的返回结果格式,减少前后端开发⼈员沟通成本,是⼀种 RESTful 接⼝标准化的开发约定。下⾯代码只对请求成功的情况进⾏封装,异常处理相关后续再做详细说明。

@Data
public class AjaxResponse {
  private boolean isok;  //请求是否处理成功
  private int code; //请求响应状态码(200、400、500)
  private String message;  //请求结果描述信息
  private Object data; //请求结果数据(通常⽤于查询操作)

  private AjaxResponse(){}

  //请求成功的响应,不带查询数据(⽤于删除、修改、新增接⼝)
  public static AjaxResponse success(){
    AjaxResponse ajaxResponse = new AjaxResponse();
    ajaxResponse.setIsok(true);
    ajaxResponse.setCode(200);
    ajaxResponse.setMessage("请求响应成功!");
    return ajaxResponse;
  }

  //请求成功的响应,带有查询数据(⽤于数据查询接⼝)
  public static AjaxResponse success(Object obj){
    AjaxResponse ajaxResponse = new AjaxResponse();
    ajaxResponse.setIsok(true);
    ajaxResponse.setCode(200);
    ajaxResponse.setMessage("请求响应成功!");
    ajaxResponse.setData(obj);
    return ajaxResponse;
  }

  //请求成功的响应,带有查询数据(⽤于数据查询接⼝)
  public static AjaxResponse success(Object obj,String message){
    AjaxResponse ajaxResponse = new AjaxResponse();
    ajaxResponse.setIsok(true);
    ajaxResponse.setCode(200);
    ajaxResponse.setMessage(message);
    ajaxResponse.setData(obj);
    return ajaxResponse;
  }
}

三、编写 Controller

我们来实现⼏个简单的 RESTful 接⼝

  • 增加⼀篇 Article ,使⽤ POST ⽅法
  • 删除⼀篇 Article ,使⽤ DELETE ⽅法,参数是 id
  • 更新⼀篇 Article ,使⽤ PUT ⽅法,以 id 为主键进⾏更新
  • 获取⼀篇 Article ,使⽤ GET ⽅法

下⾯代码中并未真正的进⾏数据库操作,后续结合 Mybatis 和 JPA 再做补充。

package top.syhan.boot.restful.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mqxu.boot.restful.common.AjaxResponse;
import com.mqxu.boot.restful.model.Article;
import com.mqxu.boot.restful.model.Reader;
import lombok.extern.slf4j.Slf4j;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.List;
/**
* @description: ArticleController
* @author: syhan
* @date: 2022-03-10
**/
@RestController
@Slf4j
@RequestMapping("/api/v1/articles")
public class ArticleController {
    /**
     * 使⽤GET⽅法,根据路径参数id查询⼀篇⽂章: GET /api/v1/articles/123
     *
     * @param id id
     * @return AjaxResponse
     */
    @GetMapping("{id}")
    public AjaxResponse getArticle(@PathVariable("id") Long id) {
        List<Reader> readerList = List.of(Reader.builder().name("aaa").age(12).build(),
				Reader.builder().name("bbb").age(13).build());
        Article article = Article.builder()
               .id(id)
               .author("mqxu")
               .content("SpringBoot 从⻘铜到王者")
               .title("SpringBoot")
               .readerList(readerList)
               .createTime(new Date()).build();
        log.info("article:" + article);
        return AjaxResponse.success(article);
    }

    /**
     * 使⽤GET⽅法,根据url传参⽅式,获取到id查询⼀篇⽂章: GET /api/v1/articles?id=123
     *
     * @param id id
     * @return AjaxResponse
     */
    @GetMapping()
    public AjaxResponse getArticleByParam(@RequestParam("id") long id) {
        List<Reader> readerList = List.of(Reader.builder().name("aaa").age(12).build(),
				Reader.builder().name("bbb").age(13).build());
        Article article = Article.builder()
               .id(id)
               .author("mqxu")
               .content("SpringBoot 从⻘铜到王者")
               .title("SpringBoot")
               .readerList(readerList)
               .createTime(new Date()).build();
        log.info("article:" + article);
        return AjaxResponse.success(article);
    }

    /**
     * 使⽤ GET⽅法,查询所有⽂章: GET /api/v1/articles/all
     *
     * @return AjaxResponse
     */
    @GetMapping("all")
    public AjaxResponse selectAll() {
        List<Reader> readerList = List.of(Reader.builder().name("aaa").age(12).build(),
				Reader.builder().name("bbb").age(13).build());
        Article article = Article.builder()
               .id(111L)
               .author("mqxu")
               .content("SpringBoot")
               .title("SpringBoot")
               .readerList(readerList)
               .createTime(new Date())
               .build();
        Article article2 = Article.builder()
               .id(222L)
               .author("mqxu")
               .content("Java")
               .title("Java")
               .readerList(readerList)
							 .createTime(new Date())
               .build();
        return AjaxResponse.success(List.of(article, article2));
    }

    /**
     * 使⽤POST⽅法(RequestBody⽅式接收参数),增加⼀篇Article : POST/api/v1/articles/body 参数在请求体中⽤JSON对象
     *
     * @param article article
     * @return AjaxResponse
     */

因为使⽤了 lombok 的 @Slf4j 注解(类的定义处),就可以直接使⽤ log 变量打印⽇志。不需要写下⾯的这⾏代码。

private static final Logger log = LoggerFactory.getLogger(HelloController.class);

四、Postman 测试

4. JSON 数据处理与 PostMan 测试

⼀、 FastJSON、Gson 和 Jackson 对⽐

开源的 Jackson:SpringBoot 默认是使⽤ Jackson 作为 JSON 数据格式处理的类库,Jackson 在各⽅⾯都⽐较优秀,所以不建议将 Jackson 替换为 Gson 或 fastjson。

Google 的 Gson:Gson 是 Google 为满⾜内部需求开发的 JSON 数据处理类库,其核⼼结构⾮常简单,toJson 与 fromJson 两个转换函数实现对象与 JSON 数据的转换。

阿⾥巴巴的 FastJson:Fastjson 是阿⾥巴巴开源的 JSON 数据处理类库,其主要特点是序列化速度快。当并发数据量越⼤的时候,越能体现出 fastjson 的优势。但选择 JSON 处理类库,快并不是唯⼀ 需要考虑的因素,与数据库或磁盘 IO 相⽐,JSON 数据序列化与反序列化的这点时间还不⾜以对软件性能产⽣⽐较⼤的影响。

性能⽐较:关于这三个类库的性能测试(截⽌ 2019 年 11 ⽉ 20 ⽇)总结如下:

  • 序列化过程性能:fastjson >= jackson > Gson
  • 反序列化性能:三者⼏乎不相上下。

fastjson 为⼈诟病的问题:虽然 fastjson 速度上有⼀定的优势,但是其为了追求速度,很⼤程度放弃了 JSON 的规范性。因此时不时在有些版本中暴露安全问题。所以⽤不⽤ fastjson 在国内软件界 还是有争议的,在国外基本没⼈⽤。

⼆、在 Spring 中注解⽅法使⽤ Jackson

不建议将 Jackson 替换为 Gson 或 fastjson 。Jackson 主要的作⽤是:

什么叫序列化与反序列化?说⽩了就是把对象转成可传输、可存储的格式( json、xml、⼆进制、甚⾄⾃定义格式)叫做序列化。反序列化顾名思义。

  • 反序列化:在客户端将请求数据上传到服务端的时候,⾃动的处理 JSON 数据对象中的字符串、 数字,将其转换为包含 Date 类型、Integer 等类型的对象。
  • 序列化:按照指定的格式、顺序等将实体类对象转换为 JSON 字符串。

所以下⾯就介绍⼀下 Jackson 的常⽤注解的使⽤⽅法,帮助我们进⾏序列化和反序列化⼯作。

常⽤注解

这些注解通常⽤于标注 Java 实体类或实体类的属性。

  • @JsonPropertyOrder(value={“pname1”,”pname2”}) 改变⼦属性在 JSON 序列化中的默认定义的顺序。如:param1 在先,param2 在后。
  • @JsonIgnore 加在属性上⾯,排除某个属性不做序列化与反序列化。
  • @JsonIgnoreProperties(ignoreUnknown = true),将这个注解写在类上之后,就会忽略 JSON 字符串中存在,但实体类不存在的属性,不予赋值,也不会出现异常。
  • @JsonIgnoreProperties({ “xxx”, “yyyy” }) 忽略某些属性不进⾏序列化。
  • @JsonProperty(anotherName) 为某个属性换⼀个名称,体现在 JSON 数据⾥⾯。
  • @JsonInclude(JsonInclude.Include.NON_NULL) 排除为空的元素不做序列化反序列化。
  • @JsonFormat(pattern = “yyyy-MM-dd HH:mm:ss”, timezone = “GMT+8”) 指定⽇期类型的属性格式。
@JsonPropertyOrder(value={"content","title"})
public class Article {
    @JsonIgnore
    private Long id;
    @JsonProperty("auther")
    private String author;

    private String title;
    private String content;
    @JsonInclude(JsonInclude.Include.NON_NULL)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;

    private List<Reader> reader;
}

上⽂代码中对应的 JSON 数据格式可以为:

{
  "auther": "",
  "content": "",
  "title": "",
  "createTime": "2022-03-10 12:12:12",
  "reader": [
    { "name": "zhangsan", "age": 22 },
    { "name": "lisi", "age": 23 }
  ]
}
  • 因为定义了 JsonPropertyOrder,content 在先,title 在后
  • 因为定义了 JsonIgnore,id 属性被忽略
  • 因为定义了 JsonProperty,author 属性变为 auther
  • 因为定义了 JsonInclude 和 JsonFormat ,createTime 不要为空,并且格式为 “yyyy-MM-dd HH:mm:ss”

通常会对⽇期类型转换进⾏全局配置,⽽不是在每⼀个 Java bean ⾥⾯配置。

spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

三、⼿动数据转换

除了在 Spring 框架内实现⾃动的前后端 JSON 数据与 Java 对象的转换,我们还可以使⽤ Jackson ⾃⼰写代码进⾏转换。

//jackson的ObjectMapper 转换对象
ObjectMapper mapper = new ObjectMapper();
//将某个java对象转换为JSON字符串
String jsonStr = mapper.writeValueAsString(javaObj);
//将jsonStr转换为Demo类的对象
Demo demo = mapper.readValue(jsonStr, Demo.class);

当 JSON 字符串代表的对象的字段多于类定义的字段时,使⽤ readValue 会抛出 UnrecognizedPropertyException 异常。

在类的定义处加上 @JsonIgnoreProperties(ignoreUnknown = true) 可以解决这个问题。

四、Jackson 全局配置

在 Spring 框架内使⽤ Jackson 的时候,通常需要⼀些特殊的全局配置,来应对我们 JSON 序列化与反序列化中出现的各种问题。

Spring Boot 提供了两种配置⽅式,⼀是配置⽂件的⽅式

spring:
  jackson:
    #⽇期类型格式化
    date-format: yyyy-MM-dd HH:mm:ss
    serialization:
      #格式化输出,通常为了节省⽹络流量设置为false。因为格式化之后会带有缩进,⽅便阅读。
      indent_output:
        false
        #某些类对象⽆法序列化的时候,是否报错
      fail_on_empty_beans: false
      #设置空如何序列化,⻅下⽂代码⽅式详解
    defaultPropertyInclusion: NON_EMPTY
    deserialization:
      #json对象中有不存在的属性时候,是否报错
      fail_on_unknown_properties: false
    parser:
      #允许出现特殊字符和转义符
      allow_unquoted_control_chars:
        true
        #允许出现单引号
      allow_single_quotes: true

⼆是通过代码的⽅式,⽅式⼀更容易,⽅式⼆更灵活。

⽅式⼀⽆法解决的问题,可以尝试使⽤⽅式⼆。

@Bean
@Primary
@ConditionalOnMissingBean(ObjectMapper.class)
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilderbuilder)
{
        ObjectMapper objectMapper = builder.createXmlMapper(false).build();
        // 通过该⽅法对mapper对象进⾏设置,所有序列化的对象都将按改规则进⾏系列化
        // Include.Include.ALWAYS 默认
        // Include.NON_DEFAULT 属性为默认值不序列化
        // Include.NON_EMPTY 属性为 空("") 或者为 NULL 都不序列化,则返回的json是没有这个字段的。这样对移动端会更省流量
        // Include.NON_NULL 属性为NULL 不序列化
				objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
				objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        // 允许出现特殊字符和转义符
				objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
        // 允许出现单引号
        objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
        // 字段保留,将null值转为""
        objectMapper.getSerializerProvider().setNullValueSerializer(new JsonSerializer<Object>()
        {
            @Override
            public void serialize(Object o, JsonGenerator jsonGenerator,
                                  SerializerProvider serializerProvider)
                    throws IOException
           {
                jsonGenerator.writeString("");
           }
        });
        return objectMapper;
}

5. 使⽤ Mockito 编码完成接⼝测试

之前我们都是使⽤ Postman 来进⾏接⼝测试的,本节我们来使⽤编码的⽅式来进⾏接⼝测试。

⼀、编码实现接⼝测试

1. 为什么要写代码做测试

因为在做系统⾃动化持续集成的时候,会要求⾃动做单元测试,只有所有的单元测试都跑通了,才能打包构建。⽐如:使⽤ maven 在打包之前将所有的测试⽤例执⾏⼀遍。这⾥重点是⾃动化,所以 Postman 这种⼯具很难插⼊到持续集成的⾃动化流程中去。

2. JUnit 测试框架

我们先回顾⼀下 JUnit 常⽤的测试注解,在 JUnit4 和 JUnit5 中,注解的写法有些变化。

junit4 junit5 特点
@Test @Test 声明⼀个测试⽅法
@BeforeClass @BeforeAll 在当前类的所有测试⽅法之前执⾏。注解在【静态⽅法】上
@AfterClass @AfterAll 在当前类中的所有测试⽅法之后执⾏。注解在【静态⽅法】上
@Before @BeforeEach 在每个测试⽅法之前执⾏。注 解在【⾮静态⽅法】上
@After @AfterEach 在每个测试⽅法之后执⾏。注 解在【⾮静态⽅法】
@RunWith(SpringRunner.class) @ExtendWith(SpringExtens ion.class) 类 class 定义上

3. Mockito 测试框架

Mockito 是 GitHub 上使⽤最⼴泛的 Mock 框架,可以与 JUnit 结合使⽤。

Mockito 框架可以创建和配置 mock 对象,使⽤ Mockito 简化了具有外部依赖的类的测试开发。 Mockito 测试框架可以帮助我们模拟 HTTP 请求,从⽽达到在服务端测试⽬的。

因为其不会真的发送 HTTP 请求,只是模拟,从⽽节省了 HTTP 请求的⽹络传输,测试速度更快。

确认⼀下模块依赖

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
   <exclusions>
      <exclusion>
         <groupId>org.junit.vintage</groupId>
         <artifactId>junit-vintage-engine</artifactId>
      </exclusion>
   </exclusions>
</dependency>

spring-boot-starter-test ⾃动包含 Junit 5 和 Mockito 框架,以下测试代码是基于 Junit5 的。

package top.syhan.boot.restful.controller;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpMethod;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
@Slf4j
public class ArticleControllerTest {
    //mock对象
    private static MockMvc mockMvc;
    //在所有测试⽅法执⾏之前进⾏mock对象初始化
    @BeforeAll
    static void setUp() {
        mockMvc = MockMvcBuilders.standaloneSetup(new ArticleController()).build();
    }

    //测试⽅法
    @Test
    public void saveArticle() throws Exception {
        String article = """
               {
                    "id": 1,
                    "author": "syhan",
                    "title": "SpringBoot",
                    "content": "SpringBoot",
                    "createTime": "2022-03-12 12:12:12",
                    "readerList":[{"name":"aaa","age":18},
										{"name":"bbb","age":20}]
               }""";
        MvcResult result = mockMvc.perform(
                        MockMvcRequestBuilders
.request(HttpMethod.POST,
"/api/v1/articles/body")
                               .contentType("application/json")
                               .content(article)
               )
               .andExpect(MockMvcResultMatchers.status().isOk())
//HTTP:status 200
.andExpect(MockMvcResultMatchers.jsonPath("$.data.author").value("syhan"))
.andExpect(MockMvcResultMatchers.jsonPath("$.data.readerList[0].age").value(18))
               .andDo(print())
               .andReturn();
        result.getResponse().setCharacterEncoding("UTF-8");
        log.info(result.getResponse().getContentAsString());
   }
}

MockMvc 对象有以下⼏个基本的⽅法:

  • perform : 模拟执⾏⼀个 RequestBuilder 构建的 HTTP 请求,会执⾏ SpringMVC 的流程并映射到相应的控制器 Controller 执⾏。
  • contentType:发送请求内容的序列化的格式,”application/json”表示 JSON 数据格式。
  • andExpect: 添加 RequsetMatcher 验证规则,验证控制器执⾏完成后结果是否正确,或者说是结果是否与我们期望(Expect)的⼀致。
  • andDo: 添加 ResultHandler 结果处理器,⽐如调试时打印结果到控制台。
  • andReturn: 最后返回相应的 MvcResult,然后进⾏⾃定义验证/进⾏下⼀步的异步处理。

上⾯的整个过程,我们都没有使⽤到 Spring Context 依赖注⼊、也没有启动 tomcat web 容器。整个测试过程⼗分轻量级,速度很快。

测试注意

因为没有⽤到 Spring,所以在 application.yml 中的时间格式化全局配置⽆效,别忘了在 Article 实体类加上。

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;

然后直接运⾏测试⽅法即可

⼆、真实 Servlet 容器环境下的测试

上⾯的测试执⾏速度⾮常快,但是有⼀个问题:它没有启动 Servlet 容器和 Spring 上下⽂,⾃然也就⽆法实现依赖注⼊(不⽀持@Resource 和 @AutoWired 注解),这就导致它在从控制层到持久层全 流程测试中有很⼤的局限性。

换⼀种写法:看看有没有什么区别。

在测试类上⾯加上这样两个注解,并且 mockMvc 对象使⽤ @Resource ⾃动注⼊,删掉 BeforeAll 注解。

启动测试,看看和之前有没有什么区别?

看到上⾯这个截图,是不是已经明⽩了?

该测试⽅法真实的启动了⼀个 Tomcat 容器、以及 Spring 上下⽂,所以我们可以进⾏依赖注⼊ (@Resource),测试实现的效果和使⽤ MockMvcBuilders 构建 MockMVC 对象的效果是⼀样的, 但是有⼀个⾮常明显的缺点:每次做⼀个接⼝测试,都会真实启动⼀次 Servlet 容器和 Spring 上下⽂,加载项⽬⾥定义的所有的 Bean,导致执⾏过程会⽐较缓慢。

1. @SpringBootTest 注解

是⽤来创建 Spring 的上下⽂ ApplicationContext,保证测试在上下⽂环境⾥运⾏。单独使⽤ @SpringBootTest 不会启动 servlet 容器。所以只是使⽤ SpringBootTest 注解,不可以使⽤ @Resource 和@Autowired 等注解进⾏ bean 的依赖注⼊。(准确的说是可以使⽤,但被注解的 bean 为 null)。

2. @ExtendWith(@RunWith 注解)

  • RunWith ⽅法为我们构造了⼀个的 Servlet 容器运⾏运⾏环境,并在此环境下测试。然⽽为什么要构建 servlet 容器?因为使⽤了依赖注⼊,注⼊了 MockMvc 对象,⽽在上⼀个例⼦⾥⾯是我们⾃⼰ new 的。

  • ⽽ @AutoConfigureMockMvc 注解,该注解表示 mockMvc 对象由 spring 依赖注⼊构建,你只负责使⽤就可以了。这种写法是为了让测试在 servlet 容器环境下执⾏。

简单地说:如果你单元测试代码使⽤了“依赖注⼊@Resource”就必须加上@ExtendWith,如果你 不是⼿动 new MockMvc 对象就加上@AutoConfigureMockMvc

实际上@SpringBootTest 注解已经包含了 @ExtendWith 注解,如果使⽤了前者,也可以忽略后者。

三、轻量级测试

在 ExtendWith 的 AutoConfigureMockMvc 注解的共同作⽤下,启动了 SpringMVC 的运⾏容器,并且把项⽬中所有的 @Bean 全部都注⼊进来。把所有的 bean 都注⼊进来是不是很臃肿?这样会拖慢单元测试的效率。如果我只是想测试⼀下控制层 Controller,怎么办?或者说我只想具体到测试⼀下 ArticleRestController ,怎么办?要把应⽤中所有的 bean 都注⼊么?有没有轻量级的解决⽅案?⼀ 定是有的。

@AutoConfigureMockMvc
@ExtendWith(SpringExtension.class)
@WebMvcTest(ArticleController.class)

使⽤@WebMvcTest 替换@SpringBootTest

  • @SpringBootTest 注解告诉 SpringBoot 去寻找⼀个主配置类(例如带有 @SpringBootApplication 的配置类),并使⽤它来启动 Spring 应⽤程序上下⽂。 SpringBootTest 加载完整的应⽤程序并注⼊所有可能的 bean ,因此速度会很慢。
  • @WebMvcTest 注解主要⽤于 controller 层测试,只覆盖应⽤程序的 controller 层, @WebMvcTest(ArticleController.class) 只加载 ArticleController 这⼀个 Bean ⽤作测试。所以 WebMvcTest 要快得多,因为我们只加载了应⽤程序的⼀⼩部分。

四、MockMvc 更多的⽤法总结

//模拟GET请求:
mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", userId));
//模拟Post请求:
mockMvc.perform(MockMvcRequestBuilders.post("uri", parameters));
//模拟⽂件上传:
mockMvc.perform(MockMvcRequestBuilders.multipart("uri").file("fileName", "file".getBytes("UTF-8")));
//模拟session和cookie:
mockMvc.perform(MockMvcRequestBuilders.get("uri").sessionAttr("name", "value"));
mockMvc.perform(MockMvcRequestBuilders.get("uri").cookie(new Cookie("name", "value")));
//设置HTTP Header:
mockMvc.perform(MockMvcRequestBuilders
                       .get("uri", parameters)
                       .contentType("application/x-www-form-urlencoded")
                       .accept("application/json")
                       .header("", ""));

6. Swagger3 即 OpenAPI 使⽤

⼀、为什么要发布 API 接⼝⽂档

现在很多公司都采取前后端分离的开发模式,前端和后端的⼯作由不同的⼯程师完成。在这种开发模式下,维护⼀份及时更新且完整的 API ⽂档将会极⼤的提⾼我们的⼯作效率。

Swagger 给我们提供了⼀个全新的维护 API ⽂档的⽅式,下⾯我们来了解⼀下它的优点:

  • 代码变,⽂档变。只需要少量的注解,Swagger 就可以根据代码⾃动⽣成 API ⽂档,很好的保证了⽂档的时效性。
  • 跨语⾔性,⽀持 40 多种语⾔。
  • Swagger UI 呈现出来的是⼀份可交互式的 API ⽂档,我们可以直接在⽂档⻚⾯尝试 API 的调⽤,省去了准备复杂的调⽤参数的过程。
  • 还可以将⽂档规范导⼊相关的⼯具(例如 SoapUI), 这些⼯具将会为我们⾃动地创建⾃动化测试。

⼆、背景及名词解释

  • OpenAPI 是规范的正式名称。规范的开发⼯作于 2015 年启动,当时 SmartBear(负责 Swagger ⼯具开发的公司)将 Swagger 2.0 规范捐赠给了 Open API Initiative,该协会由来⾃技术领域不同领域的 30 多个组织组成。此后,该规范被重命名为 OpenAPI 规范

  • Swagger

    • 是⼀个 API ⽂档维护组织,后来成为了 Open API 标准的主要定义者。现在最新的版本为 17 年发布的 Swagger3(Open Api3)。
    • 是⼀个 Open API 规范实现⼯具包,由于 Swagger ⼯具是由参与创建原始 Swagger 规范的团队开发的,因此通常仍将这些⼯具视为该规范的代名词。⽬前可以认为 Swagger3 就是 Open API 3.0。
  • OpenAPI 3.0:2017 年 7 ⽉,Open API Initiative 最终发布了 OpenAPI Specification 3.0.0。它对 2.0 规范进⾏了很多改进。Open API 3.0 规范可以⽤ JSON 或 YAML 编写,并且在记录 RESTful API ⽅⾯做得很好。同时标志着 Swagger2 成为过去式。

  • SpringFox 是 spring 社区维护的⼀个项⽬(⾮官⽅),帮助使⽤者将 swagger2 集成到 Spring 中。常常⽤于 Spring 中帮助开发者⽣成⽂档,并可以轻松的在 SpringBoot 中使⽤。截⾄ 2020 年 4 ⽉,尚未⽀持 OpenAPI3 标准。

  • SpringDoc 也是 spring 社区维护的⼀个项⽬(⾮官⽅),帮助使⽤者将 swagger3 集成到 Spring 中。也是⽤来在 Spring 中帮助开发者⽣成⽂档,并可以轻松的在 SpringBoot 中使⽤。

三、整合 springdoc-openapi

添加 openapi 依赖

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-ui</artifactId>
    <version>1.6.6</version>
</dependency>

就这么简单,⽂档就构建完成了,不需要做任何的其他配置。

集成完成之后,启动服务,浏览器输⼊:http://localhost:8080/swagger-ui.html

四、将 API 分组分组展示

配置⽅法

新建⼀个 HelloController 配合

重启服务,可以通过下拉选择分组查看组内 API:

五、使⽤ OpenAPI 测试

输⼊接⼝需要的参数,点击 Execute 即可

更多内容请看官⽹:https://springdoc.org/


文章作者: Syhan
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Syhan !
评论
  目录