Listener是Servlet的,不属于Spring framework,也就是说我们无法在Listener中主动注入Spring bean。本学习将解决这个问题。
在解决之前,我们先进一步了解Spring的注入机制。在Spring中,我们可以使用@Inject,@Anwired,@Resource等方式实现对自动扫描和自动注入。 同一上下文环境中,bean只实例化一次,在不同类中注入的,都是同一个bean(同一对象) 。我们通常在根上下文中进行扫描,即使我们在不同的类中都进行注入,实际是注入的是同一个对象的。
我们将通过小测试来验证这点。
设置一个简单的Service,打印对象地址,同时在构造函数中给出log,看看在哪个阶段进行实例化。
public interface MyTestService { public void whoAmI(String className); }
@Service public class MyTestServiceImpl implements MyTestService{ private static final Logger log = LogManager.getLogger(); public MyTestServiceImpl(){ log.info("MyTestServiceImpl instance is created, address is " + this); } @Override public void whoAmI(String className) { log.info("{} : {}" , className,this); } }
在AuthenticationController中
@Controller public class AuthenticationController { @Inject private AuthenticationService authenticationService; @RequestMapping(value="login",method=RequestMethod.GET) public ModelAndView login(Map<String,Object> model,HttpSession session){ myTestService.whoAmI(this.getClass().getName()); ... ... } ... ... }
在TicketController中
@Controller @RequestMapping("ticket") public class TicketController { @Inject private MyTestService myTestService; @RequestMapping(value = {"", "list"}, method = RequestMethod.GET) public String list(Map<String,Object> model){ this.myTestService.whoAmI(this.getClass().getName()); ... ... } }
输出结果:
14:19:19.985 [localhost-startStop-1] [INFO ] (Spring) ContextLoader - Root WebApplicationContext: initialization started ... ... 14:19:20.633 [localhost-startStop-1] [INFO ] (Spring) AutowiredAnnotationBeanPostProcessor - JSR-330 'javax.inject.Inject' annotation found and supported for autowiring 14:19:20.934 [localhost-startStop-1] [INFO ] MyTestServiceImpl:12 <init>() - MyTestServiceImpl instance is created, address is cn.wei.flowingflying.customer_support.site.test.MyTestServiceImpl@407cec ... ... 六月 23, 2017 2:19:21 下午 org.apache.catalina.core.ApplicationContext log 信息: Initializing Spring FrameworkServlet 'springDispatcher' ... ... 14:19:23.217 [http-nio-8080-exec-5] [INFO ] MyTestServiceImpl:16 whoAmI() - cn.wei.flowingflying.customer_support.site.AuthenticationController : cn.wei.flowingflying.customer_support.site.test.MyTestServiceImpl@407cec ... ... 14:19:36.195 [http-nio-8080-exec-8] wei [INFO ] MyTestServiceImpl:16 whoAmI() - cn.wei.flowingflying.customer_support.site.TicketController : cn.wei.flowingflying.customer_support.site.test.MyTestServiceImpl@407cec
我们看到在AuthenticationController和TicketController中注入的对象实际地址一样,都是407cec,即为同一对象,是在Root Context中被实例化,且只实例化一次。了解这点非常重要,不同Controller对某个注入的Service进行操作,是可能相互影响的。
Listener是Serlvet container的,不是Spring framework的,不是任何的Spring Component,不在自动扫描的范围内,我们在里面标记的任何@Inject不会被注入。
我们创建一个Session Listener作测试
@WebListener public class WeiTempListener implements HttpSessionListener { private static final Logger log = LogManager.getLogger(); @Inject private MyTestService myTestService; public WeiTempListener() { } public void sessionCreated(HttpSessionEvent se) { log.info("------------------------------------"); this.myTestService.whoAmI(this.getClass().getName()); } public void sessionDestroyed(HttpSessionEvent se) { } }
14:50:31.164 [http-nio-8080-exec-4] [INFO ] WeiTempListener:32 sessionCreated() - ------------------------------------ 六月 23, 2017 2:50:31 下午 org.apache.catalina.session.StandardSession tellNew 严重: Session event listener threw exception java.lang.NullPointerException at cn.wei.flowingflying.customer_support.site.WeiTempListener.sessionCreated(WeiTempListener.java:33)
前面已经看到注入的实例化是在Root Context中进行。我们需要在Listener的初始化过程中,想办法从Root Context中获得实例。我们需要:
public class BootStrap implements WebApplicationInitializer{ @Override public void onStartup(ServletContext container) throws ServletException { container.getServletRegistration("default").addMapping("/resource/*"); AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext(); rootContext.register(RootContextConfiguration.class); container.addListener(new ContextLoaderListener(rootContext)); //【1】设置Listener的加载位置,在完成Root Context之后 container.addListener(WeiTempListener.class); ... ... } }
我们再看看WeiTempListener
//【1】删除@WebListener标记,采用手动在BootStrap中加入 //【2】增加ServletContextListener接口,以获得初始化入口 public class WeiTempListener implements HttpSessionListener,ServletContextListener { private static final Logger log = LogManager.getLogger(); @Inject private MyTestService myTestService; public WeiTempListener() { // 这在Root Context初始化之前执行,因此我们不能在构造函数中进行设置 log.info("-----------------WeiTempListener-------------------"); } public void sessionCreated(HttpSessionEvent se) { this.myTestService.whoAmI(this.getClass().getName()); // 测试 } public void sessionDestroyed(HttpSessionEvent se) { } //【3】在contextInitialized()中获得Spring的rootContext实例 @Override public void contextInitialized(ServletContextEvent event) { // 根据BoorStrap的执行顺序,这时RootContext的初始化已经完成,包括Service的实例化,可以注入。 // 无法自动注入是因为Listerner并不是Spring的bean(如不是@Controller),我们要想办法手动让Listerner成为bean。 // (1)获取Spring的root WebApplicationContext WebApplicationContext rootContext = WebApplicationContextUtils.getRequiredWebApplicationContext(event.getServletContext()); // (2)获取根上下文扫描和注入bean的factory AutowireCapableBeanFactory factory = rootContext.getAutowireCapableBeanFactory(); // (3)无法扫描是因为Listener不是Spring的bean,类上没有加spring的annotation,我们需要手动设置这个对象(this)作为Factory中的一个bean,这样才能对里面的属性进行注入 factory.autowireBeanProperties(this, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE,true); // (4)在factory中对这个新的bean进行初始化。 factory.initializeBean(this,"WeiTempListener"); log.info(this.myTestService); //测试一下注入情况 } public void contextDestroyed(ServletContextEvent sce) { } }
虽然我们将Listener手动设置为fatory可以认识的bean,但仍不是spring下一个真正意义的bean。其他的bean中不能将其注入,部分地我们可以通过factory的registerSingleton(),将其设置为singleton bean来解决(即确保注入的都是同一的bean),但依然收到限制,有些内容仍无法正常执行,如计划执行,构造后和注销前的回调函数。
这个小例子场景,我们在下一学习中继续使用,再此作个说明,用户请求帮助(通过websocket发其chat),客服(另一用户)选择需要帮助的用户(加入chat),双方之间进行通话:
在webSocket chat中我们通过SessionRegisterService打算维护在线的session。对用户退出登录(主动退出,session超时而被删除)时,如果该用户在chat中,需要行chat close动作,可以利用Consumer进行触发。
public interface SessionRegistryService { public void addSession(HttpSession session); public void updateSessionId(HttpSession session, String oldSessionId); public void removeSession(HttpSession session); /** 注册回调函数 用户开启chat进行回掉函数或者触发函数的注册 */ public void registerOnRemoveCallback(Consumer<HttpSession> callback); /** 注销回掉函数 用户关闭chat进行注销 */ public void deregisterOnRemoveCallback(Consumer<HttpSession> callback); }
SessionListener没有什么特别:
public class SessionListener implements HttpSessionListener, ServletContextListener { @Inject private SessionRegistryService sessionRegistryService; public void sessionCreated(HttpSessionEvent event) { this.sessionRegistryService.addSession(event.getSession()); } public void sessionIdChanged(HttpSessionEvent event, String oldSessionId) { this.sessionRegistryService.updateSessionId(event.getSession(), oldSessionId); } public void sessionDestroyed(HttpSessionEvent event) { this.sessionRegistryService.removeSession(event.getSession()); } @Override public void contextInitialized(ServletContextEvent event) { .... 见前面 .... } ... ... }
@Service public class DefaultSessionRegistryService implements SessionRegistryService{ private final Map<String, HttpSession> sessions = new Hashtable<>(); /** Consumer的具体操作是:如果httpSession相同,则删除,里面已经进行了判断,所以就不需要Predicate */ private final Set<Consumer<HttpSession>> callbacks = new HashSet<>(); /** callbacksAddesWhileLocked是个比较有意思的处理,需要学习: * 我们几乎同时收到了同一个用户(同一个httpSession)要求退出登录 和 chat申请的两个操作,一般来讲虽然不会如此,多页面的请求有可能会造成几乎同时到达,由或者session到期的瞬间。callbacksAddedWhileLocked用于对这个时间差的session进行处理,即请求加入,然后马上推出登录,即removeSession()和registerOnRemoveCallback()几乎同时操作。理想顺序是有先后,而不是同时进行,但实际多线程运行的顺序无法保证。callbacksAddedWhileLocked来避免同时运行的问题。 */ private final Set<Consumer<HttpSession>> callbacksAddedWhileLocked = new HashSet<>(); @Override public void addSession(HttpSession session) { this.sessions.put(session.getId(), session); } @Override public void updateSessionId(HttpSession session, String oldSessionId) { synchronized(this.sessions) { this.sessions.remove(oldSessionId); addSession(session); } } @Override public void removeSession(HttpSession session) { this.sessions.remove(session.getId()); synchronized(this.callbacks){ this.callbacksAddedWhileLocked.clear(); this.callbacks.forEach(c -> c.accept(session)); try { this.callbacksAddedWhileLocked.forEach(c -> c.accept(session)); } catch(ConcurrentModificationException ignore) { } } } @Override public void registerOnRemoveCallback(Consumer<HttpSession> callback) { this.callbacksAddedWhileLocked.add(callback); synchronized(this.callbacks){ this.callbacks.add(callback); } } @Override public void deregisterOnRemoveCallback(Consumer<HttpSession> callback) { synchronized(this.callbacks){ this.callbacks.remove(callback); } } }
相关链接: 我的Professional Java for Web Applications相关文章