JavaEE Filter和Listener

Servlet可以增加Filter处理链,也可以注册Listener对事件进行监听。

Filter

Filter作为一种过滤器,既可以对客户端请求进行预处理,也可以对服务器响应进行后处理,是个典型的处理链。

Filter的工作流程是:
1. Filter对用户请求进行拦截并进行预处理;
2. 如果Filter没有截断用户请求,则将用户请求交由Servlet处理;
3. Servlet根据用户请求生成响应,Filter拦截此响应并进行后处理,最后将响应发送给用户。

日志Filter

Filter最普遍的作用就是提供日志记录,我们下面定义一个日志过滤器,记录请求的URL和响应的ContentType。

// LogFilter.java

@WebFilter(filterName="LogFilter", urlPatterns="/*")
public class LogFilter implements Filter {
    private static String logdir="F:\\workspace\\javaEE\\WebDemo\\res\\log\\";
    private PrintStream logger;
    private static final SimpleDateFormat sdf=
            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public void init(FilterConfig fConfig) throws ServletException {
        GregorianCalendar calendar=new GregorianCalendar();
        int year=calendar.get(Calendar.YEAR);
        int month=calendar.get(Calendar.MONTH);
        int day=calendar.get(Calendar.DAY_OF_MONTH);
        String logfile=logdir+year+"_"+month+"_"+day+".log";
        try {
            logger=new PrintStream(new FileOutputStream(logfile));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest hrequest=(HttpServletRequest) request;
        logger.println("[RESQUEST] "+sdf.format(new Date()));
        logger.println("url="+hrequest.getScheme()+"://"+hrequest.getServerName()+":"+hrequest.getServerPort()+hrequest.getRequestURI());
        chain.doFilter(request, response);
        logger.println("[RESPONSE] "+sdf.format(new Date()));
        logger.println("contentType="+response.getContentType());
        logger.println("==============================");
    }

    public void destroy() {
        if(logger!=null) {
            logger.flush();
            logger.close();
        }
    }
}

这个过滤器有三个方法:
- init:对Filter进行初始化,这里负责创建log输出流;
- doFilter:对客户端请求进行预处理,对服务器响应进行后处理,这里负责打印log;
- destroy:对Filter进行销毁,主要是对资源进行回收,这里对log输出流进行关闭。

可以发现,Filter的编写和Servlet很相似,Filter的doFilter方法相比于Servlet的service方法只多了一个FilterChain类型的参数,这个参数负责将用户请求“传递”下去。另外Filter也需要用@WebFilter注解进行配置(另外还可以在web.xml文件中进行配置),此注解有以下属性:
- asyncSupport:指定是否支持异步操作;
- dispatcherTypes:指定对哪种dispatch模式的请求进行过滤,支持ASYNCERRORFORWARDINCLUDEREQUEST这五个值的任意组合,默认对这五种模式进行过滤;
- filterName:指定该Filter的名字;
- initParams:指定该Filter的初始配置参数;
- servletNames:该属性可指定多个Servlet名称,Filter仅对这几个Servlet进行过滤;
- urlPatterns:该属性可指定多个URL,Filter可对这些URL进行过滤。

这里我们指定urlPatterns属性值为/*,即对所有用户请求进行拦截。

运行该web应用,随便点击几个页面,我们可以在log文件中看到日志记录:

==============================
[RESQUEST] 2017-02-04 21:22:53
url=http://localhost:8080/WebDemo/
[RESPONSE] 2017-02-04 21:22:53
contentType=text/html;charset=UTF-8
==============================
[RESQUEST] 2017-02-04 21:23:01
url=http://localhost:8080/WebDemo/mytag.jsp
[RESPONSE] 2017-02-04 21:23:01
contentType=text/html;charset=GBK
==============================
[RESQUEST] 2017-02-04 21:23:16
url=http://localhost:8080/WebDemo/image
[RESPONSE] 2017-02-04 21:23:16
contentType=image/jpg
==============================
...

跳转Filter

Filter可以将多个Servlet中的共同部分抽取出来,使Servlet将注意力集中在特定请求的处理上,例如为request设置编码字符集等。

之前我们写过一个登录-跳转-欢迎程序,用户通过登录页面登录,跳转页面根据用户名和密码判断跳转到哪个页面,如果匹配则跳转到在线页面,否则重新回到登录页面(这个程序可以在这里找到)。我们可以在这个程序的基础上进行改进:使用Filter拦截对欢迎页面的请求,判断请求用户是否在线,在线则“放行”,否则跳转到登录页面。



@WebFilter(
        initParams = { 
                @WebInitParam(name = "encoding", value = "GBK"), 
                @WebInitParam(name = "loginPage", value = "/user-login.jsp")
        }, 
        urlPatterns = { "/online" })
public class UserLoginFilter implements Filter {
    private String encoding;
    private String loginPage;

    public void init(FilterConfig fConfig) throws ServletException {
        encoding=fConfig.getInitParameter("encoding");
        loginPage=fConfig.getInitParameter("loginPage");
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        request.setCharacterEncoding(encoding);
        HttpSession session=((HttpServletRequest)request).getSession(true);
        String user=(String) session.getAttribute("user");
        if(user==null || user.equals("")) {
            RequestDispatcher dispatcher=request.getRequestDispatcher(loginPage);
            dispatcher.forward(request, response);
        }
        else
            chain.doFilter(request, response);
    }

    public void destroy() {}
}

这个Filter将拦截发送给OnlineServlet的请求,并判断用户是否在线,在线则“放行”,否则跳转到登录页面。

注意到我们在@WebFilter中提供了两个@WebInitParam配置参数,并在init方法中获取。

使用此Filter可以让OnlineServlet专注于处理欢迎页面,而不用进行多余的判断,如下所示:

// OnlineServlet.java

    // 注释掉的语句是原来“多余”的判断语句
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //PageContext pageContext=JspFactory.getDefaultFactory().getPageContext(
        //      this, request, response, null, true, 8192, true);
        //HttpSession session=pageContext.getSession();
        HttpSession session=request.getSession(true);
        String user=(String) session.getAttribute("user");
        //if(user==null || user.equals("")) {
        //  RequestDispatcher dispatcher=request.getRequestDispatcher("/user-login.jsp");
        //  dispatcher.forward(request, response);
        //}
        //else {
            response.setContentType("text/html;charset=GBK");
            PrintWriter out=response.getWriter();
            out.println("<html><head><title>online</title></head><body>");
            out.println("亲爱的"+user+",欢迎您!");
            out.println("</body></html>");
        //}
    }

我们运行改进后的程序:
- 先访问http://localhost:8080/WebDemo/online,可以发现显示的页面是登录页面,可以知道用户请求被“拦截”了;
- 然后在登录页面输入adminaaaaa,可以发现显示的页面还是登录页面(因为密码错误),但是浏览器的地址栏变成http://localhost:8080/WebDemo/user-login,可见之前的一次登录由UserLoginServlet处理了;
- 再在登录页面输入adminadmin,可以发现显示的页面变成了在线页面,但浏览器的地址栏还是http://localhost:8080/WebDemo/user-login,可见登录请求是由UserLoginServlet处理的;
- 现在再访问http://localhost:8080/WebDemo/online,可以发现显示的页面依然是在线页面,证明用户请求被“放行”了。

Listener

当Web应用运行时,Web应用内部会不断发生各种事件,如Web应用启动/停止、会话开始/结束、用户请求到达等。Listener可以监听这些事件的发生,并使我们可以参与到Web应用的生命周期中。

与事件驱动程序一样,不同的事件需要注册不同的监听器来监听,常用的监听器有:
- ServletContextListener:监听Web应用的启动/停止;
- ServletRequestListener:监听用户请求;
- HttpSessionListener:监听会话的开始/结束;
- ServletContextAttributeListener:监听application范围内属性的变化;
- ServletRequestAttributeListener:监听request范围内属性的变化;
- HttpSessionAttributeListener:监听session范围内属性的变化。

在Web应用启动时初始化DBCP

如果Web应用需要不断的和数据库进行交互,那么在Web应用启动时初始化DBCP(Database Connection Pool)将是个不错的选择。这可以通过ServletContextListener来完成:

// DBCPListener.java

@WebListener
public class DBCPListener implements ServletContextListener {
    public void contextInitialized(ServletContextEvent sce)  {
        ServletContext application=sce.getServletContext();
        Properties prop=new Properties();
        Enumeration<String> names=application.getInitParameterNames();
        while(names.hasMoreElements()) {
            String name=names.nextElement();
            prop.put(name, application.getInitParameter(name));
        }
        DBCPManager manager=DBCPManager.getInstance(prop);
        application.setAttribute("manager", manager);
    }

    public void contextDestroyed(ServletContextEvent sce)  {
        ServletContext application=sce.getServletContext();
        application.removeAttribute("manager");
    }
}

在Web应用启动时,将回调ServletContextListener的contextInitialized方法。在此方法中,我们创建了一个DBCP Manager,它负责管理数据库连接池,接着我们将其添加到application作用域中,这样一来,此Web应用中的所有jsp页面和Servlet都能很容易地取得数据库连接,从而和数据库进行交互。

我们需要将commons-dbcp2-2.1.1.jarcommons-logging-1.2.jarcommons-pool2-2.4.2.jar这三个jar包放到WEB-INF\lib目录下;另外,DBCP进行数据库连接配置的参数名和Java有所不同,因此我们还需要修改web.xml文件:

<!-- web.xml -->

  <!-- Java中为driver -->
  <context-param>
      <param-name>driverClassName</param-name>
      <param-value>com.mysql.jdbc.Driver</param-value>
  </context-param>
  <!-- Java中为user -->
  <context-param>
    <param-name>username</param-name>
    <param-value>root</param-value>
  </context-param>

好了,现在我们就可以很方便地与数据库进行交互了。为了展示究竟有多方便,我们给出一个实例。还记得之前我们写过一个查询数据库中某个用户的信息的程序吗(详细信息在这里)?我们修改db.jsp文件:

<!-- db.jsp -->

<%
    //Properties prop=new Properties();
    //Enumeration<String> names=application.getInitParameterNames();
    //while(names.hasMoreElements()) {
    //  String name=names.nextElement();
    //  prop.put(name, application.getInitParameter(name));
    //}

    //if(prop.getProperty("name")!=null)
    //  out.println("application can obtain name<br>");
    //else
    //  out.println("application cannot obtain name<br>");

    String name=config.getInitParameter("name");

    // 获取数据库连接这两句就够了
    DBCPManager manager=(DBCPManager) application.getAttribute("manager");
    Connection conn=manager.connect();
    //Class.forName(prop.getProperty("driver"));
    //config.getServletContext();
    //Connection conn=DriverManager.getConnection(prop.getProperty("url"), prop);
    Statement state=conn.createStatement();
    ResultSet result=state.executeQuery("select * from user where name='"+name+"';");
%>
<%
    manager.disconnect(conn);
%>

对比修改前后(注释都是修改之前的),修改后只需要两句代码就能取得数据库连接,是不是要简单清晰很多。

其他监听器的用法和ServletContextListener差不多,它们之间最大的不同就是事件不同,或者说触发时机不同,可以根据需要注册不同的监听器。

源码

上述所有源代码已上传到github:
https://github.com/jzyhywxz/WebDemo

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值