多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Spring Boot自动装配原理与手写Starter实战

Spring Boot自动装配原理与手写Starter实战 说到Spring Boot最值得研究的机制自动装配绝对排得上第一梯队。我面试的时候特别喜欢问一个问题你引入一个Starter之后Spring Boot是怎么知道该创建哪些Bean的能答清楚的人其实不多。更多人只是记住“加依赖就能用”但完全没想过这个“开箱即用”背后是一整套SPI加载、条件判断、配置绑定和排序规则在运作。这篇文章我把从看Spring Boot源码到手写一个可运行Starter的完整过程整理一遍。先讲自动装配到底解决了什么时代痛点再拆解它的源码链条然后带着你从零写一个请求追踪Starter最后把这几年在自动装配上踩过的坑、看日志的方法、以及把Starter和监控体系打通的经验一并分享出来。适合正在用Spring Boot做开发、想真正理解底层原理或者打算把自己的通用能力封装成Starter给团队使用的朋友。1. 没有自动装配的年代从手工配置到开箱即用1.1 早年间写Bean装配是什么体验如果你是从Spring 3.x、4.x那个年代过来的一定记得一个项目刚建好时的痛苦必须自己写一堆XML或Java配置把项目里每一个依赖Bean手工组装起来。比如要引入一个Redis你得先配置Jedis连接池再配置RedisTemplate还要指定Key和Value的序列化器最后把这些Bean注册进Spring容器。每个项目几乎都要复制粘贴这一大段代码换个团队甚至换个写法运行结果都可能不一样。那时候最常见的配置文件长这样bean idredisConnectionFactory classorg.springframework.data.redis.connection.jedis.JedisConnectionFactory property namehostName valuelocalhost/ property nameport value6379/ /bean bean idredisTemplate classorg.springframework.data.redis.core.RedisTemplate property nameconnectionFactory refredisConnectionFactory/ ... /bean问题还不只是代码量。更难受的是Bean之间的依赖关系完全靠开发者脑记。你加一个组件可能因为漏配一个依赖Bean启动到一半就报BeanCreationException。更别提多个环境之间的配置差异只能靠一堆properties文件和各种profile硬撑。所以Spring Boot出来的时候那句“约定大于配置”为什么能打动这么多人因为它切切实实解决了手工装配的体力劳动。1.2 Spring Boot的约定与自动装配的诞生Spring Boot做的事情本质上就是把所有通用组件的初始化逻辑收编到框架里。它会根据你classpath上已有的依赖加上你在配置文件里设置的参数自动判断“这个组件你应该需要”然后帮你把必要的Bean创建好。举个最直观的例子你在pom里加上spring-boot-starter-webSpring Boot发现classpath下有了DispatcherServlet和Tomcat相关类就会自动注册内嵌Web服务器和Spring MVC的基础设施不需要你写一行配置。如果classpath下没有这些类对应的自动配置类就会悄悄退场完全不打扰你。这里有个关键点自动配置不是“无脑全部加载”而是基于一堆条件的判断。每个自动配置类上都有类似ConditionalOnClass、ConditionalOnMissingBean这样的注解条件不满足就不生效。所以同一个Starter在不同的项目里最终创建出来的Bean集合可能是不同的这完全看项目的运行环境。1.3 Starter是插头自动装配是插座搞清楚自动装配是什么之后再看Starter就非常简单了。一个Starter不是把一堆jar包简单塞在一起它通常包含两部分依赖描述比如spring-boot-starter-web会传递依赖Spring MVC、Tomcat、Jackson等。自动配置类负责在条件满足的情况下向容器里注册默认Bean和配置属性。可以把Starter理解成插头它决定了你的项目能“接上”哪一类设备自动装配就是插座内部已经布好了电线插头一插电就通。我们平时写自定义Starter做的就是两件事声明好依赖、写清楚自动配置类。2. 自动装配的源码链条从SpringBootApplication到条件评估2.1 三个注解各司其职自动装配的入口相信每个人都见过SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }但这个注解本身不神奇它是由三个注解组合出来的SpringBootConfiguration本质是Configuration标明这是一个配置类。EnableAutoConfiguration真正的自动装配开关核心是向容器导入一个名为AutoConfigurationImportSelector的类。ComponentScan负责扫描当前包和子包下的Component、Service等组件。自动装配的启动核心在EnableAutoConfiguration身上。它使用了Import(AutoConfigurationImportSelector.class)把“候选配置”的选择逻辑交给了Spring的ImportSelector机制。2.2 AutoConfigurationImportSelector自动装配总指挥AutoConfigurationImportSelector是实现DeferredImportSelector的一个类这个接口很有意思。普通ImportSelector在解析配置类时立即执行而DeferredImportSelector会延迟到所有Configuration处理完之后再执行。这么设计的目的是先让用户自己定义的Bean注册进容器再让自动配置类介入这样自动配置里的ConditionalOnMissingBean才能准确判断“用户是否已经定义过某个Bean”。这个Selector的核心逻辑可以理解为四步从固定路径读取候选自动配置类列表。去重。根据SpringBootApplication上的exclude属性和配置文件里的spring.autoconfigure.exclude排除指定配置类。对配置类进行排序最后交给Spring按照Conditional注解逐个评估。所以你在启动日志里看到的那一大段“Conditions Evaluation Report”就是这个Selector加载完候选类之后逐条执行条件判断的结果。排错的时候这里就是最重要的现场。2.3 spring.factories到AutoConfiguration.imports注册文件的变迁自动配置类不是一个一个扫描出来的那样太慢也容易乱Spring采用SPI方式从一个固定的资源文件里读取类名列表。Spring Boot 2.6及之前文件路径是META-INF/spring.factories里面用键值对形式声明自动配置类org.springframework.boot.autoconfigure.EnableAutoConfiguration\ com.example.autoconfigure.ApiTraceAutoConfiguration从Spring Boot 2.7开始官方引入了一个更清晰的新文件路径是META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件内容就是一行一个自动配置类的全限定类名com.example.autoconfigure.ApiTraceAutoConfiguration这里要特别注意Spring Boot 3.x只认新文件不再从spring.factories读取自动配置类。如果你在升级老项目遇到自动配置全部失效十有八九就是少了这个.imports文件。官方做这个迁移主要是为了消除spring.factories里多个key混在一起的问题也为了自动配置列表更好排查。现在写新Starter我建议两个文件都放既兼容老版本又面向未来。2.4 条件注解为什么是安全阀自动配置类加载到Spring容器之后如果没有条件控制那所有依赖都会被强制初始化这显然不行。所以Spring Boot定义了一整套条件注解全部由Conditional演化而来ConditionalOnClassclasspath下存在指定类时才生效。ConditionalOnMissingClassclasspath下不存在指定类时才生效。ConditionalOnBean容器中已有指定Bean时才生效。ConditionalOnMissingBean容器中没有指定Bean时才生效。ConditionalOnProperty配置项满足指定值时才生效。ConditionalOnWebApplication当前是Web应用时才生效。这些注解的组合使用让自动配置拥有了“安装即用、缺省即退”的能力。一个常见的自动配置类会长这样AutoConfiguration ConditionalOnClass(RedisOperations.class) ConditionalOnMissingBean(value RedisTemplate.class) public class RedisAutoConfiguration { ... }没有引入Redis客户端依赖RedisOperations就不存在整个配置类直接跳过即使客户端存在但用户已经自己定义了RedisTemplate框架也不会重复注册。这就是自动配置不会“打架”的核心原因。还有一个冷知识ConditionalOnClass底层通过ASM读取类的元数据来判断不是直接Class.forName所以即使某个类真的不在classpath里也不会因为判断这个注解而抛出ClassNotFoundException。这一点在排查问题时非常有用不要被表象吓到。3. 手写一个API Trace Starter从需求到可运行3.1 先想清楚Starter的边界和默认行为理论讲完开始实战。我以“API请求追踪Starter”为例功能很简单为每个HTTP请求生成一个Trace ID放到响应头里再顺手记录一下请求耗时。这个需求在微服务链路追踪、接口排障时很常见又没有引入过多外部依赖非常适合用来演示自动装配的完整流程。动手之前先定义三件事默认行为开关默认开启响应头默认叫X-Trace-Id。扩展点允许使用方配置开关、响应头名称、排除路径。边界如果使用方自己定义了一个同类型Filter自动配置必须让位。这样设计的好处是使用者零配置就能用同时又有地方覆盖默认值不会被Starter锁死。3.2 工程结构starter与autoconfigure分开的意义很多人第一次开发Starter会直接在同一个Maven模块里写配置类和代码然后打包给别人用。这样短期内没毛病但后期会很难受。建议按官方惯例拆成两个模块api-trace-spring-boot-autoconfigure放自动配置类、属性类、业务组件。api-trace-spring-boot-starter一个几乎只有pom的模块依赖上面的autoconfigure模块可能还补充一些传递依赖。为什么这么做因为自动配置类本身需要被打进一个独立jar使用方只依赖starter壳子。这样如果你出多个思想变体比如webflux-trace-spring-boot-starter之类可以复用同一个autoconfigure模块。另外如果使用方只想引入自动配置能力而不想要传递依赖拆开以后可以通过排除依赖精确控制。工程结构大概是这样api-trace-spring-boot-starter-parent ├── api-trace-spring-boot-starter │ └── pom.xml └── api-trace-spring-boot-autoconfigure └── src/main/java/com/example/autoconfigure └── src/main/resources/META-INF/spring/...3.3 属性类、过滤器与自动配置类代码拆解先写一个属性类用ConfigurationProperties绑定配置前缀ConfigurationProperties(prefix api.trace) public class ApiTraceProperties { private boolean enabled true; private String headerName X-Trace-Id; private ListString excludePatterns new ArrayList(); public boolean isEnabled() { return enabled; } // 剩下的getter/setter省略 }然后是核心过滤器TraceFilter它就做两件事生成Trace ID、计算耗时public class TraceFilter extends OncePerRequestFilter { private static final Logger log LoggerFactory.getLogger(TraceFilter.class); private final ApiTraceProperties properties; public TraceFilter(ApiTraceProperties properties) { this.properties properties; } Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String traceId UUID.randomUUID().toString(); long start System.currentTimeMillis(); try { response.setHeader(properties.getHeaderName(), traceId); chain.doFilter(request, response); } finally { long cost System.currentTimeMillis() - start; log.info([trace] uri{}, traceId{}, cost{}ms, request.getRequestURI(), traceId, cost); } } }接下来是自动配置类AutoConfiguration EnableConfigurationProperties(ApiTraceProperties.class) ConditionalOnClass(TraceFilter.class) ConditionalOnWebApplication(type ConditionalOnWebApplication.Type.SERVLET) public class ApiTraceAutoConfiguration { Bean ConditionalOnMissingBean ConditionalOnProperty( prefix api.trace, name enabled, havingValue true, matchIfMissing true ) public FilterRegistrationBeanTraceFilter traceFilterRegistration(ApiTraceProperties properties) { FilterRegistrationBeanTraceFilter registration new FilterRegistrationBean(); registration.setFilter(new TraceFilter(properties)); registration.setOrder(Ordered.HIGHEST_PRECEDENCE 10); registration.addUrlPatterns(/*); return registration; } }这段代码有几个细节需要说明EnableConfigurationProperties(ApiTraceProperties.class)会自动把属性Bean注册进容器自动配置类里就能直接注入它不需要再声明一个Bean方法。ConditionalOnClass(TraceFilter.class)看起来有点多余因为类都在同一个jar里但它是个“声明性安全阀”如果别人只是把这段自动配置类复制到别的地方可以防止类缺失导致启动失败。用FilterRegistrationBean而不是直接注册TraceFilter是为了和Servlet容器里的过滤器生命周期对齐同时可以设置顺序。ConditionalOnProperty默认情况下matchIfMissingtrue意味着不配置api.trace.enabled也会开启真正做到开箱即用。3.4 注册自动配置类与版本兼容自动配置类写好后必须在resources目录里创建注册文件。以Spring Boot 2.7为例路径是src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件内容一行一个全限定类名com.example.autoconfigure.ApiTraceAutoConfiguration如果还想兼容Spring Boot 2.6及以前同时在src/main/resources/META-INF/spring.factories里加一行org.springframework.boot.autoconfigure.EnableAutoConfigurationcom.example.autoconfigure.ApiTraceAutoConfiguration注意Spring Boot 2.7和2.6其实都支持spring.factories但2.7之后会看到警示日志所以新项目直接用.imports就好。3.5 在测试工程中验证自动装配Starter打包完新建一个普通Spring Boot工程pom里加上这个Starter依赖。不需要任何配置直接启动。启动日志里能看到过滤器已经被注册访问一个接口响应头上会出现X-Trace-Id。如果再给application.yml加几行配置api: trace: enabled: true header-name: X-Trace-Id所有配置项都会自动绑定到ApiTraceProperties。如果希望确认Bean确实是从自动配置类里创建的可以在启动类里临时写一个ApplicationRunner注入FilterRegistrationBean并打印它的来源Bean ApplicationRunner traceCheck(FilterRegistrationBean? staticFilterRegistration) { return args - System.out.println(staticFilterRegistration.getFilter().getClass()); }看到输出是TraceFilter就说明自动装配链路已经全部打通。4. 自动装配实战的5个翻车点与排查链路4.1 条件注解写错了类配置类静默失效Starter开发里最常见的坑就是自动配置类没有生效但启动又不报错。因为你写的ConditionalOnClass可能引了一个使用方classpath里没有的类整个配置类被静默跳过。这种情况下连日志都不会有非常容易误判成“Starter没被打进包”。排查方法很简单在启动时加debugtrue或者启动参数--debugSpring Boot会在启动日志里打印一份Conditions Evaluation Report。里面有两段关键内容Positive matches哪些自动配置类生效了。Negative matches哪些自动配置类没有生效以及具体是哪个条件不满足。有一次我给项目加了个自定义Starter发现自己的配置类始终不在Positive matches里。打开报告才看到ConditionalOnClass里写了一个被optional排除的依赖类。问题根源不是自动配置机制而是依赖被裁剪了。所以遇到类似问题第一反应应该是看这个报告而不是乱猜。4.2 Bean冲突与ConditionalOnMissingBean的边界ConditionalOnMissingBean是自动配置让位于业务实现的经典手段。但它有个边界要注意它判断的是BeanDefinition阶段容器中是否已经注册了目标类型的Bean。如果使用方是在一个Configuration里通过Bean方法返回一个接口的实现类判断通常没有问题但如果使用方的Bean是动态注册或者用了Import时机就会变得非常微妙可能造成两边都创建Bean。我的建议是Starter对外尽量定义抽象接口让使用者通过实现接口并把自己定义为某类型Bean来覆盖。比如我这个TraceFilter可以抽象出一个TraceIdGenerator接口自动配置里定义默认实现并用ConditionalOnMissingBean注解。这样只要使用方注册了自定义TraceIdGenerator框架就自动放弃默认实现两边完全解耦。4.3 属性绑定不生效多半是松散绑定和元数据的问题ConfigurationProperties有一个特性叫Relaxed Binding就是配置文件里的header-name和headerName会映射到同一个字段。这大大方便了使用方但也容易让人忽略另一个问题IDE根本不会给你任何属性提示除非你在pom里引入spring-boot-configuration-processor依赖。没有这个依赖使用方只能翻文档才能知道有哪些属性可配体验非常差。而且这个processor在编译时时会生成一个spring-configuration-metadata.json被Spring Boot的配置处理机制读取。所以我在每个Starter的autoconfigure模块里都会加上dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-configuration-processor/artifactId optionaltrue/optional /dependency这样别人在application.yml里写前缀时编辑器能自动补全属性和默认值说明。4.4 自动配置排序失败Bean还没准备好自动配置类之间是有依赖顺序的。比如我的TraceFilter如果要在日志里打印请求体就需要一个ObjectMapper。而Jackson的自动配置类和我自己写的配置类执行顺序不一定我优先。虽然Spring容器在注入ObjectMapper时会延迟解析但如果我在自动配置方法内部通过BeanFactory去拿它或者写了某个ConditionalOnBean顺序就变得关键了。解决办法是给自动配置类加排序注解AutoConfiguration AutoConfigureAfter(JacksonAutoConfiguration.class) public class ApiTraceAutoConfiguration { }也可以用AutoConfigureOrder设置全局顺序。但能不用ConditionalOnBean就尽量不用因为这类条件在很大程度上依赖Bean的注册顺序容易产生循环依赖或判断结果不稳定。我在实际开发中更倾向于把依赖对象作为Bean方法的参数传进去让容器在创建Bean时自动保证依赖就绪。4.5 IDEA社区版创建Spring Boot项目的正确姿势很多同学用的是IDEA社区版它跟商业版最大的差别是没有Spring Initializr按钮。但创建Spring Boot项目并不难手动Maven工程加三样东西就行。首先在pom里引入父工程和starterparent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent然后加依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency接着写启动类然后点击运行Main方法即可。也可以用命令行mvn spring-boot:runIDEA社区版完全能胜任Spring Boot日常开发只是少了自动生成项目这一步而已。另一个办法是到start.spring.io网页上生成一个zip包解压后用IDEA打开这样更省事。5. 把Starter做厚自动装配结合Actuator做可观测性5.1 为什么Starter应该自带监控能力Starter一旦被接入生产项目它就不再只是“一个人干的活”而是运行在别人系统里的公共组件。如果组件没有可观测性出了问题根本不知道是它导致的还是外部原因。很多团队会把Spring Boot Admin直接接进来但Admin能展示的内容基本都是依赖Actuator暴露的端点。所以我的原则是凡是自动装配对外提供了运行时能力的Starter都应该在classpath存在Actuator时自动补充健康检查和基础指标。这样使用方只要加一个spring-boot-starter-actuator监控数据就自动出现不需要额外配置。5.2 用HealthIndicator和Metrics让Starter状态可观测还拿TraceFilter举例我可以给这个Starter增加一个健康检查用来上报过滤器的运行状态和最近一段时间的调用量。此时只需要在自动配置类里再加一个配置方法并用条件注解控制Bean ConditionalOnClass(name org.springframework.boot.actuate.health.HealthIndicator) public HealthIndicator apiTraceHealthIndicator() { return () - Health.up() .withDetail(filterEnabled, properties.isEnabled()) .withDetail(headers, properties.getHeaderName()) .build(); }注意这里我用了name属性而不是直接写HealthIndicator.class是因为Starter的编译期可能没有引入Actuator依赖直接引用这个类会导致编译失败。使用字符串类名既能让ConditionalOnClass判断又不影响编译。如果想导出计数器指标可以在自动配置类里注入MeterRegistryBean ConditionalOnClass(name io.micrometer.core.instrument.MeterRegistry) public MeterBinder apiTraceMetrics() { return registry - { Counter.builder(api.trace.request.total) .description(Total trace requests) .register(registry); }; }使用方引入Actuator后这些指标会出现在/actuator/metrics/api.trace.request.total端点上接Prometheus、Grafana都非常方便。5.3 利用条件注解导出可选的监控端点这种“有Actuator我就提供Actuator能力没有我也不报错”的做法正是自动装配思想的延伸。它是有条件地装配而不是把监控强行塞进每一个使用方。实现上只需要两步在Starter的pom里对spring-boot-starter-actuator依赖声明为optionaltrue/optional。在自动配置类里用ConditionalOnClass(name org.springframework.boot.actuate.endpoint.annotation.Endpoint)或更具体的条件控制监控相关的Bean只在Actuator存在时才创建。这样Starter既能保持最小依赖又能在需要的时候自动升级为一个“可观测组件”。6. 最后聊两句实战心得自动装配这套机制我到底花了多久才真正吃透说起来有点丢人前几年我也只是停留在会用Starter的阶段直到有一次自己的自动配置类无缘无故失效我被迫打开了Conditions Evaluation Report才把整条链路从AutoConfigurationImportSelector开始捋清楚。从那以后我再也没有在“配置类为什么不生效”这种问题上浪费过时间。我现在判断一个Starter好不好用只看三点默认值是不是合理、扩展点是否清晰、有没有借助条件注解自动适配运行环境。自动装配的核心价值不是“把所有Bean一股脑塞给用户”而是“按条件选择最合适的默认实现同时把覆盖能力留给使用者”。把这一点想明白写Starter就像搭积木一样自然。
返回列表