平时写代码的过程中,我们使用不同的工具框架来提升开发效率,除了基础框架之外,我们自己也想造轮子,封装各种业务平台功能;
一旦需造轮子的时候,那么就需要使用Java造轮子利器:反射;
一些项目中常见的反射应用场景:
- 泛化调用: 提前不知道目标RPC的接口和方法,而是开发在后台输入值,根据输入的配置动态请求。 这也是提升效率的一部分,因为不可能所以得RPC接口都要亲自对接的,总要有一部分可以灵活的调用不同接口。
- 自测入口: 我们的逻辑代码一般会散落在应用的不同位置,如果想要debug,一般会有一个核心入口,但是如果核心入口太长,想要进入我们的逻辑分支很难呢? 我们可以统一收口下测试入口,这个测试入口在开发环境下就是可以访问到所有的对象,服务,组件等功能,直接对目标逻辑进行调试即可, 这个也是需要反射.
其实上面两个场景也说明了反射应用场景的一个特点: 我不知道现在要调用哪个类和哪个方法,等到运行时才知道要调用的类和方法。
反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。Oracle文档: 反射让开发人员可以通过外部类的全路径名创建对象,并使用这些类,实现一些扩展的功能。反射让开发人员可以枚举出类的全部成员,包括构造函数、属性、方法。以帮助开发者写出正确的代码。测试时可以利用反射 API 访问类的私有成员,以保证测试代码覆盖率。
概念上的东西就说这么多,回到实际的coding部分:
反射使用
1. 准备测试对象
import io.mybatis.provider.Entity; import javax.persistence.Id; import java.io.Serializable; @Entity.Table(value = "Blog",autoResultMap = true) public class Blog implements Serializable { @Id @Entity.Column private Long id; @Entity.Column private String title; public Blog() { } private Blog(Long id) { this.id = id; } public Blog(Long id, String title) { this.id = id; this.title = title; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } @Override public String toString() { return "Blog{" + "id=" + id + ", title='" + title + '\'' + '}'; } } 复制代码
2. 拿到Class信息
有三种方式可以拿到Class信息:
- 第一种方法有限制条件:需要导入类的包;
- 第二种:需要知道对象的全路径,也是最常使用的方式
- 第三种方法Blog对象,仅仅想拿到Class信息,一般不需要反射。
并且在同一个运行环境中,三种方式拿到的Class对象是同一个,每个类只生成一个Class对象,Class对象一般在JVM的metaspace区域中。
@Test public void testClass() throws ClassNotFoundException { Class<Blog> blogClass = Blog.class; Class<?> blogClassForName = Class.forName("me.aihe.bizim.dal.bean.Blog"); Class<? extends Blog> blogClassForGet = new Blog().getClass(); System.out.println(blogClassForGet == blogClass); System.out.println(blogClassForGet == blogClassForName); } 复制代码
3. 关键API
拿到Class信息之后,我们基本上就可以对这个对象做各自想要的操作了;
反射中有两种命名方式:
- getXXX 获取公共的对象,即标记为public的
- getDeclaredXXX 获取已经声明的,也就是对象中所有的XXX
setAccessible 函数用于动态获取访问权限,一般对象如果声明为private的需要setAccessible(true)才可以进行调用。
构造方法操作 - Constructor
如果类的构造方法一定要传参数,可以根据获取到的Constructor来创建对象;
@Test public void testConstruct() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { Class<?> aClass = Class.forName("me.aihe.bizim.dal.bean.Blog"); Constructor<?>[] constructors = aClass.getConstructors(); Constructor<?>[] declaredConstructors = aClass.getDeclaredConstructors(); for (Constructor<?> constructor : constructors) { System.out.println("getConstructors:" + constructor); } for (Constructor<?> constructor : declaredConstructors) { System.out.println("getDeclaredConstructors:" + constructor); } // 使用构造方法 Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(Long.class); declaredConstructor.setAccessible(true); Object blog = declaredConstructor.newInstance(1L); System.out.println(blog); } 复制代码
属性操作 - Field
拿到了对象的属性之后,可以通过Field进行set和get操作,在更新数据库的时候,如果要带上创建人修改人,可以根据请求上下文中的用户,同feild方式写入到数据库的对象中(对象的命名保持统一)
@Test public void testFiled() throws ClassNotFoundException, IllegalAccessException, NoSuchFieldException { Class<?> aClass = Class.forName("me.aihe.bizim.dal.bean.Blog"); Field title = aClass.getDeclaredField("title"); title.setAccessible(true); Blog obj = new Blog(); System.out.println(obj); title.set(obj,"测试"); System.out.println(obj); System.out.println(title.get(obj)); } 复制代码
一个可以往某个对象根据fieldName设置值,和获取值的工具方法:
public static Object getFiledValue(Object obj,String filedName){ try { Class<?> clazz = obj.getClass(); Field field = null; do { try { field = clazz.getDeclaredField(filedName); if (field != null){ field.setAccessible(true); break; } }catch (Exception e){ } clazz = clazz.getSuperclass(); } while (!clazz.equals(Object.class)); if (field != null){ Object o = field.get(obj); return o; } }catch (Exception e){ log.error("getFiledValue error",e); return null; } return null; } public static void setField(Object obj,String filedName,Object value){ Class<?> clazz = obj.getClass(); Field field = null; do { try { field = clazz.getDeclaredField(filedName); if (field != null){ field.setAccessible(true); } }catch (Exception e){ } clazz = clazz.getSuperclass(); } while (!clazz.equals(Object.class)); if (field != null){ try { field.set(obj,value); }catch (Exception e){ log.error("setField error ",e); } } } 复制代码
方法操作 - Method
和属性操作类似,不同的是方法可能有多个参数;
@Test public void testMethod() throws ClassNotFoundException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { Class<?> aClass = Class.forName("me.aihe.bizim.dal.bean.Blog"); Method setTitle = aClass.getMethod("setTitle", String.class); Method getTitle = aClass.getMethod("getTitle"); Blog obj = new Blog(); System.out.println(obj); setTitle.invoke(obj,"测试"); System.out.println(obj); System.out.println(getTitle.invoke(obj)); } 复制代码
注解操作 - Annoation
注解可以加在字段上,方法上,类上,有时候我们拿到了对象或者方法也不知道做什么,通过注解,可以很好的做想做的事情。当然注解已经不算是反射的范畴了,但是可以让反射的功能更加强大。
- 比如Spring中标记了@RequestMapping注解的方法,我们知道这个是用来处理Http请求的,就可以在框架内做一些处理。
- 一些自定义的注解,在启动的时候扫描到这些注解,将这些对象放到某个集合中统一处理。
获取类、方法、属性上的注解:
@Test public void testAnnotation() throws ClassNotFoundException, NoSuchMethodException, NoSuchFieldException { Class<?> aClass = Class.forName("me.aihe.bizim.dal.bean.Blog"); Annotation[] annotations = aClass.getAnnotations(); Method getTitle = aClass.getMethod("getTitle"); Annotation[] annotations1 = getTitle.getAnnotations(); Field title = aClass.getDeclaredField("title"); Annotation[] annotations2 = title.getAnnotations(); for (Annotation annotation : annotations) { System.out.println("类注解:" + annotation); } for (Annotation annotation : annotations1) { System.out.println("方法注解:" + annotation); } for (Annotation annotation : annotations2) { System.out.println("属性注解:" + annotation); } } 复制代码
4. 泛型操作 - Type
有时候在代码中写的是泛型,不确定具体是什么对象,也可以通过泛型拿到泛型信息: 泛型类型相对比较复杂。
泛型常用API
在类、方法、构造器、属性上都可以获取到泛型:
java.lang.Class中的相关方法:
- Type[] getGenericInterfaces() 返回类实例的接口的泛型类型
- Type getGenericSuperclass() 返回类实例的父类的泛型类型
java.lang.reflect.Constructor中的相关方法:
- Type[] getGenericExceptionTypes() 返回构造器的异常的泛型类型
- Type[] getGenericParameterTypes() 返回构造器的方法参数的泛型类型
java.lang.reflect.Method中的相关方法:
- Type[] getGenericExceptionTypes() 返回方法的异常的泛型类型
- Type[] getGenericParameterTypes() 返回方法参数的泛型类型
- Type getGenericReturnType() 返回方法返回值的泛型类型
java.lang.reflect.Field中的相关方法:
- Type getGenericType() 返回属性的泛型类型
获取泛型具体的Class
@Test public void testGeneric(){ BlogImpl blog = new BlogImpl(); Class kClass = blog.getKClass(); Class vClass = blog.getVClass(); System.out.println(kClass); System.out.println(vClass); } public static abstract class BaseClass<K,V>{ public Class getKClass(){ Type genericSuperclass = this.getClass().getGenericSuperclass(); ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass; Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); Class tClass = (Class) actualTypeArguments[0]; return tClass; } public Class getVClass(){ Type genericSuperclass = this.getClass().getGenericSuperclass(); ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass; Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); Class tClass = (Class) actualTypeArguments[1]; return tClass; } public abstract V execute(K k); } public static class BlogImpl extends BaseClass<Blog,Long>{ @Override public Long execute(Blog blog) { return blog.getId(); } } 复制代码
获取泛型的Class信息
private static Class<?> getDetailClass(Type type, int i) { if (type instanceof ParameterizedType) { // 处理泛型类型 ParameterizedType parameterizedType = (ParameterizedType)type; Type actualTypeArgument = parameterizedType.getActualTypeArguments()[i]; return getDetailClass(actualTypeArgument, i); } else if (type instanceof GenericArrayType) { // 处理数组泛型 return (Class<?>)((GenericArrayType)type).getGenericComponentType(); } else if (type instanceof TypeVariable) { // 处理泛型擦除对象<R> return (Class<?>)getDetailClass(((TypeVariable)type).getBounds()[0], 0); } else { return (Class<?>)type; } } 复制代码
5. 反射工具类
在commons-lang3包中,也已经封装了对应的反射工具,如果已经引入了这个包,直接使用即可:
泛型的性能为什么比直接调用差?
可以看到泛型性能比直接调用差很多,为什么泛型的性能会比正常调用差?
- 泛型在执行的时候会校验方法、字段名称,校验是否有对应的权限,会比直接调用多一部分逻辑。
- 无法被JIT优化,JIT可以帮助java的字节码到原生的机器码层面上,这样的话减少了java字节码的再解析操作,而反射方法是无法被jit优化的。
- 调用过程中的封装与解封操作,invoke 方法的参数是 Object[] 类型,在调用的时候需要进行一次封装。产生了额外的开销。
性能测试代码:
import org.apache.commons.lang3.time.StopWatch; import org.junit.jupiter.api.Test; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class PerformanceTest { private static int COUNT = 100000; @Test public void testReflect() throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { StopWatch started = StopWatch.createStarted(); for (int i = 0; i < COUNT; i++) { Class<?> aClass = Class.forName("me.aihe.bizim.dal.bean.Blog"); Method setTitle = aClass.getMethod("setTitle", String.class); Object obj = aClass.newInstance(); setTitle.invoke(obj,"测试"); } System.out.println("泛型 " + COUNT + "次耗时(ms):" + started.getTime()); } @Test public void testDirectUse() throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { StopWatch started = StopWatch.createStarted(); for (int i = 0; i < COUNT; i++) { Blog blog = new Blog(); blog.setTitle("测试"); } System.out.println("直接调用 " + COUNT + "次耗时(ms):" + started.getTime()); } } 复制代码
总结
- 反射在Java的框架中非常常见,如果说自己想要封装一些通用框架,需要了解反射相关的知识。
- 本文主要是知识层面的内容,介绍了反射常用的API,泛型的一些API;
- 最后就可以验证下泛型是比直接调用性能差一些,主要原因是要校验权限,无法JIT优化,自动装箱拆箱等导致的。
- 泛型的优点是强大且灵活,缺点是难用并且慢...
希望能对大家有所帮助