Spring源码篇(九)自动配置扫描class的原理

文章目录

前言

spring是怎样通过@ComponentScan,或者自动配置扫描到了依赖包里class的?

ClassLoader

这里涉及到了class Loader的机制,有些复杂,jdk中提供默认3个class Loader:

  • Bootstrap ClassLoader:加载jdk核心类库;加载%JAVA_HOME\lib%下的jar;
  • ExtClassLoader:加载jdk扩展类库;加载%JAVA_HOME\lib\ext%下的jar;
  • AppClassLoader:加载classpath下的class,以及关联到maven仓库里的jar;

AppClassLoaderExtClassLoader父类都是URLClassLoader,我们自定义也是继承URLClassLoader进行扩展的;

所以,当我们使用类加载器加载资源时,它会找上面这些路径,而AppClassLoader是找当前执行程序的classpath,也就是我们target/classes目录,如果有是maven引用了其他依赖包,那么也会将maven地址下的依赖包的路径加到AppClassLoaderURL里,如果是多模块的项目,还会把引用的其他模块下target/classes的目录也加进来。

如何加载jar包里的class

假设需要获取一个jar包里的class该如何?

如下4个步骤即可:

java 复制代码
    public static void main(String[] args) throws Exception {

        String packageName = "com.liry.springplugin";
        // 1. 转换为 com/liry/springplugin
        String packagePath = ClassUtils.convertClassNameToResourcePath(packageName);

        // 2. 通过类加载器加载jar包URL
//        ClassLoader classLoader = Test.class.getClassLoader();
        ClassLoader classLoader = new URLClassLoader(new URL[]{new URL("file:E:\\workspace\\git\\test-plugin\\spring-plugin\\target\\spring-plugin-1.0-SNAPSHOT.jar")});
        URL resources = classLoader.getResource(packagePath);

        // 3. 打开资源通道
        JarFile jarFile = null;
        URLConnection urlConnection = resources.openConnection();
        if (urlConnection instanceof java.net.JarURLConnection) {
            java.net.JarURLConnection jarURLConnection = (java.net.JarURLConnection) urlConnection;
            jarFile = jarURLConnection.getJarFile();
        }

        // 定义一个结果集
        List<String> resultClasses = new ArrayList<>();

        // 4. 遍历资源文件
        Enumeration<JarEntry> entries = jarFile.entries();

        while (entries.hasMoreElements()) {
            JarEntry entry = entries.nextElement();
            // 文件全路径
            String path = entry.getName();
            // 判断是否在指定包路径下,jar包里有多层目录、MF文件、class文件等多种文件信息
            if (path.startsWith(packagePath)) {
                // 使用spring的路径匹配器匹配class文件
                if (path.endsWith(".class")) {
                    resultClasses.add(path);
                }
            }
        }
        resultClasses.forEach(System.out::println);
    }

说明一下,加载jar包的问题;

上面给出了两种方式

第一种:使用类加载加载

java 复制代码
ClassLoader classLoader = Test.class.getClassLoader();

第二种:使用URLClassLoader加载

JAVA 复制代码
ClassLoader classLoader = new URLClassLoader(new URL[]{new URL("file:E:\\workspace\\git\\test-plugin\\spring-plugin\\target\\spring-plugin-1.0-SNAPSHOT.jar")});

这两种方式不同之处在于,查找jar的路径,第一种方式因为我测试项目使用的maven,在pom.xml里引入了spring-plugin-1.0-SNAPSHOT的包,所以才能通过类加载器直接进行加载,这是因为使用maven,maven引用的依赖路径会被加入到AppClassLoader种,然后使用Test.class.getClassLoader()去加载class时,会委派给AppClassLoader进行加载,才会加载到。

所以,如果不是在maven种引入的包,使用第二种方式。

自动配置扫描class的原理

那么这里简单的走一下自动配置流程:

  1. 启动类上有@EanbleConfiguration,会读取META-INF/spring.factories里配置的配置类
  2. 读取后解析成beanDefinition,然后判断是否配置类,如果是就找配置类注解(@ComponentScan @Import @Component @Service等这样的注解),如果配置类有扫描class的注解,就去扫描
  3. 最后得到所有的bean的beanDefinition

spring中的加载方式

在spring中加载class的方式就是上面的方式,我这里就在上面示例的基础上增加一些细节,如下:

java 复制代码
 static PathMatcher pathMatcher = new AntPathMatcher();
    
    public static void getClassResource() throws Exception {

        String packageName = "com.liry.springplugin";
        // 1. 转换为 com/liry/springplugin
        String packagePath = ClassUtils.convertClassNameToResourcePath(packageName);

        // 2. 通过类加载器加载jar包URL
        ClassLoader classLoader = Test.class.getClassLoader();
        URL resources = classLoader.getResource(packagePath);
        
        // spring的资源文件对象
        UrlResource rootResource = new UrlResource(resources);

        // 3. 打开资源通道
        JarFile jarFile = null;
        URLConnection urlConnection = resources.openConnection();
        if (urlConnection instanceof java.net.JarURLConnection) {
            java.net.JarURLConnection jarURLConnection = (java.net.JarURLConnection) urlConnection;
            jarFile = jarURLConnection.getJarFile();
        }

        // 定义一个结果集
        List<Resource> resultClasses = new ArrayList<>();

        // 4. 遍历资源文件
        Enumeration<JarEntry> entries = jarFile.entries();

        // 包路径以 / 结尾拥于后面进行替换
        if (packagePath.endsWith("/")) {
            packagePath += "/";
        }
        while (entries.hasMoreElements()) {
            JarEntry entry = entries.nextElement();
            // 文件全路径
            String path = entry.getName();
            // 判断是否在指定包路径下,jar包里有多层目录、MF文件、class文件等多种文件信息
            if (path.startsWith(packagePath)) {
                // 这里去掉指定的包名,比如com/liry/springplugin/AutoConfig.class,结果就是AutoConfig.class
                String relativePath = path.substring(packagePath.length() + 1);
                // 使用spring的路径匹配器匹配class文件
                if (pathMatcher.match("**/*.class", relativePath)) {
                    Resource relative = rootResource.createRelative(relativePath);
                    resultClasses.add(relative);
                }
            }
        }
        resultClasses.forEach(d -> System.out.println(d.getFilename()));

    }

上面这段已经和spring中加载class的方式是一样的了,对应源码位置:

org.springframework.core.io.support.PathMatchingResourcePatternResolver#doFindPathMatchingJarResources

源码

如果配置类存在@ComponentScan,会拿到注解里的值,也就是basePackages,然后走到:

org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan

这里就是扫描所有的class,然后再构建出beanDefinition对象

org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#scanCandidateComponents

org.springframework.core.io.support.PathMatchingResourcePatternResolver#findPathMatchingResources

最后走到这里,这里就是读取class的地方,这里的逻辑就和上面的例子是一样的

org.springframework.core.io.support.PathMatchingResourcePatternResolver#doFindPathMatchingJarResources

这一步就是在匹配@ComponentScan的basePackages下的class。

总结

  1. 当前项目路径会被加载到AppClassLoader,而且使用maven,对应的maven里的jar路径也会被加载到AppClassLoader中;
  2. 注册配置类,启动类为入口;
  3. 通过配置类找到找到@ComponentScan扫描指定路径class,如果找到配置类,还有@Import开头的注解,以及@EnableConfiguration这些注解,也都是把class找到,然后判断是否配置类,如果是就再去找这些注解,以此循环;
  4. 如果是@EnableConfiguration注解,读取META-INF/spring.factories文件里的value,并解析成配置类,再循环;通样的如果是@Import注解,引入的是一个非DeferredImportSelector的配置类也是如此,
  5. 最后项目中就存在所有的bean的beanDefinition
相关推荐
代码之光_198035 分钟前
保障性住房管理:SpringBoot技术优势分析
java·spring boot·后端
ajsbxi40 分钟前
苍穹外卖学习记录
java·笔记·后端·学习·nginx·spring·servlet
StayInLove1 小时前
G1垃圾回收器日志详解
java·开发语言
对许1 小时前
SLF4J: Failed to load class “org.slf4j.impl.StaticLoggerBinder“
java·log4j
鹿屿二向箔1 小时前
基于SSM(Spring + Spring MVC + MyBatis)框架的咖啡馆管理系统
spring·mvc·mybatis
无尽的大道1 小时前
Java字符串深度解析:String的实现、常量池与性能优化
java·开发语言·性能优化
小鑫记得努力1 小时前
Java类和对象(下篇)
java
binishuaio1 小时前
Java 第11天 (git版本控制器基础用法)
java·开发语言·git
zz.YE1 小时前
【Java SE】StringBuffer
java·开发语言
老友@1 小时前
aspose如何获取PPT放映页“切换”的“持续时间”值
java·powerpoint·aspose