Java 先後分離時,利用延遲隊列實現session

一、介紹

1.一、session

  • 在WEB開發中,服務器能夠爲每一個用戶瀏覽器建立一個會話對象(session對象),注意:一個瀏覽器獨佔一個session對象(默認狀況下)。所以,在須要保存用戶數據時,服務器程序能夠把用戶數據寫到用戶瀏覽器獨佔的session中,當用戶使用瀏覽器訪問其它程序時,其它程序能夠從用戶的session中取出該用戶的數據,爲用戶服務。html

  • 服務器是如何實現一個session爲一個用戶瀏覽器服務的?前端

服務器建立session出來後,會把session的id號,以cookie的形式回寫給客戶機,這樣,只要客戶機的瀏覽器不關,再去訪問服務器時,都會帶着session的id號去,服務器發現客戶機瀏覽器帶session id過來了,就會使用內存中與之對應的session爲之服務。java

注:上文描述,參考自文末的參考連接中第1條連接。node

1.二、延遲隊列

  • 一、DelayQueue隊列中的元素必須是Delayed接口的實現類,該類內部實現了getDelay()compareTo()方法,第一個方法是比較兩個任務的延遲時間進行排序,第二個方法用來獲取延遲時間。
  • 二、DelayQueue隊列沒有大小限制,所以向隊列插數據不會阻塞
  • 三、DelayQueue中的元素只有當其指定的延遲時間到了,纔可以從隊列中獲取到該元素。不然線程阻塞。
  • 四、DelayQueue中的元素不能爲null
  • 五、DelayQueue內部是使用PriorityQueue實現的。compareTo()比較後越小的越先取出來。

注:上文描述,參考自文末的參考連接中第2條連接。ios

二、先後端分離方案

  • 前端爲html,利用ajax(後期改成axios)來請求json交互,restful風格
  • 後端以springboot爲基礎框架,接口暴露爲適應跨域要求,利用在控制層添加@CrossOrigin註解實現
  • 功能抽象:登錄、登錄後頁面查看(須要鑑權)

三、存在的問題

  • 由於跨域問題,JSESSIONID每次請求都會變化,致使後端沒法維護一個合適的session
  • 故須要簡便、快速、低成本的一種方法,來實現session的存儲與維護。

四、方案

4.一、session的特色

  • 一、以惟一鍵key來插入和獲取對象
  • 二、session有自動過時時間,到期後系統會自動清理。
  • 三、每次新的請求過來獲取session,該key值過時時間重置

4.二、DelayQueue的設計

  • 一、採用concurrentHashmap來保存session信息
  • 二、採用DelayQueue延遲隊列來存儲concurrentHashmap中的key
  • 三、模擬會話監聽器,即sessionListener,專門開啓一個守護線程(阻塞式take)從DelayQueue隊列中獲取過時的指針,再根據指針刪除concurrentHashmap中對應元素。

4.三、登錄方案的設計

  • 一、sessionId的設計,可利用uuid或其餘規則來實現。
  • 二、在後臺的登錄方法中,若用戶名與密碼正確,則生成該sessionId,按照必定格式返回給前臺。
  • 三、前臺接收到該sessionId後,可存儲到cookie中,將其封裝到httpheader中,後續請求均附帶該header

五、實際代碼

5.一、前臺請求的發送

  • 前臺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.二、後臺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方法運行,輸出爲:ajax

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.三、登錄成功後頁面鑑權

利用aop的環繞aroud,在請求過來時,查看該sessionId是否存在該delayQueue中,簡要代碼以下:spring

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返回封裝的類,不影響閱讀,故代碼未貼出來。apache

六、其餘

參考連接:json

相關文章
相關標籤/搜索