10-Listener&Filter

什么是监听器?

监听器就是一个实现特定接口的普通 Java 程序,这个程序专门用于监听另一个 Java 对象的方法调用或属性改变,当被监听对象发生上述事件后,监听器某个方法将立即被执行。

Servlet 监听器

在 Servlet 规范中定义了多种类型的监听器,它们用于监听的事件源分别为 ServletContext,HttpSession 和 ServletRequest 这三个域对象,一共8个。

监听作用域的创建和销毁

  • ServletContextListener<I>
    void contextInitialized(ServletContextEvent sce);
    void contextDestroyed(ServletContextEvent sce);
    
    • 作用:用来监听 ServletContext 对象创建和销毁
    • 创建:服务器启动,web 应用加载后立即创建代表当前 web 应用的 ServletContext 对象
    • 销毁:服务器关闭或 web 应用被移除出容器,随着 web 应用的销毁而销毁
  • HttpSessionListener<I>
    void sessionCreated(HttpSessionEvent se);
    void sessionDestroyed(HttpSessionEvent se);
    
    • 作用:用来监听 HttpSession 对象创建和销毁
    • 创建:第一次调用 request.getSession() 时,创建代表当前会话的 session 对象
    • 销毁:① 超时 ② 调用 invalidate() ③ server 非正常关闭,随着 web 应用的销毁而销毁
  • ServletRequestListener<I>
    void requestDestroyed(ServletRequestEvent sre);
    void requestInitialized(ServletRequestEvent sre);
    
    • 作用:用来监听 ServletRequest 对象创建和销毁
    • 创建:请求开始,创建代表请求的 request 对象
    • 销毁:请求结束,代表请求的 request 对象销毁

补充:如果 server 是正常关闭,session会被"钝化"而非销毁

  • 当 server 正常关闭时,还存活着的 session 会随着服务器的关闭被以文件的形式保存在 tomcat/work/SESSIONS.ser,这个过程被称之为"session的钝化"。也就是说,如果你注册了 HttpSessionListener 后,其中的 sessionDestroyed() 在 server 正常关闭时不会被调用
  • 当 server 再次正常开启时,会加载该文件,从中恢复之前保存起来的 session 对象。同理,server - restart 的时候也不会调用监听器中的 sessionCreated()
  • 想要随着 session 被 "钝化/活化" 的对象,类必须实现 serializable<I>

监听作用域中属性变化

  • ServletContextAttributeListener 此接口的实现接收 Web 应用程序的 Servlet 上下文中的属性列表更改通知
    void attributeAdded(ServletContextAttributeEvent scab)
        Notification that a new attribute was added to the servlet context.
    void attributeRemoved(ServletContextAttributeEvent scab)
        Notification that an existing attribute has been removed from the servlet context.
    void attributeReplaced(ServletContextAttributeEvent scab)
        Notification that an attribute on the servlet context has been replaced.
    
  • HttpSessionAttributeListener 实现此侦听器接口可获取此 Web 应用程序内会话属性列表更改的通知
    void attributeAdded(HttpSessionBindingEvent se)
        Notification that an attribute has been added to a session.
    void attributeRemoved(HttpSessionBindingEvent se)
        Notification that an attribute has been removed from a session.
    void attributeReplaced(HttpSessionBindingEvent se)
        Notification that an attribute has been replaced in a session.
    
  • ServletRequestAttributeListener 由想要在 request 域属性更改时获得通知的开发人员实现
    void attributeAdded(ServletRequestAttributeEvent srae)
        Notification that a new attribute was added to the servlet request.
    void attributeRemoved(ServletRequestAttributeEvent srae)
        Notification that an existing attribute has been removed from the servlet request.
    void attributeReplaced(ServletRequestAttributeEvent srae)
        Notification that an attribute was replaced on the servlet request.
    

补充:替换的时候要注意!

public void attributeReplaced(ServletContextAttributeEvent scab) {
    // 这里的 getValue() 获取的是之前的值
    System.out.println(scab.getName() + " = 属性被替换了 = " + scab.getValue());
    // 如果想要拿到最新的值
    System.out.println(scab.getServletContext().getAttribute(scab.getName()));
}

Bean在Session中状态变化

标题的完整名称:使 JavaBean 自己感知自己在 Session 中状态变化的监听器

  • HttpSessionBindingListener 使对象在被绑定到会话或从会话中取消对它的绑定时得到通知
    void valueBound(HttpSessionBindingEvent event)
        通知对象它将被绑定到某个会话并标识该会话
    void valueUnbound(HttpSessionBindingEvent event)
        通知对象要从某个会话中取消对它的绑定并标识该会话
    
  • HttpSessionActivationListener 绑定到会话的对象可以侦听通知它们会话将被钝化和会话将被激活的容器事件
    void sessionWillPassivate(HttpSessionEvent se) 通知会话即将被钝化
    void sessionDidActivate(HttpSessionEvent se) 通知会话刚刚被激活
    

注意:

  1. 调用session.invalidate()session超时,session 都会先将绑定在身上的属性解绑,然后再消失

  2. 这两个监听器很特殊,不需要自己去写类实现,也不需要在 web.xml 中注册,只要使 JavaBean 实现这个接口就能起作用

监听器实现

  • 和编写其它事件监听器一样,编写 Servlet 监听器也需要实现一个特定的接口,并针对相应动作覆盖接口中的相应方法。
    • 接口声明的方法都会有 Event 对象,该对象代表"事件"
    • Event 对象主要作用就是用来获取"事件源"
  • 将监听器实现类注册到被监听对象身上
    • 和其它事件监听器略有不同的是,Servlet 监听器的注册不是直接注册在事件源上,而是由 web 容器负责注册,开发人员只需在 web.xml 文件中使用 <listener> 标签配置好监听器,web容器就会自动把监听器注册到事件源中。
      <listener>
          <listener-class>监听器的全路径名</listener-class>
      </listener>
      
    • 一个 web.xml 文件中可以配置多个 Servlet 事件监听器,web 服务器按照它们在 web.xml 文件中的注册顺序来加载和注册这些 Serlvet 事件监听器。

exer:用户登录列表踢人

功能分析

  • index.jsp
    • 未登录:[登陆]
    • 已登陆:[用户列表] [注销]
  • login.jsp
    • 提供登陆表单
    • 提交到 LoginServlet
  • MySCListener
    • implements ServletContextListener
    • contextInitialized() 里创建一个 userMap<User,HttpSession> 放入 application 域中
  • User
    • 实现 Serializable,HttpSessionBindingListener 接口
    • 登录信息还要存放在 context.userMap 中,重写 hashCode()equals() 以校验是否重复登陆
  • LoginServlet
    • 检查用户名密码是否正确
    • 即使用户名/密码正确,也得看看是否在 application 域中是否存在。否则就有可能造成重复登陆
      • 已存在:根据 user,拿出对应存在的 session,然后使之失效。User 实现了 HttpSessionBindingListener,所以,实现的方法内部会自动从 context.userMap 中删除该键值对的(老 user 踢掉,新 user 登录)
      • 不存在:直接存入 session 域即可。User 实现了HttpSessionBindingListener,所以,实现的方法内部会自动向 context.userMap 添加该键值对的
    • 重定向到主页
  • LogoutServlet
    • session.invalidate()
    • 重定向到主页
  • listUser.jsp
    • 展示所有已登陆的用户(如果是管理员,提供踢人功能)
    • 注意:用 <c:forEach> 的时候,嵌套的 if 语句中 test="${sessionScope.user.role == 'admin'}",可别写成 test="${entry.user.role}"
  • KickUserServlet
    • 使要踢的用户的 session 失效
    • 重定向到 listUser.jsp

代码实现

index.jsp

<body>
    <h1>用户踢人案例</h1>
    <c:if test="${sessionScope.user == null }">
        欢迎光临! 游客!
        <a href="${pageContext.request.contextPath }/login.jsp">[登陆]</a>
    </c:if>
    <c:if test="${sessionScope.user != null }">
        欢迎回来! ${sessionScope.user.username }!
        <a href="${pageContext.request.contextPath }/servlet/LogoutServlet">[注销]</a>
        <a href="${pageContext.request.contextPath }/listUser.jsp">[在线用户列表]</a>
    </c:if>
</body>

MySCListener(web.xml 中记得注册)

public class MySCListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        ServletContext context = sce.getServletContext();
        context.setAttribute("userMap", new LinkedHashMap<User,HttpSession>());
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {}
}

User

public class User implements Serializable, HttpSessionBindingListener {
    private String id;
    private String username;
    private String password;
    private String role;

    // 构造器(无参, 含参)

    // getter, setter

    // 根据 id 和 username 字段重写这俩方法
    // hashCode(), equals(Object obj)

    @Override
    public void valueBound(HttpSessionBindingEvent event) {
        // 向 userMap 中保存自己的登陆信息
        ServletContext context = event.getSession().getServletContext();
        Map<User,HttpSession> map = (Map<User,HttpSession>)context.getAttribute("userMap");
        map.put(this, event.getSession());
    }

    @Override
    public void valueUnbound(HttpSessionBindingEvent event) {
        // 从userMap 中移除自己的登陆信息
        ServletContext context = event.getSession().getServletContext();
        ((Map<User,HttpSession>)context.getAttribute("userMap")).remove(this);
    }

LogoutServlet

public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    if(request.getSession(false) != null)
        request.getSession().invalidate();
    response.sendRedirect(request.getContextPath());
}

LoginServlet

public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    request.setCharacterEncoding("utf-8");
    response.setContentType("text/html;charset=utf-8");
    // 获取用户名密码
    String username = request.getParameter("username");
    String password = request.getParameter("password");

    // 校验用户名密码
    User user = null;
    String sql = "select * from users where username=? and password=?";
    QueryRunner runner = new QueryRunner(DaoUtils.getSource());
    try {
        user = runner.query(sql, new BeanHandler<User>(User.class), username, password);
    } catch (SQLException e) {
        e.printStackTrace();
    }

    if(user != null) {
        Map<User, HttpSession> map =
            (Map<User, HttpSession>)getServletContext().getAttribute("userMap");
        HttpSession session = map.get(user);
        // 如果已登录,则踢掉,再重新登录该用户
        if(session != null) session.invalidate();
        // 向 session 域添加登陆标记
        request.getSession().setAttribute("user", user);
        // 重定向到主页
        response.sendRedirect(request.getContextPath());
    } else response.getWriter().write("username/password is wrong!");
}

listUser.jsp

<body>
    <c:if test="${sessionScope.user==null }">
        <c:redirect url="/login.jsp" context="${pageContext.request.contextPath }"></c:redirect>
    </c:if>
    <div align="center">
        <h1>在线用户</h1>
        <c:forEach items="${applicationScope.userMap }" var="entry">
            ${entry.key.username }
            <c:if test="${sessionScope.user.role == 'admin' }">
                <a href="${pageContext.request.contextPath }/servlet/KickUserServlet?id=${entry.key.id}">Out</a>
            </c:if>
        <br>
        </c:forEach>
    </div>
</body>

KickUserServlet

public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    // 1. 获取要踢下线的人的 id
    String id = request.getParameter("id");
    User user = null;
    // 2. 根据 id 查询出用户
    String sql = "select * from users where id = ?";
    try {
        user = new QueryRunner(DaoUtils.getSource()).query(
                    sql, new BeanHandler<User>(User.class), id);
    } catch (SQLException e) {
        e.printStackTrace();
        throw new RuntimeException(e);
    }
    // 3. 从 userMap 中找到这个用户,对应的 session
    Map<User,HttpSession> map =
        (Map<User,HttpSession>) getServletContext().getAttribute("userMap");
    HttpSession session = map.get(user);
    // 4. 使 session 无效
    if(session != null) session.invalidate();
    // 5. 重定向到 listUser.jsp
    response.sendRedirect(request.getContextPath() + "/listUser.jsp");
}

过滤器

简单说明

Filter 也称之为过滤器,它是 Servlet 技术中最实用的技术,web 开发人员通过 Filter 技术,对 web 服务器管理的所有 web 资源:例如Jsp,Servlet,静态图片文件或静态 html 文件等进行拦截,从而实现一些特殊的功能。例如实现URL 级别的权限访问控制、过滤敏感词汇、压缩响应信息等一些高级功能。

过滤器会根据请求的 URL 分析要拦截什么样的请求。所谓的"拦截",就是在真正请求资源之前,将代表请求的 request 对象和代表响应的 response 对象截获。从而实现:在资源执行之前做一些额外的操作、在资源执行之后 做一些额外的操作、控制是否允许访问资源。

生命周期

  • 当 sever 启动,web 应用加载后,立即创建这个 web 应用中的所有的过滤器。创建出来后,立即调用 init() 执行初始化操作
  • 而后一直驻留在内存中,为后续的拦截进行服务。每次拦截到请求后都会导致 doFilter() 执行
  • 在 sever 关闭/web应用被移除出容器,随着web应用的销毁,过滤器对象销毁。销毁之前,调用 destroy() 执行善后工作

开发过程

1. 写一个类实现 Filter<I>

  • void init(FilterConfig filterConfig)
    • FilterConfig:代表 web.xml 中对当前过滤器的配置信息
    • 通过该形参可获取 ServletContext 对象:getServletContext()
    • 还可以获取初始化信息:Enumeration getInitParameterNames()String getInitParameter(String name)
  • void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    • FilterChain:代表过滤器链的对象。一个资源可能被多个过滤器所拦截到,拦截的顺序和过滤器在 web.xml 中 <filter-mapping> 的配置顺序相同。所有对当前资源访问进行拦截的过滤器,按照拦截顺序就组成了一个过滤器链
    • FilterChain 提供了 doFilter(),这个方法一旦被调用,就表明当前过滤器没问题了,执行过滤器链的下一个节点。这个过滤器链的最后一个节点就是要访问的资源
    • 执行过程有点像"方法调用"
      ![](_v_images/20200727081911668_22707.png =350x)
  • void destroy()

2. 在 web.xml 注册过滤器

<filter>
    <filter-name>______</filter-name>
    <filter-class>______</filter-class>
    <init-param>
        <param-name>______</param-name>
        <param-value>______</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>______</filter-name>
    <url-pattern>______</url-pattern>
    <servlet-name>______</servlet-name>
    <dispatcher>______</dispatcher>
</filter-mapping>
  • 一个 <filter> 可以配置多个 <filter-mapping>
  • <filter-mapping> 里可以配置多个 <url-pattern> (写法和 <servlet-mapping> 相同),也可以配置多个 <servlet-name> (其中填入的名字明确通知要拦截哪个名字的 Servlet)
  • <filter-mapping> 里还可以配置一个叫 <dispatcher> 的子标签,该标签用来配置拦截哪种方式的对资源的访问。如果不配置,默认只拦截对"REQUEST"方式的请求;也可以配置多个,表示拦截多种方式对资源的访问
    • REQUEST:当用户直接访问页面时,Web 容器将会调用过滤器。除此之外,该过滤器不会被调用
    • INCLUDE:如果目标资源是通过 RequestDispatcher 的 include() 访问时,那么该过滤器将被调用。除此之外,该过滤器不会被调用
    • FORWARD:如果目标资源是通过 RequestDispatcher 的 forward() 访问时,那么该过滤器将被调用,除此之外,该过滤器不会被调用
    • ERROR:如果目标资源是通过声明式异常处理机制调用时,那么该过滤器将被调用。除此之外,过滤器不会被调用

执行流程


exer:全站乱码过滤器

主要就是想修改 request 对象中请求参数的乱码,但 request 里并没有设置请求参数的方法。但可以换个思路,既然改造不了请求参数,但可以改造拿请求参数的方法!即通过重写 getParameterMap()getParameterValues()getParameter() 这三个方法,解决请求参数乱码问题。

改造方法 → 改造对象 → 3 种方式:① 继承 ② 装饰 ③ 动态代理。

这里选用 [装饰]。不用 ① 是因为对象都有了;不用 ③ 是因为要改造的仨方法在重写时,内部会有调用关系。

isNotEncode 变量的意义:getParameterMap() 有个小细节,在该方法第 1 次被调用的时候,sever 会根据请求参数组成这个 Map,然会返回,同时,还会将这个组织好的 Map 缓存起来;之后再调用该方法,就不会去请求参数中重新组织了,而是直接返回缓存的这个Map。所以,这个编解码的过程只能做一次。故设立一个成员变量,用来标识 Map 是否已经被解决乱码问题,如果是true,说明没解决,此时解决乱码问题,并将 该变量设为 false。如果是false,说明乱码问题已经解决(内存中已经有那个Map了),直接返回 Map 即可。

public class EncodingFilter implements Filter {

    private String charset;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        String v = filterConfig.getInitParameter("charset");
        charset = (v == null ? "utf-8" : v);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        response.setContentType("text/html;charset="+charset);
        chain.doFilter(new MyRequest((HttpServletRequest) request), response);
    }

    @Override
    public void destroy() {}

    // 装饰模式
    class MyRequest extends HttpServletRequestWrapper {

        private HttpServletRequest request;

        private boolean isNotEncode = true;

        public MyRequest(HttpServletRequest request) {
            super(request);
            this.request = request;
        }

        @Override
        public Map<String, String[]> getParameterMap() {
            try {
                if (request.getMethod().equalsIgnoreCase("post")) {
                    request.setCharacterEncoding(charset);
                    return request.getParameterMap();
                } else if (request.getMethod().equalsIgnoreCase("get")) {
                    Map<String,String[]> map = request.getParameterMap();
                    if (isNotEncode) {
                        for (Entry<String,String[]> entry : map.entrySet()) {
                            String[] vs = entry.getValue();
                            for (int i = 0; i<vs.length;i++)
                                vs[i] = new String(vs[i].getBytes("iso8859-1"), charset);
                        }
                        isNotEncode = false;
                    }
                    return map;
                } else return request.getParameterMap();
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }
        }

        @Override
        public String[] getParameterValues(String name) {
            return getParameterMap().get(name);
        }

        @Override
        public String getParameter(String name) {
            String[] ss = getParameterValues(name);
            return ss==null ? null : ss[0];
        }
    }
}
<filter>
    <description>全站乱码过滤器</description>
    <filter-name>EncodingFilter</filter-name>
    <filter-class>cn.edu.nuist.filter.EncodingFilter</filter-class>
    <init-param>
        <param-name>charset</param-name>
        <param-value>utf-8</param-value>
    </init-param>
</filter>

exer:30天自动登录过滤器

LoginServlet

public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    request.setCharacterEncoding("utf-8");
    response.setContentType("text/html;charset=utf-8");
    // 获取用户名密码
    String username = request.getParameter("username");
    String password = MD5Utils.md5(request.getParameter("password"));
    // 校验用户名密码
    User user = null;
    String sql = "select * from users where username=? and password=?";
    QueryRunner runner = new QueryRunner(DaoUtils.getSource());
    try {
        user = runner.query(sql, new BeanHandler<User>(User.class), username, password);
    } catch (SQLException e) {
        e.printStackTrace();
    }

    if(user != null) {
        // 向session域添加登陆标记
        request.getSession().setAttribute("user", user);
        // 自动登陆功能
        if("true".equals(request.getParameter("autoLogin"))) {
            Cookie autoLoginC = new Cookie("autoLogin", username+"="+password);
            autoLoginC.setMaxAge(3600*24*7);
            autoLoginC.setPath(request.getContextPath());
            response.addCookie(autoLoginC);
        } else {
            Cookie c = new Cookie("autoLogin", "");
            c.setMaxAge(0);
            c.setPath(request.getContextPath());
            response.addCookie(c);
        }
        // 重定向到主页
        response.sendRedirect(request.getContextPath());
    } else response.getWriter().write("username/password is wrong!");
}

AutoLoginFilter

public class AutoLoginFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest)request;
        // 只有未登录的用户
        if(httpRequest.getSession(false)==null
                || httpRequest.getSession().getAttribute("user")==null) {
            Cookie[] cs = httpRequest.getCookies();
            Cookie findC = null;
            if(cs != null)
                for(Cookie c : cs)
                    if("autoLogin".equals(c.getName()))
                        findC = c;
            // 只有带了 autoLogin-Cookie 的client
            if(findC != null) {
                String username = findC.getValue().split("=")[0];
                String password = findC.getValue().split("=")[1];
                // 校验用户名密码
                User user = null;
                String sql = "select * from users where username=? and password=?";
                QueryRunner runner = new QueryRunner(DaoUtils.getSource());
                try {
                    user = runner.query(sql, new BeanHandler<User>(User.class)
                            , username, password);
                } catch (SQLException e) {
                    e.printStackTrace();
                }
                // 只有 AutoLogin-Cookie 中的用户名密码正确
                if(user != null) httpRequest.getSession().setAttribute("user", user);
            }
        }
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {}

}

LogoutServlet

public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    if(request.getSession(false) != null)
        request.getSession().invalidate();
    // 删除自动登陆Cookie
    Cookie c = new Cookie("autoLogin","");
    c.setPath(request.getContextPath());
    c.setMaxAge(0);
    response.addCookie(c);
    response.sendRedirect(request.getContextPath());
}

猜你喜欢

转载自www.cnblogs.com/liujiaqi1101/p/13388673.html