ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot多模块工程搭建:IDEA新建Module全流程详解

Spring Boot多模块工程搭建:IDEA新建Module全流程详解 学Spring的第一课很多人以为是IoC、是AOP、是Bean的生命周期但以我这些年带新人的经验来看第一个卡住大家的往往是更基础的东西——怎么在IDE里把一个Spring工程的结构搭对。就拿“新建module模块”这件事来说看起来就是右键点几下实际上里面藏着父子工程、Maven聚合、依赖管理、启动类位置这一整串知识点。这篇文章我就从“新建module”这个小切口进去把多模块Spring工程的搭建逻辑、实操步骤和常见坑一次讲清楚尤其适合刚接触Spring Boot、第一次用IntelliJ IDEA建多模块工程的同学。1. 学Spring第一步先把module模块的结构搞明白1.1 为什么学Spring要先学会建module直接建一个单模块的Spring Boot工程几分钟就能跑起来那为什么还要学多模块因为真实项目几乎都是多模块的而且Spring框架本身的学习也需要你理解模块化。举个例子你想研究Spring的IoC容器想看BeanFactory是怎么加载配置的那你就得能拉下来Spring源码工程源码工程就是典型的多模块结构——spring-core、spring-beans、spring-context、spring-aop这些模块各司其职你只有理解了模块之间的依赖关系才知道该从哪个模块看起。再往后你接触Spring Cloud微服务一个服务就是一个独立模块服务之间通过注册中心互相发现如果你连module的概念都没建立起来后面全是麻烦。我见过不少同学跟着教程建了一个单一工程代码全堆在一个模块里UserController和UserService还有各种配置类全在同一个包下。这样写学习demo没问题但一旦工程大了依赖关系乱成一团改一处牵连一片你根本不知道哪个类被谁引用着。所以“新建module”这个动作看起来是IDE操作实际上是让你建立一种工程化的思维方式把一个大的系统按职责拆成若干小块每块独立编译、独立测试、独立演化再通过明确的依赖关系组合起来。1.2 模块化到底解决了什么问题模块化解决的核心问题有三个。第一是依赖清晰。单模块工程里所有类都在一个classpath下互相可见你觉得能用就直接new一下时间久了根本理不清谁依赖谁。多模块工程里模块A要使用模块B的类必须在pom里显式声明依赖IDE和编译器都会帮你把关依赖关系一目了然。第二是编译和测试的效率。改了一个模块的代码只需要重新编译这个模块而不是整个工程。我维护过一个老系统单模块几万个类每次改一行代码编译能等十几秒跑一次测试要几分钟。后来拆成模块改哪个模块就编哪个模块效率提升非常明显。第三是职责边界。用生活中的例子类比一个家庭装修水电工、木工、油漆工各管各的。如果让一个人全包短期看着省事后面出了问题根本不知道找谁。模块化就是给代码“分工种”每个人只对自己负责的部分负责。1.3 IDE里的Module和Maven里的Module不是一回事这个必须单独拎出来说因为很多新手就是在这里懵掉的。在IntelliJ IDEA里Module是一个工程组织单位每个Module对应一个独立的代码区域和构建单元你可以在一个Project下创建多个Module。而在Maven的世界里Module是聚合工程也就是父工程下的子工程通过modules标签定义。当你用IDEA创建一个Maven父工程然后在里面新建Module时这两层概念重合了IDEA里的Module同时也是Maven里的子工程。但如果你的Module不是通过Maven方式创建的那它就没有对应Maven模块的概念父子关系也就不成立。所以你在实操时新建Module一定要选择Maven类型或者Spring Initializr这样才能保证IDEA的Module和Maven的模块正确关联。很多人的报错“子模块不被父工程管理”或者“IDEA不识别module”多半是因为创建方式不对。2. 动手新建module之前先把环境捋顺2.1 版本选型JDK、Maven和Spring Boot的搭配新建模块这件事本身不难但环境不对会给你添一堆堵。先说版本组合这个最容易出问题的点。目前Spring Boot已经到3.x系列我推荐新学者直接用**Spring Boot 3.x JDK 17**的组合。Spring Boot 3基于Spring Framework 6底层用Jakarta EE命名空间很多老教程里的javax包名在新版本里根本编译不过去。如果你跟着旧教程学一上来就是一堆红线和编译错误很打击信心。当然如果你之后要维护的是公司里的老项目那就得用Spring Boot 2.x JDK 8。这个不冲突学习阶段用一个工作了再补另一个。我更建议学习和新项目都直接用3.x毕竟现在新项目已经很少从2.x起步了。Maven版本建议3.8以上3.6及以下版本对JDK 17的支持有坑编译时会报一些奇怪的错。IDEA版本的话2023.1以后的版本对Spring Initializr和Maven的支持都更完善别再守着老版本了。2.2 IDEA社区版用Spring Boot也没问题很多同学用的是IntelliJ IDEA Community版担心社区版不支持Spring Boot开发。这个可以不担心。社区版确实没有Ultimate版的Spring Assistant插件但你创建工程可以走两条路线。一条是到 start.spring.io 官网把工程下载下来再在IDEA里以Maven工程方式导入另一条是直接在IDEA里新建一个普通的Maven工程然后在pom.xml里手动加Spring Boot的依赖保存后IDEA会自动从中央仓库下载依赖效果是一样的。我用社区版给团队做过整套微服务的开发和调试Spring Boot、MyBatis、Redis、RabbitMQ这些都能正常开发和运行。区别只是少了一些Spring特有的代码提示和跳转但写起来完全没问题。2.3 统一的Maven配置多模块工程最怕“每个人本地的Maven配置都不一样”这样协同起来全是问题。至少两个配置必须统一。第一是本地仓库路径。建议在settings.xml里把localRepository指到一个固定的目录比如D:/maven-repo别用默认的~/.m2/repository否则每个人依赖都下到不同位置磁盘飘红排查问题也更麻烦。第二是镜像和中央仓库。国内开发建议配置阿里云镜像否则下载Spring Boot那几百兆的依赖默认的中央仓库速度会很感人。配置方法是在settings.xml的mirrors标签里加一段mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/central/url /mirror这个配置是所有新建module的前提因为Maven创建模块时会重新解析依赖如果下载慢或者下载失败Module就创建不完整。3. 完整实操用IDEA从零新建Spring Boot多模块工程3.1 第一步创建一个空的Maven父工程多模块工程是“先有父后有子”。我们用IDEA创建一个空的Maven父工程。打开IDEA选择 New Project左侧选择 MavenProject SDK 选你本机的JDK 17或以上版本然后填上GroupId和ArtifactId。GroupId通常写公司的域名倒序比如com.exampleArtifactId写项目名比如spring-study。注意一个关键点创建完之后直接把父工程里的src目录删掉。父工程在纯聚合场景下是不写代码的它只做两件事管理子模块的列表管理所有子模块的依赖版本。删掉src之后你就理解了这个约定——父工程就是一张“清单表”不是工作台。删完src之后父工程的pom.xml看起来是这样的?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdspring-study/artifactId version1.0-SNAPSHOT/version packagingpom/packaging modules modulespring-common/module modulespring-web/module /modules /project注意packaging标签的值必须是pom这是聚合工程和普通工程最根本的区别。如果不是pom后面新建子模块时Maven会一脸茫然。3.2 第二步在父工程下新建子module模块这一步是整篇文章的核心也是标题所指的“新建module模块”。在IDEA中右键父工程名选择 New再选 Module。这里会弹出新建模块的向导选择 Maven Archetype 或者直接 Maven 都可以。快速建议是如果要写Spring Boot应用用Spring Initializr如果是普通工具模块或者封装模块直接用Maven。以IDEA社区版为例没有Spring Initializr选项所以我一般这样操作选Maven然后手动改pom。你需要填三项Name模块名比如spring-common、spring-web小写中划线风格GroupId继承父工程的com.example就行ArtifactId模块名本身创建后IDEA会自动在父工程的pom.xml的modules里加上这个新模块你不需要手动写这个自动行为正好验证了父子关系已经建立了。3.3 第三步给子模块写pom.xml把Spring Boot依赖加进来这一步有几个细节很关键我展开说。假设我有一个spring-web模块要写一个Web接口pom.xml就得引入spring-boot-starter-web。?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdcom.example/groupId artifactIdspring-study/artifactId version1.0-SNAPSHOT/version /parent artifactIdspring-web/artifactId dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency /dependencies /project这里有一个核心知识点子模块pom里的parent标签指向父工程这样它就能继承父工程里的所有依赖管理信息。而Spring Boot的版本号为什么不用写因为父工程通过dependencyManagement统一管理。新版父工程pom应该长这样dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version3.2.5/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagementspring-boot-dependencies这个POM把Spring Boot所有依赖的版本都锁好了子模块里写依赖只需要写groupId和artifactId版本号全由父工程统一给。这就是为什么你经常看别人写的pom子模块里看不到版本号——版本号统一在父工程里管理好处是升级依赖版本时只改父工程一处就行不会出现子模块各用各的版本导致冲突。3.4 第四步写启动类和第一个接口子模块的代码结构通常要有一个启动类放在包的根部比如com.example.web.WebApplication。package com.example.web; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication public class WebApplication { public static void main(String[] args) { SpringApplication.run(WebApplication.class, args); } }SpringBootApplication关键点在于它里面包含了ComponentScan扫描范围是当前包及子包。所以你的Controller、Service、Mapper这些Bean必须放在启动类所在包的子包下面一旦放错了包Spring容器扫描不到接口404Bean找不到这些问题排查起来特别耗费精力。写一个测试接口package com.example.web.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; RestController RequestMapping(/api) public class DemoController { GetMapping(/hello) public String hello() { return Hello Spring Module!; } }启动WebApplication浏览器访问localhost:8080/api/hello看到返回内容这个模块就活起来了。3.5 第五步让common模块被web模块引用多模块之间的依赖关系理解了会非常顺手。我现在建一个spring-common模块里面放一个通用工具类package com.example.common.util; public class StringUtils { public static boolean isEmpty(String str) { return str null || str.length() 0; } }然后在spring-web模块里使用它这时就需要在spring-web的pom里加依赖dependency groupIdcom.example/groupId artifactIdspring-common/artifactId version${project.version}/version /dependency这里用${project.version}引当前父工程版本号不需要写死。加了依赖之后spring-web里的Controller就能引StringUtils了package com.example.web.controller; import com.example.common.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; RestController RequestMapping(/api) public class DemoController { GetMapping(/check) public String check(String word) { return StringUtils.isEmpty(word) ? empty : word; } }实际开发中你会习惯把实体类、DTO、工具类放到common模块把Web层、业务层放到独立模块这个分层方式在后面做微服务时非常有用。4. 模块依赖、构建顺序与Maven命令的底层逻辑4.1 为什么多模块依赖要先install一个让新手非常困惑的问题是我在web模块里引用了common模块的类但IDEA显示找不到怎么解决这里不得不提Maven的构建顺序。多模块工程里Maven会按依赖关系自动决定构建顺序父工程先build然后是基础模块最后是依赖基础模块的上层模块。但如果你单独构建spring-web模块而不先去构建spring-commonMaven在本地仓库里找不到spring-common的jar包就会报无法解析依赖的错误。所以正确操作是先在父工程下执行mvn install把spring-common安装到本地仓库。之后web模块再构建时就能从本地仓库找到common的jar包了。实操上你在IDEA右侧的Maven面板里选中父工程执行clean和install一次后续所有模块构建就都顺畅了。4.2 父pom的dependencies和dependencyManagement有什么区别这是面试也常问的知识点我一次讲透。dependencies写在父工程里所有子模块都会无条件继承这些依赖就算子模块自己没用到也会被引入进来。dependencyManagement写在父工程里只做版本管理不强制引入依赖。子模块需要用到时自己声明依赖但可以不写版本号由父工程统一管理。实际工作中我通常的约定是依赖类型写在父工程哪里是否会传递到子模块所有模块都会用的基础依赖如lombokdependencies是部分模块才会用到的依赖如redis、mybatisdependencyManagement否具体模块的独有依赖子模块自行声明只对当前模块生效这个设计的好处是父工程成为一张“依赖清单表”你打开父pom就能看到整个项目用了哪些技术、各是什么版本而子模块里只关心自己真正需要的东西。4.3 多模块工程的构建生命周期一个多模块工程顶层执行命令时Maven会逐个子模块执行。比如在父工程下执行mvn clean效果是所有子模块都会执行clean。执行mvn install会按依赖顺序安装所有模块。执行mvn spring-boot:run则需要在spring-boot应用的子模块下执行父工程下没有这个插件绑定直接执行会报错。这里还要提醒一下给Spring Boot模块加spring-boot-maven-plugin时配置要写在子模块里不要写在父工程。因为父工程是pom打包方式不存在可执行jar插件绑定在父工程里没有意义。把插件写进父工程build里子模块再继承子模块会报找不到主类或没有可执行jar的错。下面是子模块pom里正确的插件配置build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build4.4 模块之间别搞循环依赖多模块工程最隐蔽的坑是模块间的循环依赖common依赖webweb又依赖commonMaven构建时会直接死循环或者报错。Maven本身不支持循环依赖一定要避免在设计阶段就规划清楚依赖方向。我们的约定是核心的基础模块永远只被上层模块依赖不去依赖上层模块上层模块可以依赖多个基础模块但不要互相交叉依赖。如果确实出现两个业务模块需要互相调用不通过Maven直接依赖互相引用而是把公共部分下沉到common模块或者通过接口解耦。5. 新手最容易踩的坑和排查思路实录5.1 IDEA不识别module模块怎么办明明在IDEA里创建了module但Maven面板里看不到或者module的目录没有变成识别的工程目录最常用的解决方法是右键不生效的pom.xml选择 Add as Maven Project。IDEA会自动识别并加入Maven管理。如果你右键压根没有这个选项先检查一下pom.xml是不是有效文件里面有没有语法错误再检查IDEA是不是把该文件排除了File - Project Structure - Modules 里查看。还有一种常见情况是创建module时选错了类型建了一个纯粹的IDE模块而不是Maven模块。这种错误没有快捷修复方式只能删了重新创建。5.2 启动类找不到或者404启动类找不到的报错很典型Error: Could not find or load main class。通常是因为你在父工程下执行了Spring Boot的run命令但父工程根本没有主类。解决办法是确认运行目标是子模块的启动类而不是父工程。接口404的问题90%是启动类的位置不对。比如启动类在com.example.webController在com.example.common.controllerSpring默认只扫描启动类所在包及其子包common包不在扫描范围内自然找不到接口。解决方案有两种一是把Controller放到com.example.web.controller下二是用ComponentScan显式指定扫描包范围但后者容易产生重复扫描问题不建议新手用。5.3 Maven依赖标红、版本冲突怎么办子模块引入依赖后如果依赖标红先右键项目选择 Maven - Reload Project。还不行再检查本地仓库里有没有对应jar包。依赖版本冲突的报错很常见比如你引入了两个模块它们各自传递依赖了不同版本的jacksonMaven的仲裁规则默认是选最近声明的那一层依赖。少部分情况会冲突到编译不过去需要手动在pom里用exclusions排除多余的传递依赖。排查思路是在IDEA的Maven面板里右键依赖选择 Show Diagram图形化展示依赖树一眼就能看到冲突的版本是从哪里传进来的。5.4 端口被占用Spring Boot默认端口是8080。你在跑多个模块的测试时第二个模块一启动就报Port 8080 was already in use。这个简单临时改端口在application.yml里server: port: 8081但如果多个服务需要同时启动更合理的方式是每个子模块用不同的端口并在配置里把服务名区分开。5.5 新建module时使用默认设置导致编译器版本不对这个问题特别隐蔽新手容易踩。新建module时IDEA会把Project SDK带到模块的Java编译器设置里如果你的全局SDK是JDK 17但Maven的编译插件配置的是JDK 8编译时就会报source option 8 is no longer supported之类的错误。解决方案是在父工程的pom里显式指定编译器版本properties java.version17/java.version maven.compiler.source17/maven.compiler.source maven.compiler.target17/maven.compiler.target /properties同时在IDEA的Project Structure里把模块的SDK和Language Level都调成一致。我先问你是不是感觉这些坑看着都不大但每一个都能让人卡一小时以上反正我当年是被它们轮番蹂躏过的。5.6 模块创建后发现目录结构不对创建module时如果选择Archetype不对生成的目录可能不是标准的src/main/java结构。比如选了maven-archetype-quickstart生成的是src/main/java加一个默认的App类你需要自己建src/main/resources目录来放配置文件选了maven-archetype-webapp目录里还会有src/main/webapp但Spring Boot不需要这个结构。我的建议是普通模块直接不选Archetype就选最基础的Maven模块目录自己建结构最干净Spring Boot应用模块用Spring Initializr自动生成标准的Spring Boot目录结构省去手动建目录的麻烦。6. 多模块工程后续还能这样扩展6.1 从学习demo到微服务工程当你熟悉了新建module的基本操作后面做微服务时其实就是复制这个套路一个父工程下每个微服务是一个module公共部分抽成common模块数据库实体抽成entity模块各个服务之间通过Feign或者HTTP进行调用。我最近在处理一个Spring Cloud Alibaba的项目结构就是demo-parent父工程管理所有版本demo-common通用工具、实体、统一返回demo-gateway网关模块demo-auth认证服务demo-business业务服务每个新服务加进来步骤跟上面完全一样右键父工程新建module在父pom的modules列表里自动出现子模块pom引入spring-cloud相关依赖服务之间需要调用时加Feign依赖最后在配置文件里注册到Nacos。你会发现费那么大劲搞明白“新建module”这件事后面都是这么一个个叠加出来的。6.2 模块的单元测试和独立运维多模块还有一个好处因为模块是独立编译的每个模块都可以有自己的测试目录你可以针对性跑某一个模块的测试而不是整个工程全量测试。比如改完common模块的工具类只想跑common模块的测试在IDEA的Maven面板里选中spring-common模块执行mvn test就行其他模块不会参与构建。这种精细化操作在工程规模变大以后特别受用。再有就是上线部署微服务场景下每个模块可以独立打包镜像、独立扩缩容不会因为改一个接口就把整个系统全部重新发布一遍。6.3 结合Spring源码学习的用法如果你是为了研究Spring源码才新建module我再给一个具体的建议不要一开始就拉全部源码工程那个工程量太大容易迷茫。更好的方式是自己建一个多模块工程模仿Spring框架拆几个模块出来比如my-core放IoC相关的模拟容器、my-beans放Bean定义和实例化逻辑、my-context放ApplicationContext模拟实现然后自己写代码去实现一次Bean的创建和注入。这样做的好处是Spring源码里对应的概念你在自己的demo里都见过一遍再去读Spring的源码会顺畅很多。很多大佬学习Spring源码都是这个路径先拆解再还原。我自己当年是怎么把这套吃透的呢其实就是在一次报错一次排查里磨出来的。有一次我把一个子模块的启动类写错了位置花了整整一晚上排查最后发现只是包路径的问题。从那之后我对模块的目录结构、包命名、启动类位置这些细节就格外敏感。新建module这个动作看起来不起眼但它就是Spring学习这条路上第一块多米诺骨牌摆正了它后面很多跟头都能避开。最后再分享一个我自己的小习惯每次新建完一个module我都会先跑一次完整的mvn clean install等所有模块都构建成功、依赖都稳定了再开始写代码。这个习惯帮我挡掉了一大批因为依赖没就绪引发的乱七八糟的问题。你也不妨试一下真的能省下很多时间。
返回列表