servlet3.x 续集,进一步了解
今天对servlet3.0做一点点的补充,主要是针对注解式 和原有的配置式的一个对比,让从以前的配置式同学更好的到现在的注解式。
1 异步请求支持
在将异步请求之前,让我们先了解一下,为什么要支持异步,及以前servlet 工作的过程
正常的servlet请求流程
在这种阻塞式的请求下,当请求数据过多,并且后服务处理逻辑复杂,需要的相应时间比较就,比如大数据量的导出, 导致Tomcat 假死的情况,无法处理请求。
异步请求
从对比中可以发现,异步时,servlet主线程在接收请求之后,创建子线程,然后主线程就结束了,子线程的处理过程就与主线程无关,这是,servlet就可以在很短的时间内接收一个新的请求。当然,我们这里只是将servlet的异步处理,在特别大的并发访问下, 后台服务对请求的接收也是有限的,我们不讨论。
1.1 如何实现
下面让我们看看 如何实现servlet 的异步请求。
AsyncContext 对象, servlet 提供的异步容器对象,当需要此对象时,必须告知容器此Servlet支持异步处理,
如果使用@WebServlet来标注,则可以设置其asyncSupported为true
如果使用web.xml设置Servlet,则可以在中设置标签为true:
AsyncContext 是一个接口,他可以通过request 的 startAsync
AsyncContext 在调用了startAsync()方法取得AsyncContext对象之后,此次请求的响应会被延后,并释放容器分配的线程
AsyncContext 是一个容器,可以getRequest()、getResponse()方法取得请求、响应对象 。
AsyncContext 还提供了complete()或dispatch()方法,对子线程进行终止 或者跳转到指定路径,同时释放所有的 资源信息 request 、response
AsyncContext 还提供了超时设置setTimeOut(), 当超时时间到达时,会自动停止异步子线程,返回请求,同时释放所有的 资源信息 request 、response。
所以AsyncContext 是异步的关键,既可以在主线程启动子线程,同时设置超时时间,又可以在子线程中,对请求对象进行共享传递,还可以对子线程进行终止,返回response。
//无参方法,直接利用原有的请求与响应对象来创建AsyncContext
public AsyncContext startAsync() throws IllegalStateException {
return request.startAsync();
}
//可以传入自行创建的请求、响应封装对象
public AsyncContext startAsync(ServletRequest servletRequest,
ServletResponse servletResponse)
throws IllegalStateException {
return request.startAsync(servletRequest, servletResponse);
}
1.2 一起看代码
主线程
@WebServlet(name = "asyncServlet",
urlPatterns = "/asyncServlet",
asyncSupported = true)
public class MyAsyncServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.write("this is a asyncServelt <br>");
System.out.println(Thread.currentThread().getName()+ "主线程开始....");
out.write( "主线程开始运行.....<br>");
//经过测试 发现req.getAsyncContext() 一直是null, 可能需要进行特殊设置
// req.getAsyncContext().start(new MyAsyncThread(out));
AsyncContext ac = req.startAsync();
MyAsyncThread th = new MyAsyncThread(ac);
ac.start(th);
System.out.println(Thread.currentThread().getName()+ "主线程结束....");
out.write("主线程运行结束.....<br>");
}
}
子线程
public class MyAsyncThread implements Runnable {
private AsyncContext ac;
//将AsyncContext 对象通过构造器传入子线程,便可以恭喜请求内容
public MyAsyncThread(AsyncContext ac) {
this.ac = ac;
}
@Override
public void run() {
int sum = 0;
try {
PrintWriter out = ac.getResponse().getWriter();
out.print("子线程开始运行.....<br>");
out.print("sum = " + sum);
for (int i = 0; i < 10; i++) {
System.out.println("i = " + i );
Thread.sleep(1000);
sum += i;
}
out.print("sum = " + sum + "<br>");
out.print("子线程运行结束.....<br>");
// 结束子线程,进行数据返回
//ac.complete();
//结束子线程同时,将数据返回至指定的跳转页面,相当与 RequestDispathcher的include()
ac.dispatch("/index.jsp");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
==总结:==
对上面的整个测试结果来看,我们主要使用异步的目的是为了尽快释放主线程,释放servlet资源,以增加访问能力。
那在代码中,==我们是否能够或者说需要调用多个ac.start(th); 方法,启动多个线程呢?==我认为是不需要的,因为servlet异步是为了释放资源,而不是使用多个线程去完成业务规则。而且同一个request的 AsyncContext是相同的,只要调用complete方法,资源都会被释放,所以多个start 多个线程没有意义。
所以我觉得,当时设计AsyncContext 初衷就是为了封装子线程资源,达到资源共享,资源销毁的目的,然后将请求做分离。 有点类似FutureTask 封装Callable实现,获取返回值的目的一样。
在测试中,我们可以知道,如果子线程中没有释放response资源,用户页面就一直在等待,所以,如果有场景需要及时返还,在使用AsyncContext 时,就不要使用response ,而是让资源在主线程中直接释放掉。
2 当web.xml 与配置同时存在
在servlet3.0 中,编程式方式可以取代之前的配置式,但是在3.0中,其实文件与注解可以同时存在,我们来比较下如果两个同时存在会有哪些问题
web.xml
<web-app 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-app_3_1.xsd"
version="3.1">
<!-- version 需要是3.x以上, 描述符需要是web-app_3_x.xsd 以上版本 -->
</web-app>
Servlet 共存
Web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app 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-app_3_1.xsd"
version="3.1">
<!--注册servlet-->
<!--servlet 名称 MySerlvet-->
<!--url /MySerlvet-->
<servlet>
<servlet-name>MySerlvet</servlet-name>
<servlet-class>com.Henry.servlet.MySerlvet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>MySerlvet</servlet-name>
<url-pattern>/MySerlvet</url-pattern>
</servlet-mapping>
</web-app>
代码注册
//相同servlet名称 MySerlvet
//Url /MySerlvet
//写法1
@WebServlet("/MySerlvet")
//写法2
@WebServlet(name = "MySerlvet" ,urlPatterns = "/MySerlvet")
public class MySerlvet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
PrintWriter out = resp.getWriter();
out.write(" this is a servlet ");
}
}
==结论==
当@WebServlet(“/MySerlvet”)使用这种写法的时候, 如果web.xml 中,存在一个相同的 url-pattern 却不指定servlet-name时,系统启动时报错
当@WebServlet(name = “MySerlvet” ,urlPatterns = “/MySerlvet”)使用 这种写法时,制定了servlet-name时, 如果web.xml中存在相同的servlet名称,则以web.xml为主
原因其实是因为,两个servlet 不同的servlet,虽然路径可以包含,但url-pattern 是不允许使用一样的,
当@WebServlet(“/MySerlvet”) 其实是没有指定名称,系统会默认指定一个名称,此时就出现了两个servlet名称不同但是 url-pattern却相同的情况,所以报错
Filter共存
Web.xml
<filter>
<filter-name>myFilter</filter-name>
<filter-class>com.Henry.filter.MyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>myFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
代码
@WebFilter(filterName = "myFilter",urlPatterns = "/*")
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
System.out.println("========after============");
filterChain.doFilter(servletRequest,servletResponse);
System.out.println("========befor=============");
}
@Override
public void destroy() {
}
}
==结论==
其实与servlet是相同的, 当 @WebFilter(filterName = “myFilter”,urlPatterns = “/*”) 指定了名称,如果web.xml中存在相同名称的filter, 则以web.xml为主。 如果是@WebFilter(“/”) 这种,那两个filter是可以同时存在的。因为Filter 本么有URL的冲突
Listener 共存
这个没什么好进行测试的, 直接可以获取到结论, @WebListener 只有一个属性,所以如果当@WebListener 和web.xml 存在相同的Listener 配置,以web.xml内容生效
3 文件上传
在servlet3.0中,request增加了对文件上传处理MulitPart请求的支持。
在定义的servlet中增加@MultipartConfig 注解,同时form的enctype=”multipart/form-data” method=”post”
<html>
<head>
<title>$Title$</title>
</head>
<body>
<form action="${pageContext.request.contextPath}/uploadFile" method="post" enctype="multipart/form-data">
文件<input type="file" name="text"/>
<input type="submit" value="上传"/>
</form>
</body>
</html>
@WebServlet("/uploadFile")
@MultipartConfig //表示支持文件上传 处理MulitPart请求
public class fileServlet extends HttpServlet{
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String path = this.getServletContext().getRealPath("/text");
Part part = req.getPart("text");
part.write(path + "/xxx.txt");
}
}
这样就可以文件上传了,通过获取request.getPart 或者getParts方法。
唯一不好的地方就是无法获取到对应的文件实际名称,需要更加hearder 去截取文件名称,不算方便