Java编程自学之路:反射


Java反射

反射简介

反射(Reflection)是Java程序开发语言的特征之一,它允许运行中的Java程序获取自身的信息,并且可以操作类或对象的内部属性;

通过反射机制,可以在运行时访问Java对象的属性、方法等;

应用场景

反射的应用场景主要有:

  1. 开发通用框架:反射最重要的用于就是开发各种通用框架,很多商用框架都是配置化的,为了保证框架的通用性,他们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这就需要通过反射–运行时动态加载来实现;
  2. 动态代理:在切面编程(AOP)中,需要拦截特定的方法,通常,会选择动态代理方式来实现;
  3. 注解:注解本身仅起到标记作用,实际需要利用反射机制,根据注解标记去调用对应的注解解释器,执行行为;
  4. 可扩展性:应用程序可以通过使用完全限定名创建可扩展对象实例来使用外部用户定义类,比如数据库驱动类;

反射缺点

任何事务都是有双面性的,反射实现了一系列功能的同事,也引入了一些缺点:

  1. 性能开销:由于反射涉及动态解析,因此无法执行某些Java虚拟机优化;因此,反射操作的性能相对较差,在性能敏感性应用中因避免频繁调用;
  2. 破坏封装性:反射调用方法时可以忽略权限检查,因此可能破坏封装性而导致安全问题;
  3. 内部曝光:由于反射允许代码执行非反射代码中的非法操作,如访问私有字段或方法;这可能导致代码功能失常并可能破坏代码可移植性;反射代码打破了抽象,可能会随着平台的升级而改变行为;

反射机制

类加载流程

类加载完成过程如下:

  1. 编译阶段:Java编译器对.java文件编译完成,在磁盘中生成.class文件;.class文件是二进制文件,内容是只有JVM能够识别的机器码;
  2. 加载阶段:JVM的类加载器读取字节码文件,取出二进制数据,加载到内存中,解析.class文件内的信息;类加载器会根据类的全限定名来获取此类的二进制字节流;将字节流所代表的的静态存储结构转化为方法区的运行时数据结构,在内存中生成代表这个类的java.lang.Class对象;
  3. 执行阶段:加载结束后,JVM开始进行链接阶段(包括验证、准备、初始化);经过这一系列操作,类的变量会被初始化;
  4. 底层调用:将JVM的调用提交至操作系统进行计算;

Class对象

想使用反射,首先需要获得待操作的类所对应的Class对象;在Java中,无论生成某个类的多少个对象,这些对象都会对应于同一个Class对象。这个Class对象是由JVM生成的,通过它能够获悉整个类的结构;所以,java.lang.Class可以是为所有反射API的入口;

反射的本质就是:在运行时,把Java类中的各种成分映射成一个个的Java对象;

JVM自动创建类的Class对象后,存储在JVM的方法区中;且一个类有且只有一个Class对象;

方法反射调用

方法的反射调用,也就是Method.invoke方法;

Method.invoke方法源码:

public final class Method extends Executable {
  public Object invoke(Object obj, Object ... args) throws ... {
    MethodAccessor ma = methodAccessor;
    if(ma == null) {
      ma = acquireMethodAccessor();
    }
    return ma.invoke(obj,args);
  }
}

说明:

  • NativeMethodAccessorImpl:本地方法来实现反射调用;
  • DelegationMethodAccessorImpl:委派模式来实现反射调用;

Method.invoke方法调用实际上是委派给MethodAccessor接口来处理;每个Method实例的第一次反射调用都会生成一个委派实现,它所委派的具体实现便是一个本地实现(NativeMethodAccessorImpl);

Java的反射机制还设立了另一种动态生成字节码的实现,直接使用invoke指令来调用目标方法;通过委派实现,能够在本地实现与动态实现中切换;动态实现不需要经过Java到C++再到Java,效率比本地实现高20倍,但生成字节码很耗时,仅调用一次的话,反而是本地实现要快3到4倍;(在16次调用时,动态调用性能追上本地调用)

反射调用开销

方法的反射调用会带来不少性能开销,主要原因为:

  • 变长参数方法导致的Object数组;
  • 基本类型的自动装箱、拆箱;
  • 方法内联;

Class.forName()会调用本地方法,Class.getMethod()则会遍历该类的共有方法,如果没有匹配到,它还将遍历父类的公有方法,可想而知,这两个操作都非常费时;

反射使用

java.lang.reflect包

Java中的java.lang.reflect包提供了反射功能,包中的类都没有public构造方法;

java.lang.reflect包的核心接口和类如下:

  • Member接口:反映关于单个成员(字段或方法)或构造函数的标识信息;
  • Field类:提供一个类的域信息以及访问类的域的接口;
  • Method类:提供一个类的方法的信息以及访问类的方法的接口;
  • Constructor类:提供一个类的构造函数的信息以及访问类的狗仔函数的接口;
  • Array类:提供动态生成和访问JAVA数组的方法;
  • Modifier类:提供了static方法和常量,对类和成员访问修饰符进行解码;
  • Proxy类:提供动态地生成代理类和类实例的静态方法;

获取Class对象

获取Class对象的三种方法:

  1. Class.forName()静态方法:使用类的完全限定名来反射对象的类;常用应用场景为:在JDBC开发中常用此方法加载数据库驱动;
  2. 类名 + .class
  3. ObjectgetClass方法:Object类中有getClass方法,因为所有类都继承Object类;从而调用Object类来获取Class对象;

代码示例:

public class ReflectClassDemo {
  
  public static void main(String[] args) throws ClassNotFoundException {
    //m1  jdbc驱动
    Class c1 = Class.forName("org.mysql.jdbc.Driver");
    // double 数组
    Class c2 = Class.forName("[D"); 
    System.out.println(c2.getCanonicalName());   // double[]
    
    //m2
    Class c3 = java.io.PrintStream.class;
    Class c4 = int[] [] [].class;
    System.out.println(c4.getCanonicalName())   // int[][][]
    
    //m3
    Set<String> set = new HashSet<>();
    Class c5 = set.getClass();
    System.out.println(c5.getCanonicalName());  // java.util.HashSet
  }
}

判断类实例

判断是否为某个类的实例方式:

  • instanceof关键字;
  • Class对象isinstance方法(Native方法);

示例代码:

public class InstanceOfDemo {
  public static void main(String[] args) {
    
    ArrayList arr = new ArrayList();
    
    if (arr instanceof List) {
      System.out.println("ArrayList is  List");
    }
    
    if (List.class.isInstance(arr)) {
      System.out.println("ArrayList is  List");
    }
  }
}

创建实例

通过反射来创建实例对象有以下方式:

  • Class对象的newInstance方法;
  • Constructor对象的newInstance方法;

示例代码:

public class ReflectNewInstanceDemo {
  public static void main(String[] args) throws Exception {
    
    //m1
    Class<?> c1 = StringBuilder.class;
    StringBuilder sb = (StringBuilder) c1.newInstance();
    sb.append("hello");
    
    //m2
    //获取String对应class对象
    Class<?> c2 = String.class;
    //获取String类带一个String参数的构造器
    Constructor con = c2.getConstructor(String.class);
    //使用构造器构造对象
    String str = (String) con.newInstance("world");
  }
}

创建数组实例

数组在Java中是比较特殊的一种类型,它可以赋值给一个对象引用;Java中,可以通过Array.newInstance来创建数组实例;

示例代码:

public class ReflectArrayDemo {
  public static void main(String[] args) throws ClassNotFoundException {
    Class<?> cls = Class.forName("java.lang.String");
    Object arr = Array.newInstance(cls,20);
    Array.set(arr,0,"Scala");
    Array.set(arr,1,"java");
    System.out.println(Array.get(arr,1));
  }
}

d

Class对象提供以下方法获取对象的成员:

  • getFiled:根据名称获取共有的类成员;
  • getDeclaredField:根据名称获取以声明的类成员,但不能获取起父类成员;
  • getFields:获取所有共有的类成员;
  • getDeclaredFields:获取所有已声明的类成员;
public class ReflectFieldDemo {
  class FieldDemo<T> {
    public boolean b = false;
    public String name = "Alice";
    public List<Integer> list;
    public T val;
  }
  
  public static void main(String[] args) throws NoSuchFieldException {
    Field f1 = FieldDemo.class.getField("b");
    System.out.println("Type: %s%n" ,f1.getType());
    
    Field f2 = FieldDemo.class.getField("val");
    System.out.println("type: %s%n", f2.getType());
  }
}

Method

Class对象提供以下方法获取对象的方法:

  • getMethod:返回类或接口的特定方法。其中第一个参数为方法名称,后面的参数为方法参数对应的Class对象;
  • getDeclaredMethod:返回类或接口的特定声明方法。其中第一个参数为方法名称,后面的参数为方法参数对应Class对象;
  • getMethods:返回类或接口的所有公有方法,包括起父类的公有方法;
  • getDeclaredMethods:返回类或接口声明的所有方法,但不包括继承的方法;

获取一个Method对象后,可以用invoke方法来调用这个方法。

public class ReflectMethodDemo {
  public static void main(String[] args) throws Exception {
    Method[] m1 = System.class.getMethods();
    for (Method m: m1) {
      System.out.println(m);
      System.out.println(m.invoke(null));
    }
  }
}

Constructor

Class对象提供以下方法获取对象的构造方法:

  • getConstructor:返回类的特定公有构造方法,参数为方法参数对应的Class对象;
  • getDeclaredConstructor:返回类的特定构造方法,参数为方法参数对应的Class对象;
  • getConstructors:返回类的所有共有构造方法;
  • getDeclaredConstructors:返回类的所有构造方法;

获取一个Constructor对象后,可以使用newInstance方法来创建类实例;

public class ReflectConstructorDemo{
  public static void main(String[] args) throws Exception {
    Constructor constructor = String.class.getConstructor(String.class);
    String str = (String) constructor.newInstance("helloworld");
    System.out.println(str);
   }
}

绕开访问限制

反射可以通过setAccessible(true)来绕开Java的访问限制,直接访问私有成员、私有方法;

代理

静态代理

静态代理其实就是指设计模式中的代理模式;代理模式为其它对象提供一种代理以控制对这个对象的访问;

//定义抽象类
public class Subject {
  public abstract void Request();
}

//实现抽象类
class RealSubject extends Subject {
  @Override
  public Request() {
    System.out.println("real Subject");
  }
}

//定义代理类,用来保存一个引用使代理可以访问实体,并提供一个与Subject的接口相同的接口,这样代理就可以用来替代实体
class Proxy extends Subject {
  
  private RealSubject real;
  
  @Override 
  public void Request() {
    if (null ==  real) {
      real = new RealSubject();
    }
    real.Request();
  }
}
  • 优点:能够访问正常实体无法访问的资源,增强现有的接口业务功能;
  • 缺点:真实实体与代理的功能本质上是相同的,代理只起到了中介作用,但代理的存在会导致系统结构比较臃肿,增加维护难度;

动态代理

为了解决静态代理的问题,所以有了动态代理的概念;

动态代理是一种方便运行是动态构建代理、动态处理代理方法调用的机制,很多场景都是利用类似机制实现的,比如包装RPC调用、面向切面编程等;

实现动态代理的方式很多,比如JDK自身提供的动态代理,主要就是利用了反射机制;高性能的字节码操作机制,类似ASM、cglib、javassist等;

Java动态代理基于经典代理模式,引入了一个InvocationHandlerInvocationHandler负责统一管理所有的方法调用;

动态代理步骤:

  1. 获取真实实体上所有接口列表;
  2. 确认要生成的代理类的类名,默认为:com.sun.proxy.$ProxyXXX
  3. 根据需要实现的接口信息,在代码中动态创建该Proxy类的字节码;
  4. 将对应的字节码转换为对应的class对象;
  5. 创建InvocationHandler实例handler,用来实现proxy所欲方法调用;
  6. Proxyclass对象以创建的handler对象为参数,实例化一个proxy对象;

JDK动态代理的实现是基于实现接口的方式,使得Proxy与真实实体具有相同的功能;

InvocationHandler接口

每一个动态代理类都必须要实现InvocationHandler接口,并且每个代理类的实例都关联到了一个handler。当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由InvocationHandler接口的invoke方法来进行调用;

接口定义:

public interface InvocationHandler {
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

说明:

  • proxy:代理的真实对象;
  • method:要调用真实对象的某个方法的Method对象;
  • args:调用真实对象的某个方法时接受的参数;

Proxy类

Proxy类的作用是用来动态创建一个代理对象的类,它提供了许多方法,但使用最多的就是newProxyInstance方法;

方法定义:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler handler) throws IllegalArgumentException

说明:

  • loader:一个ClassLoader对象,定义了生成代理对象进行加载的ClassLoader
  • interface:一个Class<?>对象的数组,表示的是将要提供给代理的对象提供的一组接口,代理对象宣称实现这组接口,代理对象即可调用这组接口的方法;
  • handler:一个InvocationHandler对象,表示的是当动态代理对象调用方法时,会关联到哪一个InvocationHandler对象上;

JDK动态代理

//定义接口
public  interface Subject  {
  void hello(String str);
  
  String bye();
}

//定义一个类并实现接口
public class RealSubject implements Subject {
  @Override
  public void hello(String str) {
    System.out.println("Hello " + str);
  }
  
  @Override
  String bye() {
    System.out.println("Goodbye");
    return "Over";
  }
}

//动态代理
public class InvocationHandlerDemo implements InvocationHandler {
  //要代理的对象
  private Object subject;
  
  //自定义构造方法
  public InvocationHandlerDemo(Object obj) {
    this.subject  = obj;
  }
  
  @Override
  public Object invoke(Object obj, Method method, Object[] args) throws Throwable {
    //代理真实实体前,添加一些自定义操作
    System.out.println("Before Method");
    
    Object obj = method.invoke(subject,args);
    
    //代理真实实体后,添加一些自定义操作
    System.out.println("After Method");
    
    return obj;
  } 
}

//调用测试
public class ClientDemo{
  public static void main(String[] args) {
    //要代理的实体
    Subject realSubject = new RealSubject();
    
    //绑定代理实体
    InvocationHandler handler = new InvocationHandlerDemo(realSubject);
    /*
    通过Proxy的newProxyInstance方法来创建代理对象,
    参数1:handler.getClass().getClassLoader(),使用handler类的ClassLoader对象来加载我们的代理对象;
    参数2:realSubject.getClass().getInterfaces(),这里为代理对象提供的接口是针对实体所实现的接口,表示我要dialing的是该真实实体,这样就可以调用这组接口中的方法了;
    参数3:handler,将代理对象关联到InvocationHandler对象上
    */
    Subject  subject = (Subject) Proxy.newProxyInstance(handler.getClass().getClassLoader(), realSubject.getClass().getInterfaces(), handler);
    
    System.out.println(subject.getClass().getName());
    subject.hello("World");
    String result = subject.bye();
  }
}

JDK动态代理特点:

优点:相对于静态代理模式,不需要硬编码接口,代码复用率高;

缺点:强制要求代理类实现InvocationHandler接口;

CGLIB动态代理

CGLIB提供了与JDK动态代理不同的方案;很多框架,例如Spring AOP中,就使用了CGLIB动态代理;

CGLIB底层,其实是借助了ASM这个强大的Java字节码框架去进行字节码增强操作;

CGLIB动态代理步骤:

  • 生成代理类的二进制字节码文件;
  • 加载二进制字节码,生成Class对象;
  • 通过反射获得实例构造,并创建代理类实例;

CGLIB动态代理特点:

  • 优点:使用字节码增强,比JDK动态代理方式性能更高;可以在运行时对类或者是接口进行增强操作,且委托类无需实现接口;
  • 缺点:不能对final类及final方法进行代理;

文章作者: Semon
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Semon !
评论
  目录