1

前言

使用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=JackGET /user?name=JackGET
详情/user/getInfo?id=1GET /user/1GET
创建/user/create?name=JackPOST /userPOST
修改/user/update?id=1&name=JackPOST /user/1PUT
删除/user/delete?id=1GET /user/1DELETE

增删查改传统和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,数字越大级别越高 越来满足此模型

9.png

  • 使用HTTP作为传输方式,不是http传输就不是restful API。
  • 引入资源概念,每个资源都有对应url;restful API是用URL描述资源,请求接口中无动作。
  • 使用HTTP方法进行不同操作、使用HTTP状态码表示不同结果。
  • 超媒体控制:在资源的表达中包含了链接信息。这种规范在大部分工作中很难达到,一般满足到level2。

1.2 查询请求

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

  1. 编写针对Restful API测试用例(使用web浏览器地址栏是检验不了PUT、post)
  2. 使用注解声明Restful API
  3. 在Restful API中传递参数

1.2.1 编写针对Restful API测试用例

首先需要引入测试依赖;

<dependency>  
 <groupId>org.springframework.boot</groupId>  
 <artifactId>spring-boot-starter-test</artifactId>  
</dependency>

我们有时候执行: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> user(@RequestParam(name="username",required = false,defaultValue = "Linda") String username){  
        User user = new User();  
  List<User> 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> user(UserQueryCondition condition){  
 //使用反射工具   System.out.println(ReflectionToStringBuilder.toString(condition, ToStringStyle.MULTI_LINE_STYLE));  
  User user = new User();  
  List<User> 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> 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<User> 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详解

jsonPath参考:https://github.com/json-path/...

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违背
10.png

@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注解使用场景
  1. 我们在列表查询时候为了安全考虑不显示用户的密码,但是在用户详情查询(可能具备密码校验)需要返回password密码字段。
  2. 控制返回同一个对象时候,在不同条件下返回不同的视图对象。
1.3.3.2 @JsonView注解使用步骤
  1. 使用接口声明多个视图
  2. 在值对象的GET方法上指定视图
  3. 在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> 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<User> 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=<null>
        //  username=Jack
        //  password=<null>
        //  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。
其注解可以参考:https://blog.csdn.net/danielz...

我们在开发修改和删除的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接口并运行测试用例后输出:
11.png

上面输出的参数校验结果,一:是英文的;二是:不便于我们封装。
我们可以在对象属性的校验注解里面的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<MyConstraint,Object> {
    /**
     * 注意:
     * 1.ConstraintValidator<A,T>  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);  
}

startshineye
91 声望26 粉丝

我在规定的时间内,做到了我计划的事情;我自己也变得自信了,对于外界的人跟困难也更加从容了,我已经很强大了。可是如果我在规定时间内,我只有3分钟热度,哎,我不行,我就放弃了,那么这个就是我自己的问题,因为你自己...