对Druid的SpringStatInterceptor的一点简单改动

Druid是阿里开源的一款功能非常强大的数据库查询系统,除了常规情况下用到的连接池的功能外,还提供了一系列的监控功能,从多个纬度进行了数据库使用的情况的统计和监控。
大部分监控功能如:SQL监控,URL监控,SESSION监控,都无需做额外配置即可使用,唯独Spring监控需要额外配置一个(methodinterceptor)方法拦截器。
而网上搜索到的资料,甚至于阿里自己的文档都推荐如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<bean id="druid-stat-interceptor"
class="com.alibaba.druid.support.spring.stat.DruidStatInterceptor">
</bean>
<bean id="druid-stat-pointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut"
scope="prototype">
<property name="patterns">
<list>
<value>com.xx.oo.controller.*</value>
</list>
</property>
</bean>
<aop:config>
<aop:advisor advice-ref="druid-stat-interceptor"
pointcut-ref="druid-stat-pointcut" />
</aop:config>

其中,DruidStatInterceptor是Druid包里的一个MethodInterceptor,他通过你在
com.xx.oo.controller.*
中配置的规则,来拦截目标类的方法,然后通过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
SpringMethodStat lastMethodStat = SpringMethodStat.current();
SpringMethodInfo methodInfo = getMethodInfo(invocation);
SpringMethodStat methodStat = springStat.getMethodStat(methodInfo, true);
if (methodStat != null) {
methodStat.beforeInvoke();
}
long startNanos = System.nanoTime();
Throwable error = null;
try {
return invocation.proceed();
} catch (Throwable e) {
error = e;
throw e;
} finally {
long endNanos = System.nanoTime();
long nanos = endNanos - startNanos;
if (methodStat != null) {
methodStat.afterInvoke(error, nanos);
}
SpringMethodStat.setCurrent(lastMethodStat);
}
}

的一系列处理,来记录接口的运行情况,再通过JMS协议的处理,用HTML页面的形式呈现数据给使用者。

但是这个拦截的使用上,个人觉得不是很习惯,相信很多兄弟在项目中其实更习惯通过anno注解来实现切面的Point cut。相比拦截目标类中的全部方法,注解的形式无疑更加灵活,比如,可以通过设置Point cut,拦截Spring RequestMapping,实现统计项目中所有对外接口的运行情况。
那么,该如何做呢?
第一步,我们先用注解的形式声明一个切面类。

@Aspect
@Component
public class TestInterceptor {
}
其中,@Aspect标注他是一个切面,@Component是为了Spring的ComponentScan可以自动生成并加载该类的实例。
第二步,我们用注解的形式声明point cut。

1
2
3
4
5
@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
Object result = pjp.proceed();
return result;

此时,你在Object result = pjp.proceed();这一行打上断点,你通过Swagger-ui或者Postman、JMeter等工具调用你Spring的Restful接口,已经可以正确的debug到断点处。
写到此处,就产生了一个问题,Druid原本的拦截器实现是一个MethodInterceptor,他的invoke(方法拦截器的实现方法)方法参数是MethodInvocation类型,而我们的@Aspect的切面类,doAround(AOP的实现方法,还有before,after等其他种类)方法参数是ProceedingJoinPoint类型,这里类型不同,很多代码不是单单Ctrl C V就能移植过来的,该如何解决?
实际上DEBUG的时候,你如果留心观察了doAround的pjp参数,你会发现它的类型是MethodInvocationProceedingJoinPoint,这个类implements了ProceedingJoinPoint接口,

1
2
3
4
public class MethodInvocationProceedingJoinPoint implements ProceedingJoinPoint, JoinPoint.StaticPart {
private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
private final ProxyMethodInvocation methodInvocation;

而他的field list里正好有我们所需要的methodInvocation,ProxyMethodInvocation接口继承于MethodInvocation接口,我们只需要设法拿到pjp的methodInvocation字段,就可以把MethodInterceptor的处理转换到AOP里来。这时,你通过阅读源码发现,MethodInvocationProceedingJoinPoint并没有方法可以直接获取methodInvocation字段,再次陷入了难题。
第三步,使用反射获取对象的私有属性
实际上,我们可以通过反射的手段来获取对象的私有属性,而且代码也非常简单。

1
2
3
4
5
6
7
if (pjp instanceof MethodInvocationProceedingJoinPoint) {
MethodInvocationProceedingJoinPoint pointCut = (MethodInvocationProceedingJoinPoint) pjp;
Field field = MethodInvocationProceedingJoinPoint.class.getDeclaredField("methodInvocation");
field.setAccessible(true);
Object value = field.get(pointCut);
MethodInvocation invocation = (MethodInvocation) value;
}

我们先通过 instanceof 来确保强转不会出错,
然后用class.getDeclaredField方法来获取“methodInvocation”的Field对象,但是,methodInvocation是私有属性,不能直接访问,field.setAccessible(true)设置可访问性,然后Field的GET方法,可以通过传入对象来获取对象的该属性的value,它返回的类型是一个Object,这里再通过一次强转,获取到我们需要的MethodInvocation对象。

至此,AOP里已经拿到了MethodInterceptor invoke方法的参数,剩下的,就是简单的代码移植了。

其实对于Druid原本实现的这些折腾,并没有太多实用上的意义,只是个人出于学习的目的,对Druid源码中自己一些感兴趣的地方进行学习、归纳、试图转换为自己的知识储备路途中的一些尝试。重点在切面、拦截器、反射的使用,改造的效果反而并不重要。