Servlet3.0
学习教程:https://www.bilibili.com/video/BV1hx411G7Dv?from=search&seid=1315587023546661230
案例项目地址:https://gitee.com/LiuDaiHua/servlet3.0
https://blog.csdn.net/boli1020/article/details/16113789
https://blog.51cto.com/nxlhero/2442019
https://www.ibm.com/developerworks/cn/java/j-lo-comet/index.html
Servlet3.0规范是JavaEE6.0规范中的子规范。其要求运行环境最低是JDK6.0,Tomcat7.0。而之前学习的是Servlet2.5版本的规范,其是JavaEE5.0规范的子规范。其要求运行环境最低是JDK5.0,Tomcat5.0。
在Eclipse中使用servlet3.0规范,则需在创建动态Web工程时就要指定。其具体用法是,在Eclipse中在创建动态Web工程时,指定创建的“动态Web模块版本”为3.0版本。此时创建的Web工程中默认是没有web.xml文件的。
Servlet3.0的若干新特性极大的简化了Web应用的开发和部署。
注意:Servlet4.0才开始支持jdk8的,而Tomcat9.0.x才开始使用Servlet4.0,所以当使用Tomcat9.0.x时要求最低的java环境是java8。
由于Idea中默认只集成了Servlet4.0,没关系我们就使用选择4.0的版本,不影响我们测试Servlet3.0功能。在Servlet3.0中已经不在需要web.xml了,所以我们不用在勾选。如果你勾选了,IDEA会给你创建一个web.xml,这个时候你要注意一下该文件里是使用的Servlet3.0还是2.5的,以防对我们接下来的开发有影响。
上图我是选择Java而不是Java Enterprise(企业级项目),当然你也可以像下面这样
图4:点击Java Enterprise创建web项目得到的结果
如果你选的是Java Enterprise创建web项目时就需要指定Tomcat版本,并且创建好的项目默认依赖了你所指定版本的开发包(例如servlet-api.jar等,如上图4),如果你是点击Java创建的项目,就需要在项目创建好后,手动添加Configuration和tomcat依赖包(如下图)
更推荐使用选择Java创建的web项目,不要直接在创建项目时就依赖tomcat,而是在开发时,动态选择tomcat的方式。
手动添加tomcat,点击Add Configuration,我们创建一个tomcat server,选择Tomcat9版本,因为Tomcat9才开始支持Java8新语法,
如果出现红色灯泡,是因为tomcat启动我们的项目需要先把我们的项目达成一个war包,这个war包打在哪里,生成的war包名字是什么,这些你可以自己点击File->Project Structure->Artifacts,可以看到有一个默认的设置,你可以手动设置war包的名称,和部署的位置。如果你不想修改这些,可以直接点击创建tomcat server时的那个fix小灯泡,它会选择Artifacts里的默认设置去填充到Deployment里(点击后,红色小灯泡消失,Deployment多了一个war包,Apply即可。):
tomcat server已经添加完成,现在我们如果创建Servlet,会报错提示缺少javax.servlet依赖包,我们需要手动添加,点击File->Project Settings->Libraries-> + -> Java -> 找到tomcat环境下lib包里的servlet-api.jar和jsp-api.jar添加进去-> Apply。
点击src目录,选择new->Servlet -> HelloServlet,我是在com.wonders.myservlet包下建的,默认创建的如下:
1 package com.wonders.myservlet; 2 3 import javax.servlet.ServletException; 4 import javax.servlet.annotation.WebServlet; 5 import javax.servlet.http.HttpServlet; 6 import javax.servlet.http.HttpServletRequest; 7 import javax.servlet.http.HttpServletResponse; 8 import java.io.IOException; 9 10 @WebServlet(name = "HelloServlet") 11 public class HelloServlet extends HttpServlet { 12 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 13 14 } 15 16 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 17 18 } 19 }
注意一点,它是使用注解@WebServlet声明Servlet的,注解中name指的是Servlet名称,Servlet的mapping还没写,一般我都不指定名称,使用value指定映射了路径就行,注意要写/,value默认可以省略:
@WebServlet("/HelloServlet") public class HelloServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.getWriter().write("Hello Servlet3.0,I'm late!"); }
启动后直接访问http://localhost:8080/servlet3_java_war_exploded/即可看到首页内容,访问http://localhost:8080/servlet3_java_war_exploded/HelloServlet即可看到打印的信息。
Servlet3.0规范中允许在定义的Servlet、Filter与Listener三大组件时使用注解,而不在用web.xml进行注册了。Servlet规范允许Web项目没有web.xml配置文件。
Servlet3.0规范中使用@WebServlet()注解来注册当前的Servlet类。该注解具有多个属性,常用属性的类型与意义如下表所示:
序号 |
属性名 |
属性类型 |
属性说明 |
1 |
urlPatterns |
String[] |
相当于<url-pattern/>的值。将该属性匹配的请求交给该Servlet处理。 |
2 |
value |
String[] |
与urlPatterns意义相同,但此属性名可以省略。不能与urlPatterns属性同时使用。 |
3 |
name |
String |
相当于<servlet-name/>的值 |
4 |
loadOnStartup |
int |
相当于<loadOnStartup/>,默认值为-1 |
1 package com.wonders.myservlet; 2 3 import javax.servlet.ServletException; 4 import javax.servlet.annotation.WebInitParam; 5 import javax.servlet.annotation.WebServlet; 6 import javax.servlet.http.HttpServlet; 7 import javax.servlet.http.HttpServletRequest; 8 import javax.servlet.http.HttpServletResponse; 9 import java.io.IOException; 10 import java.io.PrintWriter; 11 12 @WebServlet(name = "param-servlet", 13 value = {"/params", "/helloparam"}, 14 initParams = { 15 @WebInitParam(name = "age", value = "20"), 16 @WebInitParam(name = "sex", value = "1"), 17 }, 18 loadOnStartup = 2 // tomcat启动时就初始化这个servlet 19 ) 20 public class ParamServlet extends HttpServlet { 21 @Override 22 public void init() throws ServletException { 23 super.init(); 24 System.out.println("ParamServlet初始化了"); 25 } 26 27 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 28 doGet(request, response); 29 } 30 31 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 32 response.setContentType("text/html;charset=UTF-8"); 33 PrintWriter writer = response.getWriter(); 34 writer.append("Hello Servlet3.0,I'm late!<br/>"); 35 36 // 获取servlet名称 37 writer.append("servletName = " + this.getServletName() + "<br/>"); 38 39 // 获取初始化参数 40 writer.append("initParams sex = " + this.getInitParameter("sex") + "<br/>"); 41 } 42 }
Servlet3.0规范中使用@WebFilter()注解来注册当前的Filter类。该注解具有多个属性,常用属性的类型与意义如下表所示:
序号 |
属性名 |
属性类型 |
属性说明 |
1 |
urlPatterns |
String[] |
相当于定义在<filter-mapping>元素下的<url-pattern/>的值。将该属性匹配的请求都交给该Filter处理。 |
2 |
value |
String[] |
与urlPatterns意义相同,但此属性名可以省略。不能与urlPatterns属性同时使用。 |
3 |
servletNames |
String[] |
相当于定义在<filter-mapping>元素下的 <servlet-name/>的值。表示拦截该Servlet的请求。 |
4 |
dispatchTypes |
DispatcherType[] |
相当于定义在<filter-mapping>元素下的<loadOnStartup/>,默认值为-1。表示拦截Dispatcher指定的值,例如:拦截forward请求 |
如果你不是使用Tomcat9开发web项目或者说你依赖的servlet-api.jar不是tomcat9以上的,你写的Filter除了需要实现doFilter方法之外,还需要实现init和destory方法,这两个方法在Tomcat9中使用了java8的新语法【Servlet4.0中Filter做出了一点改变】,它们被声明为默认方法。
package com.wonders.myservlet; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @WebServlet("/ForwardServlet") public class ForwardServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.getRequestDispatcher("forward.jsp").forward(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req, resp); } }
package com.wonders.myfilter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import java.io.IOException; //@WebFilter(filterName = "HelloFilter", value = "/*") // 拦截所有请求 //@WebFilter(servletNames={"param-servlet"}) // 拦截发送给名称为param-servlet这个servlet的请求 @WebFilter(value = "/*", dispatcherTypes = {DispatcherType.FORWARD}) // 拦截forward类型的请求 public class HelloFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException { System.out.println("before"); chain.doFilter(req, resp); System.out.println("after"); } }
Listener不像Servlet和Filter,Servlet有javax.servlet.Servlet,Filter有javax.servlet.Filter,但是Listener没有javax.servlet.Listener,你必须指明要监听什么,所有的监听类都是java.util.EventListener的子类,但是用于web开发的Listener都在javax.servlet包下,有如下监听类:
ServletContextAttributeListener:监听ServletContext上下文新增属性和移除属性事件
ServletContextListener【常用】:监听ServletContext初始化和销毁时间
ServletRequestAttributeListener:监听ServletRequest新增属性和移除属性事件
ServletRequestListener:监听ServletRequest创建和销毁事件
HttpSessionListener:监听HttpSession创建和销毁事件
HttpSessionAttributeListener:监听HttpSession新增属性和移除属性事件
我们在开发一个Listener时要选择实现的接口,在web项目框架中常常是通过实现ServletContext接口,在上下文启动的时候做一些事情。
package com.wonders.mylistener; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; @WebListener // @WebListener注解中只有一个value属性,用于描述这个listener的信息 public class HelloListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { System.out.println("starting!"); } }
从启动信息中我们也可以看出,tomcat先启动自身的server,然后在取启动我们的上下文ServletContext,最后才是设置那些需要启动时就加载的servlet。
web-app标签中配置了metadata-complete为true时,则表示对三大组件的注册方式,只有web.xml中的注册起作用,将忽略注解的注册;若为false,则表示两种注册方式同时起作用。默认值为false。
若对于Servlet采用了两种方式同时进行注册【servlet3.0和servlet4.0】,则需要注意:
若两种方式的url-pattern值不同,那么相当于该Servlet具有两个url-pattern
注意:当在web.xml的web-app标签中配置了metadata-complete为true时,注解方式配置的Servlet将失效。
若对于Filter采用了两种方式同时进行注册【servlet3.0和servlet4.0】,则需要注意:
无论url-pattern的值是否相同,其都是作为两个独立的Filter出现的。
注意:当在web.xml的web-app标签中配置了metadata-complete为true时,注解方式配置的Filter将失效。
若对于Filter采用了两种方式同时进行注册【servlet3.0和servlet4.0】,则需要注意:
注意:当在web.xml的web-app标签中配置了metadata-complete为true时,注解方式配置的Listener将失效。
在servlet3.0之前,文件上传十分复杂,你需要从一大堆请求文本中先分割出文件名称,然后再从这堆文本中分割出内容区域,如下对于同一个form表单:
<form method="POST" enctype="multipart/form-data" action="UpoladServletOrigin"> File to upload use Servlet origin: <input type="file" name="upfile"><br/> Notes about the file: <input type="text" name="note" value="www"><br/> <br/> <input type="submit" value="Press"> </form>
在不同的浏览器中会产生不同的请求信息(以下分别是谷歌和ie):
------WebKitFormBoundaryhgFK0yqxLaFImmBT Content-Disposition: form-data; name="upfile"; filename="a.txt" Content-Type: text/plain 大红包 ------WebKitFormBoundaryhgFK0yqxLaFImmBT Content-Disposition: form-data; name="note" www ------WebKitFormBoundaryhgFK0yqxLaFImmBT--
-----------------------------7e42a62f1b04de Content-Disposition: form-data; name="upfile"; filename="C:\Users\wonders\Desktop\a.txt" Content-Type: text/plain 大红包 -----------------------------7e42a62f1b04de Content-Disposition: form-data; name="note" www -----------------------------7e42a62f1b04de--
你要从各大浏览器的请求信息中分割出上传文件的内容以及其他表单域的内容,相当繁琐(具体可参数代码)。为了简化文件上传,我们常使用apache的第三方工具file-upload,但是写起文件上传也是十分麻烦,我们来看一个简单的案例:
1 @WebServlet("/UploadServletJar") 2 public class UploadServletJar extends HttpServlet { 3 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 4 // 单例配置工厂类 5 DiskFileItemFactory factory = new DiskFileItemFactory(); 6 ServletContext servletContext = this.getServletConfig().getServletContext(); 7 File repository = (File) servletContext.getAttribute("javax.servlet.context.tempdir"); 8 factory.setRepository(repository); 9 10 ServletFileUpload upload = new ServletFileUpload(factory); 11 try { 12 List<FileItem> items = upload.parseRequest(request); 13 Iterator<FileItem> iter = items.iterator(); // 迭代form表单的每一个字段 14 while (iter.hasNext()) { 15 FileItem item = iter.next(); 16 if (!item.isFormField()) { 17 String contextPath = request.getServletContext().getRealPath("/"); 18 File uploadedFile = new File(contextPath + item.getName()); 19 item.write(uploadedFile); 20 } else { 21 System.out.println(item.getFieldName() + "===" + item.getString()); 22 } 23 } 24 } catch (Exception e) { 25 e.printStackTrace(); 26 } 27 }
在使用第三方包的时候,我们不需要自己手动的解析request中的信息了,第三方包已经帮我们做好,我们只需要拿到表单域里的内容即可。
为了更进一步简化文件上传,在Servlet3.0中提供了Part,它可以方便的帮助我们上传文件,案例如下:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Part upfile = request.getPart("upfile"); String contextPath = request.getServletContext().getRealPath("/"); String fileName = upfile.getHeader("content-disposition").split("filename=")[1].replaceAll("\"", ""); upfile.write(contextPath + fileName); }
在Servlet3.0中表单域的每一个项被视为一个Part,你可以根据项的名称获取该项的内容,相当方便。
Servlet是单例多线程的。当一个请求到达服务器后,服务器会马上为该请求创建一个相应的Servlet线程,为该请求服务。那么,一个请求就一定会有一个Servlet线程为之服务吗?答案是否定的。服务器会为每一个Servlet实例创建一个Servlet线程池,而线程池中该Servlet实例的线程对象并不是“取之不尽”的,而是有上限的。当达到该上限后,再有请求要访问该Servlet,那么该请求就只能等待了。只有当又有了空闲的Servlet线程对象后才能为该请求分配Servlet线程对象。
1 /** 2 * Servlet是单例多线程的,servlet容器在初始化Servlet时会为每一个Servlet创建一个 3 * 线程池,当请求到来时,servlet会从线程池中获取一个线程去执行,然后等待着这个线程的执行结果, 4 * 最终将结果返回给客户端 5 */ 6 @WebServlet("/NoAsyncServlet1") 7 public class NoAsyncServlet1 extends HttpServlet { 8 /** 9 * 这段代码访问的结果是:客户端的浏览器不停的转圈圈,直到8秒后,才返回结果。对于客户端 10 * 来说体验较差,能不能先告诉我一个结果,然后你们在后台继续执行耗时逻辑,等你们的耗时逻辑执行完了 11 * 在来通知我?看AsyncServlet2 12 */ 13 @Override 14 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 15 resp.setContentType("text/html;charset=UTF-8"); 16 PrintWriter out = resp.getWriter(); 17 out.append("主线程开始运行<br/>"); 18 // 我要执行一个很耗时的操作 19 try { 20 for (int i = 0; i < 8; i++) { 21 System.out.println("i == " + i); 22 sleep(1000); 23 } 24 } catch (InterruptedException e) { 25 System.out.println(e); 26 } 27 out.append("主线程运行结束<br/>"); 28 } 29 }
1 @WebServlet("/AsyncServlet2") 2 public class AsyncServlet2 extends HttpServlet { 3 /** 4 * 首先Servlet已经从线程池中取一个线程来执行这个请求了,只要这个线程没执行完,Servlet就会一直等待, 5 * 那我们在另外创建一个线程去执行这个耗时的任务,然后让Servlet的那个线程执行结束。 6 * 这段代码的执行结果是:客户端浏览器不需要等待这这个耗时的任务执行完就可以立即看到一个响应结果,任务在 7 * 后台被执行。 8 * 9 * 异步确实是异步了,但是在页面上并没有子线程out.append里的内容,这是因为主线程很快就结束了,主线程 10 * 结束,意味着out内容已经输出到客户端,response结束,在此之后在往out添加内容,就无效了,因为整个交互 11 * 已经结束。 12 * 13 * 我们如何在子线程里把子线程的执行信息输出到客户端?有两种方式: 14 * 1、在主线程中等待子线程执行结果,如下,使用join或主线程sleep。使用join时,相当于同步执行,客户端需要长时间等待。 15 * 2、使用AsyncContext,参见AsyncServlet3 16 */ 17 @Override 18 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 19 resp.setContentType("text/html;charset=UTF-8"); 20 PrintWriter out = resp.getWriter(); 21 out.append("主线程开始运行<br>"); 22 Thread t = new Thread(() -> { 23 out.append("子线程开始运行<br>"); 24 // 我要执行一个很耗时的操作 25 try { 26 for (int i = 0; i < 8; i++) { 27 System.out.println("i == " + i); 28 out.append("i == " + i + "<br>"); 29 sleep(1000); 30 } 31 } catch (InterruptedException e) { 32 System.out.println(e); 33 } 34 out.append("子线程运行结束<br>"); 35 }); 36 t.start(); 37 38 // try { 39 // t.join(); 40 // } catch (InterruptedException e) { 41 // e.printStackTrace(); 42 // } 43 44 out.append("主线程运行结束<br>"); 45 } 46 }
打开上面代码join注释,此刻相当于同步执行,请求页面结果如下:
主线程开始运行 子线程开始运行 i == 0 i == 1 i == 2 i == 3 i == 4 i == 5 i == 6 i == 7 子线程运行结束 主线程运行结束
为了支持异步处理,在Servlet3.0中,在ServletRequest上提供了一些列支持异步的方法,如:创建异步上下文:startAsync,获取创建的异步上下文:getAsyncContext,检查当前Servlet版本是否支持异步:isAsyncSupported,检查异步请求是否已经启动isAsyncSupported等。
创建异步上下文,ServletRequest中提供了两种重载的方法:
// 将此请求置于异步模式,并使用原始(未包装)的ServletRequest和ServletResponse对 // 象初始化其AsyncContext。 AsyncContext startAsync() throws IllegalStateException; // 将此请求置于异步模式,并使用给定的请求和响应对象初始化其AsyncContext。 AsyncContext startAsync(ServletRequest var1, ServletResponse var2)
在调用了startAsync方法取得AsyncContext对象之后,此请求的响应会被延后,并释放容器分配的线程。可以通过AsyncContext的getRequest()、getResponse()方法取得请求、响应对象。
响应延后到什么时候呢?也就是说我(异步上下文)该什么时候告知容器我执行完成了?
客户端的响应将暂缓至调用AsyncContext的 complete()方法 或 dispatch()方法 或 setTimeout方法指定的时间(默认30s) 为止,complete方法表示响应完成,dispatch表示将请求转发给指定url进行响应,setTimeout表示响应超时。
也就是说当异步上下文调用了complete或dispatch或已经超时的时候,容器就会得到通知,知道异步上下文交互完成了,这个时候容器会把响应结果返回。
若要能调用ServletRequest的startAsync方法以取得AsyncContext,必须先告知Servlet容器此Servlet支持异步处理。如果是Servlet3.0可以使用注解告知:
@WebServlet(value = "/AsyncServlet3",asyncSupported = true)
如果是在web.xml中配置,使用async-supported标签:
<servlet> <servlet-name>AsyncServlet3</servlet-name> <servlet-class>com.wonders.AsyncServlet3</servlet-class> <async-supported>true</async-supported> </servlet>
如果Servlet是异步处理的,若其有前端过滤器,则过滤器亦需要标识支持异步处理:
@WebFilter(urlPatterns = "/AsyncServlet3", asyncSupported = true)
<filter> <filter-name>Async3Filter</filter-name> <filter-class>com.wonders.Async3Filter</filter-class> <async-supported>true</async-supported> </filter>
/** * 配置asyncSupported = true,告诉容器这个Servlet支持异步处理。 * * 对于异步上下文对象结束的方式: * 1、在异步子线程这使用asyncContext.complete()方法:该方法表示完 * 成该请求上的异步操作,并关闭用于初始化当前异步对象的响应。 * 2、在异步子线程中使用asyncContext.dispatch()方法:该方法将请求和 * 响应的控制权委托给调度目标,并且在调度目标完成执行后将关闭响应。在结束异步 * 操作的同时,会将参数所指定的页面内容包含到当前异步对象相关的标准输出流中。 * 其执行效果相当于RequestDispatcher对象的include()方法的执行效果。 * 3、在异步Servlet主线程中设置asyncContext的超时时限,当超时时限到达时, * 完成异步操作,并关闭用于初始化当前异步对象的响应。 */ @WebServlet(value = "/AsyncServlet3", asyncSupported = true) public class AsyncServlet3 extends HttpServlet { ExecutorService executorService = Executors.newFixedThreadPool(10); /** * 对于每一个请求,Servlet会使用原生的请求和响应初始化AsyncContext,并释放容器所分配的线程,响应被延后。 * 对于这些被延后的响应的请求,创建一个实现Runnable接口的对象Task,Task用于处理我们耗时的任务。并且 * 创建一个线程池,线程池的数量固定,让这些必须长时间处理的请求,在这些有限数量的线程中完成,而不用 * 每次请求都占用容器分配的线程。 */ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8"); PrintWriter out = resp.getWriter(); out.append("主线程开始运行<br>"); AsyncContext asyncContext = req.startAsync(); executorService.submit(new Task1(asyncContext)); // asyncContext.start(new Task1(asyncContext)); // 如果不使用线程池,可以AsyncContext#start方法启动异步任务 out.append("主线程运行结束<br>"); } @Override public void destroy() { executorService.shutdown(); } }
/** * 模拟耗时任务 */ public class Task1 implements Runnable { private AsyncContext asyncContext; Task1(AsyncContext asyncContext) { this.asyncContext = asyncContext; } @Override public void run() { try { PrintWriter out = asyncContext.getResponse().getWriter(); out.append("子线程开始运行<br>"); for (int i = 0; i < 20; i++) { System.out.println("i == " + i); out.append("i == " + i + "<br>"); sleep(1000); // 开启for循环下的注释,并注释36行asyncContext.complete(); // asyncContext调用complete后,只是告知容器该请求的异步上下文执行完 // 成,可以把响应结果返回给客户端了。complete后异步上下文中的线程并没有被执 // 行完成,但是整个交互环境(asyncContext)已经算是无效了,不管线程中做什么,两个 // 重要的交互对象request和response都已经离开了,线程在做什么都视为已经和请求响应无关。 // if (i == 6) { // asyncContext.complete(); // } // System.out.println(asyncContext); } out.append("子线程运行结束<br>"); asyncContext.complete(); } catch (InterruptedException e) { System.out.println(e); } catch (IOException e) { System.out.println(e); } } }
主线程开始运行 主线程运行结束 子线程开始运行 i == 0 i == 0 i == 1 i == 1 i == 2 i == 2 i == 3 i == 3 i == 4 i == 4 i == 5 i == 5 i == 6 i == 6 i == 7 i == 7 子线程运行结束
虽然客户端等待响应时间和使用AsyncServlet2中开启join注释一样,但是输出的结果却是不同的。在本例中,从页面返回结果看出主线程先执行完成,耗时的子线程后执行,这说明肯定是异步执行的,不然的话不可能先输出主线程执行的内容,然后输出子线程执行的内容。现在给客户端的体验是主线程执行结束了,又直到子线程执行结束才返回,主线程执行结束后为什么不直接返回,在等待着什么?是等待着asyncContext.complete()被调用吗?与其说是在等待着asyncContext.complete()不如说是在等待request和response对象被释放,由于我们在子线程中占用了request和response对象,导致主线程无法尽快结束,所以在客户端体验的结果就是长时间的等待。而调用asyncContext.complete()会告知容器异步上下文交互完成了,异步响应对象被关闭后,他们会归还到容器中,然后返回给客户端。所以我们应该尽快的调用complete或dispatch或设置很短的超时时间,让使用到request和response的异步上下文尽快关闭request和response,这样容器就会今早收到通知,然后把响应返回到客户端。
在Servlet3.0中使用异步,不应该占用着request和response而是尽快的返回它们,如果把它们尽早的返回了,我们怎么通知客户端任务执行完成状态呢?只要不占用request和response对象就行,我们可以使用session对象,它任务完成状态设置到session里,在由客户端不断的刷新以获取session里最新的任务执行状态。
对程序改进如下,使用timeout尽早的释放request和response:
使用timeout设置极短的超时时间(注意你设置的时间要确保异步任务已经执行,或者说已经获取了session,如果你像下面注释那样设置超时为1ms,然后在异步任务里一进去就等待,如果你在异步任务没获取session之前访问async.jsp一定会报一个错误。),让异步上下文里的request和response尽早关闭,然后容器尽早把响应结果返回。
@WebServlet(value = "/AsyncServlet4", asyncSupported = true) public class AsyncServlet4 extends HttpServlet { ExecutorService executorService = Executors.newFixedThreadPool(10); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8"); PrintWriter out = resp.getWriter(); out.append("主线程开始运行<br>"); AsyncContext asyncContext = req.startAsync(); asyncContext.setTimeout(1000); // 设置超时要在其启动之前 // asyncContext.setTimeout(1); // 设置超时要在其启动之前 executorService.submit(new Task2(asyncContext)); out.append("主线程运行结束<br>"); out.append("任务执行完成,请到<a href='async.jsp'>这里</a>查看"); } @Override public void destroy() { executorService.shutdown(); } }
/** * 模拟耗时任务 */ public class Task2 implements Runnable { private AsyncContext asyncContext; Task2(AsyncContext asyncContext) { this.asyncContext = asyncContext; } @Override public void run() { try { // sleep(10000); HttpServletRequest request = (HttpServletRequest) this.asyncContext.getRequest(); HttpSession session = request.getSession(); int sum = 0; for (int i = 0; i <= 10; i++) { session.setAttribute("sum", sum); sum += i; System.out.println("i == " + i); sleep(1000); } session.setAttribute("sum", sum); } catch (InterruptedException e) { System.out.println(e); } } }
<html> <head> <title>Title</title> </head> <body> <p> <% int sum = (Integer) request.getSession().getAttribute("sum"); out.println("execute process:" + sum + "<br/>"); if (sum == 55) { out.println("complete!"); } else { out.println("executing!"); } %> </p> <script> var interval = window.setInterval(function () { <% if (sum == 55) { %> window.clearInterval(interval); return; <% } %> window.location.reload(); }, 1000); </script> </body> </html>
在上面的例子中我们使用的在客户端不断的刷新以获取最新的任务执行状态,在实际生产场景中,经常使用Ajax技术,去局部刷新,而不必每次都刷新整个页面。我们能不能在服务端不断的往客户端推送任务执行的状态,而不是让客户端每次主动刷新呢?在文章最上面参考的异步文章中第三篇讲述的就是在Servlet3.0异步引入后,一个典型的案例(该案例已经包含在本项目中),可以参考用于加深理解。
@Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8"); PrintWriter out = resp.getWriter(); out.append("主线程开始运行<br>"); AsyncContext asyncContext = req.startAsync(); asyncContext.setTimeout(1); // 设置超时要在其启动之前 asyncContext.addListener(new AsyncListener() { /** * 在asyncContext.complete()时执行 * @param asyncEvent * @throws IOException */ @Override public void onComplete(AsyncEvent asyncEvent) throws IOException { System.out.println("onComplete"); } /** * 在超时 时执行 * @param asyncEvent * @throws IOException */ @Override public void onTimeout(AsyncEvent asyncEvent) throws IOException { System.out.println("onTimeout"); } @Override public void onError(AsyncEvent asyncEvent) throws IOException { System.out.println("onError"); } @Override public void onStartAsync(AsyncEvent asyncEvent) throws IOException { } }); executorService.submit(new Task2(asyncContext)); out.append("主线程运行结束<br>"); out.append("任务执行完成,请到<a href='async.jsp'>这里</a>查看"); }
熟悉Struts2的开发者一定会对其通过插件的方式与包括Spring在内的各种常用框架的整合记忆犹新。将相应的插件封装成JAR包并放在类路径下,Struct2运行时便能自动加载这些插件。这一点在SpringBoot中也体现的淋漓尽致,只需要在类路径下引入相应的JAR就可以自动配置。现在Servlet3.0提供了类似的特性,开发者可以通过插件的方式很方便的扩充已有的Web应用的功能,而不需要修改原有的应用。
所谓组件可插性是指,JavaEE6.0项目支持将打为Jar包的Servlet、Filter、Listener直接插入到正在运行的Web项目中。当然,这些Jar包中同时包含有相应的配置文件,即web-fragment.xml。这一点就是利用SPI服务发现机制,扫描全局目录下的JAR,查找META-INF目录下的web-fragment.xml。web-fragment.xml是在Servlet3.0时才有的web片段项目属性描述符。
下面以“将打为Jar包的Servlet插入到Web项目中”为例,对于Filter与Listener用法相同。
新建web工程片段servlet3.0-java-fragment,创建HelloServlet
package com.wonders; @WebServlet("/HelloServlet") public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { PrintWriter out = resp.getWriter(); out.println("Hello Servlet"); } }
在src目录下创建META-INF/web-fragment.xml
<?xml version="1.0" encoding="UTF-8"?> <web-fragment id="WebFragment_ID" version="3.1" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-fragment_3_1.xsd"> </web-fragment>
这个web-fragment.xml和web.xml类似,你可以在其中定义Servlet等三大组件,我这里是使用注解方式注册Servlet了,如果你不想使用注解方式,可以在web-fragment.xml中注册。
现在我们这个片段web工程已经创建好了,我们使用IDEA将其打成一个JAR包,在需要使用这个片段的web工程的web项目中,直接将这个JAR包添加到classpath的lib目录下即可。
当我们启动servlet3.0-java-fragment-use工程时,也可以访问JAR包里的HelloServlet。
Servlet3.0对于ServletContext进行了功能增强,可以对Servlet,Filter及Listener进行动态注册。所谓动态注册是指,Web应用在运行过程中通过代码对Servlet、Filter或Listener进行注册。
为了系统安全考虑,这个动态注册是有限制的,只能在应用启动时注册,而不能在应用运行过程中进行注册。这个应用启动时间点,可以通过ServletContextListener监听器来把握。
在Servlet3.0之后,ServletContext中提供了动态注册三大组件的方法,但是能动态注册的前提是在容器启动的时候。所以我们需要创建一个监听器:
1 package com.wonders; 2 3 import javax.servlet.*; 4 import javax.servlet.annotation.WebListener; 5 import java.util.EnumSet; 6 import java.util.HashMap; 7 8 @WebListener 9 public class MyRegisterListener implements ServletContextListener { 10 11 /** 12 * This method is called when the servlet context is 13 * initialized(when the Web application is deployed). 14 * You can initialize servlet context related data here. 15 * 16 * @param sce 17 */ 18 public void contextInitialized(ServletContextEvent sce) { 19 // 获取上下文ServletContext 20 ServletContext servletContext = sce.getServletContext(); 21 22 // 动态注册Listener 23 servletContext.addListener("com.noredister.MyListener"); 24 25 // 动态注册Servlet 26 ServletRegistration.Dynamic dynamic = servletContext.addServlet("my-servlet", "com.noredister.MyServlet"); 27 dynamic.addMapping("/MyServlet"); 28 dynamic.setInitParameters(new HashMap<String, String>() {{ 29 put("key1", "value1"); 30 put("key2", "value2"); 31 }}); 32 33 // 动态注册Filter 34 FilterRegistration.Dynamic dynamic1 = servletContext.addFilter("my-filter", "com.noredister.MyFilter"); 35 // 拦截所有的默认请求,该拦截器将在所有已注册的拦截器之后执行 36 dynamic1.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*"); 37 38 // 动态注册Listener[注意注册的Listener不能是ServletContextListener] 39 servletContext.addListener("com.noredister.MyListener"); 40 } 41 42 43 }
package com.noredister; /** * 这个Servlet并没有使用注解声明也没有在xml声明 */ public class MyServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8"); PrintWriter writer = resp.getWriter(); writer.println("动态注册Servlet成功! <br>"); Enumeration<String> initParameterNames = this.getInitParameterNames(); String name = initParameterNames.nextElement(); while (name != null) { String value = this.getInitParameter(name); writer.println(name + " = " + value + "<br>"); initParameterNames.nextElement(); } } }
package com.noredister; public class MyFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("Filter动态注册成功!"); filterChain.doFilter(servletRequest, servletResponse); } }
package com.noredister; public class MyListener implements ServletRequestListener { @Override public void requestInitialized(ServletRequestEvent sre) { System.out.println("=======MyListener"); } }