Java反射特性的使用、特点和原理

798 阅读12分钟

动态代理与CGLIB

CGLIB

CGLIB是一个强大的、高性能的代码生成库。其被广泛应用于AOP框架(Spring、dynaop)中,用以提供方法拦截操作。Hibernate作为一个比较受欢迎的ORM框架,同样使用CGLIB来代理单端(多对一和一对一)关联(延迟提取集合使用的另一种机制)。

使用

实现

CGLIB底层使用了ASM(一个短小精悍的字节码操作框架)来操作字节码生成新的类。除了CGLIB库外,脚本语言(如Groovy何BeanShell)也使用ASM生成字节码。ASM使用类似SAX的解析器来实现高性能。我们不鼓励直接使用ASM,因为它需要对Java字节码的格式足够的了解

注意

由于CGLIB的大部分类是直接对Java字节码进行操作,这样生成的类会在Java的永久堆中。如果动态代理操作过多,容易造成永久堆满,触发OutOfMemory异常。

动态代理

Java动态代理机制的出现,使得Java开发人员不用手工编写代理类,只要简单地制定一组接口及委托类对象,便能动态地获得代理类。代理类会负责将所有的方法调用分配到委托对象上反射执行,配置执行过程中,开发人员还可以进行修改

代理设计模式

代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息、过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。

  • 为了保持行为的一致性,代理类和委托类通常会实现相同的接口
  • 引入代理能够控制对委托对象的直接访问,可以很好的隐藏和保护委托对象,也更加具有灵活性

相关的类和接口

要了解 Java 动态代理的机制,首先需要了解以下相关的类或接口:

  • java.lang.reflect.Proxy:这是 Java 动态代理机制的主类,它提供了一组静态方法来为一组接口动态地生成代理类及其对象
  • java.lang.reflect.InvocationHandler:这是调用处理器接口,它自定义了一个invoke方法,用于几种处理在动态代理类对象上的方法调用。通常在该方法中实现对委托类的代理访问。
  • java.lang.ClassLoader:Proxy 静态方法生成动态代理类同样需要通过类装载器来进行装载才能使用,它与普通类的唯一区别就是其字节码是由 JVM 在运行时动态生成的而非预存在于任何一个.class 文件中。

代理机制及其特点

首先让我们来了解一下如何使用 Java 动态代理。具体有如下四步骤:

  • 通过实现 InvocationHandler 接口创建自己的调用处理器;
  • 通过为 Proxy 类指定 ClassLoader 对象和一组 interface 来创建动态代理类;
  • 通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型;
  • 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。

特点

动态生成的代理类本身的一些特点

  • 包:如果所代理的接口都是 public 的,那么它将被定义在顶层包(即包路径为空),如果所代理的接口中有非 public 的接口(因为接口不能被定义为 protect或private,所以除 public之外就是默认的package访问级别,那么它将被定义在该接口所在包,这样设计的目的是为了最大程度的保证动态代理类不会因为包管理的问题而无法被成功定义并访问;
  • 类修饰符:该代理类具有 final 和 public 修饰符,意味着它可以被所有的类访问,但是不能被再度继承;
  • 类名:格式是“$ProxyN”,其中 N 是一个逐一递增的阿拉伯数字,代表 Proxy 类第 N 次生成的动态代理类,值得注意的一点是,并不是每次调用 Proxy 的静态方法创建动态代理类都会使得 N 值增加,原因是如果对同一组接口(包括接口排列的顺序相同)试图重复创建动态代理类,它会很聪明地返回先前已经创建好的代理类的类对象,而不会再尝试去创建一个全新的代理类,这样可以节省不必要的代码重复生成,提高了代理类的创建效率。
  • 类继承关系:Proxy 类是它的父类,这个规则适用于所有由 Proxy 创建的动态代理类。而且该类还实现了其所代理的一组接口;

代理类实例的一些特点

  • 每个实例都会关联一个InvocationHandler(调用处理器对象),在代理类实例上调用其代理接口中声明的方法时,最终都会由InvocationHandler的invoke方法执行;
  • java.lang.Object中有三个方法也同样会被分派到调用处理器的 invoke 方法执行,它们是 hashCode,equals 和 toString;

被代理接口的一组特点

  1. 要注意不能有重复的接口
  2. 接口对于类装载器必须可见,否则类装载器将无法链接它们
  3. 被代理的所有非 public 的接口必须在同一个包中,接口的数目不能超过65535

缺点

Proxy只能对interface进行代理,无法实现对class的动态代理。观察动态生成的代理继承关系图可知原因,他们已经有一个固定的父类叫做Proxy,Java语法限定其不能再继承其他的父类

Java反射使用

Java 的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法; 并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。

Class 对象理解

要理解Class对象,需要先了解一下RTTI吧。 RTTI(Run-Time Type Identification)运行时类型识别,其作用是在运行时识别一个对象的类型和类的信息。

Java是如何让在运行时识别对象和类的信息的?主要有两种方式: 一种是传统的RRTI,它假定在编译期已知道了所有类型。 另一种是反射机制,它允许在运行时发现和使用类的信息。

每个类都有一个Class对象,每当编译一个新类就产生一个Class对象(更恰当地说,是被保存在一个同名的.class文件中)。比如创建一个Student类,那么,JVM就会创建一个Student对应Class类的Class对象,该Class对象保存了Student类相关的类型信息。

Class类的对象作用是运行时提供或获得某个对象的类型信息

反射使用

获取 Class 类对象

获取反射中的Class对象有三种方法。

第一种,使用 Class.forName 静态方法。

Class class1 = Class.forName("reflection.TestReflection");

第二种,使用类的.class 方法

Class class2 = TestReflection.class;

第三种,使用实例对象的 getClass() 方法。

TestReflection testReflection = new TestReflection();
Class class3 = testReflection.getClass();

反射创造对象

通过反射创建类对象主要有两种方式:

img

实例代码:

//方式一
Class class1 = Class.forName("reflection.Student");
Student student = (Student) class1.newInstance();
System.out.println(student);
​
//方式二
Constructor constructor = class1.getConstructor();
Student student1 = (Student) constructor.newInstance();
System.out.println(student1);

反射获取类的构造器

img

Class class1 = Class.forName("reflection.Student");
Constructor[] constructors = class1.getDeclaredConstructors();
for (int i = 0; i < constructors.length; i++) {
    System.out.println(constructors[i]);
 }
​

反射获取类的成员变量

// student 一个私有属性age,一个公有属性email
public class Student {
​
    private Integer age;
​
    public String email;
}
​
public class TestReflection {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
        Class class1 = Class.forName("reflection.Student");
        Field email = class1.getField("email");
        System.out.println(email);
        Field age = class1.getField("age");
        System.out.println(age);
    }
}
​
复制代码

反射获取类的方法

img

demo

public class Student {
​
    private void testPrivateMethod() {
        
    }
    public void testPublicMethod() {
        
    }
}
​
public class TestReflection {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
        Class class1 = Class.forName("reflection.Student");
​
        Method[] methods = class1.getMethods();
        for (int i = 0; i < methods.length; i++) {
            System.out.println(methods[i]);
        }
    }
}

Java反射原理

反射调用链

img

反射获取实例

forName

forName()反射获取类信息,并没有将实现留给了java,而是交给了jvm去加载。

主要是先获取 ClassLoader, 然后调用 native 方法,获取信息,加载类则是回调 java.lang.ClassLoader.

    @CallerSensitive
    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        // 先通过反射,获取调用进来的类信息,从而获取当前的 classLoader
        Class<?> caller = Reflection.getCallerClass();
        // 调用native方法进行获取class信息
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }
    
ClassLoader
newInstance

主要做了三件事:

  • 权限检测,如果不通过直接抛出异常;
  • 查找无参构造器,并将其缓存起来;
  • 调用具体方法的无参构造方法,生成实例并返回;
getConstuctor

为获取匹配的构造方器;分三步:

  • 先获取所有的constructors, 然后通过进行参数类型比较;
  • 找到匹配后,通过 ReflectionFactory copy一份constructor返回;
  • 否则抛出 NoSuchMethodException;
​
​
 @CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }
    // sun.reflect.DelegatingConstructorAccessorImpl
    public Object newInstance(Object[] args)
      throws InstantiationException,
             IllegalArgumentException,
             InvocationTargetException
    {
        return delegate.newInstance(args);
    }
    // sun.reflect.NativeConstructorAccessorImpl
    public Object newInstance(Object[] args)
        throws InstantiationException,
               IllegalArgumentException,
               InvocationTargetException
    {
        // We can't inflate a constructor belonging to a vm-anonymous class
        // because that kind of class can't be referred to by name, hence can't
        // be found from the generated bytecode.
        if (++numInvocations > ReflectionFactory.inflationThreshold()
                && !ReflectUtil.isVMAnonymousClass(c.getDeclaringClass())) {
            ConstructorAccessorImpl acc = (ConstructorAccessorImpl)
                new MethodAccessorGenerator().
                    generateConstructor(c.getDeclaringClass(),
                                        c.getParameterTypes(),
                                        c.getExceptionTypes(),
                                        c.getModifiers());
            parent.setDelegate(acc);
        }
​
        // 调用native方法,进行调用 constructor
        return newInstance0(c, args);
    }

反射获取方法

inflation 机制

Java 方法反射调用底层的原理,主要有两种方式

  • native 方法
  • 动态生成类的方式

native 调用的方式比 Java 类直接调用的方式慢 20 倍,但是第一次生成动态类又比较耗时,因此 JVM 才有了一个优化策略,在某阈值之前使用 native 调用,在此阈值之后使用动态生成类的方式。这样既可以保证在反射方法少数调用的情况下,不用生成新的类,又可以保证调用次数很多的情况下使用性能更优的动态类的方式

JVM 设置了 15 这个 sun.reflect.inflationThreshold 阈值,反射方法调用超过 15 次时(从 0 开始),采用 ASM 生成新的类,保证后面的调用比 native 要快。如果小于 15 次的情况下,还不如生成直接 native 来的简单直接,还不造成额外类的生成、校验、加载。这种方式被称为 「inflation 机制」。

总结

  1. 反射类及反射方法的获取,都是通过从列表中搜寻查找匹配的方法,所以查找性能会随类的大小方法多少而变化;
  2. 每个类都会有一个与之对应的Class实例,从而每个类都可以获取method反射方法,并作用到其他实例身上;
  3. 反射也是考虑了线程安全的,放心使用;
  4. 反射使用软引用relectionData缓存class信息,避免每次重新从jvm获取带来的开销;
  5. 反射调用多次生成新代理Accessor, 而通过字节码生存的则考虑了卸载功能,所以会使用独立的类加载器;
  6. 当找到需要的方法,都会copy一份出来,而不是使用原来的实例,从而保证数据隔离;
  7. 调度反射方法,最终是由jvm执行invoke0()执行

反射应用和问题

反射应用

反射是Java框架的灵魂技术,很多框架都使用了反射技术,如spring,Mybatis,Hibernate等。

JDBC 的数据库的连接

在JDBC连接数据库中,一般包括加载驱动,获得数据库连接等步骤。而加载驱动,就是引入相关Jar包后,通过Class.forName() 即反射技术,加载数据库的驱动程序。

Spring 框架的使用

Spring 通过 XML 配置模式装载 Bean,也是反射的一个典型例子。

装载过程:

  • 将程序内XML 配置文件加载入内存中
  • Java类解析xml里面的内容,得到相关字节码信息
  • 使用反射机制,得到Class实例
  • 动态配置实例的属性,使用

这样做当然是有好处的:

不用每次都去new实例了,并且可以修改配置文件,比较灵活。

反射存在的问题

性能问题

java反射的性能并不好,原因主要是编译器没法对反射相关的代码做优化。 相关参考java-reflection-why-is-it-so-slow

安全问题

我们知道单例模式的设计过程中,会强调将构造器设计为私有,因为这样可以防止从外部构造对象。但是反射可以获取类中的域、方法、构造器,修改访问权限。所以这样并不一定是安全的。

看个例子吧,通过反射使用私有构造器实例化。

public class Student {
    private String name;
    private Student(String name) {
        System.out.println("我是私有构造器,我被实例化了");
        this.name = name;
    }
    public void doHomework(String subject) {
        System.out.println("我的名字是" + name);
        System.out.println("我在做"+subject+"作业");
    }
}
public class TestReflection {
    public static void main(String[] args) throws Exception {
        Class clazz = Class.forName("reflection.Student");
        // 获取私有构造方法对象
        Constructor constructor = clazz.getDeclaredConstructor(String.class);
        // true指示反射的对象在使用时应该取消Java语言访问检查。
        constructor.setAccessible(true);
        Student student = (Student) constructor.newInstance("jay@huaxiao");
        student.doHomework("数学");
    }
}
复制代码

反射不管是不是私有,都可以调用。 所以,使用反射通常需要程序的运行没有安全限制。如果一个程序对安全性有强制要求,最好不要使用反射啦。

ASM

ASM API

ASM API基于访问者模式,为我们提供了ClassVisitor,MethodVisitor,FieldVisitor API接口,每当ASM扫描到类字段是会回调visitField方法,扫描到类方法是会回调MethodVisitor,下面我们看一下API接口

ClassVisitor方法解析

public abstract class ClassVisitor {
        ......
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces);
    //访问类字段时回调
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value);
    //访问类方法是回调
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions);
    public void visitEnd();
}

MethodVisitor方法解析

public abstract class MethodVisitor {
        ......
    public void visitParameter(String name, int access);
    //访问本地变量类型指令 操作码可以是LOAD,STORE,RET中一种;
    public void visitIntInsn(int opcode, int operand);
    //域操作指令,用来加载或者存储对象的Field
    public void visitFieldInsn(int opcode, String owner, String name, String descriptor);
    //访问方法操作指令
    public void visitMethodInsn(int opcode, String owner, String name, String descriptor);
    public void visitEnd();
}

ASM 使用Demo

java源码

 public int add(int a,int b) {
        return a+b+num1;
 }

class字节码

 public int add(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: aload_0
         4: getfield      #2                  // Field num1:I
         7: iadd
         8: ireturn
      LineNumberTable:
        line 14: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/wuba/asmdemo/Test;
            0       9     1     a   I
            0       9     2     b   I

ASM对应的API

            mv = cw.visitMethod(ACC_PUBLIC, "add", "(II)I", null, null);
            mv.visitCode();
            mv.visitVarInsn(ILOAD, 1);
            mv.visitVarInsn(ILOAD, 2);
            mv.visitInsn(IADD);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitFieldInsn(GETFIELD, "com/wuba/asmdemo/Test", "num1", "I");
            mv.visitInsn(IADD);
            mv.visitInsn(IRETURN);
            Label l1 = new Label();
            mv.visitLabel(l1);
            mv.visitLocalVariable("this", "Lcom/wuba/asmdemo/Test;", null, l0, l1, 0);
            mv.visitLocalVariable("a", "I", null, l0, l1, 1);
            mv.visitLocalVariable("b", "I", null, l0, l1, 2);
            mv.visitMaxs(2, 3);
            mv.visitEnd();

\