JAVA設計模式之責任鏈模式案例之風控評估因子的實現

責任鏈模式

顧名思義,責任鏈模式(Chain of Responsibility Pattern)爲請求創建了一個接收者對象的鏈。這種模式給予請求的類型,對請求的發送者和接收者進行解耦。這種類型的設計模式屬於行爲型模式。

在這種模式中,通常每個接收者都包含對另一個接收者的引用。如果一個對象不能處理該請求,那麼它會把相同的請求傳給下一個接收者,依此類推。

介紹

意圖:避免請求發送者與接收者耦合在一起,讓多個對象都有可能接收請求,將這些對象連接成一條鏈,並且沿着這條鏈傳遞請求,直到有對象處理它爲止。

主要解決:職責鏈上的處理者負責處理請求,客戶只需要將請求發送到職責鏈上即可,無須關心請求的處理細節和請求的傳遞,所以職責鏈將請求的發送者和請求的處理者解耦了。

何時使用:在處理消息的時候以過濾很多道。

如何解決:攔截的類都實現統一接口。

關鍵代碼:Handler 裏面聚合它自己,在 HanleRequest 裏判斷是否合適,如果沒達到條件則向下傳遞,向誰傳遞之前 set 進去。

應用實例: 1、紅樓夢中的"擊鼓傳花"。 2、JS 中的事件冒泡。 3、JAVA WEB 中 Apache Tomcat 對 Encoding 的處理,Struts2 的攔截器,jsp servlet 的 Filter。

優點
1、降低耦合度。它將請求的發送者和接收者解耦。
2、簡化了對象。使得對象不需要知道鏈的結構。
3、增強給對象指派職責的靈活性。通過改變鏈內的成員或者調動它們的次
序,允許動態地新增或者刪除責任。
4、增加新的請求處理類很方便。

缺點
1、不能保證請求一定被接收。
2、系統性能將受到一定影響,而且在進行代碼調試時不太方便,可能會造成循環調用。
3、可能不容易觀察運行時的特徵,有礙於除錯。

使用場景
1、有多個對象可以處理同一個請求,具體哪個對象處理該請求由運行時刻自動確定。
2、在不明確指定接收者的情況下,向多個對象中的一個提交一個請求。
3、可動態指定一組對象處理請求。

注意事項:在 JAVA WEB 中遇到很多應用。

舉例:

⽤戶登錄⻛險評估:

異地登錄認定有⻛險(不在⽤戶經常登陸地)
登錄的位移速度超過正常值,認定有⻛險
更換設備,認定有⻛險
登錄時段-習慣,出現了異常,存在⻛險
每天的累計登錄次數超過上限,認定有⻛險
密碼輸⼊錯誤或者輸⼊差異性較⼤,認定⻛險
如果⽤戶的輸⼊特徵(輸⼊每個控件所需時⻓ ms)發⽣變化

在進行正式使用責任鏈之前風險評估需要提前準備幾個實體類

1: 用來封裝用戶本次登錄數據的實體類

//INFO 2020-03-31 10:12:00 QQ EVALUATE [張三] 6ebaf4ac780f40f486359f3ea6934620 "12355421" Beijing "116.4,39.5"
//[1200,15000,2100] "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36"
public class EvaluateData implements Serializable {
    private long evaluateTime;   //時間
    private String applicationName;   // 應⽤名
    private String userIdentify;  //⽤戶唯⼀標識
    private String loginSequence;  //登錄序列號
    private String ordernessPassword;  //密碼
    private String cityName;    //城市
    private GeoPoint geoPoint;   //經緯度實體類
    private Double[] inputFeatures;  //輸入特徵
    private String deviceInformation; //設備

2: 用來封裝用戶登錄成功的數據的實體類

//INFO 2020-03-31 10:12:00 QQ SUCCESS [張三] 6ebaf4ac780f40f486359f3ea6934620 "12355421" Beijing "116.4,39.5"
//[1200,15000,2100] "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36"
public class LoginSuccessData implements Serializable {
    private long evaluateTime;
    private String applicationName;
    private String userIdentify;
    private String loginSequence;
    private String ordernessPassword;
    private String cityName;
    private GeoPoint geoPoint;
    private Double[] inputFeatures;
    private String deviceInformation;

3: 用來封裝記錄用戶登錄的歷史狀態 根據後面的需求進行添加修改

public class HistoryData implements Serializable {
    private Set<String> historyCities; //登錄過的歷史 城市集合
    private Set<String> historyDeviceInformations; // 設備
    private Integer currentDayLoginCount;// 一天登錄的次數
    //        星期幾       幾點     幾次
    private Map<String,Map<String,Integer>> historyLoginTimeSlot;//歷史上登錄的時間

    //歷史密碼合集
    private Set<String> historyOrdernessPasswords;

4: 用來封裝狀態情況的枚舉類型的實例類

/**
 * 聲明所有的風險元素
 */
public enum  RiskFactor {
    /**
     * @AREA  登陸地
     * @DEVICE  設備
     * @TOTAL   累計登錄次數
     * @TIMESLOT   登錄時段
     * @SIMILARITY   密碼相似度
     * @INPUTFEATURE   輸入特徵
     * @SPEED  位移速度
     * */

    AREA("area"),DEVICE("device"),
    TOTAL("total"),TIMESLOT("timeslot"),
    SIMILARITY("similarity"),INPUTFEATURE("inputfeature"),
    SPEED("speed");

    private String name;
    RiskFactor(String name){
       this.name=name;
    }//構造方法

}

5: 用來封裝傳遞的評估因子的實例類

/評估報告
public class EvaluateReport implements Serializable {
    private String applicationName;
    private String userIdentify;
    private String loginSequence;
    private long evaluateTime;
    private String cityName;
    private GeoPoint geoPoint;
    //          評估因子
    private Map<RiskFactor,Boolean> metrics=new HashMap<RiskFactor,Boolean>();

    public void signReport(RiskFactor riskFactor,boolean flag){
        metrics.put(riskFactor,flag);
    }

    public EvaluateReport(String applicationName, String userIdentify, String loginSequence, long evaluateTime, String cityName, GeoPoint geoPoint) {
        this.applicationName = applicationName;
        this.userIdentify = userIdentify;
        this.loginSequence = loginSequence;
        this.evaluateTime = evaluateTime;
        this.cityName = cityName;
        this.geoPoint = geoPoint;

        //初始化所有風險因子都是false
        metrics.put(RiskFactor.AREA,false);
        metrics.put(RiskFactor.DEVICE,false);
        metrics.put(RiskFactor.SIMILARITY,false);
        metrics.put(RiskFactor.SPEED,false);
        metrics.put(RiskFactor.TIMESLOT,false);
        metrics.put(RiskFactor.INPUTFEATURE,false);
        metrics.put(RiskFactor.TOTAL,false);
    }

    /**
     *                                                  AREA DEVICE INPUTFEATURE SIMILARITY SPEED TIMESLOT TOTAL
     * QQ zhangsan 001 1585640451510 Beijing 116.4,39.5 true false    false        false    true   false     false
     * @return
     */
     @Override
     public String toString(){
         //把風險因子按順序拿
         String report=metrics.keySet()  //拿到keySet 然後
                 .stream()//轉成流
                 .sorted((RiskFactor o1, RiskFactor o2) -> o1.name().compareTo(o2.name()))// 對所有的key 進行排列
                 .map(riskFactor -> metrics.get(riskFactor)+"")//把每個key轉化成它對應的風險值
                 .reduce((v1,v2)->v1+" "+v2) //轉成一長流的字符串
                 .get();
//         for (RiskFactor riskFactor : metrics.keySet()
//                 .stream()
//                 .sorted((RiskFactor o1, RiskFactor o2) -> o1.name().compareTo(o2.name())).collect(Collectors.toList())) {
//             System.out.println(riskFactor);
//         }

          return applicationName+" "+userIdentify+" "+loginSequence+" "+evaluateTime+" "+cityName+ " "+geoPoint.getLongtitude()+","+geoPoint.getLatitude()+" "+report;

     }

    public static void main(String[] args) {
        System.out.println( new EvaluateReport("QQ","zhangsan","001",new Date().getTime(),"Beijing",new GeoPoint(116.4,39.5)));
    }

}

責任鏈

步驟一 : 首先創建抽象的類作爲責任鏈的鏈條。

public abstract class Evaluate {
    //都是做什麼評估的   風險因子是什麼
    private RiskFactor RiskFactor;
    public RiskFactor getRiskFactor() {
        return RiskFactor;
    }
    public Evaluate(com.baizhi.enties.RiskFactor riskFactor) {
        RiskFactor = riskFactor;
    }
    /**
     * 對輸入的數據進行評估
     * @evaluateData 需要評估的數據   登錄的數據
     * @historyData  歷史數據
     * @evaluateReport  評估報告
     * @evaluateChain  責任鏈條  負責驅動下一個Evaluate  責任調度
     * */
    public abstract void eval(EvaluateData evaluateData, HistoryData historyData,
                              EvaluateReport evaluateReport,EvaluateChain evaluateChain);
}

步驟二: 創建觸發鏈條並記錄觸發次數,和觸發下個責任的記錄器類

public class EvaluateChain {
    //pos  位置
   private int position=0;
   // 持有所有的Evaluate 的實例
   private List<Evaluate> evaluates;
    public EvaluateChain(List<Evaluate> evaluates) {
        this.evaluates = evaluates;
    }
    // 觸發鏈的執行  評估類的數據要傳過來
    public void daChain(EvaluateData evaluateData, HistoryData historyData,
                        EvaluateReport evaluateReport){
        //判斷是否已經調完Evaluate
        if(position<evaluates.size()){
            //獲取一個責任
            Evaluate evaluate = evaluates.get(position);
            position+=1; //調的位置每調一次加一
            //往下傳
            evaluate.eval(evaluateData,historyData,evaluateReport,this);
        }
    }
}

步驟三: 寫要觸發的責任類
異地登錄評估(☆)

/**
 * 異地登錄評估(☆)
 * 評估數據:當前登錄城市
 * 歷史數據: 留存⽤戶曾經登錄過的城市Set集合】
 * */
public class AreaEvaluate extends Evaluate {
    public AreaEvaluate() {
       super(RiskFactor.AREA);
    }


    @Override
    public void eval(EvaluateData evaluateData, HistoryData historyData,
                     EvaluateReport evaluateReport, EvaluateChain evaluateChain) {
        evaluateReport.signReport(getRiskFactor(),
                doEval(evaluateData.getCityName(),historyData.getHistoryCities()));

        //驅動調用下一個評估
        evaluateChain.daChain(evaluateData,historyData,evaluateReport);
    }

    //                       目前登錄的城市         歷史上登錄的城市集合
    public boolean doEval(String cityName, Set<String> historyCities){
        if(historyCities==null || historyCities.size()==0){//說明是第⼀次使⽤
            return false;
        }else{//如果登錄過該城市就沒有⻛險
            return !historyCities.contains(cityName);
        }
    }
}

更換設備評估(☆)

/**
 *更換設備評估(☆)
 * 評估數據:當前登錄設備信息
 * 歷史數據: ⽤戶最近登錄過的N個設備-去重
 * */
public class DeviceEvaluate extends Evaluate {


    public DeviceEvaluate() {
        super(RiskFactor.DEVICE);
    }

    @Override
    public void eval(EvaluateData evaluateData, HistoryData historyData, EvaluateReport evaluateReport, EvaluateChain evaluateChain) {
        evaluateReport.signReport(getRiskFactor(),doDevice(evaluateData.getDeviceInformation(),historyData.getHistoryDeviceInformations()));
        //驅動調用下一個評估
        evaluateChain.daChain(evaluateData,historyData,evaluateReport);
    }

    public boolean doDevice(String device, Set<String> historyDeviceInformations){
        if (historyDeviceInformations==null | historyDeviceInformations.size()==0){
            return false;
        }else return !historyDeviceInformations.contains(device);

    }
}

當天累計登錄N次評估(☆)

/**
 * 當天累計登錄N次評估(☆)
 * 評估數據:-
 * 歷史數據: 獲取當天⽤戶累計登錄次數
 * */
public class TotalEvaluate extends Evaluate {
    private Integer threshold=0;

    /**
     * 允許用戶最大的登錄次數
     * @param threshold
     */
    public TotalEvaluate(Integer threshold) {
        super(RiskFactor.TOTAL);
        this.threshold=threshold;
    }

    @Override
    public void eval(EvaluateData evaluateData, HistoryData historyData,
                     EvaluateReport evaluateReport, EvaluateChain evaluateChain) {

    }

    public boolean toTotal(Integer currentDayLoginCount){
        if(currentDayLoginCount==0){//第一次登
            return false;
        }else return currentDayLoginCount>threshold;
    }
}

登錄時段習慣評估(☆☆)

/**
 *登錄時段習慣評估(☆☆)
 * 評估數據:獲取當前評估時間 dayOfWeek 、 hourOfDay
 * 歷史數據: 留存⽤戶的所有的歷史登錄數據 dayOfWeek 、 hourOfDay 、 count
 * 需要從⽤戶的歷史登錄時段中,計算出哪些時段是⽤戶的登錄習慣。
 * */
public class TimeSlotEvaluate extends Evaluate {
    private  Integer threshold;

    public TimeSlotEvaluate(Integer threshold) {
        super(RiskFactor.TIMESLOT);
        this.threshold = threshold;
    }

    @Override
    public void eval(EvaluateData evaluateData, HistoryData historyData, EvaluateReport evaluateReport, EvaluateChain evaluateChain) {
        evaluateReport.signReport(getRiskFactor(),toTimeSlot(evaluateData.getEvaluateTime(),historyData.getHistoryLoginTimeSlot(),threshold ));


        evaluateChain.daChain(evaluateData,historyData,evaluateReport);
    }

    /**
     * @param evaluateTime
     * @param historyLoginTimeSlot
     * @param threshold :設定多累計登錄多少次以後再對用戶進行評估
     */
    public boolean toTimeSlot(long evaluateTime, Map<String, Map<String,Integer>> historyLoginTimeSlot, int threshold){
        String[] WEEKS={"星期日","星期一","星期二","星期三","星期四","星期五","星期六"};
        Calendar calendar = Calendar.getInstance();  //獲取一個日曆的實例
        calendar.setTimeInMillis(evaluateTime); //將登錄的時間存進去
        //Calendar.DAY_OF_WEEK  會獲取到一個1~7 的數字 代表星期幾  從WEEKS 數組中取相應的字符串
        String dayOfWeek = WEEKS[calendar.get(Calendar.DAY_OF_WEEK) - 1];
        //時間格式 小時 以兩位數字表示
        DecimalFormat decimalFormat=new DecimalFormat("00");
        //登錄的時間  幾點
        String hourOfDay=  decimalFormat.format(calendar.get(Calendar.HOUR_OF_DAY));//01 02 ... 24

        //用戶第一次登錄
        if(historyLoginTimeSlot==null || historyLoginTimeSlot.size()==0){
            return false;
        }
        //用戶是否達到評估計算閾值標準,如果登錄總次小於閾值,不做評估
        if(!historyLoginTimeSlot.containsKey(dayOfWeek)){  // 判斷 本次登錄的星期 之前是否有同星期的登錄記錄
            // 沒有則看看是否到達閾值
            Integer totalCount = historyLoginTimeSlot.entrySet() // 將歷史各個時間的登錄相加 得到目前的總登錄次數
                    .stream()// 星期幾  Map<小時,次數>
                    .map(t -> t.getValue().entrySet().stream().map(v -> v.getValue()).reduce((v1, v2) -> v1 + v2).get()) // 每天登錄總數
                    .reduce((v1, v2) -> v1 + v2)
                    .get();

            return totalCount >= threshold; // 沒到達閾值 不進行評估 返回false 到達閾值 本次登錄不在之前的習慣星期內 返回true
        }else{//有的話 去else 查看登錄時間段是否符合
            //獲取歷史上這個星期X 當天的登錄時段數據
            Map<String, Integer> historyTimeSlot = historyLoginTimeSlot.get(dayOfWeek);
            if(!historyTimeSlot.containsKey(hourOfDay)){//該天登錄過,但是在該時段沒有登錄
                return true;
            }else{//該天登錄過,但是在該時段也登錄過

                //判斷當前的登錄時段是否使用戶的登錄習慣 獲取這個時間登錄過幾次
                Integer currentHourLoginCount = historyTimeSlot.get(hourOfDay);

                //升序登錄時間段集合
                List<Integer> sortedList = historyTimeSlot.entrySet()
                        .stream()
                        .map(t -> t.getValue()) //每個時段登錄總和
                        .sorted().collect(Collectors.toList());

                //獲取用戶登錄時段的閾值,大於或者等於該值都是習慣  (獲取當天各時間段登錄次數前三分之一的最後一個的次數 ,大於或等於這個次數便是習慣)
                Integer thresholdTimeSlotCount=sortedList.get((sortedList.size()*2)/3);
                return  currentHourLoginCount<thresholdTimeSlotCount;
                /*
                //計算出所有登錄總次數大於thresholdTimeSlotCount值所有hourOfDay集合-習慣時段
                List<String> habbitTimeSlot = historyTimeSlot.entrySet()
                        .stream()
                        .filter(t -> t.getValue() >= thresholdTimeSlotCount)
                        .map(t -> t.getKey())
                        .collect(Collectors.toList());

                return !habbitTimeSlot.contains(hourOfDay);
                */
            }
        }
    }
}

後面還有難度較高 會涉及到算法 單獨其他文章來寫

密碼相似度評估(☆☆☆)
⽤戶輸⼊特性評估(☆☆☆☆)
登錄位移速度評估(☆☆☆☆)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章