Package org.springframework.session.data.redis

Source Code of org.springframework.session.data.redis.RedisOperationsSessionRepository$RedisSession

/*
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.springframework.session.data.redis;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.session.ExpiringSession;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.events.SessionDestroyedEvent;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.util.Assert;

/**
* <p>
* A {@link org.springframework.session.SessionRepository} that is implemented
* using Spring Data's
* {@link org.springframework.data.redis.core.RedisOperations}. In a web
* environment, this is typically used in combination with
* {@link SessionRepositoryFilter}. This implementation supports
* {@link SessionDestroyedEvent} through {@link SessionMessageListener}.
* </p>
*
* <h2>Creating a new instance</h2>
*
* A typical example of how to create a new instance can be seen below:
*
* <pre>
* JedisConnectionFactory factory = new JedisConnectionFactory();
*
* RedisOperationsSessionRepository redisSessionRepository = new RedisOperationsSessionRepository(
*     factory);
* </pre>
*
* <p>
* For additional information on how to create a RedisTemplate, refer to the <a
* href =
* "http://docs.spring.io/spring-data/data-redis/docs/current/reference/html/"
* >Spring Data Redis Reference</a>.
* </p>
*
* <h2>Storage Details</h2>
*
* <p>
* Each session is stored in Redis as a <a
* href="http://redis.io/topics/data-types#hashes">Hash</a>. Each session is set
* and updated using the <a href="http://redis.io/commands/hmset">HMSET
* command</a>. An example of how each session is stored can be seen below.
* </p>
*
* <pre>
*     HMSET spring:session:sessions:<session-id> creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:<attrName> someAttrValue sessionAttr2:<attrName> someAttrValue2
* </pre>
*
* <p>
* An expiration is associated to each session using the <a
* href="http://redis.io/commands/expire">EXPIRE command</a> based upon the
* {@link org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession#getMaxInactiveInterval()}
* . For example:
* </p>
*
* <pre>
*    EXPIRE spring:session:sessions:<session-id> 1800
* </pre>
*
* <p>
* The {@link RedisSession} keeps track of the properties that have changed and
* only updates those. This means if an attribute is written once and read many
* times we only need to write that attribute once. For example, assume the
* session attribute "sessionAttr2" from earlier was updated. The following
* would be executed upon saving:
* </p>
*
* <pre>
*     HMSET spring:session:sessions:<session-id> sessionAttr2:<attrName> newValue
*     EXPIRE spring:session:sessions:<session-id> 1800
* </pre>
*
* Each session expiration is also tracked to the nearest minute. This allows a
* background task to cleanup expired sessions in a deterministic fashion. For
* example:
*
* <pre>
*     SADD spring:session:expirations:<expire-rounded-up-to-nearest-minute> <session-id>
*     EXPIRE spring:session:expirations:<expire-rounded-up-to-nearest-minute> 1800
* </pre>
*
* The Redis expiration is still placed on each key to ensure that if the server
* is down when the session expires, it is still cleaned up.
*
* @since 1.0
*
* @author Rob Winch
*/
public class RedisOperationsSessionRepository implements SessionRepository<RedisOperationsSessionRepository.RedisSession> {
    /**
     * The prefix for each key of the Redis Hash representing a single session. The suffix is the unique session id.
     */
    static final String BOUNDED_HASH_KEY_PREFIX = "spring:session:sessions:";

    /**
     * The key in the Hash representing {@link org.springframework.session.ExpiringSession#getCreationTime()}
     */
    static final String CREATION_TIME_ATTR = "creationTime";

    /**
     * The key in the Hash representing {@link org.springframework.session.ExpiringSession#getMaxInactiveInterval()}
     */
    static final String MAX_INACTIVE_ATTR = "maxInactiveInterval";

    /**
     * The key in the Hash representing {@link org.springframework.session.ExpiringSession#getLastAccessedTime()}
     */
    static final String LAST_ACCESSED_ATTR = "lastAccessedTime";

    /**
     * The prefix of the key for used for session attributes. The suffix is the name of the session attribute. For
     * example, if the session contained an attribute named attributeName, then there would be an entry in the hash named
     * sessionAttr:attributeName that mapped to its value.
     */
    static final String SESSION_ATTR_PREFIX = "sessionAttr:";

    private final RedisOperations<String,ExpiringSession> sessionRedisOperations;

    private final RedisSessionExpirationPolicy expirationPolicy;

    /**
     * If non-null, this value is used to override {@link RedisSession#setDefaultMaxInactiveInterval(int)}.
     */
    private Integer defaultMaxInactiveInterval;

    /**
     * Allows creating an instance and uses a default {@link RedisOperations} for both managing the session and the expirations.
     *
     * @param redisConnectionFactory the {@link RedisConnectionFactory} to use.
     */
    @SuppressWarnings("unchecked")
    public RedisOperationsSessionRepository(RedisConnectionFactory redisConnectionFactory) {
        this(createDefaultTemplate(redisConnectionFactory), createDefaultTemplate(redisConnectionFactory));
    }

    /**
     * Creates a new instance. For an example, refer to the class level javadoc.
     *
     * @param sessionRedisOperations The {@link RedisOperations} to use for managing the sessions. Cannot be null.
     * @param expirationRedisOperations The {@link RedisOperations} to use for managing expiration mappings. Cannot be null.
     */
    public RedisOperationsSessionRepository(RedisOperations<String, ExpiringSession> sessionRedisOperations, RedisOperations<String,String> expirationRedisOperations) {
        Assert.notNull(sessionRedisOperations, "sessionRedisOperations cannot be null");
        Assert.notNull(expirationRedisOperations, "expirationRedisOperations cannot be null");
        this.sessionRedisOperations = sessionRedisOperations;
        this.expirationPolicy = new RedisSessionExpirationPolicy(sessionRedisOperations, expirationRedisOperations);
    }

    /**
     * Sets the maximum inactive interval in seconds between requests before newly created sessions will be
     * invalidated. A negative time indicates that the session will never timeout. The default is 1800 (30 minutes).
     *
     *  @param defaultMaxInactiveInterval the number of seconds that the {@link Session} should be kept alive between
     *                                    client requests.
     */
    public void setDefaultMaxInactiveInterval(int defaultMaxInactiveInterval) {
        this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
    }

    @Override
    public void save(RedisSession session) {
        session.saveDelta();
    }

    @Scheduled(cron="0 * * * * *")
    public void cleanupExpiredSessions() {
        this.expirationPolicy.cleanExpiredSessions();
    }

    @Override
    public RedisSession getSession(String id) {
        return getSession(id, false);
    }

    /**
     *
     * @param id the session id
     * @param allowExpired
     *            if true, will also include expired sessions that have not been
     *            deleted. If false, will ensure expired sessions are not
     *            returned.
     * @return
     */
    private RedisSession getSession(String id, boolean allowExpired) {
        Map<Object, Object> entries = getSessionBoundHashOperations(id).entries();
        if(entries.isEmpty()) {
            return null;
        }
        MapSession loaded = new MapSession();
        loaded.setId(id);
        for(Map.Entry<Object,Object> entry : entries.entrySet()) {
            String key = (String) entry.getKey();
            if(CREATION_TIME_ATTR.equals(key)) {
                loaded.setCreationTime((Long) entry.getValue());
            } else if(MAX_INACTIVE_ATTR.equals(key)) {
                loaded.setMaxInactiveInterval((Integer) entry.getValue());
            } else if(LAST_ACCESSED_ATTR.equals(key)) {
                loaded.setLastAccessedTime((Long) entry.getValue());
            } else if(key.startsWith(SESSION_ATTR_PREFIX)) {
                loaded.setAttribute(key.substring(SESSION_ATTR_PREFIX.length()), entry.getValue());
            }
        }
        if(!allowExpired && loaded.isExpired()) {
            return null;
        }
        RedisSession result = new RedisSession(loaded);
        result.originalLastAccessTime = loaded.getLastAccessedTime() + TimeUnit.SECONDS.toMillis(loaded.getMaxInactiveInterval());
        result.setLastAccessedTime(System.currentTimeMillis());
        return result;
    }

    @Override
    public void delete(String sessionId) {
        ExpiringSession session = getSession(sessionId, true);
        if(session == null) {
            return;
        }

        String key = getKey(sessionId);
        expirationPolicy.onDelete(session);

        // always delete they key since session may be null if just expired
        this.sessionRedisOperations.delete(key);
    }

    @Override
    public RedisSession createSession() {
        RedisSession redisSession = new RedisSession();
        if(defaultMaxInactiveInterval != null) {
            redisSession.setMaxInactiveInterval(defaultMaxInactiveInterval);
        }
        return redisSession;
    }

    /**
     * Gets the Hash key for this session by prefixing it appropriately.
     *
     * @param sessionId the session id
     * @return the Hash key for this session by prefixing it appropriately.
     */
    static String getKey(String sessionId) {
        return BOUNDED_HASH_KEY_PREFIX + sessionId;
    }

    /**
     * Gets the key for the specified session attribute
     *
     * @param attributeName
     * @return
     */
    static String getSessionAttrNameKey(String attributeName) {
        return SESSION_ATTR_PREFIX + attributeName;
    }

    /**
     * Gets the {@link BoundHashOperations} to operate on a {@link Session}
     * @param sessionId the id of the {@link Session} to work with
     * @return the {@link BoundHashOperations} to operate on a {@link Session}
     */
    private BoundHashOperations<String, Object, Object> getSessionBoundHashOperations(String sessionId) {
        String key = getKey(sessionId);
        return this.sessionRedisOperations.boundHashOps(key);
    }

    @SuppressWarnings("rawtypes")
    private static RedisTemplate createDefaultTemplate(RedisConnectionFactory connectionFactory) {
        Assert.notNull(connectionFactory,"connectionFactory cannot be null");
        RedisTemplate<String, ExpiringSession> template = new RedisTemplate<String, ExpiringSession>();
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setConnectionFactory(connectionFactory);
        return template;
    }

    /**
     * A custom implementation of {@link Session} that uses a {@link MapSession} as the basis for its mapping. It keeps
     * track of any attributes that have changed. When
     * {@link org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession#saveDelta()} is invoked
     * all the attributes that have been changed will be persisted.
     *
     * @since 1.0
     * @author Rob Winch
     */
    final class RedisSession implements ExpiringSession {
        private final MapSession cached;
        private Long originalLastAccessTime;
        private Map<String, Object> delta = new HashMap<String,Object>();

        /**
         * Creates a new instance ensuring to mark all of the new attributes to be persisted in the next save operation.
         */
        RedisSession() {
            this(new MapSession());
            delta.put(CREATION_TIME_ATTR, getCreationTime());
            delta.put(MAX_INACTIVE_ATTR, getMaxInactiveInterval());
            delta.put(LAST_ACCESSED_ATTR, getLastAccessedTime());
        }

        /**
         * Creates a new instance from the provided {@link MapSession}
         *
         * @param cached the {@MapSession} that represents the persisted session that was retrieved. Cannot be null.
         */
        RedisSession(MapSession cached) {
            Assert.notNull("MapSession cannot be null");
            this.cached = cached;
        }

        public void setLastAccessedTime(long lastAccessedTime) {
            cached.setLastAccessedTime(lastAccessedTime);
            delta.put(LAST_ACCESSED_ATTR, getLastAccessedTime());
        }

        @Override
        public boolean isExpired() {
            return cached.isExpired();
        }

        @Override
        public long getCreationTime() {
            return cached.getCreationTime();
        }

        @Override
        public String getId() {
            return cached.getId();
        }

        @Override
        public long getLastAccessedTime() {
            return cached.getLastAccessedTime();
        }

        @Override
        public void setMaxInactiveInterval(int interval) {
            cached.setMaxInactiveInterval(interval);
            delta.put(MAX_INACTIVE_ATTR, getMaxInactiveInterval());
        }

        @Override
        public int getMaxInactiveInterval() {
            return cached.getMaxInactiveInterval();
        }

        @Override
        public Object getAttribute(String attributeName) {
            return cached.getAttribute(attributeName);
        }

        @Override
        public Set<String> getAttributeNames() {
            return cached.getAttributeNames();
        }

        @Override
        public void setAttribute(String attributeName, Object attributeValue) {
            cached.setAttribute(attributeName, attributeValue);
            delta.put(getSessionAttrNameKey(attributeName), attributeValue);
        }

        @Override
        public void removeAttribute(String attributeName) {
            cached.removeAttribute(attributeName);
            delta.put(getSessionAttrNameKey(attributeName), null);
        }

        /**
         * Saves any attributes that have been changed and updates the expiration of this session.
         */
        private void saveDelta() {
            String sessionId = getId();
            getSessionBoundHashOperations(sessionId).putAll(delta);
            delta = new HashMap<String,Object>(delta.size());

            expirationPolicy.onExpirationUpdated(originalLastAccessTime, this);
        }
    }
}
TOP

Related Classes of org.springframework.session.data.redis.RedisOperationsSessionRepository$RedisSession

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.