问题
Background
Logging a project using aspects such that all methods, classes, and constructors that are marked with the @Log annotation have information written to a log file.
Problem
Methods appear to be called recursively one-level deep, but the code does not show any such recursive relationship.
Actual
Logged results:
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}])
Expected
Expected logged results:
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])
Code
The logging aspect:
@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);
}
}
The Log interface definition:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR})
public @interface Log {
boolean secure() default false;
}
The decompiled service 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));
}
The payload implementation:
@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;
}
Question
What is causing:
- the
secure = trueconstructor annotation attribute to be ignored; and - the
validateandsetMailServerSettingsmethods to be called and logged twice (once securely and once not)?
I suspect the issues are related.
回答1:
Solution:
To fix duplication issue need to adjust loggedClass() pointcut definition:
@Pointcut("execution(* (@Log *) .*(..)) && !@annotation(Log)")
public void loggedClass() { }
Please also find a link to Proof of concept in the Additional information section.
Explanation:
Issue related to join points (defined by @Pointcut annotation), their patterns cross each other - and this is the reason of duplication in the logs.
In our case all @Pointcuts named descriptive enough, e.g.:
loggedClass()covers all methods in the classes annotated by@Log.loggedSecureMethod()covers all methods annotated by@Log(secure = true). Others remain are similar to this one, so let's ignore them for explanation.
So in case when EmailPayloadImpl is annotated with @Log and EmailPayloadImpl.validate() is annotated with @Log(secure = true) - we will have 2 active join points: one secure and one non-secure. And this will cause adding 2 log entries.
Assuming that we want to introduce priority in the annotations, i.e. method-level annotation should overwrite class-level one - the simplest way will be just to avoid crossing join point patterns.
So we will need to have 3 groups for methods:
- Methods annotated with
@Log(secure = true)=loggedSecureMethod() - Methods annotated with
@Log=loggedMethod() Methods without
@Logannotation, but within a class annotated with@Log, which is:@Pointcut("execution(* (@Log *) .*(..)) && !@annotation(Log)") public void loggedClass() { }
Additional information:
- In case it will be needed to handle also
@Log(secure = true)on the class level - need to add additional join point similar tologgedClass()of course. - Added Proof of concept >> in the GitHub
回答2:
Basically your example has two issues which are causing the the duplication and the "ignoring" of the secured annotation.
First of all you have a @Log class level annotation in EmailPayloadImpl and a pointcut @Pointcut("execution(* (@Log *) .*(..))") named loggedClass which basically applies log on all of the class methods including the constructors. So that means that any method call on EmailPayloadImpl will end up with insecure log as the default value for secure attribute is false in Log annotation.
Second issue is the fact that your pointcuts for constructors are not correct that is why the secure = true is ignored. You are using the visibility keywords which is not correct when creating pointcuts for constructors. So the pointcut
@Pointcut("execution(public @Log( secure = true ) *.new(..))")
public void loggedSecureConstructor() { }
Should be changed to
@Pointcut("execution(@Log( secure = true ) *.new(..))")
public void loggedSecureConstructor() { }
The only difference is removal of the public visibility keyword. Same should be done for loggedConstructor().
So as you constructor pointcuts were incorrect the @Log annotation on the constructor was ignored as there was no pointcut which should have used it. And the
@Logon the class was just applying log for all of the class methods including the constructor.
Tho make your example for you need to fix the pointcuts and remove the class level annotation.
See your a fix of your example
Lets fix the pointcuts in LogAspect
@Aspect
public class LogAspect {
@Pointcut("execution(@Log( secure = true ) *.new(..))")
public void loggedSecureConstructor() { }
@Pointcut("execution(@Log( secure = true ) * *.*(..))")
public void loggedSecureMethod() { }
@Pointcut("execution(@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);
}
}
Remove the class level @Log annotation
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;
}
来源:https://stackoverflow.com/questions/52506915/various-pointcut-expression-scopes-trigger-multiple-advice-calls-unexpectedly