Java开发工程师(Web方向) - 02.Servlet技术 - 第3章.Servlet应用

时间:2021-09-26 19:42:32

第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应用测试

本次得分为:36.00/36.00, 本次测试的提交时间为:2017-08-10, 如果你认为本次测试成绩不理想,你可以选择再做一次。
1单选(2分)

下面哪个方法不是过滤器的生命周期中的方法?

  • A.doFilter
  • B.init
  • C.destroy
  • D.service2.00/2.00
2单选(2分)

下面哪项说法是错误的?

  • A.客户端的请求可以交由多个过滤器处理
  • B.在部署描述符中,如果过滤器定义在监听器前,则容器会先初始化过滤器2.00/2.00
  • C.过滤器和监听器都是Web服务端应用组件
  • D.ServletRequestAttributeListener是属于EventListeners
3单选(2分)

下面说法正确的是?

  • A.ServletContext的属性是线程安全的
  • B.HttpSession的属性是理论上线程安全2.00/2.00
  • C.其它选项都是正确的
  • D.ServletRequest的属性不是线程安全
4单选(2分)

下面获取请求转发对象(RequestDispatcher)方法错误的是?

  • A.通过ServletRequest对象的getNamedDispatcher方法直接获取2.00/2.00
  • B.通过ServletContext对象的getNamedDispatcher的方法获取
  • C.通过ServletRequest对象的getRequestDispatcher方法直接获取
  • D.通过ServletContext对象的getRequestDispatcher方法直接获取
5单选(2分)

下面关于Web应用组件启动顺序的说法错误的是?

  • A.其它选项都不正确2.00/2.00
  • B.Web应用组件启动过程中,过滤器的优先级高于Servlet
  • C.Web应用组件启动过程中,监听器的优先级高于过滤器
  • D.Web应用组件启动过程中,监听器的优先级高于Servlet
6多选(3分)

下面哪些是Servlet并发处理的特点?

  • A.线程不安全1.00/3.00
  • B.单实例1.00/3.00
  • C.线程安全
  • D.多线程1.00/3.00
7多选(3分)

下面哪些方法是可以做到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
8判断(2分)

请求转发是一次请求,一次响应

  • A.×
  • B.√2.00/2.00
9判断(2分)

请求重定向是两次请求,两次响应

  • A.×
  • B.√2.00/2.00
10判断(2分)

请求转发的过程中,浏览器的地址栏会发生变化

  • A.√
  • B.×2.00/2.00
11判断(2分)

请求重定向的过程中,浏览器的地址栏不会发生变化

  • A.√
  • B.×2.00/2.00
12判断(2分)

请求转发可以跨不同的Web应用程序

  • A.√
  • B.×2.00/2.00
13判断(2分)

请求重定向可以跨不同的Web应用程序

  • A.×
  • B.√2.00/2.00
14判断(2分)

请求重定向中第二次请求的完成是由浏览器自动完成的

  • A.×
  • B.√2.00/2.00
15判断(2分)

当存在多个监听器的时候,其初始化顺序是按照部署描述符中定义的顺序来初始化监听器的

  • A.×
  • B.√2.00/2.00
16判断(2分)

当多个客户端(一定数量)同时请求同一个Servlet的时候,容器可以同时处理

  • A.√2.00/2.00
  • B.×
17判断(2分)

监听器可以分为EventListeners和LifecycleListeners两种类型

  • A.×
  • B.√2.00/2.00