第3章.Servlet应用
转发与重定向
转发:浏览器发送资源请求到ServletA后,ServletA传递请求给ServletB,ServletB生成响应后返回给浏览器。
请求转发:forward:将当前的request和response对象交给指定的web组件处理
这个过程中浏览器只发了一次请求接收了一次响应(浏览器并不知道转发,地址栏url不变)
1. 获取转发对象RequestDispatcher--由Servlet容器创建,用于封装由路径所标志的服务器资源;
2. 然后调用转发对象中的forward方法;(另一个方法为include()--原有的和被转发的web组件均可输出相应信息)
如何获取RequestDispatcher?两种方法:
通过HttpServletRequest获取
通过ServletContext获取(之前提到ServletContext三个作用中的一条:获取转发)
i.e. ServletForward.java将请求转发给ServletForwardExample.java
RequestDispatcher rd = request.getRequestDispatcher("/forwardExample"); // 从HttpServletRequest中获取转发对象("/forwardExample"为绝对路径)
rd.forward(request,response); // 转发
i.e. 通过ServletContext获取转发对象:两种方法
1. rd = this.getServletContext().getNamedDispatcher("ServletForwardExample");
// ("ServletForwardExample"为Servlet名称)
2. rd = this.getServletContext().getRequestDispatcher("/forwardExample");
重定向:sendRedirect
study case: 用户登录
登录验证完成后,浏览器接收到服务器发来的另外一个地址的响应信息;
浏览器自动向服务器发送跳转请求,并得到服务器返回的跳转结果。
通过response对象发送给浏览器一个新url地址,让其重新请求。
两次请求,两次响应--浏览器地址栏url改变
i.e. ServletRedirect.java将请求重定向到ServletRedirectExample.java
ServletRedirect.java中:
response.sendRedirect("redirectExample"); // ("redirectExample"为相对路径)
若使用绝对路径"/redirectExample",则重定向到地址localhost:8080/redirectExample
-- 若想重定向到当前web项目的某资源:相对路径;若想到其他web项目:绝对路径。
ServletRedirectExample.java中:
syso: request.getParameter("user");
// 若直接使用传来的request进行读取数据等操作,错误:两次请求和两次响应
在Chrome的developer tools中,可以看到两次请求:
redirect?user=aaa -- response header: location:....../redirectExample
redirectExample。
转发&重定向总结:
浏览器地址栏变化:转发不变;重定向变化。
请求范围:在同一个web项目中转发;重定向可在不同web项目间重定向
请求过程:转发为一次,重定向为两次
过滤器与监听器
过滤器 filter:
通过自定义的过滤规则来过滤请求与响应。
用于对用户请求进行预处理、和对请求响应进行后处理的web引用组件。
对Servlet容器进行请求和响应的对象进行检查和修改。
过滤器本身并不生成请求和响应对象,只是提供了过滤的功能。
过滤器在Servlet被调用之前检查request对象,并能够修改request header和request的内容;
在servlet被调用之后,能够检查response对象,并能够修改response header和response的内容。
过滤器工作原理:
客户端发送原始请求到Servlet容器,该请求在到达容器之前会经过过滤器处理,过滤之后请求转发到对应Servlet;Servlet处理完请求后进行了响应,该原始响应发还到过滤器,最后由过滤器将过滤后的请求返回给客户端。
过滤器使用场景:
用户认证:过滤一部分非法用户,验证用户是否已经登录,是否拥有访问权限等
编解码处理:如果请求有乱码,可以通过过滤器进行预处理,以得到正确的编码结果
数据压缩处理:当请求数据较大时,对请求进行压缩,以减轻服务端的处理压力
过滤器的生命周期:创建和销毁是由servlet容器负责的
servlet容器根据部署描述符创建filter的实例对象(只会创建一次);
调用init();完成初始化工作,为拦截用户请求做好准备(同样只会调用一次);
(和servlet一样,可以配置一个filterConfig的对象,用来存储filter的配置信息)
初始化完成后,进入正式的过滤操作,doFilter()(类似于servlet中的service()):对请求和响应做实际处理
当客户端请求/响应与过滤器相关联的url时,过滤器会执行对应的doFilter()方法。
在web应用被移除或服务器停止时,会调用destroy()(只会调用一次,将过滤器资源释放)销毁filter对象。
i.e. TestFilter.java
new class--name:TestFilter.java--Interface:Filter(servlet)
--init(FilterConfig); doFilter(); destroy();
配置web.xml:
<filter>
<init-param> <--!由filterConfig.getInitParameter(param-name)读取-->
<param-name>filterParam</param-name>
<param-value>1</param-value>
</init-param>
<filter-name>TestFilter</filter-name>
<filter-class>com.netease.server.example.web.controller.TestServlet</filter-class>
<filter>
<filter-mapping>
<filter-name>TestFilter</filter-name>
<url-pattern>/hello/world/*</url-pattern>
<--! 符合该url pattern的请求才会经过该过滤器-->
</filter-mapping>
在刚才创建的TestFilter.java中完善三个方法:
public void init(FilterConfig filterConfig) throws ServletException {
// 获取在web.xml中配置的filter参数并打印
String value = filterConfig.getInitParameter("filterParam");
syso(value);
} public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 实现登陆过的用户可直接进行访问,否则会跳转到登录页面进行登陆步骤
HttpServletRequest req = (HttpServletRequest) request; // 强制类型转换
HttpSession session = req.getSession(); // 得到会话
// 检查属性,判断是否登陆过
if (session.getAttribute("userName") == null) {
HttpServletResponse res = (HttpServletResponse) response;
res.sendRedirect("../index.html"); // 跳转到登陆页面
} else {
// 访问对应资源
chain.doFilter(request, response);// 请求传递到下一个过滤器或是对应的Servlet
}
}
部署,访问/hello/world此时用户为未登录状态,重定向到index.html;
登陆后在地址栏输入/hello/world不进行重定向,直接返回响应。
过滤器链:
若一个请求通过filter-mapping匹配到多个filter,web服务器会根据部署描述符中的先后顺序决定调用顺序。
FilterChain chain.doFilter(req, res);
监听器:listener
监听事件发生,在事件发生前后能够做出相应处理的web应用组件
事件源:当有相应事件发生时,事件源将通知发送到对应的监听器
监听器:向事件源进行注册
处理:监听器收到通知后,进行对应操作
Servlet监听器不是直接注册到事件源上,而是由servlet容器进行注册。开发人员只需配置好即可。
监听器分类:按监听对象划分
监听应用程序环境 (ServletContext): ServletContextListener, ServletContextAttributeListener
监听用户请求对象 (ServletRequest): ServletRequestListener, ServletRequestAttributeListener
监听用户会话对象 (HTTPSession): HttpSessionListener, HttpSessionAttributeListener, HttpSessionActivationListener (监听session在写入磁盘或从磁盘中重新加载到jvm中时) HttpSessionBindingListener (在session对象进行调用attribute方法和removeAttribute方法时)
两大类:对象本身的监听器 (对象的创建和销毁时);对象属性的监听器 (当属性有增删改查时)
监听器应用场景:
应用监听:每一个用户对应一个session,对session进行监听,可以做到对用户登录的统计
任务触发:如在招聘网站某用户的简历状态更新了,比如变成了面试成功,便可以向应聘者发送一封邮件通知
监听器启动顺序:
与过滤器顺序一样,根据部署描述符中的先后顺序决定。
i.e. TestListener.java--interface (很多选项,如HttpSessionAttributeListener, ServletContextListener, ServletRequestListener)
@Override:
requestDestroyed(ServletRequestEvent);
requestInitialized(ServletRequestEvent);
contextInitialized(ServletContextEvent);
contextDestroyed(ServletContextEvent);
attributeAdded(HttpSessionBindingEvent);
attributeRemoved(HttpSessionBindingEvent);
attributeReplaced(HttpSessionBindingEvent);
web.xml:在部署描述符中配置这些listener:
<listener>
<listener-class>com.netease.server.example.web.controller.listener.TestListener</listener-class>
</listener>
listener自动被注册到相应事件,
如session.setAttribute("userName", userName); 时触发HttpSessionAttributeListener.attributeAdded
Servlet容器先创建各种监听器;再创建过滤器;最后创建Servlet对象。
Servlet并发处理
当有多个客户端同时访问服务器的同一个Servlet时:串行处理(效率低),并发处理(Servlet采用)
Servlet容器接收到来自客户端的请求后;
将请求发给调度器,由调度器进行统一的请求派发;
调度器从Servlet容器的线程池 (记得Tomcat的线程池吗) 中选取一个工作线程,把请求派发给该线程,由该线程执行servlet的service方法;
此时Servlet容器又接收到了另一个请求;
调度器从工作线程池中选出另外一个工作组线程来服务新的请求;
容器并不关心请求访问的是否为同一个servlet,当容器收到对同一个servlet的多个请求时,这个servlet的service方法会在多线程中并发执行
当线程处理完请求后,会被放回线程池中;
若线程池中的线程都被占用,且有新的请求过来,则这些新请求会做排队处理
Servlet容器也会配置一个最大排队数量,如果超过这个数量,Servlet容器将会拒绝响应的请求。
Servlet并发处理的特点:
单实例:不管有多少请求,只有一个Servlet实例对象
多线程:同时处理多个请求
线程不安全:单个实例却有多个线程同时访问:Sync问题
若要实现线程安全:
变量的线程安全:
参数变量本地化--局部变量
使用同步块:synchronized加锁处理--注意要尽可能缩小加锁的代码块
属性的线程安全:
ServletContext可以多线程读取--线程不安全:需要做同步处理
HTTPSession理论上线程安全,只能在处理同一个请求的线程中被访问
但是当用户打开属于同一个session的多个浏览器窗口时,对同一个session会进行多次请求,会分配给多个线程处理:需要同步处理
ServletRequest是线程安全的,因为对于同一个请求,只有一个线程进行处理
要避免在Servlet中创建线程:servlet本身被多线程执行,会导致情况复杂。
多个Servlet同时访问外部对象时需要加锁处理
写代码时尽量避免使用servlet实例变量,无法避免时则需要加上同步处理,而保证性能安全需要缩小同步处理的范围。
i.e. 线程不安全
ConcurrentServlet.java superclass: HttpServlet
@Override: init(); doGet(); destroy();
String name; protected void doGet(HttpServletRequest req, HttpServletResponse resp)
// 该servlet功能,读取并打印username
throws ServletException, IOException {
name = req.getParameter("username");
PrintWriter out = resp.getWriter();
out.println(name);
}
web.xml部署描述符中配置该servlet。
正常情况下该servlet运行没问题。当多个请求同时访问时,name这个实例变量就很可能出现错误。
在out.println()之前加入Thread.sleep(5000);后:
问题演示--实例变量的线程不安全:
1. 在地址栏打入/concurrent?username=ddd后,页面等待五秒,返回username ddd,正常
2. 在地址栏打入/concurrent?username=aaa后,页面等待五秒,返回username aaa,正常
3.在地址栏打入/concurrent?username=ddd, 立即新开窗口打入/concurrent?username=aaa,两个页面等待后的输出均为username aaa。
bugfix:加上synchronize同步块:
String name; protected void doGet(HttpServletRequest req, HttpServletResponse resp)
// 该servlet功能,读取并打印username
throws ServletException, IOException {
synchronsized(this) {
name = req.getParameter("username");
PrintWriter out = resp.getWriter();
out.println(name);
}
}
保证了实例变量的线程安全。
Servlet应用测试
下面哪个方法不是过滤器的生命周期中的方法?
- A.doFilter
- B.init
- C.destroy
- D.service2.00/2.00
下面哪项说法是错误的?
- A.客户端的请求可以交由多个过滤器处理
- B.在部署描述符中,如果过滤器定义在监听器前,则容器会先初始化过滤器2.00/2.00
- C.过滤器和监听器都是Web服务端应用组件
- D.ServletRequestAttributeListener是属于EventListeners
下面说法正确的是?
- A.ServletContext的属性是线程安全的
- B.HttpSession的属性是理论上线程安全2.00/2.00
- C.其它选项都是正确的
- D.ServletRequest的属性不是线程安全
下面获取请求转发对象(RequestDispatcher)方法错误的是?
- A.通过ServletRequest对象的getNamedDispatcher方法直接获取2.00/2.00
- B.通过ServletContext对象的getNamedDispatcher的方法获取
- C.通过ServletRequest对象的getRequestDispatcher方法直接获取
- D.通过ServletContext对象的getRequestDispatcher方法直接获取
下面关于Web应用组件启动顺序的说法错误的是?
- A.其它选项都不正确2.00/2.00
- B.Web应用组件启动过程中,过滤器的优先级高于Servlet
- C.Web应用组件启动过程中,监听器的优先级高于过滤器
- D.Web应用组件启动过程中,监听器的优先级高于Servlet
下面哪些是Servlet并发处理的特点?
- A.线程不安全1.00/3.00
- B.单实例1.00/3.00
- C.线程安全
- D.多线程1.00/3.00
下面哪些方法是可以做到Servlet线程安全?
- A.注意Servlet中属性的线程安全0.75/3.00
- B.尽量避免在Servlet中创建线程0.75/3.00
- C.注意Servlet中声明的变量的线程安全,尽量不要使用实例变量0.75/3.00
- D.多个Servlet访问的外部对象需要加锁处理0.75/3.00
请求转发是一次请求,一次响应
- A.×
- B.√2.00/2.00
请求重定向是两次请求,两次响应
- A.×
- B.√2.00/2.00
请求转发的过程中,浏览器的地址栏会发生变化
- A.√
- B.×2.00/2.00
请求重定向的过程中,浏览器的地址栏不会发生变化
- A.√
- B.×2.00/2.00
请求转发可以跨不同的Web应用程序
- A.√
- B.×2.00/2.00
请求重定向可以跨不同的Web应用程序
- A.×
- B.√2.00/2.00
请求重定向中第二次请求的完成是由浏览器自动完成的
- A.×
- B.√2.00/2.00
当存在多个监听器的时候,其初始化顺序是按照部署描述符中定义的顺序来初始化监听器的
- A.×
- B.√2.00/2.00
当多个客户端(一定数量)同时请求同一个Servlet的时候,容器可以同时处理
- A.√2.00/2.00
- B.×
监听器可以分为EventListeners和LifecycleListeners两种类型
- A.×
- B.√2.00/2.00