一、问题提出
在JAVA中,获取一个类的全部父类(包括父类的父类)是比较简单的,只需要通过反射(Class的getSuperclass()方法)即可。然而,如果想获得一个类的所有子类,或者获得实现某一个接口的所有实现类,相对比较麻烦。
用过Eclipse的开发人员都知道,通过F4键或(Ctrl+T组合键)可以查到指定类的类层次结构。仔细想一下该快捷键的实现原理,或许可以找到一个可行的设计思路。
首先,需要确定一个前提是,寻找所有子类,必须先指定搜索的文件范围。打个比方,要寻找一个古人的所有后代成员,必须设置查找的地理范围是在中国内,否则就无从入手。
二、解决思路
结合实际的项目部署环境,查找子类的方法需要有两种方式。第一种,在开发环境,可以直接遍历指定范围下的所有源代码文件,再结合反射的知识;第二种,假设项目已经打成jar包,或者引入第三方依赖,则只能通过jar包获得所有类文件。
2.1非spring环境代码实现
下面给出具体的代码实现
1.若是开发环境,则通过递归查找指定目录下的类文件的全路径,代码如下
/**
* 递归查找指定目录下的类文件的全路径
* @param baseFile 查找文件的入口
* @param fileList 保存已经查找到的文件集合
*/
public static void getSubFileNameList(File baseFile, List<String> fileList){
if(baseFile.isDirectory()){
File[] files = baseFile.listFiles();
for(File tmpFile : files){
getSubFileNameList(tmpFile,fileList);
}
}
String path = baseFile.getPath();
if(path.endsWith(".java")){
String name1 = path.substring(path.indexOf("src")+4, path.length());
String name2 = name1.replaceAll("\\\\", ".");
String name3 = name2.substring(0, name2.lastIndexOf(".java"));
fileList.add(name3);
}
}
2.若是jar包环境,则可以通过JarFile这个工具类,获得所有全部类信息
/**
* 从jar包读取所有的class文件名
*/
private static List<String> getClassNameFrom(String jarName){
List<String> fileList = new ArrayList<String>();
try {
JarFile jarFile = new JarFile(new File(jarName));
Enumeration<JarEntry> en = jarFile.entries(); // 枚举获得JAR文件内的实体,即相对路径
while (en.hasMoreElements()) {
String name1 = en.nextElement().getName();
if(!name1.endsWith(".class")){//不是class文件
continue;
}
String name2 = name1.substring(0, name1.lastIndexOf(".class"));
String name3 = name2.replaceAll("/", ".");
fileList.add(name3);
}
} catch (IOException e) {
e.printStackTrace();
}
return fileList;
}
3.从前两步可以得到所有子类或所有接口实现类的类路径信息,有了类的全路径,就可以通过反射拿到类的信息,用来判断是否满足条件
/**
* 判断一个类是否继承某个父类或实现某个接口
*/
public static boolean isChildClass(String className,Class parentClazz){
if(className == null) return false;
Class clazz = null;
try {
clazz = Class.forName(className);
if(Modifier.isAbstract(clazz.getModifiers())){//抽象类忽略
return false;
}
if(Modifier.isInterface(clazz.getModifiers())){//接口忽略
return false;
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
return parentClazz.isAssignableFrom(clazz);
}
4.写几个简单的测试类,用来说明问题
public abstract class Animal {
public abstract void eat();
public abstract void walk();
}
package bean;
public class Cat extends Animal{
@Override
public void eat() {
System.err.println("小猫吃东西");
}
@Override
public void walk() {
System.err.println("小猫走路");
}
}
public class Dog extends Animal{
@Override
public void eat() {
System.err.println("小狗吃东西");
}
@Override
public void walk() {
System.err.println("小狗走路");
}
}
public class Person {
private String name;
private int age;
public Person(){
}
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public void sayHello(){
System.err.println("hello,i am " + this.name);
}
}
5.入口方法,打印输出结果(jar包可直接使用Eclipse导出可执行jar文件)
List<String> fileList = new ArrayList<String>();
File baseFile = new File(getSrcPath()+File.separator+"src"+File.separator+"bean");
if(baseFile.exists()){//开发环境,读取源文件
getSubFileNameList(baseFile,fileList);
}else{//jar包环境
fileList = getClassNameFrom("server.jar");
}
System.err.println("Animal类的所有子类有");
for(String name:fileList){
if(isChildClass(name, Animal.class))
System.err.println(name);
}
6.代码运行结果如下
2.2spring环境代码实现
如果项目使用了spring框架,则一切变得更简单了。因为spring本身就需要扫描各种类文件。结合spring提供的API,我们可以设计一个高可用的类扫描工具。
直接分享源码
public class ClassScanner {
private static Logger logger = LoggerFactory.getLogger(ClassScanner.class);
/**
* 默认过滤器(无实现)
*/
private final static Predicate<Class<?>> EMPTY_FILTER = clazz -> true;
/**
* 扫描目录下的所有class文件
*
* @param scanPackage 搜索的包根路径
* @return
*/
public static Set<Class<?>> getClasses(String scanPackage) {
return getClasses(scanPackage, EMPTY_FILTER);
}
/**
* 返回所有的子类(不包括抽象类)
*
* @param scanPackage 搜索的包根路径
* @param parent
* @return
*/
public static Set<Class<?>> listAllSubclasses(String scanPackage, Class<?> parent) {
return getClasses(scanPackage, (clazz) -> {
return parent.isAssignableFrom(clazz) && !Modifier.isAbstract(clazz.getModifiers());
});
}
/**
* 返回所有带制定注解的class列表
*
* @param scanPackage 搜索的包根路径
* @param annotation
* @return
*/
public static <A extends Annotation> Set<Class<?>> listClassesWithAnnotation(String scanPackage,
Class<A> annotation) {
return getClasses(scanPackage, (clazz) -> {
return clazz.getAnnotation(annotation) != null;
});
}
/**
* 扫描目录下的所有class文件
*
* @param pack 包路径
* @param filter 自定义类过滤器
* @return
*/
public static Set<Class<?>> getClasses(String pack, Predicate<Class<?>> filter) {
ResourcePatternResolver patternResolver = new PathMatchingResourcePatternResolver();
MetadataReaderFactory metaFactory = new SimpleMetadataReaderFactory(patternResolver);
String path = ClassUtils.convertClassNameToResourcePath(pack);
String location = ResourceUtils.CLASSPATH_URL_PREFIX + path + "/**/*.class";
Resource[] resources;
Set<Class<?>> result = new HashSet<>();
try {
resources = patternResolver.getResources(location);
for (Resource resource : resources) {
MetadataReader metaReader = metaFactory.getMetadataReader(resource);
if (resource.isReadable()) {
String clazzName = metaReader.getClassMetadata().getClassName();
if (clazzName.contains("$")) {
// 忽略内部类
continue;
}
// Class<?> clazz = Class.forName(clazzName);
Class<?> clazz = Thread.currentThread().getContextClassLoader().loadClass(clazzName);
if (filter.test(clazz)) {
result.add(clazz);
}
}
}
} catch (Exception e) {
logger.error("", e);
}
return result;
}
public static void main(String[] args) {
Set<Class<?>> clazzs = getClasses("com.kingston.mmorpg.framework.util");
System.err.println(clazzs);
Set<Class<?>> clazzs2 = listAllSubclasses("com.kingston.mmorpg.game", Message.class);
System.err.println(clazzs2);
}
}