目录描述
1、restful风格介绍
这张图左边是传统的服务请求,右边是restful风格的,可以对比的看一下,下边的描述是restful的特点
这一张是restful的阶梯图
2 编写一个Restful Api
简单的restful 并测试
demo pom文件加入测试依赖
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<!--<version>2.1.2.RELEASE</version>-->
<!--<scope>test</scope>-->
</dependency>
创建测试类UserControllerTest
package com.whale;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.WebApplicationType;
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;
import java.io.Serializable;
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest implements Serializable {
/**
* 注入web环境的ApplicationContext容器;
*/
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@Before
public void setUp(){
/**
* MockMvcBuilder是用来构造MockMvc的构造器,其主要有两个实现:
* StandaloneMockMvcBuilder和DefaultMockMvcBuilder,分别对应两种测试方式,
* 即独立安装和集成Web环境测试(此种方式并不会集成真正的web环境,而是通过相应的Mock API进行模拟测试,无须启动服务器)。
*/
//创建一个MockMvc进行测试;
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
@Test
public void whenQuerySuccess() throws Exception {
//andExpect:添加ResultMatcher验证规则,验证控制器执行完成后结果是否正确;
mockMvc.perform(
MockMvcRequestBuilders.get("/user").
contentType(MediaType.APPLICATION_JSON_UTF8)) //用contentType表示具体请求中的媒体类型信息,MediaType.APPLICATION_JSON表示互联网媒体类型的json数据格式
.andExpect(MockMvcResultMatchers.status().isOk()) //期望的状态码 200
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3));验证数组的length是否为3,jsonPath的使用
}
}
运行测试类
期望 200 实际 404 (因为我还没写这个服务)
现在开始写一个restful接口
package com.whale.web;
import com.whale.model.User;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.io.Serializable;
import java.util.List;
@RestController
public class UserController implements Serializable {
@RequestMapping(value = "/user",method = RequestMethod.GET)
public List<User> query(){
return null;
}
}
package com.whale.model;
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = -3379923885183732446L;
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
再次运行测试类 错误信息如下 ,不像之前那样404了,是一个JSON path 的错误,期望集合的长度是3,我们直接返回了null
修改返回值,再次测试,成功
@RestController
public class UserController implements Serializable {
@RequestMapping(value = "/user",method = RequestMethod.GET)
public List<User> query(){
List<User> list = new ArrayList<User>();
User user = new User();
list.add(user);
list.add(user);
list.add(user);
return list;
}
}
测试带表单参数的restful一
接口
@RestController
public class UserController implements Serializable {
@RequestMapping(value = "/user",method = RequestMethod.GET)
public List<User> query(@RequestParam String username){
//@RequestParam String username 如果请求过来没有username这个参数会返回一个400错误
System.out.println(username);
List<User> list = new ArrayList<User>();
User user = new User();
list.add(user);
list.add(user);
list.add(user);
return list;
}
}
测试
@Test
public void whenQuerySuccess() throws Exception {
//andExpect:添加ResultMatcher验证规则,验证控制器执行完成后结果是否正确;
mockMvc.perform(
MockMvcRequestBuilders.get("/user").
param("username","hello"). //带参数
contentType(MediaType.APPLICATION_JSON_UTF8)) //用contentType表示具体请求中的媒体类型信息,MediaType.APPLICATION_JSON表示互联网媒体类型的json数据格式
.andExpect(MockMvcResultMatchers.status().isOk()) //期望的状态码 200
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3));验证length是否为3,jsonPath的使用
}
测试带表单参数的restful二
接口
@RequestMapping(value = "/user1",method = RequestMethod.GET)
public List<User> query1(User u){
//ReflectionToStringBuilder.toString org.apache.commons.lang3的工具类 可以以字符串的形式打印对象
System.out.println(ReflectionToStringBuilder.toString(u,ToStringStyle.MULTI_LINE_STYLE));
List<User> list = new ArrayList<User>();
User user = new User();
list.add(user);
list.add(user);
list.add(user);
return list;
}
测试
@Test
public void whenQuerySuccess() throws Exception {
//andExpect:添加ResultMatcher验证规则,验证控制器执行完成后结果是否正确;
mockMvc.perform(
MockMvcRequestBuilders.get("/user1").
param("username","hello") //带参数
.param("password","123456")
.contentType(MediaType.APPLICATION_JSON_UTF8)) //用contentType表示具体请求中的媒体类型信息,MediaType.APPLICATION_JSON表示互联网媒体类型的json数据格式
.andExpect(MockMvcResultMatchers.status().isOk()) //期望的状态码 200
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3));验证length是否为3,jsonPath的使用
}
Pageable 分页
分页的话 我们需要在方法的参数上绑定一个对象
Pageable pageable
他在package org.springframework.data.domain 包下
它里面有两个重要属性
page 第几页
size 每页多少条数据
也可以指定默认值
@PageableDefault(page = 0,size = 10) Pageable pageable
它还支持排序
sort
jsonPath
它的用法参考 https://github.com/json-path/JsonPath 上面已经很详细了
3、 @PathVariable 映射url片段到java方法的参数上
传统方法使用表单参数
/**
* @param id
* @return
*/
@RequestMapping("/user3")
public User getInfo(String id){
System.out.println("=================");
System.out.println(id);
User u = new User();
u.setUsername("tom");
return u;
}
测试
@Test
public void whenGetInfoSuccess() throws Exception {
//andExpect:添加ResultMatcher验证规则,验证控制器执行完成后结果是否正确;
mockMvc.perform(
MockMvcRequestBuilders.get("/user3")
.param("id","1")
.contentType(MediaType.APPLICATION_JSON_UTF8)) //用contentType表示具体请求中的媒体类型信息,MediaType.APPLICATION_JSON表示互联网媒体类型的json数据格式
.andExpect(MockMvcResultMatchers.status().isOk()) //期望的状态码 200
.andExpect(MockMvcResultMatchers.jsonPath("$.username").value("tom"));
}
这样可以打印出id
映射url片段到java方法的参数上
改造
注意:
- 方法参数上必须加@PathVariable注解 否则绑定不到参数上
- 当方法参数名称与映射路径上的{}里面的名称不一致时,需要给@PathVariable加name属性,如下所示
/**
* @param idxx
* @return
*/
@RequestMapping("/user4/{id}")
public User getInfo4(@PathVariable(name = "id") String idxx){
System.out.println("=================");
System.out.println(idxx);
User u = new User();
u.setUsername("tom");
return u;
}
测试
@Test
public void whenGetInfoSuccess() throws Exception {
//andExpect:添加ResultMatcher验证规则,验证控制器执行完成后结果是否正确;
mockMvc.perform(
MockMvcRequestBuilders.get("/user4/1")
.contentType(MediaType.APPLICATION_JSON_UTF8)) //用contentType表示具体请求中的媒体类型信息,MediaType.APPLICATION_JSON表示互联网媒体类型的json数据格式
.andExpect(MockMvcResultMatchers.status().isOk()) //期望的状态码 200
.andExpect(MockMvcResultMatchers.jsonPath("$.username").value("tom"));
}
正则表达式的使用
在上面的路径映射中我们可以接受数字,也可以接受字符串
如何限制路径上可接受的数据类型呢,我们可以使用正则表达式
/**
* @param idxx
* @return
*/
@RequestMapping("/user4/{id:\\d++}")
public User getInfo4(@PathVariable(name = "id") String idxx){
System.out.println("=================");
System.out.println(idxx);
User u = new User();
u.setUsername("tom");
return u;
}
再次测试
@Test
public void whenGetInfoSuccess() throws Exception {
//andExpect:添加ResultMatcher验证规则,验证控制器执行完成后结果是否正确;
mockMvc.perform(
MockMvcRequestBuilders.get("/user4/a")
.contentType(MediaType.APPLICATION_JSON_UTF8)) //用contentType表示具体请求中的媒体类型信息,MediaType.APPLICATION_JSON表示互联网媒体类型的json数据格式
.andExpect(MockMvcResultMatchers.status().isOk()) //期望的状态码 200
.andExpect(MockMvcResultMatchers.jsonPath("$.username").value("tom"));
}
出现404错误
4、@JsonView 的使用
情景:
我们user 中有name 和password ,但当我们查询user列表的时候并不需要将password返回到前台
而查询单个用户的时候我们经过一些权限的认证然后把密码返回
使用步骤
- 使用接口来声明多个视图
- 在属性的get方法上指定视图
- 在controller方法上指定视图
实战
/**
* @param idxx
* @return
*/
@RequestMapping("/user4/{id:\\d++}")
@JsonView(User.UserDetailView.class)//这个方法使用详情视图
public User getInfo4( @PathVariable(name = "id") String idxx){
System.out.println("=================");
System.out.println(idxx);
User u = new User();
u.setUsername("tom");
return u;
}
测试
@Test
public void whenGetInfoSuccess() throws Exception {
//andExpect:添加ResultMatcher验证规则,验证控制器执行完成后结果是否正确;
String result = mockMvc.perform(
MockMvcRequestBuilders.get("/user4/1")
.contentType(MediaType.APPLICATION_JSON_UTF8)) //用contentType表示具体请求中的媒体类型信息,MediaType.APPLICATION_JSON表示互联网媒体类型的json数据格式
.andExpect(MockMvcResultMatchers.status().isOk()) //期望的状态码 200
.andExpect(MockMvcResultMatchers.jsonPath("$.username").value("tom"))
.andReturn().getResponse().getContentAsString(); //将返回结果转换为字符串 并 定义 一个变量result接收
System.out.println(result);
}
结果可以看到返回了name 和 password属性
当把@JsonView主键中的详情视图替换为简单视图 ,结果如下
可以看出只打印出了name属性
5、简单代码重构
重构之前先将代码上传到git上
https://blog.youkuaiyun.com/uotail/article/details/80211897
https://blog.youkuaiyun.com/autfish/article/details/52513465
idea 中代码上传至GitHub
ok
@RequestMapping("user")
//每个方法的路径前面都有一个user 可以抽取出来放到类上 ,spring 会将类上的路径+方法上的路径 作为访问路径
//因此我们经常看到有些controller有方法没写路径,其实这个方法的路径就是他所在类的路径
@RestController
public class UserController implements Serializable {
//@RequestMapping(value = "/user",method = RequestMethod.GET)
//@GetMapping("user")
@GetMapping
public List<User> query(@RequestParam String username){
//@RequestParam String username 如果请求过来没有username这个参数会返回一个400错误
System.out.println(username);
List<User> list = new ArrayList<User>();
User user = new User();
list.add(user);
list.add(user);
list.add(user);
return list;
}
/**
* @param idxx
* @return
*/
@GetMapping("{id:\\d++}")
@JsonView(User.UserSimpleView.class)
public User getInfo4( @PathVariable(name = "id") String idxx){
System.out.println("=================");
System.out.println(idxx);
User u = new User();
u.setUsername("tom");
return u;
}
}
6、@RequestBody映射请求体到java方法的参数
先写一个测试方法
@Test
public void whenCreateSuccess() throws Exception {
//{"username":"tom"} ,在java 双引号需要转义
String content ="{\"username\":\"tom\"}";
mockMvc.perform(MockMvcRequestBuilders.post("/user")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(content))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(jsonPath("$.id").value("1"));
}
直接运行的话会报一个405
首先我们使用这个路径的方法的,但它上面有@GetMapping表示接受get请求,而我们发送的是post请求
- 注意 MockMvcRequestBuilders.post("/user") 里面的路径前必须加这个斜杠/ 否则会是404错误
下面我们先写一个接受post请求的方法
//当我们的请求路径直接为 “/user” 时 ,如果是get请求就找对应的get方法,如果是post请求就找这个方法
//此时我们分别有两个方法没有写映射路径,一个是post 一个是get
@PostMapping
public User createUser(User u){
System.out.println(u.getId());
System.out.println(u.getUsername());
System.out.println(u.getPassword());
User user = new User();
user.setId(1);
return user;
}
再次运行测试方法
可以看出我们的json字符串并没有绑定到参数上
解决
给参数前面加 @RequestBody 注解 再次运行 测试方法
可以看出请求的json字符串已经绑定到方法参数上去了
7、日期类型参数的处理
一般我们在传date类型数据的时候回把时间转换成字符串如 yyyy-HH-mm 类型
其实在现在这种前后端分离的架构中这种情况是不允许的,因为我们的接口可能会被各种设备调用,
他们对时间的格式要求都是不一样的,所有我们应该返回一个统一的时间戳,它精确到毫秒。
测试方法
首先应该在user 实体里加一个date类型的生日
@Test
public void whenCreateSuccess() throws Exception {
Date date = new Date();
System.out.println(date.getTime());
//{"username":"tom"} ,在java 双引号需要转义
String content ="{\"username\":\"tom\",\"birthday\":\""+date.getTime() +"\"}";
System.out.println(content);
String result = mockMvc.perform(MockMvcRequestBuilders.post("/user")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(content))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(jsonPath("$.id").value("1"))
.andReturn().getResponse().getContentAsString();
System.out.println(result);
}
接收方法
//当我们的请求路径直接为 “/user” 时 ,如果是get请求就找对应的get方法,如果是post请求就找这个方法
//此时我们分别有两个方法没有写映射路径,一个是post 一个是get
@PostMapping
public User createUser(@RequestBody User u){
System.out.println(u.getId());
System.out.println(u.getUsername());
System.out.println(u.getPassword());
System.out.println(u.getBirthday());
User user = new User();
user.setId(1);
return user;
}
测试结果
可以看见请求参数中的时间戳被转换成了date类型,
方法返回的时候又会把date类型再次转换成时间戳
8、@Valid注解校验
@Valid
平常我们对数据的校验如下面这种
if(StringUtils.isBlank(u.getPassword())){
}
这种方法很繁琐,可以用两个注解 替代上面的方案
在user 的 password属性上面 加约束
@NotBlank
它是javax.validation.constraints.NotBlank;
包下的
以前也会用这个@org.hibernate.validator.constraints.NotBlank
不过这个已经过时了
然后在需要验证的参数前面加上这个注解@Valid
maven: javax.validation:validation-api:2.0.1.Final] javax.validation public @interface Valid
现在再次运行上次的测试类,因为我们没有穿password的属性,所以报一个400错误,请求错误
- 而且这个请求也没有进入我们的方法体,有时候我们还需要对这个请求进行处理,比如 打个日志说此用户没有密码,这个怎么办?
- 这个就是BindingResult 做的事情,它和@Valid搭配使用,校验的错误信息都会存到存到它里面
BindingResult
方法如下
@PostMapping
public User createUser(@Valid @RequestBody User u, BindingResult errors){
//如果有错误 循环打印
if(errors.hasErrors()){
errors.getAllErrors().stream().forEach(error-> System.out.println(error.getDefaultMessage()));
}
System.out.println(u.getId());
System.out.println(u.getUsername());
System.out.println(u.getPassword());
System.out.println(u.getBirthday());
User user = new User();
user.setId(1);
return user;
}
再次运行测试类
发现已经不是400错误了,测试是绿条
打印结果如上 有一句 must not be blank
hibernate validator其他校验注解
官方文档
Hibernate Validator 6.0.14.Final - JSR 380 Reference Implementation: Reference Guide
https://www.cnblogs.com/mr-yang-localhost/p/7812038.html
https://www.cnblogs.com/firstdream/p/8832838.html
9、put 请求做修改
先写一个测试用例
他和创建基本一样,都要传用户基本信息,
但是有两点不一样
- 请求为put请求
- 创建的时候我们不知道用户id,但修改的时候需要传入id
@Test
public void whenUpdateSuccess() throws Exception {
Date date = new Date();
System.out.println(date.getTime());
//{"username":"tom"} ,在java 双引号需要转义
String content ="{\"id\":\"1\",\"username\":\"tom\",\"birthday\":\""+date.getTime() +"\"}";
System.out.println(content);
String result = mockMvc.perform(MockMvcRequestBuilders.put("/user/1")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(content))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(jsonPath("$.id").value("1"))
.andReturn().getResponse().getContentAsString();
System.out.println(result);
}
运行
因为我们前面写的get映射无法处理put请求
put映射方法
@PutMapping("{id:\\d++}")
public User updateUser(@Valid @RequestBody User u, BindingResult errors){
//如果有错误 循环打印
if(errors.hasErrors()){
errors.getAllErrors().stream().forEach(error-> System.out.println(error.getDefaultMessage()));
}
System.out.println(u.getId());
System.out.println(u.getUsername());
System.out.println(u.getPassword());
System.out.println(u.getBirthday());
User user = new User();
user.setId(1);
return user;
}
给user 的 birthday加 past主键 ,限制时间为过去式
@Past // 过去时间
private Date birthday;
修改测试用例
@Test
public void whenUpdateSuccess() throws Exception {
//Date date = new Date();
//jdk 8 时间操作 当前时间加一年 默认时区 转换为毫秒
Date date = new Date(LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
System.out.println(date.getTime());
//{"username":"tom"} ,在java 双引号需要转义
String content ="{\"id\":\"1\",\"username\":\"tom\",\"birthday\":\""+date.getTime() +"\"}";
System.out.println(content);
String result = mockMvc.perform(MockMvcRequestBuilders.put("/user/1")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(content))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(jsonPath("$.id").value("1"))
.andReturn().getResponse().getContentAsString();
System.out.println(result);
}
运行
控制台打印出两个错误信息
一个是必须不为空,一个是必须是过去时间,但具体是哪个字段的错误信息,我们不知道
修改put方法补全字段错误信息
@PutMapping("{id:\\d++}")
public User updateUser(@Valid @RequestBody User u, BindingResult errors){
//如果有错误 循环打印
if(errors.hasErrors()){
errors.getAllErrors().stream().forEach(error-> {
FieldError fieldError = (FieldError)error;
String message = fieldError.getField()+" "+error.getDefaultMessage();
System.out.println(message);
});
}
System.out.println(u.getId());
System.out.println(u.getUsername());
System.out.println(u.getPassword());
System.out.println(u.getBirthday());
User user = new User();
user.setId(1);
return user;
}
控制台如下
自定义错误消息message
修改user
@NotBlank(message = "密码不能为空")
@JsonView(UserDetailView.class)
private String password; // 同理 这个展示在详情视图 但由于接口的继承关系 username 也会展示在详情视图
@Past(message = "生日必须为过去时间") // 过去时间
private Date birthday;
再次运行测试
自定义validator注解重用验证逻辑
参考 @NotBlank注解
红框里的三个属性必须有
自定义注解如下
@Target({ElementType.METHOD,ElementType.FIELD}) //这个注解可以标注在方法和字段上面
@Retention(RetentionPolicy.RUNTIME) //运行时注解
@Constraint(validatedBy = MyConstraintValidator.class) //约束的执行逻辑 validatedBy 具体执行约束逻辑的类
public @interface MyConstraint{
String message();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// 不用写 @Component ,当它实现ConstraintValidator接口时会自动被spring装配为bean
// 在这个校验器里面可以注入spring 管理的任意bean
public class MyConstraintValidator implements ConstraintValidator<MyConstraint,Object>{
@Override
public void initialize(MyConstraint constraintAnnotation) {
System.out.println("MyConstraintValidator init");
}
/**
* 校验逻辑
* @param value
* @param constraintValidatorContext
* @return
*/
@Override
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
/*验证逻辑*/
System.out.println(value);
return false; //true 验证 通过
}
}
再次运行如下
10、delete请求
@DeleteMapping("{id:\\d++}")
public void deleteUser(@PathVariable String id){
System.out.println(id);
}
@Test
public void whenDeleteSuccess() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.delete("/user/1")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk()); //200 删除成功
}