java - 各种切入点表达式范围意外触发多个通知调用
问题描述
背景
使用切面记录项目,使得所有标有@Log
注解的方法、类和构造函数都将信息写入日志文件。
问题
方法似乎被递归地称为一级深度,但代码没有显示任何这样的递归关系。
实际的
记录结果:
2018-09-25 12:17:29,155 |↷| EmailNotificationServiceBean#createPayload([SECURE])
2018-09-25 12:17:29,155 |↷| EmailNotificationServiceBean#createPayload([{service.notification.smtp.authentication.password=password, mail.smtp.port=25, service.notification.smtp.authentication.username=dev@localhost, mail.mime.allowutf8=true, mail.smtp.auth=false, mail.smtp.starttls.enable=false, mail.smtp.timeout=10000, mail.smtp.host=localhost}])
2018-09-25 12:17:29,193 |↷| EmailPayloadImpl#<init>([{service.notification.smtp.authentication.password=password, mail.smtp.port=25, service.notification.smtp.authentication.username=dev@localhost, mail.mime.allowutf8=true, mail.smtp.auth=false, mail.smtp.starttls.enable=false, mail.smtp.timeout=10000, mail.smtp.host=localhost}])
2018-09-25 12:17:29,193 |↷| EmailPayloadImpl#validate([SECURE])
2018-09-25 12:17:29,194 |↷| EmailPayloadImpl#validate([{service.notification.smtp.authentication.password=password, mail.smtp.port=25, service.notification.smtp.authentication.username=dev@localhost, mail.mime.allowutf8=true, mail.smtp.auth=false, mail.smtp.starttls.enable=false, mail.smtp.timeout=10000, mail.smtp.host=localhost}, SMTP connection and credentials])
2018-09-25 12:17:29,195 |↷| EmailPayloadImpl#setMailServerSettings([SECURE])
2018-09-25 12:17:29,196 |↷| EmailPayloadImpl#setMailServerSettings([{service.notification.smtp.authentication.password=password, mail.smtp.port=25, service.notification.smtp.authentication.username=dev@localhost, mail.mime.allowutf8=true, mail.smtp.auth=false, mail.smtp.starttls.enable=false, mail.smtp.timeout=10000, mail.smtp.host=localhost}])
预期的
预期的记录结果:
2018-09-25 12:17:29,155 |↷| EmailNotificationServiceBean#createPayload([SECURE])
2018-09-25 12:17:29,193 |↷| EmailPayloadImpl#<init>([SECURE])
2018-09-25 12:17:29,193 |↷| EmailPayloadImpl#validate([SECURE])
2018-09-25 12:17:29,195 |↷| EmailPayloadImpl#setMailServerSettings([SECURE])
代码
日志记录方面:
@Aspect
public class LogAspect {
@Pointcut("execution(public @Log( secure = true ) *.new(..))")
public void loggedSecureConstructor() { }
@Pointcut("execution(@Log( secure = true ) * *.*(..))")
public void loggedSecureMethod() { }
@Pointcut("execution(public @Log( secure = false ) *.new(..))")
public void loggedConstructor() { }
@Pointcut("execution(@Log( secure = false ) * *.*(..))")
public void loggedMethod() { }
@Pointcut("execution(* (@Log *) .*(..))")
public void loggedClass() { }
@Around("loggedSecureMethod() || loggedSecureConstructor()")
public Object logSecure(final ProceedingJoinPoint joinPoint) throws Throwable {
return log(joinPoint, true);
}
@Around("loggedMethod() || loggedConstructor() || loggedClass()")
public Object log(final ProceedingJoinPoint joinPoint) throws Throwable {
return log(joinPoint, false);
}
private Object log(final ProceedingJoinPoint joinPoint, boolean secure) throws Throwable {
final Signature signature = joinPoint.getSignature();
final Logger log = getLogger(signature);
final String className = getSimpleClassName(signature);
final String memberName = signature.getName();
final Object[] args = joinPoint.getArgs();
final CharSequence indent = getIndentation();
final String params = secure ? "[SECURE]" : Arrays.deepToString(args);
log.trace("\u21B7| {}{}#{}({})", indent, className, memberName, params);
try {
increaseIndent();
return joinPoint.proceed(args);
} catch (final Throwable t) {
final SourceLocation source = joinPoint.getSourceLocation();
log.warn("\u2717| {}[EXCEPTION {}] {}", indent, source, t.getMessage());
throw t;
} finally {
decreaseIndent();
log.trace("\u21B6| {}{}#{}", indent, className, memberName);
}
}
Log
接口定义:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR})
public @interface Log {
boolean secure() default false;
}
反编译的服务 bean:
@Log
public class EmailNotificationServiceBean
implements EmailNotificationService {
@Log(secure = true)
@Override
public EmailPayload createPayload(Map<String, Object> settings) throws NotificationServiceException {
Map<String, Object> map = settings;
JoinPoint joinPoint = Factory.makeJP((JoinPoint.StaticPart)ajc$tjp_2, (Object)this, (Object)this, map);
Object[] arrobject = new Object[]{this, map, joinPoint};
return (EmailPayload)LogAspect.aspectOf().logSecure(new EmailNotificationServiceBean$AjcClosure7(arrobject).linkClosureAndJoinPoint(69648));
}
有效载荷实现:
@Log
public class EmailPayloadImpl extends AbstractPayload implements EmailPayload {
@Log(secure = true)
public EmailPayloadImpl(final Map<String, Object> settings)
throws NotificationServiceException {
validate(settings, "SMTP connection and credentials");
setMailServerSettings(settings);
}
@Log(secure = true)
private void validate(final Map<String, Object> map, final String message)
throws NotificationServiceException {
if (map == null || map.isEmpty()) {
throwException(message);
}
}
@Log(secure = true)
private void setMailServerSettings(final Map<String, Object> settings) {
this.mailServerSettings = settings;
}
问题
是什么原因造成的:
secure = true
要忽略的构造函数注释属性;和- 和方法被调用和记录两次(一次安全,一次不安全)
validate
?setMailServerSettings
我怀疑这些问题是相关的。
解决方案
解决方案:
要解决重复问题需要调整loggedClass()
切入点定义:
@Pointcut("execution(* (@Log *) .*(..)) && !@annotation(Log)")
public void loggedClass() { }
另请参阅附加信息部分中的概念证明链接。
解释:
与连接点相关的问题(由@Pointcut
注释定义),它们的模式相互交叉 - 这就是日志中重复的原因。
在我们的例子中,所有@Pointcut
s 的名称都足够具有描述性,例如:
loggedClass()
涵盖由 注释的类中的所有方法@Log
。loggedSecureMethod()
涵盖由 注释的所有方法@Log(secure = true)
。其余的与此类似,因此我们忽略它们进行解释。
因此,如果 whenEmailPayloadImpl
被注释@Log
和EmailPayloadImpl.validate()
被注释@Log(secure = true)
- 我们将有 2 个活动连接点:一个安全的和一个非安全的。这将导致添加 2 个日志条目。
假设我们要在注解中引入优先级,即方法级别注解应该覆盖类级别的注解——最简单的方法就是避免交叉连接点模式。
所以我们需要有3组方法:
@Log(secure = true)
用=注释的方法loggedSecureMethod()
@Log
用=注释的方法loggedMethod()
没有
@Log
注释的方法,但在一个用 注释的类中@Log
,即:@Pointcut("execution(* (@Log *) .*(..)) && !@annotation(Log)") public void loggedClass() { }
附加信息:
- 如果还需要
@Log(secure = true)
在类级别上处理它 - 当然需要添加类似loggedClass()
的附加连接点。 - 在GitHub中添加了概念证明 >>
推荐阅读
- git - git cherry-pick:手动接受“我们的”或“他们的”冲突文件中的帅哥
- node.js - Express JS 自定义高水位线
- c# - 我应该如何阅读键盘输入以在 WPF 中创建 2D 游戏?
- sql - 多值……不能用于 WHERE 或 HAVING
- python - 如何在 sklearn 中使用带有自定义估计器的交叉验证?
- flyway - Flyway - 当架构的版本比最新的可用迁移更新时如何解决问题
- google-apps-script - Google Apps 脚本限制范围
- c++ - 在 ROS 工作空间中使用 catkin_make 时 Qt 出现的问题
- docker - 在 x86 上使用 Qemu for ARM/x86 构建映像
- c# - 检查表是否包含元素