十二、Spring Boot 中使用監聽器

前言: 沒有任何花哨的前言,都是基礎、實用的東西。

1. 監聽器介紹

什麼是 web 監聽器?web 監聽器是一種 Servlet 中特殊的類,它們能幫助開發者監聽 web 中特定的事件,比如 ServletContext, HttpSession, ServletRequest 的創建和銷燬;變量的創建、銷燬和修改等。可以在某些動作前後增加處理,實現監控。

2. Spring Boot 中監聽器的使用

web 監聽器的使用場景很多,比如監聽 servlet 上下文用來初始化一些數據、監聽 http session 用來獲取當前在線的人數、監聽客戶端請求的 servlet request 對象來獲取用戶 的訪問信息等等。這一節中,我們主要通過這三個實際的使用場景來學習一下 Spring Boot 中監聽器的使用。

2.1 監聽 Servlet 上下文對象

監聽 servlet 上下文對象可以用來初始化數據,用於緩存。什麼意思呢?我舉一個很常見的場景,比如用戶在點擊某個站點的首頁時,一般都會展現出首頁的一些信息,而這些信息基本上或者大部分時間都保持不變的,但是這些信息都是來自數據庫。如果用戶的每次點擊,都要從數據庫中去獲取數據的話,用戶量少還可以接受,如果用戶量非常 大的話,這對數據庫也是一筆很大的開銷。

針對這種首頁數據,大部分都不常更新的話,我們完全可以把它們緩存起來,每次用戶點擊的時候,我們都直接從緩存中拿,這樣既可以提高首頁的訪問速度,又可以降低服務器的壓力。如果做的更加靈活一點,可以再加個定時器,定期的來更新這個首頁緩存。就類似與 CSDN 個人博客首頁中排名的變化一樣。

下面我們針對這個功能,來寫一個 demo,在實際中,
讀者可以完全套用該代碼,來實現自己項目中的相關邏輯。
首先寫一個 Service,模擬一下從數據庫查詢數據:

[@Service](https://my.oschina.net/service)
public class UserService {
   /**
	 * 獲取用戶信息
	 * [@return](https://my.oschina.net/u/556800)
	 */
	public User getUser() {
		// 實際中會根據具體的業務場景,從數據庫中查詢對應的信息
		return new User(1L, "理性思考", "123456");
	}
}

然後寫一個監聽器,實現 ApplicationListener<ContextRefreshedEvent> 接口, 重寫 onApplicationEvent 方法,將 ContextRefreshedEvent 對象傳進去。如果我們想在加載或刷新應用上下文時,也重新刷新下我們預加載的資源,就可以通過監聽 ContextRefreshedEvent 來做這樣的事情。

/**
 * 使用ApplicationListener來初始化一些數據到application域中的監聽器
 */
[@Component](https://my.oschina.net/u/3907912)
public class MyServletContextListener implements ApplicationListener<ContextRefreshedEvent> {

	[@Override](https://my.oschina.net/u/1162528)
	public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
		// 先獲取到application上下文
		ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
		// 獲取對應的service
		UserService userService = applicationContext.getBean(UserService.class);
		User user = userService.getUser();
		// 獲取application域對象,將查到的信息放到application域中
		ServletContext application = applicationContext.getBean(ServletContext.class);
		application.setAttribute("user", user);
	}
}

正如註釋中描述的一樣,首先通過 contextRefreshedEvent 來獲取 application 上下 文,再通過 application 上下文來獲取 UserService 這個 bean,項目中可以根據實際業務場景,也可以獲取其他的 bean,然後再調用自己的業務代碼獲取相應的數據,最後存儲到 application 域中,這樣前端在請求相應數據的時候,我們就可以直接從 application 域中獲取信息,減少數據庫的壓力。下面寫一個 Controller 直接從 application 域中獲取 user 信息來測試一下。

[@RestController](https://my.oschina.net/u/4486326)
@RequestMapping("/listener")
public class TestController {

	@Resource
	private UserService userService;

	@GetMapping("/user")
	public User getUser(HttpServletRequest request) {
		ServletContext application = request.getServletContext();
		return (User) application.getAttribute("user");
	}

}

啓動項目,在瀏覽器中輸入 http://localhost:8080/listener/user 測試一下即 可,

如果正常返回 user 信息,那麼說明數據已經緩存成功。不過 application 這種是緩存在內存中,對內存會有消耗,後面的課程中我會講到 redis,到時候再給大家介紹一 下 redis 的緩存。

2.2 監聽 HTTP 會話 Session 對象

監聽器還有一個比較常用的地方就是用來監聽 session 對象,來獲取在線用戶數量,現在有很多開發者都有自己的網站,監聽 session 來獲取當前在下用戶數量是個很常見的 使用場景,下面來介紹一下如何來使用。

@Component
public class MyHttpSessionListener implements HttpSessionListener {

	private static final Logger logger = LoggerFactory.getLogger(MyHttpSessionListener.class);

	/**
	 * 記錄在線的用戶數量
	 */
	public Integer count = 0;

	@Override
	public synchronized void sessionCreated(HttpSessionEvent httpSessionEvent) {
		logger.info("新用戶上線了");
		count++;
		httpSessionEvent.getSession().getServletContext().setAttribute("count", count);
	}

	@Override
	public synchronized void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
		logger.info("用戶下線了");
		count--;
		httpSessionEvent.getSession().getServletContext().setAttribute("count", count);
	}
}

可以看出,首先該監聽器需要實現 HttpSessionListener 接口,然後重寫 sessionCreatedsessionDestroyed 方法,在 sessionCreated 方法中傳遞一個 HttpSessionEvent 對象,然後將當前 session 中的用戶數量加 1,sessionDestroyed 方法剛好相反,不再贅述。然後我們寫一個 Controller 來測試一下。

/**
	 * 獲取當前在線人數,該方法有bug
	 * @param request
	 * @return
	 */
	@GetMapping("/total")
	public String getTotalUser(HttpServletRequest request) {
		Integer count = (Integer) request.getSession().getServletContext().getAttribute("count");
		return "當前在線人數:" + count;
	}

Controller 中是直接獲取當前 session 中的用戶數量,啓動服務器,在瀏覽器中輸入 localhost:8080/listener/total 可以看到返回的結果是 1,再打開一個瀏覽器,請求相同的地址可以看到 count 是 2 ,這沒有問題。但是如果關閉一個瀏覽器再打開,理論上應該還是 2,但是實際測試卻是 3。原因是 session 銷燬的方法沒有執行(可以在後臺控制檯觀察日誌打印情況),當重新打開時,服務器找不到用戶原來的 session, 於是又重新創建了一個 session,那怎麼解決該問題呢?我們可以將上面的 Controller 方法改造一下:

@GetMapping("/total2")
	public String getTotalUser(HttpServletRequest request, HttpServletResponse response) {
		Cookie cookie;
		try {
			// 把sessionId記錄在瀏覽器中
			cookie = new Cookie("JSESSIONID", URLEncoder.encode(request.getSession().getId(), "utf-8"));
			cookie.setPath("/");
			//設置cookie有效期爲2天,設置長一點
			cookie.setMaxAge( 48 * 60 * 60);
			response.addCookie(cookie);
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		Integer count = (Integer) request.getSession().getServletContext().getAttribute("count");
		return "當前在線人數:" + count;
	}

可以看出,該處理邏輯是讓服務器記得原來那個 session,即把原來的 sessionId 記錄 在瀏覽器中,下次再打開時,把這個 sessionId 傳過去,這樣服務器就不會重新再創建了。重啓一下服務器,在瀏覽器中再次測試一下,即可避免上面的問題。

2.3 監聽客戶端請求 Servlet Request 對象

使用監聽器獲取用戶的訪問信息比較簡單,實現 ServletRequestListener 接口即可,然後通過 request 對象獲取一些信息。如下

@Component
public class MyServletRequestListener implements ServletRequestListener {

	private static final Logger logger = LoggerFactory.getLogger(MyServletRequestListener.class);

	@Override
	public void requestInitialized(ServletRequestEvent servletRequestEvent) {
		HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest();
		logger.info("session id爲:{}", request.getRequestedSessionId());
		logger.info("request url爲:{}", request.getRequestURL());

		request.setAttribute("name", "理性思考");
	}

	@Override
	public void requestDestroyed(ServletRequestEvent servletRequestEvent) {

		logger.info("request end");
		HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest();
		logger.info("request域中保存的name值爲:{}", request.getAttribute("name"));

	}

}

這個比較簡單,不再贅述,接下來寫一個 Controller 測試一下即可

@GetMapping("/request")
	public String getRequestInfo(HttpServletRequest request) {
		System.out.println("requestListener中的初始化的name數據:" + request.getAttribute("name"));
		return "success";
	}

3. Spring Boot 中自定義事件監聽

在實際項目中,我們往往需要自定義一些事件和監聽器來滿足業務場景,比如在微服務中會有這樣的場景:微服務 A 在處理完某個邏輯之後,需要通知微服務 B 去處理另一 個邏輯,或者微服務 A 處理完某個邏輯之後,需要將數據同步到微服務 B,這種場景非常普遍,這個時候,我們可以自定義事件以及監聽器來監聽,一旦監聽到微服務 A 中的某事件發生,就去通知微服務 B 處理對應的邏輯。

3.1 自定義事件

自定義事件需要繼承 ApplicationEvent 對象,在事件中定義一個 User 對象來模擬數 據,構造方法中將 User 對象傳進來初始化。如下:

public class MyEvent extends ApplicationEvent {

	private User user;

	public MyEvent(Object source, User user) {
		super(source);
		this.user = user;
	}

	public User getUser() {
		return user;
	}

	public void setUser(User user) {
		this.user = user;
	}
}

3.2 自定義監聽器

接下來,自定義一個監聽器來監聽上面定義的 MyEvent 事件,自定義監聽器需要實現 ApplicationListener 接口即可。如下:

@Component
public class MyEventListener implements ApplicationListener<MyEvent> {
	@Override
	public void onApplicationEvent(MyEvent myEvent) {
		// 把事件中的信息獲取到
		User user = myEvent.getUser();
		// 處理事件,實際項目中可以通知別的模塊或者處理其他邏輯等等
		System.out.println("用戶名:" + user.getUsername());
		System.out.println("密碼:" + user.getPassword());

	}
}

然後重寫 onApplicationEvent 方法,將自定義的 MyEvent 事件傳進來,因爲該事件中,我們定義了 User 對象(該對象在實際中就是需要處理的數據,在下文來模擬), 然後就可以使用該對象的信息了。

OK,定義好了事件和監聽器之後,需要手動發佈事件,這樣監聽器才能監聽到,這需 要根據實際業務場景來觸發,針對本文的例子,我寫個觸發邏輯,如下:

@Service
public class UserService {

	@Resource
	private ApplicationContext applicationContext;

	/**
	 * 發佈事件
	 *
	 * @return
	 */
	public User getUser2() {
		User user = new User(1L, "理性思考", "123456");
		// 發佈事件
		MyEvent event = new MyEvent(this, user);
		applicationContext.publishEvent(event);
		return user;
	}

}

在 service 中注入 ApplicationContext,在業務代碼處理完之後,通過 ApplicationContext 對象手動發佈 MyEvent 事件,這樣我們自定義的監聽器就能監聽 到,然後處理監聽器中寫好的業務邏輯

最後,在 Controller 中寫一個接口來測試一下:
@GetMapping("/request")
	public String getRequestInfo(HttpServletRequest request) {
		System.out.println("requestListener中的初始化的name數據:" + request.getAttribute("name"));
		return "success";
	}

在瀏覽器中輸入 http://localhost:8080/listener/publish,然後觀察一下控制 臺打印的用戶名和密碼,即可說明自定義監聽器已經生效。

4. 總結

本課系統的介紹了監聽器原理,以及在 Spring Boot 中如何使用監聽器,列舉了監聽器 的三個常用的案例,有很好的實戰意義。最後講解了項目中如何自定義事件和監聽器, 並結合微服務中常見的場景,給出具體的代碼模型,均能運用到實際項目中去,希望讀 者認真消化。


祝各位週末愉快!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章