使用 SpringBoot 構建一個RESTful API


背景

這么簡單的事情也值得寫一篇文章?

去年搞一個項目時候,因為參與的人員越來越多,很快就暴露出一個問題:大家每個人都有自己的一套(代碼)風格。考慮團隊協同作戰,就提議制訂一下相關的規范,把大家的步調搞得一致一些,也有利於項目的推進效率。執行過程中出現了特別大的分歧(意料之中),其中爭議最大的就是 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM