天天看点

java反射与热加载(全)

0 简介

0.1 反射是什么?

反射能使java程序动态调取类。举例:

对于以下HelloWorld类:

package reflect;

public class HelloWorld {
    String str = "world";

    public static void main( String[] args ){
        System.out.println("Hello world!");
    }

    public HelloWorld(String s) {
        str = s;
    }

    public HelloWorld(){}

    @Override
    public String toString() {
        return "Hello " + str + "!";
    }


}
           

我们分别使用普通方法和反射来调用:

public static void main(String[] args)  {
    //不通过反射:
    HelloWorld h1 = new HelloWorld("not reflect");
    //通过反射:
    HelloWorld h2 = null;
    try {
        h2 = (HelloWorld) Class.forName("reflect.HelloWorld")
                .getConstructor(String.class).newInstance("reflect");
    } catch (Exception e) {
        e.printStackTrace();
    }

    System.out.println(h1.toString());
    System.out.println(h2.toString());

}
           

输出:

Hello not reflect!
Hello reflect!

           

0.2 反射作用

反射主要用于通用框架开发。其意义在于,代码中不需要实际import类,只需要给出类名的字符串,就能获取类。比如说Spring框架,就需要使用配置文件加载对象和类,这时就需要使用反射。我们把上面的反射改一下:

public static void main(String[] args) {
    Scanner in = new Scanner(System.in);
    System.out.println("输入你想加载的类:");
    String clazz = in.next();
    Class<?> myClass = null;
    try {
        myClass = Class.forName(clazz);
        myClass.getMethod("main",String[].class).invoke(null, (Object) new String[0]);
    } catch (Exception e) {
        e.printStackTrace();
    }

}
           

代码中甚至没有出现任何我们想调用的类的信息,可见反射的灵活性。我们用它调用HelloWorld:

输入你想加载的类:
reflect.HelloWorld
Hello world!

           

再给个小例子。Object类的toString方法也用了反射获取类名:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
           

实际上我们在使用的时候,暂且将反射理解为获取类来操作的一种方法就好了。

1 原理

这一部分主要讲反射对类对象的获取(因为基本上来说获取了类对象以后其他部分就非常好理解了)。如果对反射不怎么熟悉,可先前往第二节查看反射的API。

1.1 类加载

简单回忆以下JVM如何加载类:

java反射与热加载(全)

(图片来源于网络,详情见篇尾)

加载:加载类进入内存

验证:验证类信息是否正确合法

初始化:静态变量赋值

使用:使用类

卸载:程序退出,卸载类

我们关注加载这一部分。虚拟机会做以下三件事:

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

能做到这些的就是靠ClassLoader类。

1.2 初识ClassLoader

JVM内置三个ClassLoader:

Bootstrap classLoader:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。

ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar

AppClassLoader:主要负责加载应用程序的主函数类

他们使用双亲委派来控制类加载进程:

java反射与热加载(全)

(图片来源于网络,详情见篇尾)

具体的ClassLoader是一个抽象类,我们在自定义ClassLoader时,需要重写覆盖findClass(),在里面写自定义的加载逻辑。在CLassLoader中,它通过调用一个本地方法defineClass来根据class文件的字节数组byte[] b造出一个对应的Class对象,并将类信息塞到方法区中。

java反射与热加载(全)

(图片来源于网络,详情见篇尾)

具体实现我们无从得知,但我们暂且只要知道ClassLoader能够加载类,且是通过一个本地方法加载的类即可。

1.3 反射的类获取

现在我们能回到反射了。就比如说下面的代码,它是怎么加载类的呢?

先看forName方法:

public static Class<?> forName(String className)
            throws ClassNotFoundException {
    Class<?> caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
           

然后就没了…

forName0就是native方法,我们无法再向下追踪了。但注意到,ClassLoader还是作为了forName0方法的一个参数,也就是说,接下来的任务应该还是由ClassLoader处理。

那在看看使用ClassLoader加载类的方法?

loadClass的源码:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}
           

有双亲委派那味了。

总的来说,反射是通过ClassLoader或直接自己调用本地方法来获取类的。比如对一个对象使用getClass方法,它就直接去调本地方法了:

(这也太短了…)

虽然说本地方法具体实现无从得知,但我们大概能理解到,这些获取类的方式,应该就是ClassLoader自己跑去了方法区,加载或找到了代表这个类的java.lang.Class对象,再返回给我们操纵。

java反射与热加载(全)

(图片来源于网络,详情见篇尾)

2 基本使用

底层原理就扒到这儿吧。接下来就是反射的常见使用方法了。

2.1 获取类

前面已经提到了一些获取类的方法,比如

forName()

方法,

getClass()

方法等等。对于反射,一般考虑以下四种方法获取类:

  • 通过Class的静态方法forName获取:体现反射的动态性
  • 调用运行时类本身的.class属性
  • 通过运行时类的对象getClass获取
  • 通过ClassLoader获取

其中第四种应该不是反射,但是也能获取类。

代码示例:

package reflect;

import javax.tools.ToolProvider;

public class GetClass {

    public static void main(String[] args) throws Exception{

        Class<?> clazz1 = Class.forName("reflect.HelloWorld");

        Class<?> clazz2 = HelloWorld.class;

        HelloWorld helloWorld = new HelloWorld();
        Class<?> clazz3 = helloWorld.getClass();

        ClassLoader classLoader = ToolProvider.getSystemToolClassLoader();
        Class<?> clazz4 = classLoader.loadClass("reflect.HelloWorld");

        System.out.println(clazz1 == clazz2);
        System.out.println(clazz2 == clazz3);
        System.out.println(clazz3 == clazz4);

    }
}
           

但是我们注意到,事实上使用上述任何一种方式,获得的Class对象都是相同的。我们从输出可以看出:

true
true
true
           

关于获取类还有以下性质:

  • 通过.class的方法获取对象的Class对象不会初始化任何内容,通过ClassLoader也如此;forName方式会初始化静态块,而getClass会初始化所有内容,原因是在这之前进行了实例化。
  • 静态代码块只会被初始化一次,无论重新获取类多少次。

2.2 实例化类

获得Class对象以后剩下的部分就很好办了,直接调用Class对象里的方法即可。两种实例化方法:

  • 直接调用newInstance方法,返回空构造器构造对象;
  • 先获取需要的构造器,再对构造器调用newInstance方法,返回特定构造对象;

代码示例:

Class<?> clazz1 = Class.forName("reflect.HelloWorld");

Object h1 = clazz1.newInstance();
Object h2 = clazz1.getConstructor(String.class).newInstance("h2");

System.out.println(h1);
System.out.println(h2);
           

对于第二种方法,左边填形参类,右边填实参对象。

2.3 调用方法

反射提供了getMethod方法获取一个类对象的方法对象,对于Method调用invoke方法即可调用需要的方法。下面是一个示例:

Class<?> clazz1 = Class.forName("reflect.HelloWorld");
Object h1 = clazz1.newInstance();

HelloWorld.main(null);
clazz1.getMethod("main", String[].class).invoke(null, (Object) new String[0]);

System.out.println(h1.toString());
System.out.println(clazz1.getMethod("toString").invoke(h1));
           

前两行调用了反射获取类和实例。3-4行演示了静态方法调用,5-6行演示了动态方法调用。

对于getMethod方法,第一个参数填方法名,剩下的填形参类;

invoke方法的第一个参数填其由哪个实例调用。如果是静态方法,就填null;剩下的参数填实参对象。

2.4 其他

java文档:https://docs.oracle.com/javase/7/docs/api/java/lang/Class.html

贴几个可能有用的:

返回内部名:

getName

public String getName()
           
Returns the name of the entity (class, interface, array class, primitive type, or void) represented by this

Class

object, as a

String

.

If this class object represents a reference type that is not an array type then the binary name of the class is returned, as specified by The Java™ Language Specification.

If this class object represents a primitive type or void, then the name returned is a

String

equal to the Java language keyword corresponding to the primitive type or void.

If this class object represents a class of arrays, then the internal form of the name consists of the name of the element type preceded by one or more ‘

[

’ characters representing the depth of the array nesting. The encoding of element type names is as follows:
Element Type Encoding
boolean Z
byte B
char C
class or interface Lclassname;
double D
float F
int I
long J
short S

The class or interface name classname is the binary name of the class specified above.

Examples:

String.class.getName()
     returns "java.lang.String"
 byte.class.getName()
     returns "byte"
 (new Object[3]).getClass().getName()
     returns "[Ljava.lang.Object;"
 (new int[3][4][5][6][7][8][9]).getClass().getName()
     returns "[[[[[[[I"
 
           
  • Returns:

    the name of the class or interface represented by this object.

获得接口:

getInterfaces

public Class<?>[] getInterfaces()
           

Determines the interfaces implemented by the class or interface represented by this object.

If this object represents a class, the return value is an array containing objects representing all interfaces implemented by the class. The order of the interface objects in the array corresponds to the order of the interface names in the

implements

clause of the declaration of the class represented by this object. For example, given the declaration:
class Shimmer implements FloorWax, DessertTopping { ... }
           
suppose the value of

s

is an instance of

Shimmer

; the value of the expression:
s.getClass().getInterfaces()[0]
           
is the

Class

object that represents interface

FloorWax

; and the value of:
s.getClass().getInterfaces()[1]
           
is the

Class

object that represents interface

DessertTopping

.

If this object represents an interface, the array contains objects representing all interfaces extended by the interface. The order of the interface objects in the array corresponds to the order of the interface names in the

extends

clause of the declaration of the interface represented by this object.

If this object represents a class or interface that implements no interfaces, the method returns an array of length 0.

If this object represents a primitive type or void, the method returns an array of length 0.

  • Returns:

    an array of interfaces implemented by this class.

获得域变量:

getField

public Field getField(String name)
               throws NoSuchFieldException,
                      SecurityException
           
Returns a

Field

object that reflects the specified public member field of the class or interface represented by this

Class

object. The

name

parameter is a

String

specifying the simple name of the desired field.

The field to be reflected is determined by the algorithm that follows. Let C be the class represented by this object:

  1. If C declares a public field with the name specified, that is the field to be reflected.
  2. If no field was found in step 1 above, this algorithm is applied recursively to each direct superinterface of C. The direct superinterfaces are searched in the order they were declared.
  3. If no field was found in steps 1 and 2 above, and C has a superclass S, then this algorithm is invoked recursively upon S. If C has no superclass, then a

    NoSuchFieldException

    is thrown.
See The Java Language Specification, sections 8.2 and 8.3.
  • Parameters:

    name

    - the field name
  • Returns:

    the

    Field

    object of this class specified by

    name

  • Throws:

    NoSuchFieldException

    - if a field with the specified name is not found.

    NullPointerException

    - if

    name

    is

    null

    SecurityException

    - If a security manager, s, is present and any of the following conditions is met: invocation of [

    s.checkMemberAccess(this, Member.PUBLIC)

    ](https://docs.oracle.com/javase/7/docs/api/java/lang/SecurityManager.html#checkMemberAccess(java.lang.Class, int)) denies access to the field the caller’s class loader is not the same as or an ancestor of the class loader for the current class and invocation of

    s.checkPackageAccess()

    denies access to the package of this class
  • Since:

    JDK1.1

2.5 例1 - 计算器

可以使用反射实现C语言的函数指针:

代码示例:

package reflect;

import java.lang.reflect.Method;

public class Pointer {

    public static int add(int a, int b) {
        return a + b;
    }

    public static int subtract(int a, int b){
        return a - b;
    }

    public static int calculate(Method method, int a, int b) throws Exception {
        return (int) method.invoke(null,a,b);
    }

    public static void main(String[] args) throws Exception {
        Method add = Pointer.class.getMethod("add",int.class,int.class);
        Method subtract = Pointer.class.getMethod("subtract", int.class, int.class);

        System.out.println(calculate(add,1,1));
    }
}
           

我们实现了使用calculate函数,传入一个方法和两个数计算结果的操作。如果不使用反射,calculate方法下就需要写大量冗长的case语句。而使用反射时,当我们需要定义一种新运算时,我们只需要新建一个其对应的方法即可,甚至不需要修改calculate函数。

2.6 例2 - local judge

(以下情节纯属虚构)

假设一个情景。小明遇到一道算法题,他觉得这道题很难,所以他向你寻求帮助。因为你这道题已经AC了,所以他想请你帮他测试一些样例,检测一下自己的代码对不对。但你手上的样例太多了,不想帮他一个一个测,所以你打算写个判题机。

小明的题是这样的:

A+B

给出两个数,请输出两个数的和。

Simple Input

1 1
           
Simple Output
2
           

小明也把他的代码Answer.java发了给你,于是你写了一个丐中丐版本的本地测试机:

package reflect;

import java.io.*;
import java.util.Scanner;

public class LocalJudge1 {
    static PrintStream defOut = System.out;
    static InputStream defIn = System.in;

    public static void main(String[] args) throws Exception {
        String[] cases = {"1 1","2 2","1 4"};
        String[] answers = {"2","4","5"};

        File caseFile = new File("src/reflect/case.txt");  //暂存样例的文件
        File ansFile = new File("src/reflect/ans.txt");  //暂存答案的文件

        for (int i = 1; i <= 3; i++) {

            //将样例导入样例文件
            System.setOut(new PrintStream(caseFile));
            System.out.println(cases[i-1]);

            //运行待测程序,将输出导入答案文件
            System.setIn(new FileInputStream(caseFile));
            System.setOut(new PrintStream(ansFile));
            Answer.main(null);  //TODO: 这里有问题么?

            //获取答案
            Scanner in = new Scanner(ansFile);
            String ans = in.next();
            in.close();

            //比对答案
            if (!ans.equals(answers[i-1])) {
                System.setOut(defOut);
                System.out.println("WA");
                System.exit(-1);
            }

        }
        System.setOut(defOut);
        System.out.println("AC");
    }


}
           

假设我们这里没有使用反射,直接使用

Answer.main(null)

调用main函数。这样写有什么问题么?

于是你拿小明代码一测,输出WA了。怪事情了。单独测样例的时候没问题啊?于是你细看了一眼小明的代码,发现小明的Answer.java是这么写的:

package reflect;

import java.util.Scanner;

public class Answer {

    static int ans = 0;

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        ans += in.nextInt();
        ans += in.nextInt();
        System.out.print(ans);
    }
    
}
           

豁,写A+B静态变量都用上了。倒也不能说写的不对,但这也是问题所在。Answer类的静态变量只在类加载的时候初始化了。当第二次调用main方法时,ans就已经不是零了。那看来还是判题机的问题。那这样写呢?

我们把

Answer.main(null)

替换为以下代码:

ClassLoader classLoader = ToolProvider.getSystemToolClassLoader();
Class<?> answer = classLoader.loadClass("reflect.Answer");
answer.getMethod("main",String[].class).invoke(null, (Object) new String[0]);
           

这样,每次调用main方法前都会“重新加载”一次类,静态变量就能初始化了。这样想着,你便把代码改了上去。不幸的是,又WA了。为啥啊?

原因是,尽管我们试图使用了ClassLoader重新加载类,但类其实没有被重新加载。回忆第一节双亲委派的部分,如果一个类已经被加载了,JVM便不会试图再加载一次,这也就是为什么说静态变量仅初始化一次的原因。那有什么解决方案呢?

3 热替换

上节我们说到,我们希望jvm动态重载类。然而由于双亲委派机制的存在,我们的类无法正常重载。我们的目标是,在每次运行待测程序前重置静态变量,即需要动态重载类。接下来我们使用热替换技术,在程序运行时重新加载类。

3.1 ClassLoader续

我们来再认识ClassLoader类。ClassLoader是一个抽象类,里面有这些方法比较重要:

loadClass:ClassLoader的入口,上文有所提及。当双亲委派都无法加载时,调用自己的findClass方法;

findClass:自定义ClassLoader需要重写该方法调用类;

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}
           

defineClass:重写findClass时调用该方法,将字节数据转化为类对象;

protected final Class<?> defineClass(String name, byte[] b, int off, int len)
    throws ClassFormatError
{
    return defineClass(name, b, off, len, null);
}
           

下面给一个常见重写ClassLoader的方法:

static class LameLoader extends ClassLoader {
    private File basedir;
    private String className;

    public LameLoader(File basedir, String clazns) throws Exception {
        super(getSystemClassLoader());
        this.basedir = basedir;
        this.className = clazns;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            FileInputStream fin = new FileInputStream(basedir);
            byte[] raw = new byte[(int) basedir.length()];
            fin.read(raw);
            fin.close();
            return defineClass(className, raw, 0, raw.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}
           

3.2 动态类重载

现在我们写一个能够打破双亲委派的ClassLoader。意识到如果我们要做到这一点,还必须要重写loadClass方法。最草率的一种方法就是直接清除所有双亲委派的代码,直接使用defineClass把我们需要加载的类写入jvm。

我们一般的ClassLoader是这样处理的:

java反射与热加载(全)

现在为了实现动态重载:

java反射与热加载(全)

下面给出一个能够动态重载类的例子:

static class CustomClassLoader extends ClassLoader {

    private File basedir;
    private String className;

    public CustomClassLoader(File basedir, String clazns) throws Exception {
        super(null);
        this.basedir = basedir;
        this.className = clazns;

        FileInputStream fin = new FileInputStream(basedir);
        byte[] raw = new byte[(int) basedir.length()];
        fin.read(raw);
        fin.close();
        defineClass(className, raw, 0, raw.length);


    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        Class<?> cls = null;
        cls = findLoadedClass(name);
        if (!this.className.equals(name) && cls == null) cls = getSystemClassLoader().loadClass(name);
        if (cls == null) throw new ClassNotFoundException(name);
        if (resolve)
            resolveClass(cls);
        return cls;
    }

}
           

它的逻辑很简单,就是干脆一开始就把类通过defineClass塞到了jvm内存里。当我们使用这个ClassLoader时,如果使用它load不是我们需要的类,就转发给System ClassLoader代理。如果是我们需要的类,就直接通过findLoadedClass方法找到我们之前塞到jvm里面的类。不是最好的方法,不过也足够用了。

使用例:

ClassLoader classLoader = new CustomClassLoader(new File("src/reflect/Answer.class"),"reflect.Answer");
Class<?> answer = classLoader.loadClass("reflect.Answer");
answer.getMethod("main",String[].class).invoke(null, (Object) new String[0]);
           

这次我们使用新的CustomClassLoader以后,小明的代码于是就不出所料的AC了。

3.3 热加载示例

我们在HotLoad类下试图使用热加载的方式调用HelloWorld的main方法:

public static void main(String[] args) throws Exception {
    reLoad();
    while (true) {
        ClassLoader classLoader = new CustomClassLoader(new File("src/reflect/HelloWorld.class"),"reflect.HelloWorld");
        Thread.sleep(2000);
        Class<?> clazz = classLoader.loadClass("reflect.HelloWorld");
        clazz.getMethod("main",String[].class).invoke(null, (Object) new String[0]);
    }

}

public static void reLoad() {
  ToolProvider.getSystemJavaCompiler().run(null,null,null,"src/reflect/HelloWorld.java");
}
           

程序每隔两秒调用HelloWorld.main()一次。每当我们调用一次reLoad()时我们就能重新编译HelloWorld以更新该类。我们现在启动另一个程序来更新HelloWorld的class:

package reflect;

public class HotLoaderController {

    public static void main(String[] args) throws Exception {
        HotLoad.reLoad();
    }

}
           

我们先修改HelloWorld.java,使其print “Hello world again!”。的代码后运行HotLoaderController,神奇的事发生了:

java反射与热加载(全)

我们没有关闭HotLoad程序,而它调用的类却在它运行时自动更新了,这就是热加载。热加载也是一个典型破坏双亲委派的例子,从而实现用户对程序动态性的追求。

参考

https://zhuanlan.zhihu.com/p/66853751

https://blog.csdn.net/codeyanbao/article/details/82875064

https://blog.csdn.net/zhaocuit/article/details/93038538

https://blog.csdn.net/liyongshun82/article/details/52872557

https://www.jianshu.com/p/166c5360a40b

https://segmentfault.com/a/1190000019768368

本文有些内容参考了以上老哥的文章,在此深表谢意。个人强推第一篇链接里文章,讲的很详细。