Spring AOP - 在带注释的方法之间传递参数

Spring AOP - passing arguments between annotated methods

我编写了一个实用程序来监视单个业务交易。例如,Alice 调用了一个调用更多方法的方法,而我只想要有关 Alice 的调用的信息,与 Bob 对同一方法的调用分开。

现在入口点创建一个事务对象,并将其作为参数传递给每个方法:

class Example {
  public Item getOrderEntryPoint(int orderId) {
    Transaction transaction = transactionManager.create();
    transaction.trace("getOrderEntryPoint");
    Order order = getOrder(orderId, transaction);
    transaction.stop();
    logger.info(transaction);
    return item;
  }

  private Order getOrder(int orderId, Transaction t) {
    t.trace("getOrder");
    Order order = getItems(itemId, t);
    t.addStat("number of items", order.getItems().size());
    for (Item item : order.getItems()) {
      SpecialOffer offer = getSpecialOffer(item, t);
      if (null != offer) {
        t.incrementStat("offers", 1);
      }
    }
    t.stop();
    return order;
  }

  private SpecialOffer getSpecialOffer(Item item, Transaction t) {
    t.trace("getSpecialOffer(" + item.id + ")", TraceCategory.Database);
    return offerRepository.getByItem(item);
    t.stop();
  }
}

这将在日志中打印如下内容:

Transaction started by Alice at 10:42
Statistics:
    number of items : 3
    offers          : 1
Category Timings (longest first):
    DB   : 2s 903ms
    code : 187ms
Timings (longest first):
    getSpecialOffer(1013) : 626ms
    getItems              : 594ms
Trace:
  getOrderEntryPoint (7ms)
      getOrder (594ms)
          getSpecialOffer(911) (90ms)
          getSpecialOffer(1013) (626ms)
          getSpecialOffer(2942) (113ms)

它工作得很好,但传递交易对象很丑陋。有人建议使用 AOP,但我不知道如何将第一种方法中创建的事务传递给所有其他方法。

交易对象非常简单:

public class Transaction {
  private String uuid = UUID.createRandom();
  private List<TraceEvent> events = new ArrayList<>();
  private Map<String,Int> stats = new HashMap<>();
}

class TraceEvent {
  private String name;
  private long   durationInMs;
}

使用它的app是一个Web app,而且这个是多线程的,但是各个事务都是在单线程上的——没有多线程,异步代码,资源竞争等等

我的注释尝试:

@Around("execution(* *(..)) && @annotation(Trace)")
public Object around(ProceedingJoinPoint point) {
  String methodName = MethodSignature.class.cast(point.getSignature()).getMethod().getName();
  //--- Where do i get this call's instance of TRANSACTION from? 
  if (null == transaction) {
    transaction = TransactionManager.createTransaction();
  }
  transaction.trace(methodName);
  Object result = point.proceed();
  transaction.stop();
  return result;

简介

很遗憾,您的伪代码无法编译。它包含几个句法和逻辑错误。此外,一些助手 classes 丢失了。如果我今天没有空闲时间并且正在寻找一个谜题来解决,我就不会费心去制作我自己的 MCVE,因为那实际上是你的工作。请务必阅读 MCVE 文章并学习下次创建一个,否则您将无法在这里获得很多合格的帮助。这是你的免费镜头,因为你是 SO 的新手。

原情况:在方法调用中传递事务对象

应用程序助手 classes:

package de.scrum_master.app;

public class Item {
  private int id;

  public Item(int id) {
    this.id = id;
  }

  public int getId() {
    return id;
  }

  @Override
  public String toString() {
    return "Item[id=" + id + "]";
  }
}
package de.scrum_master.app;

public class SpecialOffer {}
package de.scrum_master.app;

public class OfferRepository {
  public SpecialOffer getByItem(Item item) {
    if (item.getId() < 30)
      return new SpecialOffer();
    return null;
  }
}
package de.scrum_master.app;

import java.util.ArrayList;
import java.util.List;

public class Order {
  private int id;

  public Order(int id) {
    this.id = id;
  }

  public List<Item> getItems() {
    List<Item> items = new ArrayList<>();
    int offset = id == 12345 ? 0 : 1;
    items.add(new Item(11 + offset, this));
    items.add(new Item(22 + offset, this));
    items.add(new Item(33 + offset, this));
    return items;
  }
}

跟踪 classes:

package de.scrum_master.trace;

public enum TraceCategory {
  Code, Database
}
package de.scrum_master.trace;

class TraceEvent {
  private String name;
  private TraceCategory category;
  private long durationInMs;
  private boolean finished = false;

  public TraceEvent(String name, TraceCategory category, long startTime) {
    this.name = name;
    this.category = category;
    this.durationInMs = startTime;
  }

  public long getDurationInMs() {
    return durationInMs;
  }

  public void setDurationInMs(long durationInMs) {
    this.durationInMs = durationInMs;
  }

  public boolean isFinished() {
    return finished;
  }

  public void setFinished(boolean finished) {
    this.finished = finished;
  }

  @Override
  public String toString() {
    return "TraceEvent[name=" + name + ", category=" + category +
      ", durationInMs=" + durationInMs + ", finished=" + finished + "]";
  }
}

交易classes:

我在这里尝试模仿您自己的 Transaction class 并进行尽可能少的更改,但为了模拟您的跟踪的简化版本,我不得不添加和修改很多内容输出。这不是线程安全的,我定位最后一个未完成的 TraceEvent 的方式并不好,只有在没有异常的情况下才能干净地工作。但我希望你明白了。关键是让它基本上工作,然后获得类似于您的示例的日志输出。如果这最初是我的代码,我会以不同的方式解决它。

package de.scrum_master.trace;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;

public class Transaction {
  private String uuid = UUID.randomUUID().toString();
  private List<TraceEvent> events = new ArrayList<>();
  private Map<String, Integer> stats = new HashMap<>();

  public void trace(String message) {
    trace(message, TraceCategory.Code);
  }

  public void trace(String message, TraceCategory category) {
    events.add(new TraceEvent(message, category, System.currentTimeMillis()));
  }

  public void stop() {
    TraceEvent event = getLastUnfinishedEvent();
    event.setDurationInMs(System.currentTimeMillis() - event.getDurationInMs());
    event.setFinished(true);
  }

  private TraceEvent getLastUnfinishedEvent() {
    return events
      .stream()
      .filter(event -> !event.isFinished())
      .reduce((first, second) -> second)
      .orElse(null);
  }

  public void addStat(String text, int size) {
    stats.put(text, size);
  }

  public void incrementStat(String text, int increment) {
    Integer currentCount = stats.get(text);
    if (currentCount == null)
      currentCount = 0;
    stats.put(text, currentCount + increment);
  }

  @Override
  public String toString() {
    return "Transaction {" +
      toStringUUID() +
      toStringStats() +
      toStringEvents() +
      "\n}\n";
  }

  private String toStringUUID() {
    return "\n  uuid = " + uuid;
  }

  private String toStringStats() {
    String result = "\n  stats = {";
    for (Entry<String, Integer> statEntry : stats.entrySet())
      result += "\n    " + statEntry;
    return result + "\n  }";
  }

  private String toStringEvents() {
    String result = "\n  events = {";
    for (TraceEvent event : events)
      result += "\n    " + event;
    return result + "\n  }";
  }
}
package de.scrum_master.trace;

public class TransactionManager {
  public Transaction create() {
    return new Transaction();
  }
}

示例驱动程序应用程序:

package de.scrum_master.app;

import de.scrum_master.trace.TraceCategory;
import de.scrum_master.trace.Transaction;
import de.scrum_master.trace.TransactionManager;

public class Example {
  private TransactionManager transactionManager = new TransactionManager();
  private OfferRepository offerRepository = new OfferRepository();

  public Order getOrderEntryPoint(int orderId) {
    Transaction transaction = transactionManager.create();
    transaction.trace("getOrderEntryPoint");
    sleep(100);
    Order order = getOrder(orderId, transaction);
    transaction.stop();
    System.out.println(transaction);
    return order;
  }

  private Order getOrder(int orderId, Transaction t) {
    t.trace("getOrder");
    sleep(200);
    Order order = new Order(orderId);
    t.addStat("number of items", order.getItems().size());
    for (Item item : order.getItems()) {
      SpecialOffer offer = getSpecialOffer(item, t);
      if (null != offer)
        t.incrementStat("special offers", 1);
    }
    t.stop();
    return order;
  }

  private SpecialOffer getSpecialOffer(Item item, Transaction t) {
    t.trace("getSpecialOffer(" + item.getId() + ")", TraceCategory.Database);
    sleep(50);
    SpecialOffer specialOffer = offerRepository.getByItem(item);
    t.stop();
    return specialOffer;
  }

  private void sleep(long millis) {
    try {
      Thread.sleep(millis);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  public static void main(String[] args) {
    new Example().getOrderEntryPoint(12345);
    new Example().getOrderEntryPoint(23456);
  }
}

如果你运行这段代码,输出如下:

Transaction {
  uuid = 62ec9739-bd32-4a56-b6b3-a8a13624961a
  stats = {
    special offers=2
    number of items=3
  }
  events = {
    TraceEvent[name=getOrderEntryPoint, category=Code, durationInMs=561, finished=true]
    TraceEvent[name=getOrder, category=Code, durationInMs=451, finished=true]
    TraceEvent[name=getSpecialOffer(11), category=Database, durationInMs=117, finished=true]
    TraceEvent[name=getSpecialOffer(22), category=Database, durationInMs=69, finished=true]
    TraceEvent[name=getSpecialOffer(33), category=Database, durationInMs=63, finished=true]
  }
}

Transaction {
  uuid = a420cd70-96e5-44c4-a0a4-87e421d05e87
  stats = {
    special offers=2
    number of items=3
  }
  events = {
    TraceEvent[name=getOrderEntryPoint, category=Code, durationInMs=469, finished=true]
    TraceEvent[name=getOrder, category=Code, durationInMs=369, finished=true]
    TraceEvent[name=getSpecialOffer(12), category=Database, durationInMs=53, finished=true]
    TraceEvent[name=getSpecialOffer(23), category=Database, durationInMs=63, finished=true]
    TraceEvent[name=getSpecialOffer(34), category=Database, durationInMs=53, finished=true]
  }
}

AOP重构

前言

请注意,我在这里使用 AspectJ,因为关于您的代码的两件事永远不会与 Spring AOP 一起工作,因为它与 delegation pattern based on dynamic proxies:

一起工作
  • 自调用(内部调用同class或超class的方法)
  • 拦截私有方法

由于这些 Spring AOP 限制,我建议您重构代码以避免上述两个问题,或者将 Spring 应用程序配置为 use full AspectJ via LTW (load-time weaving)

如您所见,我的示例代码根本不使用 Spring,因为 AspectJ 完全独立于 Spring 并且适用于任何 Java 应用程序(或其他 JVM 语言) ).

重构思路

现在您应该怎么做才能避免传递跟踪信息(Transaction 个对象)、污染您的核心应用程序代码并将其与跟踪调用纠缠在一起?

  • 您将事务跟踪提取到一个方面,负责所有 trace(..)stop() 调用。
  • 不幸的是,您的 Transaction class 包含不同类型的信息并执行不同的操作,因此您无法完全摆脱有关如何跟踪每个受影响方法的上下文信息。但至少您可以从方法主体中提取上下文信息,并使用带参数的注释将其转换为声明形式。
  • 这些注释可以由负责处理事务跟踪的方面作为目标。

添加和更新代码,迭代 1

事务跟踪相关注释:

package de.scrum_master.trace;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(RUNTIME)
@Target(METHOD)
public @interface TransactionEntryPoint {}
package de.scrum_master.trace;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(RUNTIME)
@Target(METHOD)
public @interface TransactionTrace {
  String message() default "__METHOD_NAME__";
  TraceCategory category() default TraceCategory.Code;
  String addStat() default "";
  String incrementStat() default "";
}

重构的应用程序 classes with annotations:

package de.scrum_master.app;

import java.util.ArrayList;
import java.util.List;

import de.scrum_master.trace.TransactionTrace;

public class Order {
  private int id;

  public Order(int id) {
    this.id = id;
  }

  @TransactionTrace(message = "", addStat = "number of items")
  public List<Item> getItems() {
    List<Item> items = new ArrayList<>();
    int offset = id == 12345 ? 0 : 1;
    items.add(new Item(11 + offset));
    items.add(new Item(22 + offset));
    items.add(new Item(33 + offset));
    return items;
  }
}

这里没什么,只是在 getItems() 中添加了注释。但是示例应用程序 class 发生了巨大的变化,变得更加清晰和简单:

package de.scrum_master.app;

import de.scrum_master.trace.TraceCategory;
import de.scrum_master.trace.TransactionEntryPoint;
import de.scrum_master.trace.TransactionTrace;

public class Example {
  private OfferRepository offerRepository = new OfferRepository();

  @TransactionEntryPoint
  @TransactionTrace
  public Order getOrderEntryPoint(int orderId) {
    sleep(100);
    Order order = getOrder(orderId);
    return order;
  }

  @TransactionTrace
  private Order getOrder(int orderId) {
    sleep(200);
    Order order = new Order(orderId);
    for (Item item : order.getItems()) {
      SpecialOffer offer = getSpecialOffer(item);
      // Do something with special offers
    }
    return order;
  }

  @TransactionTrace(category = TraceCategory.Database, incrementStat = "specialOffers")
  private SpecialOffer getSpecialOffer(Item item) {
    sleep(50);
    SpecialOffer specialOffer = offerRepository.getByItem(item);
    return specialOffer;
  }

  private void sleep(long millis) {
    try {
      Thread.sleep(millis);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  public static void main(String[] args) {
    new Example().getOrderEntryPoint(12345);
    new Example().getOrderEntryPoint(23456);
  }
}

看到了吗?除了一些注释之外,事务跟踪逻辑什么也没有留下,应用程序代码只处理它的核心问题。如果您还删除 sleep() 方法,该方法只会使应用程序变慢以用于演示目的(因为我们想要一些测量时间 >0 毫秒的漂亮统计数据),class 会变得更加紧凑。

但是我们当然需要将事务跟踪逻辑放在某个地方,更准确地说,将其模块化为一个 AspectJ 方面:

事务跟踪方面:

package de.scrum_master.trace;

import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;

@Aspect("percflow(entryPoint())")
public class TransactionTraceAspect {
  private static TransactionManager transactionManager = new TransactionManager();

  private Transaction transaction = transactionManager.create();

  @Pointcut("execution(* *(..)) && @annotation(de.scrum_master.trace.TransactionEntryPoint)")
  private static void entryPoint() {}

  @Around("execution(* *(..)) && @annotation(transactionTrace)")
  public Object doTrace(ProceedingJoinPoint joinPoint, TransactionTrace transactionTrace) throws Throwable {
    preTrace(transactionTrace, joinPoint);
    Object result = joinPoint.proceed();
    postTrace(transactionTrace);
    addStat(transactionTrace, result);
    incrementStat(transactionTrace, result);
    return result;
  }

  private void preTrace(TransactionTrace transactionTrace, ProceedingJoinPoint joinPoint) {
    String traceMessage = transactionTrace.message();
    if ("".equals(traceMessage))
      return;
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    if ("__METHOD_NAME__".equals(traceMessage)) {
      traceMessage = signature.getName() + "(";
      traceMessage += Arrays.stream(joinPoint.getArgs()).map(arg -> arg.toString()).collect(Collectors.joining(", "));
      traceMessage += ")";
    }
    transaction.trace(traceMessage, transactionTrace.category());
  }

  private void postTrace(TransactionTrace transactionTrace) {
    if ("".equals(transactionTrace.message()))
      return;
    transaction.stop();
  }

  private void addStat(TransactionTrace transactionTrace, Object result) {
    if ("".equals(transactionTrace.addStat()) || result == null)
      return;
    if (result instanceof Collection)
      transaction.addStat(transactionTrace.addStat(), ((Collection<?>) result).size());
    else if (result.getClass().isArray())
      transaction.addStat(transactionTrace.addStat(), Array.getLength(result));
  }

  private void incrementStat(TransactionTrace transactionTrace, Object result) {
    if ("".equals(transactionTrace.incrementStat()) || result == null)
      return;
    transaction.incrementStat(transactionTrace.incrementStat(), 1);
  }

  @After("entryPoint()")
  public void logFinishedTransaction(JoinPoint joinPoint) {
    System.out.println(transaction);
  }
}

让我解释一下这方面的作用:

  • @Pointcut(..) entryPoint() 说:在 @TransactionEntryPoint 注释的代码中查找所有方法。这个切入点用在两个地方:

    1. @Aspect("percflow(entryPoint())") 说:为从事务入口点开始的每个控制流创建一个方面实例。

    2. @After("entryPoint()") logFinishedTransaction(..) 说:在入口点方法完成后执行此建议(AOP 术语,用于链接到切入点的方法)。相应的方法只是打印交易统计信息,就像在 Example.getOrderEntryPoint(..).

    3. 末尾的原始代码一样
  • @Around("execution(* *(..)) && @annotation(transactionTrace)") doTrace(..)说:包装由 TransactionTrace 注释的方法并执行以下操作(方法主体):

    • 添加新的微量元素并开始测量时间
    • 执行原始(包装)方法并存储结果
    • 用测量时间更新微量元素
    • 添加一种统计信息(可选)
    • 增加另一种统计数据(可选)
    • return 将方法的结果包装到其调用者
  • 私有方法只是 @Around 建议的帮手。

运行更新 Example class 和活动 AspectJ 时的控制台日志是:

Transaction {
  uuid = 4529d325-c604-441d-8997-45ca659abb14
  stats = {
    specialOffers=2
    number of items=3
  }
  events = {
    TraceEvent[name=getOrderEntryPoint(12345), category=Code, durationInMs=468, finished=true]
    TraceEvent[name=getOrder(12345), category=Code, durationInMs=366, finished=true]
    TraceEvent[name=getSpecialOffer(Item[id=11]), category=Database, durationInMs=59, finished=true]
    TraceEvent[name=getSpecialOffer(Item[id=22]), category=Database, durationInMs=50, finished=true]
    TraceEvent[name=getSpecialOffer(Item[id=33]), category=Database, durationInMs=51, finished=true]
  }
}

Transaction {
  uuid = ef76a996-8621-478b-a376-e9f7a729a501
  stats = {
    specialOffers=2
    number of items=3
  }
  events = {
    TraceEvent[name=getOrderEntryPoint(23456), category=Code, durationInMs=452, finished=true]
    TraceEvent[name=getOrder(23456), category=Code, durationInMs=351, finished=true]
    TraceEvent[name=getSpecialOffer(Item[id=12]), category=Database, durationInMs=50, finished=true]
    TraceEvent[name=getSpecialOffer(Item[id=23]), category=Database, durationInMs=50, finished=true]
    TraceEvent[name=getSpecialOffer(Item[id=34]), category=Database, durationInMs=50, finished=true]
  }
}

你看,它看起来与原始应用程序几乎相同。

进一步简化的想法,迭代 2

当阅读方法Example.getOrder(int orderId)时,我想知道你为什么要调用order.getItems(),循环它并在循环中调用getSpecialOffer(item)。在您的示例代码中,除了更新事务跟踪对象之外,您不会将结果用于任何其他用途。我假设在您的真实代码中,您在该方法中对订单和特价商品做了一些事情。

但以防万一您真的不需要该方法中的那些调用,我建议

  • 您将调用直接纳入方面,摆脱 TransactionTrace 注释参数 String addStat()String incrementStat().
  • Example 代码会变得更简单并且
  • class 中的注释 @TransactionTrace(message = "", addStat = "number of items") 也会消失。

如果你认为它有意义,我将把这个重构留给你。