目标:整合redis实现分布式session存储
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.38version>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
dependency>
redis:
host: 127.0.0.1
port: 6379
max-idle: 5
max-total: 10
max-wait-millis: 3000
@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*******/
}
@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;
}
}
接口(定义契约)—-抽象类(通用方法)—-实现类(具体实现)
接口:接口定义两个方法声明,一个是获取key的前缀,一个是过期时间
public interface KeyPrefix {
public int expireSeconds();
public String getPrefix();
}
抽象类:
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为例:
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。
下面我们执行:
String token = UUIDUtil.uuid();
redisService.set(MiaoshaUserKey.token,token,user);
那么就相当于:
redisService.set("MiaoshaUserKey:tk",UUID,user对象);
那么,redis 的set方法具体是:
public 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中:
CookieUtil.writeLoginToken(response,token);
写入cookie:
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,再根据UUID从redis 中拿到User对象。
以浏览商品列表为例:
@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,取出相应的值。
其中:
MiaoshaUser user = userService.getByToken(token,response);
的具体实现是:
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过期时间方式,在这里页面比较少的情况下,临时这样,但是在页面比较多的情况下,显然是不合适的,可以用一个过滤器,拦截所有的请求,然后在这个过滤器里进行登录过期时间的刷新。
我们发现,后面涉及到商品等其他的接口,按照这种写法,每次都要先获取cookie,然后从redis中获取user信息,获取成功,我们才能进行下一步操作。显然太过冗余,我们可以将其剥离出来,写在一个地方,避免冗余的代码。
我们的controller可以写成:
@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这个方法里,对这个参数进行相应的处理:
@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中某个参数的重写:
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{
@Autowired
private UserArgumentResolver userArgumentResolver;
@Override
public void addArgumentResolvers(List argumentResolvers) {
argumentResolvers.add(userArgumentResolver);
}
}
这样,只要某个方法中传入了MiaoShaUser这个对象,那么就会进入resolveArgument()这个方法进行判断是否能拿到这个对象。
当然,我们可能更加常用的方式是springmvc拦截器来实现这个功能。并且在拦截器中,还可以实现更加复杂的逻辑,比如不仅可以判断user是否已经登陆,还可以针对特殊的url进行特别的处理。更加方便,在ssm电商项目中就是这样干的。