文章目录
- 第五章 Servlet 技术(下)
- 5.1 下载文件
- 5.2 上传文件
- 5.2.1 利用Apache开源类库实现文件上传
- 5.2.2 利用Servlet API 中的Part接口实现文件的上传
- 5.3 动态生成图像
- 5.4 读写Cookie
- 5.5 访问 Web 应用的工作目录
- 5.6 转发和包含
- 5.6.1 请求转发
- 5.6.2 包含
- 5.6.3 请求范围
- 5.7 重定向
- 5.8 访问Servlet容器内的其他Web应用
- 5.9 避免并发问题
- 5.9.1 合理决定在Servlet中定义的变量的作用域
- 5.9.2 使用java同步机制对多线程同步
- 5.10 对客户请求的异步处理
- 5.10.1 异步处理的流程
- 5.10.3 异步监听器
- 5.10.4 非阻塞I/O
第五章 Servlet 技术(下)
5.1 下载文件
下载文件是指把服务器端的文件发送给客户端。Servlet能够向客户端发送任意格式的文件数据。
通过ServeltContext的getResourceAsStrem()方法得到一个读取文件的输入流,通过response.getOutputStream()得到一个用于输出响应正文的输出流。
package com.whw.servelt;import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;public class DownloadServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {OutputStream out;//输出响应正文的输出流InputStream in; //读取文件的输入流String filename = req.getParameter("filename");if (filename == null){out = resp.getOutputStream();out.write("Please input filename.".getBytes());out.close();return;}//获取读取本地文件的输入流in = getServletContext().getResourceAsStream("store/"+filename);int len = 0;int length = in.available();//设置响应正文的MIME类型resp.setContentType("application/force-download"); //当浏览器收到这种类型的响应正文时,会以下载的方式来处理响应正文resp.setHeader("Content-Length",String.valueOf(length));resp.setHeader("Content-Disposition","attachment;filename=\"" + filename + "\"");out = resp.getOutputStream();byte[] bytes = new byte[length];while ((len = in.read(bytes)) != -1){out.write(bytes,0, length);}in.close();out.close();doPost(req, resp);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {doGet(req, resp);}
}
application/force-download,响应正文的MIME类型,浏览器会以下载的方式来处理响应正文。
5.2 上传文件
当客户端向服务器上传文件时,客户端发送的HTTP请求正文采用 “mulitpart/form-data” 数据类型,它表示复杂的包括多个子部分的复杂的复合型表单。
<form name="uploadFrom" enctype="multipart/form-data" method="post" action="/upload">
5.2.1 利用Apache开源类库实现文件上传
- fileupload软件包(commons-fileupload-X.jar):负责上传文件的软件包。
- I/O软件包(commons-i/o-X.jar):负责输入输出的软件包。
fielUpload软件包中的主要接口和类的类框图
对于一个正文部分为 "multipart/-form-data"类型的HTTP请求,fileupload软件包把请求正文中的每子部分看做时一个FileItem对象。FileItem对象分为两种类型:
- formField:普通表单域。
- 非formField:上传文件类型。
FileItemFactory时创建FileItem对象的工厂。DiskFileItemFactory类和DiskFileItem类分别实现了FileItemFactory接口和FileItem接口。DiskFileItem类表示基于硬盘的FileItem,DiskFileItem类能够把客户端上传的文件数据保存到硬盘上。DiskFileItemFactory是创建DiskFileItem的工厂。
//创建一个基于磁盘的FileItem工厂
DiskFikeItemFactory factory = new DiskFileItemFactory();
//设置向硬盘写数据时的缓冲区大小
factory.setSizeThreshold();
//设置临时目录
factory.setRepository(new File(tempFilePath));
ServletFileUpload类为文件上传处理器。与FileItemFactory关联。
//创建一个文件上传处理器ServletFileUpload upload = new ServletFileUpload(factory);//设置允许上传文件的最大尺寸upload.setSizeMax(4*1024*1024);
ServletFileUpload 类的parseRequest(RequestContext ctx)方法能够解析HttpServletReqest 对象中的符合表单数据,返回包含一组Fileltem对象的List集合。
List<FileItem> items = upload.parseRequest(req);for (FileItem item : items){if (item.isFormField()){//处理普通的表单域} else {//处理上传文件}}
package com.whw.servlet;import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.RequestContext;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.servlet.ServletRequestContext;import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;public class UploadFileServlet extends HttpServlet {String filePath;String tempFilePath;@Overridepublic void init(ServletConfig config) throws ServletException {super.init(config);ServletContext servletContext = config.getServletContext();filePath = servletContext.getRealPath("/store");tempFilePath = servletContext.getRealPath("/temp");}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {req.setCharacterEncoding("utf-8");//创建基于磁盘文件的FileItem工厂DiskFileItemFactory factory = new DiskFileItemFactory();//设置缓冲区factory.setSizeThreshold(4*1024);//设置临时目录factory.setRepository(new File(tempFilePath));//创建文件上传处理器ServletFileUpload upload = new ServletFileUpload(factory);//设置上传文件的最大尺寸upload.setSizeMax(4*1024*1024);//通过HttpservletRequest创建一个RequestContext对象RequestContext requestContext = new ServletRequestContext(req);try {List<FileItem> items = upload.parseRequest(requestContext);for (FileItem item : items){if (item.isFormField()){processFormField(item);} else {processUploadFile(item,resp.getWriter());}}} catch (Exception e) {e.printStackTrace();}}@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {doPost(req,resp);}private void processFormField(FileItem item){String fieldName = item.getFieldName();String string = item.getString();System.out.println(fieldName + ":" + string);}private void processUploadFile(FileItem item, PrintWriter writer) throws Exception {String name = item.getName();File file = new File(filePath + "/" + name);long size = item.getSize();if (name.equals("") || size == 0){return;}item.write(file);writer.println(name+" is saved.");}
}
5.2.2 利用Servlet API 中的Part接口实现文件的上传
Servlet API 会把复合表单中的每个子部分看做一个Part对象,Part对象提供了用于读取子部分中各个信息的方法。
- getHeader(String name):读取子部分的请求头中特定的选项的值。
- getContentType():读取子部分的请求正文的数据类型。
- getSize():读取子部分的请求正文的长度,以字节为单位。
- getName():读取子部分的名字,和HTML中
<input>
元素的name相同。 - write(String filename):把子部分的请求正文写到参数filename指定的文件中。 ·
package com.whw.servlet;import org.apache.commons.lang.StringUtils;import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;@WebServlet(name = "uploadPart",urlPatterns = "/uploadPart")
@MultipartConfig 标识Servlet支持文件上传
public class UploadServletPart extends HttpServlet {private String filePath; //存放上传文件的目录@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {req.setCharacterEncoding("utf-8");PrintWriter out = resp.getWriter();Collection<Part> parts = req.getParts();for (Part part : parts){//获取子部分的头,content-disposition 为头描述,//对于普通域 form-data; name="username"//对于文件 form-data; name="file1"; filename="QQ图片20200912214424.jpg"String header = part.getHeader("content-disposition");String name = part.getName();String contentType = part.getContentType();long size = part.getSize();System.out.println(header);System.out.println(name);System.out.println(contentType);System.out.println(size);if (StringUtils.isBlank(contentType)){//普通域,通过参数名获取参数值String parameter = req.getParameter(name);System.out.println(parameter);} else if (part.getName().indexOf("file") != -1){//因为表单域的Input名为file1 file2//获取上传文件的文件名String fileName = getFileName(header);part.write(filePath + File.separator + fileName);out.print(fileName + " is save");}}}/* 从一个Part的请求头中获取文件名 */public String getFileName(String header) {//如果一个Part的请求头的内容为://form-data; name="file1"; filename="FromClient.rar"//那么其中文件名为“FromClient.rar”String fileName=header.substring(header.lastIndexOf("=")+2,header.length()-1);return fileName;}@Overridepublic void init(ServletConfig config) throws ServletException {super.init(config);ServletContext servletContext = config.getServletContext();filePath = servletContext.getRealPath("/store");}
}
5.3 动态生成图像
- java.awt.image.BufferedImage:表示位于缓存中的图像。
- java.awt.Graphics:表示画笔。
- javax.imageio.ImageIO:能够把原始图像转换为特定的格式,并且能利用外界的输出流来输出图像数据。
5.4 读写Cookie
Cookie的英文意思是点心,它是客户端访问Web服务器时,服务器在客户端硬盘上存放的数据,服务器可以根据Cookie来跟踪客户状态,这对于需要区别客户场合特别有用。
Cookie的运行机制是HTTP协议规定的,多数Web服务器和浏览器都支持Cookie。Web服务器为了支持Cookie,需要具备以下功能:
- 在HTTP响应中添加Cookie。
- 解析HTTP请求中的Cookie。
浏览器为了支持Cookie,需要具备以下功能:
- 解析HTTP响应结果中的Cookie。
- 把Cookie保存到本地硬盘。
javax.servlet.http.Cookie类来表示每个Cookie。每个Cookie对象包含一个Cookie的名字和Cookie的值。
Cookie cookie = new Cookie("username","Tom");response.addCookie(cookie);
从请求中取出Cookie
Cookie[] cookies = req.getCookies();for(Cookie cookie : cookies){cookie.getName();//获取cookie的名字cookie.getValue();//获取Cookie的值}
Servlet在向客户端写Cookie时,可以根据Cookie类的setMaxAge(int expiry)来设置Cookie的有效期,单位为秒。
- expiry > 0:表示Cookie在客户端硬盘上保存的时间为expiry。
- expiry = 0:表示浏览器删除当前Cookie。
- expiry < 0:表示浏览器不要把Cookie保存到客户端硬盘。
app1应用中的一个web组件X在浏览器端保存了一个Cookie。处于安全原因,只有app1中的web组件能够访问该Cookie。
如果要改变Cookie的共享范围,那么app1应用中的web组件在写Cookie的时候,可以通过setPath(String path)和setDomain(String domain)来设置Cookie的path和domain属性。
- 让同一个Tomcat中的应用共享Cookie(app1和app2)。app1中的web组件设置Cookie时的代码如下:
Cookie cookie = new Cookie("username","Tom");cookie.setPath("/"); // "/"表示Tomcat的根路径,因此同一个Tomcat中的web应用能够共享该Cookie。response.addCookie(cookie);
- 只能Tomcat服务器A中的app2应用访问。
Cookie cookie = new Cookie("username","Tom");cookie.setPath("/app2/"); response.addCookie(cookie);
此时,app1应用也无法访问该Cookie,只有app2可以。
- 只能让Tomcat服务器A中的app1应用中位于“/sub”子路径下的web组件访问该Cookie。
Cookie cookie = new Cookie("username","Tom");cookie.setPath("/app1/sub/"); response.addCookie(cookie);
- 让Tocmat服务器B下的所有web应用访问,如果Tocmat服务器B的域名为 “www.cat.com”。
Cookie cookie = new Cookie("username","Tom");cookie.setDomain(".cat.com");response.addCookie(cookie);
setDomain()的参数为 “.cat.com”,该参数必须以 “.” 开头。如果想限定在Tomcat服务器B中的某个web应用访问,这同前三点一样设置setPath()即可。
5.5 访问 Web 应用的工作目录
每个Web应用都有一个工作目录。Servlet容器会把与这个Web应用有关的临时文件存放在这个目录下。
<XATALINA_HOME> /work/[enginename/[hostname]/[contextpath]
如helloapp应用被发布到Tomcat服务器的名为 “Catalina” 的Engine的localhost的虚拟主机中。那么默认工作目录为:
<XATALINA_HOME> /work/Catalina/localhost/helloap
可以在<context>
元素中配置workDir属性来指定工作目录。
Servlet可以通过以下方式来获取当前web应用的工作目录。
File workDir = (File) getServletContext().getAttribute("javax.servlet.context.tempdir");
5.6 转发和包含
Servlet对象时由Servlet容器创建的,Servlet对象的service()方法也是由容器调用。因此一个Servlet对象时无法直接调用另一个Servlet对象的service()方法的。因为一个Servlet对象时服务获得另一个Servlet对象的引用的。
Servlet规范为Web组件之间的协作提供了两种途径:
- 请求转发:servlet(源组件)先对客户请求做一些预处理,然后把请求转发给其他Web组件来完成包括响应结果在内的后续操作。
- 包含:源组件把其他Web组件生成的响应结果包含到自身的响应结果中。
请求转发与包含具有以下相同的特点:
- 源组件和目标组件处理的都是同一个客户请求,源组件和目标组件共享同一个S而vletRequest对象和ServletResponse对象。
- 目标组件都可以是Servlet、JSP或HTML。
- 都依赖javax.servlet.RequestDispatcher接口。
javax.servlet.RequestDispatcher 接口表示请求分发器。有两个方法:
// 把请求转发给目标组件public void forward(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException ;// 包含目标组件的响应结果 public void include(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException;
Servlet可以通过两种方式获取RequestDispatcher对象:
- 调用ServletContext对象的 getReqeustDispathcer(String path)方法。path表示目标组件的路径。
- 调用ServletRequest对象的 getReqeustDispathcer(String path)方法。path表示目标组件的路径。
两种方式的区别在于第一种需要填写绝对路径。第二种既可以是绝对路径,也可以是相对路径。绝对路径即以"/“开头的路径,”/" 表示当前web应用的URL入口。所谓相对路径,就是指相对于当前Servlet的路径,不一“/”开头。
5.6.1 请求转发
dispatcher.forward(request,response)方法的处理流程:
- 清空位于存放响应正文的缓冲区。
- 如果目标组件为Servlet或JSP,就调用它们的servie()方法,该方法产生响应结果发送到客户端。如果目标组件为文件系统中的静态HTML文档,就读取文档中的数据并把它发送到客户端。
请求转发具有以下特征:
- 由于forword()方法先清空用于存放响应正文数据的缓冲区。因此Servlet源组件生成的响应结果不会被发送到客户端。至于目标组件生成的响应结果才会被发送到客户端。
- 如果源组件在进行请求转发之前就提交了响应结果(如调动ServletResponse的flushBuffer()方法),或者调用了ServletResponse关联的输出流的close()方法,那么forword()方法会抛出异常,为了避免该异常,不应该在源组件中提交响应结果。
5.6.2 包含
RequestDispatcher对象的include()方法处理流程:
- 如果目标组件为Servlet或JSP,就调用它们的service()方法,把该方法生成的响应结果添加到源组件的响应结果中,如果为HTML文档,就直接把文档内容添加到源组件的响应结果中。
- 返回原组件的服务方法中,继续执行后序代码块。
与请求转发相比,具有以下特点:
- 源组件与被包含的目标组件的输出数据都会被添加到相应结果。
- 在目标组件中对响应状态码或者响应头做的修改都会被忽略。
5.6.3 请求范围
请求范围是指服务器响应一次客户请求的过程,从Servlet接收到一个客户请求开始,到返回响应结果结束。
转发或包含源组件和目标组件都是在同一个请求中。
5.7 重定向
重定向的运作流程:
- 用户在浏览器端输入URL,请求访问服务器端的某个组件。
- 服务器端返回一个302的响应结果,该响应结果的含义为:让浏览器再请求访问另一个Web组件。响应结果中含有另一个Web组件的URL。另一个Web组件有可能在同一个服务器上,有可能不在同一个服务器上。
- 浏览器收到这种响应结果后,再立即自动请求访问另一个web组件。
- 浏览器接收到来自另一个web组件的响应结果。
HttpServletResponse接口的sendRedirect(String location)方法用于重定向。
重定向的特点:
- Servelt源组件的结果不会被发送到客户端,只有重定向的目标组件的响应结果才会发送到客户端。
- 如果源组件在重定向之前已经提交了响应结果,sendRedirect()方法会抛出IllealStateException。
- 源组件和目标组件不是同一个请求,不共享请求范围内的数据。
- 对于response.sendRedirect(String location)方法中的参数location,如果以 “/” 开头,表示当前服务器的根路径,如果以 “http://” 开头表示一个完整的URL。
- 目标组件不必是痛一个web应用上的组件。
5.8 访问Servlet容器内的其他Web应用
对于同一个容器中的A应用和B应用,A只要得到了B的ServletContext对象,就能够访问B中的资源了。
ServletContext接口的getContext(String uriPath)用于得到其他对象的ServletContext对象。uriPath为其他应用的URL入口。
//getServletContext() 获取当前应用的ServletContextg对象,通过当前应用的ServletContext对象获取其他应用的ServletContext context = getServletContext().getContext("/B");
一个Web应用随意访问其他Web应用的各种资源是不安全的,所以在Tocmat中<Context>
元素的crossContext属性用于设置该选项。
- 当crossContext为false,那么
<Context>
对应的应用无法得到其他Web应用的ServletContext对象。 - 当crossContext为true时,可以得到其他Web应用的ServletContext对象。
5.9 避免并发问题
Servlet容器为了保证能同时响应多个客户的要求访问同一个Servlet的HTTP的请求,通常会为每个请求分配一个工作线程,这些线程并发执行同一个Servlet对象的service()方法。所以可能会导致并发问题。
public class HelloServlet1 extends GenericServlet {private String username=null; //username为实例变量/** 响应客户请求*/public void service(ServletRequest request,ServletResponse response)throws ServletException, IOException {//把username请求参数赋值给username实例变量 username=request.getParameter("username");try{Thread.sleep(3000); //特意延长响应客户请求的时间}catch(Exception e){e.printStackTrace();}//设置HTTP响应的正文的MIME类型及字符编码response.setContentType("text/html;charset=GBK");/*输出HTML文档*/PrintWriter out = response.getWriter();out.println("<html><head><title>HelloServlet</TITLE></head>");out.println("<body>");out.println("你好: "+username);out.println("</body></html>");out.close(); //关闭PrintWriter}
}
http://localhost:8080/helloapp?username=老鼠
http://localhost:8080/helloapp/hello?username=小鸭
结果:
你好:小鸭
你好:小鸭
在解决并发问题时,主要遵循以下原则:
- 根据实际需要,决定变量的作用域,时实例变量还是局部变量。
- 对于多个线程同时访问共享数据而导致的并发问题的场合,使用java同步机制对线程进行同步。
- 不提倡使用javax.servlet.SingleThreadModel接口。
5.9.1 合理决定在Servlet中定义的变量的作用域
局部变量和实例变量的区别:
- 局部变量在方法中定义,每当一个线程执行局部变量所在的方法,在线程的堆栈中就会创建这个局部变量,当线程执行完这个方法,局部变量就会结束生命周期。
- 实例变量在类中定义,类的每个实例都会都拥有自己的实例变量。当一个实例结束生命周期,那么他的实例变量也就结束生命周期。如果同一时刻多个线程执行同一个实例的方法,而这个方法会访问一个实例变量,那么这些线程访问的是同一个实例变量。
5.9.2 使用java同步机制对多线程同步
public class AdderServlet2 extends GenericServlet{private int sum=100; //sum为实例变量/** 响应客户请求*/public void service(ServletRequest request,ServletResponse response)throws ServletException, IOException {//获得increase请求参数 int increase=Integer.parseInt(request.getParameter("increase"));//设置HTTP响应的正文的MIME类型及字符编码response.setContentType("text/html;charset=GB2312");/*输出HTML文档*/PrintWriter out = response.getWriter();out.println("<html><head><title>AdderServlet</TITLE></head>");out.println("<body>");synchronized(this){ //使用同步代码块out.println(sum+"+"+increase+"=");try{Thread.sleep(3000); //特意延长响应客户请求的时间}catch(Exception e){e.printStackTrace();}sum+=increase;out.println(sum);}out.println("</body></html>");out.close(); //关闭PrintWriter}
}
5.10 对客户请求的异步处理
Servlet API 3.0之前,Servlt容器会为每个HTTP请求都分配一个工作线程,即对每一次HTTP请求,容器都会从主线程池中取出一个空闲工作线程,由该线程处理请求,当某个请求中涉及到一些耗时的操作时(如IO),那么该线程会长期被占用,只有完成了对当前HTTP请求的响应,才能释放回线程池供后序使用。在并发访问量比较大的情况下,会影响服务器的访问性能。所以在3.0开始,引入的异步处理机制。
Servlet的异步处理机制为:Servlet从HttpServletRequest对象中获取一个AsyncContext对象,该对象表示异步处理的上下文。
AsyncContext把响应请求的任务传给另一个新的现场,由新的线程来完成对请求的处理并向客户端返回响应结果。最初由Servelt容器为HTTP请求分配的工作线程便可以及时的释放回主线程,从而及时的处理更多的请求。
所以,Servlet的异步处理机制就是把响应请求的任务从一个线程传给另一个线程来处理。
5.10.1 异步处理的流程
- 在Servlet类中把@WebServlet标注的asyncSuport属性设为true,使得该Servlet支持异步处理。
@WebServlet(name = "async", urlPatterns = "/async",asyncSupported = true)
在web.xml中的配置
<servlet><servlet-name>async</servlet-name><servlet-class>AsyncServlet</servlet-class><async-supported>true</async-supported></servlet>
- 通过ServletRequest对象的startAsync()方法获取AsyncContext对象。
AsyncContext asyncContext = req.startAsync();
AsyncContext 接口为异步处理请求提供了上下文,它具有以下方法:
//获取上下文中的ServletRequest 对象ServletRequest getRequest();//获取上下文中的ServletResponse 对象ServletResponse getResponse();boolean hasOriginalRequestAndResponse();void dispatch();//把请求派给参数指定的web组件void dispatch(String path);void dispatch(ServletContext var1, String var2);//告诉Serlvet容器任务完成,返回响应结果void complete();//启动一个异步线程void start(Runnable var1);//添加一个异步监听器void addListener(AsyncListener var1);void addListener(AsyncListener var1, ServletRequest var2, ServletResponse var3);<T extends AsyncListener> T createListener(Class<T> var1) throws ServletException;//设置异步线程处理请求任务的超时时间(毫秒)void setTimeout(long var1);long getTimeout();
- 调用setTimeout(long timeout)设置超时时间。(不是必须的)
- 启动异步线程来执行处理请求的任务。
- 调用complete()方法来告诉容器任务完成,或者调用dispatch()方法把请求转发给其他Web组件。
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;@WebServlet(name = "async",urlPatterns = "/async",asyncSupported = true)
public class AsyncServlet extends HttpServlet {//线程池ThreadPoolExecutor executor = new ThreadPoolExecutor(100,200,5000L, TimeUnit.MICROSECONDS, new ArrayBlockingQueue<Runnable>(100));@Overrideprotected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setContentType("text/plain;charset=GBK");AsyncContext asyncContext = req.startAsync();asyncContext.setTimeout(1*60*1000);//第一种启动异步线程的方式asyncContext.start(new MyTask(asyncContext));//第二中启动异步线程的方式Thread thread = new Thread(new MyTask(asyncContext));thread.start();//第三种启动异步线程的方式executor.execute(new MyTask(asyncContext));}@Overridepublic void destroy() {//关闭线程池executor.shutdown();}class MyTask implements Runnable{private AsyncContext asyncContext;public MyTask(AsyncContext asyncContext){this.asyncContext = asyncContext;}@Overridepublic void run() {ServletRequest request = asyncContext.getRequest();ServletResponse response = asyncContext.getResponse();try {PrintWriter out = response.getWriter();out.print("处理完成");asyncContext.complete();} catch (IOException e) {e.printStackTrace();}}}
}
第二种方式通过 “new Thread()” 语句亲自创建新的线程,把它作为异步线程,当大量的用户并发访问时,会导致服务器创建大量的新线程,这会大大降低服务器的运行性能。
第三种方式是使用线程池,所有的异步线程都放在线程池中。下图表示主线程池和异步线程池的关系。
使用线程池的有点事可以更加灵活的根据实际需求来设置线程池。
以下是线程池类的构造方法:
public ThreadPoolExecutor(int corePoolSize, //线程池维护的线程最小数量int maximumPoolSize,//线程池维护的线程最大数量long keepAliveTime,//线程池维护的线程允许的空闲时间TimeUnit unit,//空闲时间阿德单位BlockingQueue<Runnable> workQueue)//线程池使用的缓冲队列
ThreadPoolExecutor类的excute(Runnable r)方法会从线程池中取出一个空闲的线程来执行指定参数的任务。
executor.execute(new MyTask(asyncContext));
5.10.3 异步监听器
在处理异步请求的时候,可以利用异步监听器AsyncListener来捕获并处理异步线程运行中的特定事件。AsyncListener声明了四个方法:
//异步线程执行完毕时调用void onComplete(AsyncEvent var1) throws IOException;//异步线程执行超时时调用void onTimeout(AsyncEvent var1) throws IOException;// 异步线程执行出错是调用void onError(AsyncEvent var1) throws IOException;//异步线程开始时调用void onStartAsync(AsyncEvent var1) throws IOException;
注册监听器
asyncContext.addListener(new AsyncListener() {@Overridepublic void onComplete(AsyncEvent asyncEvent) throws IOException {}@Overridepublic void onTimeout(AsyncEvent asyncEvent) throws IOException {}@Overridepublic void onError(AsyncEvent asyncEvent) throws IOException {}@Overridepublic void onStartAsync(AsyncEvent asyncEvent) throws IOException {}});
5.10.4 非阻塞I/O
非阻塞I/O与阻塞I/O的概念:
- 阻塞I/O包括以下两种情况
- (1)当一个线程在通过输入流执行 读操作时,如果输入流的可读数据还没有准备好,那么当前线程就会进入阻塞状态(也可以理解为等待),只有当读到了数据或者达到了数据末尾,线程才会从读方法中退出。
如:服务器端读取客户端发送的请求数据时,如果请求数据很大(传文件等),那么这些数据在网络上传输需要一定的时间,此时服务器读取请求数据的线程可能会进入阻塞状态。 - (2)当一个线程通过输出流进行 写操作时,因为某种原因,暂时不能向目的地写数据,那么当前线程就会进入阻塞状态,只有完成了写操作,线程才会从写方法中退出。
如:下载文件时,服务器向客户端发送的响应正文比较大,需要在网络上传输一些时间,这时,服务器端负责输出响应结果的线程可能进入阻塞状态。 非阻塞I/O: - (1) 当一个线程通过输入流执行读操作时,如果输入流的可读数据暂时还未准备好,那么当前线程不会进入阻塞状态,而是立即退出读方法。只有当输入流中有可读数据时,才进行读操作。
- (2) 当一个线程通过输出流执行写操作时,因为某种原因,暂时不能向目的地写数据,那么当前线程不会进入阻塞状态,而是立即退出读方法。只有可以向目的地写数据时,才进行写操作。
java中,传统的输入/输出操作都采用的阻塞I/O方方式,所以,从Servlet API 3.1开始引入了非阻塞I/O,它建立在异步处理的基础上,引入了两个监听器:
- ReadListener接口:监听ServletInputStream输入流的行为。
//输入流中有可读数据时触发此方法void onDataAvailable() throws IOException;// 输入流中所有数据读取完触发void onAllDataRead() throws IOException;// 输入操作出现错误触发void onError(Throwable var1);
- WriteListener接口:监听ServeltOutputStream输出流的行为。
//可以向输出流写数据时触发void onWritePossible() throws IOException;// 输出操作出现错误void onError(Throwable var1);
支持异步处理的Servlet类中进行如下操作:
ServletInputStream input= req.getInputStream();ServletOutputStream output= resp.getOutputStream();//MyReadListener自定义的读监听器input.setReadListener(new MyReadListener(input, asyncContext));//MyWriteListener自定义的写监听器output.setWriteListener(new MyWriteListener(output, asyncContext));
在读监听器或写监听器类中写包含非阻塞I/O的代码。