Java類加載器的做用是尋找類文件,而後加載Class字節碼到JVM內存中,連接(驗證、準備、解析)並初始化,最終造成能夠被虛擬機直接使用的Java類型。java
有兩種類加載器:
1 啓動類加載器(Bootstrap ClassLoader)
由C++語言實現(針對HotSpot VM),負責將存放在<JAVA_HOME>lib目錄或-Xbootclasspath參數指定的路徑中的類庫加載到JVM內存中,像java.lang.、java.util.、java.io.*等等。能夠經過vm參數「-XX:+TraceClassLoading」來獲取類加載信息。咱們沒法直接使用該類加載器。算法
2 其餘類加載器(Java語言實現)
1)擴展類加載器(Extension ClassLoader)
負責加載<JAVA_HOME>libext目錄或java.ext.dirs系統變量指定的路徑中的全部類庫。咱們能夠直接使用這個類加載器。
2)應用程序類加載器(Application ClassLoader),或者叫系統類加載器
負責加載用戶類路徑(classpath)上的指定類庫,咱們能夠直接使用這個類加載器。通常狀況,若是咱們沒有自定義類加載器默認就是用這個加載器。
3)自定義類加載器
經過繼承ClassLoader類實現,主要重寫findClass方法。bootstrap
在JVM虛擬機中,若是一個類加載器收到類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器完成。每一個類加載器都是如此,只有當父加載器在本身的搜索範圍內找不到指定的類時(即ClassNotFoundException),子加載器纔會嘗試本身去加載。安全
也就是說,對於每一個類加載器,只有父類(依次遞歸)找不到時,才本身加載 。這就是雙親委派模型。服務器
爲何須要雙親委派模型呢?這能夠提升Java的安全性,以及防止程序混亂。
提升安全性方面:
假設咱們使用一個第三方Jar包,該Jar包中自定義了一個String類,它的功能和系統String類的功能相同,可是加入了惡意代碼。那麼,JVM會加載這個自定義的String類,從而在咱們全部用到String類的地方都會執行該惡意代碼。
若是有雙親委派模型,自定義的String類是不會被加載的,由於最頂層的類加載器會首先加載系統的java.lang.String類,而不會加載自定義的String類,防止了惡意代碼的注入。ide
防止程序混亂
假設用戶編寫了一個java.lang.String的同名類,若是每一個類加載器都本身加載的話,那麼會出現多個String類,致使混亂。若是本加載器加載了,父加載器則不加載,那麼以哪一個加載的爲準又不能肯定了,也增長了複雜度。工具
咱們能夠自定義類加載器,只需繼承ClassLoader抽象類,並重寫findClass方法(若是要打破雙親委派模型,須要重寫loadClass方法)。緣由能夠查看ClassLoader的源碼:測試
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
這個是ClassLoader中的loadClass方法,大體流程以下:
1)檢查類是否已加載,若是是則不用再從新加載了;
2)若是未加載,則經過父類加載(依次遞歸)或者啓動類加載器(bootstrap)加載;
3)若是還未找到,則調用本加載器的findClass方法;
以上可知,類加載器先經過父類加載,父類未找到時,纔有本加載器加載。ui
由於自定義類加載器是繼承ClassLoader,而咱們再看findClass方法:this
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
能夠看出,它直接返回ClassNotFoundException。
所以,自定義類加載器必須重寫findClass方法。
自定義類加載器示例代碼:
類加載器HClassLoader:
class HClassLoader extends ClassLoader { private String classPath; public HClassLoader(String classPath) { this.classPath = classPath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] data = loadByte(name); return defineClass(name, data, 0, data.length); } catch (Exception e) { e.printStackTrace(); throw new ClassNotFoundException(); } } /** * 獲取.class的字節流 * * @param name * @return * @throws Exception */ private byte[] loadByte(String name) throws Exception { name = name.replaceAll("\\.", "/"); FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class"); int len = fis.available(); byte[] data = new byte[len]; fis.read(data); fis.close(); // 字節流解密 data = DESInstance.deCode("1234567890qwertyuiopasdf".getBytes(), data); return data; } }
被加載的類Car:
public class Car { public Car() { System.out.println("Car:" + getClass().getClassLoader()); System.out.println("Car Parent:" + getClass().getClassLoader().getParent()); } public String print() { System.out.println("Car:print()"); return "carPrint"; } }
測試代碼:
@Test public void testClassLoader() throws Exception { HClassLoader myClassLoader = new HClassLoader("e:/temp/a"); Class clazz = myClassLoader.loadClass("com.ha.Car"); Object o = clazz.newInstance(); Method print = clazz.getDeclaredMethod("print", null); print.invoke(o, null); }
以上代碼,展現了自定義類加載器加載類的方法。
須要注意的是:
執行測試代碼前,必須將Car.class文件移動到e:/temp/a下,而且按包名創建層級目錄(這裏爲com/ha/)。由於若是不移動Car.class文件,那麼Car類會被AppClassLoader加載(自定義類加載器的parent是AppClassLoader)。
上面介紹了Java類加載器的相關知識。對於自定義類加載器,哪裏能夠用到呢?
主流的Java Web服務器,好比Tomcat,都實現了自定義的類加載器。由於它要解決幾個問題:
1)Tomcat上能夠部署多個不一樣的應用,可是它們可使用同一份類庫的不一樣版本。這就須要自定義類加載器,以便對加載的類庫進行隔離,不然會出現問題;
2)對於非.class的文件,須要轉爲Java類,就須要自定義類加載器。好比JSP文件。
這裏舉一個其它的例子:Java核心代碼的加密。
假設咱們項目當中,有一些核心代碼不想讓別人反編譯看到。當前知道有兩種方法,一種是經過代碼混淆(推薦Allatori,商用收費);一種是本身編寫加密算法,對字節碼加密,加大反編譯難度。
代碼混淆若是用Allatori,比較簡便。注意控制本身編寫類的訪問權限便可。接口用public,內部方法用private,其餘的用默認的(即不加訪問修飾符)或者protected。代碼混淆這裏不過多說明,這裏主要介紹一下字節碼加密。
大概的流程能夠以下:
.class加密代碼:
@Test public void testEncode() { String classFile = "e:/temp/a/com/ha/Car.class"; FileInputStream fis = null; try { fis = new FileInputStream(classFile); int len = fis.available(); byte[] data = new byte[len]; fis.read(data); fis.close(); data = DESInstance.enCode("1234567890qwertyuiopasdf".getBytes(), data); String outFile = "e:/temp/a/com/ha/EnCar.class"; FileOutputStream fos = new FileOutputStream(outFile); fos.write(data); fos.close(); } catch (Exception e) { e.printStackTrace(); } }
類加載器中解密,查看上文中的:
// 字節流解密 data = DESInstance.deCode("1234567890qwertyuiopasdf".getBytes(), data);
加解密工具類:
public class DESInstance { private static String ALGORITHM = "DESede"; /** * 加密 * * @param key * @param src * @return */ public static byte[] enCode(byte[] key, byte[] src) { byte[] value = null; SecretKey deskey = new SecretKeySpec(key, ALGORITHM); try { Cipher cipher = Cipher.getInstance(ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, deskey); value = cipher.doFinal(src); } catch (Exception e) { e.printStackTrace(); } return value; } /** * 解密 * * @param key * @param src * @return */ public static byte[] deCode(byte[] key, byte[] src) { byte[] value = null; SecretKey deskey = new SecretKeySpec(key, ALGORITHM); try { Cipher cipher = Cipher.getInstance(ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, deskey); value = cipher.doFinal(src); } catch (Exception e) { e.printStackTrace(); } return value; } }
注意祕鑰是24位,不然會報錯:
java.security.InvalidKeyException: Invalid key length
若是解密密碼錯誤,則是以下錯誤:
javax.crypto.BadPaddingException: Given final block not properly padded
固然,這樣作仍是會被反編譯破解,要加大難度,還須要其餘處理的。