使用resource中的jar包资源作为UrlClassloader(二)
对于jar中jar,症结的关键在于,这个jar是在内存中的,更具体的,是在jvm的resource中,无法直接使用URLClassLoader
有两种类型方式、4种方法解决:
1 解压式-tomcat
2 3 4 jar中jar-springboot
核心的区别在于,一者从还是磁盘加载jar,一者从内存字节数组加载jar不产生任何临时文件
1 很简单,取得资源,释放写入到当前磁盘目录,返回URL,使用URLClassLoader加载
缺点:写入磁盘耗时浪费性能;内存加密资源jar包会暴露字节码加密(三)jar包解密后删除
package lc3;
import java.io.*;
import java.net.URL;
import java.net.URLClassLoader;
/**
* https://www.cnblogs.com/silyvin/articles/12163982.html
* https://www.cnblogs.com/silyvin/p/12164432.html
* https://www.cnblogs.com/silyvin/articles/12178528.html 1
* Created by joyce on 2020/1/7.
*/
/**
* 方案一
* 取得jar resource输入流
* 写到磁盘
* UrlClassLoader加载
*/
public class ResourceClassloader1 {
public static void main(String []f) throws Exception {
/**
* 这两行只能在ide下工作
*/
// URL url1 = ResourceClassloader.class.getResource("jars/MySub-1.0.0.jar");
// URL url2 = ResourceClassloader.class.getResource("jars/MySubBrother-1.0.0.jar");
InputStream in1 = ResourceClassloader1.class.getResourceAsStream("jars/MySub-1.0.0.jar");
InputStream in2 = ResourceClassloader1.class.getResourceAsStream("jars/MySubBrother-1.0.0.jar");
ResourceLoader loader = new ResourceLoader(new URL[]{copyJar(in1, "MySub.jar"), copyJar(in2, "MySubBro.jar")});
test(loader);
test(ResourceLoader.class.getClassLoader());
}
private static void test(ClassLoader classLoader) {
try {
Class c1 = classLoader.loadClass("lc3.ResourceF");
c1.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
try {
Class c1 = classLoader.loadClass("lc3.ResourceG");
// c1.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
try {
Class c1 = classLoader.loadClass("lc3.ResourceH");
c1.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
private static class ResourceLoader extends URLClassLoader {
public ResourceLoader(URL[] urls) {
super(urls);
}
}
private static URL copyJar(InputStream inputStream, String name) throws IOException {
File exist = new File(name);
if(exist.exists())
return new URL("file:" + name);
int len = -1;
byte [] bytes = new byte[1024];
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while ((len = inputStream.read(bytes)) != -1) {
byteArrayOutputStream.write(bytes, 0, len);
}
inputStream.close();
File file = new File(name);
OutputStream outputStream = new FileOutputStream(file);
outputStream.write(byteArrayOutputStream.toByteArray());
outputStream.close();
URL url = new URL("file:" + name);
return url;
}
}
2 遍历所有JarInputStream jar内存流,保存入<jar entry name, byte[],在findclass中,以资源完整名称作为findclass找到的依据
(注意,这个案例使用ClassLoader,不使用UrlClassLoader)
缺点:对类加载动的较多,很难完全把握,稳定性不行,总有想不到的地方JDBC注册原理与自定义类加载器解决com.cloudera.hive.jdbc41.HS2Driver的加载【重点】中5.1.39的mysql及mysql-bin就是不行,5.1.0-bin就行,莫名
参考:
java – 创建一个ClassLoader以从字节数组加载JAR文件.
我正在寻找一个自定义类加载器,它将从自定义网络加载JAR文件.最后,我必须使用的是JAR文件的字节数组. 内存中,不上磁盘
我无法将字节数组转储到文件系统上并使用URLClassLoader.
我的第一个计划是从流或字节数组创建一个JarFile对象,但它只支持File对象.
我已经写了一些使用JarInputStream的东西:
public class RemoteClassLoader extends ClassLoader {
private final byte[] jarBytes;
public RemoteClassLoader(byte[] jarBytes) {
this.jarBytes = jarBytes;
}
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(name);
if (clazz == null) {
try {
InputStream in = getResourceAsStream(name.replace('.', '/') + ".class");
ByteArrayOutputStream out = new ByteArrayOutputStream();
StreamUtils.writeTo(in, out);
byte[] bytes = out.toByteArray();
clazz = defineClass(name, bytes, 0, bytes.length);
if (resolve) {
resolveClass(clazz);
}
} catch (Exception e) {
clazz = super.loadClass(name, resolve);
}
}
return clazz;
}
@Override
public URL getResource(String name) {
return null;
}
@Override
public InputStream getResourceAsStream(String name) {
try (JarInputStream jis = new JarInputStream(new ByteArrayInputStream(jarBytes))) {
JarEntry entry;
while ((entry = jis.getNextJarEntry()) != null) {
if (entry.getName().equals(name)) {
return jis;
}
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
* 这里的getResourceAsStream,只会找本加载器的,会忽略父加载器的(下面是官方的),而且不建议改写loadClass,而是改写findClass
public URL getResource(String name) {
URL url;
if (parent != null) {
url = parent.getResource(name);
} else {
url = getBootstrapResource(name);
}
if (url == null) {
url = findResource(name);
}
return url;
}
URLClassLoader的getResource是会先考虑父加载器的,所以说本方法对类加载器改动加大,很难把握
好了,我们自己写一个,增加了缓存功能:
package lc3;
import java.io.*;
import java.util.HashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
/**
* https://www.cnblogs.com/silyvin/articles/12178528.html 2
* Created by joyce on 2020/1/7.
*/
/**
* 方案二
* 遍历所有JarInputStream jar内存流
* 保存<jarentry_name : byte[]>
* 以资源完整名称作为findclass找到的依据
*/
public class ResourceClassloader2 {
public static void main(String []f) throws Exception {
InputStream url1 = ResourceClassloader1.class.getResourceAsStream("jars/MySub-1.0.0.jar");
InputStream url2 = ResourceClassloader1.class.getResourceAsStream("jars/MySubBrother-1.0.0.jar");
ResourceLoader loader = new ResourceLoader(new JarInputStream[]{new JarInputStream(url1), new JarInputStream(url2)});
test(loader);
test(ResourceLoader.class.getClassLoader());
/**
* 显示findclass只调用一次【注意】
*/
test(loader);
}
private static void test(ClassLoader classLoader) {
try {
Class c1 = classLoader.loadClass("lc3.ResourceF");
c1.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
try {
Class c1 = classLoader.loadClass("lc3.ResourceG");
// c1.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
try {
Class c1 = classLoader.loadClass("lc3.ResourceH");
c1.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
private static class ResourceLoader extends ClassLoader {
JarInputStream [] list = null;
private HashMap<String, byte[]> classes = new HashMap<>();
public ResourceLoader(JarInputStream [] jarInputStream) {
this.list = jarInputStream;
for(JarInputStream jar : list) {
JarEntry entry;
try {
while ((entry = jar.getNextJarEntry()) != null) {
String name = entry.getName();
ByteArrayOutputStream out = new ByteArrayOutputStream();
int len = -1;
byte [] tmp = new byte[1024];
while ((len = jar.read(tmp)) != -1) {
out.write(tmp, 0, len);
}
byte[] bytes = out.toByteArray();
classes.put(name, bytes);
}
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("total classes - " + classes.size());
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
System.out.println("find " + name);【注意】
try {
InputStream in = getResourceAsStream(name.replace('.', '/') + ".class");
ByteArrayOutputStream out = new ByteArrayOutputStream();
int len = -1;
byte [] tmp = new byte[1024];
while ((len = in.read(tmp)) != -1) {
out.write(tmp, 0, len);
}
byte[] bytes = out.toByteArray();
/**
* 三个类都是475长度
*/
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
@Override
public InputStream getResourceAsStream(String name) {
System.out.println("getResourceAsStream - " + name);
if(classes.containsKey(name)) {
return new ByteArrayInputStream(classes.get(name));
}
System.out.println("getResourceAsStream - error - " + name);
return super.getResourceAsStream(name);
}
}
}
3 既然UrlClassLoader只认URL,那么我们由内存jar流伪造一个
缺点:可能遇到与tomcat url factory冲突,factory already defined
参考:
如何将bytearray转换为Jar 极其牛逼
这篇文章要从字节数组加载jar而不写入文件;与上一篇原理一样,而且改进了对jarinputstream做了缓存;但不断得到随机错误,故作者用了另一种方式,通过反射调用SystemClassLoader.addURL加载了从内存伪造的URL
我们写一个:
package lc3;
import java.io.*;
import java.net.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* https://www.cnblogs.com/silyvin/articles/12178528.html 3
* Created by joyce on 2020/1/7.
*/
/**
* 方案三,从内存伪造一个URL供给URLClassLoader
*/
public class ResourceClassloader3 {
public static void main(String []f) throws Exception {
List<URL> list = ResourceLoader.init(new String [] {"jars/MySub-1.0.0.jar", "jars/MySubBrother-1.0.0.jar"});
ResourceLoader loader = new ResourceLoader(list.toArray(new URL[list.size()]));
test(loader);
test(ResourceLoader.class.getClassLoader());
}
private static void test(ClassLoader classLoader) {
try {
Class c1 = classLoader.loadClass("lc3.ResourceF");
c1.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
try {
Class c1 = classLoader.loadClass("lc3.ResourceG");
// c1.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
try {
Class c1 = classLoader.loadClass("lc3.ResourceH");
c1.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
private static class ResourceLoader extends URLClassLoader {
public static List<URL> init(String [] resourceJars) throws Exception {
List<java.net.URL> urls = new ArrayList<>();
Map<String, ByteArrayOutputStream> map = new ConcurrentHashMap<>();
java.net.URL.setURLStreamHandlerFactory(new URLStreamHandlerFactory() {
public URLStreamHandler createURLStreamHandler(String urlProtocol) {
System.out.println("Someone asked for protocol: " + urlProtocol);
if ("myjarprotocol".equalsIgnoreCase(urlProtocol)) {
return new URLStreamHandler() {
@Override
protected URLConnection openConnection(URL url) throws IOException {
String key = url.toString().split(":")[1];
return new URLConnection(url) {
public void connect() throws IOException {}
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(map.get(key).toByteArray());
}
};
}
};
}
return null;
}
});
for(String resourceJar : resourceJars) {
InputStream in = ResourceLoader.class.getResourceAsStream(resourceJar);
int len = -1;
byte [] bytes = new byte[1024];
ByteArrayOutputStream jarBytes = new ByteArrayOutputStream();
while ((len = in.read(bytes)) != -1) {
jarBytes.write(bytes, 0, len);
}
map.put(resourceJar, jarBytes);
urls.add(new URL("myjarprotocol:" + resourceJar));
}
return urls;
}
public ResourceLoader(URL[] urls) {
super(urls);
}
}
}
4 与3差不多,无非是用系统类加载器加载内存伪造的url
缺点:除了factory already defined,还有类的隔离性受损
package lc3;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* https://www.cnblogs.com/silyvin/articles/12178528.html 4
* Created by joyce on 2020/1/7.
*/
/**
* 方案四,从内存伪造一个URL供给系统类加载器
*/
public class ResourceClassloader4 {
public static void main(String []f) throws Exception {
List<URL> list = init(new String [] {"jars/MySub-1.0.0.jar", "jars/MySubBrother-1.0.0.jar"});
URLClassLoader systemClassloader = (URLClassLoader) ClassLoader.getSystemClassLoader();
Method systemClassloaderMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
systemClassloaderMethod.setAccessible(true);
for(URL url : list) {
systemClassloaderMethod.invoke(systemClassloader, url);
}
test(ClassLoader.getSystemClassLoader());
}
private static void test(ClassLoader classLoader) {
try {
Class c1 = classLoader.loadClass("lc3.ResourceF");
c1.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
try {
Class c1 = classLoader.loadClass("lc3.ResourceG");
// c1.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
try {
Class c1 = classLoader.loadClass("lc3.ResourceH");
c1.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
public static List<URL> init(String [] resourceJars) throws Exception {
List<java.net.URL> urls = new ArrayList<>();
Map<String, ByteArrayOutputStream> map = new ConcurrentHashMap<>();
java.net.URL.setURLStreamHandlerFactory(new URLStreamHandlerFactory() {
public URLStreamHandler createURLStreamHandler(String urlProtocol) {
System.out.println("Someone asked for protocol: " + urlProtocol);
if ("myjarprotocol".equalsIgnoreCase(urlProtocol)) {
return new URLStreamHandler() {
@Override
protected URLConnection openConnection(URL url) throws IOException {
String key = url.toString().split(":")[1];
return new URLConnection(url) {
public void connect() throws IOException {}
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(map.get(key).toByteArray());
}
};
}
};
}
return null;
}
});
for(String resourceJar : resourceJars) {
InputStream in = ResourceClassloader4.class.getResourceAsStream(resourceJar);
int len = -1;
byte [] bytes = new byte[1024];
ByteArrayOutputStream jarBytes = new ByteArrayOutputStream();
while ((len = in.read(bytes)) != -1) {
jarBytes.write(bytes, 0, len);
}
map.put(resourceJar, jarBytes);
urls.add(new URL("myjarprotocol:" + resourceJar));
}
return urls;
}
}
其它参考:
loadClass方法,这个方法每部会现在自己已经加载的类中查找,如果找到就返回。找不到则向父类查找,如果父类都找不到这才开始自己加载,调用findClass方法。所以我们只要覆盖findClass方法就可以实现自己定义的加载了。顺带一提,ClassLoader中有一个方法叫defineClass(String, byte[], int, int);这个方法通过传进去一个Class文件的字节数组,就可以方法区生成一个Class对象。所以要实现findClass的目标就很明确了,只要将Class文件读取进来,然后生成byte数组,调用defineClass方法就可以了。
本加载器缓存——父加载器——本加载器findClass——本加载器defineClass
一是直接.class文件中查找,二是从jar包中加载。
1 从class文件中加载非常简单。只要找到相应的文件,就可以通过字节流读取进来。
2.1 从jar读取则相对麻烦一点,java给我们提供了一个专门用来读取jar包文件的类,抽象成一个JarFile的对象。通过调用这个对象的getInputStream方法,也是可以获取文件的输入流,从而读取字节数组。笔者做了一点相应的缓存,如果每次查找文件都要先读取jar文件,再遍历查找class文件是非常耗时的操作。于是,笔者选择再加载之前,把所有的jar包中的所有class读取到内存中,保存在一个map对象中。建立一个全限定名和字节数组的映射。这样在加载阶段,就能省下很多的时间了。这个地方个人认为,jar都在磁盘了,直接使用URLClassLoader不是更好吗
2.2 对于在内存中的,我们使用JarInputStream读取包括class在内的各entry,就如本文4种方式
其它
https://segmentfault.com/a/1190000013532009
https://www.jianshu.com/p/ee7fdb691826

我们再来算一下,这4种方式的内存
1 系统类加载器1份(资源形式),自定义加载器1份
2 系统类加载器1份(资源形式),自定义加载器1份,堆map一份,对堆的那份清理

3 4 系统类加载器1份(资源形式),自定义加载器1份,堆map一份,同样,对堆的那份清理

2021.4.30
第4种打整包插件,urlfactory already set 补充了方式3的劣势,以及我们在tomcat项目里面最终没有使用的原因
浙公网安备 33010602011771号