如何每隔一小时以固定的时间间隔删除会话数据?

时间:2013-04-20 20:22:58

标签: java spring jsp session quartz-scheduler

我正在生成随机令牌,原因如此question中提到的放在java.util.List中,而List保留在会话范围内。

在进行一些Google搜索后,我决定在会话中List每小时删除所有元素(令牌)。

我可以考虑使用Quartz API,但这样做似乎不可能操纵用户的会话。我在Spring中尝试使用Quartz API(1.8.6,2.x与我正在使用的Spring 3.2不兼容)可以在下面看到。

package quartz;

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;

public final class RemoveTokens extends QuartzJobBean
{    
    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException
    {
        System.out.println("QuartzJobBean executed.");
    }
}

它在application-context.xml文件中配置如下。

<bean name="removeTokens" class="org.springframework.scheduling.quartz.JobDetailBean">
    <property name="jobClass" value="quartz.RemoveTokens" />
    <property name="jobDataAsMap">
        <map>
            <entry key="timeout" value="5" />
        </map>
    </property>
</bean>

<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
      <property name="jobDetail" ref="removeTokens"/>
      <property name="startDelay" value="10000"/>
      <property name="repeatInterval" value="3600000"/>
</bean>

<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
  <property name="triggers">
      <list>
          <ref bean="simpleTrigger" />
      </list>
  </property>
</bean>

RemoveTokens类中的重写方法每小时执行一次,初始间隔为10秒,如XML中所配置,但是不可能执行某些类的某些方法来删除{{{}中可用的标记。 1}}存储在用户的会话中。有可能吗?

以定义的时间间隔(每小时)删除存储在会话范围中的此List的公平方法是什么?如果通过使用这个Quartz API来实现它会好得多。


修改

根据下面的答案,我尝试了以下但不幸的是,它没有什么区别。

List文件中,

application-context.xml

这需要以下额外的命名空间,

<task:annotation-driven executor="taskExecutor" scheduler="taskScheduler"/>
<task:executor id="taskExecutor" pool-size="5"/>
<task:scheduler id="taskScheduler" pool-size="10"/>

以下bean已注册为会话范围bean。

xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="http://www.springframework.org/schema/task
                    http://www.springframework.org/schema/task/spring-task-3.2.xsd"

它实现的界面,

package token;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import org.apache.commons.lang.StringUtils;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

@Service
//@Scope("session")
public final class SessionToken implements SessionTokenService
{
    private List<String> tokens;

    private static String nextToken()
    {
        long seed = System.currentTimeMillis(); 
        Random r = new Random();
        r.setSeed(seed);
        return Long.toString(seed) + Long.toString(Math.abs(r.nextLong()));
    }

    @Override
    public boolean isTokenValid(String token)
    {        
        return tokens==null||tokens.isEmpty()?false:tokens.contains(token);
    }

    @Override
    public String getLatestToken()
    {
        if(tokens==null)
        {
            tokens=new ArrayList<String>(0);
            tokens.add(nextToken());            
        }
        else
        {
            tokens.add(nextToken());
        }

        return tokens==null||tokens.isEmpty()?"":tokens.get(tokens.size()-1);
    }

    @Override
    public boolean unsetToken(String token)
    {                
        return !StringUtils.isNotBlank(token)||tokens==null||tokens.isEmpty()?false:tokens.remove(token);
    }

    @Override
    public void unsetAllTokens()
    {
        if(tokens!=null&&!tokens.isEmpty())
        {
            tokens.clear();
        }
    }
}

此bean在package token; import java.io.Serializable; public interface SessionTokenService extends Serializable { public boolean isTokenValid(String token); public String getLatestToken(); public boolean unsetToken(String token); public void unsetAllTokens(); } 文件中配置如下。

application-context.xml

现在,我正在以下课程中注入此服务。

<bean id="sessionTokenCleanerService" class="token.SessionToken" scope="session">
    <aop:scoped-proxy proxy-target-class="false"/>
</bean>

package token; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @Service public final class PreventDuplicateSubmission { @Autowired private final SessionTokenService sessionTokenService=null; @Scheduled(fixedDelay=3600000) public void clearTokens() { System.out.println("Scheduled method called."); sessionTokenService.unsetAllTokens(); } } 文件中,

application-context.xml

以上两个bean都使用<bean id="preventDuplicateSubmissionService" class="token.PreventDuplicateSubmission"/> 进行注释,它们应该是@Service文件中context:component-scan的一部分(或其名称)。

在前面的代码片段中使用dispatacher-servelt.xml注释注释的方法会以给定的速率定期调用,但会导致引发以下明显的异常。

@Scheduled

要清除存储在用户会话中的数据,应该以定义的时间间隔(每个用户的会话)定期调用执行此任务的方法,而这种尝试不是这种情况。这是什么方式?问题可能只是:如何从每个用户的会话中触发一个固定的时间间隔? Spring或Servlet API是否支持完成此任务?

7 个答案:

答案 0 :(得分:9)

评论

恭喜使用令牌来阻止重新提交(“引入同步令牌”重构从核心J2EE模式一书)。 :^)

Quartz对于复杂或精确的调度很有价值。但是你的要求不需要Quartz。学习CDI,java.util.Timer,ScheduledExecutor和/或EJB计时器可能更有用。

如果使用调度程序,正如您所提到的,最好让所有用户共享一个单例调度程序,而不是使用调度程序和调度程序。每个用户会话的线程实例。

如果您存储对HttpSession的引用或将其作为参数传递,请务必小心。存储引用可防止会话完成时的垃圾收集 - 导致(大?)内存泄漏。尝试使用HttpSessionListener&amp; amp;其他技巧,可能无法正常工作很乱。将HttpSession作为参数传递给方法会使它们人为地依赖于Servlet容器而难以进行单元测试,因为您必须模拟复杂的HttpSession对象。将所有会话数据包装在单个对象UserSessionState中更为清晰 - 在会话中存储对 this 的引用。对象实例变量。这也称为上下文对象模式 - 即将所有作用域数据存储在一个/少数POJO上下文对象中,独立于HTTP协议类。

答案

我建议您解决两个问题:

  1. 使用java.util.Timer单例实例

    您可以将java.util.Timer(在Java SE 1.3中引入)替换为ScheduledExecutor(在Java SE 5中引入),以获得几乎相同的解决方案。

    这适用于JVM。它不需要jar设置也不需要配置。 您调用timerTask.schedule,传入TimerTask个实例。到期时,调用timerTask.run。您可以通过释放已用内存的timerTask.canceltimerTask.purge删除计划任务。

    您对TimerTask进行编码,包括它的构造函数,然后创建一个新实例并将其传递给schedule方法,这意味着您可以在TimerTask中存储所需的任何数据或对象引用;您可以保留对它的引用并在以后随时调用setter方法。

    我建议您创建两个全局单例:Timer实例和TimerTask实例。在您的自定义TimerTask实例中,保留所有用户会话的列表(以POJO形式,如UserSessionState或Spring / CDI bean,而不是HttpSession的形式)。向此类添加两个方法:addSessionObjectremoveSessionObject,参数为UserSessionState或类似。在TimerTask.run方法中,遍历UserSessionState集并清除数据。

    创建自定义HttpSessionListener - 从sessionCreated中,将新的UserSessionState实例放入会话并调用TimerTask.addUserSession;来自sessionDestroyed,调用TimerTask.removeUserSession。

    调用全局范围的单例timer.schedule来安排TimerTask实例清除会话范围的引用内容。

  2. 使用带有上限大小的令牌列表(不清除)

    不要根据经过的时间清理令牌。而是限制列表大小(例如25个令牌)并存储最近生成的令牌。

    这可能是最简单的解决方案。向列表中添加元素时,请检查是否已超出最大大小,如果是这样,请从列表中最早的索引插入并插入:

    if (++putIndex > maxSize) {
        putIndex = 0;
    }
    list.put(putIndex, newElement);
    

    这里不需要调度程序,也不需要形成&amp;维护一组所有用户会话。

答案 1 :(得分:3)

我的想法会是这样的:

简短版:

  1. 将任务间隔更改为1分钟;
  2. unsetAllTokensIfNeeded方法添加到SessionTokenService,每分钟都会调用一次,但如果确实需要,则只清理令牌列表(在这种情况下,决定是根据时间完成的)。
  3. 详细版本:

    您的计划任务将每分钟调用unsetAllTokensIfNeeded实施的SessionToken方法。方法实现将检查上一次令牌列表是否干净,并在一小时前调用unsetAllTokens

    但是为了在每个作用域SessionTokenService的会话中调用它,你需要现有SessionTokenService的列表,这可以通过在创建时将它注册到将清理它的服务来实现。 (这里你应该使用WeakHashMap来避免硬引用,这样可以避免垃圾收集器收集对象。)

    实施将是这样的:

    会话代币:

    接口:

    public interface SessionTokenService extends Serializable {
        public boolean isTokenValid(String token);
        public String getLatestToken();
        public boolean unsetToken(String token);
        public void unsetAllTokens();
        public boolean unsetAllTokensIfNeeded();
    }
    

    实现:

    @Service
    @Scope("session")
    public final class SessionToken implements SessionTokenService, DisposableBean, InitializingBean {
    
        private List<String> tokens;
        private Date lastCleanup = new Date();
    
    // EDIT {{{
    
        @Autowired
        private SessionTokenFlusherService flusherService;
    
        public void afterPropertiesSet() {
            flusherService.register(this);
        }
    
        public void destroy() {
            flusherService.unregister(this);
        }
    
    // }}}
    
        private static String nextToken() {
            long seed = System.currentTimeMillis(); 
            Random r = new Random();
            r.setSeed(seed);
            return Long.toString(seed) + Long.toString(Math.abs(r.nextLong()));
        }
    
        @Override
        public boolean isTokenValid(String token) {        
            return tokens == null || tokens.isEmpty() ? false : tokens.contains(token);
        }
    
        @Override
        public String getLatestToken() {
            if(tokens==null) {
                tokens=new ArrayList<String>(0);
            }
            tokens.add(nextToken());            
    
            return tokens.isEmpty() ? "" : tokens.get(tokens.size()-1);
        }
    
        @Override
        public boolean unsetToken(String token) {                
            return !StringUtils.isNotBlank(token) || tokens==null || tokens.isEmpty() ? false : tokens.remove(token);
        }
    
        @Override
        public void unsetAllTokens() {
            if(tokens!=null&&!tokens.isEmpty()) {
                tokens.clear();
                lastCleanup = new Date();
            }
        }
    
        @Override
        public void unsetAllTokensIfNeeded() {
            if (lastCleanup.getTime() < new Date().getTime() - 3600000) {
               unsetAllTokens();
            }
        }
    
    }
    

    Session Token Flusher:

    接口:

    public interface SessionTokenFlusherService {
        public void register(SessionToken sessionToken);
        public void unregister(SessionToken sessionToken);
    }
    

    实现:

    @Service
    public class DefaultSessionTokenFlusherService implements SessionTokenFlusherService {
    
        private Map<SessionToken,Object> sessionTokens = new WeakHashMap<SessionToken,Object>();
    
        public void register(SessionToken sessionToken) {
            sessionToken.put(sessionToken, new Object());
        }
    
        public void unregister(SessionToken sessionToken) {
            sessionToken.remove(sessionToken);
        }
    
        @Scheduled(fixedDelay=60000) // each minute
        public void execute() {
            for (Entry<SessionToken, Object> e : sessionToken.entrySet()) {
                e.getKey().unsetAllTokensIfNeeded();
            }
        }
    
    }
    

    在这种情况下,您将使用Spring中的注释驱动任务功能:

    <task:annotation-driven executor="taskExecutor" scheduler="taskScheduler"/>
    <task:scheduler id="taskScheduler" pool-size="10"/>
    

    从我的观点来看,这将是一个简单而好的解决方案。

    修改

    使用这种方法,当spring结束bean配置时,你或多或少地将SessionToken注册到Flusher,并在spring销毁bean时将其删除,这在会话终止时完成。 / p>

答案 2 :(得分:3)

我认为这应该简单得多。

关于同步器令牌实施

  1. 同步器令牌模式中的令牌并不意味着可以重复使用。令牌被认为仅对一次提交有效。不多也不少。

  2. 在任何给定的时间点,您只需为会话保存一个令牌。

  3. 当向用户显示表单时,表单应作为隐藏表单元素包含在表单中。

  4. 提交时,您所要做的就是检查表单中的令牌和会话是否匹配。如果是,请允许表单提交,并在会话中重置令牌的值。

  5. 现在,如果用户重新提交相同的表单,(使用旧令牌)令牌将不匹配,您可以检测到双提交或陈旧提交

  6. 另一方面,如果用户重新加载表单本身,则更新的标记现在将出现在隐藏的表单元素中。

  7. 结论 - 无需为用户保存令牌列表。每个会话一个令牌就是所需要的。此模式被广泛用作防止CSRF攻击的安全措施。页面上的每个链接只能调用一次。即使这样也可以通过每个会话只有一个令牌来完成。作为参考,您可以看到CSRF Guard V3如何工作https://www.owasp.org/index.php/Category:OWASP_CSRFGuard_Project#Source_Code

    关于会话

    1. 只要执行线程以某种方式绑定到http请求/响应对,会话对象就有意义。或者只要用户浏览您的网站即可。

    2. 一旦用户离开,会话(来自JVM)也是如此。因此,您无法使用计时器重置它

    3. 会话由服务器序列化,以确保当用户重新访问您的网站时(jsessionid用于标识哪个会话应针对哪个浏览器会话进行反序列化),它们可以生动。

    4. 会话超时,如果超时到期,服务器会在用户重新访问时启动新会话。

    5. 结论 - 没有合理的理由不得不定期刷新用户的会话 - 而且无法做到这一点。

      如果我误解了某些内容,请告诉我,我希望这会有所帮助。

答案 3 :(得分:2)

我对石英知之甚少,但这就是我在你的情况下会做的事情: 春天3.2,对吗? 在您的应用程序上下文中:

<task:annotation-driven executor="taskExecutor" scheduler="taskScheduler"/>
<task:scheduler id="taskScheduler" pool-size="10"/>

您将需要任务命名空间...

在某些类(例如SessionCleaner.java)中:

    @Scheduled(fixedRate=3600000)
    public void clearSession(){
         //clear the session    
    }

我要做的是将会话数据用作Spring管理的会话范围bean:

<bean id="mySessionData" class="MySessionDataBean" scope="session">
</bean>

然后将其注入我需要的地方。那个氏族看起来就像那样。

class SessionCleaner{
        @Autowired
        private MySessionDataBean sessionData;

        @Scheduled(fixedRate=3600000)
        public void clearSession(){
            sessionData.getList().clear();//something like that    
        }
}

答案 4 :(得分:1)

我对Quartz不太了解。但是,看到你的异常,我能猜到的是什么 调度是异步通信的一部分。 因此,使用多线程和事件驱动模型来实现它。 因此,在您的情况下,您正在尝试创建一个bean preventDuplicateSubmissionService。容器创建bean作为object的一部分。但是当您访问它时,Quartz创建的线程作为异步调度的一部分正在尝试获取服务。但是,对于该线程,bean没有实例化。可能你正在使用scope =“request”&gt; 。当范围是每个http请求请求其对象被创建时。在你尝试访问bean的情况下,那么没有创建具有请求范围的bean,因为bean的访问是在非http模式下进行的,所以当线程尝试时抛出异常的服务。当我在serviceImpl中尝试使用多线程访问bean时,我也遇到了同样的问题。在我的情况下,没有创建请求范围的bean,因为它是为每个http请求模式创建的。 我在这里实施了solutin,它对我有用。 Accessing scoped proxy beans within Threads of

答案 5 :(得分:1)

您设置的令牌移除定期作业似乎并未在特定用户会话的上下文中运行 - 也不是所有用户会话。

我不确定您是否有任何机制可以扫描所有活动会话并对其进行修改,但我建议的替代方案是:

在服务器端存储一对带有时间戳的唯一令牌。在呈现表单时,仅将令牌发送给用户(从不发送时间戳)。提交表单时,查找生成该令牌的时间 - 如果超过超时值,则拒绝它。

这样你甚至不需要计时器来删除所有令牌。使用计时器还会删除新创建的令牌

答案 6 :(得分:1)

在原始的问题方法(按照shcedule正确运行)中使用以下代码...

package quartz;

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;

public final class RemoveTokens extends QuartzJobBean implements javax.sevlet.http.HttpSessionListener {

    static java.util.Map<String, HttpSession> httpSessionMap = new HashMap<String, HttpSession>();

    void sessionCreated(HttpSessionEvent se) {
        HttpSession ss = se.getSession();
        httpSessionMap.put(ss.getId(), ss);
    }

    void sessionDestroyed(HttpSessionEvent se) {
        HttpSession ss = se.getSession();
        httpSessionMap.remove(ss.getId());
    }

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        Set<String> keys = httpSessionMap.keySet();

        for (String key : keys) {
            HttpSession ss = httpSessionMap.get(key);
            Long date = (Long) ss.getAttribute("time");

            if (date == null) {
                date = ss.getCreationTime();
            }

            long curenttime = System.currentTimeMillis();
            long difference = curenttime - date;

            if (difference > (60 * 60 * 1000)) {
                /*Greater than 1 hour*/
                List l = ss.getAttribute("YourList");
                l.removeAll(l);
                ss.setAttribute("time", curenttime);
            }
        }
    }
}