间歇性"建议优先级循环错误"使用Spring AOP

时间:2018-03-16 18:04:51

标签: spring aspectj spring-aop

我使用Spring AOP在我的代码上添加了一些性能检测。我有一个单元测试来测试建议是否被触发。该测试在本地工作但在Jenkins CI工作中间歇性地失败,其中"建议优先级循环错误"

java.lang.IllegalStateException: Failed to load ApplicationContext
at org.springframework.test.context.TestContext.getApplicationContext(TestContext.java:157)
at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.injectDependencies(DependencyInjectionTestExecutionListener.java:109)
at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.prepareTestInstance(DependencyInjectionTestExecutionListener.java:75)
at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:321)
at org.springframework.test.context.testng.AbstractTestNGSpringContextTests.springTestContextPrepareTestInstance(AbstractTestNGSpringContextTests.java:133)
...
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'testTrackingUrlDao' defined in file [***/build/classes/java/test/com/example/analytics/aspect/TestTrackingUrlDao.class]: Initialization of bean failed; nested exception is java.lang.IllegalArgumentException: Advice precedence circularity error
...
Caused by: java.lang.IllegalArgumentException: Advice precedence circularity error
at org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator.sortAdvisors(AspectJAwareAdvisorAutoProxyCreator.java:82)
at org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator.findEligibleAdvisors(AbstractAdvisorAutoProxyCreator.java:90)
at org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator.getAdvicesAndAdvisorsForBean(AbstractAdvisorAutoProxyCreator.java:68)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.wrapIfNecessary(AbstractAutoProxyCreator.java:359)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.postProcessAfterInitialization(AbstractAutoProxyCreator.java:322)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:407)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1461)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:519)
... 66 more

我的aroundafterThrowingafter切入点具有相同的正则表达式。最初我没有明确地在建议上设置顺序,并且Spring上下文初始化间歇性地失败了实际代码,而不仅仅是单元测试。阅读完link之后,我重新安排了我在"周围的建议。然后是"在推后" "之后""订单并使用Order注释添加显式顺序。现在代码在生产中运行良好。但是我们的Jenkins CI工作中的单元测试间歇性地失败(有时会通过,有时会失败)。我在某处读到它说不同的jdk版本可能导致问题。我不确定究竟是什么意思以及如何解决这个问题。由于遗留问题,我仍然在Spring 3.1.x版上,顺便说一句。

这是Aspect代码:

@Aspect
@Component
public class AnalyticsAspect {

    @Order(1)
    @Around("execution(* com.example.analytics.dao.*.find*(..)) || execution(* com.example.analytics.dao.*.get*(..))")
    public Object updateReadCounterTimer(ProceedingJoinPoint joinPoint) throws Throwable {
        LOGGER.info("updateReadCounterTimer");
        long currTime = System.currentTimeMillis();
        Object retVal = joinPoint.proceed();
        readCounter.stopWatch.start(currTime);
        readCounter.stopWatch.stop();
        LOGGER.info("updateReadCounterTimer done");
        return retVal;
    }

    @Order(2)
    @Around("execution(* com.example.analytics.dao.*.save*(..)) || execution(* com.example.analytics.dao.*.set*(..))")
    public Object updateWriteCounterTimer(ProceedingJoinPoint joinPoint) throws Throwable {
        LOGGER.info("updateWriteCounterTimer");
        long currTime = System.currentTimeMillis();
        Object retVal = joinPoint.proceed();
        writeCounter.stopWatch.start(currTime);
        writeCounter.stopWatch.stop();
        LOGGER.info("updateWriteCounterTimer done");
        return retVal;
    }

    /**
    * This is called whenever there's an exception thrown from the matching DAO methods
    */
    @Order(3)
    @AfterThrowing(pointcut = "execution(* com.example.alytics.dao.*.find*(..)) || execution(* com.example.analytics.dao.*.get*(..))",
                throwing = "error")
    public void updateErrorReadCounter(JoinPoint jointPoint, Throwable error) {
        LOGGER.info("updateErrorReadCounter");
        readCounter.failureCounter.increment();
        LOGGER.info("updateErrorReadCounter done");
    }

    @Order(4)
    @AfterThrowing("execution(* com.example.alytics.dao.*.save*(..)) || execution(* com.example.analytics.dao.*.set*(..))")
    public void updateErrorWriteCounter(JoinPoint jointPoint) {
        LOGGER.info("updateErrorWriteCounter");
        writeCounter.failureCounter.increment();
        LOGGER.info("updateErrorWriteCounter done");
    }

    /**
    * NOTE assumption is that all reader method names will have the "find***" or "get***" pattern
    */
    @Order(5)
    @After("execution(* com.example.alytics.dao.*.find*(..)) || execution(* com.example.analytics.dao.*.get*(..))")
    public void updateSuccessReadCounter(JoinPoint jointPoint) {
        LOGGER.info("updateSuccessReadCounter");
        readCounter.successCounter.increment();
        LOGGER.info("updateSuccessReadCounter done");
    }

    /**
    * NOTE assumption is that all writer method names will have the "save***" or "set***" pattern
    */
    @Order(6)
    @After("execution(* com.example.analytics.dao.*.save*(..)) || execution(* com.example.analytics.dao.*.set*(..))")
    public void updateSuccessWriteCounter(JoinPoint jointPoint) {
        LOGGER.info("updateSuccessWriteCounter");
        writeCounter.successCounter.increment();
        LOGGER.info("updateSuccessWriteCounter done");
    }

}

以下是单元测试类的一些片段:

@ContextConfiguration(locations = { "classpath:applicationContext-test.xml" })
public class AnalyticsAspectTest extends AbstractTestNGSpringContextTests {

    @Autowired
    @Qualifier("testTrackingUrlDao")
    TrackingUrlDao trackingUrlDao;

    @Autowired
    @Qualifier("testAnalyticEventDao")
    AnalyticEventDao analyticEventDao;

    @Autowired
    AnalyticsAspect analyticsAspect;

    @Test
    public void testReadSuccessCounterOnTrackingUrlDaoFindById() {
        List<TrackingUrl> trackingUrls = trackingUrlDao.findById("abc");
        assertEquals(analyticsAspect.readCounter.successCounter.getValue(), 1);
        assertEquals(trackingUrls.size(), 1);
    }
    ...
}

/**
* This is a dummy implementation of AnalyticEventDao interface so that we can autowire it
* into the VideoAnalyticsAspectTest class 
*/
@Component
class TestAnalyticEventDao implements AnalyticEventDao {

    @Override
    public List<AnalyticEvent> findById(String id) {
    ...
    }

    @Override
    public void save(AnalyticEvent event) {
    ...
    }
}

/**
* This is a dummy implementation of TrackingUrlDao interface so that we can autowire it
* into the AnalyticsAspectTest class 
*/
@Component
class TestTrackingUrlDao implements TrackingUrlDao {
    // set it to true to simulate exception when DAO method is called
    private boolean testFailure = false;

    @Override
    public List<TrackingUrl> findById(String id) {
...
    }

    @Override
    public void save(TrackingUrl trackingUrl) {
...
    }

}

1 个答案:

答案 0 :(得分:0)

为什么不通过将每个操作的三个建议(读/写)合并为一个来避免循环问题?此外,您的代码中存在一些问题:

  • 在<{em> proceed()之后开始你的秒表,即你什么都没有计时。我修好了。
  • 如果出现错误,您永远不会停止秒表。我通过将其移动到finally块来修复它。
  • 我也认为你的记录太多了。是否真的有必要在每个之前和之后记录秒表和反击动作?并且日志输出看起来非常神秘。我在计数器更新之前删除了日志记录,因为日志记录将比增量操作花费更长的时间。我只是在计数器更新后记录。不过,我在秒表前后保留了记录,以免变化太大。
@Aspect
@Component
public class AnalyticsAspect {
  private static final Log LOGGER = new Log4JLogger();
  private StatisticsCollector readCounter;
  private StatisticsCollector writeCounter;

  @Around("execution(* com.example.analytics.dao.*.find*(..)) || execution(* com.example.analytics.dao.*.get*(..))")
  public Object readTimerAndCounter(ProceedingJoinPoint joinPoint) throws Throwable {
    Object retVal = null;
    LOGGER.info("Read timer - start");
    readCounter.stopWatch.start(System.currentTimeMillis());
    try {
      retVal = joinPoint.proceed();
    }
    catch (Exception e) {
      readCounter.failureCounter.increment();
      LOGGER.info("Read error - counter updated");
      throw e;
    }
    finally {
      readCounter.stopWatch.stop();
      LOGGER.info("Read timer - stop");
    }
    readCounter.successCounter.increment();
    LOGGER.info("Read success - counter updated");
    return retVal;
  }

  @Around("execution(* com.example.analytics.dao.*.save*(..)) || execution(* com.example.analytics.dao.*.set*(..))")
  public Object writeTimerAndCounter(ProceedingJoinPoint joinPoint) throws Throwable {
    Object retVal;
    LOGGER.info("Write timer - start");
    writeCounter.stopWatch.start(System.currentTimeMillis());
    try {
      retVal = joinPoint.proceed();
    }
    catch (Exception e) {
      writeCounter.failureCounter.increment();
      LOGGER.info("Write error - counter updated");
      throw e;
    }
    finally {
      writeCounter.stopWatch.stop();
      LOGGER.info("Write timer - stop");
    }
    writeCounter.successCounter.increment();
    LOGGER.info("Write success - counter updated");
    return retVal;
  }
}

如果效果更好,请尝试。当然我无法测试它,因为你没有共享任何应用程序代码。