背景

这么简单的事情也值得写一篇文章?

去年搞一个项目时候,因为参与的人员越来越多,很快就暴露出一个问题:大家每个人都有自己的一套(代码)风格。考虑团队协同作战,就提议制订一下相关的规范,把大家的步调搞得一致一些,也有利于项目的推进效率。执行过程中出现了特别大的分歧(意料之中),其中争议最大的就是 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 各个部分的含义:

  1. groupId/artifactId/packaging/version

      <groupId>tech.exchange</groupId>
    <artifactId>springboot</artifactId>
    <packaging>pom</packaging>
    <version>0.1</version>

    用于声明项目的组织、名称、打包方式和版本。

  2. modules

      <modules>
    <module>api</module>
    </modules>

    用于声明项目(多模块项目)内的多个模块,这里仅包含一个模块:api。

  3. properties

      <properties>
    ......
    </properties>

    用于声明项目 pom.xml 中可能多次使用或需要统一设置的属性(值),比如 SpringBoot 版本号。

  4. dependencyManagement/dependencies

    <dependencyManagement>
    <dependencies>
    ......
    </dependencies>
    </dependencyManagement>

    用于声明项目需要使用的依赖(Jar)名称和版本,项目的模块只需要声明具体使用的依赖名称即可,版本由项目统一指定。

  5. 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>
  1. artifactId

      <artifactId>api</artifactId>

    用于声明模块名称。

  2. 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

它的作用就是告诉 SpringMain 为入口,自动装载各种各样的 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

使用 SpringBoot 构建一个RESTful API的更多相关文章

  1. 基于SpringBoot开发一个Restful服务,实现增删改查功能

    前言 在去年的时候,在各种渠道中略微的了解了SpringBoot,在开发web项目的时候是如何的方便.快捷.但是当时并没有认真的去学习下,毕竟感觉自己在Struts和SpringMVC都用得不太熟练. ...

  2. [译]Spring Boot 构建一个RESTful Web服务

    翻译地址:https://spring.io/guides/gs/rest-service/ 构建一个RESTful Web服务 本指南将指导您完成使用spring创建一个“hello world”R ...

  3. 使用python的Flask实现一个RESTful API服务器端

    使用python的Flask实现一个RESTful API服务器端 最近这些年,REST已经成为web services和APIs的标准架构,很多APP的架构基本上是使用RESTful的形式了. 本文 ...

  4. Spring MVC 中使用 Swagger2 构建动态 RESTful API

    当多终端(WEB/移动端)需要公用业务逻辑时,一般会构建 RESTful 风格的服务提供给多终端使用. 为了减少与对应终端开发团队频繁沟通成本,刚开始我们会创建一份 RESTful API 文档来记录 ...

  5. Spring Boot 构建一个 RESTful Web Service

    1  项目目标: 构建一个 web service,接收get 请求 http://localhost:8080/greeting 响应一个json 结果: {"id":1,&qu ...

  6. 用 Go 快速开发一个 RESTful API 服务

    何时使用单体 RESTful 服务 对于很多初创公司来说,业务的早期我们更应该关注于业务价值的交付,而单体服务具有架构简单,部署简单,开发成本低等优点,可以帮助我们快速实现产品需求.我们在使用单体服务 ...

  7. 使用python的Flask实现一个RESTful API服务器端[翻译]

    最近这些年,REST已经成为web services和APIs的标准架构,很多APP的架构基本上是使用RESTful的形式了. 本文将会使用python的Flask框架轻松实现一个RESTful的服务 ...

  8. 一个Restful Api的访问控制方法

    最近在做的两个项目,都需要使用Restful Api,接口的安全性和访问控制便成为一个问题,看了一下别家的API访问控制办法. 新浪的API访问控制使用的是AccessToken,有两种方式来使用该A ...

  9. 实现一个 RESTful API 服务器

    RESTful 是目前最为流行的一种互联网软件结构.因为它结构清晰.符合标准.易于理解.扩展方便,所以正得到越来越多网站的采用. 什么是 REST REST(REpresentational Stat ...

随机推荐

  1. Java(25)常见异常整理

    作者:季沐测试笔记 原文地址:https://www.cnblogs.com/testero/p/15228418.html 博客主页:https://www.cnblogs.com/testero ...

  2. [软工顶级理解组] Alpha阶段测试报告

    [软工顶级理解组] Alpha阶段测试报告 在测试过程中发现了多少Bug? 测试阶段发现并已修复的bug: 尚且存在,但是难以解决或者不影响使用的bug: 计算重修课程的时候,如果重修课程的课程号和原 ...

  3. BUAA 2020 软件工程 个人项目作业

    BUAA 2020 软件工程 个人项目作业 Author: 17373051 郭骏 项目 内容 这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任健) 这个作业的要求在哪里 个人项目作业 ...

  4. Noip模拟45 2021.8.21

    一定别删大括号,检查是;还是, ceil函数里面要写double,否则根本没用!!!!!!! T1 打表 正解:打表 考场上很难真正把柿子理解着推出来 况且想要理解题意就很难,比如我就理解错了 半猜着 ...

  5. 按之字形顺序打印二叉树 牛客网 剑指Offer

    按之字形顺序打印二叉树 牛客网 剑指Offer 题目描述 请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推 ...

  6. Tarjan算法离线 求 LCA(最近公共祖先)

    本文是网络资料整理或部分转载或部分原创,参考文章如下: https://www.cnblogs.com/JVxie/p/4854719.html http://blog.csdn.net/ywcpig ...

  7. cf17A Noldbach problem(额,,,素数,,,)

    题意: 判断从[2,N]中是否有超过[包括]K个数满足:等于一加两个相邻的素数. 思路: 枚举. 也可以:筛完素数,枚举素数,直到相邻素数和超过N.统计个数 代码: int n,k; int prim ...

  8. 恶意代码分析实战四:IDA Pro神器的使用

    目录 恶意代码分析实战四:IDA Pro神器的使用 实验: 题目1:利用IDA Pro分析dll的入口点并显示地址 空格切换文本视图: 带地址显示图形界面 题目2:IDA Pro导入表窗口 题目3:交 ...

  9. SQL告警,执行时间长?教你写一手好 SQL !

    博主(编码砖家)负责的项目主要采用阿里云数据库MySQL,最近频繁出现慢SQL告警,执行时间最长的竟然高达5分钟.导出日志后分析,主要原因竟然是没有命中索引和没有分页处理 . 其实这是非常低级的错误, ...

  10. mac 工作区

    https://www.zhihu.com/question/20917614 http://www.bjhee.com/mission-control.html 窗口切换 https://sspai ...