ClassLoader和类加载机制

01 背景
最近在做项目的过程中,由于系统需要提供一个对外接口,使系统使用者可以以脚本的形式提交自己的代码,每个用户可以在系统规范的约束下编写脚本,由系统去执行用户的代码,实现了热部署。
什么叫热部署呢?简单来说就是把代码当成U盘或者外设一样即插即用,每个用户可以维护自己的解决方案(也就是一段脚本,一个单独的类),在更新修改解决方案的过程中而不需要重新编译启动整个系统。我们采用的方案就是GroovyClassLoader,我主要讲一讲自己对ClassLoader的理解和使用。

02

类加载与类加载器

类加载:

类加载的过程就是将Class文件中描述的各种信息加载到虚拟机中,供程序后期运行和使用的。
类加载的生命周期主要分为五个步骤:

1、加载:

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

2、验证

为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害到自身的安全。包括文件格式验证,元数据验证,字节码验证,符号引用验证。

3、准备

为变量分配内存,设置类变量的初始值。

4、解析

将常量池中的符号应用替代为直接引用。

5、初始化

是类加载生命周期的最后一个过程,执行类中定义的java程序代码

类加载器:

在前面的类加载过程中,大部分动作都是完全由虚拟机主导和控制的。而类加载器使得用户可以在加载的过程中参与进来,结合前面的内容,类加载器就是将“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机外部来实现。将主动权交给程序猿。
类加载器和这个类本身确定了其在java虚拟机中的唯一性,每一个类加载器都有一个独立的类命名空间,也就意味着,如果比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那么这两个类就注定不相同。

1、Bootstrap Class Loader:负责加载JAVA_HOME/lib目录下或-Xbootclasspath指定目录的jar包;

2、Extention Class Loader:加载JAVA_HOME/lib/ext目录下的或-Djava.ext.dirs指定目录下的jar包。

3、System Class Loader:加载classpath或者-Djava.class.path指定目录下的类或jar包。

ClassLoader各司其职,加载在不同路径下的class文件,值得注意的是,类加载采用的是双亲委托的设计模式,即传入一个类限定名,逐层向上到Bootstrap Class Loader中查找,如果找到即返回,若没有找到,则在Extention Class Loader中找,若还没有找到则在System Class Loader下找,即classpath中,如果还没有找到,则调用findClass(name)方法,执行用户自己的类加载逻辑(可能在其他的地方)

ClassLoader中的几个重要的方法:

1、loadClass(String name, boolean resolve):加载类的方法,在jdk1.2以前需要重写该方法实现用户自己的逻辑,1.2以后为了向下兼容,仍然可以重写该方法,但是建议用户将自己的加载逻辑实现在findName(name)中。这样系统先向上寻找能否加载到该类,如果加在不到,将调用用户自定义的findName函数加载对象.

   /**     
 * @param name    类名字      
* @param resolve  是否解析,如果只是想知道该class是否存在可以设置该参数为false     
 * @return 返回一个class泛型     
 * @throws ClassNotFoundException      
*/     
protected Class<?> loadClass(String name, boolean resolve)             
throws ClassNotFoundException {         
/**          
* getClassLoadingLock(name)          
* 为类的加载操作返回一个锁对象。为了向后兼容,这个方法这样实现:如果当前的classloader对象注册了并行能力,         
 * 方法返回一个与指定的名字className相关联的特定对象,否则,直接返回当前的ClassLoader对象。         
 */         
synchronized (getClassLoadingLock(name)) {             
// 首先查看class是否已经被加载过            
 Class<?> c = findLoadedClass(name);             
if (c == null) {                
long t0 = System.nanoTime();                
 try {                    
 //如果父加载器不为空,则委托给父加载器去加载                     
if (parent != null) {                         
c = parent.loadClass(name, false);                     
} else {                        
 /**                         
 *  如果父加载器为空,说明父加载器已经是Bootstrap ClassLoader了,则直接使用根加载器加载,也就是使用虚拟机加                          
*  载器加载                          
*/                        
 c = findBootstrapClassOrNull(name);                    
 }                 
} catch (ClassNotFoundException e) {                    
 // ClassNotFoundException thrown if class not found                     
// from the non-null parent class loader                
 }                
 //如果以上的加载器在自己的路径上面都没有加载到,则调用findClass(name)调用用户自定义的加载器                
 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();                
 }            
 }            
 //根据resolve参数决定是否解析该类             
if (resolve) {                
 resolveClass(c);            
 }            
 return c;        
 }     
}

2、ClassLoader getParent() :可以返回委托的父类加载器。在你自定义加载器找不到相应类的时候,可以调用此方法,不过在ClassLoader的默认实现中,ClassLoader先判断父类加载器是否可以加载,然后再调用用户自定义的findClass方法。

3、 resolveClass():若resolve参数为true的时候,我们需要调用该函数,resolve我们的classLoader。

4、ClassLoader getSystemClassLoader():提供了一个直接访问系统classloader的方法。

03

废话少说上代码!

下面我将以一个例子来阐述如何使用ClassLoader,自定义的ClassLoader将加载被加密的类,而且这个类存储的路径不在ClassPath中,也不可以被Bootstrap Class Loader和Extention Class Loader加载,在实际应用中,可以是网络中传递过来的加密字节流,抑或着是实现脚本的热部署操作。

package com.siyu;

import java.io.*;
public class ClassLoaderTest extends ClassLoader {     
//自定义加载器加载该路径下面的文件     private String directory;      
public ClassLoaderTest(String directory) {         
this.directory = directory;    
 }     
/**     
 * 重写findClass,用户可以做以下的事情      
* 1.可以加载boot、ext、system加载器所加载不了的路径下的文件      
* 2.可以解密加密后的class文件      
*/     
@Override     
protected Class<?> findClass(String name) throws ClassNotFoundException {         
//解密密钥        
byte key = (byte) 1;         
//加密文件的路径         
String fileName = directory + name + ".class";         
File file = new File(fileName);         
byte[] decryptedByte = readFromFile(file);         
//解密为原始的class文件         
for (int i = 0; i < decryptedByte.length; i++) {             
decryptedByte[i] = (byte) (decryptedByte[i] ^ key);         
}         
//defineClass实现了链接阶段的验证等         
return defineClass(null, decryptedByte, 0, decryptedByte.length);     
}       
private byte[] readFromFile(File fileName) {         
try {             
byte[] bytes = null;             
FileInputStream fin = new FileInputStream(fileName);              
int i;             
if ((i = fin.read()) != -1) {                 
//初始化数组大小和文件大小一样                 
bytes = new byte[fin.available()];                 
fin.read(bytes);             
}            
return bytes;         
} catch (FileNotFoundException e) {             
e.printStackTrace();             
return null;        
 } catch (IOException e) {             
e.printStackTrace();            
 return null;        
 }    
 }      
private byte[] encrypt(byte[] bytes) {         
byte key = (byte) 1;         
//依次加密的代码        
 for (int i = 0; i < bytes.length; i++) {             
bytes[i] = (byte) (bytes[i] ^ key); 
//利用异或加密         
}         
return bytes;    
 }      
public void encryptFile(String fileName, String directory) {        
 try {            
 String name = fileName.substring(fileName.lastIndexOf("\\") + 1, fileName.length() - 6);             
//加密文件的路径             
String destFileName = directory + "encryted" + name + ".class";             
//如果加密文件不存在则创建加密文件             
File f = new File(destFileName);            
 if (f == null) {                 
f.createNewFile();             
}             
//加密             
byte[] encryptedByte = encrypt(readFromFile(new File(fileName)));             
FileOutputStream fos = new FileOutputStream(destFileName);             
//把加密后的字节写入到加密文件中             
fos.write(encryptedByte);        
 } catch (FileNotFoundException e) {             
e.printStackTrace();        
 } catch (IOException e) {             
e.printStackTrace();        
 }     
}      
 public static void main(String[] args) {         
//设置加密路径         
ClassLoaderTest classLoaderTest=new ClassLoaderTest("C:\\EncryptedClass\\");        
 //将test.class加密后存储到EncryptedClass目录下         classLoaderTest.encryptFile("C:\\Users\\jasonchu.zsy\\IdeaProjects\\BoKeTest\\out\\production\\BoKeTest\\com\\siyu\\test.class"              
,"C:\\EncryptedClass\\");         
try {             
Class<?> t=classLoaderTest.loadClass("encrytedtest");        
 } catch (ClassNotFoundException e) {             
e.printStackTrace();        
 }    
 }
}

在main函数中先将一个编译好的class文件加密后存储在非classpath路径下,然后用自定义classLoader进行加载,加密为了简单起见,使用的是异或加密,利用的原理是二进制的数经过两次异或操作后得到的值是相同的。路径也使用的绝对路径,大家可以根据需要自行进行修改,有什么问题可以继续交流,谢谢。

原文发布于微信公众号 - 人工智能LeadAI(atleadai)

原文发表时间:2017-10-07

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏IT可乐

Spring详解(三)------DI依赖注入

  上一篇博客我们主要讲解了IOC控制反转,也就是说IOC 让程序员不在关注怎么去创建对象,而是关注与对象创建之后的操作,把对象的创建、初始化、销毁等工作交给s...

1825
来自专栏生信小驿站

R语言基础操作①基础指令

q()——退出R程序 tab——自动补全 ctrl+L——清空console ESC——中断当前计算

862
来自专栏诸葛青云的专栏

C语言被忽视的一些小东西!C语言基础教程之错误处理

C 语言不提供对错误处理的直接支持,但是作为一种系统编程语言,它以返回值的形式允许您访问底层数据。在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或...

330
来自专栏Java进阶之路

由浅入深谈 Java 的类加载机制

1510
来自专栏眯眯眼猫头鹰的小树杈

猫头鹰的深夜翻译:理解java的classloader

Java ClassLoader是java运行系统中一个至关重要但是经常被忽略的组件。它负责在运行时寻找并加载类文件。创建自定义的ClassLoader可以彻底...

1144
来自专栏电光石火

关于PHP字符编码的函数区别

在以前的学习当中,比方说有一次的写采集过程中转换字符的编码的时候老是失败,转换的结果总没有完全输出,后来经过网络查询得知是iconv有一个“-”漏洞,所以我们有...

1768
来自专栏老马说编程

(87) 类加载机制 / 计算机程序的思维逻辑

上节,我们探讨了动态代理,在前几节中,我们多次提到了类加载器ClassLoader,本节就来详细讨论Java中的类加载机制与ClassLoader。 类加载...

1798
来自专栏Java架构师历程

JVM加载class文件的原理

当Java编译器编译好.class文件之后,我们需要使用JVM来运行这个class文件。那么最开始的工作就是要把字节码从磁盘输入到内存中,这个过程我们叫做【加载...

1162
来自专栏python3

python3--模块collections,time,random,sys

有如下值集合[11,22,33,44,55,66,77,88,99,90......],将所有大于66的值保存至字典的第一个key中,小于66的值保存至第二个k...

732
来自专栏随心DevOps

【实战】如何使用 Python 从 Redis 中删除 4000万 KEY

本文主要涉及 Redis 的以下两个操作和其 Python 实现,目录: SCAN 命令 DEL 命令 使用 Python SCAN 使用 Python DEL...

4168

扫码关注云+社区