SpringBoot启动原理五阶段拆解与排查实战

发布时间:2026/11/2 10:10:50

SpringBoot启动原理五阶段拆解与排查实战 1. 先把启动这件事拆成一条时间线SpringBoot启动原理这个话题我在带新人的时候讲过不下十遍每次讲到一半都会被打断“哥能不能别一上来就贴源码先告诉我这玩意儿到底按什么顺序在跑”这个问题问得很实在。SpringApplication.run(MyApp.class, args)这一行代码看起来平平无奇但它在你按下启动按钮到控制台打印出Started MyApp in 2.317 seconds之间串起了一条相当长的流水线推断应用类型、装载初始化器和监听器、准备环境、打印 Banner、创建容器、刷新容器、启动内嵌 Web 服务器、回调 Runner。任何一个环节出问题你看到的可能就是一行红色堆栈而不是那句让人安心的启动日志。这篇文章我想干的事情很明确把这条流水线摊开按阶段讲清楚每一步在做什么、为什么这么设计、出了问题该往哪儿看。适合两类人看。一类是刚接触 SpringBoot 不久能跑起来但是心里没底想知道SpringBootApplication这个注解到底施了什么魔法另一类是已经用了两三年平时 CRUD 写得飞起但一遇到启动慢、Bean 找不到、自动配置不生效这类问题就开始靠猜。后面我会给出可复现的排查手法、参数配置和一份常见报错速查表你照着做基本能覆盖八成启动期问题。我个人的习惯是把整个启动过程切成五段来看这个切法不是官方定义但非常贴合排查问题的思路构造 SpringApplication 对象 → 准备 Environment → 创建 ApplicationContext → refresh 容器 → 收尾回调。记住这五段后面所有细节都往这五个格子里塞脑子里就不会乱。下面挨个展开。2. 构造阶段SpringApplication 对象是怎么来的2.1 一个 new 操作背后的四次推断很多人以为SpringApplication.run()是静态方法直接开跑其实它内部第一件事就是new SpringApplication(primarySources)把应用的主配置类塞进去做一次对象构造。这次构造干了四件在启动早期就必须确定下来的事情。第一件是推断应用类型。WebApplicationType.deduceFromClasspath()会去类路径上找特征类如果存在DispatcherServlet且不存在WebFlux相关的DispatcherHandler判定为SERVLET如果只有 WebFlux 的那一套判定为REACTIVE两个都没有就是NONE。这个判断结果直接决定了后面创建哪种 ApplicationContext也决定了内嵌服务器要不要起。我踩过一次坑某个批处理项目为了复用工具类把spring-boot-starter-web顺手引了进来结果应用类型被判定成 SERVLET启动时莫名其妙占了一个端口。排查了半天才发现是依赖污染。所以做纯后台任务的项目依赖要卡干净别图省事。第二件是加载初始化器和监听器。注意这里是加载而不是执行对象在这一步就已经实例化好了。加载来源是SpringFactoriesLoader去META-INF/spring.factories里找ApplicationContextInitializer和ApplicationListener两个 key 对应的实现类。这里有个细节值得说加载完成后会用AnnotationAwareOrderComparator排序所以Order和Ordered接口在这一步就已经生效了。第三件是推断主类。deduceMainApplicationClass()通过抓取当前线程栈找到main方法所在的类。这一步看着无关紧要但它决定了启动日志里打印的那个类名也影响部分条件判断。第四件是设置 headless 属性。也就是把java.awt.headless置为true避免在没有图形界面的服务器上因为加载 AWT 相关类而抛异常。这个设置很多人不知道但只要你的项目里间接引了任何带图片处理的库它就能救你一命。2.2 为什么初始化器要在容器创建之前加载好这是个典型的“为什么这么设计”的问题。ApplicationContextInitializer的作用是在容器refresh()之前对容器做定制比如注册自定义的作用域、往环境里塞属性源、修改 BeanFactory 的某些默认行为。它的执行时机在prepareContext阶段也就是容器对象已经 new 出来、但里面的 Bean 一个都没创建的时候。如果等到容器刷新完再加载初始化器那就毫无意义了——该定制的都定制完了。所以框架必须在构造SpringApplication的时候就把这些类找出来、实例化好、排好序等到合适的时机一举调用。这个设计思路其实贯穿了整个启动流程凡是需要“提前到场”的角色都在更早的阶段完成实例化和排序。监听器也是同理ApplicationStartingEvent这个事件在run()方法的第三行左右就发出了那时候环境还没准备好所以监听器必须在此之前就绪。理解了这个设计逻辑你就能明白为什么有些监听器里注入 Bean 会失败。ApplicationStartingEvent和ApplicationEnvironmentPreparedEvent阶段容器还不存在或者还没刷新完你在这个时机去Autowired一个 Service注定拿不到。正确做法是用ApplicationContextInitializer或者EnvironmentPostProcessor再或者干脆监听更晚的事件比如ApplicationReadyEvent。2.3 事件广播一条贯穿全程的进度条SpringBoot 把启动过程切成了若干个事件按顺序发出来。这套事件机制是我排查启动问题的第一把工具因为它给了你一个精确的时间坐标系。事件触发时机容器是否可用ApplicationStartingEventrun() 刚开始环境未准备否ApplicationEnvironmentPreparedEventEnvironment 准备完成否ApplicationContextInitializedEvent容器创建初始化器已执行部分ApplicationPreparedEventBean 定义已加载未刷新部分ApplicationStartedEvent容器刷新完成Runner 未执行是ApplicationReadyEventRunner 执行完毕可以接客是ApplicationFailedEvent启动过程中抛异常视情况我自己常用的手法是在一个ApplicationListener里给每个事件打时间戳两个相邻事件的差值就是那个阶段的耗时。这个手写方案虽然土但它比任何监控组件都直接因为你能精确知道到底是卡在环境准备还是卡在容器刷新。后面第 6 章我会给出更专业的做法但最初定位问题时这套打点足够了。3. 环境准备配置到底是怎么被读进来的3.1 prepareEnvironment 里的三次推送prepareEnvironment这个方法看着不长但它内部的操作密度极高。整体的动作可以概括成三步先创建或获取ConfigurableEnvironment然后调用configureEnvironment做基本配置比如把命令行参数包装成属性源、设置ConversionService最后把环境对象推给监听器。真正有意思的是配置文件的加载时机。SpringBoot 2.4 之后换成了ConfigData这套 API由ConfigDataEnvironmentPostProcessor负责处理。它是在ApplicationEnvironmentPreparedEvent被广播的时候执行的也就是说配置文件是在环境已经存在、但容器还没创建的时候读进来的。这个顺序保证了一件重要的事后面的所有条件装配、Bean 定义解析都能读到配置文件里的值。ConfigData这套机制支持spring.config.import导入外部配置、支持用---分隔的多文档 YAML、支持 profile 分组spring.profiles.group。这几个特性在微服务项目里特别有用。举个我实际用过的场景一个项目需要在测试环境和预发环境之间切换多套数据源配置以前要维护好几份application-xxx.yml后来改用spring.config.import: optional:configtree:/etc/app/conf/把配置挂在容器卷上代码仓库里只保留一份默认配置运维改配置不用动代码。这个改动看起来小但它把配置的归属权从开发团队交回了运维团队。有个细节要注意addConfigFilePropertySources的执行顺序决定了属性源的优先级。命令行参数commandLineArgs的优先级高于配置文件系统属性高于配置文件但低于命令行参数。如果你发现配置文件里改了值不生效先怀疑是不是被启动参数覆盖了。3.2 启动 Banner 与“提前退出”的判断打印 Banner 这个动作看起来纯属装饰但它所在的时机很关键在环境准备完、容器创建前。这个位置意味着 Banner 里可以读取配置比如spring.main.banner-modeoff就是在这里生效的。我见过有团队把版本号和构建时间写进banner.txt用占位符替换每次启动日志里就带着版本信息排查线上问题时特别方便。紧接着 Banner 打印框架还做了另一件事判断是否需要提前退出。如果你在SpringApplication上设置了setWebApplicationType(NONE)或者通过spring.main.web-application-typenone关掉了 Web 支持那后续就不会创建 Web 容器。更典型的场景是WebApplicationType.NONE配合setRegisterShutdownHook(false)用于那些一次性执行完就退出的命令行工具。这里有个容易被忽略的点提前退出的判断发生在容器创建之前。也就是说如果你在配置文件里写了spring.main.web-application-typenone但代码里已经new了一个AnnotationConfigServletWebServerApplicationContext两者会打架。我的建议是这类全局性的开关尽量只在一个地方配置别代码和配置各写一半。4. 容器创建与 refresh真正干活的十二步4.1 createApplicationContext 的选型逻辑环境准备好之后createApplicationContext()会根据前面推断出来的webApplicationType决定实例化哪个容器。SERVLET 对应AnnotationConfigServletWebServerApplicationContextREACTIVE 对应AnnotationConfigReactiveWebServerApplicationContextNONE 对应AnnotationConfigApplicationContext。这三个类名很长但你只要记住它们的继承关系就能理解差异前两个都在GenericApplicationContext的基础上增加了 Web 服务器相关的能力比如持有ServletContext、支持ServletContextInitializer的收集。这个选型为什么不能推迟到后面因为容器的类型决定了它具备哪些能力而prepareContext阶段要往容器里塞东西比如把主配置类注册成 Bean 定义、设置Environment、调用初始化器这些操作依赖具体的容器类型。所以类型必须在创建时就确定不能中途换。这也解释了为什么你不能在运行时把SERVLET类型的应用改成REACTIVE——两套容器的内部结构完全不同。prepareContext阶段还有个容易被忽略的动作把主配置类注册为 Bean 定义。这一步用的是AnnotatedBeanDefinitionReader它会解析主类上的注解但注意此时只是注册定义真正的注解解析包括ComponentScan扫出来的那些类发生在后面的ConfigurationClassPostProcessor里。很多新人会困惑“为什么我在ApplicationContextInitializer里拿不到自己写的 Bean”原因就在这儿——那时候 Bean 定义还没解析完呢。4.2 refresh() 的骨架与关键节点refreshContext最终调到AbstractApplicationContext.refresh()这是整个启动流程里最重的一段。方法体里有十二个左右的步骤我不打算逐个念只挑几个对排查问题最有价值的说。obtainFreshBeanFactory之后是prepareBeanFactory这一步会注册一些基础设施 Bean比如environment、systemProperties、systemEnvironment。然后是postProcessBeanFactory这是个扩展点子类可以在这里做定制。真正的重头戏是invokeBeanFactoryPostProcessors。这一步会执行所有BeanFactoryPostProcessor其中最重要的就是ConfigurationClassPostProcessor。它负责解析Configuration类处理ComponentScan、Import、Bean、PropertySource这些注解。自动配置就是在这里被加载的因为EnableAutoConfiguration引入的是一个DeferredImportSelector它会在所有普通配置类解析完之后才执行这个“延迟”的设计保证了用户自己定义的 Bean 优先级更高。registerBeanPostProcessors之后是onRefresh这是 Web 应用启动内嵌服务器的位置。再往后finishBeanFactoryInitialization会实例化所有非懒加载的单例 Bean这一步的耗时通常占了整个启动时间的大头。最后finishRefresh发布ContextRefreshedEvent、启动Lifecycle相关的组件。我建议你把这张顺序表贴在工位上遇到“Bean 初始化顺序不对”“某个 PostProcessor 没执行”这类问题对一眼就知道卡在哪儿。4.3 内嵌 Tomcat 是在哪一步起来的这个问题被问的频率极高。答案是onRefresh阶段具体链路是ServletWebServerApplicationContext.onRefresh()调用createWebServer()后者从 BeanFactory 里找到ServletWebServerFactory默认是TomcatServletWebServerFactory调用它的getWebServer(...)然后TomcatWebServer在构造过程中完成initialize()这里面就包含了tomcat.start()。理解这个时机有个实际好处端口绑定发生在 Bean 实例化之前。这意味着如果端口被占用你会在“实例化 Bean”之前就收到报错日志里还没出现任何业务 Bean 的初始化信息。反过来如果你看到日志已经打印到某个 Service 的初始化然后才报端口冲突那说明你的容器是外部容器模式不是内嵌模式。还有个细节finishRefresh里会发布ServletWebServerInitializedEvent事件里带着实际的端口号。如果你的项目配置了server.port0随机端口想知道真实端口就监听这个事件。测试环境做集成测试时这个技巧很好用能避免端口冲突。4.4 Bean 实例化阶段的两个暗坑finishBeanFactoryInitialization这一步会创建所有非懒加载的单例。有两个坑我见过不止一次。第一个是循环依赖。Spring 用三级缓存来解但这套机制只对单例 字段注入或 setter 注入有效。构造器注入产生的循环依赖解不了会直接抛BeanCurrentlyInCreationException。我的经验是构造器注入虽然优雅但在大型项目里要小心尤其是两个 Service 互相依赖对方的某个方法时。这种设计本身就是有问题的正确的做法是抽一个第三个类出来。第二个是初始化顺序。PostConstruct、InitializingBean.afterPropertiesSet、自定义init-method的执行顺序是固定的先PostConstruct再afterPropertiesSet最后init-method。如果某个 Bean 在自己的初始化方法里依赖了另一个还没初始化的 Bean就会拿到 null 或者代理对象。这种情况优先怀疑是依赖关系没声明清楚而不是去调顺序。5. 自动配置条件装配到底怎么判断的5.1 从 spring.factories 到 AutoConfiguration.importsSpringBootApplication是个组合注解拆开是SpringBootConfiguration、EnableAutoConfiguration、ComponentScan。前两个好理解关键是EnableAutoConfiguration它通过Import(AutoConfigurationImportSelector.class)引入了自动配置的选择逻辑。在 SpringBoot 2.7 之前自动配置类列表放在META-INF/spring.factories里key 是org.springframework.boot.autoconfigure.EnableAutoConfiguration。2.7 开始引入了META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports这个新文件3.x 里完全取代了旧方式。这个变化不只是文件名改了加载逻辑也调整了——新方式支持重复条目去重、支持更细的排序。如果你维护的是老项目升级到 3.x这里是个必查项自己写的 starter 如果还在用spring.factories注册自动配置升级后会静默失效。所谓“静默”是指它不会报错只是自动配置不生效你得自己发现某个 Bean 突然没注入了。这种问题排查起来极其费时间我在升级时专门写了个脚本扫所有依赖 jar把还在用旧格式的列出来。5.2 条件注解的评估顺序与常见误判自动配置类加载进来之后并不是全部生效每个类上都挂着条件注解。常见的几类ConditionalOnClass类路径上有指定类才生效、ConditionalOnMissingBean容器里没有指定 Bean 才生效、ConditionalOnProperty配置项匹配才生效、ConditionalOnWebApplicationWeb 应用才生效。这些条件的评估有固定的顺序框架内部由OnClassCondition、OnBeanCondition等一组ConfigurationCondition处理。顺序很重要因为ConditionalOnClass涉及类加载如果类不存在后续的判断就没必要做。SpringBoot 会先做类级别的过滤再做 Bean 级别的过滤这个顺序保证了性能。一个高频误判场景你自己写了个DataSource的 Bean但发现配置根本没生效日志里也没有报错。原因往往是自动配置的ConditionalOnMissingBean判断在你的 Bean 定义注册之前就执行了。因为自动配置是延迟导入的理论上用户配置优先但如果你用的是Bean方法放在一个还没被解析的配置类里顺序就会反过来。这种情况下用--debug启动看自动配置报告最直接报告里会明确写出每个配置类是被哪个条件否决的。5.3 排除自动配置的三种姿势有时候自动配置帮倒忙比如你只想用 Redis 客户端但不想让它自动建连接池或者你引入了两个数据源相关依赖导致冲突。排除方式有三种注解上写SpringBootApplication(exclude {DataSourceAutoConfiguration.class})、配置文件写spring.autoconfigure.exclude...、或者干脆用EnableAutoConfiguration(excludeName ...)按类名排。我的建议是优先用配置文件理由是排除操作通常和部署环境有关写在配置里可以按 profile 区分注解写死了就改不动。另外exclude生效的前提是被排除的类确实在自动配置列表里如果类名写错了它不会报错只是白写。这种情况用--debug报告一核对就清楚了。6. 实操把启动过程真正“看”出来6.1 用 --debug 拿到自动配置报告这是最廉价也最有效的排查手段。启动时加上--debug参数控制台会打印一份AUTO-CONFIGURATION REPORT分成Positive matches和Negative matches两部分。前者告诉你哪些自动配置生效了、为什么生效后者告诉你哪些没生效、被哪个条件卡住了。报告很长别硬看用重定向把输出存成文件再搜java -jar app.jar --debug startup.log 21然后搜DataSourceAutoConfiguration之类的关键词。我排查过一个“连接池没生效”的问题就是靠报告发现ConditionalOnClass判断失败根本原因是某个依赖被provided作用域标了打包时没进去。除了--debug还有个更细的开关debugtrue写在配置文件里效果一样。另外logging.level.org.springframework.boot.autoconfigureDEBUG也能输出类似信息粒度更细但噪音更大我一般不用。6.2 启动耗时埋点与火焰图想优化启动速度第一步是量化。我在项目里常做的是记录每个事件的时间戳输出成一张表。这个方法简单但只能定位到阶段级别无法细到具体 Bean。更细的做法是用ApplicationStartup。从 2.4 开始SpringApplication支持注入一个ApplicationStartup实现框架内部会在关键节点打上StartupStep。自带的BufferingApplicationStartup会把所有 step 缓存起来配合 Actuator 的/actuator/startup端点能直接看到完整的调用树每个 Bean 的创建耗时一目了然。配置方式是new SpringApplicationBuilder(App.class).applicationStartup(new BufferingApplicationStartup(2048)).run(args)注意缓冲区大小要够太小会丢数据。如果连 Bean 级别都定位不到那就上 JFR 或者 async-profiler 抓火焰图。我常用的命令是-XX:StartFlightRecordingduration60s,filenamestartup.jfr跑完用 JDK 自带的工具打开看。火焰图里如果看到大片ClassLoader.loadClass说明是类加载开销大如果看到SocketInputStream.read那八成是在启动时做了远程调用这是大忌。提示启动慢的头号原因永远是“在启动过程中做业务上的远程调用”。数据库连池预热、配置中心拉取、缓存预热这些动作能异步就异步能延后就延后。6.3 手写一个极简启动器验证理解光学理论容易飘我建议你花半小时写个几十行的迷你版把关键调用串一遍。不用完整实现只要把new 容器 → 注册配置类 → 设置环境 → 调用 refresh → 发事件这条线跑通你对整个流程的理解会瞬间落地。核心代码大概是这样public class MiniApplication { public static void main(String[] args) { AnnotationConfigApplicationContext ctx new AnnotationConfigApplicationContext(); ctx.getEnvironment().getPropertySources() .addFirst(new MapPropertySource(custom, Map.of(app.name, mini))); ctx.register(AppConfig.class); ctx.refresh(); System.out.println(started: ctx.getBean(app.name)); ctx.close(); } }这段代码没有 Banner、没有环境推断、没有事件广播但它跑通之后你会发现refresh()才是真正干活的地方。再去对照 SpringBoot 的源码看refreshContext你就知道框架在标准流程之上加了哪些东西——环境处理、事件、自动配置、内嵌容器全都是“加固”而不是“重写”。这种“先跑通再对照”的学习路径比一上来就啃源码效率高得多。7. 常见问题与排查实录7.1 启动卡住不动日志停在一半这是最让人抓狂的一类问题。日志停在某一行不往下走也没有异常。我的排查顺序是先用jstack抓一份线程栈看主线程卡在哪个方法上再用jcmd pid Thread.print抓一份做对比如果两次栈顶方法一样基本就锁定了。常见卡点有这么几个。第一是随机数生成阻塞旧版 JVM 在熵不足时会卡在SecureRandom的初始化解决方案是加-Djava.security.egdfile:/dev/urandom。第二是DNS 解析超时某些环境下主机名反查很慢表现就是卡在InetAddress.getLocalHost()。第三是数据库连接超时连接池默认超时时间可能很长日志里看不出来但线程栈会显示在 socket read 上。第四是死锁这种一般会有明显的锁持有信息。我遇到过一个比较邪门的某个项目在容器环境里启动要卡三分钟本地却正常。最后发现是/dev/random熵池不够又恰好用了某个安全相关的库。这个场景现在少了但如果你在嵌入式设备上跑还是值得留意。7.2 启动报错速查表报错关键词大概率原因处置方向Port already in use端口被占用换端口或找出占用进程No qualifying bean of type自动配置没生效或包没扫到看 debug 报告、检查 ComponentScan 范围Circular reference构造器注入形成环改字段注入或重构依赖关系Failed to configure a DataSource引了数据源依赖但没配连接信息补配置或排除自动配置BeanDefinitionOverrideException同名 Bean 定义冲突检查是否重复注册或开启允许覆盖NoClassDefFoundError依赖缺失或作用域错误检查打包产物里的 jarUnable to start ServletWebServer容器初始化失败看 cause通常是端口或 ServletContext 问题这张表里的每一条我都在真实场景里遇到过。其中BeanDefinitionOverrideException在 2.1 之后才出现因为默认禁止了 Bean 覆盖。有些老项目升级上来会因为这个报错解决方式有两种给冲突的 Bean 加Primary或者不同名或者设置spring.main.allow-bean-definition-overridingtrue。我个人倾向于改代码因为这个开关一开以后所有覆盖都静默通过等于把隐患藏起来了。7.3 我踩过的几个坑你可以直接绕开第一个坑在PostConstruct里做重活。有个项目在某个 Service 的PostConstruct里初始化了一个本地缓存读了两万条数据启动时间直接多了八秒。后来改成实现ApplicationRunner异步加载启动时间立刻降下来。判断标准很简单启动路径上的代码只做“声明和连接”不做“数据和计算”。第二个坑日志配置放在最后。早期我把日志相关的初始化放在ApplicationReadyEvent里结果启动过程中所有日志都用默认格式输出排查问题时根本看不清是哪个模块打的。后来改成在EnvironmentPostProcessor里就把日志配置加载好整个启动过程的日志都可读。第三个坑误以为Order能控制一切。Order只对同一类型的组件集合生效比如多个Filter、多个ApplicationRunner。它管不了 Bean 的实例化顺序。想控制实例化顺序得靠DependsOn或者干脆重构依赖关系。这一点我解释了很多次还是有同事搞混。第四个坑忽略spring.main.lazy-initialization的副作用。这个开关确实能让启动变快但它把 Bean 的创建推迟到第一次使用结果是启动快了、首次请求慢了而且某些配置错误会被推迟到运行时才暴露。我的建议是本地开发别开压测环境可以开生产环境要评估首次请求的延迟能否接受。第五个坑把ApplicationRunner当成万能初始化入口。Runner 执行的时候容器已经刷新完成但它仍然在run()方法的调用栈里也就是说它执行完之前应用还不算“就绪”。如果你在里面做了耗时操作健康检查会一直不通过。需要长时间预热的逻辑正确做法是监听ApplicationReadyEvent然后丢到异步线程池里执行。7.4 优雅停机与启动的对称性说完启动顺带提一句停机因为这两件事在代码组织上是成对的。server.shutdowngraceful开启优雅停机后容器收到终止信号会先停止接收新请求等待在途请求处理完再销毁 Bean。spring.lifecycle.timeout-per-shutdown-phase30s控制最长等待时间。对称性体现在哪儿你在启动时PostConstruct里打开的资源在停机时要有对应的关闭动作。PreDestroy是个选择但要注意它的执行时机在容器关闭过程中如果这时候还依赖别的 Bean可能已经销毁了。更稳的做法是实现SmartLifecycle它能控制停机的先后顺序。这块内容展开能写一篇新的这里只提醒一句启动时开的连接停机时不关跑久了就是句柄泄漏。8. 定制化扩展在正确的钩子上做正确的事8.1 五个扩展点该选哪个SpringBoot 给了很多扩展点新人容易挑花眼。我按执行时机列一下你对号入座就行。EnvironmentPostProcessor在环境准备阶段执行适合做配置加工比如从外部配置中心拉配置塞进PropertySource。ApplicationContextInitializer在容器创建后、刷新前执行适合注册作用域、改 BeanFactory 配置。BeanFactoryPostProcessor在 Bean 定义加载完、实例化前执行适合批量修改 Bean 定义比如动态替换某个属性的值。BeanPostProcessor在每个 Bean 初始化前后执行适合做代理增强、属性注入。ApplicationRunner和CommandLineRunner在容器就绪后执行适合做启动后的数据初始化。选择的核心原则是时机越早越好但要看你的依赖是否就绪。比如你想改某个 Bean 的属性就必须等到 Bean 定义加载完你想读配置就必须在环境准备好之后。搞错时机是这类扩展点最常见的错误。8.2 自定义 starter 的关键结构自己写 starter 的骨架其实很固定一个xxx-spring-boot-autoconfigure模块放自动配置类一个xxx-spring-boot-starter模块做依赖聚合只放 pom不放代码。自动配置类上要挂AutoConfiguration然后按需加条件注解最后在AutoConfiguration.imports文件里注册。有个细节很多人漏掉自动配置类不要加ComponentScan。加了会导致扫描范围失控把不该装配的类也装配进来。正确做法是用Import精确引入需要的配置类。另外配置属性的绑定建议用ConfigurationProperties配合EnableConfigurationProperties比Value更好管理也支持元数据提示。写 starter 时我习惯配一个spring-configuration-metadata.json这样 IDE 里敲配置项会有提示。这个文件可以手写也可以用注解处理器自动生成后者更省心但需要额外加一个依赖。8.3 启动参数的选择与取舍最后说几个我在生产环境常用的启动参数都是踩过坑之后定下来的。-XX:UseG1GC加-XX:MaxGCPauseMillis200这个组合对启动时间影响不大但能让启动后的第一次 Full GC 来得晚一些。-Xms和-Xmx设成一样大避免运行期堆扩容带来的抖动虽然会牺牲一点内存弹性但对稳定性有好处。-Dspring.main.lazy-initializationfalse明确关掉懒加载防止不同环境配置不一致。--spring.profiles.active统一在启动命令里指定不写在配置文件里避免打包后忘记改。还有一个容易忽略的启动脚本里加超时控制。我见过应用启动过程中卡死但进程还活着健康检查一直不通过最后靠人工发现。用 systemd 的话配TimeoutStartSec用容器的话配startupProbe都是必要的兜底。启动流程再优雅也得防着它卡住不动。这套东西我用了好几年从单体到微服务从本地到容器核心逻辑一直没变。SpringBoot 的启动过程看着复杂本质上就是“准备环境、创建容器、刷新容器、回调通知”这条主干其余全是围绕主干做的增强。把这根主干记牢剩下的细节遇到再查比死记硬背强得多。
延伸阅读

更多相关文章

2026/11/2 10:05:49

MATLAB BP神经网络手写数字识别:完整代码与实战调优指南

简介:这份资源面向学习机器学习与MATLAB编程的初学者及课程实践者,提供BP神经网络手写数字识别的完整实现代码与配套数据集,帮助读者从零跑通一个经典的模式识别项目。压缩包共5027个文件,约6.15MB,其中5000个bmp图片为…

2026/11/2 10:05:49

MATLAB手写BP神经网络实现手写数字识别:从零搭建到调优实战

简介:这份资源面向学习机器学习与MATLAB编程的初学者及课程实践者,提供BP神经网络手写数字识别的完整实现代码,帮助读者从零跑通数据预处理、网络构建、训练与评估全流程。压缩包共5027个文件,约6.15MB,其中5000个bmp图…

2026/11/2 10:05:49

基于TCP/IP的展厅智能中控软件:架构、通信与二次开发实战

简介:面向展厅、会议室等场景的中控系统搭建需求,这份资源提供了一套基于TCP/IP协议的可视化智能中控软件程序,适合弱电集成人员、展厅运维者及希望低成本自建中控的爱好者使用。软件采用所见即所得的拖拽式编辑方式,无需编程即可…

2026/11/2 11:16:01

Three.js 批量渲染 GLB:InstancedMesh、矩阵归一化与资源释放

最近在做一个三维钓鱼网页,岸边需要重复摆放树木、石头和芦苇。同一份模型会出现在不同位置,大小和朝向也不一样。 项目里采用了 InstancedMesh。把 GLB 转成实例批次时,有几件事需要一起处理:模型可能包含多个 Mesh;…

2026/11/2 11:16:01

【C++】AVL 树

目录 1. AVL的概念​编辑 2. AVL 树的实现 2.1 AVL 树的结构 2.2 AVL 树的插入 2.3 旋转 2.4 AVL 树的查找 2.5 AVL 树的平衡检查 2.6 AVL树的删除 1. AVL的概念 AVL 树由前苏联数学家 G.M. Adelson-Velsky(阿德尔森 - 韦尔斯基) 和 E.M. Landi…

2026/11/2 11:16:01

涉密单位班车管理:地图、数据与人员信息的边界

涉密单位班车管理:地图、数据与人员信息的边界 涉密单位的班车项目,第一个被否掉的功能通常是「实时定位」。 不是因为定位没用,而是因为定位要落在公网地图上,而公网地图本身就是一条泄漏路径:车辆轨迹连起来&#xf…

2026/11/2 11:16:01

09-02-内存-AssetBundle与Addressables内存策略

AssetBundle 与 Addressables 内存策略篇章:09-核心技术-内存管理篇 | 阅读时间:约 40 分钟 | 前置知识:了解 Unity 资源加载基本概念一、引言AssetBundle 是 Unity 资源管理的基石级系统,Addressables 在其上提供了更高级的抽象。…

2026/11/2 11:11:00

App投Meta广告初试,真实数据都在这

上一篇说了,我在试Meta的广告。到现在一共花了118美元,先把这笔账算一下。01 iOS贵,几天就停了我建了两个广告组,一个推安卓,一个推iOS,目标都是移动应用安装,每组每天预算5美元。投的是美国&am…

2026/11/2 6:08:01

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/2 6:39:12

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 1:04:04

工业级SPI隔离设计:硬件抗扰与信号完整性实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 1:04:04

PLFM_RADAR:主动探测数据通道,构建低误报的数据感知系统

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 1:04:04

工业SPI隔离设计:MAX14431与STM32F746硬件抗干扰实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 6:08:01

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/2 6:39:12

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑