精华内容
下载资源
问答
  • 线程上下文类加载器

    千次阅读 2019-08-27 17:56:00
    线程上下文类加载器 名词解释 SPI:Service Provider Interface 服务提供者接口 当前类加载器(Current ClassLoader) 每个都会使用自己的类加载器(加载自身的类加载器)来去加载所依赖的,如果ClassA引用了Class...

    线程上下文类加载器

    名词解释

    SPI:Service Provider Interface 服务提供者接口

    当前类加载器(Current ClassLoader)

    每个类都会使用自己的类加载器(加载自身的类加载器)来去加载所依赖的类,如果ClassA引用了ClassB,NameClassA的类加载器就会加载ClassB(前提是ClassB没有被加载)

    线程上下文类加载器(Context ClassLoader)

    线程上下文类加载器是从jdk1.2开始引入的,类Thread中的getContextCLassLoader()与setContextClassLoader(ClassLoader classloader) 分别用来获取和设置上下文类加载器.如果没有通过用setContextClassLoader(ClassLoader classloader)进行设置的话,线程将继承其父线程的上下文类加载器。 Java应用运行时的初始线程的上下文加载器是系统类加载器,在线程中运行的代码可以通过该类加载器来加载类与资源 。这句话的解释就在launcher的源代码中

    看一下Launcher类的源码:

    public class Launcher {
        private static URLStreamHandlerFactory factory = new Launcher.Factory();
        private static Launcher launcher = new Launcher();
        private static String bootClassPath = System.getProperty("sun.boot.class.path");
        private ClassLoader loader;
        private static URLStreamHandler fileHandler;
    
        public static Launcher getLauncher() {
            return launcher;
        }
    
        public Launcher() {
            Launcher.ExtClassLoader var1;
            try {
                var1 = Launcher.ExtClassLoader.getExtClassLoader();
            } catch (IOException var10) {
                throw new InternalError("Could not create extension class loader", var10);
            }
    
            try {
                // 设置this.loader为系统类加载器
                this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
            } catch (IOException var9) {
                throw new InternalError("Could not create application class loader", var9);
            }
    
            // 设置线程上下文类加载器是 this.loader(系统类加载器).
            Thread.currentThread().setContextClassLoader(this.loader);
            String var2 = System.getProperty("java.security.manager");
            if (var2 != null) {
                SecurityManager var3 = null;
                if (!"".equals(var2) && !"default".equals(var2)) {
                    try {
                        var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
                    } catch (IllegalAccessException var5) {
                        ;
                    } catch (InstantiationException var6) {
                        ;
                    } catch (ClassNotFoundException var7) {
                        ;
                    } catch (ClassCastException var8) {
                        ;
                    }
                } else {
                    var3 = new SecurityManager();
                }
    
                if (var3 == null) {
                    throw new InternalError("Could not create SecurityManager: " + var2);
                }
    
                System.setSecurityManager(var3);
            }
    
        }
    
        public ClassLoader getClassLoader() {
            return this.loader;
        }
    

    JVM双亲委托加载模型的问题

    Java提供了很多的服务提供者接口(SPI),提供这些接口让第三方去实现这些接口,常见的SPI有: jdbc、JNDI、JAXP(解析XML)等。这些SPI接口是由Java核心类库提供的,而这些SPI的实现代码则是作为Java应用依赖的jar包被引入类的classpath下的,SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由系统类加载器(System ClassLoader)来加载的。根据双亲委派类加载模型,父加载器(启动类加载器)加载的类无法访问子加载器(系统类加载器或者应用类加载器) 加载的类,所以就无法访问第三方的实现类,这是双亲委托模型的一个尴尬局面。

    线程上下文类加载器的重要性

    父ClassLoader可以使用当前线程Thread.currentThread().getContextClassLoader()所指定的classloader加载的类。
    这就改变了父ClassLoader不能使用子ClassLoader或是其他没有直接父子关系的CLassLoader加载的类的情况,即改变了双亲委托模型。

    通过JDBC看线程上下文类加载器

    //举例说明jdbc获取mysql的连接
    Driver driver = Class.forName("com.mysql.jdc.Driver");
    Connection conn = driver.getConnection(url,username,password);
    // 这是一段伪代码 我们主要看的是Class.forName()
    

    看一下Class.forName()的源码

    public static Class<?> forName(String className)
                    throws ClassNotFoundException {
            Class<?> caller = Reflection.getCallerClass();
            // className:要加载的Class名字 | 是否初始化:此处需要初始化 |调用者的类加载器 | 调用者的Class对象
            return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }
    
     public static Class<?> forName(String name, boolean initialize,
                                       ClassLoader loader)
            throws ClassNotFoundException
        {
            Class<?> caller = null;
            SecurityManager sm = System.getSecurityManager();
            if (sm != null) {
                // Reflective call to get caller class is only needed if a security manager
                // is present.  Avoid the overhead of making this call otherwise.
                //获取调用此forName方法的类的Class对象A
                caller = Reflection.getCallerClass();
                if (sun.misc.VM.isSystemDomainLoader(loader)) {
                    //获取A的类加载器
                    ClassLoader ccl = ClassLoader.getClassLoader(caller);
                    //安全检查
                    if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
                        sm.checkPermission(
                            SecurityConstants.GET_CLASSLOADER_PERMISSION);
                    }
                }
            }
            //name:要被加载的|initialize:是否初始化|loader:指定的类加载器(自定义加载器此处是系统类加载器)|caller调用者的Class对象
            return forName0(name, initialize, loader, caller);
        }
        //forName0是一个本地方法
        /** Called after security check for system loader access checks have been made. */
        private static native Class<?> forName0(String name, boolean initialize,
                                                ClassLoader loader,
                                                Class<?> caller)
            throws ClassNotFoundException;
    

    tomcat类加载器

    在Tomcat目录结构中,有三组目录(“/common/”,“/server/”和“shared/”)可以存放公用Java类库,此外还有第四组Web应用程序自身的目录“/WEB-INF/”,把java类库放置在这些目录中的含义分别是:

    放置在common目录中:类库可被Tomcat和所有的Web应用程序共同使用。
    放置在server目录中:类库可被Tomcat使用,但对所有的Web应用程序都不可见。
    放置在shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。
    放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。
    为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,如下图所示

    在这里插入图片描述

    灰色背景的3个类加载器是JDK默认提供的类加载器,这3个加载器的作用前面已经介绍过了。而 CommonClassLoader、CatalinaClassLoader、SharedClassLoader 和 WebAppClassLoader 则是 Tomcat 自己定义的类加载器,它们分别加载 /common/、/server/、/shared/* 和 /WebApp/WEB-INF/* 中的 Java 类库。其中 WebApp 类加载器和 Jsp 类加载器通常会存在多个实例,每一个 Web 应用程序对应一个 WebApp 类加载器,每一个 JSP 文件对应一个 Jsp 类加载器。从图中的委派关系中可以看出,CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 自己能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。而 JasperLoader 的加载范围仅仅是这个 JSP 文件所编译出来的那一个 Class,它出现的目的就是为了被丢弃:当服务器检测到 JSP 文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 Jsp 类加载器来实现 JSP 文件的 HotSwap 功能。

    spring的类的加载方式

    先看一下tomcat的类加载器 Tomcat 加载器的实现清晰易懂,并且采用了官方推荐的“正统”的使用类加载器的方式。那么如果有 10 个 Web 应用程序都用到了spring的话,可以把Spring的jar包放到 common 或 shared 目录下让这些程序共享。Spring 的作用是管理每个web应用程序的bean,getBean时自然要能访问到应用程序的类,而用户的程序显然是放在 /WebApp/WEB-INF 目录中的(由 WebAppClassLoader 加载),那么在 CommonClassLoader 或 SharedClassLoader 中的 Spring 容器如何去加载并不在其加载范围的用户程序(/WebApp/WEB-INF/)中的Class呢?

    spring项目都要加一个listener

    <listener>
    		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    

    这个类根据名字知道是个上下文类加载的监听器,查看ContextLoaderListener的源码:

    /**
    引导启动和关闭spring的根WebApplicationContext的监听器。
     * Bootstrap listener to start up and shut down Spring's root {@link WebApplicationContext}.
     * Simply delegates to {@link ContextLoader} as well as to {@link ContextCleanupListener}.
     *
     * <p>This listener should be registered after {@link org.springframework.web.util.Log4jConfigListener}
     * in {@code web.xml}, if the latter is used.
     *
     * <p>As of Spring 3.1, {@code ContextLoaderListener} supports injecting the root web
     * application context via the {@link #ContextLoaderListener(WebApplicationContext)}
     * constructor, allowing for programmatic configuration in Servlet 3.0+ environments.
     * See {@link org.springframework.web.WebApplicationInitializer} for usage examples.
     *
     * @author Juergen Hoeller
     * @author Chris Beams
     * @since 17.02.2003
     * @see #setContextInitializers
     * @see org.springframework.web.WebApplicationInitializer
     * @see org.springframework.web.util.Log4jConfigListener
     */
    public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
    	public ContextLoaderListener() {
    	}
    
    	public ContextLoaderListener(WebApplicationContext context) {
    		super(context);
    	}
    
    
    	/**
    	 * Initialize the root web application context.
    	 */
    	@Override
    	public void contextInitialized(ServletContextEvent event) {
    		initWebApplicationContext(event.getServletContext());
    	}
    
    
    	/**
    	 * Close the root web application context.
    	 */
    	@Override
    	public void contextDestroyed(ServletContextEvent event) {
    		closeWebApplicationContext(event.getServletContext());
    		ContextCleanupListener.cleanupAttributes(event.getServletContext());
    	}
    
    }
    

    他的核心是调用父类的initWebApplicationContext

    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 {
    			// Store context in local instance variable, to guarantee that
    			// it is available on ServletContext shutdown.
    			if (this.context == null) {
    				this.context = createWebApplicationContext(servletContext);
    			}
    			if (this.context instanceof ConfigurableWebApplicationContext) {
    				ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
    				if (!cwac.isActive()) {
    					// The context has not yet been refreshed -> provide services such as
    					// setting the parent context, setting the application context id, etc
    					if (cwac.getParent() == null) {
    						// The context instance was injected without an explicit parent ->
    						// determine parent for root web application context, if any.
    						ApplicationContext parent = loadParentContext(servletContext);
    						cwac.setParent(parent);
    					}
    					configureAndRefreshWebApplicationContext(cwac, servletContext);
    				}
    			}
    			servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
    			// 获取线程上下文类加载器,默认为WebAppClassLoader
    			ClassLoader ccl = Thread.currentThread().getContextClassLoader();
    			// 如果spring的jar包放在每个webapp自己的目录中 (也可以放在tomcat common目录下,)放在tocat的			// common目录本来是要用CommonClassLoader 加载器去加载的,这样怎么加载/webApp/WEB-INF/下的				// class呢,spring考虑到了自己可能被放到其他位置,所以直接用TCCL来解决所有可能面临的情况。
                // 放在webapp目录中线程上下文类加载器就是WebAppClassLoader。放在common目录下 			   				// ContextLoader.class.getClassLoader()获取的就是commonClassLoader。
    			// 此时线程上下文类加载器会与本类的类加载器(加载spring的)相同,都是WebAppClassLoader
                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;
    		}
    	}
    

    所以,spring根本不会去管自己被放在哪里,它统统使用TCCL来加载类,而TCCL默认设置为WebAppClassLoader,也就是说哪个WebApp应用调用了spring,spring就去取该应用自己的WebAppClassLoader来加载bean

    总结

    线程上下文加载器就是当前线程的Current ClassLoader在双亲委托模型下,类加载器由下至上的,即下层的类加载器会委托上层进行加载。但是对于SPI来说,有些接口是java核心库所提供的,而java核心库是由启动类加载器来加载的,而这些接口的实现来自于不同的jar包(厂商提供),java的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求,而通过给当前线程设置上下文加载器就可以设置上下文类加载器来实现对于接口实现类的加载。

    展开全文
  • 双亲委派模型与线程上下文类加载器

    万次阅读 多人点赞 2019-04-14 11:39:54
    SPI机制是什么,有哪些应用场景,又带来了哪些问题? 双亲委派模型是Java推荐的加载模型,但违背该模型的案例有哪些?为什么会违背,又是怎么解决这种case的?...线程上下文类加载器的作用与应用场景?

    摘要:

    • SPI机制是什么,有哪些应用场景,又带来了哪些问题?
    • 双亲委派模型是Java推荐的类加载模型,但违背该模型的案例有哪些?为什么会违背,又是怎么解决这种case的?
    • JDBC驱动加载的案例有哪些,SPI机制为它带来了哪些方便?
    • 线程上下文类加载器的作用与应用场景?

    一、引子

    SPI机制简介

    SPI的全名为Service Provider Interface,主要是应用于厂商自定义组件或插件中,在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java SPI机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。 有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

    Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件,该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader。JDBC SPI mysql的实现如下所示。

    在这里插入图片描述

    SPI机制带来的问题

    Java 提供了很多服务SPI,允许第三方为这些接口提供实现。这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现则是由各供应商来完成。终端只需要将所需的实现作为 Java 应用所依赖的 jar 包包含进类路径(CLASSPATH)就可以了。问题在于SPI接口中的代码经常需要加载具体的实现类:SPI的接口是Java核心库的一部分,是由启动类加载器来加载的;而SPI的实现类是由系统类加载器来加载的。启动类加载器是无法找到 SPI 的实现类的(因为它只加载 Java 的核心库),按照双亲委派模型,启动类加载器无法委派系统类加载器去加载类。也就是说,类加载器的双亲委派模式无法解决这个问题。

    线程上下文类加载器正好解决了这个问题。线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。


    二、线程上下文类加载器

    线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。Java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器,在线程中运行的代码可以通过此类加载器来加载类和资源。

    线程上下文类加载器从根本解决了一般应用不能违背双亲委派模式的问题,使得java类加载体系显得更灵活。 上面所提到的问题正是线程上下文类加载器的拿手好菜。如果不做任何的设置,Java应用的线程上下文类加载器默认就是系统类加载器。因此,在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。

    // Now create the class loader to use to launch the application  
    try {  
        loader = AppClassLoader.getAppClassLoader(extcl);  
    } catch (IOException e) {  
        throw new InternalError(  
    "Could not create application class loader" );  
    }  
      
    // Also set the context class loader for the primordial thread.  
    Thread.currentThread().setContextClassLoader(loader);
    

    三. 违背双亲委派案例之JDBC

    1、JDBC驱动注册的常用几种方式

    Java数据库连接(Java Database Connectivity,简称 JDBC)是Java语言用来规范客户端程序如何访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法。 JDBC驱动包就是上述接口的实现,由数据库厂商开发,是java和具体数据库之间的连接桥梁。每一种数据库对应一款驱动jar,甚至每一个版本的数据库都有自己对应版本的驱动。我们知道,JDBC规范中明确要求Driver(数据库驱动)类必须向DriverManager注册自己,所以在与数据库交互前必须完成驱动注册,那么先来看看平时我们是如何注册JDBC驱动的。

    方式一:Class.forName(“com.mysql.jdbc.Driver”)
     	try {
     			// 注册
                Class.forName(driver);
                conn = (Connection)DriverManager.getConnection(url, user, passwd);
            } catch (Exception e) {
                System.out.println(e);
            }
    

    使用该方式注册的关键在于 Class.forName(driver);,这句话的作用是加载并初始化指定驱动。mysql jdbc正是在Driver初始化的时候完成注册:

    package com.mysql.jdbc;
    
    import com.mysql.jdbc.NonRegisteringDriver;
    import java.sql.DriverManager;
    import java.sql.SQLException;
    
    public class Driver extends NonRegisteringDriver implements java.sql.Driver {
        public Driver() throws SQLException {
        }
    	// 类初始化时完成驱动注册
        static {
            try {
                DriverManager.registerDriver(new Driver());
            } catch (SQLException var1) {
                throw new RuntimeException("Can\'t register driver!");
            }
        }
    }
    
    方式二:System.setProperty(“jdbc.drivers”,“com.mysql.jdbc.Driver”)
    	try {
                //Class.forName(driver);
                System.setProperty("jdbc.drivers", driver);
                conn = (Connection)DriverManager.getConnection(url, user, passwd);
        } catch (Exception e) {
                System.out.println(e);
        }
    

    这种方式是通过系统的属性设置注册驱动,最终还是通过系统类加载器完成。

    	// DriverManager 中的静态代码块
     	static {
            loadInitialDrivers();
            println("JDBC DriverManager initialized");
     	}
    
    	// 初始化 DriverManager
        private static void loadInitialDrivers() {
            String drivers;
            try {
                drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                    public String run() {
                        return System.getProperty("jdbc.drivers");
                    }
                });
            } catch (Exception ex) {
                drivers = null;
            }
            // If the driver is packaged as a Service Provider, load it.
            // Get all the drivers through the classloader
            // exposed as a java.sql.Driver.class service.
            // ServiceLoader.load() replaces the sun.misc.Providers()
    
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
    
                    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                    Iterator<Driver> driversIterator = loadedDrivers.iterator();
    
                    /* Load these drivers, so that they can be instantiated.
                     * It may be the case that the driver class may not be there
                     * i.e. there may be a packaged driver with the service class
                     * as implementation of java.sql.Driver but the actual class
                     * may be missing. In that case a java.util.ServiceConfigurationError
                     * will be thrown at runtime by the VM trying to locate
                     * and load the service.
                     *
                     * Adding a try catch block to catch those runtime errors
                     * if driver not available in classpath but it's
                     * packaged as service and that service is there in classpath.
                     */
                    try{
                        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);
                }
            }
        }
    
    
    方式三:SPI服务加载机制注册驱动
    	try {
    			// Class.forName(driver);
                conn = (Connection)DriverManager.getConnection(url, user, passwd);
        } catch (Exception e) {
                System.out.println(e);
        }
    

    各位可以发现,这种方式与第一种方式唯一的区别就是经常写的Class.forName被注释掉了,但程序依然可以正常运行,这是为什么呢?这是因为,从JDK1.6开始,Oracle就修改了加载JDBC驱动的方式,即JDBC4.0。在JDBC 4.0中,我们不必再显式使用Class.forName()方法明确加载JDBC驱动。当调用getConnection方法时,DriverManager会尝试自动设置合适的驱动程序。前提是,只要mysql的jar包在类路径中。

    那到底是在哪一步自动注册了mysql driver的呢?我们接下来进一步分析。


    2、SPI服务加载机制注册驱动原理分析

    重点就在DriverManager.getConnection()中。我们知道,调用类的静态方法会初始化该类,而执行其静态代码块是初始化类过程中必不可少的一环。DriverManager的静态代码块:

    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    

    初始化方法loadInitialDrivers()的代码我们其实已经见过了,第二种和第三种的驱动注册逻辑都在这里面:

    private static void loadInitialDrivers() {
        String drivers;
        try {
    		// 先读取系统属性 : 对应上面第二种驱动注册方式
    		drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        
        // 通过SPI加载驱动类
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                    // Do nothing
                }
                return null;
            }
        });
        
        // 加载系统属性中的驱动类 : 对应上面第二种驱动注册方式
        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);
                // 使用AppClassloader加载
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }
    

    从上面可以看出,JDBC中的DriverManager加载Driver的步骤顺序依次是:

    1. 通过SPI方式,读取 META-INF/services 下文件中的类名,使用线程上下文类加载器加载;
    2. 通过System.getProperty(“jdbc.drivers”)获取设置,然后通过系统类加载器加载。

    我们现在只讨论SPI方式的实现,来看刚才的代码:

    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    Iterator<Driver> driversIterator = loadedDrivers.iterator();
    
    try{
        while(driversIterator.hasNext()) {
            driversIterator.next();
        }
    } catch(Throwable t) {
    // Do nothing
    }
    

    注意driversIterator.next()这条语句完成了驱动的注册工作,如下所示:

    private S nextService() {
        if (!hasNextService())
            throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
        try {
            // 加载实现类,注意还没有初始化;以JDBC为例,此时还没有完成驱动注册
            c = Class.forName(cn, false, loader);
        } catch (ClassNotFoundException x) {
            fail(service,
                 "Provider " + cn + " not found");
        }
        // service就是SPI,以JDBC为例,service就是Java Driver接口;此处判断c是否为Driver的实现
        if (!service.isAssignableFrom(c)) {
            fail(service,
                 "Provider " + cn  + " not a subtype");
        }
        try {
            // c是spi的实现,c.newInstance()会触发类的初始化动作,以JDBC为例,这一操作会完成驱动注册
            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
    }
    

    好,那句因SPI而省略的代码现在解释清楚了,那我们继续看给这个方法传的loader是怎么来的。因为Class.forName(DriverName, false, loader)代码所在的类在java.util.ServiceLoader类中,而ServiceLoader.class又加载在BootrapLoader中,因此传给 forName 的 loader 必然不能是BootrapLoader(启动类加载器只能加载java核心类库)。这时候只能使用线程上下文类加载器了:把自己加载不了的类加载到线程上下文类加载器中(通过Thread.currentThread()获取),而线程上下文类加载器默认是使用系统类加载器AppClassLoader。

    回头再看ServiceLoader.load(Class)的代码,的确如此:

    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
    

    ContextClassLoader默认存放了AppClassLoader的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作。

    到这儿差不多把SPI机制解释清楚了。直白一点说就是:我(JDK)提供了一种帮你(第三方实现者)加载服务(如数据库驱动、日志库)的便捷方式,只要你遵循约定(把类名写在/META-INF里),那当我启动时我会去扫描所有jar包里符合约定的类名,再调用forName加载。但我的ClassLoader是没法加载的,那就把它加载到当前执行线程的线程上下文类加载器里,后续你想怎么操作就是你的事了。


    四. Tomcat与Spring的类加载器案例

    接下来将介绍《深入理解java虚拟机》一书中的案例,并解答它所提出的问题(部分类容来自于书中原文)。

    Tomcat中的类加载器

    在Tomcat目录结构中,有三组目录(“/common/”,“/server/”和“shared/”)可以存放公用Java类库,此外还有第四组Web应用程序自身的目录“/WEB-INF/”,把java类库放置在这些目录中的含义分别是:

    • 放置在common目录中:类库可被Tomcat和所有的Web应用程序共同使用;
    • 放置在server目录中:类库可被Tomcat使用,但对所有的Web应用程序都不可见;
    • 放置在shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见;
    • 放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。

    为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,如下图所示:
    在这里插入图片描述
    灰色背景的3个类加载器是JDK默认提供的类加载器,这3个加载器的作用前面已经介绍过了。而 CommonClassLoader、CatalinaClassLoader、SharedClassLoader 和 WebAppClassLoader 则是 Tomcat 自己定义的类加载器,它们分别加载 /common/、/server/、/shared/* 和 /WebApp/WEB-INF/* 中的 Java 类库。其中 WebApp 类加载器和 Jsp 类加载器通常会存在多个实例,每一个 Web 应用程序对应一个 WebApp 类加载器,每一个 JSP 文件对应一个 Jsp 类加载器。

    从图中的委派关系中可以看出,CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 自己能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。而 JasperLoader 的加载范围仅仅是这个 JSP 文件所编译出来的那一个 Class,它出现的目的就是为了被丢弃:当服务器检测到 JSP 文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 Jsp 类加载器来实现 JSP 文件的 HotSwap 功能。

    Spring加载问题

    Tomcat 加载器的实现清晰易懂,并且采用了官方推荐的“正统”的使用类加载器的方式。这时作者提一个问题:如果有 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,简直完美~

    源码分析

    有兴趣的可以接着看看具体实现。在web.xml中定义的listener为org.springframework.web.context.ContextLoaderListener,它最终调用了org.springframework.web.context.ContextLoader类来装载bean,具体方法如下(删去了部分不相关内容):

    public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    	try {
    		// 创建WebApplicationContext
    		if (this.context == null) {
    			this.context = createWebApplicationContext(servletContext);
    		}
    		// 将其保存到该webapp的servletContext中		
    		servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
    		// 获取线程上下文类加载器,默认为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);
    		}
    		
    		return this.context;
    	}
    	catch (RuntimeException ex) {
    		logger.error("Context initialization failed", ex);
    		throw ex;
    	}
    	catch (Error err) {
    		logger.error("Context initialization failed", err);
    		throw err;
    	}
    }
    

    具体说明都在注释中,spring考虑到了自己可能被放到其他位置,所以直接用线程上下文类加载器来解决所有可能面临的情况。


    五. 总结

    通过上面的两个案例分析,我们可以总结出线程上下文类加载器的适用场景:

    • 当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。

    • 当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。


    六. 更多

    更多关于JVM内存模型的结构、Java对象在虚拟机中的创建、定位过程、内存异常分析等相关知识的介绍,请各位看官移步我的博文请移步我的博文[《JVM 内存模型概述》]。


    ##引用:
    《真正理解线程上下文类加载器(多案例分析)》
    《java中的SPI机制》

    展开全文
  • 线程上下文类加载器的作用及实现

    千次阅读 2018-12-05 16:11:28
    为什么用线程上下文?因为SPI接口一般在核心库里,有引导类加载器(BootStrap)加载,然后具体实现是在系统类加载器(APP)里面,引导只加载核心库里的,又不能代理给系统类加载器,因为引导是系统的祖先 ...
    public Launcher() {
            Launcher.ExtClassLoader var1;
            try {
                //初始化扩展类加载器,注意这里构造函数没有入参,即无法获取根类加载器
                var1 = Launcher.ExtClassLoader.getExtClassLoader();
            } catch (IOException var10) {
                throw new InternalError("Could not create extension class loader", var10);
            }
    
            try {
                //初始化应用类加载器,注意这里的入参就是扩展类加载器
                this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
            } catch (IOException var9) {
                throw new InternalError("Could not create application class loader", var9);
            }
    
            //设置上下文类加载器,这个后面会详细说
            Thread.currentThread().setContextClassLoader(this.loader);
    
           //删除了一些安全方面的代码
           //...
    }
    

    //设置上下文类加载器,这个后面会详细说
    Thread.currentThread().setContextClassLoader(this.loader);

    补充一个代码段:

    private static Connection getConnection(
            String url, java.util.Properties info, Class<?> caller) throws SQLException {
            //获取调用者的类加载器
            ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
            synchronized(DriverManager.class) {
                //如果为null,则使用上下文类加载器
                //这里是重点,什么时候类加载器才会为null? 当然就是由根类加载器加载的类了
                if (callerCL == null) {
                    callerCL = Thread.currentThread().getContextClassLoader();
                }
            }
    
            //...省略
    
            for(DriverInfo aDriver : registeredDrivers) {
                //使用上下文类加载器去加载驱动
                if(isDriverAllowed(aDriver.driver, callerCL)) {
                    try {
                        //如果加载成功,则进行连接
                        Connection con = aDriver.driver.connect(url, info);
                        //...
                    } catch (SQLException ex) {
                        if (reason == null) {
                            reason = ex;
                        }
                    }
                } 
                //...
            }
        }
    

    重点说明:
    为什么上下文类加载器就可以加载到数据库驱动呢?回到上面一开始Launcher初始化类加载器的源码,我们发现原来所谓的上下文类加载器本质上就是应用类加载器,有没有豁然开朗的感觉?上下文类加载器只是为了解决类的逆向访问提出来的一个概念,并不是一个全新的类加载器,它本质上就是应用类加载器。

    链接:https://www.jianshu.com/p/a6ba4f152968

    转载:https://blog.csdn.net/zhoudaxia/article/details/35897057
    为什么用线程上下文?因为SPI接口一般在核心库里,有引导类加载器(BootStrap)加载,然后具体实现是在系统类加载器(APP)里面,引导类只加载核心库里的,又不能代理给系统类加载器,因为引导类是系统类的祖先

    这个更好理解:https://blog.csdn.net/javazejian/article/details/73413292
    在这里插入图片描述
    但是我有个疑问:
    在这里插入图片描述

    可以这样理解吗:
    我要新建一个对象,那返回的应该是调用new object()方法时候线程(creator)的加载器,(这时候应该是app加载器),然后一路向上走到了bootstrap加载器,然后发现这个类是在核心jar包里的,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载。但具体的方法在app加载器里。这时候系统会判断(比如遍历寻找),如果这个这个方法属于核心jar包,那么就调用这个线程,获取执行new Object的这个线程的加载器(比如app加载器),去加载具体的实现类。没设置的话默认是系统加载器,也就是app.
    在这里插入图片描述

    那么dubbo的SPI为什么需要用这种方式??
    jar包是放在classpath底下的,也就是属于app加载器,那里面为什么要用线程类加载器来加载呢??看到一个回答但不知道对不对:
    在这里插入图片描述

    展开全文
  • 真正理解线程上下文类加载器(多案例分析)

    万次阅读 多人点赞 2016-09-25 13:31:36
    线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使上层代码可以逆向使用下层的系统类加载器。本文通过JDBC和Tomcat两个案例分析,详细解释了其中的原理。

    前置知识: java类加载器不完整分析

    #前言
    此前我对线程上下文类加载器(ThreadContextClassLoader,下文使用TCCL表示)的理解仅仅局限于下面这段话:

    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来加载类。

    而线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。

    一直困恼我的问题就是,它是如何打破了双亲委派模型?又是如何逆向使用类加载器了?直到今天看了jdbc的驱动加载过程才茅塞顿开,其实并不复杂,只是一直没去看代码导致理解不够到位。

    JDBC案例分析

    我们先来看平时是如何使用mysql获取数据库连接的:

    // 加载Class到AppClassLoader(系统类加载器),然后注册驱动类
    // Class.forName("com.mysql.jdbc.Driver").newInstance(); 
    String url = "jdbc:mysql://localhost:3306/testdb";    
    // 通过java库获取数据库连接
    Connection conn = java.sql.DriverManager.getConnection(url, "name", "password"); 
    

    以上就是mysql注册驱动及获取connection的过程,各位可以发现经常写的Class.forName被注释掉了,但依然可以正常运行,这是为什么呢?这是因为从Java1.6开始自带的jdbc4.0版本已支持SPI服务加载机制,只要mysql的jar包在类路径中,就可以注册mysql驱动。

    那到底是在哪一步自动注册了mysql driver的呢?重点就在DriverManager.getConnection()中。我们都是知道调用类的静态方法会初始化该类,进而执行其静态代码块,DriverManager的静态代码块就是:

    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    

    初始化方法loadInitialDrivers()的代码如下:

    private static void loadInitialDrivers() {
        String drivers;
        try {
    		// 先读取系统属性
    		drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        // 通过SPI加载驱动类
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                    // Do nothing
                }
                return null;
            }
        });
        // 继续加载系统属性中的驱动类
        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);
                // 使用AppClassloader加载
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }
    

    从上面可以看出JDBC中的DriverManager的加载Driver的步骤顺序依次是:

    1. 通过SPI方式,读取 META-INF/services 下文件中的类名,使用TCCL加载;
    2. 通过System.getProperty("jdbc.drivers")获取设置,然后通过系统类加载器加载。
      下面详细分析SPI加载的那段代码。

    JDBC中的SPI

    先来看看什么是SP机制,引用一段博文中的介绍:

    SPI机制简介
    SPI的全名为Service Provider Interface,主要是应用于厂商自定义组件或插件中。在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java SPI机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。
    SPI具体约定
    Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader

    知道SPI的机制后,我们来看刚才的代码:

    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    Iterator<Driver> driversIterator = loadedDrivers.iterator();
    
    try{
        while(driversIterator.hasNext()) {
            driversIterator.next();
        }
    } catch(Throwable t) {
    // Do nothing
    }
    

    注意driversIterator.next()最终就是调用Class.forName(DriverName, false, loader)方法,也就是最开始我们注释掉的那一句代码。好,那句因SPI而省略的代码现在解释清楚了,那我们继续看给这个方法传的loader是怎么来的。

    因为这句Class.forName(DriverName, false, loader)代码所在的类在java.util.ServiceLoader类中,而ServiceLoader.class又加载在BootrapLoader中,因此传给 forName 的 loader 必然不能是BootrapLoader,复习双亲委派加载机制请看:java类加载器不完整分析 。这时候只能使用TCCL了,也就是说把自己加载不了的类加载到TCCL中(通过Thread.currentThread()获取,简直作弊啊!)。上面那篇文章末尾也讲到了TCCL默认使用当前执行的是代码所在应用的系统类加载器AppClassLoader。

    再看下看ServiceLoader.load(Class)的代码,的确如此:

    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
    

    ContextClassLoader默认存放了AppClassLoader的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作。

    到这儿差不多把SPI机制解释清楚了。直白一点说就是,我(JDK)提供了一种帮你(第三方实现者)加载服务(如数据库驱动、日志库)的便捷方式,只要你遵循约定(把类名写在/META-INF里),那当我启动时我会去扫描所有jar包里符合约定的类名,再调用forName加载,但我的ClassLoader是没法加载的,那就把它加载到当前执行线程的TCCL里,后续你想怎么操作(驱动实现类的static代码块)就是你的事了。

    好,刚才说的驱动实现类就是com.mysql.jdbc.Driver.Class,它的静态代码块里头又写了什么呢?是否又用到了TCCL呢?我们继续看下一个例子。

    校验实例的归属

    com.mysql.jdbc.Driver加载后运行的静态代码块:

    static {
    	try {
    		// Driver已经加载到TCCL中了,此时可以直接实例化
    		java.sql.DriverManager.registerDriver(new com.mysql.jdbc.Driver());
    	} catch (SQLException E) {
    		throw new RuntimeException("Can't register driver!");
    	}
    }
    

    registerDriver方法将driver实例注册到系统的java.sql.DriverManager类中,其实就是add到它的一个名为registeredDrivers的静态成员CopyOnWriteArrayList中 。

    到此驱动注册基本完成,接下来我们回到最开始的那段样例代码:java.sql.DriverManager.getConnection()。它最终调用了以下方法:

    private static Connection getConnection(
         String url, java.util.Properties info, Class<?> caller) throws SQLException {
         /* 传入的caller由Reflection.getCallerClass()得到,该方法
          * 可获取到调用本方法的Class类,这儿获取到的是当前应用的类加载器
          */
         ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
         synchronized(DriverManager.class) {
             if (callerCL == null) {
                 callerCL = Thread.currentThread().getContextClassLoader();
             }
         }
    
         if(url == null) {
             throw new SQLException("The url cannot be null", "08001");
         }
    
         SQLException reason = null;
         // 遍历注册到registeredDrivers里的Driver类
         for(DriverInfo aDriver : registeredDrivers) {
             // 检查Driver类有效性
             if(isDriverAllowed(aDriver.driver, callerCL)) {
                 try {
                     println("    trying " + aDriver.driver.getClass().getName());
                     // 调用com.mysql.jdbc.Driver.connect方法获取连接
                     Connection con = aDriver.driver.connect(url, info);
                     if (con != null) {
                         // Success!
                         return (con);
                     }
                 } catch (SQLException ex) {
                     if (reason == null) {
                         reason = ex;
                     }
                 }
    
             } else {
                 println("    skipping: " + aDriver.getClass().getName());
             }
    
         }
         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为调用getConnetction的当前类加载器,从中寻找driver的class对象
                aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
            } catch (Exception ex) {
                result = false;
            }
    	// 注意,只有同一个类加载器中的Class使用==比较时才会相等,此处就是校验用户注册Driver时该Driver所属的类加载器与调用时的是否同一个
    	// driver.getClass()拿到就是当初执行Class.forName("com.mysql.jdbc.Driver")时的应用AppClassLoader
            result = ( aClass == driver.getClass() ) ? true : false;
        }
    
        return result;
    }
    

    由于TCCL本质就是当前应用类加载器,所以之前的初始化就是加载在当前的类加载器中,这一步就是校验存放的driver是否属于调用者的Classloader。例如在下文中的tomcat里,多个webapp都有自己的Classloader,如果它们都自带 mysql-connect.jar包,那底层Classloader的DriverManager里将注册多个不同类加载器的Driver实例,想要区分只能靠TCCL了。

    Tomcat与spring的类加载器案例

    接下来将介绍《深入理解java虚拟机》一书中的案例,并解答它所提出的问题。(部分类容来自于书中原文)

    Tomcat中的类加载器

    在Tomcat目录结构中,有三组目录(“/common/*”,“/server/*”和“shared/*”)可以存放公用Java类库,此外还有第四组Web应用程序自身的目录“/WEB-INF/*”,把java类库放置在这些目录中的含义分别是:

    • 放置在common目录中:类库可被Tomcat和所有的Web应用程序共同使用。
    • 放置在server目录中:类库可被Tomcat使用,但对所有的Web应用程序都不可见。
    • 放置在shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。
    • 放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。

    为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,如下图所示
    Tomcat中的类加载器

    灰色背景的3个类加载器是JDK默认提供的类加载器,这3个加载器的作用前面已经介绍过了。而 CommonClassLoader、CatalinaClassLoader、SharedClassLoader 和 WebAppClassLoader 则是 Tomcat 自己定义的类加载器,它们分别加载 /common/*、/server/*、/shared/* 和 /WebApp/WEB-INF/* 中的 Java 类库。其中 WebApp 类加载器和 Jsp 类加载器通常会存在多个实例,每一个 Web 应用程序对应一个 WebApp 类加载器,每一个 JSP 文件对应一个 Jsp 类加载器。

    从图中的委派关系中可以看出,CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 自己能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。而 JasperLoader 的加载范围仅仅是这个 JSP 文件所编译出来的那一个 Class,它出现的目的就是为了被丢弃:当服务器检测到 JSP 文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 Jsp 类加载器来实现 JSP 文件的 HotSwap 功能。

    Spring加载问题

    Tomcat 加载器的实现清晰易懂,并且采用了官方推荐的“正统”的使用类加载器的方式。这时作者提一个问题:如果有 10 个 Web 应用程序都用到了spring的话,可以把Spring的jar包放到 common 或 shared 目录下让这些程序共享。Spring 的作用是管理每个web应用程序的bean,getBean时自然要能访问到应用程序的类,而用户的程序显然是放在 /WebApp/WEB-INF 目录中的(由 WebAppClassLoader 加载),那么在 CommonClassLoader 或 SharedClassLoader 中的 Spring 容器如何去加载并不在其加载范围的用户程序(/WebApp/WEB-INF/)中的Class呢?

    解答

    答案呼之欲出:spring根本不会去管自己被放在哪里,它统统使用TCCL来加载类,而TCCL默认设置为了WebAppClassLoader,也就是说哪个WebApp应用调用了spring,spring就去取该应用自己的WebAppClassLoader来加载bean,简直完美~

    源码分析

    有兴趣的可以接着看看具体实现。在web.xml中定义的listener为org.springframework.web.context.ContextLoaderListener,它最终调用了org.springframework.web.context.ContextLoader类来装载bean,具体方法如下(删去了部分不相关内容):

    public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    	try {
    		// 创建WebApplicationContext
    		if (this.context == null) {
    			this.context = createWebApplicationContext(servletContext);
    		}
    		// 将其保存到该webapp的servletContext中		
    		servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
    		// 获取线程上下文类加载器,默认为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);
    		}
    		
    		return this.context;
    	}
    	catch (RuntimeException ex) {
    		logger.error("Context initialization failed", ex);
    		throw ex;
    	}
    	catch (Error err) {
    		logger.error("Context initialization failed", err);
    		throw err;
    	}
    }
    

    具体说明都在注释中,spring考虑到了自己可能被放到其他位置,所以直接用TCCL来解决所有可能面临的情况。

    总结

    通过上面的两个案例分析,我们可以总结出线程上下文类加载器的适用场景:

    1. 当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。
    2. 当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。
    展开全文
  • 1 线程上下文类加载器 2 何时使用Thread.getContextClassLoader()? 3 类加载器与Web容器 4 类加载器与OSGi 总结 1 线程上下文类加载器  线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的...
  • 这里就用JDBC为例来探索一下为何,如何利用 线程上下文类加载器 破坏双亲委派模型。 java给数据库操作提供了一个Driver接口: public interface Driver { Connection connect(String url, java.util....
  • 类加载机制概念 Java虚拟机把描述的class文件加载到内存,对其进行校验、转换解析、初始化等操作,最终得到可以被虚拟机直接使用的java类型,这就是虚拟机的加载机制。 加载 将class文件读入到内存中,并将其...
  • 此前我对线程上下文类加载器(ThreadContextLoader)的理解仅仅局限于下面这段话: Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC...
  • 线程上下文加载器就是当前线程的Current ClassLoader 在双亲委托模型下,加载器由下至上的,即下层的加载器会委托上层进行加载。但是对于SPI来说,有些接口是java 核心库所提供的,而java核心库是由启动...
  • //如果调用者的类加载器不是null,就用调用者的类加载器加载驱动的实现,否则使用当前线程上下文类加载器, //我们的程序得到的callerCL是系统类加载器。因此synchronized里边的if不会进入执行。 ClassLoader...
  • 在复习前面的加载机制时发现对线程上下文类加载器的概念很模糊,所以今天再来总结一下 线程上下文类加载器  我们知道JVM虚拟机采用双亲委派模式来加载,而且在加载的整个过程中只有在加载阶段可以别程序员...
  • 线程上下文类加载器(context class loader)是从JDK 1.2开始引入的。 java.lang.Thread中的方法getContextClassLoader()和setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有...
  • 为什么叫作“线程上下文加载器”呢,因为这个加载器保存在线程私有数据里,只要是同一个线程,一旦设置了线程上下文加载器,在线程后续执行过程中就能把这个加载器取出来用。 线程上下文加载器其实是线程私有...
  • 文章目录线程上下文类加载器(Thread Context ClassLoader)线程上下文类加载器如何破坏双亲委派模型sun.misc.Launcher 线程上下文类加载器(Thread Context ClassLoader) 线程上下文件类加载器(Thread Context ...
  • Java线程上下文类加载器与SPI

    千次阅读 2016-08-24 22:23:29
    线程上下文类加载器(context class loader)是从JDK 1.2开始引入的。 java.lang.Thread中的方法getContextClassLoader()和setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有...
  • 1 线程上下文类加载器线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上...
  • 在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的线程上下文类加载器在很多 SPI 的实现中都会用到。 简单来说,Java上下文类加载器的作用就是为了SPI机制才存在的,在Java的...
  • Tomcat加载机制以及线程上下文类加载器   tomcat加载需要解决的问题 tomcat是一个很经典的web服务器,一个服务器就相当于一个Java应用,而在这个Java应用中又有着多个被部署的Web应用,因此,有着如下的几个...
  • title: 加载机制(七):线程上下文类加载器 date: 2019-03-21 20:14:06 categories: Java虚拟机 tags: 加载机制 线程上下文类加载器 双亲委托机制的破坏 我们知道,class文件的加载是按照双亲委托机制完成的...
  • Java虚拟机(九):线程上下文类加载器 http://blog.csdn.net/limingjian/article/details/41927445  Brian Goetz对线程安全的定义:当多个线程访问一个对象时,如果不考虑这些线程在运行时环境下的调度和...
  • 线程上下文类加载器和volatile

    千次阅读 2019-03-20 20:33:10
    为什么要有线程上下文类加载器? Jvm的双亲加载即有一定缺陷,JDK核心类库提供很多spi,包括jdbc、jbi、JCE等等,Jdk只规定了这些接口之间的逻辑关系,但不提供具体实现。 例如:Jdbc这个类库在加载的时候想要实现...
  • 线程类上下文加载器的一般使用模式(获取-使用-还原) ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); try{ Thread.currnetThread().setContextClassLoader(targetTccl); ...
  • 线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果...
  • 1.线程上下文类加载器的重要性: 父ClassLoader可以使用当前线程的Thread.currentThread().getContextClassLoader()所指定的classLoder加载的,这就 改变了父ClassLoader不能使用子ClassLoader或者是没有直接父子...
  • 面试时被问到,又一个盲点: 原文链接,感谢作者,讲得很清楚:... Thread.setContextClassLoader(ClassLoader cl) ...在Java中提供了对于线程设置ContextClassLoader
  • 最近在读《架构探险-从零开始写java web框架》一书时,看到了一个获取类加载器的地方是这样写的: public static ClassLoader getClassLoader() { return Thread.currentThread().getContextClassLoader(); ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 87,915
精华内容 35,166
关键字:

线程上下文类加载器