前言

反射blog有很多,不再赘述,但是反射的作用具体实现场景就会比较少,这里举个例子

一个需求

使用参数的方式传入需要执行的类名,然后执行相应类的同名方法

普通的实现方法(静态加载)

因为需要考虑执行的是不同类的同名方法,所以用接口来规范这个方法,然后增加两个类去实现这个接口即可,最后通过判断执行哪一个类

  • 接口 Stand
package com.test.dynamicLoading;

public interface Stand {
    public void run();

}
  • 实现类A.java
package com.test.dynamicLoading;

public class A implements Stand{
    public void run() {

        System.out.println("A running");
    }
}
  • 实现类B.java
package com.test.dynamicLoading;

public class B implements Stand{

    public void run() {
        System.out.println("B running");
    }

    private void privateRun() {
        System.out.println("C privateRun running");
    }
}
  • 执行类 runAll.java
package com.test.dynamicLoading;

public class runAll {

    public static void main(String[] args) {
        if ("A".equals(args[0])) {
            System.out.println("loading A");
            A a = new A();
            a.run();
        }
        if ("B".equals(args[0])) {
            System.out.println("loading B");
            B b = new B();
            b.run();
        }
    }

}

在命令行进行编译

~/Desktop/sourceCode/java-test/src/main/java/com/test/dynamicLoading> javac runAll.java

runAll.java:8: 错误: 找不到符号
            A a = new A();
            ^
  符号:   类 A
  位置: 类 runAll
runAll.java:8: 错误: 找不到符号
            A a = new A();
                      ^
  符号:   类 A
  位置: 类 runAll
runAll.java:13: 错误: 找不到符号
            B b = new B();
            ^
  符号:   类 B
  位置: 类 runAll
runAll.java:13: 错误: 找不到符号
            B b = new B();
                      ^
  符号:   类 B
  位置: 类 runAll
4 个错误
exit 1

Ok,符合预期,对于可能使用到的类,需要同时编译,这是静态加载(静态加载的类的源程序在编译时期加载(必须存在))的过程,也就是说,编译runAll.java的时候,就要求所有可能被用到的类都需要存在且一起编译

~/Desktop/sourceCode/java-test/src/main/java/com/test/dynamicLoading> javac runAll.java B.java A.java Stand.java

以上编译通过,在同级目录会产生runAll.class,A.class,B.class文件,开始执行

~/Desktop/sourceCode/java-test/src/main/java> java com.test.dynamicLoading.runAll A

loading A
A running
~/Desktop/sourceCode/java-test/src/main/java> java com.test.dynamicLoading.runAll B

loading B
B running

需要注意的是,执行java的时候,因为有package的关系,所以需要退出到响应的目录层级操作:至于为什么请参考:使用java命令运行class文件提示“错误:找不到或无法加载主类“的问题分析

程序顺利执行,对应传入的不同参数,执行不同的效果,好了重头戏来了,如果我现在要增加一个类C,然后接着执行呢?如何操作

  • 第一步:先写类C.java
package com.test.dynamicLoading;

public class C implements Stand{
    @Override
    public void run() {
        System.out.println("C running");
    }
}

第二步,修改runAll.java代码

package com.test.dynamicLoading;

public class runAll {
		
    public static void main(String[] args) {
        // 静态加载类,在编译时刻就需要加载所有的可能使用到的类
        if ("A".equals(args[0])) {
            System.out.println("loading A");
            A a = new A();
            a.run();
        }
        if ("B".equals(args[0])) {
            System.out.println("loading B");
            B b = new B();
            b.run();
        }
       // 新增C
        if ("C".equals(args[0])) {
            System.out.println("loading C");
            C c = new C();
            c.run();
        }
    }

}

第三步,重新编译

~/Desktop/sourceCode/java-test/src/main/java/com/test/dynamicLoading> javac runAll.java C.java Stand.java A.java B.java

因为要重新编译runAll.java,所以需要把依赖的几个类包括接口都同时编译

第四步,执行

~/Desktop/sourceCode/java-test/src/main/java> java com.test.dynamicLoading.runAll C

loading C
C running

目前为止都是正常的,但是,这样操作巧妙么?不,很臃肿,而且随着类的增加,需要不断的去变更runAll.java代码,而且需要重新进行编译才能把类加载进去

使用反射的方式进行动态加载

反射及作用是什么这里不再赘述,有几篇文章很好可以建立认知

  1. 谈谈Java反射机制:https://www.jianshu.com/p/6277c1f9f48d
  2. 框架开发之Java注解的妙用:https://www.jianshu.com/p/b560b30726d4
  3. 深入理解Java类型信息(Class对象)与反射机制
  4. 我竟然不再抗拒 Java 的类加载机制了:https://zhuanlan.zhihu.com/p/73078336
  5. 好怕怕的类加载器:https://zhuanlan.zhihu.com/p/54693308

步骤一:写基础类A.java,B.java,Stand.java,上文中都可以直接使用

步骤二:重新写一个使用反射操作来动态获取的执行类的类

package com.test.dynamicLoading;
import java.lang.reflect.Method;

public class dynamicRunAll {

    public static void main(String[] args) {
         try {
             Class c1 = Class.forName(args[0]); 
             System.out.println("获取该类的名称:" + c1.getName());
             Method[] methods = c1.getDeclaredMethods();  // 获取申明的方法,包括private修饰的
             for(Method m:methods) {
                 System.out.println("该类的方法有:" + m);
             }
             Stand stand  = (Stand)c1.newInstance();
             stand.run();
         } catch (Exception e) {
            System.out.println(e);
        }
    }
}

第三步:编译

# 先删除所有的.class文件
~/Desktop/sourceCode/java-test/src/main/java/com/test/dynamicLoading> rm *.class  
# 编译文件
~/Desktop/sourceCode/java-test/src/main/java/com/test/dynamicLoading> javac dynamicRunAll.java Stand.java

这边需要注意的是,因为动态加载的关系,所以编译时并不需要提前知道该类(可以实现在运行过程中,通过传入不同的"className"创建不同的实例,这个过程完全可以在运行时确定),大家都遵循着一个规范,只要这个规范下的类都可以被之后加载,用来约束的就是Stand这个接口

上述过程结束后,我们得到了dynamicRunAll.class,Stand.class两个文件,当我们需要使用B作为参数传入时,首先就需要去编译B.java

~/Desktop/sourceCode/java-test/src/main/java/com/test/dynamicLoading> javac B.java Stand.java

然后执行dynamicRunAll注意需要传入的参数需要为该类的全限定名,类似绝对路径的意思,这里我们传入的参数是com.test.dynamicLoading.B

~/Desktop/sourceCode/java-test/src/main/java> java com.test.dynamicLoading.dynamicRunAll com.test.dynamicLoading.B

获取该类的名称:com.test.dynamicLoading.B
该类的方法有:private void com.test.dynamicLoading.B.privateRun()
该类的方法有:public void com.test.dynamicLoading.B.run()
B running

Ok,完美执行,现在我们有个新需求,需要增加一个C类当做参数的判断,在上一节中已经建了一个C.java直接拿来用,但是我们不再需要去修改执行文件,也不需要再次去编译执行文件,也就是说不需要中断主服务(加载的类信息可以在系统运行过程中动态添加),我们只需要把这个新写的类编译一下,然后就可以直接使用即可

  • 首先编译C.java
~/Desktop/sourceCode/java-test/src/main/java/com/test/dynamicLoading> javac C.java Stand.java
  • 其次当做参数直接传入
~/Desktop/sourceCode/java-test/src/main/java> java com.test.dynamicLoading.dynamicRunAll com.test.dynamicLoading.C

获取该类的名称:com.test.dynamicLoading.C
该类的方法有:public void com.test.dynamicLoading.C.run()
C running

这个过程中不需要改动dynamicRunAll.java,也不需要重新编译,使用反射实现的方式非常的优美。

总结

java的反射作用的地方有很多,比如注解中也有广泛的运用,而且和注解强相关的一些框架如spring中使用的也非常频繁,通过反射获取执行的类然后生成类实例只是反射中一部分的作用而已;反射的理念有点像是一种代理,本身真实的对象并不需要显性的露面,也有点像IOC,把需要执行的动作交给调用方,不是自己把所有类都加载完毕,然后挑某一个执行,而是调用方想执行哪一个类,就去加载该类,然后再执行;打一个比方:静态加载就像,你(调用方)去饭店吃饭,厨子(执行类)不需要把菜单上的所有菜(调用类)都炒好(静态加载),然后你选一个想吃的拿过来吃;动态加载就像,你从菜单中点其中一个菜,然后告诉厨子要吃这个,厨子再进行做菜;这是我的浅显理解,请不吝赐教