Servlet可以增加Filter处理链,也可以注册Listener对事件进行监听。
Filter作为一种过滤器,既可以对客户端请求进行预处理,也可以对服务器响应进行后处理,是个典型的处理链。
Filter的工作流程是: 1. Filter对用户请求进行拦截并进行预处理; 2. 如果Filter没有截断用户请求,则将用户请求交由Servlet处理; 3. Servlet根据用户请求生成响应,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模式的请求进行过滤,支持ASYNC、ERROR、FORWARD、INCLUDE和REQUEST这五个值的任意组合,默认对这五种模式进行过滤; - 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可以将多个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,可以发现显示的页面是登录页面,可以知道用户请求被“拦截”了; - 然后在登录页面输入admin和aaaaa,可以发现显示的页面还是登录页面(因为密码错误),但是浏览器的地址栏变成http://localhost:8080/WebDemo/user-login,可见之前的一次登录由UserLoginServlet处理了; - 再在登录页面输入admin和admin,可以发现显示的页面变成了在线页面,但浏览器的地址栏还是http://localhost:8080/WebDemo/user-login,可见登录请求是由UserLoginServlet处理的; - 现在再访问http://localhost:8080/WebDemo/online,可以发现显示的页面依然是在线页面,证明用户请求被“放行”了。
当Web应用运行时,Web应用内部会不断发生各种事件,如Web应用启动/停止、会话开始/结束、用户请求到达等。Listener可以监听这些事件的发生,并使我们可以参与到Web应用的生命周期中。
与事件驱动程序一样,不同的事件需要注册不同的监听器来监听,常用的监听器有: - ServletContextListener:监听Web应用的启动/停止; - ServletRequestListener:监听用户请求; - HttpSessionListener:监听会话的开始/结束; - ServletContextAttributeListener:监听application范围内属性的变化; - ServletRequestAttributeListener:监听request范围内属性的变化; - HttpSessionAttributeListener:监听session范围内属性的变化。
如果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.jar、commons-logging-1.2.jar和commons-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
