飞道的博客

Spring Security技术栈学习笔记(二)RESTful API详解

362人阅读  评论(0)

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技术栈学习笔记(二)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技术栈学习笔记(十)开发记住我功能
Spring Security技术栈学习笔记(十一)开发短信验证码登录
Spring Security技术栈学习笔记(十二)将短信验证码验证方式集成到Spring Security
Spring Security技术栈学习笔记(十三)Spring Social集成第三方登录验证开发流程介绍
Spring Security技术栈学习笔记(十四)使用Spring Social集成QQ登录验证方式
Spring Security技术栈学习笔记(十五)解决Spring Social集成QQ登录后的注册问题
Spring Security技术栈学习笔记(十六)使用Spring Social集成微信登录验证方式

示例代码下载地址:

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

更多干货分享,欢迎关注我的微信公众号:爪哇论剑(微信号:itlemon)


转载:https://blog.csdn.net/Lammonpeter/article/details/79655671
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场