session
最近在工作共使用session,却没有做session的一致性踩了一个坑,导致线上环境只能单节点跑,今天来复盘一下。
session与cookie得差异:
session存于服务器,cookie存于客户端,session相比cookie更加安全,容量更加大。当然session消耗服务器资源,cookie存储于客户端与服务器压力更小。session一次会话有效,cookie具体时间可以由你来定
tomcat中的session
浏览器向服务器发送请求时会带有一个sessionId,tomcat通过这个sessionId进行判断此浏览器以获得存于服务器中的session。
tomcat中实现是javax.servle.http的类或接口有Request.class,ApplicationHttpRequest.Class....,
在servlet中获取session,通常是调用request的getSession方法。这个方法需要传入一个boolean参数,这个参数就是实现刚才说的,当jsessionid为空或从session池中获取不到相应的session对象时,选择创建一个新的session还是不创建。
Request.class的核心代码逻辑;
protected Session doGetSession(boolean create) {
……
// 先获取所在context的manager对象
Manager manager = null;
if (context != null)
manager = context.getManager();
if (manager == null)
return (null); // Sessions are not supported
//这个requestedSessionId就是从Http request中解析出来的
if (requestedSessionId != null) {
try {
//manager管理的session池中找相应的session对象
session = manager.findSession(requestedSessionId);
} catch (IOException e) {
session = null;
}
//判断session是否为空及是否过期超时
if ((session != null) && !session.isValid())
session = null;
if (session != null) {
//session对象有效,记录此次访问时间
session.access();
return (session);
}
}
// 如果参数是false,则不创建新session对象了,直接退出了
if (!create)
return (null);
if ((context != null) && (response != null) &&
context.getCookies() &&
response.getResponse().isCommitted()) {
throw new IllegalStateException
(sm.getString("coyoteRequest.sessionCreateCommitted"));
}
// 开始创建新session对象
if (connector.getEmptySessionPath()
&& isRequestedSessionIdFromCookie()) {
session = manager.createSession(getRequestedSessionId());
} else {
session = manager.createSession(null);
}
// 将新session的jsessionid写入cookie,传给browser
if ((session != null) && (getContext() != null)
&& getContext().getCookies()) {
Cookie cookie = new Cookie(Globals.SESSION_COOKIE_NAME,
session.getIdInternal());
configureSessionCookie(cookie);
response.addCookieInternal(cookie);
}
//记录session最新访问时间
if (session != null) {
session.access();
return (session);
} else {
return (null);
}
}
以下是,ApplicationHttpRequest的getSession源码
/**
* 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) {
if (crossContext) {
// 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
// 如果存在session则直接返回
if (session != null && session.isValid()) {
return (session.getSession());
}
HttpSession other = super.getSession(false);
if (create && (other == null)) {
// First create a session in the first context: the problem is
// that the top level request is the only one which can
// create the cookie safely
other = super.getSession(true);
}
if (other != null) {
Session localSession = null;
try {
localSession =
context.getManager().findSession(other.getId());
if (localSession != null && !localSession.isValid()) {
localSession = null;
}
} catch (IOException e) {
// Ignore
}
if (localSession == null && create) {
localSession =
context.getManager().createSession(other.getId());
}
if (localSession != null) {
// 更新访问时间
localSession.access();
session = localSession;
return session.getSession();
}
}
return null;
} else {
return super.getSession(create);
}
}
StandardSession
从ApplicationHttpRequest中可以追溯到StandardSession,下面是StandardSession的部分重要源码,我摘抄下来方便大家理解,你可以看到他封装成了一个StandardSessionFacade,提供方法给外部使用,而且里面的attributes是一个concurrentHashMap。
/**
* The collection of user data attributes associated with this Session.
*/
protected ConcurrentMap<String, Object> attributes = new ConcurrentHashMap<>();
/**
* Set the session identifier for this session.
* 给这个session建立一个唯一的标志
* @param id The new session identifier
*/
@Override
public void setId(String id) {
setId(id, true);
}
/**
* Return the <code>HttpSession</code> 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<StandardSessionFacade>(){
@Override
public StandardSessionFacade run(){
return new StandardSessionFacade(fsession);
}
});
} else {
facade = new StandardSessionFacade(this);
}
}
return (facade);
}
sessionId
setId(String id)
StandardSession 中的 setId(String id)) 方法时提供给制造session者创造唯一sessionId标志,用过调用链,可以追溯到
ManagerBase类,一下为ManagerBase的部分源码
@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 session = createEmptySession();
// Initialize the properties of the new session and return it
session.setNew(true);
session.setValid(true);
session.setCreationTime(System.currentTimeMillis());
session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);
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);
}
查看其generateSessionId();下面使其生成的sessionId的源码,通过sessionIdGenerator.generateSessionId();生成sessionId,sessionIdGenerator是一个接口
/**
* The set of currently active Sessions for this Manager, keyed by
* session identifier.
*/
protected Map<String, Session> sessions = new ConcurrentHashMap<>();
/**
* Generate and return a new session identifier.
* @return a new session id
*/
protected String generateSessionId() {
String result = null;
do {
if (result != null) {
// Not thread-safe but if one of multiple increments is lost
// that is not a big deal since the fact that there was any
// duplicate is a much bigger issue.
duplicates++;
}
result = sessionIdGenerator.generateSessionId();
} while (sessions.containsKey(result));
return result;
}
session过期
在核心源码中doGetSession中我们可以看到access()方法,其中会更新我们的session登录时间。StandardSession.Class中我们能看到 isValid()方法,用于判断此session是否过期
/**
* Return the <code>isValid</code> flag for this session.
*/
@Override
public boolean isValid() {
if (!this.isValid) {
return false;
}
if (this.expiring) {
return true;
}
// accessCount,在每次access的时候都是自增,endAccess时自减
if (ACTIVITY_CHECK && accessCount.get() > 0) {
return true;
}
//这就是判断距离上次访问是否超时的过程
if (maxInactiveInterval > 0) {
// 判断上一次访问时间
int timeIdle = (int) (getIdleTimeInternal() / 1000L);
if (timeIdle >= maxInactiveInterval) {
expire(true);
}
}
return this.isValid;
}
/**
* Return the idle time from last client access time without invalidation check
* @see #getIdleTime()
*/
@Override
public long getIdleTimeInternal() {
long timeNow = System.currentTimeMillis();
long timeIdle;
if (LAST_ACCESS_AT_START) {
timeIdle = timeNow - lastAccessedTime;
} else {
timeIdle = timeNow - thisAccessedTime;
}
return timeIdle;
}
上面源码中可以看到超过了时间后太会调用expire方法,粘贴重要源码
public void expire(boolean notify) {
synchronized (this) {
......
//设立标志位
setValid(false);
//计算一些统计值,例如此manager下所有session平均存活时间等
long timeNow = System.currentTimeMillis();
int timeAlive = (int) ((timeNow - creationTime)/1000);
synchronized (manager) {
if (timeAlive > manager.getSessionMaxAliveTime()) {
manager.setSessionMaxAliveTime(timeAlive);
}
int numExpired = manager.getExpiredSessions();
numExpired++;
manager.setExpiredSessions(numExpired);
int average = manager.getSessionAverageAliveTime();
average = ((average * (numExpired-1)) + timeAlive)/numExpired;
manager.setSessionAverageAliveTime(average);
}
// 将此session从manager对象的session池中删除
manager.remove(this);
......
}
}