天穹gateway网关系列3用户自定义动态filter
开源地址
https:github。comXiaoMimone一、为什么需要用户自定义动态filter
在系列文章《如何设计filter链》里我们介绍了filter的设计思路以及它们是如何被加载串联为一条链路的。这些filter目前都是网关里内置的filter,比如我们默认支持日志filter,mockfilter等。但在实际的使用中,很多情景用户是需要可以自定义过滤器以满足一些自己的功能要求。所以自定义过滤器就非常有必要了。
更进一步,动态加载这些自定义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方法即可。publicabstractclassCustomRequestFilterextendsRequestFilter{OverridepublicfinalFullHttpResponsedoFilter(FilterContextcontext,Invokerinvoker,ApiInfoapiInfo,FullHttpRequestrequest){if(this。allow(apiInfo)){try{context。setNext(false);returnexecute(context,invoker,apiInfo,request);}catch(Throwableex){log。error(invokecustomfilter:{}error:{},this。getDef()。getName(),ex。getMessage());filterchain已经执行过了,不再第二次执行了if(!context。isNext()){returninvoker。doInvoker(context,apiInfo,request);}returnHttpResponseUtils。create(Result。fromException(ex));}}else{returninvoker。doInvoker(context,apiInfo,request);}}publicFullHttpResponsenext(FilterContextcontext,Invokerinvoker,ApiInfoapiInfo,FullHttpRequestrequest){context。setNext(true);returninvoker。doInvoker(context,apiInfo,request);}publicabstractFullHttpResponseexecute(FilterContextcontext,Invokerinvoker,ApiInfoapiInfo,FullHttpRequestrequest);}复制代码
下图是一个实际的filter例子,可以看到处理逻辑都写到了execute方法里面。resources里面的FilterDef唯一定义该filter。
在实际编写自定义filter时,用户可能会用到更加丰富的能力,比如使用调用一个rpc接口,获取一个动态配置等,为此我们在RequestFilter里提供了getBean方法以获取这些能力。在filter中引入dubbo
Dubbodubbothis。getBean(Dubbo。class);
举例:MethodInfomethodInfonewMethodInfo();methodInfo。setServiceName(com。xiaomi。planet。user。module。api。service。SpecialUserService);methodInfo。setMethodName(testMethod);methodInfo。setGroup(staging);methodInfo。setVersion(1。0);methodInfo。setParameterTypes(newString〔〕{java。lang。Integer,java。lang。Integer});methodInfo。setArgs(newObject〔〕{1,1});Objectresultdubbo。call(methodInfo);复制代码在filter中引入nacos
Nacosnacosthis。getBean(Nacos。class);
举例:NacosConfignacosConfignewNacosConfig();nacosConfig。setDataId(configKey);nacosConfig。setGroupId(DEFAULTGROUP);Stringconfignacos。getConfig(nacosConfig);复制代码在filter中获取请求参数和header处理getMapString,StringqueryParamsHttpRequestUtils。getQueryParams(request。uri());处理postStringpostStrnewString(HttpRequestUtils。getRequestBody(request));处理表单HttpPostRequestDecoderdecodernewHttpPostRequestDecoder(newDefaultHttpDataFactory(false),request);ListInterfaceHttpDatapostDatadecoder。getBodyHttpDatas();for(InterfaceHttpDatadata:postData){if(data。getHttpDataType()InterfaceHttpData。HttpDataType。Attribute){MemoryAttributeattribute(MemoryAttribute)data;kv。put(attribute。getName(),attribute。getValue());}}处理headerFullHttpRequestrequest。headers()复制代码在filter里返回自定义结果返回HttpResponseUtils。create(),举例returnHttpResponseUtils。create(Result。fail(GeneralCodes。Forbidden,HttpResponseStatus。FORBIDDEN。reasonPhrase()));复制代码在filter里区分环境envGroup值有3种:staging,online(线上外网),intranet(线上内网)StringenvfilterContext。getAttachment(envGroup,staging);复制代码其他filter里的一些处理获取filter传递进来的参数filterParamsthis。getFilterParams(apiInfo);在实际调用下游之前的一些代码。。。省略代码。。。实际调用下游next(context,invoker,apiInfo,request)在实际调用下游之后的一些代码。。。省略代码。。。复制代码3、动态加载自定义filter
在编写好自定义filter并上传审核完成后,控制台会广播通知gateway集群里的每个节点,有新的filter加入,是时候reloadfilterchain了。
在第一节RequestFilterChain的reload方法基础上,我们加入加载自定义filter的逻辑吧。入口还是reload方法,它在获取用户定义的filter列表时,调用了FilterManager的getUserFilterList方法。热加载filter的逻辑我们都写到了FilterManager里面。Slf4jComponentpublicclassRequestFilterChainimplementsIRequestFilterChain{AutowiredprivateApplicationContextac;AutowiredprivateFilterManagerfilterManager;privatefinalCopyOnWriteArrayListRequestFilterfilterListnewCopyOnWriteArrayList();加载filterpublicvoidreload(Stringtype,ListStringnames){log。info(reloadfilter);获取系统定义的filterMapString,RequestFiltermapac。getBeansOfType(RequestFilter。class);ListRequestFilterlistnewArrayList(map。values());log。info(systemfiltersize:{},list。size());获取用户定义的filterListRequestFilteruserFilterListfilterManager。getUserFilterList(type,names)。stream()。filter(itfilterUserFilterWithGroup(it))。collect(Collectors。toList());log。info(userfiltersize:{}type:{}names:{},userFilterList。size(),type,names);list。addAll(userFilterList);listsortFilterList(list);。。。省略部分代码。。。}}复制代码
FilterManager的getUserFilterList方法
(getUserFilterListloadRequestFilterloadFilter)省略一部分代码,可前往https:github。comXiaoMimonetreemastergatewayall查看publicclassFilterManager{publicListRequestFiltergetUserFilterList(Stringtype,ListStringnames){try{if(!configService。isAllowUserFilter()){log。info(skipuserfilter);returnLists。newArrayList();}将老的filterjar包删除deleteOldFilter(type,names);从文件中心将编译好的filterjar包下载到本地downloadFilter(type,names);ListStringjarListgetJarPathList();log。info(jarList:{},jarList);热加载filterreturnloadRequestFilter(jarList);}catch(Throwableex){log。error(getUserFilterListex:{},ex。getMessage());returnLists。newArrayList();}}publicListRequestFilterloadRequestFilter(ListStringpathNameList){if(pathNameList。size()0){returnLists。newArrayList();}try{URL〔〕urlspathNameList。stream()。map(p{try{returnnewURL(file:p);}catch(MalformedURLExceptione){log。error(e。getMessage());}returnnull;})。filter(itnull!it)。toArray(URL〔〕::new);returnArrays。stream(urls)。map(url{try{log。info(loadrequestfilterurl:{},url);URLClassLoaderclassLoadernewURLClassLoader(newURL〔〕{url});returnloadFilter(url。getFile(),classLoader);}catch(Throwablee){log。error(loadfiltererror,url:{},msg:{},url,e。getMessage(),e);}returnnull;})。filter(itnull!it)。collect(Collectors。toList());}catch(Throwableex){log。error(ex。getMessage(),ex);}returnLists。newArrayList();}publicRequestFilterloadFilter(Stringurl,URLClassLoaderclassLoader)throwsIOException,ClassNotFoundException,IllegalAccessException,InstantiationException{StringcontentZipUtils。readFile(url,FilterDef);PropertiespropertiesnewProperties();properties。load(newStringInputStream(content));StringfilterClassproperties。getProperty(filter);Classlt;?clazzclassLoader。loadClass(filterClass);RequestFilterins(RequestFilter)clazz。newInstance();Stringnameproperties。getProperty(name);Stringauthorproperties。getProperty(author);Stringgroupsproperties。getProperty(groups);log。info(loadFilter,name:{},author:{},groups:{},name,author,groups);classLoaderMap。put(name,classLoader);ins。setDef(newFilterDef(0,name,author,groups));ins。setGetBeanFunction(getBean());returnins;}}复制代码
至此,用户可以随时增加一个新的gatewayfilter,或者更新那些已经存在的filter,而不用进行任何重启。4、使用业务自定义filter
在添加实际的apiinfo接口时,选择适合你接口的filter启用吧。