Spring AOP - Custom Annotation not coming through in JoinPoint - java

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.

Related

Annotation value not reading from springboot properties.file

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

Modify POJO class fields with custom setter or custom annotation (in Spring Boot)

Given a POJO in Spring Boot with several dozen fields of type String which is deserialized by Jackson. For demonstration purposes the following example only contains three fields:
#NoArgsConstructor
public class SomeRequest {
#JsonProperty("field_1")
private String field1;
#JsonProperty("field_2")
private String field2;
#JsonProperty("field_3")
private String field3;
}
I'm looking for a way to override the setter method but only for certain fields, i.e. I'd like to avoid repeating the below code for every affected field. This is doable for a handful number of fields but gets tedious for more than a handful.
public setField2(String field2) {
this.field2 = field2 + "?";
}
My idea was to place an annotation on the field like this:
#NoArgsConstructor
public class SomeRequest {
// ...
#JsonProperty("field_2")
#AppendQuestionMark
private String field2;
// ...
}
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.FIELD)
public #interface AppendQuestionMark {
}
But I'm lacking information on how to "implement" the AppendQuestionMark annotation which would override the field's setter method.
Or am I thinking way too complicated?
You can't change the settermethod's body if that's what you are asking. But you can create a method that will take an object (i.e. SomeRequest) as input and check which fields have your Annotation and change the values for those fields as you want.
For example, I created an annotation AppendStr.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.FIELD)
public #interface AppendStr {
public String str();;
}
Then I created another class 'AppendStrImpl` that will handle the implementation. I used the following code -
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class AppendStrImpl {
public void changeFields(Object object) throws Exception {
Class<?> clazz = object.getClass();
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
if (field.isAnnotationPresent(AppendStr.class)) {
// get the getter method name from the field name
String fieldName = field.getName();
String getterMethodName =
"get" +
fieldName.substring(0, 1).toUpperCase() +
fieldName.substring(1);
Method getterMethod = clazz.getMethod(getterMethodName);
String returnValue = (String) getterMethod.invoke(object);
String setterMethodName = getterMethodName.substring(0, 1).replace("g", "s")
+ getterMethodName.substring(1);
Method setterMethod = clazz.getMethod(setterMethodName, String.class);
setterMethod.invoke(object, returnValue + getAppendingString(field));
System.out.println((String) getterMethod.invoke(object));
}
}
}
private String getAppendingString(Field field) {
return field.getAnnotation(AppendStr.class)
.str();
}
}
And this is my POJO class -
public class POJO {
#AppendStr(str = "?")
private String filed1;
#AppendStr(str = "!")
private String filed2;
private String filed3;
#AppendStr(str = "+")
private String filed4;
// ... getters and setters
}
Then I called this method from the main method -
POJO pojo = new POJO("a", "b", "c", "d");
AppendStrImpl appendStrImpl = new AppendStrImpl();
try {
appendStrImpl.changeFields(pojo);
} catch (Exception e) {
e.printStackTrace();
}
Now you can make this call with hard coding or you can use #Aspect too if you want.
The github link is here.
Instead of creating a new annotation that appends a question mark to one generic string field in your pojo you can use the already present JsonDeserialize annotation over the string fields you are interested:
#Data
#NoArgsConstructor
public class SomeRequest {
#JsonProperty("field_1")
private String field1;
#JsonProperty("field_2")
//here the custom deserializer appends the question mark character
#JsonDeserialize(using = StringAppendQuestionMarkDeserializer.class)
private String field2;
}
In your spring boot project you can register the custom deserializer with the JsonComponent annotation like below:
#JsonComponent
public class StringAppendQuestionMarkDeserializer extends JsonDeserializer<String> {
#Override
public String deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
return node.asText() + "?";
}
}
A spring boot test example using the custom deserializer:
#JsonTest
class CorespringApplicationTests {
#Test
void testDeserialize() throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
SomeRequest request = mapper.readValue("{\"field_1\":\"value1\",\"field_2\":\"value2\"}", SomeRequest.class);
System.out.println(request); //<-- SomeRequest(field1=value1, field2=value2?)
}
}
Something like the following should do the trick:
#Aspect
#Component
public class AppendQuestionMarkAspect {
#Around("#annotation(AppendQuestionMark)")
public Object appendQuestionMark(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] arguments = joinPoint.getArgs();
return joinPoint.proceed(new Object[] {((String) arguments[0]) + "?"});
}
}
Of course, it would be advisable to check that only one argument exists and that it is, in fact, a String. Or you can also define the pointcut as to be applied only to methods starting with set. But the essence of the code is there.

#PropertyMapping with list parameter

I have the next annotation with #PropertyMapping:
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Documented
#ImportAutoConfiguration
#PropertyMapping("spring.test.testcontainers.datasource")
public #interface AutoConfigureDatasourceContainer {
#PropertyMapping("containers")
DatasourceContainer[] containers() default {};
}
And child annotation:
#Target({})
#Retention(RUNTIME)
public #interface DatasourceContainer {
#PropertyMapping("port")
int port() default 31335;
#PropertyMapping("username")
String username() default "";
#PropertyMapping("password")
String password() default "";
#PropertyMapping("database")
String database() default "";
#PropertyMapping("type")
DatasourceType type() default DatasourceType.MYSQL;
}
I expect that usages of this two annotations in next form:
#AutoConfigureDatasourceContainer(containers =
{
#DatasourceContainer(username = "username", password = "password", database = "users", type = DatasourceType.MYSQL)
})
will produce next property:
spring.test.testcontainers.datasource.containers[0].port=31335
spring.test.testcontainers.datasource.containers[0].username=username
.....
and so on. But this is not so.
I didn't find any examples in documentation about situations like this.
What is wrong with my code?

How to get values from custom sub-annotation?

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).

Spring AOP using method & parameter annotations

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());
}
}
}
}

Categories

Resources