ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Spring Boot启动失败?端口占用、依赖冲突与配置不生效全解析

Spring Boot启动失败?端口占用、依赖冲突与配置不生效全解析 新手期写Spring Boot最让人心态崩掉的往往不是业务逻辑写不出来而是项目明明照教程敲完了本地却怎么都跑不起来。端口被占、依赖冲突、配置不生效这三座大山几乎人人都会遇到区别只是早踩还是晚踩。我这些年带新人和线上救火碰到的启动失败案例里九成都能归到这三类。这篇文章就把这三个问题的来龙去脉、排查手段和治本方案摊开说透顺带聊几个新手问得特别多的延伸问题。如果你正在被某个诡异报错折磨或者想系统梳理自己的排错思路这应该能帮你省下不少查资料的时间。1. 端口占用先搞清楚为什么是你再决定怎么改1.1 端口被占的几种常见现场Spring Boot默认起在8080端口只要这个端口上已经有一个进程在监听Tomcat就起不来。报错长这样*************************** APPLICATION FAILED TO START *************************** Description: Web server failed to start. Port 8080 was already in use.后面往往还跟着一行Action: Identify and stop the process thats listening on port 8080 or configure this application to listen on another port.看到这一句事情的性质就清楚了不是你的代码写错了而是端口资源冲突。我见过最多的情况有这几种上一个项目没关干净IDEA里还有java进程驻留在后台自己同时开了多个实例比如orders服务和users服务都想占8080电脑上装了MySQL、Redis或者其他开发软件占用了8080Windows机器上svchost.exe占用80端口这是最让新手困惑的一种如果你在用Docker容器端口映射冲突也会出现类似报错。很多新手第一反应是去搜“8080端口被占怎么办”然后照着教程把端口改成8081。这能解决问题但如果你每次都只靠改端口长期下来会积累很多隐患。先搞明白是谁占的再决定是让路还是绕路这才是治本逻辑。1.2 三步定位、两步解决无论你是什么操作系统思路都一样的先找端口对应的进程再确认它能不能杀最后决定是杀进程还是改端口。Windows下开一个CMD或PowerShell执行netstat -ano | findstr :8080输出大概是TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING 23032最后一列23032是PID。接着查这个PID是谁tasklist | findstr 23032如果看到进程名是java.exe好就是以前启动的Spring Boot进程没退出。确认它已经没什么用了直接杀掉taskkill /PID 23032 /FmacOS或Linux下用lsof更方便lsof -i :8080输出中能看到监听8080的进程名和PID比如java 12345 user 41u IPv6 0x... 0t0 TCP *:8080 (LISTEN)然后就是老规矩kill -9 12345如果排查出来是别的重要服务占了端口比如MySQL、Redis那就不该乱杀了。更干净的做法是让Spring Boot换个端口。改端口有四种方式按优先级从高到低我列一下命令行参数java -jar app.jar --server.port8081环境变量SERVER_PORT8081配置文件application.propertiesserver.port8081配置文件application.ymlserver.port: 8081至于Windows下svchost.exe占用80端口我得特别提醒一句svchost.exe千万不要看到就杀它是Windows系统服务宿主进程里面挂着一堆系统服务。80端口通常被http.sys这块系统组件占着最常见的原因是IIS的World Wide Web Publishing Service或者Windows Activation Service在运行。想彻底让出来就跑services.msc找到这两个服务停掉或者到“启用或关闭Windows功能”里关掉IIS相关组件。如果只是临时用你就让Spring Boot换个端口不用跟系统死磕。1.3 端口规划从“这次能跑”到“以后少踩”端口被占的本质是“命名冲突”。一次两次改端口没问题但一个项目里如果每个环境都靠手改迟早出乱子。我的习惯是开发、测试、预发、生产环境在配置中心或profiles里明确端口段比如dev是8080test是8081生产走网关80/443内部服务之间能错开就错开避免所有服务一股脑堆在同一个端口如果是写自动化测试用server.port0让操作系统随机分配端口避免测试环境里的端口冲突给项目统一加一个context-path比如server.servlet.context-path/api这样不仅能用路径区分模块也能降低端口层面的直接冲突。还有一个细节值得注意如果改了端口还不行检查一下是不是有多个Spring Boot实例在跑。有时候IDEA里没关干净旧进程你改了配置文件重新启动但旧的还在占着8080。这种时候看启动日志就能发现“一个成功一个失败”的现象。总之端口问题是最容易定位的也是最能帮你建立排错信心的问题。2. 依赖冲突Maven/Gradle的“版本暗战”2.1 冲突是怎么发生的你以为你只引入了一个依赖但Maven会把依赖的依赖也拉进来这就是所谓的传递依赖。你引入spring-boot-starter-web它又会拉一堆子依赖比如Tomcat、Jackson、Spring核心包。如果项目里另外某个第三方库也依赖了同一个组件的不同版本冲突就来了。Maven裁决版本的原则是“路径最近优先”如果两个版本的路径深度一样就按先声明者优先。听起来很合理但坑就坑在选出来的版本不一定能满足所有调用方的需要。A库用的是guava 32的新方法B库却把guava 23拉到了更近的依赖路径上运行时A的代码一调用那个新方法JVM直接抛异常。依赖冲突的特征非常明显常见的有NoSuchMethodError代码里直接调了一个jar包里不存在的方法NoClassDefFoundError某个类在classpath里找不到很可能是被旧版本覆盖了ClassCastException同一个类被不同ClassLoader加载转型失败还有一类更隐蔽Spring Bean创建失败、Filter不生效、SPI机制加载了错误的实现。大部分时候这些报错看起来像代码问题实际上根本不是你写的业务代码的问题。所以先别急着改代码先查classpath里有哪些版本的包。2.2 一个真实的定位过程我之前在项目里接一个开源SDK启动时Spring容器都建完了结果某个Bean初始化时报出了如下异常java.lang.NoSuchMethodError: void org.slf4j.Logger.info(java.lang.String, java.lang.Object, java.lang.Object)看到NoSuchMethodError第一反应就是版本覆盖。排查思路是这样的先确认是谁引入了哪个版本的slf4j-api。Maven项目里直接执行mvn dependency:tree -Dincludesorg.slf4j:slf4j-api这条命令会专门筛选出slf4j-api的依赖路径。输出里我看到两条路径一条是Spring Boot统一管理的1.7.30另一条是那个SDK强制带进来的1.7.25。问题就出在SDK需要1.7.30里才有的重载方法但Maven把1.7.25解析到了更近的位置于是运行时调用了老版本直接炸了。解决方式是在那个SDK的依赖上排除掉它自己带的老版本dependency groupIdcom.example/groupId artifactIdthird-party-sdk/artifactId version1.2.0/version exclusions exclusion groupIdorg.slf4j/groupId artifactIdslf4j-api/artifactId /exclusion /exclusions /dependency排除掉之后Maven就只能走高版本的路径异常消失。这个案例特别典型报错方法名非常具体翻译成人话就是“你在用新版APIclasspath里却塞着老jar包”。2.3 排查依赖冲突的实用工具命令行工具是最可靠的mvn dependency:tree mvn dependency:tree -Dincludescom.google.guava:guava mvn dependency:tree -Dverbose-Dverbose能让你看到哪些依赖被忽略以及为什么被忽略对判断冲突原因很有帮助。如果在IDEA里开发我非常推荐装一个Maven Helper插件社区版完全可以用。装完之后打开pom.xml底部会多出一个Dependency Analyzer标签页里面有Conflicts标签直接列出所有有版本冲突的依赖还能看到冲突路径左侧树形图点一下右键就能生成exclusion比命令行直观得多。Gradle项目的话用这个命令./gradlew dependencies --configuration runtimeClasspath再配合grep按关键字过滤效率也很高。还有一种情况特别容易迷惑新手两个功能相似的包同时存在比如一个用springfox-swagger2一个用springdoc-openapi它们内部都依赖了spring-plugin核心包版本不一致时Swagger界面就是出不来。这种问题的排查方式完全相同先看依赖树再找满足条件的最小版本集合用exclusion或dependencyManagement统一掉。2.4 治本依赖版本管理规范比学会排错更重要的是不要让冲突发生。以下几点是我在实际项目中定下来的硬规矩所有Spring Boot项目继承spring-boot-starter-parent它能通过spring-boot-dependencies这个BOM统一管理常用依赖的版本。你引入starter时不需要写version版本都是对齐过的如果公司有自己的parent pom要在dependencyManagement里统一锁定常用三方库的版本而不是每个模块各写各的version不要轻易手动覆盖Spring Boot已经管理好的版本。非覆盖不可时至少要做全量回归Spring Boot版本和Spring Cloud版本必须匹配否则会有一堆莫名其妙的问题。Spring Boot和Spring Cloud的对应关系我列一个最常见的匹配表大家可以留档Spring Boot版本对应Spring Cloud版本2.3.xHoxton.SR9 / 2.2.x2.6.x2021.0.x (Jubilee)3.0.x2022.0.x3.2.x2023.0.x从2.3升到2.6或者从2.6升到3不只是改一个版本号那么简单后面第4章我会专门提几个容易踩的点。依赖冲突这个问题新手刚开始会觉得烦但一旦你理解了传递依赖和Maven仲裁机制它反而会成为你理解构建工具的最佳窗口。遇到一次、排一次、记一次比看十篇教程都有用。3. 配置不生效改了等于没改问题出在哪3.1 配置来源的优先级比你想象的更复杂Spring Boot支持非常多种配置来源配置文件、环境变量、Java系统属性、命令行参数、profile文件等等。它们之间不是平等的有严格的优先级关系。从高到低大概是这样的命令行参数比如--server.port9090Java系统属性比如-Dserver.port9090操作系统环境变量比如SERVER_PORT9090项目外部路径下的application-{profile}.yml项目内部classpath下的application-{profile}.yml项目外部路径下的application.yml项目内部classpath下的application.yml很多人改配置发现不生效以为是Spring Boot的bug其实压根儿是被高优先级来源覆盖了。举个例子你在IDEA的Run Configuration里设置了环境变量SERVER_PORT9090然后你去改application.yml里的server.port: 8080启动完一看还是9090。因为环境变量的优先级比配置文件高。所以排查配置不生效的第一步不是怀疑自己写错了而是先问一句有没有别的地方以更高优先级把这个配置项给冲掉了启动时用--debug参数可以看到不少线索。3.2 那些“看起来没错就是不生效”的典型雷区我归纳了几个新手最高频的配置不生效场景每个都是真实发生过的。第一个是配置类根本不在扫描范围。SpringBootApplication默认扫描启动类所在包以及它下面的所有子包。如果你的启动类在com.example.demo配置类却放在com.example.config没问题能扫到。但如果启动类在com.example.demo配置类放在org.example.config这种完全不相干的包下Spring就摸不到它。表现为Bean没有创建、配置值全是null、自动配置没起作用。解决方式是把启动类放到包的根路径或者显式用ComponentScan指定扫描范围。第二个是ConfigurationProperties缺少绑定入口。我经常看到有人写ConfigurationProperties(prefix app) public class AppProperties { private String name; // getter setter }然后就没有然后了注入的时候永远是null。这个类虽然带上了ConfigurationProperties注解但如果Spring没有把它注册成Bean绑定就不会发生。要让配置文件里的app.name能映射到这个类至少选一种方式在类上加Component在启动类或某个配置类上写EnableConfigurationProperties(AppProperties.class)Spring Boot 2.2以后可以直接在主类上写ConfigurationPropertiesScan。我个人更推荐用ConfigurationPropertiesScan语义更清晰也不会把配置类混进组件扫描体系。第三个是YAML缩进问题。YAML对空格极其敏感而且要求用空格不能用Tab。缩进不一致通常启动就会报解析异常但还有一种更隐蔽的情况是文件里混了Tab和空格语法看着没报错实际解析出来的值跟你预期完全不一样。所以不管用什么编辑器都要确保YAML文件用的是空格缩进我一般都直接在IDEA里把Tab自动替换成4个空格。第四个是profile没有激活。你创建了application-prod.yml但如果没有在application.yml里写spring.profiles.activeprod也没有通过启动参数指定--spring.profiles.activeprod这个文件根本不会被加载。启动日志会告诉你当前状态如果看到No active profile set, falling back to default profiles: default那说明你根本没激活任何profileapplication-prod.yml就是空气。第五个是改了配置但没重新编译。尤其是用IDEA开发改了application.yml后如果devtools没有介入可能还是跑的target目录里的旧配置。这时候别纠结代码先做一次mvn clean package或者IDEA里Build Project再启动。3.3 用日志和Actuator验证配置配置不生效的时候最忌讳盲改。我建议按这个顺序来验证先看启动日志。Spring Boot启动时会在日志里打印很多有用的信息比如Tomcat initialized with port 8081、The following 1 profile is active: dev。你期望的值和日志里打出来的值不一致说明配置确实没按你想的加载这时候再去查优先级覆盖。第二招是引入Actuator它会把生效的配置串成一个可视化端点。加依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency暴露需要的端点management: endpoints: web: exposure: include: env,configprops,health,info启动后访问/actuator/env能看到每个配置项的来源和最终生效值哪个来源覆盖了哪个来源一目了然。访问/actuator/configprops可以看到ConfigurationProperties绑定的实际结果是判断“配置类有没有加载”的利器。第三招是我调试时的笨办法写一个CommandLineRunner临时打印配置值Component public class PrintConfigRunner implements CommandLineRunner { private final AppProperties props; public PrintConfigRunner(AppProperties props) { this.props props; } Override public void run(String... args) { System.out.println(app.name props.getName()); } }如果值是null说明绑定链路有问题如果值不是你想要的那个说明有更高优先级来源覆盖。打印完删掉这个类就行。配置不生效这件事最坑的地方在于它是“静默失败”不报错但结果不对。一旦你掌握了“先看启动日志、再用Actuator验证、最后查优先级”这套方法问题范围就能缩小一大半。4. 从“跑起来”到“跑明白”几个高频延伸问题4.1 IDEA社区版也能愉快开发Spring Boot有些新手问“IntelliJ IDEA社区版怎么用Spring Boot”。社区版只是没有内置Spring Initializr向导不代表不能写Spring Boot项目。我的做法是在start.spring.io网站上把项目参数填好生成zip包本地解压后用IDEA直接open作为Maven项目导入。之后配置JDK、配置Maven home path、装Lombok插件该调试调试该热部署热部署和旗舰版差别不大。这里有个小坑社区版默认不会自动加载Maven的更改你改了pom.xml后要手动点一下刷新按钮不然新依赖没进classpathIDE里飘红编译也过不去。这个按钮在Maven面板里图标是两个箭头组成的一个循环点了之后等右下角索引跑完就行。4.2 监控到底要做哪些事Actuator先行Admin兜底很多人问我“Spring Boot实现监控都有哪些需求和功能”。我给新手一个最务实的清单健康检查服务是不是活着数据库、Redis这些下游是否正常指标查看内存、线程、HTTP请求耗时和数量接口列表当前项目注册了哪些路由方便排查和核对日志级别动态调整线上出问题临时把某个类的日志降到DEBUG线程快照看有没有死锁和堆积。上面这些Actuator基本全都能给。先把Actuator用熟比直接上Spring Boot Admin更重要因为Admin只是把Actuator的数据画在了页面上。如果服务多了想看个统一面板Spring Boot Admin的玩法也不复杂。建一个admin-server模块依赖spring-boot-admin-starter-server主类加EnableAdminServer默认跑在9090。然后业务项目里引入spring-boot-admin-starter-client配置指向admin server的地址spring: boot: admin: client: url: http://localhost:9090业务项目启动后会自动注册到admin server网页上能看到实例列表、在线状态、内存曲线等。注意版本别乱配admin server 2.x对应Spring Boot 2.xadmin server 3.x对应Spring Boot 3.x跨版本是拉不上数据的。4.3 给第三方提供的接口到底放哪模块还是独立服务我还有一篇文章的评论区里有人问对外提供给第三方的接口应该放在哪里是单独一个服务还是放在原本的业务服务里我的建议分两种情况。如果系统还是单体架构第三方接口数量不多就在单体内部开一个独立包比如com.example.api.external所有对外接口都从这个包里出不要跟内部Controller混在一起。同时给这组对外接口统一前缀比如/open/api/v1/单独做一套鉴权过滤器和签名校验避免外部请求直接摸到内部管理接口。如果第三方接口不止一两个而且合作方多、接口演进频繁我强烈建议拆成独立服务。理由很实际对外暴露的数据结构跟内部模型往往是两码事独立服务可以不把内部表结构泄漏出去安全策略隔离比如限流、IP白名单、API Key管理都放在这个服务上不影响内部接口版本迭代方便可以并行维护v1和v2不影响业务主链路日志审计独立谁调用了什么、调了几次、返回什么都清晰可查。对外接口无论放哪有一件事别省接口幂等。第三方调用方经常重复请求你至少要保证重复提交不会产生重复订单、重复扣款。我一般要求所有写操作带requestId或idempotencyKey服务端根据这个键做去重配合一个简单的Redis分布式锁或唯一索引就能挡住绝大多数重复问题。4.4 版本升级注意2.3.x、2.6.x到3.x不是改个数字最后聊一个我在社区和群里被反复问的问题Spring Boot版本到底怎么选尤其是从老的2.3.x升到2.6.x再升到3.x。先说结论如果项目稳定运行没必要为了追新而追新。但如果是新项目我建议直接用Spring Boot 3.x的稳定版因为它在生态上已经足够成熟。从旧版本升级时有几个点必须留意Spring Boot 3.x强制要求JDK 17以上JDK 8是跑不起来的javax包全面换成了jakarta包。以前写import javax.servlet.*的业务代码升级后要批量替换成import jakarta.servlet.*不换就编译不过Spring Security的配置方式也变了WebSecurityConfigurerAdapter这种老写法废掉了改成基于SecurityFilterChain Bean的写法2.6.x开始默认禁止循环依赖如果老项目里存在循环依赖升级后启动会直接报错需要在配置里设置spring.main.allow-circular-referencestrue缓解但更推荐把代码结构调整好2.6.x之后’ Actuator默认只暴露health端点env、metrics这些一律不暴露想用就得显式在include里写出来。升级的正确姿势是先读官方Release Notes再跑一遍全量测试最后逐个解决编译错误和启动差异。不要试图一次性把Spring Boot、Spring Cloud和基础JDK全换掉分层升级出问题更好定位。最后再分享一点个人体会。我带项目的习惯是遇到报错先看全日志、再动手端口占用、依赖冲突、配置不生效看起来是三个完全不同的问题本质上考验的是同一种能力——读懂运行时的提示理解框架的默认规则。把这些基本功打牢后面学微服务、学云原生都会顺很多。另外建议你每次解决完一个坑把报错关键字、根因、处理方式记到本地笔记里三个月后回头看你会发现踩坑的频率大幅下降因为绝大多数坑本质上都是同一类问题。
返回列表