Shiro quartz2.* 冲突解决



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的定时任务再也没有执行过。

源码探析

  1. 为什么首个用户登陆时会触发任务?
  2. 为什么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的运行机制更加了解,问题是最终解决了,然而中间的曲折也不容小视,网友们共享的代码也不一定是正确的,是否有效可用还是需要我们自己加以验证,这也更加要求自己要深入顶层,研究深层的运行逻辑,而不是游离于表面,浮于使用框架和工具而不知其所以。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值