Skip to content

12 Pipeline与Valve:如何实现容器间的调用、事务管理、权限验证?

你好,我是郭屹。今天我们继续手写MiniTomcat。

上一节课我们把项目结构进一步抽象成两层Container,分别是Context和Wrapper,从而实现一个服务器管理多个容器,而容器又可以管理多个Servlet,层层嵌套,提升了容器的扩展性。然后我们在这个基础上,参考Tomcat的项目结构,进行对应的调整,让它更贴近Tomcat源码本身。

接下来我们再转向通用部分的组件,首先考虑的就是日志。日志可以有效帮助我们调试程序运行过程中的问题,在合理的地方打印日志也可以帮助我们了解服务器的运行情况,所以我们接下来会 定义通用的日志组件

在日志组件定义完毕后,我们紧接着会学习 职责链模式 在Tomcat中的应用,当服务器要调用某个具体的Servlet时,是经过这些Contaienr一层一层调用的,所以Tomcat中每个Container的invoke()都是通过职责链模式调用的。

图片

我们一起来动手实现。

项目结构

这节课我们新增Logger、Pipeline、Valve、ValveContext接口,以及处理日志Logger与Valve的实现类等众多Java文件,具体内容后面我们会详细说明。你可以看一下现在这个项目的目录结构。

MiniTomcat
├─ src
│  ├─ main
│  │  ├─ java
│  │  │  ├─ com
│  │  │  │  ├─ minit
│  │  │  │  │  ├─ connector
│  │  │  │  │  │  ├─ http
│  │  │  │  │  │  │  ├─ DefaultHeaders.java
│  │  │  │  │  │  │  ├─ HttpConnector.java
│  │  │  │  │  │  │  ├─ HttpHeader.java
│  │  │  │  │  │  │  ├─ HttpProcessor.java
│  │  │  │  │  │  │  ├─ HttpRequestImpl.java
│  │  │  │  │  │  │  ├─ HttpRequestLine.java
│  │  │  │  │  │  │  ├─ HttpResponseImpl.java
│  │  │  │  │  │  │  ├─ ServletProcessor.java
│  │  │  │  │  │  │  ├─ SocketInputStream.java
│  │  │  │  │  │  │  ├─ StatisResourceProcessor.java
│  │  │  │  │  │  ├─ HttpRequestFacade.java
│  │  │  │  │  │  ├─ HttpResponseFacade.java
│  │  │  │  │  ├─ core
│  │  │  │  │  │  ├─ ContainerBase.java
│  │  │  │  │  │  ├─ StandardContext.java
│  │  │  │  │  │  ├─ StandardContextValve.java
│  │  │  │  │  │  ├─ StandardPipeline.java
│  │  │  │  │  │  ├─ StandardWrapper.java
│  │  │  │  │  │  ├─ StandardWrapperValve.java
│  │  │  │  │  ├─ logger
│  │  │  │  │  │  ├─ Constants.java
│  │  │  │  │  │  ├─ FileLogger.java
│  │  │  │  │  │  ├─ LoggerBase.java
│  │  │  │  │  │  ├─ SystemErrLogger.java
│  │  │  │  │  │  ├─ SystemOutLogger.java
│  │  │  │  │  ├─ session
│  │  │  │  │  │  ├─ StandardSession.java
│  │  │  │  │  │  ├─ StandardSessionFacade.java
│  │  │  │  │  ├─ startup
│  │  │  │  │  │  ├─ BootStrap.java
│  │  │  │  │  ├─ util
│  │  │  │  │  │  ├─ CookieTools.java
│  │  │  │  │  │  ├─ StringManager.java
│  │  │  │  │  ├─ valves
│  │  │  │  │  │  ├─ AccessLogValve.java
│  │  │  │  │  │  ├─ ValveBase.java
│  │  │  │  ├─ Connector.java
│  │  │  │  ├─ Container.java
│  │  │  │  ├─ Context.java
│  │  │  │  ├─ Logger.java
│  │  │  │  ├─ Pipeline.java
│  │  │  │  ├─ Request.java
│  │  │  │  ├─ Response.java
│  │  │  │  ├─ Session.java
│  │  │  │  ├─ Valve.java
│  │  │  │  ├─ ValveContext.java
│  │  │  │  ├─ Wrapper.java
│  │  ├─ resources
│  ├─ test
│  │  ├─ java
│  │  │  ├─ test
│  │  │  │  ├─ HelloServlet.java
│  │  │  │  ├─ TestServlet.java
│  │  ├─ resources
├─ webroot
│  ├─ test
│  │  ├─ HelloServlet.class
│  │  ├─ TestServlet.class
│  ├─ hello.txt
├─ pom.xml

引入日志组件

首先我们开始着手定义我们服务器的通用日志组件,先看一下Logger接口的定义。

package com.minit;
public interface Logger {
    public static final int FATAL = Integer.MIN_VALUE;
    public static final int ERROR = 1;
    public static final int WARNING = 2;
    public static final int INFORMATION = 3;
    public static final int DEBUG = 4;
    public String getInfo();
    public int getVerbosity();
    public void setVerbosity(int verbosity);
    public void log(String message);
    public void log(Exception exception, String msg);
    public void log(String message, Throwable throwable);
    public void log(String message, int verbosity);
    public void log(String message, Throwable throwable, int verbosity);
}

我们可以看到接口主要定义了不同的日志级别,以及重载了多个log方法,支持不同的传参。

接下来我们在Container接口里新增与Logger相关的Getter和Setter方法定义,用作Container的通用实现,你可以看一下示例代码。

package com.minit;
public interface Container {
    public Logger getLogger();
    public void setLogger(Logger logger);
}

有了这些定义之后,我们在之前已经定义好的/com/minit/logger目录下提供LoggerBase作为Logger接口的抽象实现类,主体如下:

package com.minit.logger;
public abstract class LoggerBase implements Logger {
    protected int debug = 0;
    protected static final String info = "com.minit.logger.LoggerBase/1.0";
    protected int verbosity = ERROR;
    public int getDebug() {
        return (this.debug);
    }
    public void setDebug(int debug) {
        this.debug = debug;
    }
    public String getInfo() {
        return (info);
    }
    public int getVerbosity() {
        return (this.verbosity);
    }
    public void setVerbosity(int verbosity) {
        this.verbosity = verbosity;
    }
    public void setVerbosityLevel(String verbosity) {
        if ("FATAL".equalsIgnoreCase(verbosity))
            this.verbosity = FATAL;
        else if ("ERROR".equalsIgnoreCase(verbosity))
            this.verbosity = ERROR;
        else if ("WARNING".equalsIgnoreCase(verbosity))
            this.verbosity = WARNING;
        else if ("INFORMATION".equalsIgnoreCase(verbosity))
            this.verbosity = INFORMATION;
        else if ("DEBUG".equalsIgnoreCase(verbosity))
            this.verbosity = DEBUG;
    }
    //这个log方法由上层业务程序员实现
    public abstract void log(String msg);

    public void log(Exception exception, String msg) {
        log(msg, exception);
    }
    //核心方法,printStackTrace,然后调用一个业务实现的log(msg)
    public void log(String msg, Throwable throwable) {
        CharArrayWriter buf = new CharArrayWriter();
        PrintWriter writer = new PrintWriter(buf);
        writer.println(msg);
        throwable.printStackTrace(writer);
        Throwable rootCause = null;
        if  (throwable instanceof ServletException)
            rootCause = ((ServletException) throwable).getRootCause();
        if (rootCause != null) {
            writer.println("----- Root Cause -----");
            rootCause.printStackTrace(writer);
        }
        log(buf.toString());
    }
    public void log(String message, int verbosity) {
        if (this.verbosity >= verbosity)
            log(message);
    }
    public void log(String message, Throwable throwable, int verbosity) {
        if (this.verbosity >= verbosity)
            log(message, throwable);
    }
}

public void log(String msg, Throwable throwable)这个方法是核心,根据具体实现可以知道,当存在Exception异常时,后端会调用printStackTrace抛出异常,然后调用abstract void log(String msg)方法,记录日志,这个方法是抽象方法,所以交给具体的实现类去记录。

接下来我们再定义通用类,为后续定义实现类做准备。首先在/com/minit/util包内定义StringManager工具类,这个类的作用在于提供单独的实例用来管理各自包下的日志打印,只需要调用getManager方法即可,不需要频繁创建日志打印对象。主体内容如下所示:

package com.minit.util;
public class StringManager {
    private StringManager(String packageName) {
    }
    public String getString(String key) {
        if (key == null) {
            String msg = "key is null";
            throw new NullPointerException(msg);
        }
        String str = null;
        str = key;
        return str;
    }
    //用参数拼串
    public String getString(String key, Object[] args) {
        String iString = null;
        String value = getString(key);
        try {
            //消除null对象
            Object nonNullArgs[] = args;
            for (int i=0; i<args.length; i++) {
                if (args[i] == null) {
                    if (nonNullArgs==args) nonNullArgs=(Object[])args.clone();
                    nonNullArgs[i] = "null";
                }
            }
            //拼串
            iString = MessageFormat.format(value, nonNullArgs);
        } catch (IllegalArgumentException iae) {
            StringBuffer buf = new StringBuffer();
            buf.append(value);
            for (int i = 0; i < args.length; i++) {
                buf.append(" arg[" + i + "]=" + args[i]);
            }
            iString = buf.toString();
        }
        return iString;
    }
    public String getString(String key, Object arg) {
        Object[] args = new Object[] {arg};
        return getString(key, args);
    }
    public String getString(String key, Object arg1, Object arg2) {
        Object[] args = new Object[] {arg1, arg2};
        return getString(key, args);
    }
    public String getString(String key, Object arg1, Object arg2,
                            Object arg3) {
        Object[] args = new Object[] {arg1, arg2, arg3};
        return getString(key, args);
    }
    public String getString(String key, Object arg1, Object arg2,
                            Object arg3, Object arg4) {
        Object[] args = new Object[] {arg1, arg2, arg3, arg4};
        return getString(key, args);
    }
    private static Map<String,StringManager> managers = new ConcurrentHashMap<>();
    //每个package有相应的StringManager
    public synchronized static StringManager getManager(String packageName) {
        StringManager mgr = (StringManager)managers.get(packageName);
        if (mgr == null) {
            mgr = new StringManager(packageName);
            managers.put(packageName, mgr);
        }
        return mgr;
    }
}

再之后,我们为Logger分别定义Constants常量类、SystemErrLogger标准错误日志类,还有SystemOutLogger标准输出日志类,这几个类目前的定义比较简单,你可以看一下。

Constants常量类:

package com.minit.logger;
public class Constants {
    public static final String Package = "com.minit.logger";
}

SystemErrLogger标准错误日志类:

package com.minit.logger;
public class SystemErrLogger extends LoggerBase {
    protected static final String info =
            "com.minit.logger.SystemErrLogger/0.1";
    public void log(String msg) {
        System.err.println(msg);
    }
}

SystemOutLogger标准输出日志类:

package com.minit.logger;
public class SystemOutLogger extends LoggerBase {
    protected static final String info =
            "com.minit.logger.SystemOutLogger/1.0";
    public void log(String msg) {
        System.out.println(msg);
    }
}

有了前面的铺垫,接下来我们在这里定义LoggerBase其中一个实现类:FileLogger,这个类可以做到根据时间自动生成日志文件,你可以参考我给出的代码主体部分。

package com.minit.logger;
public class FileLogger extends LoggerBase{
    private String date = "";
    private String directory = "logs";
    protected static final String info = "com.minit.logger.FileLogger/0.1";
    private String prefix = "minit.";
    private StringManager sm = StringManager.getManager(Constants.Package);
    private boolean started = false;
    private String suffix = ".log";
    private boolean timestamp = true;
    private PrintWriter writer = null;
    public String getDirectory() {
        return (directory);
    }
    public void setDirectory(String directory) {
        String oldDirectory = this.directory;
        this.directory = directory;
    }
    public String getPrefix() {
        return (prefix);
    }
    public void setPrefix(String prefix) {
        String oldPrefix = this.prefix;
        this.prefix = prefix;
    }
    public String getSuffix() {
        return (suffix);
    }
    public void setSuffix(String suffix) {
        String oldSuffix = this.suffix;
        this.suffix = suffix;
    }
    public boolean getTimestamp() {
        return (timestamp);
    }
    public void setTimestamp(boolean timestamp) {
        boolean oldTimestamp = this.timestamp;
        this.timestamp = timestamp;
    }
    public void log(String msg) {
        // 当前时间Construct the timestamp we will use, if requested
        Timestamp ts = new Timestamp(System.currentTimeMillis());
        String tsString = ts.toString().substring(0, 19);
        String tsDate = tsString.substring(0, 10);
        // 如果日期变化了,新生成一个log文件
        // If the date has changed, switch log files
        if (!date.equals(tsDate)) {
            synchronized (this) {
                if (!date.equals(tsDate)) {
                    close();
                    date = tsDate;
                    open();
                }
            }
        }
        // 记录日志,带上时间戳
        if (writer != null) {
            if (timestamp) {
                writer.println(tsString + " " + msg);
            } else {
                writer.println(msg);
            }
        }
    }
    private void close() {
        if (writer == null)
            return;
        writer.flush();
        writer.close();
        writer = null;
        date = "";
    }
    private void open() {
        File dir = new File(directory);
        if (!dir.isAbsolute())
            dir = new File(System.getProperty("catalina.base"), directory);
        dir.mkdirs();
        // 打开日志文件 Open the current log file
        try {
            String pathname = dir.getAbsolutePath() + File.separator +
                    prefix + date + suffix;
            writer = new PrintWriter(new FileWriter(pathname, true), true);
        } catch (IOException e) {
            writer = null;
        }
    }
}

根据定义的属性,还有void log(String msg)方法,服务器会在 /logs/ 目录下生成一个类似minit.yyyy-MM-dd.log格式的日志文件,内部标明了这个文件创建的日期,如果记录的这个日期和当前日期不一样就关闭当前文件,同时再创建一个新的文件,这样就做到每天的日志文件不同,加以区分,方便定位问题。

里面的open()方法可以用来打开一个文件,close()则用于关闭。

最后我们在BootStrap中创建这个Logger,并且指派给Container使用,用来打印日志。

package com.minit.startup;
public class BootStrap {
    public static final String WEB_ROOT =
            System.getProperty("user.dir") + File.separator + "webroot";
    private static int debug = 0;
    public static void main(String[] args) {
        if (debug >= 1)
            log(".... startup ....");
        HttpConnector connector = new HttpConnector();
        StandardContext container = new StandardContext();
        connector.setContainer(container);
        container.setConnector(connector);
        Logger logger = new FileLogger();
        container.setLogger(logger);
        connector.start();
    }
    private static void log(String message) {
        System.out.print("Bootstrap: ");
        System.out.println(message);
    }
    private static void log(String message, Throwable exception) {
        log(message);
        exception.printStackTrace(System.out);
    }
}

在这里,我们把Logger传给Container的原因在于,多个Container可以使用不同的Logger,针对不同的目录和文件进行操作。

最后我们再完善一些代码,Logger的定义就改造完毕了。

首先我们在ContainerBase中增加与日志相关的代码。

package com.minit.core;
public abstract class ContainerBase implements Container {
    //ContainerBase中增加与日志相关的代码
    protected Logger logger = null;
    public Logger getLogger() {
        if (logger != null)
            return (logger);
        if (parent != null)
            return (parent.getLogger());
        return (null);
    }
    public synchronized void setLogger(Logger logger) {
        Logger oldLogger = this.logger;
        if (oldLogger == logger)
            return;
        this.logger = logger;
    }
    protected void log(String message) {
        Logger logger = getLogger();
        if (logger != null)
            logger.log(logName() + ": " + message);
        else
            System.out.println(logName() + ": " + message);
    }

    protected void log(String message, Throwable throwable) {
        Logger logger = getLogger();
        if (logger != null)
            logger.log(logName() + ": " + message, throwable);
        else {
            System.out.println(logName() + ": " + message + ": " + throwable);
            throwable.printStackTrace(System.out);
        }
    }
    protected String logName() {
        String className = this.getClass().getName();
        int period = className.lastIndexOf(".");
        if (period >= 0)
            className = className.substring(period + 1);
        return (className + "[" + getName() + "]");
    }
}

再在StandardContext中增加与日志相关的代码。

package com.minit.core;
public class StandardContext extends ContainerBase implements Context {
    public StandardContext() {
        try {
            // create a URLClassLoader
            URL[] urls = new URL[1];
            URLStreamHandler streamHandler = null;
            File classPath = new File(Bootstrap.WEB_ROOT);
            String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString() ;
            urls[0] = new URL(null, repository, streamHandler);
            loader = new URLClassLoader(urls);
        } catch (IOException e) {
            System.out.println(e.toString() );
        }
        log("Container created.");
    }
}

在HttpConnector中增加与日志相关的代码。

public class HttpConnector implements Connector, Runnable {
    private String info = "com.minit.connector.http.HttpConnector/0.1";
    private int port = 8080;
    int minProcessors = 3;
    int maxProcessors = 10;
    int curProcessors = 0;
    Deque<HttpProcessor> processors = new ArrayDeque<>();
    public static Map<String, HttpSession> sessions = new ConcurrentHashMap<>();
    Container container = null;
    private String threadName = null;

    //启动connector,记录启动日志
    public void start() {
        threadName = "HttpConnector[" + port + "]";
        log("httpConnector.starting  " + threadName);
        Thread thread = new Thread(this);
        thread.start();
    }

    private HttpProcessor newProcessor() {
        HttpProcessor initprocessor = new HttpProcessor(this);
        initprocessor.start();
        processors.push(initprocessor);
        curProcessors++;
        log("newProcessor");
        return ((HttpProcessor) processors.pop());
    }
    //记录日志
    private void log(String message) {
        Logger logger = container.getLogger();
        String localName = threadName;
        if (localName == null)
            localName = "HttpConnector";
        if (logger != null)
            logger.log(localName + " " + message);
        else
            System.out.println(localName + " " + message);
    }
    //记录日志
    private void log(String message, Throwable throwable) {
        Logger logger = container.getLogger();
        String localName = threadName;
        if (localName == null)
            localName = "HttpConnector";
        if (logger != null)
            logger.log(localName + " " + message, throwable);
        else {
            System.out.println(localName + " " + message);
            throwable.printStackTrace(System.out);
        }
    }
    public Container getContainer() {
        return this.container;
    }
    public void setContainer(Container container) {
        this.container = container;
    }
    @Override
    public String getInfo() {
        return this.info;
    }
}

引入日志组件后,我们转向第二部分,也是Tomcat设计里的一个特色:职责链。

引入职责链模式

当服务器要调用某个具体的Servlet的时候,是先经过这些container的invoke()方法,一层一层调用的。每一个Container内部在真正的任务执行前(如执行Servlet),都会途径过滤层,这些层叫作Valve,一个一个地执行Valve之后再执行Servlet,这样可以给Container做一些过滤的操作,比如权限校验、日志打印、报错输出等。

在Tomcat中,还引入了一个概念——Pipeline,Container的invoke()方法没有硬编码,而是调用Pipeline的invoke方法。

public void invoke(Request request, Response response) throws IOException, ServletException {
    pipeline.invoke(request, response);
}

简单来讲,就是每一层Container都有一个Pipeline,也是一根链条,这根链条是许多Valve串起来的。调用某个Container的invoke(),就是找到Pipeline的第一个Valve进行调用,第一个Valve会调用下一个,一个一个传下去,到最后一个Basic Valve,然后调用下一层容器,直到结束。

而这个Basic Valve则是在每个Container里面都默认存在的,通过Pipeline来依次调用每一个Valve,这就是职责链模式,而且这种方式也像Pipeline名称一样,流水线似的从前往后。

图片

这里我们先定义通用的Valve、ValveContext与Pipeline接口。Valve接口表示的Container中的一段用户增加的逻辑,主要就是一个invoke方法。

package com.minit;
public interface Valve {
    public String getInfo();
    public Container getContainer();
    public void setContainer(Container container);
    public void invoke(Request request, Response response,ValveContext context)
            throws IOException, ServletException;
}

ValveContext接口负责调用下一个Valve,这样就会形成一系列对Valve的调用。

package com.minit;
import java.io.IOException;
import javax.servlet.ServletException;
public interface ValveContext {
    public String getInfo();
    public void invokeNext(Request request, Response response) throws IOException, ServletException;
}

Pipeline表示的是Container中的Valve链条,其中有特殊的basic。Pipeline启动Valve链条的调用。

package com.minit;
import java.io.IOException;
import javax.servlet.ServletException;
public interface Pipeline {
    public Valve getBasic();
    public void setBasic(Valve valve);
    public void addValve(Valve valve);
    public Valve[] getValves();
    public void invoke(Request request, Response response) throws IOException, ServletException;
    public void removeValve(Valve valve);
}

接着定义实现基类——ValveBase。

package com.minit.valves;
public abstract class ValveBase implements Valve {
    protected Container container = null;
    protected int debug = 0;
    protected static String info = "com.minit.valves.ValveBase/0.1";
    public Container getContainer() {
        return (container);
    }
    public void setContainer(Container container) {
        this.container = container;
    }
    public int getDebug() {
        return (this.debug);
    }
    public void setDebug(int debug) {
        this.debug = debug;
    }
    public String getInfo() {
        return (info);
    }
}

结合之前的内容,我们将Log与Valve结合,定义AccessLogValve类。这个Valve的作用是记录日志,你可以看一下程序的主体。

package com.minit.valves;
public final class AccessLogValve extends ValveBase {
    //下面的属性都是与访问日志相关的配置参数
    public static final String COMMON_ALIAS = "common";
    public static final String COMMON_PATTERN = "%h %l %u %t \"%r\" %s %b";
    public static final String COMBINED_ALIAS = "combined";
    public static final String COMBINED_PATTERN = "%h %l %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\"";
    public AccessLogValve() {
        super();
        setPattern("common");
    }
    private String dateStamp = "";
    private String directory = "logs";
    protected static final String info =
            "com.minit.valves.AccessLogValve/0.1";
    protected static final String months[] =
            { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
                    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
    private boolean common = false;
    private boolean combined = false;
    private String pattern = null;
    private String prefix = "access_log.";
    private String suffix = "";
    private PrintWriter writer = null;
    private DateTimeFormatter dateFormatter = null;
    private DateTimeFormatter dayFormatter = null;
    private DateTimeFormatter monthFormatter = null;
    private DateTimeFormatter yearFormatter = null;
    private DateTimeFormatter timeFormatter = null;
    private String timeZone = null;
    private LocalDate currentDate = null;
    private String space = " ";
    private long rotationLastChecked = 0L;

    //省略属性的getter/setter

    //这是核心方法invoke
    public void invoke(Request request, Response response, ValveContext context)
            throws IOException, ServletException {
        // 先调用context中的invokeNext,实现职责链调用
        // Pass this request on to the next valve in our pipeline
        context.invokeNext(request, response);

        //以下是本valve本身的业务逻辑
        LocalDate date = getDate();
        StringBuffer result = new StringBuffer();
        // Check to see if we should log using the "common" access log pattern
        //拼串
        if (common || combined) {
            //拼串,省略
        } else { //按照模式拼串
            // Generate a message based on the defined pattern
            boolean replace = false;
            for (int i = 0; i < pattern.length(); i++) {
                char ch = pattern.charAt(i);
                if (replace) {
                    result.append(replace(ch, date, request, response));
                    replace = false;
                } else if (ch == '%') {
                    replace = true;
                } else {
                    result.append(ch);
                }
            }
        }
        log(result.toString(), date);
    }
    private synchronized void close() {
        if (writer == null)
            return;
        writer.flush();
        writer.close();
        writer = null;
        dateStamp = "";
    }
    //按照日期生成日志文件,并记录日志
    public void log(String message, LocalDate date) {
        // Only do a logfile switch check once a second, max.
        long systime = System.currentTimeMillis();
        if ((systime - rotationLastChecked) > 1000) {
            // We need a new currentDate
            currentDate = LocalDate.now();
            rotationLastChecked = systime;
            // Check for a change of date
            String tsDate = dateFormatter.format(currentDate);
            // If the date has changed, switch log files
            if (!dateStamp.equals(tsDate)) {
                synchronized (this) {
                    if (!dateStamp.equals(tsDate)) {
                        close();
                        dateStamp = tsDate;
                        open();
                    }
                }
            }
        }
        // Log this message
        if (writer != null) {
            writer.println(message);
        }
    }

    //打开日志文件
    private synchronized void open() {
        // Create the directory if necessary
        File dir = new File(directory);
        if (!dir.isAbsolute())
            dir = new File(System.getProperty("minit.base"), directory);
        dir.mkdirs();
        // Open the current log file
        try {
            String pathname = dir.getAbsolutePath() + File.separator +
                    prefix + dateStamp + suffix;
            writer = new PrintWriter(new FileWriter(pathname, true), true);
        } catch (IOException e) {
            writer = null;
        }
    }
    //替换字符串
    private String replace(char pattern, LocalDate date, Request request,
                           Response response) {
        //省略
    }
    private LocalDate getDate() {
        // Only create a new Date once per second, max.
        long systime = System.currentTimeMillis();
        if ((systime - currentDate.getLong(ChronoField.MILLI_OF_SECOND)) > 1000) {
            currentDate = LocalDate.now();
        }
        return currentDate;
    }
}

接下来我们定义StandardPipeline,提供Pipeline的标准实现,主体如下:

package com.minit.core;
public class StandardPipeline implements Pipeline{
    public StandardPipeline() {
        this(null);
    }
    public StandardPipeline(Container container) {
        super();
        setContainer(container);
    }
    protected Valve basic = null; //basic valve
    protected Container container = null;
    protected int debug = 0;
    protected String info = "com.minit.core.StandardPipeline/0.1";
    protected Valve valves[] = new Valve[0]; //一组valve,可以逐个调用

    public Valve getBasic() {
        return (this.basic);
    }
    public void setBasic(Valve valve) {
        // Change components if necessary
        Valve oldBasic = this.basic;
        if (oldBasic == valve)
            return;
        // Start the new component if necessary
        if (valve == null)
            return;
        valve.setContainer(container);
        this.basic = valve;
    }

    //添加valve
    public void addValve(Valve valve) {
        // Add this Valve to the set associated with this Pipeline
        synchronized (valves) {
            Valve results[] = new Valve[valves.length +1];
            System.arraycopy(valves, 0, results, 0, valves.length);
            valve.setContainer(container);
            results[valves.length] = valve;
            valves = results;
        }
    }

    public Valve[] getValves() {
        if (basic == null)
            return (valves);
        synchronized (valves) {
            Valve results[] = new Valve[valves.length + 1];
            System.arraycopy(valves, 0, results, 0, valves.length);
            results[valves.length] = basic;
            return (results);
        }
    }
    //核心方法invoke
    public void invoke(Request request, Response response)
            throws IOException, ServletException {
        System.out.println("StandardPipeline invoke()");
        // 转而调用context中的invoke,发起职责链调用
        // Invoke the first Valve in this pipeline for this request
        (new StandardPipelineValveContext()).invokeNext(request, response);
    }

    public void removeValve(Valve valve) {
        synchronized (valves) {
            // Locate this Valve in our list
            int j = -1;
            for (int i = 0; i < valves.length; i++) {
                if (valve == valves[i]) {
                    j = i;
                    break;
                }
            }
            if (j < 0)
                return;
            valve.setContainer(null);
            // Remove this valve from our list
            Valve results[] = new Valve[valves.length - 1];
            int n = 0;
            for (int i = 0; i < valves.length; i++) {
                if (i == j)
                    continue;
                results[n++] = valves[i];
            }
            valves = results;
        }
    }

    //内部类,维护了stage,表示valves数组中的位置,逐个invoke
    protected class StandardPipelineValveContext implements ValveContext {
        protected int stage = 0;

        public void invokeNext(Request request, Response response)
                throws IOException, ServletException {
            System.out.println("StandardPipelineValveContext invokeNext()");
            int subscript = stage;
            stage = stage + 1;
            // Invoke the requested Valve for the current request thread
            if (subscript < valves.length) {
                valves[subscript].invoke(request, response, this);
            } else if ((subscript == valves.length) && (basic != null)) {
                basic.invoke(request, response, this);
            } else {
                throw new ServletException("standardPipeline.noValve");
            }
        }
    }
}

在StandardPipeline类中,我们使用了一个数组保存当前valves的值以及单独的Basic Valve。

protected Valve valves[] = new Valve[0];
protected Valve basic = null;

对Pipeline的调用变成了启动StandardPipelineValveContext的invokeNext()。

public void invoke(Request request, Response response)
        throws IOException, ServletException {
    System.out.println("StandardPipeline invoke()");
    (new StandardPipelineValveContext()).invokeNext(request, response);
}

其中StandardPipelineValveContext是StandardPipeline里定义的一个内部类,在这个内部类里,维护了一个stage的域,用来记录Valve的编号。

protected class StandardPipelineValveContext implements ValveContext {
    protected int stage = 0;
    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.length) {
            valves[subscript].invoke(request, response, this);
        } else if ((subscript == valves.length) && (basic != null)) {
            basic.invoke(request, response, this);
        } else {
            throw new ServletException("standardPipeline.noValve");
        }
    }
}

在判断条件中,根据编号调用valve.invoke(),调用到最后,就是调用Basic Valve的invoke()方法。而valve.invoke()方法调用的本质在于 调用ValveContext的invokeNext()方法, 随后执行本身的业务任务,参考AccessLogValve也是这样。

public void invoke(Request request, Response response, ValveContext context) throws IOException, ServletException {
    context.invokeNext(request, response);
}

这样一个一个传递下去,整个链路就调用完毕。

因为整个服务器的起点是Connector和Container,所以我们得把Pipeline加入到Container中,这就需要调整ContainerBase里的实现。你可以看一下当前ContainerBase类的实现,主体如下:

package com.minit.core;
import com.minit.*;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public abstract class ContainerBase implements Container, Pipeline {
    protected Map<String, Container> children = new ConcurrentHashMap<>();
    protected ClassLoader loader = null;
    protected String name = null;
    protected Container parent = null;
    protected Logger logger = null;
    protected Pipeline pipeline = new StandardPipeline(this); //增加pipeline支持
    public Pipeline getPipeline() {
        return (this.pipeline);
    }
    public void invoke(Request request, Response response) throws IOException, ServletException {
        System.out.println("ContainerBase invoke()");
        pipeline.invoke(request, response);
    }
    public synchronized void addValve(Valve valve) {
        pipeline.addValve(valve);
    }
    public Valve getBasic() {
        return (pipeline.getBasic());
    }
    public Valve[] getValves() {
        return (pipeline.getValves());
    }
    public synchronized void removeValve(Valve valve) {
        pipeline.removeValve(valve);
    }
    public void setBasic(Valve valve) {
        pipeline.setBasic(valve);
    }
    public ClassLoader getLoader() {
        if (loader != null)
            return (loader);
        if (parent != null)
            return (parent.getLoader());
        return (null);
    }
    public synchronized void setLoader(ClassLoader loader) {
        ClassLoader oldLoader = this.loader;
        if (oldLoader == loader) {
            return;
        }
        this.loader = loader;
    }

    protected void log(String message) {
        Logger logger = getLogger();
        if (logger != null)
            logger.log(logName() + ": " + message);
        else
            System.out.println(logName() + ": " + message);
    }

    protected void log(String message, Throwable throwable) {
        Logger logger = getLogger();
        if (logger != null)
            logger.log(logName() + ": " + message, throwable);
        else {
            System.out.println(logName() + ": " + message + ": " + throwable);
            throwable.printStackTrace(System.out);
        }
    }

}

主要是增加了StandardPipeline的处理,在ContainerBase中引入Pipeline,调用invoke()就变成了调用Pipeline中的invoke()方法实现。

    protected Pipeline pipeline = new StandardPipeline(this);
    public Pipeline getPipeline() {
        return (this.pipeline);
    }
    public void invoke(Request request, Response response) throws IOException, ServletException {
        System.out.println("ContainerBase invoke()");
        pipeline.invoke(request, response);
    }
    public synchronized void addValve(Valve valve) {
        pipeline.addValve(valve);
    }

而在StandardContext的构造方法中我们也进行调整,增加对Pipeline的处理。

 public StandardContext() {
        super();
        pipeline.setBasic(new StandardContextValve());
}

处理之后,在StandardContext类里调用invoke()方法就很简单了,只要调用ContainerBase的invoke()启动Pipeline,随后调用Pipiline中的invoke()就可以了。

public void invoke(Request request, Response response) throws IOException, ServletException {
    super.invoke(request, response);
}

你可以看一下StandardContext类中新增和调整的代码。

package com.minit.core;
public class StandardContext extends ContainerBase implements Context{
    public StandardContext() {
        super();
        pipeline.setBasic(new StandardContextValve());
        try {
            // create a URLClassLoader
            URL[] urls = new URL[1];
            URLStreamHandler streamHandler = null;
            File classPath = new File(BootStrap.WEB_ROOT);
            String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString() ;
            urls[0] = new URL(null, repository, streamHandler);
            loader = new URLClassLoader(urls);
        } catch (IOException e) {
            System.out.println(e.toString() );
        }
        log("Container created.");
    }

    public void invoke(Request request, Response response)
            throws IOException, ServletException {
        System.out.println("StandardContext invoke()");
        super.invoke(request, response);
    }

    public Wrapper getWrapper(String name){
        StandardWrapper servletWrapper = servletInstanceMap.get(name);
        if ( servletWrapper == null) {
            String servletClassName = name;
            servletWrapper = new StandardWrapper(servletClassName,this);
            this.servletClsMap.put(name, servletClassName);
            this.servletInstanceMap.put(name, servletWrapper);
        }
        return servletWrapper;
    }
}

下面是被StandardContext引用的StandardContextValve类的定义。

package com.minit.core;
final class StandardContextValve extends ValveBase {
    private static final String info =
            "org.apache.catalina.core.StandardContextValve/1.0";
    public String getInfo() {
        return (info);
    }
    public void invoke(Request request, Response response, ValveContext valveContext)
            throws IOException, ServletException {
        System.out.println("StandardContextValve invoke()");
        StandardWrapper servletWrapper = null;
        String uri = ((HttpRequestImpl)request).getUri();
        String servletName = uri.substring(uri.lastIndexOf("/") + 1);
        String servletClassName = servletName;
        StandardContext context = (StandardContext)getContainer();
        servletWrapper = (StandardWrapper)context.getWrapper(servletName);
        try {
            System.out.println("Call service()");
            servletWrapper.invoke(request, response);
        }
        catch (Exception e) {
            System.out.println(e.toString());
        }
        catch (Throwable e) {
            System.out.println(e.toString());
        }
    }
}

我们以前写在StandardContext类里面的invoke()方法实现代码,现在用StandardContextValve的invoke()来取代了,从这里面拿到Wrapper后直接调用。这里没有invokeNext()的实现,因为这个Valve是Basic Valve,是最后调用的。

同理,以前写在StandardWrapper类的invoke实现代码,现在也要用一个Valve来取代了。

package com.minit.core;
public class StandardWrapperValve extends ValveBase {
    @Override
    public void invoke(Request request, Response response, ValveContext context) throws IOException, ServletException {
        // TODO Auto-generated method stub
        System.out.println("StandardWrapperValve invoke()");
        HttpServletRequest requestFacade = new HttpRequestFacade((HttpRequestImpl) request);
        HttpServletResponse responseFacade = new HttpResponseFacade((HttpResponseImpl) response);
        Servlet instance = ((StandardWrapper)getContainer()).getServlet();
        if (instance != null) {
            instance.service(requestFacade, responseFacade);
        }
    }
}

而当前的StandardWrapper,修改调整了哪些代码呢?我列出来了,你看一下。

package com.minit.core;
public class StandardWrapper extends ContainerBase implements Wrapper {
    public StandardWrapper(String servletClass, StandardContext parent) {
        super();
        pipeline.setBasic(new StandardWrapperValve());
        this.parent = parent;
        this.servletClass = servletClass;
        try {
            loadServlet();
        } catch (ServletException e) {
            e.printStackTrace();
        }
    }

    public void invoke(Request request, Response response)
            throws IOException, ServletException {
        System.out.println("StandardWrapper invoke()");
        super.invoke(request, response);
    }
}

到这里我们的职责链模式就改造好了,最后还有一些调整和改动,让它更贴近Tomcat。主要是HttpRequestImpl和HttpResponseImpl两个实现类,分别支持实现Request接口和Response接口。你可以看一下 Gitee 中的代码。

ServletProcessor类里的process方法签名也一并调整成Request与Response。

package com.minit.connector.http;
public class ServletProcessor {
    public void process(Request request, Response response) throws IOException, ServletException {
        this.connector.getContainer().invoke(request, response);
    }
}

到这里这节课的改造就结束了,快试着运行一下吧!

小结

这节课我们先引入了日志组件,通过将日志输出到文件,并用不同日期加以区分,可以帮助我们更好记录服务器运行状态,尽快定位问题。

随后我们重点研究了Tomcat中的Pipeline,通过Pipeline和Valve的处理,带出了责任链这一设计模式,确保我们在流程走通的前提下,在每一层Container之间增加权限校验、日志打印、错误输出等自定义的处理。

这节课代码参见: https://gitee.com/yaleguo1/minit-learning-demo/tree/geek_chapter12

思考题

学完了这节课的内容,我们来思考一个问题:我们在某个容器的Pipeline中增加三个Valve和一个Basic Valve,那么具体的调用次序是怎样的?

欢迎你把你想到的方案分享到评论区,也欢迎你把这节课的内容分享给其他朋友,我们下节课再见!