使用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项目里面最终没有使用的原因