首页 > 解决方案 > Spring AOP - 在带注释的方法之间传递参数

问题描述

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

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

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,但我不知道如何将第一种方法中创建的事务传递给所有其他方法。

Transaction 对象非常简单:

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;
}

使用它的应用程序是一个 Web 应用程序,并且是多线程的,但各个事务都在一个线程上——没有多线程、异步代码、资源竞争等。

我尝试注释:

@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;

标签: aopspring-aop

解决方案


介绍

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

原情况:在方法调用中通过事务对象

应用程序助手类:

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;
  }
}

跟踪类:

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 + "]";
  }
}

事务类:

在这里,我尝试Transaction用尽可能少的更改来模仿您自己的类,但是为了模拟跟踪输出的简化版本,我必须添加和修改很多内容。这不是线程安全的,我定位最后一个未完成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 一起使用,因为它与基于动态代理的委托模式一起使用:

  • 自调用(内部调用同一类或超类的方法)
  • 拦截私有方法

由于这些 Spring AOP 限制,我建议您重构代码以避免上述两个问题,或者将 Spring 应用程序配置为通过 LTW(加载时编织)使用完整的 AspectJ

正如您所注意到的,我的示例代码根本没有使用 Spring,因为 AspectJ 完全独立于 Spring,并且可以与任何 Java 应用程序(或其他 JVM 语言)一起使用。

重构思路

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

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

添加和更新代码,迭代 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 "";
}

使用注释重构应用程序类:

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(). 但是示例应用程序类发生了巨大变化,变得更加简洁和简单:

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 ms 的漂亮统计数据),则该类变得更加紧凑。

但是当然我们需要将事务跟踪逻辑放在某个地方,更准确地说是将其模块化为 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(..)

  • @Around("execution(* *(..)) && @annotation(transactionTrace)") doTrace(..)说:包装由注释的方法TransactionTrace并执行以下操作(方法体):

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

Example运行更新的类和活动 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变得更加简单和
  • 课堂上的注释@TransactionTrace(message = "", addStat = "number of items")也会消失。

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


推荐阅读