导读:监听器用来监视ServletContext/Session/Request的创建、初始化、销毁以及其中的属性变动。
监听器的分类
常见的主要有以下6个,分别处理Servlet全局、Session和Request。
- ServletContextListener
- ServletContextAttributeListener
- HttpSessionListener
- HttpSessionAttributeListener
- ServletRequestListener
- ServletRequestAttributeListenser
监听器的作用
由于每当一个用户访问一个网站都会产生一个会话或是一个请求,所以可以使用监听器来监视这些会话和请求,能够用于
- 统计在线人数
- 统计访问量
- 应用启动时信息初始化
- 与Spring结合
监听器的实现
根据需要实现上述的六种接口,对象监听器实现Init和Destroy方法,属性监听器实现Add/Remove/Replace方法。然后在web.xml中对监听器进行配置,相比于Servlet和Filter,监听器的配置更为简单。
ServletContextListener举例
public class MyServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
String appName=sce.getServletContext().getInitParameter("app_name");
String version=sce.getServletContext().getInitParameter("version");
sce.getServletContext().setAttribute("app_name",appName);
sce.getServletContext().setAttribute("version",version);
System.out.println("Init:"+appName+" "+version);
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
String appName=(String)sce.getServletContext().getAttribute("app_name");
String version=(String)sce.getServletContext().getAttribute("version");
System.out.println("Destroy:"+appName+" "+version);
}
}
在web.xml中进行配置,设置全局属性,用于获取。
<context-param>
<param-name>app_name</param-name>
<param-value>Listener Web</param-value>
</context-param>
<context-param>
<param-name>version</param-name>
<param-value>1.0</param-value>
</context-param>
<listener>
<listener-class>listener.MyServletContextListener</listener-class>
</listener>
这样,一旦WEB应用一启动,监听器就能够监视ServletContext对象的创建和销毁,控制台就会输出"Init:Listener 1.0",关闭WEB应用,就会输出"Destroy:Listener 1.0"。
对于Session和Request也类似,只要一创建新的Session和Request,就会被监听器所捕获到,执行操作。
ServletContextAttributeListener举例
属性监视器用来监视属性的添加、变换和移除。新建一个全局属性监听器。
public class MyServletContextAttrListener implements ServletContextAttributeListener {
@Override
public void attributeAdded(ServletContextAttributeEvent scae) {
System.out.println(scae.getName()+scae.getValue());
}
@Override
public void attributeRemoved(ServletContextAttributeEvent scae) {
System.out.println(scae.getName()+scae.getValue());
}
@Override
public void attributeReplaced(ServletContextAttributeEvent scae) {
System.out.println(scae.getName()+scae.getValue());
}
}
新建一个jsp,用于添加全局属性:
<%
application.setAttribute("servletContextPar","servlet");
%>
访问这个jsp,由于执行了添加属性的操作,所以会在后台输出属性名和属性值。
其它属性监听器类似。
监听器实现单一登录
类似QQ这种软件,当用户在别处登录时,本地登录将会被“挤掉”,可以用监听器来实现。下面的案例,使用HttpSessionAttributeListener来实现这个功能。
登录:
index首页有一个表单,用于提交用户名和密码,然后发送请求参数到login.jsp。在login.jsp中,将传过来的用户名存放在名为loginUser的session属性中,接着重定向到主页main.jsp。
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>
<%
String username=request.getParameter("username");
session.setAttribute("loginUser",username);
response.sendRedirect(request.getContextPath()+"/main.jsp");
%>
main.jsp中,需要获取重定向过来的username,并在页面上显示:
<%
String user = (String)session.getAttribute("loginUser");
%>
<ul class="navig">
<li><a href="index.html" class="scroll"><%=user %></a><span>|</span></li>
<li><a href="index.html" class="scroll">HOME</a><span>|</span></li>
<li><a href="#portfolio" class="scroll">PORTFOLIO</a><span>|</span></li>
<li><a href="#contact" class="scroll">CONTACT</a></li>
</ul>
过滤器拦截:
如果用户未经登录,就想访问main.jsp,会被拒绝,重新定向到登录页面,并附带一条错误信息,这就需要创建一个Filter来处理。
public class SessionFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest hrequest=(HttpServletRequest)request;
HttpServletResponse hresponse=(HttpServletResponse)response;
//获取session中的loginUser
String loginUser=(String)hrequest.getSession().getAttribute("loginUser");
if(loginUser==null){
//如果为空,则表示用户没有登录,返回到index.jsp并附带消息
hresponse.sendRedirect(hrequest.getContextPath()+"/index.jsp?flag=1");
}else{
chain.doFilter(request,response);
}
}
}
在index.jsp登录页面中,先获取过滤器传过来的flag参数,再显示警告。
<%
String flag=request.getParameter("flag");
%>
<script type="text/javascript">
var flag='<%=flag%>'
if(flag=="1"){
alert("尚未登录或在异地登录,请重新登录!")
}
</script>
单例类构建:
由于只允许一个用户登录,需要一个单例类来封装这个用户。这个单例类用来映射用户名-SessionId,SessionId-Session的关系。由于3.0以上的Servlet不能由sessionId获取到session,所以需要直接写一个由sessionId获取session的方法。
还有一点,为什么不直接使用用户名去映射session,而是要通过sessionId来中转一下。这是因为同一个浏览器的同一个用户,如果退出了浏览器,此时session还在,当这个用户再访问网站时,会建立一个新的session,释放旧的session,浪费资源。如果通过用户名-sessionId,得知当前浏览器当前用户又一次登录了,直接用这个用户名之前的sessionId获取之前的session就行了,避免了资源浪费。
public class LoginCache {
//单例类要点
private static LoginCache instance=new LoginCache();
private LoginCache(){}
public static LoginCache getInstance(){ return instance; }
//用户名跟sessionId映射
private Map<String,String>loginUserSession=new HashMap<>();
//sessionId和session的映射
private Map<String, HttpSession>loginSession=new HashMap<>();
public String getSessionIdByUsername(String username){
return loginUserSession.get(username);
}
public HttpSession getSessionBySessionId(String sessionId){
return loginSession.get(sessionId);
}
public void setSessionIdByUserName(String username,String sessionId){
loginUserSession.put(username,sessionId);
}
public void setSessionBySessionId(String sessionId,HttpSession session){
loginSession.put(sessionId,session);
}
}
监听器构建:
public class LoginSessionListener implements HttpSessionAttributeListener {
private static final String LOGIN_USER="loginUser";
@Override
public void attributeAdded(HttpSessionBindingEvent hsbe) {
//获取新添加的属性名
String attrName=hsbe.getName();
//如果=loginUser,就表示有新用户登录了
if(LOGIN_USER.equals(attrName)){
//获得新登录的用户名
String attrVal=(String)hsbe.getValue();
//获得当前会话(新登录)的Session对象
HttpSession session=hsbe.getSession();
//获得SessionId
String sessionId=session.getId();
//在LoginCache中查找新登录的用户名是否存在
String sessionId2=LoginCache.getInstance().getSessionIdByUsername(attrVal);
if(sessionId2==null){
}
//如果存在
else{
//就获取之前用户的SessionId2,并将其释放
HttpSession session2=LoginCache.getInstance().getSessionBySessionId(sessionId2);
session2.invalidate();
}
//添加刚才登录的Session作为已存在的Session
LoginCache.getInstance().setSessionIdByUserName(attrVal,sessionId);
LoginCache.getInstance().setSessionBySessionId(sessionId,session);
}
}
}
这样,当从别的浏览器登录时,刷新当前浏览器,就会被挤掉。