責任鏈模式
顧名思義,責任鏈模式(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);
*/
}
}
}
}
後面還有難度較高 會涉及到算法 單獨其他文章來寫
密碼相似度評估(☆☆☆)
⽤戶輸⼊特性評估(☆☆☆☆)
登錄位移速度評估(☆☆☆☆)