http://www.hillfly.com/2017/178.html
项目里需要对过期的shiro session进行清理,shiro自带了shiro-quartz模块可以胜任这项工作
接入shiro-quartz
具体配置如下:
<!-- pom.xml -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-cas</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-quartz</artifactId>
<version>${shiro-version}</version>
</dependency>
<!-- spring-shiro.xml -->
<!-- 会话管理器 -->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="globalSessionTimeout" value="604800000"/>
<property name="deleteInvalidSessions" value="true"/>
<property name="sessionValidationSchedulerEnabled" value="true"/>
<property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
<property name="sessionDAO" ref="sessionDAO"/>
</bean>
<!-- 会话验证调度器 -->
<bean id="sessionValidationScheduler" class="org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler">
<property name="sessionValidationInterval" value="1800000"/>
<property name="sessionManager" ref="sessionManager"/>
</bean>
globalSessionTimeout:全局session过期时间
sessionValidationInterval:session校验间隔时间
deleteInvalidSessions:是否删除非法session(已退出、已过期)
sessionValidationSchedulerEnabled:是否定时校验session
到此,这就已经可以基本满足我们的需求了。
搭配Spring4. + Quartz2.
很快别的问题就出现了。后面我们需要在系统中新增定时任务,显然quartz是首选,然而我们发现shiro-quartz引入的quartz版本居然是1.6.1。然而quartz最新稳定版都到了2.3.*,我们总不可能为了接入低版本quartz而更换我们的底层Spring版本吧,于是,更换shiro-quartz模块里的quartz成为我们的唯一选择。
<!-- pom.xml -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-quartz</artifactId>
<version>${shiro-quartz-version}</version>
<exclusions>
<exclusion>
<groupId>org.opensymphony.quartz</groupId>
<artifactId>quartz</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.1</version>
</dependency>
然而并没有预期的那样成功,项目启动报错:Java.lang.InstantiationError: org.quartz.SimpleTrigger
调试发现在新版quartz中的SimppleTrigger是个接口类,无法实例化。进一步调试可以发现shiro-quartz中的QuartzSessionValidationScheduler和QuartzSessionValidationJob是针对quartz1.6.1的实现与调用,显然我们这里需要对这两个类进行自定义开发了。当然造轮子前先看看网友们有没有写好的实现,这个答案显然是肯定的,下面就是网友们的答案,google了几页都是这个:
/**
* 基于Quartz 2.* 版本的实现
*
*/
public class QuartzSessionValidationJob implements Job {
/**
* Key used to store the session manager in the job data map for this job.
*/
public static final String SESSION_MANAGER_KEY = "sessionManager";
/*--------------------------------------------
| I N S T A N C E V A R I A B L E S |
============================================*/
private static final Logger log = LoggerFactory.getLogger(QuartzSessionValidationJob.class);
/*--------------------------------------------
| C O N S T R U C T O R S |
============================================*/
/*--------------------------------------------
| A C C E S S O R S / M O D I F I E R S |
============================================*/
/*--------------------------------------------
| M E T H O D S |
============================================*/
/**
* Called when the job is executed by quartz. This method delegates to the <tt>validateSessions()</tt> method on the
* associated session manager.
*
* @param context
* the Quartz job execution context for this execution.
*/
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap jobDataMap = context.getMergedJobDataMap();
ValidatingSessionManager sessionManager = (ValidatingSessionManager) jobDataMap.get(SESSION_MANAGER_KEY);
if (log.isDebugEnabled()) {
log.debug("Executing session validation Quartz job...");
}
sessionManager.validateSessions();
if (log.isDebugEnabled()) {
log.debug("Session validation Quartz job complete.");
}
}
}
/**
* 基于Quartz 2.* 版本的实现
*/
public class QuartzSessionValidationScheduler implements SessionValidationScheduler {
public static final long DEFAULT_SESSION_VALIDATION_INTERVAL = DefaultSessionManager.DEFAULT_SESSION_VALIDATION_INTERVAL;
private static final String JOB_NAME = "SessionValidationJob";
private static final Logger log = LoggerFactory.getLogger(QuartzSessionValidationScheduler.class);
private static final String SESSION_MANAGER_KEY = QuartzSessionValidationJob.SESSION_MANAGER_KEY;
private Scheduler scheduler;
private boolean schedulerImplicitlyCreated = false;
private boolean enabled = false;
private ValidatingSessionManager sessionManager;
private long sessionValidationInterval = DEFAULT_SESSION_VALIDATION_INTERVAL;
public QuartzSessionValidationScheduler() {
}
public QuartzSessionValidationScheduler(ValidatingSessionManager sessionManager) {
this.sessionManager = sessionManager;
}
protected Scheduler getScheduler() throws SchedulerException {
if (this.scheduler == null) {
this.scheduler = StdSchedulerFactory.getDefaultScheduler();
this.schedulerImplicitlyCreated = true;
}
return this.scheduler;
}
public void setScheduler(Scheduler scheduler) {
this.scheduler = scheduler;
}
public void setSessionManager(ValidatingSessionManager sessionManager) {
this.sessionManager = sessionManager;
}
public boolean isEnabled() {
return this.enabled;
}
public void setSessionValidationInterval(long sessionValidationInterval) {
this.sessionValidationInterval = sessionValidationInterval;
}
public void enableSessionValidation() {
if (log.isDebugEnabled()) {
log.debug("Scheduling session validation job using Quartz with session validation interval of ["
+ this.sessionValidationInterval + "]ms...");
}
try {
SimpleTrigger trigger = TriggerBuilder
.newTrigger()
.startNow()
.withIdentity(JOB_NAME, Scheduler.DEFAULT_GROUP)
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInMilliseconds(sessionValidationInterval))
.build();
JobDetail detail = JobBuilder
.newJob(QuartzSessionValidationJob.class)
.withIdentity(JOB_NAME, Scheduler.DEFAULT_GROUP)
.build();
detail.getJobDataMap().put(SESSION_MANAGER_KEY, this.sessionManager);
Scheduler scheduler = getScheduler();
scheduler.scheduleJob(detail, trigger);
if (this.schedulerImplicitlyCreated) {
scheduler.start();
if (log.isDebugEnabled()) {
log.debug("Successfully started implicitly created Quartz Scheduler instance.");
}
}
this.enabled = true;
if (log.isDebugEnabled())
log.debug("Session validation job successfully scheduled with Quartz.");
} catch (SchedulerException e) {
if (log.isErrorEnabled())
log.error("Error starting the Quartz session validation job. Session validation may not occur.", e);
}
}
public void disableSessionValidation() {
if (log.isDebugEnabled()) {
log.debug("Stopping Quartz session validation job...");
}
Scheduler scheduler;
try {
scheduler = getScheduler();
if (scheduler == null) {
if (log.isWarnEnabled()) {
log.warn("getScheduler() method returned a null Quartz scheduler, which is unexpected. Please check your configuration and/or implementation. Returning quietly since there is no validation job to remove (scheduler does not exist).");
}
return;
}
} catch (SchedulerException e) {
if (log.isWarnEnabled()) {
log.warn("Unable to acquire Quartz Scheduler. Ignoring and returning (already stopped?)", e);
}
return;
}
try {
scheduler.unscheduleJob(new TriggerKey("SessionValidationJob", "DEFAULT"));
if (log.isDebugEnabled())
log.debug("Quartz session validation job stopped successfully.");
} catch (SchedulerException e) {
if (log.isDebugEnabled()) {
log.debug("Could not cleanly remove SessionValidationJob from Quartz scheduler. Ignoring and stopping.", e);
}
}
this.enabled = false;
if (this.schedulerImplicitlyCreated)
try {
scheduler.shutdown();
} catch (SchedulerException e) {
if (log.isWarnEnabled())
log.warn("Unable to cleanly shutdown implicitly created Quartz Scheduler instance.", e);
} finally {
setScheduler(null);
this.schedulerImplicitlyCreated = false;
}
}
}
其实就是使用了quartz2.*版本中的API来创建scheduler、Trigger等实例,具体流程还是与旧版本的实现一致。需要注意的时,校验任务是在enableSessionValidation()这个方法执行时才会注册到quartz中,也就是说如果enableSessionValidation()没有被调用过,也就不会存在校验定时任务。接下来咱们还需要在xml里进行配置:
<!-- spring-shiro.xml -->
<!-- 会话验证调度器 -->
<bean id="sessionValidationScheduler" class="com.outsourceCC.shiro.QuartzSessionValidationScheduler">
<!-- 设置调度时间间隔,单位毫秒,默认就是1小时(1800000) -->
<property name="sessionValidationInterval" value="900000"/>
<!-- 会话验证调度器,sessionManager默认就是使用 -->
<property name="sessionManager" ref="sessionManager"/>
</bean>
<!-- 会话管理器 -->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="globalSessionTimeout" value="1800000"/>
<!-- 会话过期时是否删除过期的会话 ,默认是开启的-->
<property name="deleteInvalidSessions" value="true"/>
<!-- 是否开启会话验证器,默认是开启的 -->
<property name="sessionValidationSchedulerEnabled" value="true"/>
<!-- 设置会话验证调度器,默认就是使用 -->
<property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
<property name="sessionDAO" ref="redisSessionDAO"/>
</bean>
这里的sessionValidationScheduler换成了我们上面自定义的类。此时再启动系统,没有报错,一切都很顺利。
由于我这里设置的调度间隔时长为15分钟,即900000毫秒,如果没有拦截日志,那么一段时候后quartz执行时会有如下类似的记录:
2017-05-28 17:19:02,684- DEBUG QuartzScheduler_Worker-1 com.outsourceCC.shiro.QuartzSessionValidationJob - Executing session validation Quartz job...
2017-05-28 17:19:02,685- INFO QuartzScheduler_Worker-1 org.apache.shiro.session.mgt.AbstractValidatingSessionManager - Validating all active sessions...
2017-05-28 17:19:07,760- INFO QuartzScheduler_Worker-1 org.apache.shiro.session.mgt.AbstractValidatingSessionManager - Finished session validation. No sessions were stopped.
2017-05-28 17:19:07,761- DEBUG QuartzScheduler_Worker-1 com.outsourceCC.shiro.QuartzSessionValidationJob - Session validation Quartz job complete.
然而喝杯茶后发现。。。没有任何任务执行日志。在QuartzSessionValidationJob中打了断点,最终确认:系统启动后,除了首个用户登陆时会触发任务执行,之后shiro-quartz的定时任务再也没有执行过。
源码探析
- 为什么首个用户登陆时会触发任务?
- 为什么shiro-quartz的定时任务没有定时执行?
网络上千篇一律都是这么写的,难道大家都没有问题?google一阵子无果后,只能自己一点点剥洋葱找原因了。
shiro探析
首先考虑第一个问题,由于咱们自定义的scheduler是被SessionManager引用的,我们就反编译DefaultWebSessionManager这个类进行查找线索。最终跟到AbstractNativeSessionManager这个类时终于找到了有用的线索。
//AbstractNativeSessionManager.class
public static final long DEFAULT_SESSION_VALIDATION_INTERVAL = 3600000L;
protected boolean sessionValidationSchedulerEnabled = true;
protected SessionValidationScheduler sessionValidationScheduler;
protected long sessionValidationInterval = 3600000L;
显然咱们在xml中配置的定时任务信息是被注入到这个类的属性中了,可以看到shiro默认的检验间隔时长为3600000L即一小时,这里的sessionValidationScheduler就必然是我们前面注入的自定义的Scheduler了,我们看看它在哪被调用了。
protected final Session doGetSession(SessionKey key) throws InvalidSessionException {
this.enableSessionValidationIfNecessary();
log.trace("Attempting to retrieve session with key {}", key);
Session s = this.retrieveSession(key);
if(s != null) {
this.validate(s, key);
}
return s;
}
protected Session createSession(SessionContext context) throws AuthorizationException {
this.enableSessionValidationIfNecessary();
return this.doCreateSession(context);
}
private void enableSessionValidationIfNecessary() {
SessionValidationScheduler scheduler = this.getSessionValidationScheduler();
if(this.isSessionValidationSchedulerEnabled() && (scheduler == null || !scheduler.isEnabled())) {
this.enableSessionValidation();
}
}
至此,第一个问题的答案已然出现。Shiro会在创建Session、获取Session时调用enableSessionValidationIfNecessary方法,如果存在scheduler并且没有启动,则调用我们上面提到的enableSessionValidation方法去创建定时任务,反之不作处理。这就解释了为什么系统启动后的首个用户登陆会触发定时任务了,因为用户登录时会创建Session,所以会触发createSession(),进而触发enableSessionValidationIfNecessary()和enableSessionValidation()。
quartz探析
问题一弄清楚后,对于问题二就更疑惑了。在问题一的探析中我们已经确认在首个用户登陆后会触发enableSessionValidation()方法,进而创建校验的定时任务,那么为什么quartz没有按我们预想的那样每隔一段时间就执行job呢?还得从代码里找答案了。
//QuartzSessionValidationScheduler - enableSessionValidation()
SimpleTrigger trigger = TriggerBuilder
.newTrigger()
.startNow()
.withIdentity(JOB_NAME, Scheduler.DEFAULT_GROUP)
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInMilliseconds(sessionValidationInterval))
.build();
上面就是咱们自定义类中创建任务触发器的核心代码。startNow()
好理解,就是立即执行,这也印证了为什么首个用户登陆时会立即触发一次定时任务;withIntervalInMilliseconds(sessionValidationInterval)
间隔执行时间为sessionValidationInterval毫秒,调试过程看值的确也是我们注入的900000。
看起来API的调用并没有问题,可是为什么偏偏首次执行完毕后就再也不触发了呢??没办法,只能反编译看看SimpleTrigger和TriggerBuilder代码了。
//SimpleScheduleBuilder.class
public MutableTrigger build() {
SimpleTriggerImpl st = new SimpleTriggerImpl();
st.setRepeatInterval(this.interval);
st.setRepeatCount(this.repeatCount);
st.setMisfireInstruction(this.misfireInstruction);
return st;
}
跟代码到build()方法,这里初始化并返回了SimpleTriggerImpl实例,关注到这初始化的几个参数,interval、repeatCount、misfireInstruction。显然interval是调度间隔时间,而misfireInstruction是quartz异常恢复后的设置项,repeatCount从字面来看很容易理解是重复执行次数。
在这里打断点进入调试,发现interval的值为我们注入的900000,repeatCount为0,misfireInstruction为0。
慢着,repeatCount=0是不是不对经啊亲,重复执行次数为0,根据我的理解不就是执行一次后再也不执行了吗!?这不就是问题二的现象了吗!?我们继续跟进去看repeatCount等参数的默认设置:
//SimpleScheduleBuilder.class
private long interval = 0L;
private int repeatCount = 0;
private int misfireInstruction = 0;
???repeatCount默认值就是0,那么它的具体含义是什么?我们去官方看看这个参数的注释:
getRepeatCount()
Specify a the number of time the trigger will repeat - total number of firings will be this number + 1.
!!注意最后一句:total number of firings will be this number + 1
中文字义就是指任务总触发次数就是repeatCount+1!!!然后这个repeatCount默认值是0,也就意味着对应的job整个生命周期内只会执行一次!在咱们系统中就是首个用户登陆触发的那一次!这也就解释了咱们的问题二了!
如何解决?
考虑到session校验这种任务并没有具体的次数限制,应该一直执行下去,从官方文档里了解到,当repeatCount值为-1时,则代表任务会无限执行下去,遂我们的代码修改如下:
SimpleTrigger trigger = TriggerBuilder
.newTrigger()
.startNow()
.withIdentity(JOB_NAME, Scheduler.DEFAULT_GROUP)
.withSchedule(SimpleScheduleBuilder
.simpleSchedule()
.withIntervalInMilliseconds(sessionValidationInterval)
.repeatForever()) //重点在于repeatForever()
.build();
总结
此次探析让我们对Shiro和quartz的运行机制更加了解,问题是最终解决了,然而中间的曲折也不容小视,网友们共享的代码也不一定是正确的,是否有效可用还是需要我们自己加以验证,这也更加要求自己要深入顶层,研究深层的运行逻辑,而不是游离于表面,浮于使用框架和工具而不知其所以。