理解Java反射的正确姿势

反射简介

反射是Java的高级特性之一,但是在实际的开发中,使用Java反射的案例却非常的少,但是反射确实在底层框架中被频繁的使用。

比如:JDBC中的加载数据库驱动程序,Spring框架中加载bean对象,以及态代理,这些都使用到反射,因为我们要想理解一些框架的底层原理,反射是我们必须要掌握的。

理解反射我们先从他的概念入手,那么什么是反射呢?

反射就是在运行状态能够动态的获取该类的属性和方法,并且能够任意的使用该类的属性和方法,这种动态获取类信息以及动态的调用对象的方法的功能就是反射。

实现上面操作的前提是能够获取到该类的字节码对象,也就是.class文件,在反射中获取class文件的方式有三种:

  1. 类名.class    如:Person.class

  2. 对象.class    如:person.class

  3. Class.forName(全类名)获取   如:Class.forName("ldc.org. demo.person")

Class对象

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

我们看过JVM的相关书籍都会详细的了解到,Java文件首先要通过编译器编译,编译成Class文件,然后通过类加载器(ClassLoader)将class文件加载到JVM中。

在JVM中Class文件都与一个Class对象对应,在因为Class对象中包含着该类的类信息,只要获取到Class对象便可以操作该类对象的属性与方法。

在这里深入理解反射之前先来深入的理解Class对象,它包含了类的相关信息。

Java中我们在运行时识别对象和类的信息,也叫做RTTI,方式主要有来两种:

  1. 传统的RTTI(Run-Time Type Information)

  2. 反射机制

那么什么是RTTI呢?RTTI称为运行时类型识别,传统的RTTI是在编译时就已经知道所有类型;而反射机制则是在程序运行时才确定的类型信息。

想要运行时使用类型信息,就必须要获取Class对象的引用,获取Class对象的方式上面已经提及。

这里有点区别的就是使用(.class)方式获取Class对象,并不会初始化Class对象,而使用(forName("全类名"))的方式会自动初始化Class对象

当一个.class文件要被加载到JVM中的时候,会进行如下的准备工作,首先会检查这个类是否被加载,若是没有被加载就会根据全类名找到class文件,接着加载Class文件,并创建类的静态成员引用。

但是在程序中并非是一开始就完全加载该类的class文件,而是在程序用的地方再加载,即为懒加载模式

当加载完Class文件后,接着就会验证Class文件中的字节码,并静态域分配存储空间。这个过程也叫做链接

最后一步就是进行初始化,即为了使用类而提前做的准备工作如下图所示:

反射

反射对应到Java中的类库就是在java.lang.reflect下, 在该包下包含着FieldMethodConstructor类。

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,并且除了有两个测试方法privateMethodpublicMethod用于测试私有方法和公共方法的获取。接着执行如下代码:

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= User.class;
//泛化class可以直接得到具体的对象,而不再是Object
Useruser= user.newInstance();

泛化实现了在获取实例的时候直接就可以获取到具体的对象,因为在编译器的时候就会做类型检查。当然也可以使用通配符的方式,例如:Class<?>

反射实际应用

经过上面的反射的原理介绍,下面就要开始反射的实际场景的应用,所有的技术,你知道的该技术的应用场景永远是最值钱。这个是越多越好,知道的场景越多思路就越多。

反射的实际场景的应用,这里主要列举这几个方面:

  1. 动态代理

  2. JDBC 的数据库的连接

  3. Spring 框架的使用

动态代理实际就是使用反射的技术来实现,在程序运行时创建一个代理类,用来代理给定的接口,实现动态处理对其所代理的方法的调用。

实现动态代理主要有以下几个步骤:

  1. 实现InvocationHandler接口,重写invoke方法,实现被代理对象的方法调用的逻辑。

  2. Proxy.getProxyClass获取代理类

  3. 执行方法,代理成功

动态代理的实现代码如下所示,首先创建自己类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 id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
    <property name="jdbcUrl" value="${jdbc.url}"></property>
    <property name="driverClass" value="${jdbc.driverName}"></property>
    <property name="user" value="${jdbc.username}"></property>
    <property name="password" value="${jdbc.pwd}"></property>
</bean>

上面的配置文件非常的熟悉,在标签里面有属性,属性有属性值,以及标签还有子标签,子标签也有属性和属性值,那么怎么用他们初始化成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:

<property name="jdbcUrl" value="${jdbc.url}"></property>
<property name="driverClass" value="${jdbc.driverName}"></property>
<property name="user" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.pwd}"></property>

我们就可以通过以下代码来初始化这些值:

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配置文件中,这样就不用修改源代码,灵活、可配置

缺点:反射的性能问题一直是被吐槽的地方,反射是一种解释操作,用于属性字段和方法的接入时要远远慢于直接使用代码,因此普通程序也很少使用反射。

1. 人人都能看懂的 6 种限流实现方案!

2. 一个空格引发的“惨案“

3. 大型网站架构演化发展历程

4Java语言“坑爹”排行榜TOP 10

5. 我是一个Java类(附带精彩吐槽)

6. 看完这篇Redis缓存三大问题,保你能和面试官互扯

7. 程序员必知的 89 个操作系统核心概念

8. 深入理解 MySQL:快速学会分析SQL执行效率

9. API 接口设计规范

10. Spring Boot 面试,一个问题就干趴下了!

扫码二维码关注我

·end·

—如果本文有帮助,请分享到朋友圈吧—

我们一起愉快的玩耍!

你点的每个赞,我都认真当成了喜欢

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值