SpringBoot 2.x系列:构建RESTful Web服务

概览

前面的几篇文章,介绍了如使用Spring Boot来访问数据库,我们已经掌握了访问数据访问层组件的实现方法。接下来我们将会讨论另外一个组件,即如何使用Spring Boot来构建Web服务层。

为什么需要构建Web服务层?

看过前面文章的同学们知道,目前我们用Spring Boot构建的应用可以访问数据库,进行数据的存储和提取。但是我们的应用还没有对外提供服务的能力,在当下的分布式系统及微服务架构中,RESTful风格是一种主流的 Web 服务表现方式。接下来重点讨论下对RESTful的理解

RESTful理解

RESTful的API你可能经常听到,那具体是什么含义,什么样的API才满足RESTful的API呢,可能就比较迷惑了,

REST(Representational State Transfer,表述性状态转移)表述性状态转移这个确实抽象,它本质上只是一种架构风格而不是一种规范,这种架构风格把位于服务器端的访问入口看作一个资源,每个资源都使用 URI(Universal Resource Identifier,统一资源标识符) 得到一个唯一的地址,且在传输协议上使用标准的 HTTP 方法,比如最常见的 GET、PUT、POST 和 DELETE。满足这样风格的API,我们称之为RESTful API。举个例子,说明下满足RESTful风格API如何定义:

URL HTTP请求方法 描述
http://www/example.com/v1/users GET 获取User对象列表
http://www/example.com/v1/user/{id} GET 根据id查询User对象
http://www/example.com/v1/user POST 新增User对象
http://www/example.com/v1/user PUT 更新User对象
http://www/example.com/v1/user/{id} DELETE 根据ID删除User对象

另一方面,客户端与服务器端的数据交互涉及序列化、反序列号问题。关于序列化完成业务对象在网络环境上的传输的实现方式有很多,常见的有文本和二进制两大类。

目前 JSON 是一种被广泛采用的序列化方式,本课程中所有的代码实例我们都将 JSON 作为默认的序列化方式。

说了对RESTful理解后,使用Spring Boot如何构建RESTful风格的Web服务我们来个简单入门。

简单入门

实例代码对应的仓库地址:

github.com/dragon8844/…

gitee.com/drag0n/spri…

引入依赖

在pom.xml的文件中引入相关依赖:

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

    <!--简化代码,自动生成get、set-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- 方便等会写单元测试 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.jupiter</groupId>
                <artifactId>junit-jupiter-api</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
复制代码

添加配置

在resources目录下创建应用的配置文件application.yml,添加如下配置内容:

# 用来修改web服务的端口
server:
  port: 8080
复制代码

配置文件中配置web服务的端口,如果不配置,默认使用也是8080端口。

编写代码

定义User请求VO

@Data
public class UserReqVO implements Serializable {

    private String username;
    private String password;

}
复制代码

定义User响应VO

@Data
public class UserRespVO implements Serializable {

    private Integer id;
    private String username;
    private Date createTime;
}
复制代码

编写Controller用来暴露API

@RestController
@RequestMapping(value = "v1")
@Slf4j
public class UserController {

    @PostMapping(value = "/user", produces = MediaType.APPLICATION_JSON_VALUE)
    public R<Boolean> insert(@RequestBody UserReqVO userReqVO){
        log.info("接受的参数,username:{}, password:{}", userReqVO.getUsername(), userReqVO.getPassword());
        //调用Service层保存user
        return R.ok(true);
    }

    @GetMapping(value = "/user/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
    public R<UserRespVO> selectById(@PathVariable Integer id){
        log.info("接受的参数id:{}",id);
        //模拟生成UserRespVO对象,实际应该是调用Service获取
        UserRespVO userRespVO = new UserRespVO();
        userRespVO.setCreateTime(new Date());
        userRespVO.setId(id);
        userRespVO.setUsername("张三");
        return R.ok(userRespVO);
    }

    @PutMapping(value = "/user/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
    public R<Boolean> updateById(@PathVariable Integer id, @RequestBody UserReqVO userReqVO){
        log.info("接受的参数,username:{}, password:{}",userReqVO.getUsername(), userReqVO.getPassword());
        //调用Service层更新user
        return R.ok(true);
    }

    @DeleteMapping(value = "/user/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
    public R<Boolean> deleteById(@PathVariable Integer id){
        log.info("接受的参数id:{}",id);
        //调用Service层删除user
        return R.ok(true);
    }
}
复制代码

在Controller中我们针对User资源定义了4个API,分别对应User资源新增、根据id查询User、根据id更新User、根据id删除User。我们使用了一系列的注解@RestController、@RequestMapping、@PostMapping、@PutMapping、@GetMapping、@DeleteMapping、@RequestBody、@PathVariable等等,稍后会具体介绍各个注解的含义。

到这里,一个典型的 RESTful 服务已经开发完成了,现在我们可以通过 java –jar 命令直接运行 Spring Boot 应用程序了。那么如何验证我们写的API是否可用,我们将引入 Postman 来演示如何通过 HTTP 协议暴露的API进行远程服务访问。我们以查询User为例,使用Postman 访问“http://localhost:8080/v1/user/1”端点以得到响应结果。

单元测试

除了使用Postman第三方的工具进行测试,我们还可以编写针对Controller层的单元测试

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@Slf4j
public class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void insert() throws Exception {

        UserReqVO userReqVO = new UserReqVO();
        userReqVO.setPassword("123456");
        userReqVO.setUsername("张三");
        //设置值
        ObjectMapper mapper = new ObjectMapper();
        ObjectWriter ow = mapper.writer().withDefaultPrettyPrinter();
        String requestJson = ow.writeValueAsString(userReqVO);

        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/v1/user")
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestJson))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print())
                .andReturn();
        log.info("mvcResult:{}", mvcResult.getResponse().getContentAsString(Charset.forName("UTF-8")));
    }

    @Test
    public void selectById() throws Exception {
        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/v1/user/1"))
                    .andExpect(MockMvcResultMatchers.status().isOk())
                    .andDo(MockMvcResultHandlers.print())
                    .andReturn();
        log.info("mvcResult:{}", mvcResult.getResponse().getContentAsString(Charset.forName("UTF-8")));
    }

    @Test
    public void updateById() throws Exception{
        UserReqVO userReqVO = new UserReqVO();
        userReqVO.setPassword("123456");
        userReqVO.setUsername("张三");
        //设置值
        ObjectMapper mapper = new ObjectMapper();
        ObjectWriter ow = mapper.writer().withDefaultPrettyPrinter();
        String requestJson = ow.writeValueAsString(userReqVO);

        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.put("/v1/user/1")
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestJson))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print())
                .andReturn();
        log.info("mvcResult:{}", mvcResult.getResponse().getContentAsString(Charset.forName("UTF-8")));

    }

    @Test
    public void deleteById()throws Exception{
        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.delete("/v1/user/1"))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print())
                .andReturn();
        log.info("mvcResult:{}", mvcResult.getResponse().getContentAsString(Charset.forName("UTF-8")));
    }
}
复制代码

注解汇总

在快速入门中,我们使用Spring Boot来暴露我们Web服务的时候,使用了很多注解,接下来我将会详细介绍下每个注解。

基础注解

在原有 Spring Boot 应用程序的基础上,我们可以通过构建一系列的 Controller 类暴露 RESTful 风格的 API。这里的 Controller 与 Spring MVC 中的 Controller 概念上一致,最简单的 Controller 类如下代码所示:

@RestController
public class HelloController {
 
    @GetMapping("/")
    public String index() {
        return "Hello World!";
    }
}
复制代码

上述代码使用了@RestController和@GetMapper这两个注解。

@RestController 注解继承自 Spring MVC 中的 @Controller 注解,顾名思义就是一个基于 RESTful 风格的 HTTP 端点,并且会自动使用 JSON 实现 HTTP 请求和响应的序列化/反序列化方式。

通过这个特性,在构建 RESTful 服务时,我们可以使用 @RestController 注解代替 @Controller 注解以简化开发。不必没有方法上添加@ResponseBody注解。

另外一个 @GetMapping 注解也与 Spring MVC 中的 @RequestMapping 注解类似。我们先来看看 @RequestMapping 注解的定义,该注解所提供的属性都比较容易理解,如下代码所示:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
    String name() default "";

    @AliasFor("path")
    String[] value() default {};

    @AliasFor("value")
    String[] path() default {};

    RequestMethod[] method() default {};

    String[] params() default {};

    String[] headers() default {};

    String[] consumes() default {};

    String[] produces() default {};
}
复制代码

@GetMapping 的注解的定义与 @RequestMapping 非常类似,只是默认使用了 RequestMethod.GET 指定 HTTP 方法,如下代码所示:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(
    method = {RequestMethod.GET}
)
public @interface GetMapping {}
复制代码

除了@GetMapping注解外,Spring Boot 2.X还提供了 @PostMapping、@PutMapping、@DeleteMapping、@PatchMapping,这些注解都是显示的指定了Http请求的方式,方便开发了开发人员的使用。当然也可以使用@RequestMapping注解达到相同的效果。

控制请求和响应的注解

Spring Boot 提供了一系列简单有用的注解来简化对请求输入的控制过程,常用的包括 @PathVariable、@RequestParam、@RequestBody和@ModelAttribute注解。

其中 @PathVariable 注解用于获取路径参数,即从类似 url/{id} 这种形式的路径中获取 {id} 参数的值。

该注解定义如下:

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PathVariable {
    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";

    boolean required() default true;
}

复制代码

使用 @PathVariable 注解时,我们只需要指定一个参数的名称和URL中的参数值进行绑定,如下实例代码:

@GetMapping(value = "/user/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
    public R<UserRespVO> selectById(@PathVariable Integer id){
复制代码

{id}和方法上的参数id绑定。

@RequestParam注解的作用与@PathVariable 注解类似,也是用于获取请求中的参数,但是它面向类似 url?id=XXX 这种路径形式。

该注解的定义如下代码所示,相较 @PathVariable 注解,它只是多了一个设置默认值的 defaultValue 属性。

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
    @AliasFor("name")
    String value() default "";
    @AliasFor("value")
    String name() default "";
    boolean required() default true;
    String defaultValue() default ValueConstants.DEFAULT_NONE;
}
复制代码

@RequestBody 注解用来处理 content-type 为 application/json 类型时的编码内容,通过 @RequestBody 注解可以将请求体中的 JSON 字符串绑定到相应的 JavaBean 上。

如下代码所示就是一个使用 @RequestBody 注解来控制输入的场景。

@PostMapping(value = "/user", produces = MediaType.APPLICATION_JSON_VALUE)
    public R<Boolean> insert(@RequestBody UserReqVO userReqVO){
复制代码

小结

如何通过Spring Boot构建RESTful Web服务的内容就这么多,我们首先介绍对RESUful理解,然后通过一个简单的入门讲解了如何通过Spring Boot来构建一个标准的RESTful API。并且在入门教程中,我们体验到了Spring Boot提供的强大的注解,帮我们很简单就实现了该功能。后面我们对Spring Boot提供的注解进行了总结汇总,详细介绍了每个注解的具体功能和使用方式。

最后说一句

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下,您的支持是我坚持写作最大的动力,多谢支持。

此外,关注公众号:黑色的灯塔,专注Java后端技术分享,涵盖Spring,Spring Boot,SpringCloud,Docker,Kubernetes中间件等技术。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享