1、为什么需要自定义类加载器
在《类加载器》 中讲的,默认类加载器只能加载固定路径下的class,如果有特定路径下的class,需要自定义
安全性:系统自身需要一些jar,class,如果业务类代码中也有相同的class,破坏系统,类似双亲委托安全性
可以看看tomcat自定义类加载器的原因,别的就大同小异了
1 2 3 4 5 6 7 8 a)、要保证部署在tomcat上的每个应用依赖的类库相互独立,不受影响。 b)、由于tomcat是采用java语言编写的,它自身也有类库依赖,为了安全考虑,tomcat使用的类库要与部署的应用的类库相互独立。 c)、有些类库tomcat与部署的应用可以共享,比如说servlet-api,使用maven编写web程序时,servlet-api的范围是provided, 表示打包时不打包这个依赖,因为我们都知道服务器已经有这个依赖了。 d)、部署的应用之间的类库可以共享。这听起来好像与第一点相互矛盾,但其实这很合理,类被类加载器加载到虚拟机后, 会生成代表该类的class对象存放在永久代区域,这时候如果有大量的应用使用spring来管理,如果spring类库不能共享, 那每个应用的spring类库都会被加载一次,将会是很大的资源浪费。
2、自定义加载器 这儿主要说下我司的自定义类加载器;更复杂点的可以看看tomcat的类加载机制
为什么需要自定义类加载器?这可以参考章节1的答案
主要在于应用与基础平台的隔离,相对应用:可以有更大技术选型自由度,不用考虑基础平台的jar包版本、相对平台:更可靠安全,不被应用class影响
类加载器结构 虽然JAVA使用了类加载的委派机制,但并没严格要求开发者必须遵守该机制,我们可以打破这种”双亲委派”机制
目录结构
目录
说明
/servicesdir
业务实现jar包
/thirddir
业务依赖jar包
/platformdir
平台依赖jar包
类加载器
1.PlatformClassLoader平台加载器
1.1.加载/platformdir下的jar包
1.2.在加载时,采用了默认的“双亲委派”
2.AppClassLoader应用加载器
2.1.加载/servicesdir,/thirddir下的jar
2.2.该类加载器一定程度上打破了默认的“双亲委派”
2.2.0.loadClass方法中,如果本加载器没有load到对应的类,则会检查该类是否处于平台类加载器白名单中:
2.2.1.如果处于白名单中,则委派PlatformClassLoader加载
2.2.2.否则,通过super.loadClass(String,boolean)走默认的双亲委派
此处白名单类:平台核心类,不能被同名业务类干扰
预加载 《类加载器》 中说过,程序启动后,并不会加载所有类,在运行中实现到时,才会去加载。这儿就有性能损耗。
按类加载规则,一个类只加载一次
可以测试一下,加载需要的损耗
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 /** * 类加载时间性能测试 * * 看一下类加载需要消耗的时间 * Created by Jack on 2018/10/8. */ public class ClassLoaderTest1 { public static void main(String[] args) throws SQLException { long s = System.nanoTime(); LoaderClass loaderClass = new LoaderClass(); long e = System.nanoTime(); //第一次时间 System.out.println(e - s); e = System.nanoTime(); //第二次实例,但已经加载过,不再需要加载 LoaderClass loaderClass1 = new LoaderClass(); long e1 = System.nanoTime(); //第二次时间 System.out.println(e1 - e); } } //输出 2409737 396
可以从输出看到性能损耗是不小的,这部分损耗可以通过预加载来消除
随着程序运行时间越久,被触发的业务越多,那加载到的业务类越多。
预加载类的逻辑 ClassWarmUp
1.在classloader中loadClass时,把className加入到LinkedBlockingDeque中
2.为了性能,异步把deque中的class写入到文件中,需要起一个后台线程
2.1 后台线程,从deque中取出class,写入到文件中
3.下次从文件中预先加载class
打包 对于/servicesdir 与 /thirddir 都好处理,但对于platformdir是怎么打包的呢?毕竟在开发时,只是引入一个平台基础jar就行
使用 有了自定义类加载器,在应用主函数中,就不能直接new了,不然就会使用AppClassLoader
所以需要使用反射机制
1 2 3 Class<?> loadClass = platformClassLoader.loadClass("com.jack.Start"); Method startMethod = loadClass.getMethod("startUp"); startMethod.invoke(loadClass);
这样,通过Start加载的类也会通过platformClassLoader去加载
创建springcontext也一样,这儿还需使用到Thread.currentThread().getContextClassLoader()【下面有详解】
1 2 3 4 5 6 7 8 9 10 11 12 ClassLoader currentThreadLoader = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(appClassLoader); Class<?> contextClass = appClassLoader .loadClass("org.springframework.context.support.FileSystemXmlApplicationContext"); Class<?>[] parameterTypes = new Class[] { String[].class }; Constructor<?> constructor = contextClass.getConstructor(parameterTypes); return constructor.newInstance(new Object[] { xmlPaths.toArray(new String[0]) }); // switch back the thread context classloader Thread.currentThread().setContextClassLoader(currentThreadLoader);
3、反常 “双亲委派”模型有优点,也有力不从心的地方
Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。
这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由系统类加载器(System ClassLoader)来加载的。引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。 而线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。
场景:
当高层提供了统一的接口让低层去实现,同时又要在高层加载(或者实例化)低层的类时,就必须要通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类
当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管
解决方案: 从jdk1.2开始引入的,类Thread中的getContextClassLoader()与setContextClassLoader(ClassLoader c1),分别用来获取和设置类加载器
一般使用模式:获取-使用-还原
1 2 3 4 5 6 7 8 ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); try{ Thread.currentThread().setContextClassLoader(targetTccl); excute(); } finally { Thread.currentThread().setContextClassLoader(classLoader); }
jdbc 以jdbc看下场景1的情况
1 2 3 4 Class.forName("com.mysql.jdbc.Driver") String url = "jdbc:mysql://localhost:3306/testdb"; // 通过java库获取数据库连接 Connection conn = java.sql.DriverManager.getConnection(url, "name", "password");
1.Class.forName(“com.mysql.jdbc.Driver”); 在com.mysql.jdbc.Driver中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Driver extends NonRegisteringDriver implements java.sql.Driver { static { try { java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); } } public Driver() throws SQLException { // Required for Class.forName().newInstance() } }
通过Class.forName(),主要就是执行初始化static代码块,也就是向DriverManager注册Driver
此时:应用类、Driver是由AppClassLoader加载,但由于双亲委派java.sql.DriverManager是由BootstrapClassLoader加载
2.java.sql.DriverManager.getConnection 获取连接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 private static Connection getConnection( String url, java.util.Properties info, ClassLoader callerCL) throws SQLException { java.util.Vector drivers = null; synchronized(DriverManager.class) { if(callerCL == null) { callerCL = Thread.currentThread().getContextClassLoader(); } } if(url == null) { throw new SQLException("The url cannot be null", "08001"); } println("DriverManager.getConnection(\"" + url + "\")"); if (!initialized) { initialize(); } synchronized (DriverManager.class){ drivers = readDrivers; } SQLException reason = null; for (int i = 0; i < drivers.size(); i++) { DriverInfo di = (DriverInfo)drivers.elementAt(i); if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) { println(" skipping: " + di); continue; } try { println(" trying " + di); Connection result = di.driver.connect(url, info); if (result != null) { // Success! println("getConnection returning " + di); return (result); } } catch (SQLException ex) { if (reason == null) { reason = ex; } } } if (reason != null) { println("getConnection failed: " + reason); throw reason; } println("getConnection: no suitable driver found for "+ url); throw new SQLException("No suitable driver found for "+ url, "08001"); } private static Class getCallerClass(ClassLoader callerClassLoader, String driverClassName) { Class callerC = null; try { callerC = Class.forName(driverClassName, true, callerClassLoader); } catch (Exception ex) { callerC = null; // being very careful } return callerC; }
这其中有两行代码:
1 2 3 4 callerCL = Thread.currentThread().getContextClassLoader(); callerC = Class.forName(driverClassName, true, callerClassLoader);
这儿是取线程上下文中的classloader,也就是AppClassLoader;如果不取此classloader,那么Class.forName(driverClassName)就是使用DriverManager的BootstrapClassLoader加载,那必然是加载不到,这也就是父层类加载器加载不了低层类。
还有个问题,为什么在应用程序中已经加载过Driver,到了getConnection()又要再加载,还得通过Thread.currentThread().getContextClassLoader()?
其实在getConnection()中,只是对比class是否是同一个,像tomcat那样,各个应用都有自己的mysql-driver的jar包,就只能通过classloader来区分,因为class是不是相同需要classname+classloader组合鉴别
spring 对于场景2的问题
如果有 10 个 Web 应用程序都用到了spring的话,可以把Spring的jar包放到 common 或 shared 目录下让这些程序共享。Spring 的作用是管理每个web应用程序的bean,getBean时自然要能访问到应用程序的类,而用户的程序显然是放在 /WebApp/WEB-INF 目录中的(由 WebAppClassLoader 加载),那么在 CommonClassLoader 或 SharedClassLoader 中的 Spring 容器如何去加载并不在其加载范围的用户程序(/WebApp/WEB-INF/)中的Class呢?
答案呼之欲出:spring根本不会去管自己被放在哪里,它统统使用线程上下文加载器来加载类,而线程上下文加载器默认设置为了WebAppClassLoader,也就是说哪个WebApp应用调用了spring,spring就去取该应用自己的WebAppClassLoader来加载bean
org.springframework.web.context.ContextLoader类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public WebApplicationContext initWebApplicationContext(ServletContext servletContext) { if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) { throw new IllegalStateException( "Cannot initialize context because there is already a root application context present - " + "check whether you have multiple ContextLoader* definitions in your web.xml!"); } Log logger = LogFactory.getLog(ContextLoader.class); servletContext.log("Initializing Spring root WebApplicationContext"); if (logger.isInfoEnabled()) { logger.info("Root WebApplicationContext: initialization started"); } long startTime = System.currentTimeMillis(); try { // Determine parent for root web application context, if any. ApplicationContext parent = loadParentContext(servletContext); // Store context in local instance variable, to guarantee that // it is available on ServletContext shutdown. this.context = createWebApplicationContext(servletContext, parent); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); ClassLoader ccl = Thread.currentThread().getContextClassLoader(); if (ccl == ContextLoader.class.getClassLoader()) { currentContext = this.context; } else if (ccl != null) { currentContextPerThread.put(ccl, this.context); } if (logger.isDebugEnabled()) { logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" + WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]"); } if (logger.isInfoEnabled()) { long elapsedTime = System.currentTimeMillis() - startTime; logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms"); } return this.context; } catch (RuntimeException ex) { logger.error("Context initialization failed", ex); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex); throw ex; } catch (Error err) { logger.error("Context initialization failed", err); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err); throw err; } }
关键代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 // 获取线程上下文类加载器,默认为WebAppClassLoader ClassLoader ccl = Thread.currentThread().getContextClassLoader(); // 如果spring的jar包放在每个webapp自己的目录中 // 此时线程上下文类加载器会与本类的类加载器(加载spring的)相同,都是WebAppClassLoader if (ccl == ContextLoader.class.getClassLoader()) { currentContext = this.context; } else if (ccl != null) { // 如果不同,也就是上面说的那个问题的情况,那么用一个map把刚才创建的WebApplicationContext及对应的WebAppClassLoader存下来 // 一个webapp对应一个记录,后续调用时直接根据WebAppClassLoader来取出 currentContextPerThread.put(ccl, this.context); }
这样做的目的在于当通过ConetxtLoader的静态方法获取context的时候,能保证获取的是当前web application的context.实际上就是对于tomcat下面的任何一个线程,我们都能很方便的找出这个线程对应的webapplicationContext.于是在一些不能方便获取servletContext的场合,我们可以通过当前线程获取webapplicationContext.
1 2 3 4 5 6 7 8 9 10 public static WebApplicationContext getCurrentWebApplicationContext() { ClassLoader ccl = Thread.currentThread().getContextClassLoader(); if (ccl != null) { WebApplicationContext ccpt = currentContextPerThread.get(ccl); if (ccpt != null) { return ccpt; } } return currentContext; }
总结 简而言之就是ContextClassLoader默认存放了AppClassLoader的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作
4、参考资料 以jdbc为例搞清contextClassLoader