使用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;
    }

}

 

 

其它参考:

自定义类加载器-从.class和.jar中读取

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

posted on 2020-01-10 23:09  silyvin  阅读(1178)  评论(0编辑  收藏  举报