开源地址
一、为什么需要用户自定义动态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<String, String> queryParams = HttpRequestUtils.getQueryParams(request.uri());
//处理post
String postStr = new String(HttpRequestUtils.getRequestBody(request));
//处理表单
HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(new DefaultHttpDataFactory(false), request);
List<InterfaceHttpData> 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<RequestFilter> filterList = new CopyOnWriteArrayList<>();
//加载filter
public void reload(String type, List<String> names) {
log.info("reload filter");
//获取系统定义的filter
Map<String, RequestFilter> map = ac.getBeansOfType(RequestFilter.class);
List<RequestFilter> list = new ArrayList<>(map.values());
log.info("system filter size:{}", list.size());
//获取用户定义的filter
List<RequestFilter> 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<RequestFilter> getUserFilterList(String type, List<String> names) {
try {
if (!configService.isAllowUserFilter()) {
log.info("skip user filter");
return Lists.newArrayList();
}
//将老的filter jar包删除
deleteOldFilter(type, names);
//从文件中心将编译好的filter jar包下载到本地
downloadFilter(type, names);
List<String> jarList = getJarPathList();
log.info("jarList:{}", jarList);
//热加载filter
return loadRequestFilter(jarList);
} catch (Throwable ex) {
log.error("getUserFilterList ex:{}", ex.getMessage());
return Lists.newArrayList();
}
}
public List<RequestFilter> loadRequestFilter(List<String> 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启用吧。