公司动态
从零搭建SpringBoot项目,这些配置细节值得注意
你打开IDEA新建一个Spring Initializr项目勾选好Web、JPA、MySQL驱动点击Finish然后信心满满地启动。三秒钟后红字刷屏——数据库连不上、端口被占用、Bean创建失败。你开始怀疑人生SpringBoot不是号称“零配置”吗其实零配置的幻觉只存在于demo里真正落到生产环境每一个细节都在等你踩坑。这篇文章不打算给你一份官方文档的复述而是把我从零搭建项目时反复踩过、查过、改过的那些配置细节按照“从骨架到血肉”的顺序掰开揉碎讲清楚。版本号第一步就决定生死很多人建项目时图省事直接选用默认的SpringBoot版本。但默认版本往往是“稳定得过于保守”的版本可能不兼容你后续想用的新特性。比如你想用Java 17的record语法SpringBoot 2.x就需要额外加参数而3.x原生支持。更坑的是你从网上搜到一段配置代码作者用的SpringBoot 2.7你用的是3.2结果语法完全变了——比如javax.servlet变成了jakarta.servlet。这种翻天覆地的变化一旦到了项目中期才发现重构成本高到你只想骂人。搭建第一件事不是写代码而是确定SpringBoot大版本与Java版本的矩阵关系SpringBoot 3.x要求Java 17以上2.x支持Java 8~17。同时关联的SpringCloud版本、MyBatis-Plus版本、Hutool版本全都得跟主版本对上。建议先去mvnrepository把依赖的兼容性查清楚写进一个《版本清单》文档里以后升级不会手忙脚乱。配置文件别只看application.yml项目的配置文件默认叫application.yml但你最好别把所有配置都堆在里面。SpringBoot支持多环境配置但很多人压根没用对。正确姿势是拆分成application.yml公共配置、application-dev.yml开发环境、application-prod.yml生产环境。主文件里用spring.profiles.active: dev来激活某个环境。但问题来了当你打包上线时总有人忘了改这个激活参数导致生产环境加载了dev的配置比如连上了测试数据库——这事故我可见过不止一次。更稳妥的做法是主文件里不写spring.profiles.active而是在启动命令中用--spring.profiles.activeprod显式指定。你在IDEA里运行可以配一个SpringBoot启动项加入环境变量SPRING_PROFILES_ACTIVEdev部署到服务器时用java -jar app.jar --spring.profiles.activeprod这样代码里就没有硬编码环境了。另外不要把所有敏感信息都写在配置文件里并推送到Git。至少要把密码、密钥等通过环境变量或外部配置中心注入。比如spring.datasource.password: ${DB_PASSWORD}这样你的仓库里只是一个小小的占位符真正的密码在服务器的环境变量里。如果你觉得麻烦也可以使用Jasypt做加密但引入了额外复杂度自己权衡。依赖管理隐式传递依赖是甜蜜的毒药SpringBoot的starter非常好用但它把你本应明确声明的依赖全部变成了隐式传递依赖。举个例子你引入了spring-boot-starter-web它同时带来了Tomcat、Jackson、Spring MVC等一堆库。你写代码时RestController、ObjectMapper直接就能用看上去很美。但哪天你想升级Tomcat版本以解决安全漏洞你会发现在pom.xml里根本找不到Tomcat的坐标——因为它藏在starter的传递依赖里。解决方法是在pom.xml中显式声明你想控制的依赖版本。比如你想用Tomcat 10.1.x可以明确指出tomcat-embed-core的版本或者直接在properties中覆盖tomcat.version。这让你对实际运行的组件有掌控权。另外要注意依赖冲突的排查。启动时如果出现NoSuchMethodError或ClassNotFoundException多半是某个库的传递依赖版本跟你的冲突了。此时别急着百度用mvn dependency:tree看看依赖树找到重复的类然后加exclusion排除不需要的依赖。宁可多写几行排除也别让“看似干净”的依赖树埋下运行时炸弹。端口与上下文路径本地跑得欢线上全白搭你默认端口是8080本地开发一点问题没有。但到了生产环境8080往往被其他服务占用了于是你开始改端口。改端口很简单server.port: 9090。但还有更多细节不要只在application.yml里写死端口最好用环境变量覆盖比如server.port: ${SERVER_PORT:8080}这样本地不设环境变量就用8080生产环境设置SERVER_PORT9090就能无缝切换。除了端口上下文路径即URL前缀也容易忽略。如果你提供服务给网关调用网关可能要求所有服务都有一个统一的前缀比如/order、/user。你可以在配置里设置server.servlet.context-path: /order但注意这会让所有Controller的路径都加上这个前缀。而Actuator的健康检查路径也会被影响——如果你忘了在监控系统里调整健康检查就会404。建议把server.servlet.context-path也做成环境变量可配置比如CONTEXT_PATH这样不同环境灵活切换。还有一个隐蔽的坑当设置了context-path后Swagger的API文档地址也要跟着变否则访问不到UI页面。这些细微之处全是经验堆出来的。日志配置别让console输出骗了你默认情况下SpringBoot用Logback只在控制台打印日志没有文件输出。开发时没问题但生产环境你重启后日志就没了排查线上问题时只能干瞪眼。日志配置决不能依赖默认值必须显式定义日志级别、输出格式和滚动策略。在application.yml里你可以简单配置logging.file.name: app.log但这只能写一个文件时间长了会非常大。推荐在resources下新建logback-spring.xml这是SpringBoot推荐的日志配置文件名它能利用springProfile标签实现不同环境不同日志配置。比如开发环境输出到控制台即可生产环境则按天滚动并且保留30天。关键细节日志文件的保存路径尽量用相对路径或环境变量比如logging.file.path: ${LOG_PATH:./logs}否则每次发布不同目录日志就乱了。另外要注意控制台日志的编码——在某些CentOS系统上如果默认编码不是UTF-8中文日志会乱码。可以在logback-spring.xml中设置charsetUTF-8/charset。还有logging.level.rootWARN可以减少无用日志但你的业务包要单独设置成INFO或DEBUG否则看不到自己的日志。别把日志级别全局定成INFO就完事需要给关键包设置更细的级别例如logging.level.com.example.order: DEBUG调试时特别有用。数据源连接池默认HikariCP有你可能不知道的坑SpringBoot默认使用HikariCP性能确实好。但默认配置太过保守容易在生产环境出问题。HikariCP的默认maximum-pool-size是10但很多业务系统并发量远不止10个SQL同时执行一旦连接池耗尽请求就会排队等待响应时间飙升。你需要在配置里显式调大spring.datasource.hikari.maximum-pool-size: 50。不过也不是越大越好因为每个连接都占用数据库资源太大反而会耗尽数据库的并发连接数。建议公式核心线程数 (1 IO等待时间 / CPU计算时间)不过实际粗略估算可以先设为20~50压测后调整。另外connection-timeout默认30秒这太长了——用户等不起。通常设成3秒3000ms超过就快速失败这样能及时报警而不是让请求线程全部挂死。还有一个被忽略的参数idle-timeout和max-lifetime。HikariCP默认idle-timeout为10分钟但max-lifetime是30分钟。生产环境如果数据库或防火墙会对空闲连接进行回收那么max-lifetime必须小于数据库的wait_timeout否则你的连接可能被数据库端强行断开而HikariCP不知道拿到坏连接后响应超时。将max-lifetime设置为比数据库wait_timeout小几秒比如120000ms可以有效避免“connection is closed”这类诡异报错。事务与连接池的“串味”一个必踩的坑你可能会在Service方法上写Transactional期待着事务回滚。但如果你在同一个类内部调用带事务注解的方法事务会失效这是Spring AOP的经典陷阱。代理机制意味着只有通过外部代理调用才会触发事务内部调用this.method()是绕过代理的。解决办法把需要事务的方法放在另一个Service类里或者注入自身Autowired private UserService self;然后self.method()。但这个细节不算配置更配置相关的是当你配置了事务管理器后要考虑连接池的隔离级别和传播行为。例如某个方法里调用了第三方接口耗时5秒但这个方法里还执行了一段数据库查询事务会把数据库连接占用5秒导致连接池耗尽。解决方式是把耗时操作移出事务或者用Transactional(propagation Propagation.NOT_SUPPORTED)让该方法不参与事务。事务不要死脑筋地用默认的REQUIRED要思考每个方法是否真的需要开启事务以及事务的时长是否可控。健康检查与监控Actuator不是摆设很多人压根不引入Actuator觉得没有它项目也能跑。但没有健康检查的生产环境就像蒙眼开车——你根本不知道服务还有没有在正常工作。引入spring-boot-starter-actuator然后在配置中暴露必要的端点management.endpoints.web.exposure.include: health,info,metrics。这里要注意暴露端点过多会有安全隐患比如shutdown端点如果暴露了别人可以远程关掉你的服务。默认情况下Actuator只暴露health所以需要手动添加info和metrics。health端点默认显示“UP”但你可自定义健康检查器比如检查某个外部依赖是否可用。写一个类实现HealthIndicator重写health()方法返回Health.up()或Health.down()。注意如果你的项目依赖了数据库health端点会自动检查数据库连接但如果数据库断开health返回DOWN那么负载均衡器就会把入口流量摘掉——这是好事但你要确保监控系统能及时报警否则服务挂了你还没发现。另外metrics端点可以提供JVM内存、GC、线程等数据配合PrometheusGrafana就能构建一个轻量监控体系。趁着搭建初期就把Actuator引入别等项目上线了再补到时候配置会更乱。配置文件中的“蜜糖”随机端口与占位符SpringBoot的配置支持占位符例如server.port: ${random.int[8000,9000]}这会让项目每次启动都用随机端口——这个功能非常适合微服务场景下的多实例本地调试或者测试并行跑多个实例。但生产环境绝对不能用随机端口因为服务注册中心需要固定端口才能注册。另一个占位符技巧是使用${USERNAME}来读取系统环境变量但要小心Linux shell中变量未定义时SpringBoot报错。你可以在占位符后加默认值${DATABASE_NAME:defaultdb}。如果你没有给环境变量设置默认值且机器上不存在该变量项目启动就会直接失败。这个失败是好事至少不会让系统跑在错误配置下。但有一个细节Windows和Linux环境变量名称是大小写敏感的而SpringBoot在解析占位符时是大小写敏感的${db_url}和${DB_URL}不同。建议统一用大写加下划线同时为它们提供默认值。打包与启动jar包不是万能的spring-boot-maven-plugin默认打成可执行jar包直接java -jar就能跑。但有一个细节如果项目使用了多个依赖jar包可能非常大。默认的jar包结构是BOOT-INF/lib这样启动反而更快。但有时你需要在运行时动态替换某些配置比如数据库密码。那么外部化配置就非常关键SpringBoot会自动读取./config/目录下的application.yml或者用--spring.config.locationfile:/etc/yourproject/指定外部配置目录。这个很重要——不要把生产配置打进jar包否则每次改密码都要重新打包。另外启动参数-Dspring.profiles.activeprod和--spring.profiles.activeprod是有区别的-D是JVM系统属性--是SpringBoot ApplicationArguments后者优先级更高。部署时建议用--传参这样可以覆盖jar包内的配置。还有一个坑如果你在pom.xml里设置了finalName但你们的CI/CD脚本根据target/xxx.jar的固定路径去找一旦改了文件名脚本就崩了。确保打包命名规范并在CI模板中预留版本变量否则每次手动改脚本迟早出事故。开发体验热插拔与DevTools开发阶段你不希望每次改一行代码就重启整个应用。SpringBoot的DevTools可以实现自动重启但很多人用错了DevTools会自动重启但它的重启原理是使用两个ClassLoader对静态资源的加载可能不生效。更关键的是DevTools默认只监听classpath下的文件变化如果你用IDEA需要开启“允许自动构建”否则你改了Java代码IDEA只保存不编译DevTools根本感知不到。你可以在IDEA设置里勾选“Build project automatically”但这样在大型项目中每次修改都全量编译耗时很长。建议把热部署优化为编辑代码后按CtrlF9BuildDevTools检测到class变化后2秒内重启这样比手动重启快得多。还有DevTools不要打入生产jar包——它默认会被排除但如果你用了一些奇特的构建方式可能打包进去导致生产环境多了一条额外端口如8000。在pom.xml中给DevTools依赖加上optionaltrue/optional这是最稳妥的做法。当然如果你不想用DevTools也可以用JRebel或Spring Boot的调试模式-Xrunjdwp不过那就超出配置讨论了。密码一列加密与权限管理配置文件中明文写密码等于把钥匙挂在门外。虽然本文前面提到用环境变量但仍需要讨论加密的细节。SpringBoot的配置内容是可以加密的但只能通过Jasypt或Vault这类工具。Jasypt集成简单加上starter后配置一个jasypt.encryptor.password然后在配置文件中写ENC(密文)。但有个大坑jasypt.encryptor.password本身也是一条配置如果它写在了配置文件里那么加密就失效了。这个盐值必须放在外部环境变量或JVM参数中例如启动时加-Djasypt.encryptor.passwordyourSecretKey。另外Jasypt的加密算法默认是PBEWITHMD5ANDDES现在看已经不够安全。建议换成PBEWITHHMACSHA512ANDAES_256并在配置中指定你需要的算法名。如果你不想引入额外依赖那么最低要求是配置文件的权限设为600只有运行用户可读。在Linux上chmod 600 application-prod.yml防止其他用户读取。注意如果多个运维需要查看日志那么密码只能你来管理。这属于安全配置的范畴别怕麻烦。时区与编码看似小事后患无穷默认的JVM时区跟随系统如果你的服务器时区是UTC那么数据库的DATETIME字段如果存的是当前时间读写时就会相差8小时。SpringBoot的spring.jackson.time-zone可以指定JSON序列化的时区但不改变数据库连接时区。你现在就该在连接串中指定jdbc:mysql://localhost:3306/db?serverTimezoneAsia/Shanghai。在SpringBoot 3.x中如果使用MySQL 8这个参数变成了connectionTimeZone别搞混。另外Java的LocalDateTime不存在时区概念但Date有所以建议实体类统一用LocalDateTime。编码方面Tomcat默认URI编码是UTF-8但如果你用了Linux上其他字符集HTTP请求参数可能乱码。加一行配置server.servlet.encoding.forcetrue确保请求和响应都被强制编码为UTF-8。永远不要在代码里依赖平台默认字符集配置文件里也写到明处。构建工具Maven与Gradle的差异化细节你是用Maven还是GradleSpringBoot官方两者都支持。但Gradle的配置写法跟Maven大不相同。如果你用Gradle那么依赖版本管理要放在ext或dependencyLocking中。Gradle的implementation与api的区别直接影响依赖传递如果你用implementation则依赖不会暴露给消费者这可以防止传递依赖污染但也可能导致其他模块引用不到该库。Gradle的依赖锁定很关键在dependencyLocking里启用锁定后构建可复现但每次升级依赖都需要执行./gradlew dependencies --write-locks。而Maven则相对简单但Maven的optional和provided作用域容易混淆。optional表示不会传递依赖但编译时可用而provided表示编译期提供运行时不打包。显然如果某依赖同时被两个模块使用你要想想该放哪个作用域。最终建议团队熟悉哪个就选哪个但要在项目初始决定中途切换构建工具是伤筋动骨的大事。配置元数据让你在IDEA里少点错你在application.yml里手写属性的时候IDEA有没有给你提示如果没有你可能没有添加配置元数据。SpringBoot提供了spring-boot-configuration-processor依赖可以生成自定义配置的元数据。比如你写了一个ConfigurationProperties(prefix app.order)类加上这个处理器后在application.yml中输入app.order.时IDE会弹出完整字段提示。这个细节能极大提升配置的准确度避免拼写错误。虽然元数据不影响运行但影响开发体验。我见过太多人因为拼错配置项启动时属性没生效找半天才发现是大小写或拼写问题。加上依赖后编译时会生成spring-configuration-metadata.json你可以审查它是否覆盖了所有字段。在类上标记ConfigurationProperties并启用Component这样自动扫描注册但如果你想让配置类更清晰可以搭配EnableConfigurationProperties。注意ConfigurationProperties与Value不同前者是类型安全且支持校验Validated后者逐字段解析。能用ConfigurationProperties绝不用Value除非只有一两个字段。构建后验证启动即报错 vs 启动后静默失败配置错误在两处暴露启动时报错和运行时报错。启动时报错是幸福运行时报错才是魔鬼。例如数据库密码错误启动时数据源初始化失败会直接抛出异常但你若把spring.datasource.hikari.initialization-fail-timeout设置为负数则即使连接失败也启动成功之后每次请求才失败——这样状态很难排查。建议保留默认的fail-fast行为。另外你有没有试过在本地启动成功但把jar扔到服务器上就启动失败这是配置文件差异导致的。有个小技巧在application.yml中添加一个自定义的启动后检查逻辑。比如在ApplicationRunner中检查关键配置项是否为空为空就抛异常。这是最后一道防线比人肉查看配置可靠得多。收个尾配置管理是一笔长期的债务从零搭建SpringBoot项目表面上是几个依赖和一堆YAML实际上是配置哲学的问题。好的配置让系统在环境变化时优雅适应烂的配置让每次上线都如履薄冰。你需要记住这些细节并把它们固化到团队的Checklist中版本矩阵、多环境配置、日志策略、连接池参数、健康检查、安全密码、时区编码。也许你现在的项目只有几百行配置但当服务扩展到20个配置粒度、命名规范、统一管理就会成为大问题。尽早引入配置中心Nacos、Apollo是一个有远见的决定但也要警惕过度设计。如果你只有一个单机服务用环境变量加外部配置文件完全足够。最后提醒一句每一次“先这样吧”地忽略配置细节未来都会变成“怎么又出问题”的深夜报警。从零搭建项目最值得注意的不是那些能让你跑起Hello World的步骤而是那些让你在生产环境睡得着觉的细节。