spring是怎样通过@ComponentScan,或者自动配置扫描到了依赖包里class的?
这里涉及到了class Loader的机制,有些复杂,jdk中提供默认3个class Loader:
%JAVA_HOME\lib%
下的jar;%JAVA_HOME\lib\ext%
下的jar;AppClassLoader
和ExtClassLoader
父类都是URLClassLoader
,我们自定义也是继承URLClassLoader
进行扩展的;
所以,当我们使用类加载器加载资源时,它会找上面这些路径,而AppClassLoader
是找当前执行程序的classpath
,也就是我们target/classes
目录,如果有是maven引用了其他依赖包,那么也会将maven地址下的依赖包的路径加到AppClassLoader
的URL
里,如果是多模块的项目,还会把引用的其他模块下target/classes
的目录也加进来。
假设需要获取一个jar包里的class该如何?
如下4个步骤即可:
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包的问题;
上面给出了两种方式
第一种:使用类加载加载
ClassLoader classLoader = Test.class.getClassLoader();
第二种:使用URLClassLoader
加载
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种引入的包,使用第二种方式。
那么这里简单的走一下自动配置流程:
@EanbleConfiguration
,会读取META-INF/spring.factories
里配置的配置类@ComponentScan @Import @Component @Service
等这样的注解),如果配置类有扫描class的注解,就去扫描在spring中加载class的方式就是上面的方式,我这里就在上面示例的基础上增加一些细节,如下:
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。
@ComponentScan
扫描指定路径class,如果找到配置类,还有@Import
开头的注解,以及@EnableConfiguration
这些注解,也都是把class找到,然后判断是否配置类,如果是就再去找这些注解,以此循环;@EnableConfiguration
注解,读取META-INF/spring.factories
文件里的value,并解析成配置类,再循环;通样的如果是@Import
注解,引入的是一个非DeferredImportSelector
的配置类也是如此,