背景
这么简单的事情也值得写一篇文章?
去年搞一个项目时候,因为参与的人员越来越多,很快就暴露出一个问题:大家每个人都有自己的一套(代码)风格。考虑团队协同作战,就提议制订一下相关的规范,把大家的步调搞得一致一些,也有利于项目的推进效率。执行过程中出现了特别大的分歧(意料之中),其中争议最大的就是 Http API 这部分,主要发现两个问题:
- 很多同学认为 RESTful 风格的 API 就是好的(先进的),可是为什么好,说不上来?
- 很多同学认为自己知道 RESTful 风格的 API 是什么样子的,可是坐下来一聊,发现每个人的都不一样,谁都说服不了对方?
最后因为项目周期关系,没有在这个事情上面多纠结,以业务优先为幌子,大家先按自己的风格推进项目,在实践的过程中逐步建立规范。
后来 Review 代码的时候,也发现一个很有意思的现象:即使对于同一位同学,他所写出来的 RESTful 风格的 API 实际也是不一致的,主要表现两个现象:
- 类似的业务场景,API Url和参数的设计明显不同,这位同学也说不上原因,可能和写具体 API 时的心情有关系;
- 有一些业务场景,API 的设计已经明显偏离 RESTful,这位同学用“不这么写,业务逻辑没法儿实现”来搪塞;
和多位同学沟通之后,我得出一个结论:让这些自己以为自己很懂 RESTful 的同学按照自己的理解制订一套规范,用于约束什么样的场景应该如何设计 API,实际上是做不到的。
这么流行的东西,应该是标准化(大家的共识)程度很高的,为什么还会有这种现象?直到最近几天看到一篇 文章:
REST stands for “representational state transfer,” described by Roy Fielding in his dissertation. Sadly, that dissertation is not widely read, and so many people have their own idea of what REST is, leading to a lot of confusion and disagreement.
大意就是,REST 是一篇老外的论文(2000年的时候)里提出来的;但是呢,这篇论文应该很多人没有实际看过,大家都是根据网络上的“只言片语”来理解的,所以导致了很多的曲解和不一致(表示无法更认同)。
论文 确实有点儿长,估计很少有人真的会去看(包括我自己),这里推荐大家看一篇源自微软的文章,讨论的内容很全面,基本可以作为 RESTful API 入门和实践的指南。
本文的后续章节以该篇文章为基础,主要讲述如何使用 SpringBoot 构建 RESTful API 的若干关键技术点,已具备相关经验的同学可以忽略这部分内容。
创建 SpringBoot 项目/模块
使用 Idea 创建 Maven 项目(Project),名称:SpringBoot;再创建一个模块,名称:api,用于构建 RESTful API,这里仅列出 pom.xml。
SpringBoot pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>tech.exchange</groupId>
<artifactId>springboot</artifactId>
<packaging>pom</packaging>
<version>0.1</version>
<modules>
<module>api</module>
</modules>
<properties>
<spring-boot.version>2.6.1</spring-boot.version>
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
<maven-assembly-plugin.version>3.3.0</maven-assembly-plugin.version>
<java.version>17</java.version>
<encoding>UTF-8</encoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${encoding}</encoding>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>${maven-assembly-plugin.version}</version>
<configuration>
<descriptors>
<!--suppress UnresolvedMavenProperty -->
<descriptor>
${maven.multiModuleProjectDirectory}/src/assembly/package.xml
</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
简要介绍一下 pom.xml 各个部分的含义:
-
groupId/artifactId/packaging/version
<groupId>tech.exchange</groupId> <artifactId>springboot</artifactId> <packaging>pom</packaging> <version>0.1</version>
用于声明项目的组织、名称、打包方式和版本。
-
modules
<modules> <module>api</module> </modules>
用于声明项目(多模块项目)内的多个模块,这里仅包含一个模块:api。
-
properties
<properties> ...... </properties>
用于声明项目 pom.xml 中可能多次使用或需要统一设置的属性(值),比如 SpringBoot 版本号。
-
dependencyManagement/dependencies
<dependencyManagement> <dependencies> ...... </dependencies> </dependencyManagement>
用于声明项目需要使用的依赖(Jar)名称和版本,项目的模块只需要声明具体使用的依赖名称即可,版本由项目统一指定。
-
build/plugins
<build> <plugins> ...... </plugins> </build>
用于声明项目或模块构建(编译、打包或其它)需要使用的插件。
api pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>tech.exchange</groupId>
<artifactId>springboot</artifactId>
<version>0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>api</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
-
artifactId
<artifactId>api</artifactId>
用于声明模块名称。
-
dependencies/dependency
<dependencies> <dependency> ...... </dependency> </dependencies>
用于声明模块具体需要使用的依赖(Jar)。
创建 RESTful API 应用
创建一个名称为 Main 的类:
package tech.exchange.springboot.api;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author yurun
*/
@SpringBootApplication
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
}
@SpringBootApplication
@SpringBootApplication 是三个注解(Annotation)的合集:
@SpringBootApplication = @Configuration + @EnableAutoConfiguration + @ComponentScan
它的作用就是告诉 Spring 以 Main 为入口,自动装载各种各样的 Bean(组件或配置)到 容器 中,最终形成一个 SpringBoot 应用,用以接收和响应外部请求。
这些 Bean 来源于三个方面:
@Configuration
Tags the class as a source of bean definitions for the application context.
装载 Main 中我们自定义的 Beans(本文示例中没有包含自定义 Bean)。
@EnableAutoConfiguration
Tells Spring Boot to start adding beans based on classpath settings, other beans, and various property settings.
根据类路径、其它 Beans或属性配置装载需要的 Beans,比如:示例中包含依赖 spring-boot-starter-web,会自动装载 web 相关的 Beans。
@ComponentScan
Tells Spring to look for other components, configurations, and services in the tech/exchange/springboot/api package。
扫描包(Package)路径:tech/exchange/springboot/api,装载包下面的 Beans。
SpringApplication.run
SpringApplication.run(Main.class, args);
启动 SpringBoot 应用,默认情况下会看到如下输出:
2021-12-02 14:44:15.537 INFO 58552 --- [ main] tech.exchange.springboot.api.Main : Starting Main using Java 17.0.1 on bogon with PID 58552 (/Users/yurun/workspace/tech-exchange/springboot/api/target/classes started by yurun in /Users/yurun/workspace/tech-exchange/springboot)
2021-12-02 14:44:15.538 INFO 58552 --- [ main] tech.exchange.springboot.api.Main : No active profile set, falling back to default profiles: default
2021-12-02 14:44:16.160 INFO 58552 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2021-12-02 14:44:16.170 INFO 58552 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2021-12-02 14:44:16.170 INFO 58552 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.55]
2021-12-02 14:44:16.213 INFO 58552 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2021-12-02 14:44:16.214 INFO 58552 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 633 ms
2021-12-02 14:44:16.436 INFO 58552 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2021-12-02 14:44:16.443 INFO 58552 --- [ main] tech.exchange.springboot.api.Main : Started Main in 1.209 seconds (JVM running for 1.584)
其中,8080 (http) 表示应用实例端口号为8080,支持 HTTP 请求。
Rest Controller
目前,应用还是一个 空 的应用,无法实际接收任何请求或响应。SpringBoot 中 HTTP 请求或响应需要通过 Controller 实现,一个 Controller 内可以支持(包含)一个或多个 HTTP 请求或响应的实现,也就是一个或多个 API 的实现。
package tech.exchange.springboot.api.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author yurun
*/
@RestController
@RequestMapping("/hello")
public class HelloController {
}
@RestController
@RestController 用于标识 HelloController 是一个 Controller,SpringBoot 应用启动后会被自动装载到容器中。
@RequestMapping
HelloController 相当于是一个 API 的集合,内部可包含多个 API 的具体实现,如:
/hello/a1
/hello/a2
/hello/a3
......
可以使用 @RequestMapping 统一标识这些 API 请求路径的父路径,如:/hello,内部的 API 请求路径无需再包含此父路径,使用 /a1、/a2、/a3 即可。
Get/Post/Put/Patch/Delete
目前,HelloController 还是一个 空 的 Controller,不包含任何 API 的实现。
RESTfull API 共涉及 5 种 请求类型:Get/Post/Put/Patch/Delete,3种参数类型:请求路径参数、请求参数和请求体参数。每一种请求类型模拟实现一个 API,用于演示 API 的实现过程,以及每一种参数类型的使用方式。
Get
在 HelloController 中添加方法(Method) get:
@GetMapping("/get/{name1}")
public String get(@PathVariable String name1, @RequestParam(defaultValue = "name2dv") String name2) {
return "hello " + name1 + " " + name2;
}
@GetMapping
@GetMapping 用于标识方法 get 仅响应 HTTP GET 请求,且请求路径为 /hello/get/{name1},其中 {name1} 为请求路径参数名称,实际请求时需要替换为具体的参数值,如:value1。
@PathVariable
@PathVariable String name1
@PathVariable 用于标识请求方法参数,接收请求路径参数。假设请求路径为 /hello/value1,执行请求时参数值 value1 会被传递给请求方法 get 的参数 name1。
请求路径参数默认为必填项(不支持修改),发起请求时必须填写,否则请求会失败。
@RequestParam
@RequestParam(defaultValue = "name2dv") String name2
@RequestParam 用于标识请求方法参数,接收请求参数。假设请求路径为 /hello?name2=value2,执行请求时参数值 value2 会被传递给请求方法 get 的参数 name2;假设请求路径为 /hello,执行请求时请求方法 get 的参数 name2 会被设置为默认值(defaultValue) name2dv。
请求参数默认为必填项(可以通过注解属性 required 修改),发起请求时必须填写;如果有设置 defaultValue,则发起请求时可以不填写,使用默认值代替,否则请求会失败。
调用示例
请求:curl http://localhost:8080/hello/get/value1
响应:hello value1 name2dv
请求:curl http://localhost:8080/hello/get/value1?name2=value2
响应:hello value1 value2
请求路径参数和请求方法参数的参数名称需要保持一致,如不一致,需要通过注解属性额外指定(下同);
发起请求时填写的参数类型需要和请求方法声明的参数类型兼容(下同);
请求路径参数和请求方法参数可以使用零个或多个(下同);
Get 场景中请求体参数应用场景不多,本文不予讨论。
Post
Post 中请求路径参数和请求参数的使用方式与 Get 相同,不再赘述,仅实现请求体参数的使用方式。请求体参数的使用方式与 HTTP Request Header Content-Type 的具体值有关,本文仅讨论最常用的类型:application/json。
发起请求时需要使用 JSON 传递请求体参数,请求方法需要通过 类 接收请求体参数值。
@PostMapping("/post")
public String post(@RequestBody PostParams params) {
return "hello " + params.getName1() + " " + params.getName2();
}
@PostMapping
@PostMapping 用于标识方法 post 仅响应 HTTP POST 请求,且请求路径为 /hello/post。
@RequestBody
@RequestBody PostParams params
@RequestBody 用于标识请求方法参数,接收请求体参数(JSON)。
假设请求体参数:
{"name1": "value1", "name2": "value2"}
需要创建一个类用于接收体参数:
public class PostParams {
private String name1;
private String name2 = "name2dv";
public String getName1() {
return name1;
}
public void setName1(String name1) {
this.name1 = name1;
}
public String getName2() {
return name2;
}
public void setName2(String name2) {
this.name2 = name2;
}
}
执行请求时会将每个 JSON 字段按名称分别赋值给 类 实例(param)字段。如果 name1 不存在于 JSON 中,则 name1 为 null;如果 name2 不存在于 JSON 中,则 name2 为 name2dv;如果 JSON 中的某些字段不存在于 类 中,这些字段将会被忽略。
调用示例
请求:curl -H "Content-Type:application/json" -X POST --data '{"name1": "value1", "name2": "value2"}' http://localhost:8080/hello/post
响应:hello value1 value2
请求:curl -H "Content-Type:application/json" -X POST --data '{}' http://localhost:8080/hello/post
响应:hello null name2dv
请求:curl -H "Content-Type:application/json" -X POST --data '{"name1": "value1", "name3": "value2"}' http://localhost:8080/hello/post
响应:hello value1 name2dv
请求体参数仅能为零个或一个(下同);
请求体参数字段类型需要和类字段类型兼容(下同);
请求路径参数、请求参数和请求体参数可以混合使用(下同)。
Put
@PutMapping("/put")
public String put(@RequestBody PostParams params) {
return "hello " + params.getName1() + " " + params.getName2();
}
@PutMapping
@PutMapping 用于标识方法 put 仅响应 HTTP PUT 请求,且请求路径为 /hello/put。
其余内容同上,不再赘述。
Patch
@PatchMapping("/patch")
public String patch(@RequestBody PostParams params) {
return "hello " + params.getName1() + " " + params.getName2();
}
@PatchMapping
@PatchMapping 用于标识方法 patch 仅响应 HTTP PATCH 请求,且请求路径为 /hello/patch。
其余内容同上,不再赘述。
Delete
@DeleteMapping("/delete")
public String delete(@RequestBody PostParams params) {
return "hello " + params.getName1() + " " + params.getName2();
}
@DeleteMapping
@DeleteMapping 用于标识方法 delete 仅响应 HTTP DELETE 请求,且请求路径为 /hello/delete。
其余内容同上,不再赘述。
结语
本文介绍了一篇讲述 RESTfull API 的文章,并以此为基础,演示了一个使用 SpringBoot 构建 RESTfull API 应用的完整过程,核心的配置和注解也给出了说明,希望对大家有帮助。
附
https://github.com/tech-exchange/springboot/blob/master/api/src/main/java/tech/exchange/springboot/api/controller/HelloController.java
https://github.com/tech-exchange/springboot/blob/master/api/src/main/java/tech/exchange/springboot/api/controller/PostParams.java