Java 日志注解

介绍如何使用Java注解来添加日志。

注解

元注解

元注解的作用是注解其他注解,

  • @Target用于描述注解范围。

    1
    2
    3
    4
    5
    6
    7
    CONSTRUCTOR:用于描述构造器
    FIELD:用于描述域
    LOCAL_VARIABLE:用于描述局部变量
    METHOD:用于描述方法
    PACKAGE:用于描述包
    PARAMETER:用于描述参数
    TYPE:用于描述类、接口(包括注解类型) 或enum声明
  • @Retention用于描述注解生命周期。

    1
    2
    3
    SOURCE:在源文件中有效(即源文件保留)
    CLASS:在class文件中有效(即class保留)
    RUNTIME:在运行时有效(即运行时保留),可以通过反射获取该注解的属性值
  • @Documented用于生命构建注解文档。

  • @Inherited用于描述该注解是可被子类继承的。

注解格式

1
public @interface 注解名 {定义体}

日志注解

Log注解定义,参数包含type日志类型、desc日志描述、throwable是否catch异常、withResult是否记录打印结果。

1
2
3
4
5
6
7
8
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.PARAMETER })
public @interface Log {
String type() default LogConst.RUN_LOG;
String desc() default "";
boolean throwable() default true;
boolean withResult() default false;
}

需要注意,注解的默认值必须是常量(不可以设置为枚举)

那么常量如何定义?

1
2
3
4
5
6
7
8
9
10
public class LogConst {
// 日志类型
/* 访问日志 */
public final static String ACCESS_LOG = "ACCESS";
/* 事件日志 */
public final static String EVENT_LOG = "EVENT";
/* 运行日志 */
public final static String RUN_LOG = "RUN";
/* 异常日志 */
public final static String EXCP_LOG = "EXCEPTION";

开启注解和AOP配置

1
2
<context:annotation-config/>
<aop:aspectj-autoproxy proxy-target-class="true"/>

在注解中其实还可以做更多的事?在注解中我们可以增加异常监控,增加事件数量监控等等。

具体实现

通过切片来获取当前获取被注释执行的函数情况,这里使用aroundExec来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Aspect
@Component
public class LogAop {
@Around(value = "@annotation(com.simyy.web.aop.Log)")
public Object aroundExec(ProceedingJoinPoint pjp) throws Throwable {
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
String logType = method.getAnnotation(Log.class).type();
boolean throwable = method.getAnnotation(Log.class).throwable();
boolean withResult = method.getAnnotation(Log.class).withResult();
// 设置过无允许抛出异常或设置日志类型为异常日志
if (throwable == false || logType.equals(LogConst.EXCP_LOG)) {
return withoutExpProcess(pjp, logType , withResult);
} else {
return withExpProcess(pjp, logType , withResult);
}
}
}

对于异常日志处理方式就是通过增加try-catch来捕获并记录日常情况。
其中,getEmptyObjectByClassType是用于在捕获异常后返回空对象或失败对象,getContentByType是用于按固定格式打印日志记录的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private Object withoutExpProcess(ProceedingJoinPoint pjp, String logType, boolean withResult) throws Throwable {
Object[] args = pjp.getArgs();
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
Class returnType = ((MethodSignature) pjp.getSignature()).getReturnType();

Long start = System.currentTimeMillis();

Object retVal;
Exception exp = null;
try {
retVal = pjp.proceed();
} catch (Exception e) {
retVal = getEmptyObjectByClassType(returnType);
exp = e;
}

if (exp == null) {
// 异常日志类型不需要捕获正常运行记录,因此直接返回
if (logType.equals(LogConst.EXCP_LOG)) {
return retVal;
}

Long useTime = System.currentTimeMillis() - start;
String afterLog;
if (withResult == true) {
afterLog = getContentByType(AFTER_WITH_RESULT, method, args, useTime, retVal);
} else {
afterLog = getContentByType(AFTER, method, args, useTime, retVal);
}
LOGGER.info(afterLog);

} else {
String errorLog = getContentByType(ERROR, method, args, null, null);
LOGGER.error(errorLog, exp);
}

return retVal;
}

相反,对于不需要捕获异常的运行日志,只需要记录访问情况,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private Object withExpProcess(ProceedingJoinPoint pjp, String logType, boolean withResult) throws Throwable {
Object[] args = pjp.getArgs();
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
Class returnType = ((MethodSignature) pjp.getSignature()).getReturnType();

Long start = System.currentTimeMillis();

String beforeLog = getContentByType(BEFORE, method, args, null, null);
LOGGER.info(beforeLog);

Object retVal = pjp.proceed();

Long useTime = System.currentTimeMillis() - start;
if (withResult == true) {
String afterLog = getContentByType(AFTER_WITH_RESULT, method, args, useTime, retVal);
LOGGER.info(afterLog);
}

return retVal;
}

用于返回空对象的函数,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private Object getEmptyObjectByClassType(Class classType) {
if (classType.equals(List.class)) {
return Collections.EMPTY_LIST;
}
if (classType.equals(Map.class)) {
return Collections.EMPTY_MAP;
}
if (classType.equals(Set.class)) {
return Collections.EMPTY_SET;
}
if (classType.equals(boolean.class) || classType.equals(Boolean.class)) {
return false;
}
// 对于特殊的执行result对象需要特殊处理
// do somethind
return null;
}

定义日志打印格式用于getContentByType函数,

1
2
3
4
5
6
7
8
9
10
11
private final String BEFORE_TMPL = "TYPE=[%s] DESC=[%s] METHOD=[%s] PARAMS=[%s]";
private final String AFTER_TMPL = "TYPE=[%s] DESC=[%s] METHOD=[%s] PARAMS=[%s] TIME=[%d ms]";
private final String AFTER_TMPL_WITH_RESULT = "TYPE=[%s] DESC=[%s] METHOD=[%s] PARAMS=[%s] TIME=[%d ms] RESULT=[%s]";
private final String ERROR_TMPL = BEFORE_TMPL;

// 获取当前类名+函数名称
private String getClassAndMethodName(Method method) {
String methodName = method.getName();
String className = method.getDeclaringClass().getSimpleName();
return className + "." + methodName;
}

More

日志采用异步打印提高性能;
增加异常监控或重要事件处理监控方案
毕竟反射还是带来一定的性能损耗
对于异常error的代码行这样会丢失,那可以通过Throable.getStackTrace()方法获取运行栈进行分析获取需要的错误行和错误原因。