项目地址
jvm_01
前面提到了双亲委派原则,是为了保证类的唯一,那么我们怎么去破坏这个机制呢?
思路:直接绕开loadClass方法
那么我们要怎么去绕开loadClass方法呢?
有两种办法:
- 前面我们提到了自定义加载器,所以我们可以自定义加载器
- Spi机制绕开loadClass方法,使用当前线程设定关联类加载器
我们先来了解一下Spi机制。
Spi机制
Spi机制加载第三方扩展的jar包类初始化,对我们接口的类实现初始化。
那么有哪些jar使用过Spi机制呢?我们很熟悉的mysql连接池就使用了,我们先看看这个jar包的结构。
我们可以看到这里又是一个奇怪的名字。。。没错,我们今天看的就是他。
为什么要这么命名,都后面就揭晓了。先看看这个文件
只有这一个信息,这个信息如果写代码写多了,那么一定会熟悉,这就是一个对象的完整路径。我们进去看看。
这时候我们就知道了,他是一个实现的了java.sql.Driver(这个名字也就是service文件夹下的文件名)接口。
好了,现在我们来看看怎么使用Spi机制。
现在我们可以看到,代码已经完成。并且创建了文件。
Main.java
package com.lxq;
import java.util.ServiceLoader;
/**
* @author 龙小虬
* @date 2021/4/12 11:33
*/
public class Main {
public static void main(String[] args) {
ServiceLoader<SpiService> load = ServiceLoader.load(SpiService.class);
load.forEach((k)->{
System.out.println(k);
});
}
}
resources/META-INF/services/com.lxq.SpiService
com.lxq.impl.SpiService01
com.lxq.impl.SpiService02
运行结果:
现在可以发现程序运行成功了。那他到底是怎么实现的呢?
查看ServiceLoader的load()方法
ClassLoader cl = Thread.currentThread().getContextClassLoader();
获取当前线程的ClassLoad()ServiceLoader.load(service, cl);
传参,并执行重载的方法。
new 出新对象。
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
Objects.requireNonNull(svc, "Service interface cannot be null");
判断service是否为空loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
如果当前线程的加载器为空,那么就使用系统加载器。对于系统加载器具体是是什么,后面我们再来看。
再调用reload();
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
providers.clear();
对providers集合进行清空lookupIterator = new LazyIterator(service, loader);
对对象进行初始化。
我们在看到hasNextService()
方法
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
String fullName = PREFIX + service.getName();
获取service变量的名字,也就是我们前面传入的SpiService的路径+包名。
通过debug也可以看到是这样的。这个字符串熟悉吗?就是我们在resources文件下的目录文件吧?这也就是为什么文件名要这样命名的原因。if (loader == null)
如果当前线程的类加载器是null也就是启动类加载器。那么就使用系统加载器,这个后面会提到。如果不为null,那么获取目录,然后就读取文件结束了。
可能到这里还有很多人不理解为什么使用当前线程的加载器呢?
我们看看,当前加载器是什么。
可以看到当前线程的加载器是应用类加载器。如果我们需要破坏双亲委派原则,是不是就要打乱他的读取顺序?那么就很好理解了,只要我们不从启动类加载器开始读取,就开始破坏了。
也可以这样,假如我们更改目前线程的加载器
package com.lxq;
import java.util.ServiceLoader;
/**
* @author 龙小虬
* @date 2021/4/12 11:33
*/
public class Main {
public static void main(String[] args) {
Thread.currentThread().setContextClassLoader(Thread.currentThread().getContextClassLoader().getParent());
System.out.println(Thread.currentThread().getContextClassLoader());
ServiceLoader<SpiService> load = ServiceLoader.load(SpiService.class);
load.forEach((k)->{
System.out.println(k);
});
}
}
运行结果:
这里可以看到,Spi机制执行的没有将两个实现类加载进去。因为我们使用的是应用类加载器的父类加载器(扩展类加载器)
那么,如果改为启动类加载器呢?
诶?竟然加载出来了???
前面提到了,如果当前加载器为null,那么会使用系统加载器,看看系统加载器吧。
initSystemClassLoader();
初始化系统加载器
我们可以看到scl
变量是从这两个地方获取到的。先看到sun.misc.Launcher.getLauncher();
,这个Launcher应该很熟悉了,第一篇文章提到过,
到这里就可以看到了,原来我们的系统加载器就是应用类加载器。这一切就已经说明了,当我们的当前线程为启动类加载器的时候,会自动改变成使用应用类加载器。
目前使用Spi机制破坏双亲委派原则,就结束了。那么我们来自定义实现吧。
自定义加载器
应用场景:
需要自己自定义class文件夹目录
- 网络上的class文件
- 本地硬盘
继承ClassLoader,并且覆写findClass()方法。
package com.lxq;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
/**
* @author 龙小虬
* @date 2021/4/12 12:21
*/
public class MyLoadClass extends ClassLoader{
private String fileName;
public MyLoadClass(String fileName){
this.fileName = fileName;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = getClassFileBytes(new File(fileName));
return defineClass(name,data,0,data.length);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
//此方法不需要理解
private byte[] getClassFileBytes(File file) throws Exception {
FileInputStream fileInputStream = new FileInputStream(file);
FileChannel fileChannel = fileInputStream.getChannel();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
WritableByteChannel writableByteChannel = Channels.newChannel(byteArrayOutputStream);
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
while (true){
int i = fileChannel.read(byteBuffer);
if(i==0 || i==-1){
break;
}
byteBuffer.flip();
writableByteChannel.write(byteBuffer);
byteBuffer.clear();
}
fileInputStream.close();
return byteArrayOutputStream.toByteArray();
}
}
运行代码
package com.lxq;
import java.util.ServiceLoader;
/**
* @author 龙小虬
* @date 2021/4/12 11:33
*/
public class Main02 {
public static void main(String[] args) throws ClassNotFoundException {
MyLoadClass myLoadClass = new MyLoadClass("D:\\SpiService03.class");
Class<?> aClass = myLoadClass.findClass("com.lxq.impl.SpiService03");
System.out.println(aClass.getClassLoader());
}
}
运行结果:
现在我们的自定义加载器就可用了。这样看起来还没怎么发现他的作用在哪。那我们再来个热部署吧。在项目部署到服务器上的时候,我们都需要进行测试,那么在测试环境的情况下我们就需要用到热部署,这样会比重启快很多。
手写热部署
我们知道热部署主要用于测试环境,因为测试环境只更改部分代码,那么热部署具体是怎样的呢?首先他肯定是时时刻刻注意着文件是否发生改变。所以我们需要创建一个线程来对文件更改进行监控。
那么,怎么知道文件被更改了呢?
其实记录文件的最后修改时间就行了,一旦最后修改时间发生变化,那么文件就被更改了。就需要我们重新初始化对象。
所以,我们首先在原加载器中,添加文件的最后修改时间。
public long getLastModified(){
return file.lastModified();
}
再获取多个编译后的class文件,注意文件内容需要修改。
public class SpiService03 implements SpiService {
@Override
public String output() {
return "热部署进行中";
}
}
public class SpiService03 implements SpiService {
@Override
public String output() {
return "热部署进行1";
}
}
public class SpiService03 implements SpiService {
@Override
public String output() {
return "热部署进行2";
}
}
最后创建线程。
new Thread(()->{
while (true){
try{
Thread.sleep(2000);
}catch (Exception e){
}
end = myLoadClass1.getLastModified();
if(end!=start) {
MyLoadClass myLoadClass2 = new MyLoadClass("D:\\SpiService03.class");
try {
Class<?> aClass2 = myLoadClass2.findClass("com.lxq.impl.SpiService03");
SpiService spiService2 = getSpiService(aClass2);
System.out.println(spiService2.output());
start = end;
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}).start();
}
最终代码:
package com.lxq;
import java.util.ServiceLoader;
/**
* @author 龙小虬
* @date 2021/4/12 11:33
*/
public class Main02 {
static long start;
static long end;
public static void main(String[] args) throws ClassNotFoundException {
MyLoadClass myLoadClass1 = new MyLoadClass("D:\\SpiService03.class");
Class<?> aClass1 = myLoadClass1.findClass("com.lxq.impl.SpiService03");
SpiService spiService1 = getSpiService(aClass1);
System.out.println(spiService1.output());
start = myLoadClass1.getLastModified();
new Thread(()->{
while (true){
try{
Thread.sleep(2000);
}catch (Exception e){
}
end = myLoadClass1.getLastModified();
if(end!=start) {
MyLoadClass myLoadClass2 = new MyLoadClass("D:\\SpiService03.class");
try {
Class<?> aClass2 = myLoadClass2.findClass("com.lxq.impl.SpiService03");
SpiService spiService2 = getSpiService(aClass2);
System.out.println(spiService2.output());
start = end;
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}).start();
}
static SpiService getSpiService(Class<?> aClass){
try {
return (SpiService)aClass.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
}
运行结果: