前言

使用SpringMVC 开发RESTful API主要讲解一下内容

1. 使用Spring MVC编写Restful API

2.使用Spring MVC处理其他web应用常见的需求和场景

3.Restful API开发常用辅助框架(swagger,MockMvc)

1.使用Spring MVC编写Restful API

1.1 Restful简介

1.1.1 传统接口和Restful API对比

增删查改传统和Restful API的URL对比

传统

Restful API

查询

/user/query?name=Jack

GET

/user?name=Jack

GET

详情

/user/getInfo?id=1

GET

/user/1

GET

创建

/user/create?name=Jack

POST

/user

POST

修改

/user/update?id=1&name=Jack

POST

/user/1

PUT

删除

/user/delete?id=1

GET

/user/1

DELETE

增删查改传统和Restful API的特点对比

传统

Restful API

用URL描述行为(分别带有操作动词:通过这些动词知道行为)

用URL描述资源(url上看不到行为:上面详情、修改、删除都是对id=1的用户;用户id为1的用户对系统来说是一个资源)

行为描述用url动词,http结果不管成功失败都是返回json,也许状态码都是200

用HTTP方法描述行为(用GET、POST、PUT、DELETE描述行为),使用HTTP状态码来标识不同结果

url上使用键值对传递参数较多

使用json交互数据

Restful API只是一种风格,并不是强制标准

1.1.2 Rest成熟度模型

一下模型中,把Restful成熟度分为了4级。0-3,数字越大级别越高 越来满足此模型

api java restful 例子 java rest api 框架_API

使用HTTP作为传输方式,不是http传输就不是restful API。

引入资源概念,每个资源都有对应url;restful API是用URL描述资源,请求接口中无动作。

使用HTTP方法进行不同操作、使用HTTP状态码表示不同结果。

超媒体控制:在资源的表达中包含了链接信息。这种规范在大部分工作中很难达到,一般满足到level2。

1.2 查询请求

编写Restful API需要编写以下内容:

编写针对Restful API测试用例(使用web浏览器地址栏是检验不了PUT、post)

使用注解声明Restful API

在Restful API中传递参数

1.2.1 编写针对Restful API测试用例

首先需要引入测试依赖;

org.springframework.boot

spring-boot-starter-test

我们有时候执行:mvn clean install时候下载不下来对应依赖时候,我们在本地依赖仓库删除所依赖,然后重新执行:mvn clean install

在test包下创建测试类:

/*
* 使用SpringRunner类运行测试用例
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest {
/*
* 伪造mvc环境 伪造的环境不会真正去启动tomcat
*/ @Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
/*
* 每次执行测试用例前执行这个方法
*/
@Before
public void setup(){
/*
* 构造mvc环境
*/
mockMvc=MockMvcBuilders.webAppContextSetup(wac).build(); }
@Test
public void whenQuerySuccess() throws Exception{
//发送一个url为/user的GET请求
mockMvc.perform(MockMvcRequestBuilders.get("/user")//请求user
.param("username","Jack")//传递请求参数
.contentType(MediaType.APPLICATION\_JSON))//发送请求类型:application-json
.andExpect(MockMvcResultMatchers.status().isOk())//结果执行的期望\-返回状态码isOk
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3));//期望返回结果为3
}
}

1.2.2 使用注解声明Restful API

@RestController 标明此controller提供RestAPI;是@Controller 和@ResponseBody组合。

@RequestMapping及其变体。映射http请求url到java方法。

@RequestParam 映射请求参数到java方法的参数。

@PageableDefault指定分页参数默认值。

创建一个参数非必传的方法:

@RestController
public class UserController {
@RequestMapping(value = "/user",method = RequestMethod.GET)
public List user(@RequestParam(name="username",required = false,defaultValue = "Linda") String username){
User user = new User();
List users = Arrays.asList(user, user, user);
return users;
}
}

正常运行测试用例:success

创建一个多条件封装(对象)查询的方法

创建查询条件封装:
public class UserQueryCondition {
private String username;
private int age;
//getter setter省略
}
test方法中多参数传递:
@Test
public void whenQuerySuccess() throws Exception{
//发送一个url为/user的GET请求
mockMvc.perform(MockMvcRequestBuilders.get("/user")//请求user
.param("username","Jack")//传递请求参数
.param("age","18")
.contentType(MediaType.APPLICATION\_JSON))//发送请求类型:application-json
.andExpect(MockMvcResultMatchers.status().isOk())//结果执行的期望返回状态码isOk
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3));//期望返回结果为3
}
后端UserController
运行测试用例:
@RequestMapping(value = "/user",method = RequestMethod.GET)
public List user(UserQueryCondition condition){
//使用反射工具 System.out.println(ReflectionToStringBuilder.toString(condition, ToStringStyle.MULTI_LINE_STYLE));
User user = new User();
List users = Arrays.asList(user, user, user);
return users;
}
后端输出:
2020-02-20 08:29:19.120 INFO 18416 --- [ main] o.s.t.web.servlet.TestDispatcherServlet : FrameworkServlet '': initialization completed in 37 ms
com.yxm.security.dto.UserQueryCondition@1c3d9e28[
username=Jack
age=18
]
使用@PageableDefault注解的方法
注意:PageableDefault注解是在:org.springframework.data.domain.Pageable
@RestController
public class UserController {
@RequestMapping(value = "/user",method = RequestMethod.GET)
public List user(UserQueryCondition condition, @PageableDefault(page = 1,size = 10,sort = "username,asc") Pageable pageable){
//使用反射工具
System.out.println(ReflectionToStringBuilder.toString(condition, ToStringStyle.MULTI_LINE_STYLE));
System.out.println(pageable.getPageSize());
System.out.println(pageable.getPageNumber());
System.out.println(pageable.getSort());
User user = new User();
List users = Arrays.asList(user, user, user);
return users;
}
}

测试请求方法:

@Test

public void whenQuerySuccess() throws Exception{
//发送一个url为/user的GET请求
mockMvc.perform(MockMvcRequestBuilders.get("/user")//请求user
.param("username","Jack")//传递请求参数
.param("age","18")
.param("size", "15")
.param("page", "3")
.param("sort", "age,desc")
.contentType(MediaType.APPLICATION\_JSON))//发送请求类型:application-json
.andExpect(MockMvcResultMatchers.status().isOk())//结果执行的期望\-返回状态码isOk
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3));//期望返回结果为3
}

jsonPath详解

1.3 详情请求

讲解:

@PathVariable 映射url片段到java方法的参数

在url声明中使用正则表达式来规范url的模式

@JsonView控制json输出内容

1.3.1 @PathVariable 映射url片段到java方法的参数

测试用例:

@Test
public void whenDetailInfoSuccess() throws Exception{
//发送一个url为/user/1 的GET请求
mockMvc.perform(MockMvcRequestBuilders.get("/user/1")
.contentType(MediaType.APPLICATION\_JSON\_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.username")
.value("Jack"));
}
后端Restful API接口
@RequestMapping(value \= "/user/{id}",method \= RequestMethod.GET)
public User DetailInfo(@PathVariable(name \= "id") String xxx){
User user = new User();
user.setUsername("Jack");
return user;
}

1.3.2 在url声明中使用正则表达式来规范url的模式

有时候我们需要在请求的参数中检验url中参数是否符合特定格式要求。比如:我们上面获取用户详情接口是id必须是数字。

创建测试用例,返回的应该是400错误

@Test

public void whenDetailInfoFail() throws Exception{
mockMvc.perform(MockMvcRequestBuilders.get("/user/a")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().is4xxClientError());
}

运行测试用例(后端代码如上):

服务端返回了200

客户端报错了,这和我们Restful API违背

api java restful 例子 java rest api 框架_json_02

@RequestMapping(value = "/user/{id:\\d+}",method = RequestMethod.GET)
public User DetailInfo(@PathVariable(name = "id") String xxx){
User user = new User();
user.setUsername("Jack");
return user;
}

此时运行测试用例:whenDetailInfoFail测试用例通过,服务端返回4xx

1.3.3 @JsonView控制json输出内容

1.3.3.1 @JsonView注解使用场景

我们在列表查询时候为了安全考虑不显示用户的密码,但是在用户详情查询(可能具备密码校验)需要返回password密码字段。

控制返回同一个对象时候,在不同条件下返回不同的视图对象。

1.3.3.2 @JsonView注解使用步骤

使用接口声明多个视图

在值对象的GET方法上指定视图

在controller方法上指定视图

import com.fasterxml.jackson.annotation.JsonView;
public class User {
/*
*使用接口声明多个视图
*/
public interface UserSimpleView{};
public interface UserDetailView extends UserSimpleView{};
private String username;
private String password;
//在值对象的GET方法上指定视图
@JsonView(UserSimpleView.class)
public String getUsername() {
return username;
}
@JsonView(UserDetailView.class)
public String getPassword() {
return password;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
}
controller处理:
@RestController
public class UserController {
@RequestMapping(value = "/user",method = RequestMethod.GET)
@JsonView(User.UserSimpleView.class)
public List user(UserQueryCondition condition, @PageableDefault(page = 1,size = 10,sort = "username,asc") Pageable pageable){
//使用反射工具
System.out.println(ReflectionToStringBuilder.toString(condition, ToStringStyle.MULTI_LINE_STYLE));
System.out.println(pageable.getPageSize());
System.out.println(pageable.getPageNumber());
System.out.println(pageable.getSort());
User user = new User();
List users = Arrays.asList(user, user, user);
return users;
}
@RequestMapping(value = "/user/{id:\\d+}",method = RequestMethod.GET)
@JsonView(User.UserDetailView.class)
public User DetailInfo(@PathVariable(name = "id") String xxx){
User user = new User();
user.setUsername("Jack");
return user;
}
}
测试用例中打印出结果详情:
@Test
public void whenQuerySuccess() throws Exception{
//发送一个url为/user的GET请求
String result = mockMvc.perform(MockMvcRequestBuilders.get("/user")//请求user
.param("username","Jack")//传递请求参数
.param("age","18")
.param("size", "15")
.param("page", "3")
.param("sort", "age,desc")
.contentType(MediaType.APPLICATION_JSON))//发送请求类型:application-json
.andExpect(MockMvcResultMatchers.status().isOk())//结果执行的期望返回状态码isOk
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3))//期望返回结果为3
.andReturn().getResponse().getContentAsString();
System.out.println("whenQuerySuccess:"+result);
//结果输出:whenQuerySuccess:[{"username":null},{"username":null},{"username":null}]
}
@Test
public void whenDetailInfoSuccess() throws Exception{
//发送一个url为/user/1 的GET请求
String result = mockMvc.perform(MockMvcRequestBuilders.get("/user/1")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.username")
.value("Jack"))
.andReturn().getResponse().getContentAsString();
System.out.println("whenDetailInfoSuccess:"+result);
//输出结果:whenDetailInfoSuccess:{"username":"Jack","password":null}
}
1.4 创建请求
讲解:
@RequestBody 映射请求体到java方法的参数
日期类型参数处理
@Valid注解和BingingResult验证请求参数的合法性并处理校验结果
写任何方法时候我们以测试用例为入口,先写测试用例,再写对应方法实现。
1.user
public class User {
public interface UserSimpleView{};
public interface UserDetailView extends UserSimpleView{};
private String id;
private String username;
@NotBlank
private String password;
private Date birthday;
@JsonView(UserSimpleView.class)
public Date getBirthday() {
return birthday;
}
@JsonView(UserSimpleView.class)
public String getId() {
return id;
}
//在值对象的GET方法上指定视图
@JsonView(UserSimpleView.class)
public String getUsername() {
return username;
}
@JsonView(UserDetailView.class)
public String getPassword() {
return password;
}
}
1.Test
@Test
public void whenCreateSuccess() throws Exception{
long time = new Date().getTime();
String content = "{\"username\\":\"Jack\",\"password\":null,\"birthday\":"+time+"}";
String resut = mockMvc.perform(MockMvcRequestBuilders.post("/user")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(content)).andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value("1"))//返回的json对象属性id值为1
.andReturn().getResponse().getContentAsString();
System.out.println("Test:"+resut);
//Test:{"id":"1","username":"Jack","password":null,"birthday":1582190517178}
}
3. create接口
/**
* 接口日期常见处理:
* 1.通过日期格式化转换,比如: yyyy-MM-dd HH:mm:ss
* 但是当我们多个终端:web、app、第三方同时调用这个接口时候:并且在每个终端显示的格式可能不一样
* app端显示时分秒、web端显示年月日
* 2.所有参数传递时候:不用指定格式时间传递,那传什么呢?传时间戳
* 时间戳是一个精确到毫秒的值,前端拿到时间戳之后决定怎样展示。
* @param user
* @return
*/
@PostMapping
public User create(@Valid @RequestBody User user, BindingResult errors){
/**
* 参数校验:最常用方式:
* 1.自己写代码校验:自己写 非常繁琐 可能 有时候有代码重复修改
* 2.java发展到现在,其实常见的都有现有框架组件去解决的:在对象属性上添加注解:@Valid会根据参数对象属性注解进行校验;
*
*上面注解添加@Valid在我们进行参数校验时候,如果没有过的话直接打回来返回4xx错误码(Restful API就是按照code);有时候参数没有校验通过的时候,我们也是需要进入方法体做一些处理的
* 此时用到注解:BingingResult
*
* BingingResult类需要跟@Valid配合的
*/
if(errors.hasErrors()){
errors.getAllErrors().stream().forEach(error->System.out.println(error.getDefaultMessage()));
//may not be empty
}
//使用反射工具
System.out.println("create:"+ReflectionToStringBuilder.toString(user, ToStringStyle.MULTI_LINE_STYLE));
//create:com.yxm.security.dto.User@6ffdbeef[
// id=
// username=Jack
// password=
// birthday=Thu Feb 20 17:21:57 CST 2020
//]
user.setId("1");
return user;
}

1.5 修改和删除请求

用户修改和删除接口;主要涉及到:

常见验证注解

自定义错误处理消息

自定义校验注解

上面讲到,@Valid会根据修饰参数对象的属性上的注解校验规则来校验,并将校验后的结果封装到:BindingResult errors中去,作为方法参数传进来。

1.51.常见验证注解(Hibernate Validator)

常见验证注解主要指代:Hibernate Validator。

其注解可以参考:...

我们在开发修改和删除的Restful API接口引入注解校验只是,与新增类似,我们先I写测试用例,然后运行后再写代码(原则:测试用例也是代码,我们需要先保证测试用例代码先跑起来,说明测试用例代码没错才能起到真正校验能力),报405说明请求method不支持。

书写测试用例:

@Test
public void whenUpdateSuccess() throws Exception{
//与创建区别:创建时候是post请求,我们修改时候用put请求,创建时候是没有id的但是修改时候需要根据id去修改,所以有id
Date date = new Date(LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());//定义未来时间作为生日过去时间的校验
System.out.println(date);
String content = "{\"id\":\"1\",\"username\":\"Jack\",\"password\":null,\"birthday\":"+date.getTime()+"}";
String resut = mockMvc.perform(MockMvcRequestBuilders.put("/user/1")//针对id为1的用户进行修改
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(content)).andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value("1"))//返回的json对象属性id值为1
.andReturn().getResponse().getContentAsString();
System.out.println("Test:"+resut);
//Test:{"id":"1","username":"Jack","password":null,"birthday":1582190517178}
}

书写controller接口并运行测试用例后输出:

api java restful 例子 java rest api 框架_User_03

上面输出的参数校验结果,一:是英文的;二是:不便于我们封装。

我们可以在对象属性的校验注解里面的message属性添加我们的校验结果。

public class User {
/*
* 使用接口声明多个视图
*/
public interface UserSimpleView{};
public interface UserDetailView extends UserSimpleView{};
private String id;
private String username;
@NotBlank(message = "密码不能为空")
private String password;
@Past(message = "生日必须是过去的时间")
private Date birthday;//声明生日是过去时间
}

在很多情况下,默认的Hibernate Validator提供的注解并不能满足我们的需求,不能满足我们的业务逻辑,有时候我们的业务逻辑是有复杂数据的:比如这个订单在数据库中存在与否,是不是重复的?这些并不是简单判断传过来的值就可以了,还要做一些其他东西,所以我们需要自个去写一些校验的逻辑。自个去写的逻辑怎样用注解去标识呢?

定义注解:MyConstraint

@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MyConstraintValidator.class)//定义当前注解用什么类去校验;把校验的逻辑写到某个类里
public @interface MyConstraint {
/**
* 参考Hibernate Validator相关注解我们知道 校验类的相关注解需要添加基本的三个属性
*/
String message() default "{org.hibernate.validator.constraints.NotBlank.message}";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
}

定义注解逻辑检验处理类:

public class MyConstraintValidator implements ConstraintValidator {
/**
* 注意:
* 1.ConstraintValidator A-指代注解 T-指代注解A所修饰属性的类型;我们定义成Object
* 2.我们可以把spring的bean通过Autowire注解引入到此类里面的成员变量
* 3.在MyConstraintValidator类上不用添加@Component注解,因为实现ConstraintValidator的类会自动被spring管理
* @param myConstraint
*/
/* @Autowired
private HelloService helloService;*/
@Override
public void initialize(MyConstraint myConstraint) {
System.out.println("my ConstraintValidator init");
}

@Override

public boolean isValid(Object o, ConstraintValidatorContext context) {
System.out.println(o);
return false;
}
}
3.在user实体上使用:
@MyConstraint(message = "这是一个测试")
private String username;

4.我们运行测试用例(由于我们的注解校验类的isValid是一个返回false的方法,所以会始终输出message):

生日必须是过去的时间

这是一个测试

密码不能为空

删除接口:

Test

public void whenDeleteSuccess() throws Exception{
mockMvc.perform(MockMvcRequestBuilders.delete("/user/1")//针对id为1的用户进行修改
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk());
}
后端接口:
@DeleteMapping("/{id:\\d+}")
public void delete(@PathVariable String id) {
System.out.println("update:" + id);
}