聊聊反射中的那些事~

145 阅读7分钟

反射在我们日常的业务开发中其实用到的机会很少,而当我们学习某些设计模式或者看某些框架的源码的时候,你会发现有很多的反射的应用,那么在这里聊一聊反射的那些事。

反射是什么?

反射指的是程序在运行时可以获取自身的信息,在Java中给定了类的名字,就可以知道里面的属性和方法。通俗点说就是可以透视了,在我面前毫无保留。

用反射我们可以做什么?

  • 1、在运行时我们可以判断任意一个对象属于哪个类。
  • 2、在运行时我们可以获取一个类所具有的成员变量和方法。
  • 3、在运行时任意调用对象的方法。
  • 4、在运行时构造任意一个类的对象newInstance()。

反射的好处就是可以大大的提高灵活性和扩展性,可以在运行时做非常多的事情,但是它的问题也比较明显。

  • 1、在写反射的代码的时候多数情况下都是非常抽象的,无法确定我们所执行代码的具体的业务含义,所以它的可读性和可维护性是比较差的。
  • 2、反射的执行效率是比较低的(后面会说为什么低)。
  • 3、反射其实破坏了封装性,我们封装一个类其实要屏蔽很多类中的成员变量和方法,而反射却能获取并使用它们,所以其实反射是破坏了封装性的。

所以我们在平常的业务开发的时候最好能避免使用反射,但是我们一定要学反射,这对我们读一些中间件的代码和做一些通用能力的组件或者中间件的时候有很大的帮助。

反射为什么慢?

  • 1、反射的代码不能执行某些虚拟机的优化。我们知道Java程序的运行过程是先将Java文件编译成Java字节码文件,也就是class文件,然后调用的时候将字节码文件交给JVM虚拟机进行解释执行。而为了提升执行效率JVM增加了一些优化的手段,如JIT优化,简单来说就是会将我们的执行频繁的代码(热点代码)通过JIT解析成机器码缓存起来,以便后续使用。反射的代码是运行时动态解析的,代码的内容是不确定的,所以不能执行优化。
  • 2、使用反射时,参数都需要包装成Object[]类型,但是当方法真正执行的时候,又需要拆包成真正的类型,这些过程不仅消耗时间,执行过程中还会产生很多对象,进而影响GC。
  • 3、反射调用方法的时候会从方法数组中遍历查找,然后检查可见性,同时参数也需要进行额外的检查,比如类型检查,这些动作都是耗时的。

反射怎么拿到的类的各种信息的?

Java中的Class是java反射机制的基础,通过Class类我们可以获的一个类的相关信息了,Class是一个比较特殊的类,它用于封装被加载到JVM的类的信息,加载过后就会与之生成一个Class对象,而我们所通过反射获的的方法,成员变量其实都是对Class对象的访问。每一个类都有一个Class对象,并且是唯一的,运行程序时JVM会进行检查所需要加载的Class是否已经加载,如果没有加载JVM会根据类名查找class文件,然后将其加载,这相关的类加载的知识后面会单独的写出。

刚才我们说反射可以通过构造方法实例化对象,那么Java中有哪些可以创建对象的方式呢?

Java中有哪些种方式创建对象

  • 1、我们最常用的new的方式
  • 2、上面说到的反射的方式newInstance()
  • 3、使用clone方法,这里多说一点。使用clone方法就要实现Cloneable接口,然后实现其定义的clone方法,如果没有重写clone方法,那么就会调用Object类中的clone方法。而这种拷贝的方式分为两种,分别是浅拷贝深拷贝。浅拷贝我们得到的对象其实和源对象是相等的,也就是两个对象的引用其实是一个,而深拷贝是基于浅拷贝的基础上,制定了一个新的对象,那么对clone方法的重写的方式不同就决定着使用哪种拷贝方式。实现深拷贝还有另一种方式,也就是下面要说的使用反序列化。
  • 4、使用反序列化,而反序列化其实也是基于反射实现的,过程就是先将原拷贝对象写入文件,然后读取文件,使用的核心方法就是writeObject,和readObject,实现过程可以自行搜一搜,因为我确实不怎么用。
  • 5、使用方法句柄(MethodHandle),可以简介的调用构造函数创建对象。Java7以后增加java.lang.invoke 包也可以实现动态方法的调用,不同的是,reflect模拟的JAva代码层次方法的调用,而invoke模拟字节码层次的调用,invokeAPi提供了许多静态方法,可以快速创建MethodHandle,比如findConstructor,findGetter等等。所以对于动态方法的调用不仅仅只有反射一种,invokeAPi也是一种,所以在某些中间件中看到MethodHandle不要惊讶 推荐个文章,大家可以看一看,# Java JVM 动态方法调用之方法句柄 MethodHandle
  • 6、使用sun.misc.Unsafe类进行直接的内存操作,包括内存分配和对象实例化,功能很强大,也很危险,不建议使用。首先Unsafe类在不同的Java版本和不同的JVM实现中可能存在差异,而且它可以绕过Java的安全检查,可能导致内存泄漏、非法访问、数据损坏等问题。同时Java鼓励使用构造函数和工厂方法创建对象,确保对象正确初始化。所以慎用。

反射常见的一些使用场景

  • 1、动态代理
  • 2、JDBC中的class.forName
  • 3、BeanUtils中的copy
  • 4、RPC框架
  • 5、ORM框架
  • 6、Spring中IOC和DI 看到了吧大多数的时候场景都是抽象的或者中间件,所以反射是必备的技能。其他的以后慢慢说,在这里最后说一下动态代理吧

动态代理

与之对应的就是静态代理,静态代理是编译器确定的,静态代理其实很简单,最简单的例子就是实现类实现接口,完成接口的调用,也就是我们使用时指定了哪个实现类完成的这个接口能力,但是这有个弊端,就是我们需要手写很多代码的同时,如果一个接口需要代理多个对象的时候,那么这个使用会增加很大的复杂度。所以动态代理就出来了。

动态代理是运行时确定的,反射是动态代理的实现方式之一,动态代理主要的用途就是在各种框架中在运行期生成代理类,通过代理类做很多事情,比如AOP,过滤器,拦截器等。

Java中实现代理的方式有两种。1、JDK动态代理。2、Cglib动态代理。

JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。Cglib大理是在运行时动态生成某个类的子类,所以如果某个类被标记为final,那么它是无法使用Cglib做动态代理的。

SpringBoot2.x中为什么默认指定Cglib代理

上面我们说使用JDK代理的类必须要实现一个接口,它的定义就是生成的代理类需要赋值给一个接口变量,比如

@Autowired
UserService userService;

但是如果是这样

@Autowired
UserServiceImpl userService;

将类型指定为实现类就会导致类型转化异常,大家可以配置一下
spring.aop.proxy-target-class=false然后按照上面的方式试一试 而Cglib是通过生成子类实现的,不管赋值给接口还是实现类都可以。

菜鸡写手,欢迎大家指出错误。