/* * Copyright 2008-2014 by Emeric Vernat * * This file is part of Java Melody. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.bull.javamelody; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSessionActivationListener; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; /** * Listener de session http pour le monitoring. * C'est la classe de ce listener qui doit être déclarée dans le fichier web.xml de la webapp. * Ce listener fait également listener de contexte de servlet * et listener de passivation/activation de sessions. * @author Emeric Vernat */ public class SessionListener implements HttpSessionListener, HttpSessionActivationListener, ServletContextListener, Serializable { private static final String SESSION_ACTIVATION_KEY = "javamelody.sessionActivation"; private static final long serialVersionUID = -1624944319058843901L; // au lieu d'utiliser un int avec des synchronized partout, on utilise un AtomicInteger private static final AtomicInteger SESSION_COUNT = new AtomicInteger(); @SuppressWarnings("all") private static final List<String> CONTEXT_PATHS = new ArrayList<String>(); // attention : this est mis en session, cette map doit donc restée statique @SuppressWarnings("all") private static final ConcurrentMap<String, HttpSession> SESSION_MAP_BY_ID = new ConcurrentHashMap<String, HttpSession>(); private static final ThreadLocal<HttpSession> SESSION_CONTEXT = new ThreadLocal<HttpSession>(); private static boolean instanceCreated; private boolean instanceEnabled; static final class SessionInformationsComparator implements Comparator<SessionInformations>, Serializable { private static final long serialVersionUID = 1L; /** {@inheritDoc} */ @Override public int compare(SessionInformations session1, SessionInformations session2) { if (session1.getLastAccess().before(session2.getLastAccess())) { return 1; } else if (session1.getLastAccess().after(session2.getLastAccess())) { return -1; } else { return 0; } } } /** * Constructeur. */ public SessionListener() { super(); if (instanceCreated) { // ce listener a déjà été chargé précédemment et est chargé une 2ème fois donc on désactive cette 2ème instance // (cela peut arriver par exemple dans glassfish v3 lorsque le listener est déclaré dans le fichier web.xml // et déclaré par ailleurs dans le fichier web-fragment.xml à l'intérieur du jar) // mais il peut être réactivé dans contextInitialized (issue 193) instanceEnabled = false; } else { instanceEnabled = true; setInstanceCreated(true); } } /** * Constructeur. * @param instanceEnabled boolean */ public SessionListener(boolean instanceEnabled) { super(); this.instanceEnabled = instanceEnabled; setInstanceCreated(true); } private static void setInstanceCreated(boolean newInstanceCreated) { instanceCreated = newInstanceCreated; } static int getSessionCount() { if (!instanceCreated) { return -1; } // nous pourrions nous contenter d'utiliser SESSION_MAP_BY_ID.size() // mais on se contente de SESSION_COUNT qui est suffisant pour avoir cette valeur // (SESSION_MAP_BY_ID servira pour la fonction d'invalidateAllSessions entre autres) return SESSION_COUNT.get(); } static long getSessionAgeSum() { if (!instanceCreated) { return -1; } final long now = System.currentTimeMillis(); long result = 0; for (final HttpSession session : SESSION_MAP_BY_ID.values()) { try { result += now - session.getCreationTime(); } catch (final Exception e) { // Tomcat can throw "java.lang.IllegalStateException: getCreationTime: Session already invalidated" continue; } } return result; } // méthode conservée pour compatibilité ascendante // (notamment https://wiki.jenkins-ci.org/display/JENKINS/Invalidate+Jenkins+HTTP+sessions) static void invalidateAllSessions() { invalidateAllSessionsExceptCurrentSession(null); } // since 1.49 static void invalidateAllSessionsExceptCurrentSession(HttpSession currentSession) { for (final HttpSession session : SESSION_MAP_BY_ID.values()) { try { if (currentSession != null && currentSession.getId().equals(session.getId())) { // si l'utilisateur exécutant l'action a une session http, // on ne l'invalide pas continue; } session.invalidate(); } catch (final Exception e) { // Tomcat can throw "java.lang.IllegalStateException: getLastAccessedTime: Session already invalidated" continue; } } } static void invalidateSession(String sessionId) { final HttpSession session = getSessionById(sessionId); if (session != null) { // dans Jenkins notamment, une session invalidée peut rester un peu dans cette map try { session.invalidate(); } catch (final Exception e) { // Tomcat can throw "java.lang.IllegalStateException: getLastAccessedTime: Session already invalidated" return; } } } private static HttpSession getSessionById(String sessionId) { final HttpSession session = SESSION_MAP_BY_ID.get(sessionId); if (session == null) { // In some cases (issue 473), Tomcat changes id in session withtout calling sessionCreated. // In servlet 3.1, HttpSessionIdListener.sessionIdChanged could be used. for (final HttpSession other : SESSION_MAP_BY_ID.values()) { if (other.getId().equals(sessionId)) { return other; } } } return session; } private static void removeSessionsWithChangedId() { for (final Map.Entry<String, HttpSession> entry : SESSION_MAP_BY_ID.entrySet()) { final String id = entry.getKey(); final HttpSession other = entry.getValue(); if (!id.equals(other.getId())) { SESSION_MAP_BY_ID.remove(id); } } } private static void addSession(final HttpSession session) { SESSION_MAP_BY_ID.put(session.getId(), session); } private static void removeSession(final HttpSession session) { final HttpSession removedSession = SESSION_MAP_BY_ID.remove(session.getId()); if (removedSession == null) { // In some cases (issue 473), Tomcat changes id in session withtout calling sessionCreated. // In servlet 3.1, HttpSessionIdListener.sessionIdChanged could be used. removeSessionsWithChangedId(); } } static List<SessionInformations> getAllSessionsInformations() { final Collection<HttpSession> sessions = SESSION_MAP_BY_ID.values(); final List<SessionInformations> sessionsInformations = new ArrayList<SessionInformations>( sessions.size()); for (final HttpSession session : sessions) { try { sessionsInformations.add(new SessionInformations(session, false)); } catch (final Exception e) { // Tomcat can throw "java.lang.IllegalStateException: getLastAccessedTime: Session already invalidated" continue; } } sortSessions(sessionsInformations); return Collections.unmodifiableList(sessionsInformations); } static List<SessionInformations> sortSessions(List<SessionInformations> sessionsInformations) { if (sessionsInformations.size() > 1) { Collections.sort(sessionsInformations, Collections.reverseOrder(new SessionInformationsComparator())); } return sessionsInformations; } static SessionInformations getSessionInformationsBySessionId(String sessionId) { final HttpSession session = getSessionById(sessionId); if (session == null) { return null; } // dans Jenkins notamment, une session invalidée peut rester un peu dans cette map try { return new SessionInformations(session, true); } catch (final Exception e) { // Tomcat can throw "java.lang.IllegalStateException: getLastAccessedTime: Session already invalidated" return null; } } /** * Définit la session http pour le thread courant. * @param session HttpSession */ static void bindSession(HttpSession session) { if (session != null) { SESSION_CONTEXT.set(session); } } /** * Retourne la session pour le thread courant ou null. * @return HttpSession */ static HttpSession getCurrentSession() { return SESSION_CONTEXT.get(); } /** * Enlève le lien entre la session et le thread courant. */ static void unbindSession() { SESSION_CONTEXT.remove(); } /** {@inheritDoc} */ @Override public void contextInitialized(ServletContextEvent event) { final long start = System.currentTimeMillis(); // lecture de la propriété système java.io.tmpdir uniquement // pour lancer une java.security.AccessControlException si le SecurityManager est activé, // avant d'avoir une ExceptionInInitializerError pour la classe Parameters System.getProperty("java.io.tmpdir"); final String contextPath = Parameters.getContextPath(event.getServletContext()); if (!instanceEnabled) { if (!CONTEXT_PATHS.contains(contextPath)) { // si jars dans tomcat/lib, il y a plusieurs instances mais dans des webapps différentes (issue 193) instanceEnabled = true; } else { return; } } CONTEXT_PATHS.add(contextPath); Parameters.initialize(event.getServletContext()); LOG.debug("JavaMelody listener init started"); // on initialise le monitoring des DataSource jdbc même si cette initialisation // sera refaite dans MonitoringFilter au cas où ce listener ait été oublié dans web.xml final JdbcWrapper jdbcWrapper = JdbcWrapper.SINGLETON; jdbcWrapper.initServletContext(event.getServletContext()); if (!Parameters.isNoDatabase()) { jdbcWrapper.rebindDataSources(); } final long duration = System.currentTimeMillis() - start; LOG.debug("JavaMelody listener init done in " + duration + " ms"); } /** {@inheritDoc} */ @Override public void contextDestroyed(ServletContextEvent event) { // nettoyage avant le retrait de la webapp au cas où celui-ci ne suffise pas SESSION_MAP_BY_ID.clear(); SESSION_COUNT.set(0); LOG.debug("JavaMelody listener destroy done"); } // Rq : avec les sessions, on pourrait faire des statistiques sur la durée moyenne des sessions // (System.currentTimeMillis() - event.getSession().getCreationTime()) // ou le délai entre deux requêtes http par utilisateur // (System.currentTimeMillis() - httpRequest.getSession().getLastAccessedTime()) /** {@inheritDoc} */ @Override public void sessionCreated(HttpSessionEvent event) { if (!instanceEnabled) { return; } // pour être notifié des passivations et activations, on enregistre un HttpSessionActivationListener (this) final HttpSession session = event.getSession(); // Since tomcat 6.0.21, because of https://issues.apache.org/bugzilla/show_bug.cgi?id=45255 // when tomcat authentication is used, sessionCreated is called twice for 1 session // and each time with different ids, then sessionDestroyed is called once. // So we do not count the 2nd sessionCreated event and we remove the id of the first event if (session.getAttribute(SESSION_ACTIVATION_KEY) == this) { // si la map des sessions selon leurs id contient une session dont la clé // n'est plus égale à son id courant, alors on l'enlève de la map // (et elle sera remise dans la map avec son nouvel id ci-dessous) removeSessionsWithChangedId(); } else { session.setAttribute(SESSION_ACTIVATION_KEY, this); // pour getSessionCount SESSION_COUNT.incrementAndGet(); } // pour invalidateAllSession addSession(session); } /** {@inheritDoc} */ @Override public void sessionDestroyed(HttpSessionEvent event) { if (!instanceEnabled) { return; } final HttpSession session = event.getSession(); // plus de removeAttribute // (pas nécessaire et Tomcat peut faire une exception "session already invalidated") // session.removeAttribute(SESSION_ACTIVATION_KEY); // pour getSessionCount SESSION_COUNT.decrementAndGet(); // pour invalidateAllSession removeSession(session); } /** {@inheritDoc} */ @Override public void sessionDidActivate(HttpSessionEvent event) { if (!instanceEnabled) { return; } // pour getSessionCount SESSION_COUNT.incrementAndGet(); // pour invalidateAllSession addSession(event.getSession()); } /** {@inheritDoc} */ @Override public void sessionWillPassivate(HttpSessionEvent event) { if (!instanceEnabled) { return; } // pour getSessionCount SESSION_COUNT.decrementAndGet(); // pour invalidateAllSession removeSession(event.getSession()); } // pour hudson/Jenkins/jira/confluence/bamboo void registerSessionIfNeeded(HttpSession session) { if (session != null) { synchronized (session) { if (!SESSION_MAP_BY_ID.containsKey(session.getId())) { sessionCreated(new HttpSessionEvent(session)); } } } } // pour hudson/Jenkins/jira/confluence/bamboo void unregisterSessionIfNeeded(HttpSession session) { if (session != null) { try { session.getCreationTime(); // https://issues.jenkins-ci.org/browse/JENKINS-20532 // https://bugs.eclipse.org/bugs/show_bug.cgi?id=413019 session.getLastAccessedTime(); } catch (final IllegalStateException e) { // session.getCreationTime() lance IllegalStateException si la session est invalidée synchronized (session) { sessionDestroyed(new HttpSessionEvent(session)); } } } } // pour hudson/Jenkins/jira/confluence/bamboo void unregisterInvalidatedSessions() { for (final Map.Entry<String, HttpSession> entry : SESSION_MAP_BY_ID.entrySet()) { final HttpSession session = entry.getValue(); if (session.getId() != null) { unregisterSessionIfNeeded(session); } else { // damned JIRA has sessions with null id, when shuting down final String sessionId = entry.getKey(); SESSION_MAP_BY_ID.remove(sessionId); } } // issue 198: in JIRA 4.4.*, sessionCreated is called two times with different sessionId // but with the same attributes in the second than the attributes added in the first, // so SESSION_COUNT is periodically counted again SESSION_COUNT.set(SESSION_MAP_BY_ID.size()); } void removeAllActivationListeners() { for (final HttpSession session : SESSION_MAP_BY_ID.values()) { try { session.removeAttribute(SESSION_ACTIVATION_KEY); } catch (final Exception e) { // Tomcat can throw "java.lang.IllegalStateException: xxx: Session already invalidated" continue; } } } /** {@inheritDoc} */ @Override public String toString() { return getClass().getSimpleName() + "[sessionCount=" + getSessionCount() + ']'; } }