拦截器+外部配置文件实现类似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);
    }

}

 附:拦截器,过滤器区别等  https://www.cnblogs.com/AIPAOJIAO/p/14017338.html

posted @ 2022-03-08 14:06  鼠标的博客  阅读(188)  评论(0编辑  收藏  举报