aop - 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;
解决方案
介绍
不幸的是,您的伪代码无法编译。它包含几个语法和逻辑错误。此外,缺少一些辅助类。如果我今天没有空闲时间并且正在寻找要解决的难题,我就不会费心制作自己的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
。这个切入点用在两个地方:@Aspect("percflow(entryPoint())")
说:为从事务入口点开始的每个控制流创建一个方面实例。@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")
也会消失。
如果您认为有意义,我将把这个重构留给您。
推荐阅读
- javascript - then() 和 catch() 子句都可以触发还是只是其中之一?
- python - 从 python 的 eval() 中删除乘法
- laravel - PayPal API 未连接(未给出错误)
- jsf - java.lang.ClassCastException:com.sun.faces.facelets.el.TagMethodExpression 无法转换为 java.lang.String
- c++ - gcc 如何允许 constexpr strlen() 但 clang 不允许?
- flutter - 从 ui 取消 Flutters 计算功能
- python - 尽管配置,Django 信息和调试日志被忽略
- java - 在不丢失同步的情况下加载 JTable
- java - 如何在 Spring MVC / Hibernate 中处理 CRUD 的 1:N 外键?
- android - NestedScrollView 不滚动?知道如何解决这个问题