More
More
文章目录
  1. delegation model
  2. loadClass –> findClass
  3. defineClass
  4. 说点什么
  5. 应用

自定义类加载器

这周在看《深入理解Java虚拟机 JVM高级特性与最佳实践(高清完整版)》,就地取材写写第7章中提到的类加载器。以下源码截自java8。

delegation model

1
2
3
4
5
6
7
8
* <p> The <tt>ClassLoader</tt> class uses a delegation model to search for
* classes and resources. Each instance of <tt>ClassLoader</tt> has an
* associated parent class loader. When requested to find a class or
* resource, a <tt>ClassLoader</tt> instance will delegate the search for the
* class or resource to its parent class loader before attempting to find the
* class or resource itself. The virtual machine's built-in class loader,
* called the "bootstrap class loader", does not itself have a parent but may
* serve as the parent of a <tt>ClassLoader</tt> instance.

截取自源码开篇注释。“delegation model”大部分文章译为“双亲委派模型”(个人感觉不是很贴切,“双”字很容易产生误解),阐述了一种类加载顺序关系。请求查找类或资源时,ClassLoader实例会先交给父级类加载器处理(组合实现,非继承),依次类推直到”bootstrap class loader”,父级无法处理(在其范围内找不到对应类/资源)了再由自己加载。据说这样可以避免同名类引发的安全隐患。类加载顺序如下图。

image

loadClass –> findClass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/**
* Loads the class with the specified <a href="#name">binary name</a>.
* This method searches for classes in the same manner as the {@link
* #loadClass(String, boolean)} method. It is invoked by the Java virtual
* machine to resolve class references. Invoking this method is equivalent
* to invoking {@link #loadClass(String, boolean) <tt>loadClass(name,
* false)</tt>}.
*
* @param name
* The <a href="#name">binary name</a> of the class
*
* @return The resulting <tt>Class</tt> object
*
* @throws ClassNotFoundException
* If the class was not found
*/
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
/**
* Loads the class with the specified <a href="#name">binary name</a>. The
* default implementation of this method searches for classes in the
* following order:
*
* <ol>
*
* <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
* has already been loaded. </p></li>
*
* <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
* on the parent class loader. If the parent is <tt>null</tt> the class
* loader built-in to the virtual machine is used, instead. </p></li>
*
* <li><p> Invoke the {@link #findClass(String)} method to find the
* class. </p></li>
*
* </ol>
*
* <p> If the class was found using the above steps, and the
* <tt>resolve</tt> flag is true, this method will then invoke the {@link
* #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
*
* <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
* #findClass(String)}, rather than this method. </p>
*
* <p> Unless overridden, this method synchronizes on the result of
* {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
* during the entire class loading process.
*
* @param name
* The <a href="#name">binary name</a> of the class
*
* @param resolve
* If <tt>true</tt> then resolve the class
*
* @return The resulting <tt>Class</tt> object
*
* @throws ClassNotFoundException
* If the class could not be found
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// VM未加载返回null;已加载返回类对象
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;
}
}
/**
* Finds the class with the specified <a href="#name">binary name</a>.
* This method should be overridden by class loader implementations that
* follow the delegation model for loading classes, and will be invoked by
* the {@link #loadClass <tt>loadClass</tt>} method after checking the
* parent class loader for the requested class. The default implementation
* throws a <tt>ClassNotFoundException</tt>.
*
* @param name
* The <a href="#name">binary name</a> of the class
*
* @return The resulting <tt>Class</tt> object
*
* @throws ClassNotFoundException
* If the class could not be found
*
* @since 1.2
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
// 由自定义类加载器重载
}

体现了以上所述的类加载逻辑。findClass为自定义类加载器提供了入口。

defineClass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
* Converts an array of bytes into an instance of class <tt>Class</tt>,
* with an optional <tt>ProtectionDomain</tt>. If the domain is
* <tt>null</tt>, then a default domain will be assigned to the class as
* specified in the documentation for {@link #defineClass(String, byte[],
* int, int)}. Before the class can be used it must be resolved.
*
* <p> The first class defined in a package determines the exact set of
* certificates that all subsequent classes defined in that package must
* contain. The set of certificates for a class is obtained from the
* {@link java.security.CodeSource <tt>CodeSource</tt>} within the
* <tt>ProtectionDomain</tt> of the class. Any classes added to that
* package must contain the same set of certificates or a
* <tt>SecurityException</tt> will be thrown. Note that if
* <tt>name</tt> is <tt>null</tt>, this check is not performed.
* You should always pass in the <a href="#name">binary name</a> of the
* class you are defining as well as the bytes. This ensures that the
* class you are defining is indeed the class you think it is.
*
* <p> The specified <tt>name</tt> cannot begin with "<tt>java.</tt>", since
* all classes in the "<tt>java.*</tt> packages can only be defined by the
* bootstrap class loader. If <tt>name</tt> is not <tt>null</tt>, it
* must be equal to the <a href="#name">binary name</a> of the class
* specified by the byte array "<tt>b</tt>", otherwise a {@link
* NoClassDefFoundError <tt>NoClassDefFoundError</tt>} will be thrown. </p>
*
* @param name
* The expected <a href="#name">binary name</a> of the class, or
* <tt>null</tt> if not known
*
* @param b
* The bytes that make up the class data. The bytes in positions
* <tt>off</tt> through <tt>off+len-1</tt> should have the format
* of a valid class file as defined by
* <cite>The Java&trade; Virtual Machine Specification</cite>.
*
* @param off
* The start offset in <tt>b</tt> of the class data
*
* @param len
* The length of the class data
*
* @param protectionDomain
* The ProtectionDomain of the class
*
* @return The <tt>Class</tt> object created from the data,
* and optional <tt>ProtectionDomain</tt>.
*
* @throws ClassFormatError
* If the data did not contain a valid class
*
* @throws NoClassDefFoundError
* If <tt>name</tt> is not equal to the <a href="#name">binary
* name</a> of the class specified by <tt>b</tt>
*
* @throws IndexOutOfBoundsException
* If either <tt>off</tt> or <tt>len</tt> is negative, or if
* <tt>off+len</tt> is greater than <tt>b.length</tt>.
*
* @throws SecurityException
* If an attempt is made to add this class to a package that
* contains classes that were signed by a different set of
* certificates than this class, or if <tt>name</tt> begins with
* "<tt>java.</tt>".
*/
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}

defineClass:接收以字节数组表示的类字节码,并把它转换成 Class 实例,该方法转换一个类的同时,会先要求装载该类的父类以及实现的接口类。重写findClass将使用到。

1
2
3
4
5
6
7
8
9
10
public class Test {
private static String link = "rebey.cn";
static {
System.out.println("welcome to: "+link);
}
public void print() {
System.out.println(this.getClass().getClassLoader());
}
}

将这段java代码编译成.class文件(可通过javac指令),放在 了E:\201706下。同时在我的测试项目下也有一个/201705/src/classLoader/Test.java,代码相同。区别就是一个有包名一个没有包名。如果class文件中源码包含package信息,届时可能会抛出java.lang.NoClassDefFoundError (wrong name)异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package classLoader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
public class CustomClassLoader extends ClassLoader{
private String basedir; // 需要该类加载器直接加载的类文件的基目录
public CustomClassLoader(String basedir) {
super(null);
this.basedir = basedir;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> c = findLoadedClass(name);
if (c == null) {
byte[] bytes = loadClassData(name);
if (bytes == null) {
throw new ClassNotFoundException(name);
}
c = defineClass(name, bytes, 0, bytes.length);
}
return c;
}
// 摘自网络
public byte[] loadClassData(String name) {
try {
name = name.replace(".", "//");
FileInputStream is = new FileInputStream(new File(basedir + name + ".class"));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b = 0;
while ((b = is.read()) != -1) {
baos.write(b);
}
is.close();
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

编写自定义的类加载器,继承ClassLoader,重写了findClass方法,通过defineClass将读取的byte[]转为Class。然后通过以下main函数调用测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package classLoader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Loader {
public static void main(String[] arg) throws NoSuchMethodException, ClassNotFoundException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException{
// 走自定义加载器
CustomClassLoader ccl = new CustomClassLoader("E://201706//");
Class<?> clazz = ccl.findClass("Test");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("print", null);
method.invoke(obj, null);
System.out.println("--------------我是分割线-------------------");
// 走委派模式
// 隐式类加载
Test t1 = new Test();
t1.print();
System.out.println("--------------我是分割线-------------------");
// 显式类加载
Class<?> t2 = Class.forName("classLoader.Test");
Object obj2 = t2.newInstance();
Method method2 = t2.getDeclaredMethod("print", null);
method2.invoke(obj2, null);
System.out.println("--------------我是分割线-------------------");
Class<Test> t3 = Test.class;
Object obj3 = t3.newInstance();
Method method3 = t3.getDeclaredMethod("print", null);
method3.invoke(obj3, null);
}
}
输出结果:
welcome to: rebey.cn
classLoader.CustomClassLoader@6d06d69c
--------------我是分割线-------------------
welcome to: rebey.cn
sun.misc.Launcher$AppClassLoader@73d16e93
--------------我是分割线-------------------
sun.misc.Launcher$AppClassLoader@73d16e93
--------------我是分割线-------------------
sun.misc.Launcher$AppClassLoader@73d16e93

静态代码块随着类加载而执行,而且只会执行一次,所以这里t2、t3加载完成是并没有再输出。

说点什么

ClassLoader线程安全;

同个类加载器加载的.class类实例才相等;

Class.forName(xxx.xx.xx) 返回的是一个类, .newInstance() 后才创建实例对象 ;

Java.lang.Class对象是单实例的;

执行顺序:静态代码块 > 构造代码块 > 构造函数

1、父类静态变量和静态代码块(先声明的先执行);

2、子类静态变量和静态代码块(先声明的先执行);

3、父类的变量和代码块(先声明的先执行);

4、父类的构造函数;

5、子类的变量和代码块(先声明的先执行);

6、子类的构造函数。

应用

通过自定义加载类,我们可以:

①加载指定路径的class,甚至是来自网络(自定义类加载器:从网上加载class到内存、实例化调用其中的方法)、DB(自定义的类装载器-从DB装载class(附上对类装载器的分析));

②给代码加密;(如何有效防止Java程序源码被人偷窥?

③装逼(- -);


更多有意思的内容,欢迎访问笔者小站: rebey.cn

打赏
手机扫一扫,支持CHE~