问题
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 = true
constructor annotation attribute to be ignored; and - the
validate
andsetMailServerSettings
methods 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 @Pointcut
s 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
@Log
annotation, 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
@Log
on 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