Tomcat 的本质其实就是一个 WEB 服务器 + 一个 Servlet 容器,那么它必然需要处理网络的连接与 Servlet 的管理,因此,Tomcat 设计了两个核心组件来实现这两个功能,分别是连接器和容器,连接器用来处理外部网络连接,容器用来处理内部 Servlet。
public class Connector extends LifecycleMBeanBase {
/**
* Coyote protocol handler.
*/
// 协议的handler
protected final ProtocolHandler protocolHandler;
/**
* Coyote adapter.
*/
protected Adapter adapter = null;
}
根据protocol=”HTTP/1.1”,生成ProtocolHandler的子类Http11NioProtocol
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
http/1.1 -> protocolHandlerClassName = "org.apache.coyote.http11.Http11NioProtocol";
public class Http11NioProtocol extends AbstractHttp11JsseProtocol<NioChannel> {
public Http11NioProtocol() {
// nioEndpoint 利用nio和reactor模式实现io操作
super(new NioEndpoint());
}
protected class SocketProcessor extends SocketProcessorBase<NioChannel> {
public SocketProcessor(SocketWrapperBase<NioChannel> socketWrapper, SocketEvent event) {
super(socketWrapper, event);
}
@Override
protected void doRun() {
getHandler().process(socketWrapper, SocketEvent.OPEN_READ);
}
// 处理连接的handler
protected static class ConnectionHandler<S> implements AbstractEndpoint.Handler<S> {
Http11Processor processor = new Http11Processor(this, adapter);
processor.process(wrapper, status);
}
}
Endpoint 是用来实现 TCP/IP 协议的,Processor 用来实现 HTTP 协议
org.apache.tomcat.util.net.NioEndpoint.SocketProcessor#doRun->org.apache.coyote.AbstractProtocol.ConnectionHandler#process->org.apache.coyote.AbstractProcessorLight#process->org.apache.coyote.AbstractProcessorLight#process->org.apache.coyote.http11.Http11Processor#service->
org.apache.catalina.connector.CoyoteAdapter#service(设置request的属于哪个host实例)->
connector.getService().getContainer().getPipeline().getFirst().invoke.invoke->
org.apache.catalina.core.StandardEngineValve#invoke->
org.apache.catalina.valves.AbstractAccessLogValve#invoke->
org.apache.catalina.valves.ErrorReportValve#invoke->
org.apache.catalina.core.StandardHostValve#invoke->
org.apache.catalina.authenticator.AuthenticatorBase#invoke->
org.apache.catalina.core.StandardContextValve#invoke->
org.apache.catalina.core.StandardWrapperValve#invoke->
org.apache.catalina.core.StandardWrapper#allocate->
org.apache.catalina.core.StandardWrapper#loadServlet->
StandardEngineValve
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// Select the Host to be used for this Request
// 从requets里知道改请求属于哪个host
Host host = request.getHost();
if (host == null) {
// HTTP 0.9 or HTTP 1.0 request without a host when no default host
// is defined. This is handled by the CoyoteAdapter.
return;
}
if (request.isAsyncSupported()) {
request.setAsyncSupported(host.getPipeline().isAsyncSupported());
}
// Ask this Host to process this request
host.getPipeline().getFirst().invoke(request, response);
}
org.apache.catalina.connector.CoyoteAdapter#postParseRequest
从service的host集合中找到匹配当前hostname的host实例并找到uri与wrapper的映射,并复制到request的MappingData中
connector.getService().getMapper().map(serverName, decodedURI,
version, request.getMappingData());
Mapper 组件里保存了 Web 应用的配置信息,其实就是容器组件与访问路径的映射关系,比如 Host 容器里配置的域名、Context 容器里的 Web 应用路径,以及 Wrapper 容器里 Servlet 映射的路径,你可以想象这些配置信息就是一个多层次的 Map
首先,根据协议和端口号选定 Service 和 Engine。我们知道 Tomcat 的每个连接器都监听不同的端口
然后,根据域名选定 Host。Service 和 Engine 确定后,Mapper 组件通过 URL 中的域名去查找相应的 Host 容器
之后,根据 URL 路径找到 Context 组件。Host 确定以后,Mapper 根据 URL 的路径来匹配相应的 Web 应用的路径
最后,根据 URL 路径找到 Wrapper(Servlet)。Context 确定后,Mapper 再根据web.xml中配置的 Servlet 映射路径来找到具体的 Wrapper 和 Servlet。
Pipeline-Valve 是责任链模式,责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将再调用下一个处理者继续处理
Valve 表示一个处理点
public interface Valve {
public Valve getNext();
public void setNext(Valve valve);
public void invoke(Request request, Response response)
}
public interface Pipeline extends Contained {
public void addValve(Valve valve);
public Valve getBasic();
public void setBasic(Valve valve);
public Valve getFirst();
}
整个调用过程由连接器中的 Adapter 触发的,它会调用 Engine 的第一个 Valve
// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
Acceptor
Selector
Processor
ServerSocketChannel 通过 accept() 接受新的连接,accept() 方法返回获得 SocketChannel 对象,然后将 SocketChannel 对象封装在一个 PollerEvent 对象中,并将 PollerEvent 对象压入 Poller 的 Queue 里,这是个典型的“生产者 - 消费者”模式,Acceptor 与 Poller 线程之间通过 Queue 通信。
Poller 本质是一个 Selector,它内部维护一个 Queue,这个 Queue 定义如下
private final SynchronizedQueue<PollerEvent> events = new SynchronizedQueue<>();
将这些事件注册到select里,进行nio
Socket acceptor thread Socket poller thread Worker threads pool
Endpoint 组件的主要工作就是处理 I/O,而 NioEndpoint 利用 Java NIO API 实现了多路复用 I/O 模型
tomcat处理请求用的线程池使用无界队列,但重写队列,使得达到核心线程数,也能可以新增线程,主要利用记录当然任务数大于当前线程,就能新建线程
当没有空闲线程,则新建线程,不会添加到队列。(加队列的条件,线程池大小达到最大容量,或有空闲线程)
jdk线程池,没有空闲线程概念,(加队列的条件,线程池大小达到最大容量)
public class TaskQueue extends LinkedBlockingQueue<Runnable> {
...
@Override
//线程池调用任务队列的方法时,当前线程数肯定已经大于核心线程数了
public boolean offer(Runnable o) {
//如果线程数已经到了最大值,不能创建新线程了,只能把任务添加到任务队列。
if (parent.getPoolSize() == parent.getMaximumPoolSize())
return super.offer(o);
//执行到这里,表明当前线程数大于核心线程数,并且小于最大线程数。
//表明是可以创建新线程的,那到底要不要创建呢?分两种情况:
//1. 如果已提交的任务数小于当前线程数,表示还有空闲线程,无需创建新线程
if (parent.getSubmittedCount()<=(parent.getPoolSize()))
return super.offer(o);
//2. 如果已提交的任务数大于当前线程数,线程不够用了,返回false去创建新线程
if (parent.getPoolSize()<parent.getMaximumPoolSize())
return false;
//默认情况下总是把任务添加到任务队列
return super.offer(o);
}
}
public class SynchronizedStack<T> {
//内部维护一个对象数组,用数组实现栈的功能
private Object[] stack;
//这个方法用来归还对象,用synchronized进行线程同步
public synchronized boolean push(T obj) {
index++;
if (index == size) {
if (limit == -1 || size < limit) {
expand();//对象不够用了,扩展对象数组
} else {
index--;
return false;
}
}
stack[index] = obj;
return true;
}
//这个方法用来获取对象
public synchronized T pop() {
if (index == -1) {
return null;
}
T result = (T) stack[index];
stack[index--] = null;
return result;
}
//扩展对象数组长度,以2倍大小扩展
private void expand() {
int newSize = size * 2;
if (limit != -1 && newSize > limit) {
newSize = limit;
}
//扩展策略是创建一个数组长度为原来两倍的新数组
Object[] newStack = new Object[newSize];
//将老数组对象引用复制到新数组
System.arraycopy(stack, 0, newStack, 0, size);
//将stack指向新数组,老数组可以被GC掉了
stack = newStack;
size = newSize;
}
}
public abstract class ClassLoader {
//每个类加载器都有个父加载器
private final ClassLoader parent;
public Class<?> loadClass(String name) {
//查找一下这个类是不是已经加载过了
Class<?> c = findLoadedClass(name);
//如果没有加载过
if( c == null ){
//先委托给父加载器去加载,注意这是个递归调用
if (parent != null) {
c = parent.loadClass(name);
}else {
// 如果父加载器为空,查找Bootstrap加载器是不是加载过了
c = findBootstrapClassOrNull(name);
}
}
// 如果父加载器没加载成功,调用自己的findClass去加载
if (c == null) {
c = findClass(name);
}
return c;
}
protected Class<?> findClass(String name){
//1. 根据传入的类名name,到在特定目录下去寻找类文件,把.class文件读入内存
...
//2. 调用defineClass将字节数组转成Class对象
return defineClass(buf, off, len);
}
// 将字节码数组解析成一个Class对象,用native方法实现
protected final Class<?> defineClass(byte[] b, int off, int len){
...
}
}
Tomcat 的自定义类加载器 WebAppClassLoader的WebappClassLoaderBase属性 打破了双亲委托机制,它首先自己尝试去加载某个类,如果找不到再代理给父类加载器,其目的是优先加载 Web 应用自己定义的类。具体实现就是重写 ClassLoader 的两个方法:findClass 和 loadClass。
public Class<?> findClass(String name) throws ClassNotFoundException {
...
Class<?> clazz = null;
try {
//1. 先在Web应用目录下查找类
clazz = findClassInternal(name);
} catch (RuntimeException e) {
throw e;
}
if (clazz == null) {
try {
//2. 如果在本地目录没有找到,交给父加载器去查找
clazz = super.findClass(name);
} catch (RuntimeException e) {
throw e;
}
//3. 如果父类也没找到,抛出ClassNotFoundException
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
先在 Web 应用本地目录下查找要加载的类。如果没有找到,交给父加载器去查找,它的父加载器就是上面提到的系统类加载器 AppClassLoader。
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = null;
//1. 先在本地cache查找该类是否已经加载过
clazz = findLoadedClass0(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
//2. 从系统类加载器的cache中查找是否加载过
clazz = findLoadedClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
// 3. 尝试用ExtClassLoader类加载器类加载
ClassLoader javaseLoader = getJavaseClassLoader();
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 4. 尝试在本地目录搜索class并加载
try {
clazz = findClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 5. 尝试用系统类加载器(也就是AppClassLoader)来加载
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
//6. 上述过程都加载失败,抛出异常
throw new ClassNotFoundException(name);
}
先在本地目录下加载,为了避免本地目录下的类覆盖 JRE 的核心类,先尝试用 JVM 扩展类加载器 ExtClassLoader 去加载
在stardardContext的startinternal中
if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
在org.apache.catalina.startup.HostConfig#start->org.apache.catalina.startup.HostConfig#deployDirectory部署webapps下所有文件夹,一个文件夹一个standardContext,并加入standardHost,每个standardContext都会新建WebApploader,达到隔离app的目的
ExtClassLoader - 本地目录下加载 - 父加载器加载(sharedclassloader)
CommonClassLoader对应<Tomcat>/common/*
CatalinaClassLoader对应 <Tomcat >/server/*
SharedClassLoader对应 <Tomcat >/shared/*
WebAppClassloader对应 <Tomcat >/webapps/<app>/WEB-INF/*目录
catalina.properties
common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
server.loader=
shared.loader=
private void initClassLoaders() {
try {
// parent为null,默认设置 getSystemClassLoader()方法返回的ClassLoader 作为其父类,getSystemClassLoader()返回的 ClassLoader 通常就是 AppClassLoader
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 = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
Common类加载器,负责加载Tomcat和Web应用都复用的类
Catalina类加载器,负责加载Tomcat专用的类,而这些被加载的类在Web应用中将不可见
Shared类加载器,负责加载Tomcat下所有的Web应用程序都复用的类,而这些被加载的类在Tomcat中将不可见
WebApp类加载器,负责加载具体的某个Web应用程序所使用到的类,而这些被加载的类在Tomcat和其他的Web应用程序都将不可见
Jsp类加载器,每个jsp页面一个类加载器,不同的jsp页面有不同的类加载器,方便实现jsp页面的热插拔
Wrap组件封装了servlet
public synchronized Servlet loadServlet() throws ServletException {
Servlet servlet;
// 1. 创建一个Servlet实例
servlet = (Servlet) instanceManager.newInstance(servletClass);
// 2.调用了Servlet的init方法,这是Servlet规范要求的
initServlet(servlet);
return servlet;
}
当请求到来时,Context 容器的 BasicValve 会调用 Wrapper 容器中 Pipeline 中的第一个 Valve,然后会调用到 StandardWrapperValve
public final void invoke(Request request, Response response) {
//1.实例化Servlet
servlet = wrapper.allocate();
//2.给当前请求创建一个Filter链
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
//3. 调用这个Filter链,Filter链中的最后一个Filter会调用Servlet
filterChain.doFilter(request.getRequest(), response.getResponse());
}
连接器是调用 CoyoteAdapter 的 service 方法来处理请求的,而 CoyoteAdapter 会调用容器的 service 方法
当容器的 service 方法返回时,CoyoteAdapter 判断当前的请求是不是异步 Servlet 请求,如果是,就不会销毁 Request 和 Response 对象,也不会把响应信息发到浏览器
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) {
//调用容器的service方法处理请求
connector.getService().getContainer().getPipeline().
getFirst().invoke(request, response);
//如果是异步Servlet请求,仅仅设置一个标志,
//否则说明是同步Servlet请求,就将响应数据刷到浏览器
if (request.isAsync()) {
async = true;
} else {
request.finishRequest();
response.finishResponse();
}
//如果不是异步Servlet请求,就销毁Request对象和Response对象
if (!async) {
request.recycle();
response.recycle();
}
}
private LogFactory() {
// 通过ServiceLoader尝试加载Log的实现类
ServiceLoader<Log> logLoader = ServiceLoader.load(Log.class);
Constructor<? extends Log> m=null;
for (Log log: logLoader) {
Class<? extends Log> c=log.getClass();
try {
m=c.getConstructor(String.class);
break;
}
catch (NoSuchMethodException | SecurityException e) {
throw new Error(e);
}
}
//如何没有定义Log的实现类,discoveredLogConstructor为null
discoveredLogConstructor = m;
}
public Log getInstance(String name) throws LogConfigurationException {
//如果discoveredLogConstructor为null,也就没有定义Log类,默认用DirectJDKLog(封装了jul)
if (discoveredLogConstructor == null) {
return DirectJDKLog.getInstance(name);
}
try {
return discoveredLogConstructor.newInstance(name);
} catch (ReflectiveOperationException | IllegalArgumentException e) {
throw new LogConfigurationException(e);
}
}
利特尔法则:系统中的请求数 = 请求的到达速率 × 每个请求处理时间
线程池大小 = 每秒请求数 × 平均请求处理时间
线程池大小 = (线程 I/O 阻塞时间 + 线程 CPU 时间 )/ 线程 CPU 时间
public boolean offer(Runnable o) {
//we can't do any checks
if (parent==null) return super.offer(o);
//we are maxed out on threads, simply queue the object
if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
//we have idle threads, just add it to the queue
if (parent.getSubmittedCount()<=(parent.getPoolSize())) return super.offer(o);
//if we have less threads than maximum force creation of a new thread
if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
//if we reached here, we need to add it to the queue
return super.offer(o);
}
tomcat工作线程池,如果线程池大小小于最大线程池大小,则不会添加到队列(LinkedBlockingQueue)(和默认的线程池处理不一样,默认的是如果线程池大小等于核心线程池大小的时候,先插入队列,队列满了才添加线程)(tomcat线程池的最大线程数就是jdk线程池的核心线程数)
如果有空闲线程(parent.getSubmittedCount()<=(parent.getPoolSize())),插入队列,
如果没空闲添加线程
public class HostConfig implements LifecycleListener {
public void lifecycleEvent(LifecycleEvent event) {
if (event.getType().equals(Lifecycle.START_EVENT)) {
start();
}
public void start() {
if (host.getDeployOnStartup())
deployApps();
}
}
standardhost通过事件通知hostconfig加载webapps
加载解析/WEB-INF/web.xml
engine,host,context,wrapper; 四个容器中每个容器都包含自己的管道对象,管道对象用来存放若干阀门对象,但tomcat会为他们制定一个默认的基础阀门【StandardEngineValve,StandardHostValve,StandardContextValve ,StandardWrapperValve】。四个基础阀门放在各自容器管道的最后一位,用于查找下一级容器的管道.
一个pipeline包含多个Valve,这些阀共分为两类,一类叫基础阀(通过getBasic、setBasic方法调用),一类是普通阀(通过addValve、removeValve调用)。 一个管道一般有一个基础阀(通过setBasic添加),可以有0到多个普通阀(通过addValve添加)。
自定的valve类文件需要发布到CATALINA_HOME/lib目录下而不是应用的发布目录WEB-INF/classes
初始化阶段standardContext已经保存所有StandardWrapper信息(path和servlet等信息,servlet默认还未实例化)
protected volatile Servlet instance = null;
启动阶段,start service时mapper监听器启动,mapperListener.start();遍历所有Wrapper把path和对应Wrapper信息放入StandardService的Mapper中hostname->contextList(WebResourceRoot,context的描述)
public void addContextVersion(String hostName, Host host, String path,
String version, Context context, String[] welcomeResources,
WebResourceRoot resources, Collection<WrapperMappingInfo> wrappers) {}
在mapperLister中注册host,registerHost(host)->registerContext->org.apache.catalina.mapper.MapperListener#prepareWrapperMappingInfo
保存path和对应wrapper在WrapperMappingInfo中
private void registerHost(Host host) {
String[] aliases = host.findAliases();
mapper.addHost(host.getName(), aliases, host);
for (Container container : host.findChildren()) {
if (container.getState().isAvailable()) {
registerContext((Context) container);
}
}
// Default host may have changed
findDefaultHost();
if(log.isDebugEnabled()) {
log.debug(sm.getString("mapperListener.registerHost",
host.getName(), domain, service));
}
}
private void prepareWrapperMappingInfo(Context context, Wrapper wrapper,
List<WrapperMappingInfo> wrappers) {
String wrapperName = wrapper.getName();
boolean resourceOnly = context.isResourceOnlyServlet(wrapperName);
String[] mappings = wrapper.findMappings();
for (String mapping : mappings) {
boolean jspWildCard = (wrapperName.equals("jsp")
&& mapping.endsWith("/*"));
wrappers.add(new WrapperMappingInfo(mapping, wrapper, jspWildCard,
resourceOnly));
}
}
public class WrapperMappingInfo {
private final String mapping;
private final Wrapper wrapper;
private final boolean jspWildCard;
private final boolean resourceOnly;
public WrapperMappingInfo(String mapping, Wrapper wrapper,
boolean jspWildCard, boolean resourceOnly) {
this.mapping = mapping;
this.wrapper = wrapper;
this.jspWildCard = jspWildCard;
this.resourceOnly = resourceOnly;
}
}
org.apache.coyote.AbstractProcessorLight#process->org.apache.coyote.http11.Http11Processor#service->
org.apache.catalina.connector.CoyoteAdapter#service->engine.getPipeline().getFirst().invoke
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
org.apache.catalina.core.StandardEngineValve#invoke->org.apache.catalina.valves.AbstractAccessLogValve#invoke->
org.apache.catalina.valves.ErrorReportValve#invoke->org.apache.catalina.core.StandardHostValve#invoke->
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// Select the Context to be used for this Request
Context context = request.getContext();
if (context == null) {
return;
}
if (request.isAsyncSupported()) {
request.setAsyncSupported(context.getPipeline().isAsyncSupported());
}
boolean asyncAtStart = request.isAsync();
try {
context.bind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER);
if (!asyncAtStart && !context.fireRequestInitEvent(request.getRequest())) {
// Don't fire listeners during async processing (the listener
// fired for the request that called startAsync()).
// If a request init listener throws an exception, the request
// is aborted.
return;
}
// Ask this Context to process this request. Requests that are
// already in error must have been routed here to check for
// application defined error pages so DO NOT forward them to the the
// application for processing.
try {
if (!response.isErrorReportRequired()) {
context.getPipeline().getFirst().invoke(request, response);
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
container.getLogger().error("Exception Processing " + request.getRequestURI(), t);
// If a new error occurred while trying to report a previous
// error allow the original error to be reported.
if (!response.isErrorReportRequired()) {
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
throwable(request, response, t);
}
}
// Now that the request/response pair is back under container
// control lift the suspension so that the error handling can
// complete and/or the container can flush any remaining data
response.setSuspended(false);
Throwable t = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
// Protect against NPEs if the context was destroyed during a
// long running request.
if (!context.getState().isAvailable()) {
return;
}
// Look for (and render if found) an application level error page
if (response.isErrorReportRequired()) {
// If an error has occurred that prevents further I/O, don't waste time
// producing an error report that will never be read
AtomicBoolean result = new AtomicBoolean(false);
response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, result);
if (result.get()) {
if (t != null) {
throwable(request, response, t);
} else {
status(request, response);
}
}
}
if (!request.isAsync() && !asyncAtStart) {
context.fireRequestDestroyEvent(request.getRequest());
}
} finally {
// Access a session (if present) to update last accessed time, based
// on a strict interpretation of the specification
if (ACCESS_SESSION) {
request.getSession(false);
}
context.unbind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER);
}
}
org.apache.catalina.authenticator.AuthenticatorBase#invoke->org.apache.catalina.core.StandardContextValve#invoke->
org.apache.catalina.core.StandardWrapperValve#invoke->org.apache.catalina.core.StandardWrapper#allocate->
org.apache.catalina.core.StandardWrapper#loadServlet
filterChain.doFilter(request, response);->
org.apache.catalina.core.ApplicationFilterChain#doFilter
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);
}
}
}
org.apache.catalina.connector.CoyoteAdapter#service->org.apache.catalina.connector.CoyoteAdapter#postParseRequest处理host+uri到wrapper的映射
connector.getService().getMapper().map(serverName, decodedURI,
version, request.getMappingData());
对于 tomcat 数据读取总结如下:
响应数据写入的总结
对于请求体的写采用阻塞的方式,调用NioSelectorPool 的 write(ByteBuffer buf, NioChannel socket, Selector selector, long writeTimeout) 方法。在该方法中又会调用 NioBlockingSelector 的 write() 方法
请求响应时在另一个block-poller线程执行
NioEndpoint对象中维护了一个NioSelecPool对象,这个NioSelectorPool中又维护了一个BlockPoller线程,这个线程就是基于辅Selector进行NIO的逻辑。以执行servlet后,得到response,往socket中写数据为例,最终写的过程调用NioBlockingSelector的write方法
先使用socket的原selector,检测可写事件,如果遇到网络问题,写失败,再丢到上面的BlockPoller线程,使用新的selector写数据
org.apache.tomcat.util.net.NioBlockingSelector#write
public int write(ByteBuffer buf, NioChannel socket, long writeTimeout,MutableInteger lastWrite) throws IOException {
SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector());
if ( key == null ) throw new IOException("Key no longer registered");
KeyAttachment att = (KeyAttachment) key.attachment();
int written = 0;
boolean timedout = false;
int keycount = 1; //assume we can write
long time = System.currentTimeMillis(); //start the timeout timer
try {
while ( (!timedout) && buf.hasRemaining()) {
if (keycount > 0) { //only write if we were registered for a write
//直接往socket中写数据
int cnt = socket.write(buf); //write the data
lastWrite.set(cnt);
if (cnt == -1)
throw new EOFException();
written += cnt;
//写数据成功,直接进入下一次循环,继续写
if (cnt > 0) {
time = System.currentTimeMillis(); //reset our timeout timer
continue; //we successfully wrote, try again without a selector
}
}
//如果写数据返回值cnt等于0,通常是网络不稳定造成的写数据失败
try {
//开始一个倒数计数器
if ( att.getWriteLatch()==null || att.getWriteLatch().getCount()==0) att.startWriteLatch(1);
//将socket注册到辅Selector,这里poller就是BlockSelector线程
poller.add(att,SelectionKey.OP_WRITE);
//阻塞,直至超时时间唤醒,或者在还没有达到超时时间,在BlockSelector中唤醒
att.awaitWriteLatch(writeTimeout,TimeUnit.MILLISECONDS);
}catch (InterruptedException ignore) {
Thread.interrupted();
}
if ( att.getWriteLatch()!=null && att.getWriteLatch().getCount()> 0) {
keycount = 0;
}else {
//还没超时就唤醒,说明网络状态恢复,继续下一次循环,完成写socket
keycount = 1;
att.resetWriteLatch();
}
if (writeTimeout > 0 && (keycount == 0))
timedout = (System.currentTimeMillis() - time) >= writeTimeout;
} //while
if (timedout)
throw new SocketTimeoutException();
} finally {
poller.remove(att,SelectionKey.OP_WRITE);
if (timedout && key != null) {
poller.cancelKey(socket, key);
}
}
return written;
}
当调用Response#getOutpuStream()方法或者Response#getWriter()写出数据时,数据并不是直接发送出去,而是缓存在内部的OutputBuffer中。
org.apache.catalina.connector.Response
public class Response implements HttpServletResponse {
/**
* The associated output buffer.
*/
protected final OutputBuffer outputBuffer;
/**
* The associated output stream.
*/
protected CoyoteOutputStream outputStream;
/**
* The associated writer.
*/
protected CoyoteWriter writer;
/**
* @return the servlet output stream associated with this Response.
*
* @exception IllegalStateException if <code>getWriter</code> has
* already been called for this response
* @exception IOException if an input/output error occurs
*/
// getOutputStream()方法将内容写出到outputBuffer
// 返回底层的CoyoteOutputStream字节输出流。该输出流将会把内容写出到outputBuffer中。
@Override
public ServletOutputStream getOutputStream()
throws IOException {
if (usingWriter) {
throw new IllegalStateException
(sm.getString("coyoteResponse.getOutputStream.ise"));
}
usingOutputStream = true;
if (outputStream == null) {
outputStream = new CoyoteOutputStream(outputBuffer);
}
return outputStream;
}
public class OutputBuffer extends Writer {
private static final StringManager sm = StringManager.getManager(OutputBuffer.class);
public static final int DEFAULT_BUFFER_SIZE = 8 * 1024;
/**
* Encoder cache.
*/
private final Map<Charset, C2BConverter> encoders = new HashMap<>();
/**
* Default buffer size.
*/
private final int defaultBufferSize;
// ----------------------------------------------------- Instance Variables
/**
* The byte buffer.
*/
private ByteBuffer bb;
/**
* The char buffer.
*/
private final CharBuffer cb;
public void append(byte src[], int off, int len) throws IOException {
if (bb.remaining() == 0) {
appendByteArray(src, off, len);
} else {
int n = transfer(src, off, len, bb);
len = len - n;
off = off + n;
// 满了才会flush
if (isFull(bb)) {
flushByteBuffer();
appendByteArray(src, off, len);
}
}
}
private void flushByteBuffer() throws IOException {
realWriteBytes(bb.slice());
clear(bb);
}
public void realWriteBytes(ByteBuffer buf) throws IOException {
if (closed) {
return;
}
if (coyoteResponse == null) {
return;
}
// If we really have something to write
if (buf.remaining() > 0) {
// real write to the adapter
try {
coyoteResponse.doWrite(buf);
} catch (CloseNowException e) {
// Catch this sub-class as it requires specific handling.
// Examples where this exception is thrown:
// - HTTP/2 stream timeout
// Prevent further output for this response
closed = true;
throw e;
} catch (IOException e) {
// An IOException on a write is almost always due to
// the remote client aborting the request. Wrap this
// so that it can be handled better by the error dispatcher.
throw new ClientAbortException(e);
}
}
}
connector/Response.java里的outputbuffer调用coyote/Response.java里的outputbuffer(Http11OutputBuffer extends OutputBuffer)
将connector.response里的outputbuffer数据通过coyote.response里的outputbffer写入到socketbuffer
if (!inputBuffer.parseHeaders()) {
// We've read part of the request, don't recycle it
// instead associate it with the socket
openSocket = true;
readComplete = false;
break;
}
if (openSocket) {
if (readComplete) {
return SocketState.OPEN;
} else {
return SocketState.LONG;
}
} else {
return SocketState.CLOSED;
}
if (state == SocketState.OPEN) {
// In keep-alive but between requests. OK to recycle
// processor. Continue to poll for the next request.
connections.remove(socket);
release(processor);
wrapper.registerReadInterest();
}
数据不完整,等待下个读事件
void endRequest() throws IOException {
if (swallowInput && (lastActiveFilter != -1)) {
int extraBytes = (int) activeFilters[lastActiveFilter].end();
byteBuffer.position(byteBuffer.position() - extraBytes);
}
}
没被消费的body,被吞掉,byteBuffer的位置往前移动,继续等待下次读事件触发
一个 Tomcat 代表一个 Server 服务器,一个 Server 服务器可以包含多个 Service 服务,Tomcat 默认的 Service 服务是 Catalina,而一个 Service 服务可以包含多个连接器,因为 Tomcat 支持多种网络协议,包括 HTTP/1.1、HTTP/2、AJP 等等,一个 Service 服务还会包括一个容器,容器外部会有一层 Engine 引擎所包裹,负责与处理连接器的请求与响应,连接器与容器之间通过 ServletRequest 和 ServletResponse 对象进行交流。
也可以从 server.xml 的配置结构可以看出 tomcat 整体的内部结构:
<Server port="8005" shutdown="SHUTDOWN">
<Service name="Catalina">
<Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443" URIEncoding="UTF-8"/>
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443"/>
<Engine defaultHost="localhost" name="Catalina">
<Host appBase="webapps" autoDeploy="true" name="localhost" unpackWARs="true">
<Context docBase="handler-api" path="/handler" reloadable="true" source="org.eclipse.jst.jee.server:handler-api"/>
</Host>
</Engine>
</Service>
</Server>
Engine:表示一个虚拟主机的引擎,一个 Tomcat Server 只有一个 引擎,连接器所有的请求都交给引擎处理,而引擎则会交给相应的虚拟主机去处理请求;
Host:表示虚拟主机,一个容器可以有多个虚拟主机,每个主机都有对应的域名,在 Tomcat 中,一个 webapps 就代表一个虚拟主机,当然 webapps 可以配置多个;
Context:表示一个应用容器,一个虚拟主机可以拥有多个应用,webapps 中每个目录都代表一个 Context,每个应用可以配置多个 Servlet。
Engine -> Host -> Context -> Wrapper -> Servlet
解压war包,导入class
读取WEB-INF文件夹里xml配置(或注解(springboot))
类加载servert class
一个线程监听连接,另外一个线程处理请求
所有请求的入口和出口
连接器,监听接口
接受连接请求,分配线程让container来处理这个请求。
protocol 监听的协议,默认是http/1.1
port 指定服务器端要创建的端口号
minThread服务器启动时创建的处理请求的线程数
maxThread最大可以创建的处理请求的线程数
enableLookups如果为true,则可以通过调用request.getRemoteHost()进行DNS查询来得到远程客户端的实际主机名,若为false则不进行DNS查询,而是返回其ip地址
redirectPort指定服务器正在处理http请求时收到了一个SSL传输请求后重定向的端口号
acceptCount指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理
connectionTimeout指定超时的时间数(以毫秒为单位)
SSLEnabled 是否开启 sll 验证,在Https 访问时需要开启。
默认NioEndpoint,监听端口
生产者,消费者模式
Socket acceptor thread
Socket poller thread
Woker threads pool ####
service收集connector
通用容器,包括以下容器
servlet引擎(责任链模式)
An Engine represents the entry point (within Catalina) that processes every request. The Engine implementation for Tomcat stand alone analyzes the HTTP headers included with the request, and passes them on to the appropriate Host (virtual host).
处理请求,把请求分配到对应的host
主机
webapp
Servlet
线程池
catalina -> server -> (service1 service2 …)
service -> (engine connector1 connector2 …)
engine -> host -> context -> wrapper
Common ClassLoader
Catalina ClassLoader
Shared ClassLoader
下面的简图是Tomcat9版本的官方文档给出的Tomcat的类加载器的图。
Bootstrap
|
System
|
Common
/ \
Webapp1 Webapp2 ..
$JAVA_HOME/jre/lib/ext
路径下的类。CLASSPATH
系统变量所定义路径的所有的类。这3个部分,在上面的Java双亲委派模型图中都有体现。不过可以看到ExtClassLoader没有画出来,可以理解为是跟bootstrap合并了,都是去JAVA_HOME/jre/lib
下面加载类
Web应用默认的类加载顺序是(打破了双亲委派规则):
/WEB-INF/classes
中的类。/WEB-INF/lib/*.jap
中的jar包中的类。如果在配置文件中配置了<Loader delegate="true"/>
,那么就是遵循双亲委派规则,加载顺序如下:
/WEB-INF/classes
中的类。/WEB-INF/lib/*.jap
中的jar包中的类。@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = null;
// 1. 从本地缓存中查找是否加载过此类
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
// 2. 从AppClassLoader中查找是否加载过此类
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);
// 3. 尝试用ExtClassLoader类加载器加载类,防止Web应用覆盖JRE的核心类
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) {
tryLoadingFromJavaseLoader = true;
}
boolean delegateLoad = delegate || filter(name, true);
// 4. 判断是否设置了delegate属性,如果设置为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
}
}
// 5. 默认是设置delegate是false的,那么就会先用WebAppClassLoader进行加载
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
}
// 6. 如果此时在WebAppClassLoader没找到类,那么就委托给AppClassLoader去加载
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);
}
一个acceptor线程
两个Poller多路复用线程,调用selector.select(timeout)函数
http-nio-8080-BlockPoller — NioBlockingSelector
如果socket.write返回0,说明缓存区已满,则设置selector监控可写,线程阻塞等待socket可写
poller.add(att, SelectionKey.OP_WRITE, reference);
att.awaitWriteLatch(AbstractEndpoint.toTimeout(writeTimeout), TimeUnit.MILLISECONDS);
写读或可写,则countDown,唤醒阻塞线程
org.apache.tomcat.util.net.NioBlockingSelector.BlockPoller
if (sk.isReadable()) {
countDown(socketWrapper.getReadLatch());
}
if (sk.isWritable()) {
countDown(socketWrapper.getWriteLatch());
}
Ajp-nio-8009-BlockPoller 处理request
Http-nio-8080-Acceptor一个监听线程
Http-nio-8080-ClientPoller poller线程
http-nio-8080-exec-n处理request的线程池中的线程
一次连接countUpOrAwaitConnection(latch.countUpOrAwait),一共10000个latch
使用aqs同步10000个AtomicLong count
LimitLatch{
private class Sync extends AbstractQueuedSynchronizer
}
poller线程(selector线程)
2: select socket事件:
keyCount = selector.selectNow();
selector.select(selectorTimeout)等待read事件
监听到一个请求之后,把socket发送给poller
poller从events工作队列里拿pollerEvent,把socket赋值给event,然后添加到events工作队列
线程池角度:pollerevent是放在队列里的任务,poller是消费者,不断取event
BlockPoller 线程,该线程主要处理阻塞的读写操作。对于请求体数据读取,根据以前文章一般由 tomcat io 线程进行,当数据不可读的时候的时候(客户端数据未发送完毕),会注册封装的 OPEN_READ 事件到 BlockPoller 线程中,然后阻塞当前线程(一般为tomcat io线程)。对于响应数据的写入,根据以前文章一般也由 tomcat io 线程进行,当数据不可写的时候(原始 socket 发送缓冲区满),会注册封装的 OPEN_WRITE 事件对象到 BlockPoller 线程中,然后阻塞当前线程。
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- Note: A "Server" is not itself a "Container", so you may not
define subcomponents such as "Valves" at this level.
Documentation at /docs/config/server.html
-->
<Server port="8005" shutdown="SHUTDOWN">
<Listener className="org.apache.catalina.startup.VersionLoggerListener" />
<!-- Security listener. Documentation at /docs/config/listeners.html
<Listener className="org.apache.catalina.security.SecurityListener" />
-->
<!--APR library loader. Documentation at /docs/apr.html -->
<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
<!-- Prevent memory leaks due to use of particular java/javax APIs-->
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
<!-- Global JNDI resources
Documentation at /docs/jndi-resources-howto.html
-->
<GlobalNamingResources>
<!-- Editable user database that can also be used by
UserDatabaseRealm to authenticate users
-->
<Resource name="UserDatabase" auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved"
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>
<!-- A "Service" is a collection of one or more "Connectors" that share
a single "Container" Note: A "Service" is not itself a "Container",
so you may not define subcomponents such as "Valves" at this level.
Documentation at /docs/config/service.html
-->
<Service name="Catalina">
<!--The connectors can use a shared executor, you can define one or more named thread pools-->
<!--
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
maxThreads="150" minSpareThreads="4"/>
-->
<!-- A "Connector" represents an endpoint by which requests are received
and responses are returned. Documentation at :
Java HTTP Connector: /docs/config/http.html
Java AJP Connector: /docs/config/ajp.html
APR (HTTP/AJP) Connector: /docs/apr.html
Define a non-SSL/TLS HTTP/1.1 Connector on port 8080
-->
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<!-- A "Connector" using the shared thread pool-->
<!--
<Connector executor="tomcatThreadPool"
port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
-->
<!-- Define a SSL/TLS HTTP/1.1 Connector on port 8443
This connector uses the NIO implementation. The default
SSLImplementation will depend on the presence of the APR/native
library and the useOpenSSL attribute of the
AprLifecycleListener.
Either JSSE or OpenSSL style configuration may be used regardless of
the SSLImplementation selected. JSSE style configuration is used below.
-->
<!--
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="150" SSLEnabled="true">
<SSLHostConfig>
<Certificate certificateKeystoreFile="conf/localhost-rsa.jks"
type="RSA" />
</SSLHostConfig>
</Connector>
-->
<!-- Define a SSL/TLS HTTP/1.1 Connector on port 8443 with HTTP/2
This connector uses the APR/native implementation which always uses
OpenSSL for TLS.
Either JSSE or OpenSSL style configuration may be used. OpenSSL style
configuration is used below.
-->
<!--
<Connector port="8443" protocol="org.apache.coyote.http11.Http11AprProtocol"
maxThreads="150" SSLEnabled="true" >
<UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" />
<SSLHostConfig>
<Certificate certificateKeyFile="conf/localhost-rsa-key.pem"
certificateFile="conf/localhost-rsa-cert.pem"
certificateChainFile="conf/localhost-rsa-chain.pem"
type="RSA" />
</SSLHostConfig>
</Connector>
-->
<!-- Define an AJP 1.3 Connector on port 8009 -->
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
<!-- An Engine represents the entry point (within Catalina) that processes
every request. The Engine implementation for Tomcat stand alone
analyzes the HTTP headers included with the request, and passes them
on to the appropriate Host (virtual host).
Documentation at /docs/config/engine.html -->
<!-- You should set jvmRoute to support load-balancing via AJP ie :
<Engine name="Catalina" defaultHost="localhost" jvmRoute="jvm1">
-->
<Engine name="Catalina" defaultHost="localhost">
<!--For clustering, please take a look at documentation at:
/docs/cluster-howto.html (simple how to)
/docs/config/cluster.html (reference documentation) -->
<!--
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>
-->
<!-- Use the LockOutRealm to prevent attempts to guess user passwords
via a brute-force attack -->
<Realm className="org.apache.catalina.realm.LockOutRealm">
<!-- This Realm uses the UserDatabase configured in the global JNDI
resources under the key "UserDatabase". Any edits
that are performed against this UserDatabase are immediately
available for use by the Realm. -->
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"/>
</Realm>
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<!-- SingleSignOn valve, share authentication between web applications
Documentation at: /docs/config/valve.html -->
<!--
<Valve className="org.apache.catalina.authenticator.SingleSignOn" />
-->
<!-- Access log processes all example.
Documentation at: /docs/config/valve.html
Note: The pattern used is equivalent to using pattern="common" -->
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
</Host>
</Engine>
</Service>
</Server>
server
service
Connector
engine
host
context
Valve
Realm
https://www.processon.com/view/link/5d75f8a7e4b04a19501b07d5
https://www.jianshu.com/p/76ff17bc6dea
http://objcoding.com/2019/05/30/tomcat-architecture/
https://time.geekbang.org/column/article/96764
https://www.cnblogs.com/haimishasha/p/10740606.html
https://juejin.cn/post/6844903881067986957
https://blog.csdn.net/qq_26222859/article/details/45823755