ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Spring Boot多模块项目搭建全攻略:架构设计、打包部署与高频踩坑排查

Spring Boot多模块项目搭建全攻略:架构设计、打包部署与高频踩坑排查 Spring Boot 这个框架现在在 Java 后端基本已经成了事实标准。但很多人在看教程的时候会有一种感觉增删改查的示例一眼就能看懂一旦脱离 demo 进入真实项目立刻懵掉——模块怎么拆、架构怎么搭、依赖版本怎么管、踩到诡异问题怎么排查教程里根本不讲。这篇内容我不打算写那种“从零开始 Hello World”的重复内容而是围绕你真正会用到的三块架构设计思路、多模块项目搭建的完整过程以及几个高频进阶技巧一次性把关键的链路打通。无论你是刚接触 Spring Boot 的新手还是已经开始写业务但没系统梳理过项目结构的开发者这篇都可以帮你把散落的知识串起来。1. 架构设计先把项目结构想明白再动手有人说“先跑起来再说”这话没毛病但只对 demo 成立。真实项目一旦开始堆业务代码组织方式的优劣会被指数级放大。你前期抽出半小时想的架构能帮你后面少加好几天班。1.1 分层架构为什么是默认答案Spring Boot 项目最经典的分层是 controller / service / mapperdao往上再加一个 entity / dto / vo 的领域对象层。这套分层的本质不是“大家都这么写”而是把请求处理、业务逻辑、数据访问三个不同方向的职责隔离。controller 层只做参数接收、参数校验、结果包装不写业务service 层承载核心业务逻辑事务边界也基本落在这里mapper 层只处理 SQL 和数据库交互。我见过不少项目把业务逻辑一把梭写在 controller 里刚开始效率很高等接口变多、逻辑变复杂后改一个公共逻辑要动十几个接口那种酸爽谁写谁知道。需要特别提醒的是很多人会把 entity 直接抛给前端或者直接用 Map 接收参数。这个习惯在单体应用或内部系统里问题不大但一旦对外提供接口或者做微服务拆分字段暴露、数据结构不一致的问题会立刻冒出来。正确的做法是每一层职责隔离controller 用 VO 接收前端参数、返回前端需要的数据结构service 用 DTO 做业务流转mapper 操作的是 entity。虽然初期会多写几个类但换来的是后期完全可控的修改范围。1.2 单体、多模块、微服务到底怎么选“微服务”这个词这几年被炒得很热甚至有面试不问微服务就感觉不稳的说法。但放到实际项目里我的建议很直接业务没复杂到那个程度单体就够了。单体架构并不是“落后”的代名词。你有一个业务域完整、独立、没有大量并发拆分诉求单体应用维护成本最低开发效率最高调试也最方便。所谓的“微服务架构”本质是用网络调用的方式解耦应用代价是引入服务发现、配置中心、分布式事务、链路追踪等一系列复杂度团队人数不够或运维能力不强硬上微服务大概率被反噬。比较合理的选择路径是单模块单体 → 多模块单体 → 微服务。多模块单体是很多团队忽略的中间态它在代码组织层面提前做了模块化等未来发现某一个模块确实需要独立部署时把模块抽出去变成独立服务成本远小于从一团乱麻的单体里硬拆。1.3 多模块拆分的核心考量多模块项目推荐按业务域拆分而不是按技术层拆分。按技术层拆分会出现一个尴尬局面你有一个 user 模块、一个 order 模块于是建了 user-controller、user-service、user-mapper还有 order-controller、order-service、order-mapper……其实本质上还是单模块只是把文件分散到不同目录里模块间的依赖完全没理清。按业务域拆分的思路是user 模块里包含 controller、service、mapper、entityorder 模块同理模块内部自成体系对外只暴露必要的接口。这样做的收益是模块与模块之间的边界真正清晰一个业务域的修改不会波及其他域开发和测试的心智负担都小很多。至于要不要抽一个公共模块来放工具类、通用返回结果、异常处理、配置类答案是要但别乱放。常见的做法是最外层放一个 parent 聚合 POM 管理所有模块的版本再抽一个 framework 或 common 模块存放公共能力。需要注意的是common 模块尽量不要依赖业务模块否则会形成循环依赖业务模块依赖 common 没问题但 business 模块之间的依赖要克制。2. 从零搭建一个多模块 Spring Boot 项目概念讲完就得上手了。这里我带你把一个多模块项目从空白目录到能启动、能打包的完整过程过一遍过程中会把最容易踩的坑一起交代清楚。2.1 父 POM 管版本这点决定了你能少踩多少坑多模块项目的父 POM 核心作用是统一管理依赖版本它本身不写业务代码packaging 一定是 pom。实际项目中我强烈建议依赖版本全部交给父 POM 的 dependencyManagement 管理子模块只声明 artifactId 不写 version。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent groupIdcom.example/groupId artifactIddemo-parent/artifactId version1.0.0/version packagingpom/packaging modules moduledemo-common/module moduledemo-user/module moduledemo-order/module moduledemo-admin/module /modules这里有两个关键点。第一spring-boot-starter-parent 本身已经帮你锁定了一大批依赖的版本第二用 spring-boot-dependencies 或者自定义 dependencyManagement 时所有第三方依赖版本也建议集中管理避免不同模块用了同一个依赖的不同版本那就是人为制造冲突。版本选择上如果你刚接触 Spring Boot直接选 2.7.x 比硬上 3.x 更稳妥。Spring Boot 3 基于 Jakarta EE部分依赖还在适配过程中很多老项目的写法会有兼容问题。等到你熟悉了 2.x 的套路再迁移顺畅得多。2.2 模块划分business 模块间不要互相依赖拆分模块时最实用的方式是按业务域建模块比如 demo-user、demo-order再单独建一个 demo-common 放公共内容。结构大致是这样的demo-parent ├── demo-common │ ├── src/main/java/... │ └── pom.xml ├── demo-user │ ├── src/main/java/... │ └── pom.xml ├── demo-order │ ├── src/main/java/... │ └── pom.xml └── demo-admin ├── src/main/java/... └── pom.xmldemo-admin 是启动模块Application 类放这里它聚合依赖其它模块。demo-user 和 demo-order 都只依赖 demo-common不互相依赖。如果 order 模块需要获取用户信息通过接口调用或由 admin 层做编排不要在 order 模块里直接注入 user 模块的 service这样会让模块边界失效。这里的依赖方向可以总结成一句话common 被所有人依赖business 模块只依赖 common启动模块依赖所有业务模块。2.3 启动类位置和打包配置启动类放在 demo-admin 里这基本是约定俗成的。不要放在父工程目录下也不要放到某个业务模块里否则后续打包、扫描都要出问题。启动类的核心注解是 SpringBootApplication它包含了组件扫描、自动配置、配置属性绑定三个能力。package com.example.admin; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication public class DemoAdminApplication { public static void main(String[] args) { SpringApplication.run(DemoAdminApplication.class, args); } }启动类一旦放在 com.example.admin 包下默认扫描范围就是 com.example.admin 包及子包。所以业务模块的代码路径也建议统一以 com.example 为根包比如 com.example.user、com.example.order这样启动类能顺利扫描到所有模块的组件。如果启动类不在根包需要显式指定 ComponentScan 扫描多个包这时候特别容易漏或者扫到了不该扫的类。说到 Maven 打包有一个非常经典的坑默认的 maven-jar-plugin 只能打出一个普通的 jar这种 jar 直接用 java -jar 启动会报“没有主清单属性”。Spring Boot 项目必须用 spring-boot-maven-plugin 重打包而且只要在启动模块配置这个插件业务模块不需要配置。我在 demo-admin 的 pom 里加上build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build2.4 包扫描与配置加载的那些坑多模块项目最常见的两个问题一个是接口文档扫不到 controller一个是启动后 bean 找不到。大部分人第一反应是“代码没问题啊”实际上都是包扫描范围没覆盖到对应模块的组件。如果你把启动类放在 com.example.admin而 user 模块的 Controller 在 com.example.user.controller默认扫描会只扫 com.example.admin 下的内容user 模块的组件根本不会被注册进容器。最直接的解决办法是把所有模块的根包统一成 com.example启动类放在 com.example.admin 下这样 SpringBootApplication 默认扫描的是 com.example 包所有子包都能扫到。还有个常见问题是 application.yml 只会在启动模块里读取业务模块里的配置文件不会自动加载。如果你想在某个模块里单独放配置需要配置 spring.config.import 或引入配置中心不然配置就“消失”了。这一点在多模块协作时特别容易忽略。3. 进阶技巧四个高频实战场景的解法架构和搭建讲完之后进入实际开发中更容易卡脖子的场景我挑几个热搜词里反复出现的问题展开讲。3.1 数据库字段级加密后查询怎么做字段级加密是一个很真实的业务需求比如用户手机号、身份证号、银行卡号数据库泄露也不能直接看到明文。用 Spring Boot MyBatis 时通常用 TypeHandler 做透明加解密。核心思路是实现一个自定义 TypeHandler在写入数据库时自动加密查询出来时自动解密。这样业务代码里完全不需要感知加密逻辑。加密算法推荐用 AES-GCM安全强度高且支持随机 IV但如果要支持等值查询IV 就不能随机生成否则每次加密结果都不一样数据库里相同明文的字段无法匹配。这里要延展讲一下“加密了怎么做查询”。很多人做完加密后第一个问题都是“我 where 条件怎么写”。答案分两种情况等值查询可以把查询参数也用同样算法加密然后直接查加密后的密文也就是“加密方案选确定性加密”。IV 固定或者由密钥派生确保同一个明文每次加密结果一致。这种方式简单直接等值匹配完全没问题。模糊查询这个比较麻烦。如果直接 LIKE那么就必须在数据库端把列解密后再匹配性能会很差而且要把解密密钥暴露给数据库。实际业务中我见过可行的方案是对加密字段生成一个可检索的辅助列比如用明文前几位 哈希或者用支持可搜索加密的中间件。这里务必做好详细的方案设计因为这种需求没有银弹。TypeHandler 写完后在实体类字段上加上 TableField(typeHandler MyEncryptTypeHandler.class)再在 mybatis 配置里指定对应的 type-handlers-packageSQL 层感知不到任何变化。3.2 SQL 执行 10 秒自动关闭真的存在吗有人问“JVM 或者 Spring Boot 会默认设置 SQL 执行 10 秒自动关闭吗”直接回答Spring Boot 默认没有这个全局配置。你的一条 SQL 跑很久应用层一般不会主动掐断它除非你在代码层面显式设置了事务超时或 JDBC Statement 查询超时。在这个问题上你真正需要分开理解的是“连接超时”和“查询超时”不是一回事。数据库连接的 socketTimeout 控制的是读取数据等待时间事务的 timeout 控制的是整个事务的执行时长而 SQL 本身的超时则由 Statement 的 queryTimeout 控制。Spring Boot 中可以通过 datasource 的 hikari 配置设置 connection-timeout但这个是拿连接的超时不是 SQL 执行超时。如果你确实需要为数据库查询加上兜底的超时控制可以设置 JDBC 连接串参数spring: datasource: url: jdbc:mysql://localhost:3306/demo?connectTimeout3000socketTimeout10000这是数据库驱动的超时设置会在底层对 Socket 读写做限制。更细粒度的 SQL 超时可以用 MyBatis 拦截器实现或者直接在注解事务中指定 timeout 属性。真实项目建议至少要配置一层兜底否则一条慢 SQL 把线程池占满整个应用就雪崩了。3.3 springfox 3.0.0 遇上 Spring Boot 2.6接口文档崩了怎么办这是我踩过的坑网上问得也特别多。Spring Boot 2.6 开始默认的路径匹配策略从 AntPathMatcher 改成了 PathPatternParser而 springfox 3.0.0 内部还在用 AntPathMatcher结果就是应用能启动但访问 /swagger-ui/ 或 /v3/api-docs 时直接报错。最快的解决办法是在 application.yml 里加一句spring: mvc: pathmatch: matching-strategy: ant_path_matcher这样把路径匹配策略改回 AntPathMatcherspringfox 就能正常工作。但要注意这个配置是全局的对系统所有接口的路径匹配都会生效如果有特殊规则需要提前排查。更推荐的做法是新项目直接放弃 springfox改用 springdoc-openapi。它在 Spring Boot 2.6 上支持更友好配置也更清爽。迁移成本不高只需要把依赖替换调整一下注解包名即可。3.4 Ajax 参数传到后端却拿不到先用这四个问题定位“Spring Boot 无法通过 ajax 的参数返到前端”这个描述看起来是双向问题但实际开发中大概率是几种固定原因。我总结了一套排查顺序第一检查请求方式和后端参数接收方式对不对。如果是 POST JSON 格式的 body后端就要用 RequestBody 接收如果是普通的 application/x-www-form-urlencoded后端用多个 RequestParam 或者一个对象接参。这两类方式不能混用用错了必拿不到值。第二检查字段名是否一致。JSON 里是 firstNameJava 对象里是 first_name当然映射不上。加上 Jackson 的全局命名策略配置或用 JsonProperty 显式指定能避免这种低级问题。第三检查 Content-Type。浏览器发送 Application/json 而服务端接口用 RequestParam参数自然为空因为 Spring MVC 在解析这两种格式时走的是不同的适配器。第四检查跨域。如果前后端分离前端页面和后端不在同一个源预检请求或跨域被拦请求可能压根没到后端方法里。用 CrossOrigin 或全局 CorsFilter 解决注意不要用 CrossOrigin 加在 controller 上就以为万事大吉全局配置更稳妥。这些排查顺序写下来其实也对应了一个方法论先定位问题在哪一层再针对那一层去查具体配置不要一上来就怀疑框架有 bug。绝大多数“奇怪问题”最后都是入参、注解、配置这三件事没对齐。4. 常见问题速查与排查思路按我的经验下面这些问题是 Spring Boot 项目里出现频率最高的整理成一张速查表遇到类似现象可以先对照看看。4.1 高频问题速查表问题现象常见原因解决办法启动报 “No qualifying bean of type”组件没有被扫描到检查启动类包路径或显式 ComponentScan打包后 java -jar 启动报“没有主清单属性”没配 spring-boot-maven-plugin 或配错模块在启动模块配置该插件接口返回 404 但方法存在Controller 未被扫描或路径写错检查包扫描范围和 RequestMapping 映射配置项读不到application.yml 不在当前模块或前缀不对调整配置文件位置或 ConfigurationProperties 前缀Ajax 拿不到参数请求格式和后端接收方式不匹配按 3.4 的顺序逐一排查Swagger 访问报错Spring Boot 2.6 与 springfox 不兼容修改匹配策略或换 springdoc加密字段查不到数据加密方案不支持等值查询改用确定性加密或增加辅助存索引数据库连接池耗尽慢 SQL 或连接未释放加超时控制、优化 SQL、使用连接监控表格里每一个问题我都在前面正文中展开过实际遇到时可以快速定位到对应章节去细看。4.2 通用排错方法论做一个 Java 后端项目问题排查最重要的是“分层定位”思路。收到一个异常先确认它在哪一层暴露出来是前端请求根本没到后端还是 controller 层就报错了还是 service 层逻辑异常还是 mapper 层 SQL 执行失败。很多刚入行的开发者一遇到报错就从头到尾看日志我的建议是反过来先看异常堆栈的顶部再往调用链上游追踪。Spring Boot 的日志输出已经比较友好了重点找 “Caused by” 部分那才是真正的原因。然后再结合配置、依赖版本、包路径三个维度排查。另外千万不要忽略依赖冲突。同一个类出现在多个 jar 包或不同模块锁定了不同版本可能会出现“明明代码没错但行为诡异”的情况。这时用 mvn dependency:tree 看依赖树快速定位冲突。最后再说一个我实际操作中的体会我见过太多人把 Spring Boot 当成“写 CRUD 的工具”其实它的价值在于帮你把工程化的节奏带起来。无论你从这篇学到多少东西我真心建议你动手把一个多模块项目完整敲一遍别拷贝我的代码自己从父 POM 开始建踩一踩包扫描的坑、打包的坑、配置加载的坑。每踩一个坑你对 Spring Boot 的理解就会深一层。这也是我做技术这些年最认同的一个成长路径框架是学出来的更是踩坑踩出来的。
返回列表