• 拦截器+外部配置文件实现类似AOP方式的日志切面功能


    filter应用场景:

    1)过滤敏感词汇(防止sql注入)

    2)设置字符编码

    3)URL级别的权限访问控制

    4)压缩响应信息

    拦截器本质上是面向切面编程(AOP),符合横切关注点的功能都可以放在拦截器中来实现,主要的应用场景包括:

    • 登录验证,判断用户是否登录。

    • 权限验证,判断用户是否有权限访问资源,如校验token

    • 日志记录,记录请求操作日志(用户ip,访问时间等),以便统计请求访问量。

    • 处理cookie、本地化、国际化、主题等。

    • 性能监控,监控请求处理时长等。

    • 通用行为:读取cookie得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提取Locale、Theme信息等,只要是多个处理器都需要的即可使用拦截器实现)

    执行顺序: Filter->Interceptor.preHandle->Handler->Interceptor.postHandle->Interceptor.afterCompletion->Filter

     

    一般情况下,记录日志主要通过切面Aspect+注解annotation实现,如果是已经成型发布的服务,再去做日志记录,代价比较大,可能需要大量侵入代码

    切面和注解,实质是通过spring的java类代理实现的,可以拦截到方法,这里用拦截器可以拦截请求

    /**
     * 自定义操作日志拦截器类,代替注解和切面实现
     * 与session相关的操作需要注意下,有的不能在afterCompletion中操作*/
    
    @Component
    public class OperateLogInterceptor implements HandlerInterceptor {
    
        private Logger logger = LoggerFactory.getLogger("operateLog");
        private static final String I18N_PREFIX = "operatelog.";
        private static final SpelExpressionParser PARSER = new SpelExpressionParser();
        private static final DefaultParameterNameDiscoverer DISCOVERER = new DefaultParameterNameDiscoverer();
    
        @Autowired
        private LocaleMessage localeMessage;
    
        /**
         * 请求到达之前执行
         */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
            request.getSession();
            return true;
        }
    
        /**
         * 请求执行结束后,ModelAndView返回之前执行
         * 如果抛出异常,这里不会回调,直接执行afterCompletion
         */
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView v) {
        }
    
        /**
         * 请求全部完成后执行
         */
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) {
            Map<String, List<OperateLogJo>> map = OperateLogFileLoad.getOperateLogJsonMap();
            //补充配置文件中的日志信息
            String servletPath = request.getServletPath();
            List<OperateLogJo> list = null;
            if (map.containsKey(servletPath)) {
                list = map.get(servletPath);
            } else {
                for (String key : map.keySet()) {
                    if (new AntPathMatcher().match(key, servletPath)) {
                        list = map.get(key);
                        break;
                    }
                }
            }
            if (CollectionUtils.isNotEmpty(list)) {
                int size = list.size();
                OperateLogJo jo = null;
                //应该保证这个context只需要prepare一次
                EvaluationContext context = null;
                //说明这个接口是唯一的,否则需要通过requestMethod继续寻找,如果requestMethod也相同,继续看triggerCondition是true的
                if (size == 1) {
                    jo = list.get(0);
                } else {
                    String requestMethod = request.getMethod().toLowerCase();
                    for (OperateLogJo logJo : list) {
                        if (requestMethod.equals(logJo.getRestMethod())) {
                            String triggerCondition = logJo.getTriggerCondition();
                            String dataSize = logJo.getDataSize();
                            //说明这种请求类型下不需要通过触发条件判断唯一接口
                            if (StringUtils.isEmpty(triggerCondition)) {
                                jo = logJo;
                            } else {
                                //不包含#号的,就是值给的有问题,下面的逻辑不用执行了,这里不能用startsWith,因为有这种!#id==0
                                //改逻辑,triggerCondition里的变量不一定是body,也可能是parameter,pathVariable
                                if (triggerCondition.contains("#")) {
                                    context = prepareContext(request, (HandlerMethod) handler, resolveParam(triggerCondition), resolveParam(dataSize));
                                    Boolean o = PARSER.parseExpression(triggerCondition).getValue(context, Boolean.class);
                                    //这种情况就麻烦了,需要在context里计算triggerCondition表达式必须为true,才是要找到的情形
                                    if (o) {
                                        jo = logJo;
                                        break;
                                    }
                                }
                            }
                        }
                    }
                }
                //如果都循环完了也没有找到,那就是研发配置的不对,自己查问题
                if (jo != null) {
                    Log log = new Log();
                    log.setSubType(locale(jo.getSubType()));
                    log.setOperateType(locale(jo.getOperateType()));
                    log.setOperateTag(locale(jo.getOperateTag()));
                    log.setOperateObj(locale(jo.getOperateObj()));
                    log.setWeight(jo.getWeight());
                    log.setAuthType(locale(jo.getAuthType()));
                    log.setRestUri(locale(jo.getRestUri()));
                    log.setRestMethod(locale(jo.getRestMethod()));
                    log.setTriggerCondition(locale(jo.getTriggerCondition()));
                    log.setOperateResult("success");
                    String dataSize = locale(jo.getDataSize());
                    //通用的做法是:遍历注解里传入的值,哪些字段包含"#"符号,包含的则在下面去解析
                    // 这里决定不采用这种通用处理方式,因为没必要遍历那么多字段,已经定义死了,不提供个性化和通用化
                    if (dataSize.contains("#")) {
                        if (context == null) {
                            context = prepareContext(request, (HandlerMethod) handler, resolveParam(log.getTriggerCondition()), resolveParam(dataSize));
                        }
                        resolveELField(context, log, dataSize);
                    } else {
                        if (StringUtil.isEmpty(dataSize)) {
                            log.setDataSize(0);
                        } else {
                            try {
                                log.setDataSize(Integer.valueOf(dataSize));
                            } catch (NumberFormatException e1) {
                                log.setDataSize(-1);
                            }
                        }
    
                    }
                    if (e != null || (e = (Exception) request.getAttribute(DispatcherServlet.EXCEPTION_ATTRIBUTE)) != null) {
                        log.setOperateResult("failed");
                        log.setExceptionMsg(e.getMessage());
                    }
                    OperationLogUtil.completeLog(log);
                    logger.info(LoggerHelper.message(JSONObject.toJSONString(log)));
                }
            }
        }
    
        /**
         * 预处理EvaluationContext
         * 如果根本没有参数列表,返回null,使用方需要了解null情况
         */
        private EvaluationContext prepareContext(HttpServletRequest request, HandlerMethod handler, String var1, String var2) {
            //方法的参数名
            String[] parameterNames = DISCOVERER.getParameterNames(handler.getMethod());
            Class<?>[] classTypes = handler.getMethod().getParameterTypes();
            Map<String, Class<?>> typeMap = new HashMap<>(256);
            for (int i = 0; i < parameterNames.length; i++) {
                typeMap.put(parameterNames[i], classTypes[i]);
            }
            if (parameterNames != null && parameterNames.length > 0) {
                EvaluationContext context = new StandardEvaluationContext();
                // 获取url后的参数
                Map<String, String[]> parameterMap = request.getParameterMap();
                if (!MapUtils.isEmpty(parameterMap)) {
                    for (Map.Entry<String, String[]> m : parameterMap.entrySet()) {
                        String key = m.getKey();
                        Class<?> type = typeMap.get(key);
                        context.setVariable(key, convertValue(type, m.getValue()[0]));
                    }
                }
                //获取pathVariable里的参数
                Map<String, Object> pathVariableMap = (Map) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
                if (!MapUtils.isEmpty(pathVariableMap)) {
                    for (Map.Entry<String, Object> m : pathVariableMap.entrySet()) {
                        String key = m.getKey();
                        Class<?> type = typeMap.get(key);
                        context.setVariable(key, convertValue(type, String.valueOf(m.getValue())));
                    }
                }
                //var1和var2哪个是body变量,只能排除了
                String bodyVariable = null;
                if (StringUtils.isNotEmpty(var1)) {
                    if (!parameterMap.containsKey(var1) && !pathVariableMap.containsKey(var1)) {
                        bodyVariable = var1;
                    }
                }
                //如果还是没找到
                if (StringUtils.isEmpty(bodyVariable)) {
                    if (StringUtils.isNotEmpty(var2)) {
                        if (!parameterMap.containsKey(var2) && !pathVariableMap.containsKey(var2)) {
                            bodyVariable = var2;
                        }
                    }
                }
                if (StringUtils.isNotEmpty(bodyVariable) && typeMap.containsKey(bodyVariable)) {
                    // 获取body中的请求参数,如@RequestBody注解参数,post请求参数
                    String body;
                    try {
                        body = new RequestWrapper(request).getBody();
                    } catch (IOException e) {
                        body = null;
                    }
                    context.setVariable(bodyVariable, JSONObject.parseObject(body, typeMap.get(bodyVariable)));
                }
                return context;
            }
            return null;
        }
    
        private void resolveELField(EvaluationContext context, Log log, String dataSize) {
            if (dataSize != null && dataSize.contains("#")) {
                Object o = PARSER.parseExpression(dataSize).getValue(context);
                if (o != null) {
                    try {
                        log.setDataSize(Integer.valueOf(o.toString()));
                    } catch (NumberFormatException e) {
                        log.setDataSize(-1);
                    }
    
                } else {
                    //未解析到
                    log.setDataSize(-1);
                }
            } else {
                try {
                    log.setDataSize(Integer.valueOf(dataSize));
                } catch (NumberFormatException e1) {
                    log.setDataSize(-1);
                }
            }
        }
    
        private String locale(String msg) {
            return localeMessage.getMessage(I18N_PREFIX + msg, msg);
        }
    
        private Object convertValue(Class clazz, String value) {
            if (clazz.getName().equals("java.lang.Integer")) {
                return Integer.valueOf(value);
            } else if (clazz.getName().equals("java.lang.Long")) {
                return Long.valueOf(value);
            } else if (clazz.getName().equals("java.lang.Boolean")) {
                return Boolean.valueOf(value);
            } else {
                return value;
            }
        }
    
        /**
         * Spring源码解析搬不过来,自己写了一个非通用的,未考虑复合形式的
         * 找到第一个#号后,第一个运算符(退一步,第一个非字母和数字的,因为按命名规范讲变量都是数字字母组成)前的那一部分
         * 几种情形:#id==1   #id.equals(1)  #vo.getX()  '1'.equals#id
         * !#id==1   !#id.equals(1)  !#vo.getX()   !'1'.equals#id
         */
        private String resolveParam(String el) {
            StringBuilder sb = new StringBuilder();
            if (StringUtils.isNotEmpty(el) && el.contains("#")) {
                for (int i = el.indexOf("#") + 1; i < el.length(); i++) {
                    char c = el.charAt(i);
                    if (Character.isLetter(c) || Character.isDigit(c)) {
                        sb.append(c);
                    } else {
                        break;
                    }
                }
            }
            return sb.toString();
        }
    
    }

    装载

    /**
     * 自定义操作日志拦截器装载类
     * @see OperateLogFileLoad*/
    
    @Configuration
    public class OperationLogWebMvcConfig implements WebMvcConfigurer {
    
        private static Logger log = LoggerFactory.getLogger(OperationLogWebMvcConfig.class);
        @Autowired
        private OperateLogInterceptor operateLogInterceptor;
    
        /**
         * 这里的设计要注意参数在路径里的情况,解析的时候怎么匹配和解析
         * 参考Spring已经封装好的一个工具AntPathMatcher.match(pattern, lookupPath)
         *
         * @param registry
         */
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            log.info("add operation log interceptors start---->");
            Map<String, List<OperateLogJo>> map = OperateLogFileLoad.getOperateLogJsonMap();
            List<String> list = map.keySet().stream().map(e -> e.startsWith("/") ? e : "/" + e).collect(Collectors.toList());
            if (CollectionUtils.isNotEmpty(list)) {
                registry.addInterceptor(operateLogInterceptor).addPathPatterns(list);
            } else {
                log.info("no requests for operation log need to be intercepted");
            }
            log.info("add operation log interceptors finished---->");
        }
    
    }

    配置文件预加载到内存

    /**
     * 读取组件配置的操作日志json配置文件并解析到内存中
     * 注意:json文件中多写的属性,会被丢弃*/
    public class OperateLogFileLoad {
    
        private static Logger log = LoggerFactory.getLogger(OperateLogFileLoad.class);
        private static final Map<String, List<OperateLogJo>> OPERATE_LOG_JSON_MAP = new HashMap<>();
    
        /**
         * 这里预计80%的接口是不重复的(restUri + restMethod + triggerCondition组合),因此key还是用restUri,多个value用list保存
         * 有的组件的classPath额外指定了config目录,有的没有,这里需要兼容下
         */
        static {
            log.info("Start reading log.json file and loading operation log config");
            try {
                ClassPathResource resource = new ClassPathResource("doc/operatelog/log.json");
                if (!resource.exists()) {
                    resource = new ClassPathResource("config/doc/operatelog/log.json");
                }
                if (!resource.exists()) {
                    log.warn("No file of log.json is found,please make sure that you don't need to logging the operation log");
                } else {
                    String json = IOUtils.toString(resource.getInputStream(), Charset.forName("UTF-8"));
                    List<OperateLogJo> list = JSONObject.parseArray(json, OperateLogJo.class);
                    if (CollectionUtils.isNotEmpty(list)) {
                        for (OperateLogJo jo : list) {
                            String method = StringUtils.isEmpty(jo.getRestMethod()) ? "get" : jo.getRestMethod().toLowerCase();
                            jo.setRestMethod(method);
                            String restUri = jo.getRestUri();
                            if (StringUtils.isNotEmpty(restUri)) {
                                List<OperateLogJo> jos = OPERATE_LOG_JSON_MAP.getOrDefault(restUri, new ArrayList<>());
                                jos.add(jo);
                                OPERATE_LOG_JSON_MAP.put(restUri, jos);
                            }
                        }
                    }
                }
            } catch (IOException e) {
                log.error("Reading or resolving log.json file failed ,please check the file content");
            }
            log.info("Finished log.json file loading");
        }
    
        /**
         * 调用get的时候会触发static 静态代码块执行
         *
         * @return 返回一个不可更改的map,对修改关闭
         */
        public static Map<String, List<OperateLogJo>> getOperateLogJsonMap() {
            return Collections.unmodifiableMap(OPERATE_LOG_JSON_MAP);
        }
    
    }
  • 相关阅读:
    使用Python和百度对外共享的TTS接口实现文本转语音
    最简单的AI代码——价值“一个亿”
    【CSS】让滚动条消失
    足球圈移动端网页Demo
    golang通过errgroup单chan多个grouting通讯样例
    gin返回前端excel文件流
    xorm构建复杂sql
    CSS实现标签闪动效果
    vue_js遍历双重数组对象
    vue_html实现加载双重数组对象
  • 原文地址:https://www.cnblogs.com/yb38156/p/15853174.html
Copyright © 2020-2023  润新知