Skip to content

07 对内的保护:引入门面模式封装内部实现类

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

在前面的学习内容中,我们为了符合Servlet规范,新增了HttpRequest与HttpResponse类,但截止到现在我们只对Request请求相关的代码进行了实现,所以这节课我们就来改造Response返回代码。

另外,我们也注意到,在HttpProcessor类里,我们直接使用的是HttpRequest与HttpResponse,这两个对象要传入Servlet里,但在这两个类中我们也定义了许多内部的方法,一旦被用户知晓我们的实现类,那么这些内部方法就暴露在用户面前了,这是我们不愿看到的,也是我们需要规避的。因此这节课我们计划用门面(Facade)设计模式来解决这个问题。

下面就让我们一起来动手实现。

项目结构

这节课的项目结构主要新增了HttpRequestFacade.java与HttpResponseFacade.java两个类,如下所示:

MiniTomcat
├─ src
│  ├─ main
│  │  ├─ java
│  │  │  ├─ server
│  │  │  │  ├─ DefaultHeaders.java
│  │  │  │  ├─ HttpConnector.java
│  │  │  │  ├─ HttpHeader.java
│  │  │  │  ├─ HttpProcessor.java
│  │  │  │  ├─ HttpRequest.java
│  │  │  │  ├─ HttpRequestFacade.java
│  │  │  │  ├─ HttpRequestLine.java
│  │  │  │  ├─ HttpResponse.java
│  │  │  │  ├─ HttpResponseFacade.java
│  │  │  │  ├─ HttpServer.java
│  │  │  │  ├─ Request.java
│  │  │  │  ├─ Response.java
│  │  │  │  ├─ ServletProcessor.java
│  │  │  │  ├─ SocketInputStream.java
│  │  │  │  ├─ StatisResourceProcessor.java
│  │  ├─ resources
│  ├─ test
│  │  ├─ java
│  │  │  ├─ test
│  │  │  │  ├─ HelloServlet.java
│  │  ├─ resources
├─ webroot
│  ├─ test
│  │  ├─ HelloServlet.class
│  ├─ hello.txt
├─ pom.xml

Response返回信息解析

上一节课我们完成了Request请求信息解析,有了前文铺垫,接下来针对Response的改造就容易很多了。

我们在前面提到过,HTTP协议中规定Response返回格式由以下几部分组成:状态行、响应头、空行、响应体。我们常用的状态码一般为200、401、404、500、503、504等,我们在HttpResponse里用switch条件语句先关联常用的状态码与状态信息。

package server;
public class HttpResponse implements HttpServletResponse {
  protected String getStatusMessage(int status) {
    switch (status) {
        case SC_OK:
            return ("OK");
        case SC_ACCEPTED:
            return ("Accepted");
        case SC_BAD_GATEWAY:
            return ("Bad Gateway");
        case SC_BAD_REQUEST:
            return ("Bad Request");
        case SC_CONTINUE:
            return ("Continue");
        case SC_FORBIDDEN:
            return ("Forbidden");
        case SC_INTERNAL_SERVER_ERROR:
            return ("Internal Server Error");
        case SC_METHOD_NOT_ALLOWED:
            return ("Method Not Allowed");
        case SC_NOT_FOUND:
            return ("Not Found");
        case SC_NOT_IMPLEMENTED:
            return ("Not Implemented");
        case SC_REQUEST_URI_TOO_LONG:
            return ("Request URI Too Long");
        case SC_SERVICE_UNAVAILABLE:
            return ("Service Unavailable");
        case SC_UNAUTHORIZED:
            return ("Unauthorized");
        default:
            return ("HTTP Response Status " + status);
    }
}

其中定义的状态常量都来自HttpServletResponse里的定义。

接下来我们关注头部信息的操作,使用headers这样一个concurrentHashMap存储头部的键值信息,里面是content-length和content-type的时候,我们还会设置相关属性。代码如下:

public class HttpResponse implements HttpServletResponse {
    Map<String, String> headers = new ConcurrentHashMap<>();
    @Override
    public void addHeader(String name, String value) {
        headers.put(name, value);
        if (name.toLowerCase() == DefaultHeaders.CONTENT_LENGTH_NAME) {
            setContentLength(Integer.parseInt(value));
        }
        if (name.toLowerCase() == DefaultHeaders.CONTENT_TYPE_NAME) {
            setContentType(value);
        }
    }

    @Override
    public void setHeader(String name, String value) {
        headers.put(name, value);
        if (name.toLowerCase() == DefaultHeaders.CONTENT_LENGTH_NAME) {
            setContentLength(Integer.parseInt(value));
        }
        if (name.toLowerCase() == DefaultHeaders.CONTENT_TYPE_NAME) {
            setContentType(value);
        }
    }
}

有了内部的头部信息,我们还会提供一个sendHeaders方法,按照HTTP协议的规定拼接,包含状态行、头信息和空行,将Header打印出来,如下所示:

public void sendHeaders() throws IOException {
    PrintWriter outputWriter = getWriter();
    //下面这一端是输出状态行
    outputWriter.print(this.getProtocol());
    outputWriter.print(" ");
    outputWriter.print(status);
    if (message != null) {
        outputWriter.print(" ");
        outputWriter.print(message);
    }
    outputWriter.print("\r\n");

    if (getContentType() != null) {
        outputWriter.print("Content-Type: " + getContentType() + "\r\n");
    }
    if (getContentLength() >= 0) {
        outputWriter.print("Content-Length: " + getContentLength() + "\r\n");
    }

    //输出头信息
    Iterator<String> names = headers.keySet().iterator();
    while (names.hasNext()) {
        String name = names.next();
        String value = headers.get(name);
        outputWriter.print(name);
        outputWriter.print(": ");
        outputWriter.print(value);
        outputWriter.print("\r\n");
    }

    //最后输出空行
    outputWriter.print("\r\n");
    outputWriter.flush();
}

完整的HttpResponse改造如下,由于代码太长,其中非关键的实现方法就不列在这里了。

package server;
public class HttpResponse implements HttpServletResponse {
    HttpRequest request;
    OutputStream output;
    PrintWriter writer;
    String contentType = null;
    long contentLength = -1;
    String charset = null;
    String characterEncoding = null;
    String protocol = "HTTP/1.1";
    //headers是一个保存头信息的map
    Map<String, String> headers = new ConcurrentHashMap<>();
    //默认返回OK
    String message = getStatusMessage(HttpServletResponse.SC_OK);
    int status = HttpServletResponse.SC_OK;

    public HttpResponse(OutputStream output) {
        this.output = output;
    }
    public void setRequest(HttpRequest request) {
        this.request = request;
    }
    //状态码以及消息文本,没有考虑国际化
    protected String getStatusMessage(int status) {
        switch (status) {
            case SC_OK:
                return ("OK");
            case SC_ACCEPTED:
                return ("Accepted");
            case SC_BAD_GATEWAY:
                return ("Bad Gateway");
            case SC_BAD_REQUEST:
                return ("Bad Request");
            case SC_CONTINUE:
                return ("Continue");
            case SC_FORBIDDEN:
                return ("Forbidden");
            case SC_INTERNAL_SERVER_ERROR:
                return ("Internal Server Error");
            case SC_METHOD_NOT_ALLOWED:
                return ("Method Not Allowed");
            case SC_NOT_FOUND:
                return ("Not Found");
            case SC_NOT_IMPLEMENTED:
                return ("Not Implemented");
            case SC_REQUEST_URI_TOO_LONG:
                return ("Request URI Too Long");
            case SC_SERVICE_UNAVAILABLE:
                return ("Service Unavailable");
            case SC_UNAUTHORIZED:
                return ("Unauthorized");
            default:
                return ("HTTP Response Status " + status);
        }
    }
    public void sendHeaders() throws IOException {
        PrintWriter outputWriter = getWriter();
        //下面这一端是输出状态行
        outputWriter.print(this.getProtocol());
        outputWriter.print(" ");
        outputWriter.print(status);
        if (message != null) {
            outputWriter.print(" ");
            outputWriter.print(message);
        }
        outputWriter.print("\r\n");
        if (getContentType() != null) {
            outputWriter.print("Content-Type: " + getContentType() + "\r\n");
        }
        if (getContentLength() >= 0) {
            outputWriter.print("Content-Length: " + getContentLength() + "\r\n");
        }
        //输出头信息
        Iterator<String> names = headers.keySet().iterator();
        while (names.hasNext()) {
            String name = names.next();
            String value = headers.get(name);
            outputWriter.print(name);
            outputWriter.print(": ");
            outputWriter.print(value);
            outputWriter.print("\r\n");
        }
        //最后输出空行
        outputWriter.print("\r\n");
        outputWriter.flush();
    }
    @Override
    public String getCharacterEncoding() {
        return this.characterEncoding;
    }
    @Override
    public String getContentType() {
        return this.contentType;
    }
    @Override
    public PrintWriter getWriter() throws IOException {
        writer = new PrintWriter(new OutputStreamWriter(output, getCharacterEncoding()), true);
        return writer;
    }
    @Override
    public void setCharacterEncoding(String arg0) {
        this.characterEncoding = arg0;
    }
    @Override
    public void setContentType(String arg0) {
        this.contentType = arg0;
    }
    @Override
    public void addHeader(String name, String value) {
        headers.put(name, value);
        if (name.toLowerCase() == DefaultHeaders.CONTENT_LENGTH_NAME) {
            setContentLength(Integer.parseInt(value));
        }
        if (name.toLowerCase() == DefaultHeaders.CONTENT_TYPE_NAME) {
            setContentType(value);
        }
    }
    @Override
    public String getHeader(String name) {
        return headers.get(name);
    }
    @Override
    public Collection<String> getHeaderNames() {
        return headers.keySet();
    }
    public void setHeader(String name, String value) {
        headers.put(name, value);
        if (name.toLowerCase() == DefaultHeaders.CONTENT_LENGTH_NAME) {
            setContentLength(Integer.parseInt(value));
        }
        if (name.toLowerCase() == DefaultHeaders.CONTENT_TYPE_NAME) {
            setContentType(value);
        }
    }

Processor中的改造

修改完response之后,自然地,我们在HttpProcessor中,将原来引用或初始化Response类的地方,全部用HttpResponse替代。

HttpProcessor.java
  // create Response object
  HttpResponse response = new HttpResponse(output);
  response.setRequest(request);

ServletProcessor类process代码如下:

public class ServletProcessor {
    public void process(HttpRequest request, HttpResponse response) {
        String uri = request.getUri();
        String servletName = uri.substring(uri.lastIndexOf("/") + 1);
        URLClassLoader loader = null;
        try {
            // create a URLClassLoader
            URL[] urls = new URL[1];
            URLStreamHandler streamHandler = null;
            //这个URLClassloader的工作目录设置在HttpServer.WEB_ROOT
            File classPath = new File(HttpServer.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() );
        }
        //response默认为UTF-8编码
        response.setCharacterEncoding("UTF-8");
        Class<?> servletClass = null;
        try {
            servletClass = loader.loadClass(servletName);
        }
        catch (ClassNotFoundException e) {
            System.out.println(e.toString());
        }
        //回写头信息
        try {
            response.sendHeaders();
        } catch (IOException e1) {
            e1.printStackTrace();
        }
        //创建servlet新实例,调用service()
        Servlet servlet = null;
        try {
            servlet = (Servlet) servletClass.newInstance();
            servlet.service(request, response);
        }
        catch (Exception e) {
            System.out.println(e.toString());
        }
        catch (Throwable e) {
            System.out.println(e.toString());
        }
    }
}

在此我们完成了HttpRequest与HttpResponse的替换。

Facade模式的应用

现在有一个问题需要我们解决:我们直接使用的是HttpRequest与HttpResponse,这两个对象要传入Servlet中,但在这两个类中我们也定义了许多内部的方法,一旦被用户知晓我们的实现类,则这些内部方法就暴露在用户面前了。

这样其实是不好的。面向对象编程的思想,是将内部实现的结构和复杂性包装在一层壳里面,能不对外暴露就不要对外暴露。作为客户程序,最好只知道最小知识集。另外,这个Request和Response类是要传给外部的Servlet程序的,跳出了Tomcat本身,如果这个写Servlet的程序员他知道传的这个类里面有一些额外的方法,原理上他可以进行强制转换之后调用,这样也不是很安全。

接下来我们看看如何使用门面设计模式,规避上述问题。

先解释一下Facade模式,Facade这个词来自于建筑学,字面意思是“立面”,就是我们在大街上看到的门面。门面把建筑的内部结构包装起来给人们展示了一个新的友好的有特色的外观。软件中用同样的手法,将软件的内部结构进行包装,对外用简单的API供人使用。

下面是Facade的结构图:

图片

看上图,本来一个软件中有几个类,一个类里面有一堆方法,给外面的client程序直接用会增添很多复杂性而且不安全,于是中间加了一层Facade,提供一个简单的doSomething()方法。这样对使用者来说很简单方便,也便于内部结构的调整,未来需要改动内部实现的时候,因为有这个Facade存在,所以并不需要改动对外的接口。

Facade类是一个新类,外部使用者没法根据它来强制转换获得内部的结构和方法,这样将实际实现的几个类保护起来了,提高了安全性。这正是我们现在在处理Request和Response的时候所希望看到的。我们按照这个思路写自己的Facade。

首先定义HttpRequestFacade与HttpResponseFacade,分别实现HttpServletRequest与HttpServletResponse。你可以看一下我给出的代码主体部分,大部分都是直接转到request和response里面的相应调用,完整代码就不一一展示了,你可以看最后的Gitee链接。

HttpRequestFacade.java:

package server;
public class HttpRequestFacade implements HttpServletRequest {
    private HttpServletRequest request;
    public HttpRequestFacade(HttpRequest request) {
        this.request = request;
    }
    /* implementation of the HttpServletRequest*/
    public Object getAttribute(String name) {
        return request.getAttribute(name);
    }
    public Enumeration getAttributeNames() {
        return request.getAttributeNames();
    }
    public String getCharacterEncoding() {
        return request.getCharacterEncoding();
    }
    public int getContentLength() {
        return request.getContentLength();
    }
    public String getContentType() {
        return request.getContentType();
    }
    public Cookie[] getCookies() {
        return request.getCookies();
    }
    public Enumeration getHeaderNames() {
        return request.getHeaderNames();
    }
    public String getHeader(String name) {
        return request.getHeader(name);
    }
    public Enumeration getHeaders(String name) {
        return request.getHeaders(name);
    }
    public ServletInputStream getInputStream() throws IOException {
        return request.getInputStream();
    }
    public int getIntHeader(String name) {
        return request.getIntHeader(name);
    }
    public String getMethod() {
        return request.getMethod();
    }
    public String getParameter(String name) {
        return request.getParameter(name);
    }
    public Map getParameterMap() {
        return request.getParameterMap();
    }
    public Enumeration getParameterNames() {
        return request.getParameterNames();
    }
    public String[] getParameterValues(String name) {
        return request.getParameterValues(name);
    }
    public String getQueryString() {
        return request.getQueryString();
    }
    public BufferedReader getReader() throws IOException {
        return request.getReader();
    }
    public String getRequestURI() {
        return request.getRequestURI();
    }
    public StringBuffer getRequestURL() {
        return request.getRequestURL();
    }
    public HttpSession getSession() {
        return request.getSession();
    }
    public HttpSession getSession(boolean create) {
        return request.getSession(create);
    }
    public void removeAttribute(String attribute) {
        request.removeAttribute(attribute);
    }
    public void setAttribute(String key, Object value) {
        request.setAttribute(key, value);
    }
    public void setCharacterEncoding(String encoding) throws UnsupportedEncodingException {
        request.setCharacterEncoding(encoding);
    }
}

HttpResponseFacade.java类主体定义如下:

package server;

public class HttpResponseFacade implements HttpServletResponse {
    private HttpServletResponse response;
    public HttpResponseFacade(HttpResponse response) {
        this.response = response;
    }
    public void addDateHeader(String name, long value) {
        response.addDateHeader(name, value);
    }
    public void addHeader(String name, String value) {
        response.addHeader(name, value);
    }
    public boolean containsHeader(String name) {
        return response.containsHeader(name);
    }
    public String encodeUrl(String url) {
        return response.encodeUrl(url);
    }
    public String encodeURL(String url) {
        return response.encodeURL(url);
    }
    public void flushBuffer() throws IOException {
        response.flushBuffer();
    }
    public String getCharacterEncoding() {
        return response.getCharacterEncoding();
    }
    @Override
    public String getContentType() {
        return null;
    }
    @Override
    public void setCharacterEncoding(String s) {
    }
    public void setContentLength(int length) {
        response.setContentLength(length);
    }
    public void setContentType(String type) {
        response.setContentType(type);
    }
    public void setHeader(String name, String value) {
        response.setHeader(name, value);
    }
    public ServletOutputStream getOutputStream() throws IOException {
        return response.getOutputStream();
    }
    @Override
    public PrintWriter getWriter() throws IOException {
        return response.getWriter();
    }
    @Override
    public void addCookie(Cookie arg0) {
        response.addCookie(arg0);
    }
    @Override
    public String getHeader(String arg0) {
        return response.getHeader(arg0);
    }
    @Override
    public Collection<String> getHeaderNames() {
        return response.getHeaderNames();
    }
    @Override
    public Collection<String> getHeaders(String arg0) {
        return response.getHeaders(arg0);
    }
}

在此两个Facade就定义完毕了,前面我们也说过,我们要做的是向Servlet传入参数时规避内部方法,而Facade的作用就是封装不希望暴露的方法,更深层的内部方法不予展示。因而在ServletProcessor中,Servlet调用service时我们传入HttpRequestFacade与HttpResponseFacade,你可以看一下需要调整的部分代码。

ServletProcessor.java

Servlet servlet = null;
//调用servlet的service()方法时传入的是Facade对象
try {
    servlet = (Servlet) servletClass.newInstance();
    HttpRequestFacade requestFacade = new HttpRequestFacade(request);
    HttpResponseFacade responseFacade = new HttpResponseFacade(response);
    servlet.service(requestFacade, responseFacade);
}

这样在Servlet中,我们看到的只是Facade,看不见内部方法,应用程序员想进行强制转化也不行,这样既简单又安全。

还有,按照Servlet的规范,客户自定义的Servlet是要继承HttpServlet的,在调用的service方法内,它的实际行为是通过method判断调用的是哪一个方法,如果是Get方法就调用doGet(),如果是Post方法调用的就是doPost(),其他的方法也是一样的道理。

所以在我们自定义的HttpRequest里,一定要实现getMethod方法,我们来调整一下。

package server;
public class HttpRequest implements HttpServletRequest{
    @Override
    public String getMethod() {
      return new String(this.requestLine.method, 0, this.requestLine.methodEnd);
    }
}

这样做可以简化客户程序,让业务程序员写Servlet的时候只需要重载doGet()或doPost()方法即可。

这样我们这个阶段的改造就完成了。

测试

我们这节课的实现只是内部程序结构的变动,并不影响用户的Servlet和客户端的访问。所以还是可以沿用以前的测试办法进行验证。

小结

这节课我们主要做了两件事:一是解析Response的返回信息,存储Header信息,可以让我们更好地处理服务器响应;二是采用Facade门面模式,新增HttpRequestFacade与HttpResponseFacade,封装我们不希望暴露的方法体,避免被业务程序员直接调用实现类的内部方法。

通过这两件事情,我们既符合了Servlet的规范,又让MiniTomcat本身在面对Servlet应用程序员的时候更加安全。这个Facade的设计是Tomcat中被人称赞的优点,我们自己尝试设计框架的时候可以借鉴。

本节课完整代码参见: https://gitee.com/yaleguo1/minit-learning-demo/tree/geek_chapter07

思考题

学完了这节课的内容,我们来思考一个问题:如果我们不引入Facade,那么一个Servlet程序员有可能做些什么我们不希望他们做的事情?

欢迎你把你思考后的结果分享到评论区,也欢迎你把这节课的内容分享给其他朋友,我们下节课再见!