美文网首页SpringSpringFramework
动态改变spring interceptor拦截路径和参数 20

动态改变spring interceptor拦截路径和参数 20

作者: 王帅199207 | 来源:发表于2017-08-15 10:58 被阅读1740次

    最近在项目中做一些针对API监控和过滤的开发,需求是能在前台管理页面动态配置(在不重启应用的情况下)拦截器拦截的URL拦截参数(比如并发次数等),并且拦截器数目也不一定,要求有一定的扩展性。

    面对这个需求有如下几个问题需要考虑:

    1. mysql中规则存储的表结构设计
    2. 前台页面复用的问题
    3. 拦截器类拦截的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;
        }
    
    }
    

    相关文章

      网友评论

        本文标题:动态改变spring interceptor拦截路径和参数 20

        本文链接:https://www.haomeiwen.com/subject/nqjmrxtx.html