在容器的基类ContainerBase
类中的模板方法startInternal
中会提交一个定时的后台任务ContainerBackgroundProcessorMonitor
去监控容器,源码如下:
protected synchronized void startInternal() throws LifecycleException { // Start our subordinate components, if any logger = null; getLogger(); Cluster cluster = getClusterInternal(); if (cluster instanceof Lifecycle) { ((Lifecycle) cluster).start(); } Realm realm = getRealmInternal(); if (realm instanceof Lifecycle) { ((Lifecycle) realm).start(); } // Start our child containers, if any Container children[] = findChildren(); List
> results = new ArrayList<>(); for (int i = 0; i < children.length; i++) { results.add(startStopExecutor.submit(new StartChild(children[i]))); } MultiThrowable multiThrowable = null; for (Future
result : results) { try { result.get(); } catch (Throwable e) { log.error(sm.getString("containerBase.threadedStartFailed"), e); if (multiThrowable == null) { multiThrowable = new MultiThrowable(); } multiThrowable.add(e); } } if (multiThrowable != null) { throw new LifecycleException(sm.getString("containerBase.threadedStartFailed"), multiThrowable.getThrowable()); } // Start the Valves in our pipeline (including the basic), if any if (pipeline instanceof Lifecycle) { ((Lifecycle) pipeline).start(); } setState(LifecycleState.STARTING); // Start our thread if (backgroundProcessorDelay > 0) { monitorFuture = Container.getService(ContainerBase.this).getServer() .getUtilityExecutor().scheduleWithFixedDelay( new ContainerBackgroundProcessorMonitor(), 0, 60, TimeUnit.SECONDS); } }
在ContainerBackgroundProcessorMonitor
任务中对当前容器进行了判断,如果满足backgroundProcessorDelay>0
和容器当前状态属于正常,则启动了一个定时任务ContainerBackgroundProcessor
,ContainerBackgroundProcessor
任务核心处理如下:
protected void processChildren(Container container) { ClassLoader originalClassLoader = null; try { if (container instanceof Context) { Loader loader = ((Context) container).getLoader(); // Loader will be null for FailedContext instances if (loader == null) { return; } // Ensure background processing for Contexts and Wrappers // is performed under the web app's class loader originalClassLoader = ((Context) container).bind(false, null); } container.backgroundProcess(); Container[] children = container.findChildren(); for (int i = 0; i < children.length; i++) { if (children[i].getBackgroundProcessorDelay() <= 0) { processChildren(children[i]); } } } catch (Throwable t) { ExceptionUtils.handleThrowable(t); log.error(sm.getString("containerBase.backgroundProcess.error"), t); } finally { if (container instanceof Context) { ((Context) container).unbind(false, originalClassLoader); } } }
在这个方法中调用当前容器的 backgroundProcess
方法,以及递归调用子孙的 backgroundProcess
方法。backgroundProcess
是 Container
接口中的方法,也就是说所有类型的容器都可以实现这个方法,在这个方法里完成需要周期性执行的任务,在递归调用子容器的backgroundProcess
时同样也对backgroundProcessorDelay
参数做了判断,在这个参数大于0时,当前容器会开始自己的周期任务来自己执行backgroundProcess
方法,不需要父容器代为执行。
backgroundProcess
在容器基类ContainerBase
中提供了默认实现:
public void backgroundProcess() { if (!getState().isAvailable()) return; Cluster cluster = getClusterInternal(); if (cluster != null) { try { cluster.backgroundProcess(); } catch (Exception e) { log.warn(sm.getString("containerBase.backgroundProcess.cluster", cluster), e); } } Realm realm = getRealmInternal(); if (realm != null) { try { realm.backgroundProcess(); } catch (Exception e) { log.warn(sm.getString("containerBase.backgroundProcess.realm", realm), e); } } Valve current = pipeline.getFirst(); while (current != null) { try { current.backgroundProcess(); } catch (Exception e) { log.warn(sm.getString("containerBase.backgroundProcess.valve", current), e); } current = current.getNext(); } fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null); }
基类中处理流程如下:
1. 执行容器中Cluster组件的周期性任务。
2. 执行容器中Realm组件的周期性任务。
3. 执行容器中valve的周期性任务。
4. 触发容器生命周期事件。
容器如果要自己去实现周期性任务只需重写此方法即可,如果不想受父容器管理则需同时修改backgroundProcessorDelay
参数值,tomcat
的热加载就是由StandardContext
重写了此方法和修改了backgroundProcessorDelay>0
实现,而热部署则是由HostConfig
监听了容器的Lifecycle.PERIODIC_EVENT
事件实现。
tomcat
的热加载是由Context
容器重写了周期性任务处理方法backgroundProcess
实现的,其源码如下:
public void backgroundProcess() { if (!getState().isAvailable()) return; Loader loader = getLoader(); if (loader != null) { try { loader.backgroundProcess(); } catch (Exception e) { log.warn(sm.getString( "standardContext.backgroundProcess.loader", loader), e); } } Manager manager = getManager(); if (manager != null) { try { manager.backgroundProcess(); } catch (Exception e) { log.warn(sm.getString( "standardContext.backgroundProcess.manager", manager), e); } } WebResourceRoot resources = getResources(); if (resources != null) { try { resources.backgroundProcess(); } catch (Exception e) { log.warn(sm.getString( "standardContext.backgroundProcess.resources", resources), e); } } InstanceManager instanceManager = getInstanceManager(); if (instanceManager != null) { try { instanceManager.backgroundProcess(); } catch (Exception e) { log.warn(sm.getString( "standardContext.backgroundProcess.instanceManager", resources), e); } } super.backgroundProcess(); }
其处理流程为:
1. 获取WebappLoader
,然后执行WebappLoader
的周期性处理方法,检查WEB-INF/classes
和WEB-INF/lib
目录下的类文件,如果有变化,则会调用Context
容器的reload方法
。
2. 获取Manager
,然后周期性检查是否有过期的session
.
3. 获取WebResourceRoot
,然后周期性检查静态资源是否有变化。
4. 调用父容器的backgroundProcess
方法。
其中热加载处理的核心为Context
的reload
方法,在这个方法中会停止和销毁Context
及其子容器、Context
关联的listener
和filter
、Context
的Pipline
和各种valve
、Context
的类加载器以及加载器加载的类文件资源,然后重新启动Context
重建被销毁的资源。在reload
方法中并没有调用Manager
的destroy
方法,所以不会销毁Session
,其源码如下:
/** * Reload this web application, if reloading is supported. *
* IMPLEMENTATION NOTE: This method is designed to deal with * reloads required by changes to classes in the underlying repositories * of our class loader and changes to the web.xml file. It does not handle * changes to any context.xml file. If the context.xml has changed, you * should stop this Context and create (and start) a new Context instance * instead. Note that there is additional code in *
CoyoteAdapter#postParseRequest()
to handle mapping requests * to paused Contexts. * * @exception IllegalStateException if thereloadable
* property is set tofalse
. */ @Override public synchronized void reload() { // Validate our current component state if (!getState().isAvailable()) throw new IllegalStateException (sm.getString("standardContext.notStarted", getName())); if(log.isInfoEnabled()) www.jmhat.com(sm.getString("standardContext.reloadingStarted", getName())); // Stop accepting requests temporarily. setPaused(true); try { stop(); } catch (LifecycleException e) { log.error( sm.getString("standardContext.stoppingContext", getName()), e); } try { start(); } catch (LifecycleException e) { log.error( sm.getString("standardContext.startingContext", getName()), e); } setPaused(false); if(log.isInfoEnabled()) www.jmhat.com(sm.getString("standardContext.reloadingCompleted", getName())); }
tomncat
的热加载是默认关闭的,如果需要开启,需要在conf
目录下的context.xml
文件中设置reloadable
参数来开启这个功能。
tomcat
的热部署就是重新部署我们的Web应用,对应到代码中也是就是我们的Context
容器,上面我们提到了热加载,在热加载中我们只是停止和销毁了容器的部分组件,但是在热部署中会整个销毁或者新建Context
对象,由于Context
容器是Host
容器的子容器,所以这个任务交给了Host
容器来处理,但不是直接通过backgroundProcess
方法来处理的,但是通过监听backgroundProcess
方法触发的生命周期事件fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null)
来实现Context
的热部署,具体是由HostConfig
来监听这个事件,然后它在接到通知后执行了Check
方法,在Check
方法中处理了检测是否Web
应用目录由变化,如果有变化则进行部署,其源码如下:
/** * Check status of all webapps. */ protected void check() { if (host.getAutoDeploy()) { // Check for resources modification to trigger redeployment DeployedApplication[] apps = deployed.values().toArray(new DeployedApplication[0]); for (int i = 0; i < apps.length; i++) { if (!isServiced(apps[i].name)) checkResources(apps[i], false); } // Check for old versions of applications that can now be undeployed if (host.getUndeployOldVersions()) { checkUndeploy(); } // Hotdeploy applications deployApps(); } }
在deployApps()
方法中实现了对Web
应用的部署,源码如下:
protected void deployApps() { File appBase = host.getAppBaseFile(); File configBase = host.getConfigBaseFile(); String[] filteredAppPaths = filterAppPaths(appBase.list()); // Deploy XML descriptors from configBase deployDescriptors(configBase, configBase.list()); // Deploy WARs deployWARs(appBase, filteredAppPaths); // Deploy expanded folders deployDirectories(appBase, filteredAppPaths); }
AppClassLoader 只能加载一个 Servlet 类,在加载第二个同名 Servlet 类时,AppClassLoader 会返回第一个 Servlet 类的 Class 实例,这是因为在 AppClassLoader 看来,同名的 Servlet 类只被加载一次。
因此 Tomcat 的解决方案是自定义一个类加载器 WebAppClassLoader, 并且给每个 Web 应用创建一个类加载器实例。我们知道,Context 容器组件对应一个 Web 应用,因此,每个 Context 容器负责创建和维护一个 WebAppClassLoader 加载器实例。这背后的原理是,不同的加载器实例加载的类被认为是不同的类,即使它们的类名相同。这就相当于在 Java 虚拟机内部创建了一个个相互隔离。
两个 Web 应用之间怎么共享库类,并且不能重复加载相同的类。我们知道,在双亲委托机制里,各个子加载器都可以通过父加载器去加载类,那么把需要共享的类放到父加载器的加载路径下不就行了吗,应用程序也正是通过这种方式共享 JRE 的核心类。
因此 Tomcat 的设计者又加了一个类加载器 SharedClassLoader,作为 WebAppClassLoader 的父加载器,专门来加载 Web 应用之间共享的类。如果 WebAppClassLoader 自己没有加载到某个类,就会委托父加载器 SharedClassLoader 去加载这个类,SharedClassLoader 会在指定目录下加载共享类,之后返回给 WebAppClassLoader,这样共享的问题就解决了。
如何隔离 Tomcat 本身的类和 Web 应用的类?我们知道,要共享可以通过父子关系,要隔离那就需要兄弟关系了。兄弟关系就是指两个类加载器是平行的,它们可能拥有同一个父加载器,但是两个兄弟类加载器加载的类是隔离的。基于此 Tomcat 又设计一个类加载器 CatalinaClassLoader,专门来加载 Tomcat 自身的类。
Tomcat 和各 Web 应用之间需要共享一些类时该怎么办呢?还是再增加一个 CommonClassLoader,作为 CatalinaClassLoader 和 SharedClassLoader 的父加载器。CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。
Tomcat
的自定义类加载器WebAppClassLoader
打破了双亲委托机制,它首先自己尝试去加载某个类,如果找不到再代理给父类加载器,其目的是优先加载 Web
应用自己定义的类。具体实现就是重写ClassLoader
的两个方法:findClass
和 loadClass
。
其中findClass
源码如下:
/** * Find the specified class in our local repositories, if possible. If * not found, throw ClassNotFoundException
. * * @param name The binary name of the class to be loaded * * @exception ClassNotFoundException if the class was not found */ @Override public Class
findClass(String name) throws ClassNotFoundException { if (log.isDebugEnabled()) log.debug(" findClass(" + name + ")"); checkStateForClassLoading(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 + ")"); try { if (securityManager != null) { PrivilegedAction
> dp = new PrivilegedFindClassByName(name); clazz = AccessController.doPrivileged(dp); } else { clazz = findClassInternal(name); } } catch(AccessControlException ace) { log.warn(sm.getString("webappClassLoader.securityException", name, 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) { try { clazz = super.findClass(name); } catch(AccessControlException ace) { log.warn(sm.getString("webappClassLoader.securityException", name, 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; }
在 findClass 方法里,主要有四个步骤:
1. 如果使用了SecurityManager,则会先检查权限。
2. 先在 Web 应用本地目录下查找要加载的类。
3. 如果没有找到,交给父加载器去查找,它的父加载器就是上面提到的系统类加载器 AppClassLoader。
4. 如何父加载器也没找到这个类,抛出 ClassNotFound 异常。
loadClass
源码如下:
/** * Load the class with the specified name, searching using the following * algorithm until it finds and returns the class. If the class cannot * be found, returns ClassNotFoundException
. *
*
- Call
findLoadedClass(String)
to check if the * class has already been loaded. If it has, the same * Class
object is returned. *
- If the
delegate
property is set to true
, * call the loadClass()
method of the parent class * loader, if any. *
- Call
findClass()
to find this class in our locally * defined repositories. *
- Call the
loadClass()
method of our parent * class loader, if any. *
* If the class was found using the above steps, and the * resolve
flag is true
, this method will then * call resolveClass(Class)
on the resulting Class object. * * @param name The binary name of the class to be loaded * @param resolve If true
then resolve the class * * @exception ClassNotFoundException if the class was not found */ @Override public Class
loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { if (log.isDebugEnabled()) log.debug("loadClass(" + name + ", " + resolve + ")"); Class
clazz = null; // Log access to stopped class loader checkStateForClassLoading(name); // (0) Check our previously loaded local class cache clazz = findLoadedClass0(name); 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 clazz = findLoadedClass(name); 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 Java SE classes. This implements // SRV.10.7.2 String resourceName = binaryNameToPath(name, false); ClassLoader javaseLoader = getJavaseClassLoader(); boolean tryLoadingFromJavaseLoader; try { // Use getResource as it won't trigger an expensive // ClassNotFoundException if the resource is not available from // the Java SE class loader. However (see // https://www.jmhat.com/bugzilla/show_bug.cgi?id=58125 for // details) when running under a security manager in rare cases // this call may trigger a ClassCircularityError. // See https://www.jmhat.com/bugzilla/show_bug.cgi?id=61424 for // details of how this may trigger a StackOverflowError // Given these reported errors, catch Throwable to ensure any // other edge cases are also caught URL url; if (securityManager != null) { PrivilegedAction
dp = new PrivilegedJavaseGetResource(resourceName); url = AccessController.doPrivileged(dp); } else { url = javaseLoader.getResource(resourceName); } tryLoadingFromJavaseLoader = (url != null); } catch (Throwable t) { // Swallow all exceptions apart from those that must be re-thrown ExceptionUtils.handleThrowable(t); // The getResource() trick won't work for this class. We have to // try loading it directly and accept that we might get a // ClassNotFoundException. tryLoadingFromJavaseLoader = true; } if (tryLoadingFromJavaseLoader) { try { clazz = javaseLoader.loadClass(name); 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 = sm.getString("webappClassLoader.restrictedPackage", name); www.jmhat.com(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 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 } // (3) Delegate to parent unconditionally 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); }
loadClass
主要有六个步骤:
Tomcat
是用Wrapper
容器来管理Servlet
的,并且StandardWrapper
通过loadServlet
方法来实例化 Servlet
,其流程图如下:
Filter
的实例是在Context
容器中进行管理的,Context
容器用Map
集合来保存Filter
。其核心方法源码如下:
/** * Invoke the next filter in this chain, passing the specified request * and response. If there are no more filters in this chain, invoke * the service()
method of the servlet itself. * * @param request The servlet request we are processing * @param response The servlet response we are creating * * @exception IOException if an input/output error occurs * @exception ServletException if a servlet exception occurs */ @Override public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { if( Globals.IS_SECURITY_ENABLED ) { final ServletRequest req = request; final ServletResponse res = response; try { java.security.AccessController.doPrivileged( new java.security.PrivilegedExceptionAction
() { @Override public Void run() throws ServletException, IOException { internalDoFilter(req,res); return null; } } ); } catch( PrivilegedActionException pe) { Exception e = pe.getException(); if (e instanceof ServletException) throw (ServletException) e; else if (e instanceof IOException) throw (IOException) e; else if (e instanceof RuntimeException) throw (RuntimeException) e; else throw new ServletException(e.getMessage(), e); } } else { internalDoFilter(request,response); } } private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { // Call the next filter if there is one if (pos < n) { ApplicationFilterConfig filterConfig = filters[pos++]; try { Filter filter = filterConfig.getFilter(); if (request.isAsyncSupported() && "false".equalsIgnoreCase( filterConfig.getFilterDef().getAsyncSupported())) { request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE); } if( Globals.IS_SECURITY_ENABLED ) { final ServletRequest req = request; final ServletResponse res = response; Principal principal = ((HttpServletRequest) req).getUserPrincipal(); Object[] args = new Object[]{req, res, this}; SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal); } else { filter.doFilter(request, response, this); } } catch (IOException | ServletException | RuntimeException e) { throw e; } catch (Throwable e) { e = ExceptionUtils.unwrapInvocationTargetException(e); ExceptionUtils.handleThrowable(e); throw new ServletException(sm.getString("filterChain.filter"), e); } return; } // We fell off the end of the chain -- call the servlet instance try { if (ApplicationDispatcher.WRAP_SAME_OBJECT) { lastServicedRequest.set(request); lastServicedResponse.set(response); } if (request.isAsyncSupported() && !servletSupportsAsync) { request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE); } // Use potentially wrapped request from this point if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse) && Globals.IS_SECURITY_ENABLED ) { final ServletRequest req = request; final ServletResponse res = response; Principal principal = ((HttpServletRequest) req).getUserPrincipal(); Object[] args = new Object[]{req, res}; SecurityUtil.doAsPrivilege("service", servlet, classTypeUsedInService, args, principal); } else { servlet.service(request, response); } } catch (IOException | ServletException | RuntimeException e) { throw e; } catch (Throwable e) { e = ExceptionUtils.unwrapInvocationTargetException(e); ExceptionUtils.handleThrowable(e); throw new ServletException(sm.getString("filterChain.servlet"), e); } finally { if (ApplicationDispatcher.WRAP_SAME_OBJECT) { lastServicedRequest.set(null); lastServicedResponse.set(null); } } }
从 ApplicationFilterChain 的源码我们可以看到几个关键信息:
Tomcat 是通过 Context 容器来管理这些监听器的。Context 容器将两类事件分开来管理,分别用不同的集合来存放不同类型事件的监听器:
// 监听属性值变化的监听器 private List