Servlet API 架构详解

Servlet、GenericServlet、HttpServlet

Servlet

对于web容易来说,所有servlet都必须有的行为,规范在servlet接口中:

package javax.servlet;

import java.io.IOException;

public interface Servlet {
    public void init(ServletConfig config) throws ServletException;
    public ServletConfig getServletConfig();
    public void service(ServletRequest req, ServletResponse res) 
                   throws ServletException, IOException;
    public String getServletInfo();
    public void destroy();
}

当我们所实例化的一个servlet被容器载入后,容器会建立该servlet的接口的实例,根据servlet设定的一些初始化和其他信息(一般在web.xml中),创建一个servletConfig(稍后会介绍servletConfig接口)的实例,然后调用servlet接口的init方法并且传入一个servletconfig实例,完成servlet的初始化。

servlet初始化完成之后,如果这时候收到了对于某个servlet的请求,那么这时候容器就会先通过urlPattern找到这个servlet,然后调用这个servlet的service方法,传入ServletRequest、ServletResponse的实例,注意这两个接口并不是http开头的,这好似因为最初设计的时候,并没有限制servlet只能用于http协议上,但实际上大到现在servlet还是只用在了http上哈哈。

当一个servlet已经没用了,容器准备回收它之前,容器会呼叫它的destroy方法,来执行一些资源的回收与释放。

GenericServlet

GenericServlet是一个抽象类,它实现了servlet接口

package javax.servlet;

import java.io.IOException;
import java.util.Enumeration;
public abstract class GenericServlet 
    implements Servlet, ServletConfig, java.io.Serializable {
    private transient ServletConfig config;
    public GenericServlet() {}
    public void destroy() {}
    public String getInitParameter(String name) {
        return getServletConfig().getInitParameter(name);
    }
    public Enumeration getInitParameterNames() {
        return getServletConfig().getInitParameterNames();
    }      
    public ServletConfig getServletConfig() {
        return config;
    }
    public ServletContext getServletContext() {
        return getServletConfig().getServletContext();
    }
    public String getServletInfo() {
        return "";
    }
    public void init(ServletConfig config) throws ServletException {
        this.config = config;
        this.init();
    }
    public void init() throws ServletException {}
    public void log(String msg) {
        getServletContext().log(getServletName() + ": "+ msg);
    }
    public void log(String message, Throwable t) {
        getServletContext().log(getServletName() + ": " + message, t);
    }
    public abstract void service(ServletRequest req, ServletResponse res)
                             throws ServletException, IOException;
    public String getServletName() {
        return config.getServletName();
    }
}

GenericServlet实现了servlet的接口,其中有一个接受servletconfig参数的init方法,会传入servletconfig的实例,之后才会调用无参数的init方法,所以** 如果我们重新定义servlet的初始化,不建议直接重定义接受参数的init方法,而是建议重新定义无参数的init方法 **
GenericServlet對Servlet介面的service()方法沒有實作,僅標示為abstract,service()方法的實作由子類別

HttpServlet

我们看到GenericServlet对于servlet接口的service方法没有具体实现,标记为抽象方法,这是因为service方法交给了它的子类去实现,比如具体的httpservlet:

package javax.servlet.http;

略...

public abstract class HttpServlet extends GenericServlet {
    public HttpServlet() {}

    略...

    @Override
    public void service(ServletRequest req, ServletResponse res)
        throws ServletException, IOException {

        HttpServletRequest  request;
        HttpServletResponse response;
        
        try {
            request = (HttpServletRequest) req;
            response = (HttpServletResponse) res;
        } catch (ClassCastException e) {
            throw new ServletException("non-HTTP request or response");
        }
        service(request, response);
    }

    protected void service(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {

        String method = req.getMethod();

        if (method.equals(METHOD_GET)) {
            long lastModified = getLastModified(req);
            if (lastModified == -1) {
                doGet(req, resp);
            } else {
        略...          
    }
}

HttpServlet实现了javax.servlet.http接口,同时又继承实现了GenericServlet未实现的service方法。在web容器的环境下,实际上传入的请求和响应对象的是HttpServletRequest、HttpServletResponse接口。他们其实都是ServletRequest、ServletResponse的子接口,增加了一些关于http的操作。

Servlet API 架构详解_第1张图片
image.png

ServletConfig

在web容易启动后,会读取(Annotation)或web.xml里面的设定,根据其中对每个servlet的设定将servlet载入并且实例化,并未每个servlet的配置产生一个servletconfig的对象,随后调用servlet的接口的init方法,并且将产生的servletconfig的对象当作参数传入。

servletconfig其实就是将(Annotation)或web.xml中为servlet配置的信息整合保存到一个对象中,容器会为每个servlet产生一个servlet对象和一个servletconfig对象。genericservlet同时实现了这两个接口,servlet和servletconfig。

GenericServlet主要的目的,就是将servlet调用init方法传入的config对象封装起来:

private transient ServletConfig config;
public void init(ServletConfig config) throws ServletException {
    this.config = config;
    this.init();
}
public void init() throws ServletException {
}

GenericServlet在實作Servlet的init()方法時,也呼叫了另一個無參數的init()方法,基本上你在撰寫Servlet時,如果有一些初始時所要執行的動作,可以重新定義這個無參數的init()方法,而不是直接重新定義有ServletConfig參數的init()方法。

对于一般的对象,我们如果想在初始化之后,执行一些要执行的动作,我们一般需要重新定义构造函数,在构造函数中完成后续的动作即可。但在写servlet的时候,却不一样,如果我们想要执行一些与web资源相关的初始化操作时,我们要重新定义init方法。举个例子,如果我们想要使用servletconfig来做一些事情,我们不能在构造函数中定义,因为我们实例化servlet的时候,容器还没有执行init方法传入servletconfig对象,所以我们这时候还没有servletconfig的实例

GenericServlet也包括了Servlet與ServletConfig所定義方法的簡單實作,實作內容主要是透過ServletConfig來取得一些相關資訊,例如:

public ServletConfig getServletConfig() {
    return config;
}
public String getInitParameter(String name) {
    return getServletConfig().getInitParameter(name);
}
public Enumeration getInitParameterNames() {
    return getServletConfig().getInitParameterNames();
}   
public ServletContext getServletContext() {
    return getServletConfig().getServletContext();
}

所以当我们继承httpservlet实现一个servlet的时候,就可以直接利用这些方法取得相关的servletconfig的信息,而不用意识到servletconfig的存在。
若要使用標註設定個別Servlet的初始參數,可以在@WebServlet中使用@WebInitParam設定initParams。例如:

...
@WebServlet(name="ServletConfigDemo", urlPatterns={"/conf"},
            initParams={
                @WebInitParam(name = "PARAM1", value = "VALUE1"),
                @WebInitParam(name = "PARAM2", value = "VALUE2")
            }
)
public class ServletConfigDemo extends HttpServlet {
    private String PARAM1;
    private String PARAM2;
    
    public void init() throws ServletException {
        super.init();
        PARAM1 = getServletConfig().getInitParameter("PARAM1");
        PARAM2 = getServletConfig().getInitParameter("PARAM2");
    }
    ....
}

若要在web.xml中設定個別Servlet的初始參數,可以在標籤之中,使用進行設定,web.xml中的設定會覆蓋標註的設定。例如:

...

    ServletConfigDemo
    cc.openhome.ServletConfigDemo
    
        PARAM1
        VALUE1
    
    
        PARAM2
        VALUE2
    

...

由於ServletConfig必須在Web容器將Servlet實例化後,呼叫有參數的init()方法再將之傳入,是與Web應用程式資源相關的物件,所以在繼承HttpServlet後,通常會重新定義無參數的init()方法以進行Servlet初始參數的取得。GenericServlet定義了一些方法,將ServletConfig封裝起來,便於取得設定資訊,所以取得Servlet初始參數的程式碼也可以改寫為:

...
@WebServlet(name="ServletConfigDemo", urlPatterns={"/conf"},
            initParams={
                @WebInitParam(name = "PARAM1", value = "VALUE1"),
                @WebInitParam(name = "PARAM2", value = "VALUE2")
            }
)
public class AddMessage extends HttpServlet {
    private String PARAM1;
    private String PARAM2;
    public void init() throws ServletException {
        super.init();
        PARAM1 = getInitParameter("PARAM1");
        PARAM2 = getInitParameter("PARAM2");
    }
    ….
}

下面這個範例簡單地示範如何設定、使用Servlet初始參數,其中登入成功與失敗的網頁,可以由初始參數設定來決定:

import java.io.*;
import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;

@WebServlet(name = "LoginServlet", urlPatterns = {"/login.do"},
            initParams = {
                @WebInitParam(name = "SUCCESS", value = "success.view"),
                @WebInitParam(name = "ERROR", value = "error.view")
            }
)
public class LoginServlet extends HttpServlet {
    private String SUCCESS_VIEW;
    private String ERROR_VIEW;

    @Override
    public void init() throws ServletException {
        super.init();
        SUCCESS_VIEW = this.getInitParameter("SUCCESS");
        ERROR_VIEW = this.getInitParameter("ERROR");
    }

    protected void doPost(HttpServletRequest request,
                          HttpServletResponse response)
                            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");
        String name = request.getParameter("name");
        String passwd = request.getParameter("passwd");
        if ("caterpillar".equals(name) && "123456".equals(passwd)) {
            request.getRequestDispatcher(SUCCESS_VIEW)
                    .forward(request, response);
        } else {
            request.getRequestDispatcher(ERROR_VIEW)
                    .forward(request, response);
        }
    }
}

ServletContext

ServletContext接口定义了Servlet所运行程序的一个总体的行为与信息,你可以使用ServletContext实例对象來取得所请求资源的URL、应用程序的初始化參數,甚至动态的设定Servlet实例。

ServletContext本身的名字让人产生误解,因为它以Servlet作为开头,容易被误认为只是一个单独的Servlet的代表对象。但实际上,当整个Web应用程序加载进入Web容器之后,容器会生成一个ServletContext对象作为整个应用程序的代表,并设定给ServletConfig,你只要通过调用ServletConfig的getServletContext()方法就可以取得ServletContext对象。
以下我们先介绍几个容易搞错,需要注意的方法:

  • getRequestDispatcher()方法可以取得RequestDispatcher实例对象,使用时路徑的指定必须以"/"作为开头,这个斜杠代表应用程序的根目录(Context Root)。
context.getRequestDispatcher("/pages/some.jsp").forward(request, response);

以"/"作为开头有时候也可以理解为程序环境的初始化路径(Context-relative),沒有以"/"作为开头的(Request-relative)路径则成为(Request-relative)请求相对路径。事实上,HttpServletRequest 的getRequestDispatcher()方法在实例化的时候,若是环境相对路径,就直接交给ServletContext的 getRequestDispatcher();若是请求相对路径,则会先转换为环境相对路径再转发给ServletContext的 getRequestDispatcher()來取得RequestDispatcher。

  • 如果想要知道Web程序中的某个目录中有哪些档案,则可以使用getResourcePaths()方法。例如:
    for(String avatar : getServletContext().getResourcePaths("/")) {
    // 顯示 avatar 文字...
    }

使用時指定路徑必須以"/"作為開頭,表示相對於應用程式環境根目錄,傳回的路徑會像是:

/welcome.html
/catalog/
/catalog/index.html
/catalog/products.html
/customer/
/customer/login.jsp
/WEB-INF/
/WEB-INF/web.xml
/WEB-INF/classes/com.acme.OrderServlet.class

可以看到,这个方法会将WEB-INF的目录信息文件信息也列出来。如果是目录信息,会以“/”作为结尾。

如果想在Web程序中读取程序目录中某个文档的内容,则可以使用getResourceAsStream()方法,使用时指定路径以"/"作为开头,表示相对于应用程序的环境根目录,或者相对是/WEB-INF/lib中JAR档案里META-INF/resources的路徑(Web程序中,JAR檔案必須放在/WEB-INF/lib中,這是規定),执行结果会返回一个InputStream对象,接下来我们就可以用这个对象来读取文档的内容。底下是個讀取PDF並傳送給客戶端的範例:

import java.io.*;
import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;

@WebServlet(name="Ebook", urlPatterns={"/ebook.view"},
            initParams={
                @WebInitParam(name="PDF_FILE", value="/WEB-INF/jdbc.pdf")
            }
)
public class Ebook extends HttpServlet {
    private String PDF_FILE;

    @Override
    public void init() throws ServletException {
        super.init();
        PDF_FILE = getInitParameter("PDF_FILE");
    }
    
    protected void doGet(HttpServletRequest request,
                          HttpServletResponse response)
                      throws ServletException, IOException {
        String passwd = request.getParameter("passwd");
        if("123456".equals(passwd)) {
            response.setContentType("application/pdf");
            InputStream in = this.getServletContext()
                                 .getResourceAsStream(PDF_FILE);
            OutputStream out = response.getOutputStream();
            byte[] buffer = new byte[1024];
            int length = -1;
            while((length = in.read(buffer)) != -1) {
                out.write(buffer, 0, length);
            }
            in.close();
            out.close();
        }
    }
}

每个Web程序都会有一个相对应的ServletContext,针对「应用程序」初始化时所需用到的一些参数的信息,你可以在web.xml中摄制应用程序的初始化参数,設定時使用标签来定义。例如:


    
       MESSAGE
       /WEB-INF/messages.txt
    

在程序中可以通过调用ServletContext的getInitParameter()方法來读取初始化参数。因此Web應用程式初始參數常被稱為ServletContext初始參數。

在整個Web程序的生命周期中,Servlet所需共用的信息参数,則可以设定为ServletContext属性。由于ServletContext在Web应用程序存活的时候会一直存在,所以設定為ServletContext属性的信息,除非你主动移除,否则也是一直存在web程序中的。

可以透過ServletContext的setAttribute()方法設定物件為ServletContext屬性,之後可透過ServletContext的getAttribute()方法取出該屬性。若要移除屬性,則透過ServletContext的removeAttribute()方法。

注意!ServletContext不是线程安全的,所以必要时我们还需要注意存取变量属性时的线程安全问题。

你可能感兴趣的:(Servlet API 架构详解)