类加载器应用

2022-05-29  本文已影响0人  程序员札记

类加载器在编码中应该如何应用了?在JDK源码中最典型的应用Java SPI服务发现机制,业务代码中的应用就是自定义类加载器,典型的如Tomcat的类加载器体系,下面逐一讲解。

Thread contextClassLoader

contextClassLoader是Thread类的一个私有属性,并不是一个单独的ClassLoader子类,可通过getContextClassLoader方法获取或者通过setContextClassLoader方法修改,主线程在启动的时候默认设置成AppClassLoader,子线程初始化时默认继承父线程的类加载器。这个类加载器有啥用了?这个主要是为Servlet容器如Tomcat服务的,因为Servlet容器都是一个线程处理一个http请求,不同的http请求可能请求完全独立的不同的应用。为了保证不同应用间的类隔离,单个线程在处理针对某个应用的http请求时需要使用针对该应用的类加载器而不能是容器主线程使用的类加载器,即需要通过Thread对象来传递和保存目标类加载器的引用。在创建子线程的init方法中contextClassLoader的设置代码如下:


image.png

其中parent就是通过currentThread()方法获取的父线程对象,其get和set方法的源码如下:

public ClassLoader getContextClassLoader() {
        if (contextClassLoader == null)
            return null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            ClassLoader.checkClassLoaderPermission(contextClassLoader,
                                                   Reflection.getCallerClass());
        }
        return contextClassLoader;
}
 
public void setContextClassLoader(ClassLoader cl) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission("setContextClassLoader"));
        }
        contextClassLoader = cl;
}

跟一般的get和set方法实现一样,在此基础上增加了安全校验相关。contextClassLoader是在代码中应用类加载器加载目标类的正确选择,不建议使用SystemClassLoader,因为SystemClassLoader是全局共享的,默认是AppClassLoader实例,而contextClassLoader是线程相关的,提供了更大的灵活性。

Java SPI机制

SPI机制约定与实现

Java SPI全称是Service Provider Interface,即服务提供接口,目的是接口与实现分离,可以灵活方便的替换具体的实现类,接口由Java定义,如JDBC、JCE、JNDI、JAXP 和 JBI等标准接口,具体的实现由开发商提供,如Mysql的JDBC接口实现就是常用的mysql-connector驱动包。在JAVA SPI机制中约定,当服务的提供者(如某个新的日志组件),实现了某个服务接口之后,需要在jar包的META-INF/services目录中同时创建一个以该服务接口命名的文件,文件中只有一行字符串记录该服务接口对应的具体实现类的全限定类名,以mysql-connector驱动包为例说明,如下图:

image.png

文件内容是:

image.png

那么Java是如何定位找到某个接口的实现类了?Java提供了一个工具类ServiceLoader,通过读取jar包中配置文件的内容得到具体的实现类,在通过反射构造具体的实例。

ServiceLoader的实现如下:

public static <S> ServiceLoader<S> load(Class<S> service) {
     //使用当前线程的ContextClassLoader,主要为了兼容Servlet容器
     ClassLoader cl = Thread.currentThread().getContextClassLoader();
     //load方法实际是构造了一个新的ServiceLoader
     return ServiceLoader.load(service, cl);
 }

  public static <S> ServiceLoader<S> load(Class<S> service,
                                         ClassLoader loader)
 {
     return new ServiceLoader<>(service, loader);
 }

 public void reload() {
     //providers是ServiceLoader的私有属性,LinkedHashMap类,用于按照初始化顺序保存已经初始化的类
     providers.clear();
     //lookupIterator是ServiceLoader的私有属性,私有的内部类LazyIterator,用于实现惰性加载
     lookupIterator = new LazyIterator(service, loader);
 }

 private ServiceLoader(Class<S> svc, ClassLoader cl) {
     service = Objects.requireNonNull(svc, "Service interface cannot be null");
     //未指定类加载器,默认使用getSystemClassLoader()返回的系统类加载器,即AppClassLoader
     loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
     acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
     reload();
 }

 //DriverManager调用此方法完成驱动类加载
 public Iterator<S> iterator() {
     return new Iterator<S>() {
         Iterator<Map.Entry<String,S>> knownProviders
             = providers.entrySet().iterator();

         public boolean hasNext() {
             if (knownProviders.hasNext())
                 return true;

             return lookupIterator.hasNext();
         }

         public S next() {
             //如果链表中还有下一个则返回链表中的元素,否则调用lookupIterator查找下一个
             if (knownProviders.hasNext())
                 return knownProviders.next().getValue();
             return lookupIterator.next();
         }

         public void remove() {
             throw new UnsupportedOperationException();
         }

     };
 }

 private class LazyIterator
     implements Iterator<S>
 {

     Class<S> service;
     ClassLoader loader;
     Enumeration<URL> configs = null;
     Iterator<String> pending = null;
     String nextName = null;

     private LazyIterator(Class<S> service, ClassLoader loader) {
         this.service = service;
         this.loader = loader;
     }

     private boolean hasNextService() {
         if (nextName != null) {
             return true;
         }
         if (configs == null) {
             try {
                 //PREFIX是字符串常量,"META-INF/services/",此处是拼出来完整的service文件名
                 String fullName = PREFIX + service.getName();
                 //查找service文件
                 if (loader == null)
                     configs = ClassLoader.getSystemResources(fullName);
                 else
                     configs = loader.getResources(fullName);
             } catch (IOException x) {
                 fail(service, "Error locating configuration files", x);
             }
         }
         //逐一读取配置文件,并遍历配置文件中的类名
         while ((pending == null) || !pending.hasNext()) {
             if (!configs.hasMoreElements()) {
                 return false;
             }
             //读取并解析service文件中实现类的类名,hasNext为false即当前配置文件读取完成会进入循环重新解析下一个配置文件
             pending = parse(service, configs.nextElement());
         }
         //nextName即实现类的类名
         nextName = pending.next();
         return true;
     }

     private S nextService() {
         if (!hasNextService())
             throw new NoSuchElementException();
         String cn = nextName;
         nextName = null;
         Class<?> c = null;
         try {
             //通过ClassLoader加载目标类
             c = Class.forName(cn, false, loader);
         } catch (ClassNotFoundException x) {
             fail(service,
                  "Provider " + cn + " not found");
         }
         if (!service.isAssignableFrom(c)) {
             fail(service,
                  "Provider " + cn  + " not a subtype");
         }
         try {
             //构造一个目标类的实例
             S p = service.cast(c.newInstance());
             providers.put(cn, p);
             return p;
         } catch (Throwable x) {
             fail(service,
                  "Provider " + cn + " could not be instantiated",
                  x);
         }
         throw new Error();          // This cannot happen
     }

     public boolean hasNext() {
         if (acc == null) {
             return hasNextService();
         } else {
             PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                 public Boolean run() { return hasNextService(); }
             };
             return AccessController.doPrivileged(action, acc);
         }
     }

     public S next() {
         if (acc == null) {
             return nextService();
         } else {
             PrivilegedAction<S> action = new PrivilegedAction<S>() {
                 public S run() { return nextService(); }
             };
             return AccessController.doPrivileged(action, acc);
         }
     }

     public void remove() {
         throw new UnsupportedOperationException();
     }

 }

 //判断目标对象的实例是否实现了目标接口,如果是则强转
 public T cast(Object obj) {
     if (obj != null && !isInstance(obj))
         throw new ClassCastException(cannotCastMsg(obj));
     return (T) obj;
 }

用于解析配置文件的parseLine方法的实现如下:

private Iterator<String> parse(Class<?> service, URL u)
        throws ServiceConfigurationError
    {
        InputStream in = null;
        BufferedReader r = null;
        ArrayList<String> names = new ArrayList<>();
        try {
            //u是目标配置文件的资源地址
            in = u.openStream();
            r = new BufferedReader(new InputStreamReader(in, "utf-8"));
            int lc = 1;
            //解析配置文件,直到文件解析完成
            while ((lc = parseLine(service, u, r, lc, names)) >= 0);
        } catch (IOException x) {
            fail(service, "Error reading configuration file", x);
        } finally {
            try {
                if (r != null) r.close();
                if (in != null) in.close();
            } catch (IOException y) {
                fail(service, "Error closing configuration file", y);
            }
        }
        //返回已经解析的类名列表
        return names.iterator();
    }
 
 
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
                          List<String> names)
        throws IOException, ServiceConfigurationError
    {
        String ln = r.readLine();
        //返回-1 跳出循环
        if (ln == null) {
            return -1;
        }
        int ci = ln.indexOf('#');
        //如果有# 则取#之前的内容
        if (ci >= 0) ln = ln.substring(0, ci);
        ln = ln.trim();
        int n = ln.length();
        if (n != 0) {
            //如果包含空格或者\t则报错格式非法
            if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
                //fail方法就是对外抛出异常的
                fail(service, u, lc, "Illegal configuration-file syntax");
            int cp = ln.codePointAt(0);
            //是否是java允许的开始标识符,如必须是数字或者字母
            if (!Character.isJavaIdentifierStart(cp))
                fail(service, u, lc, "Illegal provider-class name: " + ln);
            //逐一遍历类名,检查是否包含非法标识符
            for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
                cp = ln.codePointAt(i);
                if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
                    fail(service, u, lc, "Illegal provider-class name: " + ln);
            }
            //providers是已加载的服务,names是当前配置文件已经解析过的类名
            if (!providers.containsKey(ln) && !names.contains(ln))
                names.add(ln);
        }
        return lc + 1;
    }
 
 private static void fail(Class<?> service, String msg)
        throws ServiceConfigurationError
    {
        throw new ServiceConfigurationError(service.getName() + ": " + msg);
    }
 
private static void fail(Class<?> service, URL u, int line, String msg)
        throws ServiceConfigurationError
    {
        fail(service, u + ":" + line + ": " + msg);
    }

自定义服务提供接口

自定义一个Say接口,并添加3个实现类,如下:

package serviceLoad;
 
public interface Say {
 
    String sayHello();
}
 
package serviceLoad;
 
public class SayImpl implements Say {
    @Override
    public String sayHello() {
        return "Hello World";
    }
}
package serviceLoad;
 
public class SayImpl2 implements Say {
    @Override
    public String sayHello() {
        return "Hello World2";
    }
}
package serviceLoad;
 
public class SayImpl3 implements Say {
    @Override
    public String sayHello() {
        return "Hello World3";
    }
}

在resource文件夹下建META-INF/services目录,并添加一个名为serviceLoad的文件夹,里面的内容是三个实现类的类名,如下:


image.png

测试用例如下:

@Test
    public void test() throws Exception {
 
        //这里使用默认的系统类加载器加载实现类
        ServiceLoader<Say> services=ServiceLoader.load(Say.class);
        //生产环境为了兼容类似于Servlet容器下不同线程类加载器可能不同的情形使用ContextClassLoader
ServiceLoader<Say> services=ServiceLoader.load(Say.class,Thread.currentThread().getContextClassLoader());
        for(Say say:services){
            System.out.println(say.sayHello());
        }
 
 
    }

Google AutoService

上一节的示例中,我们是手工创建了一个services目录和配置文件,容易出错或者遗漏,为此Google提供了一个AutoService的注解,可自动创建service目录和对应的配置文件,只需在服务提供接口上增加一个注解即可,如下:

package serviceLoad;
 
 
import com.google.auto.service.AutoService;
 
@AutoService(Say.class)
public class SayImpl implements Say {
    @Override
    public String sayHello() {
        return "Hello World";
    }
}

当测试用例运行的时候会自动创建services目录和对应的配置文件,其核心实现只有一个类AutoServiceProcessor,该类继承自AbstractProcessor接口,JVM会回调该接口,允许用户自定义对特定注解的处理逻辑,其关键逻辑如下:

 @Override
  //改写AbstractProcessor的注解处理方法
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    try {
      return processImpl(annotations, roundEnv);
    } catch (Exception e) {
      // We don't allow exceptions of any kind to propagate to the compiler
      StringWriter writer = new StringWriter();
      e.printStackTrace(new PrintWriter(writer));
      fatalError(writer.toString());
      return true;
    }
  }
 
  private boolean processImpl(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    //如果注解已经解析并处理完成则生成配置文件
    if (roundEnv.processingOver()) {
      generateConfigFiles();
    } else {
       //解析注解,会验证注解标注的类是否是目标接口的实现类
      processAnnotations(annotations, roundEnv);
    }
 
    return true;
  }

该注解实现并不复杂,但是引入了一堆Google的jar包,生产应用中可按需改写实现。

JDBC驱动加载

JDBC接口就是标准的服务提供接口,那么Java是如何利用ServiceLoader加载数据库驱动的呢?一个应用可能会有mysql或者oracle两种数据库驱动实现,Java如何根据数据库连接url选择正确的驱动实现了?答案就在java.sql.DriverManager,因为我们使用JDBC的相关接口前必须调用DriverManager获取对应数据库的JDBC链接,所以DriverManager承担了查找并加载实现类的任务,代码如下:

static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
 
 
private static void loadInitialDrivers() {
        String drivers;
        try {
            //读取系统属性jdbc.drivers,默认为空
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
      
        //加载所有的java.sql.Driver接口实现类
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
 
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                //捕获加载过程的异常,如果加载失败即没有对应的接口实现类,当使用该接口时会报错
                try{
                   //使用迭代比遍历跟上面示例中的for循环遍历效果一样
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });
 
        println("DriverManager.initialize: jdbc.drivers = " + drivers);
 
        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                //用系统加载器初始化系统属性指定的驱动实现类
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
}

上述代码解释了DriverManager如何通过ServiceLoader加载具体接口实现类的,那么DriverManager是如何在不知道具体实现类的前提下调用这些实现类了?答案就是接口,跟Spring的依赖注入一样,具体实现类的实例会注入到DriverManager中,DriverManager通过接口调用该实例的方法即可,如下:

private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        /*
         *获取调用类的类加载器,如果调用类为空则获取当前线程的类加载器
         */
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            // synchronize loading of the correct classloader.
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }
 
        if(url == null) {
            throw new SQLException("The url cannot be null", "08001");
        }
 
        println("DriverManager.getConnection(\"" + url + "\")");
 
        //用于记录第一个抛出的异常,然后重新抛出
        SQLException reason = null;
        //registeredDrivers是一个CopyOnWriteArrayList,DriverManager的静态私有属性,用于保存已经加载并从初始化过的驱动类实例
        //驱动类实例化的时候必须调用registerDriver方法注册该实例
        //DriverInfo是DriverManager的内部类,对Driver实例的简单包装
        //因为无法直接判断跟url匹配的驱动示例,这里就全部遍历一遍,将判断交给实例了
        for(DriverInfo aDriver : registeredDrivers) {
            // 如果callerCL没有权限加载该驱动实现类,则跳过该驱动类
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println("    trying " + aDriver.driver.getClass().getName());
                    //如果有权限则调用该驱动类的实例建立连接
                    //connect方法规定如果不是驱动支持的连接url则返回null,如果url匹配但是连接失败则抛出异常
                    Connection con = aDriver.driver.connect(url, info);
                    if (con != null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }
 
            } else {
                println("    skipping: " + aDriver.getClass().getName());
            }
 
        }
 
        //抛出第一个异常
        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 boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
        boolean result = false;
        if(driver != null) {
            Class<?> aClass = null;
            try {
                //使用目标classLoader重新加载驱动实现类
                aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
            } catch (Exception ex) {
                result = false;
            }
             //判断目标classLoader加载的驱动实现类和已有的驱动实现类是否相同,如果相同则返回true
             //说明加载驱动类的类加载器和classLoader是同一个
             result = ( aClass == driver.getClass() ) ? true : false;
        }
 
        return result;
    }

最后看下Mysql驱动实现类的实现,如下:

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!");
        }
    }
 
    //为newInstance方法构造实例使用
    public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}

理解上述加载逻辑的核心在于,static块中的代码并不是在类加载的时候执行的,类加载和链接的过程中不会执行任何字节码指令,只有执行了new,getstatic等指令触发了类的初始化才会执行static块中的代码。因此在启动类加载器加载DriverManager时对应的驱动类实际没有加载到内存中,而是等到需要建立数据库连接,应用程序调用DriverManager的静态方法getConnection时才触发了DriverManager静态块代码的执行,开始加载驱动实现类到内存中,在ServiceLoader加载完成后会调用newInstance方法触发驱动实现类的初始化,进而导致驱动类自身的static块被执行,new一个驱动类实例并将其注册到DriverManager中。

很多人说contextClassLoader破坏了类加载委托模型,理由是DriverManager这类Java定义的接口位于rt.jar中,由启动类加载器加载,而接口的实现类却是通过contextClassLoader加载的。个人认为并没有破坏,这恰好告诉我们在自定义类加载器加载某个类时如何让这个类能够被Java定义的标准类加载器使用,即通过接口调用的方式,如果直接是基于实现类调用,则出现自定义类加载器加载的类不能与标准类加载器加载的类兼容的问题。

自定义类加载器

自定义类加载器场景

什么场景下需要自定义类加载器了?通常有三种:

第一种场景真实应用的很少,非容器类应用通常通过脚本将依赖的jar包路径追加到classpath上,容器类应用比如Tomcat因为要实现待加载的第三方应用的代码的隔离,所以不能将所有第三方应用的代码都追加classpath上,而是通过自定义类加载器分别加载位于不同路径下的第三方应用的代码。

热部署实际应用的场景非常有限,因为一个class文件的变动可能会直接导致其他class执行报错,如缺乏某个方法,因此现有的热部署插件如JRebel 都只支持本地开发环境的热部署,无法做到对已经编译完成的jar包中的某个类的热部署。要实现热部署本身并不复杂,用一个新的自定义类加载器加载变化后的类文件即可,可通过文件的时间戳判断文件是否变化。因为jsp页面对应的jsp类不会被其他任何类所引用,所以当jsp页面发生变动,只有对应的jsp类变了,Tomcat可以利用热部署的方式安全的重新加载该类。

利用类加载器实现类隔离的前提是必须违背委派模型,因为在委派模型下同名的类不可能被加载两次。实现思路是改写loadClass方法的委派逻辑,先尝试用自定义的类加载器加载目标类,如果找不到再请求父类类加载器加载,详情参考下面的Tomcat类加载器分析。

实现自定义类加载器

实现一个自定义类加载器建议直接继承自URLClassLoader,也可继承ClassLoader类,然后改写其中的findClass方法,前者更简单更安全,因为URLClassLoader本身继承自SecureClassLoader,支持安全校验,URLClassLoader本身是基于URL读取目标资源,支持读取任一合法路径或者有效的网络地址下的资源。以读取其他文件路径下的自定义类加载器说明,如下:


public TestA() {
   }

   public void say(){
       System.out.println("TestA");
   }


class MyClassLoader extends ClassLoader{
   public MyClassLoader(ClassLoader parent) {
       super(parent);
   }

   public MyClassLoader() {

   }


   @Override
   //此处将findClass方法的访问权限由protected改成public,为了方便通过findClass强制加载某个类
   public Class<?> findClass(String name) throws ClassNotFoundException {
       String fileName="D:\\git\\"+name.replace(".", File.separator)+".class";
       System.out.println(fileName);
       try {
           File file=new File(fileName);
           if(file.exists()){
               InputStream ins=new FileInputStream(file);
               ByteArrayOutputStream baos = new ByteArrayOutputStream();
               byte[] buffer = new byte[4096];
               int bytesNumRead = 0;
               while ((bytesNumRead = ins.read(buffer)) != -1) {
                   baos.write(buffer, 0, bytesNumRead);
               }
               byte[] classData=baos.toByteArray();
               System.out.println("file  size->"+classData.length);
               return defineClass(name, classData, 0,classData.length);
           }
       } catch (Exception e) {

       }
       return null;
   }
}

class MyClassLoader2 extends URLClassLoader {
   public MyClassLoader2(URL[] urls, ClassLoader parent) {
       super(urls, parent);
   }

   public MyClassLoader2(URL[] urls) {
       super(urls);
   }

   public MyClassLoader2(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
       super(urls, parent, factory);
   }

   @Override
   public Class<?> findClass(String name) throws ClassNotFoundException {
       return super.findClass(name);
   }
}

public class ClassLoaderTest {
   public static void main(String[] args) throws Exception {
       //forName的实现遵循委派模型,所以返回的是父类加载器即AppClassLoader加载的TestA
       Class aClass=Class.forName("jvmTest.TestA", false,new MyClassLoader());
       //因为类是JVM的启动类,肯定由AppClassLoader加载,当执行到下面这行代码时需要解析TestA的类引用,返回的Class实例也是
       //AppClassLoader加载的,所以结果是true
       System.out.println(aClass==TestA.class);

       //不走委派模型,强制由MyClassLoader加载jvmTest.TestA
       Class aClass2=new MyClassLoader().findClass("jvmTest.TestA");
       //结果是false
       System.out.println(aClass2==TestA.class);

       File file=new File("D:\\git\\");
       MyClassLoader2 classLoader2=new MyClassLoader2(new URL[]{file.toURI().toURL()});
       Class aClass3=classLoader2.findClass("jvmTest.TestA");
       //结果是false
       System.out.println(aClass2==TestA.class);
   }

}

编译完成后,将TestA.class复制到D:\git\jvmTest\目录下,然后执行main方法即可。

自定义类加载器加载的类的调用

上述示例只是简单的比较了通过自定义类加载器加载的类aClass和通过AppClassLoader加载的类TestA.class是否一致,那么可以将aClass强转成TestA.class,然后调用say方法么?测试代码如下:

public class ClassLoaderTest {
   public static void main(String[] args) throws Exception {

       //不走双亲委派模型,强制由MyClassLoader加载jvmTest.TestA
       Class aClass2=new MyClassLoader().findClass("jvmTest.TestA");
       //结果是false
       System.out.println(aClass2==TestA.class);
       //创建一个目标类实例
       Object obj= aClass2.newInstance();
       //结果为false,无法直接将obj强转成TestA
       System.out.println(obj instanceof TestA);
       //强转报错java.lang.ClassCastException: jvmTest.TestA cannot be cast to jvmTest.TestA
       TestA a=(TestA) obj;
       a.say();

   }

}

两个TestA class虽然全限定名一样,但是因为加载的类加载器不同,对JVM而言是两个不同的类,所以无法强转,有什么办法可以解决这个问题了?答案就是采用给SPI机制一样方式,将TestA改成一个接口,提供一个实现类TestAImpl,如下:

package jvmTest;
 
public interface TestA {
 
    void say();
 
}
package jvmTest;
 
public class TestAImpl implements TestA {
 
    public void say() {
        System.out.println("Say Hello World");
    }
}
public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
 
        //不走双亲委派模型,强制由MyClassLoader加载jvmTest.TestA
        Class aClass2=new MyClassLoader().findClass("jvmTest.TestAImpl");
        //结果是false
        System.out.println(aClass2==TestAImpl.class);
        //创建一个目标类实例
        Object obj= aClass2.newInstance();
        //结果为true
        System.out.println(obj instanceof TestA);
        //正常调用
        TestA a=(TestA) obj;
        a.say();
 
    }
 
}

同样,编译完成后,将TestAImpl.class复制到D:\git\jvmTest\目录下,然后执行main方法即可。上述示例中是代码写死了指定某个类加载器加载特定类,怎样用我们的自定义类加载器来替换默认的AppClassLoader了?即能否指定应用整体的类加载器而不是某个类的类加载器?Tomcat做到了,参考下节。

Tomcat类加载器

类加载器的层次关系

Tomcat的启动入口是org.apache.catalina.startup.Bootstrap类的start方法,相关代码如下:

public void start()
        throws Exception {
        //catalinaDaemon是BootStrap的Object类型的私有属性,但是实际是org.apache.catalina.startup.Catalina实例
        //这么做是为了避免AppClassLoader加载BootStrap的时候将Catalina类给加载了
        //init方法为Catalina实例的初始化方法
        if( catalinaDaemon==null ) init();
 
        //通过反射调用Catalina实例的start方法,完成Tomcat的启动
        Method method = catalinaDaemon.getClass().getMethod("start", (Class [] )null);
        method.invoke(catalinaDaemon, (Object [])null);
 
    }
 
public void init() throws Exception {
        //初始化类加载器
        initClassLoaders();
        //设置主线程的类加载器是catalinaLoader
        Thread.currentThread().setContextClassLoader(catalinaLoader);
        //通过catalinaLoader加载需要借助SecurityManager做权限控制的类
        SecurityClassLoad.securityClassLoad(catalinaLoader);
 
        // Load our startup class and call its process() method
        if (log.isDebugEnabled())
            log.debug("Loading startup class");
        //创建一个新的Catalina实例
        Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
        Object startupInstance = startupClass.getConstructor().newInstance();
 
        // Set the shared extensions class loader
        if (log.isDebugEnabled())
            log.debug("Setting startup class properties");
 
        //调用Catalina的setParentClassLoader方法,设置为sharedLoader
        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;
 
    }
 
    private void initClassLoaders() {
        try {
            //初始化三个加载器
            commonLoader = createClassLoader("common", null);
            if( commonLoader == null ) {
                // no config file, default to this loader - we might be in a 'single' env.
                commonLoader=this.getClass().getClassLoader();
            }
            //catalinaLoader和sharedLoader的父类加载器都是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);
        }
    }
 
 
    private void initClassLoaders() {
        try {
            //初始化三个加载器
            //common最终会转换成catalina.properties文件中的配置选项common.loader
            //根据该配置获取目标资源的URL,然后构造URLClassLoader
            commonLoader = createClassLoader("common", null);
            if( commonLoader == null ) {
                // no config file, default to this loader - we might be in a 'single' env.
                commonLoader=this.getClass().getClassLoader();
            }
            //catalinaLoader和sharedLoader的父类加载器都是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);
        }
    }

catalina.properties位于Tomcat根目录的conf目录下,默认配置如下图:

shared.loader用于指定多应用共享的资源,如spring的依赖jar包;server.loader用于指定只被Tomcat自身使用的依赖包,比如Tomcat的扩展功能性jar包。

createClassLoader方法最终实现如下:

//set是解析出来的jar包资源路径
    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<URLClassLoader>() {
                    @Override
                    public URLClassLoader run() {
                        if (parent == null)
                            //commonLoader的parent为null,这时默认为SystemClassLoader,即AppClassLoader
                            return new URLClassLoader(array);
                        else
                            return new URLClassLoader(array, parent);
                    }
   });

Catalina类的start方法执行后,因为Catalina是catalinaLoader加载的,所以根据JVM的类加载约束,以Catalina作为根类,直接或者间接引用到的所有的即Tomcat自己的类都是通过catalinaLoader加载的。因为catalinaLoader和sharedLoader没有父子层次关系,所以保证了Tomcat自己的类同sharedLoader的类的隔离。

那么Tomcat如何保证各应用之间的类,Tomcat容器的应用的类之间的隔离了?答案是WebappClassLoader。Tomcat启动后会为webapp目录下的所有应用分别创建一个StandardContext,然后调用其startInternal方法,在此方法中会创建一个新的WebappLoader,代码如下:

     if (getLoader() == null) {
            //getParentClassLoader返回的是Catalina的parentClassLoader,即sharedLoader
            //WebappLoader是WebappClassLoader的管理类
            WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
            webappLoader.setDelegate(getDelegate());
            setLoader(webappLoader);
        }

getParentClassLoader是Container接口的方法,对应的set方法是setParentClassLoader,该方法在Catalina的内部类SetParentClassLoaderRule中被调用,如下:

final class SetParentClassLoaderRule extends Rule {
 
    public SetParentClassLoaderRule(ClassLoader parentClassLoader) {
 
        this.parentClassLoader = parentClassLoader;
 
    }
 
    ClassLoader parentClassLoader = null;
 
    @Override
    public void begin(String namespace, String name, Attributes attributes)
        throws Exception {
 
        if (digester.getLogger().isDebugEnabled()) {
            digester.getLogger().debug("Setting parent class loader");
        }
 
        Container top = (Container) digester.peek();
        //设置父类加载器
        top.setParentClassLoader(parentClassLoader);
 
    }
 
 
}

SetParentClassLoaderRule的调用位于Catalina的createStartDigester方法中,如下:

image.png

WebappLoader创建完成后会调用接着调用其start方法,最终调用WebappLoader的startInternal方法,startInternal方法调用createClassLoader方法完成

private WebappClassLoaderBase createClassLoader()
throws Exception {
//loadClass就是ParallelWebappClassLoader类的全限定类名
//forName方法没有指定类加载器,默认使用调用方即当前类的类加载器加载,即catalinaLoader
Class<?> clazz = Class.forName(loaderClass);
WebappClassLoaderBase classLoader = null;

    if (parentClassLoader == null) {
        parentClassLoader = context.getParentClassLoader();
    }
    Class<?>[] argTypes = { ClassLoader.class };
    Object[] args = { parentClassLoader };
    //将ParentClassLoader即sharedLoader作为构造参数构建一个新的
    Constructor<?> constr = clazz.getConstructor(argTypes);
    classLoader = (WebappClassLoaderBase) constr.newInstance(args);

    return classLoader;
}

上述代码解释了WebappClassLoader的父类加载器是sharedLoader。到此,Tomcat类加载器的层次关系就介绍完了,如下:


image.png

ParallelWebappClassLoader

ParallelWebappClassLoader是Tomcat用于支持并行类加载的类加载器,其类继承关系如下图:

image.png

核心实现都在WebappClassLoaderBase中,重点关注其loadClass方法的实现,如下:

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
 
        synchronized (getClassLoadingLock(name)) {
            if (log.isDebugEnabled())
                log.debug("loadClass(" + name + ", " + resolve + ")");
            Class<?> clazz = null;
 
            //检查类加载器的状态
            checkStateForClassLoading(name);
 
            //从之前加载过的本地Class缓存中查找
            clazz = findLoadedClass0(name);
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Returning class from cache");
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
 
            //通过ClassLoader的本地方法查找JVM是否已经加载过该类
            clazz = findLoadedClass(name);
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Returning class from cache");
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
 
            // 获取目标类的资源名称
            String resourceName = binaryNameToPath(name, false);
            //通过系统类加载器加载Java核心类,避免这部分类被重复加载
            ClassLoader javaseLoader = getJavaseClassLoader();
            boolean tryLoadingFromJavaseLoader;
            try {
                URL url;
                if (securityManager != null) {
                    PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
                    url = AccessController.doPrivileged(dp);
                } else {
                    url = javaseLoader.getResource(resourceName);
                }
                //判断能否找到目标资源
                tryLoadingFromJavaseLoader = (url != null);
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                //getResource方法抛出异常,继续通过loadClass方法重试
                tryLoadingFromJavaseLoader = true;
            }
 
            if (tryLoadingFromJavaseLoader) {
                try {
                    //通过loadClass加载
                    clazz = javaseLoader.loadClass(name);
                    if (clazz != null) {
                        if (resolve)
                            resolveClass(clazz);
                        return clazz;
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }
 
            if (securityManager != null) {
                int i = name.lastIndexOf('.');
                if (i >= 0) {
                    try {
                        //检查当前类能够访问目标类,如目标类的构造方法是否是public
                        securityManager.checkPackageAccess(name.substring(0,i));
                    } catch (SecurityException se) {
                        String error = "Security Violation, attempt to use " +
                            "Restricted Class: " + name;
                        log.info(error, se);
                        throw new ClassNotFoundException(error, se);
                    }
                }
            }
            //是否委托给父类类加载器加载,delegate默认为false
            boolean delegateLoad = delegate || filter(name, true);
 
            
            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
                }
            }
 
            if (log.isDebugEnabled())
                log.debug("  Searching local repositories");
            try {
                //在本地的资源库中查找目标类,如果找到了会将其方法本地缓存中
                clazz = findClass(name);
                if (clazz != null) {
                    if (log.isDebugEnabled())
                        log.debug("  Loading class from local repository");
                    if (resolve)
                        resolveClass(clazz);
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
 
            //如果查找失败,并且不是委托父类加载器加载的,则尝试通过父类类加载器加载
            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);
    }

Web应用加载

Servlet容器通过三个接口同应用交互,以Spring应用为例

这三类接口的实现Tomcat是如何处理的了?答案还是在StandardContext的startInternal方法中,其中Listener实现类通过listenerStart方法处理的,该方法会查找web.xml文件中所有Listener标签对应的Listener接口的实现类,并通过InstanceManager创建一个新的实例,创建完成后将各Listener实例保存至StandardContext的对应Listener列表中,在必要时回调,如果是ServletContextListener则调用其contextInitialized方法,即事件回调;Servlet实现类是通过loadOnStartup方法处理,该方法会回调Servlet对应的包装类StandardWrapper的load方法,进而调用loadServlet方法,通过InstanceManager完成Servlet的加载和实例化;Filter实现类通过filterStart方法处理,该方法会解析出所有的Filter定义,但是不会实例化,而是等到需要的时候在getFilter方法中通过InstanceManager实例化,相关代码比较多,不一一展开了。

上面三个接口的实现类都是通过InstanceManager的newInstance(String className)方法完成类加载和实例化的,实例化后通过接口调用对应的方法,InstanceManager的默认实现是DefaultInstanceManager,该类的初始化同样在StandardContext的startInternal方法中,其实现如下:

public Object newInstance(String className) throws IllegalAccessException,
            InvocationTargetException, NamingException, InstantiationException,
            ClassNotFoundException, IllegalArgumentException, NoSuchMethodException, SecurityException {
        //classLoader是构建时通过catalinaContext.getLoader().getClassLoader()初始化,catalinaContext即当前的StandardContext
        //classLoader即StandardContext中初始化的WebAppClassLoader的实例
        Class<?> clazz = loadClassMaybePrivileged(className, classLoader);
        return newInstance(clazz.getConstructor().newInstance(), clazz);
    }

这三个接口的实现类最终都是通过同一个WebAppClassLoader加载,不同应用的WebAppClassLoader实例不同,从而保证了不同应用之间的类隔离,因为Tomcat自身的代码是通过catalinaLoader加载的,跟WebAppClassLoader没有父子层级关系,所以各应用的代码同Tomcat自身的代码是隔离的。

热部署

当Tomcat发现某个class发生变更后会重新创建一个类加载器重新加载,因为需要重新加载的类很多,所以耗时长,因为之前加载的类一直位于永久代(JDK8改成元空间)并未删除可能导致永久代内存不足异常。通过StandardContext的backgroundProcess方法实现,该方法会回调Tomcat的多个资源对象的backgroundProcess方法,比如WebAppLoader,会有一个后台线程定时执行该方法。WebAppLoader的backgroundProcess方法如下:

 public void backgroundProcess() {
         //如果需要重载或者发生修改了
        if (reloadable && modified()) {
            try {
                Thread.currentThread().setContextClassLoader
                    (WebappLoader.class.getClassLoader());
                if (context != null) {
                     //调用StandardContext的reload方法
                    context.reload();
                }
            } finally {
                if (context != null && context.getLoader() != null) {
                    Thread.currentThread().setContextClassLoader
                        (context.getLoader().getClassLoader());
                }
            }
        }
    }
 
 
public synchronized void reload() {
 
        // Validate our current component state
        if (!getState().isAvailable())
            throw new IllegalStateException
                (sm.getString("standardContext.notStarted", getName()));
 
        if(log.isInfoEnabled())
            log.info(sm.getString("standardContext.reloadingStarted",
                    getName()));
 
        // Stop accepting requests temporarily.
        setPaused(true);
 
        try {
            //停止当前StandardContext
            stop();
        } catch (LifecycleException e) {
            log.error(
                sm.getString("standardContext.stoppingContext", getName()), e);
        }
 
        try {
            //重新执行start方法,即是重新执行startInternal方法,会创建一个新的WebAppLoader,重新加载整个应用
            start();
        } catch (LifecycleException e) {
            log.error(
                sm.getString("standardContext.startingContext", getName()), e);
        }
 
        setPaused(false);
 
        if(log.isInfoEnabled())
            log.info(sm.getString("standardContext.reloadingCompleted",
                    getName()));
 
    }

当JSP页面发生变更后Tomcat会重新编译变更后的JSP页面并生成新的class,然后重新加载该Class,因为加载该类不会对其他类产生任何影响,所以不需要创建一个新的类加载器。相关代码在org.apache.jasper.JspCompilationContext类的compile方法,如下:

isOutDated方法通过时间戳判断jsp页面文件是否变更。重新加载的逻辑在org.apache.jasper.servlet.JspServletWrapper的getServlet方法,如下图:

image.png

当重新生成jsp页面的class后getReloadInternal方法返回true,从而通过InstanceManager重新加载该类。

上一篇 下一篇

猜你喜欢

热点阅读