如何优雅的扫描指定包下的所有class

2023-11-10 18:59

本文主要是介绍如何优雅的扫描指定包下的所有class,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

如果你是框架代码编写者,或者要学习如何编写框架,那么 获取指定包下所有class对象 这个操作时必不可少的。下面我来讲解下过程。

比如我们要扫描com.hadluo包下的A.class和B.class :

思路:

  1. 递归找出环境变量下指定包(com.hadluo)下面的所有以.class结尾的文件(也有可能是jar)。
  2. 截取文件: F:..../com/hadluo 之前的不要,然后去掉.class文件后缀,结果:com/hadluo/A , com/hadluo/B 。
  3. 将上面结果变成com.hadluo.A 然后通过Class.forName加载。
  4. 如果是第一步是jar的话在进行jar内部扫描处理。

方法一:通过JDK原生类实现

我们先了解下ClassLoader下的 getResources() 方法:

// 找出当前环境下/name 的所有的资源磁盘路径(包括jar包和一般文件)
public Enumeration<URL> getResources(String name) ;

getResources会找出sun.boot.class.path和java.class.path解析出的路径集合中所有名称为name的资源。

ClassLoader三个加载路径:

  • sun.boot.class.path所代表的启动类路径
  • java.ext.dirs所代表的扩展类路径
  • java.class.path所代表的应用类路径

对于目录和jar包底层处理方式是不同的,目录是通过sun.misc.URLClassPath的内部类FileLoader处理的,jar包是通过sun.misc.URLClassPath的内部类JarLoader处理的

举个例子:

public class Test {public static void main(String[] args) throws NoSuchMethodException, SecurityException, IOException {Enumeration<URL> dirs = Thread.currentThread().getContextClassLoader().getResources("com");while(dirs.hasMoreElements()) {System.err.println(dirs.nextElement());}}
}
// 打印结果  , file是协议,代表是磁盘文件。 如果是jar:/代表是jar包
file:/D:/dev/scala_pro/test/bin/com

我们项目有com包,于是把我们的com包的磁盘路径打了出来。接下来只需要递归遍历这个路径获取后缀是.class的文件,然后根据class的名称,路径利用Class.forName加载成Class对象就行了,整个代码如下:

public class Test {public static void main(String[] args) throws NoSuchMethodException, SecurityException, IOException, ClassNotFoundException {// 结果 classList<Class<?>> result = null ;String scan = "com.hadluo";// 把 . 转换成 \ 因为下面是文件操作scan = scan.replaceAll("\\.", "/");Enumeration<URL> dirs = Thread.currentThread().getContextClassLoader().getResources(scan);while(dirs.hasMoreElements()) {URL url = dirs.nextElement() ;if(url.getProtocol().equals("file")) {List<File> classes = new ArrayList<File>();// 递归 变量路径下面所有的 class文件listFiles(new File(url.getFile()),classes);// 加载我们所有的 class文件 就行了result= loadeClasses(classes,scan);}else if(url.getProtocol().equals("jar")) {// 等下再说}}// 打印结果for(Class<?> clazz :result) {System.err.println(clazz.getName());}}private static List<Class<?>> loadeClasses(List<File> classes,String scan) throws ClassNotFoundException {List<Class<?>> clazzes = new ArrayList<Class<?>>();for(File file : classes) {// 因为scan 就是/  , 所有把 file的 / 转成  \   统一都是:  /String fPath = file.getAbsolutePath().replaceAll("\\\\","/") ;// 把 包路径 前面的 盘符等 去掉 , 这里必须是lastIndexOf ,防止名称有重复的String packageName = fPath.substring(fPath.lastIndexOf(scan));// 去掉后缀.class ,并且把 / 替换成 .    这样就是  com.hadluo.A 格式了 , 就可以用Class.forName加载了packageName = packageName.replace(".class","").replaceAll("/", ".");// 根据名称加载类clazzes.add(Class.forName(packageName));}return clazzes ;}/** * 查找所有的文件 * * @param dir 路径 * @param fileList 文件集合 */private static void listFiles(File dir, List<File> fileList) {if (dir.isDirectory()) {for (File f : dir.listFiles()) {listFiles(f, fileList);}} else {if(dir.getName().endsWith(".class")) {fileList.add(dir);}}}
}--------------------运行结果
com.hadluo.A
com.hadluo.B

如果是jar 我们利用JarURLConnection 类来变量jar里面文件 , 我们接上面的url 来实验下jar的加载:

JarURLConnection urlConnection = (JarURLConnection) url.openConnection();
// 从此jar包 得到一个枚举类
Enumeration<JarEntry> entries = urlConnection.getJarFile().entries();
// 遍历jar
while (entries.hasMoreElements()) {// 获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件JarEntry entry = entries.nextElement();//得到该jar文件下面的类实体System.err.println(entry.getName());
}

我们先导入一个jedis.jar进来

然后 将扫描路径改成 redis , 运行上面程序:

redis/
redis/clients/
redis/clients/util/
redis/clients/jedis/
redis/clients/jedis/params/
redis/clients/util/MurmurHash.class
redis/clients/util/JedisByteHashMap.class
redis/clients/util/IOUtils.class
redis/clients/util/RedisInputStream.class
.....
........
redis/clients/jedis/JedisCluster$1.class
redis/clients/jedis/JedisCluster$10.class
redis/clients/jedis/BinaryJedisCluster$37.class
META-INF/maven/
META-INF/maven/redis.clients/
META-INF/maven/redis.clients/jedis/
META-INF/maven/redis.clients/jedis/pom.xml
META-INF/maven/redis.clients/jedis/pom.properties

我们发现 把我们jar里面的所有class(包括内部类)都扫描出来了。而且把META-INF的资源文件也扫描出来了。

接下来我们只需要处理 .class结尾的文件就行了:

//file:/D:/dev/scala_pro/test/src/jedis-2.8.0.jar!/redis
JarURLConnection urlConnection = (JarURLConnection) url.openConnection();
// 从此jar包 得到一个枚举类
Enumeration<JarEntry> entries = urlConnection.getJarFile().entries();
// 遍历jar
while (entries.hasMoreElements()) {// 获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件JarEntry entry = entries.nextElement();//得到该jar文件下面的类实体if(entry.getName().endsWith(".class")) {// 因为scan 就是/  , 所有把 file的 / 转成  \   统一都是:  /String fPath = entry.getName().replaceAll("\\\\","/") ;// 把 包路径 前面的 盘符等 去掉String packageName = fPath.substring(fPath.lastIndexOf(scan));// 去掉后缀.class ,并且把 / 替换成 .    这样就是  com.hadluo.A 格式了 , 就可以用Class.forName加载了packageName = packageName.replace(".class","").replaceAll("/", ".");// 根据名称加载类
//            			System.err.println(packageName);
//            			System.err.println(Class.forName(packageName).getName());}
}

整个 工具类代码 会在文章结尾给大家。

方法二:通过Spring工具类实现

这种方法就相当简单了,因为是站在巨人肩膀上,请让我来跟您分析分析。

先引入spring 包:

  <dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>5.1.6.RELEASE</version>
</dependency>

然后在看实例:

import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;public class Main {public static void main(String[] args) throws IOException {PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();// 加載資源    classpath*:com/hadluo/**/*.class : 找环境变量下的 com/hadluo下的 所有.class文件Resource[] resources = resolver.getResources("classpath*:redis/**/*.class");for(Resource res :resources) {System.err.println(res.getURI());}}
}

这样就把 redis.* 包下的所有class文件找出来了,包括jar包。

接下来 我们只需要获取 class的包路径,然后 Class.forName() 就可以了:

public class Main {public static void main(String[] args) throws IOException, ClassNotFoundException {PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();// 加載資源    classpath*:com/hadluo/**/*.class : 找环境变量下的 com/hadluo下的 所有.class文件Resource[] resources = resolver.getResources("classpath*:com/luo/**/*.class");for(Resource res :resources) {// 先获取resource的元信息,然后获取class元信息,最后得到 class 全路径String clsName = new SimpleMetadataReaderFactory().getMetadataReader(res).getClassMetadata().getClassName();// 通过名称加载System.err.println(Class.forName(clsName).getName());}}
}

整个 过程相当之简单,比我们自己写!!!!

 

PathMatchingResourcePatternResolver 原理

其它的上层我就不追踪了,核心的加载就是下面这个方法:

	protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {Set<Resource> result = new LinkedHashSet<>(16);ClassLoader cl = getClassLoader();// 通过classLoader的 getResources方法Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));while (resourceUrls.hasMoreElements()) {URL url = resourceUrls.nextElement();result.add(convertClassLoaderURL(url));}if ("".equals(path)) {// The above result is likely to be incomplete, i.e. only containing file system references.// We need to have pointers to each of the jar files on the classpath as well...addAllClassLoaderJarRoots(cl, result);}return result;}

发现 也是跟我们开头自己写的一样,通过 ClassLoader的 getResources 去加载 资源。万变不离其宗 ,框架代码也是在jdk基础上建立的。

 

提出一个问题:

如果你的项目是JDK7以下的话,需要谨慎传入包名,防止永久代溢出。比如说传入的是 com , 由于com可能会有很多不相关的类被加载,JDK7以下是需要占用永久代空间的。而且永久代是固定的,很容易由于 class 过多而OOM溢出。

线上永久代内存溢出 案例 请见我这篇文章:

 

JDK7 线上永久代内存溢出​githubs.xyz图标

 

 

最后奉献给大家这个工具类,如果没有依赖spring,也可以把 springScanf 方法删除,用jdk原生的。 最后希望大家会喜欢:

Scanf.java​githubs.xyz

 

 

 

强烈推荐一个走向Java架构师道路的博客

 

Java架构师修炼​githubs.xyz图标

 

本文完~

这篇关于如何优雅的扫描指定包下的所有class的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/384636

相关文章

Java抽象类Abstract Class示例代码详解

《Java抽象类AbstractClass示例代码详解》Java中的抽象类(AbstractClass)是面向对象编程中的重要概念,它通过abstract关键字声明,用于定义一组相关类的公共行为和属... 目录一、抽象类的定义1. 语法格式2. 核心特征二、抽象类的核心用途1. 定义公共接口2. 提供默认实

Python一次性将指定版本所有包上传PyPI镜像解决方案

《Python一次性将指定版本所有包上传PyPI镜像解决方案》本文主要介绍了一个安全、完整、可离线部署的解决方案,用于一次性准备指定Python版本的所有包,然后导出到内网环境,感兴趣的小伙伴可以跟随... 目录为什么需要这个方案完整解决方案1. 项目目录结构2. 创建智能下载脚本3. 创建包清单生成脚本4

python获取指定名字的程序的文件路径的两种方法

《python获取指定名字的程序的文件路径的两种方法》本文主要介绍了python获取指定名字的程序的文件路径的两种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要... 最近在做项目,需要用到给定一个程序名字就可以自动获取到这个程序在Windows系统下的绝对路径,以下

SpringBoot实现不同接口指定上传文件大小的具体步骤

《SpringBoot实现不同接口指定上传文件大小的具体步骤》:本文主要介绍在SpringBoot中通过自定义注解、AOP拦截和配置文件实现不同接口上传文件大小限制的方法,强调需设置全局阈值远大于... 目录一  springboot实现不同接口指定文件大小1.1 思路说明1.2 工程启动说明二 具体实施2

详解Java中三种状态机实现方式来优雅消灭 if-else 嵌套

《详解Java中三种状态机实现方式来优雅消灭if-else嵌套》这篇文章主要为大家详细介绍了Java中三种状态机实现方式从而优雅消灭if-else嵌套,文中的示例代码讲解详细,感兴趣的小伙伴可以跟... 目录1. 前言2. 复现传统if-else实现的业务场景问题3. 用状态机模式改造3.1 定义状态接口3

java -jar example.jar 产生的日志输出到指定文件的方法

《java-jarexample.jar产生的日志输出到指定文件的方法》这篇文章给大家介绍java-jarexample.jar产生的日志输出到指定文件的方法,本文给大家介绍的非常详细,对大家的... 目录怎么让 Java -jar example.jar 产生的日志输出到指定文件一、方法1:使用重定向1、

基于Python实现数字限制在指定范围内的五种方式

《基于Python实现数字限制在指定范围内的五种方式》在编程中,数字范围限制是常见需求,无论是游戏开发中的角色属性值、金融计算中的利率调整,还是传感器数据处理中的异常值过滤,都需要将数字控制在合理范围... 目录引言一、基础条件判断法二、数学运算巧解法三、装饰器模式法四、自定义类封装法五、NumPy数组处理

Golang如何对cron进行二次封装实现指定时间执行定时任务

《Golang如何对cron进行二次封装实现指定时间执行定时任务》:本文主要介绍Golang如何对cron进行二次封装实现指定时间执行定时任务问题,具有很好的参考价值,希望对大家有所帮助,如有错误... 目录背景cron库下载代码示例【1】结构体定义【2】定时任务开启【3】使用示例【4】控制台输出总结背景

springboot项目打jar制作成镜像并指定配置文件位置方式

《springboot项目打jar制作成镜像并指定配置文件位置方式》:本文主要介绍springboot项目打jar制作成镜像并指定配置文件位置方式,具有很好的参考价值,希望对大家有所帮助,如有错误... 目录一、上传jar到服务器二、编写dockerfile三、新建对应配置文件所存放的数据卷目录四、将配置文

python3如何找到字典的下标index、获取list中指定元素的位置索引

《python3如何找到字典的下标index、获取list中指定元素的位置索引》:本文主要介绍python3如何找到字典的下标index、获取list中指定元素的位置索引问题,具有很好的参考价值,... 目录enumerate()找到字典的下标 index获取list中指定元素的位置索引总结enumerat