Spring,SpringBoot如何做组件的扫描

Spring,SpringBoot如何做组件的扫描


很久没有提笔写博客了,手都生疏了,最近有朋友遇到疑惑问我,Spring,SpringBoot是如何做的扫描工作的,在给他解答之后,决定动手写一篇博客说明。

引言

大家想想spring中是如何做的声明式组件扫描的呢?估计很多人忘记了是用@ComponentScan注解了吧。

Spring扫描

假设

如果你是Spring,你来做组件的扫描工作,你会怎么做?

分析

要扫描一个组件,需要的条件有:在哪里、怎么扫描。

在哪里

@ComponentScan或者@ComponentScans,了解其一即可。

public @interface ComponentScan {
    @AliasFor("basePackages")
    String[] value() default {};

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

    Class<?>[] basePackageClasses() default {};
  ...
}

看了如上代码,其中basePackages()就是组件的包名集合, basePackageClasses()就是组件的类对象集合。

怎么扫

获取类集合进行注解的解析判断

 for (Class<?> clazz : classSet) {
	Annotation[] annotations = clazz.getDeclaredAnnotations();
 	for (Annotation a : annotations) {
      //判断是否有如下的类注解
      if(a instanceof Service||Component||Configuration){
         //对这个类做进一步的扫描工作
      }
    }
 }

SpringBoot扫描

思想

大家觉得SpringBoot与Spring相比好在哪,想必特别明显一点的就是无需进行繁琐的配置工作。其实这就是SpringBoot的思想所在,即约定大于配置就是减少人为的配置,直接用默认的配置就能获得我们想要的结果。

体现点

以下是最为明显的约定大于配置的体现。

  • 目录结构
    • src/main/java 源代码目录
    • src/main/resource 资源目录
  • 资源文件名
    • application-*.properties
    • application-*.yml
    • boostap.yml|properties

扫描相关

初学SpringBoot的爱好者,会出现一个问题,就是有时候组件扫描不到,最后发现是因为该文件与启动源文件在同一级目录。然后解决方案就是说在启动类加@ComponentScan注解,指定那个组件即可。

分析

SpringBoot默认情况下,会扫描启动类之下的所有目录的类,通过这样一种形式,来减少使用者的配置工作。这一步工作就是获取启动类的目录,递归扫描之下的所有class。

实现

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;

import java.net.JarURLConnection;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * @author linxu
 * <tip>take care of yourself.everything is no in vain.</tip>
 * this is tool that can load class & scan the classes in target direction.
 */
public class ClassUtil {
    /**
     * logger
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(ClassUtil.class);

    /**
     * getClassLoader
     */
    public static ClassLoader getClassLoader() {
        return Thread.currentThread().getContextClassLoader();
    }

    /**
     * @param className     include package name  like com.xx.ClassName
     * @param isInitialized do init class or not.
     * @throws RuntimeException if load fail.
     */
    public static Class<?> loadClass(String className, boolean isInitialized) {
        Class<?> cls;
        try {
            cls = Class.forName(className, isInitialized, getClassLoader());
        } catch (ClassNotFoundException e) {
            LOGGER.error("load class failure", e);
            throw new RuntimeException(e);
        }
        return cls;
    }

    /**
     * @param className include package name  like com.xx.ClassName
     *                  default to init the class.
     */
    public static Class<?> loadClass(String className) {
        return loadClass(className, true);
    }

    /**
     * @param packageName 包名
     *                    scan class below the specified package
     */
    public static Set<Class<?>> getClassSet(String packageName) {
        Set<Class<?>> classSet = new HashSet<>();
        try {
            Enumeration<URL> urls = getClassLoader().getResources(packageName.replace(".", "/"));
            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                if (url != null) {
                    String protocol = url.getProtocol();
                    if ("file".equals(protocol)) {
                        String packagePath = url.getPath().replaceAll("%20", " ");
                        System.out.println(packagePath);
                        addClass(classSet, packagePath, packageName);
                    } else if ("jar".equals(protocol)) {
                        JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
                        if (jarURLConnection != null) {
                            JarFile jarFile = jarURLConnection.getJarFile();
                            if (jarFile != null) {
                                Enumeration<JarEntry> jarEntries = jarFile.entries();
                                while (jarEntries.hasMoreElements()) {
                                    JarEntry jarEntry = jarEntries.nextElement();
                                    String jarEntryName = jarEntry.getName();
                                    if (jarEntryName.endsWith(".class")) {
                                        String className = jarEntryName.substring(0, jarEntryName.lastIndexOf(".")).replaceAll("/", ".");
                                        doAddClass(classSet, className);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        } catch (Exception e) {
            LOGGER.error("get class set failure", e);
            throw new RuntimeException(e);
        }
        return classSet;
    }

    private static boolean isNotEmpty(String obj) {
        return !"".equals(obj) && obj != null;
    }

    /**
     * recursively add all class to set.
     *
     * @param classSet    hash set
     * @param packagePath path
     * @param packageName package name
     */
    private static void addClass(Set<Class<?>> classSet, String packagePath, String packageName) {
        //do filter.
        File[] files = new File(packagePath).listFiles(file -> (file.isFile() && file.getName().endsWith(".class")) || file.isDirectory());
        for (File file : files) {
            String fileName = file.getName();
            //if .class
            if (file.isFile()) {
                String className = fileName.substring(0, fileName.lastIndexOf("."));
                if (isNotEmpty(packageName)) {
                    className = packageName + "." + className;
                }
                doAddClass(classSet, className);
            } else {
                //if direction
                String subPackagePath = fileName;
                if (isNotEmpty(packagePath)) {
                    subPackagePath = packagePath + "/" + subPackagePath;
                }
                String subPackageName = fileName;
                if (isNotEmpty(packageName)) {
                    subPackageName = packageName + "." + subPackageName;
                }
                //递归添加
                addClass(classSet, subPackagePath, subPackageName);
            }
        }
    }

    private static void doAddClass(Set<Class<?>> classSet, String className) {
        Class<?> cls = loadClass(className, false);
        classSet.add(cls);
    }

}

其他的后续注入或者创建工作,在这里就不一一列出。

发布了57 篇原创文章 · 获赞 32 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/rekingman/article/details/103874003