1 类加载机制
1.1 双亲委派机制
1.2 沙箱安全机制
保护JDK内部的类不会被应用覆盖,java不允许定义以java开头的类。
1.3 缓存机制
● 在类加载器级别,被加载过的类都在类加载器对象内有缓存,再次访问时只需调用findLoadedClass()即可获取到类。
● 该缓存为jvm层面缓存,在java CLassLoader对象中存在一个classed属性,可以查看当前类加载器加载了哪些类。
1.4 全盘委托机制
当一个ClassLoader加载当前类时,当前类的父类以及其依赖的所有类都由这个CLassLoader加载,同时这个类加载器加载当前类的父类以及其依赖的类时,也都符合双亲委派机制。
2 类加载器
类加载器继承体系
3 类加载过程
● 加载:通过类加载器将class文件加载到JVM中
● 验证:验证class文件是否符合JDK规范
● 准备:为静态变量分配内存并赋默认值,其中final修饰的静态变量在改阶段赋初值
● 解析:将符号引用解析为直接引用
● 初始化:静态变量赋初值,执行静态代码块(clinit方法),初始化当前类的父类
● 静态变量赋初值&final静态变量赋初值时机验证代码:
package com;
public class App {
private static App app = new App(5);
private static int a = 10;
private final static int b = 20;
private int c;
private int d;
public App(int e){
this.c = a-e;
this.d = b-e;
}
public static void main(String[] args) {
System.out.println(app.c);
System.out.println(app.d);
}
}
打印后输出:
c=-5:结果分析在静态变量app初始化时,静态变量a还未赋初值
d=15:结果分析在静态变量app初始化时,静态变量b已赋初值
通过字节码分析也可以看出来,
对于变量b的赋初值并未在初始化方法clinit方法中完成
对于变量a的赋初值是在创建完变量app后完成的
4 加载类的三种方式
● 使用类:创建类的对象,调用类的静态方法,访问类的静态属性
● 类加载器的loadClass()方法,该方法只会将类加载进内存,不会执行后续初始化逻辑
● Class.forName()方法,该方法会执行类的初始化方法
● 类加载器loadClass()&Class.forName()区别的验证代码
public class Person {
public static Integer age =10;
static {
System.out.println(11111);
}
}
class PersonTest{
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
ClassLoader classLoader = Test.class.getClassLoader();
Class<?> aClass = classLoader.loadClass("com.load_class.Person");
// aClass.newInstance();
System.out.println("=====================");
Class.forName("com.load_class.Person");
}
}
5 通过类加载器引入外部jar
● 使用URLClassLoader
● 外部jar可以通过本地文件,远程服务器,classpath等方式指定
● 代码示例
public static void main(String[] args) throws Exception {
URL url = new URL("file:D:\\workplat\\test\\target\\test-1.0-SNAPSHOT.jar");
URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
Object o = classLoader.loadClass("com.Test").newInstance();
o.getClass().getMethod("sayHello").invoke(o);
}
6 通过自定义类加载器实现代码加密
● 准备测试类HelloTest
public class HelloTest {
public void sayHello(){
System.out.println("hello test11");
}
}
● 自定义类加密工具类
public class ClassFileTranformUtils {
public static final byte[] MAGIC_HEADER = new byte[]{(byte) 0xfe, (byte) 0x62, (byte) 0x69, (byte) 0x6e};
public static Boolean tranformClassFile(String classFile){
File file = new File(classFile);
File outFile = new File(file.getParent(),file.getName().replace(".class",".gz"));
try(
FileInputStream is = new FileInputStream(classFile);
FileOutputStream os = new FileOutputStream(outFile)
) {
os.write(MAGIC_HEADER);
byte[] buf = new byte[1024];
int len = 0;
while ((len=is.read(buf))!=-1){
os.write(buf,0,len);
}
}catch (Exception e){
return false;
}
return true;
}
public static void main(String[] args) {
ClassFileTranformUtils.tranformClassFile("D:\\workplat\\test\\test1\\target\\classes\\com\\test\\HelloTest.class");
}
}
● 自定义类加载器
public class CustomClasloader extends ClassLoader {
private String classPath;
public CustomClasloader(String classPath) {
this.classPath = classPath;
}
@Override
public Class findClass(String name) throws ClassNotFoundException {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String name) throws ClassNotFoundException {
String classFullName = this.classPath + name.replace(".", File.separator) + ".gz";
File classFile = new File(classFullName);
if (!classFile.exists()) {
throw new ClassNotFoundException(name);
}
try (
FileInputStream is = new FileInputStream(classFile);
ByteArrayOutputStream os = new ByteArrayOutputStream();
) {
is.skip(4);
byte[] buf = new byte[1024];
int len = 0;
while ((len=is.read(buf))!=-1){
os.write(buf,0,len);
}
return os.toByteArray();
} catch (Exception e) {
}
return null;
}
}
● 编写类加载器测试类Main
public class Main {
public static void main(String[] args) throws Exception {
CustomClasloader customClasloader = new CustomClasloader(
"D:\\workplat\\test\\test1\\target\\classes\\",
Main.class.getClassLoader().getParent());
Object o = customClasloader.loadClass("com.test.HelloTest").newInstance();
o.getClass().getMethod("sayHello").invoke(o);
}
}
● 注意,自定义类加载器的父类加载器默认为AppclassLoader,如果测试类HelloTest与测试类Main在同一工程内,由于父类委托机制,此时运行的当前工程内的HelloTest类,而不是加密处理后的class类。
● 思考
○ 如何实现自动类加密打包?可通过maven插件
○ 如何读取jar包内的class文件并解密?参考URLClassLoader实现
7 自定义类加载器实现热加载
● 由于被加载过的类都会有缓存,因此备份加载过的类修改后重新部署上去不会实时生效,需要重新启动服务让类重新加载才会生效
● 自定义类加载器可以实现不重启服务,让修改的类实时生效
● 实现方式一:简单粗暴,每次使用类时都有新的类加载器对象去加载,这样缓存不会生效
● 实现方式二:后台增加定时任务探测对应文件是否修改过,如果修改过则替换类加载器为新的类加载器对象
8 自定义类加载器打破双亲委派
● 自定义类加载器实现loadClass()方法,读取文件方式
public class CustomClasloader extends ClassLoader {
private String classPath;
public CustomClasloader(String classPath, ClassLoader parent) {
super(parent);
this.classPath = classPath;
}
@Override
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) {
c = findClass(name);
if (c == null) {
c = super.loadClass(name, resolve);
}
}
return c;
}
}
@Override
public Class findClass(String name) throws ClassNotFoundException {
byte[] b = loadClassData(name);
if (b == null) {
return null;
}
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String name) throws ClassNotFoundException {
String classFullName = this.classPath + name.replace(".", File.separator) + ".gz";
File classFile = new File(classFullName);
if (!classFile.exists()) {
return null;
}
try (
FileInputStream is = new FileInputStream(classFile);
ByteArrayOutputStream os = new ByteArrayOutputStream();
) {
is.skip(4);
byte[] buf = new byte[1024];
int len = 0;
while ((len = is.read(buf)) != -1) {
os.write(buf, 0, len);
}
return os.toByteArray();
} catch (Exception e) {
}
return null;
}
}
9 ServiceLoader+类加载器实现外部jar方法调用
● jdk提供的SPI机制
● 在项目test1中添加接口HelloService
public interface HelloService {
public void sysHello(String name);
}
● 在项目test2添加test1依赖,并添加HelloService实现类
import com.service.HelloService;
/**
* @author gzl
* @Date 2024/12/28 11:01
*/
public class HelloServiceImpl implements HelloService {
@Override
public void sysHello(String name) {
System.out.println("Hello test2 "+ name);
}
}
● 在项目test2中添加SPI配置文件META-INF/services/com.service.HelloService
com.service.HelloServiceImpl
● 打包test2项目生成jar包
● 在项目test1中添加测试类Main
public class Main {
public static void main(String[] args) throws Exception {
URL url = new URL("file:D:\\workplat\\test\\test2\\target\\test2-1.0-SNAPSHOT.jar");
URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
ServiceLoader<HelloService> load = ServiceLoader.load(HelloService.class, classLoader);
for (HelloService helloService : load) {
helloService.sysHello("jack");
}
}
}
● 如果使用自定义类加载器加载外部jar时,会使SPI失效,因为外部我们只通过自定义加载器去加载了class类,其内部的resources文件没有加载进来,解决的方式有以下两种
○ 使用java -cp {libFile}将外部jar包添加新classpath路径
○ 参照URLClassLoader将resources下的配置文件加载进来
10 SpringFactoriesLoader+类加载器实现外部jar方法调用
● SpringFactoriesLoader是spring提供的spi机制
● 在项目test1中添加接口HelloService
public interface HelloService {
public void sysHello(String name);
}
● 在项目test2添加test1依赖,并添加HelloService实现类
import com.service.HelloService;
/**
* @author gzl
* @Date 2024/12/28 11:01
*/
public class HelloServiceImpl implements HelloService {
@Override
public void sysHello(String name) {
System.out.println("Hello test2 "+ name);
}
}
● 在项目test2中添加SPI配置文件META-INF/spring.factories
com.service.HelloService=com.service.impl.HelloServiceImpl
● 打包test2项目生成jar包
● 在项目test1自定义类加载器实现外部jar读取
public class CustomClasloader extends URLClassLoader {
private String classPath;
public CustomClasloader(String classPath) throws MalformedURLException {
super(new URL[]{new URL("file:"+classPath)});
this.classPath=classPath;
}
//
// @Override
// protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// if (name.startsWith("java")){
// return super.loadClass(name, resolve);
// }
// synchronized (getClassLoadingLock(name)) {
// // First, check if the class has already been loaded
// Class<?> c = findLoadedClass(name);
// if (c == null) {
// c = findClass(name);
// if (c == null) {
// c = super.loadClass(name, resolve);
//
// }
// }
// return c;
// }
// }
@Override
public Class findClass(String name) throws ClassNotFoundException {
byte[] b = loadClassData(name);
if (b == null) {
return null;
}
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String name) throws ClassNotFoundException {
String classFullName = name.replace(".", "/") + ".class";
try (
InputStream resource = getResourceAsStream(classFullName);
ByteArrayOutputStream os = new ByteArrayOutputStream();
) {
if (resource==null){
return null;
}
// is.skip(4);
byte[] buf = new byte[1024];
int len = 0;
while ((len = resource.read(buf)) != -1) {
os.write(buf, 0, len);
}
return os.toByteArray();
} catch (Exception e) {
}
return null;
}
}
● 在项目1添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.7.18</version>
</dependency>
● 在项目test1中添加测试类Main
public class Main {
public static void main(String[] args) throws Exception {
CustomClasloader classLoader = new CustomClasloader("D:\\workplat\\test\\test2\\target\\test2-1.0-SNAPSHOT.jar");
List<HelloService> helloServices = SpringFactoriesLoader.loadFactories(HelloService.class, classLoader);
for (HelloService helloService : helloServices) {
helloService.sysHello("jack");
}
}
}
注意:如果修改loadClass()改变双亲委派机制的默认行为,先执行findClass()方法,未找到类时再次父类加载器中寻找,此时自动义类加载加载HelloServiceImpl类时,会回调找父类HelloService,找父类的逻辑因为优先进findClass()方法,导致HelloService类被自定义类加载器再加载一遍,而不是使用AppClassLoader中的HelloService类,从而导致报错
Caused by: java.lang.IllegalArgumentException: Class [com.service.impl.HelloServiceImpl] is not assignable to factory type [com.service.HelloService]
at org.springframework.core.io.support.SpringFactoriesLoader.instantiateFactory(SpringFactoriesLoader.java:177)
... 2 more
想要解决以上问题有两种方式:
● 方式一:只需要让回调找父类时父类不被自定义类加载即可,即重写自定义类加载器getResource()方法
@Override
public URL getResource(String name) {
return findResource(name);
}
● 方式二:在测试类Main上再封装一层,让自定义类加载器加载测试类Main,这样保证了HelloServiceImpl和HelloService同时被自定义类加载器加载。
11 实现同名类多版本共存
实现思路:创建新的类加载器对象去加载不同路径的类,即可以实现同名类多版本共存