Java 前后分离时,利用延迟队列实现session

1、介绍

1.1、session

  • 在WEB开发中,服务器可以为每个用户浏览器创建一个会话对象(session对象),注意:一个浏览器独占一个session对象(默认情况下)。因此,在需要保存用户数据时,服务器程序可以把用户数据写到用户浏览器独占的session中,当用户使用浏览器访问其它程序时,其它程序可以从用户的session中取出该用户的数据,为用户服务。

  • 服务器是如何实现一个session为一个用户浏览器服务的?

服务器创建session出来后,会把session的id号,以cookie的形式回写给客户机,这样,只要客户机的浏览器不关,再去访问服务器时,都会带着session的id号去,服务器发现客户机浏览器带session id过来了,就会使用内存中与之对应的session为之服务。

注:上文描述,参考自文末的参考链接中第1条链接。

1.2、延迟队列

  • 1、DelayQueue队列中的元素必须是Delayed接口的实现类,该类内部实现了getDelay()compareTo()方法,第一个方法是比较两个任务的延迟时间进行排序,第二个方法用来获取延迟时间。
  • 2、DelayQueue队列没有大小限制,因此向队列插数据不会阻塞
  • 3、DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。否则线程阻塞。
  • 4、DelayQueue中的元素不能为null
  • 5、DelayQueue内部是使用PriorityQueue实现的。compareTo()比较后越小的越先取出来。

注:上文描述,参考自文末的参考链接中第2条链接。

2、前后端分离方案

  • 前端为html,利用ajax(后期改为axios)来请求json交互,restful风格
  • 后端以springboot为基础框架,接口暴露为适应跨域要求,利用在控制层添加@CrossOrigin注解实现
  • 功能抽象:登陆、登陆后页面查看(需要鉴权)

3、存在的问题

  • 因为跨域问题,JSESSIONID每次请求都会变化,导致后端无法维护一个合适的session
  • 故需要简便、快速、低成本的一种方法,来实现session的存储与维护。

4、方案

4.1、session的特点

  • 1、以唯一键key来插入和获取对象
  • 2、session有自动过期时间,到期后系统会自动清理。
  • 3、每次新的请求过来获取session,该key值过期时间重置

4.2、DelayQueue的设计

  • 1、采用concurrentHashmap来保存session信息
  • 2、采用DelayQueue延迟队列来存储concurrentHashmap中的key
  • 3、模拟会话监听器,即sessionListener,专门开启一个守护线程(阻塞式take)从DelayQueue队列中获取过期的指针,再根据指针删除concurrentHashmap中对应元素。

4.3、登陆方案的设计

  • 1、sessionId的设计,可利用uuid或其他规则来实现。
  • 2、在后台的登陆方法中,若用户名与密码正确,则生成该sessionId,按照一定格式返回给前台。
  • 3、前台接收到该sessionId后,可存储到cookie中,将其封装到httpheader中,后续请求均附带该header

5、实际代码

5.1、前台请求的发送

  • 前台axios中,具体请求示例如下:
var headers = {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Credentials': 'true',
            'Authorization': $.cookie("jsessionId"),
        };
axios({
        headers: headers,
        method: method,  //GET、PUT、POST、PATCH、DELETE等
        url: url,
        timeout: 50000, // 请求的超时时间
        data: data,
    })
    .then(function (response) {
       //TODO 正确返回后的处理或回调
       
    })
    .catch(function (error) {
        if (error.response) {
            console.log(error.response);
        } else if (error.request) {
            // The request was made but no response was received
            // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
            // http.ClientRequest in node.js
            console.log(error.request);
        } else {
            // Something happened in setting up the request that triggered an Error
            console.log('Error', error.message);
        }
    });

5.2、后台DelayQueue

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

/**
 * @Auther: jiangcaijun
 * @Date: 2018/4/17 15:15
 * @Description: 延迟队列,单例模式。利用ConcurrentHashMap来存储信息
 */
public class CacheSingleton<K, V> {

    /*session自动过期时间,单位:秒*/
    private static int liveTime = 5;

    //在类内部实例化一个实例
    private static CacheSingleton instance = new CacheSingleton();
    //私有的构造函数,外部无法访问
    private CacheSingleton(){
        Thread t = new Thread(){
            @Override
            public void run(){
                dameonCheckOverdueKey();
            }
        };
        t.setDaemon(true);
        t.start();
    }
    //对外提供获取实例的静态方法
    public static CacheSingleton getInstance() {
        return instance;
    }

    public ConcurrentHashMap<K, V> concurrentHashMap = new ConcurrentHashMap<K, V>();
    public DelayQueue<DelayedItem<K>> delayQueue = new DelayQueue<DelayedItem<K>>();

    /**
     * 根据key,获取相应的值
     * @param k
     * @return
     */
    public Object get(K k){
        V v = concurrentHashMap.get(k);
        DelayedItem<K> tmpItem = new DelayedItem<K>(k, liveTime);
        if (v != null) {
            delayQueue.remove(tmpItem);
            delayQueue.put(tmpItem);
            System.out.println(String.format("%s \t %s",new SimpleDateFormat("HH:mm:ss").format(new Date()), "获取 "+ k + "成功,生命周期重新计算:"+ liveTime +"秒"));
        }else{
            System.out.println(String.format("%s \t %s",new SimpleDateFormat("HH:mm:ss").format(new Date()), "获取"+ k +"失败,对象已过期"));
        }
        return v;
    }

    /**
     * 移除相应的键值对
     * @param k
     */
    public void remove(K k){
        V v = concurrentHashMap.get(k);
        DelayedItem<K> tmpItem = new DelayedItem<K>(k, liveTime);
        if (v != null) {
            delayQueue.remove(tmpItem);
            System.out.println(String.format("%s \t %s",new SimpleDateFormat("HH:mm:ss").format(new Date()), "主动删除 "+ k + "成功"));
        }else{
            System.out.println(String.format("%s \t %s",new SimpleDateFormat("HH:mm:ss").format(new Date()), "删除失败,该 "+ k +"已被删除"));
        }
    }

    /**
     * 插入键值对
     * @param k
     * @param v
     */
    public void put(K k,V v){
        V v2 = concurrentHashMap.put(k, v);
        DelayedItem<K> tmpItem = new DelayedItem<K>(k, liveTime);
        if (v2 != null) {
            delayQueue.remove(tmpItem);
            System.out.println(String.format("%s \t %s",new SimpleDateFormat("HH:mm:ss").format(new Date()), "覆盖插入 "+ k + ",生命周期重新计算:"+ liveTime +"秒"));
        }else{
            System.out.println(String.format("%s \t %s",new SimpleDateFormat("HH:mm:ss").format(new Date()), "新插入 "+ k + ",生命周期初始化:"+ liveTime +"秒"));
        }
        delayQueue.put(tmpItem);

    }

    /**
     * 专门开启一个守护线程(阻塞式)从 delayQueue 队列中获取过期的指针,再根据指针删除hashmap中对应元素。
     */
    public void dameonCheckOverdueKey(){
        System.out.println(String.format("%s \t %s",new SimpleDateFormat("HH:mm:ss").format(new Date()), "守护进程开启"));
        while (true) {
            DelayedItem<K> delayedItem = null;
            try {
                delayedItem = delayQueue.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (delayedItem != null) {
                concurrentHashMap.remove(delayedItem.getT());
                System.out.println(String.format("%s \t %s",new SimpleDateFormat("HH:mm:ss").format(new Date()),"自动删除过期key: "+delayedItem.getT()));
            }
            try {
                Thread.sleep(300);
            } catch (Exception e) {
                // TODO: handle exception
            }
        }
    }

    /**
     * TODO
     */
    public static void main(String[] args) throws InterruptedException {
        /*模拟客户端调用*/
        CacheSingleton.getInstance().put("1", 1);
        CacheSingleton.getInstance().put("2", 2);
        Thread.sleep(4000);
        CacheSingleton.getInstance().get("2");
        Thread.sleep(2000);
        CacheSingleton.getInstance().get("2");
        Thread.sleep(2000);
        CacheSingleton.getInstance().get("2");
        Thread.sleep(5500);
        CacheSingleton.getInstance().put("1", 2);
        CacheSingleton.getInstance().get("2");
        Thread.sleep(5000);
        System.out.println(String.format("%s \t %s",new SimpleDateFormat("HH:mm:ss").format(new Date()),"main方法结束"));
    }
}


class DelayedItem<T> implements Delayed{

    private T t;
    private long liveTime ;
    private long removeTime;

    public DelayedItem(T t,long liveTime){
        this.setT(t);
        this.liveTime = liveTime;
        this.removeTime = TimeUnit.NANOSECONDS.convert(liveTime, TimeUnit.SECONDS) + System.nanoTime();
    }

    @Override
    public int compareTo(Delayed o) {
        if (o == null) return 1;
        if (o == this) return  0;
        if (o instanceof DelayedItem){
            DelayedItem<T> tmpDelayedItem = (DelayedItem<T>)o;
            if (liveTime > tmpDelayedItem.liveTime ) {
                return 1;
            }else if (liveTime == tmpDelayedItem.liveTime) {
                return 0;
            }else {
                return -1;
            }
        }
        long diff = getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
        return diff > 0 ? 1:diff == 0? 0:-1;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(removeTime - System.nanoTime(), unit);
    }

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
    @Override
    public int hashCode(){
        return t.hashCode();
    }

    @Override
    public boolean equals(Object object){
        if (object instanceof DelayedItem) {
            return object.hashCode() == hashCode() ?true:false;
        }
        return false;
    }

}

在过期时间为5秒的情况下,模拟session,main方法运行,输出为:

16:56:25 	 新插入 1,生命周期初始化:5秒
16:56:25 	 守护进程开启
16:56:25 	 新插入 2,生命周期初始化:5秒
16:56:29 	 获取 2成功,生命周期重新计算:5秒
16:56:30 	 自动删除过期key: 1
16:56:31 	 获取 2成功,生命周期重新计算:5秒
16:56:33 	 获取 2成功,生命周期重新计算:5秒
16:56:38 	 自动删除过期key: 2
16:56:38 	 新插入 1,生命周期初始化:5秒
16:56:38 	 获取2失败,对象已过期
16:56:43 	 自动删除过期key: 1
16:56:43 	 main方法结束

5.3、登陆成功后页面鉴权

利用aop的环绕aroud,在请求过来时,查看该sessionId是否存在该delayQueue中,简要代码如下:

import com.bigdata.weathercollect.constant.GlobalConstant;
import com.bigdata.weathercollect.exception.UnauthorizedException;
import com.bigdata.weathercollect.service.ServiceStatus;
import com.bigdata.weathercollect.session.CacheSingleton;
import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @Auther: jiangcaijun
 * @Date: 2018/4/16 15:58
 * @Description:
 *      @Component:注册到Spring容器,必须加入这个注解
 *      @Aspect // 该注解标示该类为切面类,切面是由通知和切点组成的。
 */
@Component
@Aspect
public class ExceptionAspect {

    private static Logger logger = LoggerFactory.getLogger(ExceptionAspect.class);

    @Autowired
    private HttpServletRequest request;
    /**
     * 这里会报错,但不影响运行
     */
    @Autowired
    private HttpServletResponse response;

    @Pointcut("execution(public * com.bigdata.weathercollect.controller.*.*(..))")
    public void exceptionAspect() {
    }

    @Around("exceptionAspect()")
    public Object around(ProceedingJoinPoint joinPoint){

        String url = request.getRequestURI();
        ServiceStatus serviceStatus = null;
        Boolean flag = false;
        if(url != null){
            String jsessionId = request.getHeader("Authorization");
            if(StringUtils.isNotBlank(jsessionId)) {
                //这里进行sessionId的校验
                if(CacheSingleton.getInstance().get(jsessionId) != null){
//                    logger.info("该用户已登陆,id:{}", jsessionId);
                    flag = true;
                }
            }
            if(!flag){
                logger.error("该用户未登陆");
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
                return new ServiceStatus(ServiceStatus.Status.Fail, "尚未登陆或会话已过期",401);
            }
        }


        try {
            return joinPoint.proceed();
        } catch (UnauthorizedException e) {
            logger.error("出现Exception:url为" + url + ";错误类型为"+e.getMessage()+"");
            serviceStatus =  new ServiceStatus(ServiceStatus.Status.Fail, "认证失败:" + e.getMessage(),401);
        } catch (Exception e) {
            logger.error("出现Exception:url为" + url + ";错误类型为"+e.getMessage()+"");
            serviceStatus =  new ServiceStatus(ServiceStatus.Status.Fail, "失败:" + e.getMessage(),500);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return serviceStatus;
    }
}

注:其中,ServiceStatus为自定义的json返回封装的类,不影响阅读,故代码未贴出来。

6、其他

参考链接:

猜你喜欢

转载自my.oschina.net/u/3136014/blog/1797408
今日推荐