spring MVC 开发 restful服务 一 restful的入门与测试

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/uotail/article/details/86587200

1、restful风格介绍

这张图左边是传统的服务请求,右边是restful风格的,可以对比的看一下,下边的描述是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方法的参数上

改造

注意:

  1. 方法参数上必须加@PathVariable注解 否则绑定不到参数上
  2. 当方法参数名称与映射路径上的{}里面的名称不一致时,需要给@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返回到前台
而查询单个用户的时候我们经过一些权限的认证然后把密码返回

使用步骤

  1. 使用接口来声明多个视图
  2. 在属性的get方法上指定视图
  3. 在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.csdn.net/uotail/article/details/80211897
https://blog.csdn.net/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 请求做修改

先写一个测试用例

他和创建基本一样,都要传用户基本信息,
但是有两点不一样

  1. 请求为put请求
  2. 创建的时候我们不知道用户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 删除成功

    }

猜你喜欢

转载自blog.csdn.net/uotail/article/details/86587200