AspectJ 在具有匹配参数值的注释上定义切入点

AspectJ defining Pointcut on annotation with matching argument value

我有一个使用 Processannotation. This annotation has the property called name. Process contains tasks in it. The tasks are defined with another annotation 定义的进程,名为 Task。这个注解有属性processName。 我有一个 generic process with name as Generic. The tasks for this process are Task1, Task2 and Task3 ,所有三个都带有 processName 作为 Generic。 我可以使用 aspectj 这样,所有具有相同 processName 的任务都分组在“'Process'”下吗?此外,当 GenericProcess.execute 被调用时,其中的任务也都需要被触发。 我正在尝试的代码目前在 github 中。 感谢您的帮助。

也许您应该提到您的应用程序甚至没有 运行,它退出时出现错误。总的来说,这几乎是一团糟。也许您应该在将其发布到 GitHub 之前对其进行测试并查看日志。您还应该发布显示错误的日志输出,而不是简单地说 "it does not work"。我不得不解决很多问题:

  • 首先我删除了(或者更确切地说重命名)application.ymllogback.xml 因为我既没有你的 H2 数据库,你的项目也没有创建,我也没有在控制台上看到任何日志输出。当然,这些设置可能对您有用,对我来说则不行。

  • 然后在你的 Application 中有 Process process = context.getBean(Process.class); 这没有意义,因为 Process 不是一个 bean class 而是一个注解。你从 中拿走了我的代码,改变了很多东西,然后再也没有任何东西可以组合在一起了。现在您的应用程序如下所示:

package com.spring.aspect.interfaces;

import com.spring.aspect.interfaces.process.GenericProcess;
import org.modelmapper.ModelMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class Application {
  private static final Logger log = LoggerFactory.getLogger(Application.class);

  @Bean
  public ModelMapper modelMapper() {
    return new ModelMapper();
  }

  public static void main(String[] args) {
    ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
    GenericProcess process = context.getBean(GenericProcess.class);
    log.info("Generic process = {}", process);
    process.execute();
  }
}
  • 在你的任务方面 Task{1,2,3} 切入点 execution(public com.spring.aspect.interfaces.entity.Job com.spring.aspect.interfaces.process.GenericProcessImpl.process(..)) && args(context) 无效,因为既没有 GenericProcessImpl.process(..) 方法也没有 Context 参数.你拥有的是一个没有参数的 void execute() 方法。

  • 此外,该方法仅来自已实现的 GenericProcess 接口,即我们可以使用该接口而不是特定的实现作为切入点。

  • 我也不认为需要使用昂贵的 @Around 建议,如果您只想记录一些内容,@Before 就足够了。那么这个怎么样?

package com.spring.aspect.interfaces.task;

import com.spring.aspect.interfaces.annotation.Provided;
import com.spring.aspect.interfaces.annotation.Required;
import com.spring.aspect.interfaces.annotation.Task;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Task(name = "Task1", processName = "Generic")
@Required(values = { "param1" })
@Provided(values = { "param2" })
@Aspect
public class Task1 {
  Logger logger = LoggerFactory.getLogger(Task1.class.getName());

  @Before("execution(public void com.spring.aspect.interfaces.process.GenericProcess+.execute())")
  public void task(JoinPoint thisJoinPoint) {
    logger.info("{}", thisJoinPoint);
  }
}
package com.spring.aspect.interfaces.task;

import com.spring.aspect.interfaces.annotation.Provided;
import com.spring.aspect.interfaces.annotation.Required;
import com.spring.aspect.interfaces.annotation.Task;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Task(name = "Task2", processName = "Generic")
@Required(values = { "param2" })
@Provided(values = { "param3" })
@Aspect
public class Task2 {
  Logger logger = LoggerFactory.getLogger(Task2.class.getName());

  @Before("execution(public void com.spring.aspect.interfaces.process.GenericProcess+.execute())")
  public void task(JoinPoint thisJoinPoint) {
    logger.info("{}", thisJoinPoint);
  }
}
package com.spring.aspect.interfaces.task;

import com.spring.aspect.interfaces.annotation.Required;
import com.spring.aspect.interfaces.annotation.Task;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Task(name = "Task3", processName = "Generic")
@Required(values = { "param1", "param2" })
@Aspect
public class Task3 {
  Logger logger = LoggerFactory.getLogger(Task3.class.getName());

  @Before("execution(public void com.spring.aspect.interfaces.process.GenericProcess+.execute())")
  public void task(JoinPoint thisJoinPoint) {
    logger.info("{}", thisJoinPoint);
  }
}
  • 最后但同样重要的是,在 TaskAspect 中,您使用 @annotation(com.spring.aspect.interfaces.annotation.Task) 匹配带有 @Task 注释的方法。但是你有 class 带有那个注释,所以你应该改用 @within(com.spring.aspect.interfaces.annotation.Task)

  • 比尝试通过 target(task)Task 注释绑定到参数更容易的方法是只使用 @within(task) 来代替,让你们两个注释匹配并立即绑定参数。

  • 还记得我们在上面将任务切入点从 @Around 更改为 @Before 吗?对于 @Before,AspectJ 不生成方法,而是将代码直接编织到目标 classes 中。您也不能依赖它来获得 @Around 建议,因此我对您上一个问题的回答有效但有点不干净。不过,有一个特殊的切入点指示符 adviceexecution()。它的目的是拦截来自其他方面(甚至来自同一方面)的执行建议。这更干净,更通用。在这种情况下,它甚至是唯一有效的方法。

  • 最后,你用的是args(proceedingJoinPoint),我也不知道为什么。您是否尝试将截获的 Task{1,2,3} 建议的切入点绑定到 TaskAspect 建议?它不会那样工作,因为 AspectJ 将执行通知的连接点绑定到类型为 JoinPointProceedingJoinPoint 的现有第一个参数。也许它将它绑定到第二个连接点参数。无论如何,我们不需要它,所以让我们删除它。真是做作,哇

  • 还有一点,为什么你使用 task.getClass().getName() 而不是新引入的 @Task 注释属性来记录信息?

package com.spring.aspect.interfaces.aspect;

import com.spring.aspect.interfaces.annotation.Task;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Aspect
public class TaskAspect {
  static Logger logger = LoggerFactory.getLogger(TaskAspect.class.getName());

  @Around("@within(task) && adviceexecution()")
  public Object groupTasks(ProceedingJoinPoint proceedingJoinPoint, Task task) throws Throwable {
    logger.info("Generalizing the task aspects");
    logger.info("  {}", proceedingJoinPoint);
    logger.info("  Task = {}", task.name());
    logger.info("  Process = {}", task.processName());
    return proceedingJoinPoint.proceed();
  }
}

现在应用程序和各个方面终于可以再次运行了,至少如果您使用 -javaagent:"/path/to/aspectjweaver.jar" 参数启动应用程序。现在您应该看到这样的日志输出(删除 AspectJ weaver 输出和一些领先的记录器列):

  .   ____          _            __ _ _
 /\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.1.3.RELEASE)

(...)
o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
c.spring.aspect.interfaces.Application   : Started Application in 2.948 seconds (JVM running for 4.934)
c.spring.aspect.interfaces.Application   : Generic process = com.spring.aspect.interfaces.process.GenericProcessImpl@25d0cb3a
c.s.aspect.interfaces.aspect.TaskAspect  : Generalizing the task aspects
c.s.aspect.interfaces.aspect.TaskAspect  :   adviceexecution(void com.spring.aspect.interfaces.task.Task3.task(JoinPoint))
c.s.aspect.interfaces.aspect.TaskAspect  :   Task = Task3
c.s.aspect.interfaces.aspect.TaskAspect  :   Process = Generic
com.spring.aspect.interfaces.task.Task3  : execution(void com.spring.aspect.interfaces.process.GenericProcessImpl.execute())
c.s.aspect.interfaces.aspect.TaskAspect  : Generalizing the task aspects
c.s.aspect.interfaces.aspect.TaskAspect  :   adviceexecution(void com.spring.aspect.interfaces.task.Task1.task(JoinPoint))
c.s.aspect.interfaces.aspect.TaskAspect  :   Task = Task1
c.s.aspect.interfaces.aspect.TaskAspect  :   Process = Generic
com.spring.aspect.interfaces.task.Task1  : execution(void com.spring.aspect.interfaces.process.GenericProcessImpl.execute())
c.s.aspect.interfaces.aspect.TaskAspect  : Generalizing the task aspects
c.s.aspect.interfaces.aspect.TaskAspect  :   adviceexecution(void com.spring.aspect.interfaces.task.Task2.task(JoinPoint))
c.s.aspect.interfaces.aspect.TaskAspect  :   Task = Task2
c.s.aspect.interfaces.aspect.TaskAspect  :   Process = Generic
com.spring.aspect.interfaces.task.Task2  : execution(void com.spring.aspect.interfaces.process.GenericProcessImpl.execute())
c.s.a.i.process.GenericProcessImpl       : Generic Process execution is invoked

更新:

  • 关于日志输出,我稍微修改了TaskAspect
  • 我还制作了包范围的记录器,这样我就可以从测试中注入模拟。
  • 随后,我将您的 ProcessTest 重构到我的 TaskAspectTest 中,并将其移动到与 TaskAspect 相同的包中。它看起来如下:
package com.spring.aspect.interfaces.aspect;

import com.spring.aspect.interfaces.process.GenericProcess;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

/**
 * Attention: Run this test with JVM parameter -javaagent:/path/to/aspectjweaver.jar
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class TaskAspectTest {
  @Autowired
  private GenericProcess genericProcess;

  @Mock
  private Logger mockLogger;
  private Logger originalLogger;

  @Before
  public void setup() {
    originalLogger = TaskAspect.logger;
    TaskAspect.logger = mockLogger;
  }

  @After
  public void cleanup() {
    TaskAspect.logger = originalLogger;
  }

  /**
   * The way TaskAspect is currently implemented, its only side effect is logging output,
   * so the only way we can check if the aspect is executed as expected is to inject a
   * mock logger and verify if it was called as often as expected, i.e. once for each
   * Task1, Task2, Task3, with 1+3 log lines per execution.
   */
  @Test
  public void testAspectExecution() {
    genericProcess.execute();
    verify(mockLogger, times(3)).info(anyString());
    verify(mockLogger, times(9)).info(anyString(), any(Object.class));
  }
}

更新二:

  • 我现在还向您的 Maven 构建添加了 AspectJ LTW 配置,请参阅 these commits in my fork
  • 我为您创建了一个 pull request (PR),您可以接受它,以便将我的所有更改添加到您的项目中,而无需复制和粘贴。