1. 为什么我最终放弃了Spring XML配置
先聊点实际的。我最早接触Spring的时候,项目里到处是applicationContext.xml、spring-mvc.xml、spring-mybatis.xml这些文件,一个稍微上点规模的项目,配置文件能堆出几百上千行。那时候团队里有一个不成文的规矩:谁动spring-context.xml里的bean定义,必须在上线群里吼一声,因为一不小心覆盖了别人的bean id,轻则启动报错,重则线上故障。这种“配置文件即雷区”的开发体验,相信不少老Javaer都经历过。
后来Spring 3.0开始完善注解支持,Spring Boot进一步把“约定大于配置”推向极致,纯注解配置才真正成为主流。我现在新起的项目,基本不再写XML配置文件,全部用@Configuration、@ComponentScan、@Bean这些注解搞定。标题里说的“纯注解配置”,指的是完全不用XML来描述bean的创建和依赖关系,所有逻辑都用Java类和注解表达。
这样做带来的改变是直观的:配置从“字符串拼接”变成了“类型安全的Java代码”,IDE的自动补全、重构、编译期检查全部能用上。以前XML里写错一个bean的类名,要等容器启动到那一行才报错;现在注解方式下,大部分错误在编译阶段就能暴露。
更重要的是,注解配置让项目的结构变得清晰。一个@Configuration类就是一个小型模块的组装车间,@Bean方法就是车间的工位,每个工位负责生产一个组件。这种表达方式比XML里一堆<bean>标签要直观得多,也更容易做代码审查。
当然,纯注解不是万能的,后文我会专门说哪些场景下XML仍然有意义。先不急着下结论,我们把核心的注解用法和底层逻辑拆开来看。
2. 常用注解的职责边界:谁负责扫描,谁负责装配
很多新手容易把各种注解混为一谈,其实可以从“对象生命周期管理”的角度把它们分组:第一类是告诉容器“有哪些组件需要被管理”的注解,第二类是告诉容器“组件之间的依赖关系怎么建立”的注解,第三类是“怎么创建、何时销毁”的定制化注解。分清楚这三类,注解配置的骨架就立住了。
2.1 组件登记类注解:@Component及其衍生注解
@Component是组件登记的根注解,它下面衍生出了@Controller、@Service、@Repository。这三个衍生注解在功能上和@Component没有本质区别,都是把类注册成Spring容器管理的bean,但语义上更清晰:
@Controller:表现层,处理Web请求@Service:业务逻辑层,承载核心业务@Repository:数据访问层,负责数据库操作
不过要提醒一句,如果项目里分层不严格,用@Component也是完全没问题的。衍生注解最大的价值是配合AOP做切面筛选。比如用@Pointcut("within(@org.springframework.stereotype.Service *)")可以精准切入所有Service层的类,如果都用@Component,这个切面就不好写了。
使用组件登记注解的前提是开启扫描。纯注解方案下,启动类或配置类上一般会标注@ComponentScan,指定要扫描的基础包路径。Spring Boot里这个工作被@SpringBootApplication自动做了,默认扫描启动类所在包及其子包。
2.2 依赖注入类注解:@Autowired、@Resource与@Value的取舍
组件登记完成之后,接下来就是装配。@Autowired是Spring家族的注解,按类型注入。它有几个值得注意的细节:
- 默认情况下,
@Autowired要求待注入的bean必须存在,否则启动报错。如果某个依赖允许缺失,需要设置required = false。 - 当容器中存在多个同类型的bean时,
@Autowired会优先匹配@Primary标记的bean,否则根据字段名和bean的id匹配,再不行就抛NoUniqueBeanDefinitionException。
@Resource是JDK自带的注解,按名称注入。它和@Autowired的差异在很多面试题里都出现过,但在实际项目中,我更倾向于统一使用@Autowired。原因很简单:同一个项目里混用两种注入方式,会让人困惑到底该按照什么规则去找bean,排查问题时容易多绕一圈。
@Value则用来注入配置值,支持SpEL表达式,比如:
@Value("${server.port:8080}") private Integer port;这个写法能从application.properties或application.yml中读取server.port,读不到就用默认值8080。用纯注解配置之后,很多原本写在XML里的<property name="xxx" value="yyy"/>就由@Value承担了。
2.3 配置类与Bean方法:@Configuration、@Bean、@Import的组合拳
@Configuration注解是纯注解配置的核心载体。标注了@Configuration的类,内部可以用@Bean方法创建bean实例。需要特别注意的是,@Configuration类会被Spring通过CGLIB动态代理增强,确保@Bean方法只被调用一次,返回的实例是单例的。
举个例子来说明:
@Configuration public class DataSourceConfig { @Bean public DataSource dataSource() { DruidDataSource ds = new DruidDataSource(); ds.setUrl("jdbc:mysql://localhost:3306/test"); ds.setUsername("root"); ds.setPassword("123456"); return ds; } }这样定义之后,其他地方只要@Autowired一个DataSource,Spring就会调用dataSource()方法拿实例。如果你在dataSource()方法里做了一些依赖其他bean的操作,比如:
@Bean public DataSource dataSource() { DataSource ds = new DruidDataSource(); ds.setUrl(environment.getProperty("jdbc.url")); return ds; }这里的environment如果是通过构造器或参数注入进来的,都没问题。但要注意,@Configuration类的普通方法修饰符必须是public或protected,不能是private或final,否则CGLIB无法增强。
@Import注解用来快速引入其他配置类或普通类。如果一个配置类A依赖配置类B的bean,可以在A上标注@Import(B.class),这样加载A时B也会被一并处理。这个注解在模块化开发中很常用,比在@ComponentScan里增加扫描路径更精确。
2.4 生命周期回调:@PostConstruct和@PreDestroy
XML时代通过<bean init-method="init" destroy-method="destroy"/>指定初始化和销毁方法。纯注解方案下,@PostConstruct和@PreDestroy承担了同样的职责,而且是JSR-250规范,不绑定Spring。
@Service public class OrderService { @PostConstruct public void init() { // 初始化连接池、加载缓存等 } @PreDestroy public void destroy() { // 释放资源、关闭连接 } }这套注解的语义比Spring自带的InitializingBean、DisposableBean接口更简洁。不过要注意,@PreDestroy只在Spring容器正常关闭时才会触发,如果进程被强杀,是没机会执行的。所以在@PreDestroy里只做常规的优雅释放即可,不要依赖它做关键的数据持久化。
3. 纯注解方案的底层运作逻辑:配置类是怎么被处理的
单纯会用注解还不够,搞明白容器内部怎么处理这些注解,排查问题时会稳很多。很多时候线上报了一个“NoSuchBeanDefinitionException”,不是代码写错了,而是对容器的处理时机理解偏差。
3.1 ConfigurationClassPostProcessor的介入时机
Spring容器启动时,ConfigurationClassPostProcessor是一个非常重要的BeanDefinitionRegistryPostProcessor。它的职责是扫描并解析所有标注了@Configuration的类,把类里的@Bean方法转换成BeanDefinition,同时处理@ComponentScan、@Import、@ImportResource等注解。
这里有一个大家容易忽略的点:ConfigurationClassPostProcessor处理配置类是在普通bean实例化之前完成的。也就是说,配置类本身像是“元数据”,先把整个bean的蓝图(BeanDefinition)都整理好,后面的实例化、属性填充、初始化才是按照这些蓝图逐个执行。
理解了这一点,就能解释一个常见现象:为什么@Configuration类里的@Bean方法可以互相调用,而不用手动new对象?因为容器在解析阶段已经把整个配置类内部的方法调用关系都算进去了,执行@Bean方法时,如果发现参数或方法返回类型能关联到其他bean,Spring会直接去容器里取现成的实例,而不是重新创建。
3.2 @Bean方法的单例保障机制
@Bean方法默认是单例的,这个单例保障靠的是@Configuration类被CGLIB代理。Spring在创建配置类代理时会拦截所有@Bean方法的调用,先去容器中查找是否已有对应bean,如果存在就直接返回已有实例,不存在才执行方法体创建新的。
所以下面这个写法是安全的:
@Configuration public class AppConfig { @Bean public Dog dog() { return new Dog(); } @Bean public Person person() { return new Person(dog()); } }虽然person()方法调用了dog(),但因为dog()被拦截和缓存,容器中始终只有一个Dog实例。
这里需要特别注意:如果配置类标注的是@Component而不是@Configuration,那么CGLIB增强不会生效,dog()方法每次调用都会创建新实例。这在某些场景下可能是有意为之的,但大多数情况下会引发隐蔽的单例失效问题。所以,如果一个类里同时存在多个@Bean方法,且方法之间有依赖关系,请务必使用@Configuration而不是@Component。
3.3 三级缓存与循环依赖的注解视角
很多人在Spring面试题里被问过三级缓存,这里从纯注解配置的角度简单说几句。循环依赖指的是A依赖B,B又依赖A。Spring通过三级缓存机制,在bean创建过程中提前暴露“早期引用”,从而打破循环。
但三级缓存不是万能的,它对构造器注入的循环依赖无能为力。原因是构造器注入要求在bean实例化阶段就拿到依赖,而此刻对象还没创建出来,早期引用无法暴露。所以如果你在纯注解环境下使用构造器注入,遇到了循环依赖问题,那大概率不是配置错误,而是设计问题。
我个人的建议是:
- 字段注入和setter注入可以容忍部分循环依赖,但会掩盖设计问题,尽量少用。
- 构造器注入更符合不可变性和依赖清晰的原则,但结构上要避免循环。
- 如果A和B之间确实需要互相协作,考虑引入中间层C,把双向依赖改造成单向依赖链。
4. 三个高频场景拆解:条件装配、属性绑定与自定义注解
4.1 @Conditional与@Profile实现条件装配
在实际项目中,经常要根据环境、配置项、类路径等因素决定是否创建某个bean。比如测试环境使用内存数据库,生产环境使用MySQL。纯注解方案下,条件装配有两种常用方式。
@Profile适合按环境维度做切换:
@Configuration @Profile("dev") public class DevDataSourceConfig { @Bean public DataSource dataSource() { // 返回内存数据库 } } @Configuration @Profile("prod") public class ProdDataSourceConfig { @Bean public DataSource dataSource() { // 返回MySQL数据源 } }启动时通过spring.profiles.active=dev或prod来激活对应的配置类。
@Conditional则更灵活,适用于基于任意条件的判断,例如:
@Bean @ConditionalOnProperty(name = "cache.enabled", havingValue = "true") public CacheManager cacheManager() { return new CaffeineCacheManager(); }在Spring Boot项目里,@ConditionalOnProperty、@ConditionalOnClass这些条件注解使用频率很高,它们本质上也是@Conditional的衍生实现。写代码时要注意,条件注解判断的是容器的ConditionContext和AnnotatedTypeMetadata,不要在里面执行过重的IO或网络操作,否则会拖慢启动速度。
4.2 @ConfigurationProperties做配置项分组绑定
@Value适合单个配置项的注入,但当一个模块有多项配置时,建议用@ConfigurationProperties把配置集中到一个类里管理。
@Component @ConfigurationProperties(prefix = "app.oss") public class OssProperties { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; // getter/setter省略 }配置文件里这样写:
app.oss.endpoint=https://oss-cn-hangzhou.aliyuncs.com app.oss.access-key-id=xxx app.oss.access-key-secret=yyy app.oss.bucket-name=my-bucket这样做的好处是:配置字段和业务字段分开,类型转换由Spring自动完成,IDE能自动提示属性名。相比散落的@Value,这种集中式管理的可维护性高了一个档次。
要注意一个坑:@ConfigurationProperties默认要求配置类上有getter和setter方法,Spring Boot 2.2之后的版本可以用@ConfigurationPropertiesScan配合@ConstructorBinding做不可变绑定。如果配置项很多,建议在配置类上增加@Validated注解,配合javax.validation约束做参数校验。
4.3 自定义注解配合AOP实现通用能力
纯注解开发中,自定义注解是非常实用的扩展点。比如要记录某些方法的执行耗时,可以自己定义一个注解:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface LogCostTime { String describe() default ""; }然后用AOP实现切面:
@Aspect @Component public class CostTimeAspect { @Around("@annotation(logCostTime)") public Object around(ProceedingJoinPoint joinPoint, LogCostTime logCostTime) throws Throwable { long start = System.currentTimeMillis(); Object result = joinPoint.proceed(); long cost = System.currentTimeMillis() - start; System.out.println(logCostTime.describe() + "耗时" + cost + "ms"); return result; } }有朋友在热词里提到“自定义注解属性如何使用SpEL表达式”,这个需求很常见。比如注解上写了一个属性,想在切面里根据方法参数动态计算,可以借助SpelExpressionParser:
@Around("@annotation(cacheKey)") public Object handleCache(ProceedingJoinPoint joinPoint, CacheKey cacheKey) throws Throwable { String expression = cacheKey.value(); MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Object[] args = joinPoint.getArgs(); String[] paramNames = signature.getParameterNames(); StandardEvaluationContext context = new StandardEvaluationContext(); for (int i = 0; i < paramNames.length; i++) { context.setVariable(paramNames[i], args[i]); } String key = parser.parseExpression(expression).getValue(context, String.class); // 后续缓存逻辑 }这样在方法上写@CacheKey("#orderId + ':' + #userId")就能动态生成缓存key。这种“注解+SpEL”的组合模式在缓存、权限校验、日志埋点中非常常见,值得收藏一份。
5. 从XML迁移到注解时最容易踩的坑
纯注解不是银弹,迁移过程中有些坑几乎是每个人都会遇到的。下面按我个人经验,把它们按发生频率排个序。
5.1 扫描路径不当导致的Bean缺失
@ComponentScan的basePackage如果设置得太宽,会把无关的类也扫进来,轻则多生成一些无用的bean,重则因为类加载顺序引发意外冲突。设置得太窄,业务类不在扫描范围内,启动时就会报找不到bean的错。
Spring Boot默认扫描启动类所在包及子包,所以启动类的位置要放在项目包结构的根上。如果模块比较复杂,可以用多个配置类分别扫描不同包,比如:
@Configuration @ComponentScan(basePackages = {"com.example.core", "com.example.common"}) public class CoreAutoConfiguration { }建议在写@ComponentScan时,把包路径写具体一点,不要图省事直接扫根路径。项目后期的维护成本会告诉你这个选择有多重要。
5.2 @Configuration类被CGLIB代理后的“假象”
前面提到@Configuration类会被CGLIB增强,这意味着配置类里的方法不能是private或final。如果你把@Bean方法写成private,Spring Boot 2.x版本启动时会直接报错:
@Bean method must not be private or final这个问题在IDE里往往不会提前提示,只有真正启动时才暴露。还有一种更隐蔽的情况:配置类里如果定义了一些内部私有方法,而这些方法被@Bean方法调用,需要注意代理的生效范围。代理只会拦截@Bean方法的调用,内部普通方法的调用不在拦截范围内,所以不会出现配置方法互相调用时被重复执行的诡异问题。
5.3 @Autowired的required与多个实现类的冲突
当一个接口有多个实现类时,@Autowired会因不知道注入哪个而报错。经常看到的解决方案是加@Qualifier("beanName")明确指定名称,或者在某一个实现类上标注@Primary表示优先选择。
但在实际项目中,我更推荐在构造器上使用@Qualifier:
@Service public class OrderServiceImpl implements OrderService { private final PaymentService paymentService; public OrderServiceImpl(@Qualifier("wechatPayService") PaymentService paymentService) { this.paymentService = paymentService; } }这样写的好处是,依赖关系在构造时就已经确定,后续不容易被修改。同时IDE的代码导航也更清晰,能直接跳到具体的实现类。
另外注意required属性:如果某个依赖可选,设置required = false后,@Autowired注入的位置一定要做null判断,否则调用时会抛NPE。更好的做法是用Optional<T>或者ObjectProvider<T>接收,代码更安全。
5.4 多模块项目的配置文件炸裂问题
纯注解配置不代表没有配置,它只是把“配置描述”从XML换成了Java类。在多模块项目中,最怕的是每个模块都写一个@Configuration类,然后互相@Import,导致依赖关系混乱。
我见过一个项目,启动类上堆了十几个@Import,每次新增模块都要手工加一行,非常痛苦。后来我们用Spring Boot的AutoConfiguration机制,通过META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件声明自动配置类,配合@ConditionalOnClass、@ConditionalOnMissingBean等条件注解,模块间彻底解耦了。这个思路可以沿用:不要把配置类手动装配写死在代码里,而是让容器按条件自动加载。
5.5 AOP代理与@Transactional的经典坑
纯注解环境下,事务管理一般用@Transactional。这个注解本身不难,但和AOP代理混在一起时容易出问题。
最典型的场景是:同一个类内部方法A调用方法B,B上有@Transactional,结果事务没生效。原因是this.methodB()调用不会经过代理对象,代理链上的事务拦截器根本没机会执行。正确做法是:
- 把方法B拆分到另一个Service里,通过注入的代理对象调用。
- 或者通过
AopContext.currentProxy()获取当前代理对象再调用。
这个坑在XML时代也存在,但XML配置因为代码路径更少,问题容易被忽略。用了注解之后,事务边界散落在各个方法上,排查成本反而上去了,所以更要重视代理的生效范围。
另外,@Transactional默认只在RuntimeException和Error时回滚,受检异常不会触发回滚。如果业务中有受检异常也需要回滚,必须显式指定rollbackFor = Exception.class,否则数据会出现“半成功”状态。
6. 说句公道话:哪些场景保留XML反而更合适
纯注解配置是主流,但我不赞成“完全消灭XML”这种极端情绪。在实际开发中,有些场景用XML反而更直观、更好维护。
6.1 第三方SDK的Bean定义不可控时
很多第三方SDK的类没有Spring注解,如果类里的配置项特别多,用@Bean方法一个个set参数会显得很啰嗦。这种情况用XML定义反而更紧凑:
<bean id="sdkClient" class="com.thirdparty.SdkClient"> <property name="appId" value="${sdk.app-id}"/> <property name="privateKey" value="${sdk.private-key}"/> </bean>不过好消息是,现在大多数主流SDK都提供了Spring Boot Starter,这类场景正在减少。如果你维护的是老项目,第三方SDK没有Starter,那么在XML里集中管理这些bean定义也无可厚非。
6.2 需要运维动态调整配置的遗留系统
如果项目交付后运维人员需要在不重新编译的情况下调整bean属性,XML比注解更友好。毕竟改XML文件不涉及编译打包,改完重启即可。而注解配置是写死在代码里的,任何调整都要走一次发布流程。
当然,现代项目大多通过配置中心(Apollo、Nacos、Spring Cloud Config)解决这个问题,配置动态刷新、灰度发布都能实现。但如果是传统企业的内部系统,没有引入配置中心的成本预算,保留部分XML做“运行时可变配置”是务实的做法。
6.3 多环境差异极大的复杂数据源配置
纯注解方案下,多环境配置主要通过@Profile区分。但如果你要面对的不是“dev/prod”两个环境,而是几十个定制化环境,每个环境的连接参数差异很大,XML里的占位符配合外置properties文件可能比Java配置更简单。这不是技术先进性的问题,而是维护成本的权衡。
7. 从XML迁到注解后的项目结构实操建议
迁移过程不是把XML里的<bean>改成@Bean就算完成,更关键的是重新梳理模块边界和依赖方向。以下是一套我验证过多次的改造思路。
7.1 分类型组织配置类,而不是一个类管到底
很多人把全部@Bean塞进一个AppConfig里,结果是这个类越来越臃肿,直到无人敢动。我建议按“基础设施层、业务组件层、接口适配层”三个维度拆分配置类:
- 基础设施层:
DataSourceConfig、CacheConfig、MQConfig - 业务组件层:
OrderModuleConfig、UserModuleConfig - 接口适配层:
WebMvcConfig、FeignConfig、WebSocketConfig
每个配置类只负责自己范围内的bean创建,通过@Import或自动配置机制按需装配。这样新增一个模块时,只需要添加一个新配置类,不用去动启动类。
7.2 善用@ImportSelector和@ImportBeanDefinitionRegistrar
如果你在写一个框架或通用组件,需要根据外部条件动态决定导入哪些配置,@ImportSelector比硬编码@Import更灵活。
public class MyImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[]{"com.example.config.FeatureConfig"}; } }再搭配一个自定义注解:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Import(MyImportSelector.class) public @interface EnableFeature { }使用方只需要在启动类上标注@EnableFeature,就能自动装配相关配置。这种模式参考了Spring Boot的@EnableXxx系列注解,比如@EnableAsync、@EnableScheduling。如果你是框架设计者,这套机制能让你的组件接入成本降到最低。
7.3 配置类使用package-private还是public
关于配置类的可见性,我的经验是:配置类其实是“模块的组装说明”,它应该尽量保持精简。Spring官方示例里配置类经常是public的,但在实际项目中,如果一个配置类只在包内被引用,可以考虑用package-private修饰,减少外部误用。
不过有个例外:如果你的配置类会被其他模块@Import,那么它必须是public且保证有可用的无参构造器。这一点在写通用组件时尤其要注意,否则使用方一旦引入,启动器就会因为无法实例化配置类而报错。
8. 注解配置的性能损耗和排查技巧
有人担心纯注解配置会让启动变慢,这确实是存在的,主要体现在启动期扫描和CGLIB代理创建上。但现代Spring Boot应用启动时间通常控制在几秒到十几秒,这点开销完全可以接受。真正要关注的不是注解本身的损耗,而是配置写得是否“精准”。
8.1 启动慢的常见原因
@ComponentScan扫描范围太大,把无关包也都扫了一遍。- 配置类里做了大量的
@ConfigurationProperties绑定和@PostConstruct初始化。 - 条件注解中触发了懒加载类的提前初始化。
排查启动慢的问题,可以在application.properties里开启调试日志:
debug=true或者使用Spring Boot Actuator的/actuator/metrics接口查看启动时长和bean初始化耗时。
8.2 如何快速定位Bean创建顺序的依赖关系
当配置类之间有复杂的@Bean方法相互注入时,你可以通过启动日志查看bean的创建顺序。Spring Boot启动时默认只打印少量日志,可以临时把日志级别调到DEBUG:
logging.level.org.springframework.beans.factory=DEBUG logging.level.org.springframework.context.annotation=DEBUG这样能看到每个配置类是如何被处理和加载的。如果遇到“BeanDefinitionOverrideException”,说明两个配置类都想定义同名bean,需要检查是不是有重复扫描或重复导入。
8.3 用Spring Boot的配置属性报告反查问题
配置项多的时候,想要确认某个配置有没有生效,可以访问/actuator/configprops和/actuator/env端点。如果你不想引入Actuator,也可以写一个简单的ApplicationRunner,在启动后打印关键配置值:
@Component public class ConfigPrinterRunner implements ApplicationRunner { private final OssProperties ossProperties; public ConfigPrinterRunner(OssProperties ossProperties) { this.ossProperties = ossProperties; } @Override public void run(ApplicationArguments args) { System.out.println("OSS endpoint: " + ossProperties.getEndpoint()); } }这种方法在排查“本地正常、生产环境配置不对”的问题时非常高效。
9. 一套可直接参考的纯注解配置骨架
写到这里,我整理一个简化的项目配置骨架,方便对照落地。这是一个典型的Spring Boot服务,不使用任何XML,全部注解驱动。
9.1 启动类
@SpringBootApplication @EnableConfigurationProperties(OssProperties.class) @EnableAsync public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }启动类只负责入口和顶层注解,细节配置全部下放到各个配置类。
9.2 数据源配置
@Configuration public class DataSourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource dataSource() { return DataSourceBuilder.create().build(); } }如果项目里引入了MyBatis,再单独写MyBatis配置:
@Configuration @MapperScan("com.example.mapper") public class MyBatisConfig { @Bean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); factoryBean.setTypeAliasesPackage("com.example.entity"); return factoryBean.getObject(); } }9.3 接口层配置
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/api/**") .excludePathPatterns("/api/login", "/api/register"); } @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("https://example.com") .allowedMethods("GET", "POST", "PUT", "DELETE"); } }9.4 定时任务与异步配置
@Configuration @EnableScheduling public class SchedulingConfig { } @Configuration @EnableAsync public class AsyncConfig implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(100); executor.initialize(); return executor; } }这套骨架已经覆盖了大多数中小型项目的需求。如果有更复杂的场景,比如需要整合Quartz、Redis、MQ,无非在对应配置类里增加@Bean方法即可,整体的组织方式保持一致。
个人实际项目中的体会是,纯注解配置最大的收益不是“少写了几个标签”,而是让配置本身变成代码的一部分,可以被版本管理、被review、被测试。它让Spring应用的结构从“配置文件驱动的黑盒”变成了“结构化代码驱动的透明系统”。但这套模式也要求团队每个成员都理解注解背后的容器机制,否则一旦出现问题,排查成本会显著上升。所以如果你想彻底转向纯注解开发,别只记注解用法,花点时间弄懂容器如何加载配置类、如何创建和代理bean,这笔投入一定是值得的。