1. Servlet概述

Java Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层

Servlet 在 Web 应用程序中的位置:

Servlet - 图1

Servlet 执行以下主要任务:

  • 读取客户端(浏览器)发送的显式的数据。这包括网页上的 HTML 表单,或者也可以是来自 applet 或自定义的 HTTP 客户端程序的表单。
  • 读取客户端(浏览器)发送的隐式的 HTTP 请求数据。这包括 cookies、媒体类型和浏览器能理解的压缩格式等等。
  • 处理数据并生成结果。这个过程可能需要访问数据库,执行 RMI 或 CORBA 调用,调用 Web 服务,或者直接计算得出对应的响应。
  • 发送显式的数据(即文档)到客户端(浏览器)。该文档的格式可以是多种多样的,包括文本文件(HTML 或 XML)、二进制文件(GIF 图像)、Excel 等。
  • 发送隐式的 HTTP 响应到客户端(浏览器)。这包括告诉浏览器或其他客户端被返回的文档类型(例如 HTML),设置 cookies 和缓存参数,以及其他类似的任务。

下图显示了一个典型的 Servlet 生命周期方案。

  • 第一个到达服务器的 HTTP 请求被委派到 Servlet 容器。
  • Servlet 容器在调用 service() 方法之前加载 Servlet。
  • 然后 Servlet 容器处理由多个线程产生的多个请求,每个线程执行一个单一的 Servlet 实例的 service() 方法。

Servlet - 图2

2. Servlet API

2.1Servlet接口

Servlet接口时Servletde 核心,所有的Servlet类都必须实现这个接口,在Servlet接口中一共定义了5个方法,其中有三个方法都有Servlet容器来调用,容器会在Servlet的生命周期的不同阶段调用特定的方法。

方法名称 方法描述
init(ServletConfig servletConfig) 负责初始化Servlet对象,容器在创建号Servlet对象后会调用该方法
service(ServletRequest var1, ServletResponse var2) 负责响应客户的请求,为客户提供响应的服务,当容器接收到客户端要求访问的特定Servlet对象请求时,就会调用该Servlet对象的service()方法
destroy() 负责释放Servlet对象占用的资源,当Servlet对象结束生命周期时,容器会调用此方法
getServletConfig() 返回一个ServletConfig对象,该对象中包含了Servlet的初始化参数信息
getServletInfo() 返回一个字符串,在该字符串中包含了Servlet的创建者,版本和版权等信息

Servlet的生命周期简介

  1. 初始化阶段:
    1. Servlet容器加载Servlet类,把它的.class文件中的数据读入到内存中
    2. Servlet容器创建ServletConfig对象,
    3. Servlet容器创建Servlet对象
    4. Servlet容器调用Servlet对象的init()方法
  2. 运行阶段:每次用户请求到达时,Servlet容器调用一个线程去执行Servlet的Service方法
  3. 销毁阶段:当Web应用被终止时,会调用destory()方法,释放资源,比如文件流关闭和数据库连接关闭等

2.2 GenericServlet抽象类

GenericServlet抽象类为Servlet接口提供了通用实现,它与任何网络应用曾协议无关。它除了实现Servlet类之外,还实现了ServletConfig接口,和Serializable接口

从源码可以看到GenericServlet有两个主要特点

  1. 实现了init方法,GenericServlet类有一个ServletConfig类型的私有实例变量config,当调用init的时候,会将传入ServletConfig对象保存在私有实例变量中。
  1. private transient ServletConfig config;
  2. public void init(ServletConfig config) throws ServletException {
  3. this.config = config;
  4. this.init();
  5. }
  6. public void init() throws ServletException {
  7. }
  1. service方法是唯一没有实现的方法,如果要继承GenericServlet抽象类,必须实现service方法,提供具体服务
  1. public abstract void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
  1. destory方法是空实现,如果需要,继承后可以重写该方法
  1. public void destroy() {}
  1. 实现了ServletConfig接口中的所有方法,其实就是调用私有属性config的方法,这种写法叫做装饰者模式,为自己附加了一个servletConfig装饰身份,这种写法也可以达到继承的目的,并且可以比继承更加灵活一些,有兴趣可以课余扩展阅读设计模式。

    2.3 HttpServlet抽象类

    HttpServlet是GenericServlet的子类,提供了与Http协议相关的通用实现,它适合运行在与客户端采用HTTP协议通信的Servlet容器或者Web服务器中,在开发web应用中,自定义的Servlet类一般都是扩展(继承)HttpServlet类

Http协议将客户端请求分为了【GET】, 【POST】, 【DELETED】等多种方式,而HttpServlet针对每一种请求方式都提供了相应的服务方法,如【doGet】,【doPost】,【doPut】,【doDelete】

源码中httpServlet的service实现实际是调用它的一个重载service方法

  1. public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
  2. HttpServletRequest request;
  3. HttpServletResponse response;
  4. try {
  5. request = (HttpServletRequest)req;
  6. response = (HttpServletResponse)res;
  7. } catch (ClassCastException var6) {
  8. throw new ServletException("non-HTTP request or response");
  9. }
  10. this.service(request, response); //实际调用的是重载方法
  11. }

而这个重载service的默认实现就是根据method的值来分别调用【doGet】,【doPost】,【doPut】,【doDelete】等

  1. protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  2. String method = req.getMethod();
  3. long lastModified;
  4. if (method.equals("GET")) {
  5. lastModified = this.getLastModified(req);
  6. if (lastModified == -1L) {
  7. this.doGet(req, resp);
  8. } else {
  9. long ifModifiedSince;
  10. try {
  11. ifModifiedSince = req.getDateHeader("If-Modified-Since");
  12. } catch (IllegalArgumentException var9) {
  13. ifModifiedSince = -1L;
  14. }
  15. if (ifModifiedSince < lastModified / 1000L * 1000L) {
  16. this.maybeSetLastModified(resp, lastModified);
  17. this.doGet(req, resp);
  18. } else {
  19. resp.setStatus(304);
  20. }
  21. }
  22. } else if (method.equals("HEAD")) {
  23. lastModified = this.getLastModified(req);
  24. this.maybeSetLastModified(resp, lastModified);
  25. this.doHead(req, resp);
  26. } else if (method.equals("POST")) {
  27. this.doPost(req, resp);
  28. } else if (method.equals("PUT")) {
  29. this.doPut(req, resp);
  30. } else if (method.equals("DELETE")) {
  31. this.doDelete(req, resp);
  32. } else if (method.equals("OPTIONS")) {
  33. this.doOptions(req, resp);
  34. } else if (method.equals("TRACE")) {
  35. this.doTrace(req, resp);
  36. } else {
  37. String errMsg = lStrings.getString("http.method_not_implemented");
  38. Object[] errArgs = new Object[]{method};
  39. errMsg = MessageFormat.format(errMsg, errArgs);
  40. resp.sendError(501, errMsg);
  41. }
  42. }

所以实际开发时,可以重写整个重载后的service方法,也可以根据你实际情况重写相应的【doGet】,【doPost】,【doPut】,【doDelete】等

2.4 ServletConfig接口

ServletConfig接口的签名

  1. public interface ServletConfig {
  2. String getServletName(); //获取Servlet的名称
  3. ServletContext getServletContext();
  4. String getInitParameter(String var1); //获取Servlet初始化的参数
  5. Enumeration<String> getInitParameterNames(); //获取初始化参数名称列表
  6. }

2.5 ServletContext接口

ServletContext是Servlet与Servlet容器之间直接通信的接口。Servlet容器在启动一个Web应用时,会为他创建一个ServletContext对象。每个Web应用都有唯一的ServletContext对象,可以把ServletContext对象形象的理解为Web应用的总管家,同样的一个Web应用中的所有Servlet对象都共享一个总管家,Servlet对象们可以通过这个总管家来访问容器中的各种资源。

ServletContext对象的几个常用方法:

方法 描述
String getInitParameter(String var1);
Enumeration getInitParameterNames();
获取Web应用的初始化参数或参数名列表
getRealPath(String path) 根据参数的指定的虚拟路径,返回文件系统中的真实路径
getContextPath() 获取Web应用的URL入口
getResourceAsStream(String path) 返回一个用于读取文件的输入流,参数path为web应用根目录
setAttribute(String name, Object value) 存储一个值到ServletContext中
getAttribute(String name) 从ServleteContext中获取值

2.6 HttpServletRequest接口

HttpServletRequest代表客户端的请求。它是ServletRequest的子接口,当Servlet容器接收到客户端要求访问特定Servlet请求时,容器先解析客户端的原始请求数据,把他封装成一个ServletRequest对象。如果客户端是浏览器,则默认是Http协议,则会封装成为HttpServletRequest对象。

HttpServletRequest常用方法

方法 说明
getParameter(String name)
getParameterValues(String name)
根据请求的参数名,获取请求的参数值
Enumeration getParameterNames(); 返回请求中所有的参数名称列表
getParameterMap(); 获取请求中所有的参数键值对
getRequestURL() 获取请求路径
getRequestURI() URL除去域名或者ip和端口部分,剩下的就是URI
getMethod() 获取请求方式
getServletPath() 获取请求的Servlet名称

2.7 HttpServlteResponse接口

当容器接收到请求时,除了封装一个ServletRequest对象外,还会生成一个默认的ServlteResponse对象,将两个对象一起传递给service方法,在service方法中可以对客户端的响应做相应的修改已达到开发的目的。
同样ServletResponse接口也有一个特定的子接口是HttpServlteResponse。

HttpServlteResponse接口的常用

getWriter() 获取一个PrintWriter对象直接写入内容到响应的正文中去
setContentType() 设置响应类型
setRedirect() 请求重定向

3. 会话机制

3.1 cookie

Cookie的英文愿意是”点心”它是客户端访问Web服务器时,服务器在客户端硬盘上存放的信息,好像是服务器送给给客户的”点心”。 服务器可以根据Cookie来跟踪客户状态,这对于需要区别客户的场合特别有用。

Cookie的运行机制时由Http协议规定的,多数Web服务器和浏览器都支持Cookie:

  1. HTTP响应结果中添加Cookie数据。
  2. 解析HTTP请求中的Cookie数据。

而绝大多数浏览器为了支持Cookie,具备了以下功能

  1. 解析HTTP相应结果中的Cookie数据,并保存到本地磁盘
  2. 读取本地磁盘上的Cookie数据,并把它添加到HTTP请求中

image.png
以上行为均是http协议会话机制的默认行为
当然也可以人工使用代码进行干预:

1. 在服务器端在cookies中新增一个cookie

  1. Cookie cookie = new Cookie("username", "马云");
  2. resp.addCookie(cookie);

2. 在服务器获取cookie中的值

  1. Cookie[] cookies = req.getCookies();
  2. Arrays.stream(cookies)
  3. .forEach((cookie -> System.out.println("cookie name:" + cookie.getName() + ",cookie value:" +cookie.getValue())));

3. setMaxAge方法

setMaxAge的方法来设置Cookie的有效期。参数expiry以秒为单位,它具有如下含义:

  • 当expiry大于0,就指示浏览器在客户端硬盘上保存Cookie的时间为expiry秒
  • 当expiry等于0,就指示浏览器删除当前的Cookie。
  • 当expiry小于0,就指示浏览器不要把Cookie保存到客户端磁盘。仅仅知识将该cookie存在于浏览器进程中,当浏览器关闭进程时,Cookie也就消失了。

浏览器默认的有效期为-1。

4. getMaxAge方法

读取coookie的有效期

课堂代码.: 写一个CookieServlet类,doGet()方法中,

  1. 先读取客户端所有cookie,把每个Cookie的名字,值,和有效期都写回给客户端,
  2. 向客户端新增一个cookie, maxAge默认
  3. 连续访问CookieServlet类之后,启动另一个不同的浏览器访问,观察是否有cookie

5. 设置path

  1. Cookie cookie = new Cookie("username", "马云");
  2. cookie.setPath("/servlet/member/");
  3. resp.addCookie(cookie);

如此设置一下, 则该cookie只对servlet工程,member模块前缀的servlet可见

3.2HttpSession

3.1 什么是HttpSession

session机制采用的是在服务器端保持 HTTP 状态信息的方案 。由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识具体的用户,这个机制就是Session机制

当程序需要为某个客户端的请求创建一个session时,服务器首先检查这个客户端的请求里是否包含了一个session标识(即sessionId),如果已经包含一个sessionId则说明以前已经为此客户创建过session,服务器就按照session id把这个session检索出来使用(如果检索不到,可能会新建一个,这种情况可能出现在服务端已经删除了该用户对应的session对象)。如果客户请求不包含sessionId,则为此客户创建一个session并且生成一个与此session相关联的sessionId,这个session id将在本次响应中返回给客户端保存。
image.png

3.2 HttpSession的生命周期

HttpSession在用户第一次访问Web应用中支持会话的任意一个程序,或者,web应用在session销毁后用户访问Web应用的程序时,tomcat会为新的访问创建一个会话。

在用户关闭浏览器或者到达指定时间之后,会话会被销毁。

ssession默认存活时间为30分钟

3.3 HttpSession常用的API

  1. getID()
  2. invalidate(): 销毁当前会话
  3. setAttribute(Streing name, Object value)
  4. getAttribute(String name)
  5. getAttributeNames()
  6. removeAttribute(String name)
  7. isNew()
  8. setMaxInactiveInterval()
  9. getMaxInactiveInterval()
  10. getServletContext()

4. 转发和包含

Servlet对象由Servlet容器创建,并且Servlet对象的Service()方法也有容器调用。一个Servlet对象可否直接调用另一个Servlet对象的Service()方法呢? 答案是否定的,因为一个Servlet对象无法获取另一个Servlet对象的引用。

但是Web应用在相应客户端的一个请求时,很有可能业务逻辑非常复杂并且代码量庞大,需要多个Web组件共同协作,才能生成响应结果,一个Servlet对象无法直接调用另一个Servlet对象的service()方法,但是Servlet规范为Web组件之间的协作提供了两种途径:

  • 请求转发:Servlet(原组件)先对客户请求做了一些预处理操作,然后把请求转发给其他Web组件(目标组件)来完成包括生成响应结果在内的后续操作
  • 包含:Servlet(原组件)把其他Web组件(目标组件)生成的响应结果包含到自身的响应结果中

转发和包含有几个共同特点:

  • 原组件和目标组件至始至终都共享同一个ServletRequest和ServletResponse对象
  • 目标组件都可以是Servlet,JSP或者HTML文档
  • 都依赖javax.servlet.RequesDispatcher接口

RequesDisDipatcher接口源码中只包含了两个方法:

  1. void forward(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
  2. void include(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;

在调用他们时,都要将当前ServletRequest对象和ServletReponse对象当作参数经行传递

而获取RequesDisDipatcher接口实例的方式有两个

  1. ServletContext的getRequestDispatcher(String path)方法,参数path只能是目标组件的绝对路径
  2. ServletRequest的getRequestDispatcher(String path) 方法,参数既可以是绝对路径,也可以是相对路径

绝对路径就是相对于web目录根路径

4.1请求转发

关键步骤:

  1. RequestDispatcher dispatcher = req.getRequestDispatcher("output"); //1.获取转发的实例
  2. dispatcher.forward(req, resp); //2. 转发操作

使用forword方法有几点要注意

  • 由于forward()方法先清空用于存放响应正文数据的缓冲区,因此Servlet原组件生成的响应结果不会被发送到客户端,只有目标组件生成的响应结果生成的响应结果才会被发送到客户端
  • 如果源文件在请求转发之前,已经提交了响应结果(例如调用了flushBuffer()方法,或者close方法,那么forward()方法会抛出异常ILLegalStateException异常。

4.2 包含

include方法可以将数个目标组件的响应结果都包含在自己的响应结果中
关键代码:

  1. RequestDispatcher source1 = req.getRequestDispatcher("source1");
  2. RequestDispatcher source2 = req.getRequestDispatcher("source1");
  3. source1.include(req, resp);
  4. source2.include(req, resp);

它的处理逻辑如下:

  • 如果目标组件为Servlet或者JSP,就调用他们的响应的service()方法,把该方法产生的响应正文添加到原组件的响应结果中,如果目标组件为HTML文档,就直接把文档的内容添加到原组件的响应结果中。
  • 返回源组件的服务方法中,继续执行后续代码块

原组件与被包含的目标组件的输出数据都会被添加到响应结果中
目标组件中对响应状态代码或者响应头所做的修改都会被忽略

5.重定向

Http协议规定了一种重定向机制,它的基本机制内容如下

  1. 用户在浏览器输入忒的那个URL,请求访问服务器端的某个组件
  2. 服务器端的组件返回一个状态代码为302的响应结果,该响应结果的含义规定:让浏览器端再请求访问另一个Web组件,响应结果中附带提供了另一个Web组件的URL,另一个Web组件有可能再同一个Web服务器上,也有可能再另一个Web服务器上
  3. 浏览器接收到这种响应结果后,再立即自动请求访问另一个web组件
  4. 浏览器端接受到来自另一个Web组件的响应结果

ServletAPI中HttpServletResponse接口的sendRedirect(String location)方法用于重定向,location必须是绝对路径或者外部网址。

6.过滤器

过滤器,顾名思义,就是过滤器,可以将某批servlet组件中重复功能的代码提取到过滤器中统一编码和执行,可以减少重复编码,提高开发效率和软件的可维护性。
image.png多个过滤器可以串联使用

6.1 Filter接口

方法 描述
init(FilterConfig filterConfig) 初始化方法,在Web应用启动的时候,servlet容器先创建包含了过滤器配置信息的FilterConfig对象,然后在创建Filter对象,接着嗲用Filter对象的init方法。参数filterConfig封装好了过滤器的初始化参数
doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) 完成实际的过滤业务操作,请求URL时,先调用filter的doFilter方法,然后再通过FilterChain参数的doFilter方法进行请求转发,做后续处理
destory() 释放资源

6.2 串联过滤器

过滤器可以将其串联起来使用,按照web.xml中的定义的先后顺序依次调用。
image.png

7.监听器

监听器,顾名思义,就是监听器,servlet可以创建监听器去监听其他对象的发生的事件,并在发生事件时采取响应的行动

servlet中监听对象主要是ServletContext, HttpSession和 ServletRequest 等域对象的创建和销毁事件,以及他们的属性发生修改的事件

ServletContextListener对象: ServletContext的监听器
ServletRequestListener对象: ServletRequest的监听器
ServletSessionListener对象: ServletSession的监听器

创建监听器方式

  1. 实现相应的的监听器
  2. web.xml中注册监听器
  1. <listener>
  2. <listener-class>com.class2.listener.MyListener</listener-class>
  3. </listener>

8. 使用注解的方式配置Servlet

之前都是使用Web.xml的方式来配置我们的开发的Servlet程序, Tomcat启动的时候会加载web.xml文件的配置信息到内存当中,并以此信息来初始化Servlet实例

从Servlet3 版本开始,为了简化对Web组件的发布过程,可以不必再web.xml文件中配置web组件, 而是直接再相关的类中使用Annotation标注来配置发布信息。

  1. WebServlet注解 ```java @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface WebServlet { String name() default “”; String[] value() default {}; String[] urlPatterns() default {}; int loadOnStartup() default -1; WebInitParam[] initParams() default {}; boolean asyncSupported() default false; String smallIcon() default “”; String largeIcon() default “”; String description() default “”; String displayName() default “”; }
  1. 2. WebFilter注解
  2. ```java
  3. @Target({ElementType.TYPE})
  4. @Retention(RetentionPolicy.RUNTIME)
  5. @Documented
  6. public @interface WebFilter {
  7. String description() default "";
  8. String displayName() default "";
  9. WebInitParam[] initParams() default {};
  10. String filterName() default "";
  11. String smallIcon() default "";
  12. String largeIcon() default "";
  13. String[] servletNames() default {};
  14. String[] value() default {};
  15. String[] urlPatterns() default {};
  16. DispatcherType[] dispatcherTypes() default {DispatcherType.REQUEST};
  17. boolean asyncSupported() default false;
  18. }
  1. WebListener
    1. @Target({ElementType.TYPE})
    2. @Retention(RetentionPolicy.RUNTIME)
    3. @Documented
    4. public @interface WebListener {
    5. String value() default "";
    6. }

9. 文件的上传下载

9.1 文件上传

(略,利用Apache开源类库实现文件上传,后续学习框架springmvc中再统一讲)

9.2 下载下载

下载文件是指把服务器端的文件发送到客户端。Servlet能够向客户端发送任意格式的文件数据。

  1. protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  2. OutputStream out;
  3. InputStream in;
  4. String filename = req.getParameter("filename");
  5. if (filename == null) {
  6. out = resp.getOutputStream();
  7. out.write("please input filename.".getBytes());
  8. out.close();
  9. return;
  10. }
  11. in = getServletContext().getResourceAsStream("/file/" + filename);
  12. int length = in.available();
  13. resp.setContentType("application/force-download"); //指定响应类型为下载
  14. resp.setHeader("Content-Length", String.valueOf(length)); //指定文件的大小
  15. resp.setHeader("Content-Disposition", "attachment;filename=\""+filename+"\""); //指定下载的文件名
  16. out = resp.getOutputStream();
  17. int bytesRead = 0;
  18. byte[] buffer = new byte[1024];
  19. while ((bytesRead = in.read(buffer)) != -1) {
  20. out.write(buffer, 0, bytesRead);
  21. }
  22. in.close();
  23. out.close();
  24. }

9.3 模拟生成随机验证码

  1. @Override
  2. protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  3. /**
  4. * 1.绘图
  5. */
  6. Random r = new Random();
  7. //创建一个位于缓冲区中的图像,
  8. BufferedImage image = new BufferedImage(90, 20, BufferedImage.TYPE_INT_RGB);
  9. //获得Graphics画笔
  10. Graphics g = image.getGraphics();
  11. g.setColor(new Color(255, 255, 255));
  12. //画一个矩形
  13. g.fillRect(0, 0, 90, 30);
  14. //随机设置画笔的颜色, 再设置画笔的字体,风格和大小
  15. g.setColor(new Color(r.nextInt(255), r.nextInt(255), r.nextInt(255)));
  16. g.setFont(new Font("微软雅黑", Font.BOLD | Font.ITALIC, 22));
  17. //生成一个随机的六位数
  18. String code = r.nextInt(1000000) + "";
  19. System.out.println("code = " + code); //打印再控制台
  20. g.drawString(code, 0, 20);
  21. //随机画八条干扰线
  22. for (int i = 0; i < 8; i++) {
  23. g.setColor(new Color(r.nextInt(255), r.nextInt(255), r.nextInt(255)));
  24. g.drawLine(r.nextInt(90), r.nextInt(20), r.nextInt(90), r.nextInt(20));
  25. }
  26. /**
  27. * 2.将图片写回客户端
  28. */
  29. response.setContentType("image/jpeg");
  30. ServletOutputStream out = response.getOutputStream();
  31. ImageIO.write(image, "jpeg", out);
  32. out.close();
  33. }

以上代码可以随机获取验证码返回客户端,例如:
image.png

问题:如何在服务端保存我们的验证码,用以校验
课堂代码: 使用单例模式的单例对象保存验证码

10. Servlet中提供的并发问题的解决方案

10.1 Servlet的线程不安全问题描述

在Internet中,一个Web应用可能被来自四面八方的客户并发访问(即同时访问), 而且有可能这些客户并发访问的是Web应用中的同一个Servlet。Servlet容器为了保证能同时响应多个客户的要求访问同一个Servlet的Http请求,通常会为每个请求分配一个工作线程,这些工作线程并发执行同一个Servlet对象的service()方法,去修改多个线程均可见的共享变量时,就可能会导致并发问题。

案例代码同Runnable接口实现线程的多窗口售票问题。解决方案也是相同,就是利用java的同步机制来解决。

10.2 对客户请求的异步处理

10.2.1 异步处理概述

在ServletAPI 3.0版本之前,Servlet容器针对每个HTTP请求都会分配一个工作线程。即对于每一次HTTP请求,Servlet容器都会从主线程池中取出一个空闲的工作线程,由该线程从头到尾负责处理请求,如果在相应某个HTTP请求的过程中涉及到进行I/O操作,访问数据库,或者其他耗时操作,那么该工作线程会被长时间占用,只有当工作线程完成了对当前HTTP请求的响应,才会释放回线程池以供后续请求使用。

在并发访问量很大的情况下,如果线程池中的许多工作线程都被长时间占用,这将严重影响服务器的并发访问性能,为了解决这种问题,从ServletAPI 3 开始,引入了异步处理机制,随后Servlet API 3.0中引入了非阻塞I/O类进一步增强异步处理能力。

Servlet异步处理的机制为: Servlet从HttpServletRequest对象中获取一个AsyncContext对象,该对象表示异步处理的上下文。AsyncContext把响应当前请求的任务传给一个新的线程,由这个新的线程来完成对请求的处理并向客户端返回响应结果。最初由Servlet容器为Http请求分配的工作线程便可以及时地释放回主线程翅,从而及时处理更多的请求。

总而言之,Servlet异步处理机制,就是把响应请求的任务从一个线程传给另一个线程来处理。
下图为异步处理示意图:
image.png

10.2.2 异步处理流程

  • 设置Servlet的配置asyncSupported=true
  • 主线程中request.startAsync()方法获取AsyncContext
  • 启动子线程,例如如下三种方式 :
    1. Runnable方式:asyncContext.start(new MyTask(asyncContext)),其中MyTask是一个Runnable的实现类
    2. Thread方式: new Thread(new MyTask(asyncContext)).start()
    3. 线程池方式: executor.execute(new MyTask(asyncContext))
  • 在子线程中调用complete()方法告知servlet容器任务完成,返回响应结果

AsyncContext接口部分源码

  1. public interface AsyncContext {
  2. ServletRequest getRequest();
  3. ServletResponse getResponse();
  4. void dispatch(String var1);
  5. void complete();
  6. void start(Runnable var1);
  7. void addListener(AsyncListener var1);
  8. void addListener(AsyncListener var1, ServletRequest var2, ServletResponse var3);
  9. void setTimeout(long var1);
  10. }

10.2.3 异步监听器AsyncListener

除了ServletContext, HttpSession和ServletRequest等有监听器之外,我们的异步机制也有其独有的监听器AsyncListener,该接口有四个方法,源码如下

  1. public interface AsyncListener extends EventListener {
  2. void onComplete(AsyncEvent var1) throws IOException; 异步线程执行完毕时调用
  3. void onTimeout(AsyncEvent var1) throws IOException; //异步线程超时时调用
  4. void onError(AsyncEvent var1) throws IOException; //异步线程出错时调用
  5. void onStartAsync(AsyncEvent var1) throws IOException; //异步线程开始时调用
  6. }

使用asyncContext的addListener(AsyncListener asycnListener)来在代码中注册该监听器

10.2.4 非阻塞式I/O的引入

目的和上述的异步是一致的,只是这里使用了非阻塞式I/O模型,当异步线程利用IO流读写大量数据时,会使得异步线程也处于阻塞状态(如超大附件上传,下载),异步线程也阻塞住了同样会削弱服务器的并发访问的能力,所以Servlet API在3.1开始,引入了非阻塞I/O机制,它建立在异步处理的基础之上。

阻塞式I/O和非阻塞式I/O简介(以读数据为例,写数据同理)

  1. 阻塞式I/O:当线程在通过输入流执行读操作时,如果输入流的刻度数据暂时还未准备号,那么当前线程会进入阻塞状态,只有当读到了数据或者到达了数据末尾,线程才会从读方法中退出。
  2. 非阻塞式I/O: 当线程在通过输入流执行 读操作时,如果发现输入流的可读数据还未准备号,那么当前线程不会进入阻塞状态,而是退出读方法,使其可以去马上执行其他任务,而不是阻塞在那里等待I/O完成。当数据准备完成后,再改变I/O状态,通知系统分配线程来处理。

servlet中引入的非阻塞式I/O模型

主要时引入了两个监听器:
ReadListener接口: 监听ServletInputStream输入流行为。
WriteListener接口: 监听ServletOutputStream输出流行为。

这里只介绍ReadListener,它的源码如下:

  1. public interface ReadListener extends EventListener {
  2. void onDataAvailable() throws IOException; //输入流中有可读数据时出发此方法
  3. void onAllDataRead() throws IOException; //输入流中所有数据读完时出发此方法
  4. void onError(Throwable var1); //输入流出现错误时出发此方法
  5. }

基本使用:
同样需要获取异步对象AsyncContext
在主Servlet类中通过request获取ServletInputStream
并且设置监听器ReadListener接口

  1. AsyncContext asyncContext = request.startAsync();
  2. ServletInputStream inputStream = request.getInputStream();
  3. inputStream.setReadListener(new MyReadListener(inputStream, asyncContext));

其中MyReadListener实现了ReadListener接口,我的案例代码实现如下

  1. class MyReadListener implements ReadListener {
  2. private ServletInputStream servletInputStream;
  3. private AsyncContext asyncContext;
  4. private StringBuilder sb = new StringBuilder();
  5. public MyReadListener(ServletInputStream inputStream, AsyncContext context) {
  6. this.servletInputStream = inputStream;
  7. this.asyncContext = context;
  8. }
  9. //输入流中有可读数据会调用此方法
  10. @Override
  11. public void onDataAonvailable() throws IOException {
  12. try {
  13. System.out.println("流中有可用数据" + LocalTime.now());
  14. //模拟数据读取5秒
  15. TimeUnit.SECONDS.sleep(5);
  16. int len;
  17. byte[] buffer = new byte[1024];
  18. while (servletInputStream.isReady()
  19. && (len = servletInputStream.read(buffer)) > 0) {
  20. String data = new String(buffer, 0, len);
  21. sb.append(data);
  22. }
  23. System.out.println("流中数据读取结束" + LocalTime.now());
  24. } catch (Exception e) {
  25. e.printStackTrace();
  26. }
  27. }
  28. @Override
  29. public void onAllDataRead() throws IOException {
  30. System.out.println("数据读取完成" + LocalTime.now());
  31. asyncContext.getRequest().setAttribute("msg", sb.toString());
  32. asyncContext.dispatch("/output");
  33. }

通过前端上传文件后,会出发onDataAonvailable()方法,所有数据读取完毕后会自动调用onAllDataRead() 方法,并在这个方法中调用asyncContext.dispatch(path); 来将请求派发给另一个servlet来处理。

11. 嵌入式服务器简介

目前比较流行的框架Springboot集成Web项目就是使用嵌入式的Tomcat服务器,即无需外部tomcat服务器来启动加载我们的web项目,而是将tomcat服务器嵌入我们的java项目当中,将其当作进程内的Servlet容器来运行,可以使得java应用程序更为灵活的控制Servlet容器。

下面我写了一个简单的通过java的main方法启动嵌入式tomcat服务器,并提供服务

  1. public class Application {
  2. private static Tomcat tomcat;
  3. private static final int DEFAULT_PORT = 8080; //默认端口
  4. //服务器启动
  5. public static void run(int port){
  6. tomcat = new Tomcat();
  7. //设置服务器以及虚拟主机的根路径
  8. tomcat.setBaseDir(".");
  9. tomcat.getHost().setAppBase(baseDir);
  10. //设置tomcat接受Http请求的监听端口号
  11. tomcat.setPort(port);
  12. //获取连接器,如果没有连接器,会获得一个默认的连接器
  13. Connector connector = tomcat.getConnector();
  14. //加入服务器默认的web应用,即在页面输入localhost:8080后默认访问的程序
  15. //第二个参数可以等同于外部tomcat下webapps下的ROOT项目的路径
  16. Context context1 = tomcat.addWebapp("", "D:\\developers\\javadeveloper\\workspace\\idea\\servlet\\out\\artifacts\\web_war_exploded");
  17. //addWebapp可以重复添加项目到tomcat容器中
  18. //启动tomcat
  19. try {
  20. tomcat.start();
  21. } catch (LifecycleException e) {
  22. e.printStackTrace();
  23. }
  24. //阻塞此线程,让其一直存在于后台
  25. StandardServer server = (StandardServer) tomcat.getServer();
  26. server.await();
  27. }
  28. public static void main(String[] args) {
  29. Application.run(Application.DEFAULT_PORT);
  30. }
  31. }

转载 https://www.yuque.com/mr_wan/java_web/servlet2