ARTICLE DETAIL

资讯详情

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

Spring Boot 3.2+JDK17企业级项目架构搭建与服务拆分实践

Spring Boot 3.2+JDK17企业级项目架构搭建与服务拆分实践 1. 项目整体设计与技术选型为什么拆成两个服务1.1 服务拆分的核心逻辑api_service 和 admin_service 各管一摊拿到“Spring Boot 3.2.10 OpenJDK 17”这个技术栈要搭企业级项目第一个动作不是急着写代码而是先把服务边界划清楚。从标题看这次要同时产出两套接口服务xxx_api_service对外提供业务接口xxx_admin_service做管理平台接口。很多新手习惯把两类接口揉在一个工程里配一个context-path区分前缀比如/api/**和/admin/**前期开发确实省事但后患无穷。我倾向于从第一天就分成两个独立Spring Boot应用分别打jar包、分别部署。原因有三点面向的调用方完全不同。api_service面向C端用户、移动端、第三方系统接口需要鉴权、限流、幂等控制、签名校验admin_service面向内部运营人员和管理员走的是后台登录态、操作审计、RBAC权限控制。两类接口的安全模型差别很大混在一个服务里Security配置、拦截器、过滤器都得做两套逻辑代码里全是if (path.startsWith(/admin))这种味道很冲的写法。扩容和发布策略不一样。C端流量有峰值api_service要能水平扩展多个实例负载均衡管理平台并发量低但要求稳定和可审计两个服务混在一起扩容时管理接口的实例也被迫一起拉起白白消耗资源发布时也容易互相影响。故障隔离。如果某个对外接口被刷、内存被打满或者有慢SQL拖垮连接池独立部署能尽量不让管理平台跟着遭殃。管理后台挂了的损失比对外接口挂了一部分严重得多。至于降级方案如果你确实想先快速验证业务也可以先用一个服务加模块区分但我的建议是在Maven多模块的层面就划分好子模块边界哪怕最终部署成一个应用也要保证代码层是解耦的后面拆服务只是个配置问题而不是重构问题。1.2 版本组合为什么是 Spring Boot 3.2.10 OpenJDK 17这个版本组合属于目前企业级项目里比较主流且稳健的搭配。Spring Boot 3.2.x 是继3.1之后的功能迭代版本支持到2024年前后的维护周期相比3.0、3.1稳定性已经过大量生产验证。3.2.10 是3.2.x 分支里比较靠后的维护版本等于把前面几个补丁版本的bug修复都收进来了所以站在能拿到的稳定性和安全性角度选它没问题。JDK 17 是 LTS长期支持版本会持续收到安全更新和性能优化是Spring Boot 3.x 的基线版本之一。Spring Boot 3.x 整体是基于 Jakarta EE 9 的包名从javax.*换成了jakarta.*底层的Spring Framework 6.x 也从设计上就只支持 JDK 17。如果你还用JDK 8那只能停留在Spring Boot 2.7.x拿不到3.x的新特性。这里有个容易踩的坑Spring Boot 3.2.x 之后官方对 JDK 21 也做了支持但并不是说 JDK 21 就一定比 17 好。如果你的团队对虚拟线程、新的垃圾回收器没有刚需JDK 17 Spring Boot 3.2.10 的匹配度更成熟网上踩坑案例多、解决方案也多。我见过有人一上来就用 JDK 21 Spring Boot 3.4结果遇到某个三方库的反射机制不兼容排查起来非常痛苦。再看构建工具。Spring Boot 3.x 对 Maven 和 Gradle 都支持但企业里 Maven 的使用率明显更高尤其是多模块项目管理Maven 的dependencyManagement和parent继承机制非常成熟。这次的项目规划我默认按 Maven 多模块来做这也是大多数中大型 Java 项目实际在用的组织方式。2. 企业级模块规划落地从顶层目录到代码分层2.1 多模块 Maven 项目划分与职责边界企业级项目的模块划分最重要的是职责不重叠、依赖方向清晰。一个实用的规划方案如下模块名职责定位依赖关系xxx-parent父POM统一管理依赖版本、插件配置、Java版本无xxx-common通用工具类、异常体系、统一返回结果、常量定义仅依赖第三方工具库xxx-api-service对外API服务独立Spring Boot应用依赖 common、dao、外部SDKxxx-admin-service管理平台API服务独立Spring Boot应用依赖 common、daoxxx-dao数据访问层放 Entity、Mapper、MyBatis 接口等依赖 commonxxx-message消息通知、MQ 生产者消费者可选延迟拆分依赖 common、daoxxx-api-service和xxx-admin-service是最终运行的可执行模块各自包含自己的Application启动类、Controller、Service、配置类。xxx-common禁止依赖任何业务模块只能放与业务无关的通用能力比如ResultT统一封装、BizException、PageQuery分页对象、日期工具、JSON工具等。xxx-dao的设计要克制一点不要所有表都往这一个模块里堆如果等业务扩大可以按领域拆成xxx-dao-order、xxx-dao-user前期先合并避免过度设计。关于接口定义我建议在xxx-api-service模块内部单独建一个api包存放对外的 RPC 接口或者 Feign 接口定义。如果你后续要对接 Spring Cloud OpenFeign对外接口的 interface 和 DTO 需要被其他服务引用那就把接口定义单独抽到一个xxx-api-contract模块只放 interface 和请求响应 DTO不依赖任何实现。不过如果目前没有跨服务调用需求先在 service 模块内定义即可等需要时再抽不要提前建一堆空模块。2.2 服务内部分层与包结构设计每个可运行服务内部的包结构我习惯按“先按技术分层再按业务分域”来组织。拿xxx_api_service举例com.xxx.api ├── Application.java ├── controller // HTTP 层负责参数接收和响应封装不写业务逻辑 ├── service // 业务逻辑层事务、规则、编排都在这里 │ └── impl // Service 实现类 ├── repository // 数据仓储层调用 mapper 或第三方存储 ├── model │ ├── dto // 对外交互的数据传输对象 │ ├── vo // 视图对象专门给前端返回 │ └── entity // 数据库实体 ├── mapper // MyBatis Mapper 接口 ├── config // 配置类如 SecurityConfig、CorsConfig ├── interceptor // 拦截器登录态、日志、幂等校验 ├── task // 定时任务 └── util // 业务相关的工具类Controller 层只做三件事接收参数、调用 Service、把 Service 的返回值包成ResultT返回。Service 层放业务规则事务注解加在 Service 实现类的公开方法上。Mapper 层只做数据库操作不做业务判断。DTO 和 VO 要区分开DTO 是服务与服务、服务与外部系统之间的数据协议VO 是给前端展示的模型不要混用。xxx_admin_service的结构类似但它多一个管理后台特有的security子包用来放后台用户登录、权限校验、操作日志相关的代码。如果两个服务要共享一套用户权限体系可以把用户、角色、菜单相关的表设计和通用逻辑放进xxx-dao和xxx-common但认证流程的控制权要留在各自服务里否则后面想独立演进会很麻烦。包命名别用复数也别把controller、service这种层名包到很深的层级里比如com.xxx.api.controller.order就比com.xxx.api.order.controller容易维护。前者先固定技术层再按业务分子包后者每个业务域都有一套自己的 controller/service/mapper结构看起来清晰但跨业务的公共逻辑很容易复制粘贴通用代码不好收敛。3. 核心配置实操POM、YAML 与环境隔离3.1 父 POM 版本管理与构建插件配置多模块项目的第一步是把根目录的pom.xml搭好。父 POM 的关键职责是锁定所有依赖的版本子模块不需要也不能自己随便写version全靠dependencyManagement统一管理。我给出一个可复制的模板parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.10/version relativePath/ /parent groupIdcom.xxx/groupId artifactIdxxx-parent/artifactId version1.0.0-SNAPSHOT/version packagingpom/packaging properties java.version17/java.version maven.compiler.source17/maven.compiler.source maven.compiler.target17/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding mybatis-plus.version3.5.7/mybatis-plus.version hutool.version5.8.27/hutool.version jjwt.version0.12.5/jjwt.version /properties modules modulexxx-common/module modulexxx-dao/module modulexxx-api-service/module modulexxx-admin-service/module /modules dependencyManagement dependencies dependency groupIdcom.xxx/groupId artifactIdxxx-common/artifactId version${project.version}/version /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version${mybatis-plus.version}/version /dependency /dependencies /dependencyManagement注意 MyBatis-Plus 从3.5.x开始Spring Boot 3 需要使用专门的 starter 坐标mybatis-plus-spring-boot3-starter直接用老坐标会在启动时因为javax和jakarta命名空间不一致而报ClassNotFoundException。Maven 编译插件在 Spring Boot 父 POM 里已经默认配置好 Java 17但为了保证不同开发机上的编译行为一致建议在父 POM 显式声明maven-compiler-plugin的release为 17不要只靠java.version属性。spring-boot-maven-plugin只加在两个可执行模块里xxx-common和xxx-dao不需要。而且要注意在子模块里配置 plugin 时加一个executions把repackage关掉避免公共模块被重新打成可执行 jar 导致引用方加载不到类。build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId executions execution goals goalrepackage/goal /goals /execution /executions /plugin /plugins /build3.2 多环境配置与应用参数设计每个服务模块的src/main/resources下配置文件按环境分成多份命名规则为application.yml公共配置、application-dev.yml、application-test.yml、application-prod.yml。application.yml里放所有环境一致的配置环境差异只放变化的部分。一个典型的api_service配置长这样server: port: 8080 servlet: context-path: /api spring: application: name: xxx-api-service profiles: active: dev datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/xxx_api?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: ${DB_USERNAME:root} password: ${DB_PASSWORD:root} data: redis: host: ${REDIS_HOST:localhost} port: 6379 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl management: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: show-details: when_authorized probes: enabled: truexxx_admin_service建议用不同的端口比如 8081并单独设置context-path: /admin。生产环境里如果走网关context-path不一定需要但本地联调时它能有效避免两个服务同时启动时的路由冲突。配置信息里的敏感项比如数据库密码、Redis 密码、短信密钥绝对不能硬编码在 YAML 里提交到 Git。正确做法是用占位符从环境变量读取生产环境由部署平台注入也可以用 Jasypt 做配置项加密ENC(...)方式虽然多一层依赖但对安全合规要求高的企业项目几乎是标配。3.3 Actuator、Micrometer 与监控指标接入Spring Boot Actuator 是生产环境的必备组件它能暴露健康检查、指标、线程信息、环境变量等端点。但如果配置不当它本身也是重大安全隐患。热搜里提到的 “spring boot actuator 漏洞”本质上就是运维把 endpoints 全部暴露到了公网导致攻击者通过/actuator/env拿到环境变量、通过/actuator/heapdump下载内存快照从中扒出数据库密码和密钥。所以 Actuator 的核心配置原则就一句话生产环境只暴露真正需要的端点其他一律关掉。management: endpoints: web: exposure: include: health,info,metrics,prometheus exclude: env,beans,configprops,mappings,threaddump,heapdump endpoint: health: show-details: when_authorized probes: enabled: trueshow-details: when_authorized表示健康检查详情只有在登录后才展示否则只返回 UP/DOWN这样可以防止攻击者通过/actuator/health的详细信息探测服务内部状态。probes.enabled: true会激活 k8s 探针必需的/actuator/health/liveness和/actuator/health/readiness端点为容器化部署做准备。Micrometer 是 Spring Boot 3.x 默认的指标门面接 Prometheus 只需加一个依赖dependency groupIdio.micrometer/groupId artifactIdmicrometer-registry-prometheus/artifactId /dependency启动后访问/actuator/prometheus就能看到标准的 Prometheus 指标格式。配合监控大盘JVM 内存、GC 次数、HTTP 请求耗时、线程池状态全都有数据了。这一步建议项目初始化时就接上等线上出问题才想起来加监控往往已经晚了。4. 安全体系搭建认证、鉴权与端点防护4.1 api_service 与 admin_service 的认证策略差异两个服务的安全设计不能照搬同一套代码。api_service面向外部调用方通常采用 Token 或者签名机制。如果是给自家 App 提供接口常见的方案是登录后下发 JWT后续请求在 Header 里带Authorization: Bearer token如果是给第三方开放平台提供接口还需要 AppId/AppSecret 签名校验、时间戳防重放、nonce 防重复请求。admin_service是内部管理平台接口虽然也用 JWT但它更强调操作审计和权限控制。每个管理端接口都应该有对应的权限标识比如system:user:add用户在角色-菜单模型下被分配权限点接口校验通过才放行。用 Spring Security 6 做这件事比较标准Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf - csrf.disable()) .sessionManagement(session - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth - auth .requestMatchers(/auth/login, /captcha, /actuator/health).permitAll() .requestMatchers(/**).hasAuthority(ADMIN) .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }csrf.disable()是因为无状态 JWT 方案本身不依赖 CookieCSRF 攻击的载体被移除了但如果你保留了 Session 登录方式就不能随便关 CSRF。这个取舍要看你最终采用什么会话模型。JWT 的密钥要足够长至少 256 bit不要用123456这种短密钥。建议通过环境变量注入生产环境用专门的密钥管理服务保存定期轮换。解析 JWT 时算法要固定验签不要用algorithm: none或者从 Token 头里动态取算法这是 JWT 签名绕过漏洞的常见成因。4.2 Actuator 端点安全与整体防护策略如果你的服务直接暴露在公网Actuator 端点的位置要放在网关或 Nginx 后面做一层访问控制。Spring Security 配置里显式放行/actuator/health但其他 Actuator 端点一律需要认证。如果服务不要求外网访问只在内网开放也仍然要做认证内网也不是法外之地。整体防护上我建议从这几个维度同时下手控制暴露范围只保留health,info,metrics,prometheus其他端点全部关闭。尤其heapdump和env在生产环境能不开就不开。网络层隔离Actuator 端口和管理端口分开或者通过 Nginxlocation规则只允许内网 IP 访问/actuator/**路径。参数校验Controller 接收的参数统一使用Validated注解避免脏数据进入业务逻辑DTO 里用NotBlank、Size、Pattern约束字段出参不要直接返回数据库实体。敏感数据脱敏日志里禁止打印完整的手机号、身份证、银行卡号配置好 Logback 的Converter做脱敏接口返回时也做脱敏处理。4.3 CORS 跨域配置与常见误区跨域问题在前后端分离项目里非常高频。管理平台前端一般部署在独立的域名或端口比如http://localhost:9528访问http://localhost:8081的接口浏览器会先发 OPTIONS 预检请求如果服务端没正确处理前端就报 CORS 错误。Spring Boot 里最直接的配置是实现WebMvcConfigurerConfiguration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(http://localhost:*, https://admin.xxx.com) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }几个容易忽略的细节allowedOriginPatterns比allowedOrigins灵活支持*通配符而且配合allowCredentials(true)时不会因为携带凭证而失效。maxAge设大一点比如 3600 秒可以减少预检请求的次数显著提升前端接口响应速度。如果用了 Spring SecuritySecurityConfig和CorsConfig要配合。Security 里如果开启了cors()它的优先级更高两边的配置要一致否则可能出现 Security 层放行了但业务层还被拦的情况。5. 常见问题排查与部署避坑5.1 初始化阶段的高频异常速查表Spring Boot 3 JDK 17 项目在初始化和联调阶段我收集了几个发生概率极高的异常整理成速查表遇到了可以直接对照现象根因解决方案启动报java.lang.ClassNotFoundException: javax.servlet.Filter引用了基于javax.*的老三方库换用适配 Jakarta EE 9 的版本或搜索对应-jakarta后缀 artifact访问接口返回 404Swagger 有页面但调不通context-path配了/api但 Controller 的RequestMapping也写了/api导致路径变成/api/api/xxx统一约定要么用context-path要么全写在 Controller 上不要两头都加请求报 400 Bad Request请求体 JSON 与 DTO 字段类型不匹配或缺少必填字段且没传检查RequestBodyDTO 字段名和类型加上Validated让错误信息更明确MyBatis 报Invalid bound statement (not found)Mapper 接口和 XML 文件没有对应上或mapper-locations路径不对确认 XML 放在resources/mapper/**下且 namespace 和接口全限定名一致部署报No active profile set, falling back to 1 default profile启动时没有指定--spring.profiles.activeprod启动命令加-Dspring.profiles.activeprod或设置环境变量SPRING_PROFILES_ACTIVE服务无响应日志出现大量connection timed out数据库连接池满或 Redis 连接未释放检查连接池最大连接数配置排查是否有慢 SQL 或 Redis key 未设置过期时间导致阻塞JWT 解析报SignatureException: JWT signature does not matchToken 里的签名与本地密钥计算的签名不一致确认签发和校验使用的是同一个密钥密钥长度是否满足算法要求自定义注解用 AOP 切不到在同一个类内部调用方法走的是 this 调用没经过代理把注解方法拆到另一个 Service或注入自身的代理对象或改用Resource注入5.2 容器化部署镜像选型与内存参数调整企业项目最终基本都会容器化部署。JDK 17 项目做镜像时基础镜像的选择直接影响最终镜像大小和运行时内存表现。最省事的是直接用eclipse-temurin:17-jre-alpine这个是 Eclipse 基金会官方维护的 JDK 发行版镜像体积小与 Spring Boot 3 兼容性经过大量验证。很多人在网上搜到openjdk:17-jdk-slim这个镜像已经进入维护模式不建议继续使用。Dockerfile 参考FROM eclipse-temurin:17-jre-alpine WORKDIR /app ENV TZAsia/Shanghai COPY target/xxx-api-service-1.0.0-SNAPSHOT.jar /app/app.jar EXPOSE 8080 ENTRYPOINT [java, -XX:MaxRAMPercentage75.0, -jar, /app/app.jar, --spring.profiles.activeprod]-XX:MaxRAMPercentage75.0这个参数很关键。容器里如果你直接写-Xmx512m会跟实际容器的内存配额对不上Java 17 官方给出的容器内存感知机制是通过比例设置堆最大值MaxRAMPercentage表示 JVM 最多使用容器内存的百分之多少这样无论你给容器分配 2G 还是 4GJVM 都会自动调整不用跟着改配置。JDK 17 里 G1 收集器是默认的配合容器环境下自动启用的 JFR、JIT 分层编译性能表现是够的。如果服务内存压力极大可以再观察 GC 日志调优方向一般集中在新生代比例和MaxGCPauseMillis不要一开始就上 ZGC它对大多数 CRUD 类服务没有明显收益反而增加复杂度。5.3 几个我踩过的坑和对应的规避习惯搭这个架子的时候我一个一个踩过不少坑挑几个说出来供参考。第一个坑是 Service 实现类里忘了加Transactional导致更新用户信息时先改了主表再改从表失败但主表数据已经提交了。后来我给自己定了一条规矩所有涉及写操作的方法不管逻辑多简单都先评估是否要加事务不要相信单条 SQL 不会出问题。第二个坑是application.yml里的数据库连接参数少配了serverTimezone本地用默认时区没问题但测试服务器在 UTC 时区时间全差了 8 小时。排查过程非常折腾最后才发现是时区问题。现在我的习惯是把serverTimezoneAsia/Shanghai直接写死在连接串里省得环境一换就出问题。第三个坑是 JWT 过滤器注册顺序。Spring Security 6 里如果你没有把自定义过滤器加在UsernamePasswordAuthenticationFilter之前登录状态解析总是晚于安全规则匹配导致明明接口放行了又因为认证信息还没设置而被拦截。现在我的习惯是自定义认证过滤器必须在这种位置addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)否则某些/permitAll的接口没问题但需要用户信息的接口就会莫名拿到 null。第四个坑是 Actuator 的heapdump端点。前期联调我图省事把exposure.include配成了*部署到测试环境后同事发现可以通过/actuator/heapdump下载到几百兆的内存文件里面甚至能搜到数据库密码。从那次之后我对 Actuator 的配置策略就变成白名单模式只开必须要的端点其余一律 deny。6. 最后再分享一点项目启动的建议项目架子搭好之后建议按下面的顺序做启动验证能省不少联调时间先跑mvn clean install -DskipTests确认所有模块能编译、打包通过。只启动xxx-api-service验证数据库连接、Redis 连接、Actuator/actuator/health返回 UP。再启动xxx-admin-service确认两个服务端口不冲突网关或 Nginx 路由能正确转发。写一个最简单的登录接口把 JWT 从生成、校验到拦截器放行的整条链路走通。接入 Prometheus 抓取指标确认/actuator/prometheus有数据输出。这套流程走完项目的底座就算稳了。后面加业务功能都是在已经验证过的框架上填代码不容易出现那种“功能写完了但不知道是框架问题还是业务问题”的困境。架构规划这件事最有价值的不是目录结构本身而是每一步选择背后的理由。为什么要拆服务、为什么要统一管理依赖、为什么要控制端点暴露范围这些决策会在项目生命周期的某个时刻变成你需要翻的账本。把账算清楚了架子就不会倒。
返回列表