您现在的位置是:首页 > 文章 > 有趣的 Java 反射(基础) 网站文章

有趣的 Java 反射(基础)

孙玉超 2021-04-30 10:49:13 0 评论 714 浏览 0 收藏 0


反射是允许 Java 程序在运行时动态获取对象字段、调用对象方法的一种机制。

我相信单纯的通过定义,你可能不太理解反射,下面我们看一个 Demo 。


案例引入


我们现在有个配置文件 test.properties 内容如下

init-method=a

假设现在有个需求,我希望代码通过读取配置文件的内容来调用相应的方法。想一想没学过反射的话,怎样来实现这个需求。

新建测试类

public class ReflectionTest {
    public static void main(String[] args) throws Exception {
        Properties p = new Properties();
        p.load(new FileInputStream("src/main/resources/test.properties"));
        String method = (String) p.get("init-method");
        ReflectionTest r = new ReflectionTest();
        Method m = r.getClass().getMethod(method);
        m.invoke(r);
    }

    public void a(){
        System.out.println("这是a方法");
    }

    public void b(){
        System.out.println("这是b方法");
    }
}

我们在代码中读取配置文件,根据配置文件的 key 来获取 value,然后通过一个 Method 对象调用了对象的 a 方法。反复修改配置文件中的 init-method 的值发现我们可以通过修改配置文件,不重启程序,不修改代码的情况下动态决定我们调用哪个方法。


由此联想,当前学 Spring 的时候,是不是在 xml 文件中配置过对象的 init-method 和 destroy-method 。到这里,我相信你对反射可能有个大概的了解。


反射相关类 Class、Method、Constructor、Field


反射离不开 Class 类,正如上面的 Demo ,我们通过 ReflectionTest 对象 的 Class 对象,获取了与这个对象相关的 Method 对象,返回通过 Method 对象去调用原对象 ReflectionTest 的方法。这个 Method 是什么?在理解这个之前,我们需要先有一些基本认知。


当一个类被加载到 JVM 时会生成一个该类唯一的 Class 对象,这个 Class 对象是 JVM 生成的,并且一个类只有一个 Class 对象,一个类的所有实例,共享这个 Class 对象,或者说持有它的引用。



参考上图,一个类有多个对象,我们可以通过对象来获取这个类的 Class 对象,然后通过这个 Class 对象来获取 Method ,然后通过 Method 调用这个对象的某个方法。到这里你可能大致有点感觉了,Method 这个类是 Java 中所有类中方法的一种抽象描述,你可以通过它以传参的方式来调用对象的任意方法。


我们也可以通过 类名.class 的方式来获取该类的 Class 对象,然后通过这个 Class 对象调用 Constructor 类反过来创建该类的对象。

Constructor<ReflectionTest> constructor = ReflectionTest.class.getConstructor();
ReflectionTest reflectionTest = constructor.newInstance();
reflectionTest.a();

字段也是一样的操作

ReflectionTest rt = new ReflectionTest();
Field field = ReflectionTest.class.getField("field");//字段名
Object o = field.get(rt);//调用哪个对象的字段
System.out.println(o);


访问私有


如果我们把第一个 Demo 方法 a 的修饰符改成 private ,你会发现程序运行就抛出 NoSuchMethodException 异常。当成员变量、方法、构造方法被私有修饰符 private 修饰时,我们要通过另一个方法 getDeclareXXX 来获取相关的抽象描述。

public void a(){
    System.out.println("这是a方法");
}
Method m = r.getClass().getDeclaredMethod(method);


反射爆破


上面我们虽然可以通过 API 获得私有的成员方法,但是如果我们使用 m.invoke(o) 调用其实会报错(测试这个报错,需要把 main 方法所在类的包不同于测试对象 的那个包,也就是重新在另一个包写 main 方法测试),因为私有的成员方法不允许直接调用,反射提供了一种方法,我们可以在反射使用中,将这个私有方法设置为可访问。这样就可以合法访问了

ReflectionTest r= new ReflectionTest();
Method m = ReflectionTest.class.getDeclaredMethod("a");
m.setAccessible(true); //爆破
m.invoke(r);

构造方法和成员变量也都一样,设置可访问即可


业务案例


曾经有一个计算会员档案完善度的需求,根据会员档案实体类中字段的填写来计算完善程度。试想一下常规的做法

int completedDegree = 0;
if(field1 != null){
    completedDegree+=5
} else if(field2 != null){
    completedDegree+ = 5
}...

不使用反射的传统做法就只能一个字段一个字段去判断,假设后面增加实体类的字段,这里还需要改代码。使用反射的做法:

先在 yml 中配置需要计算完善度的字段集合

application:
  participate-completed-calc-fields:
    - "username"
    - "storeAddress"
    #...

从 yml 映射集合到 Java 代码

/**
 * 需要计算完善度的字段集合,从配置文件直接映射过来
 */
@NotEmpty
private List<String> participateCompletedCalcFields = new ArrayList<>();

定义方法体计算完善度

/**
 * 计算完善度
 */
public int calcCompleteDegree() throws IllegalAccessException {
    int count = 0;
    Field[] field = this.getClass().getDeclaredFields();//获取所有字段
    for (Field value : field) {
        if (participateCompletedCalcFields.contains(value.getName())) {//如果字段需要计算完善度
            value.setAccessible(true);//爆破 private 设置可访问
            Object o = value.get(this);
            if (o != null && StringUtils.isNotBlank(o.toString())) {
                count++;
            }
        }
    }
    return (int) Math.round(count * 1.0 * 100 / participateCompletedCalcFields.size());
}

使用反射之后一堆的 if 和 else 都没了,而且代码看起来是不是特有逼格


反射的动态性


要描述动态性我们首先了解一下静态性,当我们写代码的时候如果有语法错误,编译器就会给我们提示,比如标识符找不到。看下面代码

Scanner in = new Scanner(System.in);
int num = in.nextInt();
if(num == 1){
    Girl girl = new Girl();
} else if(num == 2){
    Class<?> girl = Class.forName("Girl");
} else {
    Object o = new Object();
}

单纯的看这段代码,我们没有定义 Girl 这个类,在程序执行到 num 读取之前,我们其实不知道到底会执行哪个代码块,使用 javac 编译的话,会报错找不到符号。如果在 IDEA 中也会报红,没有 Girl 这个类,这个就是静态特性,编译的时候就给你提示了。为什么 num == 2 的代码块不会呢,这就体现了反射的动态性,它在运行时代码真的执行到了这一行,才会去加载名为 Girl 的类,如果找不到,才会抛出异常。

从这里我们也可以看出,不要过分的追求男女朋友,当缘分到来,自然会遇到对的人~~~~


结语


Java 流行框架底层几乎都是反射,根据配置用反射实现调用、定义等。比如 Spring 声明 Bean,MyBatis 的 Mapper 根据接口去关联 xml 文件中的 SQL ,这里还有代理等。有句话是这么说的:反射是框架的灵魂~




转载请注明出处:转载请注明出处

上一篇 : 电商平台秒杀系统设计 下一篇 : Java 线程池

留言评论

所有回复

暮色妖娆丶

96年草根站长,2019年7月接触互联网踏入Java开发岗位,喜欢前后端技术。对技术有强烈的渴望,2019年11月正式上线自己的个人博客