反射、注解和动态代理


原文地址

一、Java 反射机制及基本用法

反射是指计算机程序在运行时访问、检测和修改它本身状态或行为的一种能力,是一种元编程语言特性,有很多语言都提供了对反射机制的支持,它使程序能够编写程序。Java 的反射机制使得 Java 能够动态的获取类的信息和调用对象的方法。反射有时也被称为内省,这两个词汇都隐喻了“让类型自我审视并提供自身的描述信息”。

在 Java 中,Class(类类型)是反射编程的起点,代表运行时类型信息(RTTI,Run-Time Type Identification)。java.lang.reflect 包含了 Java 支持反射的主要组件,Field提供了有关类和接口的属性信息以及对它的动态访问权限;Constructor提供关于类的单个构造方法的信息以及它的访问权限;Method提供类或接口中某个方法的信息。它们的关系如下图所示。

Constructor 和 Method 与 Field 的区别在于前者继承自抽象类 Executable,是可以在运行时动态调用的,而 Field 仅仅具备可访问的特性,且默认为不可访问。下面了解下它们的基本用法:

获取 Class 对象的三种方式

静态方法 Class.forName 适合于已知类的全路径名,典型应用如加载 JDBC 驱动。对同一个类,不同方式获得的 Class 对象是相同的。

// 1. 采用静态方法Class.forName(全限类名)获取类的Class对象。最安全/性能最好/最常用
Class clazz0 = Class.forName("com.yhthu.java.ClassTest");
System.out.println("clazz0:" + clazz0);

// 2. 采用类字面常量(类的名字.class)
Class clazz1 = ClassTest.class;
System.out.println("clazz1:" + clazz1);

// 3. 采用对象的getClass方法获取它的Class对象
ClassTest classTest = new ClassTest();
Class clazz2 = classTest.getClass();
System.out.println("clazz2:" + clazz2);

// 4. 判断Class对象是否相同
System.out.println("Class对象是否相同:" + ((clazz0.equals(clazz1)) && (clazz1.equals(clazz2))));

注意:三种方式获取的 Class 对象相同的前提是使用了相同的类加载器,比如上述代码中默认采用应用程序类加载器(sun.misc.Launcher$AppClassLoader)。不同类加载器加载的同一个类,也会获取不同的 Class 对象:

// 自定义类加载器
ClassLoader myLoader = new ClassLoader() {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
            InputStream is = getClass().getResourceAsStream(fileName);
            if (is == null) {
                return super.loadClass(name);
            }
            byte[] b = new byte[is.available()];
            is.read(b);
            return defineClass(name, b, 0, b.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(name);
        }
    }
};
// 采用自定义类加载器加载
Class clazz3 = Class.forName("com.yhthu.java.ClassTest", true, myLoader);
// clazz0与clazz3并不相同
System.out.println("Class对象是否相同:" + clazz0.equals(clazz3));

查看类信息

通过 Class 的 getDeclaredXxxx 和 getXxx 方法获取构造器、方法和域对象。

两者的区别在于:

  • 前者返回的是当前 Class 对象申明的构造器、方法和域,包含修饰符为 private 的;
  • 后者只返回修饰符为 public 的构造器、方法和域,但包含从基类中继承的。
// 返回申明为public的方法,包含从基类中继承的
for (Method method: String.class.getMethods()) {
    System.out.println(method.getName());
}

// 返回当前类申明的所有方法,包含private的
for (Method method: String.class.getDeclaredMethods()) {
    System.out.println(method.getName());
}

Fields、Constructors与methods类似,省略。

利用反射动态创建对象实例

  • 通过 Class 的 newInstance 方法和 Constructor 的 newInstance 方法方法均可新建类型为 Class 的对象
  • 通过 Method 的 invoke 方法可以在运行时动态调用该方法;
  • 通过 Field 的 set 方法可以在运行时动态改变域的值,但需要首先设置其为可访问(setAccessible)。

二、注解

注解(Annotation)是 Java5 引入的一种代码辅助工具,它的核心作用是对类、方法、变量、参数和包进行标注,通过反射来访问这些标注信息,以此在运行时改变所注解对象的行为。Java 中的注解由内置注解和元注解组成。内置注解主要包括:

  • @Override - 检查该方法是否是重载方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
  • @Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
  • @SuppressWarnings - 指示编译器去忽略注解中声明的警告。
  • @SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
  • @FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口。

这里,我们重点关注元注解,元注解位于 java.lang.annotation 包中,主要用于自定义注解。元注解包括:

  • @Retention - 标识这个注解怎么保存,是只在代码中,还是编入 class 文件中,或者是在运行时可以通过反射访问,枚举类型分为别 SOURCE、CLASS 和 RUNTIME;
  • @Documented - 标记这些注解是否包含在用户文档中。
  • @Target - 标记这个注解应该是哪种 Java 成员,枚举类型包括 TYPE、FIELD、METHOD、CONSTRUCTOR 等;
  • @Inherited - 标记这个注解可以继承超类注解,即子类 Class 对象可使用 getAnnotations() 方法获取父类被 @Inherited 修饰的注解,这个注解只能用来申明类。
  • @Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。

自定义元注解需重点关注两点:1)注解的数据类型;2)反射获取注解的方法。首先,注解中的方法并不支持所有的数据类型,仅支持八种基本数据类型、String、Class、enum、Annotation 和它们的数组。比如以下代码会产生编译时错误:

@Documented
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationTest {
    // 1. 注解数据类型不能是Object;2. 默认值不能为null
    Object value() default null;
    // 支持的定义方式
    String value() default "";
}

其次,上节中提到的反射相关类(Class、Constructor、Method 和 Field)和 Package 均实现了 AnnotatedElement 接口,该接口定义了访问反射信息的方法,主要如下:

// 获取指定注解类型
getAnnotation(Class<T>):T;
// 获取所有注解,包括从父类继承的
getAnnotations():Annotation[];
// 获取指定注解类型,不包括从父类继承的
getDeclaredAnnotation(Class<T>):T
// 获取所有注解,不包括从父类继承的
getDeclaredAnnotations():Annotation[];
// 判断是否存在指定注解
isAnnotationPresent(Class<? extends Annotation>:boolean

当使用上例中的 AnnotationTest 标注某个类后,便可在运行时通过该类的反射方法访问注解信息了。

@AnnotationTest("yhthu")
public class AnnotationReflection {

    public static void main(String[] args) {
        AnnotationReflection ar = new AnnotationReflection();
        Class clazz = ar.getClass();
        // 判断是否存在指定注解
        if (clazz.isAnnotationPresent(AnnotationTest.class)) {
            // 获取指定注解类型
            Annotation annotation = clazz.getAnnotation(AnnotationTest.class);
            // 获取该注解的值
            System.out.println(((AnnotationTest) annotation).value());
        }
    }
}

当自定义注解只有一个方法 value() 时,使用注解可只写值,例如:@AnnotationTest(“yhthu”)

三、动态代理

参考上一篇:动态代理

代理是一种结构型设计模式,当无法或不想直接访问某个对象,或者访问某个对象比较复杂的时候,可以通过一个代理对象来间接访问,代理对象向客户端提供和真实对象同样的接口功能。经典设计模式中,代理模式有四种角色:

  • Subject 抽象主题类——申明代理对象和真实对象共同的接口方法;
  • RealSubject 真实主题类——实现了 Subject 接口,真实执行业务逻辑的地方;
  • ProxySubject 代理类——实现了 Subject 接口,持有对 RealSubject 的引用,在实现的接口方法中调用 RealSubject 中相应的方法执行;
  • Cliect 客户端类——使用代理对象的类。

在实现上,代理模式分为静态代理和动态代理,静态代理的代理类二进制文件是在编译时生成的,而动态代理的代理类二进制文件是在运行时生成并加载到虚拟机环境的。JDK 提供了对动态代理接口的支持,开源的动态代理库(Cglib、Javassist 和 Byte Buddy)提供了对接口和类的代理支持,本节将简单比较 JDK 和 Cglib 实现动态代理的异同,后续章节会对 Java 字节码编程做详细分析。

3.1 JDK 动态代理接口

JDK 实现动态代理是通过 Proxy 类的 newProxyInstance 方法实现的,该方法的三个入参分别表示:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
  • ClassLoader loader,定义代理生成的类的加载器,可以自定义类加载器,也可以复用当前 Class 的类加载器;
  • Class<?>[] interfaces,定义代理对象需要实现的接口;
  • InvocationHandler h,定义代理对象调用方法的处理,其 invoke 方法中的 Object proxy 表示生成的代理对象,Method 表示代理方法, Object[] 表示方法的参数。

通常的使用方法如下:

private Object getProxy() {
    return Proxy.newProxyInstance(JDKProxyTest.class.getClassLoader(), new Class<?>[]{Subject.class},
            new MyInvocationHandler(new RealSubject()));
}

private static class MyInvocationHandler implements InvocationHandler {
    private Object realSubject;

    public MyInvocationHandler(Object realSubject) {
        this.realSubject = realSubject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Some thing before method invoke");
        Object result = method.invoke(realSubject, args);
        System.out.println("Some thing after method invoke");
        return result;
    }
}

类加载器采用当前类的加载器,默认为应用程序类加载器(sun.misc.Launcher$AppClassLoader);接口数组以 Subject.class 为例,调用方法处理类 MyInvocationHandler 实现 InvocationHandler 接口,并在构造器中传入 Subject 的真正的业务功能服务类 RealSubject,在执行 invoke 方法时,可以在实际方法调用前后织入自定义的处理逻辑,这也就是 AOP(面向切面编程)的原理。
关于 JDK 动态代理,有两个问题需要清楚:

  • Proxy.newProxyInstance 的代理类是如何生成的?Proxy.newProxyInstance 生成代理类的核心分成两步:
// 1. 获取代理类的Class对象
Class<?> cl = getProxyClass0(loader, intfs);
// 2. 利用Class获取Constructor,通过反射生成对象
cons.newInstance(new Object[]{h});

与反射获取 Class 对象时搜索 classpath 路径的. class 文件不同的是,这里的 Class 对象完全是 “无中生有” 的。getProxyClass0 根据类加载器和接口集合返回了 Class 对象,这里采用了缓存的处理。

// 缓存(key, sub-key) -> value,其中key为类加载器,sub-key为代理的接口,value为Class对象
private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
    proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
// 如果实现了代理接口的类已存在就返回缓存对象,否则就通过ProxyClassFactory生成
private static Class<?> getProxyClass0(ClassLoader loader, Class<?>... interfaces) {
    if (interfaces.length > 65535) {
        throw new IllegalArgumentException("interface limit exceeded");
    }
    return proxyClassCache.get(loader, interfaces);
}

如果实现了代理接口的类已存在就返回缓存对象,否则就通过 ProxyClassFactory 生成。ProxyClassFactory 又是通过下面的代码生成 Class 对象的。

// 生成代理类字节码文件
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);
try {
    // defineClass0为native方法,生成Class对象
    return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
    throw new IllegalArgumentException(e.toString());
}

generateProxyClass 方法是用来生成字节码文件的,根据生成的字节码文件,再在 native 层生成 Class 对象。

  • InvocationHandler 的 invoke 方法是怎样调用的?
    回答这个问题得先看下上面生成的 Class 对象究竟是什么样的,将 ProxyGenerator 生成的字节码保存成文件,然后反编译打开(IDEA 直接打开),可见生成的 Proxy.class 主要包含 equals、toString、hashCode 和代理接口的 request 方法实现。
public final class $Proxy extends Proxy implements Subject {
    // m1 = Object的equals方法
    private static Method m1;
    // m2 = Object的toString方法
    private static Method m2;
    // Subject的request方法
    private static Method m3;
    // Object的hashCode方法
    private static Method m0;

    // 省略m1/m2/m0,此处只列出request方法实现
    public final void request() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }   
}

由于生成的代理类继承自 Proxy,super.h 即是 Prxoy 的 InvocationHandler,即代理类的 request 方法直接调用了 InvocationHandler 的实现,这就回答了 InvocationHandler 的 invoke 方法是如何被调用的了。

3.2 Cglib 动态代理接口和类

Cglib 的动态代理是通过 Enhancer 类实现的,其 create 方法生成动态代理的对象,有五个重载方法:

create():Object
create(Class, Callback):Object
create(Class, Class[], Callback):Object
create(Class, Class[], CallbackFilter, Callback):Object
create(Class[], Object):Object

常用的是第二个和第三个方法,分别用于动态代理类和动态代理接口,其使用方法如下:

private Object getProxy() {
    // 1. 动态代理类
    return Enhancer.create(RealSubject.class, new MyMethodInterceptor());
    // 2. 动态代理接口
    return Enhancer.create(Object.class, new Class<?>[]{Subject.class}, new MyMethodInterceptor());
}

private static class MyMethodInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("Some thing before method invoke");
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("Some thing after method invoke");
        return result;
    }
}

从上小节可知,JDK 只能代理接口,代理生成的类实现了接口的方法;而 Cglib 是通过继承被代理的类、重写其方法来实现的,如:create 方法入参的第一个参数就是被代理类的类型。当然,Cglib 也能代理接口,比如 getProxy() 方法中的第二种方式。


  TOC