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