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

事务对象非常简单:

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;

简介

不幸的是,您的伪代码无法编译。它包含几个语法和逻辑错误。此外,缺少一些帮助程序类。如果我今天没有空闲时间,正在寻找一个难题来解决,我就不会费心用它制作自己的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的限制,我建议您重构代码以避免上述两个问题,或者通过LTW(加载时编织)将Spring应用程序配置为使用完整的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 毫秒)的漂亮统计信息,则该类将变得更加紧凑。

但是,当然,我们需要将事务跟踪逻辑放在某个地方,更准确地将其模块化为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")注释也会消失。

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

相关内容

最新更新