干货分享 | 理解 Java 反射的正确姿势

反射简介

反射是 Java 的高级特性之一,但是在实际的开发中,使用 Java 反射的案例却非常的少,但是反射确实在底层框架中被频繁的使用。
比如:JDBC 中的加载数据库驱动程序,Spring 框架中加载 bean 对象,以及态代理,这些都使用到反射,因为我们要想理解一些框架的底层原理,反射是我们必须要掌握的。
理解反射我们先从他的概念入手,那么什么是反射呢?
反射就是在运行状态能够动态的获取该类的属性和方法,并且能够任意的使用该类的属性和方法,这种动态获取类信息以及动态的调用对象的方法的功能就是反射。
实现上面操作的前提是能够获取到该类的字节码对象,也就是.class 文件,在反射中获取 class 文件的方式有三种:
类名.class 如:Person.class
对象.class 如:person.class
Class.forName(全类名) 获取 如:Class.forName(“ldc.org. demo.person”)
Class 对象

对于反射的执行过程的原理,我这里画了一张图,以供大家参考理解。

我们看过 JVM 的相关书籍都会详细的了解到,Java 文件首先要通过编译器编译,编译成 Class 文件,然后通过类加载器 (ClassLoader) 将 class 文件加载到 JVM 中。
在 JVM 中 Class 文件都与一个 Class 对象对应,在因为 Class 对象中包含着该类的类信息,只要获取到 Class 对象便可以操作该类对象的属性与方法。
在这里深入理解反射之前先来深入的理解 Class 对象,它包含了类的相关信息。
Java 中我们在运行时识别对象和类的信息,也叫做 RTTI,方式主要有来两种:
传统的 RTTI(Run-Time Type Information)
反射机制
那么什么是 RTTI 呢?RTTI 称为运行时类型识别,传统的 RTTI 是在编译时就已经知道所有类型;而反射机制则是在程序运行时才确定的类型信息。
想要运行时使用类型信息,就必须要获取 Class 对象的引用,获取 Class 对象的方式上面已经提及。
这里有点区别的就是使用 (.class) 方式获取 Class 对象,并不会初始化 Class 对象,而使用 (forName(“全类名”)) 的方式会自动初始化 Class 对象。
当一个.class 文件要被加载到 JVM 中的时候,会进行如下的准备工作,首先会检查这个类是否被加载,若是没有被加载就会根据全类名找到 class 文件,接着加载 Class 文件,并创建类的静态成员引用。
但是在程序中并非是一开始就完全加载该类的 class 文件,而是在程序用的地方再加载,即为懒加载模式。
当加载完 Class 文件后,接着就会验证 Class 文件中的字节码,并静态域分配存储空间。这个过程也叫做链接。
最后一步就是进行初始化,即为了使用类而提前做的准备工作如下图所示:

反射

反射对应到 Java 中的类库就是在 java.lang.reflect 下, 在该包下包含着 Field、Method 和 Constructor 类。
Field 是表示一个类的属性信息,Method 表示类的方法信息,Constructor 表示的是类的构造方法的信息。
在反射中常用的方法,我这里做了一个列举,当然更加详细的可以查官方的 API 文档进行学习。
方法名 作用
getConstructors() 获取公共构造器
getDeclaredConstructors() 获取所有构造器
newInstance() 获取该类对象
getName() 获取类名包含包路径
getSimpleName() 获取类名不包含包路径
getFields() 获取类公共类型的所有属性
getDeclaredFields() 获取类的所有属性
getField(String name) 获取类公共类型的指定属性
getDeclaredField(String name) 获取类全部类型的指定属性
getMethods() 获取类公共类型的方法
getDeclaredMethods() 获取类的所有方法
getMethod(String name, Class[] parameterTypes) 获得类的特定公共类型方法
getDeclaredClasses() 获取内部类
getDeclaringClass() 获取外部类
getPackage() 获取所在包
另外对于反射的使用这里附上一段小 demo,具体的实际应用,会在后面继续说到,并且也会附上代码的实现:
public class User{
private String name;
private Integer age;

public User() {
}

public User(String name, Integer age) {
    this.name = name;
    this.age = age;
}

 private void privateMethod(){
    System.err.println("privateMethod");
}

public void publicMethod(String param){
    System.err.println("publicMethod"+param);
}


@Override
public String toString() {
    return "User{" +
            "name='" + name + '\'' +
            ", age=" + age +
            '}';
}

}
在 User 的实体类中,有两个属性 age 和 name,并且除了有两个测试方法 privateMethod 和 publicMethod 用于测试私有方法和公共方法的获取。接着执行如下代码:
Class clazz=User.class;
// 获取有参构造
Constructor constructor = clazz.getConstructor(String.class, Integer.class);
// 获取该类对象并设置属性的值
Object obj = constructor.newInstance(“黎杜”, 18);

// 获得类全类名,既包含包路径
String fullClassName = clazz.getName();

// 获得类名
String className = clazz.getSimpleName();

// 获得类中公共类型(public)属性
Field[] fields = clazz.getFields();
String fieldName="";
for(Field field : fields){
// 获取属性名
fieldName=field.getName();
System.out.println(fieldName)
}

// 获得类中全部类型属性 (包括 private)
Field[] fieldsAll = clazz.getDeclaredFields();
fieldName="";
for(Field field : fieldsAll){
// 获取属性名
fieldName=field.getName();
System.out.println(fieldName)
}

// 获得指定公共属性值
Field age = clazz.getField(“age”);
Object value = age.get(obj);
System.err.println(“公共指定属性:”+value);

// 获得指定的私有属性值
Field name = clazz.getDeclaredField(“name”);
// 设置为 true 才能获取私有属性
name.setAccessible(true);
Object value2= name.get(obj);
System.err.println(“私有指定属性值:”+value2);

// 获取所有公共类型方法 这里包括 Object 类的一些方法
Method[] methods = clazz.getMethods();
String methodsName="";
for(Method method : methods){
methodsName=method.getName();
}

// 获取该类中的所有方法(包括 private)
Method[] methodsAll = clazz.getDeclaredMethods();
methodsName="";
for(Method method : methodsAll){
methodsName=method.getName();
}

// 获取并使用指定方法
Method privateMethod= clazz.getDeclaredMethod(“privateMethod”);// 获取无参私有方法
privateMethod.setAccessible(true);
privateMethod.invoke(obj);// 调用方法

Method publicMethod= clazz.getMethod(“publicMethod”,String.class);// 获取有参数方法
publicMethod.invoke(obj,“黎杜”);// 调用有参方法
看完上面的 demo 以后,有些人会说,老哥这只是一个很简单的 demo,确实是,这里为了照顾一下一些新手,先熟悉一下反射的一些方法的用法,好戏还在后头。
反射在 jdk 1.5 的时候允许对 Class 对象能够支持泛型,也称为泛化 Class,具体的使用如下:
Class user= User.class;
// 泛化 class 可以直接得到具体的对象,而不再是 Object
Useruser= user.newInstance();
泛化实现了在获取实例的时候直接就可以获取到具体的对象,因为在编译器的时候就会做类型检查。当然也可以使用通配符的方式,例如:Class<?>
反射实际应用

经过上面的反射的原理介绍,下面就要开始反射的实际场景的应用,所有的技术,你知道的该技术的应用场景永远是最值钱。这个是越多越好,知道的场景越多思路就越多。
反射的实际场景的应用,这里主要列举这几个方面:
动态代理
JDBC 的数据库的连接
Spring 框架的使用
动态代理实际就是使用反射的技术来实现,在程序运行时创建一个代理类,用来代理给定的接口,实现动态处理对其所代理的方法的调用。
实现动态代理主要有以下几个步骤:
实现 InvocationHandler 接口,重写 invoke 方法,实现被代理对象的方法调用的逻辑。
Proxy.getProxyClass 获取代理类
执行方法,代理成功
动态代理的实现代码如下所示,首先创建自己类 DynamicProxyHandler 实现 InvocationHandler :
public class DynamicProxyHandler implements InvocationHandler {
private Object targetObj;

public DynamicProxyHandler() {
    super();
}

public DynamicProxyHandler(Object targetObj) {
    super();
    this.targetObj= targetObj;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    System.err.println("开始执行targetObj的方法");
    //执行被代理的targetObj的方法
    method.invoke(targetObj, args);
    System.out.println("执行方法结束");
   return null;
}

}
然后执行 Proxy.newProxyInstance 方法创建代理对象,最后执行代理对象的方法,代码实现如下:
User user = new UserImpl();
DynamicProxyHandler dynamicProxy = new DynamicProxyHandler(user);
// 第一个参数:类加载器;第二个参数:user.getClass().getInterfaces():被代理对象的接口;第三个参数:代理对象
User userProxy = (User) Proxy.newProxyInstance(user.getClass().getClassLoader(), user.getClass().getInterfaces(), dynamicProxy);
userProxy.login();
userProxy.logout();
以上的实现是 jdk 的动态代理方式,还有一种动态代理是 Cglib 的动态代理方式,Cglib 动态代理也是被广泛的使用,比如 Spring AOP 框架中,实现了方法的拦截功能。
在 ORM 框架 Hibernate 框架也是使用 Cglib 框架来代理单端 single-ended 的关联关系。
jdk 的动态代理与 Cglib 的动态代理的区别在于 jdk 动态代理必须实现接口,而 Cglib 的动态代理是对那些没有实现接口的类,实现的原理是通过继承称为子类,并覆盖父类中的一些方法。
对于 Cglib 的动态代理这里由于篇幅的原因不再做详细讲解,下一篇将会详细的讲解 jdk 的动态代理和 Cglib 的动态代理的实现。
下面我们来看看 JDBC 中反射的应用案例,在 JDBC 中使用 Class.forName() 方法来加载数据库驱动,就是使用反射的案例。
让我们来一波入门的时候写的代码,一波回忆杀历历在目,具体的实现代码我相信也是很多人在初学者的时候也写过,如下所示:
Class.forName(“com.mysql.jdbc.Driver”); //1、使用 CLASS 类加载驱动程序 , 反射机制的体现
con = DriverManager.getConnection(“jdbc:mysql://127.0.0.1:3306/test”,“root”,“root”); //2、连接数据库
最后一个案例实现是使用反射模拟 Spring 通过 xml 文件初始化 Bean 的过程,学过 ssm 的项目都会依稀的记得 Spring 的配置文件,比如:
上面的配置文件非常的熟悉,在标签里面有属性,属性有属性值,以及标签还有子标签,子标签也有属性和属性值,那么怎么用他们初始化成 Bean 呢?
思路可以是这样的,首先得得到配置文件的位置,然后加载配置文件,加载配置文件后就可以解析具体的标签,获取到属性和属性值,通过属性值初始化 Bean。
实现的代码如下,首先加载配置文件的内容,并获取到配置文件的根节点:
SAXReader reader = new SAXReader();
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
InputStream is= classLoader.getResourceAsStream(beanXml);
Document doc = reader.read(is);
Element root = doc.getRootElement();
拿到根节点后,然后可以获取 bean 标签中的属性和属性值,当拿到属性 class 属性值后就可以通过反射初始化 Bean 对象。
for (Iterator i = root.elementIterator(“bean”); i.hasNext();) {
Element foo = (Element) i.next();
// 获取 Bean 中的属性值
Attribute idValue = foo.attribute(“id”);
Attribute clazzValue = foo.attribute(“class”);
// 通过反射获取 Class 对象
Class bean = Class.forName(clazzValue.getText());
// 并实例化 Bean 对象
Object obj = bean.newInstance();
}
除了初始化对象你还可以为 Bean 对象赋予初始值,例如上面的 bean 标签下还有 property 标签,以及它的属性值 value:
我们就可以通过以下代码来初始化这些值:
BeanInfo beanInfo = Introspector.getBeanInfo(bean);
// bean 对象的属性信息
PropertyDescriptor propertyDescriptor[] = beanInfo .getPropertyDescriptors();
for (Iterator ite = foo.elementIterator(“property”); ite.hasNext();) {
Element property= (Element) ite.next();
Attribute name = property.attribute(“name”);
Attribute value = property.attribute(“value”);
for (int i= 0; k < propertyDescriptor.length; i++) {
if (propertyDescriptor[i].getName().equalsIgnoreCase(name.getText())) {
Method method= propertyDescriptor[i].getWriteMethod();
// 使用反射将值设置进去
method.invoke(obj, value.getText());
}
}
以上就是简单的三个反射的应用案例,也是比较简单,大佬不喜勿喷哈,初学者就当是自己学多一点知识,总之一点一点进步。
反射优点和缺点

优点:反射可以动态的获取对象,调用对象的方法和属性,并不是写死的,比较灵活,比如你要实例化一个 bean 对象,你可能会使用 new User() 写死在代码中。
但是使用反射就可以使用 class.forName(user).newInstance(),而变量名 user 可以写在 xml 配置文件中,这样就不用修改源代码,灵活、可配置。
缺点:反射的性能问题一直是被吐槽的地方,反射是一种解释操作,用于属性字段和方法的接入时要远远慢于直接使用代码,因此普通程序也很少使用反射。