通过注解、AOP、反射实现对多方法的通用监控和处理

2016-08-25 18:11:49   最后更新: 2016-08-25 18:21:00   访问数量:661




在我们的项目中,通常需要对各个方法实现 log 打印、上报监控数据、上报调用状态等等,这些通用的过程如果我们在每一个方法中都手动去添加是一个非常枯燥和容易出错的任务

针对这样的场景,Spring 提供的 AOP 是一个非常有利的解决工具,同时,配合 java 的反射和注解可以实现更加灵活的处理

 

由于我们的项目中通常方法众多,方法名没有统一规则,同时,我们又常常需要对我们的通用处理做出多种多样的配置,让他满足我们不同的个性化配置需求,所以我们使用注解的方式来作为切面的依据是一个非常好的选择

注解的使用和创建可以参考:

注解的定义和使用

 

package com.techlog.test.aop; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 用于监控的通用注解 * Created by techlog on 16/8/25. */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ServiceMonitor { public String[] ignoreLogParams() default {}; }

 

 

我们的注解中只具有一个属性,那就是 ignoreLogParams,用于在打印 log 时,忽略的参数名

考虑到一个方法传入参数可能会有很多,而我们又需要在方法进入时打印 info log 记录执行情况,对于某些参数可能我们并不需要打印,因此在使用 ServiceMonitor 注解时,只要配置 ignoreLogParams 属性,在 info log 中就不会打印相应参数

当然,在实际的项目中,我们还可以增加更多的自定义配置

 

关于 AOP 的用法,可以参考:

Spring AOP 的注解实现

 

package com.techlog.test.aop; import com.localserv.cat.Cat; import com.localserv.cat.message.Transaction; import com.localserv.jmonitor.JMonitor; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.stereotype.Service; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * Created by techlog on 16/8/25. */ @Aspect @Service public class ServiceAspect { @Value("#{monitorKey}") private String monitorKey; private static final String SERVICE_MONITOR = "@annotation(ServiceMonitor)"; private static final Logger LOGGER = LoggerFactory.getLogger(ServiceAspect.class); @Around(SERVICE_MONITOR) public Object monitorAround(ProceedingJoinPoint pjp) throws Throwable { Method method; if(pjp.getSignature() instanceof MethodSignature) { MethodSignature signature = (MethodSignature) pjp.getSignature(); method = signature.getMethod(); } else { LOGGER.error("Monitor Annotation not at a method {}", pjp); return null; } ServiceMonitor monitor = method.getDeclaredAnnotation(ServiceMonitor.class); Transaction t = Cat.newTransaction(monitorKey, method.getName()); Logger logger = LoggerFactory.getLogger(pjp.getTarget().getClass()); String logcontent = method.getName(); String[] parameterNames = new DefaultParameterNameDiscoverer().getParameterNames(method); List<Object> parameters = new ArrayList<>(); List<String> ignoreLogParams = Arrays.asList(monitor.ignoreLogParams()); for (int i=0; i<method.getParameterCount(); ++i) { if (!ignoreLogParams.contains(parameterNames[i])) { logcontent += ", " + parameterNames[i] + " {}"; parameters.add(pjp.getArgs()[i]); } } logger.info(logcontent, parameters.toArray()); long start = System.currentTimeMillis(); Object result = null; try { result = pjp.proceed(); } catch (Throwable throwable) { t.setStatus(throwable); logger.error(pjp.getTarget().getClass().getCanonicalName() + " error {}", throwable); throw throwable; } finally { t.complete(); JMonitor.add(pjp.getTarget().getClass().getCanonicalName(), System.currentTimeMillis() - start); } return result; } }

 

 

整套监控的实现,最重要的当然是切面的定义部分,也就是上面的 ServiceAspect 代码,最基本的,他通过 Around 注解实现了对所有有 ServiceMonitor 注解的方法的切面,通过执行传入参数的 ProceedingJoinPoint 对象的 proceed 方法实现了方法的调用和异常的捕获

整个过程看上去非常简单,主要是使用反射获取了目标类和方法的信息,记录和上报必要的数据,但其中还是有一些细节的问题需要注意

关于反射的使用,可以参考:

RTT、Class 对象与反射

 

首先我们通过传入的 ProceedingJoinPoint 对象获取了被切面切到的方法的 Method 对象

通过 Method 对象的 getDeclaredAnnotation 方法我们获取到了我们所需要的 ServiceMonitor 对象

接下来,我们就可以通过轮询所有传入参数并与 ServiceMonitor 注解中 ignoreLogParams 属性来实现参数的选择打印了

 

问题在于,在低于 java8 的版本中,通过 Parameter 对象的 getName 方法获取到的值将会是 arg0、arg1 这样的字符串,而不是我们定义的参数名,原因就是在此前版本的编译过程中,编译器并不会将方法的参数名写入 class 文件,因此在运行时就无法获取相应的参数名了

java8 提供了 -parameters 的编译参数用来解决这个问题:

java8 新特性

那么,如果我们没有使用 java8 或者无法在生产环境中配置相应的参数怎么办呢?

Spring 提供了 DefaultParameterNameDiscoverer 类用来解决这个问题,他的 getParameterNames 将返回传入的 Method 对象的所有参数名,这样就可以满足我们的需求了

 

Log4j 提供的 Logger 对象的各种打印日志的方法都是以可变参数列表作为参数传递的,然而,在这里我们只能获取到所有参数组成的数组,那么怎么办呢?

有趣的是,数组类型可以直接作为 java 的可变参数列表方法的参数,jvm 在解析时,会自动将数组的每个元素作为可变参数列表的一个参数,而不是将整个数组作为可变参数列表的一个参数

通过下面的例子可以检验出这个结果:

package com.techlog.test.service; import org.springframework.stereotype.Service; /** * Created by techlog on 16/8/16. */ @Service public class TestService { public static void main(String[] args) { String[] strings = {"hello", "world", "!!!"}; test(strings); } public static void test(String ... params) { for (String param : params) { System.out.println(param); } } }

 

 

打印出了:

hello

world

!!!

 

class 对象提供了三种通过 class 对象获取类名的方法:

  1. getName -- 返回依赖于实现
  2. getSimpleName -- 仅获取类名
  3. getCanonicalName -- 获取完整类名(包含包名)

 

这里我们在打印 error log 时使用了 getCanonicalName 来打印更加详细的信息

 

 

 






技术帖      技术分享      log      java      反射      aop      注解      annotation      监控      monitor      aspect      aspectj     


京ICP备15018585号