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 语法。
我们正在使用 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 语法。