问题描述
业务包引用了一个公共包,公共包里使用了@PostConstruct赋值属性,项目启动后报错,断点排查项目启动时未进入此方法,多方排查后,发现是启动类未添加@ComponentScan注解,导致类注册失败。
@Component
public class Test {
@Value("${basename:language}")
private String baseName;
private static final Locale LOCALE_ZH_CN = new Locale("zh", "CN");
private static final Locale LOCALE_EN_US = new Locale("en", "US");
private static ResourceBundle RESOURCE_ZH_CN;
private static ResourceBundle RESOURCE_EN_US;
@PostConstruct
private void init() {
String BASE_NAME = this.baseName;
RESOURCE_ZH_CN = ResourceBundle.getBundle(BASE_NAME, LOCALE_ZH_CN);
RESOURCE_EN_US = ResourceBundle.getBundle(BASE_NAME, LOCALE_EN_US);
}
}
原因
在Spring框架中,@ComponentScan 注解用于指定Spring在初始化时应该扫描哪些包以查找带有 @Component 及其衍生注解(如 @Service, @Repository, @Controller 等)的类。如果你没有在配置中明确添加 @ComponentScan 注解,Spring将默认只扫描主应用类(带有 @SpringBootApplication 注解的类)所在的包及其子包。
如果你的组件(即被 @Component 等注解标记的类)位于主应用类所在包的外部或不在其子包路径下,Spring将无法自动发现这些组件,因此这些组件不会被自动注册到Spring容器中。
扩展
当你使用 @SpringBootApplication 注解一个应用启动类时,这个注解实际上是一个复合注解,它包括 @SpringBootConfiguration、@EnableAutoConfiguration 和 @ComponentScan。如果你没有在 @ComponentScan 中显式指定 basePackages 或 basePackageClasses,则Spring Boot 默认扫描启动类所在的包及其子包。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
String[] value() default {};
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
// 其他属性和方法
}
如果 basePackages 和 basePackageClasses 都是默认值(空),Spring将使用声明 @ComponentScan 的类的包作为扫描的起点。
源码中的实现
在Spring Boot的自动配置过程中,@SpringBootApplication 注解的处理是关键。这个注解包含了 @ComponentScan,而且通常不需要显式指定扫描路径。当Spring应用上下文初始化时,它会创建一个 AnnotationConfigApplicationContext 或 AnnotationConfigServletWebServerApplicationContext,这取决于你的应用类型。在这个过程中,Spring会解析配置类,包括 @ComponentScan 注解。
具体到源码实现,Spring使用 ClassPathBeanDefinitionScanner 类来执行实际的路径扫描。这个类在初始化时会接收一个 BeanDefinitionRegistry,用于注册扫描到的bean定义。扫描过程是通过调用 scan 方法实现的,该方法最终会调用 findCandidateComponents 方法来查找路径下所有符合条件的类。
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
for (String basePackage : basePackages) {
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
for (BeanDefinition candidate : candidates) {
// 注册过程
}
}
return beanDefinitions;
}