7天 Spring Boot從入門到精通

Spring Boot 工程化最佳實踐

Spring Boot 已經成爲 Java 後端事實上的標準開發框架,目前已經演進到了 2.1.5 版本。在項目開發過程中,也逐漸形成了一些公認的不錯的做法或者規範,本文試圖將其沉澱總結爲最佳實踐,供後來人學習和使用。這些實踐包含實際項目開發中的方方面面,包含但不限於工程實踐、技術細節、規範流程、技術選型等,希望能讓讀者少走彎路,同時能在團隊中形成相對統一的規範與實踐,減少不同項目之間切換的學習成本。

適合人羣:Java 後端開發人員、架構師、技術管理者。

開發工具

工欲善其事,必先利其器,爲了進行高效的開發,選擇好開發工具非常重要。對於 Spring Boot 開發而言,首選的 IDE 當然非 IntelliJ IDEA Ultimate 莫屬,功能強大,不過正版授權並不便宜;因此,我們也可以退而求其次,選擇 Spring Boot 官方提供的開發工具 Spring Tools 4,它本身不是獨立的 IDE,而是以插件形式內嵌在其他開源 IDE 中的,目前有三個選擇:

  • Spring Tools 4 for Eclipse
  • Spring Tools 4 for Visual Studio Code
  • Spring Tools 4 for Atom IDE

大家可以根據自己和團隊的熟悉程度進行選擇,不過從之前使用情況來看,性能上 Visual Studio Code 應該是首選。

Java 後端開發的構建工具選擇不多,從最開始發展到現在,也纔出現三代構建工具,它們分別是:Ant、Maven 和 Gradle。其中 Ant 已經是一個被淘汰的構建工具,我們使用 Spring Initializr 新建 Spring Boot 工程時,也只會有 Maven 和 Gradle 這兩種工程類型可以選擇,Ant 只能在一些遺留項目或者老項目中可以看到。

Maven 算是 Java 後端構建工具的老大,而且霸佔這個位置已經很多年了,GitHub 上面主流 Java 開源項目大多數都是採用 Maven 作爲構建工具;而 Gradle 作爲新興的構建工具,也是 Android 開發中默認的構建工具,但在 Java 後端開發中市場佔有率和 Maven 相比還是差了很多,但它的未來是可期的,目前我們團隊也是採用 Gradle djust:none;-webkit-font-smoothing:antialiased;box-sizing:border-box;margin:0px 0px 1.1em;outline:0px;">關於這三個構建工具的進一步對比,可以參見 Ant vs Maven vs Gradle 這篇文章。

工程結構

使用 Spring Initializr 生成的 Spring Boot 工程整體而言已經是非常標準的結構了,這裏我們重點關注一下包的劃分,大致上遵循阿里巴巴 Java 開發手冊中的基本結構,下面是一個常見的包名劃分:

├── main
│   ├── java
│   │   └── com
│   │       └── bestpractice
│   │           ├── config
│   │           ├── constants
│   │           ├── controller
│   │           ├── dao
│   │           ├── exception
│   │           ├── filter
│   │           ├── manager
│   │           ├── model
│   │           │   ├── bo
│   │           │   ├── dto
│   │           │   └── vo
│   │           ├── service
│   │           │   ├── impl
│   │           ├── task
│   │           ├── utils
│   │           └── BestPracticeApplication.java
│   │
│   └── resources

其中,每個包下面存放的類或者接口的類型說明如下:

  • config:存放配置相關的類,例如採用 @Configuration 註解的類都建議放這裏,包括但不限於 Redis 配置類,RestTemplate 配置類,Swagger 配置類等。
  • constants:存放常量類、枚舉類等。
  • controller:存放控制器類,工程對外的 RESTful 接口定義都在這個包中。
  • dao:存放數據訪問相關的類,例如與 MySQL、HBase、Elasticsearch 等的數據訪問類。
  • exception:存放全局異常處理類(使用 @ControllerAdvice 註解修飾),以及自定義的業務相關的異常類。
  • manager:存放通用業務處理相關的類,例如對第三方系統接口的封裝類、service 層通用能力的封裝類(緩存方案等)、對 dao 層中多個類的組合複用等。
  • model:存放 bean 的定義,根據領域模型層次的不同,bean 又可以進一步分爲 DO、DTO、BO、AO、VO 等,具體可以參見阿里巴巴 Java 開發手冊中的定義。
  • service:存放業務邏輯相關的處理類,通常在 service 包下面會以 interface 的方式定義接口(例如 SmsService),然後在 service/impl 包下面實現對應的接口(例如 SmsServiceImpl),從而對 controller 層的類而言,看到的永遠是接口,而不是具體的實現類,很好的實現層與層之間的解耦。
  • task:存放定時任務相關的類。
  • utils:存放工程中需要使用到的工具類。

當然,上面的這種劃分只是一種參考,你可以根據自己項目實際進行增刪改,但建議一個團隊要保持一致的風格。

根據環境區分配置文件

我們開發的服務通常會部署在不同的環境中,例如開發環境、測試環境、預發佈環境,生產環境等,而不同環境需要不同的配置,例如連接不同的 Redis、數據庫、第三方服務等等。Spring Boot 默認的配置文件是 application.properties。那麼如何實現不同的環境使用不同的配置文件呢?一個比較好的實踐是爲不同的環境定義不同的配置文件,如下所示:

  • 開發環境:application-dev.properties
  • 測試環境:application-test.properties
  • 預發佈環境:application-stg.properties
  • 生產環境:application-prd.properties

然後在啓動服務時通過增加 --spring.profiles.active 參數來指定要啓動哪個環境即可,例如啓動生產環境,命令如下所示:

java -jar sms.jar --spring.profiles.active=prd

關於 Spring Profiles 更多信息可以參見:Spring Profiles

配置文件敏感字段加解密

Jasypt 是 Java Simplified Encryption 的縮寫,旨在爲 Java 開發提供方便的加解密功能,能夠很好地集成進基於 Spring 的應用中,和 Spring Security 也可以做到無縫集成。

在 Spring Boot 中,我們通常使用它來給 application.properties 配置文件中的敏感字段,例如數據庫連接密碼、Redis 連接密碼等進行加密和解密,從而保證這些敏感信息只掌握在少數經過授權的人員手中,最大限度的保證系統安全。當然,在 Spring Boot 中如果直接使用 Jasypt 函數庫來對配置文件中的敏感字段進行加解密的話,開發者自己還是要做很多工作的,因此我們通常會使用 ulisesbocchio 對 Jasypt 封裝後的開源庫 Jasypt Spring Boot,Jasypt Spring Boot 是基於 Jasypt 實現 Spring Boot 配置文件屬性值加解密的函數庫。

Jasypt Spring Boot 的使用很簡單,首先引入依賴:

compile "com.github.ulisesbocchio:jasypt-spring-boot-starter:1.18"

然後有兩種方式可以給敏感信息加密,分別是直接調用 JAR 包中提供的 API,或者直接運行 JAR 包。直接運行 JAR 包方式對敏感信息加密的命令如下所示:

java -cp jasypt-1.9.2.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input='asce1885$Opr06' password=b3sybFCDnbRt algorithm=PBEWithMD5AndDES

其中,input 是要加密的敏感信息,password 是加密使用的鹽,我們自己設定一個值就行,algorithm 是使用加密算法執行命令後,命令行中會打印類似如下信息:

----ENVIRONMENT-----------------
Runtime: Oracle Corporation Java HotSpot(TM) 64-Bit Server VM 25.162-b12

----ARGUMENTS-------------------
algorithm: PBEWithMD5AndDES
input: asce1885$Opr06
password: b3sybFCDnbRt

----OUTPUT----------------------
VI6Z9FL6UD/FIEMGcR4PI+SzRsejrQbV

其中 OUTPUT 就是加密後的密碼。需要注意的是,上面生成密碼命令中 input='asce1885$Opr06',其中明文密碼是 asce1885$Opr06,但我們在賦值給 input 字段時前後加了單引號,這是因爲這個密碼包含了特殊字符 $,如果沒有包含特殊字符,單引號可以去掉。包含特殊字符的待加密明文不加單引號,Jasypt 在加密時解析存在問題,會把 asce1885$Opr06 解析成 asce1885

最後,將上面的 passwordOUTPUT 配置到 application.properties 文件中,如下所示,爲了對比,我把加密前和加密後的配置都列了出來:

## 加密前
spring.datasource.password=asce1885$Opr06

## 加密後
spring.datasource.password=ENC(VI6Z9FL6UD/FIEMGcR4PI+SzRsejrQbV)
jasypt.encryptor.password=b3sybFCDnbRt

可以看到,上面我們把 jasypt.encryptor.password=b3sybFCDnbRt 這個加密所用的鹽也配置在 application.properties 文件中,這樣當然是有問題的,因爲別人拿到這個鹽之後是可以直接解密出原來的敏感信息的,因此,jasypt.encryptor.password 通常會作爲啓動參數傳入,從而避免把加密鹽寫死在配置文件中導致所有人都能獲取到,如下所示:

java -jar sms.jar --spring.profiles.active=prd --jasypt.encryptor.password=b3sybFCDnbRt

至此,Jasypt 配置完成,我們在配置文件中看不到原始數據庫密碼了。

更多信息可以參考:

替換底層的 HTTP 函數庫

在 Spring Boot 項目中,底層涉及網絡請求的組件有 RestTemplate、Feign 和 Zuul,它們分別有自己默認的 HTTP 請求客戶端,很多時候爲了獲得更好的性能,我們需要替換底層默認的 HTTP 函數庫,可選的有 Apache HttpClient 和 OkHttpClient,建議採用 OkHttpClient,下面也都是以 OkHttp 的替換爲例進行說明。

RestTemplate

RestTemplate 默認使用的是 JDK 原生的 HttpURLConnection,使用 OkHttpClient 對其進行替換時,我們可以實現 Spring Cloud Commons 提供的 OkHttpClientFactory 並進行自定義的配置,如下所示:

import java.util.concurrent.TimeUnit;
import okhttp3.ConnectionPool;
import okhttp3.ConnectionSpec;
import okhttp3.OkHttpClient;
import org.springframework.cloud.commons.httpclient.OkHttpClientFactory;

public class OkHttpClientFactoryImpl implements OkHttpClientFactory {
  @Override public OkHttpClient.Builder createBuilder(boolean disableSslValidation) {
    OkHttpClient.Builder builder = new OkHttpClient.Builder();
    ConnectionPool okHttpConnectionPool = new ConnectionPool(50, 30, TimeUnit.SECONDS);
    builder.connectionPool(okHttpConnectionPool);
    builder.connectTimeout(20, TimeUnit.SECONDS);
    builder.retryOnConnectionFailure(false);
    return builder;
  }
}

然後,就可以在 RestTemplate 中配置使用 Okhttp,如下所示:

@Configuration
public class RestTemplateConfig {

  @Autowired 
  @Qualifier("OKSpringCommonsRestTemplate")
  ClientHttpRequestFactory okHttpRequestFactory;

  @Bean
  @Qualifier("OKSpringCommonsRestTemplate")
  public RestTemplate createOKCustomRestTemplate() {
    RestTemplate restTemplate = new RestTemplate();
    restTemplate.setRequestFactory(okHttpRequestFactory);
    return restTemplate;
  }
}

更多信息可以參見這篇文章:Changing HttpClient in Spring RestTemplate

Feign

Feign 默認使用的是 JDK 原生的 HTTPURLConnection,我們可以使用 Apache HTTP Client 和 Okhttp 來進行替換,替換的步驟很簡單,分爲兩步,首先引入相關的依賴庫,然後修改配置。

這裏我們主要看看使用 Okhttp 替換默認 Http Client 的步驟,首先引入依賴如下所示:

compile group: 'io.github.openfeign', name: 'feign-okhttp', version: '10.2.3'

然後增加配置如下所示:

feign.okhttp.enabled=true #表示使用 OkHttpClient
feign.httpclient.enabled=false #表示不使用 ApacheHttpClient

更多信息可以參見這篇文章:Feign 默認 Client 替換

Zuul

目前最新的 Zuul 使用的默認 HTTP 客戶端是 Apache HTTP Client,舊版本使用的是已經廢棄的 Ribbon RestClient。當然,我們也可以使用 Okhttp,要切換 Zuul 底層使用的 HTTP 客戶端,只需要在配置文件中增加如下配置,並引入對應的依賴函數庫即可:

ribbon.restclient.enabled=true #表示使用 Ribbon RestClient
ribbon.okhttp.enabled=true # 表示使用 Okhttp

相關信息可以參見官方的說明:Zuul Http Client

基於不同端口實現公有 API 和私有 API 的隔離

在微服務架構下,我們開發的後端服務可能存在需要提供外部接口供互聯網上的終端用戶訪問,也需要提供內部接口供系統內其他服務調用。外部接口通常都需要添加認證和授權的邏輯,而內部接口通常無需認證即可訪問。如果我們的服務只存在一個端口,例如 9002,那麼從互聯網上也可以通過這個端口對內部接口進行訪問,這樣就存在安全問題,內部接口永遠只能對內部可見。

那麼如何解決這個問題呢?一種不錯的方案就是我們的服務提供兩個不同的端口,分別給外部接口和內部接口使用,給內部接口使用的端口我們可以通過防火牆將其和互聯網隔離,從而達到保護的作用。具體到 Spring Boot 中,我們怎麼實現一個服務支持兩個端口呢?

區分內部和外部接口

首先通過 URL 中的路徑來區分外部接口和內部接口,如下所示:

// 外部接口的 URL 路徑以 /external/ 作爲前綴
@Controller
public class ExternalApiController {
    @GetMapping("/external/hello")
    public ResponseEntity\<String\> hello() {
        return ResponseEntity.ok("Hello stranger");
    }
}

// 內部接口的 URL 路徑以 /internal/ 作爲前綴
@Controller
public class InternalApiController {
    @GetMapping("/internal/hello")
    public ResponseEntity\<String\> hello() {
        return ResponseEntity.ok("Hello friend");
    }
}

監聽不同的端口

Spring Boot 應用默認只會監聽一個端口,但我們可以通過修改底層使用的 Tomcat 容器的來增加監聽的端口。如下所示,通過自定義 WebServerFactoryCustomizer 來實現:

@Configuration
public class TrustedPortConfiguration {

    // 提供給外部接口使用的端口
    @Value("${server.port:8080}")
    private String serverPort;

    @Value("${management.port:${server.port:8080}}")
    private String managementPort;

    // 提供給內部接口使用的端口
    @Value("${server.trustedPort:null}")
    private String trustedPort;

    @Bean
    public WebServerFactoryCustomizer servletContainer() {
        Connector[] additionalConnectors = this.additionalConnector();

        ServerProperties serverProperties = new ServerProperties();
        return new TomcatMultiConnectorServletWebServerFactoryCustomizer(serverProperties, additionalConnectors);
    }

    private Connector[] additionalConnector() {
        if (StringUtils.isEmpty(this.trustedPort) || "null".equals(trustedPort)) {
            return null;
        }

        Set<String> defaultPorts = new HashSet<>();
        defaultPorts.add(serverPort);
        defaultPorts.add(managementPort);

        if (!defaultPorts.contains(trustedPort)) {
            Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
            connector.setScheme("http");
            connector.setPort(Integer.valueOf(trustedPort));
            return new Connector[]{connector};
        } else {
            return new Connector[]{};
        }
    }

    private class TomcatMultiConnectorServletWebServerFactoryCustomizer extends TomcatServletWebServerFactoryCustomizer {
        private final Connector[] additionalConnectors;

        TomcatMultiConnectorServletWebServerFactoryCustomizer(ServerProperties serverProperties, Connector[] additionalConnectors) {
            super(serverProperties);
            this.additionalConnectors = additionalConnectors;
        }

        @Override
        public void customize(TomcatServletWebServerFactory factory) {
            super.customize(factory);

            if (additionalConnectors != null && additionalConnectors.length > 0) {
                factory.addAdditionalTomcatConnectors(additionalConnectors);
            }
        }
    }
}

通過上面的配置,我們啓動服務時,可以發現它已經支持兩個端口了,但目前通過兩個端口都可以訪問服務所提供的所有接口,所以接下來我們要做一些限制。

基於 URL 路徑和端口對請求進行過濾

通過 Spring 的過濾器可以實現請求的過濾,過濾器定義如下:

public class TrustedEndpointsFilter implements Filter {

    private int trustedPortNum = 0;
    private String trustedPathPrefix;
    private final Logger log = LoggerFactory.getLogger(getClass().getName());

    TrustedEndpointsFilter(String trustedPort, String trustedPathPrefix) {
        if (trustedPort != null && trustedPathPrefix != null && !"null".equals(trustedPathPrefix)) {
            trustedPortNum = Integer.valueOf(trustedPort);
            this.trustedPathPrefix = trustedPathPrefix;
        }
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        if (trustedPortNum != 0) {
            // 通過外部端口試圖訪問內部接口,拒絕請求
            if (isRequestForTrustedEndpoint(servletRequest) && servletRequest.getLocalPort() != trustedPortNum) {
                log.warn("denying request for trusted endpoint on untrusted port");
                ((ResponseFacade) servletResponse).setStatus(404);
                servletResponse.getOutputStream().close();
                return;
            }

            // 通過內部端口試圖訪問外部接口,拒絕請求
            if (!isRequestForTrustedEndpoint(servletRequest) && servletRequest.getLocalPort() == trustedPortNum) {
                log.warn("denying request for untrusted endpoint on trusted port");
                ((ResponseFacade) servletResponse).setStatus(404);
                servletResponse.getOutputStream().close();
                return;
            }
        }

        filterChain.doFilter(servletRequest, servletResponse);
    }

    // 通過 URL 中的路徑前綴來判斷對應的接口是內部接口還是外部接口
    private boolean isRequestForTrustedEndpoint(ServletRequest servletRequest) {
        return ((RequestFacade) servletRequest).getRequestURI().startsWith(trustedPathPrefix);
    }
}

爲了使上面的 filter 生效,我們需要把它作爲 bean 進行實例化,如下所示:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    // 內部端口
    @Value("${server.trustedPort:null}")
    private String trustedPort;

    // 內部接口 URL 路徑前綴
    @Value("${server.trustedPathPrefix:null}")
    private String trustedPathPrefix;

    @Bean
    public FilterRegistrationBean\<TrustedEndpointsFilter\> trustedEndpointsFilter() {
        return new FilterRegistrationBean<>(new TrustedEndpointsFilter(trustedPort, trustedPathPrefix));
    }
}

最後,我們在 application.properties 文件中配置端口和 URL 路徑前綴如下:

server.port=9002
server.trustedPort=9003
server.trustedPathPrefix=/internal/

Lombok

Java 編程中經常需要寫很多樣板代碼,不僅降低了開發效率而且也影響代碼的可讀性,Lombok 的引入能夠很好地解決這個問題。Spring Initializr 默認也提供對 Lombok 的支持,可以在創建 Spring Boot 工程時勾選並引入。Lombok 提供了很多註解,能夠方便的生成樣板代碼,例如:

  • @Getter/@Setter:生成實體類的 getter 和 setter 方法。
  • @ToString:生成實體類的 toString 方法。
  • @EqualsAndHashCode:生成實體類的 hashCode 和 equals 方法。
  • @NoArgsConstructor, @RequiredArgsConstructor and @AllArgsConstructor:爲實體類生成指定類型的構造方法,分別是無參構造方法、指定部分參數的構造方法和帶有所有參數的構造方法。
  • @Data:@ToString、@EqualsAndHashCode、@Getter、@Setter 和 @RequiredArgsConstructor 疊加的效果。
  • @Builder:按照 Builder 模式生成實體類的相關 Builder 類和方法。
  • @Cleanup:實現自動資源管理功能,例如自動關閉 InputStream 等。

更多信息可以參見這篇文章:Introduction to Project Lombok

日誌記錄

Spring Boot 默認的日誌記錄框架使用的是 Logback,此外我們還可以選擇 Log4j 和 Log4j2。其中 Log4j 可以認爲是一個過時的函數庫,不推薦使用,相比之下,性能和功能也是最差的。logback 雖然是 Spring Boot 默認的,但性能上還是不及 Log4j2,因此,在現階段,日誌記錄首選 Log4j2。關於這三個日誌記錄框架的簡單對比,可以參見這篇文章:Java Logging Frameworks: Log4j vs logback vs Log4j2

當然,在實際項目開發中,我們不會直接調用上面三款日誌框架的 API 去記錄日誌,因爲這樣如果要切換日誌框架的話代碼需要修改的地方太多。因此,最佳實踐是採用 SLF4J 來進行日誌記錄,SLF4J 是基於門面模式實現的一個通用日誌框架,它本身並沒有日誌記錄的功能,實際的日誌記錄還是需要依賴 Log4j、logback 或者 Log4j2。使用 SLF4J,可以實現簡單快速地替換底層的日誌框架而不會導致業務代碼需要做相應的修改。SLF4J + Log4j2 是我們推薦的日誌記錄選型。

在使用 SLF4J 進行日誌記錄時,通常都需要在每個需要記錄日誌的類中定義 Logger 變量,如下所示:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@RestController
public class SmsController {
    private static final Logger LOGGER = LoggerFactory.getLogger(SmsController.class);
    ...
}

這顯然屬於重複性勞動,降低了開發效率,如果你在項目中引入了上節介紹的 Lombok,那麼可以使用它提供的 @Slf4j 註解來自動生成上面那個變量,默認的變量名是 log,如果我們想採用慣用的 LOGGER 變量名,那麼可以在工程的 main/java 目錄中增加 lombok.config 文件,並在文件中增加 lombok.log.fieldName=LOGGER 的配置項即可。

在微服務架構中,前端的一個請求往往會經過後端多個服務的處理才返回結果,因此,在出現問題需要定位時,需要跨多個服務的日誌進行查詢,那麼如何定位到同一次請求對應的日誌呢,這就需要有一個 traceId 將多個服務的日誌串聯起來。在 Spring Boot 開發中,我們可以在工程依賴中引入 Spring Cloud Sleuth 依賴:

dependencies {
    implementation "org.springframework.cloud:spring-cloud-starter-sleuth"
}

然後在日誌中配置文件中設置日誌的 Pattern,這裏以 Log4j2 爲例,我們在 log4j2.xml 文件中配置如下:

\<PatternLayout\>
    \<Pattern\>[%d{yyyy-MM-dd HH:mm:ss,SSS}] [%t] [%X{X-B3-TraceId}] [%X{X-B3-SpanId}] [%X{X-B3-ParentSpanId}] [%-5level] [%class{36}] [%L] [%M] [%msg%xEx]%n\</Pattern\>
\</PatternLayout\>

Pattern 用來配置工程輸出的日誌格式,上面的配置基本上涵蓋了日誌輸出所需內容。供參考,其中每個項的說明如下:

  • %d{yyyy-MM-dd HH:mm:ss,SSS}:日誌打印的日期
  • %t:線程名
  • %X{X-B3-TraceId}:Spring Cloud Sleuth 提供的,打印 traceId
  • %X{X-B3-SpanId}:Spring Cloud Sleuth 提供的,打印 spanId
  • %X{X-B3-ParentSpanId}:Spring Cloud Sleuth 提供的,打印 parentSpanId
  • %-5level:日誌級別
  • %class{36}:類名
  • %L:代碼所在行數
  • %M:方法名
  • %msg%xEx:具體的日誌信息

其中 TraceI、SpanId 和 ParentSpanId 屬於分佈式鏈路追蹤的範疇,如果你不熟悉想進一步瞭解,可以看看我的這篇文章:分佈式鏈路追蹤的前世今生

工具類採用 Hutool

無論大家從事哪一端的開發,不可避免地需要用到一系列的工具類,例如常見的有字符串處理、加解密、隨機數生成、Bean 轉爲 Map 等。通常情況下,每個人或者每個團隊都或多或少會維護自己的一套工具類,在不同項目之間共用(或者拷貝)。這種情況一來我們要自己維護一套工具類,存在成本問題;二來可能自己實現的還不一定完全正確或者最優,畢竟使用你這套工具類的人和項目還是有限的,因此,採用成熟的開源的工具類函數庫是一個比較推薦的選擇。

Hutool 就是其中的佼佼者,它的模塊和功能分類如下所示:

可以看到,功能還是很齊全的,在項目中可以根據需要進行部分引入,Hutool 中具備的功能就不要再自己造輪子了。始終要相信,代碼越少,Bug 越少。

技術選型

Redis 幾乎是每個項目必備的一箇中間件,爲了對 Redis 服務器進行訪問,我們當然需要在 Spring Boot 工程中集成 Redis 客戶端,Java 語言實現的 Redis 客戶端非常多,僅 Redis 官網列出的就有十一種。其中比較流行的有 Jedis、Redisson 和 Lettuce。

其中 Jedis 是老牌的 Redis 客戶端,也是 Spring Boot 1.x 默認的 Redis 客戶端,它使用阻塞的 I/O,方法調用都是同步的,而且 Jedis 實例不是線程安全的,需要通過連接池來使用 Jedis。Redisson 和 Lettuce 底層都是基於 Netty,方法調用是異步的,它們兩個的實例是線程安全的。

從性能上看,Redisson 和 Lettuce 完敗 Jedis,這也是爲什麼從 Spring Boot 2.x 開始,默認的 Redis 客戶端換成了 Lettuce。從功能上看,Jedis 和 Lettuce 基本差不多,都只提供 對 Redis 命令的原始封裝,是比較純粹的 Redis 客戶端;相比之下,Redisson 就顯得很強大。我們可以認爲 Jedis 和 Lettuce 提供的是低層的 API,而 Redisson 提供的是高層的 API,提供了諸如分佈式對象分佈式集合分佈式鎖和同步器分佈式服務等等增強功能。

在技術選型上,Jedis 完全不用考慮,如果不需要使用到諸如分佈式鎖等基於 Redis 實現的高級功能,那麼選擇 Lettuce 即可;否則選擇 Redisson 會方便很多。

緩存框架

現代後端開發中,對於查詢類請求,通常會增加 Redis 作爲一級緩存,避免每次查詢都去操作數據庫。當在 Redis 中查詢不到時纔會去數據庫中查找,如果不使用下面要介紹的緩存框架,類似這樣的邏輯我們需要每次都在代碼中自己去進行判斷。如果使用緩存框架,那麼可以減少類似樣板代碼的編寫,減少緩存使用的複雜度,提高代碼可讀性和提高開發效率。

在 Spring Boot 開發中,可選的緩存框架主要有 Spring Cache 和 JetCache,其中 Spring Cache 是 Spring 官方提供的緩存方案,JetCache 是阿里巴巴開源的緩存框架。在功能上面,JetCache 要強大很多,它提供了比 Spring Cache 更加強大的註解,可以原生的支持 TTL、兩級緩存、分佈式自動刷新,還提供了 Cache 接口用於手工緩存操作。

JetCache 連接 Redis 時支持 Jedis 和 Lettuce 兩種客戶端,目前不支持 Redisson。在技術選型上,沒有特殊需求的情況下,建議優先選擇 JetCache。

定時任務框架

Spring Boot 中定時任務常見的實現方案有:

  • Timer:JDK 自帶的定時器,最簡單的實現任務調度的方案,所有的任務都是由一個線程串行執行的,一般在 Web 開發中不建議使用。
  • ScheduledExecutor/@Scheduled:Java 5 推出的基於線程池設計的定時任務實現方案,通過將每個任務分配給線程池中的一個線程實現任務的並行執行,互不干擾。Spring Boot 中可以採用 @Scheduled 註解實現定時任務。在分佈式系統中,一個服務會在不同雲主機上部署多個實例,因此,直接使用 @Scheduled 的話會導致同一個定時任務在多個實例上重複執行,爲了保證同一時間只有一個定時任務執行,需要引入分佈式鎖。
  • Quartz:功能完善的定時任務框架,支持分佈式場景,使用時需要依賴 MySQL,本質上是通過數據庫鎖來避免同一個定時任務在多個實例上的重複執行,同時支持任務的失效轉移。

在技術選型上,對於要求嚴格的定時任務,推薦採用 Quartz;對於簡單且定時執行要求不嚴格的場景,可以選擇 @Scheduled 方案。

熔斷框架

微服務開發中,熔斷是不可或缺的一種能力,目前常見的熔斷框架選擇有 Sentinel、Hystrix 和 Resilience4j,關於這三者的對比,我們通過 Sentinel 官網的一張表格進行了解,如下所示:

功能 Sentinel Hystrix Resilience4j     隔離策略 信號量隔離(併發線程數限流) 線程池隔離/信號量隔離 信號量隔離   熔斷降級策略 基於響應時間、異常比率、異常數 基於異常比率 基於異常比率、響應時間   實時統計實現 滑動窗口(LeapArray) 滑動窗口(基於 RxJava) Ring Bit Buffer   動態規則配置 支持多種數據源 支持多種數據源 有限支持   擴展性 多個擴展點 插件的形式 接口的形式   基於註解的支持 支持 支持 支持   限流 基於 QPS,支持基於調用關係的限流 有限的支持 Rate Limiter   流量整形 支持預熱模式、勻速器模式、預熱排隊模式 不支持 簡單的 Rate Limiter 模式   系統自適應保護 支持 不支持 不支持   控制檯 提供開箱即用的控制檯,可配置規則、查看秒級監控、機器發現等 簡單的監控查看 不提供控制檯,可對接其它監控系統    

從中可以看到,Sentinel 功能最強大,相比 Hystrix 而言,Sentinel 提供的控制檯是一大亮點,而且,Sentinel 不止提供熔斷功能,它的定位是面向分佈式服務架構的輕量級流量控制框架,主要以流量爲切入點,從流量控制、熔斷降級、系統負載保護等多個維度來保護服務的穩定性。因此,在技術選型上,建議選擇 Sentinel。

數據庫連接池

Spring Boot 默認支持的數據庫連接池有 DBCP、DBCP 2、Tomcat JDBC Pool 和 HikariCP,其中,Spring Boot 1.x 默認使用的是 Tomcat JDBC Pool,Spring Boot 2.x 默認使用的是 HikariCP,DBCP 和 DBCP 2 目前不推薦使用。

技術選型上,如果沒有特殊原因,建議採用 HikariCP。當然你應該也聽說過或者用過阿里巴巴開源的 Druid,不熟悉的讀者千萬不要把它和實時大數據分析框架 Druid 搞混。阿里巴巴 Druid 的定位是爲監控而生的數據庫連接池,也就是說它集成和數據庫連接池和數據庫監控兩大功能,因此,也被不少人詬病功能的不純粹。

數據庫重構工具

我們的代碼都會通過 Git 進行版本管理,但通常情況下在項目迭代過程中數據庫腳本的變更都是手工維護的,那麼有沒有類似 Git 這樣的工具呢?答案當然是肯定的,目前有兩種選擇:FlywayLiquibase,在功能上,Liquibase 要強大一些,因此,推薦使用它。使用 Spring Initializr 創建 Spring Boot 工程時,默認支持這兩個工具的導入。

總結

以上便是本次 Chat 的主要內容,如果你有更多好的實踐歡迎留言交流。

參考文檔


歡迎關注我的公衆號,回覆關鍵字“大禮包” ,將會有大禮相送!!! 祝各位面試成功!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章