RESTful
一种软件架构风格、设计风格,而不是标准,只是提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。本篇博客主要讲述使用Spring MVC
开发RESTful
风格的API
。
传统的API
和RESTful API
如下表所示:
| 行为 | 传统API | RESTful API | 方法 |
| -------- | ------------- | ------------- |
| 查询 | /user/query?name=lemon
|/user?name=lemon
| GET
|
| 详情 | /user/getInfo?id=1
|/user/1
| GET
|
| 创建 | /user/create?name=lemon
|/user
| POST
|
| 修改 | /user/update?id=1&name=tom
|/user/1
| POST
|
| 删除 | /user/delete?id=1
|/user/1
| GET
|
RESTful
风格的API
有如下几个特点:
URL
描述资源
HTTP
方法描述行为,使用HTTP
状态码来表示不同的结果
JSON
进行数据交互
RESTful
只是一种风格,并不是一种强制的标准
这里介绍几个常用的注解:
@RestController
标明此Controller
提供RESTful API
@RequestMapping
及其变体(@GetMapping
、PostMapping
等),映射HTTP
请求到Java
方法
@RequestParam
映射请求参数到Java
方法的参数
@PathVariable
映射URL
片段到Java
方法的参数
@PageableDefault
指定默认分页参数
@JsonView
按照指定方式序列化Java
对象
代码案例:这里有User
和UserController
以及UserControllerTest
三个类,其中UserControllerTest
的四个测试方法分别对应UserController
类中的四个方法。
User
类package com.lemon.security.web.dto;
import lombok.Data;
/**
* @author lemon
* @date 2018/3/22 下午3:40
*/
@Data
public class User {
private String username;
private String password;
}
UserController
类package com.lemon.security.web.controller;
import com.lemon.security.web.dto.User;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
/**
* @author lemon
* @date 2018/3/22 下午3:39
*/
@RestController
public class UserController {
@RequestMapping(value = "/user1", method = RequestMethod.GET)
public List<User> query1() {
return generateUsers();
}
@GetMapping("/user2")
public List<User> query2(@RequestParam String username) {
System.out.println(username);
return generateUsers();
}
@GetMapping("/user3/{username}")
public List<User> query3(@PathVariable String username) {
System.out.println(username);
return generateUsers();
}
@GetMapping("/user4")
public List<User> query4(@PageableDefault(page = 1, size = 2, sort = "username") Pageable pageable) {
System.out.println(pageable.getPageNumber());
System.out.println(pageable.getPageSize());
System.out.println(pageable.getSort());
return generateUsers();
}
private List<User> generateUsers() {
List<User> users = new ArrayList<>();
users.add(new User());
users.add(new User());
users.add(new User());
return users;
}
}
UserControllerTest
类package com.lemon.security.web;
import com.lemon.security.web.application.MainApplication;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
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 org.springframework.web.context.WebApplicationContext;
/**
* @author lemon
* @date 2018/3/22 下午3:14
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = MainApplication.class)
public class UserControllerTest {
// 注入一个web应用环境(容器)
@Autowired
private WebApplicationContext webApplicationContext;
// MVC环境对象
private MockMvc mockMvc;
@Before
public void init() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
@Test
public void query1() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/user1")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3));
}
@Test
public void query2() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/user2")
.param("username", "lemon")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3));
}
@Test
public void query3() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/user3/lemon")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3));
}
@Test
public void query4() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/user4")
.param("size", "3")
.param("page", "1")
.param("sort", "username,desc")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3));
}
}
第三个类,也就是UserControllerTest
是RESTful API
的测试类,现在对其进行简单介绍:
由于RESSTful
风格的API
不能通过浏览器地址栏来进行测试,因为地址栏发送的请求都是GET
类型的,而RESTful API
正是通过请求方法来判断请求行为是查询、修改、删除、增加中的哪一种的,所以测试RESSTful
风格的API
都是通过编码来进行测试的。
@Autowired WebApplicationContext webApplicationContext
:注入web
环境的ApplicationContext
容器;
MockMvcBuilders.webAppContextSetup(webApplicationContext).build()
创建一个MockMvc
的MVC
环境进行测试;
MockMvcRequestBuilders.get()
方法是发送一个GET
请求,param()
是设置请求参数,contentType()
是我设置内容类型(JSON
格式),andExpect()
方法是希望得到什么样的测试结果,MockMvcResultMatchers()
是返回结果的匹配是否正确。jsonPath()
方法是解析返回的JSON
数据,关于它的介绍可以在github
上找到。
运行上面的四个测试方法都可以通过测试。对于@PathVariable
再写一个测试案例:
Controller
方法:@GetMapping("/getInfo/{id:\\d+}")
public User getInfo(@PathVariable String id) {
System.out.println("查询的对象ID为:".concat(id));
User user = new User();
user.setUsername("lemon");
return user;
}
上面的方法URL
片段进行了正则表达式的验证,ID
只能是数字。
@Test
public void getInfo() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/getInfo/1")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.username").value("lemon"));
}
接下来详细介绍@JsonView这个注解的使用。
@JsonView
的使用步骤
get
方法上指定视图
Controller
方法上指定视图
对于上面的步骤,进行如下解释如下:
一般对Java
对象进行序列化Json
的时候,会考虑到只序列化部分字段,那么就可以使用@JsonView
这个注解。在这里使用User
实体类进行举例,首先,在实体类上定义两个接口,第一个接口是简单视图(UserSimpleView
),表示之序列化username
这个字段,而第二个接口是详情视图(UserDetailView extends UserSimpleView
),表示不仅序列化username
字段,还序列化password
字段。然后使用@JsonView
注解将两个视图绑定到对应的字段的get
方法上面,由于UserDetailView
继承了UserSimpleView
这个视图,所以在Controller
方法上使用UserDetailView
视图的时候,会同时序列化两个字段,而使用UserSimpleView
的时候仅仅只会序列化username
这一个字段。下面进行代码展示:
package com.lemon.security.web.dto;
import com.fasterxml.jackson.annotation.JsonView;
/**
* @author lemon
* @date 2018/3/22 下午3:40
*/
public class User {
public interface UserSimpleView {}
public interface UserDetailView extends UserSimpleView {}
private String username;
private String password;
@JsonView(UserSimpleView.class)
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@JsonView(UserDetailView.class)
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
@GetMapping("/getSimpleUser")
@JsonView(User.UserSimpleView.class)
public User getSimpleUser() {
User user = new User();
user.setUsername("lemon");
user.setPassword("123456");
return user;
}
@GetMapping("/getDetailUser")
@JsonView(User.UserDetailView.class)
public User getDetailUser() {
User user = new User();
user.setUsername("lemon");
user.setPassword("123456");
return user;
}
从上面的步骤分析可知,第一个方法返回的user对象在序列化为json的时候,只会序列化username字段,而第二个方法则会同时序列化两个字段。
@Test
public void getSimpleUser() throws Exception {
String result = mockMvc.perform(MockMvcRequestBuilders.get("/getSimpleUser")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn().getResponse().getContentAsString();
System.out.println(result);
}
@Test
public void getDetailUser() throws Exception {
String result = mockMvc.perform(MockMvcRequestBuilders.get("/getDetailUser")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn().getResponse().getContentAsString();
System.out.println(result);
}
两个方法打印的结果分别为:
{"username":"lemon"}
{"username":"lemon","password":"123456"}
对于RESTful API
,一般都不再使用传统的参数传递,而是使用资源映射的方式,也就是使用@PathVariable
,为了保持文档的完整性,这里再次使用上面已经举过的案例:
Controller
方法:@GetMapping("/getInfo/{id:\\d+}")
public User getInfo(@PathVariable String id) {
System.out.println("查询的对象ID为:".concat(id));
User user = new User();
user.setUsername("lemon");
return user;
}
上面的方法URL
片段进行了正则表达式的验证,ID
只能是数字。
@Test
public void getInfo() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/getInfo/1")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.username").value("lemon"));
}
这里主要介绍三个知识点:
@RequestBody
映射请求体到Java
方法参数
@Valid
注解和BindingResult
验证请求参数的合法性并处理校验结果
@RequestBody
是将前台传递过来的JSON
字符串转换成Java
对象,
JSON
字符串映射到Java
对象中在之前的User
类上加上一个id
字段,然后进行下面的测试。
Controller
方法:用户创建的方法
@PostMapping("/user1")
public User create1(@RequestBody User user) {
System.out.println(ReflectionToStringBuilder.reflectionToString(user, ToStringStyle.MULTI_LINE_STYLE));
user.setId(1);
return user;
}
测试方法:测试创建用户方法
@Test
public void create1() throws Exception {
String content = "{\"username\":\"lemon\",\"password\":\"123456\"}";
mockMvc.perform(MockMvcRequestBuilders.post("/user1")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(content))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1));
}
测试方法传递过来的数据是一个JSON
字符串,正是@RequestBody
注解将JSON
字符串转化成为Java
对象。
@Valid
注解和BindingResult
验证请求参数的合法性并处理校验结果当使用Java
类来接受参数的是,往往需要对参数进行校验,而校验一般都是使用Hibernate
提供的校验器来进行校验,在Java
实体类的字段上,我们常常加上@NotBlank
、@NotNull
、@Null
、@Min
、@Max
、@NotEmpty
等注解进行校验规则定义,然后在Controller
方法参数前加上@Valid
注解来进行校验,校验的错误结果存储在BindingResult
对象内。这里我向后台传递一个JSON
字符串,人为使得username
和password
两个字段为null
。这里仅仅简单介绍表单验证的注解,下一篇博客将重点介绍。接下来请看案例:
private Integer id;
@NotEmpty(message = "用户名不能为空")
private String username;
@NotEmpty(message = "密码不能为空")
private String password;
private Date birthday;
@PostMapping("/user2")
public User create2(@Valid @RequestBody User user, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
bindingResult.getAllErrors().forEach(error -> System.out.println(error.getDefaultMessage()));
}
System.out.println(ReflectionToStringBuilder.reflectionToString(user, ToStringStyle.MULTI_LINE_STYLE));
user.setId(2);
return user;
}
@Test
public void create2() throws Exception {
String content = "{\"username\":null,\"password\":null}";
mockMvc.perform(MockMvcRequestBuilders.post("/user2")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(content))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value(2));
}
运行结果为:
用户名不能为空
密码不能为空
com.lemon.security.web.dto.User@58d79479[
id=<null>
username=<null>
password=<null>
birthday=<null>
]
由于RESTful
风格的API
是基于方法来进行区分的,所以设计到数据的修改和删除使用的方法是PUT
和DELETE
,接下来使用案例的方式介绍修改和删除API
的开发。
@Test
public void update() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.put("/user/1")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.username").value("lemon"));
}
@Test
public void delete() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.delete("/user/1")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk());
}
Controller
方法:@PutMapping("/user/{id:\\d+}")
public User update(@PathVariable Integer id) {
User user = new User();
user.setId(id);
System.out.println("模拟修改");
user.setUsername("lemon");
return user;
}
@DeleteMapping("/user/{id:\\d+}")
public void delete(@PathVariable Integer id) {
System.out.println("模拟修改,修改ID:".concat(String.valueOf(id)));
}
回顾一下RESTful风格的API,都是使用URL描述资源,使用请求方法来区别不同的API。这极大程度地简化了API开发的流程,推荐使用。