Preface
Just for Java developers, we should be familiar with Session in the J2EE specification. We can use Session to manage user session information. The most common one is to use Session to store user login, identity, permissions and status information. For most developers who use Tomcat as a web container, how does Tomcat implement Session to mark users and manage Session information?
summary
SESSION
Tomcat internally defines the interfaces related to the Session and HttpSession, the class inheritance system is shown in Figure 1.
Figure 1 Session class inheritance system
Figure 1 additionally lists the class inheritance system of Session, and they are introduced one by one here.
Session: The basic interface specification for sessions in Tomcat. Figure 1 lists the main methods it defines. Table 1 introduces these methods.
Table 1 Session interface description
method | describe |
getCreationTime()/setCreationTime(time : long) | Get and set the creation time of the Session |
getId()/setId(id: String) | Get and set the ID of the Session |
getThisAccessedTime() | Get the start time of the last request |
getLastAccessedTime() | Get the completion time of the last request |
getManager()/setManager(manager: Manager) | Get and set up Session Manager |
getMaxInactiveInterval()/setMaxInactiveInterval(interval : int) | Get the maximum access interval between setting the Session |
getSession() | Get HttpSession |
isValid()/setValid(isValid: boolean) | Get and set the valid status of the Session |
access()/endAccess() | Start and end the session access |
expire() | Set the Session Expiration |
HttpSession: An interface specification for a session provided by an HTTP client and an HTTP server. Figure 1 lists the main methods it defines, and Table 2 introduces these methods.
Table 2 HttpSession interface description
method | describe |
getCreationTime() | Get the creation time of the Session |
getId() | Get the ID of the Session |
getLastAccessedTime() | Get the completion time of the last request |
getServletContext() | Get the ServletContext to which the current session belongs |
getMaxInactiveInterval()/setMaxInactiveInterval(interval : int) | Get the maximum access interval between setting the Session |
getAttribute(name : String) /setAttribute(name : String, value : Object) | Get and set the Session scope properties |
removeAttribute(name: String) | Clear Session scope properties |
invalidate() | Invalidate the Session and undo any object bound to this Session |
ClusterSession: The session interface specification under cluster deployment. Figure 1 lists its main methods, and Table 3 introduces these methods.
Table 3 ClusterSession interface description
method | describe |
isPrimarySession() | Is it the main session of the cluster? |
setPrimarySession(boolean primarySession) | Set up the cluster master session |
StandardSession: A standard HTTP Session implementation, this article will use this implementation as an example.
When deploying a Tomcat cluster, the session status of each node in the cluster needs to be kept synchronized. Currently, Tomcat provides two synchronization strategies:
ReplicatedSession: Each time, the entire session object is synchronized to other nodes in the cluster, and the other nodes then update the entire session object. This implementation is relatively simple and convenient, but it will cause the transmission of a large amount of invalid information.
DeltaSession: Synchronizes the incrementally modified properties in the session. Since this method is incremental, it will greatly reduce the overhead of network I/O, but the implementation will be more complicated because it involves the management of session attribute operation processes.
SESSION Manager
Tomcat internally defines the Manager interface for formulating the interface specifications of the Session Manager. There are currently many implementations of the Session Manager, as shown in Figure 2.
Figure 2 Class inheritance system of Session Manager
We will describe the corresponding content in Figure 2 one by one below:
Manager: Tomcat For the interface specification defined by the Session Manager, Figure 2 has listed the main methods defined in the Manager interface, and Table 4 describes the role of these methods in detail.
Table 4 Manager interface description
method | describe |
getContainer()/setContainer(container: Container) | Get or set the container associated with the Session Manager, generally a Context container |
getDistributable()/setDistributable(distributable: boolean) | Get or set whether the Session Manager supports distributed |
getMaxInactiveInterval()/setMaxInactiveInterval(interval : int) | Get or set the maximum inactive interval for the Session created by the Session Manager |
getSessionIdLength()/setSessionIdLength(idLength: int) | Get or set the length of the Session ID created by the Session Manager |
getSessionCounter()/setSessionCounter(sessionCounter: long) | Get or set the total number of sessions created by the Session Manager |
getMaxActive()/setMaxActive(maxActive: int) | Get or set the maximum number of currently activated sessions |
getActiveSessions() | Get all the sessions currently activated |
getExpiredSessions()/setExpiredSessions(expiredSessions: long) | Get or set the number of currently expired sessions |
getRejectedSessions()/setRejectedSessions(rejectedSessions: int) | Get or set the number of sessions that have been denied to be created |
getSessionMaxAliveTime()/setSessionMaxAliveTime(sessionMaxAliveTime: int) | Gets or sets the maximum activity duration in an expired session |
getSessionAverageAliveTime()/setSessionAverageAliveTime(sessionAverageAliveTime: int) | Get or set the average activity duration for an expired session |
add(session: Session)/remove(session: Session) | Add or delete active sessions to the Session Manager |
changeSessionId(session: Session) | Set the newly generated random Session ID for the Session |
createSession(sessionId: String) | Create a new session based on the default attribute configuration of the Session Manager |
findSession(id: String) | Returns the session with the unique mark of the sessionId parameter |
findSessions() | Returns all activities managed by the Session Manager |
load()/unload() | Load Session from persistence mechanism or write Session to persistence mechanism |
backgroundProcess() | The container interface is defined as the implementation of specific container processing related work in the background. The Session Manager implements the destruction of expired Session based on this mechanism. |
ManagerBase : encapsulates an abstract class that is commonly implemented by the Manager interface. It does not provide implementation of load()/unload() and other methods. It requires specific subclasses to implement it. All Session Managers are inherited from ManagerBase.
ClusterManager : Added some interfaces under cluster deployment based on the Manager interface. All managers that implement Session management under the cluster need to implement this interface.
PersistentManagerBase: provides a basic implementation of Session persistence.
PersistentManager: Inherited from PersistentManagerBase, it can be used by configuring the <Store> element in Server.xml. PersistentManager can back up the Session information in memory to a file or database. When a Session object is backed up, the Session object is copied to memory (file or database), while the original object remains in memory. Therefore, even if the server goes down, the active Session object can still be retrieved from the memory. If the active Session object exceeds the upper limit or the Session object is idle for too long, the Session will be swapped out to memory to save memory space.
StandardManager: No need to configure the <Store> element. When Tomcat is closed normally, restarted or web application reloads, it will serialize the memory Session to the /work/Catalina/host_name/webapp_name/SESSIONS.ser file in the Tomcat directory. . When Tomcat restarts or the application is loaded, Tomcat will restore the Session in the file to memory. If the server is terminated suddenly, all sessions will be lost because StandardManager does not have a chance to implement save processing.
ClusterManagerBase: provides cluster management implementation for Session.
DeltaManager: Inherited from ClusterManagerBase. This Session Manager is Tomcat's default manager under cluster deployment. When a node in the cluster generates or modifys a session, DeltaManager will copy these modification increments to other nodes.
BackupManager: does not inherit ClusterManagerBase, but directly implements the ClusterManager interface. It is an optional Session Manager for Tomcat under cluster deployment. All Sessions in the cluster are fully replicated to a backup node. All nodes in the cluster can access this backup node to achieve the backup effect of Session in the cluster.
For simplicity, this article uses StandardManager as an example to explain the management of Session. StandardManager is a child component of StandardContext, used to manage the creation and maintenance of all sessions of the current Context. If you should read or be familiar with the content of the article "Tomcat Source Code Analysis - Life Cycle Management", then you will know that when StandardContext is officially launched, that is, StandardContext's startInternal method (see Listing 1) is called, StandardContext will still Start StandardManager.
Code Listing 1
@Override protected synchronized void startInternal() throws LifecycleException { // Omit code that is not related to Session management// Acquire clustered manager Manager contextManager = null; if (manager == null) { if ( (getCluster() != null) && distributable ) { try { contextManager = getCluster().createManager(getName()); } catch (Exception ex) { log.error("standardContext.clusterFail", ex); ok = false; } } else { contextManager = new StandardManager( ); } } // Configure default manager if none was specified if (contextManager != null) { setManager(contextManager); } if (manager!=null && (getCluster() ! = null) && distributable) { //let the cluster know that there is a context that is distributed //and that it has its own manager getCluster().registerManager(manager); } // Omit the code that is not related to Session management try { // S tart manager if ((manager != null) && (manager instanceof Lifecycle)) { ((Lifecycle) getManager()).start(); } // Start ContainerBackgroundProcessor thread super.threadStart(); } catc h(Exception e) { log.error("Error manager. start()", e); ok = false; } // Omit code that is not related to Session management}
From Listing 1, you can see that the execution steps involved in Session management in the startInternal method of StandardContext are as follows:
Create StandardManager;
If Tomcat combines Apache for distributed deployment, the current StandardManager will be registered in the cluster;
Start StandardManager;
The start method of StandardManager is used to start StandardManager, and the implementation is shown in Listing 2 of the code.
Code Listing 2
@Override public synchronized final void start() throws LifecycleException { //Omit the code for state verification if (state.equals(LifecycleState.NEW)) { init(); } else if (!state.equals(LifecycleState.INITIALIZED) && !state.equals(LifecycleState.STOPPED)) { invalidTransition(Lifecycle.BEFORE_START_EVENT); } setState(LifecycleState.STARTING_PREP); try { startInterna l(); } catch (LifecycleException e) { setState(LifecycleState.FAILED); throw e; } if (state.equals(LifecycleState.FAILED) || state.equals(LifecycleState.MUST_STOP)) { stop(); } else { // Shouldn't be necessary but acts as a check th at sub-classes are // doing what they are supposed to. if (!state.equals(LifecycleState.STARTING)) { invalidTransition(Lifecycle.AFTER_START_EVENT); } setState(LifecycleState.STARTED); } }
From Listing 2, we can see that the steps to start StandardManager are as follows:
Call the init method to initialize the StandardManager;
Call the startInternal method to start StandardManager;
Initialization of STANDARDMANAGER
After the above analysis, we know that the first step to start StandardManager is to call the init method of the parent class LifecycleBase. This method has been introduced in detail in the article "Tomcat Source Code Analysis - Lifecycle Management", so we only need to care about the initInternal of StandardManager. StandardManager itself does not implement the initInternal method, but StandardManager's parent class ManagerBase implements this method, see Listing 3 for its implementation.
Code Listing 3
@Override protected void initInternal() throws LifecycleException { super.initInternal(); setDistributable(((Context) getContainer()).getDistribut able()); // Initialize random number generation getRandomBytes(new byte[16]); }
Reading List of Code 3, we summarize the execution steps of ManagerBase's initInternal method:
Register the container itself, the StandardManager, to JMX (refer to the article "Tomcat Source Code Analysis - Lifecycle Management" for the implementation of the initInternal method of LifecycleMBeanBase);
Get the current Tomcat from the parent container StandardContext and set it to the Boolean property of ManagerBase distributable;
Call the getRandomBytes method to get a random byte array from the random number file /dev/urandom. If this file does not exist, an instance of java.security.SecureRandom is generated by reflection, and use it to generate a random byte array.
Note: The random byte array generated by calling getRandomBytes method here will not be used. The reason why it is called here is actually to complete the initialization of the random number generator so that it can be used when allocating the Session ID in the future.
Let's read the code implementation of the getRandomBytes method in detail, see Listing 4 of the code.
Code Listing 4
protected void getRandomBytes(byte bytes[]) { // Generate a byte array containing a session identifier if (devRandomSource != null && randomIS == null) { set RandomFile(devRandomSource); } if (randomIS != null) { try { int len = randomIS.read(bytes); if (len == bytes.length) { return; } if(log.isDebugEnabled()) log.debug("Got " + len + " " + bytes.length ); } catch (Exception ex) { // Ignore } devRandomSource = null; try { randomIS.close(); } catch (Exception e) { log.warn("Failed to close randomIS."); } randomIS = null; } getRandom() .nextBytes(bytes); }
setRandomFile in Listing 4
Methods (see Listing 5 for code) are used to get a random array of bytes from random number file /dev/urandom.
List of code 5
public void setRandomFile( String s ) { // as a hack, you can use a static file - and generate the same // session ids ( good for strange debugging ) if (Globals.IS_SE CURITY_ENABLED){ randomIS = AccessController.doPrivileged(new PrivilegedSetRandomFile (s)); } else { try{ devRandomSource=s; File f=new File( devRandomSource ); if( ! f.exists() ) return; randomIS= new DataInputStream( new FileInputStrea m(f)); randomIS.readLong( ); if( log.isDebugEnabled() ) log.debug( "Opening " + devRandomSource ); } catch( IOException ex ) { log.warn("Error reading " + devRandomSource, ex); i f (randomIS != null) { try { randomIS.close(); } catch (Exception e) { log.warn("Failed to close randomIS."); } } devRandomSource = null; randomIS=null; } } }
The setRandomFile method in Listing 4 (see Listing 6) generates an instance of java.security.SecureRandom through reflection, and uses this instance to generate an array of random bytes.
Code Listing 6
public Random getRandom() { if (this.random == null) { // Calculate the new random number generator seed long seed = System.currentTimeMillis(); long t1 = seed ; char entropy[] = getEntropy().toCharArray( ); for (int i = 0; i < entropy.length; i++) { long update = ((byte) entropy[i]) << ((i % 8) * 8); seed ^= update; } try { //Construct and seed a new random number generator Class<?> clazz = Class.forName(randomClass); this.random = (Random) clazz.newInstance(); this.random.setSe ed(seed); } catch (Exception e ) { // Fall back to the simple case log.error(sm.getString("managerBase.random", randomClass), e); this.random = new java.util.Random(); this.random.setS eed(seed) ); } if(log.isDebugEnabled()) { long t2=System.currentTimeMillis(); if( (t2-t1) > 100 ) log.debug(sm.getString("managerBase.seeding", randomClas s) + " " + (t2-t1)); } } return (this.random); }
According to the above analysis, the initialization of StandardManager mainly executes the initInternal method of ManagerBase.
Starting of STANDARDMANAGER
Calling the startInternal method of StandardManager is used to start StandardManager, see Listing 7.
Code Listing 7
@Override protected synchronized void startInternal() throws LifecycleException { // Force initialization of the random number generator if (log.isDebugEnable d()) log.debug("Force random number initialization starting"); generateSessionId(); if (log.isDebugEnabled ()) log.debug("Force random number initialization completed"); // Load unloaded sessions, if any try { load(); } catch (Throwable t) { log.error(sm.get String("standardManager.managerLoad" ), t); } setState(LifecycleState.STARTING); }
From Listing 7, we can see that the steps to start StandardManager are as follows:
Step 1: Call the generateSessionId method (see Listing 8) to generate a new Session ID;
Code Listing 8
protected synchronized String generatedSessionId() { byte random[] = new byte[16]; String jvmRoute = getJvmRoute(); String result = null; // Render t he result as a String of hexadecimal digits StringBuilder buffer = new StringBuilder(); do { int resultLenBytes = 0; if (result != null) { buffer = new StringBuilder(); duplicates++; } while (resultLenBytes < this.sessionIdLength) { g etRandomBytes(random); random = getDigest().digest(random); for (int j = 0; j < random.length && resultLenBytes < this.sessionIdLength; j++) { byte b1 = (byte) ((random[j] & 0xf0) >> 4); byte b2 = (byte) (random[ j] & 0x0f); if (b1 < 10) buffer.append((char) ('0' + b1)); else buffer.append((char) ('A' + (b1 - 10))); if (b2 < 10) buffer.append((char) ('0' + b2)); else buffer.append((char) ('A' + (b2 - 10))); resultLenBytes++; } } if (jvmRoute ! = null) { buffer.append('.').append(jvmRoute); } result = buffer.toString(); } while (sessions.containsKey(result)); return (result); } : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : :
Step 2 loads the persistent Session information. Why does Session need to be persisted? Since all sessions are maintained in a ConcurrentHashMap in StandardManager, the server restart or downtime causes these session information to be lost or invalid. To solve this problem, Tomcat uses persistence to ensure that these sessions will not be lost. Let’s take a look at the implementation of StandardManager’s load method, see Listing 9 of Code.
Code Listing 9
public void load() throws ClassNotFoundException, IOException { if (SecurityUtil.isPackageProtectionEnabled()){ try{ AccessController.doPr ivileged( new PrivilegedDoLoad() ); } catch (PrivilegedActionException ex){ Exception exception = ex.getException(); if (exception instanceof ClassNotFoundException){ throw (ClassNotFoundException)exception; } else if (exception instanceof IOException){ throw (IOException )exception; } if (log.isDebugEnabled()) log.debug("Unreported exception in load() " + exception); } } else { doLoad(); } }
If the security mechanism is required to be turned on and the package protection mode is turned on, the persisted Session will be loaded by creating a PrivilegedDoLoad, which is implemented as shown in Listing 10 of the code.
Listing 10
private class PrivilegedDoLoad implements PrivilegedExceptionAction<Void> { PrivilegedDoLoad() { // NOOP } public Void run() throws Exception{ do Load(); return null; } }
From Listing 10, we can see that the method that is actually responsible for loading is doLoad. According to Listing 9, we know that by default, the method that loads Session information is also doLoad. So we just need to look at the implementation of doLoad, see Listing 11 of the code.
Listing 11
protected void doLoad() throws ClassNotFoundException, IOException { if (log.isDebugEnabled()) log.debug("Start: Loading persisted sessions"); / / Initialize our internal data structures sessions.clear(); // Open an input stream to the specified pathname, if any File file = file(); if (file == null) return; if (log.isDebugEnabled()) log.debug(sm.getString("standardManager.load ing", pathname)); FileInputStream fis = null; BufferedInputStream bis = null; ObjectInputStream ois = null; Loader loader = null; ClassLoader classLoader = null; try { fis = new File InputStream(file.getAbsolutePath()); bis = new BufferedInputStream(fis); if (container ! = null) loader = container.getLoader(); if (loader != null) classLoader = loader.getClassLoader(); if (classLoader != null) { if (log.isDebugEnab led()) log.debug("Creating custom object input stream for class loader "); ois = new CustomObjectInputStream(bis, classLoader); } else { if (log.isDebugEnabled()) log.debug("Creating standard ob ject input stream"); ois = new ObjectInputStream(bis); } } catch (FileNotFoundException e) { if (log.isDebugEnabled()) log.debug("No persisted data file found"); return; } catch (IOException e) { log.err or(sm.getString("standardManager.loading .ioe", e), e); if (fis != null) { try { fis.close(); } catch (IOException f) { // Ignore } } if (bis != null) { try { bis. close(); } catch (IOException f) { // Ignore } } throw e; } // Load the previously unloaded active sessions synchronized (sessions) { try { Integer count = (Integer) ois.readObject(); int n = count.intValue(); if (log.isDebugEnabled()) log.debug("Loading " + n + " persisted sessions"); for (int i = 0; i < n; i++) { StandardSession session = getNewSession() ; session.readObjectData(ois); session.setManager(this); sessions.put(session.getIdInternal(), session); session.activate(); if (!session.is ValidInternal()) { // If session is already invalid, // expire session to prevent memory leak. session.setValid(true); session.expire(); } sessionCounter++; } } catch (ClassNotFoundExcept ion e) { log.error(sm.getString("standardManager.loading.cnfe" , e), e); try { ois.close(); } catch (IOException f) { // Ignore } throw e; } catch (IOException e) { log.error(sm.getString("standardManager.loading. ioe ", e), e); try { ois.close(); } catch (IOException f) { // Ignore } throw e; } finally { // Close the input stream try { ois.close(); } catch ( IOException f) { // ignored } // Delete the persistent storage file if (file.exists() ) file.delete(); } } if (log.isDebugEnabled()) log.debug("Finish: Loa ding persisted sessions" ); }
From Listing 11, see the execution steps of the StandardManager doLoad method are as follows:
Clear the session information maintained by the sessions cache;
Call the file method to return the Session persistent file under the current Context, such as: D:/workspace/Tomcat7.0/work/Catalina/localhost/host-manager/SESSIONS.ser;
Open the input stream of the Session persistent file and encapsulate it as CustomObjectInputStream;
Read the number of persisted sessions from the Session persistent file, and then read the Session information one by one and put it into the sessions cache.
At this point, the introduction to the launch of StandardManager is here. I will explain the allocation, tracking, destruction and other content of Session in the next article.