打造一个小型的 Java 反射工具库

写好的代码经过编译后成为字节码可以 JVM 的运行时(Runtime)里面运行,那么此时与原来我们写好的代码之间多了一层字节码。毫无疑问那是 JVM 所必需理解才能去执行的内容。有些优化情况,还会带来特定的修改来达到调优的目的,那样修改后与原本的代码差更远了。然而,是否此时 JVM 运行的就与我们最开始写的 Java 毫无联系呢?答案显然是否定的。不仅从调试的需求,还有基于反射的需求,都必须要求 JVM 内部保留一定源码信息,不能抛弃。

如果我们把一个写好的对象“解剖”来看,首先他可以有类、构造器等信息,然后一个方法,他有返回类型是什么、参数是什么,多少个参数等等的这些信息——这都与我们书写 Java 代码的时候无异。同时,JVM 会忠实地把每个对象的点点滴滴记录下来,对应着一个个特定的映射对象,这就是所谓的反射(Reflection),如同镜子一般的反射。

得到这些信息有什么用?用处巨大。用户在 IDE 里面写好了代码,编译器现在还不能聪明到就凭那些代码可自主按照人的企图去执行,还需要框架编写者根据写好的代码作进一步的分析,以决定下一步怎么做。反射所提供的信息就是很重要的信息。输入的代码成为被分析的对象,成为别的代码所解析的对象。

反射是一种灵活、动态的操作机制,用它可以解决很多写死的东西,在 Java 类库里经常使用到,但是如果操作不当会带来性能问题。

反射包位于 com.ajaxjs.util.ReflectUtil,它只有一个类,反射包提供了如下插图所示的功能。

在这里插入图片描述
该类源码在:https://gitee.com/sp42_admin/ajaxjs/blob/master/aj-base/src/main/java/com/ajaxjs/util/ReflectUtil.java

获取类对象

已知类名的全称可通过 getClassByName() 方法获取类对象(Class<?> 类型),另外有重载版本用于强类型转换(Class<T> 类型)。

// 根据类名字符串获取类对象
public static Class<?> getClassByName(String className);
// 根据类名字符串获取类对象,可强类型转换类型
public static <T> Class<T> getClassByName(String className, Class<T> clz);
// 已知接口类型,获取它的 class
public static Class<?> getClassByInterface(Type type);

// 调用列子
getClassByName("com.ajaxjs.util.TestReflectUtil");

该方法只是封装了原生方法 Class.forName(className) 并拦截了相关异常。

创建新实例

创建新实例的核心方法是 newInstance(),它有个一共有三个重载的版本,分别支持传入这三种参数:类全称、类引用和类构造器对象,还有第二个参数是实例化构造器的参数列表,如果没有则不传。以下是三个方法的签名。

// 创建新实例
/**
 * 根据类创建实例,可传入构造器参数。
 * @param clz	类对象
 * @param args	获取指定参数类型的构造函数,这里传入我们想调用的构造函数所需的参数。可以不传。
 * @return 对象实例
 */
public static Object newInstance(String className, Object... args);
public static <T> T newInstance(Constructor<T> constructor, Object... args);
public static <T> T newInstance(Class<T> clazz, Object... args);

调用例子如下。

assertNotNull(newInstance(Foo.class));
assertNotNull(newInstance(Foo.class, "a", "b"));
assertNotNull(newInstance(getConstructor(Foo.class))); // getConstructor() 返回构造器
assertNotNull(newInstance(getConstructor(Foo.class, String.class, String.class), "a", "b"));
assertNotNull(newInstance("test.com.ajaxjs.util.reflect.TestNewInstance"));

Java 支持可变参数支持零参数调用,所以无须显式传 null 参数,也就是说无论执行 newInstance(String className, Object... args) 还是 newInstance(String className) 乃同一个方法,不用额外书写 public static Object newInstance(String className) 的重载方法。其他但凡有 args 都如此类推。

前两种版本比较简单就不粘贴源码分析了。最后根据类创建实例比较特殊,我们看看源码(源码在 com.ajaxjs.util.ReflectUtil 第 41 行),如下所示。

/**
 * 根据类创建实例,可传入构造器参数。
 * 
 * @param clz  类对象
 * @param args 获取指定参数类型的构造函数,这里传入我们想调用的构造函数所需的参数。可以不传。
 * @return 对象实例
 */
public static <T> T newInstance(Class<T> clz, Object... args) {
	if (clz.isInterface()) {
		LOGGER.warning("所传递的class类型参数为接口,无法实例化");
		return null;
	}

	if (args == null || args.length == 0) {
		try {
			return clz.newInstance();
		} catch (InstantiationException | IllegalAccessException e) {
			LOGGER.warning(e);
		}
	}

	// 获取构造器
	Constructor<T> constructor = getConstructor(clz, args2class(args));
	return newInstance(constructor, args);
}

首先对于没有构造器参数的情形,直接调用 clazz.newInstance() 创建实例。若有构造器参数传入,转到获取构造器的方法返回构造器对象然后执行 newInstance(constructor, args) 创建实例对象。注意构造器参数并不能直接使用,而是先要对象其类引用,即 obj.getClass()。这一步单独列出的方法(源码在 com.ajaxjs.util.ReflectUtil 第 165 行)如下所示。

/**
 * 把参数转换为类对象列表
 * 
 * @param args 可变参数列表
 * @return 类对象列表
 */
public static Class<?>[] args2class(Object[] args) {
	Class<?>[] clazzes = new Class[args.length];

	for (int i = 0; i < args.length; i++)
		clazzes[i] = args[i].getClass();

	return clazzes;
}

第一个参数传入了 Class<T> 包含了泛型信息,因此该方法可轻松返回类型正确的实例,无需类型转换。

Java 支持构造器重载,即可有多个同名的构造器但有不同的参数。创建实例时指定不同的参数级表示使用不同的构造器。反射中,构造器本身就是一种特定的对象。获取某个类的构造器对象,可以通过下面的方法。参数 clazz 是必不可少的类对象,argClasses 是想调用的构造函数所需的参数类型列表,如果没有则不传。

/**
 * 获取类的构造器,可以支持重载的构造器(不同参数的构造器)
 * 
 * @param clazz		类对象
 * @param argClasses	指定构造函数的参数类型,这里传入我们想调用的构造函数所需的参数类型
 * @return 类的构造器
 */
public static <T> Constructor<T> getConstructor(Class<T> clazz, Class<?>... argClasses);

该方法源码(源码在 com.ajaxjs.util.ReflectUtil 第 193 行)如下所示。

/**
 * 获取某个类的所有接口
 * 
 * @param clz 目标类
 * @return 类的所有接口
 */
public static Class<?>[] getDeclaredInterface(Class<?> clz) {
	List<Class<?>> fields = new ArrayList<>();

	for (; clz != Object.class; clz = clz.getSuperclass()) {
		Class<?>[] currentInterfaces = clz.getInterfaces();
		fields.addAll(Arrays.asList(currentInterfaces));
	}

	return fields.toArray(new Class[fields.size()]);
}

调用例子如下。

Class<?>[] cs = getDeclaredInterface(ArrayList.class);

为了获取类所有的接口必须遍历所有父类,clazz.getSuperclass() 返回父类,所有 Java 对象的顶级父类是 Object,因此这是 for 循环的判断条件。Arrays.asList() 是数组转为 List 的方法,fields.addAll() 是拼凑两个 List 的方法。为什么最后不直接返回 List 而要转换为 Array 呢?主要是 Array 消耗资源比 List 低,可让 JVM GC 尽快回收 List。

调出方法对象

已知一个类,可以将其身上的方法抽出来成为一个 java.lang.reflect.Method 对象。完成这一步工作的便是调出方法对象。

Java 的反射 API 为我们提供了两种获取方法的方法:getDeclaredMethod()getMethod(),前者能获取当前类身上的方法,而不会获取父类身上的方法;而后者却可以做到。另外 getDeclaredMethod() 能够获取私有 private 方法,而 getMethod() 却做不到的。AJAXJS 认为获取 private 方法的场合很少,故封装的时候便没考虑使用 getDeclaredMethod() 而是取舍采用 getMethod()

getMethod() 用法如下所示。

import static com.ajaxjs.util.reflect.GetMethod.*;

class Foo {
    public void m1() {
    }

    public void m1(String arg) {
    }
}

class Bar extends Foo {
    public void m2() {
    }
}

@Test
public void testGetMethod() {
    assertNotNull(getMethod(new Foo(), "m1"));// 按实际对象
    assertNotNull(getMethod(Foo.class, "m1"));// 按类引用
    assertNotNull(getMethod(Foo.class, "m1", String.class)); // 按参数类型
    assertNotNull(getMethod(Foo.class, "m1", "foo"));// 按实际参数
    assertNotNull(getMethod(Bar.class, "m1"));
    assertNotNull(getMethod(Bar.class, "m1", String.class));
    assertNotNull(getMethod(Bar.class, "m2"));
}

无论是类引用还是实例都是可以传入实际参数或参数其类对象。但是传实际参数对象会有一个问题:试想想看,调用方法的时候是允许传入参数的子类对象的,也就是说,A 是 B 的父类,同时 foo(A) 声明了方法,那么 foo(B) 传入 B 类对象的参数是没问题的,——这就是所谓的“向上转型(UpCasting)”。遗憾的是直接调用 getMethod(Foo1.class, "foo", new Bar2()) 却是找不到的,如下所示。

class Foo1 {
  public void foo(Foo1 a) {
  }
}
class Bar2 extends Foo1 {
}
assertTrue(getMethod(Foo1.class, "foo", new Bar2()) == null); // 找不到

对此,可以考虑手动进行强类型转换,不过在框架编码中很难手动编码指定;使用 getMethod(Object obj, String methodName, Class.. args) 指定类型,和上个方法同理,在框架也是很难手动指定;使用 getMethodByUpCastingSearch() 自动转换,当前仅支持一个参数,且没有提供参数类对象的方法,用法如下。

public static Method getMethodByUpCastingSearch(Class<?> cls, String methodName, Object arg);

接口类型自动转换

上面谈到的是父类、子类之间的类型匹配问题的。那么接口类型呢?接口的话处理起来麻烦多,但还是可以自动分析转换的。参见下面例子:

public static class A {
  public String foo(A a) {
      return "A.foo";
  }
  public String bar(C c) {
      return "A.bar";
  }
}

public static class B extends A {
}
public static interface C {
}
public static class D implements C {
}

@Test
public void testDeclaredMethod() {
  assertTrue(getMethodByUpCastingSearch(A.class, "bar", new D()) == null); // 找不到
  assertNotNull(getDeclaredMethodByInterface(A.class, "bar", new D()));// 找到了
}

getDeclaredMethodByInterface() 内部流程是:先变量参数类型,通过 Type[] intfs = clazz.getGenericInterfaces() 方法返回类所有的接口类型,然后遍历之,如果发现接口有继承关系,还要进行遍历。所以代码内部经历三次 for 循环才可以找到匹配的方法。

忽略参数类型 getSuperClassDeclaredMethod()

查找对象父类身上指定的方法。注意该方法不需要校验参数类型是否匹配,故有可能不是目标方法,而造成异常,请谨慎使用。

执行方法

调用某一对象的方法应要知道对象的方法对象或方法的字符串,以及调用的方法参数,方法签名如下。

/**
 * 调用方法,该方法不会抛出异常
 * @param instance	对象实例,bean
 * @param method	方法对象或方法字符串
 * @param args	参数列表
 * @return 执行结果
 */
public static Object executeMethod(Object instance, Method method, Object... args);

/**
 * 调用方法。 注意获取方法对象,原始类型和包装类型不能混用,否则得不到正确的方法, 例如 Integer 不能与 int 混用。 这里提供一个 argType 的参数,指明参数类型为何。
 * @param instnace	对象实例
 * @param method	方法名称
 * @param argType	参数类型
 * @param argValue	参数值
 * @return 执行结果
 */
public static Object executeMethod(Object instnace, String method, Class<?> argType, Object argValue);
// 执行静态方法
public static Object executeStaticMethod(Method method, Object... args);

调用例子如下。

public class Foo3 {
	public void m1() {
	}

	public String m1(String arg) {
		return arg;
	}
}

class Bar3 extends Foo3 {
	public void m2() {
	}
}

@Test
public void testExecuteMethod() {
	assertEquals(executeMethod(new Foo3(), "m1"), null);
	assertEquals(executeMethod(new Foo3(), "m1", "foo"), "foo");
	assertEquals(executeMethod(new Bar2(), "m1"), null);
	assertEquals(executeMethod(new Bar3(), "m1", "bar"), "bar");
	assertEquals(executeMethod(new Bar3(), "m1", String.class, "foo"), "foo");
}

注意 executeMethod() 不会抛出异常。若需异常信息请使用如下 executeMethod_Throwable() 方法。

/**
 * 调用方法
 * @param instance	对象实例,bean
 * @param method	方法对象
 * @param args	参数列表
 * @return 执行结果
 * @throws Throwable
 */
public static Object executeMethod_Throwable(Object instance, Method method, Object... args) throws Throwable;

为了动态存入 bean 对象的值反射包还提供一执行 setter() 的方法,方法签名如下所示。

/**
 * 调用 bean 对象的 setter 方法
 * @param bean  Bean 对象
 * @param name  属性名称
 * @param value 要设置的属性值
 */
public static void setProperty(Object bean, String name, Object value);

参考

旧文:https://blog.csdn.net/zhangxin09/article/details/4749756

相关推荐
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页