自定義類加載器加載jar包中的class文件

import java.io.*;
import java.util.*;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

/**
 * @author yuwenbo1
 * @since 2021/3/24 15:23
 * 參考鏈接 {@see https://juejin.cn/post/6869589995066556430}
 */
public class CustomerLoadJarClassLoader extends ClassLoader implements Closeable {
    /**
     * META-INF.MF定義的鏈碼基礎掃描路徑
     */
    private static final String CHAINCODE_BASE_PACKAGE = "Chaincode-Base-Package";
    /**
     * 解壓的臨時包名
     */
    private static final String JAR_TMP_PACKAGE = "chaincode";

    private static final int BUFFER_SIZE = 1024;

    private Map<String, byte[]> map;
    /**
     * 如果用戶不配置,那么默認掃描com.xxx包下面的注解包
     */
    private List<String> packageScanList = Arrays.asList("com.jd.icity");

    private List<String> classNameList = new ArrayList<>(256);
    /**
     * 由頂級加載器和擴展加載器的列表信息
     */
    private List<String> bootAndExtLoaderList = Arrays.asList("sun", "java", "javax", "jdk", "javassist");

    public CustomerLoadJarClassLoader(String jarPath) throws FileNotFoundException {
        /**
         * 設置當前類加載器的父類為當前線程的類加載器,因為默認是使用的是appclassloader,
         * 但是springboot項目的classloader是springboot自定義的classloader(具體可看打包好后的jar文件的META-INF.MF文件,指定的啟動類為springboot的loader),
         * 這就會導致加載對應文件不直接屬于classloader,相關校驗就會失敗
         */
        super(Thread.currentThread().getContextClassLoader());
        if (!jarPath.endsWith(".jar")) {
            throw new IllegalArgumentException("jarFile is not a jar" + jarPath);
        }
        dealChaincodeScanList(jarPath);
        map = new HashMap<>(64);
        unzipJarAndRead(jarPath);
        dealLibJar(jarPath);
    }

    /**
     * 獲取jar名稱路徑,以及對應的需要掃描的類路徑信息
     *
     * @param jarPath 包含jar名稱的文件
     * @return
     * @throws FileNotFoundException
     */
    private void dealChaincodeScanList(String jarPath) throws FileNotFoundException {
        try {
            JarFile jarFile = new JarFile(jarPath);
            Manifest manifest = jarFile.getManifest();

            Attributes mainAttributes = manifest.getMainAttributes();
            //得到需要進行掃描的類信息
            String mainPackage = mainAttributes.getValue(CHAINCODE_BASE_PACKAGE);
            if (null != mainPackage) {
                this.packageScanList = Arrays.asList(mainPackage.split(","));
            }
        } catch (IOException e) {
            throw new FileNotFoundException("jar:" + jarPath + " not exist");
        }
    }

    /**
     * 解壓jar文件,并且進行讀取加載,
     * 如果讀取到類信息了,那么則進行讀取,
     * 如果讀取到的是jar內容,那么則進行解壓,然后進行文件處理,并讀取相應的類文件進行然后進行加載
     *
     * @param jarPath
     */
    private void unzipJarAndRead(String jarPath) {
        //直接解壓jar文件,然后遞歸查詢class文件,以及對應的.jar文件,并加載jar文件進行讀取
        try {
            /**
             * 直接在對應的jarPath路徑下進行解壓即可,因為本來路徑就是唯一的,而且清理的時候也比較方便
             */
            File tmpFolder = new File(jarPath);
            String folderParent = tmpFolder.getParent();
            File folder = new File(folderParent, JAR_TMP_PACKAGE);
            if (!folder.exists()) {
                folder.mkdir();
            }
            // 設置jvm關閉的時候自動刪除
            folder.deleteOnExit();

            JarFile jarFile = new JarFile(jarPath);
            Enumeration<JarEntry> entries = jarFile.entries();
            while (entries.hasMoreElements()) {
                JarEntry jarEntry = entries.nextElement();
                String name = jarEntry.getName();
                if (name.endsWith(".jar")) {
                    dealJarFile(name, folder, jarFile, jarEntry);
                } else if (name.endsWith(".class")) {
                    dealClass(name, jarFile, jarEntry);
                }
                //其他類型的暫時不關心,忽略即可
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 對文件是jar的類進行處理
     *
     * @param name
     * @param libJarPath 依賴的jar包解壓路徑
     */
    private static void dealJarFile(String name, File libJarPath, JarFile jarFile, JarEntry jarEntry) {
        System.out.println("unzip jar file:" + name);

        //對jar文件進行解壓
        String[] split = name.split("/");
        if (split.length > 1) {
            File file = new File(libJarPath, split[split.length - 1]);
            try {
                if (!file.exists()) {
                    boolean success = file.createNewFile();
                    if (!success) {
                        throw new RuntimeException("create file:" + file.getPath() + " exception");
                    }
                }
                unpack(jarFile, jarEntry, file);
                file.deleteOnExit();
            } catch (IOException e) {
                System.out.println("unzip jar exception:" + e.getMessage());
            }

        }
    }

    /**
     * 對jar包進行解壓
     *
     * @param jarFile
     * @param entry
     * @param file
     * @throws IOException
     */
    private static void unpack(JarFile jarFile, JarEntry entry, File file) throws IOException {
        try (InputStream inputStream = jarFile.getInputStream(entry)) {
            try (OutputStream outputStream = new FileOutputStream(file)) {
                byte[] buffer = new byte[BUFFER_SIZE];
                int bytesRead;
                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, bytesRead);
                }
                outputStream.flush();
            }
        }
    }

    /**
     * 處理加載類信息的邏輯
     * 如果在map中已經存在了,那么則直接不進行處理,
     * 如果不存在,那么則進行讀取類文件,然后放入map中,并將掃描包列表信息進行判斷,是否是設置的開頭參數,如果有才放入類列表中
     *
     * @param name
     * @param jarFile
     * @param jarEntry
     */
    private void dealClass(String name, JarFile jarFile, JarEntry jarEntry) {
        String className = name.replace(".class", "").replaceAll("/", ".");
        if (!map.containsKey(className)) {
            byte[] b = getClassByte(jarFile, jarEntry);
            if (null != b) {
                map.put(className, b);
                //需要判斷是否是需要進行掃描的包路徑,然后進行添加
                packageScanList.stream().forEach(v -> {
                    if (className.startsWith(v)) {
                        classNameList.add(className);
                    }
                });
            }
        }
    }

    /**
     * 對有依賴的jar包進行處理,加載到內存中
     */
    private void dealLibJar(String jarPath) {
        File tmpFolder = new File(jarPath);
        String folderParent = tmpFolder.getParent();
        File folder = new File(folderParent, JAR_TMP_PACKAGE);
        if (!folder.exists()) {
            System.out.println("no lib jar exist,will be return");
            return;
        }

        File[] files = folder.listFiles();
        if (files.length == 0) {
//            System.out.println("jar list is empty,will be return");
            return;
        }
        for (File file : files) {
            JarFile jarFile = null;
            try {
                jarFile = new JarFile(file);
                Enumeration<JarEntry> entries = jarFile.entries();
                while (entries.hasMoreElements()) {
                    JarEntry jarEntry = entries.nextElement();
                    String name = jarEntry.getName();
                    /**
                     * 針對lib里面的jar包,只會解析.class文件,對其他不關心
                     */
                    if (name.endsWith(".class")) {
                        dealClass(name, jarFile, jarEntry);
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public List<String> getClassNameList() {
        return classNameList;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] b;
        if (map.containsKey(name)) {
            b = map.get(name);
        } else {
            throw new ClassNotFoundException(name);
        }
        return defineClass(name, b, 0, b.length);
    }

    /**
     * 獲取某個jar里面的信息
     *
     * @param jarFile
     * @param jarEntry
     * @return
     */
    private byte[] getClassByte(JarFile jarFile, JarEntry jarEntry) {
        try (InputStream input = jarFile.getInputStream(jarEntry)) {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            while ((bytesNumRead = input.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (FileNotFoundException e) {
        } catch (IOException e) {
        }
        return null;
    }

    @Override
    public void close() throws IOException {
        if (!map.isEmpty()) {
            map.clear();
        }
        if (!classNameList.isEmpty()) {
            classNameList.clear();
        }
    }
}
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內容