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);
}
}
其他的后续注入或者创建工作,在这里就不一一列出。