#转发与重定向 浏览器把请求发送给ServletA,ServletA把请求传递给ServletB,由ServletB进行继续处理,最后输出资源响应。
#转发 ##请求转发
- forward ServletA调用forward方法把请求转发给ServletB
- 将当前的request和response对象交给指定的web组件处理 浏览器不知道ServletA转发请求给了ServletB,对于浏览器来说发出一次请求,获取一次响应
- 一次请求,一次响应 请求转发过程中,浏览器URL地址栏不会发生变化
##转发对象
- RequestDispatcher对象 由Servlet容器创建,用来封装一个由路径所标示的服务器资源,该对象有两个比较重要的方法forward方法和include方法,forward方法是指转发,include方法指包含,把请求转发后,原有组件和新组件都输出响应信息。 ###通过两种方式获取转发对象
- 通过
HttpServletRequest
获取 - 通过
ServletContext
获取
##转发实例 ###通过request.getRequestDispatcher 转发路径:注意这里是转发Servlet路径,可以填写绝对路径和相对路径的
@Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 通过request对象获取转发对象 RequestDispatcher requestDispatcher = req.getRequestDispatcher("/forwardExample"); // 转发 requestDispatcher .forward(req, resp); }
通过URL进行访问与测试
http://localhost:8080/web_project_template/forward?user=123
输出结果
Class ServletForward Method initClass ServletForward Method doGet [request]:org.apache.catalina.connector.RequestFacade@5f36101bClass ServletForwardExample Method initClass ServletForwardExample Method doGet [request]:org.apache.catalina.core.ApplicationHttpRequest@51eeeb5b
可以看到实现了转发功能并显示出ServletForwardExample的返回结果
###通过ServletContext获取转发对象 ServletContext有两种方式获取转发对象,通过this.getServletContext().getNamedDispatcher
是需要知道Servlet名称(备注:在web.xml查看)。通过this.getServletContext.getRequestDispatcher
只能够填写绝对路径
// ServletContext可以通过两种方式获取转发对象 requestDispatcher = this.getServletContext().getNamedDispatcher("ServletForwardExample"); requestDispatcher = this.getServletContext().getRequestDispatcher("/forwardExample");
##用户登录流程 浏览器发送登录请求给服务器,服务器返回登录响应,在登录验证完成之后,我们通常会发现我们的浏览器会跳转到另外的页面。而且浏览器上的地址栏也改变了。这里我们做了一次请求,在我们登录验证完成之后,服务器端向浏览器返回了另外一个URL地址的响应信息。浏览器在接收到该响应信息后,会自动的向服务器请求返回地址,然后服务器端会返回对应的跳转结果。浏览器进入到另外一个页面。
#请求重定向 服务器是希望用户在登录后,进入到用户界面。也就是说服务器端希望ServletA处理结束,ServletB继续为用户服务。
- sendRedirect方法 ServletA调用sendRedirect方法,将客户端的请求重定向到ServletB
- 请求重定向:通过response对象发送给浏览器一个新的URL地址,让其重新请求
- 两次请求,两次响应 过程对用户是透明的,浏览器默认把第二次请求做掉了,需要注意,在请求重定向后,浏览器地址栏会发生响应的改变。
##请求重定向实例 可以使用绝对路径,或者相对路径
resp.sendRedirect("redirectExample");
浏览器访问路径
http://localhost:8080/web_project_template/redirect?user=123
Chrome开发者模式,可以看到有两次应答
注意:请求转发是同一个请求对象,只进行一次响应;请求重定向是两次请求,两次响应。如果在跳转中的URL并没有对应的parameter,则获取的值为空。
通过Chrome查看第一次响应的Location内容###重定向绝对路径问题
resp.sendRedirect("/redirectExample");
如果这样填写重定向的绝对路径,如果使用maven启动,或者在部署时,并不是部署到ROOT。则会发生访问的路径为
http://localhost:8080/redirectExample
而不是,我们所需要的
http://localhost:8080/web_project_template/redirectExample
##转发&重定向总结
- 浏览器地址栏变化 转发地址栏不发生变化,重定向地址栏将会变成重定向的地址
- 请求范围 转发是在同一个web应用中进行转发,而重定向既可以重定向到本web应用,也可以重定向到外部URL。
- 请求过程 请求转发是一次请求一次响应,请求重定向是两次请求两次响应。
#过滤器与监听器
#过滤器
- 过滤请求与响应
- 自定义过滤规则 按照过滤器的规范,编写对应代码
- 用于对用户请求进行预处理,和对请求响应进行后处理的web应用组件 过滤器能够对Servlet的请求和响应对象进行检查和修改,Servlet过滤器本身并不生成请求和响应对象。提供过滤功能,过滤器能够在Servlet调用之前检查Request对象,并能够修改Request header和Request内容,在Servlet被调用之后,能够检查Response对象,修改Response的Header和内容。
##过滤器工作原理
- 1.首先通过客户端,发送原始请求到Servlet容器,由于有过滤器,则请求发送给过滤器
- 2 经过过滤器处理,请求转发给对应的Servlet
- 3 Servlet处理完成后,将原始响应发送给过滤器
- 4 最后由过滤器发送过滤后的响应给客户端
##过滤器应用场景
- 用户认证 过滤非法用户,确定用户有没有登录,是否有权限访问页面。
- 编解码处理 如果我们的请求有乱码的问题,我们可以通过过滤器进行预处理,处理完成后将正确的结果发还给Servlet进行处理
- 数据压缩处理 当我们请求的数据比较,我们可以通过过滤器进行压缩,然后再将数据发到对应的服务端,这样减轻服务端的处理压力
##过滤器生命周期 filter的创建和销毁,同样由Servlet容器负责。Web应用启动的时候,Servlet容器会根据部署描述符的配置,创建filter的实例对象。调用filter的init方法,完成过滤器的初始化。为后续的拦截请求做准备。过滤器在生命周期中只会创建一次,所以init方法只会执行一次。
跟Servlet一样,在部署描述符的filter当中,也可以配置filterconfig的对象,用来存储filter的一些配置信息。在filter完成初始化工作之后,进入正式的过滤操作doFilter方法。这个方法与servlet的service方法类似,完成过滤的实际操作。对每个请求响应做出响应处理。 当客户端请求访问与过滤器相关联的URL时,Servlet过滤器就会执行对应的doFilter方法。我们可以在这个方法当中做前置处理和后置处理。 过滤器当Web应用被移除,或者容器服务重启时,执行destory销毁方法。当Servlet容器卸载掉对应的Filter对象之前,destory方法将会被调用。只执行一次。释放过滤器资源。##过滤器实例 Servlet部署描述符(web.xml)配置filter,部署描述符当中的Filter对象下的init-param所添加的参数,在TestFilter当中的doFilter方法通过FilterConfig参数传递并使用。
filter-mapping的url-pattern使用规则与servlet一致filterParam 111 TestFilter com.netease.server.example.web.controller.filter.TestFilter TestFilter /hello/world/*
创建TestFilter并继承Filter接口
public class TestFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("Class TestFilter Method init"); String value = filterConfig.getInitParameter("filterParam"); System.out.println("Class TestFilter Method init [filter.config key=filterParam]:" + value); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("Class TestFilter Method doFilter");// TODO } @Override public void destroy() { System.out.println("Class TestFilter Method destroy"); }
登录过的用户,直接跳转,如果没有登录的用户跳转到登录界面
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("Class TestFilter Method doFilter"); // 登录过的用户,直接跳转,如果没有登录的用户跳转到登录界面 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); } }
**注意:**相对路径../index.html
的..
为返回上一层路径
FilterChain参数,有doFilter方法,主要是把请求向下传递,请求将会传递到下一个过滤器,或者是客户端所请求的Servlet。
当我们没有登录的时候,在浏览器中输入http://localhost:8080/web_project_template/hello/world
会自动跳转登录界面。 登录结束后,当我们再次在浏览器中输入http://localhost:8080/web_project_template/hello/world
会跳转到对应的资源页面。 ##过滤器链 我们在Web应用中会有多个filter,这些filter组成filter链。一个请求通过filter-mapping匹配到多个filter,这个时候web服务器就会根据filter在部署描述符中的先后顺序,决定首先调用哪个filter。当第一个filter方法被调用时,servlet容器会创建一个代表filter链的FilterChain对象,传递给Filter的doFilter方法。如果开发者在doFilter方法中,调用了FilterChain对象的doFilter方法。则会传递给下一个Filter或者被调用资源的Servlet对象。
###过滤器链请求过程
- 客户端原始请求首先发送给第一个Filter
- 第一个filter调用了doFilter方法,就会把请求传递给下一个filter,如果第二个filter下还有filter,则继续传递
- 如果没有filter则把过滤后的请求,传递给被调用资源或者servlet对象。
- Servlet处理结果后,把原始响应传送给对应的filter
#监听器 监听器一般理解,有监听器监听信息,有终端接收信息。所以监听器分为两个部分,我们需要监听的东西称之为事件源。另外就是我们的监听器。
监听器首先向某个事件源进行注册,把监听器放到我们需要监听的地方,当事件发生后,事件源将会通知发送到对应的监听器,监听后的信息,我们需要进行相应的处理。 ##定义- 监听事件发生,在事件发生前后能够做出响应处理的web应用组件
这里我们需要注意的是,Servlet监听器注册,不是注册到事件源上,而是由Servlet容器负责注册。开发人员只需要在部署描述符中进行配置,然后servlet容器就会自动的把对应的监听器注册到对应的事件源中。
##监听器分类 Listener按照监听对象进行总体划分,还可以继续划分
- 监听应用程序环境(ServletContext)
-
- ServletContextListener 对ServletContext对象创建销毁进行监听
-
- ServletContextAttributeListener 对ServletContext属性监听器,当这些对象对应的属性有增删改的变化的时候,这些监听器就会被触发。
- 监听用户请求对象(ServletRequest)
-
- ServletRequestListener 对ServletRequest对象创建销毁进行监听
-
- ServletRequestAttributeListener 对ServletRequest属性监听器,当这些对象对应的属性有增删改的变化的时候,这些监听器就会被触发。
- 监听用户会话对象(HTTPSession)
-
- HttpSessionListener 对HttpSession对象创建销毁进行监听
-
- HttpSessionAttributeListener 对HttpSession属性监听器,当这些对象对应的属性有增删改的变化的时候,这些监听器就会被触发。
-
- HttpSessionActivationListener 监听Session在持久化时,磁盘或者从磁盘中从新加载到jvm中,触发的监听器。
-
- HttpSessionBindingListener 在Session对象进行调用attribute方法和removeattribute方法时进行调用。
##监听器的应用场景
- 应用统计 对用户登录进行统计,每个用户对应一个session,所以我们可以通过监听器对一个站点用户登录进行统计。
- 任务触发 比如招聘系统中,发现招聘者的状态发生了变化,举例:面试者的状态是面试成功,则给这个应聘者发送邮件通知。
- 业务需求
##监听器启动顺序 监听器的启动顺序与过滤器是一致的,在部署描述符中出现的越靠前,我们就会 越早的进行注册(初始化)
##监听器、过滤器、Servlet启动顺序 先创建监听器、在创建过滤器、最后创建Servlet
##监听器实例 创建监听器
public class TestListener implements HttpSessionAttributeListener, ServletContextListener, ServletRequestListener { // ServletRequestListener @Override public void requestDestroyed(ServletRequestEvent sre) { System.out.println("listener: request destroy"); } @Override public void requestInitialized(ServletRequestEvent sre) { System.out.println("listener: request init"); } // ServletContextListener @Override public void contextInitialized(ServletContextEvent sce) { System.out.println("listener: context init"); } @Override public void contextDestroyed(ServletContextEvent sce) { System.out.println("listener: context destroy"); } // HttpSessionAttributeListener @Override public void attributeAdded(HttpSessionBindingEvent event) { System.out.println("listener: session attribute added."); } @Override public void attributeRemoved(HttpSessionBindingEvent event) { System.out.println("listener: session attribute removed"); } @Override public void attributeReplaced(HttpSessionBindingEvent event) { System.out.println("listener: session attribute replaced"); }}
在对应的部署描述符中配置
com.netease.server.example.web.controller.listener.TestListener
课程提供的代码修复BUG后的打印输出(该BUG在使用session前就把session注销了)。
listener: context initClass TestFilter Method initClass TestFilter Method init [filterconfig key=filterParam]:111init /hello/*init /hellolistener: request initlistener: request destroylistener: request initlistener: request destroylistener: request initinit /hello/worldClass TestFilter Method doFilterservice methoddoGet methodlistener: request destroylistener: request initsecond login: 123listener: session attribute removedlistener: session attribute added.listener: request destroy
#Servlet并发处理 使用应用开发过程中,多个客户端同时请求同一Servlet。
##线程模型
- 1 客户端发送请求给Servlet容器
- 2 Servlet容器,首先把请求传递给调度器,由调度器统一的进行请求派发。
- 3 调度器会从Servlet容器中的线程池中,选取一个工作组线程 线程池,然后把请求派发给该线程,然后由该线程执行servlet的service方法。
- 4 同时,如果客户端发送第二份请求时,调度器会选取另外一个工作组线程,来服务这个新的请求。如果发现同一servlet收到多个请求,service方法将会在多线程中并发执行。 当线程使用完毕后,会把线程在放回线程池中。
- 5 如果现在线程池中的线程都在服务,如果这时有新的请求,一般情况下进行排队处理。
- 6 Servlet容器可以配置最大请求数量,超过这个数量,Servlet容器会直接拒绝这个请求。
##Servlet并发处理
- 单实例 我们知道Servlet只会初始化一次,只调用一次init方法。也就是说在整个Servlet中只会有一个Servlet对象,不管我们有多少请求,只针对同一Servlet对象实例。
- 多线程 请求处理由多个工作线程进行处理,同时请求线程数量的大小,由线程池的配置决定
- 线程不安全 默认没有加锁操作,则多个线程同时对Servlet的属性进行变更,发生不安全
##Servlet线程安全
-
变量的线程安全
-
- 参数变量本地化 - 尽量使用局部变量
-
- 使用同步块synchronized - 进行加锁处理 加锁时,我们需要注意,尽量缩小synchronized的代码范围,不要在Servlet上增加这个关键字,对性能损耗大
-
属性的线程安全
-
- ServletContext线程不安全 可以多线程同时读写ServletContext属性的
-
- HttpSession理论上线程安全 - 实际不安全 当发生同一浏览器,打开多个标签页,同时多次访问Servlet时,会启动多线程对Servlet进行使用。则会对同一HttpSession进行操作,导致线程不安全。
-
- ServletRequest线程安全 每一个工作线程只对应一个ServletRequest,则线程安全
-
避免在Servlet中创建线程
-
多个Servlet访问外部对象加锁
Servlet安全问题主要由于使用实例变量,尽量避免使用实例变量。如果在程序设计中无法避免使用实例变量,则使用同步的操作来保护我们需要使用的实例变量,为了保证系统性能。注意同步的范围。
##Servlet线程安全实例 ###Servlet不安全实例 人为制造不安全的Servlet
public class ConcurrentServlet extends HttpServlet { String name; @Override public void init() throws ServletException { System.out.println("Class ConcurrentServlet Method init"); super.init(); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("Class ConcurrentServlet Method doGet");// synchronized (this) { // 从URL获取username存放在属性中 name = req.getParameter("username"); PrintWriter out = resp.getWriter(); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } out.println("username: " + name);// } } @Override public void destroy() { System.out.println("Class ConcurrentServlet Method destroy"); super.destroy(); }}
部署描述符(web.xml)配置
ConcurrentServlet com.netease.server.example.web.controller.ConcurrentServlet ConcurrentServlet /concurrent
首先测试的URL:http://localhost:8080/web_project_template/concurrent?username=ddd
我们在先访问http://localhost:8080/web_project_template/concurrent?username=ddd
,再快速访问(5秒内,实际并发远比这个时间小的多)http://localhost:8080/web_project_template/concurrent?username=aaa
,Chrome开发者模式:
###Servlet修改成为安全实例 添加synchronized同步块进行处理
synchronized (this) {}
Servlet部分代码
@Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("Class ConcurrentServlet Method doGet"); synchronized (this) { // 从URL获取username存放在属性中 name = req.getParameter("username"); PrintWriter out = resp.getWriter(); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } out.println("username: " + name); } }
Chrome测试结果:
我们可以看到,优先访问的ddd,能够按照正确的时间进行返回,并返回正确的数据内容。 我们也可以看到aaa能够正确返回数据,但是由于添加了同步锁,导致需要之前的同步锁执行结束后,才能够执行当前操作内容,则耗时延长。