zoukankan      html  css  js  c++  java
  • spring-session(二)与spring-boot整合实战

    前两篇介绍了spring-session的原理,这篇在理论的基础上再实战。
    spring-boot整合spring-session的自动配置可谓是开箱即用,极其简洁和方便。这篇文章即介绍spring-boot整合spring-session,这里只介绍基于RedisSession的实战。

    原理篇是基于spring-session v1.2.2版本,考虑到RedisSession模块与spring-session v2.0.6版本的差异很小,且能够与spring-boot v2.0.0兼容,所以实战篇是基于spring-boot v2.0.0基础上配置spring-session。

    源码请戮session-example

    实战

    搭建spring-boot工程这里飘过,传送门:https://start.spring.io/

    配置spring-session

    引入spring-session的pom配置,由于spring-boot包含spring-session的starter模块,所以pom中依赖:

    <dependency>
    	<groupId>org.springframework.session</groupId>
    	<artifactId>spring-session-data-redis</artifactId>
    </dependency>
    

    编写spring boot启动类SessionExampleApplication

    /**
     * 启动类
     *
     * @author huaijin
     */
    @SpringBootApplication
    public class SessionExampleApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SessionExampleApplication.class, args);
        }
    }
    

    配置application.yml

    spring:
      session:
        redis:
          flush-mode: on_save
          namespace: session.example
          cleanup-cron: 0 * * * * *
        store-type: redis
        timeout: 1800
      redis:
        host: localhost
        port: 6379
        jedis:
          pool:
            max-active: 100
            max-wait: 10
            max-idle: 10
            min-idle: 10
        database: 0
    
    编写controller

    编写登录控制器,登录时创建session,并将当前登录用户存储sesion中。登出时,使session失效。

    /**
     * 登录控制器
     *
     * @author huaijin
     */
    @RestController
    public class LoginController {
    
        private static final String CURRENT_USER = "currentUser";
    
        /**
         * 登录
         *
         * @param loginVo 登录信息
         *
         * @author huaijin
         */
        @PostMapping("/login.do")
        public String login(@RequestBody LoginVo loginVo, HttpServletRequest request) {
            UserVo userVo = UserVo.builder().userName(loginVo.getUserName())
                    .userPassword(loginVo.getUserPassword()).build();
            HttpSession session = request.getSession();
            session.setAttribute(CURRENT_USER, userVo);
            System.out.println("create session, sessionId is:" + session.getId());
            return "ok";
        }
    
        /**
         * 登出
         *
         * @author huaijin
         */
        @PostMapping("/logout.do")
        public String logout(HttpServletRequest request) {
            HttpSession session = request.getSession(false);
            session.invalidate();
            return "ok";
        }
    }
    

    编写查询控制器,在登录创建session后,使用将sessionId置于cookie中访问。如果没有session将返回错误。

    /**
     * 查询
     *
     * @author huaijin
     */
    @RestController
    @RequestMapping("/session")
    public class QuerySessionController {
    
        @GetMapping("/query.do")
        public String querySessionId(HttpServletRequest request) {
            HttpSession session = request.getSession(false);
            if (session == null) {
                return "error";
            }
            System.out.println("current's user is:" + session.getId() +  "in session");
            return "ok";
        }
    }
    
    编写Session删除事件监听器

    Session删除事件监听器用于监听登出时使session失效的事件源。

    /**
     * session事件监听器
     *
     * @author huaijin
     */
    @Component
    public class SessionEventListener implements ApplicationListener<SessionDeletedEvent> {
    
        private static final String CURRENT_USER = "currentUser";
    
        @Override
        public void onApplicationEvent(SessionDeletedEvent event) {
            Session session = event.getSession();
            UserVo userVo = session.getAttribute(CURRENT_USER);
            System.out.println("invalid session's user:" + userVo.toString());
        }
    }
    
    验证测试

    编写spring-boot测试类,测试controller,验证spring-session是否生效。

    
    /**
     * 测试Spring-Session:
     * 1.登录时创建session
     * 2.使用sessionId能正常访问
     * 3.session过期销毁,能够监听销毁事件
     *
     * @author huaijin
     */
    @RunWith(SpringRunner.class)
    @SpringBootTest
    @AutoConfigureMockMvc
    public class SpringSessionTest {
    
        @Autowired
        private MockMvc mockMvc;
    
    
        @Test
        public void testLogin() throws Exception {
            LoginVo loginVo = new LoginVo();
            loginVo.setUserName("admin");
            loginVo.setUserPassword("admin@123");
            String content = JSON.toJSONString(loginVo);
    
            // mock登录
            ResultActions actions = this.mockMvc.perform(post("/login.do")
                    .content(content).contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk()).andExpect(content().string("ok"));
            String sessionId = actions.andReturn()
                    .getResponse().getCookie("SESSION").getValue();
    
            // 使用登录的sessionId mock查询
            this.mockMvc.perform(get("/session/query.do")
                    .cookie(new Cookie("SESSION", sessionId)))
                    .andExpect(status().isOk()).andExpect(content().string("ok"));
    
            // mock登出
            this.mockMvc.perform(post("/logout.do")
                    .cookie(new Cookie("SESSION", sessionId)))
                    .andExpect(status().isOk()).andExpect(content().string("ok"));
        }
    }
    

    测试类执行结果:

    create session, sessionId is:429cb0d3-698a-475a-b3f1-09422acf2e9c
    current's user is:429cb0d3-698a-475a-b3f1-09422acf2e9cin session
    invalid session's user:UserVo{userName='admin', userPassword='admin@123'
    

    登录时创建Session,存储当前登录用户。然后在以登录响应返回的SessionId查询用户。最后再登出使Session过期。

    spring-boot整合spring-session自动配置原理

    前两篇文章介绍spring-session原理时,总结spring-session的核心模块。这节中探索spring-boot中自动配置如何初始化spring-session的各个核心模块。

    spring-boot-autoconfigure模块中包含了spinrg-session的自动配置。包org.springframework.boot.autoconfigure.session中包含了spring-session的所有自动配置项。

    其中RedisSession的核心配置项是RedisHttpSessionConfiguration类。

    @Configuration
    @ConditionalOnClass({ RedisTemplate.class, RedisOperationsSessionRepository.class })
    @ConditionalOnMissingBean(SessionRepository.class)
    @ConditionalOnBean(RedisConnectionFactory.class)
    @Conditional(ServletSessionCondition.class)
    @EnableConfigurationProperties(RedisSessionProperties.class)
    class RedisSessionConfiguration {
    
    	@Configuration
    	public static class SpringBootRedisHttpSessionConfiguration
    			extends RedisHttpSessionConfiguration {
    
    		// 加载application.yml或者application.properties中自定义的配置项:
    		// 命名空间:用于作为session redis key的一部分
    		// flushmode:session写入redis的模式
    		// 定时任务时间:即访问redis过期键的定时任务的cron表达式
    		@Autowired
    		public void customize(SessionProperties sessionProperties,
    				RedisSessionProperties redisSessionProperties) {
    			Duration timeout = sessionProperties.getTimeout();
    			if (timeout != null) {
    				setMaxInactiveIntervalInSeconds((int) timeout.getSeconds());
    			}
    			setRedisNamespace(redisSessionProperties.getNamespace());
    			setRedisFlushMode(redisSessionProperties.getFlushMode());
    			setCleanupCron(redisSessionProperties.getCleanupCron());
    		}
    
    	}
    
    }
    

    RedisSessionConfiguration配置类中嵌套SpringBootRedisHttpSessionConfiguration继承了RedisHttpSessionConfiguration配置类。首先看下该配置类持有的成员。

    @Configuration
    @EnableScheduling
    public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
    		implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware,
    		SchedulingConfigurer {
    
    
    	// 默认的cron表达式,application.yml可以自定义配置
    	static final String DEFAULT_CLEANUP_CRON = "0 * * * * *";
    
    	// session的有效最大时间间隔, application.yml可以自定义配置
    	private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
    
    	// session在redis中的命名空间,主要为了区分session,application.yml可以自定义配置
    	private String redisNamespace = RedisOperationsSessionRepository.DEFAULT_NAMESPACE;
    
    	// session写入Redis的模式,application.yml可以自定义配置
    	private RedisFlushMode redisFlushMode = RedisFlushMode.ON_SAVE;
    
    	// 访问过期Session集合的定时任务的定时时间,默认是每整分运行任务
    	private String cleanupCron = DEFAULT_CLEANUP_CRON;
    
    	private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction();
    
    	// spring-data-redis的redis连接工厂
    	private RedisConnectionFactory redisConnectionFactory;
    
    	// spring-data-redis的RedisSerializer,用于序列化session中存储的attributes
    	private RedisSerializer<Object> defaultRedisSerializer;
    
    	// session时间发布者,默认注入的是AppliationContext实例
    	private ApplicationEventPublisher applicationEventPublisher;
    
    	// 访问过期session键的定时任务的调度器
    	private Executor redisTaskExecutor;
    
    	private Executor redisSubscriptionExecutor;
    
    	private ClassLoader classLoader;
    
    	private StringValueResolver embeddedValueResolver;
    }
    

    该配置类中初始化了RedisSession的最为核心模块之一RedisOperationsSessionRepository。

    @Bean
    public RedisOperationsSessionRepository sessionRepository() {
    	// 创建RedisOperationsSessionRepository
    	RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
    	RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(
    			redisTemplate);
    	// 设置Session Event发布者。如果对此迷惑,传送门:https://www.cnblogs.com/lxyit/p/9719542.html
    	sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
    	if (this.defaultRedisSerializer != null) {
    		sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
    	}
    	// 设置默认的Session最大有效期间隔
    	sessionRepository
    			.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
    	// 设置命名空间
    	if (StringUtils.hasText(this.redisNamespace)) {
    		sessionRepository.setRedisKeyNamespace(this.redisNamespace);
    	}
    	// 设置写redis的模式
    	sessionRepository.setRedisFlushMode(this.redisFlushMode);
    	return sessionRepository;
    }
    

    同时也初始化了Session事件监听器MessageListener模块

    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer() {
    	// 创建MessageListener容器,这属于spring-data-redis范畴,略过
    	RedisMessageListenerContainer container = new RedisMessageListenerContainer();
    	container.setConnectionFactory(this.redisConnectionFactory);
    	if (this.redisTaskExecutor != null) {
    		container.setTaskExecutor(this.redisTaskExecutor);
    	}
    	if (this.redisSubscriptionExecutor != null) {
    		container.setSubscriptionExecutor(this.redisSubscriptionExecutor);
    	}
    	// 模式订阅redis的__keyevent@*:expired和__keyevent@*:del通道,
    	// 获取redis的键过期和删除事件通知
    	container.addMessageListener(sessionRepository(),
    			Arrays.asList(new PatternTopic("__keyevent@*:del"),
    					new PatternTopic("__keyevent@*:expired")));
    	// 模式订阅redis的${namespace}:event:created:*通道,当该向该通道发布消息,
    	// 则MessageListener消费消息并处理
    	container.addMessageListener(sessionRepository(),
    			Collections.singletonList(new PatternTopic(
    					sessionRepository().getSessionCreatedChannelPrefix() + "*")));
    	return container;
    }
    

    上篇文章中介绍到的spring-session event事件原理,spring-session在启动时监听Redis的channel,使用Redis的键空间通知处理Session的删除和过期事件和使用Pub/Sub模式处理Session创建事件。

    关于RedisSession的存储管理部分已经初始化,但是spring-session的另一个基础设施模块SessionRepositoryFilter是在RedisHttpSessionConfiguration父类SpringHttpSessionConfiguration中初始化。

    @Bean
    public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(
    		SessionRepository<S> sessionRepository) {
    	SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(
    			sessionRepository);
    	sessionRepositoryFilter.setServletContext(this.servletContext);
    	sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
    	return sessionRepositoryFilter;
    }
    

    spring-boot整合spring-session配置的层次:

    RedisSessionConfiguration
    	|_ _ SpringBootRedisHttpSessionConfiguration
    			|_ _ RedisHttpSessionConfiguration
    					|_ _ SpringHttpSessionConfiguration
    

    回顾思考spring-boot自动配置spring-session,非常合理。

    • SpringHttpSessionConfiguration是spring-session本身的配置类,与spring-boot无关,毕竟spring-session也可以整合单纯的spring项目,只需要使用该spring-session的配置类即可。
    • RedisHttpSessionConfiguration用于配置spring-session的Redission,毕竟spring-session还支持其他的各种session:Map/JDBC/MogonDB等,将其从SpringHttpSessionConfiguration隔离开来,遵循开闭原则和接口隔离原则。但是其必须依赖基础的SpringHttpSessionConfiguration,所以使用了继承。RedisHttpSessionConfiguration是spring-session和spring-data-redis整合配置,需要依赖spring-data-redis。
    • SpringBootRedisHttpSessionConfiguration才是spring-boot中关键配置
    • RedisSessionConfiguration主要用于处理自定义配置,将application.yml或者application.properties的配置载入。

    Tips:
    配置类也有相当强的设计模式。遵循开闭原则:对修改关闭,对扩展开放。遵循接口隔离原则:变化的就要单独分离,使用不同的接口隔离。SpringHttpSessionConfiguration和RedisHttpSessionConfiguration的设计深深体现这两大原则。

    参考

    Spring Session

  • 相关阅读:
    java基础之冒泡排序
    java基础之HashSet如何保证对象的唯一性
    java基础之日期时间工具类
    java基础之抽象类和接口的区别
    java tomcat报错: Starting Tomcat v7.0 Server at localhost' has encountered a problem问题
    [bzoj 4196][NOI 2015]软件包管理器
    [bzoj 4034][HAOI 2015]树上操作
    [bzoj 1012][JSOI2008]最大数maxnumber
    详解Trie
    [bzoj 1047][HAOI2007]理想的正方形
  • 原文地址:https://www.cnblogs.com/lxyit/p/9720159.html
Copyright © 2011-2022 走看看