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
固然,这样作仍是会被反编译破解,要加大难度,还须要其余处理的。