Tomcat中的Valve以及自定义Valve

写这篇博客的灵感来自于最近做的一个网关系统,需要把响应流量按时间序列记录到数据库中。当我准备开始写这篇博客的时候,就在想如何以简洁的话来描述Valve以及它有什么使用场景呢?

它的作用个人总结为:org.apache.catalina.Valve是Tomcat中各个连接某些org.apache.catalina.Contained实例的责任链抽象接口

它的使用场景:能在更高层次(Filter甚至Host之前)处理Request和Response对象

1,Tomcat架构简介

我们先来看看Tomcat的架构图和server.xml文件

Tomcat中的Valve以及自定义Valve

 

  1 <?xml version="1.0" encoding="UTF-8"?>
  2 <Server port="8005" shutdown="SHUTDOWN">
  3   <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  4   <!-- Security listener. Documentation at /docs/config/listeners.html
  5   <Listener className="org.apache.catalina.security.SecurityListener" />
  6   -->
  7   <!--APR library loader. Documentation at /docs/apr.html -->
  8   <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
  9   <!-- Prevent memory leaks due to use of particular java/javax APIs-->
 10   <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
 11   <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
 12   <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
 13 
 14   <!-- Global JNDI resources
 15        Documentation at /docs/jndi-resources-howto.html
 16   -->
 17   <GlobalNamingResources>
 18     <!-- Editable user database that can also be used by
 19          UserDatabaseRealm to authenticate users
 20     -->
 21     <Resource name="UserDatabase" auth="Container"
 22               type="org.apache.catalina.UserDatabase"
 23               description="User database that can be updated and saved"
 24               factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
 25               pathname="conf/tomcat-users.xml" />
 26   </GlobalNamingResources>
 27 
 28   <!-- A "Service" is a collection of one or more "Connectors" that share
 29        a single "Container" Note:  A "Service" is not itself a "Container",
 30        so you may not define subcomponents such as "Valves" at this level.
 31        Documentation at /docs/config/service.html
 32    -->
 33   <Service name="Catalina">
 34 
 35     <!--The connectors can use a shared executor, you can define one or more named thread pools-->
 36     <!--
 37     <Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
 38         maxThreads="150" minSpareThreads="4"/>
 39     -->
 40 
 41 
 42     <!-- A "Connector" represents an endpoint by which requests are received
 43          and responses are returned. Documentation at :
 44          Java HTTP Connector: /docs/config/http.html
 45          Java AJP  Connector: /docs/config/ajp.html
 46          APR (HTTP/AJP) Connector: /docs/apr.html
 47          Define a non-SSL/TLS HTTP/1.1 Connector on port 8080
 48     -->
 49     <Connector port="8080" protocol="HTTP/1.1"
 50                connectionTimeout="20000"
 51                redirectPort="8443" />
 52     <!-- A "Connector" using the shared thread pool-->
 53     <!--
 54     <Connector executor="tomcatThreadPool"
 55                port="8080" protocol="HTTP/1.1"
 56                connectionTimeout="20000"
 57                redirectPort="8443" />
 58     -->
 59     <!-- Define an SSL/TLS HTTP/1.1 Connector on port 8443
 60          This connector uses the NIO implementation. The default
 61          SSLImplementation will depend on the presence of the APR/native
 62          library and the useOpenSSL attribute of the
 63          AprLifecycleListener.
 64          Either JSSE or OpenSSL style configuration may be used regardless of
 65          the SSLImplementation selected. JSSE style configuration is used below.
 66     -->
 67     <!--
 68     <Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
 69                maxThreads="150" SSLEnabled="true">
 70         <SSLHostConfig>
 71             <Certificate certificateKeystoreFile="conf/localhost-rsa.jks"
 72                          type="RSA" />
 73         </SSLHostConfig>
 74     </Connector>
 75     -->
 76     <!-- Define an SSL/TLS HTTP/1.1 Connector on port 8443 with HTTP/2
 77          This connector uses the APR/native implementation which always uses
 78          OpenSSL for TLS.
 79          Either JSSE or OpenSSL style configuration may be used. OpenSSL style
 80          configuration is used below.
 81     -->
 82     <!--
 83     <Connector port="8443" protocol="org.apache.coyote.http11.Http11AprProtocol"
 84                maxThreads="150" SSLEnabled="true" >
 85         <UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" />
 86         <SSLHostConfig>
 87             <Certificate certificateKeyFile="conf/localhost-rsa-key.pem"
 88                          certificateFile="conf/localhost-rsa-cert.pem"
 89                          certificateChainFile="conf/localhost-rsa-chain.pem"
 90                          type="RSA" />
 91         </SSLHostConfig>
 92     </Connector>
 93     -->
 94 
 95     <!-- Define an AJP 1.3 Connector on port 8009 -->
 96     <!--
 97     <Connector protocol="AJP/1.3"
 98                address="::1"
 99                port="8009"
100                redirectPort="8443" />
101     -->
102 
103     <!-- An Engine represents the entry point (within Catalina) that processes
104          every request.  The Engine implementation for Tomcat stand alone
105          analyzes the HTTP headers included with the request, and passes them
106          on to the appropriate Host (virtual host).
107          Documentation at /docs/config/engine.html -->
108 
109     <!-- You should set jvmRoute to support load-balancing via AJP ie :
110     <Engine name="Catalina" defaultHost="localhost" jvmRoute="jvm1">
111     -->
112     <Engine name="Catalina" defaultHost="localhost">
113 
114       <!--For clustering, please take a look at documentation at:
115           /docs/cluster-howto.html  (simple how to)
116           /docs/config/cluster.html (reference documentation) -->
117       <!--
118       <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>
119       -->
120 
121       <!-- Use the LockOutRealm to prevent attempts to guess user passwords
122            via a brute-force attack -->
123       <Realm className="org.apache.catalina.realm.LockOutRealm">
124         <!-- This Realm uses the UserDatabase configured in the global JNDI
125              resources under the key "UserDatabase".  Any edits
126              that are performed against this UserDatabase are immediately
127              available for use by the Realm.  -->
128         <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
129                resourceName="UserDatabase"/>
130       </Realm>
131 
132       <Host name="localhost"  appBase="webapps"
133             unpackWARs="true" autoDeploy="true">
134 
135         <!-- SingleSignOn valve, share authentication between web applications
136              Documentation at: /docs/config/valve.html -->
137         <!--
138         <Valve className="org.apache.catalina.authenticator.SingleSignOn" />
139         -->
140 
141         <!-- Access log processes all example.
142              Documentation at: /docs/config/valve.html
143              Note: The pattern used is equivalent to using pattern="common" -->
144         <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
145                prefix="localhost_access_log" suffix=".txt"
146                pattern="%h %l %u %t &quot;%r&quot; %s %b" />
147 
148       </Host>
149     </Engine>
150   </Service>
151 </Server>

这里我们看xml文件,这里我们看到Server节点,Service节点,Engine节点和Host节点。四个节点是依次是包含的关系,也对应了架构图中的Server,Service,Engine和Host四个板块的包含关系。从架构图我们能看到Engine,Host和Context之间有个Valve。

2,Valve接口

我们来看下org.apache.catalina.Valve接口的源码

package org.apache.catalina;

import java.io.IOException;

import javax.servlet.ServletException;

import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;

/**
 * <p>A <b>Valve</b> is a request processing component associated with a
 * particular Container.  A series of Valves are generally associated with
 * each other into a Pipeline.  The detailed contract for a Valve is included
 * in the description of the <code>invoke()</code> method below.</p>
 *
 * <b>HISTORICAL NOTE</b>:  The "Valve" name was assigned to this concept
 * because a valve is what you use in a real world pipeline to control and/or
 * modify flows through it.
 *
 * @author Craig R. McClanahan
 * @author Gunnar Rjnning
 * @author Peter Donald
 */
public interface Valve {


    //-------------------------------------------------------------- Properties

    /**
     * @return the next Valve in the pipeline containing this Valve, if any.
     */
    public Valve getNext();


    /**
     * Set the next Valve in the pipeline containing this Valve.
     *
     * @param valve The new next valve, or <code>null</code> if none
     */
    public void setNext(Valve valve);


    //---------------------------------------------------------- Public Methods


    /**
     * Execute a periodic task, such as reloading, etc. This method will be
     * invoked inside the classloading context of this container. Unexpected
     * throwables will be caught and logged.
     */
    public void backgroundProcess();


    /**
     * <p>Perform request processing as required by this Valve.</p>
     *
     * <p>An individual Valve <b>MAY</b> perform the following actions, in
     * the specified order:</p>
     * <ul>
     * <li>Examine and/or modify the properties of the specified Request and
     *     Response.
     * <li>Examine the properties of the specified Request, completely generate
     *     the corresponding Response, and return control to the caller.
     * <li>Examine the properties of the specified Request and Response, wrap
     *     either or both of these objects to supplement their functionality,
     *     and pass them on.
     * <li>If the corresponding Response was not generated (and control was not
     *     returned, call the next Valve in the pipeline (if there is one) by
     *     executing <code>getNext().invoke()</code>.
     * <li>Examine, but not modify, the properties of the resulting Response
     *     (which was created by a subsequently invoked Valve or Container).
     * </ul>
     *
     * <p>A Valve <b>MUST NOT</b> do any of the following things:</p>
     * <ul>
     * <li>Change request properties that have already been used to direct
     *     the flow of processing control for this request (for instance,
     *     trying to change the virtual host to which a Request should be
     *     sent from a pipeline attached to a Host or Context in the
     *     standard implementation).
     * <li>Create a completed Response <strong>AND</strong> pass this
     *     Request and Response on to the next Valve in the pipeline.
     * <li>Consume bytes from the input stream associated with the Request,
     *     unless it is completely generating the response, or wrapping the
     *     request before passing it on.
     * <li>Modify the HTTP headers included with the Response after the
     *     <code>getNext().invoke()</code> method has returned.
     * <li>Perform any actions on the output stream associated with the
     *     specified Response after the <code>getNext().invoke()</code> method has
     *     returned.
     * </ul>
     *
     * @param request The servlet request to be processed
     * @param response The servlet response to be created
     *
     * @exception IOException if an input/output error occurs, or is thrown
     *  by a subsequently invoked Valve, Filter, or Servlet
     * @exception ServletException if a servlet error occurs, or is thrown
     *  by a subsequently invoked Valve, Filter, or Servlet
     */
    public void invoke(Request request, Response response)
        throws IOException, ServletException;


    public boolean isAsyncSupported();
}

getNext和setNext方法主要是获取/设置当前这个责任链节点的上一个节点;处理逻辑的是方法invoke;isAsyncSupported方法是表示是否支持异步。例如org.apache.catalina.valves.AccessLogValve日志记录组件就是异步的,该组件是用来记录访问日志。

3,自定义Valve

这里我们使用两种方式来自定义Valve

1)原生Tomcat使用自定义Valve

2)SpringBoot使用自定义Valve

自定义Valve代码如下:

package net.sunmonkey.valves;

import org.apache.catalina.Valve;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;

import javax.servlet.ServletException;
import java.io.IOException;

/**
 * @author Wenqin Cheng
 * @date 2020/12/30
 */
public class MyValve implements Valve {

    private Valve next;

    @Override
    public Valve getNext() {
        return next;
    }

    @Override
    public void setNext(Valve valve) {
        next = valve;
    }

    @Override
    public void backgroundProcess() {

    }

    @Override
    public void invoke(Request request, Response response) throws IOException, ServletException {

        System.out.println("=====================start===================");

        System.out.println("#getNext().getClass().getName(): "+getNext().getClass().getName());
        System.out.println(this.getClass().getName()+"#invoke");

        System.out.println("request: "+request);
        System.out.println("response: "+response);

        System.out.println("request.getServletPath():"+request.getServletPath());

        System.out.println("request.getQueryString():"+request.getQueryString());

        //例如这里可以获取请求体长度,用来记录请求流量
        System.out.println("request.getContentLength(): "+request.getContentLength());

        //例如获取响应的流量
        System.out.println("response.getBytesWritten(false): "+response.getBytesWritten(false));

        System.out.println("==================end======================");

        getNext().invoke(request, response);
    }

    @Override
    public boolean isAsyncSupported() {
        return true;
    }
}

3.1,原生Tomcat使用自定义Valve

我们只需要在server.xml配置文件中配置就可以。如下Tomcat中的Valve以及自定义Valve

3.2,SpringBoot使用自定义Valve

后续更新...

 

至此,我们访问Tomcat的任何一个页面。可以在控制台找那个看到日志Tomcat中的Valve以及自定义Valve

 

上一篇:02. Tomcat源代码—04.分析请求过程


下一篇:Tomcat 管道与阀