Java的设计初衷主要面向嵌入式领域,对于自定义的一些类,考虑使用按需加载的原则,即在程序使用的时候才加载类,节省内存的消耗,这时即可以通过类加载器来加载 。
如果平时,只是做Web开发,那应该很少会跟类打交道,但如果想深入学习Tomcat的架构,那它是必不可少的, 所谓类加载器,就是用于加载Java类到Java虚拟机中的组件中, 它负责读取Java字节码,并转换成java.lang.Class类的一个实例,使字节码.class文件得以运行,一般类加载器负责根据一个指定的类找到对应的字节码,然后根据这些字节码定义一个java类, 另外,它还可以加载资源,包括图像文件和配置文件 。
类加载器在实际使用中给我们带来的好处是, 它们可以使用Java类动态的加载到JVM中并运行,即可在程序运行时再加载类, 提供了很灵活的动态加载方式,例如 Applet,从远程服务器下载字节码到客户端再动态加载到JVM中便 可以运行。
在Java体系中,可以将系统分为以下三种类加载器。
由此可以看出,越重要的类加载器就越早被JVM载入, 这是考虑安全性,因为先加载的类加载器会充当下一个类加载器的父类加载器,在双亲委派模型的机制下 , 就能确保安全性, 双亲委派模型会在类加载器中加载时首先委托给父类加载器去加载 ,除非父类加载器不能,才自己加载 。
这种模型要求,除了顶层的启动类加载器之外 , 其他的类加载器都要有自己的父类加载器,假如有一个类要加载进来 , 一个类加载器并不会马上尝试自己将其加载,而是委派给父类加载器加载,父类加载器收到后又尝试委派给其父类加载器,以此类推, 直到委派给父类的加载器,这样一层一层的往上委派,只有当父类加载器反馈给自己没有办法加载时, 子加载器才会尝试自己加载,通过这个机制,保证了Java应用程序类加载器想要加载一个有破坏性的类,同时这个机制也保证了安全性, 设想如果应用程序类加载器想要加载一个有破坏性的java.lang.System类, 双亲委派横截会一层一层的向上委派,最终委派给启动类加载器, 而启动类加载器检查到缓存中已经有这个类, 并不会再加载这个有破坏性的System类。
另外,类加载器还拥有全盘负责机制,即当一个类加载器加载一个类时, 这个类的所有依赖,引用和其他的所有类由这个类加载器加载 , 除非程序中显式的指定另外一个类加载器加载 。
在Java中,我们用完全匹配的类名来标识一个类,即用包名和类名,而在JVM中,一个类完全匹配类名和一个类加载器的实例ID 作为唯一的标识,也就是说,同一个虚拟机可以有两个包名, 类名都相同的类, 只要它由两个不同的类加载器加载,当我们在Java中说两个类是否相等,必须针对同一个类加载器加载的前提下才有意义 , 否则就算是同样的字节码,由不同的类加载器加载,这两个类也不是相等的, 这种特性为我们提供了隔离机制,在Tomcat服务器中它十分有用 。
了解了JVM的类加载器的各种机制后,看看一个类是怎样被类加载器载入进来的, 如图13.2 所示,要加载一个类, 类加载器先判断是否已经加载过(加载过的类会缓存在内存中),如果缓存中存在此类,则直接返回此类,否则获取父类加载器,如果父类加载器为null,则由启动类加载器载入并返回Class,如果父类加载器不为null,则由父类加载器载入,载入成功则返回class,载入失败,则根据类路径查找Class文件,找到了就加载此Class文件并返回Class,找不到就抛出ClassNotFoundException异常。
类加载器属于JVM级别的设计的,我们很多时候基本不会与它打交道,假如你想深入了解Tomcat 内核 或设计开发自己的中间件, 那么你熟悉的类加载器的相关机制,在现实的设计中,根据实际情况利用类加载器可以提供类库隔离及共享,保证软件不同级别的逻辑分割程序不会互相影响,提供更好的安全性。
一般场景中使用Java默认的类加载器即可, 但有时为了达到某种目的,又不得不实现自己的类加载器,例如为了使类互相隔离, 为了实现热部署和加载功能,这时就需要自己定义类加载器,每个类加载器加载各自的资源,以达到资源的隔离效果,在对资源加载上可以沿用双亲委派机制,也可以打破双亲委派机制 。
public class Test { public Test() { System.out.println(this.getClass().getClassLoader().toString()); } }
public class TomcatClassLoader extends ClassLoader { private String name; public TomcatClassLoader(ClassLoader parent, String name) { super(parent); this.name = name; } @Override public String toString() { return this.name; } @Override protected Class> findClass(String name) throws ClassNotFoundException { InputStream is = null; byte [] data = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { is = new FileInputStream(new File("/Users/quyixiao/gitlab/tomcat/output/production/tomcat/com/luban/classloadtest/Test.class")); int c = 0 ; while ( -1 != (c = is.read())){ baos.write(c); } data = baos.toByteArray(); }catch (Exception e ){ e.printStackTrace(); }finally { try { is.close(); baos.close(); } catch (IOException e) { e.printStackTrace(); } } return this.defineClass(name,data,0 ,data.length); } public static void main(String[] args) { TomcatClassLoader loader = new TomcatClassLoader(TomcatClassLoader.class.getClassLoader() , "TomcatClassLoader"); Class clazz ; try { clazz = loader.loadClass("com.luban.classloadtest.Test"); Object object =clazz.newInstance(); }catch (Exception e){ e.printStackTrace(); } } }
重写定义一个继承了ClassLoader 的TomcatClassLoaderN类, 这个类与前面的TomcatClassLoader类很相似 , 但它除了重写findClass方法外,还重写了loadClass方法,默认loadClass方法实现了双亲委派机制的逻辑,即会先让父类加载器加载,当无法加载时,才由自己加载,这里为了破坏双亲委派机制必须重写loadClass方法,即这里先尝试交由System类加载器加载,加载失败时才会由自己加载,它并没有优先交给父类加载器,这就是打破了双亲委派机制 。
public class TomcatClassLoaderN extends ClassLoader { private String name; public TomcatClassLoaderN(ClassLoader parent, String name) { super(parent); this.name = name; } @Override public String toString() { return this.name; } @Override public Class> loadClass(String name) throws ClassNotFoundException { Class> clazz = null; ClassLoader system = getSystemClassLoader(); try { clazz = system.loadClass(name); } catch (Exception e) { e.printStackTrace(); } if (clazz != null) { return clazz; } clazz = findClass(name); return clazz; } @Override protected Class> findClass(String name) throws ClassNotFoundException { InputStream is = null; byte[] data = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { is = new FileInputStream(new File("/Users/quyixiao/gitlab/tomcat/output/production/tomcat/com/luban/classloadtest/Test.class")); int c = 0; while (-1 != (c = is.read())) { baos.write(c); } data = baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { try { is.close(); baos.close(); } catch (Exception e) { e.printStackTrace(); } } return this.defineClass(name, data, 0, data.length); } public static void main(String[] args) { TomcatClassLoaderN loader = new TomcatClassLoaderN(TomcatClassLoaderN.class.getClassLoader(), "TomcatLoaderN"); Class clazz; try { clazz = loader.loadClass("com.luban.classloadtest.Test"); Object o = clazz.newInstance(); } catch (Exception e) { e.printStackTrace(); } } }
Tomcat拥有不同的类加载器, 以实现对各种资源的控制,一般来说,Tomcat要用类加载器解决以下4个问题。
对于以上的几个问题, 如果单独的使用一个类加载器明显达不到效果,必须根据具体的情况使用若干个自定义类加载器。
下面看看Tomcat 的类加载器是怎样定义的, 如图13.3所示,启动类加载器,扩展类加载器,应用程序类加载器这三个类加载器属于JDK 级别的加载器, 它们唯一的,我们一般不会对其做任何更改,接下来 , 则是Tomcat类加载器, 在Tomcat中,最重要的一个类加载器是Common类加载器, 它的父类加载器是应用程序类加载器, 负责加载$ CATALINA_BASE/lib,$ CATALINA_HOME/lib两个目录下的所有.class文件与jar包文件,而下面的虚线框的两个类加载器主要用于在Tomcat5版本中, Tomcat5 版本中的两个类加载器实例默认与常见的类加载器实例不同。 Common类加载器是它们的父类加载器,而在Tomcat7 版本中,这两个实例变量也存在,只是catalina.properties配置文件没有对server.loader和share.loader两项进行配置,所以在程序里两个类加载器实例就被赋值为Common 类加载器实例,即一个Tomcat 7版本的实例其实就是Common类加载器实例。
private void initClassLoaders() { try { // CommonClassLoader是一个公共的类加载器,默认加载${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar下的class commonLoader = createClassLoader("common", null); // 虽然这个地方parent是null,实际上是appclassloader System.out.println("commonLoader的父类加载器===="+commonLoader.getParent()); if( commonLoader == null ) { // no config file, default to this loader - we might be in a 'single' env. commonLoader=this.getClass().getClassLoader(); } // 下面这个两个类加载器默认情况下就是commonLoader catalinaLoader = createClassLoader("server", commonLoader); sharedLoader = createClassLoader("shared", commonLoader); } catch (Throwable t) { handleThrowable(t); log.error("Class loader creation threw exception", t); System.exit(1); } }
首先创建一个Common类加载器,再把Common类加载器作为参数传入createClassLoader方法里,在这个方法里会根据catalina.properties中的server.loader和share.loader属性是否为空判断是否另外一个创建新类加载器, 如果属性为空, 则把常见的加载器直接赋值给Catalina类加载器和共享类加载器, 如果默认的配置满足不也你的需求,可以通过修改catalina.properties配置文件满足需要 。
从上图13.3 中的WebAppClassLoader来看,就大概的知道它主要用于加载Web应用程序,它的父类加载器是Common类加载器, Tomcat一般会有多个WebApp类加载器实例, 每个类加载器负责加载一个Web程序 。
对照这样的一个类加载器结构,看看上面需要解决的问题是否解决,由于每个Web应用项目都有自己的WebApp类加载器, 很好的使用Web应用程序之间的互相隔离且能通过创建新的WebApp类加载器达到热部署,这种类加载器结构能有效的使用Tomcat不受Web应用程序的影响,而Common类加载器存放使用多个Web应用程序能够互相共享类库。
private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception { String value = CatalinaProperties.getProperty(name + ".loader"); if ((value == null) || (value.equals(""))) return parent; value = replace(value); Listrepositories = new ArrayList (); StringTokenizer tokenizer = new StringTokenizer(value, ","); while (tokenizer.hasMoreElements()) { String repository = tokenizer.nextToken().trim(); if (repository.length() == 0) { continue; } // Check for a JAR URL repository try { // 从URL上获取Jar包资源 @SuppressWarnings("unused") URL url = new URL(repository); repositories.add(new Repository(repository, RepositoryType.URL)); continue; } catch (MalformedURLException e) { // Ignore } // Local repository if (repository.endsWith("*.jar")) { // GLOB: 表示整个目录下所有的Jar包资源,仅仅是.jar后缀资源 repository = repository.substring(0, repository.length() - "*.jar".length()); repositories.add(new Repository(repository, RepositoryType.GLOB)); } else if (repository.endsWith(".jar")) { // 表示目录下单个的jar包资源 repositories.add(new Repository(repository, RepositoryType.JAR)); } else { // 表示目录下所有资源,包括jar包、class文件、其他类型资源 repositories.add(new Repository(repository, RepositoryType.DIR)); } } // 基于类仓库类创建一个ClassLoader return ClassLoaderFactory.createClassLoader(repositories, parent); }
Java虚拟机利用类加载器将类载入到内存的过程中,类加载器要做很多的事情 , 例如,读取字节数组,验证,解析,初始化等,而Java提供了URLClassLoader类能方便的将Jar,Class或网络资源载入到内存中,Tomcat中则用一个工厂类ClassLoaderFactory把创建的类加载器细节进行封装,通过它可以很方便的创建自定义类加载器。
如图13.4 所示,利用createClassLoader方法并传入资源路径 和父类加载器即可以创建一个自定义类加载器, 此类加载器负责加载传入的所有资源 。
ClassLoaderFactory有个内部类Repository,它就是表示资源的类,资源的类型用一个RepositoryType表示 。
public enum RepositoryType { DIR, GLOB, JAR, URL }
每个类型代表的意思如下
通过以上的介绍,读者已经对ClassLoaderFactory类有所了解,下面用一个简单的例子展示Tomcat中常见的类加载器是如何利用ClassLoaderFactory工厂类来创建的,代码如下 。
List repositories = new ArrayList(); repositories.add(new Repository("${catalina.home}/lib",RepositoryType.DIR)); repositories.add(new Repository("${catalina.home}/lib",RepositoryType.GLOB)); repositories.add(new Repository("${catalina.base}/lib",RepositoryType.DIR)); repositories.add(new Repository("${catalina.base}/lib",RepositoryType.DIR)); ClassLoaderparent = null ; ClassLoader commonLoader = ClassLoaderFactory.createClassLoader(repositories,parent);
至此Common类加载器创建完毕,其中 $ {catalina.home}与$ {catalina.base}表示变量,它的值分别为Tomcat安装目录与Tomcat的工作目录,Parent为父类加载器, 如果它设置为null,ClassLoaderFactory创建时会使用默认的父类加载器,即系统类加载器,总结起来, 只需要下几步就能完成一个类加载器的创建,首先,把要加载的资源添加到一个列表中,其次确定父类加载器,默认的设置为null,最后把这些作为参数传入到ClassLoaderFactory工厂类。
假如我们不确定要加载的资源是网络上的还是本地的,那么可以用以下的方式进行处理。
try{ URL url = new URL("路径 "); repositories.add(new Repository("路径",RepositoryType.URL)); }catch(MalformedURLException e ){ }
这种方式处理得比较巧妙,URL 在实例化时就可以检查这个路径的有效性,假如为本地资源或才网络上不存在的路径资源,那么就会抛出异常,不会把此路径添加到资源列表中。
ClassLoaderFactory工厂类最终将资源转换成URL[]数组,因为ClassLoaderFactory生成类加载器继承于URLClassLoader的,而URLClassLoader的构造函数只支持URL[]数组,从Repository类转换成URL[]数组可以分别以下几种情况 。
public static ClassLoader createClassLoader(Listrepositories, final ClassLoader parent) throws Exception { if (log.isDebugEnabled()) log.debug("Creating new class loader"); // Construct the "class path" for this class loader Set set = new LinkedHashSet (); if (repositories != null) { for (Repository repository : repositories) { if (repository.getType() == RepositoryType.URL) { URL url = buildClassLoaderUrl(repository.getLocation()); if (log.isDebugEnabled()) log.debug(" Including URL " + url); set.add(url); } else if (repository.getType() == RepositoryType.DIR) { File directory = new File(repository.getLocation()); directory = directory.getCanonicalFile(); if (!validateFile(directory, RepositoryType.DIR)) { continue; } URL url = buildClassLoaderUrl(directory); if (log.isDebugEnabled()) log.debug(" Including directory " + url); set.add(url); } else if (repository.getType() == RepositoryType.JAR) { File file=new File(repository.getLocation()); file = file.getCanonicalFile(); if (!validateFile(file, RepositoryType.JAR)) { continue; } URL url = buildClassLoaderUrl(file); if (log.isDebugEnabled()) log.debug(" Including jar file " + url); set.add(url); } else if (repository.getType() == RepositoryType.GLOB) { // 以*.jar 结尾 File directory=new File(repository.getLocation()); directory = directory.getCanonicalFile(); if (!validateFile(directory, RepositoryType.GLOB)) { continue; } if (log.isDebugEnabled()) log.debug(" Including directory glob " + directory.getAbsolutePath()); String filenames[] = directory.list(); if (filenames == null) { continue; } for (int j = 0; j < filenames.length; j++) { String filename = filenames[j].toLowerCase(Locale.ENGLISH); // 不以jar包结尾,忽略掉 if (!filename.endsWith(".jar")) continue; File file = new File(directory, filenames[j]); file = file.getCanonicalFile(); if (!validateFile(file, RepositoryType.JAR)) { continue; } if (log.isDebugEnabled()) log.debug(" Including glob jar file " + file.getAbsolutePath()); URL url = buildClassLoaderUrl(file); set.add(url); } } } } // Construct the class loader itself final URL[] array = set.toArray(new URL[set.size()]); if (log.isDebugEnabled()) for (int i = 0; i < array.length; i++) { log.debug(" location " + i + " is " + array[i]); } return AccessController.doPrivileged( new PrivilegedAction () { @Override public URLClassLoader run() { if (parent == null) // URLClassLoader是一个可以从指定目录或网络地址加载class的一个类加载器 return new URLClassLoader(array); else return new URLClassLoader(array, parent); } }); }
在Bootstrap类中有一个init()方法 。
public void init() throws Exception { // Set Catalina path // catalina.home表示安装目录 // catalina.base表示工作目录 setCatalinaHome(); setCatalinaBase(); // 初始化commonLoader、catalinaLoader、sharedLoader // 其中catalinaLoader、sharedLoader默认其实就是commonLoader initClassLoaders(); // 设置线程的所使用的类加载器,默认情况下就是commonLoader Thread.currentThread().setContextClassLoader(catalinaLoader); // 如果开启了SecurityManager,那么则要提前加载一些类 SecurityClassLoad.securityClassLoad(catalinaLoader); // Load our startup class and call its process() method // 加载Catalina类,并生成instance if (log.isDebugEnabled()) log.debug("Loading startup class"); System.out.println("=========catalinaLoader========"+catalinaLoader); Class> startupClass = catalinaLoader.loadClass ("org.apache.catalina.startup.Catalina"); Object startupInstance = startupClass.newInstance(); // Set the shared extensions class loader // 设置Catalina实例的父级类加载器为sharedLoader(默认情况下就是commonLoader) if (log.isDebugEnabled()) log.debug("Setting startup class properties"); String methodName = "setParentClassLoader"; Class> paramTypes[] = new Class[1]; paramTypes[0] = Class.forName("java.lang.ClassLoader"); Object paramValues[] = new Object[1]; paramValues[0] = sharedLoader; Method method = startupInstance.getClass().getMethod(methodName, paramTypes); method.invoke(startupInstance, paramValues); catalinaDaemon = startupInstance; }
有一段这样的代码值得注意
Class> startupClass =catalinaLoader.loadClass("org.apache.catalina.startup.Catalina"); Object startupInstance = startupClass.newInstance(); String methodName = "setParentClassLoader"; Class> paramTypes[] = new Class[1]; paramTypes[0] = Class.forName("java.lang.ClassLoader"); Object paramValues[] = new Object[1]; paramValues[0] = sharedLoader; Method method =startupInstance.getClass().getMethod(methodName, paramTypes); method.invoke(startupInstance, paramValues);
这段代码这么多,其实就做了两个事情
Catalina catalina = new Catalina();
catalina.setParentClassLoader(sharedLoader);
创建Catalina,并且设置其父类加载器为sharedLoader。为什么这么做呢?
发现问题没有。 我们用的是catalinaLoader去加载Catalina,但发现Catalina的类加载器竟然是系统类加载器Launcher$AppClassLoader,而不是catalinaLoader的类加载器URLClassLoader,这是为什么呢?之前提到过 应用程序类加载器(Application ClassLoader):也叫系统类加载器(System ClassLoader) 它负责加载用户类路径(CLASSPATH)指定的类库,因为我是用IDEA启动Bootstrap的,catalinaLoader去加载Catalina时,先委派给其父类AppClassLoader类加载去加载 ,而AppClassLoader能加载当前CLASSPATH下的类文件,而Catalina又在当前用户路径下。 因此Catalina由AppClassLoader去加载。从网上下载一个apache-tomcat-7.0.73,将其/apache-tomcat-7.0.73/lib/catalina.jar的Catalina.class文件中加两行代码
System.out.println(“Catalina类加载器=====” + this.getClass().getClassLoader().toString());
System.out.println(“Catalina类的父类加载器=====” + this.getClass().getClassLoader().getParent().toString());
启动tomcat
从启动日志中可以看出,当前Catalina类的加载器是java.net.URLClassLoader类加载器,也就是tomcat的catalinaLoader类加载器了。为什么呢?因为我们通过脚本去启动tomcat时,
先由catalinaLoader的父类系统类加载器去寻找Catalina,显然此时的CLASSPATH下没有org.apache.catalina.startup.Catalina类,而catalinaLoader在initClassLoaders()方法中指定了repositories为$ {catalina.base}/lib,$ {catalina.base}/lib/.jar,$ {catalina.home}/lib,${catalina.home}/lib/.jar,而lib目录下的catalina.jar包中有Catalina类,因此Catalina由catalinaLoader类加载器加载 。 再来看个例子。
将之前反射创建Catalina,并设置ParentClassLoader的代码直接用new ,看会出现什么情况。
Catalina catalina = new Catalina();
catalina.setParentClassLoader(sharedLoader);
catalinaDaemon = catalina;
替换后的class文件
启动tomcat
./catalina.sh start
此时连org.apache.catalina.startup.Catalina都找不到了。 为什么呢?Bootstrap由系统类加载器加载,而系统类加载器并没有指定$ {catalina.base}/lib,$ {catalina.base}/lib/.jar, $ {catalina.home}/lib,$ {catalina.home}/lib/.jar 作为类的查找路径,而之前也提到过。 类加载器还拥有全盘负责机制,即当一个类加载器加载一个类时, 这个类的所有依赖,引用和其他的所有类由这个类加载器加载 , 除非程序中显式的指定另外一个类加载器加载 。因为Bootstrap类由系统类加载器加载,在Bootstrap中new Catalina(),当然Catalina也由系统类加载器加载,而系统类加载器并没有指定tomcat目录下的包,因此找不到Catalina类,报java.lang.ClassNotFoundException: org.apache.catalina.startup.Catalina异常。当然啦,直接用idea启动tomcat是没有问题,因为在当前CLASSPATH下有Catalina类, 都由系统类加载器加载,而Bootstrap也由系统类加载器加载,因此tomcat启动正常。
之前在StandardContext启动的博客中分析过WebappLoader。
在其start()方法中
将/WEB-INF/classes和/WEB-INF/lib目录添加到WebappClassLoader的Repository中,以后将从Repository中寻找并加载类,接下来分析WebappClassLoader类。 而WebappClassLoader类的关系如下。
public class WebappClassLoader extends WebappClassLoaderBase { public WebappClassLoader() { super(); } public WebappClassLoader(ClassLoader parent) { super(parent); } @Override public WebappClassLoader copyWithoutTransformers() { WebappClassLoader result = new WebappClassLoader(getParent()); super.copyStateWithoutTransformers(result); try { result.start(); } catch (LifecycleException e) { throw new IllegalStateException(e); } return result; } }
真正的实现都在WebappClassLoaderBase类中。在WebappClassLoaderBase实现了loadClass()方法。
public Class> loadClass(String name) throws ClassNotFoundException { return (loadClass(name, false)); } public Class> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLockInternal(name)) { if (log.isDebugEnabled()) log.debug("loadClass(" + name + ", " + resolve + ")"); Class> clazz = null; // Log access to stopped classloader if (!started) { try { throw new IllegalStateException(); } catch (IllegalStateException e) { log.info(sm.getString("webappClassLoader.stopped", name), e); } } // (0) Check our previously loaded local class cache // 先检查该类是否已经被Webapp类加载器加载。 clazz = findLoadedClass0(name); // map if (clazz != null) { if (log.isDebugEnabled()) log.debug(" Returning class from cache"); if (resolve) resolveClass(clazz); return (clazz); } // (0.1) Check our previously loaded class cache // 该方法直接调用findLoadedClass0本地方法,findLoadedClass0方法会检查JVM缓存中是否加载过此类 clazz = findLoadedClass(name); // jvm 内存 if (clazz != null) { if (log.isDebugEnabled()) log.debug(" Returning class from cache"); if (resolve) resolveClass(clazz); return (clazz); } // (0.2) Try loading the class with the system class loader, to prevent // the webapp from overriding J2SE classes // 尝试通过系统类加载器(AppClassLoader)加载类,防止webapp重写JDK中的类 // 假设,webapp想自己去加载一个java.lang.String的类,这是不允许的,必须在这里进行预防。 try { clazz = j2seClassLoader.loadClass(name); // java.lang.Object if (clazz != null) { if (resolve) resolveClass(clazz); return (clazz); } } catch (ClassNotFoundException e) { // Ignore } // (0.5) Permission to access this class when using a SecurityManager if (securityManager != null) { int i = name.lastIndexOf('.'); if (i >= 0) { try { securityManager.checkPackageAccess(name.substring(0,i)); } catch (SecurityException se) { String error = "Security Violation, attempt to use " + "Restricted Class: " + name; if (name.endsWith("BeanInfo")) { // BZ 57906: suppress logging for calls from // java.beans.Introspector.findExplicitBeanInfo() log.debug(error, se); } else { log.info(error, se); } throw new ClassNotFoundException(error, se); } } } boolean delegateLoad = delegate || filter(name); // 委托--true // (1) Delegate to our parent if requested // 是否委派给父类去加载 if (delegateLoad) { if (log.isDebugEnabled()) log.debug(" Delegating to parent classloader1 " + parent); try { clazz = Class.forName(name, false, parent); if (clazz != null) { if (log.isDebugEnabled()) log.debug(" Loading class from parent"); if (resolve) resolveClass(clazz); return (clazz); } } catch (ClassNotFoundException e) { // Ignore } } // (2) Search local repositories // 从webapp应用内部进行加载 if (log.isDebugEnabled()) log.debug(" Searching local repositories"); try { clazz = findClass(name); // classes,lib if (clazz != null) { if (log.isDebugEnabled()) log.debug(" Loading class from local repository"); if (resolve) resolveClass(clazz); return (clazz); } } catch (ClassNotFoundException e) { // Ignore } // (3) Delegate to parent unconditionally // 如果webapp应用内部没有加载到类,那么无条件委托给父类进行加载 if (!delegateLoad) { if (log.isDebugEnabled()) log.debug(" Delegating to parent classloader at end: " + parent); try { clazz = Class.forName(name, false, parent); if (clazz != null) { if (log.isDebugEnabled()) log.debug(" Loading class from parent"); if (resolve) resolveClass(clazz); return (clazz); } } catch (ClassNotFoundException e) { // Ignore } } } throw new ClassNotFoundException(name); }
先来看Webapp本地缓存中是否已经加载这个类,如果已经加载,直接返回。
protected Class> findLoadedClass0(String name) { //com.luban.Test String path = binaryNameToPath(name, true); // com/luban/Test ResourceEntry entry = resourceEntries.get(path); if (entry != null) { return entry.loadedClass; } return (null); // FIXME - findLoadedResource() } private String binaryNameToPath(String binaryName, boolean withLeadingSlash) { // 1 for leading '/', 6 for ".class" // 为什么是path的长度 + 7呢?因为要在binaryName加前缀/ 和加后缀.class,刚好是7个字符 // 如 com.a.b.C类, 最终处理后得到/com/a/b/C.class 路径 StringBuilder path = new StringBuilder(7 + binaryName.length()); if (withLeadingSlash) { path.append('/'); } path.append(binaryName.replace('.', '/')); path.append(".class"); return path.toString(); }
接下来看j2seClassLoader类加载器由来。 来看WebappClassLoaderBase的构造方法
public WebappClassLoaderBase(ClassLoader parent) { // new一个URLClassLoader,传入一个空的URL数组 super(new URL[0], parent); // 获取当前类加载器的父加载器,就是传进来的parent ClassLoader p = getParent(); if (p == null) { p = getSystemClassLoader(); } this.parent = p; // java.lang.String是由BootstrapClassLoader加载的,所以一般都会返回null ClassLoader j = String.class.getClassLoader(); if (j == null) { // 拿到的就是AppClassLoader,然后获取到ExtClassLoader // 这里的目的也就是为了拿到ExtClassLoader j = getSystemClassLoader(); while (j.getParent() != null) { j = j.getParent(); } } this.j2seClassLoader = j; securityManager = System.getSecurityManager(); if (securityManager != null) { refreshPolicy(); } }
因为系统类加载器的父类加载器为扩展类加载器,因此最终拿到j2seClassLoader为扩展类加载器。
为什么要先由j2seClassLoader类加载器先加载呢?正如注释所言, 防止webapp重写JDK中的类,假设,webapp想自己去加载一个java.lang.String的类,这是不允许的,必须在这里进行预防。
接下来就是delegate变量的用途了。 我们不止一次在博客中提到过delegate的用途了,这里再重复一遍。
每个Web应用都有各自的Class类和Jar包,一般来说,在Tomcat启动时要准备好相应的类加载器, 包括加载策略及Class文件查找,方便后面对Web应用实例化Servlet对象通过类加载器加载相关的类,因为每个Web应用不仅要达到资源的互相隔离,还要能支持重新加载,所以这里需要为每个Web应用安排不同的类加载器对象加载,重加载时可以直接将旧的类加载器对象丢弃而使用新的。
StandardContext使用了一个继承了Loader接口的WebappLoader作为Web应用的类加载器, 作为Tomcat的Web应用的类加载器的实现, 它能检测是否有Web项目的Class被更改,然后自动重新加载,每个Web应用对应一个WebappLoader,每个WebappLoader互相隔离,各自包含类互相不可见。
如图9.7所示,WebappLoader的核心工作其实交给其内部的WebappClassLoader,它才是真正的类加载器工作的加载器, 它是一个自定义的类加载器,WebAppClassLoader继承了URLClassLoader,只需要把/WEB-INF/lib和/WEB-INF/classes目录下的类和Jar包以URL形式添加到URLClassLoader中即可, 后面就可以用该类加载器对类进行加载 。
WebappClassLoader类加载器是如何达到互相隔离的和实现重新加载的呢?
WebappClassLoader并没有遵循双亲委派机制,而是按自己的策略顺序加载类, 根据委托标识,加载分为两种。
图9.8是WEBappClassLoader和其他的类加载器关系结构图,可以看出 ,对于公共资源可共享,是属于Web应用资源则通过类加载器进行隔离 , 对于重加载的实现, 也比较清晰,只需要重新实例化一个WebappClassLoader对象并把原来的WebappLoader中的旧的转换掉即可完成重新加载功能,转换掉将被GC回收。
接下来看从WebappClassLoader中查找类的方式 。
public Class> findClass(String name) throws ClassNotFoundException { if (log.isDebugEnabled()) log.debug(" findClass(" + name + ")"); // Cannot load anything from local repositories if class loader is stopped if (!started) { throw new ClassNotFoundException(name); } // (1) Permission to define this class when using a SecurityManager if (securityManager != null) { int i = name.lastIndexOf('.'); if (i >= 0) { try { if (log.isTraceEnabled()) log.trace(" securityManager.checkPackageDefinition"); securityManager.checkPackageDefinition(name.substring(0,i)); } catch (Exception se) { if (log.isTraceEnabled()) log.trace(" -->Exception-->ClassNotFoundException", se); throw new ClassNotFoundException(name, se); } } } // Ask our superclass to locate this class, if possible // (throws ClassNotFoundException if it is not found) Class> clazz = null; try { if (log.isTraceEnabled()) log.trace(" findClassInternal(" + name + ")"); // 如果配置了searchExternalFirst为true,优先从外部仓库查找, // 并且有外部仓库,则从外部的仓库进行find if (hasExternalRepositories && searchExternalFirst) { try { clazz = super.findClass(name); } catch(ClassNotFoundException cnfe) { // Ignore - will search internal repositories next } catch(AccessControlException ace) { log.warn("WebappClassLoaderBase.findClassInternal(" + name + ") security exception: " + ace.getMessage(), ace); throw new ClassNotFoundException(name, ace); } catch (RuntimeException e) { if (log.isTraceEnabled()) log.trace(" -->RuntimeException Rethrown", e); throw e; } } // 找不到再自己find if ((clazz == null)) { try { // 从WEB-INF/class或WEB-INF/lib中查找 clazz = findClassInternal(name); } catch(ClassNotFoundException cnfe) { // 1. 如果没有外部仓库,但此时找不到类,则抛出ClassNotFoundException异常 // 2. 如果有外部仓库,并且配置了searchExternalFirst为true,优先从外部仓库中查找, // 而此时依然找不到类,则没有再查找的必要了,抛出ClassNotFoundException异常 if (!hasExternalRepositories || searchExternalFirst) { throw cnfe; } } catch(AccessControlException ace) { log.warn("WebappClassLoaderBase.findClassInternal(" + name + ") security exception: " + ace.getMessage(), ace); throw new ClassNotFoundException(name, ace); } catch (RuntimeException e) { if (log.isTraceEnabled()) log.trace(" -->RuntimeException Rethrown", e); throw e; } } // 如果此时依然没有找到类 // 并且没有配置优先从外部仓库中查找,同时外部仓库存在,则去外部仓库中找类 if ((clazz == null) && hasExternalRepositories && !searchExternalFirst) { try { clazz = super.findClass(name); } catch(AccessControlException ace) { log.warn("WebappClassLoaderBase.findClassInternal(" + name + ") security exception: " + ace.getMessage(), ace); throw new ClassNotFoundException(name, ace); } catch (RuntimeException e) { if (log.isTraceEnabled()) log.trace(" -->RuntimeException Rethrown", e); throw e; } } if (clazz == null) { if (log.isDebugEnabled()) log.debug(" --> Returning ClassNotFoundException"); throw new ClassNotFoundException(name); } } catch (ClassNotFoundException e) { if (log.isTraceEnabled()) log.trace(" --> Passing on ClassNotFoundException"); throw e; } // Return the class we have located if (log.isTraceEnabled()) log.debug(" Returning class " + clazz); if (log.isTraceEnabled()) { ClassLoader cl; if (Globals.IS_SECURITY_ENABLED){ cl = AccessController.doPrivileged( new PrivilegedGetClassLoader(clazz)); } else { cl = clazz.getClassLoader(); } log.debug(" Loaded by " + cl.toString()); } return (clazz); }
大家可能对hasExternalRepositories和searchExternalFirst这两个字段比较陌生,什么意思呢?从字面意思来看hasExternalRepositories表示有外部仓库的意思,而searchExternalFirst表示是否优先从外部仓库查找。 默认hasExternalRepositories和searchExternalFirst都为false,那什么时候hasExternalRepositories为true呢?在代码中寻寻觅觅。在WebappClassLoaderBase类中有一个这样的方法。
public void addRepository(String repository) { // Ignore any of the standard repositories, as they are set up using // either addJar or addRepository if (repository.startsWith("/WEB-INF/lib") || repository.startsWith("/WEB-INF/classes")) return; // Add this repository to our underlying class loader try { URL url = new URL(repository); super.addURL(url); hasExternalRepositories = true; repositoryURLs = null; } catch (MalformedURLException e) { IllegalArgumentException iae = new IllegalArgumentException ("Invalid repository: " + repository); iae.initCause(e); throw iae; } }
向WebappClassLoader中添加非/WEB-INF/lib和/WEB-INF/classes目录下的的repository时,此时表示有外部仓库。hasExternalRepositories设置为true,那什么时候调用addRepository()方法呢?在WebappLoader中有一个addRepository()方法。
public void addRepository(String repository) { if (log.isDebugEnabled()) log.debug(sm.getString("webappLoader.addRepository", repository)); for (int i = 0; i < repositories.length; i++) { if (repository.equals(repositories[i])) return; } String results[] = new String[repositories.length + 1]; for (int i = 0; i < repositories.length; i++) results[i] = repositories[i]; results[repositories.length] = repository; repositories = results; if (getState().isAvailable() && (classLoader != null)) { classLoader.addRepository(repository); if( loaderRepositories != null ) loaderRepositories.add(repository); setClassPath(); } }
这个方法主要就是用repositories来存储仓库地址,其实repositories也不用那么麻烦,用数组来存储,其实直接用ArrayList就好了吧,可能tomcat为了节省内存考虑吧,只是扩容麻烦一点,没有必要浪费ArrayList初始化时多余申请的内存空间。 那WebappLoader中有一个addRepository()方法又是何时调用呢?
终于在VirtualWebappLoader的startInternal()方法中找到了addRepository()方法的调用,有了这些理论基础,就可以自己写例子了。
创建Servlet ,在Servlet中初始化获取其ClassLoader
将servelet-test项目添加到Tomcat的webapps目录下,此时直接启动Tomcat ,访问Servlet ,肯定会报类没有发现,因为 web-fragment-test-2.0-SNAPSHOT.jar包和servlet-test项目没有任何关系。
在server.xml中添加,当然可以指定searchExternalFirst为true
searchExternalFirst="true" virtualClasspath="/Users/quyixiao/github/web-fragment-test/target/web-fragment-test-2.0-SNAPSHOT.jar" >
为什么这么配置呢?请看ContextRuleSet
protected Class> findClassInternal(String name) throws ClassNotFoundException { if (!validate(name)) throw new ClassNotFoundException(name); ResourceEntry entry = null; String path = binaryNameToPath(name, true); // com/luban/Test if (securityManager != null) { PrivilegedActiondp = new PrivilegedFindResourceByName(name, path, true); entry = AccessController.doPrivileged(dp); } else { // ---->ResourceEntry---->loadedClass entry = findResourceInternal(name, path, true); } if (entry == null) throw new ClassNotFoundException(name); Class> clazz = entry.loadedClass; if (clazz != null) return clazz; synchronized (getClassLoadingLockInternal(name)) { // 双重较验锁 clazz = entry.loadedClass; if (clazz != null) return clazz; if (entry.binaryContent == null) throw new ClassNotFoundException(name); if (this.transformers.size() > 0) { // If the resource is a class just being loaded, decorate it // with any attached transformers // Ignore leading '/' and trailing CLASS_FILE_SUFFIX // Should be cheaper than replacing '.' by '/' in class name. String internalName = path.substring(1, path.length() - CLASS_FILE_SUFFIX.length()); for (ClassFileTransformer transformer : this.transformers) { try { byte[] transformed = transformer.transform( this, internalName, null, null, entry.binaryContent ); if (transformed != null) { entry.binaryContent = transformed; } } catch (IllegalClassFormatException e) { log.error(sm.getString("webappClassLoader.transformError", name), e); return null; } } } // Looking up the package String packageName = null; int pos = name.lastIndexOf('.'); if (pos != -1) packageName = name.substring(0, pos); Package pkg = null; if (packageName != null) { pkg = getPackage(packageName); // Define the package (if null) if (pkg == null) { try { if (entry.manifest == null) { definePackage(packageName, null, null, null, null, null, null, null); } else { definePackage(packageName, entry.manifest, entry.codeBase); } } catch (IllegalArgumentException e) { // Ignore: normal error due to dual definition of package } pkg = getPackage(packageName); } } if (securityManager != null) { // Checking sealing if (pkg != null) { boolean sealCheck = true; if (pkg.isSealed()) { sealCheck = pkg.isSealed(entry.codeBase); } else { sealCheck = (entry.manifest == null) || !isPackageSealed(packageName, entry.manifest); } if (!sealCheck) throw new SecurityException ("Sealing violation loading " + name + " : Package " + packageName + " is sealed."); } } try { clazz = defineClass(name, entry.binaryContent, 0, entry.binaryContent.length, new CodeSource(entry.codeBase, entry.certificates)); } catch (UnsupportedClassVersionError ucve) { throw new UnsupportedClassVersionError( ucve.getLocalizedMessage() + " " + sm.getString("webappClassLoader.wrongVersion", name)); } // Now the class has been defined, clear the elements of the local // resource cache that are no longer required. entry.loadedClass = clazz; entry.binaryContent = null; entry.codeBase = null; entry.manifest = null; entry.certificates = null; // Retain entry.source in case of a getResourceAsStream() call on // the class file after the class has been defined. } return clazz; }
在分析findResourceInternal()这个方法之前,先来看这一段代码。
if (this.transformers.size() > 0) { String internalName = path.substring(1, path.length() - CLASS_FILE_SUFFIX.length()); for (ClassFileTransformer transformer : this.transformers) { try { byte[] transformed = transformer.transform( this, internalName, null, null, entry.binaryContent ); if (transformed != null) { entry.binaryContent = transformed; } } catch (IllegalClassFormatException e) { log.error(sm.getString("webappClassLoader.transformError", name), e); return null; } } }
一看到transform()方法,不就是改字节码结构嘛,如果我们定义了transformers,在加载过程中会去修改类的字节码结构,那怎样使用呢?之前写过一篇这样的博客。 你不知道的java-佛怒轮回 ,里面定义了一个类TtlVariableTransformlet,将所有的局步变量加到ThreadLocal中去,关于为什么要这么做,可以去看之前的博客,但在这里,我们要借助于之前的字节码修改类完成我们的测试 。
在Tomcat中添加javassist-3.23.1-GA包
添加字节码修改器类
创建MyVirtualWebappLoader,这个类在启动时将自定义TtlTransformer添加到webappClassLoader中。
public class MyVirtualWebappLoader extends WebappLoader { @Override protected void startInternal() throws LifecycleException { super.startInternal(); try { final List> transformletList = new ArrayList >(); //添加 my Transformlet transformletList.add(TtlVariableTransformlet.class); final ClassFileTransformer transformer = new TtlTransformer(transformletList); WebappClassLoader webappClassLoader = (WebappClassLoader)getClassLoader(); webappClassLoader.addTransformer(transformer); } catch (Exception e) { e.printStackTrace(); } } }
3. 修改catalina.base/conf/server.xml中Loader的className为我们自定义的org.apache.catalina.loader.MyVirtualWebappLoader。
public class HelloServlet extends HttpServlet { public static boolean flag = true; @Override public void init() throws ServletException { System.out.println("HelloServlet 初始化方法调用"); } public HelloServlet() { } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { ClassLoader loader = this.getClass().getClassLoader(); System.out.println("====HelloServlet类加载器=========" + loader); Class clazz = loader.loadClass("com.example.servelettest.Test"); Object o = clazz.newInstance(); System.out.println("=========" + o); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("doGet方法执行"); this.doGet(req, resp); } }
当加载/com/example/servelettest/Test.class类时。会通过我们自定义的TtlVariableTransformlet修改类的字节码。
而在TtlVariableTransformlet类的结尾处将修改后的字节码输出到文件中。
当然看一下之前Test类文件内容。
看一下字节码修改之后的文件内容。
我相信现在你应该对byte[] transformed = transformer.transform(this, internalName, null, null, entry.binaryContent);其实这个例子的主要目的就是当我们向类加载器中添加ClassFileTransformer时,在容器中每次获取类时,都会被我们添加进去的ClassFileTransformer修改类字节码结构,之后再加载到Jvm中。
接下来看findResourceInternal()方法实现。
protected ResourceEntry findResourceInternal(final String name, final String path, final boolean manifestRequired) { if (!started) { log.info(sm.getString("webappClassLoader.stopped", name)); return null; } if ((name == null) || (path == null)) return null; JarEntry jarEntry = null; // Need to skip the leading / to find resources in JARs String jarEntryPath = path.substring(1); // ResourceEntry entry = resourceEntries.get(path); if (entry != null) { if (manifestRequired && entry.manifest == MANIFEST_UNKNOWN) { // This resource was added to the cache when a request was made // for the resource that did not need the manifest. Now the // manifest is required, the cache entry needs to be updated. synchronized (jarFiles) { if (openJARs()) { for (int i = 0; i < jarFiles.length; i++) { jarEntry = jarFiles[i].getJarEntry(jarEntryPath); if (jarEntry != null) { try { // 当前这个类对应的jar中拥有哪些类 entry.manifest = jarFiles[i].getManifest(); } catch (IOException ioe) { // Ignore } break; } } } } } return entry; } int contentLength = -1; InputStream binaryStream = null; boolean isClassResource = path.endsWith(CLASS_FILE_SUFFIX); boolean isCacheable = isClassResource; if (!isCacheable) { isCacheable = path.startsWith(SERVICES_PREFIX); } // jarFiles表示当前webapp下的web-inf/lib下的jar文件路径的数组 int jarFilesLength = jarFiles.length; // class文件仓库,默认就一个"web-inf/classes" int repositoriesLength = repositories.length; int i; Resource resource = null; boolean fileNeedConvert = false; for (i = 0; (entry == null) && (i < repositoriesLength); i++) { try { String fullPath = repositories[i] + path; Object lookupResult = resources.lookup(fullPath); if (lookupResult instanceof Resource) { resource = (Resource) lookupResult; } // Note : Not getting an exception here means the resource was // found // 如果没有抛异常,就表示要加载的类存在 //System.out.println("fullPath="+fullPath); ResourceAttributes attributes = (ResourceAttributes) resources.getAttributes(fullPath); contentLength = (int) attributes.getContentLength(); String canonicalPath = attributes.getCanonicalPath(); if (canonicalPath != null) { // we create the ResourceEntry based on the information returned // by the DirContext rather than just using the path to the // repository. This allows to have smart DirContext implementations // that "virtualize" the docbase (e.g. Eclipse WTP) entry = findResourceInternal(new File(canonicalPath), ""); } else { // probably a resource not in the filesystem (e.g. in a // packaged war) entry = findResourceInternal(files[i], path); } // 当前这个文件的最近修改时间 entry.lastModified = attributes.getLastModified(); if (resource != null) { try { binaryStream = resource.streamContent(); } catch (IOException e) { return null; } if (needConvert) { if (path.endsWith(".properties")) { fileNeedConvert = true; } } // Register the full path for modification checking // Note: Only syncing on a 'constant' object is needed synchronized (allPermission) { int j; // 将当前加载的类的最近一次修改时间添加到lastModifiedDates数组中 long[] result2 = new long[lastModifiedDates.length + 1]; for (j = 0; j < lastModifiedDates.length; j++) { result2[j] = lastModifiedDates[j]; } result2[lastModifiedDates.length] = entry.lastModified; lastModifiedDates = result2; // 将当前加载的类的路径添加到paths数组中 String[] result = new String[paths.length + 1]; for (j = 0; j < paths.length; j++) { result[j] = paths[j]; } result[paths.length] = fullPath; paths = result; } } } catch (NamingException e) { // Ignore } } // notFoundResources表示曾经在jar中找过这个类,但是没有找到 if ((entry == null) && (notFoundResources.containsKey(name))) return null; synchronized (jarFiles) { try { if (!openJARs()) { return null; } // 遍历jar包 for (i = 0; (entry == null) && (i < jarFilesLength); i++) { // 直接从jar中获取class对应的jarEntry jarEntry = jarFiles[i].getJarEntry(jarEntryPath); // if (jarEntry != null) { entry = new ResourceEntry(); try { entry.codeBase = getURI(jarRealFiles[i]); entry.source = UriUtil.buildJarUrl(entry.codeBase.toString(), jarEntryPath); entry.lastModified = jarRealFiles[i].lastModified(); } catch (MalformedURLException e) { return null; } contentLength = (int) jarEntry.getSize(); try { if (manifestRequired) { entry.manifest = jarFiles[i].getManifest(); } else { entry.manifest = MANIFEST_UNKNOWN; } binaryStream = jarFiles[i].getInputStream(jarEntry); } catch (IOException e) { return null; } // Extract resources contained in JAR to the workdir if (antiJARLocking && !(path.endsWith(CLASS_FILE_SUFFIX))) { byte[] buf = new byte[1024]; File resourceFile = new File(loaderDir, jarEntry.getName()); if (!resourceFile.exists()) { Enumerationentries = jarFiles[i].entries(); while (entries.hasMoreElements()) { JarEntry jarEntry2 = entries.nextElement(); if (!(jarEntry2.isDirectory()) && (!jarEntry2.getName().endsWith(CLASS_FILE_SUFFIX))) { resourceFile = new File(loaderDir, jarEntry2.getName()); try { if (!resourceFile.getCanonicalPath().startsWith( canonicalLoaderDir)) { throw new IllegalArgumentException( sm.getString("webappClassLoader.illegalJarPath", jarEntry2.getName())); } } catch (IOException ioe) { throw new IllegalArgumentException( sm.getString("webappClassLoader.validationErrorJarPath", jarEntry2.getName()), ioe); } File parentFile = resourceFile.getParentFile(); if (!parentFile.mkdirs() && !parentFile.exists()) { // Ignore the error (like the IOExceptions below) } FileOutputStream os = null; InputStream is = null; try { is = jarFiles[i].getInputStream(jarEntry2); os = new FileOutputStream(resourceFile); while (true) { int n = is.read(buf); if (n <= 0) { break; } os.write(buf, 0, n); } resourceFile.setLastModified(jarEntry2.getTime()); } catch (IOException e) { // Ignore } finally { try { if (is != null) { is.close(); } } catch (IOException e) { // Ignore } try { if (os != null) { os.close(); } } catch (IOException e) { // Ignore } } } } } } } } // 从jar中没有找到 if (entry == null) { synchronized (notFoundResources) { notFoundResources.put(name, name); } return null; } /* Only cache the binary content if there is some content * available one of the following is true: * a) It is a class file since the binary content is only cached * until the class has been loaded * or * b) The file needs conversion to address encoding issues (see * below) * or * c) The resource is a service provider configuration file located * under META=INF/services * * In all other cases do not cache the content to prevent * excessive memory usage if large resources are present (see * https://bz.apache.org/bugzilla/show_bug.cgi?id=53081). */ // .class文件的字节码内容是需要缓存到resourceEntries中的 // .properties文件结尾,并且needConvert为true时,该文件的byte[]也需要缓存到resourceEntries中 if (binaryStream != null && (isCacheable || fileNeedConvert)) { byte[] binaryContent = new byte[contentLength]; int pos = 0; try { while (true) { int n = binaryStream.read(binaryContent, pos, binaryContent.length - pos); if (n <= 0) break; pos += n; } } catch (IOException e) { log.error(sm.getString("webappClassLoader.readError", name), e); return null; } if (fileNeedConvert) { // Workaround for certain files on platforms that use // EBCDIC encoding, when they are read through FileInputStream. // See commit message of rev.303915 for details // http://svn.apache.org/viewvc?view=revision&revision=303915 String str = new String(binaryContent,0,pos); try { binaryContent = str.getBytes(CHARSET_UTF8); } catch (Exception e) { return null; } } entry.binaryContent = binaryContent; // The certificates are only available after the JarEntry // associated input stream has been fully read if (jarEntry != null) { entry.certificates = jarEntry.getCertificates(); } } } finally { if (binaryStream != null) { try { binaryStream.close(); } catch (IOException e) { /* Ignore */} } } } // Add the entry in the local resource repository synchronized (resourceEntries) { // Ensures that all the threads which may be in a race to load // a particular class all end up with the same ResourceEntry // instance ResourceEntry entry2 = resourceEntries.get(path); if (entry2 == null) { resourceEntries.put(path, entry); } else { entry = entry2; } } return entry; }
这个代码看上去那么多,其实只是先从WEB-INF/classes下查找类文件信息,如果没有找到,再从WEB-INF/lib/*.jar中查找,如果找到了,则将类字节码信息,最后修改时间,代码路径,manifest等封装成ResourceEntry对象保存到resourceEntries,则下次查找,先从resourceEntries中查找。当然最终调用defineClass()方法将类加载到Jvm中。
关于jsp这一块,之前在分析 Tomcat 源码解析一JSP编译器Jasper-佛怒火莲(上) 时做过详细的分析,而这里只是做一个小补充。先来看一个例子
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>Insert title here AServlet <% System.out.println("====aservlet类加载器=========" + this.getClass().getClassLoader().toString()); %>
在编译好jsp后,会在工作目录下生成aservlet_jsp.java文件,默认通过JDTCompile编译成aservlet_jsp.class文件
在JspServletWrapper的service()方法调用时,会触发jsp转化为servlet之后的类加载,如下所示。
其中有一行很重要的代码 servlet = (Servlet) instanceManager.newInstance(ctxt.getFQCN(), ctxt.getJspLoader());用实例管理器来加载类,但传入了JasperLoader。 getJspLoader是如何实现的呢?如果jspLoader为空,则创建一个JasperLoader,并且指定加载路径为WEB应用的工作目录 。
有了这些理论知识,再来理解StandardContext在启动过程中startInternal()方法的oldCCL = bindThread(); 和unbindThread(oldCCL);就很容易理解了。
protected ClassLoader bindThread() { ClassLoader oldContextClassLoader = Thread.currentThread().getContextClassLoader(); // 如果Context没有DirContext,其实这个Context也没什么用 if (getResources() == null) return oldContextClassLoader; // 设置线程的ClassLoader if (getLoader() != null && getLoader().getClassLoader() != null) { Thread.currentThread().setContextClassLoader (getLoader().getClassLoader()); } DirContextURLStreamHandler.bindThread(getResources()); if (isUseNaming()) { try { ContextBindings.bindThread(this, this); } catch (NamingException e) { // Silent catch, as this is a normal case during the early // startup stages } } return oldContextClassLoader; }
在bindThread()方法中有一行加粗代码特别重要,Thread.currentThread().setContextClassLoader(getLoader().getClassLoader()); 设置线程上下文类加载器,为什么这么做呢?后面再来分析。再来看unbindThread()方法的实现。
protected void unbindThread(ClassLoader oldContextClassLoader) { if (isUseNaming()) { ContextBindings.unbindThread(this, this); } DirContextURLStreamHandler.unbindThread(); Thread.currentThread().setContextClassLoader(oldContextClassLoader); }
例1 ,既然设置了设置了线程上下文类加载器,那DefaultInstanceManager的类加载器是什么呢?
结果依然是系统类加载器,为什么呢?因为我是用IDEA启动的,而所有系统类加载器能加载所有CLASSPATH下的类,因此由系统类加载器加载,如果我们用脚本启动,那此时肯定是Common类加载器,因为Common类加载器指定加载$ {catalina.base}/lib,$ {catalina.base}/lib/.jar,$ {catalina.home}/lib,$ {catalina.home}/lib/.jar包下的类,而DefaultInstanceManager肯定属于这些包下,因此其类加载器为Common类加载器。
例2 , 在servlet-test项目下创建两个监听器, com.example.servelettest.DataSourceMethodListener,和com.luban.DataSourceFiledListener,但不一样的是, DataSourceMethodListener属于servlet-test项目,而DataSourceFiledListener在Tomcat项目中。
启动项目,发现 DataSourceMethodListener 类加载器为WebappClassLoader, 而DataSourceFiledListener类加载器为sun.misc.Launcher$AppClassLoader@18b4aac2
为什么呢?先找到DataSourceMethodListener和DataSourceFiledListener实例化的地方。
进入newInstance()方法。
public Object newInstance(String className) throws IllegalAccessException, InvocationTargetException, NamingException, InstantiationException, ClassNotFoundException, IllegalArgumentException, NoSuchMethodException, SecurityException { Class> clazz = loadClassMaybePrivileged(className, classLoader); return newInstance(clazz.getDeclaredConstructor().newInstance(), clazz); }
这里的classLoader为WebappClassLoader,而delegate默认为false,在实例化DataSourceMethodListener和DataSourceFiledListener类时先从WebappClassLoader中去找,而WebappClassLoader只有WEB项目下的/WEB-INF/classes和WEB-INF/lib下所有jar包,显然DataSourceMethodListener类在servlet-test项目下,因此在servlet-test的/WEB-INF/classes下能找到DataSourceMethodListener.class文件,因此DataSourceMethodListener由WebappClassLoader加载,但定义在Tomcat项目下,由系统类加载器加载,因此当找不到DataSourceFiledListener类时,就委派给祖先类去查找,最终在系统类加载器中找到了DataSourceFiledListener,因此DataSourceFiledListener是由系统类加载器加载 。
接下来看普通的Servlet又是如何加载的呢?
通过InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager();拿到Wrapper的父类StandardContext的实例管理器。 再调用其 servlet = (Servlet) instanceManager.newInstance(servletClass);方法,而newInstance()方法中的classLoader不就是WebappClassLoader类加载器不?这也就是为什么Web项目下的Servlet是由WebappClassLoader加载 。 最终发现bindThread()和unbindThread()两个类和我们的Servlet,监听器加载并没有关系,但可以留意一下,在代码其他很多地方用到了线程上下文类加载器,在后面的博客中遇到再来分析这绑定线程上下文类加载器有什么作用了吧。
关于Tomcat的类加载器到这里又告一段落了,下一篇博客见。
类加载器属于JVM级别的设计,我们很多时候基本不会与它打交道,假如你想深入了解Tomcat内核或设计开发自己的框架或中间件,那你必须熟悉类加载器的相关机制,在现实的设计中根据实际情况利用类加载器可以提供类库的隔离及共享,保证软件的不同级别及逻辑分割不会互相影响,提供更好的安全性。
本博客的大量理论知识都来源于《Tomcat内核设计剖析》,有兴趣可以去看原著,我觉得原著写得更详细更好。
本文用到的代码 。
https://github.com/quyixiao/tomcat.git
https://github.com/quyixiao/web-fragment-test.git