Spring类扫描器基础实现,自定义类扫描器和对象注入

2020-03-25 by 没有评论

Spring的类扫描器

Spring Component Scan看起来很方便,但内部实现还是相当暴力的,可能也是java实在没提供什么特别方便的api,基本原理就是使用ClassLoader的getResources方法直接获取对应包下的所有class文件,再使用MetadataReaderFactory读取类信息,主要是获取className,之后一些反射处理就可以继续进行了。

基本示例:

public class ClasspathScanner implements EnvironmentAware{
    static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";


    protected final Logger logger = LoggerFactory.getLogger(getClass());

    private String resourcePattern = DEFAULT_RESOURCE_PATTERN;
    private Environment environment;
    private MetadataReaderFactory metadataReaderFactory;
    //lazy init
    private final MetadataReaderFactory getMetadataReaderFactory() {
        if (this.metadataReaderFactory == null) {
            this.metadataReaderFactory = new CachingMetadataReaderFactory();
        }
        return this.metadataReaderFactory;
    }

    //将package转换为classpath resource path,基本来说就是.转换为/
    private String resolveBasePackage(String basePackage) {
        return ClassUtils.convertClassNameToResourcePath(environment.resolveRequiredPlaceholders(basePackage));
    }

    private Class<?> checkClassName(String className){
        try{
            return Class.forName(className);
        }catch(ClassNotFoundException e){
            return null;
        }
    }

    //扫描包下面的类,注意这个package是支持通配符的,通常而言*可替代一层包名,**可代替0层或多层任意的包名
    private List<Class<?>> scanClasses(String basePackage) {
        try {
            String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
                    resolveBasePackage(basePackage) + '/' + this.resourcePattern;
            Resource[] resources = new PathMatchingResourcePatternResolver().getResources(packageSearchPath);
            boolean traceEnabled = logger.isTraceEnabled();
            List<Class<?>> classes = new ArrayList<>();
            for (Resource resource : resources) {
                if (traceEnabled) {
                    logger.trace("Scanning {}", resource);
                }
                if (resource.isReadable()) {
                    try {
                        MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
                        String className = metadataReader.getClassMetadata().getClassName();
                        Class<?> clazz;
                        if ((clazz = checkClassName(className)) != null) {
                            classes.add(clazz);
                        } else {
                            if (traceEnabled) {
                                logger.trace("Ignored because not matching any filter: {}", resource);
                            }
                        }
                    } catch (Throwable ex) {
                        throw new BeanDefinitionStoreException(
                                "Failed to read candidate component class: " + resource, ex);
                    }
                } else {
                    if (traceEnabled) {
                        logger.trace("Ignored because not readable: " + resource);
                    }
                }
            }
            return classes;
        } catch (IOException ex) {
            throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
        }
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }
}

以上给了扫描类的基本案例,部分代码是直接手写的,可能存在错漏,自行调整一下吧。注意这个类使用了EnvironemntAware接口,因此需要先注册到Spring内才可以生效。通常而言是设定为Configuration类,不过还有一种常用实现,定义注解扫描器

ImportBeanDefinitionRegistrar

通过定义一个注解,然后在注解上加上@Import关联到对应的类,即可实现自动扫描并注册的功能

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Configuration
@Import(ClassPathScanner.class)
public @interface Scan{
    @AliasFor("basePackages")
    String[] value() default {};

    @AliasFor("value")
    String[] basePackages() default {}
}

这个注解关联了@Configuration,因此标注的类会被Spring识别为configuration class,同时Spring会根据@Import启动对应的注册器。当然这还要求ClassPathScanner实现ImportBeanDefinitionRegistrar

然后可以实现这个接口的registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry)方法,即可获得registry对象。接下来就可以利用这个registry去注册我们想要的bean了。

BeanDefinition

扫描得到了对应的类,接下来就是对类进行筛选,无论是通过父类/接口关系还是通过注解对于反射而言都不是问题。

接下来就是如何注入Spring了。

通过上一步我们得到了BeanDefinitionRegistry对象,接下来只需要构建一个BeanDefinition即可实现注入。Spring为我们提供了BeanDefinitionBuilder来简化BeanDefinition的构建,一个简单实现Demo:

BeanDefinition definition = BeanDefinitionBuilder.genericBeanDefinition(SpiderRequestFactory.class)
                                    .addConstructorArgValue("foo")
                                    .addConstructorArgReference("spiderConfig")
                                    .addConstructorArgValue(obj1)
                                    .getBeanDefinition();

以上demo是通过定义构造器参数的方式来构建Bean,也可以定义autoWireProperty的方式进行依赖注入。可以看到这里支持直接的对象注入,beanName引用注入等方式。然后再注册到beanRegistry就可以了。

场景

这里的实现比较复杂了,实际上Spring能自动识别所有标注了@Component的类,包括其衍生注解。而Spring建立的注解传递机制也允许我们自己定义新的衍生注解完成扫描,并由Spring自行完成构建,远远不需要如此费事。

这种方式主要用于一些特殊场景,例如接口代理等的实现。最典型的例子要数mybatis,通过扫描Mapper接口可以自动构建接口的代理类并注册到Spring,而非直接把接口注册到Spring(interface也无法实例化),或者一些其他类似的场景。

当然要实现mybatis的方案还有一个关键技术是接口代理,这里就不做赘述了,也就是BeanDefinition构建出来的类需要代理对应的接口,并由spring识别为对应接口的实体对象而非代理类本身,这里就不做赘述了

标签: