一、包扫描的作用
在某些时候,我们需要得到一个包下的所有类,然后根据条件约束再去筛选特定的类。这个时候如果有一个工具,可以输入包名之后就直接扫描出报下所有的类,对于开发人员来说,是很方便的操作。
就比如说是Spring框架中用到了注解,首先的操作就是,扫描一个指定包下的所有的类,将带有Compenent注解的类加入到一个BeanPool容器中,作为后续可能会用到的被注入的初始化对象。为了方便使用,我自制了一个通用性比较强的“包扫描”工具。
二、具体操作
1、更改使用者输入的包名:
为了方便起见,允许使用这个工具的人在输入包名进行包扫描操作的时候,可以直接在工程的包名上赋值粘贴,那么这样复制出来的包名是以“com.xxxx.scanPackage.core”的模式出现的,而这种模式不能作为查找定位包下类的路径名,因为我们知道路径名是用“/”或者“//”进行分割的,而一般情况下,我们又用相对路径来获取资源,这样的灵活性大一些。所以最开始,我们要做一步操作,就是把使用者输入的包名中的“.”都变成“/”,把包名变成“com/xxxx/scanPackage/core”的模式。
2、得到指定包下的所有资源的一个集合:
用到了classLoader类加载器调用了getResources()方法,需要说明的几点是:
(1)产生classLoader
在这里classLoader的产生方式是ClassLoader classLoader = Thread.currentThread().getContextClassLoader();,得到的类加载器是当前线程用到的类加载器,那我们知道类加载还有一种得到方式是,通过Class.getClassLoader()方法产生,那为什么在这不用这种方式的类加载器?
是为了避免通过上述方法产生的类加载器,和当前线程要用到的类加载器并不一致,因为java本身就是天生的多线程,可能会导致后续操作中无法正确对目标资源进行定位。所以在涉及到有关资源的查找时,最好使用Thread.currentThread().getContextClassLoader()的方式获取合适的类加载器,这也是目前来说相对安全的一种做法。
(2)进行资源定位
利用前面产生的类加载器,调用getResources()方法,得到一个资源的枚举集合,再进行后续的遍历,在这个过程中用到的方法是ClassLoader的getResources()方法。
1)区分getResources()和getResource()方法
首先了解这俩方法的区别是getResource方法定位到的只是符合要求的资源中的第一个资源,一旦得到就结束定位,不再向后查看,而getResources方法了可以定位到资源中所所有的符合要求的资源,形成一个资源的集合。
2)区分不同的getResource()
Class的getResource():
实际上是通过Class的classLoader的getResource()方法来获取资源,在调用方法之前Class.getResource()会对资源名称做一个处理,构造出一个资源的绝对路径名称,便于后续对资源的查找定位。那么,构建的方法就是,如果资源名称是以“/”开头的,就以“/”后面的内容作为资源的绝对名称;如果是其他的开头,就构建成“修正后的包名+类名”的形式,这里说到的修正后的包名指的是将包名中的“.”换成“/”,将普通的包名称变成路径名称。
处理好资源名称之后,通过classLoader的getResource()方法对资源进行定位,如果返回的是null,就将该资源请求的操作交给ClassLoader.getSystemResource(java.lang.String),最终资源请求失败的话可以返回一个没找到的异常。
ClassLoader的getResource():
这种方法对资源进行查找,资源名称是以“/”分隔的,查找过程涉及到了类加载器加载过程中的“双亲委派”机制,意思是说,通过类加载器加载资源,当某个类加载器收到要加载资源的请求时,自己先不做处理,把任务委托给它的父类加载器,如果父类加载器还存在父,就继续向上委托,由最后一个父类加载器进行加载任务,如果父类加载成功的话,就返回得到的class对象,如果加载失败,就返回null给子类,再一层一层向下退,最终有可能又回到了最开始的那个类加载器,再由这个类加载器调用自己的findResource()去进行加载,成功的话就返回class对象,失败可以抛出没找到的异常。
public void scanPackage(String packageName) {
packageName = packageName.replace(".", "/");
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try {
Enumeration<URL> urls = classLoader.getResources(packageName);
while(urls.hasMoreElements()) {
URL url = urls.nextElement();
String protocol = url.getProtocol();
if("jar".equals(protocol)) {
dealJar(url);
} else {
dealPackage(url,packageName);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
(3)对资源进行处理
根据资源url的getProtocol()方法得到它的协议,再分别处理架包和普通包的资源:
1)处理架包
url.openConnection()返回的是一个URLConnection类型的对象,在这里返回的是URLConnection的继承类JarURLConnection类型的对象。这个方法首先是打开连接,创建一个到URL的远程连接用到的对象,符合JAR协议,然后再通过调用这个返回对象的connect()方法建立连接。之后就可以定位到架包资源形成一个枚举集合,对集合进行遍历,一直遍历到最里面的一层得到以.class为结尾的类,用包名加类名的方式构造出一个完整的className,形成一个类,之后就可以调用dealKlass(klass)方法只针对类进行处理了。
这里写到的dealKlass()方法,是在本类中的一个抽象方法,可以让用户决定对类的处理方式,具体内容由该类的实现类完成。
public abstract void dealKlass(Class<?> klass);
private void dealJar(URL url) {
try {
JarURLConnection urlConnection = (JarURLConnection) url.openConnection();
urlConnection.connect();
JarFile jarFile = urlConnection.getJarFile();
Enumeration<JarEntry> jars = jarFile.entries();
while(jars.hasMoreElements()) {
JarEntry jarEntry = jars.nextElement();
String fileName = jarEntry.getName();
if(jarEntry.isDirectory() || !fileName.endsWith(".class")) {
continue;
}
String klassName = fileName.replace("/", ".");
klassName = klassName.replace(".class", "");
Class<?> klass = Class.forName(klassName);
dealKlass(klass);
}
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
2)处理普通包
根据URL转化成统一资源定位符URI,生成一个File,得到文件列表进行遍历,一直到最里面的以.class结尾的类,用包名加类名的方式构造出一个完整的className,形成一个类,然后调用dealKlass(klass)方法只针对类进行处理。
private void dealPackage(URL url,String packageName) {
try {
File currentFile = new File(url.toURI());
File[] fileList = currentFile.listFiles();
for(File file : fileList) {
String fileName = file.getName();
if(file.isDirectory() || !fileName.endsWith(".class")) {
continue;
}
try {
String klassName = packageName + "." + fileName;
klassName = klassName.replace("/", ".");
klassName = klassName.replace(".class", "");
Class<?> klass = Class.forName(klassName);
dealKlass(klass);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
} catch (URISyntaxException e1) {
e1.printStackTrace();
}
}
注:URL和URI
URI:统一资源定位符,可以在某个规则下将一个资源独一无二的表示出来,利用编号进行标识,像是人的身份证一样,比如说张三的身份证号是123456,那么这串数字就是只属于张三一个人的,别人不会重复,URI就表示每个人都有自己特定的唯一的标志。
URL:也叫做统一资源定位符,是属于URI的子集,是通过具体的位置信息来对资源进行标识,比如说xx省xx市xx县xx小区x号楼门牌号x张三,具体到张三这个人的位置信息,这就是通过URL对张三进行定位,这种方式得到的结果和张三的身份证号是123456是一样的。
—————————————————————————————————————————————————
这个自制的“‘包扫描’”工具实现的就是,可以直接根据一个包名得到该包名称下的所有的类,先一次性得到,方便了后续对这些类进一步的操作。
来看测试结果:
public class TestScanPac {
public static void main(String[] args) {
new ScanPackage() {
@Override
public void dealKlass(Class<?> klass) {
System.out.println(klass.getName());
}
}.scanPackage("com.xxxx.scanPackage.core");;
}
}
扫描com.xxxx.scanPackage.core包下的所有类,前面说到的抽象方法dealKlass(Class<?> klass)的具体实现是输出类名,运行结果是:
com.xxxx.scanPackage.core.ScanPackage