搭建 Spring Boot 多模块工程(通过 Spring Initializr)
什么是多模块项目
多模块项目是项目构建中的概念。拿 Maven 来说,多模块项目(Multi-Module Project)是其一个重要特性,它允许我们在一个项目中管理多个子模块。
在一个 Maven 多模块项目中,每个模块都是一个独立的项目,拥有自己的 POM 文件(Project Object Model,项目对象模型)。这些模块可以互相依赖,也可以被其他项目依赖。但是,所有的模块都会被统一管理,它们共享同一套构建系统和依赖管理。
为什么要使用多模块项目
主要有以下几个原因:
- 代码组织:在大型项目中,我们经常需要把代码分成多个模块,以便更好地组织代码。每个模块可以聚焦于一个特定的功能或领域,这样可以提高代码的可读性和可维护性。
- 依赖管理:Maven 多模块项目可以帮助我们更好地管理项目的依赖。在父项目的 POM 文件中,我们可以定义所有模块共享的依赖,这样可以避免重复的依赖定义,也方便我们管理和升级依赖。
- 构建和部署:Maven 多模块项目的另一个优点是它可以统一管理项目的构建和部署。我们只需要在父项目中执行 Maven 命令,就可以对所有模块进行构建和部署。这大大简化了开发者的工作。
IDEA 搭建 Spring Boot 多模块工程骨架
构建父项目 - weblog
没有找到JDK8?
服务器url替换成aliyun,否则无法使用jdk8
项目初始化目录,删除无用的文件
创建 web 访问模块
这是项目的入口,maven 打包的打包插件在这里存放,同时,博客的前端相关内容也统一存放在这里。
勾选依赖 Lombok 和 Spring Web
创建后台管理模块
该模块负责统一放置和管理后台相关的功能
依赖仅用 Lombok,后续再增加新的模块
创建通用模块
该模块存放一些通用的功能,比如接口的日志切面,全局异常管理
pom 文件解析
weblog
在父项目的 pom.xml 文件中对项目进行统一管理
- 项目版本号,主要是涉及项目自身的开发版本,JDK 版本,项目编码
- Maven 相关内容,依赖的是 JDK 版本
- 项目依赖包的版本号
<properties> <revision>0.0.1-SNAPSHOT</revision> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.target>${java.version}</maven.compiler.target>
<lombok.version>1.18.28</lombok.version> <guava.version>31.1-jre</guava.version> <commons-lang3.version>3.12.0</commons-lang3.version> <jackson.version>2.15.2</jackson.version>
</properties>
|
- 子模块的名称以及版本号
- 常用依赖工具的名称以及版本号
<dependencyManagement> <dependencies> <dependency> <groupId>com.sakurasep</groupId> <artifactId>weblog-admin</artifactId> <version>${revision}</version> </dependency>
<dependency> <groupId>com.sakurasep</groupId> <artifactId>weblog-common</artifactId> <version>${revision}</version> </dependency>
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>${guava.version}</version> </dependency>
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>${commons-lang3.version}</version> </dependency>
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> </dependencies> </dependencyManagement>
|
项目的构建设置
这里添加了spring-boot-maven-plugin
<build> <pluginManagement> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </pluginManagement> </build>
|
weblog-web
<?xml version="1.0" encoding="UTF-8"?> <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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.sakurasep</groupId> <artifactId>weblog-springboot</artifactId> <version>${revision}</version> </parent>
<groupId>com.sakurasep</groupId> <artifactId>weblog-web</artifactId> <name>weblog-web</name> <description>weblog-web (入口项目,负责博客前台展示相关功能,打包也放在这个模块负责)</description>
<dependencies> <dependency> <groupId>com.sakurasep</groupId> <artifactId>weblog-common</artifactId> </dependency>
<dependency> <groupId>com.sakurasep</groupId> <artifactId>weblog-admin</artifactId> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
|
weblog-admin
<?xml version="1.0" encoding="UTF-8"?> <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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.sakurasep</groupId> <artifactId>weblog-springboot</artifactId> <version>${revision}</version> </parent>
<groupId>com.sakurasep</groupId> <artifactId>weblog-admin</artifactId> <name>weblog-admin</name> <description>weblog-admin (负责管理后台相关功能)</description>
<dependencies> <dependency> <groupId>com.sakurasep</groupId> <artifactId>weblog-common</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
</project>
|
weblog-common
<?xml version="1.0" encoding="UTF-8"?> <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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.sakurasep</groupId> <artifactId>weblog-springboot</artifactId> <version>${revision}</version> </parent>
<groupId>com.sakurasep</groupId> <artifactId>weblog-common</artifactId> <name>weblog-common</name> <description>weblog-module-common (此模块用于存放一些通用的功能)</description>
<dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> </dependencies>
</project>
|
测试项目
执行 mvn clean
执行 mvn package
运行该 Spring Boot 项目,在 weblog-web 找到 WeblogWebApplication 启动类
运行在 8080 端口
Spring Boot 多环境配置
多环境配置文件
在 weblog-web 添加多环境配置文件
application.yml 用于默认配置
application-dev.yml 用于开发环境
application-test.yml 用于测试环境
application-prod.yml 用于生产环境
相比于 properties 格式的配置文件,yaml 格式的显然可阅读性更好
在默认配置文件中激活 dev 环境
启动项目测试,出现以下标注语句表示激活成功
配置 Lombok
Lombok 的优点
- 简化 Getter 和 Setter 方法: 在传统的 Java 开发中,你经常需要为每个类的属性手动编写 Getter 和 Setter 方法,但是有了 Lombok,你只需要在属性上加上
@Getter
和@Setter
注解,Lombok 就会为你自动生成这些方法。
- 自动生成构造函数: 通过`@NoArgsConstructor %}、
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.28</version> <scope>provided</scope> </dependency>
|
自定义 logback 配置
logback的优点
- 性能:Logback 在性能上超越了许多其他的日志实现,尤其是在高并发环境下。
- 灵活性:Logback 提供了高度灵活的日志配置方式,支持从 XML、Groovy 以及编程式的方式进行配置。
- 功能丰富:除了基本的日志功能,Logback 还提供了如日志归档、日志级别动态修改、事件监听等高级功能。
- 与 SLF4J 集成:SLF4J 是一个日志门面(facade),使得应用程序可以在运行时更换日志实现。Logback 作为 SLF4J 的一个原生实现,可以无缝地与其集成。
- 与 Spring Boot 的自动配置集成:Spring Boot 提供了对 Logback 的自动配置,这意味着开发者无需手动配置 Logback,只需提供一个简单的配置文件即可。
添加logback
添加依赖
spring boot 默认使用 logback 为默认的日志系统,只需要添加 spring-boot-starter-web 依赖就会自动包含 logback 相关依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
|
配置 logback
在 weblog-web 的配置文件夹中添加 logback-weblog.xml
<?xml version="1.0" encoding="UTF-8"?> <configuration > <jmxConfigurator/> <include resource="org/springframework/boot/logging/logback/defaults.xml" />
<property scope="context" name="appName" value="weblog" /> <property name="LOG_FILE" value="/app/weblog/logs/${appName}.%d{yyyy-MM-dd}"/> <property name="FILE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${LOG_FILE}-%i.log</FileNamePattern> <MaxHistory>30</MaxHistory> <TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>10MB</maxFileSize> </TimeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${FILE_LOG_PATTERN}</pattern> </encoder> </appender>
<springProfile name="dev"> <include resource="org/springframework/boot/logging/logback/console-appender.xml" /> <root level="info"> <appender-ref ref="CONSOLE" /> </root> </springProfile>
<springProfile name="prod"> <include resource="org/springframework/boot/logging/logback/console-appender.xml" /> <root level="INFO"> <appender-ref ref="FILE" /> </root> </springProfile> </configuration>
|
在生产环境的 application-prod.yml 中添加 log 输出
在单元测试包下的测试类中加入以下代码
package com.sakurasep.weblogweb;
import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest @Slf4j class WeblogWebApplicationTests {
@Test void contextLoads() { }
@Test void testLog() { log.info("这是一行 Info 级别日志"); log.warn("这是一行 Warn 级别日志"); log.error("这是一行 Error 级别日志");
String author = "上杉九月"; log.info("这是一行带有占位符日志,作者:{}", author); }
}
|
运行该测试方法
激活生产环境看是否输出到文件
修改 logback-weblog.xml 中的输出路径
测试输出
自定义注解,实现 API 请求日志切面
什么是自定义注解 (Custom Annotations)?
Java 注解是从 Java 5 开始引入的,它为我们提供了一种元编程的方法,允许我们在不改变代码逻辑的情况下为代码添加元数据。这些元数据可以在编译时或运行时通过反射被访问。
自定义注解就是用户定义的,用于为代码提供元数据的注解。例如,本小节中自定义的@ApiOperationLog
注解,它用来表示一个方法在执行时需要被记录日志。
什么是 AOP (面向切面编程)?
AOP(Aspect-Oriented Programming,面向切面编程)是一个编程范式,它提供了一种能力,让开发者能够模块化跨多个对象的横切关注点(例如日志、事务管理、安全等)。
主要概念包括:
- 切点 (Pointcuts): 定义在哪里应用切面(即在哪里插入横切关注点的代码)。
- 通知 (Advices): 定义在特定切点上要执行的代码。常见的通知类型有:前置通知、后置通知、环绕通知等。
- 切面 (Aspects): 切面将切点和通知结合起来,定义了在何处和何时应用特定的逻辑。
例如,使用AOP,我们可以为所有使用@ApiOperationLog
注解的方法自动添加日志逻辑,而不需要在每个方法中手动添加。
添加依赖
在父项目的 pom.xml 添加配置
<properties> <jackson.version>2.15.2</jackson.version> </properties>
<dependencyManagement> <dependencies> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency>
</dependencies> </dependencyManagement>
|
因为日志切面属于前后端通用的功能,所以在 weblog-common 中也要引用依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency>
|
实现自定义注解
在 weblog-common 新建一个 aspect 包
package com.sakurasep.weblogcommon.aspect;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented public @interface ApiOperationLog {
String description() default "";
}
|
创建 JSON 工具类
在 weblog-common 模块下创建 utils 包,统一放置工具类,新建 JsonUtil 工具类
package com.sakurasep.weblogcommon.utils;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j;
@Slf4j public class JsonUtil {
private static final ObjectMapper INSTANCE = new ObjectMapper();
public static String toJsonString(Object obj) { try { return INSTANCE.writeValueAsString(obj); } catch (JsonProcessingException e) { return obj.toString(); } } }
|
定义日志切面类
在 aspect 包中新建切面类 ApiOperationLogAspect
package com.sakurasep.weblogcommon.aspect;
import com.sakurasep.weblogcommon.utils.JsonUtil; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.MDC; import org.springframework.stereotype.Component;
import java.lang.reflect.Method; import java.util.Arrays; import java.util.UUID; import java.util.function.Function; import java.util.stream.Collectors;
@Aspect @Component @Slf4j public class ApiOperationLogAspect {
@Pointcut("@annotation(com.sakurasep.weblogcommon.aspect.ApiOperationLog)") public void apiOperationLog() {}
@Around("apiOperationLog()") public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { try { long startTime = System.currentTimeMillis();
MDC.put("traceId", UUID.randomUUID().toString());
String className = joinPoint.getTarget().getClass().getSimpleName(); String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs(); String argsJsonStr = Arrays.stream(args).map(toJsonStr()).collect(Collectors.joining(", "));
String description = getApiOperationLogDescription(joinPoint);
log.info("====== 请求开始: [{}], 入参: {}, 请求类: {}, 请求方法: {} =================================== ", description, argsJsonStr, className, methodName);
Object result = joinPoint.proceed();
long executionTime = System.currentTimeMillis() - startTime;
log.info("====== 请求结束: [{}], 耗时: {}ms, 出参: {} =================================== ", description, executionTime, JsonUtil.toJsonString(result));
return result; } finally { MDC.clear(); } }
private String getApiOperationLogDescription(ProceedingJoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
ApiOperationLog apiOperationLog = method.getAnnotation(ApiOperationLog.class);
return apiOperationLog.description(); }
private Function<Object, String> toJsonStr() { return arg -> JsonUtil.toJsonString(arg); }
}
|
添加包扫描
在启动类中添加 @ComponentScan,扫描 com.sakurasep 包下的所有类
package com.sakurasep.weblogweb;
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication @ComponentScan({"com.sakurasep.*"}) public class WeblogWebApplication {
public static void main(String[] args) { SpringApplication.run(WeblogWebApplication.class, args); }
}
|
新增测试接口
在 weblog-web 模块中
新建接口
创建 controller 包用于存放统一接口,创建 model 包用于放置 pojo 对象
创建 User 类
package com.sakurasep.weblogweb.model;
import lombok.Data;
@Data public class User { private String username; private Integer sex;
}
|
创建 TestController 类,路径为 /test
package com.sakurasep.weblogweb.controller;
import com.sakurasep.weblogcommon.aspect.ApiOperationLog; import com.sakurasep.weblogweb.model.User; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController;
@RestController @Slf4j public class TestController { @PostMapping("/test") @ApiOperationLog(description = "测试接口") public User test(@RequestBody User user){ return user; } }
|
测试 Test 接口
使用 postman 以 json 的格式请求 /test
请求参数
出参
控制台输出日志