文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

探析在Spring Boot中为可扩展微服务构建多模块项目的方法

2024-11-28 13:58

关注

审校 | 重楼

随着软件应用程序复杂程度的不断提升,对可扩展性、模块化以及清晰度的管理变得极为关键。

Spring Boot的多模块结构能够让你对应用程序的不同部分进行独立管理,如此一来,的团队能够分别开展组件的开发、测试以及部署工作。这种结构使得代码维持着井然有序的状态,并具备模块化特性,不管是对于微服务,还是大型整体式系统,均颇具实用价值。

在本教程当中,将会构建一个多模块的Spring Boot项目,每个模块都专门负责特定的职责。学习如何设置模块、配置模块间的通信、处理错误、施行基于JWTJSON Web Token是一种开放标准(RFC 7519),用于在认证服务器(签发者)、客户端(如浏览器或移动应用)和资源服务器(接收者)之间安全地传输包含用户等实体相关信息(通过头部、载荷和签名组成)的一种数据格式,以方便对资源访问进行验证。的安全性策略以及使用Docker进行部署。

必备知识

目录

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

每个模块均具有其特定的功用:

这种结构遵循了关注点分离原则(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文件中,定义依赖项和模块:


    4.0.0
    com.example
    spring-boot-multi-module
    1.0-SNAPSHOT
    pom
    
        common
        domain
        repository
        service
        web
    
    
        11
        2.5.4
    
    
        
            
                org.springframework.boot
                spring-boot-dependencies
                ${spring.boot.version}
                pom
                import
            
        
    
    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        

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 {}

服务模块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.常见陷阱及解决方案

在实施多模块架构时,可能会遇到一些挑战。以下是一些常见的陷阱及其解决方案:

解决方案:推行一个稳健的测试策略,涵盖针对单个模块的单元测试以及针对模块间交互的集成测试。

通过了解这些陷阱并应用这些解决方案,可以有效地管理多模块架构的复杂性,并确保开发过程的顺利进行。

7.测试策略及配置

在多模块的设置当中,独立地测试每个模块,并将其当作一个单元来进行测试,这一点至关重要。

单元测试

在这里,我们将使用JUnit和MockitoJava中两个常见测试工具来执行单元测试:

@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 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();
 }
}

以上的代码示例中,可以看到我们如何定义访问规则:

现在,让我们通过图示来了解一下流程。当客户端发送请求以访问资源时,安全过滤器会首先检查所提供的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.最佳实践与高级用例

以下最佳实践确保代码的可维护性和可扩展性。

最佳实践

12.结论与关键要点

多模块Spring Boot项目具有诸多优势,它提供了模块化、可扩展性和易于维护的特点。

在本教程中,学习了如何设置模块、管理模块间通信、处理异常情况增强系统安全性,并利用Docker技术实现部署。

遵循最佳实践,结合消息传递和缓存等高级技术手段,将进一步优化的多模块架构,确保其更加稳健、高效,完美适配生产环境需求。

译者介绍

刘涛,51CTO社区编辑,某大型央企系统上线检测管控负责人。

原文How to Build Multi-Module Projects in Spring Boot for Scalable Microservices,作者:Birks Sachdev

来源:51CTO内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯