• 【自定义注解使用】增加service层方法访问日志


    本文涉及到的技术有:SLF4J,JDK动态代理,AspectJ,Java自定义注解,Java反射。

    背景

    最近工作中发现为了方便排查服务器日志,公司的service层都会有方法的访问日志,类似----------getXXX(String name=xujian,User user = {"name":"xujian","age":20})----------START,----------getXXX(String name=xujian,User user = {"name":"xujian","age":20})----------END,其中包括了方法参数的值(也可能有响应的数据)。

    但是现实情况让是最终打印出来的东西各有不同,有的短横线的个数不一样,有的参数的值的形式不一样(这大部分存在于一个对象参数,如果对象重写了toString方法打印具体对象的属性值的话还好,如果没有重写那只会打印对象所属的类的全路径+hashcode),甚至有的直接忘了给log的占位符填充值,导致日志里面并没有参数信息。

    自定义注解

    考虑使用自定义注解,通过给方法增加注解来自动打印收尾日志。

    /**
     * 用于自动织入方法访问日志,加上该注解即可自动添加方法首尾日志
     * @author xujian
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface AccessLog {
        String[] paramsName() default {};//方法的形参列表
    }
    

    动态代理/AOP

    有了自定义注解,就该用AspectJ让注解起作用,将日志打印自动织入代码。

    /**
     * 访问日志织入,自动添加方法首尾日志
     *
     * @author xujian
     * @create 2019-01-11 10:05
     **/
    @Aspect
    @Component
    public class AccessLogAop {
        @Pointcut(value = "@annotation(com.xxx.AccessLog)")
        public void pointcut() {
        }
    
        @Around(value = "pointcut()")
        public Object around(ProceedingJoinPoint pjp) throws Throwable {
            Class targetClass = pjp.getTarget().getClass();//获取被代理的原始类
            String methodName = pjp.getSignature().getName();//获取当前调用的方法名
            Class<?>[] parameterTypes = ((MethodSignature) pjp.getSignature()).getMethod().getParameterTypes();//获取参数类型列表
            Method targetMethod = targetClass.getDeclaredMethod(methodName,parameterTypes);
            AccessLog accessLog = targetMethod.getAnnotation(AccessLog.class);
            String[] paramsName = accessLog.paramsName();//获取指定的形参列表
            Field field = targetClass.getDeclaredField("logger");
            field.setAccessible(true);
            FLogger logger = (FLogger) field.get(targetClass);//获取service声明的logger属性
            String targetMethodName = pjp.getSignature().getName();
            StringBuilder sb = new StringBuilder();
            Object[] args = pjp.getArgs();//获取参数值列表
            /*
            *拼装日志内容
            */
            for (int i=0;i<args.length;i++) {
                Object o = args[i];
                if (o == null) {
                    o = "null";
                }
                Object paramValue = o;
                //判断是否是基本类型,非基本类型的话转为json字符串
                if (!o.getClass().isPrimitive() && !"java.lang.String".equals(o.getClass().getName())) {
                    paramValue = JSON.toJSONString(o);
                }
                sb.append(o.getClass().getSimpleName()+" "+paramsName[i]+" = "+paramValue);
                sb.append(",");
            }
            String beginLog = "----------"+targetMethodName+"("+sb.substring(0,sb.length()-1)+")----------START";
            logger.info(beginLog);
            Object result = pjp.proceed();
            String endLog = "----------"+targetMethodName+"("+sb.substring(0,sb.length()-1)+")----------END";
            logger.info(endLog);
            return result;
        }
    }
    

    这里需要说明一下,自定义注解里面有一个属性是“形参列表”,本来不想用这个属性,而是通过javassist字节码技术自动获取(代码如下),但是实测发现不是很完美,所以就弃用了。

    /**
         * 字节码技术来获取方法形参列表,结果不太完美(已废弃)
         * @param cls
         * @param clazzName
         * @param methodName
         * @return
         * @throws NotFoundException
         */
        @Deprecated
        private List<String> getParamNames(Class cls, String clazzName, String methodName) throws NotFoundException {
            List<String> result=new ArrayList<>();
            ClassPool pool = ClassPool.getDefault();
            ClassClassPath classPath = new ClassClassPath(cls);
            pool.insertClassPath(classPath);
            CtClass cc = pool.get(clazzName);
            CtMethod cm = cc.getDeclaredMethod(methodName);
            MethodInfo methodInfo = cm.getMethodInfo();
            CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
            LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
            if (attr == null) {
                // exception
            }
            int pos = Modifier.isStatic(cm.getModifiers()) ? 0 : 1;
            CtClass[] paramsType = cm.getParameterTypes();
            for (int i = 0; i < paramsType.length; i++){
                result.add(attr.variableName(i + pos));
            }
            return result;
        }
    

    使用

    使用很简单,你只需要在你的service类里面声明SLF4J下的Logger logger对象,然后在你的service方法里面加上自定义的注解即可,如下:

    @Service
    public class TemplateMappingServiceImpl implements TemplateMappingService {
        private static final Logger logger = LoggerFactory.getLogger(TemplateMappingServiceImpl.class);
        @Override
        @AccessLog(paramsName = {"mainContractId","signerId","isAgent","templateVar","calculatedVars"})
        public Map<String,String> getTemplateMapping(Integer mainContractId, Integer signerId, boolean isAgent, Set<String> templateVar, Map<String,String> calculatedVars) 
    }
    

    总结

    虽然这个规避了日志格式混乱和忘记打印日志的问题,但是还有很多地方可以继续完善和优化。如

    1. 可以增加异常日志的打印
    2. 由于使用了代理,所以在本类里面直接进行方法调用,其实最终调用的是this对象(被代理的原始对象)的方法,并没有增强的相关代码,所以不能打印出日志,针对这种情况可以使用代理类的方法调用。

    虽然不是很复杂,但也不失为一个AOP+自定义注解的完整实践。


    相关代码github:自定义注解增加service层方法访问日志

  • 相关阅读:
    南京师范大学2021年高等代数考研试卷
    有限阶全图边图两种颜色后同色三角形数量最少为?(2019年清华大学丘成桐数学英才班)
    关于三个变元的正整数解(2019年清华大学丘成桐数学英才班)
    [Oracle工程师手记]归档日志产生量太大时的简易分析手段
    [Oracle 工程师手记] 如何查看 FRA 的使用率
    [Oracle数据库工程师手记] Data Guard broker 与 ORA-32701
    [Oracle工程师手记]CRSD 进程与 password 文件
    [oracle 工程师手记]RMAN duplicate 发生ORA-19504、ORA-17502、ORA-15001、ORA-27140 错误的解决过程
    [Oracle工程师手记] 备份恢复双城记(三)
    [Oracle工程师手记] 备份恢复双城记(二)
  • 原文地址:https://www.cnblogs.com/xuxiaojian/p/11453724.html
Copyright © 2020-2023  润新知