Spring Cloud 快速入門
課程介紹
Spring Cloud 是一套完整的微服務解決方案,基於 Spring Boot 框架,準確的說,它不是一個框架,而是一個大的容器,它將市面上較好的微服務框架整合進來,從而簡化了開發者的程式碼量。
本課程由淺入深帶領大家一步步攻克 Spring Cloud 各大模組,接著通過一個例項帶領大家瞭解大型分散式微服務架構的搭建過程,最後深入原始碼加深對它的瞭解。
本課程共分為四個部分:
第一部分(第1-3課),初識 Spring Boot,掌握 Spring Boot 基礎知識,為後續入門 Spring Cloud 打好基礎 。
第二部分(第4-13課),Spring Cloud 入門篇,主要介紹 Spring Cloud 常用模組,包括服務發現、服務註冊、配置中心、鏈路追蹤、異常處理等。
第三部分(第14-18課),Spring Cloud 進階篇,介紹大型分散式系統中事務處理、執行緒安全等問題,並以一個例項專案手把手教大家搭建完整的微服務系統。
第四部分(第19-20課),Spring Cloud 高階篇,解析 Spring Cloud 原始碼,並講解如何部署基於 Spring Cloud 的大型分散式系統。
作者介紹
李熠,從事 Java 後端開發6年,現任職某大型網際網路公司,擔任 Java 高階開發工程師,CSDN 部落格專家,全棧工程師。
課程內容
導讀:什麼是 Spring Cloud 及應用現狀
Spring Cloud 是什麼?
在學習本課程之前,讀者有必要先了解一下 Spring Cloud。
Spring Cloud 是一系列框架的有序集合,它利用 Spring Boot 的開發便利性簡化了分散式系統的開發,比如服務發現、服務閘道器、服務路由、鏈路追蹤等。Spring Cloud 並不重複造輪子,而是將市面上開發得比較好的模組整合進去,進行封裝,從而減少了各模組的開發成本。換句話說:Spring Cloud 提供了構建分散式系統所需的“全家桶”。
Spring Cloud 現狀
目前,國內使用 Spring Cloud 技術的公司並不多見,不是因為 Spring Cloud 不好,主要原因有以下幾點:
- Spring Cloud 中文文件較少,出現問題網上沒有太多的解決方案。
- 國內創業型公司技術老大大多是阿里系員工,而阿里系多采用 Dubbo 來構建微服務架構。
- 大型公司基本都有自己的分散式解決方案,而中小型公司的架構很多用不上微服務,所以沒有采用 Spring Cloud 的必要性。
但是,微服務架構是一個趨勢,而 Spring Cloud 是微服務解決方案的佼佼者,這也是作者寫本系列課程的意義所在。
Spring Cloud 優缺點
其主要優點有:
- 集大成者,Spring Cloud 包含了微服務架構的方方面面。
- 約定優於配置,基於註解,沒有配置檔案。
- 輕量級元件,Spring Cloud 整合的元件大多比較輕量級,且都是各自領域的佼佼者。
- 開發簡便,Spring Cloud 對各個元件進行了大量的封裝,從而簡化了開發。
- 開發靈活,Spring Cloud 的元件都是解耦的,開發人員可以靈活按需選擇元件。
接下來,我們看下它的缺點:
- 專案結構複雜,每一個元件或者每一個服務都需要建立一個專案。
- 部署門檻高,專案部署需要配合 Docker 等容器技術進行叢集部署,而要想深入瞭解 Docker,學習成本高。
Spring Cloud 的優勢是顯而易見的。因此對於想研究微服務架構的同學來說,學習 Spring Cloud 是一個不錯的選擇。
Spring Cloud 和 Dubbo 對比
Dubbo 只是實現了服務治理,而 Spring Cloud 實現了微服務架構的方方面面,服務治理只是其中的一個方面。下面通過一張圖對其進行比較:
可以看出,Spring Cloud 比較全面,而 Dubbo 由於只實現了服務治理,需要整合其他模組,需要單獨引入,增加了學習成本和整合成本。
Spring Cloud 學習
Spring Cloud 基於 Spring Boot,因此在研究 Spring Cloud 之前,本課程會首先介紹 Spring Boot 的用法,方便後續 Spring Cloud 的學習。
本課程不會講解 SpringMVC 的用法,因此學習本課程需要讀者對 Spring 及 SpringMVC 有過研究。
本課程共分為四個部分:
-
第一部分初識 Spring Boot,掌握 Spring Boot 基礎知識,為後續入門 Spring Cloud 打好基礎 。
-
第二部分 Spring Cloud 入門篇,主要介紹 Spring Cloud 常用模組,包括服務發現、服務註冊、配置中心、鏈路追蹤、異常處理等。
-
第三部分 Spring Cloud 進階篇,介紹大型分散式系統中事務處理、執行緒安全等問題,並以一個例項專案手把手教大家搭建完整的微服務系統。
-
第四部分 Spring Cloud 高階篇,解析 Spring Cloud 原始碼,並講解如何部署基於 Spring Cloud 的大型分散式系統。
本課程的所有示例程式碼均可在:https://github.com/lynnlovemin/SpringCloudLesson 下載。
第01課:Spring Boot 入門
什麼是 Spring Boot
Spring Boot 是由 Pivotal 團隊提供的基於 Spring 的全新框架,其設計目的是為了簡化 Spring 應用的搭建和開發過程。該框架遵循“約定大於配置”原則,採用特定的方式進行配置,從而使開發者無需定義大量的 XML 配置。通過這種方式,Spring Boot 致力於在蓬勃發展的快速應用開發領域成為領導者。
Spring Boot 並不重複造輪子,而且在原有 Spring 的框架基礎上封裝了一層,並且它集成了一些類庫,用於簡化開發。換句話說,Spring Boot 就是一個大容器。
下面幾張圖展示了 官網 上提供的 Spring Boot 所整合的所有類庫:
Spring Boot 官方推薦使用 Maven 或 Gradle 來構建專案,本教程採用 Maven。
第一個 Spring Boot 專案
大多數教程都是以 Hello World 入門,本教程也不例外,接下來,我們就來搭建一個最簡單的 Spring Boot 專案。
首先建立一個 Maven 工程,請看下圖:
然後在 pom.xml 加入 Spring Boot 依賴:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.1.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
建立一個 Controller 類 HelloController:
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @SpringBootApplication public class HelloController { @RequestMapping("hello") String hello() { return "Hello World!"; } public static void main(String[] args) { SpringApplication.run(HelloController.class, args); } }
執行 main 方法,Spring Boot 預設會啟動自帶的 Tomcat 容器,啟動成功後,瀏覽器訪問:http://localhost:8080/hello,則會看到下圖:
我們可以注意到,沒有寫任何的配置檔案,更沒有顯示的使用任何容器,它是如何啟動程式的呢,具體原理我將在第3課中具體分析。
這裡我們可以初步分析出,Spring Boot 提供了預設的配置,在啟動類里加入 @SpringBootApplication
註解,則這個類就是整個應用程式的啟動類。
properties 和 yaml
Spring Boot 整個應用程式只有一個配置檔案,那就是 .properties
或 .yml
檔案。但是,在前面的示例程式碼中,我們並沒有看到該配置檔案,那是因為 Spring Boot 對每個配置項都有預設值。當然,我們也可以新增配置檔案,用以覆蓋其預設值,這裡以 .properties
檔案為例,首先在 resources 下新建一個名為 application.properties(注意:檔名必須是 application)的檔案,鍵入內容為:
server.port=8081 server.servlet.context-path=/api
並且啟動 main 方法,這時程式請求地址則變成了:http://localhost:8081/api/hello。
Spring Boot 支援 properties 和 yaml 兩種格式的檔案,檔名分別對應 application.properties 和 application.yml,下面貼出 yaml 檔案格式供大家參考:
server: port: 8080 servlet: context-path: /api
可以看出 properties 是以逗號隔開,而 yaml 則換行+ tab 隔開,這裡需要注意的是冒號後面必須空格,否則會報錯。yaml 檔案格式更清晰,更易讀,這裡作者建議大家都採用 yaml 檔案來配置。
本教程的所有配置均採用 yaml 檔案。
打包、執行
Spring Boot 打包分為 war 和 jar兩個格式,下面將分別演示如何構建這兩種格式的啟動包。
在 pom.xml 加入如下配置:
<packaging>war</packaging> <build> <finalName>index</finalName> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> </resources> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <artifactId>maven-resources-plugin</artifactId> <version>2.5</version> <configuration> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.18.1</version> <configuration> <skipTests>true</skipTests> </configuration> </plugin> </plugins> </build>
這個時候執行 mvn package 就會生成 war 包,然後放到 Tomcat 當中就能啟動,但是我們單純這樣配置在 Tomcat 是不能成功執行的,會報錯,需要通過編碼指定 Tomcat 容器啟動,修改 HelloController 類:
@RestController @SpringBootApplication public class HelloController extends SpringBootServletInitializer{ @RequestMapping("hello") String hello() { return "Hello World!"; } public static void main(String[] args) { SpringApplication.run(HelloController.class, args); } @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(Application.class); } }
這時再打包放到 Tomcat,啟動就不會報錯了。
接下來我們繼續看如果達成 jar 包,在 pom.xml 加入如下配置:
<packaging>jar</packaging> <build> <finalName>api</finalName> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> </resources> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> <mainClass>com.lynn.yiyi.Application</mainClass> </configuration> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> <plugin> <artifactId>maven-resources-plugin</artifactId> <version>2.5</version> <configuration> <encoding>UTF-8</encoding> <useDefaultDelimiters>true</useDefaultDelimiters> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.18.1</version> <configuration> <skipTests>true</skipTests> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.3.2</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build>
然後通過 mvn package 打包,最後通過 java 命令啟動:
java -jar api.jar
這樣,最簡單的 Spring Boot 就完成了,但是對於一個大型專案,這是遠遠不夠的,Spring Boot 的詳細操作可以參照 官網 。
下面展示一個最基礎的企業級 Spring Boot 專案的結構:
其中,Application.java 是程式的啟動類,Startup.java 是程式啟動完成前執行的類,WebConfig.java 是配置類,所有 bean 注入、配置、攔截器注入等都放在這個類裡面。
以上例項只是最簡單的 Spring Boot 專案入門例項,後面會深入研究 Spring Boot。
第02課:Spring Boot 進階
上一篇帶領大家初步瞭解瞭如何使用 Spring Boot 搭建框架,通過 Spring Boot 和傳統的 SpringMVC 架構的對比,我們清晰地發現 Spring Boot 的好處,它使我們的程式碼更加簡單,結構更加清晰。
從這一篇開始,我將帶領大家更加深入的認識 Spring Boot,將 Spring Boot 涉及到東西進行拆解,從而瞭解 Spring Boot 的方方面面。學完本文後,讀者可以基於 Spring Boot 搭建更加複雜的系統框架。
我們知道,Spring Boot 是一個大容器,它將很多第三方框架都進行了整合,我們在實際專案中用到哪個模組,再引入哪個模組。比如我們專案中的持久化框架用 MyBatis,則在 pom.xml 新增如下依賴:
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.40</version> </dependency>
yaml/properties 檔案
我們知道整個 Spring Boot 專案只有一個配置檔案,那就是 application.yml,Spring Boot 在啟動時,就會從 application.yml 中讀取配置資訊,並載入到記憶體中。上一篇我們只是粗略的列舉了幾個配置項,其實 Spring Boot 的配置項是很多的,本文我們將學習在實際專案中常用的配置項(注:為了方便說明,配置項均以 properties 檔案的格式寫出,後續的實際配置都會寫成 yaml 格式)。
配置項 | 說明 | 舉例 |
---|---|---|
server.port | 應用程式啟動埠 | server.port=8080,定義應用程式啟動埠為8080 |
server.context-path | 應用程式上下文 | server.port=/api,則訪問地址為:http://ip:port/api |
spring.http.multipart.maxFileSize | 最大檔案上傳大小,-1為不限制 | spring.http.multipart.maxFileSize=-1 |
spring.jpa.database | 資料庫型別 | spring.jpa.database=MYSQL,指定資料庫為mysql |
spring.jpa.properties.hibernate.dialect | hql方言 | spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect |
spring.datasource.url | 資料庫連線字串 | spring.datasource.url=jdbc:mysql://localhost:3306/database?useUnicode=true&characterEncoding=UTF-8&useSSL=true |
spring.datasource.username | 資料庫使用者名稱 | spring.datasource.username=root |
spring.datasource.password | 資料庫密碼 | spring.datasource.password=root |
spring.datasource.driverClassName | 資料庫驅動 | spring.datasource.driverClassName=com.mysql.jdbc.Driver |
spring.jpa.showSql | 控制檯是否列印sql語句 | spring.jpa.showSql=true |
下面是我參與的某個專案的 application.yml 配置檔案內容:
server: port: 8080 context-path: /api tomcat: max-threads: 1000 min-spare-threads: 50 connection-timeout: 5000 spring: profiles: active: dev http: multipart: maxFileSize: -1 datasource: url: jdbc:mysql://localhost:3306/database?useUnicode=true&characterEncoding=UTF-8&useSSL=true username: root password: root driverClassName: com.mysql.jdbc.Driver jpa: database: MYSQL showSql: true hibernate: namingStrategy: org.hibernate.cfg.ImprovedNamingStrategy properties: hibernate: dialect: org.hibernate.dialect.MySQL5Dialect mybatis: configuration: #配置項:開啟下劃線到駝峰的自動轉換. 作用:將資料庫欄位根據駝峰規則自動注入到物件屬性。 map-underscore-to-camel-case: true
以上列舉了常用的配置項,所有配置項資訊都可以在 官網 中找到,本課程就不一一列舉了。
多環境配置
在一個企業級系統中,我們可能會遇到這樣一個問題:開發時使用開發環境,測試時使用測試環境,上線時使用生產環境。每個環境的配置都可能不一樣,比如開發環境的資料庫是本地地址,而測試環境的資料庫是測試地址。那我們在打包的時候如何生成不同環境的包呢?
這裡的解決方案有很多:
- 每次編譯之前手動把所有配置資訊修改成當前執行的環境資訊。這種方式導致每次都需要修改,相當麻煩,也容易出錯。
- 利用 Maven,在 pom.xml 裡配置多個環境,每次編譯之前將 settings.xml 裡面修改成當前要編譯的環境 ID。這種方式會事先設定好所有環境,缺點就是每次也需要手動指定環境,如果環境指定錯誤,釋出時是不知道的。
- 第三種方案就是本文重點介紹的,也是作者強烈推薦的方式。
首先,建立 application.yml 檔案,在裡面新增如下內容:
spring: profiles: active: dev
含義是指定當前專案的預設環境為 dev,即專案啟動時如果不指定任何環境,Spring Boot 會自動從 dev 環境檔案中讀取配置資訊。我們可以將不同環境都共同的配置資訊寫到這個檔案中。
然後建立多環境配置檔案,檔名的格式為:application-{profile}.yml,其中,{profile} 替換為環境名字,如 application-dev.yml,我們可以在其中添加當前環境的配置資訊,如新增資料來源:
spring: datasource: url: jdbc:mysql://localhost:3306/database?useUnicode=true&characterEncoding=UTF-8&useSSL=true username: root password: root driverClassName: com.mysql.jdbc.Driver
這樣,我們就實現了多環境的配置,每次編譯打包我們無需修改任何東西,編譯為 jar 檔案後,執行命令:
java -jar api.jar --spring.profiles.active=dev
其中 --spring.profiles.active
就是我們要指定的環境。
常用註解
我們知道,Spring Boot 主要採用註解的方式,在上一篇的入門例項中,我們也用到了一些註解。
本文,我將詳細介紹在實際專案中常用的註解。
@SpringBootApplication
我們可以注意到 Spring Boot 支援 main 方法啟動,在我們需要啟動的主類中加入此註解,告訴 Spring Boot,這個類是程式的入口。如:
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
如果不加這個註解,程式是無法啟動的。
我們檢視下 SpringBootApplication 的原始碼,原始碼如下:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication { /** * Exclude specific auto-configuration classes such that they will never be applied. * @return the classes to exclude */ @AliasFor(annotation = EnableAutoConfiguration.class, attribute = "exclude") Class<?>[] exclude() default {}; /** * Exclude specific auto-configuration class names such that they will never be * applied. * @return the class names to exclude * @since 1.3.0 */ @AliasFor(annotation = EnableAutoConfiguration.class, attribute = "excludeName") String[] excludeName() default {}; /** * Base packages to scan for annotated components. Use {@link #scanBasePackageClasses} * for a type-safe alternative to String-based package names. * @return base packages to scan * @since 1.3.0 */ @AliasFor(annotation = ComponentScan.class, attribute = "basePackages") String[] scanBasePackages() default {}; /** * Type-safe alternative to {@link #scanBasePackages} for specifying the packages to * scan for annotated components. The package of each class specified will be scanned. * <p> * Consider creating a special no-op marker class or interface in each package that * serves no purpose other than being referenced by this attribute. * @return base packages to scan * @since 1.3.0 */ @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses") Class<?>[] scanBasePackageClasses() default {}; }
在這個註解類上有3個註解,如下:
@SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
因此,我們可以用這三個註解代替 SpringBootApplication,如:
@SpringBootConfiguration @EnableAutoConfiguration @ComponentScan public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
其中,SpringBootConfiguration 表示 Spring Boot 的配置註解,EnableAutoConfiguration 表示自動配置,ComponentScan 表示 Spring Boot 掃描 Bean 的規則,比如掃描哪些包。
@Configuration
加入了這個註解的類被認為是 Spring Boot 的配置類,我們知道可以在 application.yml 設定一些配置,也可以通過程式碼設定配置。
如果我們要通過程式碼設定配置,就必須在這個類上標註 Configuration 註解。如下程式碼:
@Configuration public class WebConfig extends WebMvcConfigurationSupport{ @Override protected void addInterceptors(InterceptorRegistry registry) { super.addInterceptors(registry); registry.addInterceptor(new ApiInterceptor()); } }
不過 Spring Boot 官方推薦 Spring Boot 專案用 SpringBootConfiguration 來代替 Configuration。
@Bean
這個註解是方法級別上的註解,主要新增在 @Configuration
或 @SpringBootConfiguration
註解的類,有時也可以新增在 @Component
註解的類。它的作用是定義一個Bean。
請看下面程式碼:
@Bean public ApiInterceptor interceptor(){ return new ApiInterceptor(); }
那麼,我們可以在 ApiInterceptor 裡面注入其他 Bean,也可以在其他 Bean 注入這個類。
@Value
通常情況下,我們需要定義一些全域性變數,都會想到的方法是定義一個 public static 變數,在需要時呼叫,是否有其他更好的方案呢?答案是肯定的。下面請看程式碼:
@Value("${server.port}") String port; @RequestMapping("/hello") public String home(String name) { return "hi "+name+",i am from port:" +port; }
其中,server.port 就是我們在 application.yml 裡面定義的屬性,我們可以自定義任意屬性名,通過 @Value
註解就可以將其取出來。
它的好處不言而喻:
- 定義在配置檔案裡,變數發生變化,無需修改程式碼。
- 變數交給Spring來管理,效能更好。
注:本課程預設針對於對 SpringMVC 有所瞭解的讀者,Spring Boot 本身基於 Spring 開發的,因此,本文不再講解其他 Spring 的註解。
注入任何類
本節通過一個實際的例子來講解如何注入一個普通類,並且說明這樣做的好處。
假設一個需求是這樣的:專案要求使用阿里雲的 OSS 進行檔案上傳。
我們知道,一個專案一般會分為開發環境、測試環境和生產環境。OSS 檔案上傳一般有如下幾個引數:appKey、appSecret、bucket、endpoint 等。不同環境的引數都可能不一樣,這樣便於區分。按照傳統的做法,我們在程式碼裡設定這些引數,這樣做的話,每次釋出不同的環境包都需要手動修改程式碼。
這個時候,我們就可以考慮將這些引數定義到配置檔案裡面,通過前面提到的 @Value
註解取出來,再通過 @Bean
將其定義為一個 Bean,這時我們只需要在需要使用的地方注入該 Bean 即可。
首先在 application.yml 加入如下內容:
appKey: 1 appSecret: 1 bucket: lynn endPoint: https://www.aliyun.com
其次建立一個普通類:
public class Aliyun { private String appKey; private String appSecret; private String bucket; private String endPoint; public static class Builder{ private String appKey; private String appSecret; private String bucket; private String endPoint; public Builder setAppKey(String appKey){ this.appKey = appKey; return this; } public Builder setAppSecret(String appSecret){ this.appSecret = appSecret; return this; } public Builder setBucket(String bucket){ this.bucket = bucket; return this; } public Builder setEndPoint(String endPoint){ this.endPoint = endPoint; return this; } public Aliyun build(){ return new Aliyun(this); } } public static Builder options(){ return new Aliyun.Builder(); } private Aliyun(Builder builder){ this.appKey = builder.appKey; this.appSecret = builder.appSecret; this.bucket = builder.bucket; this.endPoint = builder.endPoint; } public String getAppKey() { return appKey; } public String getAppSecret() { return appSecret; } public String getBucket() { return bucket; } public String getEndPoint() { return endPoint; } }
然後在 @SpringBootConfiguration
註解的類新增如下程式碼:
@Value("${appKey}") private String appKey; @Value("${appSecret}") private String appSecret; @Value("${bucket}") private String bucket; @Value("${endPoint}") private String endPoint; @Bean public Aliyun aliyun(){ return Aliyun.options() .setAppKey(appKey) .setAppSecret(appSecret) .setBucket(bucket) .setEndPoint(endPoint) .build(); }
最後在需要的地方注入這個 Bean 即可:
@Autowired private Aliyun aliyun;
攔截器
我們在提供 API 的時候,經常需要對 API 進行統一的攔截,比如進行介面的安全性校驗。
本節,我會講解 Spring Boot 是如何進行攔截器設定的,請看接下來的程式碼。
建立一個攔截器類:ApiInterceptor,並實現 HandlerInterceptor 介面:
public class ApiInterceptor implements HandlerInterceptor { //請求之前 @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception { System.out.println("進入攔截器"); return true; } //請求時 @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } //請求完成 @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { } }
@SpringBootConfiguration
註解的類繼承 WebMvcConfigurationSupport 類,並重寫 addInterceptors 方法,將 ApiInterceptor 攔截器類新增進去,程式碼如下:
@SpringBootConfiguration public class WebConfig extends WebMvcConfigurationSupport{ @Override protected void addInterceptors(InterceptorRegistry registry) { super.addInterceptors(registry); registry.addInterceptor(new ApiInterceptor()); } }
異常處理
我們在 Controller 裡提供介面,通常需要捕捉異常,並進行友好提示,否則一旦出錯,介面上就會顯示報錯資訊,給使用者一種不好的體驗。最簡單的做法就是每個方法都使用 try catch 進行捕捉,報錯後,則在 catch 裡面設定友好的報錯提示。如果方法很多,每個都需要 try catch,程式碼會顯得臃腫,寫起來也比較麻煩。
我們可不可以提供一個公共的入口進行統一的異常處理呢?當然可以。方法很多,這裡我們通過 Spring 的 AOP 特性就可以很方便的實現異常的統一處理。
@Aspect @Component public class WebExceptionAspect { private static final Logger logger = LoggerFactory.getLogger(WebExceptionAspect.class); //凡是註解了RequestMapping的方法都被攔截@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)") private void webPointcut() { } /** * 攔截web層異常,記錄異常日誌,並返回友好資訊到前端 目前只攔截Exception,是否要攔截Error需再做考慮 * * @param e *異常物件 */ @AfterThrowing(pointcut = "webPointcut()", throwing = "e") public void handleThrowing(Exception e) { e.printStackTrace(); logger.error("發現異常!" + e.getMessage()); logger.error(JSON.toJSONString(e.getStackTrace())); //這裡輸入友好性資訊 writeContent("出現異常"); } /** * 將內容輸出到瀏覽器 * * @param content *輸出內容 */ private void writeContent(String content) { HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()) .getResponse(); response.reset(); response.setCharacterEncoding("UTF-8"); response.setHeader("Content-Type", "text/plain;charset=UTF-8"); response.setHeader("icop-content-type", "exception"); PrintWriter writer = null; try { writer = response.getWriter(); } catch (IOException e) { e.printStackTrace(); } writer.print(content); writer.flush(); writer.close(); } }
這樣,我們無需每個方法都新增 try catch,一旦報錯,則會執行 handleThrowing 方法。
優雅的輸入合法性校驗
為了介面的健壯性,我們通常除了客戶端進行輸入合法性校驗外,在 Controller 的方法裡,我們也需要對引數進行合法性校驗,傳統的做法是每個方法的引數都做一遍判斷,這種方式和上一節講的異常處理一個道理,不太優雅,也不易維護。
其實,SpringMVC 提供了驗證介面,下面請看程式碼:
@GetMapping("authorize") public void authorize(@Valid AuthorizeIn authorize, BindingResult ret){ if(result.hasFieldErrors()){ List<FieldError> errorList = result.getFieldErrors(); //通過斷言丟擲引數不合法的異常 errorList.stream().forEach(item -> Assert.isTrue(false,item.getDefaultMessage())); } } public class AuthorizeIn extends BaseModel{ @NotBlank(message = "缺少response_type引數") private String responseType; @NotBlank(message = "缺少client_id引數") private String ClientId; private String state; @NotBlank(message = "缺少redirect_uri引數") private String redirectUri; public String getResponseType() { return responseType; } public void setResponseType(String responseType) { this.responseType = responseType; } public String getClientId() { return ClientId; } public void setClientId(String clientId) { ClientId = clientId; } public String getState() { return state; } public void setState(String state) { this.state = state; } public String getRedirectUri() { return redirectUri; } public void setRedirectUri(String redirectUri) { this.redirectUri = redirectUri; } }
在 controller 的方法需要校驗的引數後面必須跟 BindingResult,否則無法進行校驗。但是這樣會丟擲異常,對使用者而言不太友好!
那怎麼辦呢?
很簡單,我們可以利用上一節講的異常處理,對報錯進行攔截:
@Component @Aspect public class WebExceptionAspect implements ThrowsAdvice{ public static final Logger logger = LoggerFactory.getLogger(WebExceptionAspect.class); //攔截被GetMapping註解的方法@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)") private void webPointcut() { } @AfterThrowing(pointcut = "webPointcut()",throwing = "e") public void afterThrowing(Exception e) throws Throwable { logger.debug("exception 來了!"); if(StringUtils.isNotBlank(e.getMessage())){ writeContent(e.getMessage()); }else{ writeContent("引數錯誤!"); } } /** * 將內容輸出到瀏覽器 * * @param content *輸出內容 */ private void writeContent(String content) { HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()) .getResponse(); response.reset(); response.setCharacterEncoding("UTF-8"); response.setHeader("Content-Type", "text/plain;charset=UTF-8"); response.setHeader("icop-content-type", "exception"); PrintWriter writer = null; try { writer = response.getWriter(); writer.print((content == null) ? "" : content); writer.flush(); writer.close(); } catch (IOException e) { e.printStackTrace(); } } }
這樣當我們傳入不合法的引數時就會進入 WebExceptionAspect 類,從而輸出友好引數。
我們再把驗證的程式碼單獨封裝成方法:
protected void validate(BindingResult result){ if(result.hasFieldErrors()){ List<FieldError> errorList = result.getFieldErrors(); errorList.stream().forEach(item -> Assert.isTrue(false,item.getDefaultMessage())); } }
這樣每次引數校驗只需要呼叫 validate 方法就行了,我們可以看到程式碼的可讀性也大大的提高了。
介面版本控制
一個系統上線後會不斷迭代更新,需求也會不斷變化,有可能介面的引數也會發生變化,如果在原有的引數上直接修改,可能會影響線上系統的正常執行,這時我們就需要設定不同的版本,這樣即使引數發生變化,由於老版本沒有變化,因此不會影響上線系統的執行。
一般我們可以在地址上帶上版本號,也可以在引數上帶上版本號,還可以再 header 裡帶上版本號,這裡我們在地址上帶上版本號,大致的地址如:http://api.example.com/v1/test,其中,v1 即代表的是版本號。具體做法請看程式碼:
@Target({ElementType.METHOD,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Mapping public @interface ApiVersion { /** * 標識版本號 * @return */ int value(); } public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> { // 路徑中版本的字首, 這裡用 /v[1-9]/的形式 private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile("v(\\d+)/"); private int apiVersion; public ApiVersionCondition(int apiVersion){ this.apiVersion = apiVersion; } @Override public ApiVersionCondition combine(ApiVersionCondition other) { // 採用最後定義優先原則,則方法上的定義覆蓋類上面的定義 return new ApiVersionCondition(other.getApiVersion()); } @Override public ApiVersionCondition getMatchingCondition(HttpServletRequest request) { Matcher m = VERSION_PREFIX_PATTERN.matcher(request.getRequestURI()); if(m.find()){ Integer version = Integer.valueOf(m.group(1)); if(version >= this.apiVersion) { return this; } } return null; } @Override public int compareTo(ApiVersionCondition other, HttpServletRequest request) { // 優先匹配最新的版本號 return other.getApiVersion() - this.apiVersion; } public int getApiVersion() { return apiVersion; } } public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping { @Override protected RequestCondition<ApiVersionCondition> getCustomTypeCondition(Class<?> handlerType) { ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class); return createCondition(apiVersion); } @Override protected RequestCondition<ApiVersionCondition> getCustomMethodCondition(Method method) { ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class); return createCondition(apiVersion); } private RequestCondition<ApiVersionCondition> createCondition(ApiVersion apiVersion) { return apiVersion == null ? null : new ApiVersionCondition(apiVersion.value()); } } @SpringBootConfiguration public class WebConfig extends WebMvcConfigurationSupport { @Bean public AuthInterceptor interceptor(){ return new AuthInterceptor(); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()); } @Override @Bean public RequestMappingHandlerMapping requestMappingHandlerMapping() { RequestMappingHandlerMapping handlerMapping = new CustomRequestMappingHandlerMapping(); handlerMapping.setOrder(0); handlerMapping.setInterceptors(getInterceptors()); return handlerMapping; } }
Controller 類的介面定義如下:
@ApiVersion(1) @RequestMapping("{version}/dd") public class HelloController{}
這樣我們就實現了版本控制,如果增加了一個版本,則建立一個新的 Controller,方法名一致,ApiVersion 設定為2,則地址中 v1 會找到 ApiVersion 為1的方法,v2 會找到 ApiVersion 為2的方法。
自定義 JSON 解析
Spring Boot 中 RestController 返回的字串預設使用 Jackson 引擎,它也提供了工廠類,我們可以自定義 JSON 引擎,本節例項我們將 JSON 引擎替換為 fastJSON,首先需要引入 fastJSON:
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>${fastjson.version}</version> </dependency>
其次,在 WebConfig 類重寫 configureMessageConverters 方法:
@Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { super.configureMessageConverters(converters); /* 1.需要先定義一個convert轉換訊息的物件; 2.新增fastjson的配置資訊,比如是否要格式化返回的json資料 3.在convert中新增配置資訊 4.將convert新增到converters中 */ //1.定義一個convert轉換訊息物件 FastJsonHttpMessageConverter fastConverter=new FastJsonHttpMessageConverter(); //2.新增fastjson的配置資訊,比如:是否要格式化返回json資料 FastJsonConfig fastJsonConfig=new FastJsonConfig(); fastJsonConfig.setSerializerFeatures( SerializerFeature.PrettyFormat ); fastConverter.setFastJsonConfig(fastJsonConfig); converters.add(fastConverter); }
單元測試
Spring Boot 的單元測試很簡單,直接看程式碼:
@SpringBootTest(classes = Application.class) @RunWith(SpringJUnit4ClassRunner.class) public class TestDB { @Test public void test(){ } }
模板引擎
在傳統的 SpringMVC 架構中,我們一般將 JSP、HTML 頁面放到 webapps 目錄下面,但是 Spring Boot 沒有 webapps,更沒有 web.xml,如果我們要寫介面的話,該如何做呢?
Spring Boot 官方提供了幾種模板引擎:FreeMarker、Velocity、Thymeleaf、Groovy、mustache、JSP。
這裡以 FreeMarker 為例講解 Spring Boot 的使用。
首先引入 FreeMarker 依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency>
在 resources 下面建立兩個目錄:static 和 templates,如圖所示:
其中 static 目錄用於存放靜態資源,譬如:CSS、JS、HTML 等,templates 目錄存放模板引擎檔案,我們可以在 templates 下面建立一個檔案:index.ftl(freemarker 預設字尾為 .ftl
),並新增內容:
<!DOCTYPE html> <html> <head> </head> <body> <h1>Hello World!</h1> </body> </html>
然後建立 PageController 並新增內容:
@Controller public class PageController { @RequestMapping("index.html") public String index(){ return "index"; } }
啟動 Application.java,訪問:http://localhost:8080/index.html,就可以看到如圖所示:
第14課:Spring Cloud 例項詳解——基礎框架搭建(一)
第15課:Spring Cloud 例項詳解——基礎框架搭建(二)
第16課:Spring Cloud 例項詳解——基礎框架搭建(三)
第17課:Spring Cloud 例項詳解——業務程式碼實現
第20課:K8S+Docker 部署 Spring Cloud 叢集
閱讀全文: http://gitbook.cn/gitchat/column/5af108d20a989b69c385f47a