目标
- servlet的生命周期
- servletConfig与ServletContext定义,及其二者的区别
- 监听器
- 过滤器
- 请求响应封装器
- 异步处理
- 模拟服务器推播
1. servlet的生命周期
为什么java总是讨论生命周期?因为Java的世界里所有的东西都是对象,是对象就有初始化、执行、销毁的过程。
那servlet的生命周期,也就是它什么时候被创建,什么时候执行,什么时候被销毁?
说这之前,先说一下servlet的定义吧,也就是它的祖籍关系!!(别忘了,我们真正使用的是HttpServlet)
Paste_Image.pngHttpServlet实现了GenericServlet类,GenericServlet又实现了Servlet接口和ServletConfig接口,现在分别的解释一下这几个类和接口都是干嘛的?
- servlet原来是用来和各种网络协议交互的,因为网络协议不同,所以实现方式也就不同,所以它是个接口
- GenericServlet抽象类简单的实现了servlet接口(简单也就是不够落地,例如就没有doXXX等函数),它还实现了一些系统需求功能,比如实现(其实是一种封装,ServletConfig对象是做为参数传进来的,GenericServlet按照ServletConfig接口的规范,重新封装了对象)了ServletConfig接口(初始化参数,它是),还有类似log日志的功能,这些都是系统需求。
- servlet的基本功能和系统需求功能都实现了,那剩下的就是具体的协议交互处理了,HttpServlet就实现了具体的协议交互,里面就有了doXXX等函数
接下来,我们主要针对ServletConfig和ServletContext分别进行详细描述。
--------------------GenericServlet--------------------
GenericServlet的用途上面已经说了,现在来看一下它的代码,它是如何封装ServletConfig的
这里有个问题:
servlet是java的类,初始化为什么不用构造函数,而是用init函数
The init() method creates and loads the servlet. But the servlet instance is first created through the constructor (done by Servlet container). We cannot write constructors of a servlet class with arguments in servlet (It will throw Exception). So, They provided a init() method that accepts an ServletConfig object as an argument. ServletConfig object supplies a servlet with information about its initialization (init) parameters. Servlet class cannot declare a constructor with ServletConfig object as a argument and cannot access ServletConfig object.
More info at: http://java.sun.com/j2ee/tutorial/1_3-fcs/doc/Servlets6.html
servlet是一个类,他是由web容器来创建和初始化的,所以构造函数不能由开发人随意定义,web容器会迷糊的(结果就是会抛出异常),所以web容器会自动得给servlet添加一个空的构造器。那servlet需要参数怎么办?而且不同的servlet参数也不一样!好吧,参数不一样,我们用ServletConfig对象来解决,然后servlet类提供了一个init函数,让我们可以把参数对象ServletConfig传进去。
2. servletConfig与ServletContext定义,及其二者的区别
--------------------ServletConfig--------------------
Jsp/Servlet容器初始化一个Servlet类型的对象时,会为这个对象创建一个ServletConfig对象,在这个ServletConfig对象中包含了Servlet的初始化参数信息。此外,ServletConfig对象还在内部引用了ServletContext对象。Jsp/Servlet容器在调用Servlet对象的init(ServletConfig config)方法时,会把ServletConfig类型的对象做为参数传递给servlet对象,也相当于把ServletContext传递给了servlet对象。以下是ServletConfig定义的方法:
getInitParamter(String name)
getInitParamterNames()
getServletContext()
getServletName()
下面这个是一个具体的代码举例:
Paste_Image.png
这里有一个问题:
在GenericServlet中,为什么要有两个init函数?
请仔细看一下GenericServlet的实现,一个有参的init函数参数是固定的,就是ServletConfig对象做为参数,如果这个可以被继承并重写,那web容器怎么给servlet传ServletConfig参数?所以这个带参数的init函数不能重写,那就再来一个无参的init函数吧,做为初始化参数!
通常使用标注来设置初始化参数,之后若想改变这些参数,用web.xml重新设置,就可以覆盖标注。而且不用修改代码,重新编译。
--------------------ServletContext--------------------
ServletContext是servlet与servlet容器之间的直接通信接口!Servlet容器在启动一个webapp时,会为它创建一个ServletContext对象,即servlet上下文环境。每个webapp都有唯一的ServletContext对象。同一个webapp的所有servlet对象共享一个ServletContext,servlet对象还可以通过ServletContext来访问容器中的各种资源。
webapp?一个项目就是一个webapp,每个项目都配备一个web.xml,web容器会为每个项目产生一个ServletContext对象。
ServletContext提供的方法分为以下几种类型:
用于在webapp范围内共享数据
setAttribute(String name,Java.lang.Object object)
getAttribute(String name)
getAttributeNames()
removeAttribute(String name)
访问当前webapp的资源
getContextpath() webapp的URL
getInitParameter(String name) 容器的指定初始化参数值
getInitParameterNames() 容器的所有初始化参数
getServletContextName() webapp名称
getRequestDispather(String path) 返回一个向其它web组件转发请求的RequestDispather对象
访问servlet容器的相关信息
getContext(String uripath) 根据参数指定的url,返回当前servlet容器中其它web应用的ServletContext对象
访问web容器的相关信息
getMajorVersion() servlet容器支持的java servlet API的主版本号
getMinorVersion() 上面的次版本号
getServerInfo() 返回servlet容器的名字和版本
访问服务器端的文件系统资源
getRealPath(String path) 根据参数指定的虚拟路径,返回文件系统中的真实路径
getResource(Sting path) 返回一个映射到参数指定路径的url
getResourceStream(String path) 返回一个用于读取参数指定的文件的输入流
getMimeType(String file) 返回参数指定的MIME
输出日志
log(String msg) 向servlet的日志文件写日志
log(String message, java.lang.Throwable throwable):向servlet的日志文件中写错误日志,以及异常的堆栈信息。
ServletContext对象的获取方式
servlet2.5版本与之前:
javax.servlet.http.HttpSession.getServletContext()
javax.servlet.jsp.PageContext.getServletContext()
javax.servlet.ServletConfig.getServletContext() 这就是我们这里使用的方法
servlet3.0新增方法
javax.servlet.ServletRequest.getServletContext()
----------------servletConfig与ServletContext的区别---------------------
从作用范围来说,ServletConfig作用于某个特定的Servlet,即从该Servlet实例化,那么就开始有效,但是该Servlet之外的其他Servlet不能访问;ServletContext作用于某个webapp,即在一个webapp中相当于一个全局对象,在Servlet容器启动时就已经加载,对于不同的webapp,有不同的ServletContext。
3.监听器
监听器类似于php的钩子函数,不同的是java主要用监听器来健康对象的生命周期和属性变化等。
既然是监听器,那就要有监听的对象目标(不能像php一样,随意的下钩子):
- ServletContext对象
- HttpSession对象
- HttpServletRequest对象
写一个监听器也是两部分组成:编码和监听器声明
- ServletContext对象------------------------------------------------
-
ServletContextListener
这是生命周期监控器,大白话就是监听创建和销毁行为的。
定义:
Paste_Image.png
声明:servlet3.0 前必须web.xml声明
Paste_Image.png
应用:
Paste_Image.png
这里可以看到,当触发监听器之后,在监听器中可以获得ServletContext对象,然后在该对象中读取或设置应用内共享数据。
-
某些特定的程序设置,不能在运行期间动态设置,需要web应用程序初始化的时候进行,例如HttpSession的一些cookie设置(cookie在浏览器端的失效时间),可以采用web.xml设置,也可以采用监听器的方式
Paste_Image.png
-
ServletContextAttributeListener
关于属性监控器,监控的行为就是该属性的添加、修改、移除。
Paste_Image.png
标注声明方式:
Paste_Image.png
web.xml声明方式:
Paste_Image.png
- HttpSession对象
与HttpSession相关的监听器有四个:
HttpsessionListener 监听生命周期
HttpSessionAttributeListener 监听属性修改
HttpSessionBindingListenner 监听对象绑定(不用声明)
HttpSessionActivateionListener 监听跨jvm转移
前两个是比较简单的,都是需要代码编写和声明注册两部分工作,这里主要介绍一下后两个。
HttpSessionBindingListenner
它是对象绑定监听器,通俗的讲,就是当一个对象被作为参数赋值给session之后会触发这个监听器。这个监听器是由对象直接集成该监听器接口,在对象的类中直接实现的,所以不需要标注或web.xml声明。
Paste_Image.pngHttpSessionActivateionListener
它是对象迁移监听器,如果应用程序的对象分布在多个jvm中,就涉及到对象的迁移,在迁移之前会进行序列化,这时候会触发监听器的sessionWillPassivate()函数,到了目标jvm中,还需要反序列化,就会触发监听器的sessionDidActivate()函数。(这个是抄的)
- HttpServletRequest对象
与请求相关的监听器有三个:
ServletRequestListener 生命周期监听器
ServletRequestAttributeListener 属性监听器
AsyncListener servlet3.0中新增,异步处理时会用到
4. 过滤器
-
过滤器定义----------------------------
在容器调用servlet的service()方法前,servelt并不会知道有请求的到来,而在servlet的service()方法运行之后,容器真正对浏览器进行http响应之前,浏览器也不会知道servlet真正的响应是什么。过滤器是介于servlet之前,可拦截过滤浏览器对servlet的请求,也可以改变servlet对浏览器的响应。
想想已经开发好应用程序的主要业务功能了,但现在产品又出了新的需求:
- 针对所有的servlet,测试想要了解从请求到响应之间的时间差
- 针对某些特定的页面,客户希望只有特定的几个用户才可以浏览
- 基于安全方面的考虑,用户输入的特定字符必须过滤并替换为无害字符
- 请求与响应的编码从Big5改为UTF-8
- 过滤器实现--------------------------
先来看一个计算servlet消耗时间的过滤器
Paste_Image.png标注数字的位置:
- 这是FilterConfig,和ServletContext相似,是为过滤器提供初始化参数的对象
- 在doFilter函数中,会去判断是调用下一个过滤器还是调用service()函数
-
过滤器声明----------------------------
声明就是两种方式,上面代码中是标注方式,这里看一下web.xml方式(其实和声明servlet很相似)
<filter>
<filter-name>FristFilter</filter-name>
<filter-class>filter.FirstFilter</filter-class>
#FilterConfig的设置的初始化参数
<init-param>
<param-name>param1</param-name>
<param-value>hello world</param-value>
<init-param>
<init-param>
<param-name>param2</param-name>
<param-value>good</param-value>
<init-param>
</filter>
<filter-mapping>
<filter-name>FristFilter</filter-name>
<url-pattern>/res.jsp</url-pattern>
</filter-mapping>
如果在web.xml中声明多个过滤器,按照声明的先后顺序形成过滤器链。
- 过滤器触发时机----------------------------
@WebFilter(
filterName="some",
urlPatterns={"/some"},
dispatcherTypes={
DispatcherType.FORWARD,
DispatcherType.INCLUDE,
DispatcherType.REQUEST,
DispatcherType.Error,
DispatcherType.ASYNC
}
)
可以触发的时机就是这些,如果想触发RequestDispatcher.forward()内部转发过来的请求过滤器,就需要设置DispatcherType.FORWARD,其它同理。默认是DispatcherType.REQUEST。
5. 请求响应封装器
- 请求封装器-----------------------------
如果我们已经有一个写好的项目,项目中都是用getParameter()直接获取参数。我们现在希望把参数值中的敏感词过滤掉,但又不希望修改原来的代码,这里如何实现?(如果可以修改源码,我们就可以在过滤器中使用setAttribute函数,在代码中getAttribute参数就可以)
HttpServletRequest对象有getParameter()函数,但是没有setParameter()函数。
解决办法:
- 我们自己实现一个HttpServletRequest接口,然后添加一个setAttribute()函数。(不过这个接口内容很多,不好实现啊)
- 系统已经为我们准备好了一个实现类(HttpServletRequestWrapper),我们只要继承它之后重写getParameter()函数,让这个函数直接返回过滤后的参数值!
来看看我们继承这个HttpServletRequestWrapper类之后,做了什么?
Paste_Image.png用请求封装器结合过滤器来实现我们的业务需求:
Paste_Image.png在这里试想一下,我们原来讲过的GET参数的编码问题,如果是在servlet里面接收参数之后再转码,那每个servlet都需要实现转码程序,如果这里用请求封装器去实现,在servlet直接getParameter()就是转码之后的参数值了,是不是很方便,POST同理!
- 响应封装器-----------------------------
在servlet中,如果我们希望给浏览器做响应时,对响应数据进行压缩,正常情况下,我们会调用PrintWriter或ServletOutputStream进行输出,可是目前这两个对象的输出函数都不支持压缩,因此我们需要针对这两个对象和HttpServletResponse对象进行重新封装。
以ServletOutputStream举例,下面的代码先封装一下这个对象:
public class GZIPServletOS extends ServletOutputStream {
private GZIPOutputStream gzipOS;
public GZIPServletOS(ServletOutputStream servletOS) throws IOException {
gzipOS = new GZIPOutputStream(servletOS);
}
// 由于OutputStream的所有输出底层都是调用wirte(int b)方法的,因此
// 只有让write(int b)具有压缩功能,那么OutputStream的所有输出就都具有压缩功能了
@Override
public void write(int b) throws IOException {
// TODO Auto-generated method stub
gzipOS.write(b); // 输出时用封装的GZIPOutputStream的write压缩输出
}
// 对于压缩输出流,字节流中前后数据是相互依赖压缩的,传输只能按照压缩快一块一块传输
// 不能将一块分成多个部分传输,因此GZIPOutputStream提供finish方法手动控制缓冲区的输出
// finish类似flush方法,但不过finish并不是将整个缓冲区中所有内容输出
// 而是将缓冲区中现有的所有完整的块输出,末尾不完整的块不输出,继续接受压缩字节
// 记住!压缩流只能以压缩块为单位进行输出
public void finish() {
if (gzipOS != null) // 必须在非空的时候才能使用finish,否则会抛出异常
gzipOS.finish();
}
}
接下来我们看看HttpServletResponse的封装器:
public class CompressWrapper extends HttpServletResponseWrapper {
// 基于OutputStream和PrintWriter的规则设计封装器
// 在J2SE标准下PrintWriter用封装的OutputStream进行输出
// PrintWriter在创建时也是利用OutputStream的:伪代码
// OutputStream os = new OutputStream
// PrintWriter out = new PrintWriter(os)
// 因此J2SE标准规定:如果out封装了os,那么输出时就只能其中一个
// 用os输出时就不得使用out输出,用out输出的时候就不能用os输出
// 混用就直接抛出IllegalStateException
// 这两个成员的设计就符合J2SE标准
private GZIPServletOS gzipServletOS; // OutputStream
private PrintWriter out; // PrintWriter
public CompressWrapper(HttpServletResponse resp) {
super(resp);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
// TODO Auto-generated method stub
if (out != null) { // 用os进行输出时out不能占用os
throw new IllegalStateException();
}
if (gzipServletOS == null) {
gzipServletOS = new GZIPServletOS(getResponse().getOutputStream());
}
return gzipServletOS; // 多态返回,向上隐式转换
}
@Override
public PrintWriter getWriter() throws IOException {
// TODO Auto-generated method stub
if (gzipServletOS != null) { // os已经被占用就不能在使用out了
throw new IllegalStateException();
}
if (out == null) {
gzipServletOS = new GZIPServletOS(getResponse().getOutputStream());
OutputStreamWriter osw = new OutputStreamWriter(
gzipServletOS, getResponse().getCharacterEncoding());
out = new PrintWriter(osw);
}
return out;
}
@Override
public void setContentLength(int len) {
// TODO Auto-generated method stub
// 不实现此方法内容,因为真正的输出会被压缩
}
public void finish() { // 再对finish进行包装
gzipServletOS.finish();
}
}
最后加上过滤器,我们来看看是如何实现输出压缩的,压缩步骤分为三步:
- 检查请求的accept-encoding标头是否有gzip字符串,即判断浏览器是否有压缩响应的需求;
- 如果有这样的需求,就得设置响应的content-encoding标头为gzip;
- 接着就是压缩响应封装、doFilter、手动冲刷压缩缓冲区了;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
String encodings = ((HttpServletRequest)request).getHeader("accept-encoding");
if (encodings != null && encodings.indexOf("gzip") > -1) {
CompressWrapper respWrapper = new CompressWrapper((HttpServletResponse)response);
respWrapper.setHeader("content-encoding", "gzip");
chain.doFilter(request, respWrapper);
respWrapper.finish();
}
else {
chain.doFilter(request, response);
}
}
5. 异步处理(针对的是异步上下文对象,不是多线程)
如果浏览器请求了一个比较耗时的servlet,那就会一直等待直到服务器返回响应。在Servlet3.0中引入了异步处理的机制,允许servlet重新启动一个线程去处理耗时业务逻辑,原来web容器启动的线程可以直接返回响应,新启动的异步线程后续也可以响应浏览器。
- AsyncContext
这个对象里面包含的是发起异步任务时的上下文环境,它提供了一些工具方法(dispatch,获取request,response等),获取该对象的方法:
AsyncContext startAsync()
AsyncContext startAsync(ServletRequest,ServletResponse)
第一个方法是采用当前servlet的ServletRequest,ServletResponse对象,第二个方法是由自己封装新的ServletRequest,ServletResponse对象。下面我们来看看具体的代码:
servlet类
@WebServlet(urlPatterns="/async", asyncSupported=true)
public class AsyncServlet extends HttpServlet {
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
response.setContentType("text/html;charset=GBK");
PrintWriter out = response.getWriter();
out.println("进入Servlet的时间:" + new Date() + ".<br/>");
out.flush();
AsyncContext acontext = request.startAsync();
acontext.setTimeout(20*1000);
acontext.start(new Executor(acontext));
out.println("结束Servlet的时间:" + new Date() + ".<br/>");
out.flush();
}
}
Executor做了什么!
public class Executor implements Runnable {
private AsyncContext context;
public Executor(AsyncContext context) {this.context = context;}
public void run(){
try {
Thread.sleep(5000);
ServletRequest request = context.getRequest();
List<String> books = new ArrayList<String>();
books.add("book1"); books.add("book2"); books.add("book3");
request.setAttribute("books", books);
context.dispatch("/async.jsp");
} catch (Exception e) {
e.printStachTrace();
}
}
}
在Executor中,让线程睡眠5秒来模拟耗时的业务逻辑,最后调用AsyncContext的dispatch方法把请求转发到指定的jsp页面。被异步请求的页面需要指定session="false",表明不会重新创建session,下面是async.jsp的内容:
<%@ page contentType="text/html;chaset=GBK" language="java" session="fasle" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<ul>
<c:forEach items="${books}" var="book" >
<li>${book}</li>
</c:forEach>
</ul>
<%
out.println("业务调用结束的时间:" + new Date());
request.getAsyncContext().complete();//完成异步调用
%>
这里说明异步也可以延时响应客户端。请忽略jsp页面的写法,后续再讲解,它使用JSTL标签库。
以上是异步调用的代码,我们需要对servlet进行声明,告知web容器该servlet支持异步:
- @WebServlet中指定asyncSupported=true
- web.xml中配置
<servlet>
<servlet-name>async</servlet-name>
<servlet-class>com.abc.AsyncServlet</servlet-class>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>async</servlet-name>
<url-pattern>/async</url-pattern>
</servlet-mapping>
对于支持异步调用的Servlet来说,当Servlet以异步方式启用新线程后,该Servlet的执行不会被阻塞,该Servlet将可以向客户端浏览器生成相应——当新线程执行完成后,新线程生成的相应将再次被送往客户端浏览器。
当servlet启动异步线程之后,对于该线程是否执行成功,是否遇到问题我们就不知道了,为了调试代码,我们可以用Servlet3.0支持的异步监听器AsyncListener来实现对异步任务线程的监控,该监听器提供的方法如下:
- onStartAsync(AsyncEvent event):当异步调用开始时触发该方法
- onComplete(AsyncEvent event):当异步调用结束时触发该方法
- onError(AsyncEvent event):当异步调用出错时触发该方法
- onTimeout(AsyncEvent event):当异步调用超时时触发该方法
监听器代码
public class MyAsyncListener implements AsyncListener {
public void onComplete(AsyncEvent event) {
System.out.println("异步调用完成:" + new Date());
}
public void onError(AsyncEvent event) {
System.out.println("异步调用出错:" + new Date());
}
public void onStartAsync(AsyncEvent event) {
System.out.println("异步调用开始:" + new Date());
}
public void onTimeout(AsyncEvent event) {
System.out.println("异步调用超时:" + new Date());
}
}
通过AsyncContext来注册监听器
AsyncContext acontext = request.startAsync();
acontext.addListener(new MyAsyncListener());
6. 模拟服务器推播
开发web如果要实现服务器主动推送信息至浏览器,基本上只有几种方式:
- websocket
- 浏览器轮询,每隔一个时间段请求一次服务器
- comet技术
java在servlet3.0之后,实现了异步机制,在上一节,我们已经看异步任务的简单实现方式,这节就是利用异步机制实现,大致分为这几部分:
- 多线程异步执行的程序
- 接收请求的servlet
- 发起请求的页面
多线程异步执行程序:
Paste_Image.png
这里是Runnable多线程方式,实现了一个ServletContext的生命周期监听器,在这个监听器里面,循环处理异步任务上下文ArrayList,这个ArrayList的添加是在接收请求的servlet中实现的。
接收请求的servlet:
Paste_Image.png
这里就是接收请求的servlet,取得AsyncContext上下文对象,然后添加至asyncs队列中。这里如果需要监控多线程背后什么怎么运行的,可以采用上一节的异步监听方式。
请求页面这里就不发了,就是ajax异步请求servlet,然后得到数据之后,动态更新到DOM里就可以了!
网友评论