Spring Cloud 從入門到精通

課程介紹html

Spring Cloud 是一套完整的微服務解決方案,基於 Spring Boot 框架,準確的說,它不是一個框架,而是一個大的容器,它將市面上較好的微服務框架集成進來,從而簡化了開發者的代碼量。前端

本課程由淺入深帶領你們一步步攻克 Spring Cloud 各大模塊,接着經過一個實例帶領你們瞭解大型分佈式微服務架構的搭建過程,最後深刻源碼加深對它的瞭解。java

本課程共分爲四個部分:mysql

第一部分(第1-3課),初識 Spring Boot,掌握 Spring Boot 基礎知識,爲後續入門 Spring Cloud 打好基礎 。git

第二部分(第4-13課),Spring Cloud 入門篇,主要介紹 Spring Cloud 經常使用模塊,包括服務發現、服務註冊、配置中心、鏈路追蹤、異常處理等。github

第三部分(第14-18課),Spring Cloud 進階篇,介紹大型分佈式系統中事務處理、線程安全等問題,並以一個實例項目手把手教你們搭建完整的微服務系統。web

第四部分(第19-20課),Spring Cloud 高級篇,解析 Spring Cloud 源碼,並講解如何部署基於 Spring Cloud 的大型分佈式系統。spring

做者介紹sql

李熠,從事 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 實現了微服務架構的方方面面,服務治理只是其中的一個方面。下面經過一張圖對其進行比較:

圖片來自 http://www.uml.org.cn/wfw/201711292.asp?artid=20127

能夠看出,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>

相關文章
相關標籤/搜索