1. Spring Boot多模块项目中的依赖陷阱
在Spring Boot多模块项目中,我们经常会遇到一个令人头疼的问题:当基础模块被打包后,其他依赖它的模块在编译或运行时却报"Class Not Found"或"Package does not exist"错误。这个问题看似简单,实则隐藏着Spring Boot打包机制的一个重要特性。
1.1 spring-boot-maven-plugin的repackage机制
spring-boot-maven-plugin的核心功能之一就是repackage。当你在模块中配置了这个插件,它会在Maven的package阶段之后执行,将项目打包成一个可执行的fat jar。这个fat jar的特殊之处在于:
- 它将所有依赖的jar包解压后重新打包到BOOT-INF/lib目录下
- 将项目自身的类文件放在BOOT-INF/classes目录下
- 使用自定义的类加载器来加载这些资源
这种结构虽然让jar包可以直接通过java -jar运行,但却导致了一个严重问题:这个jar包无法作为依赖被其他项目正常引用。因为标准的Java类加载器无法识别BOOT-INF目录下的类文件。
1.2 问题重现场景
假设我们有一个典型的多模块项目结构:
parent-project ├── common-module (基础模块) ├── service-module (业务模块,依赖common-module) └── web-module (Web模块,依赖service-module)当common-module的pom.xml中配置了spring-boot-maven-plugin但没有特殊处理时:
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>执行mvn clean install后,service-module在编译时会报错,提示找不到common-module中的类。即使编译通过,运行时也会抛出ClassNotFoundException。
2. 双包策略:可执行jar与可依赖jar的解决方案
2.1 classifier属性的妙用
Maven的classifier属性允许我们为同一个artifact生成不同版本的输出。Spring Boot官方文档(84.5节)建议,对于既需要作为依赖又被其他模块引用的项目,可以使用classifier来区分两种打包结果。
具体配置如下:
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <classifier>exec</classifier> </configuration> </plugin> </plugins> </build>这样配置后,Maven会生成两个jar文件:
your-module-1.0.jar- 普通的可依赖jaryour-module-1.0-exec.jar- 可执行的fat jar
2.2 双包策略的工作原理
这种配置之所以能解决问题,是因为它改变了Maven的生命周期行为:
- 在package阶段,Maven首先生成标准的jar包(可依赖)
- 然后spring-boot-maven-plugin执行repackage目标,将标准jar作为源,生成带exec分类器的可执行jar
- 安装到本地仓库或部署到远程仓库时,两个jar都会被发布
- 其他模块引用时,默认使用不带分类器的标准jar
2.3 实际项目中的配置建议
在多模块项目中,我建议采用以下配置策略:
- 在父pom.xml中定义spring-boot-maven-plugin的基本配置,但不要启用repackage
<build> <pluginManagement> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring-boot.version}</version> </plugin> </plugins> </pluginManagement> </build>- 在需要生成可执行jar的模块(通常是启动模块)中显式配置exec分类器
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <classifier>exec</classifier> </configuration> </plugin> </plugins> </build>- 对于纯工具类模块(被其他模块依赖),完全不配置spring-boot-maven-plugin
3. 常见问题与进阶配置
3.1 JDK版本问题
在使用spring-boot-maven-plugin时,必须明确指定JDK版本,否则可能遇到Lambda表达式相关的编译错误。建议在pom.xml中添加:
<properties> <java.version>11</java.version> <maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.target>${java.version}</maven.compiler.target> </properties>3.2 BOOT-INF目录导致的资源访问问题
当你的代码需要访问jar包内的资源文件时,可能会因为BOOT-INF目录结构而失败。例如:
// 传统方式可能失效 InputStream is = getClass().getResourceAsStream("/config.properties"); // 需要改为 InputStream is = getClass().getResourceAsStream("/BOOT-INF/classes/config.properties");对于Spring Boot应用,更推荐使用ResourceLoader来加载资源:
@Autowired ResourceLoader resourceLoader; Resource resource = resourceLoader.getResource("classpath:config.properties");3.3 多模块项目中的依赖管理
对于复杂的多模块项目,建议在父pom中使用dependencyManagement统一管理依赖版本:
<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>然后在子模块中声明依赖时不需要指定版本:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>4. 最佳实践与经验分享
在实际项目中,我总结了以下几点经验:
模块划分原则:将可复用的工具类、DTO、公共配置等放在基础模块;将业务逻辑放在服务模块;将Web相关代码放在Web模块。
打包策略:
- 只有包含main方法的启动模块才需要配置exec分类器
- 基础模块不应该使用spring-boot-maven-plugin
- Web模块通常是生成可执行jar的最佳位置
调试技巧:
- 使用
mvn dependency:tree检查依赖关系 - 使用
mvn help:effective-pom查看最终生效的POM配置 - 在IDE中,确保正确识别了模块间的依赖关系
- 使用
持续集成:
- 在CI/CD流水线中,先单独构建基础模块
- 使用
-pl和-am参数控制构建顺序,例如:mvn clean install -pl common-module mvn clean install -pl web-module -am
版本管理:
- 使用CI流水线自动更新版本号
- 对于SNAPSHOT版本,频繁部署到本地仓库
- 对于正式版本,确保所有模块版本一致
通过合理配置spring-boot-maven-plugin的classifier属性,我们完美解决了多模块项目中的依赖问题。这种双包策略既保留了Spring Boot的便捷性,又不牺牲Maven多模块项目的灵活性。在实际开发中,根据项目复杂度选择合适的模块划分和打包策略,可以显著提高开发效率和项目可维护性。