深入浅出Java反射
反射,它就像是一种魔法,引入运行时自省能力,赋予了 Java 语言令人意外的活力,通过运行时操作元数据或对象,Java 可以灵活地操作运行时才能确定的信息
这里笔者就深入浅出总结下Java反射,若有不正确地方,感谢评论区指正交流~ 建议打开idea,写一个Java反射的demo,跟着调试,效果会更好 :)
反射的概念是由Smith在1982年首次提出的,主要是指程序可以访问、检测和修改它本身状态或行为的一种能力。有了反射,使Java相对于C、C++等语言就有了很强大的操作对象属性及其方法的能力,注意,反射与直接调用对象方法和属性相比,性能有一定的损耗,但是如果不是用在对性能有很强的场景下,反射都是一个很好且灵活的选择。
说到反射,首先要了解什么是Class。每个类都会产生一个对应的Class对象,一般保存在.class文件中。所有类都是在对其第一次使用时,动态加载到JVM的,当程序创建一个对类的静态成员的引用时,就会加载这个类。Class对象仅在需要的时候才会加载,static初始化是在类加载时进行的。类加载时,类加载器首先会检查这个类的Class对象是否已被加载过,如果尚未加载,默认的类加载器就会根据类名查找对应的.class文件。
class文件
任何一个Class文件都对应着唯一一个类或接口的信息(这里的类包括抽象类哈),但反过来,类或接口信息并不一定都定义在文件里(比如类或接口可能动态生成,Spring中AOP的实现中就有可能动态生成代理类)。Class文件是一组以8字节为基础单位的二进制文件,各个数据项严格按照顺序紧凑着排列,中间几乎没有任何分隔符,也就是说整个Class文件存储的几乎都是程序运行所需的必要数据。
想在运行时使用类型信息,必须获取对象(比如类Base对象)的Class对象的引用,使用Class.forName(“Base”)可以实现该目的,或者使用Base.class。注意,有一点很有趣,使用”.class”来创建Class对象的引用时,不会自动初始化该Class对应类,使用forName()会自动初始化该Class对应类。使用”.class”不会自动初始化是因为被延迟到了对静态方法(构造器隐私地是静态的)或者非常数静态域进行首次引用时才进行。
为了使用类而做的准备工作一般有以下3个步骤:
- 加载:由类加载器完成,找到对应的字节码,创建一个Class对象
- 链接:验证类中的字节码,为静态域分配空间
- 初始化:如果该类有超类,则对其初始化,执行静态初始化器和静态初始化块
如果不知道某个对象的确切类型,RTTI可以告诉你,但是有一个前提:这个类型在编译时必须已知,这样才能使用RTTI来识别它。而Class类与java.lang.reflect类库一起对反射进行了支持,该类库包含Field、Method和Constructor类,这些类的对象由JVM在启动时创建,用以表示未知类里对应的成员。
反射机制并没有什么神奇之处,当通过反射与一个未知类型的对象打交道时,JVM只是简单地检查这个对象,看它属于哪个特定的类。因此,那个类的.class对于JVM来说必须是可获取的,要么在本地机器上,要么从网络获取。所以对于RTTI和反射之间的真正区别只在于:
- RTTI:编译器在编译时打开和检查.class文件
- 反射:运行时打开和检查.class文件
反射应用实践
反射获取对象中所有属性值:
public class Person {
private String name;
private int age;
// ...
} Person person = new Person();
person.setName("luo");
person.setAge(25); try {
Class clazz = person.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
System.out.println(field.getType() + " | " + field.getName() + " = " + field.get(person));
} // 通过反射获取某一个方法
Method method = clazz.getMethod("setName", String.class);
method.invoke(person, "bei");
} catch (Exception e) {
e.printStackTrace();
}
平常的项目开发基本很少与反射打交道,因为框架已经帮我们做了很多的事情了。但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架,也是利用CGLIB 反射机制才得以实现。
反射技术在在框架和中间件技术应用较多,有一句老话就是反射是Java框架的基石。典型的使用就是Spring的IoC实现,不管对象谁管理创建,只要我能用就行。再比如RPC技术可以借助于反射来实现,本地主机将要远程调用的对象方法等信息发送给远程主机,这些信息包括class名、方法名、方法参数类型、方法入参等,远程主机接收到这些信息后就可以借助反射来获取并执行对象方法,然后将结果返回即可。
说了那么多,那么Java反射是如何实现的呢?简单来说Java反射就是靠JVM和Class相关类来实现的,Class相关类包括Field、Method和Constructor类等。类加载器加载完成一个类之后,会生成类对应的Class对象、Field对象、Method对象、Constructor对象,这些对象都保存在JVM(方法区)中,这也说明了反射必须在加载类之后进行的原因。使用反射时,其实就是与上述所说的这几个对象打交道呀(貌似Java反射也就这么一回事哈)。
既然了解了Java反射原理,可以试想一下C++为什么没有反射呢,想让C++拥有反射该如何做呢?Java相对于C++实现反射最重要的差别就是Java可以依靠JVM这一悍将,可以由JVM保存对象的相关信息,然后应用程序使用时直接从JVM中获取使用。但是C++编译后直接变成了机器码了,貌似类或者对象的啥信息都没了。。。 其实想让C++有用反射能力,就需要保存能够操作类方法、类构造方法、类属性的这些信息,这些信息要么由应用程序自己来做,要么由第三方工具来保存,然后应用程序使用从它那里获取,这些信息可以通过(函数)指针来记录,使用时通过指针来调用。
反射机制
这里我们以Method.invoke流程来分析反射流程:
public class Person {
private String name;
private int age; public String getName() {
return name;
}
// 其他setter/getter方法
} public static void main(String[] args) throws Exception {
Person person = new Person();
person.setName("luo");
person.setAge(26); for (int i = 0; i < 20; i++) {
Method method = Person.class.getMethod("getName");
System.out.println(method.invoke(person));
}
}
以上代码通过反射调用person对象中的方法,下面跟着源码看下Method.invoke的执行流程:
// Method
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
// 这里会调用reflectionFactory.newMethodAccessor(this)创建一个新的MethodAccessor
// 并赋值给methodAccessor,下次就不会进入到这里了
// ma实际类型是DelegatingMethodAccessorImpl,代理对目标方法的调用
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
} class DelegatingMethodAccessorImpl extends MethodAccessorImpl {
private MethodAccessorImpl delegate;
DelegatingMethodAccessorImpl(MethodAccessorImpl var1) {
this.setDelegate(var1);
} public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
return this.delegate.invoke(var1, var2);
} void setDelegate(MethodAccessorImpl var1) {
this.delegate = var1;
}
} // NativeMethodAccessorImpl
public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
// ReflectionFactory.inflationThreshold()默认15,如果某一个Method反射调用超过15次,
// 则自动生成GeneratedMethodAccessor赋值给DelegatingMethodAccessorImpl.delegate
if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
// 通过asm自动生成MethodAccessorImpl的实现类GeneratedMethodAccessor
MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
this.parent.setDelegate(var3);
} return invoke0(this.method, var1, var2);
}
// native方法,jni方式调用对应方法,调用的是对应的java方法
private static native Object invoke0(Method var0, Object var1, Object[] var2);
下面分别看下NativeMethodAccessorImpl和GeneratedMethodAccessor的调用栈信息:
Java默认在执行Method.invoke超过15次时(通过-Dsun.reflect.inflationThreshold可更改次数值,每个方法对应一个Method对象),JVM会通过asm生成GeneratedMethodAccessor类,由该类调用对应的method方法。执行NativeMethodAccessorImpl.invoke是通过调用JNI方法,在JNI方法中再调用对应的java method方法,这种方式相对于使用GeneratedMethodAccessor.invoe方法来说,前者性能较弱,原因有以下几点:
- 针对本地方法,jvm无法优化,无法动态inline,其他高级的优化方案都无法优化jni。
- 执行native涉及到运行栈切换(虚拟机栈切换到本地方法栈),如果本地方法中再调用java方法是有一定的开销的,肯定比不上Java中调用Java方法。
- 二者内存模型不一样,参数需要转换,比如字符串,数组,复杂结构。转换成本非常高。此开销和调用接口参数有关。
在默认情况下,方法的反射调用为委派实现,委派给本地实现来进行方法调用。在调用超过 15 次之后,委派实现便会将委派对象切换至动态实现。这个动态的字节码是在Java运行过程中通过ASM自动生成的,它将直接使用 invoke 指令来调用目标方法。Java实现的版本在初始化时需要较多时间,但长久来说性能较好;native版本正好相反,启动时相对较快,但运行时间长了之后速度就比不过Java版了。这是HotSpot的优化方式带来的性能特性,同时也是许多虚拟机的共同点:跨越native边界会对优化有阻碍作用,它就像个黑箱一样让虚拟机难以分析也将其内联,于是运行时间长了之后反而是托管版本的代码更快些。
GeneratedMethodAccessor机制
默认method.invoke调用超过15次,会调用MethodAccessorGenerator.generate生成对应GeneratedMethodAccessorN类,代码如下:
// MethodAccessorGenerator
private MagicAccessorImpl generate(final Class<?> var1, String var2, Class<?>[] var3, Class<?> var4, Class<?>[] var5, int var6, boolean var7, boolean var8, Class<?> var9) {
ByteVector var10 = ByteVectorFactory.create();
this.asm = new ClassFileAssembler(var10);
this.declaringClass = var1;
this.parameterTypes = var3;
this.returnType = var4;
this.modifiers = var6;
this.isConstructor = var7;
this.forSerialization = var8;
this.asm.emitMagicAndVersion();
short var11 = 42;
boolean var12 = this.usesPrimitiveTypes();
if (var12) {
var11 = (short)(var11 + 72);
} if (var8) {
var11 = (short)(var11 + 2);
} var11 += (short)(2 * this.numNonPrimitiveParameterTypes());
this.asm.emitShort(add(var11, (short)1));
final String var13 = generateName(var7, var8);
this.asm.emitConstantPoolUTF8(var13);
this.asm.emitConstantPoolClass(this.asm.cpi());
this.thisClass = this.asm.cpi();
if (var7) {
if (var8) {
this.asm.emitConstantPoolUTF8("sun/reflect/SerializationConstructorAccessorImpl");
} else {
this.asm.emitConstantPoolUTF8("sun/reflect/ConstructorAccessorImpl");
}
} else {
this.asm.emitConstantPoolUTF8("sun/reflect/MethodAccessorImpl");
} this.asm.emitConstantPoolClass(this.asm.cpi());
this.superClass = this.asm.cpi();
this.asm.emitConstantPoolUTF8(getClassName(var1, false));
this.asm.emitConstantPoolClass(this.asm.cpi());
this.targetClass = this.asm.cpi();
short var14 = 0;
if (var8) {
this.asm.emitConstantPoolUTF8(getClassName(var9, false));
this.asm.emitConstantPoolClass(this.asm.cpi());
var14 = this.asm.cpi();
} this.asm.emitConstantPoolUTF8(var2);
this.asm.emitConstantPoolUTF8(this.buildInternalSignature());
this.asm.emitConstantPoolNameAndType(sub(this.asm.cpi(), (short)1), this.asm.cpi());
if (this.isInterface()) {
this.asm.emitConstantPoolInterfaceMethodref(this.targetClass, this.asm.cpi());
} else if (var8) {
this.asm.emitConstantPoolMethodref(var14, this.asm.cpi());
} else {
this.asm.emitConstantPoolMethodref(this.targetClass, this.asm.cpi());
} this.targetMethodRef = this.asm.cpi();
if (var7) {
this.asm.emitConstantPoolUTF8("newInstance");
} else {
this.asm.emitConstantPoolUTF8("invoke");
} this.invokeIdx = this.asm.cpi();
if (var7) {
this.asm.emitConstantPoolUTF8("([Ljava/lang/Object;)Ljava/lang/Object;");
} else {
this.asm.emitConstantPoolUTF8("(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;");
} this.invokeDescriptorIdx = this.asm.cpi();
this.nonPrimitiveParametersBaseIdx = add(this.asm.cpi(), (short)2); for(int var15 = 0; var15 < var3.length; ++var15) {
Class var16 = var3[var15];
if (!isPrimitive(var16)) {
this.asm.emitConstantPoolUTF8(getClassName(var16, false));
this.asm.emitConstantPoolClass(this.asm.cpi());
}
} this.emitCommonConstantPoolEntries();
if (var12) {
this.emitBoxingContantPoolEntries();
} if (this.asm.cpi() != var11) {
throw new InternalError("Adjust this code (cpi = " + this.asm.cpi() + ", numCPEntries = " + var11 + ")");
} else {
this.asm.emitShort((short)1);
this.asm.emitShort(this.thisClass);
this.asm.emitShort(this.superClass);
this.asm.emitShort((short)0);
this.asm.emitShort((short)0);
this.asm.emitShort((short)2);
this.emitConstructor();
this.emitInvoke();
this.asm.emitShort((short)0);
var10.trim();
final byte[] var17 = var10.getData(); // class数据
return (MagicAccessorImpl)AccessController.doPrivileged(new PrivilegedAction<MagicAccessorImpl>() {
public MagicAccessorImpl run() {
try {
// 生成反射类GeneratedMethodAccessorN,对应的classLoader为DelegatingClassLoader
return (MagicAccessorImpl)ClassDefiner.defineClass(var13, var17, 0, var17.length, var1.getClassLoader()).newInstance();
} catch (IllegalAccessException | InstantiationException var2) {
throw new InternalError(var2);
}
}
});
}
}
注意:生成对应GeneratedMethodAccessorN类,其对应的classLoader是DelegatingClassLoader,生成的GeneratedMethodAccessorN类是放在永久代的,那么就会产生一个问题,如果数量过多,则会占用永久代太多空间(java8中已没有永久代空间,类数据放在直接内存中)。
生成的GeneratedMethodAccessorN类是什么样的呢?如下所示:
package sun.reflect; import com.luo.test.InvokeBean;
import java.lang.reflect.InvocationTargetException; public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
public GeneratedMethodAccessor1() {
} public Object invoke(Object var1, Object[] var2) throws InvocationTargetException {
if (var1 == null) {
throw new NullPointerException();
} else {
InvokeBean var10000;
Integer var10001;
try {
var10000 = (InvokeBean)var1;
if (var2.length != 1) {
throw new IllegalArgumentException();
} var10001 = (Integer)var2[0];
} catch (NullPointerException | ClassCastException var4) {
throw new IllegalArgumentException(var4.toString());
} try {
return var10000.test(var10001);
} catch (Throwable var3) {
throw new InvocationTargetException(var3);
}
}
}
}
其实就是直接调用目标对象的具体方法了,和正常的方法调用没太大区别。关于GeneratedMethodAccessorN类加载器更多细节可点击https://www.sohu.com/a/124124072_494943查看~
参考资料:
2、http://rednaxelafx.iteye.com/blog/548536
3、https://www.sohu.com/a/124124072_494943
深入浅出Java反射的更多相关文章
- 第28章 java反射机制
java反射机制 1.类加载机制 1.1.jvm和类 运行Java程序:java 带有main方法的类名 之后java会启动jvm,并加载字节码(字节码就是一个类在内存空间的状态) 当调用java命令 ...
- Java反射机制
Java反射机制 一:什么事反射机制 简单地说,就是程序运行时能够通过反射的到类的所有信息,只需要获得类名,方法名,属性名. 二:为什么要用反射: 静态编译:在编译时确定类型,绑定对象,即通过 ...
- java反射(基础了解)
package cn.itcast_01; /** *Person类 */ public class Person { /** 姓名 */ private String name; ...
- java基础知识(十一)java反射机制(上)
java.lang.Class类详解 java Class类详解 一.class类 Class类是java语言定义的特定类的实现,在java中每个类都有一个相应的Class对象,以便java程序运行时 ...
- java基础知识(十一)java反射机制(下)
1.什么是反射机制? java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象都能够调用他的属性和方法,这种动态获取属性和方法的功能称为java的反射机制. ...
- java反射学习之二万能EXCEL导出
一.EXCEL导出的实现过程 假设有一个对象的集合,现在需要将此集合内的所有对象导出到EXCEL中,对象有N个属性:那么我们实现的方式是这样的: 循环这个集合,在循环集合中某个对象的所有属性,将这个对 ...
- java反射学习之一反射机制概述
一.反射机制背景概述 1.反射(reflection)是java被视为动态语言的一个关键性质 2.反射机制指的是程序在运行时能获取任何类的内部所有信息 二.反射机制实现功能概述 1.只要给定类的全名, ...
- java反射 之 反射基础
一.反射 反射:JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意一个方法和属性:这种动态获取的信息以及动态调用对象的方法的功能称为 ...
- java反射 cglib asm相关资料
有篇文章对java反射的调用的效率做了测试,写的比较好.猛击下面地址 http://www.blogjava.net/stone2083/archive/2010/09/15/332065.html ...
随机推荐
- Android自定义多宫格解锁控件
在此之前,一直在想九宫格的实现方法,经过一个上午的初步研究终于完成了一个简单的N*N的宫格解锁组件,代码略显粗糙,仅仅做到简单的实现,界面等后期在做优化,纯粹是学习的目的,在算法上有点缺陷,如果有错误 ...
- (网页)thinkpad 笔记本开机看看坏了没?
嗯!只是想看一下是不是块砖头. 出现许多细节问题: 1.不用注册联机的乱七八糟的,验证还慢,直接选择脱机的. 2.推荐密码:zaq!2wsx直接进去,以后可以改. 3.有很多选项都可以不勾选,(只勾选 ...
- 解决Chrome与jQuery菜单兼容问题
题外,Chrome最近在耗电量方面超过了Edge,不过内存占用还是高啊,开发时偶尔用用.这不,http://jqueryui.com/menu/的官方菜单都支持的不好,改改吧! 打开jquery-ui ...
- html:常见行内标签,常见块级标签,常见自闭合标签
本文内容: 常见行内标签 常见块级标签 常见自闭合标签 首发日期:2018-02-12 修改: 2018-04-25:删除了不常用的标签 常见行内标签: 什么是行内标签: 行内标签就是在页面内只占据刚 ...
- no plugin found for prefix 'tomcat 7' in the current project and in the plugin groups的解决方法
解决方法一: 找到这个settings.xml文件,进行编辑,在pluginGroups标签下加入下面的配置 <pluginGroups><pluginGroup>org.ap ...
- LeetCode算法题-Best Time to Buy and Sell Stock
这是悦乐书的第172次更新,第174篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第31题(顺位题号是121).假设有一个数组,其中第i个元素是第i天给定股票的价格.如果 ...
- [福大软工] Z班 团队作业——系统设计 作业成绩
团队作业--系统设计 作业链接 http://www.cnblogs.com/easteast/p/7709763.html 作业情况 这次作业大家完成度都很高,大家的团队分工,任务布置都安排得很到位 ...
- [福大软工] Z班 团队Alpha阶段成绩汇总
团队成绩汇总表 团队 T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 T11 总分 Dipper 9 85 90 26 42 27.5 120 74 25 111 19 628.5 SW ...
- 创建 tomcat 服务的镜像
如何设计 Tomcat 的 Dockerfile $ sudo docker search tomcat |wc -l 285 在 dockerhub 上搜索与 tomcat 相关的镜像,有如此之多的 ...
- jQ not()选择器 与 css3 :not( selector )选择器
1.jQ not() 2.css3 not w3c在线演示地址 http://www.w3school.com.cn/tiy/t.asp?f=css_sel_not 总结: 注意两者还是有区别的 ...