反射是指计算机程序在运行时访问, 检测和修改它本身状态或行为的一种能力, 是一种元编程语言特性, 有很多语言都提供了对反射机制的支持, 它使程序能够编写程序. 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()方法中的第二种方式.
四, 案例: 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 分层网络请求框架》 https://www.jianshu.com/p/37fca658cf32 .
那么, 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(// ......);
上述代码实现均为伪代码, 仅说明解决方案思路.
在该案例中, 综合使用了自定义注解, 反射以及动态代理, 是对上述理论知识的一个具体应用.
来源: https://www.cnblogs.com/younghao/p/9751566.html