先针对用户分布式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 {
/*****redis config start*******/
@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;
/*****redis config end*******/
}

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) {//0代表永不过期
this(0, prefix);
}

public BasePrefix( int expireSeconds, String prefix) {
this.expireSeconds = expireSeconds;
this.prefix = prefix;
}

public int expireSeconds() {//默认0代表永不过期
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;
}
//生成真正的key
String realKey = prefix.getPrefix() + key;//MiaoshaUserKey:tkUUID
int seconds = prefix.expireSeconds();//超时时间
if(seconds <= 0) {
jedis.set(realKey, str);
}else {
jedis.setex(realKey, seconds, str);//set进redis中
}
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.setDomain(COOKIE_DOMAIN);
ck.setPath("/");//设值在根目录
ck.setHttpOnly(true);//不允许通过脚本访问cookie,避免脚本攻击
ck.setMaxAge(MiaoshaUserKey.token.expireSeconds());
log.info("write cookieName:{},cookieValue:{}",ck.getName(),ck.getValue());
response.addCookie(ck);
}

这样,下面继续访问的时候,先根据cookie拿到UUID,再根据UUIDredis 中拿到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) {
//先判断token是否为空
if(StringUtils.isEmpty(token)){
return null;
}
//根据token到redis中拿到相应的value
MiaoshaUser user = redisService.get(MiaoshaUserKey.token,token,MiaoshaUser.class);
redisService.set(MiaoshaUserKey.token,token,user);//key--->UserKey:tkUUID,value--->Serialized User
//如果此时拿到user成功了,这里要重新设置一下redis过期时间
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进行特别的处理。更加方便,在蜗牛商城电商项目中就是这样干的。