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模式的请求进行过滤,支持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
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
,可以发现显示的页面依然是在线页面,证明用户请求被“放行”了。
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.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