用过spring框架后知道包扫描是一个非常好用的功能,只需要在某个包下写自己的类,框架就能自动帮我们加载到容器中,从而在各处使用,今天自己来实现一下包扫描。
原理其实很简单,就是找到某个目录下的所有class文件,然后使用类加载器加载到jvm中,再使用反射生成一个该类的对象即可。
需求
实现一个文件监控的服务,当文件或者目录发生变化时,根据不同的文件做不同的处理。要监控的目录要支持从配置文件中读取
思路
首先想到的是
- 监控目录,得知文件的变动信息
- 根据文件名字判断需要做什么处理
很容易写出面的伪代码:
// 当文件发生变化时
public dealOnChange(String filePath){if(filePath.equal("aa")){// todo}else if(filepath.equal("bb")){// todo}
}
上面代码确实很容易实现,但是扩展能力不强。当某天需要新增一个监控文件,就需要修改 dealOnChange
方法,增加一个分支判断,当修改了代码就有可能引入bug,不满足 OCP 原则。
OCP要求软件实体应该是可扩展的,但不应该修改。这意味着,如果需要更改行为,应该使用继承或组合来实现,而不是修改现有代码。它还要求代码应该是可重用的,而不是重新编写。这样,你就可以利用现有的代码,而不是重写它。
可以定义一个文件变动处理器接口,不同的文件变动时,使用不同处理器的实现类,伪代码如下:
interface FileChangeListener {void deal(String filePath);void select(String filePath);
}// 当文件发生变化时
class Main {List<FileChangeListener> fileChangeListenerList;public dealOnChange(String filePath){FileChangeListener listener;for( FileChangeListener f : fileChangeListenerList) {if(f.select(filePath)) {listener = f;break}}// if listener is null ,do other...listener.deal(filePath)}public static void main(String[] args) {// todo: 把所有实现了 FileChangeListener 接口的类都的对象都添加到 fileChangeListenerList 集合中// fileChangeListenerList = loadAllListenerClass()while(true) {// 假设getChange 可以获取到变化的文件路径String filePath = getChange();dealOnChange(filePath)}}
}
上面的伪代码可以实现这样的功能: 当需要增加一个监控文件时,只需新写一个类,然后实现 FileChangeListener 接口,在这个类中处理文件变化时需要做的动作。这样与第一版的区别是:增加功能时,不修改旧代码,而是新增代码
现在关键的问题来了,怎么实现 loadAllListenerClass()
:
加载指定目录下的class
因为一堆 class 文件可以打包成 jar包, 然后使用 java -jar
的方式运行。也可以不用打包,直接运行 java Main
。 不同的运行方式导致从目录中找文件的方式也不一样。分别如下:
直接从目录中加载
public List<Class<?>> getClassListFromDir(String packagePath) {// 先获取包的路径String localPath = this.getClass().getClassLoader().getResource(packagePath.replace(".","/")).getPath();File classFile = new File(localPath );List<Class<?>> klassList = new ArrayList<>();// 遍历这个目录下的所有文件(假设都是class文件)for (File file : Objects.requireNonNull(classFile.listFiles())) {try {// 拼接 class 文件的全限定名,并加载Class<?> klass = Class.forName(packagePath + "." + file.getName().replace(".class","") );Logger.info("加载配置处理器: " + klass.getName());// 如果是接口,跳过if(klass.isInterface()){continue;}// 将类对象添加到集合中klassList.add(klass);} catch (ClassNotFoundException e) {Logger.info("加载类失败: "+ e.getMessage());}}return klassList;
}
从 jar 中加载
/*** 扫描 jar包中的文件获取class* 需要特殊的工具读取jar包中内容,不能向读取目录一样* @param packagePath 要扫描的包路径* @return 类对象*/
public static List<Class<?>> getClassListFromJarFile(String packagePath) {// 得到 jar 包的位置String jarPath = Config.class.getProtectionDomain().getCodeSource().getLocation().getPath();List<Class<?>> klassList = new ArrayList<>();JarFile jarFile = null;try {jarFile = new JarFile(jarPath);} catch (IOException e) {Logger.info(e.getMessage());}List<JarEntry> jarEntryList = new ArrayList<JarEntry>();Enumeration<JarEntry> ee = jarFile.entries();packagePath = packagePath.replace(".","/");while (ee.hasMoreElements()) {JarEntry entry = ee.nextElement();// 过滤我们出满足我们需求的东西if (entry.getName().startsWith(packagePath) && entry.getName().endsWith(".class")) {jarEntryList.add(entry);}}for (JarEntry entry : jarEntryList) {String className = entry.getName().replace('/', '.');className = className.substring(0, className.length() - 6);// 也可以采用如下方式把类加载成一个输入流// InputStream in = jarFile.getInputStream(entry);try {Logger.info("加载配置处理器: " + className);Class<?> klass = Thread.currentThread().getContextClassLoader().loadClass(className);if(klass.isInterface()){continue;}klassList.add(klass);} catch (ClassNotFoundException e) {Logger.info("加载类失败: " + e.getMessage());}}return klassList;
}
到此关键的代码已经完成。拿到类对象后,就可以使用反射 klass.newInstance()
生成实例了。
本文中的示例的完整可运行代码已开源: 欢迎star