前面兩篇簡述了Tomcat的啓動和請求處理,這篇我們來看一些平時使用中的細節用法
一.session
session對於web開發人員應該不會陌生了,session和cookie一個存儲在服務端一個存儲在客戶端,我們來看下tomcat是如何實現session的。
建議大家認真讀一下我在第一篇中發的幾個鏈接,尤其是http://blog.csdn.net/beliefer/article/category/6154740,@泰山不老生的文章對session源碼的解讀還是很詳盡的,這裏我也用我自己的思路去整理一遍。
session的生成依賴於一個session的管理器,因此在Tomcat啓動之初就生成了這個session管理器,我們來看下StandardContext的啓動,我們在Tomcat啓動的篇章已經講述了四個容器依次啓動的方式,而在context啓動的時候會打開這個manager:
Manager contextManager = null;
if (manager == null) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("standardContext.cluster.noManager",
Boolean.valueOf((getCluster() != null)),
Boolean.valueOf(distributable)));
}
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) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("standardContext.manager",
contextManager.getClass().getName()));
}
setManager(contextManager);
}
try {
// Start manager
if ((manager != null) && (manager instanceof Lifecycle)) {
((Lifecycle) getManager()).start();
}
// Start ContainerBackgroundProcessor thread
super.threadStart();
} catch(Exception e) {
log.error("Error manager.start()", e);
ok = false;
}
以上是StandardContext的startInternal方法的兩個片段,可以看到它先創建了manager對象,並賦值給了context內置的manager,最後去啓動manager
manager的startInternal方法則調用了load方法,load調用doload:
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.loading", pathname));
FileInputStream fis = null;
BufferedInputStream bis = null;
ObjectInputStream ois = null;
Loader loader = null;
ClassLoader classLoader = null;
try {
fis = new FileInputStream(file.getAbsolutePath());
bis = new BufferedInputStream(fis);
if (container != null)
loader = container.getLoader();
if (loader != null)
classLoader = loader.getClassLoader();
if (classLoader != null) {
if (log.isDebugEnabled())
log.debug("Creating custom object input stream for class loader ");
ois = new CustomObjectInputStream(bis, classLoader);
} else {
if (log.isDebugEnabled())
log.debug("Creating standard object input stream");
ois = new ObjectInputStream(bis);
}
} catch (FileNotFoundException e) {
if (log.isDebugEnabled())
log.debug("No persisted data file found");
return;
} catch (IOException e) {
log.error(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;
}
通過文件輸入流來初始化持久化的session,啓動的流程大致是這樣。
然後就是當調用的時候,我們可以看到org.apache.catalina.connector.Request類,裏面的getSession方法:
@Override
public HttpSession getSession() {
Session session = doGetSession(true);
if (session == null) {
return null;
}
return session.getSession();
}
protected Session doGetSession(boolean create) {
// There cannot be a session if no context has been assigned yet
if (context == null)
return (null);
// Return the current session if it exists and is valid
if ((session != null) && !session.isValid())
session = null;
if (session != null)
return (session);
// Return the requested session if it exists and is valid
Manager manager = null;
if (context != null)
manager = context.getManager();
if (manager == null)
return (null); // Sessions are not supported
if (requestedSessionId != null) {
try {
session = manager.findSession(requestedSessionId);
} catch (IOException e) {
session = null;
}
if ((session != null) && !session.isValid())
session = null;
if (session != null) {
session.access();
return (session);
}
}
// Create a new session if requested and the response is not committed
if (!create)
return (null);
if ((context != null) && (response != null) &&
context.getServletContext().getEffectiveSessionTrackingModes().
contains(SessionTrackingMode.COOKIE) &&
response.getResponse().isCommitted()) {
throw new IllegalStateException
(sm.getString("coyoteRequest.sessionCreateCommitted"));
}
// Attempt to reuse session id if one was submitted in a cookie
// Do not reuse the session id if it is from a URL, to prevent possible
// phishing attacks
// Use the SSL session ID if one is present.
if (("/".equals(context.getSessionCookiePath())
&& isRequestedSessionIdFromCookie()) || requestedSessionSSL ) {
session = manager.createSession(getRequestedSessionId());
} else {
session = manager.createSession(null);
}
// Creating a new session cookie based on that session
if ((session != null) && (getContext() != null)
&& getContext().getServletContext().
getEffectiveSessionTrackingModes().contains(
SessionTrackingMode.COOKIE)) {
Cookie cookie =
ApplicationSessionCookieConfig.createSessionCookie(
context, session.getIdInternal(), isSecure());
response.addSessionCookieInternal(cookie);
}
if (session == null) {
return null;
}
session.access();
return session;
}
我們可以看到在doGetSession的時候還是先獲取context,在上兩篇已經講到怎麼把context傳入每次的請求中了,然後啓動時也將manager傳入了context,因此在context中獲取manager然後再創建,創建的過程也是先查詢有沒有,再創建:
@Override
public Session createSession(String sessionId) {
if ((maxActiveSessions >= 0) &&
(getActiveSessions() >= maxActiveSessions)) {
rejectedSessions++;
throw new IllegalStateException(
sm.getString("managerBase.createSession.ise"));
}
// Recycle or create a Session instance
Session session = createEmptySession();
// Initialize the properties of the new session and return it
session.setNew(true);
session.setValid(true);
session.setCreationTime(System.currentTimeMillis());
session.setMaxInactiveInterval(this.maxInactiveInterval);
String id = sessionId;
if (id == null) {
id = generateSessionId();
}
session.setId(id);
sessionCounter++;
SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
synchronized (sessionCreationTiming) {
sessionCreationTiming.add(timing);
sessionCreationTiming.poll();
}
return (session);
}
而session的內部也是用一個map來儲存attribute的:
/**
* The collection of user data attributes associated with this Session.
*/
protected Map<String, Object> attributes = new ConcurrentHashMap<String, Object>();
之所以用concurrentHashMap,我們知道Tomcat的每個請求處理都在不同線程,因此需要用線性安全的map來存儲。
最後與start對應的是manager的stop過程,其調用unload:
@Override
public void unload() throws IOException {
if (SecurityUtil.isPackageProtectionEnabled()){
try{
AccessController.doPrivileged( new PrivilegedDoUnload() );
} catch (PrivilegedActionException ex){
Exception exception = ex.getException();
if (exception instanceof IOException){
throw (IOException)exception;
}
if (log.isDebugEnabled())
log.debug("Unreported exception in unLoad() "
+ exception);
}
} else {
doUnload();
}
}
protected void doUnload() throws IOException {
if (log.isDebugEnabled())
log.debug(sm.getString("standardManager.unloading.debug"));
if (sessions.isEmpty()) {
log.debug(sm.getString("standardManager.unloading.nosessions"));
return; // nothing to do
}
// Open an output stream to the specified pathname, if any
File file = file();
if (file == null)
return;
if (log.isDebugEnabled())
log.debug(sm.getString("standardManager.unloading", pathname));
FileOutputStream fos = null;
ObjectOutputStream oos = null;
try {
fos = new FileOutputStream(file.getAbsolutePath());
oos = new ObjectOutputStream(new BufferedOutputStream(fos));
} catch (IOException e) {
log.error(sm.getString("standardManager.unloading.ioe", e), e);
if (fos != null) {
try {
fos.close();
} catch (IOException f) {
// Ignore
}
}
throw e;
}
// Write the number of active sessions, followed by the details
ArrayList<StandardSession> list = new ArrayList<StandardSession>();
synchronized (sessions) {
if (log.isDebugEnabled())
log.debug("Unloading " + sessions.size() + " sessions");
try {
oos.writeObject(new Integer(sessions.size()));
Iterator<Session> elements = sessions.values().iterator();
while (elements.hasNext()) {
StandardSession session =
(StandardSession) elements.next();
list.add(session);
session.passivate();
session.writeObjectData(oos);
}
} catch (IOException e) {
log.error(sm.getString("standardManager.unloading.ioe", e), e);
try {
oos.close();
} catch (IOException f) {
// Ignore
}
throw e;
}
}
// Flush and close the output stream
try {
oos.flush();
} finally {
try {
oos.close();
} catch (IOException f) {
// Ignore
}
}
// Expire all the sessions we just wrote
if (log.isDebugEnabled())
log.debug("Expiring " + list.size() + " persisted sessions");
Iterator<StandardSession> expires = list.iterator();
while (expires.hasNext()) {
StandardSession session = expires.next();
try {
session.expire(false);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
} finally {
session.recycle();
}
}
if (log.isDebugEnabled())
log.debug("Unloading complete");
}
將session的內容寫入文件,和load過程相對應。
二.ServletContext
servletContext相對來說方便一點,我們直接看request中:
@Override
public ServletContext getServletContext() {
return context.getServletContext();
}
調用了context的getServletContext方法,繼續追蹤:
/**
* Return the servlet context for which this Context is a facade.
*/
@Override
public ServletContext getServletContext() {
if (context == null) {
context = new ApplicationContext(this);
if (altDDName != null)
context.setAttribute(Globals.ALT_DD_ATTR,altDDName);
}
return (context.getFacade());
}
典型的單例,我們之前講到過,一個context容器對應一個web應用,而servletContext原本就是web應用的唯一存儲容器,所以。。。不用解釋了吧。
三.listener
servletContextListener是一個比較重要的概念,在web.xml中可以進行相應的配置,並監聽項目的啓動停止,其實他的實現過程也非常簡單,在context的start過程中調用了listenerStart方法:
/**
* Configure the set of instantiated application event listeners
* for this Context. Return <code>true</code> if all listeners wre
* initialized successfully, or <code>false</code> otherwise.
*/
public boolean listenerStart() {
if (log.isDebugEnabled())
log.debug("Configuring application event listeners");
// Instantiate the required listeners
String listeners[] = findApplicationListeners();
Object results[] = new Object[listeners.length];
boolean ok = true;
for (int i = 0; i < results.length; i++) {
if (getLogger().isDebugEnabled())
getLogger().debug(" Configuring event listener class '" +
listeners[i] + "'");
try {
results[i] = instanceManager.newInstance(listeners[i]);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
getLogger().error
(sm.getString("standardContext.applicationListener",
listeners[i]), t);
ok = false;
}
}
if (!ok) {
getLogger().error(sm.getString("standardContext.applicationSkipped"));
return (false);
}
// Sort listeners in two arrays
ArrayList<Object> eventListeners = new ArrayList<Object>();
ArrayList<Object> lifecycleListeners = new ArrayList<Object>();
for (int i = 0; i < results.length; i++) {
if ((results[i] instanceof ServletContextAttributeListener)
|| (results[i] instanceof ServletRequestAttributeListener)
|| (results[i] instanceof ServletRequestListener)
|| (results[i] instanceof HttpSessionAttributeListener)) {
eventListeners.add(results[i]);
}
if ((results[i] instanceof ServletContextListener)
|| (results[i] instanceof HttpSessionListener)) {
lifecycleListeners.add(results[i]);
}
}
//Listeners may have been added by ServletContextInitializers. Put them after the ones we know about.
for (Object eventListener: getApplicationEventListeners()) {
eventListeners.add(eventListener);
}
setApplicationEventListeners(eventListeners.toArray());
for (Object lifecycleListener: getApplicationLifecycleListeners()) {
lifecycleListeners.add(lifecycleListener);
}
setApplicationLifecycleListeners(lifecycleListeners.toArray());
// Send application start events
if (getLogger().isDebugEnabled())
getLogger().debug("Sending application start events");
// Ensure context is not null
getServletContext();
context.setNewServletContextListenerAllowed(false);
Object instances[] = getApplicationLifecycleListeners();
if (instances == null)
return (ok);
ServletContextEvent event =
new ServletContextEvent(getServletContext());
for (int i = 0; i < instances.length; i++) {
if (instances[i] == null)
continue;
if (!(instances[i] instanceof ServletContextListener))
continue;
ServletContextListener listener =
(ServletContextListener) instances[i];
try {
fireContainerEvent("beforeContextInitialized", listener);
listener.contextInitialized(event);
fireContainerEvent("afterContextInitialized", listener);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
fireContainerEvent("afterContextInitialized", listener);
getLogger().error
(sm.getString("standardContext.listenerStart",
instances[i].getClass().getName()), t);
ok = false;
}
}
return (ok);
}
前面一大段將listener解析完成以後,後面去遍歷啓動,上面代碼比較長,核心在這一部分:
for (int i = 0; i < instances.length; i++) {
if (instances[i] == null)
continue;
if (!(instances[i] instanceof ServletContextListener))
continue;
ServletContextListener listener =
(ServletContextListener) instances[i];
try {
fireContainerEvent("beforeContextInitialized", listener);
listener.contextInitialized(event);
fireContainerEvent("afterContextInitialized", listener);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
fireContainerEvent("afterContextInitialized", listener);
getLogger().error
(sm.getString("standardContext.listenerStart",
instances[i].getClass().getName()), t);
ok = false;
}
}
好了,關於session、servletContext、listener三塊內容到此爲止,後續還會繼續解讀其他模塊的源碼。