先针对用户分布式session问题引入redis,并且解决了冗余代码问题。可以借鉴一下写法。
目标:整合redis
实现分布式session
存储
1. 添加依赖
1 2 3 4 5 6 7 8 9 10 11 12
| <!--fastJson--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.38</version> </dependency>
<!--redis--> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>
|
2. yml配置文件
1 2 3 4 5 6
| redis: host: 127.0.0.1 port: 6379 max-idle: 5 max-total: 10 max-wait-millis: 3000
|
3. 读取这些配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Component @Data public class RedisConfig { @Value("${redis.host}") private String redisHost; @Value("${redis.port}") private int redisPort; @Value("${redis.max-idle}") private int redisMaxTotal; @Value("${redis.max-total}") private int redisMaxIdle; @Value("${redis.max-wait-millis}") private int redisMaxWaitMillis; }
|
4. RedisPoolFactory构建redisPool
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Service public class RedisPoolFactory {
@Autowired RedisConfig redisConfig; @Bean public JedisPool JedisPoolFactory() { JedisPoolConfig poolConfig = new JedisPoolConfig(); poolConfig.setMaxIdle(redisConfig.getRedisMaxIdle()); poolConfig.setMaxTotal(redisConfig.getRedisMaxTotal()); poolConfig.setMaxWaitMillis(redisConfig.getRedisMaxWaitMillis()); JedisPool jp = new JedisPool(poolConfig, redisConfig.getRedisHost(), redisConfig.getRedisPort()); return jp; } }
|
5. 在用redisPool进行操作之前,先解决一下key的生成问题
接口(定义契约)----抽象类(通用方法)----实现类(具体实现)
接口:接口定义两个方法声明,一个是获取key的前缀,一个是过期时间
1 2 3 4 5 6 7
| public interface KeyPrefix { public int expireSeconds(); public String getPrefix(); }
|
抽象类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public abstract class BasePrefix implements KeyPrefix{ private int expireSeconds; private String prefix; public BasePrefix(String prefix) { this(0, prefix); } public BasePrefix( int expireSeconds, String prefix) { this.expireSeconds = expireSeconds; this.prefix = prefix; } public int expireSeconds() { return expireSeconds; }
public String getPrefix() { String className = getClass().getSimpleName(); return className+":" + prefix; }
}
|
具体的实现类,这里先以MiaoshaUserKey
为例:
1 2 3 4 5 6 7 8
| public class MiaoshaUserKey extends BasePrefix{
public static final int TOKEN_EXPIRE = 3600*24 * 2; private MiaoshaUserKey(int expireSeconds, String prefix) { super(expireSeconds, prefix); } public static MiaoshaUserKey token = new MiaoshaUserKey(TOKEN_EXPIRE, "tk"); }
|
那么构造出来的prefix
显然是MiaoshaUserKey:tk
,超时时间也被传递进expireSeconds
。
下面我们执行:
1 2
| String token = UUIDUtil.uuid(); redisService.set(MiaoshaUserKey.token,token,user);
|
那么就相当于:
1
| redisService.set("MiaoshaUserKey:tk",UUID,user对象);
|
那么,redis 的set方法具体是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public <T> boolean set(KeyPrefix prefix, String key, T value) { Jedis jedis = null; try { jedis = jedisPool.getResource(); String str = beanToString(value); if(str == null || str.length() <= 0) { return false; } String realKey = prefix.getPrefix() + key; int seconds = prefix.expireSeconds(); if(seconds <= 0) { jedis.set(realKey, str); }else { jedis.setex(realKey, seconds, str); } return true; }finally { returnToPool(jedis); } }
|
再下一步是将UUID
写到cookie
中:
1
| CookieUtil.writeLoginToken(response,token);
|
写入cookie:
1 2 3 4 5 6 7 8 9 10 11
| public final static String COOKIE_NAME = "login_token";
public static void writeLoginToken(HttpServletResponse response, String token){ Cookie ck = new Cookie(COOKIE_NAME,token); ck.setPath("/"); ck.setHttpOnly(true); ck.setMaxAge(MiaoshaUserKey.token.expireSeconds()); log.info("write cookieName:{},cookieValue:{}",ck.getName(),ck.getValue()); response.addCookie(ck); }
|
这样,下面继续访问的时候,先根据cookie
拿到UUID
,再根据UUID
从redis
中拿到User
对象。
以浏览商品列表为例:
1 2 3 4 5 6 7 8 9 10 11 12
| @RequestMapping("to_list") public String toList(@CookieValue(value= CookieUtil.COOKIE_NAME,required = false) String cookieToken, @RequestParam(value = CookieUtil.COOKIE_NAME,required = false) String paramToken, Model model,HttpServletResponse response){ if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)){ return "login"; } String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken; MiaoshaUser user = userService.getByToken(token,response); model.addAttribute("user",user); return "goods_list"; }
|
他是根据前面传来的token
做下面的操作,当然还可以从后端读前面的cookie
,取出相应的值。
其中:
1
| MiaoshaUser user = userService.getByToken(token,response);
|
的具体实现是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public MiaoshaUser getByToken(String token,HttpServletResponse response) { if(StringUtils.isEmpty(token)){ return null; } MiaoshaUser user = redisService.get(MiaoshaUserKey.token,token,MiaoshaUser.class); redisService.set(MiaoshaUserKey.token,token,user); if(user != null){ redisService.set(MiaoshaUserKey.token,token,user); } return user; }
|
注意:这里重新设置redis
过期时间方式,在这里页面比较少的情况下,临时这样,但是在页面比较多的情况下,显然是不合适的,可以用一个过滤器,拦截所有的请求,然后在这个过滤器里进行登录过期时间的刷新。
6. 修改代码
我们发现,后面涉及到商品等其他的接口,按照这种写法,每次都要先获取cookie
,然后从redis
中获取user
信息,获取成功,我们才能进行下一步操作。显然太过冗余,我们可以将其剥离出来,写在一个地方,避免冗余的代码。
我们的controller
可以写成:
1 2 3 4 5
| @RequestMapping("to_list") public String toList(Model model,HttpServletResponse response,MiaoshaUser user){ model.addAttribute("user",user); return "goods_list"; }
|
那么,我们在一个地方统一判断user
是否能获取到。就要用到springmvc
的机制了,我们可以试想springmvc
支持的参数都是如何进来的呢?比如这里的MiaoShaUser
是从什么地方注入进来的呢?
其实在UserArgumentResolver
这个类中就可以拿到输入的参数,比如MiaoShaUser
这个对象,然后再在resolveArgument
这个方法里,对这个参数进行相应的处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @Service public class UserArgumentResolver implements HandlerMethodArgumentResolver{ @Autowired private MiaoshaUserService userService; @Override public boolean supportsParameter(MethodParameter parameter) { Class<?> clazz = parameter.getParameterType(); return clazz== MiaoshaUser.class; }
@Override public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest webRequest, WebDataBinderFactory webDataBinderFactory) throws Exception { HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class); String paramToken = request.getParameter(CookieUtil.COOKIE_NAME); String cookieToken = CookieUtil.readLoginToken(request); if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)){ return "login"; } String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken; return userService.getByToken(token,response); } }
|
当然,这个对传入的参数进行修改的UserArgumentResolver
要被重新加入进argumentResolvers
中,相当于完成对原始的argumentResolvers
中某个参数的重写:
1 2 3 4 5 6 7 8 9 10
| @Configuration public class WebConfig extends WebMvcConfigurerAdapter{ @Autowired private UserArgumentResolver userArgumentResolver;
@Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { argumentResolvers.add(userArgumentResolver); } }
|
这样,只要某个方法中传入了MiaoShaUser
这个对象,那么就会进入resolveArgument()
这个方法进行判断是否能拿到这个对象。
当然,我们可能更加常用的方式是springmvc
拦截器来实现这个功能。并且在拦截器中,还可以实现更加复杂的逻辑,比如不仅可以判断user
是否已经登陆,还可以针对特殊的url
进行特别的处理。更加方便,在蜗牛商城电商项目中就是这样干的。