Java AOP 实例踩坑记

其实这篇文章是存了很久的草稿,写了一半没有继续完稿。终于得空继续完善它,之后还会抽时间继续研究Spring AOP 的使用和实现( ̄∇ ̄),绝不放过任何绊脚石。

尝试Spring AOP未果

项目中遇到一个性能监控需求:在线搜集统计服务方法的调用次数和平均开销时间,用于服务性能调优和服务死活监控。考虑到涉及到的统计点很多,一个个手写采集点代码非常傻,而且代码侵入性很大。

想起之前为了重构代码中的手工auto-retry(见下面的代码库 Orz),曾经找到过jcabi这样的库,其中是采用了Java中的一大“神器”,面相切面编程(AOP)。于是性能点采集逻辑也打算采用AOP的方式来实现。

// 坑爹的手工 for retry
for (int i=0; i

考虑到项目中使用了一部分spring功能(依赖注入),于是网上找资料,很多都是关于Spring AOP+AspectJ来实现的例子。比如参考的[1]、[2],里面详细的讲解了关于AOP的概念,如何使用Spring AOP和AspectJ风格的注释来实现动态代理方式的AOP编程。然而却走上了Spring AOP的踩坑之路。大名鼎鼎的Spring,自动装配bean,Spring AOP号称还能自动运行时织入切点。但是却怎么尝试都 “不work!”。参考了一下 [2] 里面的坑,还是不行。(百分百被大众/官网实例坑的设定)

暂时放弃Spring AOP,老老实实的学习一下AspectJ吧。

完工的代码(去掉了公司业务的代码框架)

来一段AspectJ风格的代码

@Aspect
public class Monitor {
    private static final Logger logger = LoggerFactory.getLogger(Monitor.class);

    @Pointcut("execution(public * *(..)) && @annotation(demo.APM)")
    private void apmHandler(){}

    @Around("apmHandler()")
    public Object apmRecordTime(ProceedingJoinPoint pjp) throws Throwable{
        Object ret = null;
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        APM apmAnnotation = method.getAnnotation(APM.class);
        String commandName = apmAnnotation.value();
        try {
            long startTime = System.nanoTime();
            ret = pjp.proceed();
            long processTime = (System.nanoTime() - startTime); // avg_time unit: nano seconds
            logger.trace("command[{}] spend avg_time:{} ms", commandName, processTime/1000000d);
        } finally{
        }
        return ret;
    }

    @AfterThrowing(pointcut="apmHandler()", throwing= "error")
    public void apmRecordException(JoinPoint jp, Throwable error){
        MethodSignature signature = (MethodSignature) jp.getSignature();
        Method method = signature.getMethod();
        APM apmAnnotation = method.getAnnotation(APM.class);
        String commandName = apmAnnotation.value();
        logger.trace("command[{}] throw exception: {}", commandName, error);
    }
}

为了方便设定切点,我使用了Java annotation的方式来做标记:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface APM {
    String value() default "";
}

凡是标记了@APM 的方法,都会被AOP切入。例如代码里定义的 pointcut 可以捕捉所有 public 并且带有@APM("xxxx") 的函数调用,通过Java Reflection 可以拿到APM的value(作为command名字,其实也可以考虑用method name)

实例化的坑

当然实际中实现的AOP类没有那么简单,还需要加入统计的工具,尤其是当需要注入外部的对象时,就不得不通过Spring bean的方式来配置管理它。例如上面的Monitor类,在Spring 的 Java Config里:

@configure
public AppConfig{
    @Bean
    public Monitor monitor(){
        Monitor monitor = new Monitor();
        // monitor.config(xxx);
        // monitor.register(xxxx);
        return monitor;
    }
}

乍眼一看感觉上面的代码没问题是吧?查看日志的时候发现Monitor实例化了两次!翻看AspectJ文档发现,有一段说Aspect类的实例化是由AspectJ接管的!

Like classes, aspects may be instantiated, but AspectJ controls how that instantiation happens -- so you can't use Java's new form to build new aspect instances. By default, each aspect is a singleton, so one aspect instance is created. This means that advice may use non-static fields of the aspect, if it needs to keep state around

The aspect is a singleton object and is created outside the Spring container. A solution with XML configuration is to use Spring's factory method to retrieve the aspect.


道理我都懂了,但如何让spring来控制这个spring之外实例化的东西呢?
参考 Configuring AspectJ aspects using Spring IoC with JavaConfig?

With this configuration the aspect will be treated as any other Spring bean and the autowiring will work as normal.
You have to use the factory-method also on Enum objects and other objects without a constructor or objects that are created outside the Spring container.

通过如此如下方式是可以成功拿到AOP的bean

import org.aspectj.lang.Aspects;

@configure
public AppConfig{
    @Bean
    public Monitor monitor(){
        Monitor monitor = Aspects.aspectOf(Monitor.class);
        // monitor.config(xxx);
        // monitor.register(xxxx);
        return monitor;
    }
}

实例化的坑(续):默认构造函数

AspectJ会使用默认构造函数来实例化Aspect的类,当你无意中实现了一个非默认构造函数又没有默认构造函数时,他会报下面的错误:

aspectj method ()V not found

所以请使用默认构造函数来实例化Aspect类!(终于明白我要拿到AOP bean的苦衷了吧)

maven配置AspectJ weaving

AspectJ插件文档里有范例的,这里贴一下自己的,当你需要在某个项目里weave你的AOP jar时,可以加入,在compile阶段就搞定了。


    
        
            org.codehaus.mojo
            aspectj-maven-plugin
            1.8
            
                1.8
                1.8
                1.8
                ${project.build.sourceEncoding}
                true
                warning
                
                    **/*.java
                    **/*.aj
                
                src/main/aspect
                src/test/aspect
                true
                
                    
                        org.demo
                        monitor
                    
                
            
            
                
                    compile
                    
                        compile
                    
                
                
                    test-compile
                    
                        test-compile
                    
                
            
            
                
                    org.aspectj
                    aspectjtools
                    ${aspectj.version}
                
                
                
                    org.demo
                    monitor
                    0.0.1
                
            
        
    

参考:

  1. Spring实战4—面向切面编程
  2. Spring AOP 注解方式实现的一些“坑”
  3. How to get a method's annotation value from a ProceedingJoinPoint?
  4. Aspectj @Around pointcut all methods in Java
  5. http://aspects.jcabi.com/
  6. Strategies for using AspectJ in a Maven multi-module reactor

你可能感兴趣的:(Java AOP 实例踩坑记)