前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >反射、注解和动态代理

反射、注解和动态代理

作者头像
良月柒
发布2019-03-20 15:50:43
5170
发布2019-03-20 15:50:43
举报

技术文章第一时间送达!

反射是指计算机程序在运行时访问、检测和修改它本身状态或行为的一种能力,是一种元编程语言特性,有很多语言都提供了对反射机制的支持,它使程序能够编写程序。Java的反射机制使得Java能够动态的获取类的信息和调用对象的方法

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

在Java中,Class(类类型)是反射编程的起点,代表运行时类型信息(RTTI,Run-Time Type Identification)。java.lang.reflect包含了Java支持反射的主要组件,如Constructor、Method和Field等,分别表示类的构造器、方法和域,它们的关系如下图所示。

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对象
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());
}

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

二、 注解

注解(Annontation)是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()方法中的第二种方式。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-10-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员的成长之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、Java反射机制及基本用法
    • 获取Class对象有三种方式,Class.forName适合于已知类的全路径名,典型应用如加载JDBC驱动。对同一个类,不同方式获得的Class对象是相同的。
    • 二、 注解
      • 自定义元注解需重点关注两点:
      • 三、动态代理
        • 3.1 JDK动态代理接口
          • 通常的使用方法如下:
          • 关于JDK动态代理,有两个问题需要清楚:
        • InvocationHandler的invoke方法是怎样调用的?
          • 3.2 Cglib动态代理接口和类
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档