Java AOP 实例踩坑记

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

尝试Spring AOP未果

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

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

// 坑爹的手工 for retry
for (int i=0; i<MAX_TRY; i++){
    try{
        doSomething();
        break;
    }catch(Exception ignore){
    }
} 

考虑到项目中使用了一部分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.

<bean id="syncLoggingAspect" class="uk.co.demo.SyncLoggingAspect" 
      factory-method="aspectOf" />

道理我都懂了,但如何让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 <init>()V not found

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

maven配置AspectJ weaving

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

<build>
    <plugins>
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>aspectj-maven-plugin</artifactId>
            <version>1.8</version>
            <configuration>
                <complianceLevel>1.8</complianceLevel>
                <source>1.8</source>
                <target>1.8</target>
                <encoding>${project.build.sourceEncoding}</encoding>
                <showWeaveInfo>true</showWeaveInfo>
                <Xlint>warning</Xlint>
                <includes>
                    <include>**/*.java</include>
                    <include>**/*.aj</include>
                </includes>
                <aspectDirectory>src/main/aspect</aspectDirectory>
                <testAspectDirectory>src/test/aspect</testAspectDirectory>
                <XaddSerialVersionUID>true</XaddSerialVersionUID>
                <aspectLibraries>
                    <aspectLibrary>
                        <groupId>org.demo</groupId>
                        <artifactId>monitor</artifactId>
                    </aspectLibrary>
                </aspectLibraries>
            </configuration>
            <executions>
                <execution>
                    <id>compile</id>
                    <goals>
                        <goal>compile</goal>
                    </goals>
                </execution>
                <execution>
                    <id>test-compile</id>
                    <goals>
                        <goal>test-compile</goal>
                    </goals>
                </execution>
            </executions>
            <dependencies>
                <dependency>
                    <groupId>org.aspectj</groupId>
                    <artifactId>aspectjtools</artifactId>
                    <version>${aspectj.version}</version>
                </dependency>
                <!-- this is your AOP jar-->
                <dependency>
                    <groupId>org.demo</groupId>
                    <artifactId>monitor</artifactId>
                    <version>0.0.1</version>
                </dependency>
            </dependencies>
        </plugin>
    </plugins>
</build>

参考:

  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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容