目录
Tomcat 组件简介
tomcat的各个组件
Http请求的执行图示:
tomcat启动源码时序图
tomcat接受请求源码时序图
带着问题看源码:发起http请求后,tomcat是如何解析的?这个需要看tomcat的整体执行请求的过程
Tomcat的执行请求流程
发起一个请求到tomcat,在connector这里的细节是:
EndPoint:首先会在NoiEndpoint中setSocketOptions()被捕获到,然后设置socket属性,从NioChannel栈中获取NioChannel,注册到 一直监听到达的TCP / IP连接的后台线程,从socket缓存栈中pop出一个SocketProcessorBase(套接字),然后丢到Executor线程中去执行后续逻辑。(若debug源码,可以将源码变更在当前线程执行就可以继续debug。)通过doRun()方法getHandler().process(socketWrapper, event);通过Handler处理器进行处理。
ConnectionHandler:process()方法:连接处理器从连接缓存池中获取processor来处理socketWrapper。
Http11Processor:后续通过getAdapter().service(request, response);适配器模式,通过适配器来处理相应逻辑,在此处只有CoyoteAdapter实现。
CoyoteAdapter:service()中将org.apache.coyote.Request 转化为org.apache.catalina.connector.Request,而这个是HttpServletRequest的子类,所以后续其实是对HttpServletRequest、HttpServletResponse的操作。后续的postParseRequest(req, request, res, response);会根据请求通过mapperListener进行一系列匹配规则,解析出具体的host、context、wrapper类,接着通过责任链模式在组件中找到目标Servlet及FilterChain拦截器链。FilterChain->Servlet依次执行。
// 通过service的mapper中的Map<Context, ContextVersion> contextObjectToContextVersionMap来匹配出执行链路
connector.getService().getMapper().map(serverName, decodedURI,
version, request.getMappingData());
// 内部调用internalMap(host.getCharChunk(), uri.getCharChunk(), version,
mappingData);实现
// 可以看到通过mapper中的host名称匹配出具体执行的host后,获取该host下context和wrapper,
// 通过一系列匹配规则(精确匹配、前缀匹配、扩展匹配、欢迎资源处理servlet)过滤生成mappingData对象
MappedHost mappedHost = exactFindIgnoreCase(hosts, host);
// ...省略
// Context mapping
ContextList contextList = mappedHost.contextList;
MappedContext[] contexts = contextList.contexts;
// 通过pipeline责任链模式来执行后续操作
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
Container执行细节:
engine:getService().getContainer()自然就是engine。根据request的mappingData.host来执行具体host的pipeline执行逻辑,而mappingData就是通过匹配规则得出的执行host、context、wrapper的对象。后续的逻辑类似,在责任链执行时会通过request中的context、wrapper来执行后续的逻辑。
Host、context:pipeline责任链执行
Wrapper:得到Servlet实例,获取当前context上下文,获取过滤器链,执行完过滤器链后,执行servlet方法。
servlet = wrapper.allocate();
// ...省略
// Create the filter chain for this request 给该request创建Filter过滤链。Filter过滤链执行完之后,执行Servelt
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
业务数据返回,和请求解析的路径一致,依次return完成。
这时就完成了一个请求的解析到执行,那么整个流程的url解析最关键的就在于CoyoteAdapter会拿到service的mapper,那么mapper到底是什么时候点被加载到数据中、engine、host、context、wrapper之间的管理关系又是什么样的?下面看下启动流程。
Tomcat启动流程
Tomcat启动分为两个主要部分分别是:load()、start()
Tomcat启动:会通过Bootstrap的main方法,先初始化daemon,主要是初始化commonLoader、catalinaLoader、sharedLoader三个类加载器(三个类加载本质都是 urlClassLoader),通过类加载加载,构造器创建Catalina类实例,便于后续init()。(问题一:jdk已经自带有类加载器了,这里创建类加载器的意义是什么?)
load():
Catalina的load方法:
// 解析server.xml输入流数据,填充属性(指定使用digester解析的输入源内容,从堆栈对象返回根元素)
// 这里就会建立service->engine->host的对应关系,并作为catalina的server属性记录
digester.parse(inputSource);
// 初始化解析出的service
getServer().init();
从下图也能看出,解析server.xml后,基本的映射关系已经建立,且server、service、engine有属性关系来进行保存的,这样各个组件之间关联关系也建立了。
Server:组件的init都是由同一个父类LifecycleBase来定义的,各子类只需要实现initInternal()方法即可(模板方法),server具体实现会services[i].init();将server.xml解析出来的services初始化。
Service:看下面service代码具体实现和注释
protected void initInternal() throws LifecycleException {
super.initInternal();
if (engine != null) {
// 初始化engine(service 一对一)
engine.init();
}
// Initialize any Executors
for (Executor executor : findExecutors()) {
if (executor instanceof JmxEnabled) {
((JmxEnabled) executor).setDomain(getDomain());
}
// 初始化线程池(一对多,由此也可以看出线程池是service之间是隔离的,组件间共享)
executor.init();
}
// Initialize mapper listener
// 初始化mapperListener,mapper监听器
mapperListener.init();
// Initialize our defined Connectors
// 初始化connectors,连接器(HTTP、AJP两种)
synchronized (connectorsLock) {
for (Connector connector : connectors) {
try {
connector.init();
} catch (Exception e) {
String message = sm.getString(
"standardService.connector.initFailed", connector);
log.error(message, e);
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE"))
throw new LifecycleException(message);
}
}
}
}
Connector:
super.initInternal();
// Initialize adapter 初始化适配器(可以看到初始化连接器时,协议处理器和适配器已经绑定了)
adapter = new CoyoteAdapter(this);
protocolHandler.setAdapter(adapter);
// ***省略
try {
// 初始化协议处理器(内部会初始化endpoint)
protocolHandler.init();
} catch (Exception e) {
throw new LifecycleException(
sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e);
}
这样整个load()过程已经结束了,接下来看下
Start():
和load()类似的会从server先进行start(),通过模板方法,执行固定逻辑,然后在server的startInternal()方法中,start()所有的service。
Service:看下service的具体start()实现:和load类似依次执行相关的start()。
protected void startInternal() throws LifecycleException {
if(log.isInfoEnabled())
log.info(sm.getString("standardService.start.name", this.name));
setState(LifecycleState.STARTING);
// Start our defined Container first
// 启动engine引擎
if (engine != null) {
synchronized (engine) {
engine.start();
}
}
// 线程池启动
synchronized (executors) {
for (Executor executor: executors) {
executor.start();
}
}
// mapper监听器启动
mapperListener.start();
// Start our defined Connectors second
// 连接器启动
synchronized (connectorsLock) {
for (Connector connector: connectors) {
try {
// If it has already failed, don't try and start it
if (connector.getState() != LifecycleState.FAILED) {
connector.start();
}
} catch (Exception e) {
log.error(sm.getString(
"standardService.connector.startFailed",
connector), e);
}
}
}
}
这里debug可以看到,mapperListener中host下的关联数据还未加载关联,整个组件的关联关系还不完善,host是如何加载到context,又如何加载到wrapper的呢?接下来看下engine的start()流程
engine():StartEngine的startInternal()方法,后续host组件在创建时通过线程池启动,Future来等待执行结果。此时可以通过注释掉,以单线程方式去执行,即可debug后续的组件start()流程。通过下面代码可见:engine的children是host,而host在解析xml时,已经成功注入mapperListener中,而host此时仍没有context数据。继续往下走:host.start();
// // 获取所有的子节点,并启动(如:host)
// Start our child containers, if any
Container children[] = findChildren();
List<Future<Void>> results = new ArrayList<>();
for (int i = 0; i < children.length; i++) {
results.add(startStopExecutor.submit(new StartChild(children[i])));
}
MultiThrowable multiThrowable = null;
// Future设计模式,拿着票据等待
for (Future<Void> 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());
}
HOST():模板方法执行子类具体实现前,会通过发送before_start事件监听的方式,来处理关联host的START事件(观察者模式),
try {
// 识别该host的xml、war中的配置、解析ContextClass()
host = (Host) event.getLifecycle();
if (host instanceof StandardHost) {
setCopyXML(((StandardHost) host).isCopyXML());
setDeployXML(((StandardHost) host).isDeployXML());
setUnpackWARs(((StandardHost) host).isUnpackWARs());
setContextClass(((StandardHost) host).getContextClass());
}
} catch (ClassCastException e) {
log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e);
return;
}
// Process the event that has occurred
if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
// Lifecycle.PERIODIC_EVENT周期性事件,若host支持自动部署,则会定期自动检查资源是否变更
// 变更后,重新load()所有组件,这也就是热部署的实现。
check();
} else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
// 创建host的class文件夹、生成文件
beforeStart();
} else if (event.getType().equals(Lifecycle.START_EVENT)) {
start();
} else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
stop();
}
后续执行engine的pipeline责任链的start();去初始化host的信息,通过LifecycleBase的init方法,走的standardpipeline的startInternal()方法。下面可以看出责任链的调用情况。
当host创建成功状态变为START时,会被hostConfig监听到start()事件,然后去发现自动部署war包,将解析出的组件信息关联到host组件中,此时host后续的context数据进行了关系映射。
Context context = null;
File xml = new File(dir, Constants.ApplicationContextXml);
File xmlCopy =
new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml");
DeployedApplication deployedApp;
boolean copyThisXml = isCopyXML();
boolean deployThisXML = isDeployThisXML(dir, cn);
try {
// 各种方式去解析构造context信息
if (deployThisXML && xml.exists()) {
synchronized (digesterLock) {
try {
context = (Context) digester.parse(xml);
} catch (Exception e) {
log.error(sm.getString(
"hostConfig.deployDescriptor.error",
xml), e);
context = new FailedContext();
} finally {
digester.reset();
if (context == null) {
context = new FailedContext();
}
}
}
if (copyThisXml == false && context instanceof StandardContext) {
// Host is using default value. Context may override it.
copyThisXml = ((StandardContext) context).getCopyXML();
}
if (copyThisXml) {
Files.copy(xml.toPath(), xmlCopy.toPath());
context.setConfigFile(xmlCopy.toURI().toURL());
} else {
context.setConfigFile(xml.toURI().toURL());
}
} else if (!deployThisXML && xml.exists()) {
// Block deployment as META-INF/context.xml may contain security
// configuration necessary for a secure deployment.
log.error(sm.getString("hostConfig.deployDescriptor.blocked",
cn.getPath(), xml, xmlCopy));
context = new FailedContext();
} else {
context = (Context) Class.forName(contextClass).getConstructor().newInstance();
}
// 设置context属性和host构造子属性的关系
Class<?> clazz = Class.forName(host.getConfigClass());
LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
context.addLifecycleListener(listener);
context.setName(cn.getName());
context.setPath(cn.getPath());
context.setWebappVersion(cn.getVersion());
context.setDocBase(cn.getBaseName());
host.addChild(context);
}
建立host与context关系,context的startInternal(),会将先启动资源文件,创建context自己的WebAppClassLoader,并启动类加载器加载web下的class文件,并发送事件通知
// 通知生命周期监听器Configure启动事件。
// 触发了ContextConfig的configureStart方法,configureStart执行webConfig方法,webConfig执行了configureContext启动了容器
// Notify our interested LifecycleListeners
fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);
contextConfig会监听并通过configureStart()的webConfig();来注入servlet,即扫描注入应用程序的web.xml的配置信息。这样整体的组件映射关系已经建立,后续就是wrapper自定义的start()操作了,没有特殊差异,关于启动流程就到这里。
在WebAppClassLoader去加载servlet的时候,这个和JAVA默认的双亲委派机制加载class文件有所差别。
tomcat类加载机制及双亲委派机制
Tomcat中web下的class类加载机制
Tomcat提供的Web应用类加载器与默认的委派机制不同。当进行类加载器时,除了JVM基础类库外,它会首先尝试通过当前类加载器加载,然后才进行委派。Servelt规范相关API禁止通过Web应用类加载器加载,因此不要在Web应用中包含这些包。
Web应用类加载器默认的加载顺序如下:
(1)从缓存中加载
(2)如果没有,则从JVM的extension(该加载器继承BootStrap,因此会加载/ext/等java se核心jar,避免核心类库被篡改)类加载器加载。
(3)如果没有,则从当前类加载器加载(按照WEB-INF/classes、WEB-INF/lib的顺序)
(4)如果没有,则从当前父类加载器加载,由于父类加载器采用默认的委派模式,所以加载顺序为System、Common、Shared。
Tomcat提供了delegate属性用于控制是否启用Java委派模式,默认为false(不启用)。当配置为true时,Tomcat将使用Java默认的委派模式:
(1)从缓存中加载
(2)如果没有,从JVM的Bootstrap类加载器加载
(3)如果没有,从父类的加载器加载(System、Common、Shared)
(4)如果没有,则从当前类加载器加载。
除了可以通过delegate属性控制是否启用Java的委派模式外,Tomcat还可以通过packegeTriggerDeny属性只让某些包路径采用Java的委派模式,Web应用类加载器对于符合packegeTriggerDeny指定包路径的类强制采用Java的委派模式。
Tomcat通过该机制实现对Web应用中的Jar包覆盖服务器提供包的目的,Java核心类库、Servelt规范相关类库是无法覆盖的,此外Java默认提供的诸如XML工具包,由于位于Jvm的Bootstrap类加载器也无法覆盖,只能通过endorsed方式实现。
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
// 先在本地缓存中查找是否已经加载过该类(对于一些已经加载了的类,会被缓存在resourceEntries这个数据结构中),如果已经加载即返回.
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;
}
// 尝试加载系统类装入器的类,防止webapp重写Java SE类
// (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);
// 加載jre/lib/ext中的jar ExtClassLoader
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://bz.apache.org/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://bz.apache.org/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<URL> 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;
}
// javaseLoader加載jre/lib/ext中的jar
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 = "Security Violation, attempt to use " +
"Restricted Class: " + name;
log.info(error, se);
throw new ClassNotFoundException(error, se);
}
}
}
boolean delegateLoad = delegate || filter(name, true);
// 如果是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
}
}
// 让系统类加载器(AppClassLoader)尝试加载该类,主要是为了防止一些基础类会被web中的类覆盖,如果加载到即返回,返回继续。
// (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);
}
双亲委派机制
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委派父类加载器去完成。每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个请求(他的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
1)当自定义一个java.lang.String对象并且尝试在程序里面使用时,会发现程序不会调用自定义的String对象,而是调用jdk提供的String对象,是因为类加载时,会先让他的父类加载器先去加载,一直向上委托,直到启动类加载器,启动类加载器会加载java包下的类,所以String对象是jdk提供的对象。
(2)同理,如果想运行自己定义的java.lang.String的main方法,程序会报错,提示找不到main方法,因为jdk提供的String对象不存在main方法。
双亲委派机制的作用就是避免类的重复加载、保护程序安全,防止核心API被随意篡改
ClassLoader
ClassLoader的loadClass实现了双亲委派机制
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 1、首先从缓存中获取
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 2、缓存中没有,就从父类加载器从加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// parent == null,代表parent为BootStrap类加载器。
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 如果还没有找到,就调用findClass加载类。
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
为什么不使用双亲委派机制?
参看:tomcat类加载器为什么要破坏双亲委派机制? - 牧云文仔 - 博客园
待探究:tomcat热加载机制:idea的热启动在依赖了tomcat的热加载同时,有什么改进?