笔者工作中制备过一个Rest服务,其中有预处理、服务中止验证、黑名单验证、实际数据返回四个模块,写在一起显得庞杂冗长,维护难度较大。
受公众号“捡田螺的小男孩”文章“实战!工作中用到哪些设计模式”一文中职责链部分的启发,将服务代码改写一遍。
改完后觉得这次该写还是挺有效的,各块权责分明了,还能有效糅合在一起,故撰文记之。
首先需要制备一个handler抽象类,作为四种处理的基类。
import java.util.Map; import javax.servlet.http.HttpServletRequest; public abstract class FutureHandler { private FutureHandler nextHandler; public void setNextHandler(FutureHandler nextHandler) { this.nextHandler = nextHandler; } public FutureHandler getNextHandler() { return nextHandler; } public boolean filter(HttpServletRequest rqst,Map<String,Object> map) { boolean result=doFilter(rqst,map); if(result==true) { if(nextHandler!=null) { return nextHandler.filter(rqst,map); }else { return true; } }else { return false; } } abstract boolean doFilter(HttpServletRequest rqst,Map<String,Object> map); }
这个类的作用是处理正确就往下nexthandler传, filter函数是精髓所在,它成功的把事做完往下传递的逻辑和各自做事分离开来。
然后是四个子类:
第一个类:预处理类
import java.time.LocalDate; import java.time.LocalTime; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import com.hy.myapp.util.RestUtil; @Component @Order(0) public class PreHandler extends FutureHandler{ @Override boolean doFilter(HttpServletRequest rqst,Map<String, Object> map) { long startMs=System.currentTimeMillis(); map.put("startMs", startMs); final String CODE="8974"; map.put("Interface ID", CODE); String startTime=LocalDate.now()+" "+LocalTime.now(); map.put("startTime", startTime); String visitorIp=RestUtil.findVisitorIpFrom(rqst); map.put("visitorIp", visitorIp); return true; } }
这个类摆在第一位,主要是取时间取IP等,不会出现不成功的情况,于是末尾直接返回true。
中断处理器类:
import java.time.LocalDate; import java.time.LocalTime; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import com.hy.myapp.service.FutureService; import com.hy.myapp.util.DateTimeUtil; @Component @Order(1) public class InterruptHandler extends FutureHandler{ @Autowired private FutureService ftService; @Override boolean doFilter(HttpServletRequest rqst,Map<String, Object> map) { String code=map.get("Interface ID")+""; if(ftService.isCodeDisabled(code)==true) { map.put("code", "301"); map.put("msg", "该ID代表的服务已经被中止使用"); String endTime=LocalDate.now()+" "+LocalTime.now(); map.put("endTime", endTime); long startMs=Long.parseLong(map.get("startMs")+""); long endMs=System.currentTimeMillis(); map.put("timeElapsed", DateTimeUtil.ms2DHMS(startMs,endMs)+""); return false; } return true; } }
这个类摆在第二位,主要检查某服务code是否被禁用,在数据库里有一张表来决定code的disabled状态是1还是0.
黑名单类:
import java.time.LocalDate; import java.time.LocalTime; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import com.hy.myapp.service.FutureService; import com.hy.myapp.util.DateTimeUtil; @Component @Order(2) public class BlacksheetHandler extends FutureHandler{ @Autowired private FutureService ftService; @Override boolean doFilter(HttpServletRequest rqst,Map<String, Object> map) { String code=map.get("Interface ID")+""; String ip=map.get("visitorIp")+""; if(ftService.isIpBlocked(code,ip)==true) { map.put("code", "302"); map.put("msg", "您的IP在黑名单中,进一步访问已被中止。"); String endTime=LocalDate.now()+" "+LocalTime.now(); map.put("endTime", endTime); long startMs=Long.parseLong(map.get("startMs")+""); long endMs=System.currentTimeMillis(); map.put("timeElapsed", DateTimeUtil.ms2DHMS(startMs,endMs)+""); return false; } return true; } }
这个类摆在第三位,主要职责是看请求IP是否在黑名单中,在数据库中有一张黑名单表,如果服务code和ip能查到的记录数大于0,则说明请求ip在黑名单。
最后就是实际处理类:
import java.time.LocalDate; import java.time.LocalTime; import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import com.hy.myapp.util.DateTimeUtil; class VarietyInfo { // 名称 private String name; // 交易单位 private String unit; // 振幅 private String amplitude; public VarietyInfo(String name,String unit,String amplitude) { this.name=name; this.unit=unit; this.amplitude=amplitude; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getUnit() { return unit; } public void setUnit(String unit) { this.unit = unit; } public String getAmplitude() { return amplitude; } public void setAmplitude(String amplitude) { this.amplitude = amplitude; } } @Component @Order(3) public class VarietyHandler extends FutureHandler{ @Override boolean doFilter(HttpServletRequest rqst,Map<String, Object> map) { map.put("code", "303"); map.put("msg", "顺利取得数据"); List<VarietyInfo> datas=new ArrayList<VarietyInfo>(); datas.add(new VarietyInfo("黄大豆/A", "10吨/手", "1元/吨")); datas.add(new VarietyInfo("大豆二号/B", "10吨/手", "1元/吨")); datas.add(new VarietyInfo("豆粕/M", "10吨/手", "1元/吨")); datas.add(new VarietyInfo("豆油/Y", "10吨/手", "2元/吨")); datas.add(new VarietyInfo("玉米/C", "10吨/手", "1元/吨")); datas.add(new VarietyInfo("玉米淀粉/CS", "10吨/手", "1元/吨")); datas.add(new VarietyInfo("聚乙烯/L", "5吨/手", "5元/吨")); datas.add(new VarietyInfo("棕榈油/P", "10吨/手", "2元/吨")); datas.add(new VarietyInfo("聚氯乙烯/V", "5吨/手", "5元/吨")); datas.add(new VarietyInfo("冶金焦炭/J", "100吨/手", "0.5元/吨")); datas.add(new VarietyInfo("焦煤/JM", "60吨/手", "0.5元/吨")); datas.add(new VarietyInfo("铁矿石/I", "100吨/手", "0.5元/吨")); datas.add(new VarietyInfo("鲜鸡蛋/JD", "5吨/手", "1元/500千克")); datas.add(new VarietyInfo("中密度纤维板/FB","500吨/手","0.05元/张")); datas.add(new VarietyInfo("细木工板/BB", "500吨/手", "0.05元/张")); datas.add(new VarietyInfo("聚丙烯/PP", "5吨/手", "1元/吨")); datas.add(new VarietyInfo("粳米/RR", "10吨/手", "1元/吨")); datas.add(new VarietyInfo("苯乙烯/EB", "5吨/手", "1元/吨")); map.put("data", datas); String endTime=LocalDate.now()+" "+LocalTime.now(); map.put("endTime", endTime); long startMs=Long.parseLong(map.get("startMs")+""); long endMs=System.currentTimeMillis(); map.put("timeElapsed", DateTimeUtil.ms2DHMS(startMs,endMs)+""); return true; } }
这个类是返回实际数据,任务到此完成。
然后,将四个类注入进来。
@RestController public class FutureRestCtrl { @Autowired private List<FutureHandler> hdlList; ... }
Component能决定四个类的示例放到hdlList里,order决定了放入顺序,故hdlList启动后就初始化完毕了。
在请求初,需要初始化handler和后继handler:
@RestController public class FutureRestCtrl { @Autowired private List<FutureHandler> hdlList; private FutureHandler handler; private void initHandlerChain() { for(int i=0;i<hdlList.size();i++) { if(i==0) { handler=hdlList.get(0); }else { FutureHandler curr=hdlList.get(i-1); FutureHandler next=hdlList.get(i); curr.setNextHandler(next); } } } .... }
initHandlerChain的作用就是把四个类的示例首尾串起来,而handler取第一个。
四个类工作起来是这样的:
while(handler!=null) { boolean result=handler.doFilter(rqst,retvalMap); if(result==false) { return retvalMap; }else { handler=handler.getNextHandler(); } }
原作中多个类依次处理,没有返回值和异常处理,这里和原作不同,如果返回值是false就没必要往下了,故做了一个循环判断,而返回值是true就继续往下,直到职责链末端为止。
最终的Rest类如下,可见比原来是苗条多了:
import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; /** * 期货Rest服务 * @author hy * 2021年11月1日 */ @RestController public class FutureRestCtrl { @Autowired private List<FutureHandler> hdlList; private FutureHandler handler; private void initHandlerChain() { for(int i=0;i<hdlList.size();i++) { if(i==0) { handler=hdlList.get(0); }else { FutureHandler curr=hdlList.get(i-1); FutureHandler next=hdlList.get(i); curr.setNextHandler(next); } } } @RequestMapping(value="/tradingVarieties", method=RequestMethod.GET) public Map<String,Object> getVarieties(HttpServletRequest rqst) { initHandlerChain(); Map<String,Object> retvalMap=new LinkedHashMap<>(); while(handler!=null) { boolean result=handler.doFilter(rqst,retvalMap); if(result==false) { return retvalMap; }else { handler=handler.getNextHandler(); } } return retvalMap; } }
以下各种情况出现的返回的json结果说明了职责链作业的正确性。
服务被中止时:
{ "Interface ID": "8974", "startTime": "2021-11-01 13:20:47.219432900", "visitorIp": "0:0:0:0:0:0:0:1", "code": "301", "msg": "该ID代表的服务已经被中止使用", "endTime": "2021-11-01 13:20:47.225423600", "timeElapsed": "6ms" }
访问IP在黑名单中时:
{ "Interface ID": "8974", "startTime": "2021-11-01 13:37:51.289498300", "visitorIp": "0:0:0:0:0:0:0:1", "code": "302", "msg": "您的IP在黑名单中,进一步访问已被中止。", "endTime": "2021-11-01 13:37:51.750911700", "timeElapsed": "462ms" }
返回正确的结果时:
{ "Interface ID": "8974", "startTime": "2021-11-01 13:21:33.431997800", "visitorIp": "0:0:0:0:0:0:0:1", "code": "303", "msg": "顺利取得数据", "data": [ { "name": "黄大豆/A", "unit": "10吨/手", "amplitude": "1元/吨" }, { "name": "大豆二号/B", "unit": "10吨/手", "amplitude": "1元/吨" }, { "name": "豆粕/M", "unit": "10吨/手", "amplitude": "1元/吨" }, { "name": "豆油/Y", "unit": "10吨/手", "amplitude": "2元/吨" }, { "name": "玉米/C", "unit": "10吨/手", "amplitude": "1元/吨" }, { "name": "玉米淀粉/CS", "unit": "10吨/手", "amplitude": "1元/吨" }, { "name": "聚乙烯/L", "unit": "5吨/手", "amplitude": "5元/吨" }, { "name": "棕榈油/P", "unit": "10吨/手", "amplitude": "2元/吨" }, { "name": "聚氯乙烯/V", "unit": "5吨/手", "amplitude": "5元/吨" }, { "name": "冶金焦炭/J", "unit": "100吨/手", "amplitude": "0.5元/吨" }, { "name": "焦煤/JM", "unit": "60吨/手", "amplitude": "0.5元/吨" }, { "name": "铁矿石/I", "unit": "100吨/手", "amplitude": "0.5元/吨" }, { "name": "鲜鸡蛋/JD", "unit": "5吨/手", "amplitude": "1元/500千克" }, { "name": "中密度纤维板/FB", "unit": "500吨/手", "amplitude": "0.05元/张" }, { "name": "细木工板/BB", "unit": "500吨/手", "amplitude": "0.05元/张" }, { "name": "聚丙烯/PP", "unit": "5吨/手", "amplitude": "1元/吨" }, { "name": "粳米/RR", "unit": "10吨/手", "amplitude": "1元/吨" }, { "name": "苯乙烯/EB", "unit": "5吨/手", "amplitude": "1元/吨" } ], "endTime": "2021-11-01 13:21:33.437013300", "timeElapsed": "6ms" }
好了,到这里就要结束了。以上职责链模式如果优雅,部分功劳拜Component和order两个Spring标签所致;如果体会不到上述模式的优雅,那就是您自己的问题了,懂得都懂,无需赘述。上面不足的地方是四个子类及其父类函数参数一致,实际上rqst只给第一个类用了,对于其它三个类是纯摆设,这也是一种代码浪费吧,这个问题留待日后看有没有办法解决。
END