作者 | 劉濤
審校 | 重樓
隨著軟件應用程序復雜程度的不斷提升,對可擴展性、模塊化以及清晰度的管理變得極為關鍵。
Spring Boot的多模塊結構能夠讓你對應用程序的不同部分進行獨立管理,如此一來,你的團隊能夠分別開展組件的開發、測試以及部署工作。這種結構使得代碼維持著井然有序的狀態,并具備模塊化特性,不管是對于微服務,還是大型整體式系統,均頗具實用價值。
在本教程當中,你將會構建一個多模塊的Spring Boot項目,每個模塊都專門負責特定的職責。你將學習如何設置模塊、配置模塊間的通信、處理錯誤、施行基于JWT(JSON Web Token:是一種開放標準(RFC 7519),用于在認證服務器(簽發者)、客戶端(如瀏覽器或移動應用)和資源服務器(接收者)之間安全地傳輸包含用戶等實體相關信息(通過頭部、載荷和簽名組成)的一種數據格式,以方便對資源訪問進行驗證。)的安全性策略以及使用Docker進行部署。
必備知識
- 具備Spring Boot和Maven的基礎知識
- 熟悉Docker和CI/CD概念(可選但有幫助)
目錄
1.為何采用多模塊項目?
2.項目結構與架構
3.如何設置父項目
4.如何創建模塊
5.模塊間通信
6.常見陷阱及解決方案
7.測試策略及配置
8.錯誤處理與日志記錄
9.安全與JWT集成
10.使用Docker和CI/CD進行部署
11.最佳實踐與高級用例
12.結論與關鍵要點
1.為何采用多模塊項目?
在單模塊項目中,各個組件(指代碼中的功能單元,像數據庫訪問層組件、業務邏輯處理組件、用戶界面展示組件等)之間往往存在緊密的耦合關系,這種設計使得編程人員在擴展項目規模和管理復雜代碼庫時面臨諸多挑戰。然而,相比之下,采用多模塊結構則能夠帶來一系列顯著的優勢:
- 模塊化:每個模塊都聚焦于特定任務,如用戶管理或庫存管理,從而簡化了管理流程和故障排除工作。
- 團隊可擴展性:團隊可以在不同的模塊上各自獨立開展工作,此舉能夠最大限度地減少沖突的發生,提高生產效率。
- 靈活部署:模塊可以獨立部署或更新,這對于微服務或具有眾多功能的大型應用程序而言特別有益。
現實案例
以某個大型電子商務應用程序為例,其架構可以清晰地劃分為以下幾個模塊:
- 客戶管理模塊:主要負責處理客戶資料、偏好設置以及身份認證等功能。
- 產品管理模塊:專注于管理產品的詳細信息、庫存量以及定價策略。
- 訂單處理模塊:負責管理訂單信息、支付流程以及訂單追蹤等功能。
- 庫存管理模塊:負責監控庫存水平,并處理與供應商之間的訂單事宜。
案例學習:網飛(Netflix)
為了闡明這些優勢,讓我們來研究一下網飛(Netflix:全球最大的流媒體視頻服務平臺之一)是如何采用多模塊架構的。
網飛是有效運用此種方式(多模塊架構)并通過微服務架構實現目標的領先典范。網飛的每個微服務均專注于特定的功能,例如用戶認證、內容推薦或者流媒體服務。
多模塊化結構使網飛能夠高效地擴展其業務運營、獨立部署更新,并保持高可用性和高性能。通過解耦服務的架構設計,網飛構建了一個強大而靈活的系統,能夠同時服務全球數以百萬計的用戶,確保內容的無縫傳輸,有效支撐起這個規模龐大且不斷動態發展的平臺。
這種架構設計不僅提升了系統的可擴展能力,同時加強了故障隔離機制,使網飛能夠快速進行創新并及時響應用戶需求。
2.項目結構與架構
現在讓我們回歸到示例項目中,你的多模塊Spring Boot項目將會使用5個關鍵模塊,其布局情況如下:
codespring-boot-multi-module/
├── common/ # Shared utilities and constants
├── domain/ # Domain entities
├── repository/ # Data access layer (DAL)
├── service/ # Business logic
└── web/ # Main Spring Boot application and controllers
每個模塊均具有其特定的功用:
- Common(通用模塊):存放跨模塊共享的工具類、常量和配置文件。
- Domain(域模塊):包含應用程序的數據模型定義。
- Repository(倉儲模塊):負責數據庫訪問和操作。
- Service(服務模塊):實現業務邏輯封裝。
- Web(網絡模塊):定義REST API接口,作為應用程序的入口層。
這種結構遵循了關注點分離原則(Separation of Concerns:軟件工程的一個基本原則,將程序分解為互不重疊的功能模塊,使每個模塊專注于解決特定的問題領域),其中每一層都保持獨立并處理自身特定的邏輯。
下方的圖表展示了各個模塊:
3.如何設置父項目
步驟1:創建根項目
讓我們運行以下命令來創建Maven父項目:
mvn archetype:generate -DgroupId=com.example -DartifactId=spring-boot-multi-module -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false cd spring-boot-multi-module
步驟2:配置父項目的pom.xml文件
在pom.xml文件中,定義依賴項和模塊:
<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://www.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>spring-boot-multi-module</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>common</module>
<module>domain</module>
<module>repository</module>
<module>service</module>
<module>web</module>
</modules>
<properties>
<java.version>11</java.version>
<spring.boot.version>2.5.4</spring.boot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
該pom.xml文件對依賴項和配置進行集中管理,從而讓跨模塊共享設置的管理變得更為便捷。
4.如何創建模塊
通用模塊
創建一個通用模塊以定義共享的工具,例如日期格式化器。創建此模塊并添加一個示例工具類:
mvn archetype:generate -DgroupId=com.example.common -DartifactId=common -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
日期格式化工具:
package com.example.common;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class DateUtils {
public static String formatDate(LocalDate date) {
return date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
}
}
域模塊
在域模塊中定義數據模型:
package com.example.domain;
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class User {
@Id
private Long id;
private String name;
// Getters and Setters
}
倉儲模塊(Repository)
創建倉儲模塊以管理數據訪問。以下是一個基本的存儲庫接口:
package com.example.repository;
import com.example.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {}
服務模塊(Service)
創建服務模塊以涵蓋業務邏輯。以下是一個服務類的示例:
package com.example.service;
import com.example.domain.User;
import com.example.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
}
網絡模塊(Web)
網絡模塊作為REST API層。
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users/{id}")
public User getUserById(@PathVariable Long id) {
return userService.getUserById(id);
}
}
5.模塊間通信
為了消除模塊間的直接依賴,我們可以借助REST API(表述性狀態轉移應用程序接口)或消息代理(例如Kafka)來實現模塊間的通信。這種做法確保了模塊間的松耦合性,使得每個模塊都能夠獨立地進行通信交流。
下圖清晰地展示了模塊間是如何相互通信的:
該圖呈現出不同系統組件如何協同運作以高效處理請求。
Web模塊負責處理傳入的 API 請求,并將其轉發給涵蓋業務邏輯的服務模塊。其后,服務模塊與倉儲模塊進行交互,以從數據庫中獲取或者更新數據。這種分層方法確保了每個模塊都能夠獨立運行,進而提升了系統的靈活性與維護的便捷性。
以使用Feign客戶端(Feign Clients :Spring Cloud 提供的一個聲明式的 HTTP 客戶端工具,用于簡化微服務間的調用)為例:
在模塊間通信的場景中,使用Feign客戶端等工具是達成服務間松耦合(loose coupling:一種軟件架構設計原則,目標是減少系統組件之間的相互依賴)的有效方式。
Feign客戶端允許一個模塊通過REST API調用無縫地與另一個模塊進行通信,而無需建立直接依賴。這種方法與前面所描述的分層架構高度契合,其中服務模塊能夠使用Feign客戶端從其他服務或微服務中獲取數據,而非直接訪問數據庫或者硬編碼 HTTP 請求。
該方式不僅簡化了代碼,還通過隔離服務依賴增強了系統的可擴展性與可維護性。
@FeignClient(name = "userServiceClient", url = "http://localhost:8081")
public interface UserServiceClient {
@GetMapping("/users/{id}")
User getUserById(@PathVariable("id") Long id);
}
6.常見陷阱及解決方案
在實施多模塊架構時,你可能會遇到一些挑戰。以下是一些常見的陷阱及其解決方案:
- 循環依賴:模塊之間可能會不經意地相互依賴,形成循環依賴的情況,從而增加構建和部署的復雜程度。解決方案:精心設計模塊接口,并使用依賴管理工具在開發的早期階段檢測并解決循環依賴問題。
- 過度工程化:存在創建過多模塊的風險,導致不必要的復雜性產生。解決方案:從最小的一組模塊開始,只有在有明確需求時才進一步進行拆分,確保每個模塊都有清晰明確的職責。
- 配置不一致:管理多個模塊的配置可能會導致不一致性。解決方案:使用集中式配置管理工具(如Spring Cloud Config)來保持模塊間配置的一致性。
- 通信開銷:模塊間通信可能會引入延遲和復雜性。解決方案:通過使用高效的協議來優化通信,并在適當的情況下考慮使用異步消息傳遞來減少延遲。
- 測試復雜性:由于模塊間的交互,多模塊項目的測試可能會更加復雜。
解決方案:推行一個穩健的測試策略,涵蓋針對單個模塊的單元測試以及針對模塊間交互的集成測試。
通過了解這些陷阱并應用這些解決方案,你可以有效地管理多模塊架構的復雜性,并確保開發過程的順利進行。
7.測試策略及配置
在多模塊的設置當中,獨立地測試每個模塊,并將其當作一個單元來進行測試,這一點至關重要。
單元測試
在這里,我們將使用JUnit和Mockito(Java中兩個常見測試工具)來執行單元測試:
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
public void testGetUserById() {
User user = new User();
user.setId(1L);
user.setName("John");
Mockito.when(userRepository.findById(1L)).thenReturn(Optional.of(user));
User result = userService.getUserById(1L);
assertEquals("John", result.getName());
}
}
集成測試
我們將使用帶有內存數據庫的Testcontainers(一個支持 Java 測試的庫,它提供了輕量級的、一次性的容器實例支持。它讓測試人員在測試中使用真實的數據庫、消息隊列等服務,而不是模擬這些服務)進行集成測試:
@Testcontainers
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class UserServiceIntegrationTest {
@Container
private static PostgreSQLContainer<?> postgresqlContainer = new PostgreSQLContainer<>("postgres:latest");
@Autowired
private UserService userService;
@Test
public void testFindById() {
User user = userService.getUserById(1L);
assertNotNull(user);
}
}
8.錯誤處理與日志記錄
錯誤處理和日志記錄是確保應用程序可靠運行且具備可調試性的重要手段。
錯誤處理
在本節中,我們將探討如何使用全局異常處理器在Spring Boot應用程序中優雅地處理錯誤。通過使用@ControllerAdvice注解,我們將建立一種集中捕獲和響應錯誤的方式,以保持代碼的整潔和響應的一致性。
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<String> handleUserNotFoundException(UserNotFoundException ex) {
return new ResponseEntity<>("User not found", HttpStatus.NOT_FOUND);
}
}
在上面的代碼示例中,我們定義了一個GlobalExceptionHandler,用于捕獲任何UserNotFoundException異常,并返回一個友好的消息,如“用戶未找到”,同時附帶404狀態碼。這樣,你就不需要在每個控制器中都處理這個異常了——只需在一個地方處理即可!
現在,讓我們來看一下這個流程圖。整個流程是這樣的:當客戶端向Web模塊發送請求時,如果一切順利,你將會收到一個成功的響應。但是,如果出現問題,比如系統未能找到指定的用戶,那么這個錯誤就會被全局異常處理器捕獲。這個處理器會記錄問題詳情,并向客戶端返回一個清晰、結構化的響應。
這種方法確保了用戶能夠收到明確的錯誤信息,同時保持了應用程序內部的安全性和隱蔽性。
日志記錄
在每個模塊中進行結構化日志記錄可以提高可追蹤性和調試效率。你可以使用像Logback這樣的集中式日志記錄系統,并包含用于追蹤請求的相關ID。
9.安全與JWT集成
在本節中,我們將詳細介紹如何配置JSON Web Tokens (JWT) 以增強應用程序的安全性。通過JWT,我們能夠保護各個終端節點,并根據用戶的角色來控制他們對應用程序不同部分的訪問權限。為了實現這一目標,我們將在SecurityConfig類中進行配置,該類將負責定義誰有權訪問應用程序中的哪些資源。
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
.and()
.oauth2ResourceServer().jwt();
}
}
在以上的代碼示例中,你可以看到我們如何定義訪問規則:
- /admin/** 終端節點僅限于具有ADMIN(管理員)角色的用戶訪問。
- /user/** 終端節點可以由具有USER或ADMIN角色的用戶訪問。
- 任何其他請求都需要用戶進行身份驗證。
接下來,我們要在應用程序中進行相關配置,具體而言,就是使用.oauth2ResourceServer().jwt()這種方式來驗證傳入的令牌。這樣做的目的在于,只有經過驗證為有效的令牌所對應的請求,才能夠被允許訪問我們受保護終端節點,從而確保了系統的安全性和資源訪問的合法性。
現在,讓我們通過圖示來了解一下流程。當客戶端發送請求以訪問資源時,安全過濾器會首先檢查所提供的JWT令牌是否有效。如果令牌有效,訪問請求將繼續傳遞到服務模塊以獲取或處理數據。如果無效,則訪問立即被拒絕,并且客戶端會收到一個異常響應。
這一流程確保了只有通過身份驗證的用戶才能訪問敏感資源,從而保證了應用程序的安全性。
10.使用Docker和CI/CD進行部署
在本節中,我們會使用 Docker 將每個模塊進行容器化處理。這樣做的目的是讓我們的應用程序能夠更便捷地在不同環境中進行部署并保持一致運行。同時,我們還將使用GitHub Actions來設置持續集成/持續部署(CI/CD)管道。當然,如果你更傾向于使用Jenkins,也可以選擇它。自動化該過程,可以確保你推送的任何更改都能自動進行構建、測試和部署。
第1步:使用Docker進行容器化處理
我們首先為Web模塊創建一個Dockerfile:
FROM openjdk:11-jre-slim
COPY target/web-1.0-SNAPSHOT.jar app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
在這里,我們使用Java 11的輕量級版本來保持較小的圖像尺寸。然后將編譯后的.jar文件復制到容器中,并將其設置為在容器啟動時運行。
第2步:使用Docker Compose進行多模塊部署
接下來,我們使用Docker Compose文件將多個模塊編排在一起:
version: '3'
services:
web:
build: ./web
ports:
- "8080:8080"
service:
build: ./service
ports:
- "8081:8081"
通過這種配置,我們能夠同時運行Web模塊和服務模塊,從而只需執行一個單獨的命令就可輕松啟動整個應用程序。每個服務都是在其對應的獨立目錄中構建的,并且我們為訪問這些服務公開了所需的端口。
使用GitHub Actions 的CI/CD示例
name: CI Pipeline
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v2
with:
java-version: '11'
- name: Build with Maven
run: mvn clean install
每當你向代碼倉庫推送新代碼或提交創建拉取請求時,該管道就會自動觸發啟動。它會執行一系列操作,首先簽出你提交的代碼,接著配置好所需的Java開發環境,然后運行Maven構建流程,從而確保整個項目能夠正常運行。
11.最佳實踐與高級用例
以下最佳實踐確保了代碼的可維護性和可擴展性。
最佳實踐
- 避免循環依賴:在設計和構建應用程序的各個模塊時,須確保模塊之間沒有循環引用,以避免構建問題(例如編譯錯誤、無限循環的依賴解析等)。
- 明確職責分離:在模塊化設計中,每個模塊應當被賦予并專注于單一的、明確的職責。
- 集中配置:集中管理配置信息,提高系統的可管理性和一致性,確保整個系統在不同環境下都能保持一致的行為和表現。
高級用例 - 使用Kafka進行異步消息傳遞:Kafka作為分布式消息平臺,實現了服務之間的解耦通信,并允許模塊可以異步地發布和訂閱事件。
- 使用Feign作為REST客戶端:使用Feign在模塊內部調用服務。定義一個Feign客戶端接口用于通信。
- 性能緩存:在服務模塊中使用Spring Cache來優化數據檢索。
12.結論與關鍵要點
多模塊Spring Boot項目具有諸多優勢,它提供了模塊化、可擴展性和易于維護的特點。
在本教程中,你學習了如何設置模塊、管理模塊間通信、處理異常情況、增強系統安全性,并利用Docker技術實現部署。
遵循最佳實踐,結合消息傳遞和緩存等高級技術手段,將進一步優化你的多模塊架構,確保其更加穩健、高效,完美適配生產環境需求。
譯者介紹
劉濤,51CTO社區編輯,某大型央企系統上線檢測管控負責人。
原文標題:How to Build Multi-Module Projects in Spring Boot for Scalable Microservices,作者:Birks Sachdev