Servlet详解

0 概述

本文由web服务说起,简单的介绍了Servlet、Servlet容器,以及它们的实现,主要通过以下方面引入或介绍:

  • web概述;
  • 什么是http协议;
  • 使用Java Socket套接字实现一次简单的http请求及响应流程,引入Servlet;
  • Servlet规范及Servlet容器;
  • Tomcat如何实现Servlet容器的功能;
  • Spring MVC如何简化Servlet的开发;
  • Spring Boot中Servlet容器的集成及Servlet的自动化配置。

文中所所涉及的内容都是一些基本概念及简单的实现原理,学习本文将对Servlet相关的知识有个全局的、系统的认识,至于一些底层的实现细节,会在后面的学习中讨论

1 从WEB说起

WEB是一种分布式的应用架构,旨在共享分布式网络上的各个web服务器中的所有互相链接的信息。
这里写图片描述

上图展示了一般web运作示意图,总的来说其具有以下3个特征
* 用超级文本技术HTML来表达信息;
* 用统一的资源定位技术URL来实现网络上信息的精准定位;
* 用网络应用层协议HTTP来规范浏览器与web服务器之间的通信过程;

2 HTTP协议

2.1 概述

  • 超文本传输协议(Hypertext Transfer Protocol )
  • 规定了客户端(主要是浏览器)与web服务器之间的通信细节;
  • 建立在TCP/IP协议的基础上,属于应用层协议,默认端口80;
  • 客户端主动发出http请求,服务器接收http请求再返回相应的结果;

2.2 http请求格式

GET /user.html HTTP/1.1 //请求方法 URI 协议/协议
Host: localhost:8888
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: Hm_lvt_6abc8f820c003b71587d5619f3080cc1=1522295652; Hm_lvt_9a714a924f22b76dc92e5ae4bcbf6486=1522295652; CNZZDATA1253407868=174549865-1510110179-%7C1522295031; JSESSIONID=A7F3A2BC6646E52F3B5735C8D1DBB890

//这里是请求体数据

2.3 http响应报文

HTTP/1.1 200 OK // 协议/版本 响应状态码 状态码说明
Date: Sat, 31 Dec 2005 23:59:59 GMT
Content-Type: text/html;charset=ISO-8859-1 //响应体的媒体类型;字符集
Content-Length: 122

<html>
<head>
<title>Wrox Homepage</title>
</head>
<body>
<!-- body goes here -->
</body>
</html>

2.4 http请求的工作流程

假设用户在浏览器输入http://www.owl.org/user.html的URL,整个web的工作流程如下
1. 浏览器与域名为www.owl.org的服务器建立tcp连接;
2. 浏览器发出访问URI:/user.html的HTTP请求;
3. 服务器收到http请求,并解析,然后发回包含user.html的HTTP响应;
4. 浏览器接收到该响应,并解析响应结果,将user.html表达后展示给用户;
5. 浏览器与web服务器之间TCP连接关闭。

3 java socket 套接字

下面将使用java socket套接字实现上述流程

  • 创建HttpServer类,监听8888端口,实现了请求的解析和响应
package com.owl.myserver;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class HttpServer {
  public static void main(String[] args) {
    try {
      //监听8888端口
      ServerSocket serverSocket = new ServerSocket(8888);
      System.out.println("正在监听端口" + serverSocket.getLocalPort());
      //等待客户TCP连接请求
      while (true) {
        final Socket socket = serverSocket.accept();
        System.out.println("与" + socket.getInetAddress() + "建立连接");
        service(socket);
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
  //解析请求信息
  private static void service(Socket socket) throws IOException, InterruptedException {
    final InputStream is = socket.getInputStream();
    Thread.sleep(500);
    final int size = is.available();
    byte[] buffer = new byte[size];
    //获取请求数据
    is.read(buffer);
    String request = new String(buffer, "UTF-8");
    System.out.println("收到请求数据:"+request);
    //解析请求第一行
    String lineOne = request.substring(0, request.indexOf("\r\n"));
    final String[] str = lineOne.split(" ");
    //读取到uri /user.html
    String uri = str[1];
    //处理业务并响应结果
    response(socket, uri);
  }

  /**
   * 响应请求
   */
  private static void response(Socket socket, String uri) throws IOException, InterruptedException {
    String contentType;//简化,只模拟两种情况
    if (uri.contains(".html")) {
      contentType = "text/html";
    } else {
      contentType = "application/octed-stream";//字节流
    }
    //准备响应头信息
    String responseHeader = "HTTP/1.1 200 OK\r\nContent-Type:" + contentType + "\r\n\r\n";
    //读取静态页面user.html
    final InputStream responseBody = HttpServer.class.getClassLoader().getResourceAsStream("test" + uri);
    final OutputStream socketOut = socket.getOutputStream();
    //写入响应头数据
    socketOut.write(responseHeader.getBytes("UTF-8"));
    int len;
    byte[] buffer = new byte[1024];
    //将html页面写入响应体中
    while ((len = responseBody.read(buffer)) != -1) {
      socketOut.write(buffer, 0, len);
    }
    Thread.sleep(1000);
    System.out.println("响应成功");
    socket.close();
  }
}
  • 启动main方法,控制台输出:
Connected to the target VM, address: '127.0.0.1:62581', transport: 'socket'
正在监听端口8888
与/0:0:0:0:0:0:0:1建立连接
收到请求数据:GET /user.html HTTP/1.1
Host: localhost:8888
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: Hm_lvt_6abc8f820c003b71587d5619f3080cc1=1522295652; Hm_lvt_9a714a924f22b76dc92e5ae4bcbf6486=1522295652; CNZZDATA1253407868=174549865-1510110179-%7C1522295031; JSESSIONID=A7F3A2BC6646E52F3B5735C8D1DBB890

响应成功
  • 打开浏览器调试工具可以看到响应结构
HTTP/1.1 200 OK
Content-Type:text/html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta content="text/html;charset=UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>owl</h1>
</body>
</html>

至此,我们已经简单的实现了一次http请求流程,这里HttpServer其实就是一个简单的web服务器;但在实际业务中,一个请求通常不只是获取一个静态文件,往往伴随着复杂的业务操作,例如对数据库的增删改查,或者根据客户端的请求动态的响应不同的结果,因此在实际的业务中需要一些增强的方法来满足各种各样的业务需求,servlet应运而生,旨在实现一个请求所要执行的具体业务。根据前面的讨论,我们大致可以对servlet有一个感性的认识:

  • 根据客户的uri能够找到并调用具体的servlet业务方法;
  • 在servlet实现中需要获取的request信息和response信息,用于服务器读取相应的请求数据以及组织响应结果;
  • 除此之外,servlet还应该能够读取整个web服务的上下文信息,以及服务的初始化配置数据,从而在不同的请求中能够初始化一些参数或者共享某些数据。

接下来将对servlet以及servlet容器作详细介绍。

4 servlet 与servlet容器

经过前面的讨论,我们已经知道servlet主要作用,即拓展web服务器的功能。servlet原意就是一种运行在服务器上的小插件,既然是插件,也就是说服务器并不依赖servlet,servlet应该具有安全、可移植、即插即用的特点,实际上服务器应该可以找到其他的插件来替代servlet(例如普通的CGI)。

4.1 使用Socket简单的模拟Servlet

为了更清楚的说明servlet的作用,我们来举个例子,假如第3节中的请求不是获取静态页面,而是根据请求参数来动态展示用户信息,请求的url为http://localhost:8888/user?name=owl&age=18,我们希望得到下面的响应页面,并且页面中的值根据请求参数动态改变:

姓名:owl
年龄:18

这时候我们的服务端就需要稍作改动,为了使代码更具有通用性,我们定义几个实体类,MyHttpRequest用来封装请求数据,MyHttpResponse用来封装响应信息,UserServlet用来动态响应上述请求,我们对第3节的代码进行改造,代码如下:
* HttpServer类

public class HttpServer {
    public static void main(String[] args) {
        try {
            //监听8888端口
            ServerSocket serverSocket = new ServerSocket(8888);
            System.out.println("正在监听端口" + serverSocket.getLocalPort());
            //等待客户TCP连接请求
            while (true) {
                Socket socket = null;
                try {
                    socket = serverSocket.accept();
                    System.out.println("与" + socket.getInetAddress() + "建立连接");
                    //初始化请求数据
                    MyHttpRequest request = new MyHttpRequest(socket.getInputStream());
                    //初始化响应数据
                    MyHttpResponse response = new MyHttpResponse(socket.getOutputStream());
                    //调用UserServlet来处理具体业务
                    UserServlet userServlet = new UserServlet();
                    userServlet.doService(request, response);
                } catch (IOException e) {
                    e.printStackTrace();
                }finally {
                    assert socket != null;
                    socket.close();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
  • MyHttpRequest类
public class MyHttpRequest {
    //请求参数
    private Map<String, String> requestParams;
    //构造器需要传入请求的输入流以解析请求参数等数据
    public MyHttpRequest(InputStream inputStream) {
        try {
            Thread.sleep(500);
            initParams(inputStream);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 解析请求参数,这里只考虑get请求的情况,即请求参数在URI中
     */
    private void initParams(InputStream inputStream) throws IOException {
        this.requestParams = new HashMap<>();
        final int size = inputStream.available();
        byte[] buffer = new byte[size];
        //读取请求数据
        inputStream.read(buffer);
        String request = new String(buffer, "UTF-8");
        System.out.println("收到请求数据:\n" + request);
        //解析请求
        String lineOne = request.substring(0, request.indexOf("\r\n"));
        //获取第一行数据
        final String[] str = lineOne.split(" ");
        String uri = str[1];
        //user?name=owl&age=18
        String[] paramsStr = uri.split("\\?")[1].split("&");
        for (String param : paramsStr) {
            String[] kv = param.split("=");
            this.requestParams.put(kv[0], kv[1]);
        }
    }
    //根据请求参数名获取请求参数值
    public String getRequestParam(String key) {
        return requestParams.get(key);
    }
}
  • MyHttpResponse
public class MyHttpResponse {
    private OutputStream outputStream;
    //响应媒体类型
    private String contentType;

    public MyHttpResponse(OutputStream outputStream) {
        //只模拟响应类型为text/html的情况
        this.contentType = "text/html";
        this.outputStream = outputStream;
    }

    public OutputStream getOutputStream() {
        return this.outputStream;
    }

    public String getContentType() {
        return contentType;
    }
}
  • UserServlet类
/**
 * 自定义userServlet类来根据请求参数动态响应页面
 */
public class UserServlet {
    //具体的业务方法
    public void doService(MyHttpRequest request, MyHttpResponse response) {
        //准备响应头数据
        String responseHeader = "HTTP/1.1 200 OK\r\nContent-Type:" + response.getContentType() + ";charset=UTF-8\r\n\r\n";
        //准备响应体数据
        String body = "<!DOCTYPE html>\n" +
                "<html lang=\"en\">\n" +
                "<head>\n" +
                "    <meta content=\"text/html;charset=UTF-8\">\n" +
                "    <title>User</title>\n" +
                "</head>\n" +
                "<body>\n" +
                "    <h1>姓名:#name</h1>\n" +
                "    <h1>年龄:#age</h1>\n" +
                "</body>\n" +
                "</html>";
        String responseBody = body.replace("#name", request.getRequestParam("name")).replace("#age", request.getRequestParam("age"));
        //响应
        try (OutputStream outputStream = response.getOutputStream()) {
            outputStream.write((responseHeader+responseBody).getBytes("utf-8"));
            Thread.sleep(1000);//睡眠一秒等待客户端处理响应结果
            System.out.println("响应成功");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

上述代码中UserServlet就简单的实现了servlet的功能,而HttpServer则做了Servlet容器该做的事情,当然我们这里做了简化处理,正常情况容器会根据请求的URI来动态调用不同的servlet,其流程如下:
这里写图片描述

4.2 Servlet规范

SUN公司制定了一套servlet规范,所谓规范,就是接口,我们先来看一下Servlet接口:
这里写图片描述

我们所编写的servlet都要实现这五个方法,除了标识servlet生命周期的init和destory方法之外,最重要的是service(ServletRequest,ServletResponse),相当于我们上文中UserServlet的doService方法。

Servlet规范为JavaWeb 应用定制了对象模型,Servlet规范还定义里容器为Servlet提供的“十八般武器”,要想精通Servlet编程,不仅要了解Servlet自身的用法,还要了解容器为它提供的各种武器的用法,常用的“武器”如下:

  • ServletRequest、HttpServletRequest: 请求对象,servlet从该对象中获取客户端请求信息;
  • ServletResponse、HttpServletResponse: 响应对象,servlet通过该对象来响应结果;
  • ServletConfig: servlet配置对象,当容器初始化一个Servlet对象时,会向servlet提供一个ServletConfig对象,servlet通过该对象来获取初始化参数信息及ServletContext对象;
  • ServletContext: servlet上下文对象,servlet通过该对象来访问容器为当前web应用提供的各种上下文信息

Servlet API 主要有两个包组成javax.servlet和javax.servlet.http,后者是Servlet的http实现,下图展示了Servlet API主要接口和类的关系:

这里写图片描述

从图中我们可以看到,Servlet接口关联了ServletConfig,可以直接调用getServletConfig方法来获取ServletConfig对象,而ServletConfig又关联ServletContext接口;GenericServlet作为Servlet的子接口,不仅继承了Servlet接口,还继承了ServletConfig接口,这样我们就可以直接在GenericServlet中获取各种配置数据及ServletContext信息了。

GenericServlet顾名思义,即通用的Servlet,既然是通用的,也就是说它与任何网络协议无关,不同的网络协议都可以通过实现GenericServlet接口来定义不同的处理逻辑,其中一个典型的实现就是HttpServlet。

HttpServlet适合客户端采用HTTP协议通信的WEB服务,我们在开发基于HTTP协议的webapp时,就是拓展HttpServlet功能,HttpServlet在service()中,根据请求方法GET、POST、PUT等分别调用相应的doGet()、doPost()、doPut()等方法,我们在拓展的时候只需要根据我们的设计来重写doXXX方法即可。

4.3 Servlet的生命周期

上一小节,我们介绍了Servlet API定义的各种接口,以及他们之间的关系,现在我们来讨论Servlet的生命周期,Servlet生命周期可以分为三个阶段:初始化阶段、运行阶段、销毁阶段,其分别对应着Servlet的init()、service()、和destory()方法。

  • 初始化阶段

Servlet容器在初始化Servlet经过以下流程:

(1) 将Servlet类加载到内存中;
(2) 创建ServletConfig对象,ServletConfig对象包含了特定的Servlet初始化配置信息,并与当前的WEB应用的ServletContext关联;
(3) 创建Servlet对象;
(4) 调用Servlet对象的init()方法,并关联ServletConfig。

下列情况之一,Servlet会进入初始化阶段:

(1) 当前WEB应用处于运行时阶段,特定Servlet被客户端首次访问,多数Servlet都会在这种情况下被初始化;
(2) 如果在web.xml文件中为一个Servlet设置了<load-on-startup>元素,那么当Servlet容器启动这个Servlet所属的WEB应用时就会初始化这个Servlet,且值越小,初始化的时间越早。

* 运行时阶段

运行时阶段是Servlet生命周期种最重要的阶段,在这个阶段Servlet可以随时响应客户请求,当Servlet被调用时,容器会创建针对本次请求的ServletRequest对象和ServletResponse对象,响应结束,容器就会ServletRequest对象和ServletResponse对象。
* 销毁阶段
当web应用被终止时,容器就会调用此web应用种的所有Servlet的destory()方法,还会销毁其关联的ServletConfig对象。

Servlet API还定义了许多监听器,用于监听整个web服务的生命周期中的各种事件:

1)监听 Session、request、context 的创建于销毁,分别为:
HttpSessionLister、ServletRequestListener、ServletContextListener

2)监听对象属性变化,分别为:
HttpSessionAttributeLister、ServletContextAttributeListener、ServletRequestAttributeListener

3)监听Session 内的对象,分别为HttpSessionBindingListener 和 HttpSessionActivationListener。与上面六类不同,这两类 Listener 监听的是Session 内的对象,而非 Session 本身,不需要在 web.xml中配置。

4.4 Servlet容器与Servlet的关系

那么是不是说只要实现了Servlet接口,就能完成请求响应呢?
答案:否!

web服务器当然还需要管理这些servlet,并准备好相应的ServletRequest和ServletResponse,在合适的时候初始化servlet并调用其service()方法,而这一部分工作则是由servlet容器完成的。

通过讨论Servlet的生命周期我们大概认识了Servlet容器的作用,Servlet容器管理着Servlet整个生命周期。servlet容器和servlet就像抢和子弹的关系,抢没有子弹是没有威力的,同样子弹没有抢也不能发挥其功能。下图展示了servlet容器响应客户请求访问特定servlet的时序图:
这里写图片描述

这个图直观展示了容器调用Servlet的流程。

4.5 Http的会话管理

由于HTTP是一个无状态协议,用户的每一个请求对于服务器来说的是新的请求,因此需要有一种手段使得web服务器能够跟踪用户的状态,session机制就是一种主流的解决方案。在Servlet API中定义了代表会话的接口 javax.servlet.http.HttpSession,其工作原理是:

(1)浏览器第一次访问服务器中任意一个支持会话的URI,Servlet容器试图寻找HTTP请求中表示Session ID的Cookie,由于还不存在这样的Cookie因此就认为一个新的会话开始了,于是创建一个HttpSession对象,为它分配唯一的Session ID作为Cookie添加到Http响应结果中,当浏览器接收到响应结果后会把表示Session ID的cookie保存在客户端;

(2)浏览器继续访问同一个服务的任意支持seeeion的URI,由于这次请求中包含了表示Session ID的Cookie,因此认为本次请求已经处于一个会话中了,Servlet容器不再创建新的HttpSession,而是根据Session ID查找HttpSession对象;

(3)重复(2)步骤,直到当前会话销毁。

4.5.1 HttpSession的生命周期

  • 创建
    根据前面的讨论我们知道Servlet容器会在以下两种情况下创建Session:

    • 访问的URI支持会话;
    • 第一次访问该URI,即Cookie中没有SessionId,或者根据SessionId找不到相应的Session

那么在服务器中Session是如何创建的呢?
答案就在HttpServletRequest的getSession()方法中,这个方法有一个重载方法,getSession(boolean create),其中,getSession()等价于getSession(true),表示查不到session时创建一个新的Session对象,当调用getSession(false)时,如果找不到Session则返回null

  • 销毁
    以下情况,会话会被销毁:

    • 服务端执行HttpSession的invalidate()方法;
    • 会话过期:当一个会话一直未被访问,超过会话的允许的不活动状态最长时间,会话就会被销毁,这表现为两种情况:第一,用户在当前页面,但不发送新的请求;第二,用户关闭浏览器窗口,且浏览器不缓存Cookie数据,因此Cookie中所标识的那个session永远不会再被访问了,等到过时,服务器将其销毁。

为了模拟Session的生命周期,我们使用SessionListener监听器来做一个实验

创建一个项目,里面有一个jsp页面和一个servlet,jsp中有一个访问该servlet的超链接:

index.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <h1>Hello World</h1>
    <a href="/session">Session Test</a>
</body>
</html>

SessionServlet

public class SessionServlet extends HttpServlet {
  private static final long serialVersionUID = 485923566163995521L;

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    final Optional<HttpSession> sessionOp = Optional.ofNullable(req.getSession(false));
    //如果session存在则响应204(No Content),页面不会跳转
    if (sessionOp.isPresent()){
      resp.setStatus(204);
      //将sessionId写到响应头内
      resp.setHeader("session",sessionOp.get().getId());
    }else {
    //否则响应403(Forbidden),拒绝访问
      resp.setStatus(403);
    }
  }
}

session监听器:MySessionListener

public class MySessionListener implements HttpSessionListener {
  //session创建时调用
  @Override
  public void sessionCreated(HttpSessionEvent se) {
    final HttpSession session = se.getSession();
    System.out.println(LocalDateTime.now()+"创建sessionId="+session.getId());
  }
  //session销毁时调用
  @Override
  public void sessionDestroyed(HttpSessionEvent se) {
    System.out.println(LocalDateTime.now()+"销毁sessionId="+se.getSession().getId());
  }
}

web.xml中配置Servlet,listener和Session失效时间

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://java.sun.com/xml/ns/javaee"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    id="WebApp_ID" version="2.5">
    <session-config>
       <!--session过期时间,当为负数时表示会话永远不会过期-->
        <session-timeout>5</session-timeout>
    </session-config>
    <servlet>
        <servlet-name>sessionServlet</servlet-name>
        <servlet-class>com.owl.servlet.SessionServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>sessionServlet</servlet-name>
        <url-pattern>/session</url-pattern>
    </servlet-mapping>
    <listener>
        <listener-class>com.owl.servlet.MySessionListener</listener-class>
    </listener>
</web-app>

用Tomcat启动项目,打开浏览器调试工具,并访问localhost:8080/index.jsp,会发现请求头中是没有任何session信息的cookie,但是响应头中有一个参数

Set-Cookie: JSESSIONID=F5B27CF161BC29F43C8E6249F3CF0451; Path=/; HttpOnly

浏览器会根据这个参数在Cookie中存入JSESSIONID数据,并在下次访问服务时加上这个JSESSIONID的Cookie。

但是,让我们困惑的是,我们只是访问了jsp,服务器为什么创建了Session?

从控制台打印的结果看,服务器确实为本次访问创建了Session

2018-04-27T17:39:12.320创建sessionId=F5B27CF161BC29F43C8E6249F3CF0451

这是由于jsp本身就是一个servlet,并且默认支持session,也就是说在访问jsp时,会在后台调用request.getSession()方法,这个参数其实是可以配置的,如下,将page的session属性设为false(默认为true),再访问就不会创建Session了。

<%@ page contentType="text/html;charset=UTF-8" session="false" language="java" %>

下面我们来测试一下访问页面中的超链接,由于第一次访问jsp时已经创建了Session,并且浏览器将JSESSIONID保存到Cookie中了,当我们访问”/session”时,发现请求头cookie中包含了一个JSESSIONID参数

Cookie: JSESSIONID=F5B27CF161BC29F43C8E6249F3CF0451

这时候服务器正常响应了我们的请求,页面没有变化,从network中可以看到,响应状态码为204,并且响应头中包含了我们设置的session参数

Date: Fri, 27 Apr 2018 09:49:10 GMT
Server: Apache-Coyote/1.1
session: F5B27CF161BC29F43C8E6249F3CF0451

此时如果我们改变cookie中的JSESSIONID的值(可以使用谷歌浏览器插件EditThisCookie)再访问超链接,这个时候由于服务器根据JSESSIONID找不到相应的会话,并且我们在Servlet中调用的是req.getSession(false)方法,因此不会创建新的会话,这个时候浏览器会报错,响应请求被拒绝

访问 localhost 的请求遭到拒绝
您未获授权,无法查看此网页。
HTTP ERROR 403

现在,我们不再访问服务器,五分钟之后会看到控制台打印如下信息,表明session被销毁了。

2018-04-27T17:45:39.268销毁sessionId=F5B27CF161BC29F43C8E6249F3CF0451

总结:

session只有在以下情况都满足才会创建:
* 调用request.getSession()或request.getSession(true);
* 当前request的Cookie中不包含JSESSIONID参数,或者根据JSESSIONID无法找出session对象;

满足以下情况之一,session会被销毁:
* 服务端执行HttpSession的invalidate()方法;
* session长时间未被访问而过期;

4.6 过滤器

过滤器是在Java Servlet 2.3 规范中定义的,其功能如下:

  • 在Web组件被调用之前检查ServletRequest对象,修改请求头和请求正文的内容,或者对请求进行预处理操作。
  • 在Web组件被调用之后检查ServletResponse对象,修改响应信息

自定义过滤器必须实现Filter接口,并在web.xml进行配置,且必须先配置所有的过滤器,再配置servlet

5. 最优秀的servlet容器——Tomcat

Tomcat是一个优秀的servlet容器,不仅完成了servlet容器的功能,还提供了web服务器的一些实用功能,前面的讨论中我们已经知道了servlet和servlet容器之间的关系,那么tomcat是如何实现servlet容器的功能的呢?要解决这个问题首先要弄明白以下问题:
1. Tomcat结构是什么样的;
2. Tomcat是如何初始化的;
3. 我们开发的各种各样的servlet是如何发布到Tomcat中并被Tomcat调用的。

5.1 第一个问题,Tomcat的结构

如果我们去查看Tomcat的server.xml配置文件,就可以初窥其组成
这里写图片描述

上图是Tomcat组成结构,主要包括以下内容:

  • 顶层元素server,每个Tomcat只有一个server;
  • 每个server中有多个service;
  • 每个service于元素中有多个连接器元素(Connector)和一个容器类元素(Container)

而其中起到关键作用的就是Connector和Container

  • Connector: Tomcat 与外部世界的连接器,监听固定端口接收外部请求,传递给 Container,并 将 Container 处理的结果返回给外部;
  • Container : Catalina,Servlet 容器,内部有多层容器组成,用于管理 Servlet 生命周期,调用 servlet 相关方法。

下面这张图对Tomcat有更加清晰的理解:
这里写图片描述

其中Service左边的都是Container,而下面的都是Connector.

5.2 第二个问题 Tomcat初始化过程

下图是Tomcat启动时序图
这里写图片描述
Tomcat启动过程涉及到各种组件逐层初始化,其过程很复杂,我们会在后续的学习中详细讨论,经过这一系列初始化过程后,Server便可以调用不同的Servlet来响应客户请求,其流程如下:

  1. 请求到达 server 端,server 根据 url 映射到相应的 Servlet
  2. 判断 Servlet 实例是否存在,不存在则加载和实例化 Servlet 并调用 init 方法
  3. Server 分别创建 Request 和 Response 对象,调用 Servlet 实例的 service 方法(service 方法 内部会根据 http 请求方法类型调用相应的 doXXX 方法)
  4. doXXX 方法内为业务逻辑实现,从 Request 对象获取请求参数,处理完毕之后将结果通过 response 对象返回给调用方
  5. 当 Server 不再需要 Servlet 时(一般当 Server 关闭时),Server 调用 Servlet 的 destroy() 方 法。

5.3 第三个问题 JavaWeb应用如何发布到Tomcat

在解决这个问题之前我们先了解Web应用的目录结构
这里写图片描述

我们开发的Javaweb中包含了各种各样的servlet,发布的时候侯只需要将Web项目打成war包,放在/webapps目录下,启动Tomcat即可(当然还有其他方法)。这是因为,在默认情况下,webapps中的所有应用都运行在名为“localhost”的虚拟主机下,当客户端发送请求时,Tomcat的类加载器会根据url的映射关系加载相应的servlet的class文件,其加载顺序如下:
(1)在JavaWeb应用的WEB-INF/classes目录下查找;
(2)在JavaWeb应用的WEB-INF/lib目录下jar文件中查找;
(3)在Tomcat的lib目录下直接查找;
(4)在Tomcat的lib目录下jar中查找;
以上过程都找不到则抛异常。
我们在开发web项目时,对于每个servlet,都会在web.xml中做以下映射:

<servlet>
    <servlet-name>UserServlet</servlet-name>
    <servlet-class>com.owl.myserver.test2.UserServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>UserServlet</servlet-name>
    <servlet-class>/user</servlet-class>
</servlet-mapping>

Tomcat的类加载器就是通过这个映射关系去加载相应的servlet并完成响应的,具体的加载及映射细节我们将在Tomcat源码分析中讨论。

6. spring mvc与servlet

SpringMVC是一个基于DispatcherServlet的MVC框架,每一个请求最先访问的都是DispatcherServlet,DispatcherServlet负责转发每一个Request请求给相应的Handler,Handler处理以后再返回相应的视图(View)和模型(Model)。

开发一个spring mvc的webapp,首先需要引入webmvc需要的各种jar包,然后配置web.xml:

<!-- Spring MVC配置 -->
<!-- ====================================== -->
<servlet>
    <servlet-name>spring</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!-- 可以自定义servlet.xml配置文件的位置和名称,默认为WEB-INF目录下,名称为[<servlet-name>]-servlet.xml,如spring-servlet.xml
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/spring-servlet.xml</param-value>&nbsp; 默认
    </init-param>
    -->
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>spring</servlet-name>
    <url-pattern>/*</url-pattern>
</servlet-mapping>


<!-- Spring配置 -->
<!-- ====================================== -->
<listener>
   <listenerclass>
     org.springframework.web.context.ContextLoaderListener
   </listener-class>
</listener>


<!-- 指定Spring Bean的配置文件所在目录。默认配置在WEB-INF目录下 -->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:config/applicationContext.xml</param-value>
</context-param>

我们第一个关注点是ContextLoaderListener监听器。

在 Servlet API中有一个ServletContextListener接口,我们前面已经说了它的作用,它能够监听ServletContext对象的生命周期,实际上就是监听Web应用的生命周期。当Servlet容器启动或终止Web应用时,会触发ServletContextEvent事件,该事件由ServletContextListener来处理。在ServletContextListener接口中定义了处理ServletContextEvent 事件的两个方法contextInitialized()和contextDestroyed()。ContextLoaderListener监听器的作用就是启动Web容器时,自动装配ApplicationContext的配置信息,也就是说只要web容器启动就会启动spring的IOC容器,完成各种bean的初始化,这为spring动态调用各种RequestMapping提供前提条件,当IOC启动后,接下来就是spring的天下了;相反,当web容器销毁,IOC容器也将销毁。

第二个关注点是DispatcherServlet

可以看到DispatcherServlet是项目唯一的Servlet,他间接实现了HttpServlet接口。DispatcherServlet,顾名思义,即Servlet调度器,是spring mvc的关键所在。
其中<url-partern>定义需要调度的url,这里我们指定所有的请求都走DispatcherServlet统一调度;DispatcherServlet会根据不同的url调用不同的controller中声明的requestMapping方法,并返视图,响应给客户。

为了让DispatcherServlet能够正常工作,需要配置相应的servlet.xml(默认为WEB-INF目录下,名称为{<servlet-name>}-servlet.xml):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"     
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"     
        xmlns:context="http://www.springframework.org/schema/context"     
   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd   
       http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd   
       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd   
       http://www.springframework.org/schema/context <a href="http://www.springframework.org/schema/context/spring-context-3.0.xsd">http://www.springframework.org/schema/context/spring-context-3.0.xsd</a>">

    <!-- 启用spring mvc 注解 -->
    <context:annotation-config />

    <!-- 设置使用注解的类所在的jar包 -->
    <context:component-scan base-package="controller"></context:component-scan>

    <!-- 完成请求和注解POJO的映射 -->
    <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" />
  
    <!-- 对转向页面的路径解析。prefix:前缀, suffix:后缀 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="/jsp/" p:suffix=".jsp" />
</beans>

servlet.xml是DispatcherServlet能够响应不同的请求并完美进行视图渲染的关键,其中具体的调用流程可以参考博客:
spring mvc 流程图,说的很详细,这里就不再赘述。

完成上述配置之后就可以写具体的Controller,开发的webapp发布到Tomcat运行即可。

7. spring boot内嵌Tomcat的实现及Servlet的自动化配置

7.1 概述

在讲spring boot开发web项目之前我们先来回顾一下传统的spring mvc项目是如何创建的,都需要哪些东西(假设我们需要实现4.1的功能):

  1. 首先我们需要引入各种各样的jar包。例如spring-webmvc、jackson等,传统的项目,jar包的引用就得捣腾半天时间;
  2. 一个web.xml文件,一个 servlet.xml文件;此外还指定了spring bean的配置文件(applicationContext.xml);
  3. UserController类,声明一个value为”/user”的requestMapping方法;
  4. 将项目部署到Tomcat上启动;

我们会发现,真正实现我们想要的功能的只有Controller类的那一个方法,剩下的所有工作都是为spring mvc的所做的通用配置;也就是说,每一个webapp,只有具体的Controller和下层的service、dao层有所不同,其他的部分基本一致,包括pom的配置、mvc的配置、以及项目的打包与发布。

有没有一种框架,能够为我们做这些通用的、重复的工作,使我们只需要关注具体的业务实现?

Spring Boot 刚好满足这种需求,spring boot的推出直接颠覆了JavaEE的开发,我们只需要进行简单的配置就可以轻松的完成一个web服务的开发(注意这里说的是web服务而不是web应用,原因后面会讨论),这是因为spring boot 具有以下特点:

(1) 遵循“习惯优于配置”的原则,使用spring boot只需要少量的配置,大部分时候可以使用通用配置;
(2) 项目快速搭建,可无配置整合各种第三方框架;
(3) 零xml,只是自动配置和Java Config;
(4) ==内嵌Servlet容器(如Tomcat),可直接以jar包运行==,这个时候我们开发的web应用已经不再是传统的web应用了,而具有完整的web服务的功能;

虽然spring boot 带来了颠覆性的效果,但其并没有新的技术,完全是一个基于spring的应用。

Spring Boot 为我们提供了简化企业级开发绝大多数场景的starter pom,只要使用了所需要的start pom就可以完成自动化配置。例如我们要实现4.1的功能,只需要完成一下步骤即可:

(1) 在IDEA种使用Spring Initializr引导,并在依赖中勾选web组件生成项目;

(2) 然后创建UserController类:

//@RestController整合了@Cotroller和@ResponseBody,接口的响应体content-type=application/json
@RestController(value = "/user")
public class UserController {
  //get请求
  @GetMapping
  public ResponseEntity<String> userInfo(@RequestParam("name") String name,
      @RequestParam("age") Integer age) {
    return new ResponseEntity<>("name=" + name + "\r\nage=" + age, HttpStatus.OK);
  }
}

(3) 启动UserApplication的main方法,浏览器访问localhost:8080/user?name=Tom&age=16即可;

是不是很简单?下面我们来分析如何对spring boot内嵌的Tomcat进行配置。

7.2 spring boot对Servlet容器(默认Tomcat)的配置

spring boot项目的配置在application.properties(或者application.yml)中,现在配置文件中没有任何配置,这说明所有的配置都有一个默认值,例如服务的端口,默认为8080。对于Servlet容器的通用配置是以“server”为前缀的,例如:

server:
  port: 8888 //端口
  connection-timeout: 60 //超时时间

当对具体的容器进行配置时只需要配置”server.容器名.参数名”的值即可,例如对Tomcat的uri-encoding配置:

server.tomcat.uri-encoding: utf-8

yml文件的格式为:

server:
  port: 8888
  connection-timeout: 60
  tomcat:
    uri-encoding: UTF-8

那么,那些默认的配置在哪里呢?
答案是ServerProperties类,spring boot中有很多这样的配置参数类,他们都有@ConfigurationProperties注解,这个注解的作用就是将pojo与配置文件中的参数关联。

在ServerProperties类中定义了各种默认配置,可以在配置文件中覆盖。

7.3 Spring Boot如何内嵌Servlet容器的

我们看到项目的pom文件中依赖了spring-boot-starter-web:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

如果打开的spring-boot-starter-web的pom文件,可以看到其依赖了spring-boot-starter-tomcat,
正是由于这个starter,项目才得以自动配置Tomcat相关的bean供容器使用,至于这个自动配置的底层原理会在后续的源码分析中讨论。

<!--spring-boot-starter-web.pom的部分代码-->
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
        </dependency>
    </dependencies>

如果我们要更换servlet容器只需要依赖spring-boot-starter-web时exclusion掉spring-boot-starter-tomcat并依赖其他容器的starter即可,例如使用jetty:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jetty</artifactId>
    </dependency>

7.4 Spring Boot如何完成servlet的自动化配置的

我们主要关注spring boot如何对DispatcherServlet进行自动化配置,这个自动化配置的过程就在DispatcherServletAutoConfiguration类中,这个类有一个内部类DispatcherServletRegistrationConfiguration,自动配置了一个DispatcherServlet的注册器ServletRegistrationBean,每一个servlet都对应一个注册器,这个注册器在设置DispatcherServlet的urlPatten使用的是上面所提到的ServerProperties配置类中的servlet.path的值,而LoadOnStartup属性则关联着WebMvcProperties中的servlet.load-on-startup属性,因此这些参数都可以自定义配置,其中WebMvcProperties配置前缀是spring.mvc。

  @Configuration
  @Conditional({DispatcherServletAutoConfiguration.DispatcherServletRegistrationCondition.class})
  @ConditionalOnClass({ServletRegistration.class})
  @EnableConfigurationProperties({WebMvcProperties.class})
  @Import({DispatcherServletAutoConfiguration.DispatcherServletConfiguration.class})
  protected static class DispatcherServletRegistrationConfiguration {
    private final ServerProperties serverProperties;
    private final WebMvcProperties webMvcProperties;
    private final MultipartConfigElement multipartConfig;

    public DispatcherServletRegistrationConfiguration(ServerProperties serverProperties, WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfigProvider) {
      this.serverProperties = serverProperties;
      this.webMvcProperties = webMvcProperties;
      this.multipartConfig = (MultipartConfigElement)multipartConfigProvider.getIfAvailable();
    }

    @Bean(
      name = {"dispatcherServletRegistration"}
    )
    @ConditionalOnBean(
      value = {DispatcherServlet.class},
      name = {"dispatcherServlet"}
    )
    public ServletRegistrationBean<DispatcherServlet> dispatcherServletRegistration(DispatcherServlet dispatcherServlet) {
      ServletRegistrationBean<DispatcherServlet> registration = new ServletRegistrationBean(dispatcherServlet, new String[]{this.serverProperties.getServlet().getServletMapping()});
      registration.setName("dispatcherServlet");
      registration.setLoadOnStartup(this.webMvcProperties.getServlet().getLoadOnStartup());
      if (this.multipartConfig != null) {
        registration.setMultipartConfig(this.multipartConfig);
      }

      return registration;
    }
  }

DispatcherServlet自定义配置举例:

server:
  servlet:
    path: /*
spring:
  mvc:
    servlet:
      load-on-startup: 1

在DispatcherServletAutoConfiguration中还有一个内部类DispatcherServletConfiguration,用来自动配置DispatcherServlet基本属性,例如是否支持Options请求、找不到handler是否抛出异常等。

此外WebMvcProperties还配置了视图解析器的参数,例如:

spring:
  mvc:
    view:
      prefix: /templates
      suffix: .ftl

前提是我们需要依赖相应的视图模板引擎,例如

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

值得注意的是,spring boot极不推荐jsp,这是因为在tomcat上,jsp不能在嵌套的tomcat容器解析,即不能在打包成可执行的jar的情况下解析
,若选择Jetty 或Undertow嵌套的容器不支持jsp

7.5 Spring Boot配置自定义的Servlet

前面已经说了,每一个Servlet都有一个注册器,我们只需要配置一个Servlet并配置相应的注册器即可完成servlet的自定义注册,为了不覆盖默认的配置,需要为自定义的servlet和servlet注册器取别名,直接上代码:

@Configuration
public class ServletConfig {

    @Bean
    public TestServlet userServlet(){
        return new UserServlet();
    }

    @Bean
    public ServletRegistrationBean userServletRegistrationBean(UserServlet userServlet){
        ServletRegistrationBean registration = new ServletRegistrationBean(userServlet);
        registration.setEnabled(true);
        registration.addUrlMappings("/servlet/test");
        return registration;
    }
}

Spring Boot还支持通过@WebServlet注解来自定义servlet,Spring Boot会扫描这个注解,并将这个注解注解的类注册到web容器中作为一个servlet。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值