Spring源码解析之ConfigurationClassPostProcessor(二)
上一个章节,笔者向大家介绍了spring是如何来过滤配置类的,下面我们来看看在过滤出配置类后,spring是如何来解析配置类的。首先过滤出来的配置类会存放在configCandidates列表, 在代码<1>处会先根据配置类的权重做一个排序,权重越低的配置类排在越前,在解析的时候也越先解析。之后会根据configCandidates列表生成一个set集合candidates,防止configCandidates列表存在相同的元素。之后会在<3>处解析这些配置类,并在<4>处校验解析出来的配置类。由于在解析配置类的时候,可能引入其他的配置类,所以spring这里做了一个do-while循环,这个循环会一直持续到spring确定不会再有新的配置类引入时退出。
public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor, PriorityOrdered, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware { …… public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { …… // Sort by previously determined @Order value, if applicable configCandidates.sort((bd1, bd2) -> {//<1> int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition()); int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition()); return Integer.compare(i1, i2); }); …… // Parse each @Configuration class ConfigurationClassParser parser = new ConfigurationClassParser( this.metadataReaderFactory, this.problemReporter, this.environment, this.resourceLoader, this.componentScanBeanNameGenerator, registry); Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);//<2> Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size()); do { parser.parse(candidates);//<3> parser.validate();//<4> …… } while (!candidates.isEmpty()); …… } …… }
那么什么情况下会出现扫描类的时候引入新的配置类呢?spring提供了@Import注解允许我们用三种不同的方式在解析配置类的时候引入新的配置类。@Import通常和@Configuration搭配使用,但也可以独立存在。我们可以用@Import直接引入一个配置类,也可以实现ImportSelector或ImportBeanDefinitionRegistrar其中一个接口,在接口里面返回或注册配置类,同样用@Import引入这两个接口的实现类。
我们在MyConfig9这个配置类上@Import注解引入MyConfig10配置类,在测试用例中将MyConfig9传给应用上下文,从运行结果可以看到即便MyConfig10并没有直接传给应用上下文,spring容器依旧会扫描出org.example.dao类路径下的类并构造相应的bean对象。
//MyConfig9.java @ComponentScan("org.example.service") @Import(MyConfig10.class) public class MyConfig9 { } //MyConfig10.java @ComponentScan("org.example.dao") public class MyConfig10 { public MyConfig10() { System.out.println("构造MyConfig10..."); } }
测试用例:
@Test public void test13() { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyConfig9.class); System.out.println("userDao:" + ac.getBean(UserDao.class)); }
运行结果:
构造MyConfig10... userDao:org.example.dao.UserDao@707194ba
接下来我们看看如何通过ImportSelector实现类来引入一个配置类,MyConfig10ImportSelector实现了ImportSelector接口,这个接口会要求开发者返回一个配置类类名的列表,然后再传给应用上下文的配置类上用@Import引入ImportSelector的实现类,从运行结果也可以看到spring容器将org.example.dao类路径下的类扫描出来。
//MyConfig11.java @ComponentScan("org.example.service") @Import(MyConfig10ImportSelector.class) public class MyConfig11 { } //MyConfig10ImportSelector.java public class MyConfig10ImportSelector implements ImportSelector { public MyConfig10ImportSelector() { System.out.println("构造MyConfig10ImportSelector..."); } @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[]{MyConfig10.class.getName()}; } }
测试用例:
@Test public void test14() { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyConfig11.class); System.out.println("userDao:" + ac.getBean(UserDao.class)); }
运行结果:
构造MyConfig10... userDao:org.example.dao.UserDao@687e99d8
ImportBeanDefinitionRegistrar和ImportSelector有些类似,也是提供一个接口让开发者提供配置类,只不过这个接口要求开发者必须对spring的一些实现设计有些了解,这个接口需要我们将配置类以BeanDefinition的形式注册进spring容器,这种做法同样可以让spring将新引入的配置类指定的类路径扫描出来。
//MyConfig12.java @ComponentScan("org.example.service") @Import(MyConfig10Registrar.class) public class MyConfig12 { } //MyConfig10Registrar.java public class MyConfig10Registrar implements ImportBeanDefinitionRegistrar { public MyConfig10Registrar() { System.out.println("构造MyConfig10Registrar..."); } @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) { AnnotatedGenericBeanDefinition beanDefinition = new AnnotatedGenericBeanDefinition(MyConfig10.class); registry.registerBeanDefinition("myConfig10", beanDefinition); } }
测试用例:
@Test public void test15() { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyConfig12.class); System.out.println("userDao:" + ac.getBean(UserDao.class)); }
运行结果:
构造MyConfig10... userDao:org.example.dao.UserDao@45b4c3a9
我们总结下三种用@Import引入配置类的方式:如果确定了要引入的配置类,我们可以简单地用@Import引入。如果在引入配置类的时候,需要加一些业务逻辑,可以实现ImportSelector或ImportBeanDefinitionRegistrar接口,然后用@Import将其实现类的类对象引入。这两个接口的区别在笔者看来ImportSelector对开发者的要求是少一些,因为仅需要开发者提供待引入的配置类的类名,我们不需要去管spring是如何将配置类包装成BeanDefinition。而ImportBeanDefinitionRegistrar接口对开发者的要求会更高一些,毕竟我们要自己将配置类包装成BeanDefinition注册进spring容器,但这个接口的灵活性也更高,我们可以从这个接口传进来的BeanDefinitionRegistry对象获取其他BeanDefinition,修改这个BeanDefinition的属性从而改变这个BeanDefinition的行为,比如:我们获取到一个BeanDefinition后再获取其类对象,然后用cglib技术生成该类对象的子类,这个子类在调用父类的方法时可以加一个打印调用父类方法的耗时日志。
那么我们来看看在ConfigurationClassParser.parse(...)方法中是如何完成递归解析的,在parse(...)方法中会遍历配置类的BeanDefinition,根据不同情况调用其他parse(...)的重载方法,从其他3个重载的parse(...)方法来看,最终都会调用到ConfigurationClassParser.processConfigurationClass(...)方法。
class ConfigurationClassParser { …… public void parse(Set<BeanDefinitionHolder> configCandidates) { for (BeanDefinitionHolder holder : configCandidates) { BeanDefinition bd = holder.getBeanDefinition(); try { if (bd instanceof AnnotatedBeanDefinition) { parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName()); } else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) { parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName()); } else { parse(bd.getBeanClassName(), holder.getBeanName()); } } catch (BeanDefinitionStoreException ex) { throw ex; } catch (Throwable ex) { throw new BeanDefinitionStoreException( "Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex); } } this.deferredImportSelectorHandler.process(); } protected final void parse(@Nullable String className, String beanName) throws IOException { Assert.notNull(className, "No bean class name for configuration class bean definition"); MetadataReader reader = this.metadataReaderFactory.getMetadataReader(className); processConfigurationClass(new ConfigurationClass(reader, beanName), DEFAULT_EXCLUSION_FILTER); } protected final void parse(Class<?> clazz, String beanName) throws IOException { processConfigurationClass(new ConfigurationClass(clazz, beanName), DEFAULT_EXCLUSION_FILTER); } protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException { processConfigurationClass(new ConfigurationClass(metadata, beanName), DEFAULT_EXCLUSION_FILTER); } …… }
在processConfigurationClass(...)方法中我们又看到一个do-while循环,之所以存在这个循环,是淫威可能存在配置类继承配置类的情况,比如我们声明了一个MyConfig13的配置类,并继承MyConfig9,我们将MyConfig13传给应用上下文,在<1>处的代码处理完MyConfig13后,会判断其父类是否有被处理的需要,如果有的话会进行第二次循环。在处理完配置类后,会在<2>处将配置类放进configurationClasses这个集合。
class ConfigurationClassParser { …… private final Map<ConfigurationClass, ConfigurationClass> configurationClasses = new LinkedHashMap<>(); …… protected void processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter) throws IOException { …… // Recursively process the configuration class and its superclass hierarchy. SourceClass sourceClass = asSourceClass(configClass, filter); do { sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);//<1> } while (sourceClass != null); this.configurationClasses.put(configClass, configClass);//<2> } …… }
在执行ConfigurationClassParser.doProcessConfigurationClass(...)的时候,会先在<1>处获取配置类的扫描路径,还是以上面MyConfig13继承MyConfig10为例,sourceClass初始的类对象为MyConfig13,所以这里会先获取到MyConfig13指向的类路径,之后会在<2>处遍历MyConfig3指向的类路径,将类路径下的类封装成BeanDefinition,再将BeanDefinition及beanName封装成BeanDefinitionHolder对象存到一个集合内并返回,即<3>处的集合。
当扫描并获取到类路径下的BeanDefinitionHolder,会遍历这些对象执行<4>和<5>这两个方法,相信这两个方法大家一定不会觉得陌生,<4>处是判断一个BeanDefinition是能可以成为配置类,可以的话是full模式还是lite模式,<5>处会再次调用ConfigurationClassParser.parse(...)解析配置类。看到这里也许有些人心里有些猜测,没错,这里会递归解析,ConfigurationClassParser.parse(...)在根据配置类指定的类路径扫描处类后,又会将尝试将一个类当做配置类进行解析。假设配置类的类路径为org.example.service,在这个类路径下有一个用@Component注解标记的TestService类,那么这个类就是一个lite模式的配置类,不管这个类有没有用@ComponentScan或@ComponentScans注解指定类路径,都会尝试对TestService类进行解析,如果有配置类路径,则再一次扫描新的类路径下的类。需要注意一点的是:注解具有继承性,@Repository、@Service、@Controller和@Configuration都继承了@Component,也就是说只要类上标记了这几个注解,spring都会认为这个类是配置类。
如果配置类有加@Import注解,则会在<6>处进行处理。如果配置类有加@@ImportResource注解,则会在<7>处将注解指定的文件名存到configClass对象内,待后续解析XML文件。同样类种有@Bean注解,在<8>处会解析出类里带@Bean注解的方法,存放到configClass对象内。
最后在<9>处判断sourceClass的父类是否需要处理,之前笔者拿MyConfig13继承MyConfig10的例子说过,在初始执行ConfigurationClassParser.doProcessConfigurationClass(...)的时候,configClass和sourceClass都是指向MyConfig13类对象,所以在<9>处会判断MyConfig13有父类,之后获取其父类MyConfig10的类名,在<10>处判断这个类的并不是java.*包下的类,这一步主要是为了防止解析java.lang.Object,大部分开发者都知道Object在Java中是所有类的父类。只要一个类的父类不为null,按类名不是java.*包下的类,且父类没有被处理过(即:不在knownSuperclasses集合),这里会将其父类放到knownSuperclasses集合并返回父类,这里会返回封装了MyConfig10的sourceClass对象。ConfigurationClassParser.doProcessConfigurationClass(...) 判断返回的sourceClass不为null,则会再次处理。如果判断是java.*包下的类,则会返回空,ConfigurationClassParser.doProcessConfigurationClass(...) 在判断返回的sourceClass为null就会退出循环。
class ConfigurationClassParser { …… private final ComponentScanAnnotationParser componentScanParser; …… protected final SourceClass doProcessConfigurationClass( ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter) throws IOException { …… // Process any @ComponentScan annotations Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);//<1> if (!componentScans.isEmpty() && !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) { for (AnnotationAttributes componentScan : componentScans) {//<2> // The config class is annotated with @ComponentScan -> perform the scan immediately Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());//<3> // Check the set of scanned definitions for any further config classes and parse recursively if needed for (BeanDefinitionHolder holder : scannedBeanDefinitions) { BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition(); if (bdCand == null) { bdCand = holder.getBeanDefinition(); } if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {//<4> parse(bdCand.getBeanClassName(), holder.getBeanName());//<5> } } } } // Process any @Import annotations processImports(configClass, sourceClass, getImports(sourceClass), filter, true);//<6> // Process any @ImportResource annotations AnnotationAttributes importResource = AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class); if (importResource != null) {//<7> String[] resources = importResource.getStringArray("locations"); Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader"); for (String resource : resources) { String resolvedResource = this.environment.resolveRequiredPlaceholders(resource); configClass.addImportedResource(resolvedResource, readerClass); } } // Process individual @Bean methods Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);//<8> for (MethodMetadata methodMetadata : beanMethods) { configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass)); } // Process default methods on interfaces processInterfaces(configClass, sourceClass); // Process superclass, if any if (sourceClass.getMetadata().hasSuperClass()) {//<9> String superclass = sourceClass.getMetadata().getSuperClassName(); if (superclass != null && !superclass.startsWith("java") &&//<10> !this.knownSuperclasses.containsKey(superclass)) { this.knownSuperclasses.put(superclass, configClass); // Superclass found, return its annotation metadata and recurse return sourceClass.getSuperClass(); } } // No superclass -> processing is complete return null; } …… }
在上面代码的<3>处会完成扫描类路径返回一个BeanDefinitionHolder对象列表,BeanDefinitionHolder对象包含BeanDefinition及其beanName,那么我们来看看在<3>处是如何来完成扫描的。
在这个方法里我们又看到ClassPathBeanDefinitionScanner这个类,不知道大家还有没有印象,笔者曾经在Spring源码解析之BeanFactoryPostProcessor(二)这一章节中介绍这个类,在初始化注解应用上下文时(AnnotationConfigApplicationContext),注解上下文的默认构造方法会初始化一个ClassPathBeanDefinitionScanner对象,当时笔者说过这个对象的作用就是用来传递一个类路径,根据类路径扫描BeanDefinition,并且笔者在这个章节也说了,虽然应用上下文对象里的ClassPathBeanDefinitionScanner对象可以根据类路径扫描BeanDefinition,但我们在配置类上指定的类路径并不是应用上下文内部的ClassPathBeanDefinitionScanner对象来扫描的,这里我们也看到了将类路径下的类扫描成BeanDefinition是在执行ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(...)方法时,执行到ComponentScanAnnotationParser.parse(...)这里会初始化一个ClassPathBeanDefinitionScanner对象来解析扫描类路径。
@ComponentScan注解允许我们设置一些扫描规则,比如:includeFilters、excludeFilters这两个属性可以规定哪些类可以扫描,哪些类不可以扫描,lazyInit可以决定类路径下的类是否是懒加载的,如果我们希望某个类路径下的类只有在需要的时候才去创建bean对象,并不需要将类路径下的每个类都加上@Lazy注解,直接针对指定该类路径@ComponentScan注解设置其lazyInit属性为true即可。下面这段代码根据我们在@ComponentScan注解里设置的属性相应的修改scanner对象的属性,比如<1>处增加将类解析为BeanDefinition的条件,<2>处增加判断一个类不是BeanDefinition的条件,<3>处设置这个类路径的类是否都是懒加载。最后会在<4>处调用scanner.doScan(...) 方法扫描出类路径下可以成为BeanDefinition的类。scanner.doScan(...) 方法方法的实现在Spring源码解析之BeanFactoryPostProcessor(二)章节已经讲过,这里就不再赘述。
class ComponentScanAnnotationParser { …… public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry, componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader); …… for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) { for (TypeFilter typeFilter : typeFiltersFor(filter)) { scanner.addIncludeFilter(typeFilter);//<1> } } for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) { for (TypeFilter typeFilter : typeFiltersFor(filter)) { scanner.addExcludeFilter(typeFilter);//<2> } } boolean lazyInit = componentScan.getBoolean("lazyInit"); if (lazyInit) { scanner.getBeanDefinitionDefaults().setLazyInit(true);//<3> } Set<String> basePackages = new LinkedHashSet<>(); 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); } for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) { basePackages.add(ClassUtils.getPackageName(clazz)); } if (basePackages.isEmpty()) { 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));//<4> } …… }
那么我们如何通过@ComponentScan注解的includeFilters和excludeFilters两个字段来指定要扫描和不扫描的类呢?我们来看下面的代码,includeFilters和excludeFilters两个字段都要求我们提供一组Filter类型的数组,我们可以向这个注解提供一个定制化的Filter数组来定制扫描规则。
Filter注解的type字段来定制五种过滤类的规则:
- ANNOTATION:按注解来过滤类,Filter注解的type字段默认就是ANNOTATION。
- ASSIGNABLE_TYPE:按类型来过滤类
- ASPECTJ:按AspectJ表达式来过滤类
- REGEX:按正则表达式来过滤类
- CUSTOM:提供一个TypeFilter接口的实现类,spring会回调该实现类根据我们实现的逻辑来过滤类。
当我们要提供注解、指定类和TypeFilter实现类时,可以存放在value或classes两个字段,从这两个字段我们从注解上来看也可以认为是同一个字段。如果是根据ASPECTJ、REGEX这两种方式来扫描类则将表达式存放在pattern字段。
public @interface ComponentScan { …… Filter[] includeFilters() default {}; Filter[] excludeFilters() default {}; …… @interface Filter { FilterType type() default FilterType.ANNOTATION; @AliasFor("classes") Class<?>[] value() default {}; @AliasFor("value") Class<?>[] classes() default {}; String[] pattern() default {}; } …… } public enum FilterType { ANNOTATION, ASSIGNABLE_TYPE, ASPECTJ, REGEX, CUSTOM }
下面笔者用三个案例来说明如何定制扫描规则,这里笔者就只例举:ANNOTATION、ASSIGNABLE_TYPE、CUSTOM这三种常用的扫描方式,ASPECTJ、REGEX这两种方式大家可自己百度谷歌,扫描规则不是本章重点,就不一一例举了。
首先我们定义@HelloScan注解,并创建Test2Service类,在类上标记@HelloScan注解,如果spring能扫描到Test2Service这个类,并根据这个类的默认无参构造方法创建bean对象,则会在控制台上打印初始化Test2Service。
//HelloScan.java package org.example.annotations; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) public @interface HelloScan { } //Test2Service.java package org.example.service; import org.example.annotations.HelloScan; @HelloScan public class Test2Service { public Test2Service() { System.out.println("初始化Test2Service..."); } }
在下面的@ComponentScan注解中,我们为了不扫描诸如:@Component、@Repository、@Service、@Controller注解,我们设置useDefaultFilters属性为false。然后我们在includeFilters这个数组里设置了<1>和<2>两个Filter元素,这两个元素都会将标记了@HelloScan注解的类扫描出来。这里我们注释了<1>处的元素,采用<2>处更为简练的元素。
import org.example.annotations.HelloScan; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.FilterType; @ComponentScan( value = "org.example.service", includeFilters = { //@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {HelloScan.class}),//<1> @ComponentScan.Filter(HelloScan.class),//<2> }, useDefaultFilters = false ) public class MyConfig14 { }
测试用例:
@Test public void test17() { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyConfig14.class); System.out.println("test2Service:" + ac.getBean(Test2Service.class)); }
运行结果:
初始化Test2Service... test2Service:org.example.service.Test2Service@609cd4d8
从运行结果可以看到,spring容器会调用Test2Service的默认无参构造方法,我们也可以从spring容器中获取Test2Service对应的bean对象。
下面我们再来看如何根据类型(ASSIGNABLE_TYPE)来扫描类,我们声明一个接口ITestService,并让Test3Service、Test4Service实现来接口。
public interface ITestService { } public class Test3Service implements ITestService { public Test3Service() { System.out.println("初始化Test3Service..."); } } public class Test4Service implements ITestService { public Test4Service() { System.out.println("初始化Test4Service..."); } }
再创建Test5Service,并让Test6Service继承Test5Service。
public class Test5Service { public Test5Service() { System.out.println("初始化Test5Service..."); } } public class Test6Service extends Test5Service { public Test6Service() { System.out.println("初始化Test6Service..."); } }
然后我们声明配置类MyConfig15,指定根据类型来扫描,只扫描实现了ITestService接口和Test5Service。
@ComponentScan( value = "org.example.service", includeFilters = { @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {ITestService.class, Test5Service.class}) }, useDefaultFilters = false ) public class MyConfig15 { }
测试用例:
@Test public void test18() { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyConfig15.class); }
运行结果:
初始化Test3Service... 初始化Test4Service... 初始化Test5Service... 初始化Test5Service... 初始化Test6Service...
从运行结果可以看到,ITestService接口的实现类Test3Service、Test4Service被扫描出来了,Test5Service本身以及Test5Service的子类Test6Service也被扫描出来了。
最后我们再看看如何自定义逻辑来扫描类,如果根据注解或根据类型来扫描已经无法满足我们的需求,必须要用一定的业务逻辑来判断一个类是否应该被扫描,我们就必须提供TypeFilter接口的实现类,在实现类中编写过滤逻辑。比如我们希望扫描类路径org.example.service下所有类名带Test和Service的类都被扫描出来,可以通过metadataReader对象获取目前待扫描的类名,判断类名中是否包含了"Test"和"Service"这两个字符串。
package org.example.filter; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.filter.TypeFilter; import java.io.IOException; public class TestFilter implements TypeFilter { @Override public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException { String className = metadataReader.getClassMetadata().getClassName(); return className.contains("Test") && className.contains("Service"); } }
然后我们在配置类上指定扫描类型为自定义(CUSTOM),并提供了TestFilter类判断一个类是否应该被扫描。
@ComponentScan( value = "org.example.service", includeFilters = { @ComponentScan.Filter(type = FilterType.CUSTOM, classes = TestFilter.class) }, useDefaultFilters = false ) public class MyConfig16 { }
测试用例:
@Test public void test19() { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyConfig16.class); }
运行结果:
初始化Test1Service... 初始化Test2Service... 初始化Test3Service... 初始化Test4Service... 初始化Test5Service... 初始化Test5Service... 初始化Test6Service...
从运行结果可以看到spring容器会根据我们指定的规则,将类路径下所有带"Test"和"Service"字符串的类都扫描出来了。