【ReflectionUtils类】带你从原生反射走进反射工具类


💥前言

反射Reflection被视为动态语言的关键,是一种功能强大且复杂的机制。JAVA反射机制允许程序在执行期间借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。一切反射操作都始于类对象。要想解剖一个类,必须先要获取到该类的字节码文件对象,而解剖使用的就是Class类中的方法,所以先要获取到每一个字节码文件对应的Class类型的对象。在本篇文章中,先介绍了原生的反射方式,然后再介绍Spring框架封装好的反射工具类,让读者朋友不仅学习到其基本概念,不用重复造轮子,更需要学习到其封装和优化的思想和手段。其实,很多优化手段是非常相通的。


💥 一、冷兵器时代:原生反射

反射机制相关的类基本都在java.lang.reflect.*包里,其基本由Class类、Method类、Field类和Constructor类组成。不过值得一提的是,获取Class类是获取Constructor、Method及Field的前提。

全类名含义
java.lang.Class代表一个类型,代表整个类
java.lang.reflect.Method代表类中的方法
java.lang.reflect.Constructor代表类中的构造方法
java.lang.reflect.Field代表类中的成员变量

既然说获取到Class类是进行反射的前提,这里就给大家介绍如下3种获取Class类的方式:

获取方式备注说明
Class.forName(全类名)通过Class的静态方法获取,类会被JVM加载到内存中,并且会进行类的静态初始化工作
对象.getClass()因为构建了对象,因此静态初始化和非静态初始化工作都会进行 ,getClass方法属于顶级Object类中的方法
类型.class类加载到内存中,但并不会做任何类的初始化工作

案例:

public class Person {
    private int age;

    private String name;

    private String idCard;

    public Person() {
    }

    private Person(int age, String name, String idCard) {
        this.age = age;
        this.name = name;
        this.idCard = idCard;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getIdCard() {
        return idCard;
    }

    private void setIdCard(String idCard) {
        this.idCard = idCard;
    }
}

-----------------------------------------------------------------------------------------------------------------------------

public class ReflectTest {
	public static void main(String[] args) throws Exception {
		/*
           如果一个 Person 对象表示一个特定的人,那么一个 Class 对象将表示一个特定类的属性。
        */
         
        //获取 Class 的方法有三种,第一种如下:通过Class 的静态方法获取
        Class<?> className = Class.forName("com.reflection.learn.Person");
        
		//获取 Class 的方法有三种,第二种如下:
		//可以先 new 一个对象,通过 getClass 获取 Class对象,从而得知此目标对象的结构
        Person personTest = new Person();
        Class<? extends Person> personTestClass = personTest.getClass();

        //获取 Class 的方法有三种,第三种如下:
        Class<Person> clazz = Person.class;

		System.out.println(className1); // class com.reflection.learn.Person
		System.out.println(className2); // class com.reflection.learn.Person
		System.out.println(className3); // class com.reflection.learn.Person
		
		// 注意:这3种方式获取到的Class对象是同一个,再度证实了我们在JVM相关知识中我们学习到的:类模板数据只有一份!!(存放在堆区)
	}
}

✨1.1 Class类

Class类的核心反射相关方法如下表:

核心方法备注
public T newInstance()创建对象,要求是类必须有一个无参的构造器和类的构造器的访问权限需要足够
public Field getField(String name)根据属性名name获取指定的public修饰属性
public Field[] getFields()返回类中public修饰的属性
public Field[] getDeclaredFields()返回类中所有的属性
public Field getDeclaredField(String name)根据属性名name获取指定的属性
public Method[] getDeclaredMethods()返回类中所有的方法
public Method getDeclaredMethod(String name, Class<?>… parameterTypes)根据方法名name和方法形参获取指定方法
public Constructor<?>[] getDeclaredConstructors()返回类中所有的构造方法
public Constructor getDeclaredConstructor(Class<?>… parameterTypes)根据方法形参获取指定的构造方法
public native Class<? super T> getSuperclass()返回调用类的父类
public Class<?>[] getInterfaces()返回调用类实现的接口集合

案例:

public class ReflectMethodTest {
	public static void main(String[] args) throws Exception {
         //获取 Class 的方法有三种,第一种如下:通过Class 的静态方法获取
        Class<?> personClass = Class.forName("com.reflection.learn.Person");
        Person person = (Person) personClass.newInstance();

        // 如下参数因为权限问题,直接获取会报java.lang.NoSuchFieldException
        Field age = personClass.getField("age");
        // 属性数组只包含public的字段:name
        Field[] fields = personClass.getFields();
        // 可以正确获取到属性:age
        Field age1 = personClass.getDeclaredField("age");
        // 属性数组只包含所有的字段:name、age、idCard
        Field[] declaredFields = personClass.getDeclaredFields();

        // 可以正确获取到setAge()方法
        Method setAge = personClass.getDeclaredMethod("setAge", int.class);
        // 可以正确获取到所有方法
        Method[] declaredMethods = personClass.getDeclaredMethods();

        // 可以正确获取到全参构造器方法:Person(int, String, String)
        Constructor<?> declaredConstructor = personClass.getDeclaredConstructor(int.class, String.class, String.class);
        // 可以正确获取到所有的构造器方法:无参构造器、全参构造器
        Constructor<?>[] declaredConstructors = personClass.getDeclaredConstructors();
	}
}

✨1.2 Method类

Method类的核心反射相关方法如下表:

核心方法备注说明
public String getName()返回属性名
public Class<?> getType()以Class类型,返回属性类型【一般配合Class类的getSimpleName()方法使用】
public Object invoke(Object obj, Object… args)执行方法
public void setAccessible(boolean flag)默认false,设置为true为打破封装(继承自AccessibleObject类)

案例:

public class ReflectMethodTest {
	public static void main(String[] args) throws Exception {
        // 获取 Class 的方法有三种,第一种如下:通过Class 的静态方法获取
        Class<?> className = Class.forName("com.reflection.learn.Person");
        Person person = (Person) personClass.newInstance();

        // 可以正确获取到setAge()方法
        Method setAge = personClass.getDeclaredMethod("setAge", int.class);
        // name:setAge
        String name = setAge.getName();
        // 调用setAge()方法,设置为18
        setAge.invoke(person, 18);
        
        Method setIdCard = personClass.getDeclaredMethod("setIdCard", String.class);
        // 打破封装
        setIdCard.setAccessible(true);
        setIdCard.invoke(person, "123456789");
        System.out.println(person.getIdCard()); // 123456789
	}
}

✨1.3 Field类

Field类的核心反射相关方法如下表:

核心方法备注说明
public String getName()返回方法名
public Class<?> getReturnType()以Class类型,返回方法类型【一般配合Class类的getSimpleName()方法使用】
public void set(Object obj, Object value)给Object对象的字段set值
public Object get(Object obj)获取Object对象的字段值
public void setAccessible(boolean flag)默认false,设置为true为打破封装 (继承自AccessibleObject类)

案例:

public class ReflectFieldTest {
	public static void main(String[] args) throws Exception {
        // 获取 Class 的方法有三种,第一种如下:通过Class 的静态方法获取
        Class<?> personClass = Class.forName("com.reflection.learn.Person");
        Person person = (Person) personClass.newInstance();

        // 获取到age字段
        Field age = personClass.getDeclaredField("age");
        // 因为age字段是private的,不能直接操作
        // 这样直接set值报:Classcom.reflection.learn.ReflectTest can not access a member of classcom.reflection.learn.Person with 		 modifiers "private"
        // age.set(person, 18);
        // 要想操作,先打破封装即可
        age.setAccessible(true);
        age.set(person, 18);
        int ageValue = (int) age.get(person);
        System.out.println(ageValue); // 18
	}
}

✨1.4 Constructor类

Constructor类的核心反射相关方法如下表:

核心方法备注说明
public String getName()返回构造方法名
public Class<?>[] getParameterTypes()返回构造方法的修饰符列表(一个方法的参数可能会有多个。)【结果集一般配合Class类的getSimpleName()方法使用】
public T newInstance(Object … initargs)创建对象【参数为创建对象的数据】

案例:

public class ReflectConstrcutorTest {
	public static void main(String[] args) throws Exception {
        //获取 Class 的方法有三种,第一种如下:通过Class 的静态方法获取
        Class<?> className = Class.forName("com.reflection.learn.Person");
        
        // 获取到全参构造器方法
        Constructor<?> declaredConstructor = personClass.getDeclaredConstructor(int.class, String.class, String.class);
        // 因为是私有的,先破坏其封装,再操作
        declaredConstructor.setAccessible(true);
        String name = declaredConstructor.getName();
        TypeVariable<? extends Constructor<?>>[] typeParameters = declaredConstructor.getTypeParameters();
        System.out.println(name); // com.reflection.learn.Person
        Person person = (Person) declaredConstructor.newInstance(18, "HuGe", "123456");
        System.out.println(person.getName()); // HuGe
        System.out.println(person.getAge()); // 18
	}
}

✨1.5 小结

java.lang.Class:是反射的源头:我们创建一个类,通过编译器javac.exe生成对应的.class文件。之后使用java.exe加载(JVM 的类加载器)此.class文件到内存中以后,此文件就是一个运行时类,存在于缓冲区中。这个运行时类本身就是一个Class是实例。且一个运行时类只加载一次。


💥二、热兵器时代:工具类增强反射

在第一章节介绍完原生反射的源码部分后,本节来介绍一下Spring框架中的反射工具类:org.springframework.util.ReflectionUtils。它不仅封装了一些反射常用API,处理了一些常见的反射相关的异常,让我们更加方便的使用反射,而且在此基础上还采用了缓存的思想,降低了反射过程中带来的性能损耗。

✨2.1 访问字段

核心方法备注说明
public static Field findField(Class<?> clazz, String name)根据指定字段名返回字段类
public static Field findField(Class<?> clazz, @Nullable String name, @Nullable Class<?> type)根据指定字段名和其类型返回字段类
public static Field findFieldIgnoreCase(Class<?> clazz, String name)根据指定字段名返回字段类,且忽略其大小写
public static void setField(Field field, @Nullable Object target, @Nullable Object value)给对象字段赋值
public static Object getField(Field field, @Nullable Object target)获得对象的字段值
public static void doWithFields(Class<?> clazz, FieldCallback fc)对目标类及其所有父类中声明的字段执行给定的回调操作
public static void makeAccessible(Field field)破坏字段的封装性,修改其accessible属性

✨2.2 调用方法

核心方法备注说明
public static Method findMethod(Class<?> clazz, String name)按照方法名查找指定类中的方法(会逐级向上查找其父类)
public static Method findMethod(Class<?> clazz, String name, @Nullable Class<?>… paramTypes)按照方法名和参数类型查找指定类中的方法
public static Object invokeMethod(Method method, @Nullable Object target)执行某个对象的指定无参方法
public static Object invokeMethod(Method method, @Nullable Object target, @Nullable Object… args)执行某个对象的指定带参方法
public static void doWithMethods(Class<?> clazz, MethodCallback mc)对目标类及其所有父类中声明的字段执行给定的回调操作
public static Method[] getAllDeclaredMethods(Class<?> leafClass)获取类中声明的所有方法,包括继承的方法,不排除重写的方法
public static Method[] getUniqueDeclaredMethods(Class<?> leafClass)获取类中声明的所有方法,包括继承的方法,但排除重写的方法,确保每个方法只出现一次
private static Method[] getDeclaredMethods(Class<?> clazz)获取类中直接声明的所有方法,不包括继承的方法
public static void makeAccessible(Method method)破坏方法的封装性,修改其accessible属性
public static boolean isEqualsMethod(@Nullable Method method)判断给定的方法是否为equals方法
public static boolean isHashCodeMethod(@Nullable Method method)判断给定的方法是否为hashCode方法
public static boolean isToStringMethod(@Nullable Method method)判断给定的方法是否为toString方法

✨2.3 构造函数

核心方法备注说明
public static Constructor accessibleConstructor(Class clazz, Class<?>… parameterTypes)根据类和构造器参数获得相应构造方法

✨2.4 异常处理

核心方法备注说明
public static void handleReflectionException(Exception ex)封装了对反射操作中可能抛出的异常的处理逻辑

✨2.5 缓存机制

核心方法备注说明
private static final Map<Class<?>, Method[]> declaredMethodsCache = new ConcurrentReferenceHashMap(256)缓存类的方法
private static final Map<Class<?>, Field[]> declaredFieldsCache = new ConcurrentReferenceHashMap(256)缓存类的的字段
public static void clearCache()用于清除内部的方法和字段缓存

✨2.6 小结

ReflectionUtils类是Spring框架提供的一个工具类,旨在简化Java反射的使用,使得开发者可以更方便地进行反射操作,同时减少了代码的冗余和复杂性。通过提供一系列静态方法,它帮助开发者在运行时查询和操作对象的状态,而无需直接处理底层的反射API和相关的异常处理。尽管如此,反射操作可能会破坏封装性、增加性能开销,并可能引发安全问题。因此,在不需要动态访问的情况下,需要慎重再慎重,最好避免使用反射。


💥三、性能对比与分析

✨性能对比

日常开发中,经常会用到反射,但是经常会忽略其性能,反射的性能确实比普通的方法性能差很多。既然ReflectionUtils是在原生反射的基础上延申并优化而来的,那我们用原生反射的方法和ReflectionUtils工具类中反射方法进行对比,对比一下在不同循环次数下的差异,测试demo如下:

    @Test
    public void test_Reflect1() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Person person = new Person();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 20000000; i++) {
            Method test1 = Person.class.getMethod("setName", String.class);
            Object invoke = test1.invoke(person, "xiaoming");
        }
        long end = System.currentTimeMillis();
        System.out.println("耗时:" +  (end - start));
    }

    @Test
    public void test_Reflect2() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Person person = new Person();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 20000000; i++) {
            Method test2 = ReflectionUtils.findMethod(Person.class, "setName", String.class);
            Object invoke = test2.invoke(person, "xiaoming");
        }
        long end = System.currentTimeMillis();
        System.out.println("耗时:" +  (end - start));
    }

上面测试demo经过不同循环次数的实验后,实验效果如下表:

调用方法次数1100010000100000100000010000000
原生耗时ms1314895334434
工具类耗时ms11152460120696

✨分析

原生反射的部分源码和ReflectionUtils工具类反射部分源码如下:

// 原生反射部分源码
public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
        throws NoSuchMethodException, SecurityException {
	checkMemberAccess(Member.DECLARED, Reflection.getCallerClass(), true);
	Method method = searchMethods(privateGetDeclaredMethods(false), name, parameterTypes);
	if (method == null) {
		throw new NoSuchMethodException(getName() + "." + name + argumentTypesToString(parameterTypes));
	}
	return method;
}

private Method[] privateGetDeclaredMethods(boolean publicOnly) {
	checkInitted();
	Method[] res;
	ReflectionData<T> rd = reflectionData();
	if (rd != null) {
		res = publicOnly ? rd.declaredPublicMethods : rd.declaredMethods;
		if (res != null) return res;
	}
	// No cached value available; request value from VM
	res = Reflection.filterMethods(this, getDeclaredMethods0(publicOnly));
	if (rd != null) {
		if (publicOnly) {
			rd.declaredPublicMethods = res;
		} else {
			rd.declaredMethods = res;
		}
	}
	return res;
}

private static Method searchMethods(Method[] methods,
                                        String name,
                                        Class<?>[] parameterTypes){
	Method res = null;
	String internedName = name.intern();
	for (int i = 0; i < methods.length; i++) {
		Method m = methods[i];
		if (m.getName() == internedName && arrayContentsEq(parameterTypes, m.getParameterTypes())
			&& (res == null || res.getReturnType().isAssignableFrom(m.getReturnType())))
			res = m;
	}
	// 利用工厂创建对象
	return (res == null ? res : getReflectionFactory().copyMethod(res));
}
    
// ====================================================================================================

// ReflectionUtils工具类反射部分源码
public static Method findMethod(Class<?> clazz, String name, Class<?>... paramTypes) {
	Assert.notNull(clazz, "Class must not be null");
	Assert.notNull(name, "Method name must not be null");
	// 会逐级向上查找其父类的方法
	for(Class<?> searchType = clazz; searchType != null; searchType = searchType.getSuperclass()) {
		Method[] methods = searchType.isInterface() ? searchType.getMethods() : getDeclaredMethods(searchType);
		Method[] var5 = methods;
		int var6 = methods.length;
	
		for(int var7 = 0; var7 < var6; ++var7) {
			Method method = var5[var7];
			if (name.equals(method.getName()) && (paramTypes == null || Arrays.equals(paramTypes, method.getParameterTypes()))) {
			return method;
			}
		}
	}
	
	return null;
}

private static Method[] getDeclaredMethods(Class<?> clazz) {
	Assert.notNull(clazz, "Class must not be null");
	Method[] result = (Method[])declaredMethodsCache.get(clazz);
	if (result == null) {
		// 这里调用原生反射API
		Method[] declaredMethods = clazz.getDeclaredMethods();
		List<Method> defaultMethods = findConcreteMethodsOnInterfaces(clazz);
		if (defaultMethods != null) {
			result = new Method[declaredMethods.length + defaultMethods.size()];
			System.arraycopy(declaredMethods, 0, result, 0, declaredMethods.length);
			int index = declaredMethods.length;

			for(Iterator var5 = defaultMethods.iterator(); var5.hasNext(); ++index) {
				Method defaultMethod = (Method)var5.next();
				result[index] = defaultMethod;
			}
		} else {
			result = declaredMethods;
		}

		declaredMethodsCache.put(clazz, result.length == 0 ? NO_METHODS : result);
	}

	return result;
}

public Method[] getDeclaredMethods() throws SecurityException {
	checkMemberAccess(Member.DECLARED, Reflection.getCallerClass(), true);
	return copyMethods(privateGetDeclaredMethods(false));
}

先对比源码:ReflectionUtils是在原生反射API的基础上又套了一层缓存declaredMethodsCache,类型是ConcurrentReferenceHashMap类型,且属于强引用,除此之外,其还舍弃了原生反射中的getReflectionFactory().copyMethod(res)等步骤。再结合不同次数下的实验结果来分析和推测:最开始原生反射速度会快于ReflectionUtils工具类,可能是因为要构建缓存。由于原生反射的缓存ReflectionData是软引用,而当循环次数越来越多,对象不断构建可能会导致gc发生,缓存会被清空,进而导致需要重新加载影响了性能。


💥四、反射优缺点

✨Java反射的优点

  1. 增加了程序的灵活性和拓展性,可以在运行的过程中动态对类进行修改和操作。
  2. 提高代码的复用率,比如动态代理 ,就是用到了反射来实现。
  3. 可以在运行时轻松获取任意一个类的方法、属性,并且还能通过反射进行动态调用。

✨Java反射的缺点

  1. 反射会需要运行时动态解析类型和方法信息,导致JVM无法对这些代码进行优化,进而导致性能要比非反射调用更低。
  2. 使用反射以后,代码的可读性会下降。
  3. 反射可以绕过一些限制访问的属性或者方法,会破坏封装性,可能会导致破坏了代码本身的抽象性。
  4. 带来了安全限制,因为使用反射需要程序运行没有安全方面的限制。

💥五、反射应用

✨5.1 代理模式中的动态代理

在JDK原生动态代理里,在获取代理示例对象过程中,会通过target.getClass().getClassLoader()来获取到目标对象的类加载器,通过target.getClass()方式获取目标对象的Class实例对象,而这些操作都有利用到Java的反射机制。

✨5.2 Java JDBC数据库操作

相信很多朋友都知道Java JDBC连接数据库主要分为七大步骤,其中第一步加载JDBC驱动,我们想在某些场景使用Oracle数据库,某些场景使用Mysql数据,就可以通过反射来动态地加载数据库驱动,从而使得程序更加灵活。

// 通过Class.forName()方法加载驱动程序类

// 加载JDBC-Mysql驱动程序:
Class.forName("com.mysql.cj.jdbc.Driver");
// 加载JDBC-Oracle驱动程序:
Class.forName("oracle.jdbc.driver.OracleDriver");

✨5.3 框架开发

许多Java框架和库依赖反射来提供通用功能。典型的例子包括:
Spring :使用反射来实现依赖注入、AOP(面向切面编程)。除此之外,反射还用于解析配置文件中指定的类名,实例化对象并调用相应的方法,使得配置更加灵活且易于维护。
Hibernate :使用反射来实现对象关系映射(ORM),在运行时动态地将数据库表与Java对象映射。

<bean id="person" class="com.wilf.boat.Person"></bean>

例:当在xml文件中配置类时,Java代码中可以通过如下方式获取

ApplicationContext ctx = new ClassPathXmlApplicationContext("config.xml");
Person person = (Person) ctx.getBean("person");

✨5.4 单元测试

在单元测试中,反射可以被用来调用私有方法、设置私有字段等,以便更好地进行测试,并确保代码的健壮性和可维护性。单元测试的详细介绍见我的代码质量专栏中:单元测试篇之Mockito+PowerMock。这里仅截取单元测试引入的jar包中利用反射的一些片段。

// 1.私有方法的执行
Whitebox.invokeMethod(A.Class, "methodA", arg1,arg2...);
Whiteboximpl.invokeMethod(A.Class, "methodA", arg1,arg2...);

// 2.mock本类的私有方法
MemberModifier.stub(MemberMatcher.method(Person.class, "methodB", int.class, List.class)).toReturn(2);

// 3.给变量赋值
WhiteboxImpl.setInternalState(Person.class, "fieldA", 1);
.....

在这里插入图片描述

如果对单元测试框架Mockito和PowerMock框架熟悉的朋友可能会知道,其实反射就是这两个框架的核心。喜欢点进源码去搂一眼的伙伴会发现一个情况,就是PowerMock框架的作者在实现使用反射的时候,考虑到反射对性能的影响,其实也是利用了缓存的,如下图所示。(如果有朋友点进ProxyFrameworks类里会发现"CGLIB$SET_THREAD_CALLBACKS",一看CGLIB就想起了动态代理,其实单元测试就充分利用了两种动态代理相互配合。)
在这里插入图片描述

✨5.5 工具库

反射允许创建通用的库和工具,这些库和工具可以在不同类型的对象上工作,而不需要知道具体的类,例如我们常用到的Gson等工具类。


创作不易,如果有帮助到你的话请给点个赞吧!我是Wasteland,下期文章再见!
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wasteland~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值