1, 注解spring-包扫描实现源码

spring的小Demo

top.itkaoti.test01.TestBean

@Component()
public class TestBean {
    
}

top.itkaoti.test01.test.TestBean01

@Component
public class TestBean01 {
}

top.itkaoti.test01.SpringTest01

public static void main(String[] args) {

    String pacakge = SpringTest01.class.getPackage().getName();
    AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(pacakge);
    Object bean = annotationConfigApplicationContext.getBean("testBean");
    System.out.println(bean.getClass());

    Object bean1 = annotationConfigApplicationContext.getBean("testBean01");
    System.out.println(bean1.getClass());

}

运行

class top.itkaoti.test01.TestBean
class top.itkaoti.test01.test.TestBean01

我们写了一个spring基于注解的加载方式, 只需要简单的设置一下,spring就能帮我们实现类的加载, 那么spring是如何找到这些包下的类呢?

源码跟踪

--1, SpringTest01.java-->
new AnnotationConfigApplicationContext(pacakge);

--2, AnnotationConfigApplicationContext.java-->
public AnnotationConfigApplicationContext(String... basePackages) {
    this();
    // 包扫描(点开)
    scan(basePackages);
    refresh();
}

--3, AnnotationConfigApplicationContext.java-->
public void scan(String... basePackages) {
    Assert.notEmpty(basePackages, "At least one base package must be specified");
    // 扫描(点开)
    this.scanner.scan(basePackages);
}

--4, ClassPathBeanDefinitionScanner.java-->
public int scan(String... basePackages) {
    int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
    // 一般spring中do开头的方法就是真正干活的(点开)
    doScan(basePackages);

    // Register annotation config processors, if necessary.
    if (this.includeAnnotationConfig) {
        AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
    }

    return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}

--5, ClassPathBeanDefinitionScanner.java-->
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Assert.notEmpty(basePackages, "At least one base package must be specified");
    Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
    for (String basePackage : basePackages) {
        // 这边实现了包扫描, 返回了BeanDefinition信息(点开)
        Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
        for (BeanDefinition candidate : candidates) {
            // 暂时不关注
            ...
        }
    }
    return beanDefinitions;
}

--6, ClassPathScanningCandidateComponentProvider.java-->
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
    if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
        return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
    }
    else {
        // (点开)
        return scanCandidateComponents(basePackage);
    }
}

--7, ClassPathScanningCandidateComponentProvider.java-->
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
    Set<BeanDefinition> candidates = new LinkedHashSet<>();
    try {
        String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
                resolveBasePackage(basePackage) + '/' + this.resourcePattern;
        // 使用了资源解析器解析(点开)
        Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
    }
    ...
}

--8, PathMatchingResourcePatternResolver.java-->
public Resource[] getResources(String locationPattern) throws IOException {
    Assert.notNull(locationPattern, "Location pattern must not be null");
    if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
        // a class path resource (multiple resources for same name possible)
        if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
            // a class path resource pattern(点开)
            return findPathMatchingResources(locationPattern);
        }
        else {
            ...
        }
    }
    else {
        ...
    }
}

--9, PathMatchingResourcePatternResolver.java-->
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
    String rootDirPath = determineRootDir(locationPattern);
    String subPattern = locationPattern.substring(rootDirPath.length());
    Resource[] rootDirResources = getResources(rootDirPath);
    Set<Resource> result = new LinkedHashSet<>(16);
    for (Resource rootDirResource : rootDirResources) {
        ...
        // 扫描VFS格式文件(不太懂, 不用关注)
        if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
            result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
        }
        // 扫描jar包中的class
        else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
            result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
        }
        else {
            // 扫描我们写的class文件(点开)
            result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
        }
    }
    return result.toArray(new Resource[0]);
}

--10, PathMatchingResourcePatternResolver.java-->
protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern)
        throws IOException {

    File rootDir;
    rootDir = rootDirResource.getFile().getAbsoluteFile();
    ...
    // (点开)
    return doFindMatchingFileSystemResources(rootDir, subPattern);
}

--11, PathMatchingResourcePatternResolver.java-->
protected Set<Resource> doFindMatchingFileSystemResources(File rootDir, String subPattern) throws IOException {
    // (点开)
    Set<File> matchingFiles = retrieveMatchingFiles(rootDir, subPattern);
    Set<Resource> result = new LinkedHashSet<>(matchingFiles.size());
    for (File file : matchingFiles) {
        result.add(new FileSystemResource(file));
    }
    return result;
}

--12, PathMatchingResourcePatternResolver.java-->
protected Set<File> retrieveMatchingFiles(File rootDir, String pattern) throws IOException {
    ...
    String fullPattern = StringUtils.replace(rootDir.getAbsolutePath(), File.separator, "/");
    if (!pattern.startsWith("/")) {
        fullPattern += "/";
    }
    fullPattern = fullPattern + StringUtils.replace(pattern, File.separator, "/");
    Set<File> result = new LinkedHashSet<>(8);
    // do开头的(点开)
    doRetrieveMatchingFiles(fullPattern, rootDir, result);
    return result;
}

--13, PathMatchingResourcePatternResolver.java-->
protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException {
    for (File content : listDirectory(dir)) {
        String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/");
        if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) {
            // 如果是目录的话
            if (!content.canRead()) {
                ...
            }
            else {
                // 递归调用自己, 获取所有目录下的文件
                doRetrieveMatchingFiles(fullPattern, content, result);
            }
        }
        // 判断符合的类
        if (getPathMatcher().match(fullPattern, currPath)) {
            result.add(content);
        }
    }
}

如果扫描jar包

// 上面的9
--9, PathMatchingResourcePatternResolver.java-->
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
    String rootDirPath = determineRootDir(locationPattern);
    String subPattern = locationPattern.substring(rootDirPath.length());
    Resource[] rootDirResources = getResources(rootDirPath);
    Set<Resource> result = new LinkedHashSet<>(16);
    for (Resource rootDirResource : rootDirResources) {
        ...
        // 扫描VFS格式文件(不太懂, 不用关注)
        if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
            result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
        }
        // 扫描jar包中的class(点开)
        else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
            result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
        }
        else {
            // 扫描我们写的class文件
            result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
        }
    }
    return result.toArray(new Resource[0]);
}


--10, -->
protected Set<Resource> doFindPathMatchingJarResources(Resource rootDirResource, URL rootDirURL, String subPattern)
        throws IOException {

    URLConnection con = rootDirURL.openConnection();
    JarFile jarFile;
    String jarFileUrl;
    String rootEntryPath;
    boolean closeJarFile;

    if (con instanceof JarURLConnection) {
        // Should usually be the case for traditional JAR files.
        JarURLConnection jarCon = (JarURLConnection) con;
        ResourceUtils.useCachesIfNecessary(jarCon);
        jarFile = jarCon.getJarFile();
        jarFileUrl = jarCon.getJarFileURL().toExternalForm();
        JarEntry jarEntry = jarCon.getJarEntry();
        rootEntryPath = (jarEntry != null ? jarEntry.getName() : "");
        closeJarFile = !jarCon.getUseCaches();
    }
    else {
        ...
    }
    ...
    Set<Resource> result = new LinkedHashSet<>(8);
    for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements();) {
        JarEntry entry = entries.nextElement();
        String entryPath = entry.getName();
        if (entryPath.startsWith(rootEntryPath)) {
            String relativePath = entryPath.substring(rootEntryPath.length());
            if (getPathMatcher().match(subPattern, relativePath)) {
                result.add(rootDirResource.createRelative(relativePath));
            }
        }
    }
    return result;    
}

我们发现spring真的太复杂了, 但是我们要关注重点, 是spring如何找到包下的class, 主要以自定义的和jar包中的类。那我们就写一个简单的包扫描程序

包扫描

/**
 * 自定义简单的包扫描工具类, 实现jar包和class文件扫描
 */
public class PackageScanner {



    public static Set<Class> scan(String pkg, ClassLoader classLoader) {
        if(classLoader == null){
            classLoader = PackageScanner.class.getClassLoader();
        }
        LinkedHashSet<Class> classes = new LinkedHashSet<>();
        try {
            Enumeration<URL> resources = classLoader.getResources(pkg.replaceAll("\\.", "/"));
            while (resources.hasMoreElements()) {
                URL url = resources.nextElement();
                String protocol = url.getProtocol();
                if ("jar".equals(protocol) || "war".equals(protocol) || "zip".equals(protocol)) {
                    // 加载jar包
                    loadJarClass(classes, url, classLoader);
                } else if ("file".equals(protocol)) {
                    // 加载文件
                    loadFileClass(classes, new File(url.getFile()), classLoader);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return classes;
    }

    // 加载jar
    private static void loadJarClass(LinkedHashSet<Class> classes, URL url, ClassLoader classLoader) {
        try {
            URLConnection urlConnection = url.openConnection();
            if(urlConnection instanceof JarURLConnection){
                JarFile jarFile = ((JarURLConnection) urlConnection).getJarFile();
                Enumeration<JarEntry> entries = jarFile.entries();
                while(entries.hasMoreElements()){
                    JarEntry jarEntry = entries.nextElement();
                    String name = jarEntry.getName();
                    if(name.endsWith(".class")){
                        String fullClassName = name.replace(".class", "").replaceAll("/", ".");
                        Class aClass = loaderClass(fullClassName, classLoader);
                        if(aClass != null){
                            classes.add(aClass);
                        }
                    }
                }
                jarFile.close();

            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    // 加载文件
    private static void loadFileClass(LinkedHashSet<Class> classes, File file, ClassLoader classLoader) {
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            for (File f : files) {
                loadFileClass(classes, f, classLoader);
            }
            return;
        }
        String absolutePath = file.getAbsolutePath();
        if(!absolutePath.endsWith(".class")){
            // 过滤非class文件
            return;
        }
        String file1 = new File(classLoader.getResource("").getFile()).getAbsolutePath()+"\\";
        String fullClassName = absolutePath.replace(file1, "").replaceAll("\\\\", ".").replace(".class", "");
        Class aClass = loaderClass(fullClassName, classLoader);
        if(aClass != null){
            classes.add(aClass);
        }
    }

    /*
     * 加载class
     */
    private static Class loaderClass(String fullClassName, ClassLoader classLoader){
        try {
            Class<?> aClass = classLoader.loadClass(fullClassName);
            return aClass;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
}

总结

spring的包扫描重点就是自己写的和jar包中的类的扫描。

spring的源码查看, 我们一定要把握重点, 那就是

  1. 略过细节, 把握整体, 整体了解之后, 再把整体拆分开来看细节。

千万不要一步一步debug, 尤其在不了解源码的情况下, 因为你会发现特别多的调用,特别多的类, 你并不知道这个调用是否需要点进去查看其是否有用, 一旦你点进去, 多调几个方法, 就会把你给绕晕了。小逗比在写这篇文章的时候因为看过spring源码的文章, 大概知道spring的整体, 扫包加载为beanDefinition, beanFactory容器加载Bean等等, 在多跑几遍, 大概能知道其调用。

  1. 如果你知道某一个关键点, 直接在其位置打断点

因为如果我们一旦知道某个关键位置的话, 我们直接打断点, 我们就能看到起调用栈信息, 也就能大概知道其调用流程。

项目源码