Check annotation value in Spring Pointcut Expression - spring

I'm trying to add an aspect that is only applied on 'non- readonly transactions', so not for #Transactional(readonly=true)
This below code is applied for all methods that have an #Transactional annotation above it.
#AfterReturning("#annotation(org.springframework.transaction.annotation.Transactional)")
public void afterTransaction() {...}
There is an alternative by getting the information from the JoinPoint but it is cumbersome and I'd rather not go down that path.
The documentation seems not very specific about this.

You can use an execution pointcut in order to limit annotation matching to joinpoints where the annotation has certain parameter values.
Simple annotation value matching
#AfterReturning("execution(#org.springframework.transaction.annotation.Transactional(readOnly=true) * *(..))")
public void process(JoinPoint joinPoint) {
System.out.println(joinPoint);
}
Annotation value matching + annotation parameter binding
#AfterReturning("execution(#org.springframework.transaction.annotation.Transactional(readOnly=true) * *(..)) && #annotation(transactional)")
public void process(JoinPoint joinPoint, Transactional transactional) {
System.out.println(joinPoint + " -> " + transactional);
}

Related

Annotation aspect for both Class Level and Method Level in Spring Boot (NullPointerException)

I am just facing interesting issue when I want to create an aspect for annotation like #Transactional.
Here is the controller and annotation:
#RestController
#SimpleAnnotation
public class HelloController{
private static final Logger LOGGER = LoggerFactory.getLogger(HelloController.class);
#GetMapping("/hello")
#SimpleAnnotation(isAllowed=true)
public String helloController(){
final String methodName = "helloController";
callAnotherMerhod();
LOGGER.info("HelloController for method : {}", methodName);
return "Hello";
}
private void callAnotherMethod(){
}
}
#Retention(RetentionPolicy.RUNTIME)
#Target({ElementType.METHOD, ElementType.TYPE})
public #interface SimpleAnnotation {
boolean isAllowed() default false;
}
Aspect for that annotation is here:
#Aspect
#Component
public class SimpleAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(SimpleAspect.class);
#Around(value = "#within(simpleAnnotation) || #annotation(simpleAnnotation)", argNames = "simpleAnnotation")
public Object simpleAnnotation(ProceedingJoinPoint proceedingJoinPoint, SimpleAnnotation simpleAnnotation) throws Throwable{
LOGGER.info("Simple annotation value: {}, ASPECT-LOG {}", simpleAnnotation.isAllowed(),proceedingJoinPoint.getSignature().getName());
return proceedingJoinPoint.proceed();
}
}
When I run the application and hit the http://localhost:8080/hello , everything is fine:
2020-11-09 11:36:48.230 INFO 8479 --- [nio-8080-exec-1] c.s.springaop.aspects.SimpleAspect : Simple annotation value: true, ASPECT-LOG helloController
2020-11-09 11:36:48.246 INFO 8479 --- [nio-8080-exec-1] c.s.s.controller.HelloController : HelloController for method : helloController
However if I removed annotation on the method:
#GetMapping("/hello")
public String helloController(){
final String methodName = "helloController";
callAnotherMethod();
LOGGER.info("HelloController for method : {}", methodName);
return "Hello";
}
Then simpleAnnotation parameter becomes null, and aspect method throws NullPointerException.
After that, I changed the order of the aspect like the below, it start to work:
#Around(value = " #annotation(simpleAnnotation) || #within(simpleAnnotation)", argNames = "simpleAnnotation")
However, in this situation if I remove annotation on class level and just put only the method level, then i am facing the same NPE.
I think, in some way aspect's conditions overwrites the values.
I tried to separate the class level annotation advice and method level advice, but in that case if i have the annotation on both the class and method level, both advices work (which I do not want)
I tried to update like this:
#Around(value = "#within(simpleAnnotation) || #annotation(simpleAnnotation) || #within(simpleAnnotation)", argNames = "simpleAnnotation")
This seems to be working, but is it a good solution?
EDIT: This solution is not working also. If I have annotation both on the class and method level and let's say class level annotation value is false, and method's level is true, then annotation value will be false.
To workaround the NPE , you may refactor the pointcut designators (#within and #annotation) to two different advice methods within the same aspect.
The logic to process based on the isAllowed value can be held in a common method and called from both the advice methods.
To illustrate :
#Aspect
#Component
public class SimpleAspect {
#Around(value = "#annotation(simpleAnnotation) && !#within(my.package.SimpleAnnotation)", argNames = "simpleAnnotation")
public Object simpleAnnotationOnMethod(ProceedingJoinPoint proceedingJoinPoint, SimpleAnnotation simpleAnnotation)
throws Throwable {
System.out.println("Simple annotation value:" + simpleAnnotation.isAllowed() + " , ASPECT-LOG :"
+ proceedingJoinPoint.getSignature().getName());
process(simpleAnnotation);
return proceedingJoinPoint.proceed();
}
#Around(value = "#within(simpleAnnotation)", argNames = "simpleAnnotation")
public Object simpleAnnotationOnType(ProceedingJoinPoint proceedingJoinPoint, SimpleAnnotation simpleAnnotation)
throws Throwable {
System.out.println("Simple annotation value:" + simpleAnnotation.isAllowed() + " , ASPECT-LOG :"
+ proceedingJoinPoint.getSignature().getName());
process(simpleAnnotation);
return proceedingJoinPoint.proceed();
}
private void process(SimpleAnnotation simpleAnnotation) {
// advice logic
}
}
Update : Modified the code as commented by #kriegaex
Your parameter binding is ambiguous due to the || because it is a (non-exclusive) OR rather than an XOR, which means that both conditions could be true at the same time. Imagine both the class and the method have an annotation. Which one should be bound?
See also this answer.
I.e. that, just like R.G said, you want to use two separate pointcuts and advices for method- and class-level annotations. You can still factor out duplicate code into a helper method inside the aspect and call it from both advice methods.

How to match two patterns in one pointcut expression

I want to define my pointcut to match two kinds of methods:
all methods from class org.mypackage.Foo with names starting with "update"
all methods from the same class with names starting with "delete"
I tried the following:
#Before("execution (* org.mypackage.Foo.update*(..) ) && execution(* org.mypackage.Foo.delete*(..) )")
public void verify()
{
//verify if user has permission to modify...
}
It doesn't work. When I call either a Foo.update*() method or a Foo.delete*() method, verify() is not invoked.
How should I change it?
There are 2 option to match patterns in pointcut expression. The pointcut expression may be either a simple reference to a named pointcut, or a pointcut expression declared in place.
By creating individual method for respective point cut. And using that method name as reference in #Before advice.
#Pointcut("execution(* org.mypackage.Foo.update*(..))")
private void fooUpdate() {}
#Pointcut("execution(* org.mypackage.Foo.delete*(..))")
private void fooDelete() {}
#Before("fooUpdate() || fooDelete()")
public void verify() {
// verify if user has permission to modify...
}
Directly declaring pointcut expression in place.
#Before("execution(* org.mypackage.Foo.update*(..)) || execution(* org.mypackage.Foo.delete*(..))")
public void verify() {
// verify if user has permission to modify...
}
Change && to || in your expression.

Get Class level annotation value in Spring AOP

I have an annotation which is a class level annotation
#Dummy(value = 123)
How I do create an aspect which gets invoked before any method execution of this annotated class. I would like to just print the value of annotation in the aspect's advice.
Following aspect would achieve the same
#Component
#Aspect
public class DummyAspect {
#Before(value = "#target(dummy) && within(com.your.package..*)")
public void before(JoinPoint jp, Dummy dummy) {
System.out.println(dummy.value());
}
}
within() - is a scoping designator to narrow the scope of classes to be advised. Without this designator the run could have undesired outcome as it could target framework classes as well.
Do go through this answer from #kriegaex to understand the designator in detail.
References : https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#aop-pointcuts-designators

Dependency-inject "dynamically specified" beans based on annotation arguments

I have a use case where it would be extraordinarily nice to dynamically instantiate beans (using some kind of factory approach) based on annotation-arguments at the injection point. Specifically, I need to be able to specify a type-argument to the bean-creating factory.
A pretty relevant example would be a JSON deserializer that needs the type which it needs to deserialize to.
I envision either:
#Inject
#DeserializeQualifier(Car.class)
private Deserializer<Car> _carDeserializer;
#Inject
#DeserializeQualifier(Bus.class)
private Deserializer<Bus> _busDeserializer;
.. or simply, if it was possible to sniff the type from the generic type argument:
#Inject
private Deserializer<Car> _carDeserializer;
#Inject
private Deserializer<Bus> _busDeserializer;
The big point here is that I would not know beforehand which types was needed in the project, as this would be a generic tool that many projects would include. So you would annotate your #Configuration class with #EnableDeserializer and could then inject any type deserializer (The factory that makes these deserializers can handle any type, but to be able create one, it would need to know the desired type of the deserialized object - plain generics would not cut it, since Java ain't using reified generics).
So, I'd need to be able to inject into the spring context, or using any other Spring magic tricks, some kind of DeserializerFactory that takes the type argument.
Basically, I need to have Spring invoke the following method based based on either, as in the first example, the qualifier argument (or the entire DeserializeQualifier-instance for that matter), or as in the second example, the generic type argument:
DeserializerFactory {
<T> Deserializer<T> createDeserializer(Class<T> type) { ... }
}
You could create a BeanFactoryPostProcessor to set attributes annotated with a custom annotation. I've set up a small Spring Boot project to play around:
// Custom annotation
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
public #interface InjectSomeClassHere {
Class value();
}
// Demo bean
#Component
public class SomeBean {
#InjectSomeClassHere(String.class)
private Class someValue;
public Class getInjectedClass() {
return someValue;
}
}
// The BeanFactoryPostProcessor
#Component
public class SomeBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
#Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
Arrays
.stream(beanFactory.getBeanDefinitionNames())
.filter(beanName -> hasAnnotatedField(beanFactory, beanName))
.forEach(beanName -> {
Object bean = beanFactory.getBean(beanName);
Stream.of(bean.getClass().getDeclaredFields()).forEach(field -> setFieldValue(bean, field));
});
}
private boolean hasAnnotatedField(ConfigurableListableBeanFactory beanFactory, String beanName) {
try {
String className = beanFactory.getBeanDefinition(beanName).getBeanClassName();
if (className == null) {
return false;
}
return Arrays.stream(Class.forName(className).getDeclaredFields())
.anyMatch(field -> field.isAnnotationPresent(InjectSomeClassHere.class));
} catch (ClassNotFoundException e) {
// Error handling here
return false;
}
}
private void setFieldValue(Object filteredBean, Field field) {
try {
// Note: field.isAccessible() is deprecated
if (!field.isAccessible()) {
field.setAccessible(true);
}
// Retrieve the value from the annotation and set the field
// In your case, you could call `createDeserializer(fieldValue);` and set the field using the return value.
// Note that you should change the type of `SomeBean#someValue` accordingly.
Class fieldValue = field.getAnnotation(InjectSomeClassHere.class).value();
field.set(filteredBean, fieldValue);
} catch (IllegalAccessException e) {
// Error handling here
e.printStackTrace();
}
}
}
// A small test to verify the outcome of the BeanFactoryPostProcessor
#RunWith(SpringRunner.class)
#SpringBootTest
public class SomeBeanTests {
#Autowired
private SomeBean someBean;
#Test
public void getInjectedClass_shouldHaveStringClassInjected() {
Assert.assertEquals(String.class, someBean.getInjectedClass());
}
}
Please note that this is a very naive implementation and requires further fine tuning. For instance, it scans all attributes in all spring components for the presence of an annotation.
Good luck with your project!

Spring AOP :- Passing method parameters dynamically to advice

My ServiceImpl class contains multiple methods.I have one LoggingAspect and i want to dynamically pass the method parameters to an advice based on the method called in the ServiceImpl. how do i acheive this?In the below code my method has 4 parameters,but one of my other method has 5 parametres and the third has 3.So how do i pass parameters dynamically based on number of method parameters?
#Aspect
public class LoggingAspect {
#Before("allGenericAppServiceImplMethods(userid,galleryId,sid,imageName)")
public void LoggingAdvice(JoinPoint joinPoint,String userid,String galleryId,String sid,String imageName){
System.out.println("String values are "+userid+" "+sid+" "+galleryId+" "+imageName);
}
#Pointcut("execution(public * com.nrollup.service.impl.GenericAppServiceImpl.*(..)) && args(userid,galleryId,sid,imageName)")
public void allGenericAppServiceImplMethods(String userid,String galleryId,String sid,String imageName)
{
}
}
You can use the around advice for this. A code snippet for your reference:
public void myAdvice(final ProceedingJoinPoint joinPoint) throws Throwable
{
Object[] arguments = joinPoint.getArgs();
}
Once you have the arguments array, you can operate upon it as required.
You can use joinPoint.proceed() to continue with the function call.

Resources