Java AOP 实例踩坑记

ejis8217 4年前
   <p>其实这篇文章是存了很久的草稿,写了一半没有继续完稿。终于得空继续完善它,之后还会抽时间继续研究Spring AOP 的使用和实现( ̄∇ ̄),绝不放过任何绊脚石。</p>    <h2>尝试Spring AOP未果</h2>    <p>项目中遇到一个性能监控需求:在线搜集统计服务方法的调用次数和平均开销时间,用于服务性能调优和服务死活监控。考虑到涉及到的统计点很多,一个个手写采集点代码非常傻,而且代码侵入性很大。</p>    <p>想起之前为了重构代码中的手工auto-retry(见下面的代码库 Orz),曾经找到过jcabi这样的库,其中是采用了Java中的一大“神器”,面相切面编程(AOP)。于是性能点采集逻辑也打算采用AOP的方式来实现。</p>    <pre>  <code class="language-javascript">// 坑爹的手工 for retry  for (int i=0; i<MAX_TRY; i++){      try{          doSomething();          break;      }catch(Exception ignore){      }  }</code></pre>    <p>考虑到项目中使用了一部分spring功能(依赖注入),于是网上找资料,很多都是关于Spring AOP+AspectJ来实现的例子。比如参考的[1]、[2],里面详细的讲解了关于AOP的概念,如何使用Spring AOP和AspectJ风格的注释来实现动态代理方式的AOP编程。然而却走上了Spring AOP的踩坑之路。大名鼎鼎的Spring,自动装配bean,Spring AOP号称还能自动运行时织入切点。但是却怎么尝试都 <strong>“不work!”</strong> 。参考了一下 [2] 里面的坑,还是不行。(百分百被大众/官网实例坑的设定)</p>    <p>暂时放弃Spring AOP,老老实实的学习一下AspectJ吧。</p>    <h2>完工的代码(去掉了公司业务的代码框架)</h2>    <p>来一段AspectJ风格的代码</p>    <pre>  <code class="language-javascript">@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);      }  }</code></pre>    <p>为了方便设定切点,我使用了Java annotation的方式来做标记:</p>    <pre>  <code class="language-javascript">@Target(ElementType.METHOD)  @Retention(RetentionPolicy.RUNTIME)  @interface APM {      String value() default "";  }</code></pre>    <p>凡是标记了@APM 的方法,都会被AOP切入。例如代码里定义的 pointcut 可以捕捉所有 public 并且带有@APM("xxxx") 的函数调用,通过Java Reflection 可以拿到APM的value(作为command名字,其实也可以考虑用method name)</p>    <h2>实例化的坑</h2>    <p>当然实际中实现的AOP类没有那么简单,还需要加入统计的工具,尤其是当需要注入外部的对象时,就不得不通过Spring bean的方式来配置管理它。例如上面的Monitor类,在Spring 的 Java Config里:</p>    <pre>  <code class="language-javascript">@configure  public AppConfig{      @Bean      public Monitor monitor(){          Monitor monitor = new Monitor();          // monitor.config(xxx);          // monitor.register(xxxx);          return monitor;      }  }</code></pre>    <p>乍眼一看感觉上面的代码没问题是吧?查看日志的时候发现Monitor实例化了两次!翻看AspectJwen dang发现一段说Aspect类的实例化是由AspectJ接管的!</p>    <p>Like classes, aspects may be instantiated, but <strong>AspectJ controls how that instantiation happens</strong> -- so you <strong>can't use Java's new form</strong> to build new aspect instances. By default, each aspect is a <strong>singleton</strong> , 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</p>    <p>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.</p>    <pre>  <code class="language-javascript"><bean id="syncLoggingAspect" class="uk.co.demo.SyncLoggingAspect"         factory-method="aspectOf" /></code></pre>    <p>如何让spring来控制这个spring之外实例化的东西呢?</p>    <p>参考 <a href="/misc/goto?guid=4959734756586382281" rel="nofollow,noindex">Configuring AspectJ aspects using Spring IoC with JavaConfig?</a></p>    <p>With this configuration the aspect will be treated as any other Spring bean and the autowiring will work as normal.</p>    <p>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.</p>    <p>通过如此如下方式是可以成功得到AOP的bean</p>    <pre>  <code class="language-javascript">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;      }  }</code></pre>    <h2>实例化的坑(续):默认构造函数</h2>    <p>AspectJ会使用默认构造函数来实例化Aspect的类,当你无意中实现了一个非默认构造函数又没有默认构造函数时,他会报下面的错误:</p>    <pre>  <code class="language-javascript">aspectj method <init>()V not found</code></pre>    <p>所以请使用默认构造函数来实例化Aspect类!(终于明白我要拿到AOP bean的苦衷了吧)</p>    <h2>maven配置AspectJ weaving</h2>    <p>未完待续( ̄∇ ̄)</p>    <p>参考:</p>    <ol>     <li><a href="/misc/goto?guid=4959734756706941097" rel="nofollow,noindex">Spring实战4—面向切面编程</a></li>     <li><a href="/misc/goto?guid=4959734756834514296" rel="nofollow,noindex">Spring AOP 注解方式实现的一些“坑”</a></li>     <li><a href="/misc/goto?guid=4959734756947829173" rel="nofollow,noindex">How to get a method's annotation value from a ProceedingJoinPoint?</a></li>     <li><a href="/misc/goto?guid=4959734757079282039" rel="nofollow,noindex">Aspectj @Around pointcut all methods in Java</a></li>     <li><a href="/misc/goto?guid=4959616827742229848" rel="nofollow,noindex">http://aspects.jcabi.com/</a></li>     <li><a href="/misc/goto?guid=4959734757249548720" rel="nofollow,noindex">Strategies for using AspectJ in a Maven multi-module reactor</a></li>    </ol>    <p> </p>    <p>来自:http://www.jianshu.com/p/62f22d821333</p>    <p> </p>