1. 管道模式
1.1 管道与阀门
在一个比较复杂的大型系统中,如果一个对象或数据流需要进行繁杂的逻辑处理,我们可以选择在一个大的组件中直接进行这些繁杂的逻辑处理。这个方式虽然达到目的,但是拓展性和可重用性差。因为牵一发而动全身。
管道模式就像一条管道把多个对象连接起来,整体看起来就像若干个阀门嵌套在管道中,处理逻辑放在阀门上。
1.2 代码实现管道与阀门
/**
* 阀门
*/
public interface Valve {
Valve getNext();
void setNext(Valve valve);
void invoke(String handing);
}
public class BasicValve implements Valve {
private Valve next = null;
@Override
public Valve getNext() {
return next;
}
@Override
public void setNext(Valve valve) {
this.next = valve;
}
@Override
public void invoke(String handing) {
System.out.println("基础阀门");
}
}
public class FirstValve implements Valve {
private Valve next = null;
@Override
public Valve getNext() {
return next;
}
@Override
public void setNext(Valve valve) {
this.next = valve;
}
@Override
public void invoke(String handing) {
System.out.println("定制阀门1");
getNext().invoke(handing);
}
}
public class SecondValve implements Valve {
private Valve next = null;
@Override
public Valve getNext() {
return next;
}
@Override
public void setNext(Valve valve) {
this.next = valve;
}
@Override
public void invoke(String handing) {
System.out.println("定制阀门2");
getNext().invoke(handing);
}
}
/**
* 管道
*/
public interface Pipeline {
// 获取第一个阀门
Valve getFirst();
Valve getBasic();
// 设置基础阀门
void setBasic(Valve valve);
// 添加阀门
void addValve(Valve valve);
}
public class StandardPipeline implements Pipeline {
// 第一个阀门
protected Valve first = null;
// 基础阀门
protected Valve basic = null;
@Override
public Valve getFirst() {
return first;
}
@Override
public Valve getBasic() {
return basic;
}
@Override
public void setBasic(Valve valve) {
this.basic = valve;
}
// 添加阀门,链式构建阀门的执行顺序(先定制、最后基础阀门)
@Override
public void addValve(Valve valve) {
if (first == null) {
first = valve;
valve.setNext(basic);
} else {
Valve current = first;
while (current != null) {
if (current.getNext() == basic) {
current.setNext(valve);
valve.setNext(basic);
}
current = current.getNext();
}
}
}
}
public class Main {
public static void main(String[] args) {
// 新建管道,添加1个基础阀门,2个定制阀门
StandardPipeline standardPipeline = new StandardPipeline();
BasicValve basicValve = new BasicValve();
standardPipeline.setBasic(basicValve);
FirstValve firstValve = new FirstValve();
standardPipeline.addValve(firstValve);
SecondValve secondValve = new SecondValve();
standardPipeline.addValve(secondValve);
String handing = "这是一个Servlet请求";
standardPipeline.getFirst().invoke(handing);
}
}
执行结果:
2. Tomcat请求过程
Tomcat中Container有4种:Engine、Host、Context、Wrapper。它们各自的实现类分别是 StandardEngine、StandardHost、StandardContext、StandardWrapper,他们都在 tomcat 的 org.apache.catalina.core 包下。
Standard 的 container 都是直接继承抽象类:org.apache.catalina.core.ContainerBase。
Host 设计的目的是:Tomcat 诞生时,服务器资源很贵,所以一般一台服务器其实可以有多个域名映射。为了满足这种需求,设计了Host。
3. Tomcat 处理一个 HTTP 请求的过程
- 用户点击网页内容,请求被发送到本机端口 8080,被在那里监听的 Connector 获得。
- Connector 把该请求交给它所在的 Service 的 Engine 来处理,并等待 Engine 的回应。
- Engine 获得请求 localhost/test/index.jsp,匹配所有的虚拟主机 Host。Engine 匹配到名为 localhost 的 Host(即使匹配不到也把请求交给该 Host 处理,因为该 Host 被定义为该 Engine 的默认主机)。
- 名为 localhost 的 Host 获得请求/test/index.jsp,匹配它所拥有的所有的 Context。Host 匹配到路径为/test 的 Context(如果匹配不到就把该请求交给路径名为“ ”的 Context 去处理)。
- path=“/test”的 Context 获得请求/index.jsp,在它的 mapping table 中寻找出对应的 Servlet。Context 匹配到 URL PATTERN 为*.jsp 的 Servlet,对应于 JspServlet类。构造 HttpServletRequest 对象和 HttpServletResponse 对象,作为参数调用 JspServlet 的 doGet() 或 doPost(),执行业务逻辑、数据存储等程序。Context 把执行完之后的 HttpServletResponse 对象返回给 Host。
- Host 把 HttpServletResponse 对象返回给 Engine。
- Engine 把 HttpServletResponse 对象返回 Connector。
- Connector 把 HttpServletResponse 对象返回给客户 Browser。
4. Tomcat管道模式
Tomcat每一种 container 都有一个自己的 StandardValve,分别是:
- StandardEngineValve
- StandardHostValve
- StandardContextValve
- StandardWrapperValve
四个 container 就相当于有四个生产线(Pipeline)。
在 CoyoteAdapter 的 service(),由下面这一句就进入 Container 的,connector.getContainer().getPipeline().getFirst().invoke(request, response)。
是的,这就是进入 container 迷宫的大门,欢迎来到 Container。
StandardEngineValve的service()
其他类似的StandardValue还有StandardHostValve、StandardContextValve、StandardWrapperValve。
4.1 Tomcat 中定制阀门
管道机制给我们带来了更好的拓展性,例如要添加一个额外的逻辑处理阀门是很容易的:
- 自定义阀门 PrintIPValve,只要继承 ValveBase 并重写 invoke()即可。注意在 invoke()中一定要执行调用下一个阀门的操作,否则会出现异常。
public class PrintIPValve extends ValveBase {
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
System.out.println("------自定义阀门 PrintIPValve:" + request.getRemoteAddr());
getNext().invoke(request, response);
}
}
- 配置Tomcat的核心配置文件server.xml,这里把阀门配置到Engine容器下,作用范围就是整个引擎,也可以根据作用范围配置在Host或者是Context下。
<Valve className="org.apache.catalina.valves.PrintIPValve" />
4.2 Tomcat 中提供常用的阀门
- AccessLogValve:请求访问日志阀门,通过此阀门可以记录所有客户端的访问日志,包括远程主机 IP、远程主机名、请求方法、请求协议、会话 ID、请求时间、处理时长、数据包大小等。它提供任意参数化的配置,可以通过任意组合来定制访问日志的格式
- JDBCAccessLogValve:同样是记录访问日志的阀门,但是它有助于将访问日志通过 JDBC 持久化到数据库中
- ErrorReportValve:这是一个将错误以 HTML 格式输出的阀门
- PersistentValve:这是对每一个请求的会话实现持久化的阀门
- RemoteAddrValve:访问控制阀门。可以通过配置决定哪些 IP 可以访问 WEB 应用
- RemoteHostValve:访问控制阀门,通过配置觉得哪些主机名可以访问 WEB 应用
- RemoteIpValve:针对代理或者负载均衡处理的一个阀门,一般经过代理或者负载均衡转发的请求都将自己的 IP 添加到请求头”X-Forwarded-For”中,此时,通过阀门可以获取访问者真实的 IP
- SemaphoreValve:这个是一个控制容器并发访问的阀门,可以作用在不同容器上