《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门,即可获取!
return AccessController.
doPrivileged(new GetSessionPrivilegedAction(create));
} else {
// RequestFacade 是个外观模式实现,核心请求还是会传递给 Request处理的
// org.apache.catalina.connector.Request
return request.getSession(create);
}
}
// org.apache.catalina.connector.Request#getSession(boolean)
/**
- @return the session associated with this Request, creating one
- if necessary and requested.
- @param create Create a new session if one does not exist
*/
@Override
public HttpSession getSession(boolean create) {
// 由 create 字段决定是否需要创建新的session, 如果不存在的话。
// Session 是tomcat的一个会话实现类,并非对接规范接口类,其会包装一个HttpSession,以便统一交互
// 因为只有 HttpSession 才是 Servlet 的接口规范,在tomcat中会以 StandardSessionFacade 实现接口,其也是一个外观模式的实现,具体工作由 StandardSession 处理。
Session session = doGetSession(create);
if (session == null) {
return null;
}
// 包装 Session 为 HttpSession 规范返回
return session.getSession();
}
// org.apache.catalina.connector.Request#doGetSession
protected Session doGetSession(boolean create) {
// There cannot be a session if no context has been assigned yet
// mappingData.context;
Context context = getContext();
if (context == null) {
return (null);
}
// Return the current session if it exists and is valid
// 此处检查session有效性时,也会做部分清理工作
if ((session != null) && !session.isValid()) {
session = null;
}
if (session != null) {
return (session);
}
// Return the requested session if it exists and is valid
// 获取manager 实例,即真正进行 Session 管理的类,其实主要分两种:1. 基于内存;2. 基于文件的持久化;
Manager manager = context.getManager();
if (manager == null) {
return (null); // Sessions are not supported
}
if (requestedSessionId != null) {
try {
// 如果不是第一次请求,则会带上服务返回的 sessionId, 就会主动查找原来的session
// 从 sessions 中查找即可
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
// 主动请求session时,才会继续后续逻辑
if (!create) {
return (null);
}
if (response != null
&& context.getServletContext()
.getEffectiveSessionTrackingModes()
.contains(SessionTrackingMode.COOKIE)
&& response.getResponse().isCommitted()) {
throw new IllegalStateException(
sm.getString(“coyoteRequest.sessionCreateCommitted”));
}
// Re-use session IDs provided by the client in very limited
// circumstances.
String sessionId = getRequestedSessionId();
if (requestedSessionSSL) {
// If the session ID has been obtained from the SSL handshake then
// use it.
} else if ((“/”.equals(context.getSessionCookiePath())
&& isRequestedSessionIdFromCookie())) {
/* This is the common(ish) use case: using the same session ID with
- multiple web applications on the same host. Typically this is
- used by Portlet implementations. It only works if sessions are
- tracked via cookies. The cookie must have a path of “/” else it
- won’t be provided for requests to all web applications.
- Any session ID provided by the client should be for a session
- that already exists somewhere on the host. Check if the context
- is configured for this to be confirmed.
*/
if (context.getValidateClientProvidedNewSessionId()) {
boolean found = false;
for (Container container : getHost().findChildren()) {
Manager m = ((Context) container).getManager();
if (m != null) {
try {
if (m.findSession(sessionId) != null) {
found = true;
break;
}
} catch (IOException e) {
// Ignore. Problems with this manager will be
// handled elsewhere.
}
}
}
if (!found) {
sessionId = null;
}
}
} else {
// 当session无效时,需要将原来的seesionId置空,删除并新创建一个使用
sessionId = null;
}
// 创建session, StandardManager -> ManagerBase
session = manager.createSession(sessionId);
// Creating a new session cookie based on that session
if (session != null
&& context.getServletContext()
.getEffectiveSessionTrackingModes()
.contains(SessionTrackingMode.COOKIE)) {
// 创建cookie信息,与session对应
Cookie cookie =
ApplicationSessionCookieConfig.createSessionCookie(
context, session.getIdInternal(), isSecure());
// 添加到response中,在响应结果一起返回给客户端
response.addSessionCookieInternal(cookie);
}
if (session == null) {
return null;
}
// 每次请求session时,必然刷新激活时间,以便判定会话是否超时
session.access();
return session;
}
从上面我们可以看到,session的流程大概是这样的:
1. 先查找是否有session信息存在,如果有则判断是否失效;
2. 如果不存在session或已失效,则使用一个新的sessionId(非必须)创建一个session实例;
3. session创建成功,则将sessionId写入到cookie信息中,以便客户端后续使用;
4. 每次请求完session,必定刷新下访问时间以续期;
session的管理主要有两种实现方式,类图如下:
我们先主要以基于内存的实现来理解下session的管理过程。实际上StandardManager基本就依托于 ManagerBase 就实现了Session管理功能,下面我们来看一下其创建session如何?
// org.apache.catalina.session.ManagerBase#createSession
@Override
public Session createSession(String sessionId) {
// 首先来个安全限制,允许同时存在多少会话
// 这个会话实际上代表的是一段时间的有效性,并非真正的用户有效使用在线,所以该值一般要求比预计的数量大些才好
if ((maxActiveSessions >= 0) &&
(getActiveSessions() >= maxActiveSessions)) {
rejectedSessions++;
throw new TooManyActiveSessionsException(
sm.getString(“managerBase.createSession.ise”),
maxActiveSessions);
}
// Recycle or create a Session instance
// 创建空的session 容器 return new StandardSession(this);
Session session = createEmptySession();
// Initialize the properties of the new session and return it
// 默认30分钟有效期
session.setNew(true);
session.setValid(true);
session.setCreationTime(System.currentTimeMillis());
session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);
String id = sessionId;
if (id == null) {
// sessionId 为空时,生成一个,随机id
id = generateSessionId();
}
// 设置sessionId, 注意此处不仅仅是set这么简单,其同时会将自身session注册到全局session管理器中.如下文
session.setId(id);
sessionCounter++;
SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
synchronized (sessionCreationTiming) {
// LinkedList, 添加一个,删除一个?
sessionCreationTiming.add(timing);
sessionCreationTiming.poll();
}
return (session);
}
// org.apache.catalina.session.StandardSession#setId
/**
- Set the session identifier for this session.
- @param id The new session identifier
*/
@Override
public void setId(String id) {
setId(id, true);
}
@Override
public void setId(String id, boolean notify) {
// 如果原来的id不为空,则先删除原有的
if ((this.id != null) && (manager != null))
manager.remove(this);
this.id = id;
// 再将自身会话注册到 manager 中,即 sessions 中
if (manager != null)
manager.add(this);
// 通知监听者,这是框架该做好的事(扩展点),不过不是本文的方向,忽略
if (notify) {
tellNew();
}
}
// org.apache.catalina.session.ManagerBase#add
@Override
public void add(Session session) {
// 取出 sessionId, 添加到 sessions 容器,统一管理
sessions.put(session.getIdInternal(), session);
int size = getActiveSessions();
// 刷新最大活跃数,使用双重锁优化更新该值
if( size > maxActive ) {
synchronized(maxActiveUpdateLock) {
if( size > maxActive ) {
maxActive = size;
}
}
}
}
// 查找session也是异常简单,只管从 ConcurrentHashMap 中查找即可
// org.apache.catalina.session.ManagerBase#findSession
@Override
public Session findSession(String id) throws IOException {
if (id == null) {
return null;
}
return sessions.get(id);
}
创建好session后,需要进行随时的维护:我们看下tomcat是如何刷新访问时间的?可能比预想的简单,其仅是更新一个访问时间字段,再无其他。
// org.apache.catalina.session.StandardSession#access
/**
- Update the accessed time information for this session. This method
- should be called by the context when a request comes in for a particular
- session, even if the application does not reference it.
*/
@Override
public void access() {
// 更新访问时间
this.thisAccessedTime = System.currentTimeMillis();
// 访问次数统计,默认不启用
if (ACTIVITY_CHECK) {
accessCount.incrementAndGet();
}
}
最后,还需要看下 HttpSession 是如何被包装返回的?
// org.apache.catalina.session.StandardSession#getSession
/**
- Return the
HttpSession
for which this object - is the facade.
*/
@Override
public HttpSession getSession() {
if (facade == null){
if (SecurityUtil.isPackageProtectionEnabled()){
final StandardSession fsession = this;
facade = AccessController.doPrivileged(
new PrivilegedAction(){
@Override
public StandardSessionFacade run(){
return new StandardSessionFacade(fsession);
}
});
} else {
// 直接使用 StandardSessionFacade 包装即可
facade = new StandardSessionFacade(this);
}
}
return (facade);
}
再最后,要说明的是,整个sessions的管理使用一个 ConcurrentHashMap 来存放全局会话信息,sessionId->session实例。
对于同一次http请求中,该session会被存储在当前的Request栈org.apache.catalina.connector.Request#session字段中,从而无需每次深入获取。每个请求进来后,会将session保存在当前的request信息中。
3. 过期session清理?
会话不可能不过期,不过期的也不叫会话了。
会话过期的触发时机主要有三个:1. 每次进行会话调用时,会主动有效性isValid()验证,此时如果发现过期可以主动清理: 2. 后台定时任务触发清理; 3. 启动或停止应用的时候清理;(这对于非内存式的存储会更有用些)
// case1. 请求时验证,如前面所述
// org.apache.catalina.connector.Request#doGetSession
protected Session doGetSession(boolean create) {
…
// Return the current session if it exists and is valid
if ((session != null) && !session.isValid()) {
session = null;
}
if (session != null) {
return (session);
}
…
}
// case2. 后台定时任务清理
// org.apache.catalina.session.ManagerBase#backgroundProcess
@Override
public void backgroundProcess() {
// 并非每次定时任务到达时都会进行清理,而是要根据其清理频率设置来运行
// 默认是 6
count = (count + 1) % processExpiresFrequency;
if (count == 0)
processExpires();
}
/**
- Invalidate all sessions that have expired.
*/
public void processExpires() {
long timeNow = System.currentTimeMillis();
// 找出所有的sessions, 转化为数组遍历
Session sessions[] = findSessions();
int expireHere = 0 ;
if(log.isDebugEnabled())
log.debug("Start expire sessions " + getName() + " at " + timeNow + " sessioncount " + sessions.length);
for (int i = 0; i < sessions.length; i++) {
// 事实上后台任务也是调用 isValid() 方法 进行过期任务清理的
if (sessions[i]!=null && !sessions[i].isValid()) {
expireHere++;
}
}
long timeEnd = System.currentTimeMillis();
if(log.isDebugEnabled())
log.debug("End expire sessions " + getName() + " processingTime " + (timeEnd - timeNow) + " expired sessions: " + expireHere);
processingTime += ( timeEnd - timeNow );
}
//case3. start/stop 时触发过期清理(生命周期事件)
// org.apache.catalina.session.StandardManager#startInternal
/**
- Start this component and implement the requirements
- of {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
- @exception LifecycleException if this component detects a fatal error
- that prevents this component from being used
*/
@Override
protected synchronized void startInternal() throws LifecycleException {
super.startInternal();
// Load unloaded sessions, if any
try {
// doLoad() 调用
load();
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString(“standardManager.managerLoad”), t);
}
setState(LifecycleState.STARTING);
}
/**
- Load any currently active sessions that were previously unloaded
- to the appropriate persistence mechanism, if any. If persistence is not
- supported, this method returns without doing anything.
- @exception ClassNotFoundException if a serialized class cannot be
- found during the reload
- @exception IOException if an input/output error occurs
*/
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));
}
Loader loader = null;
ClassLoader classLoader = null;
Log logger = null;
try (FileInputStream fis = new FileInputStream(file.getAbsolutePath());
BufferedInputStream bis = new BufferedInputStream(fis)) {
Context c = getContext();
loader = c.getLoader();
logger = c.getLogger();
if (loader != null) {
classLoader = loader.getClassLoader();
}
if (classLoader == null) {
classLoader = getClass().getClassLoader();
}
// Load the previously unloaded active sessions
synchronized (sessions) {
try (ObjectInputStream ois = new CustomObjectInputStream(bis, classLoader, logger,
getSessionAttributeValueClassNamePattern(),
getWarnOnSessionAttributeFilterFailure())) {
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.isValidInternal()) {
// If session is already invalid,
// expire session to prevent memory leak.
// 主动调用 expire
session.setValid(true);
session.expire();
}
sessionCounter++;
}
} finally {
// Delete the persistent storage file
if (file.exists()) {
file.delete();
}
}
}
} catch (FileNotFoundException e) {
if (log.isDebugEnabled()) {
log.debug(“No persisted data file found”);
}
return;
}
if (log.isDebugEnabled()) {
log.debug(“Finish: Loading persisted sessions”);
}
}
// stopInternal() 事件到达时清理 sessions
/**
- Save any currently active sessions in the appropriate persistence
- mechanism, if any. If persistence is not supported, this method
- returns without doing anything.
- @exception IOException if an input/output error occurs
*/
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));
}
// Keep a note of sessions that are expired
ArrayList list = new ArrayList<>();
try (FileOutputStream fos = new FileOutputStream(file.getAbsolutePath());
BufferedOutputStream bos = new BufferedOutputStream(fos);
ObjectOutputStream oos = new ObjectOutputStream(bos)) {
synchronized (sessions) {
if (log.isDebugEnabled()) {
log.debug(“Unloading " + sessions.size() + " sessions”);
}
// Write the number of active sessions, followed by the details
oos.writeObject(Integer.valueOf(sessions.size()));
for (Session s : sessions.values()) {
StandardSession session = (StandardSession) s;
list.add(session);
session.passivate();
session.writeObjectData(oos);
}
}
}
// Expire all the sessions we just wrote
// 将所有session失效,实际上应用即将关闭,失不失效的应该也无所谓了
if (log.isDebugEnabled()) {
log.debug(“Expiring " + list.size() + " persisted sessions”);
}
for (StandardSession session : list) {
try {
session.expire(false);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
} finally {
session.recycle();
}
}
if (log.isDebugEnabled()) {
log.debug(“Unloading complete”);
}
}
接下来我们看下具体如何清理过期的会话?实际应该就是一个remove的事。
// org.apache.catalina.session.StandardSession#isValid
/**
- Return the
isValid
flag for this session.
*/
@Override
public boolean isValid() {
if (!this.isValid) {
return false;
}
if (this.expiring) {
return true;
}
if (ACTIVITY_CHECK && accessCount.get() > 0) {
return true;
}
// 超过有效期,主动触发清理
if (maxInactiveInterval > 0) {
int timeIdle = (int) (getIdleTimeInternal() / 1000L);
if (timeIdle >= maxInactiveInterval) {
expire(true);
}
}
return this.isValid;
}
// org.apache.catalina.session.StandardSession#expire(boolean)
/**
- Perform the internal processing required to invalidate this session,
- without triggering an exception if the session has already expired.
- @param notify Should we notify listeners about the demise of
- this session?
*/
public void expire(boolean notify) {
// Check to see if session has already been invalidated.
// Do not check expiring at this point as expire should not return until
// isValid is false
if (!isValid)
return;
// 上锁保证线程安全
synchronized (this) {
// Check again, now we are inside the sync so this code only runs once
// Double check locking - isValid needs to be volatile
// The check of expiring is to ensure that an infinite loop is not
// entered as per bug 56339
if (expiring || !isValid)
return;
if (manager == null)
return;
// Mark this session as “being expired”
expiring = true;
// Notify interested application event listeners
// FIXME - Assumes we call listeners in reverse order
Context context = manager.getContext();
// The call to expire() may not have been triggered by the webapp.
// Make sure the webapp’s class loader is set when calling the
// listeners
if (notify) {
ClassLoader oldContextClassLoader = null;
try {
oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null);
Object listeners[] = context.getApplicationLifecycleListeners();
if (listeners != null && listeners.length > 0) {
HttpSessionEvent event =
new HttpSessionEvent(getSession());
for (int i = 0; i < listeners.length; i++) {
int j = (listeners.length - 1) - i;
if (!(listeners[j] instanceof HttpSessionListener))
continue;
HttpSessionListener listener =
(HttpSessionListener) listeners[j];
try {
context.fireContainerEvent(“beforeSessionDestroyed”,
listener);
listener.sessionDestroyed(event);
context.fireContainerEvent(“afterSessionDestroyed”,
listener);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
try {
context.fireContainerEvent(
“afterSessionDestroyed”, listener);
} catch (Exception e) {
// Ignore
}
manager.getContext().getLogger().error
(sm.getString(“standardSession.sessionEvent”), t);
}
}
}
} finally {
context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader);
}
}
if (ACTIVITY_CHECK) {
accessCount.set(0);
}
// Remove this session from our manager’s active sessions
// 从ManagerBase 中删除
manager.remove(this, true);
// Notify interested session event listeners
if (notify) {
fireSessionEvent(Session.SESSION_DESTROYED_EVENT, null);
}
// Call the logout method
if (principal instanceof TomcatPrincipal) {
TomcatPrincipal gp = (TomcatPrincipal) principal;
try {
gp.logout();
} catch (Exception e) {
manager.getContext().getLogger().error(
sm.getString(“standardSession.logoutfail”),
e);
}
}
// We have completed expire of this session
setValid(false);
expiring = false;
// Unbind any objects associated with this session
String keys[] = keys();
ClassLoader oldContextClassLoader = null;
try {
oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null);
for (int i = 0; i < keys.length; i++) {
removeAttributeInternal(keys[i], notify);
}
面试题总结
其它面试题(springboot、mybatis、并发、java中高级面试总结等)
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门,即可获取!
.logoutfail"),
e);
}
}
// We have completed expire of this session
setValid(false);
expiring = false;
// Unbind any objects associated with this session
String keys[] = keys();
ClassLoader oldContextClassLoader = null;
try {
oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null);
for (int i = 0; i < keys.length; i++) {
removeAttributeInternal(keys[i], notify);
}
面试题总结
其它面试题(springboot、mybatis、并发、java中高级面试总结等)
[外链图片转存中…(img-ZPnodH2g-1714769839001)]
[外链图片转存中…(img-PhE9j8b6-1714769839001)]
[外链图片转存中…(img-NxwC4JIA-1714769839001)]
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门,即可获取!