• 反射、注解和动态代理


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

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

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

    Java反射机制主要组件

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

    Java反射类及核心方法

    • 获取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()方法中的第二种方式。

    四、案例:Android端dubbo:reference化的网络访问

    Dubbo是一款高性能的Java RPC框架,是服务治理的重量级中间件。Dubbo采用dubbo:service描述服务提供者,dubbo:reference描述服务消费者,其共同必填属性为interface,即Java接口。Dubbo正是采用接口来作为服务提供者和消费者之间的“共同语言”的。
    在移动网络中,Android作为服务消费者,一般通过HTTP网关调用后端服务。在国内的大型互联网公司中,Java后端大多采用了Dubbo及其变种作为服务治理、服务水平扩展的解决方案。因此,HTTP网关通常需要Android的网络请求中提供调用的服务名称、服务方法、服务版本、服务分组等信息,然后通过这些信息反射调用Java后端提供的RPC服务,实现从HTTP协议到RPC协议的转换。

    关于Android访问网关请求,其分层结构可参考《基于Retrofit+RxJava的Android分层网络请求框架》

    那么,Android端能否以dubbo:reference化的方式申明需要访问的网络服务呢?如何这样,将极大提高Android开发人员和Java后端开发之间的沟通效率,以及Android端的代码效率。
    首先,自定义服务的消费者注解Reference,通过该注解标记某个服务。

    @Inherited
    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Reference {
        // 服务接口名
        String service() default "";
        // 服务版本
        String version() default "";
        // 服务分组
        String group() default "";
        // 省略字段
    }
    

    其次,通过接口定义某个服务消费(如果可以直接引入后端接口,此步骤可省略),在注解中指明该服务对应的后端服务接口名、服务版本、服务分组等信息;

    @Reference(service = "com.yhthu.java.ClassTestService",  group = "yhthu",  version = "v_test_0.1")
    public interface ClassTestService {
        // 实例方法
        Response echo(String pin);
    }
    

    这样就完成了服务的申明,接下来的问题是如何实现服务的调用呢?上述申明的服务接口如何定义实现呢?这里就涉及依赖注入和动态代理。我们先定义一个标记注解@Service,标识需要被注入实现的服务申明。

    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Service {
    }
    // 在需要使用服务的地方(比如Activity中)申明需要调用的服务
    @Service
    private ClassTestService classTestService;
    

    在调用classTestService的方法之前,需要注入该接口服务的实现,因此,该操作可以在调用组件初始化的时候进行。

    // 接口与对应实现的缓存
    private Map<Class<?>, Object> serviceContainer = new HashMap<>();
    // 依赖注入
    public void inject(Object obj) {
        // 1. 扫描该类中所有添加@Service注解的域
        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(Service.class)) {
                Class<?> clazz = field.getType();
                if (clazz.getAnnotation(Reference.class) == null) {
                    Log.e("ClassTestService", "接口地址未配置");
                    continue;
                }
                // 2. 从缓存中取出或生成接口类的实现(动态代理)
                Object impl = serviceContainer.get(clazz);
                if (impl == null) {
                    impl = create(clazz);
                    serviceContainer.put(clazz, impl);
                }
                // 3. 设置服务接口实现
                try {
                    field.setAccessible(true);
                    field.set(obj, impl);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    inject方法的关键有三步:

    • 扫描该类中所有添加@Service注解的字段,即可得到上述代码示例中的ClassTestService字段;
    • 从缓存中取出或生成接口类的实现。由于通过接口定义了服务,并且实现不同服务的实现方式基本一致(即将服务信息发送HTTP网关),在生成实现上可选择JDK的动态代理。
    • 设置服务接口实现,完成为接口注入实现。
    private <T> T create(final Class<T> service) {
        return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[]{service}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                // 1. 获取服务信息
                Annotation reference = service.getAnnotation(Reference.class);
                String serviceName = ((Reference) reference).service();
                String versionName = ((Reference) reference).version();
                String groupName = ((Reference) reference).group();
                // 2. 获取方法名
                String methodName = method.getName();
                // 3. 根据服务信息发起请求,返回调用结果
                return Request.request(serviceName, versionName, groupName, methodName, param);
            }
        });
    }
    

    在HTTP网关得到服务名称、服务方法、服务版本、服务分组等信息之后,即可实现对后端服务的反射调用。总的来讲,即可实现Android端dubbo:reference化的网络访问。

    // 调用ClassTestService服务的方法
    classTestService.echo("yhthu").callback(// ……);
    

    上述代码实现均为伪代码,仅说明解决方案思路。

    在该案例中,综合使用了自定义注解、反射以及动态代理,是对上述理论知识的一个具体应用。

  • 相关阅读:
    HDU 2089 不要62
    NOIP 2012 疫情控制
    提高工作效率的shell工具
    log loss与ctr预估
    推荐系统
    浮点数的间隙
    graphviz初学入门指南
    sparse_hash_map、dense_hash_map和sparsetable的实现细节
    每天离不开的工具vim
    安利开发利器 -- tmux
  • 原文地址:https://www.cnblogs.com/younghao/p/9751566.html
Copyright © 2020-2023  润新知