Is there a way to get Spring AOP to recognize the value of an argument that has been annotated? (There is no guarantee in the order of the arguments passed into the aspect, so I'm hoping to use an annotation to mark the parameter that needs to be used to process the aspect)
Any alternative approaches would also be extremely helpful.
#Target({ElementType.METHOD})
#Retention(RetentionPolicy.RUNTIME)
public #interface Wrappable {
}
#Target({ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
public #interface Key {
}
#Wrappable
public void doSomething(Object a, #Key Object b) {
// something
}
#Aspect
#Component
public class MyAspect {
#After("#annotation(trigger)" /* what can be done to get the value of the parameter that has been annotated with #Key */)
public void trigger(JoinPoint joinPoint, Trigger trigger) { }
Here is an example of an aspect class which should process a method tagged with #Wrappable annotation. Once the wrapper method is called, you can iterate over method parameters to find out if any parameter is tagged with the #Key annotation. The keyParams list contains any parameter tagged with a #Key annotation.
#Aspect
#Component
public class WrappableAspect {
#After("#annotation(annotation) || #within(annotation)")
public void wrapper(
final JoinPoint pointcut,
final Wrappable annotation) {
Wrappable anno = annotation;
List<Parameter> keyParams = new ArrayList<>();
if (annotation == null) {
if (pointcut.getSignature() instanceof MethodSignature) {
MethodSignature signature =
(MethodSignature) pointcut.getSignature();
Method method = signature.getMethod();
anno = method.getAnnotation(Wrappable.class);
Parameter[] params = method.getParameters();
for (Parameter param : params) {
try {
Annotation keyAnno = param.getAnnotation(Key.class);
keyParams.add(param);
} catch (Exception e) {
//do nothing
}
}
}
}
}
}
We cannot get the parameter annotation value as an argument to AOP like we are doing it for the method annotation because the annotation is not an actual parameter and in there you can only reference actual arguments.
args(#Key b)
This annotation will give you the value of Object(b) not the value of #Key annotation.
We can do in this way to get the value of the parameter annotation:
MethodSignature methodSig = (MethodSignature) joinpoint.getSignature();
Annotation[][] annotations = methodSig.getMethod().getParameterAnnotations();
if (annotations != null) {
for (Annotation[] annotArr : annotations) {
for (Annotation annot : annotArr) {
if (annot instanceof KeyAnnotation) {
System.out.println(((KeyAnnotation) annot).value());
}
}
}
}
Related
Created Custom annotation and add annotation at method level and pass value to Spring-Aspect.
springboot: application.properties spring.event.type=TEST
Output: PreHook Value|${spring.event.type}
I am expecting : TEST
Can someone please help how to populate value from properties file and inject to annotation.
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface PreHook {
String eventType();
}
#Aspect
#Component
public class ValidationAOP {
#Before("#annotation(com.example.demo.annotation.PreHook)")
public void doAccessCheck(JoinPoint call) {
System.out.println("ValidationAOP.doAccessCheck");
MethodSignature signature = (MethodSignature) call.getSignature();
Method method = signature.getMethod();
PreHook preHook = method.getAnnotation(PreHook.class);
System.out.println("PreHook Value|" + preHook.eventType());
}
}`
#RestController
public class AddController {
#GetMapping("/")
#PreHook(eventType = "${spring.event.type}")
public String test() {
System.out.println("Testcontroller");
return "Welcome Home";
}
}
You have to add SPEL processing to you annotation to evaluate that expression. You should not expect Spring to handle everything for you magicaly out of the box.
public void doAccessCheck(JoinPoint call) {
///(....)
PreHook preHook = method.getAnnotation(PreHook.class);
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(preHook.eventType());
String parsedType= (String) exp.getValue();
System.out.println("PreHook Value|" + parsedType);
}
Please refer below link for details. you are just few steps away.
Use property file in Spring Test
I'm looking to create a custom interface to inject properties like so...
interface Property<T> { T get(); }
I would like to then set the return value of the get() call using a custom annotation like...
#interface Key { String name(); String fallback() default ""; }
Then uses this throughout my application like...
#key(name = "my.string.property", fallback = "some default value")
Property<String> myStringProperty;
#key(name = "my.number.property", fallback = "1")
Property<Integer> myNumberProperty;
The reason we want to do this rather than using the #Value annotation is to hook these objects into our pre-existing system events with a new PropertyChanged event which can update the return value of the get() method (we will also persist these updates as we're running a distributed system which can create new nodes at anytime) and will also expose these properties in our UIs system admin page.
I've managed to get this working for fields annotated with my custom annotation using ReflectionUtils#doWithFields from my own implementation of BeanPostProcessor#postProcessBeforeInitialization. This is more of a hack as spring does all it's injection and then we're updating the field via reflection so this doesn't work when you annotate the constructor param. I used this guide for that, https://www.baeldung.com/spring-annotation-bean-pre-processor.
so my question is, is there a way to implement a factory object for spring where i can write code to read the annotation and inject an implementation based on that so i don't need to use reflection and it will work no matter where i inject as it'll be a part of the springs normal injection life cycle?
So i found a way to do this implementing the BeanDefinitionRegistryPostProcessor using the org.reflections lib to find all the Key annotations on my Property object.
I was then able create a custom bean definitions for each key which i can then register using the Key as a qualifier to allow spring to inject all my Property objects.
so first thing was adding the Qualifer annotation to my Key annotation.
#Qualifier
#interface Key {
String name();
String fallback() default "";
}
next was to create an implementation of the BeanDefinitionRegistryPostProcessor interface, this registers a bean definition with the concrete implementation of the Property interface to inject at runtime, the constructor parameters and the qualifier from the key annotation found by using the reflections to scan packages
(this was the key to replacing the use of reflections from setting the objects in my bean to just using it to dynamically lookup the key/property and making it available for injection)
#Component
public class PropertyBeanPostProcessor implements BeanDefinitionRegistryPostProcessor {
#Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
#Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
Reflections reflections = new Reflections(ClasspathHelper.forPackage("com.package.to.scan"),
new FieldAnnotationsScanner(), new MethodParameterScanner());
registerBeansForConstructors(registry, reflections.getConstructorsWithAnyParamAnnotated(Key.class));
registerBeansForMethods(registry, reflections.getMethodsWithAnyParamAnnotated(Key.class));
registerBeansForFields(registry, reflections.getFieldsAnnotatedWith(Key.class));
}
private void registerBeansForFields(BeanDefinitionRegistry registry, Set<Field> fields) {
for (Field field : fields) {
Class<?> parameterType = field.getType();
Annotation[] annotations = field.getDeclaredAnnotations();
Type genericType = field.getGenericType();
registerBeanIfPropertyType(registry, parameterType, genericType, annotations);
}
}
private void registerBeansForMethods(BeanDefinitionRegistry registry, Set<Method> methods) {
for (Method method : methods) {
Class<?>[] parameterTypes = method.getParameterTypes();
Annotation[][] annotations = method.getParameterAnnotations();
Type[] genericTypes = method.getGenericParameterTypes();
registerBeansForParameters(registry, parameterTypes, annotations, genericTypes);
}
}
private void registerBeansForConstructors(BeanDefinitionRegistry registry, Set<Constructor> constructors) {
for (Constructor constructor : constructors) {
Class<?>[] parameterTypes = constructor.getParameterTypes();
Annotation[][] annotations = constructor.getParameterAnnotations();
Type[] genericTypes = constructor.getGenericParameterTypes();
registerBeansForParameters(registry, parameterTypes, annotations, genericTypes);
}
}
private void registerBeansForParameters(BeanDefinitionRegistry registry, Class<?>[] parameterTypes, Annotation[][] annotations, Type[] genericTypes) {
for (int i = 0; i < parameterTypes.length; i++) {
Class<?> parameterType = parameterTypes[i];
Annotation[] parameterAnnotations = annotations[i];
Type genericType = genericTypes[i];
registerBeanIfPropertyType(registry, parameterType, genericType, parameterAnnotations);
}
}
private void registerBeanIfPropertyType(BeanDefinitionRegistry registry, Class<?> parameterType, Type genericType, Annotation[] parameterAnnotations) {
if (!Property.class.isAssignableFrom(parameterType)) {
return;
}
Arrays.stream(parameterAnnotations)
.filter(annotation -> Key.class.isAssignableFrom(annotation.annotationType()))
.map(Key.class::cast)
.findFirst()
.ifPresent(key -> register(registry, key, genericType));
}
private void register(BeanDefinitionRegistry registry, Key key, Type type) {
registry.registerBeanDefinition(key.name(), createDefinition(key, type));
log.info("registered property: {}", key);
}
public static BeanDefinition createDefinition(Key key, Type type) {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(PropertyImpl.class);
beanDefinition.setConstructorArgumentValues(createConstructorArgumentValues(key, type));
beanDefinition.addQualifier(createAutowireCandidateQualifier(key));
return beanDefinition;
}
private static AutowireCandidateQualifier createAutowireCandidateQualifier(Key key) {
AutowireCandidateQualifier autowireCandidateQualifier = new AutowireCandidateQualifier(Key.class);
autowireCandidateQualifier.setAttribute("name", key.name());
autowireCandidateQualifier.setAttribute("fallback", key.fallback());
return autowireCandidateQualifier;
}
private static ConstructorArgumentValues createConstructorArgumentValues(Key key, Type type) {
ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues();
constructorArgumentValues.addIndexedArgumentValue(1, key);
constructorArgumentValues.addIndexedArgumentValue(2, getPropertyType(type));
return constructorArgumentValues;
}
private static Class<?> getPropertyType(Type type) {
if (!(type instanceof ParameterizedType)) {
throw new RuntimeException("field " + type.getTypeName() + " is not parameterised");
}
ParameterizedType parameterizedType = (ParameterizedType) type;
Type[] actualGenericTypeArguments = parameterizedType.getActualTypeArguments();
if (actualGenericTypeArguments.length != 1) {
throw new RuntimeException("invalid number of generics: " + Arrays.stream(actualGenericTypeArguments).map(Type::getTypeName).collect(Collectors.toList()));
}
return TypeToken.of(actualGenericTypeArguments[0]).getRawType();
}
in the end i found this works for me but there may be a better way to create a factory that spring can trigger rather than using the reflections lib as this isn't the fastest
I am trying to use Spring AOP to capture some logging data on my controller classes.
I am using a custom annotation for this purpose but it seems to be failing.
#Around("execution(* path.to.package.endpoints.*Controller.*(..))")
private Object controllerMethodTimer(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
Annotation[][] annotations = signature.getMethod().getParameterAnnotations();
String[] parameterNames = signature.getParameterNames();
Object[] parameterValues = joinPoint.getArgs();
Map<String, String> parameters = new HashMap<>();
for (Integer i = 0; i < parameterNames.length; i++) {
for (Annotation annotation : annotations[i]) {
if (Loggable.class.isInstance(annotation)) {
Loggable loggable = (Loggable) annotation;
if (loggable.name() != ""){
parameters.put(loggable.name(), parameterValues[i].toString());
} else {
parameters.put(parameterNames[i], parameterValues[i].toString());
}
}
}
}
//do stuff
//when printing the parameters map, it is always blank
}
Loggable class:
public #interface Loggable {
String name() default "";
}
The Method In Question:
public ResponseEntity<Object> defaultLens(RestContext restContext,
#Loggable #RequestBody LensRequest parameters,
#RequestParam(value = "plugins", required = false) String commaDelimitedPluginNames) {
//some stuff
}
I've tried swapping the positions of Loggable/RequestBody in the above snippet.
What I am finding is that a log.info in the Aspect class's loops will show that RequestBody is being found and placed in the annotation array, but Loggable is not.
A previous iteration of the code which used:
for (Integer i = 0; i < parameterNames.length; i++) {
Loggable annotation = AnnotationUtils.findAnnotation(parameterValues[i].getClass(), Loggable.class);
if (annotation != null){
if (annotation.name() != ""){
parameters.put(annotation.name(), parameterValues[i].toString());
} else {
parameters.put(parameterNames[i], parameterValues[i].toString());
}
}
}
Found that Loggable was always null and the != null part of the loop was never being hit.
Resolution:
Per comment below, Custom Annotation needs #Retention(RetentionPolicy.RUNTIME)
New Loggable class looks like this:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
public #interface Loggable {
String name() default "";
}
This is working.
I have a question about getting custom annotation value which is value of another custom annotation. For example I have a #SqlInfo annotation interface which have two values which is also annotation interfaces.
SqlInfo.java
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.TYPE)
public #interface SqlInfo {
CodificationInfo codificationInfo();
DocumentInfo documentInfo();
}
#CodificationInfo and #DocumentInfo is also annotation interfaces. Each of it has his own different values.
CodificationInfo.java
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.TYPE)
public #interface CodificationInfo {
enum KEYS {
DOMAIN,
FILE,
TABLE,
CLASS
}
String domain() default "";
String fileName() default "";
String table() default "";
Class codificationClass();
}
While I am using only #CodificationInfo annotation for the class. I am getting values from it by using this method:
Annotation values getter method
public Object getClassAnnotationValue(Class c, String key) {
Annotation annotation = c.getAnnotation(CodificationInfo.class);
return getObjectByKey(annotation, key);
}
private Object getObjectByKey(Annotation annotation, String key) {
if (annotation instanceof CodificationInfo) {
if (key.equalsIgnoreCase(CodificationInfo.KEYS.TABLE.toString())) {
return ((CodificationInfo) annotation).table();
} else if (key.equalsIgnoreCase(CodificationInfo.KEYS.CLASS.toString())) {
return ((CodificationInfo) annotation).codificationClass();
} else if (key.equalsIgnoreCase(CodificationInfo.KEYS.DOMAIN.toString())) {
return ((CodificationInfo) annotation).domain();
} else if (key.equalsIgnoreCase(CodificationInfo.KEYS.FILE.toString())) {
return ((CodificationInfo) annotation).fileName();
}
}
return null;
}
I want to know how to get #CodificationInfo values while I am using #SqlInfo annotation for the class? It means - how to get values from sub-annotation?
P.S.: I know that I can use both annotations separately for the class. But I want to know the any way how to get values from sub-annotation. For example hibernate use it for #AuditOverrides annotation.
If you have a type declared like:
#SqlInfo(codificationInfo = #CodificationInfo(codificationClass = AClass.class)
public class MyType { }
you can reflectively get the inner annotation values with:
final SqlInfo sqlInfoAnnotation = (SqlInfo) c.getAnnotation(SqlInfo.class);
if (sqlInfoAnnotation == null) return;
final CodificationInfo codInfoAnnotation = sqlInfoAnnotation.codificationInfo();
final Class<?> codClass = codInfoAnnotation.codificationClass();
Note: you can avoid having to cast the annotation by not using raw types (prefer Class<?> over Class).
I am trying to get the annotation values. This is my scenario as follows:
This is the annotation I declared.
#Retention(RetentionPolicy.RUNTIME)
public #interface PluginMessage {
String name();
String version();
}
This is the class the uses the annotation for some values
#PluginMessage(name = "RandomName", version = "1")
public class Response{
private Date Time;
}
This is a generic interface which will be used in the next code snippet.
public interface ResponseListener<E> {
void onReceive(E response);
}
I Invoke this by calling the following code:
addListener(new ResponseListener<Response>() {
#Override
public void onReceive(Response response) {
System.out.println();
}
});
This is the implementation of the addListener method:
public <E> void addListener(ResponseListener<E> responseListener) {
Annotation[] annotations = responseListener.getClass().getAnnotations();
}
The annotations are always empty, any idea of what I am doing wrong? I am trying to get the value of them here.
You may get annotations here:
.addListener(new ResponseListener<Response>() {
public void onReceive(Response response) {
final Annotation[] annotations = response.getClass().getAnnotations();
for (Annotation annotation : annotations) {
System.out.println("annotation.toString() = " + annotation.toString());
}
}
});
Your .addListener implementation makes no sense. Instead of getting annotations from ResponseListener(which has no annotations) instance, you have to add listener to listeners pool. Then you have to call listener.onReceive(...) for each listener when you will receive the response. I believe something like that should be implemented there.