框架扫描并加载类的原理
在spring配置文件中,有个component-scan标签,可以扫描带有@Controller,@Service,@Component,@Repository等注解,
并且把带着些注解的类注册为一个bean。
这里的原理就是用当前线程的ClassLoader来获取classpath的位置,然后对classpath中的字节码文件,把类进行加载,加载为Class类型的反射对象,接着判断是否含有那些注解,如果有就把他们进行不同的处理,注册为一个个不同的bean。最后当检查到有@Resource或者@Autowired时,把这些获取到并且已经处理好的bean进行注入。下面我来分享一下我写的一个简易的扫描class和jar并且获取到这些文件中全部类的方法。
下面这个代码可以扫描classpath和jar中全部的类,并且获取到反射对象:
package com.rabit.classes.scanner;
import org.apache.log4j.helpers.LogLog;
import java.io.File;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* 扫描指定包下的全部类
*/
public class PackageScanner {
private List<String> classPaths = new ArrayList<String>();
private String basePack;
public PackageScanner(String basePack){
LogLog.setInternalDebugging(true);
this.basePack=basePack;
}
public List<Class> scan() throws IOException {
List<Class> list=new ArrayList<>();
System.out.println("==============开始扫描啦====================");
System.out.println(basePack);
Enumeration<URL> urlEnumeration = Thread.currentThread().getContextClassLoader().getResources(basePack.replace(".", "/"));
System.out.println(urlEnumeration);
while (urlEnumeration.hasMoreElements()) {
URL url = urlEnumeration.nextElement();//如果是jar得到的结果大概是:jar:file:/D:maven/.m2/xxx.jar反之,则是file:classes/xxx.class
System.out.println(url);
String protocol = url.getProtocol();//获取标识,后面通过判断是否是jar:file来进行不同的处理
System.out.println(protocol);
if ("jar".equalsIgnoreCase(protocol)) {
//转换为JarURLConnection
JarURLConnection connection = (JarURLConnection) url.openConnection();
if (connection != null) {
JarFile jarFile = connection.getJarFile();
if (jarFile != null) {
//得到该jar文件下面的类实体
Enumeration<JarEntry> jarEntryEnumeration = jarFile.entries();
while (jarEntryEnumeration.hasMoreElements()) {
//获取到的class文件在jar中的路径:xx/xxx/xxx
JarEntry entry = jarEntryEnumeration.nextElement();
String jarEntryName = entry.getName();
//这里我们需要过滤不是class文件和不在basePack包名下的类
if (jarEntryName.contains(".class") && jarEntryName.replaceAll("/",".").startsWith(basePack)) {
String className = jarEntryName.substring(0, jarEntryName.lastIndexOf(".")).replace("/", ".");
System.out.println(className);
try{
Class cls = Class.forName(className);
list.add(cls);
}catch (Exception|Error ex){//报错我们直接记录,然后不直接抛异常
LogLog.error(className+" is not found");
LogLog.error(ex.getMessage());
}
}
}
}
}
}else {
//然后把我们的包名basPach转换为路径名
//然后把classpath和basePack合并
String searchPath = URLDecoder.decode(url.getPath(),"UTF-8");//注意,直接获取的path中间会有乱码,需要转为utf-8
doPath(new File(searchPath));
//这个时候我们已经得到了指定包下所有的类的绝对路径了。我们现在利用这些绝对路径和java的反射机制得到他们的类对象
for (String s : classPaths) {
LogLog.debug(s);
//s是获取到这个class文件的路径:d:ideal/project/test/classes/com/rabit/classes/scanner/PackageScanner.class
s = s.substring(s.indexOf(File.separator+"classes") + 9, s.lastIndexOf(".")).replace(File.separator, ".").replace("/",".").replace("\\",".");
//我们利用这两步把路径通过replace和截取子串的办法把路径转化为类路径:com.rabit.classes.scanner.PackageScanner
LogLog.debug(s);
try{
Class cls = Class.forName(s);
list.add(cls);
}catch(Exception|Error ex){//报错我们直接记录,然后不直接抛异常
LogLog.error(s+" is not found");
LogLog.error(ex.getMessage());
}
}
}
}
return list;
}
/**
* 该方法会得到所有的类,将类的绝对路径写入到classPaths中
* @param file
*/
private void doPath(File file) {
if (file.isDirectory()) {//文件夹
//文件夹我们就递归
File[] files = file.listFiles();
for (File f1 : files) {
doPath(f1);
}
} else {//标准文件
//标准文件我们就判断是否是class文件
if (file.getName().endsWith(".class")) {
//如果是class文件我们就放入我们的集合中。
classPaths.add(file.getPath());
}
}
}
}
通过上面的方法,我们可以获取到类的反射对象,接下来的是筛选这些类对象的过程:
package com.rabit.classes.scanner;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
/**
* 将包含xxx注解的类过滤出来
*/
public class ClassGetter {
private List<Class> classList=new ArrayList<>();//扫描到的class反射类
private List<Class> fClasses=new ArrayList<>();//需要过滤的注解,只有带这些注解的类才会被加载
/**
* 加载过滤器
* @param fClasses 扫描到的class反射类
* @param annotationClasses 需要过滤的注解,只有带这些注解的类才会被加载
*/
public ClassGetter(List<Class> fClasses, Class... annotationClasses){
this.classList= Arrays.asList(annotationClasses);
this.fClasses=fClasses;
}
public List<Class> get(){
List<Class> a=fClasses.stream().map((x)->{
AtomicBoolean b= new AtomicBoolean(false);
Arrays.stream(x.getAnnotations()).map(Annotation::annotationType).forEach((z)->{
if(classList.contains(z))
b.set(true);return;
});
//这里是对被扫描到的反射类进行遍历,获取到反射类所包含的注解列表然后遍历注解列表,使用contains判断需要被过滤的注解是否在这里面,在就设置boolean原子为true,然后直接退出函数(注意,退出的是在里面的(z)->的lambda函数)
if(b.get()){
return x;//结果为true,则把这个class反射类装入stream
}
return null;//反之则返回null
}).filter(Objects::nonNull).collect(Collectors.toList());//最后过滤掉为null的值,然后转换stream为list
return a;
}
}