# Servlet详解(一)

### 什么是Servlet

Servlet（Server Applet），全称Java Servlet。一个Java Servlet就是一个小型Java应用程序，它可以继承HttpServlet实现，运行在Web服务器中。Servlet 会接收并响应来自浏览器的请求，通常是基于Http协议的请求。

如下图：

![](https://2351062869-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F7b2CdwBN9liniVJpfEAc%2Fuploads%2Fgit-blob-a68e1777aaa37d6d79855ec9b859ee4ca069afbc%2FaHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTYxMjA0MjAzMTUwNDU0.png?alt=media)

### 创建Servlet

实现Servlet或者继承Servlet的实现类：

#### 方法一：实现Servlet接口

1> 实现接口Servlet并实现以下几个 **生命周期** 方法：

* `init(ServletConfig config)`：当Servlet被创建时，将会使用此方法对自己初始化
* `service(ServletRequest req,ServletResponse res)`：当浏览器对服务器发出请求后，servlet会使用该方法处理请求
* `destory()`：当Servlet处理完请求后，在销毁前会调用此方法，然后GC会将它回收掉

**生命周期：**

当client访问Servlet时，创建并初始化Servlet， **在服务器中只有一个Servlet实例**，当服务器被关闭时，Servlet才会被销毁。

```java
package com.feathers.servletdemo;
import java.io.IOException;

import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class HelloServlet implements Servlet {

    private ServletConfig config;

    @Override
    public void init(ServletConfig config) throws ServletException {
        System.out.println("init()： servlet 初始化");
        this.config = config;
    }

    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        System.out.println("service()：servlet 收到请求了，开始取出请求的数据(request)并作出处理，\n 并将处理后的结果放置到响应对象(response)中");
        // 响应对象向客户端写东西
        response.getWriter().write("Hello Client, I'm Servlet");
    }

    @Override
    public void destroy() {
        System.out.println("destory(): servlet被销毁");
    }

    @Override // 获取配置信息，键值对的方式
    public ServletConfig getServletConfig() {
        return config;
    }

    @Override  // 返回servlet信息，比如servlet的author version copyright等
    public String getServletInfo() {
        return null;
        // return "Feather著作 Feathers 版权所有";
    }
}

```

2> 在 `web.xml` 中注册Servlet，告诉服务器有这么一个Servlet

```xml
<!-- 注册servlet到服务器 -->
  <servlet>
    <servlet-name>HelloServlet</servlet-name><!-- 注册名称 -->
    <servlet-class>com.feathers.servletdemo.HelloServlet</servlet-class><!-- java字节码路径 -->
  </servlet>
  <!-- 给注册过的Servlet配置路径 -->
  <servlet-mapping>
    <servlet-name>HelloServlet</servlet-name><!-- 给哪一个Servlet配置路径 -->
    <url-pattern>/HelloServlet</url-pattern><!-- 要配置的路径，servle访问路径  / 代表项目路径 -->
  </servlet-mapping>
```

什么？太麻烦了？还好我们有注解 `@WebServlet`

**使用java注解注册Servlet**

```java
@WebServlet("/FirstServlet") // 后面是Servlet的访问路径

@WebServlet(
    name="Hello",   // Servlet名称
    urlPatterns={"/hello.view"}, // 访问路径
    loadOnStartup=1 // 加载级别
)
public class HelloServlet extends HttpServlet {
```

启动服务器，访问此Servlet：

![](https://2351062869-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F7b2CdwBN9liniVJpfEAc%2Fuploads%2Fgit-blob-a295b9a0cc07669d9c2fdb12d84a3ed04c6c3519%2F%E8%AE%BF%E9%97%AEservlet.png?alt=media)

在tomcat中的项目结构：

![](https://2351062869-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F7b2CdwBN9liniVJpfEAc%2Fuploads%2Fgit-blob-93da2cfd49bf02a824755f798d64729951a3095d%2Ftomcat%E4%B8%AD%E7%9A%84%E9%A1%B9%E7%9B%AE%E7%BB%93%E6%9E%84.png?alt=media)

我们可以看到，Java编译的class文件全部被eclipse放置到WEB-INF—> classes目录下了。

输出：

![](https://2351062869-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F7b2CdwBN9liniVJpfEAc%2Fuploads%2Fgit-blob-1814bade56bfe0d5f975b2437d911b5918579020%2FaHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTYxMjA0MjEwMjU5NTQ3.png?alt=media)

生命周期时序图：

![](https://2351062869-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F7b2CdwBN9liniVJpfEAc%2Fuploads%2Fgit-blob-ba13e968625070213a76868ed105893bd467390c%2Fservlet%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F.png?alt=media)

服务器是如何查找Servlet的？

![](https://2351062869-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F7b2CdwBN9liniVJpfEAc%2Fuploads%2Fgit-blob-30eb83e574a77ca3e5ef59141a6615756636e797%2FaHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTYxMjA0MjE0MTA2NTAy.png?alt=media)

我们看到，通过 `<url-pattern>` 访问servlet，最后访问到他的class文件。

首先根据 `<url-pattern>` 找到它的name，然后根据name找到 `<servlet> <name>` 相同的servlet标签，根据name找到 `<servlet-class>` class路径，使用找到的class生成servlet实例到服务器内存中。

**ServletConfig接口：**

servlet容器使用servlet配置对象，该对象在初始化期间将信息传递给ServletConfig。

* `String getInitParamter(String name)`:根据初始化参数名称获取初始化参数的值，如果没有此参数，返回null
* `Enumeration getInitParamterNames()`：获取所有初始化参数名称，以枚举方式返回，如果没有参数，返回一个空的枚举

在 `web.xml` 中初始化参数：

```xml
  <servlet>
    <servlet-name>HelloServlet</servlet-name>
    <servlet-class>com.feathers.servletdemo.HelloServlet</servlet-class>
    <!-- 配置两个初始化参数 -->
    <init-param>
        <param-name>name</param-name><!-- key -->
        <param-value>Feathers</param-value><!-- value -->
    </init-param>
    <init-param>
        <param-name>age</param-name>
        <param-value>100</param-value>
    </init-param>
  </servlet>
  <servlet-mapping>
    <servlet-name>HelloServlet</servlet-name>
    <url-pattern>/HelloServlet</url-pattern>
  </servlet-mapping>
```

在Servlet service()中对初始化参数进行处理：

```java
package com.feathers.servletdemo;
import java.io.IOException;
import java.util.Enumeration;

import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class HelloServlet implements Servlet {

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

    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        // 响应对象向客户端写东西
        response.setContentType("text/html;charset=utf-8");
        response.getWriter().write("Hello Client, I'm Servlet");
        // 获取初始化参数 作出处理
        Enumeration<String> enums = getServletConfig().getInitParameterNames();
        while (enums.hasMoreElements()){
            String key = enums.nextElement();
            String value = getServletConfig().getInitParameter(key);
            response.getWriter().write(key+":"+value + "<br/>");
        }
    }

    @Override
    public void destroy() {
    }

    @Override
    public ServletConfig getServletConfig() {
        return config;
    }

    @Override
    public String getServletInfo() {
        return null;
    }

}
```

请求结果如下：

![](https://2351062869-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F7b2CdwBN9liniVJpfEAc%2Fuploads%2Fgit-blob-89356466aa7baeeeed0ad64ad01fd57e4668d4c1%2FaHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTYxMjA0MjM0MzQxNjAy.png?alt=media)

* `getServletName()`：获取Servlet的名称 `<servlet-name>`
* `getServletContext()`:获取ServletContext上下文对象

**点击form表单提交，跳转servlet**

```html
<!-- 注意action， 这个是Servlet的访问路径，但是不能加/ 不能加/ 不能加/  -->
<form action="FirstServlet" method="get">
        <table>
            <tr>
                <td>用户名：</td>
                <td><input type="text" name="username" value=""/></td>
            </tr>
            <tr>
                <td>密码：</td>
                <td><input type="password" name="password" value=""/></td>
            </tr>
            <tr>
                <td><input type="checkbox" name="remember" value=“yes”/>记住用户名</td>
            </tr>
            <tr>
                <td><input type="submit" value="提交"/></td>
            </tr>
        </table>
    </form>
```

#### 方法二、继承GenericServlet

所谓的GenericServlet只是一个Servlet的实现类，因为用户每次使用Servlet接口自定义一个Servlet都会进行许多不必要的操作，所以就封装了一个GenericServlet以方便程序员使用，

使用方法：在方法service()中进行请求的处理，具体请查看源码。

下面是它的源码:

```java
    package javax.servlet;

    import java.io.IOException;
    import java.util.Enumeration;

    //GenericServlet实现了Servlet、ServletConfig、Serializable三个接口
    public abstract class GenericServlet
        implements Servlet, ServletConfig, java.io.Serializable
    {
        private transient ServletConfig config;

        //无参的构造方法
        public GenericServlet() { }

        /*
        实现接口Servlet接口生命周期初始化init(ServletConfig Config)方法，将ServletConfig对象保存到成员变量中，以扩展他的生命周期，让service方法也能访问
        **/

        public void init(ServletConfig config) throws ServletException {
          this.config = config;
          this.init(); // 用户的初始化方法
        }

        public void init() throws ServletException {
        }

        public void destroy() { }

        //返回ServletConfig对象
        public ServletConfig getServletConfig(){
           return config;
        }

        //该方法实现接口<Servlet>中的ServletInfo，默认返回空字符串
        public String getServletInfo() {
           return "";
        }

       // service方法没有去实现，而是丢给使用者去实现
        public abstract void service(ServletRequest req, ServletResponse res)
     throws ServletException, IOException;

      // 为了方便用户调用，简化操作，实现了ServletConfig接口，这样就不用使用getServletConfig().getServletContext()获取对象了
      // 只需使用getServletContext即可
      // 即实现ServletConfig的目的
        public ServletContext getServletContext() {
           return getServletConfig().getServletContext();
        }

        public String getInitParameter(String name) {
         return getServletConfig().getInitParameter(name);
        }

        public Enumeration getInitParameterNames() {
           return getServletConfig().getInitParameterNames();

        public String getServletName() {
            return config.getServletName();
        }

        public void log(String msg) {
           getServletContext().log(getServletName() + ": "+ msg);
        }

        public void log(String message, Throwable t) {
           getServletContext().log(getServletName() + ": " + message, t);
        }
    }
```

#### 方法三、继承HttpServlet

HttpServlet继承自GenericServlet，是针对Http协议的Servlet，是进一步的封装，

使用只需要继承HttpServlet，重写doGet或doPost方法即可，doGet用来处理get请求，而doPost方法用来进行post请求处理。

源码如下：

```java
    package javax.servlet.http;
    .....

    public abstract class HttpServlet extends GenericServlet
        implements java.io.Serializable
    {

        private static final String METHOD_GET = "GET";
        private static final String METHOD_POST = "POST";
        ......
        public HttpServlet() { }

        public void service(ServletRequest req, ServletResponse res)
            throws ServletException, IOException
        {
        // 1. 强转ServletRequest与ServletResponse
            HttpServletRequest        request;
            HttpServletResponse        response;

            try {
                request = (HttpServletRequest) req;
                response = (HttpServletResponse) res;
            } catch (ClassCastException e) {
                throw new ServletException("non-HTTP request or response");
            }
            //调用重载的service()方法
            service(request, response);
        }

        //重载的service方法
        protected void service(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException
        {
            // 根据操作的不同，作出不同的处理
            // 处理方法有 doGet,doPost,doHead,doDelete...
            // 这些都是http协议中的方法,get请求，post修改，delete删除，put上传等。

            // 获取请求方式
            String method = req.getMethod();

            if (method.equals(METHOD_GET)) {  // get请求
                long lastModified = getLastModified(req);
                if (lastModified == -1) {
                    doGet(req, resp);
                } else {
                    long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
                    if (ifModifiedSince < (lastModified / 1000 * 1000)){
                        maybeSetLastModified(resp, lastModified);
                        doGet(req, resp);
                    } else {
                        resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                    }
                }

            } else if (method.equals(METHOD_HEAD)) {
                long lastModified = getLastModified(req);
                maybeSetLastModified(resp, lastModified);
                doHead(req, resp);

            } else if (method.equals(METHOD_POST)) {   // post请求
                doPost(req, resp);

            } else if (method.equals(METHOD_PUT)) {  // put 增加操作
                doPut(req, resp);

            } else if (method.equals(METHOD_DELETE)) {   // 删除操作
                doDelete(req, resp);

            } else if (method.equals(METHOD_OPTIONS)) {
                doOptions(req,resp);

            } else if (method.equals(METHOD_TRACE)) {
                doTrace(req,resp);

            } else {
                String errMsg = lStrings.getString("http.method_not_implemented");
                Object[] errArgs = new Object[1];
                errArgs[0] = method;
                errMsg = MessageFormat.format(errMsg, errArgs);

                resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
            }
        }

        ......

protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException
        {
            String protocol = req.getProtocol();
            String msg = lStrings.getString("http.method_get_not_supported");
            if (protocol.endsWith("1.1")) {
                resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
            } else {
                resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
            }
        }

        protected void doHead(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException
        {
            .......
        }
        protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException
        {
            String protocol = req.getProtocol();
            String msg = lStrings.getString("http.method_post_not_supported");
            if (protocol.endsWith("1.1")) {
                resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
            } else {
                resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
            }
        }
        protected void doPut(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException  {
            //todo
        }

        protected void doOptions(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
           //todo
        }

        protected void doTrace(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException   {
          //todo
        }

        protected void doDelete(HttpServletRequest req,
                                HttpServletResponse resp)
            throws ServletException, IOException   {
            //todo
        }

    }
```

### Servlet线程安全问题：

Servlet存在这线程安全的问题，

假设在Servlet中放置一个成员变量name，用来接受用户的表单信息。当多个用户访问Servlet时，会对name进行多次赋值，那么用户得到的结果就很有可能出错，因为多个用户共用一块内存。

解决办法也很简单，让每个用户都有一块内存保存自己的name值，只需要将name值保存在方法中，建立一个局部变量即可。因为java方法在被调用时，每次被调用，都会开辟一个新的方法区。

### 修改Servlet创建时机

通常情况下，servlet在被用户第一次调用时创建，如果servlet要进行非常耗时的创建操作，用户就会等待很久，影响用户体验，所以，我们可以将servlet设置在服务器启动之后，解决这类问题：

在 `web.xml` 中,添加加载时间标签：

```xml
<servlet>
    <servlet-name>HelloServlet</servlet-name>
    <servlet-class>com.feathers.servletdemo.HelloServlet</servlet-class>
    <!-- 让servlet随项目的启动而启动 value： 0-5  数字越小，启动优先级越高       如果多个servlet优先级相同，则按照配置顺序启动-->
    <load-on-startup>3</load-on-startup>
  </servlet>
```

### Servlet访问路径配置

相对路径：

`/ 代表 项目路径`

`/AServlet => http://localhost:8080/ServletDemo/Aservlet`

`/File/* => /File/ => http://localhost:8080/ServletDemo/File/sfsdfdsfsdfe` 匹配任意

匹配范围越大，优先级越低：

两个配置 `/File/AServlet` 与 `/File/*`

访问： `http://localhost:8080/ServletDemo/File/AServlet`

则会访问AServlet，而不会访问第二个路径，因为第一个优先级高。

后缀名匹配：

`*.do`： `http://localhost:8080/ServletDemo/sfdsfds.do`

该方式不常用，Filter过滤器常用

注意：不能同时使用 **后缀名** 匹配和 **相对路径**。例如： `/File/*.do`

### ServletContext

项目级别的对象，一个Web项目，有且只有一个ServletContext对象，在项目启动时创建，到项目关闭时销毁，可以理解为这个类集合了项目所有的功能方法，代表了项目，所以这个类非常强大。

我们使用 `ServletConfig.getServletContext()` 获得这个对象。

功能如下：

#### 获取项目参数

配置项目初始化参数：

```xml
<webapp>
  <context-param>
    <param-name>name</param-name>
    <param-value>tom</param-value>
  </context-param>
</webapp>
```

获取项目初始化参数：

`getServletContext().getInitParamter(String name)`

`getServletContext().getInitParamterNames()`

#### ServletContext 域

域是服务器两个组件之间的通讯，比如两个Servlet之间通讯。

**Application域**

在ServletContext中，有一个 `Map<String,Object>` 集合，用来保存信息，这个Map集合就是ServletContext的域。

AServlet获取ServletContext对象，向域中添加信息，BServlet也可以获取Servlet，然后从ServletContext域中获取AServlet填入的信息。

这样，每个Servlet之间就可以共享信息了。

同样存在线程不安全的问题。

```java
/*AServlet*/
// 1. 获取ServletContext
ServletContex scontext = getServletContext();
// 2. 向域中存放键值对
scontext.setAttribute(key,object);

/*BServlet*/
// 3. 在另一个Servlet中从域中取出键值对
getServletContext().getAttribute(key);
// 4. 删除key
getServletContext().removeAttribute(key);
// 5. 获取所有建
getServletContext().getAttributeNames();//返回枚举
```

#### ServletContext获取项目内的资源

获取WebRoot/WebContent下的 `user.xml` 文件：

```java
InputStream is = getServletContext().getResourceAsStream("/user.xml"); // 获得资源流
String path = getServletContext().getRealPath("/user.xml");// 获取资源绝对路径（tomcat webapp下 项目的部署路径）

// 获取WEB-INFO下的`user.xml`文件
getServletContext().getResourceAsStream("/WEB-INFO/user.xml");
```

获取class资源：

```java
// 方法1：使用上面的方法
getServletContext().getResourceAsStream("/WEB-INFO/classes/com/feathers/servlet/AServlet.class"); // 麻烦

// 方法2：
// “/” 代表src目录，或者是classes目录,详见getResourceAsStream方法
InputStrean is2 = AServlet.class.getResourceAsStream("/user.xml");
```
