【自定義注解使用】增加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層方法訪問日志


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM