什么是 web 監(jiān)聽器?web 監(jiān)聽器是一種 Servlet 中特殊的類,它們能幫助開發(fā)者監(jiān)聽 web 中特定的事件,比如 ServletContext, HttpSession, ServletRequest 的創(chuàng)建和銷毀;變量的創(chuàng)建、銷毀和修改等??梢栽谀承﹦幼髑昂笤黾犹幚恚瑢崿F(xiàn)監(jiān)控。
Spring Boot中監(jiān)聽器的使用
web 監(jiān)聽器的使用場景很多,比如監(jiān)聽 servlet 上下文用來初始化一些數(shù)據(jù)、監(jiān)聽 http session 用來獲取當前在線的人數(shù)、監(jiān)聽客戶端請求的 servlet request 對象來獲取用戶的訪問信息等等。
2.1 監(jiān)聽Servlet上下文對象
監(jiān)聽 servlet 上下文對象可以用來初始化數(shù)據(jù),用于緩存。什么意思呢?
比如用戶在點擊某個站點的首頁時,一般都會展現(xiàn)出首頁的一些信息,而這些信息基本上或者大部分時間都保持不變的,但是這些信息都是來自數(shù)據(jù)庫。如果用戶的每次點擊,都要從數(shù)據(jù)庫中去獲取數(shù)據(jù)的話,用戶量少還可以接受,如果用戶量非常大的話,這對數(shù)據(jù)庫也是一筆很大的開銷。針對這種首頁數(shù)據(jù),大部分都不常更新的話,我們完全可以把它們緩存起來,每次用戶點擊的時候,我們都直接從緩存中拿,這樣既可以提高首頁的訪問速度,又可以降低服務器的壓力。如果做的更加靈活一點,可以再加個定時器,定期的來更新這個首頁緩存。就類似與 CSDN 個人博客首頁中排名的變化一樣。
針對這個功能,接下來我們寫一個 demo,在實際中,大家可以完全套用該代碼,來實現(xiàn)自己項目中的相關邏輯。首先寫一個 Mapper,模擬一下從數(shù)據(jù)庫查詢數(shù)據(jù):
@Repository public interface UserMapper { public User getUserById(Integer id); }
然后寫一個監(jiān)聽器,實現(xiàn) ApplicationListener接口,重寫onApplicationEvent 方法,將 ContextRefreshedEvent 對象傳進去。
如果我們想在加載或刷新應用上下文時,也重新刷新下我們預加載的資源,就可以通過監(jiān)聽 ContextRefreshedEvent 來做這樣的事情。
如下:
/*使用ApplicationListener來初始化一些數(shù)據(jù)到application域中的監(jiān)聽器*/ @Component public class MyServletContextListener implements ApplicationListener<ContextRefreshedEvent> { @Resource private UserMapper userMapper; @Override public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) { // 先獲取到application上下文 ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext(); // 獲取對應的service UserService userService = applicationContext.getBean(UserService.class); User user = userMapper.getUserById(1); // 獲取application域對象,將查到的信息放到application域中 ServletContext application = applicationContext.getBean(ServletContext.class); application.setAttribute("user", user); } }
正如注釋中描述的一樣,首先通過 contextRefreshedEvent 來獲取 application 上下文,再通過 application 上下文來獲取 UserService 這個 bean,項目中可以根據(jù)實際業(yè)務場景,也可以獲取其他的bean,然后再調(diào)用自己的業(yè)務代碼獲取相應的數(shù)據(jù),最后存儲到 application 域中,這樣前端在請求相應數(shù)據(jù)的時候,我們就可以直接從 application 域中獲取信息,減少數(shù)據(jù)庫的壓力。下面寫一個Controller 直接從 application 域中獲取 user 信息來測試一下。
@RestController @RequestMapping("/listener") public class TestController { @RequestMapping ("/user") public User getUser(HttpServletRequest request) { ServletContext application = request.getServletContext(); return (User) application.getAttribute("user"); } }
通過PostMan訪問如下:
說明數(shù)據(jù)已經(jīng)緩存成功
然后,直接在數(shù)據(jù)庫修改用戶名,密碼, 再次訪問,得到的依然是之前的結果, 證明沒有再次訪問數(shù)據(jù)庫,用的是之前 緩存內(nèi)存中的數(shù)據(jù)
監(jiān)聽HTTP會話 Session對象
監(jiān)聽器還有一個比較常用的地方就是用來監(jiān)聽 session 對象,來獲取在線用戶數(shù)量,現(xiàn)在有很多開發(fā)者都有自己的網(wǎng)站,監(jiān)聽 session 來獲取當前在下用戶數(shù)量是個很常見的使用場景,下面來介紹一下如何來使用。
問題:
訪問HTML是否創(chuàng)建Session?:不會。
訪問JSP是否創(chuàng)建Session? :會
訪問Servlet是否創(chuàng)建Session?:不會(默認沒有調(diào)用getSession方法)
只有調(diào)用了getSession方法,才會創(chuàng)建Session, 觸發(fā)sessionCreated方法
@Component public class MyHttpSessionListener implements HttpSessionListener { private static final Logger logger = LoggerFactory.getLogger(MyHttpSessionListener.class); /* 記錄在線的用戶數(shù)量*/ public Integer count = 0; @Override public synchronized void sessionCreated(HttpSessionEvent httpSessionEvent) { //Notification that a session was created. logger.info("新用戶上線了"); count++; httpSessionEvent.getSession().getServletContext().setAttribute("count", count); } @Override public synchronized void sessionDestroyed(HttpSessionEvent httpSessionEvent) { //Notification that a session is about to be invalidated. logger.info("用戶下線了"); count--; httpSessionEvent.getSession().getServletContext().setAttribute("count", count); } }
可以看出,首先該監(jiān)聽器需要實現(xiàn) HttpSessionListener 接口,然后重寫 sessionCreated 和 sessionDestroyed 方法,在 sessionCreated 方法中傳遞一個 HttpSessionEvent 對象,然后將當 前 session 中的用戶數(shù)量加1, sessionDestroyed 方法剛好相反,不再贅述。然后我們寫一個 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 "當前在線人數(shù):" + count; }
監(jiān)聽客戶端請求Servlet Request對象
使用監(jiān)聽器獲取用戶的訪問信息比較簡單,實現(xiàn) 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數(shù)據(jù):" + request.getAttribute("name")); return "success"; }
其實訪問任意的控制器都會觸發(fā)ServletRequestListener監(jiān)聽事件