JavaWeb中的Listener组件
在我人生中第一次面试中,我挂在了和Listener
相关的题目上。
面试官问,如果你在使用纯Java Web开发的情况下,有什么办法能够在服务器关闭时将内存中的部分数据存入到持久层中。我当时知道肯定有这种组件或者hook方法,但我并不知道是什么。后来面试官告诉我是Listener
。
Servlet API中的Listener主要针对三种对象进行监听:
- ServletContext
- HttpRequest
- HttpSession
监听三种对象的创建和销毁
ServletAPI中提供了监听三种对象创建和销毁的Listener接口,你只需要编写一个类实现这个接口,并让这个类可以被容器发现即可(web.xml
中注册或者使用@WebListener
注解)
ServletContextListener
ServletContextListener
监听ServletContext
的创建和销毁,所以对于一个Web应用程序,我们只需要编写一个这个组件即可实现在容器关闭时做一些事。下面是一个在容器创建时构建一个JDBC数据源,并放到ServletContext
的属性中,然后当容器关闭时再销毁这个数据源的示例:
@WebListener
public class DataSourceListener implements ServletContextListener {
private Logger logger = Logger.getLogger(DataSourceListener.class.getName());
public static final String DATASOURCE_ATTR_NAME = "datasource";
/*
* web应用初始化进程开始时接收到通知
*
* 所有的ServletContextListener都会在任何web应用中的filter和servlet被初始化前接收到context初始化的通知
*
* @params sce - ServletContextEvent,包含正被初始化的ServletContext
*/
@Override
public void contextInitialized(ServletContextEvent sce) {
ComboPooledDataSource dataSource = new ComboPooledDataSource();
try {
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/dbconcept");
dataSource.setDriverClass("com.mysql.cj.jdbc.Driver");
dataSource.setUser("root");
dataSource.setPassword("root");
dataSource.setInitialPoolSize(20);
dataSource.setMaxPoolSize(100);
sce.getServletContext().setAttribute(DATASOURCE_ATTR_NAME, dataSource);
logger.info("Datasource is injected correctly!");
} catch (Throwable e) {
throw new IllegalStateException("Init datasource faild!", e);
}
}
/**
* 收到ServletContext即将被关闭的通知
*
* 所有servlet和filter都将在任意一个ServletContextListener接到通知前被销毁
*/
@Override
public void contextDestroyed(ServletContextEvent sce) {
C3P0PooledDataSource dataSource = (C3P0PooledDataSource) sce.getServletContext().getAttribute(DATASOURCE_ATTR_NAME);
try {
dataSource.close();
sce.getServletContext().removeAttribute(DATASOURCE_ATTR_NAME);
} catch (SQLException e) {
logger.warning("Exception raised when close dataSource: ");
e.printStackTrace();
}
}
}
注意这两个方法被调用的时机:
- 所有的ServletContextListener都会在任何web应用中的filter和servlet被初始化前接收到context初始化的通知
- 所有servlet和filter都将在任意一个ServletContextListener接到销毁通知前被销毁
HttpSessionListener
可以监测Session的创建与销毁,JavaEE服务器会自动为每一个浏览器分配一个Session,这和你是否使用request.getSession(false)
是无关的。即使你使用request.getSession(false)
,Session还是会被创建,只不过那次调用的返回值是null。
所以,在HttpSessionListener
中,一旦一个新的浏览器访问,sessionCreated
方法就被回调,而当Session超时或invalidate
时,sessionDestroyed
方法就会被回调。也正是因为这个原因,我们在sessionCreated
中大概只能利用session的ID,因为往往此时Session中还没有设置什么属性。
下面是一个实现了HttpSessionListener
的类,它会将一个List注入到ServletContext
中,并且在其中维护当前的所有SessionID。同时,它也实现了ServletContextListener
用于向ServletContext
中初始化List:
@WebListener
public class UserLoginLogoutListener implements javax.servlet.http.HttpSessionListener, ServletContextListener {
@Override
public void sessionCreated(HttpSessionEvent se) {
HttpSession session = se.getSession();
getSessionIdList(session.getServletContext()).add(session.getId());
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
HttpSession session = se.getSession();
getSessionIdList(session.getServletContext()).remove(session.getId());
}
private List<String> getSessionIdList(ServletContext context) {
return (List<String>) context.getAttribute("sessionids");
}
@Override
public void contextInitialized(ServletContextEvent sce) {
sce.getServletContext().setAttribute("sessionids", new ArrayList<>());
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
ServletRequestListener
这里只放上它的接口,没有实际应用的例子
public interface ServletRequestListener extends EventListener {
public void requestDestroyed(ServletRequestEvent sre);
public void requestInitialized(ServletRequestEvent sre);
}
监听三种对象的属性设置
这里的监听器主要针对三种对象中attribute
的设置、修改和移除
public interface ServletContextAttributeListener extends EventListener {
public void attributeAdded(ServletContextAttributeEvent event);
public void attributeRemoved(ServletContextAttributeEvent event);
public void attributeReplaced(ServletContextAttributeEvent event);
}
public interface HttpSessionAttributeListener extends EventListener {
public void attributeAdded(HttpSessionBindingEvent event);
public void attributeRemoved(HttpSessionBindingEvent event);
public void attributeReplaced(HttpSessionBindingEvent event);
}
public interface ServletRequestAttributeListener extends EventListener {
public void attributeAdded(ServletRequestAttributeEvent srae);
public void attributeRemoved(ServletRequestAttributeEvent srae);
public void attributeReplaced(ServletRequestAttributeEvent srae);
}
想不到有什么应用场景
HttpSession的另外两个Listener
HttpSessionBindingListener
和HttpSessionAttributeListener
的目的相同,它也是用于监测Session中的属性设置的,不同的是,它是要被设置到Session中作为属性的对象去实现的接口。
下面我们通过这个来实现一个在线用户管理,而不是刚刚的在线Session管理。
- 前提条件1:当登录成功时,系统会将登录用户的
User
对象放到Session的属性中- 前提条件2:有一个组件在该组件产生作用前就向
ServletContext
中添加了一个代表用户列表的Set集合
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements HttpSessionBindingListener {
private String username;
private String password;
@Override
public void valueBound(HttpSessionBindingEvent event) {
((Set<String>)event.getSession().getServletContext().getAttribute(UserLoginLogoutListener.ONLINE_USER_ATTR_NAME)).add(getUsername());
}
@Override
public void valueUnbound(HttpSessionBindingEvent event) {
((Set<String>)event.getSession().getServletContext().getAttribute(UserLoginLogoutListener.ONLINE_USER_ATTR_NAME)).remove(getUsername());
}
}
这是一个简单的实现,我们没有考虑一个用户在两个设备上登录会出现什么情况,从代码来看应该是同一个用户不管从多少设备登录都只记录一个,而从任意一个设备移除,该用户的登录信息就看不见了,即使还有其它设备处于登陆状态。我个人认为正确的做法应该是使用SessionID+用户名作为一个用户的唯一标识。比如像如下:
在Session过期或
invalidate
时,其中的所有属性都会被unbound
HttpSessionActivationListener
说这个之前要说Session的两个状态
- 钝化(Passivate):Session中的对象被持久化到磁盘中
- 活化(Activate):Session中的对象被从磁盘中加载到内存
HttpSessionActivationListener
用来监测该对象被添加到Session中后,是否被钝化、活化。被监测的对象希望被持久化到磁盘中,那么它必须是可序列化的(Serializable):
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements HttpSessionBindingListener, HttpSessionActivationListener, Serializable {
private String username;
private String password;
// ...省略代码...
@Override
public void sessionWillPassivate(HttpSessionEvent se) {
System.out.println("session passivate");
}
@Override
public void sessionDidActivate(HttpSessionEvent se) {
System.out.println("session active");
}
}
如果需要启动钝化活化,必须在web根目录下的META-INF
中创建context.xml
文件,并指定钝化的文件路径:
<Context>
<Manager className="org.apache.catalina.session.PersistentManager" maxIdleSwap="1">
<Store className="org.apache.catalina.session.FileStore" directory="d:/sessionfile" />
</Manager>
</Context>
上面的代码指定一分钟后Session就会被钝化到磁盘上,同时,若没到一分钟,Session没有被钝化到磁盘上服务器就关闭了,服务器关闭时也会被钝化。
开启项目,等待一分钟后,Session被钝化:
关闭项目,重启项目,重新使用之前的会话来访问,session被正常活化,之前的Session得到了利用: