Aspectj 获取业务层记录器调用并将它们放入 REST 响应对象中

Aspectj get business layer logger calls and put them into a REST response object

我们正在使用 Spring 和分层架构(REST 服务层、业务层和存储库)开发 Web 应用程序。 对于每个 REST 服务,我们返回一个通用的 RestResponse 对象,它有一个数据字段和一个消息和错误列表。

虽然当我们需要对在 REST 层中获得的数据执行验证时,我们可以在该层或业务层(或两者)中进行。我想到了一个想法,只在业务层做验证,避免重复代码。

我的想法是,当我们在REST层创建一个RestResponse对象,然后通过调用业务层方法给它设置数据。在该业务方法中,我们将进行验证并调用 Logger 来记录一些消息(警告或错误)。使用 aspectj,这些记录器调用将被拦截,并且它们的参数(消息)将直接放入我们的 RestResponse 消息列表中。

为了更清楚,这里有一些代码示例:

REST层返回给客户端的对象

public class RestResponse<T> {
    private T data;
    List<String> messages = new ArrayList();

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public final List<String> getMessages() {
        return messages;
    }

    public void setMessages(List<String> messages) {
        this.messages = messages;
    }
}

休息控制器

@RestController
public class TestRestController {
    @Autowired
    private ServiceImpl service;

    @RequestMapping("")
    public RestResponse<?> callService() {

        RestResponse<String> response = new RestResponse<>();
        // We would only set data, message list would be populated in the aspect from logger call arguments
        response.setData(service.returnData());
        return response;
    }
}

服务

@Service
public class ServiceImpl {
    private static Logger log = Logger.getLogger(ServiceImpl.class.getName());

    public String returnData() {
        log.log(Level.WARNING, "Some warning message");
        return "Some data...";
    }
}

从 RestResponse 获取消息列表并向其添加消息的方面 class

@Aspect
public class RestAspect {

    @Pointcut("call(* java.util.logging.Logger.log(..)) " +
            "&& cflow(execution(* com.gg.spring.tests.services.ServiceImpl.returnData(..))) " +
            "&& cflow(execution(* com.gg.spring.tests.rest.TestRestController.callService(..)))" +
            "&& !within(RestAspect)")
    private void logPointcut() {
    }

    @Pointcut("call(com.gg.spring.tests.rest.RestResponse.new()) " +
            "&& cflow(execution(* com.gg.spring.tests.rest.TestRestController.callService(..)))" +
            "&& !within(RestAspect)")
    private void afterConstructingResponsePointcut() {
    }

    private List<String> messages;

    @Around(value = "logPointcut()")
    public void loggerCall(ProceedingJoinPoint joinPoint) {
        Object[] response = joinPoint.getArgs();
        synchronized (this.messages) {
            this.messages.add((String) response[1]);
        }
    }

    @AfterReturning(returning = "response", pointcut = "afterConstructingResponsePointcut()")
    public void firstCall(JoinPoint jp, RestResponse response) {
        synchronized (response.getMessages()) {
            this.messages = response.getMessages();
        }
    }
}

我似乎可以使用这段代码,但是,当我使用多个线程进行测试时,我得到了一些带有 0 条消息的 RestResponse 对象,或者一些带有 3 条消息的对象,但是它们中的每一个都应该只有 1 条消息,因为这就是我在服务方法中调用日志方法的次数。

通过我编写的测试,我创建了 128 个线程,每个线程调用 Rest 层方法 1000 次,在 128 000 次调用中,有 200 次调用的消息数量与预期的不同。数字似乎不高,但在正确的情况下可能非常重要。

有没有人过去做过这样的事情并可以分享他们的经验?还有其他方法可以做到这一点,但如果这可行,那将是减少代码的好方法。非常感谢您的帮助。

有几种方法可以解决这个问题。最简单的方法是在您的方面中使用 thread-local 变量。那么你也不再需要同步块了。后者无论如何都不能解决你的问题,因为问题是多个线程写入同一个方面成员,方面是一个单例。

@Aspect
public class RestAspect {

  @Pointcut(
    "call(* java.util.logging.Logger.log(..)) "
    + "&& args(*, logMessage)"
    + "&& cflow(execution(* com.gg.spring.tests.services.ServiceImpl.returnData(..))) "
    + "&& cflow(execution(* com.gg.spring.tests.rest.TestRestController.callService(..)))"
    + "&& !within(RestAspect)")
  private void logPointcut(String logMessage) { }

  @Pointcut(
    "call(com.gg.spring.tests.rest.RestResponse.new()) "
    + "&& cflow(execution(* com.gg.spring.tests.rest.TestRestController.callService(..)))"
    + "&& !within(RestAspect)")
  private void afterConstructingResponsePointcut() {}

  private ThreadLocal<List<String>> messages = new ThreadLocal<>();

  @Around("logPointcut(logMessage)")
  public void loggerCall(ProceedingJoinPoint joinPoint, String logMessage) {
    this.messages.get().add(logMessage);
  }

  @AfterReturning(returning = "response", pointcut = "afterConstructingResponsePointcut()")
  public void firstCall(JoinPoint jp, RestResponse<String> response) {
    this.messages.set(response.getMessages());
  }
}

我写了一个像你的测试程序(128 个线程,每个线程 1000 次调用)并且可以重现你的问题并确认我的解决方案有效。

顺便说一句,我也稍微改变了方面以便使用 args() 参数绑定而不是 jp.getArgs()


解决此问题的另一种方法是为您的方面使用 non-singleton 实例化,例如 percflow。但是你需要使用原生的 AspectJ 语法。