Spring扩展
开篇:Spring 的扩展能力有多强?
很多人把 Spring 当成一个"IOC 容器"来用,但它真正强大的地方在于——几乎每个环节都留了"钩子"让你插手。就像一条工业流水线,Spring 不仅让你用它的产品,还允许你在流水线的任意位置加装自己的设备。
本篇聚焦 Spring 最实用的扩展能力:扩展点、事件机制、设计模式、条件注解。掌握这些,你才能从"Spring 使用者"进化为"Spring 驾驭者"。
一、常用扩展点
Spring Bean 从诞生到销毁,经历了一条完整的生命周期。在这条线上,Spring 留了好几个口子让你介入。
1.1 BeanFactoryPostProcessor:改 Bean 定义
这个扩展点在所有 Bean 实例化之前触发。你拿到的是 BeanDefinition(Bean 的"图纸"),可以修改属性、改类名、甚至删掉某个 Bean。
最典型的内置实现是 PropertySourcesPlaceholderConfigurer——它把 ${xxx} 占位符替换成配置文件中的真实值,就是在这个阶段完成的。
另一个经典应用:自定义属性编辑器。比如你想让 Spring 把字符串 "北京_海淀_中关村" 自动转换成 Address 对象:
public class AddressPropertyEditor extends PropertyEditorSupport {
@Override
public void setAsText(String text) {
String[] parts = text.split("_");
Address addr = new Address();
addr.setProvince(parts[0]);
addr.setCity(parts[1]);
addr.setTown(parts[2]);
setValue(addr);
}
}通过 CustomEditorConfigurer(它本身就是一个 BeanFactoryPostProcessor)注册进去,Spring 就能自动完成类型转换了。
public class AddressEditorRegistrar implements PropertyEditorRegistrar {
@Override
public void registerCustomEditors(PropertyEditorRegistry registry) {
registry.registerCustomEditor(Address.class, new AddressPropertyEditor());
}
}XML 配置中注册:
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="propertyEditorRegistrars">
<list>
<bean class="com.example.AddressEditorRegistrar"/>
</list>
</property>
</bean>这样在 XML 中配置 <property name="address" value="北京_海淀_中关村"/> 就能自动转换了。
1.2 BeanPostProcessor:改 Bean 实例
这个扩展点在 Bean 实例化之后触发,包含 before 和 after 两个方法。Spring AOP 的代理对象就是在 postProcessAfterInitialization 中创建的。
实战示例——统计方法调用次数:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodCallCount {}
@Aspect
@Component
public class MethodCallCounterAspect {
private final Map<String, AtomicInteger> callCounts = new ConcurrentHashMap<>();
@Around("@annotation(MethodCallCount)")
public Object count(ProceedingJoinPoint pjp) throws Throwable {
String methodName = pjp.getSignature().getName();
try {
return pjp.proceed();
} finally {
callCounts.computeIfAbsent(methodName, k -> new AtomicInteger(0))
.incrementAndGet();
}
}
public int getCount(String methodName) {
AtomicInteger counter = callCounts.get(methodName);
return counter != null ? counter.get() : 0;
}
}使用时只需在方法上加注解:
@MethodCallCount
public OrderResponse createOrder(OrderRequest request) {
// 业务逻辑
}注意:这个统计结果只在内存中,应用重启就归零。如果需要持久化,可以存到 Redis。高并发场景下可以用 LongAdder 替代 AtomicInteger。
1.3 InitializingBean 与 @PostConstruct:Bean 初始化回调
两者功能类似,都在 Bean 属性注入完成后执行。区别在于 @PostConstruct 是 JSR-250 标准注解,InitializingBean 是 Spring 接口。执行顺序是 @PostConstruct 先于 InitializingBean.afterPropertiesSet()。
常见用途——缓存预热:
@Component
public class CachePreloader implements InitializingBean {
@Autowired
private ProductService productService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public void afterPropertiesSet() {
// 应用启动时加载热点商品到缓存
List<Product> hotProducts = productService.findHotProducts();
hotProducts.forEach(p ->
redisTemplate.opsForValue().set("product:" + p.getId(), p)
);
}
}用 @PostConstruct 也能达到同样效果:
@Component
public class CachePreloader {
@PostConstruct
public void preload() {
// 加载热点数据到缓存
}
}1.4 ApplicationRunner 与 CommandLineRunner:应用启动后执行
和上面的区别是,这两个是在整个 Spring 容器初始化完成后才触发,所有 Bean 都已就绪,更适合做依赖其他服务的初始化操作。
@Component
public class StartupRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
// 所有 Bean 已就绪,可以安全地做初始化
// 比如注册到注册中心、启动消费者等
}
}两者的区别很小:CommandLineRunner 的参数是 String... args,ApplicationRunner 的参数是封装后的 ApplicationArguments,后者多了按名称取参数的能力。
它们在 SpringApplication.run() 的最后一步被调用:
// SpringApplication 源码片段
private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList<>();
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
AnnotationAwareOrderComparator.sort(runners);
// 依次调用...
}可以通过 @Order 注解控制多个 Runner 的执行顺序。
1.5 Shutdown Hook:优雅关闭
Spring 向 JVM 注册了 shutdown hook,在应用关闭时依次调用 @PreDestroy 方法和 DisposableBean.destroy()。很多中间件的优雅下线(比如 Dubbo 取消注册)就是基于这个机制。
@Component
public class GracefulShutdown implements DisposableBean {
@Override
public void destroy() {
// 释放资源、关闭连接池、取消注册...
}
}也可以监听 ContextClosedEvent 来实现同样的效果:
@Component
public class ShutdownListener implements ApplicationListener<ContextClosedEvent> {
@Override
public void onApplicationEvent(ContextClosedEvent event) {
// 做容器关闭前的清理工作
}
}二、事件机制
Spring 的事件机制是观察者模式的框架级实现。它最大的价值是解耦:发布者不需要知道谁在监听,监听者不需要知道谁在发布。
2.1 三要素
| 角色 | 说明 |
|---|---|
| 事件 | 继承 ApplicationEvent 的 POJO,封装事件数据 |
| 发布者 | 注入 ApplicationEventPublisher,调用 publishEvent() |
| 监听者 | 用 @EventListener 标注方法,或实现 ApplicationListener 接口 |
2.2 完整示例
场景:用户注册成功后,要发欢迎短信、记录日志、发站内信。如果在注册方法里直接调用这三个服务,耦合度太高。用事件解耦:
// 1. 定义事件
public class RegisterSuccessEvent extends ApplicationEvent {
public RegisterSuccessEvent(RegisterInfo info) {
super(info);
}
}
// 2. 发布事件
@Service
public class RegisterService {
@Autowired
private ApplicationEventPublisher publisher;
public void register(RegisterInfo info) {
// 核心注册逻辑...
publisher.publishEvent(new RegisterSuccessEvent(info));
}
}
// 3. 多个监听者各司其职
@Component
public class SmsListener {
@EventListener(RegisterSuccessEvent.class)
public void onRegister(RegisterSuccessEvent event) {
RegisterInfo info = (RegisterInfo) event.getSource();
// 发送欢迎短信
}
}
@Component
public class LogListener {
@EventListener(RegisterSuccessEvent.class)
public void onRegister(RegisterSuccessEvent event) {
// 记录注册日志
}
}
@Component
public class NotificationListener {
@EventListener(RegisterSuccessEvent.class)
public void onRegister(RegisterSuccessEvent event) {
// 发送站内信
}
}后续如果需要新增"发放新人优惠券"的逻辑,只需要再写一个 Listener,注册服务完全不用改。这就是事件驱动的好处。
2.3 同步 vs 异步
默认情况下,@EventListener 是同步的——发布者会等所有监听者执行完才返回。如果某个监听者耗时较长(如发短信),会拖慢主流程。
加上 @Async 即可异步执行:
@EventListener(RegisterSuccessEvent.class)
@Async("registerExecutor") // 指定线程池
public void onRegister(RegisterSuccessEvent event) {
// 异步发送短信
}注意:不要直接用默认的 @Async,它底层用的 SimpleAsyncTaskExecutor 不复用线程,每次都 new Thread(),高并发下会出问题。务必自定义线程池:
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("registerExecutor")
public Executor registerExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("register-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}Spring 在获取默认线程池时,会先尝试从容器中找唯一的 TaskExecutor;找不到就找 BeanName 为 taskExecutor 的;都没有才创建 SimpleAsyncTaskExecutor。所以如果你定义了唯一一个线程池 Bean,不指定名字也能生效——但这不靠谱,万一后来又加了一个就失效了。显式指定最稳妥。
2.4 事务绑定的事件
有时候你希望事件在事务提交后才触发(比如注册失败回滚了,就不应该发欢迎短信)。用 @TransactionalEventListener:
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onRegister(RegisterSuccessEvent event) {
// 只在事务成功提交后才执行
}2.5 Spring Event 的优势
| 特性 | 说明 |
|---|---|
| 解耦 | 发布者和监听者互不依赖,新增监听者零修改 |
| 一对多 | 一个事件可以有任意多个监听者 |
| 异步支持 | 配合 @Async 实现非阻塞 |
| 事务绑定 | @TransactionalEventListener 保证事务一致性 |
| 职责清晰 | 每个监听者只关心自己的业务 |
三、Spring 中的设计模式
Spring 框架本身就是设计模式的教科书。这里梳理最高频的几个。
3.1 工厂模式
IOC 容器就是最大的工厂。你只需要声明"我要什么"(配置/注解),容器帮你创建、组装、管理。BeanFactory 和 ApplicationContext 就是工厂接口。
3.2 单例模式
Spring Bean 默认就是单例的,保证对象复用和线程安全。除此之外还有 prototype(每次获取新实例)、request(每个 HTTP 请求一个)、session(每个会话一个)等作用域。
3.3 代理模式
AOP 的核心。Spring 用 JDK 动态代理(基于接口)或 CGLIB(基于继承)为 Bean 创建代理对象,在不修改原始代码的前提下增加日志、事务、权限等横切逻辑。
实战中最常见的三种 AOP 用法:
// 1. 参数校验
@Around("@annotation(ParamsCheck)")
public Object validate(ProceedingJoinPoint pjp) throws Throwable {
for (Object arg : pjp.getArgs()) {
if (arg == null) throw new IllegalArgumentException("参数不能为空");
}
return pjp.proceed();
}
// 2. 缓存
@Around("@annotation(Cacheable)")
public Object cache(ProceedingJoinPoint pjp) throws Throwable {
// 先查缓存,命中直接返回;未命中则执行方法并存缓存
return pjp.proceed();
}
// 3. 操作日志
@Around("@annotation(OpLog)")
public Object log(ProceedingJoinPoint pjp) throws Throwable {
Object result = pjp.proceed();
logger.info("method={}, result={}", pjp.getSignature().getName(), result);
return result;
}3.4 模板方法模式
TransactionTemplate 把事务操作固定为三步:执行 -> 异常回滚 -> 正常提交。你只需要实现"执行"这一步:
transactionTemplate.execute(status -> {
// 你的业务逻辑
return result;
});注意:事务中不要做 RPC 调用或操作分布式缓存,一是 RT 长会导致长事务,二是事务回滚也回滚不了 RPC。JdbcTemplate、RedisTemplate 也是模板方法的应用。
3.5 观察者模式
就是前面讲的事件机制。ApplicationEvent + ApplicationListener 是标准的观察者实现。
3.6 适配器模式
SpringMVC 中的 HandlerAdapter。DispatcherServlet 不直接调用 Controller,而是通过 HandlerAdapter 适配不同类型的处理器。RequestMappingHandlerAdapter 负责处理 @Controller 注解的方法,其他类型用其他适配器。
3.7 责任链模式
SpringMVC 的拦截器(HandlerInterceptor)和 Spring Security 的过滤器链都是责任链模式。请求像链条一样依次经过每个节点,任意节点都可以中断链条。
3.8 策略模式
借助 IOC 实现优雅的策略模式,告别 if-else:
interface PayStrategy extends InitializingBean {
void pay();
Scene getScene();
@Override
default void afterPropertiesSet() {
PayFactory.register(getScene(), this);
}
}
@Component
class WechatPay implements PayStrategy {
public void pay() { /* 微信支付 */ }
public Scene getScene() { return Scene.WECHAT; }
}
@Component
class Alipay implements PayStrategy {
public void pay() { /* 支付宝支付 */ }
public Scene getScene() { return Scene.ALIPAY; }
}
// 调用方一行代码,不用 if-else
PayFactory.get(scene).pay();SpringMVC 中的 HandlerMethodArgumentResolver 也是策略模式——不同的参数类型用不同的解析器。
四、条件注解
Spring Boot 的"自动配置"魔法,背后靠的就是条件注解。它们控制一个 @Configuration 或 @Bean 在什么条件下才生效。
| 注解 | 含义 |
|---|---|
@ConditionalOnClass | 类路径下存在指定类时生效 |
@ConditionalOnMissingBean | 容器中不存在指定 Bean 时生效 |
@ConditionalOnProperty | 配置文件中存在指定属性时生效 |
@ConditionalOnBean | 容器中存在指定 Bean 时生效 |
@ConditionalOnWebApplication | 当前是 Web 应用时生效 |
例如,Spring Boot 的 DataSourceAutoConfiguration 之所以能自动配置数据源,是因为它加了 @ConditionalOnClass(DataSource.class)——只有引入了数据库驱动依赖,这个配置才会生效。
这也解释了为什么 Spring Boot 能做到"引入 starter 就自动配置":starter 里的自动配置类都用条件注解守着,只有真正需要时才会生效。
你也可以自定义条件注解,实现 Condition 接口即可:
public class OnLinuxCondition implements Condition {
@Override
public boolean matches(ConditionContext ctx, AnnotatedTypeMetadata meta) {
return ctx.getEnvironment().getProperty("os.name", "")
.toLowerCase().contains("linux");
}
}
@Configuration
@Conditional(OnLinuxCondition.class)
public class LinuxSpecificConfig {
// 仅在 Linux 环境下生效的配置
}五、常见面试题精选
Q1:Spring 中用到了哪些设计模式?
至少可以说出七八个:工厂模式(IOC 容器)、单例模式(Bean 默认作用域)、代理模式(AOP)、模板方法(JdbcTemplate / TransactionTemplate)、观察者模式(Event 机制)、适配器模式(HandlerAdapter)、责任链模式(拦截器/过滤器链)、策略模式(HandlerMethodArgumentResolver)。关键是结合源码说出具体在哪里用的,而不是泛泛而谈。
Q2:为什么不建议直接使用 @Async?
因为默认用的 SimpleAsyncTaskExecutor 不复用线程,每次都 new Thread(),高并发下会耗尽系统资源。正确做法是自定义 ThreadPoolTaskExecutor 并在 @Async("beanName") 中指定。如果只定义了一个 TaskExecutor Bean,Spring 会自动使用它;但如果定义了多个又没指定,就会退回到默认的,所以显式指定最稳妥。
Q3:@Retryable 的实现原理?
Spring Retry(Spring 7 已内置)通过 BeanPostProcessor 扫描带 @Retryable 的方法,为其创建 AOP 代理。代理内部使用 RetryTemplate 执行重试逻辑:首次执行失败后进入循环,检查重试策略(次数、异常类型)-> 退避等待(Thread.sleep)-> 再次执行。重试用尽后抛出 RetryException,可配合 @Recover 做降级。注意这是 JVM 级别的重试,应用重启后任务丢失。
Q4:如何在 Spring 启动时做缓存预热?
四种方式:(1) 监听 ApplicationReadyEvent;(2) 实现 CommandLineRunner 或 ApplicationRunner;(3) 实现 InitializingBean;(4) 使用 @PostConstruct。推荐用 Runner,因为此时所有 Bean 已就绪,最安全。注意区分执行时机:@PostConstruct 在当前 Bean 初始化后就执行(其他 Bean 可能还没就绪),而 Runner 在所有 Bean 都初始化完后才执行。
Q5:@Scheduled 的原理?和 XXL-JOB 有什么区别?
ScheduledAnnotationBeanPostProcessor 扫描 @Scheduled 方法,注册到 ScheduledTaskRegistrar,由 TaskScheduler 调度执行。Spring 6.1 之前默认单线程池,之后需要显式配置 ThreadPoolTaskScheduler。支持 fixedRate(固定频率)、fixedDelay(固定延迟)和 cron 三种模式。@Scheduled 是单机轻量级方案,适合定时清理日志、缓存同步等场景;XXL-JOB 是分布式调度系统,支持任务分片、失败重试、可视化管理,适合大规模定时任务如关闭未支付订单、计算用户活跃度。
小结
Spring 的扩展能力覆盖了 Bean 生命周期的每个阶段:
- 定义阶段:
BeanFactoryPostProcessor修改 Bean 图纸 - 实例化阶段:
BeanPostProcessor增强 Bean 实例(AOP 在这里发生) - 初始化阶段:
@PostConstruct/InitializingBean做初始化回调 - 启动完成:
ApplicationRunner/CommandLineRunner做全局初始化 - 运行阶段:事件机制解耦模块通信,条件注解控制配置生效
- 销毁阶段:
@PreDestroy/DisposableBean做资源释放
理解这些扩展点,就理解了 Spring Boot "自动配置"的底层逻辑,也能在实际项目中写出更优雅的代码。