Is it possible to get annotations of a method in advice related with field set pointcuts?
Solution 1:
Validation based on setter methods
Currently, you are intercepting field write access using a set
poitcut. If you want to access annotations from setter methods, you need to use a pointcut which intercepts methods, such as execution
and/or @annotation
. Then it is easy to access the annotation.
The field write access this.value= value
itself is not annotated and cannot be, because the JVM does not know annotations on individual lines of code. You could of course annotated the field and then fetch the annotation from the field, but I think the annotated setter method is a good way to do it. With the little bit of information you provided, this is hard to tell.
BTW, the set
pointcut would also intercept field write access from places other than annotated methods, which does not seem to be what you want.
Another problem in your code is that in an @AfterReturning
advice, you cannot stop the value to be validated to be assigned, because, as the pointcut name implies, the aspect fires after the methods has already finished its job. You should either use a @Before
pointcut, if you want to throw a validation exception in case of negative validation, or an @Around
pointcut if you want to do something more sophisticated, such as setting a default value in case of negative validation. I am showing you the simple case in my MCVE:
Helper classes to make the example code compile:
package de.scrum_master.app;
public class Group1 {}
package de.scrum_master.app;
public class Group2 {}
package de.scrum_master.app;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface Validated {
Class<?>[] value();
}
Target class with setter to be validated and main method:
package de.scrum_master.app;
public class WovenClass {
private int value;
private String name;
public void setName(String name) {
this.name = name;
}
@Validated({ Group1.class, Group2.class })
public void setField(int value) {
this.value= value;
}
public static void main(String[] args) {
// Should not be intercepted (no validation annotation)
new WovenClass().setName("foo");
// Should yield positive validation
new WovenClass().setField(11);
// Should yield negative validation -> exception
new WovenClass().setField(0);
}
}
Validation aspect:
package de.scrum_master.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import de.scrum_master.app.Validated;
@Aspect
public class ValidationAspect {
@Before("execution(* set*(*)) && @annotation(validated) && args(value)")
public void validate(JoinPoint jp, Object value, Validated validated) {
System.out.println(jp);
System.out.println(" validation rule: " + validated);
System.out.println(" value to be set: " + value);
// Some random condition in order to mimic a negative validation
if (value.equals(0))
throw new RuntimeException("field validation failed");
}
}
Please note the consituents of the pointcut and how the annotation and the method parameter are both conveniently bound to advice method values, which you can directly access without any reflection magic for annotation access or ugly calls like jp.getArgs[0]
.
Console log:
execution(void de.scrum_master.app.WovenClass.setField(int))
validation rule: @de.scrum_master.app.Validated(value={de.scrum_master.app.Group1.class, de.scrum_master.app.Group2.class})
value to be set: 11
execution(void de.scrum_master.app.WovenClass.setField(int))
validation rule: @de.scrum_master.app.Validated(value={de.scrum_master.app.Group1.class, de.scrum_master.app.Group2.class})
value to be set: 0
Exception in thread "main" java.lang.RuntimeException: field validation failed
at de.scrum_master.aspect.ValidationAspect.validate(ValidationAspect.aj:18)
at de.scrum_master.app.WovenClass.setField(WovenClass.java:13)
at de.scrum_master.app.WovenClass.main(WovenClass.java:22)
Validation based on field write access
Update: OK, now that we have established that for whatever reason you want to intercept field setters everywhere rather than only in annotated setter methods, using an EnclosingStaticPart
advice parameter in order to get the calling method? But because chances are that some methods are not annotated with @Validated
- e.g. setName
in my example above - the advice gets triggered anyway. You have to search for the annotation via reflection and only perform validation when it is found.
I also extended the aspect to also cover validation annotations on constructors and adjusted the main class in order to test that:
package de.scrum_master.app;
public class WovenClass {
private int value;
private String name;
public WovenClass() {
}
@Validated({ Group1.class, Group2.class })
public WovenClass(int value, String name) {
this.value = value;
this.name = name;
}
public void setName(String name) {
this.name = name;
}
@Validated({ Group1.class, Group2.class })
public void setField(int value) {
this.value = value;
}
public static void main(String[] args) {
// Should not be intercepted (no validation annotation)
new WovenClass().setName("foo");
// Should yield positive validation
new WovenClass().setField(11);
// Should yield positive validation
new WovenClass(22, "bar");
// Should yield negative validation -> exception
try {
new WovenClass(0, "zot");
} catch (RuntimeException e) {
System.out.println(" " + e);
}
// Should yield negative validation -> exception
try {
new WovenClass().setField(0);
} catch (RuntimeException e) {
System.out.println(" " + e);
}
}
}
package de.scrum_master.aspect;
import java.lang.reflect.Executable;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.JoinPoint.EnclosingStaticPart;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.ConstructorSignature;
import org.aspectj.lang.reflect.MethodSignature;
import de.scrum_master.app.Validated;
@Aspect
public class ValidationAspect {
@Before("set(* *) && args(value)")
public void validateFieldSet(JoinPoint jp, EnclosingStaticPart enclosing, Object value) {
System.out.println(jp);
Signature signature = enclosing.getSignature();
boolean isMethod = signature instanceof MethodSignature;
System.out.println(" Enclosing " + (isMethod ? "method: " : "constructor: ") + enclosing);
Executable executable = isMethod
? ((MethodSignature) signature).getMethod()
: ((ConstructorSignature) signature).getConstructor();
Validated[] validatedAnnotations = executable.getDeclaredAnnotationsByType(Validated.class);
if (validatedAnnotations.length == 0)
System.out.println(" @Validated not found, skipping validation");
for (Validated validated : validatedAnnotations) {
System.out.println(" Validation rule: " + validated);
System.out.println(" Value to be set: " + value);
// Some random condition in order to mimic a negative validation
if (value.equals(0))
throw new RuntimeException("field validation failed");
}
}
}
The console log will change to:
set(String de.scrum_master.app.WovenClass.name)
Enclosing method: execution(void de.scrum_master.app.WovenClass.setName(String))
@Validated not found, skipping validation
set(int de.scrum_master.app.WovenClass.value)
Enclosing method: execution(void de.scrum_master.app.WovenClass.setField(int))
Validation rule: @de.scrum_master.app.Validated(value={de.scrum_master.app.Group1.class, de.scrum_master.app.Group2.class})
Value to be set: 11
set(int de.scrum_master.app.WovenClass.value)
Enclosing constructor: execution(de.scrum_master.app.WovenClass(int, String))
Validation rule: @de.scrum_master.app.Validated(value={de.scrum_master.app.Group1.class, de.scrum_master.app.Group2.class})
Value to be set: 22
set(String de.scrum_master.app.WovenClass.name)
Enclosing constructor: execution(de.scrum_master.app.WovenClass(int, String))
Validation rule: @de.scrum_master.app.Validated(value={de.scrum_master.app.Group1.class, de.scrum_master.app.Group2.class})
Value to be set: bar
set(int de.scrum_master.app.WovenClass.value)
Enclosing constructor: execution(de.scrum_master.app.WovenClass(int, String))
Validation rule: @de.scrum_master.app.Validated(value={de.scrum_master.app.Group1.class, de.scrum_master.app.Group2.class})
Value to be set: 0
java.lang.RuntimeException: field validation failed
set(int de.scrum_master.app.WovenClass.value)
Enclosing method: execution(void de.scrum_master.app.WovenClass.setField(int))
Validation rule: @de.scrum_master.app.Validated(value={de.scrum_master.app.Group1.class, de.scrum_master.app.Group2.class})
Value to be set: 0
java.lang.RuntimeException: field validation failed
Resources for thisEnclosingJoinPointStaticPart
and EnclosingStaticPart
How could I know existence of EnclosingStaticPart arg? I think I searched into AspectJ references kind of deeply though. It would be nice if you could provide me a reference or something about it.
-
The programming guide for native AspectJ syntax mentions the
thisEnclosingJoinPointStaticPart
variable in several places:- https://www.eclipse.org/aspectj/doc/next/progguide/printable.html#language-thisJoinPoint
- https://www.eclipse.org/aspectj/doc/next/progguide/printable.html#quick-advice
- https://www.eclipse.org/aspectj/doc/next/progguide/printable.html#reflective-access-to-the-join-point
-
The developer's notebook for the annotation-based AspectJ syntax (more relevant for you) mentions
JoinPoint.EnclosingStaticPart
:- https://www.eclipse.org/aspectj/doc/next/adk15notebook/printable.html#d0e3697
- https://www.eclipse.org/aspectj/doc/next/adk15notebook/printable.html#advice
-
The Javadoc mentions it, but does not explain anything (sorry!). So that is not a very good reference. But at least it exists.
-
If you are OK to buy a book, "AspectJ in Action, 2nd edition" by Ramnivas Laddad also mentions it in two or three places.