天穹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启用吧。
你遇到过年龄小但是辈分高的事情吗?我遇见过一个笑死人的事。我有两程姓同学结夫妻,他们村(镇上)两人千多口,有一多半姓程一家族。女同学辈分高,论辈婆婆得叫她姑奶,是独生女(老爸五六十捡的)。老爸是抗伤残老兵一等功臣参
退休后做什么事情最让人看不起?我认为退休后做两件事情最让人瞧不起。一是有了老年证后,不管是否坐车高峰期,倚老卖老,让年轻人让座位,还不道谢,在车上高声说话,不顾及他人感受。二是贪图小便宜,买菜时把叶菜扒的干干净
有人说黄河水底有很多宝贝,是真的吗?黄河流域一直有很多传说,其中就有说黄河里有数不尽的宝贝,自古不知道多少英雄好汉冒死寻宝,在黄河里探索甚至潜入黄河底打捞,又说得了多少宝贝。至于真假,众说纷纭,反正无风不起浪,黄河从
家里人出首付在西安买了套房,每月还款3900,我每月工资就4000,怎么办?2016年7月我大专毕业了,2017年12月家里拿首付给我买了套房,买房之前我的月薪是5500,而且实际我是2016年3月就上班了,可是16年整年都混混沌沌,工作一直不顺利,我换了
武汉月薪18。4k是什么水平?武汉市的月薪分五档。一,最高档,富有阶层,12000元以上。占比,百分之五。二,中高档,中产阶层,900012000元。占比,百分之二十。三,中挡,小康阶层,60009000元。占
湖北文理学院能升为重点大学吗?湖北文理学院(HubeiUniversityofArtsandScience)位于山水名城湖北省域副中心城市襄阳市,是省属本科院校,入选湖北省2011计划牵头高校。学校的前身是创办
如果你有两个儿子,一个过得好很有钱,另一个很穷,你会让有钱的那个帮穷的那个吗?我叔家很有钱,我们家相对来说就很穷。因为一些原因,我妈和我婶子将近二十年没有说过一句话,可在我们家需要钱的时候,我叔和我婶子还是二话不说借给我们十五万块钱,即便如此,我妈还是不搭理
部队突围时伤兵怎么安置?这种要分具体情况,就拿你死我活的抗日战争来吧国军部队在极端严峻条件下突围时,无论是野外突围还是向城外突围,重伤员基本都是丢弃在战场上的。这些可怜的重伤员的命运可想而知,往往会死在日
昆工和重邮的综合实力都比成都理工强,为什么后者却是双一流?成都理工有二个国家重点实验室,前二年获得多项国家级大奖。昆工和重邮有几个?科研成果方面和成都理工不是一个档次的,成都理工大学为什么会被评为一流学科高校这个问题我觉得是教育界的一个迷
活在当今社会是什么感觉?只有上世纪五六七十年代的人才知道,今天的幸福生活来之不易!要比当年和现在无法比!要凭感觉,没话说!物质丰富了,但精神疲惫了。活在当今,能挣大钱人开心,挣小钱养家糊口人苦闷,生活水平
在乡镇当公务员,每个月4000多元,该不该辞职?对比一下本地乡镇公务员工资水平,你再考虑要不要辞职笑哭笑哭。科员工资,除去五险一金,一个月2500左右,把车补乡镇补贴等所有补贴都加上,也就3700块钱左右,跟你的4000多还差了