查询用户以及用户详情

最简单的,也最常用的方式是 GET 请求,用于查询全部数据或者其中一条数据的详情。下面先对 RestfulGET 请求进行介绍。

编写测试类程序

写测试类

如何保证自己的服务可以运行,其中最重要的方式是写测试类。在第 3 章中所有 Restful 服务都将通过测试类进行测试,所以在这一小节介绍如何写一个测试类,后面的测试类主要测试函数。首先引入测试的依赖。

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

在第 2 章的基础上讲解,所以仍然使用 Demo 项目。新建包并包括 UserControllerTest 类,测试类的大纲如图 3.1 所示。

image 2024 03 31 12 38 49 062
Figure 1. 图3.1 测试类的大纲

然后,先写一个测试类程序,导入的包不另外添加,代码如下所示。

//如何运行程序
@RunWith(SpringRunner.class)
//说明这是一个Spring Boot测试用例
@SpringBootTest
public class UserControllerTest {

    //伪造测试用例,不需要执行tomcat,运行速度会快一些
    @Autowired
    private WebApplicationContext wac;
    //伪造一个MVC的环境
    private MockMvc mockMvc;

    @Before
    public void setUp() {
        mockMvc=MockMvcBuilders.webAppContextSetup(wac).build();
    }

    /**
     * @throws Exception
     * @Desciption 测试用例
     */
    @Test
    public void whenQuerySuccess() throws Exception {
        //发送GET请求
        mockMvc.perform(MockMvcRequestBuilders.get("/user")
                        .contentType(MediaType.APPLICATION_JSON_UTF8))
                //判断是否符合预期
                .andExpect(MockMvcResultMatchers.status().isOk());
    }
}

最后我们运行测试。也许读者会很疑惑,控制类还没写,明知错误为什么还要运行?其实,这里的测试类也是一个程序,我们首先要保证程序正确,可以运行,如图 3.2 所示。

image 2024 03 31 12 40 11 636
Figure 2. 图3.2 测试类运行效果

服务返回的 HTTP 状态值是 404。虽然报错,但还是符合预期,这说明测试类程序没有问题,后面会补充控制类,让这个测试类程序运行起来。

知识点

解释一下测试类用到的知识点。MockMvc 是服务端 Spring MVC 测试支持的主入口点,可以用来模拟客户端请求。这里主要用于测试,因此需要引入 MockMvc

注解 @RunWith:指定测试要运行的运行器,例如 SpringRunner.class

注解 @Autowired:自动加载 Bean,在后文会仔细介绍。

WebApplicationContext:WebMVCIOC 容器对象,需要声明并通过 @Autowired 自动装配进来。

MockMvcRequestBuilders:用于构建 MockHttpServletRequestBuilder。为什么要构建 MockHttpServletRquestBuilder?这是因为 MockHttpServletRequestBuilder 是用于构建 MockHttpRequest,作为 MockMvc 的请求对象。

MockMvc:通过 MockMvcBuilderswebAppContextSetup(WebApplicationContext context) 方法获取 DefaultMockMvcBuilder。再调用 build 方法,初始化 MockMvc

perform 方法:perform(RequestBuilder requestBuilder) throws Exception 执行请求,需要传入 MockHttpServletRequest对象,作为请求对象。

andExpect 方法:andExpect(ResultMatcher matcher) throws Exception,进行预期匹配。例如 MockMvcResultMatchers.status().isOk() 表示预期匹配响应成功。

常用注解

Spring Boot 的注解特别多,我们只介绍常用的注解。

  • @RestController:表明此 Controller 提供的是 Restful 服务。

  • @RequestMapping 及其变体:将 URL 映射到 Java

  • @RequestParam:映射请求参数到 Java 上的参数。

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

下面,我们以查询全部用户为例,分别讲解这几个常用的注解。

@RestController与@RequestMapping小测试

在图 3.2 中出现的报错还没有解决,在这个基础上讲解注解时,我们就顺便解决出现的 404 服务访问不到的问题,其中图3.3 就是执行的效果。控制类代码如下所示。

@RestController
public class UserController {
   @RequestMapping(value="/user",method=RequestMethod.GET)
   public List<User> query(){
      List<User> userList=new ArrayList();
      //将list中添加了三个空的user对象,这里暂时没对user进行属性赋值
      userList.add(new User());
      userList.add(new User());
      userList.add(new User());
      return userList;
   }
}

测试类代码如下所示。

/**
  * @throws Exception
  * @Desciption RestController与RequestMapping的测试用例
  */
@Test
public void whenQuerySuccess() throws Exception {
   //发送GET请求
   mockMvc.perform(MockMvcRequestBuilders.get("/user")
        .contentType(MediaType.APPLICATION_JSON_UTF8))
        //判断是否符合预期
        .andExpect(MockMvcResultMatchers.status().isOk())
          andExpect(MockMvcResultMatchers.jsonPath("$.length()"). value(3));
}

其中,User.java 中的代码如下所示。

public class User {
   private String username;
   private String password;
   /**
   GET与SET方法省略
   */
}

执行结果如图 3.3 所示。

image 2024 03 31 13 14 17 894
Figure 3. 图3.3 执行结果

在控制类中,使用了 @RestController 注解,说明当前的控制类可以提供 Restful 的服务。在 query 方法中添加了 @RequestMapping 注解,将 URL 映射到 Java 上,其参数 value 是请求资源 URLmethod 是对该资源的操作行为。

在测试类中,模拟了一个 Restful 服务,使用 GET 方法,其访问的路径为 /user。根据规则,这个时候,程序会执行控制器中的 query 方法。

@RequestParam测试

@RequestParam 测试的知识点稍微有点多,这里主要介绍接收单个参数值、其他的属性,以及接收多个参数值的不足。接收单个参数值,控制类代码如下所示。

/**
  * @Desciption 测试@RequestParam
  * @return
  */
@RequestMapping(value="/user",method=RequestMethod.GET)
public List<User> query2(@RequestParam String username){
   //打印观察是否接收到username参数
   System.out.println("username is :"+username);
   List<User> userList=new ArrayList();
   //将list中添加了三个空的user对象,这里暂时没对user进行属性赋值
   userList.add(new User());
   userList.add(new User());
   userList.add(new User());
   return userList;
}

测试类代码如下所示。

/**
  * @throws Exception
  * @Desciption 主要用于测试@RequestParam的单个参数接收
  */
@Test
public void whenQuerySuccess2() throws Exception {
   //发送请求
   mockMvc.perform(MockMvcRequestBuilders.get("/user")
        .param("username", "tom")
        .contentType(MediaType.APPLICATION_JSON_UTF8))
        .andExpect(MockMvcResultMatchers.status().isOk());
}

最终效果如图 3.4 所示。

image 2024 03 31 13 16 34 336
Figure 4. 图3.4 执行控制台

@RequestParam的其他属性

其他属性如图 3.5 所示。

image 2024 03 31 13 18 44 568
Figure 5. 图3.5 @RequestParam的其他属性
  • defaultValue:参数的默认值。当存在这个参数的时候,如果访问的时候没有传递参数,程序中的参数会获取 defaultValue 中的值。

  • required:指定参数是否为必需传递的参数,默认是 true

namevalue:name 别名是 value,所以 namevalue 等价。在程序中,如果我们使用这个属性,就可以不在控制类的程序中使用访问时的参数,可以使用其别名。下面是一个控制类程序的实例,测试时可以使用前面的测试类进行测试。

@RequestMapping(value="/user",method=RequestMethod.GET)
public List<User> query3(@RequestParam(name="username") String myname){
   //打印观察是否接收到username参数
   System.out.println("username is :"+myname);
   List<User> userList=new ArrayList();
   //将list中添加了三个空的user对象,这里暂时没对user进行属性赋值
   userList.add(new User());
   userList.add(new User());
   userList.add(new User());
   return userList;
}

@RequestParam的不足

通过上面的实例,我们可以接收单个参数,但如果有多个参数怎么办?这明显不能满足需求,那么我们是否可以通过对象进行传递?这种情况下,控制类代码如下所示。

/**
  * @Desciption 测试多参数传递
  * @return
  */
@RequestMapping(value="/user",method=RequestMethod.GET)
public List<User> query4(UserQuery userQuery){
    //通过反射的方式进行打印效果
    System.out.println(ReflectionToStringBuilder.toString(userQuery, ToStringStyle.MULTI_LINE_STYLE));
    System.out.println("username:"+userQuery.getUsername());
    List<User> userList=new ArrayList();
    //将list中添加了三个空的user对象,这里暂时没对user进行属性赋值
    userList.add(new User());
    userList.add(new User());
    userList.add(new User());
    return userList;
}

测试类代码如下所示。

/**
  * @throws Exception
  * @Desciption 主要用于测试多个参数的传递
  */
@Test
public void whenQuerySuccess3() throws Exception {
  //发送请求
  mockMvc.perform(MockMvcRequestBuilders.get("/user")
         .param("username", "tom")
         .param("age", "18")
         .param("toAge","20")
         .contentType(MediaType.APPLICATION_JSON_UTF8))
  .andExpect(MockMvcResultMatchers.status().isOk());
}

UserQuery.java 相关代码如下所示。

public class UserQuery {
   private String username;
   private int age;
   private int ageTo;
   private String address;
   public String getUsername() {
      return username;
   }
   //get and set
}

执行结果如图 3.6 所示。

image 2024 03 31 13 22 48 207
Figure 6. 图3.6 执行结果

在上面的程序中使用反射的方式输出了对象的属性,下面做一些分析。

ReflectionToStringBuildercommons-lang 里的一个类,系统中一般都要打印日志,因为所有实体的 ToString 方法都用的是简单的 “”,而每 “” 一个就会新建一个 String 对象,这样如果系统内存小就会爆内存(前提是系统实体比较多)。使用 ToStringBuilder 可以避免爆内存这种问题。

上面的输出是多行的,因为使用了多行输出的模式 MULTI_LINE_STYLE。默认是单行模式 NO_FIELD_NAMES_STYLE。其原理就是通过 Java 的反射功能获取值,然后组成一个 Buffer

注意:transientstatic 修饰的属性不能显示出来,但父类的可以显示出来。

@PageableDefault

控制类代码如下所示。

@RequestMapping(value="/user",method=RequestMethod.GET)
    public List<User> query5(UserQuery userQuery,@PageableDefault Pageable pageable){
    // 通过反射的方式进行输出效果
    System.out.println(ReflectionToStringBuilder.toString(userQuery, ToStringStyle.MULTI_LINE_STYLE));
    //
    System.out.println(pageable.getPageNumber());
    System.out.println(pageable.getPageSize());
    System.out.println(pageable.getSort());
    //
    List<User> userList = new ArrayList();
    // 将list中添加了三个空的user对象,这里暂时没对user进行属性赋值
    userList.add(new User());
    userList.add(new User());
    userList.add(new User());
    return userList;
}

测试类代码如下所示。

/**
  * @throws Exception
  * @Desciption 主要用于测试PageableDefault
  */
@Test
public void whenQuerySuccess4() throws Exception {
  //发送请求
  mockMvc.perform(MockMvcRequestBuilders.get("/user")
         .param("username", "tom")
         //分页参数,查询第三页,每页15条数据,按照age降序
         .param("page", "3")
         .param("size", "15")
         .param("sort", "age,desc")
         .contentType(MediaType.APPLICATION_JSON_UTF8))
  .andExpect(MockMvcResultMatchers.status().isOk());
}

执行结果如图 3.7 所示。

image 2024 03 31 13 25 17 372
Figure 7. 图3.7 执行结果

查询用户详情

本小节在讲解查询用户详情时,需要结合 @PathVariable 注解。不过考虑到一些情况,也会讲解一下 @JsonView 注解。

@PathVariable

@PathVariable 是映射 URL 片段到 Java 的注解。这里的注解也是获取 URL 中的参数,那么它与 @RequestParam 注解有什么区别?

举例子说明,使用 @RequestParam 注解时,URLhttp://host:port/path?参数名=参数值 ,这个注解获取的是这里的参数值;使用 @PathVariable 注解时,URLhttp://host:port/path/参数值 。 控制类代码如下所示。

/**
  * @Desciption 查询用户详情
  */
@RequestMapping(value="/user/{id}",method=RequestMethod.GET)
public User getInfo(@PathVariable(value="id") String idid) {
   System.out.println("id is :"+idid);
   User user=new User();
   user.setUsername("tom");
   return user;
}

测试类代码如下所示。

/**
  * @throws Exception
  * @Desciption 主要用于测试PageableDefault
  */
@Test
public void whenGetInfoSuccess() throws Exception {
   //发送请求
   mockMvc.perform(MockMvcRequestBuilders.get("/user/2")
        .contentType(MediaType.APPLICATION_JSON_UTF8))
        .andExpect(MockMvcResultMatchers.status().isOk())
          .andExpect(MockMvcResultMatchers.jsonPath("$.username"). value("tom"));
}

控制台内容如下所示。

id is :2

通过控制类,我们需要学会如何使用 @PathVariable 注解,通过注解可以获取路径中参数,这个参数是用户资源的一个查询条件,通过获取具体的条件就可以查询到具体的信息。上面的程序中没有使用数据库连接操作数据库,而是使用静态的方式模拟了这个效果。

@JsonView

@JsonViewJackson Json 中的一个注解,Spring webmvc 也支持这个注解,它的作用就是控制输入/输出后的 Json。使用步骤如下。

  • 使用接口声明多个视图。

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

  • Controller 方法上指定视图。

程序前两步代码如下所示。

public class User {
   /*
      创建两个接口,
          一个为用户简单视图,即精简版的Json数据视图。
          另一个为用户详细视图,即完整版的Json数据视图
   */
   public interface UserSimpleView{};
   public interface UserDetailView extends UserSimpleView{};
   private String username;
   private String password;

   //在对象的GET方法上指定视图
   @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;
   }
}

程序第三步代码如下所示。

/**
  * @Desciption 查询用户详情,测试JsonView的功能
  */
@RequestMapping(value="/user/{id:\\d+}")
@JsonView(User.UserSimpleView.class)
public User getInfo2(@PathVariable(value="id") String id) {
   System.out.println("id is :"+id);
   User user=new User();
   user.setUsername("tom");
   user.setPassword("123456");
   return user;
}

测试类代码如下所示。

/**
  * @Desciption 查询用户详情,测试JsonView的功能
  */
@RequestMapping(value="/user/{id:\\d+}")
@JsonView(User.UserSimpleView.class)
public User getInfo2(@PathVariable(value="id") String id) {
    System.out.println("id is :"+id);
    User user=new User();
    user.setUsername("tom");
    user.setPassword("123456");
    return user;
}

执行结果如下所示。

id is :1
result: {"username":"tom"}

从结果上可以看出,虽然 user 对象中已经有了 password 字段的值,但是在返回的值中却没有这个字段,很明显 @JsonView 注解过滤了这个字段。

所以,在一些场景中,我们可以使用这个注解来过滤返回的字段。同时,在上文的控制类程序中我们可以看到 @RequestMapping(value="/user/{id:\\d+}"),这里是 URL 声明的正则表达式,当需要参数满足一定的要求时,可以考虑使用。