Spring Boot不指定包路径就可以扫描启动类所在包及其子包下的类是怎么做到的?

Talk is cheap. Show me the code

第一步:在com.xxx.hyl.boot包下创建一个子包-beans,并在该包中创建一个Bean。

package com.xxx.hyl.boot.beans;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;
/***
 * 使用{@link ComponentScan}不指定包路径,当前类被扫描进IoC容器
 * @author 君战
 * */
@Component
public class TestBean {
}

第二步:在com.xxx.hyl.boot包下创建一个类,使用@ComponentScan注解,然后创建注解驱动 Spring 应用上下文。

package com.xxx.hyl.boot;

import com.xxx.hyl.boot.beans.TestBean;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;

/***
 * 演示使用{@link ComponentScan}注解时不指定包路径,IoC容器依然可以扫描当前包下及其子包下的类。
 * @author 君战
 *
 * */
@ComponentScan
public class ComponentScanDemo {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        // 使用当前类来作为配置类
        context.register(ComponentScanDemo.class);
        // 调用 refresh方法来刷新应用上下文
        context.refresh();
        // 通过getBean方法来获取TestBean在IoC容器中的实例
        TestBean testBean = context.getBean(TestBean.class);
        System.out.println("TestBean : " + testBean);
    }
}

第三步:查看控制台

可以发现,我们做到了和SpringBoot一样的功能,只添加了一个@ComponentScan注解,并未指定任何包路径,TestBean依然被装配进IoC容器中。

其实这并不是Spring Boot独创的,而是Spring Framework原本就有的功能,只不过因为Spring Boot的风靡而让这种用法走入大家的视野。废话不多说,接下来就进入底层源码分析。

底层源码分析

在ComponentScanAnnotationParser的parse方法中,针对@ComponentScan注解的方法返回值进行解析,并将解析到的数据设置到ClassPathBeanDefinitionScanner中。这里面有一个扩展知识点,basepackages是可以设置占位符的,具体分析请查看 你所不知道的@ComponentScan注解用法之包路径占位符 。重点是接下来的判断basePackages集合是否为空(如果未配置basePackages和basePackageClasses,那么basePackages集合就是为空),如果为空,则去获取配置类所在包来作为要扫描的包路径。重点就是使用basePackages将ClassUtils的getPackageName方法返回值保存。

// ComponentScanAnnotationParser#parse
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
	ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
			componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);
	// 获取@ComponentScan注解nameGenerator()方法返回值
	Class<? extends BeanNameGenerator> generatorClass = componentScan.getClass("nameGenerator");
	boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass);
	scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator :
			BeanUtils.instantiateClass(generatorClass));
	// 获取@ComponentScan注解scopedProxy()方法返回值
	ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy");
	if (scopedProxyMode != ScopedProxyMode.DEFAULT) {
		scanner.setScopedProxyMode(scopedProxyMode);
	} else {
		Class<? extends ScopeMetadataResolver> resolverClass = componentScan.getClass("scopeResolver");
		scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass));
	}
	// 获取@ComponentScan注解resourcePattern()方法返回值
	scanner.setResourcePattern(componentScan.getString("resourcePattern"));
	// 获取@ComponentScan注解includeFilters()方法返回值
	for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {
		for (TypeFilter typeFilter : typeFiltersFor(filter)) {
			scanner.addIncludeFilter(typeFilter);
		}
	}
	// 获取@ComponentScan注解excludeFilters()方法返回值
	for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {
		for (TypeFilter typeFilter : typeFiltersFor(filter)) {
			scanner.addExcludeFilter(typeFilter);
		}
	}
    // 获取@ComponentScan注解lazyInit()方法返回值
	boolean lazyInit = componentScan.getBoolean("lazyInit");
	if (lazyInit) {
		scanner.getBeanDefinitionDefaults().setLazyInit(true);
	}

	Set<String> basePackages = new LinkedHashSet<>();
	// 获取@ComponentScan注解basePackages()方法返回值
	String[] basePackagesArray = componentScan.getStringArray("basePackages");
	for (String pkg : basePackagesArray) {
		String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
				ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
		Collections.addAll(basePackages, tokenized);
	}
	// 获取@ComponentScan注解basePackageClasses()方法返回值
	for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
		basePackages.add(ClassUtils.getPackageName(clazz));
	}
	// 如果未指定@ComponentScan注解的basePackages()以及basePackageClasses(),那么basePackages将为空
	if (basePackages.isEmpty()) {
		// 获取配置类所在包并添加到basePackages集合中。
		basePackages.add(ClassUtils.getPackageName(declaringClass));
	}
	
	scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
		@Override
		protected boolean matchClassName(String className) {
			return declaringClass.equals(className);
		}
	});
	return scanner.doScan(StringUtils.toStringArray(basePackages));
}

ClassUtils的getPackageName方法实现也很简单,就是通过获取最后一个“.”的下标,然后从类的全限定名截取获得包名。

// ClassUtils#getPackageName(java.lang.String)
public static String getPackageName(String fqClassName) {
	Assert.notNull(fqClassName, "Class name must not be null");
	int lastDotIndex = fqClassName.lastIndexOf(PACKAGE_SEPARATOR);
	return (lastDotIndex != -1 ? fqClassName.substring(0, lastDotIndex) : "");
}

接下来,我们就DEBUG验证下猜想,不出所料,結果正如我们所猜想的那样。
在这里插入图片描述

总结

Spring Boot中只添加一个@SpringBootApplication注解就可以扫描启动所在包及其子包中所有添加了@Component注解及其派生注解的Bean这样的功能归根结底还是Spring Framework原有的功能,并不是Spring Boot所独创。

Spring在处理@ComponentScan注解时,如果未指定包路径或者class路径,默认以配置类所在的包路径作为资源扫描路径。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring Boot项目中,可以通过以下步骤检查自定义注解是否被Spring容器扫描到: 1. 检查自定义注解所在的路径:找到自定义注解的Java文件,查看它所在的路径。通常情况下,自定义注解应该定义在src/main/java目录下的某个中。 2. 检查Spring Boot项目的扫描路径:在Spring Boot项目的配置文件application.properties或application.yml中,查找以下配置项: ``` spring: main: allow-bean-definition-overriding: true application: name: your-application-name servlet: multipart: max-file-size: 1MB jackson: date-format: yyyy-MM-dd HH:mm:ss datasource: url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC driver-class-name: com.mysql.cj.jdbc.Driver username: root password: root initialization-mode: always schema: classpath:sql/schema.sql data: classpath:sql/data.sql mvc: throw-exception-if-no-handler-found: true static-path-pattern: /** resources: static-locations: classpath:/static/,classpath:/public/,classpath:/resources/,classpath:/META-INF/resources/ jpa: hibernate: ddl-auto: update show-sql: true database-platform: org.hibernate.dialect.MySQL8Dialect properties: hibernate: format_sql: true jdbc: time_zone: GMT+8 ``` 在这个配置文件中,可以找到以下配置项: ``` @ComponentScan(basePackages = {"com.example.demo"}) ``` 这个配置项指定Spring Boot项目的扫描路径。如果自定义注解所在的路径不在这个扫描路径下,那么Spring容器就无法扫描到这个注解。 3. 确认自定义注解是否被注册为Bean:在Spring Boot项目启动时Spring容器会自动扫描所有的组件(括自定义注解),并将它们注册为Bean。可以通过在Spring Boot项目中编写一个测试来确认自定义注解是否被注册为Bean。例如: ``` @RunWith(SpringRunner.class) @SpringBootTest public class DemoApplicationTests { @Autowired private ApplicationContext applicationContext; @Test public void contextLoads() { assertTrue(applicationContext.containsBean("myCustomAnnotation")); } } ``` 这个测试中,首先通过@Autowired注解注入了Spring容器的ApplicationContext对象。然后,在测试方法中,使用assertTrue方法判断自定义注解是否被注册为Bean。如果自定义注解被正确注册为Bean,那么assertTrue方法会返回true。反之,会返回false。 通过以上步骤,就可以检查自定义注解是否被Spring容器扫描到了。如果还有其他问题,请随时提出。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值