Spring Security技术栈开发企业级认证与授权(二)使用Spring MVC开发RESTful API

RESTful一种软件架构风格、设计风格,而不是标准,只是提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。本篇博客主要讲述使用Spring MVC开发RESTful风格的API

一、传统API和RESTful API

传统的APIRESTful API如下表所示:

行为 传统API RESTful API 方法
查询 /user/query?name=lemon /user?name=lemon GET
详情 /user/getInfo?id=1 /user/1 GET
创建 /user/create?name=lemon /user POST
修改 /user/update?id=1&name=tom /user/1 POST
删除 /user/delete?id=1 /user/1 GET

RESTful风格的API有如下几个特点:

  • 使用URL描述资源

  • 使用HTTP方法描述行为,使用HTTP状态码来表示不同的结果

  • 使用JSON进行数据交互

  • RESTful只是一种风格,并不是一种强制的标准

二、常用注解介绍

这里介绍几个常用的注解:

  • @RestController标明此Controller提供RESTful API

  • @RequestMapping及其变体(@GetMappingPostMapping等),映射HTTP请求到Java方法

  • @RequestParam映射请求参数到Java方法的参数

  • @PathVariable映射URL片段到Java方法的参数

  • @PageableDefault指定默认分页参数

  • @JsonView按照指定方式序列化Java对象

代码案例:这里有UserUserController以及UserControllerTest三个类,其中UserControllerTest的四个测试方法分别对应UserController类中的四个方法。

  • User
package com.lemon.security.web.dto;

import lombok.Data;

/**
 * @author lemon
 * @date 2018/3/22 下午3:40
 */
@Data
public class User {

    private String username;

    private String password;

}
  • UserController
package com.lemon.security.web.controller;

import com.lemon.security.web.dto.User;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

/**
 * @author lemon
 * @date 2018/3/22 下午3:39
 */
@RestController
public class UserController {

    @RequestMapping(value = "/user1", method = RequestMethod.GET)
    public List<User> query1() {
        return generateUsers();
    }

    @GetMapping("/user2")
    public List<User> query2(@RequestParam String username) {
        System.out.println(username);
        return generateUsers();
    }

    @GetMapping("/user3/{username}")
    public List<User> query3(@PathVariable String username) {
        System.out.println(username);
        return generateUsers();
    }

    @GetMapping("/user4")
    public List<User> query4(@PageableDefault(page = 1, size = 2, sort = "username") Pageable pageable) {
        System.out.println(pageable.getPageNumber());
        System.out.println(pageable.getPageSize());
        System.out.println(pageable.getSort());
        return generateUsers();
    }

    private List<User> generateUsers() {
        List<User> users = new ArrayList<>();
        users.add(new User());
        users.add(new User());
        users.add(new User());
        return users;
    }
}
  • UserControllerTest
package com.lemon.security.web;

import com.lemon.security.web.application.MainApplication;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
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;

/**
 * @author lemon
 * @date 2018/3/22 下午3:14
 */
@RunWith(SpringRunner.class)
@SpringBootTest(classes = MainApplication.class)
public class UserControllerTest {

    // 注入一个web应用环境(容器)
    @Autowired
    private WebApplicationContext webApplicationContext;

    // MVC环境对象
    private MockMvc mockMvc;

    @Before
    public void init() {
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }

    @Test
    public void query1() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/user1")
                .contentType(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3));
    }

    @Test
    public void query2() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/user2")
                .param("username", "lemon")
                .contentType(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3));
    }

    @Test
    public void query3() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/user3/lemon")
                .contentType(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3));
    }

    @Test
    public void query4() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/user4")
                .param("size", "3")
                .param("page", "1")
                .param("sort", "username,desc")
                .contentType(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3));
    }
}

第三个类,也就是UserControllerTestRESTful API的测试类,现在对其进行简单介绍:
由于RESSTful风格的API不能通过浏览器地址栏来进行测试,因为地址栏发送的请求都是GET类型的,而RESTful API正是通过请求方法来判断请求行为是查询、修改、删除、增加中的哪一种的,所以测试RESSTful风格的API都是通过编码来进行测试的。

  • 通过@Autowired WebApplicationContext webApplicationContext:注入web环境的ApplicationContext容器;

  • 然后通过MockMvcBuilders.webAppContextSetup(webApplicationContext).build()创建一个MockMvcMVC环境进行测试;

  • MockMvcRequestBuilders.get()方法是发送一个GET请求,param()是设置请求参数,contentType()是我设置内容类型(JSON格式),andExpect()方法是希望得到什么样的测试结果,MockMvcResultMatchers()是返回结果的匹配是否正确。jsonPath()方法是解析返回的JSON数据,关于它的介绍可以在github上找到。

运行上面的四个测试方法都可以通过测试。对于@PathVariable再写一个测试案例:

  • Controller方法:
@GetMapping("/getInfo/{id:\\d+}")
public User getInfo(@PathVariable String id) {
    System.out.println("查询的对象ID为:".concat(id));
    User user = new User();
    user.setUsername("lemon");
    return user;
}

上面的方法URL片段进行了正则表达式的验证,ID只能是数字。

  • 测试方法:
@Test
public void getInfo() throws Exception {
     mockMvc.perform(MockMvcRequestBuilders.get("/getInfo/1")
             .contentType(MediaType.APPLICATION_JSON_UTF8))
             .andExpect(MockMvcResultMatchers.status().isOk())
             .andExpect(MockMvcResultMatchers.jsonPath("$.username").value("lemon"));
 }

接下来详细介绍@JsonView这个注解的使用。
@JsonView的使用步骤

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

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

  • Controller方法上指定视图

对于上面的步骤,进行如下解释如下:
一般对Java对象进行序列化Json的时候,会考虑到只序列化部分字段,那么就可以使用@JsonView这个注解。在这里使用User实体类进行举例,首先,在实体类上定义两个接口,第一个接口是简单视图(UserSimpleView),表示之序列化username这个字段,而第二个接口是详情视图(UserDetailView extends UserSimpleView),表示不仅序列化username字段,还序列化password字段。然后使用@JsonView注解将两个视图绑定到对应的字段的get方法上面,由于UserDetailView继承了UserSimpleView这个视图,所以在Controller方法上使用UserDetailView视图的时候,会同时序列化两个字段,而使用UserSimpleView的时候仅仅只会序列化username这一个字段。下面进行代码展示:

  • User类
package com.lemon.security.web.dto;

import com.fasterxml.jackson.annotation.JsonView;

/**
 * @author lemon
 * @date 2018/3/22 下午3:40
 */
public class User {

    public interface UserSimpleView {}

    public interface UserDetailView extends UserSimpleView {}

    private String username;

    private String password;

    @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;
    }
}
  • UserController的两个方法
@GetMapping("/getSimpleUser")
@JsonView(User.UserSimpleView.class)
public User getSimpleUser() {
    User user = new User();
    user.setUsername("lemon");
    user.setPassword("123456");
    return user;
}

@GetMapping("/getDetailUser")
@JsonView(User.UserDetailView.class)
public User getDetailUser() {
    User user = new User();
    user.setUsername("lemon");
    user.setPassword("123456");
    return user;
}

从上面的步骤分析可知,第一个方法返回的user对象在序列化为json的时候,只会序列化username字段,而第二个方法则会同时序列化两个字段。

  • 两个测试方法
@Test
public void getSimpleUser() throws Exception {
    String result = mockMvc.perform(MockMvcRequestBuilders.get("/getSimpleUser")
            .contentType(MediaType.APPLICATION_JSON_UTF8))
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andReturn().getResponse().getContentAsString();
    System.out.println(result);
}

@Test
public void getDetailUser() throws Exception {
    String result = mockMvc.perform(MockMvcRequestBuilders.get("/getDetailUser")
            .contentType(MediaType.APPLICATION_JSON_UTF8))
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andReturn().getResponse().getContentAsString();
    System.out.println(result);
}

两个方法打印的结果分别为:

{"username":"lemon"}
{"username":"lemon","password":"123456"}

三、编写RESTful API

1、用户详情请求(GET)

对于RESTful API,一般都不再使用传统的参数传递,而是使用资源映射的方式,也就是使用@PathVariable,为了保持文档的完整性,这里再次使用上面已经举过的案例:

  • Controller方法:
@GetMapping("/getInfo/{id:\\d+}")
public User getInfo(@PathVariable String id) {
    System.out.println("查询的对象ID为:".concat(id));
    User user = new User();
    user.setUsername("lemon");
    return user;
}

上面的方法URL片段进行了正则表达式的验证,ID只能是数字。

  • 测试方法:
@Test
public void getInfo() throws Exception {
     mockMvc.perform(MockMvcRequestBuilders.get("/getInfo/1")
             .contentType(MediaType.APPLICATION_JSON_UTF8))
             .andExpect(MockMvcResultMatchers.status().isOk())
             .andExpect(MockMvcResultMatchers.jsonPath("$.username").value("lemon"));
 }
2、用户创建请求(POST)

这里主要介绍三个知识点:

  • @RequestBody映射请求体到Java方法参数

  • @Valid注解和BindingResult验证请求参数的合法性并处理校验结果

  • @RequestBody是将前台传递过来的JSON字符串转换成Java对象,

1)第一个知识点的案例,将JSON字符串映射到Java对象中

在之前的User类上加上一个id字段,然后进行下面的测试。
Controller方法:用户创建的方法

@PostMapping("/user1")
public User create1(@RequestBody User user) {
    System.out.println(ReflectionToStringBuilder.reflectionToString(user, ToStringStyle.MULTI_LINE_STYLE));
    user.setId(1);
    return user;
}

测试方法:测试创建用户方法

@Test
public void create1() throws Exception {
    String content = "{\"username\":\"lemon\",\"password\":\"123456\"}";
    mockMvc.perform(MockMvcRequestBuilders.post("/user1")
            .contentType(MediaType.APPLICATION_JSON_UTF8)
            .content(content))
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1));
}

测试方法传递过来的数据是一个JSON字符串,正是@RequestBody注解将JSON字符串转化成为Java对象。

2)第二个知识点的案例,@Valid注解和BindingResult验证请求参数的合法性并处理校验结果

当使用Java类来接受参数的是,往往需要对参数进行校验,而校验一般都是使用Hibernate提供的校验器来进行校验,在Java实体类的字段上,我们常常加上@NotBlank@NotNull@Null@Min@Max@NotEmpty等注解进行校验规则定义,然后在Controller方法参数前加上@Valid注解来进行校验,校验的错误结果存储在BindingResult对象内。这里我向后台传递一个JSON字符串,人为使得usernamepassword两个字段为null。这里仅仅简单介绍表单验证的注解,下一篇博客将重点介绍。接下来请看案例:

  • User类字段
private Integer id;

@NotEmpty(message = "用户名不能为空")
private String username;

@NotEmpty(message = "密码不能为空")
private String password;

private Date birthday;
  • UserController的create2()方法
@PostMapping("/user2")
public User create2(@Valid @RequestBody User user, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        bindingResult.getAllErrors().forEach(error -> System.out.println(error.getDefaultMessage()));
    }
    System.out.println(ReflectionToStringBuilder.reflectionToString(user, ToStringStyle.MULTI_LINE_STYLE));
    user.setId(2);
    return user;
}
  • 测试方法
@Test
public void create2() throws Exception {
    String content = "{\"username\":null,\"password\":null}";
    mockMvc.perform(MockMvcRequestBuilders.post("/user2")
            .contentType(MediaType.APPLICATION_JSON_UTF8)
            .content(content))
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(2));
}

运行结果为:

用户名不能为空
密码不能为空
com.lemon.security.web.dto.User@58d79479[
  id=<null>
  username=<null>
  password=<null>
  birthday=<null>
]
3、用户修改和删除请求(PUT、DELETE)

由于RESTful风格的API是基于方法来进行区分的,所以设计到数据的修改和删除使用的方法是PUTDELETE,接下来使用案例的方式介绍修改和删除API的开发。

  • 测试方法:
@Test
public void update() throws Exception {
    mockMvc.perform(MockMvcRequestBuilders.put("/user/1")
            .contentType(MediaType.APPLICATION_JSON_UTF8))
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andExpect(MockMvcResultMatchers.jsonPath("$.username").value("lemon"));
}

@Test
public void delete() throws Exception {
    mockMvc.perform(MockMvcRequestBuilders.delete("/user/1")
            .contentType(MediaType.APPLICATION_JSON_UTF8))
            .andExpect(MockMvcResultMatchers.status().isOk());
}
  • Controller方法:
@PutMapping("/user/{id:\\d+}")
public User update(@PathVariable Integer id) {
    User user = new User();
    user.setId(id);
    System.out.println("模拟修改");
    user.setUsername("lemon");
    return user;
}

@DeleteMapping("/user/{id:\\d+}")
public void delete(@PathVariable Integer id) {
    System.out.println("模拟修改,修改ID:".concat(String.valueOf(id)));
}

回顾一下RESTful风格的API,都是使用URL描述资源,使用请求方法来区别不同的API。这极大程度地简化了API开发的流程,推荐使用。

Spring Security技术栈开发企业级认证与授权系列文章列表:

Spring Security技术栈开发企业级认证与授权(一)环境搭建
Spring Security技术栈开发企业级认证与授权(二)使用Spring MVC开发RESTful API
Spring Security技术栈开发企业级认证与授权(三)表单校验以及自定义校验注解开发
Spring Security技术栈开发企业级认证与授权(四)RESTful API服务异常处理
Spring Security技术栈开发企业级认证与授权(五)使用Filter、Interceptor和AOP拦截REST服务
Spring Security技术栈开发企业级认证与授权(六)使用REST方式处理文件服务
Spring Security技术栈开发企业级认证与授权(七)使用Swagger自动生成API文档
Spring Security技术栈开发企业级认证与授权(八)Spring Security的基本运行原理与个性化登录实现
Spring Security技术栈开发企业级认证与授权(九)开发图形验证码接口
Spring Security技术栈开发企业级认证与授权(十)开发记住我功能

示例代码下载地址:

项目已经上传到码云,欢迎下载,内容所在文件夹为chapter002

猜你喜欢

转载自blog.csdn.net/lammonpeter/article/details/79655671