简述
在第一章开始就提到,Tomcat的本质什么?是容器。容器这个概念现在很火,一提到容器,我们立马可以想到虚拟化,SAAS,Docket。虽然在这里,我们不会深入去探究其他虚拟化相关技术中的容器思想,但Tomcat的容器究竟是什么,容器为Tomcat带来了什么,这次我们一起去探究。
Container
org.apache.catalina.Container 接口定义了容器的形式,有四种容器:
- Engine[引擎]:tomcat顶级容器,包含所有servlet,可包含多个Host
- Host[虚拟主机]:可包含多个Context
- Context[上下文]:包含一个Web应用,包含一个或多个wrapper
- Wrapper[包装器]:一个单独Servlet的 ServletProcessor容器
REST设计中引入一个很重要的概念,资源。Servlet对数据的解析,本质上也是一种对资源的转换。例如,查询某一条记录,输入一组特别标志,获得特定标志对应的数据,这相当将特定标志转化为数据,这是一种资源的转化;通过URL获取一个静态页面,url地址同样是一种请求输入,而静态页面作为URL对应的静态输出,这同样是一种资源的转化。Tomcat中对于资源的转化是由servlet完成的,Tomcat很少直接参与资源的转化,Tomcat给予Servlet资源转化最主要的是提供了完善的环境,而这种环境主要就是容器Container所提供的。
Engine是所有资源的总入口,提供了整体的容器,通过Engine可以实现多个Host虚拟主机同时存在。
Host虚拟主机为Tomcat提供了多虚拟主机配置的功能,将一个Tomcat通过虚拟主机配置,可以划分成为逻辑独立的多台虚拟服务容器。通过虚拟主机,可以实现多域名映射多虚拟主机,虚拟主机独立数据存储,虚拟主机独立配置等。
Context为服务提供容器,每一个在tomcat部署的服务都由一个Context提供服务,通过Context容器,Tomcat为我们提供了很多有趣的特性,其中很重要的一个特性就是Session和Cookie。HTTP虽然是无状态协议,但是通过Session和Cookie,HTTP协议实现了真正拥有上下文的交互方式。通过上下文,我们实现了有状态的资源交互。更重要的一点是,这种有趣的交互方式由容器来实现,而服务无需关心其实现。
Wrapper为每一个Servlet提供一个对应的容器。
一个父容器可以包含一个或多个子容器,如一个Engine可以设置多个Host。
很明显,Contianer其实是一种组合模式(Composite Pattern)。
将对象组合成树形结构以表示“部分整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
《设计模式--组合模式定义》
在《设计模式》中对于组合模式实现的时候需要关注的几个点:
- 显式父部件引用
- 最大化Component接口
- 声明管理子部件的操作
因此设计Container接口如下
//添加子容器
public void addChild(Container child);
//移除子容器
public void removeChild(Container child);
//查找子容器
public Container findChild(String childName);
//子容器列表
public Container[] findChildren();
//获取父容器
public Container getParent();
//设置父容器
public void setParent(Container parent);
//获取容器名称
public String getName();
//设置容器名称
public void setName(String name);
根据Container接口,设计ContainerBase抽象类如下:
public abstract class ContainerBase implements Container {
/**
* 子容器存储
*/
private HashMap<String, Container> children = new HashMap();
/**
* Name
*/
private String name ;
/**
* 父容器
*/
private Container parent;
/**
* 添加子容器
* @param child Container
*/
@Override
public void addChild(Container child) {
children.put(child.getName() ,child);
}
/**
* 移除子容器
* @param child Container
*/
@Override
public void removeChild(Container child) {
children.remove(child);
}
/**
* 查找子容器
* @param childName
*/
@Override
public Container findChild(String childName) {
return (Container) children.get(childName);
}
/**
* 获取所有子容器
* @return Contianer[]
*/
@Override
public Container[] findChildren() {
return (Container[]) children.values().toArray();
}
/**
* 设置父容器
* @param parent Container
*/
@Override
public void setParent(Container parent) {
this.parent.removeChild(this);
this.parent = parent;
}
/**
* 获取父容器
* @return Container
*/
@Override
public Container getParent() {
return parent;
}
/**
* 获取容器名称
* @return String name
*/
@Override
public String getName() {
return this.name;
}
/**
* 设置容器名称
*/
@Override
public void setName(String name) {
this.parent.removeChild(this);
this.name = name;
this.parent.addChild(this);
}
}
Engine、Host和Context从本质上来说都属于上层容器,而在这里我们通过Wrapper实现对于ServletProcessor的封装及管理。设计代码如下
PipeliningTasks和Valve
相信大家都接触过拦截器和Spring AOP,实际上Pipelining Tasks也是一种AOP思想的实现方式。
Tomcat通过Pipeline和Valve实现上述问题。
Pipeline
Pipeline整体结构如上所示。
Pipeline 设计非常形象,不知道大家有没有玩过接水管,实际上Pipeline正如管道所示。一个管道前可以设置多个阀门,但至少设置一个基础阀门。
上一节中我们讲到Context可以包含多个Wrapper,在引入Pipeline之后的结构正如下图所示。Context包含多个Wrapper,在执行每一个Wrapper之前,先要过Context的Pipeline,同时每一个Wrapper都具有属于自己的Pipeline。
我们对Pipeline进行分析,可以设计基本Pipeline接口如下
//设置基本阀门
public void setBasic(Valve basic);
//获取基本阀门
public Valve getBasic();
//添加阀门
public void addValve(Valve valve);
//删除阀门
public void remove(Valve valve);
//获得所有非基本阀门
public Valve[] getValves();
//下节说明
public void invoke(Request request, Response response);
同理,Engine、Host、Context和Wrapper经过Pipeline可以实现类似于AOP的配置,而这个配置就是上图中的“阀门”。那么Tomcat中如何实现阀门功能以及阀门的配置呢?请移步下节
Valve 和ValveContext
Valve和ValveContext实际上实现了Tomcat对于阀门的控制,先看一下Valve和ValveContext的结构设计。
Valve
//Valve信息
public String getInfo() ;
//invoke
public void invoke(Request request, Response response, ValveContext context) throws IOException;
ValveContext
//ValveContext信息
public String getInfo();
//向下移动
public void invokeNext(Request request, Response response)
throws IOException, ServletException;
我们可以看到,Valve对象invoke方法中包含ValveContext。实际上ValveContext接口似于现在使用的Iterator接口。与Iterator接口不同的是,Iterator接口将集合对象的控制权集中于接口实现类内部,通过invoke内部对对象进行控制,而ValveContext是将控制权转交给Valve进行使用。并且Valve相较Iterator更为精简,只保留需要的功能。
//比较简单,很好理解,结构图中有
public interface Contained {
public void setContainer(Container container);
public Container getContainer();
}
**
* StandardPipeline
* Created by admin on 2017/8/1.
*/
public class StandardPipeline implements Pipeline, Contained {
private Container container;
private Valve basicValve;
private List<Valve> valves = new ArrayList<>();
@Override
public Container getContainer() {
return this.container;
}
@Override
public void setContainer(Container container) {
this.container = container;
}
@Override
public void setBasic(Valve basic) {
this.basicValve = basic;
}
@Override
public Valve getBasic() {
return this.basicValve;
}
@Override
public void addValve(Valve valve) {
valves.add(valve);
}
@Override
public void remove(Valve valve) {
valves.remove(valve);
}
@Override
public Valve[] getValves() {
return (Valve[]) valves.toArray();
}
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
// Invoke the first Valve in this pipeline for this request
(new StanardValveContext()).invokeNext(request, response);
}
protected class StanardValveContext implements ValveContext {
protected String info = "com.cunchen.core.StandardPipeline/1.0";
protected int stage = 0;
@Override
public String getInfo() {
return info;
}
@Override
public void invokeNext(Request request, Response response) throws IOException, ServletException {
int subscript = stage;
stage = stage + 1;
// Invoke the requested Valve for the current request thread
if (subscript < valves.size()) {
valves.get(subscript).invoke(request, response, this);
} else if ((subscript == valves.size()) && (basicValve != null)) {
basicValve.invoke(request, response, this);
} else {
throw new ServletException("standardPipeline.noValve");
}
}
}
}
/**
* 客户请求IP记录阀DEMO
* Created by wqd on 2017/3/9.
*/
public class ClientIPLoggerValve implements Valve, Contained {
protected Container container;
protected String info;
private Logger log = Logger.getLogger(info);
@Override
public Container getContainer() {
return this.container;
}
@Override
public void setContainer(Container container) {
this.container = container;
}
@Override
public String getInfo() {
return info;
}
/**
* 代理方法
* @param request {@link Request}
* @param response {@link Response}
* @param valveContext {@link ValveContext}
* @throws IOException Valve.invoke
* @throws ServletException Valve.invoke
*/
@Override
public void invoke(Request request, Response response, ValveContext valveContext) throws IOException, ServletException {
valveContext.invokeNext(request, response);
ServletRequest request1 = request.getRequest();
if(request1 instanceof HttpServletRequest) {
HttpServletRequest hreq = (HttpServletRequest) request1;
Enumeration headerNames = hreq.getHeaderNames();
while(headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement().toString();
String headerValue = hreq.getHeader(headerName);
log.info(getInfo() + "recorder-----" + headerName + ":" + headerValue);
}
}
}
}
从代码来看,不难理解作者其实想通过ValveContext接口,可以扩展实现不同的上下文控制类。但实际上,ValveContext这种设计最直接的造成了Valve对象的代码冗余(需要Valve.invoke执行后,调用ValveContext.nextInvoke),并且实现同样的功能完全可以通过控制集合类实现。因此在Tomcat 5以后,valveContext接口被取消,Valve接口增加setNext()方法,将Valve结构改为链表形式,有兴趣的小伙伴可以查看Tomcat 7 StandardPipeline。