天穹gateway网关系列3用户自定义动态filter
开源地址
https://github.com/XiaoMi/mone一、为什么需要用户自定义动态filter
在系列文章《如何设计filter链》里我们介绍了filter的设计思路以及它们是如何被加载串联为一条链路的。这些filter目前都是网关里内置的filter,比如我们默认支持日志filter,mock filter等。但在实际的使用中,很多情景用户是需要可以自定义过滤器以满足一些自己的功能要求。所以自定义过滤器就非常有必要了。
更进一步,动态加载这些自定义filter也是必须的,如果新增一个自定义filter就需要重启我们的网关集群来更新filter链路,很显然是不被接受的。二、核心设计与实现1、总体设计
添加自定义filter: 在网关控制台上传自定义filter,后台解析代码,分析出FilterDef(能唯一定义一个filter) 。生成一条filter的记录。编译自定义filter: 将上传的代码进行编译,并存储到文件服务器,方便gateway集群拉取到jar包。审核自定义filter: filter是用户自定义的,并且会被加载到网关集群,所以一定要review一下代码进行审核。启用自定义filter: 在上述步骤完成之后,就可以启用filter使其生效了。2、编写自定义filter
所有用户自定义filter都需要实现抽象类CustomRequestFilter,CustomRequestFilter实现了RequestFilter。用户filter只需要实现CustomRequestFilter里的execute方法即可。public abstract class CustomRequestFilter extends RequestFilter { @Override public final FullHttpResponse doFilter(FilterContext context, Invoker invoker, ApiInfo apiInfo, FullHttpRequest request) { if (this.allow(apiInfo)) { try { context.setNext(false); return execute(context, invoker, apiInfo, request); } catch (Throwable ex) { log.error("invoke custom filter:{} error:{}", this.getDef().getName(), ex.getMessage()); //filter chain 已经执行过了,不再第二次执行了 if (!context.isNext()) { return invoker.doInvoker(context, apiInfo, request); } return HttpResponseUtils.create(Result.fromException(ex)); } } else { return invoker.doInvoker(context, apiInfo, request); } } public FullHttpResponse next(FilterContext context, Invoker invoker, ApiInfo apiInfo, FullHttpRequest request) { context.setNext(true); return invoker.doInvoker(context, apiInfo, request); } public abstract FullHttpResponse execute(FilterContext context, Invoker invoker, ApiInfo apiInfo, FullHttpRequest request); } 复制代码
下图是一个实际的filter例子,可以看到处理逻辑都写到了execute方法里面。resources里面的FilterDef唯一定义该filter。
在实际编写自定义filter时,用户可能会用到更加丰富的能力,比如使用调用一个rpc接口,获取一个动态配置等,为此我们在RequestFilter里提供了getBean方法以获取这些能力。在filter中引入dubbo
Dubbo dubbo = this.getBean(Dubbo.class);
举例:MethodInfo methodInfo = new MethodInfo(); methodInfo.setServiceName("com.xiaomi.planet.user.module.api.service.SpecialUserService"); methodInfo.setMethodName("testMethod"); methodInfo.setGroup("staging"); methodInfo.setVersion("1.0"); methodInfo.setParameterTypes(new String[] { "java.lang.Integer", "java.lang.Integer" }); methodInfo.setArgs(new Object[] { 1, 1 }); Object result = dubbo.call(methodInfo); 复制代码在filter中引入 nacos
Nacos nacos = this.getBean(Nacos.class);
举例:NacosConfig nacosConfig = new NacosConfig(); nacosConfig.setDataId(configKey); nacosConfig.setGroupId("DEFAULT_GROUP"); String config = nacos.getConfig(nacosConfig); 复制代码在filter中获取请求参数和header//处理get Map queryParams = HttpRequestUtils.getQueryParams(request.uri()); //处理post String postStr = new String(HttpRequestUtils.getRequestBody(request)); //处理表单 HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(new DefaultHttpDataFactory(false), request); List postData = decoder.getBodyHttpDatas(); for (InterfaceHttpData data : postData) { if (data.getHttpDataType() == InterfaceHttpData.HttpDataType.Attribute) { MemoryAttribute attribute = (MemoryAttribute) data; kv.put(attribute.getName(), attribute.getValue()); } } //处理header FullHttpRequest request.headers() 复制代码在filter里返回自定义结果//返回HttpResponseUtils.create(),举例 return HttpResponseUtils.create(Result.fail(GeneralCodes.Forbidden, HttpResponseStatus.FORBIDDEN.reasonPhrase())); 复制代码在filter里区分环境// envGroup值有3种:staging, online(线上外网), intranet(线上内网) String env = filterContext.getAttachment("envGroup", "staging"); 复制代码其他filter里的一些处理//获取filter传递进来的参数 filterParams = this.getFilterParams(apiInfo); //在实际调用下游之前的一些代码 //...省略代码... //实际调用下游 next(context, invoker, apiInfo, request) //在实际调用下游之后的一些代码 //...省略代码... 复制代码3、动态加载自定义filter
在编写好自定义filter并上传审核完成后,控制台会广播通知gateway集群里的每个节点,有新的filter加入,是时候reload filterchain了。
在第一节RequestFilterChain的reload方法基础上,我们加入加载自定义filter的逻辑吧。入口还是reload方法,它在获取用户定义的filter列表时,调用了FilterManager的getUserFilterList方法。热加载filter的逻辑我们都写到了FilterManager里面。@Slf4j @Component public class RequestFilterChain implements IRequestFilterChain { @Autowired private ApplicationContext ac; @Autowired private FilterManager filterManager; private final CopyOnWriteArrayList filterList = new CopyOnWriteArrayList<>(); //加载filter public void reload(String type, List names) { log.info("reload filter"); //获取系统定义的filter Map map = ac.getBeansOfType(RequestFilter.class); List list = new ArrayList<>(map.values()); log.info("system filter size:{}", list.size()); //获取用户定义的filter List userFilterList = filterManager.getUserFilterList(type, names).stream() .filter(it -> filterUserFilterWithGroup(it)).collect(Collectors.toList()); log.info("user filter size:{} type:{} names:{}", userFilterList.size(), type, names); list.addAll(userFilterList); list = sortFilterList(list); //...省略部分代码... } } 复制代码
FilterManager的getUserFilterList方法
(getUserFilterList -> loadRequestFilter -> loadFilter)//省略一部分代码,可前往https://github.com/XiaoMi/mone/tree/master/gateway-all查看 public class FilterManager { public List getUserFilterList(String type, List names) { try { if (!configService.isAllowUserFilter()) { log.info("skip user filter"); return Lists.newArrayList(); } //将老的filter jar包删除 deleteOldFilter(type, names); //从文件中心将编译好的filter jar包下载到本地 downloadFilter(type, names); List jarList = getJarPathList(); log.info("jarList:{}", jarList); //热加载filter return loadRequestFilter(jarList); } catch (Throwable ex) { log.error("getUserFilterList ex:{}", ex.getMessage()); return Lists.newArrayList(); } } public List loadRequestFilter(List pathNameList) { if (pathNameList.size() == 0) { return Lists.newArrayList(); } try { URL[] urls = pathNameList.stream().map(p -> { try { return new URL("file:" + p); } catch (MalformedURLException e) { log.error(e.getMessage()); } return null; }).filter(it -> null != it).toArray(URL[]::new); return Arrays.stream(urls).map(url -> { try { log.info("load request filter url:{}", url); URLClassLoader classLoader = new URLClassLoader(new URL[]{url}); return loadFilter(url.getFile(), classLoader); } catch (Throwable e) { log.error("load filter error, url: {}, msg: {}", url, e.getMessage(), e); } return null; }).filter(it -> null != it).collect(Collectors.toList()); } catch (Throwable ex) { log.error(ex.getMessage(), ex); } return Lists.newArrayList(); } public RequestFilter loadFilter(String url, URLClassLoader classLoader) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException { String content = ZipUtils.readFile(url, "FilterDef"); Properties properties = new Properties(); properties.load(new StringInputStream(content)); String filterClass = properties.getProperty("filter"); Class<?> clazz = classLoader.loadClass(filterClass); RequestFilter ins = (RequestFilter) clazz.newInstance(); String name = properties.getProperty("name"); String author = properties.getProperty("author"); String groups = properties.getProperty("groups"); log.info("loadFilter, name:{}, author:{}, groups:{} ", name, author, groups); classLoaderMap.put(name, classLoader); ins.setDef(new FilterDef(0, name, author, groups)); ins.setGetBeanFunction(getBean()); return ins; } } 复制代码
至此,用户可以随时增加一个新的gateway filter,或者更新那些已经存在的filter,而不用进行任何重启。4、使用业务自定义filter
在添加实际的apiinfo接口时,选择适合你接口的filter启用吧。
西湖的白堤和孤山古时候的文人们咏断桥残雪,十有八九会提到孤山,这大概是因为孤山和断桥原本就是连在一起的缘故吧。怎么个连法?听我慢慢说来。谁都知道,西湖上有个白堤。这白堤东起北山路的东段,从东至西横
沃尔玛回应火腿贴了一层瘦肉里面全是骨头系行业做法近日,一名男子在网络平台发布视频,称从沃尔玛购入的火腿贴了一层瘦肉里面全是骨头,引发关注。12月3日,涉事门店告诉南都记者,已有相关部门到店进行调查,门店表示产品不存在质量问题。同
一个人的时候,也要好好吃饭美食家蔡澜在今天也要好好吃饭中写过好的人生,从好好吃饭开始,好好吃饭,就是好好爱自己。人生的意义就是吃吃喝喝,就这么简单和基本。人们总是耗费太多力气追求世俗成功与他人认可,却忽视了
巴塘风云头条创作挑战赛我准备退隐江湖,每天打打杀杀,担惊受怕地过日子,不如干点正当的事情,没有风险,也不会在刀口上讨生活。躲过98年的严打,进入了2000年。我一个朋友打电话来叫我去甘孜州
关于葡萄酒的这些事,很多人知道的并不全秋季是葡萄收获的季节,葡萄不仅可以作为水果食用,还能酿造出美味的葡萄酒。虽然现在市售的葡萄酒也很多,但仍然有一部分人坚持自酿葡萄酒,认为自己做的葡萄酒是最安全最美味的。有的人喝葡萄
硬核的几种饮料中午起来朋友发微信让我写点关于调酒的,小众饮料和什么酒绝配?其实把我难住了一下,因为我并不怎么喝饮料,更别说小众的饮料但可以推荐你一些,大众的饮料配什么酒很好喝(不敢说绝配)。各种
养生食谱推荐腊八粥原料糯米150克,黑米100克,红豆薏米各80克,红枣绿豆各50克,花生25克,葡萄干15克。调料冰糖红糖各150克。制作步骤1黑米糯米放入清水盆中浸泡3小时,再换清水洗净。2薏米
最强寒潮湿冷魔法来袭!靠一身正气抗冷的广东人,少不了这些告诉大家一个好消息!姐终于脱单了!开始穿两件今年最强寒潮带着湿冷魔法来了广东的冬天终于有点参与感了靠一身正气御寒的广东人在这方面有自己的心得所谓不时不吃,时令美食安排!吃完这几样,
巩义石窟,一处小众秘境般的存在造像嘴角迷人的微笑去年回乡探亲,我陪老妈游赏了巩义石窟寺,真没想到,我的家乡还有这么一座北魏留下的古代石窟。它虽然无法和四大石窟相比,但是,走进它你会惊讶地发现,原来这里也是一处小
当所有人都静默在家,只有你愿独与大理幽会书中未谋面的大理,你曾幻想着从明天起,做一个幸福的人。喂马,劈柴,周游世界。从明天起,关心粮食和蔬菜。我有一所房子,面朝大海,春暖花开。与你相见的第一面,大理私语着我是所有人的精神
自力村,2007年被列入世界文化遗产名录广东第1处世界遗产地自力村,位于江门市开平市中部塘口镇强亚行政村,始建于清道光十七年(1837年),村名寓意为自力更生。世居村民以方姓为主。自力村2001年被评为全国重点文物保护单位,2005年被评为