最近在项目中做一些针对API监控和过滤的开发,需求是能在前台管理页面动态配置(在不重启应用的情况下)拦截器拦截的URL和拦截参数(比如并发次数等),并且拦截器数目也不一定,要求有一定的扩展性。
面对这个需求有如下几个问题需要考虑:
- mysql中规则存储的表结构设计
- 前台页面复用的问题
- 拦截器类拦截的URL和参数动态改变的问题
1.表结构的设计
因为拦截器有多个,每个参数都不一样,所以为每个拦截器都写一张表存储是不现实的。
- 规则表:
DROP TABLE IF EXISTS `rule_config`;
CREATE TABLE `rule_config` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`gmt_created` datetime NOT NULL,
`gmt_modified` datetime NOT NULL,
`url_id` int DEFAULT NULL,
`interceptor` varchar(255) DEFAULT NULL,
`rule_type` varchar(255) DEFAULT NULL,
`rule_param` text DEFAULT NULL,
`is_deleted` tinyint(1) DEFAULT NULL,
`remark` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
- 规则表中url_id字段关联的URL表:
DROP TABLE IF EXISTS `url_value`;
CREATE TABLE `url_value` (
`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT,
`gmt_created` datetime NOT NULL,
`gmt_modified` datetime NOT NULL,
`url_val` varchar(255) DEFAULT NULL,
`application` varchar(255) DEFAULT NULL,
`url_type` varchar(255) DEFAULT NULL,
`is_deleted` tinyint(1) DEFAULT NULL,
`remark` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
RULE表中的数据如下所示:
2.前台页面的问题
同样,因为拦截器有多个,每个拦截器都要有自己的配置页面,因此页面也需要能够根据传入参数interceptor不同而展现不同的配置,具体的做法就是将各种共通的配置(如URL等)写在一个页面里面,然后各拦截器独立的配置项写在独立的JS里面,通过参数判断加载哪段JS,以此来达到页面复用的目的。
参数通过controller传递(使用的都是同一个ftl页面):
/**
* 并发限制规则配置页面
*
* @param modelMap
* @return
*/
@RequestMapping("/concurrent")
public String concurrentRuleList(ModelMap modelMap) {
modelMap.put("interceptor", InterceptorConstants.CONCURRENT_INTERCEPTOR);
return "rule/rule_list";
}
/**
* 白名单规则配置页面
*
* @param modelMap
* @return
*/
@RequestMapping("/whitelist")
public String whitelistRuleList(ModelMap modelMap) {
modelMap.put("interceptor", InterceptorConstants.WHITE_LIST_INTERCEPTOR);
return "rule/rule_list";
}
/**
* 黑名单规则配置页面
*
* @param modelMap
* @return
*/
@RequestMapping("/blacklist")
public String blacklistRuleList(ModelMap modelMap) {
modelMap.put("interceptor", InterceptorConstants.BLACKLIST_INTERCEPTOR);
return "rule/rule_list";
}
这样,当要增加拦截器时,只需要在js里面增加其独立的配置项,然后controller中间增加一个方法就可以了,这样做相对于每有一个新的拦截器就去写新的页面工作量小很多。
当然,配置项和controller中对拦截器的读取也可以放入配置文件或者数据库中,最后可以做到新增拦截器后,代码完全不改动。
3.拦截器类拦截的URL和参数动态改变的问题
最初思考这个问题的时候,作为技术人员直观的思路就是在容器不重启的情况下,动态的销毁spring容器中拦截器的bean,并且根据新的配置来重新加载拦截器新的实例,这是热部署的思路。然而在查询的不少资料之后,并没有发现spring对这种操作的支持,也没有找到其他实现的方法,因此没有采用这个思路。(如果有朋友知道如何实现,欢迎留言告诉我,thanks!)
后来采用的是变通的方法,就是通过拦截器里面的代码的逻辑来进行控制:简单来说就是拦截器都拦截所有路径,然后在拦截器实现逻辑中,获取redis缓存(缓存可以在前台页面设置按钮,让用户主动刷新)中对应的配置参数,来决定是否拦截和如何拦截。
这里我实现了一个spring的拦截器,定义的逻辑是控制API的并发访问次数
拦截器逻辑:
/**
* 并发限流拦截器,限制API并发调用的次数
*
* @author wangshuai
* @date 2017年8月4日
*/
public class InServiceAccessInterceptor extends HandlerInterceptorAdapter {
private static final Logger logger = Logger.getLogger(InServiceAccessInterceptor.class);
/**
* 拦截器是否启用
*/
private boolean valid;
/**
* 从缓存获取配置
*/
@Resource
private InterceptorCacheHelper interceptorCacheHelper;
/**
* 拦截器构造函数, 传入的参数在xml中配置
* @param valid
*/
public InServiceAccessInterceptor(boolean valid) {
this.valid = valid;
}
/**
* 拦截器逻辑
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (valid) {
if (handler instanceof HandlerMethod) {
String url = request.getServletPath();
boolean isInWhiteList = WhiteListInterceptor.isInWhiteList(url);
if(isInWhiteList) {
//白名单不作拦截
return true;
}
try {
//从缓存中获取限制次数
JSONObject paramMap = interceptorCacheHelper.getParamMap(url, this.getClass().getName());
if(null == paramMap) {
//没有规则配置 则拦截器直接通过
return true;
}
String limitString = paramMap.getString("limit");
if(StringUtils.isBlank(limitString)) {
//规则中没有配置参数 拦截器通过
return true;
}
int limit = Integer.parseInt(limitString);
int counter = AccessCounts.getInstance().get(url);
boolean isLimited = check(url, counter, limit);
if (isLimited) {
throw new IllegalAccessException("并发调用次数超出限制.[" + url + "] = " + counter + " limit = " + limit);
}
MDC.put(AccessCounts.URL, url);
AccessCounts.getInstance().incrementAndGet(url);
} catch(Exception e) {
//出现异常 清除访问计数
String urlStr = MDC.get(AccessCounts.URL);
if (null != urlStr) {
AccessCounts.getInstance().decrementAndGet(urlStr);
}
MDC.remove(AccessCounts.URL);
e.printStackTrace();
throw e;
}
}
}
return true;
}
/**
* 每次执行完后,清除计数,实现限制同一个API同时调用的次数
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
if (valid) {
String url = MDC.get(AccessCounts.URL);
if(!StringUtils.isEmpty(url)) {
AccessCounts.getInstance().decrementAndGet(url);
}
MDC.remove(AccessCounts.URL);
}
}
/**
* 出现异常导致postHandle未执行,则在此清除该次访问计数
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
if (valid) {
String url = MDC.get(AccessCounts.URL);
if(!StringUtils.isEmpty(url)) {
AccessCounts.getInstance().decrementAndGet(url);
}
MDC.remove(AccessCounts.URL);
}
}
/**
* 判断是否超出限制次数
* @date 2017年8月4日
* @author wangshuai
* @param url
* @param counter
* @return
*/
private boolean check(String url, int counter, int limit) {
// System.out.println("check callpath:" + url + "============> limit:" + limit + " counter:" + counter);
if (logger.isDebugEnabled()) {
logger.debug("check callpath:" + url + "============> limit:" + limit + " counter:" + counter);
}
if (counter >= limit) {
// System.out.println("the call[" + url + "]============> is over limit:" + limit + " counter:" + counter);
logger.warn("the call[" + url + "]============> is over limit:" + limit + " counter:" + counter);
return true;
}
return false;
}
}
网友评论