Reputation: 400
I have annotated a Spring bean with custom annotations, but seems that Spring removes my custom annotations after the bean is created.
AnnotatedBean bean = ctx.getBean(AnnotatedBean.class);
Foo.findAndDoStuffWithAnnotatedThings(bean);
The second step doesn't work, my custom annotations are lost. (Propably due proxy stuff)
My bean
@Rule(name = "RoutePickupRule")
@Transactional
@Component
public class AnnotatedBean{
@Autowired
private ICarpoolDoa carpoolDAO;
@Condition
public boolean condition(CustomLocation customLocation, String userId) {
//snip
}
@Action
public void action() {
//snip
}
Example of one of my custom annotations
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Condition {
}
Where things to wrong in findAndDoStuffWithAnnotatedThings
Bean gets passed to a class where my custom annotations are verified, but my verifier can't find any annotations. (Util uses isAnnotationPresent method). Again, when I would create my bean myself using 'new' there is no problem.
public class RuleAnnotationVerifier {
public void RuleAnnotationVerifier(final Object rule) {
checkRuleClass(rule);
checkConditionMethod(rule);
checkActionMethod(rule);
}
private void checkRuleClass(final Object rule) {
if (!Utils.isClassAnnotatedWith(rule.getClass(), Rule.class)) {
throw new IllegalArgumentException(String.format("Rule '%s' is not annotated with '%s'", rule.getClass().getName(), Rule.class.getName()));
}
if (rule.getClass().getAnnotation(Rule.class).name().isEmpty()) {
throw new IllegalArgumentException(String.format("Rule '%s' annotation is empty", rule.getClass().getName()));
}
}
...
Is there a way to keep the custom annotations on a bean? My program was working correctly before changing my Class to a Bean.
Why do I want to do this?
Methods inside my class are invoked via reflection, but in the methods I'd like to use an Autowired DOA which requires the class to be a bean. :)
What I tried but didn't work
AopProxyUtils.ultimateTargetClass(bean)
Found an answer here
https://stackoverflow.com/a/14248490/3187643
Seems that it is not possible without a workaround. Answer is 3 years old so maybe there is another way now?
Upvotes: 1
Views: 5329
Reputation: 178
I found this question while trying to do much the same thing, that is: annotate some methods in a bean class with my own annotations with the intent to later hunt down and execute them via reflection.
When the beans are proxied this introduces some complications, and these complications are also affected by whether the proxy is a CGLib proxy or a JDK Dynamic proxy. I have done some experiments using Spring 4.3.9 to observe the differences in behaviour. (I am unsure how much is intentional vs being a side-effect of the proxying implementation and thus liable to act differently in future versions. I also have not experimented with the effects of using AspectJ weaving).
The reason you couldn't see the annotations is indeed because the bean was proxied (which happens if you use @Transactional
on any of its methods, or if you use Spring's proxy based AOP features to enhance the bean).
The proxy will have duplicates of methods on the target class but it won't inherit or copy their annotations - so when you inspect a Method
in the proxy's Class
you won't see the annotations that were on the original bean's method.
Therefore, you instead need to inspect the Class
of the target bean (ie: the actual bean instance which the proxy object wraps and delegates calls to after doing its thing), and AopProxyUtils.ultimateTargetClass()
is supposed give you this. It will return the Class
of your original bean and you can go through its methods and see the annotations.
(You could also use AopUtils.getTargetClass()
, but it's possible the proxied object is itself another proxy. Using ultimateTargetClass()
is supposed to follow the proxy chain all the way down whereas getTargetClass()
only goes down one level).
You didn't detail in which way ultimateTargetClass()
"didn't work" for you, but the proxying has some implications for invoking the method once you have found it.
Firstly, since you were scanning the target's class for methods, what you have is a Method
that came from the target's class and not the proxy's class.
Whether this matters depends on whether the proxy is a CGLib proxy or a JDK proxy.
If it is a CGLib proxy, then the proxy object extends the target's class, and you can anyhow just use the Method
from the target class to call it on the proxy object. ie:
theAnnotatedMethod.invoke(theProxyObject, args...);
But if it is a JDK proxy that doesn't work. You will get an exception "object is not an instance of declaring class" because the JDK proxy doesn't extend the bean's class, it just implements all its interfaces. To get around this you then need to find the method's evil twin on the proxy and invoke it with that:
Method methodOnTheProxy = theProxyObject.getClass().getMethod(theAnnotatedMethod.getName(),
theAnnotatedMethod.getParameterTypes());
methodOnTheProxy.invoke(theProxyObject, args...);
This will work - but only if the method you are trying to invoke is declared by one of the interfaces the class implements, because the JDK proxies only proxy methods that were declared in an interface implemented by the bean class.
You could create interfaces for all the methods you annotate and declare the bean to implement them, but this begs the question of why are we trying to find and invoke them by reflection when we can just call them using the interface - and that gets back to why we are using annotations instead of an interface for these methods in the first place.
In my own case (the one that led me to researching this question), I was aiming to move away from having certain 'lifecycle' methods in certain types of my beans declared in an interface and instead mark them with an annotation - consider methods like onLoad, onHidden, onDisplayed, onRemove for example. I am starting to get a lot of such methods and the implementations are often just empty for many of them. So I wanted to move to a style along the lines of how Spring controllers declare their @RequestMapping methods or unit tests have @Test on test methods instead.
JDK proxies also interfere with TYPE annotations (the ones you apply at the class level). Normally, you can use the context method getBeansWithAnnotation()
to find any beans whose class is annotated with the specified annotation, but, if the bean is proxied with a JDK proxy it will not be found by this method, whereas, if it is proxied with CGLib it will still be found. (Interestingly in both cases calling isAnnotationPresent()
on the Class
of the proxy returns false). You will also see the same problem with getBeansOfType()
when the beans have a JDK proxy.
It suggests we might prefer Spring to use CGLib for these beans.
By default, Spring will use a JDK Proxy if the bean implements any interfaces that declare methods (even if you put @Transactional on a method that isn't actually in one of those interfaces!). If there are no implemented interfaces or none of the declared interfaces declare a method then it will use CGLib. See: https://docs.spring.io/spring/docs/4.3.9.RELEASE/spring-framework-reference/html/aop.html#aop-introduction-proxies
This behaviour can be overridden in a few places. You can use @EnableTransactionManagement(proxyTargetClass=false)
on a configuration class which will make it use CGLib for proxied beans. The ancient TransactionPropertyFactoryBean
used with the pre-namespace XML configuration lets you specify proxyTargetClass as a property. There is also <aop:config proxy-target-class="true">
in XML namespace configuration. See also: Use of proxies in Spring AOP
An example program to illustrate all of this:
package com.example;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import javax.sql.DataSource;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronizationManager;
/**
* Example of finding methods with custom annotations when the bean is proxied
* Dependencies: org.springframework/spring-core/4.3.9.RELEASE
* org.springframework/spring-context/4.3.9.RELEASE
* org.springframework/spring-tx/4.3.9.RELEASE
* org.springframework/spring-jdbc/4.3.9.RELEASE org.hsqldb/hsqldb/2.4.0 (jdbc,
* tx, and hsqldb are just there as a quick way of including Transactional as
* the proxy example)
*/
@MyAnnotatedBean
public class AnnotatedProxyExample {
public static void main(String[] args) {
try(AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
Config.class)) {
//Collection<?> beans = context.getBeansWithAnnotation(MyAnnotatedBean.class).values();
//Collection<?> beans = context.getBeansOfType(AnnotatedProxyExample.class).values();
Collection<?> beans = Arrays.asList(context.getBean("myBean"));
if(beans.isEmpty()) {
System.out.println("***No beans were found");
}
else {
for(Object myBean : beans) {
if(AopUtils.isAopProxy(myBean)) {
System.out.println("myBean is an AOP proxy");
}
else {
System.out.println("myBean is not an AOP proxy");
}
System.out.println("Using myBean instance of class "
+ myBean.getClass().getName() + " returned from Spring context");
printAndCallMyAnnotatedMethods(myBean, myBean.getClass());
Class<?> ultimateTargetClass = AopProxyUtils
.ultimateTargetClass(myBean);
if(ultimateTargetClass == myBean) {
System.out.println("(myBean is also the ultimateTarget of myBean)");
}
else {
System.out.println("\nUsing the instance of class "
+ ultimateTargetClass.getName()
+ " returned by AopProxyUtils.ultimateTargetClass(MyBean):");
printAndCallMyAnnotatedMethods(myBean, ultimateTargetClass);
}
System.out.println("---------------");
}
}
}
}
private static void printAndCallMyAnnotatedMethods(Object myBean,
Class<?> targetClass) {
boolean foundAny = false;
for(Method method : targetClass.getMethods()) {
if(method.isAnnotationPresent(MyAnnotation.class)) {
foundAny = true;
MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
System.out.println("Found MyAnnotation on " + method.getName()
+ "(), value=" + annotation.value());
invokeMethod(myBean, method);
System.out.println();
}
}
if(!foundAny) {
System.out.println("***Did not find any methods with MyAnnotation");
}
}
private static void invokeMethod(Object object, Method annotatedMethod) {
if(!AopUtils.isAopProxy(object)) {
System.out.println("object to invoke method on is not an AOP proxy");
}
if(AopUtils.isCglibProxy(object)) {
System.out.println("object to invoke method on is a CGLib proxy");
}
if(AopUtils.isJdkDynamicProxy(object)) {
System.out.println("object to invoke method on is a JDK proxy");
}
String methodName = annotatedMethod.getName();
Class<?> objectClass = object.getClass();
if(objectClass.isAnnotationPresent(MyAnnotatedBean.class)) {
System.out
.println("The object's class has the MyAnnotatedBean annotation");
}
else {
System.out.println(
"***The object's class does not have the MyAnnotatedBean annotation");
}
try {
//Call the method on the object, but using the Method from the target class
System.out.println("Invoking " + methodName
+ "() on object using annotated Method from the target's class");
annotatedMethod.invoke(object);
} catch(Exception e) {
System.out.println("*** Couldn't call " + methodName
+ "() on instance of " + objectClass + " because " + e.getMessage());
}
try {
//Find and call a method on object's actual class with the same signature as annotatedMethod
//nb: if object isn't a proxy this will be the same Method as the above
Method objectMethod = objectClass.getMethod(methodName,
annotatedMethod.getParameterTypes());
if(objectMethod.equals(annotatedMethod)) {
System.out.println("(The target and object methods are the same here)");
}
else {
System.out.println("Invoking " + methodName
+ "() on object using a matching Method from object's class");
objectMethod.invoke(object);
}
} catch(NoSuchMethodException notFound) {
System.out.println("***Couldn't find matching " + methodName
+ "() on the instance of " + objectClass.getName());
} catch(Exception e) {
System.out.println("*** Couldn't call " + methodName
+ "() on instance of " + objectClass + " because " + e.getMessage());
}
}
///////////////////////////////////////////////
public void firstMethod() {
System.out.println("CALLED! firstMethod(), tx="
+ TransactionSynchronizationManager.isActualTransactionActive());
}
@MyAnnotation("roti prata")
public void secondMethod() {
System.out.println("CALLED! secondMethod(), tx="
+ TransactionSynchronizationManager.isActualTransactionActive());
}
@Transactional
@MyAnnotation("economy bee hoon")
public void thirdMethod() {
System.out.println("CALLED! thirdMethod(), tx="
+ TransactionSynchronizationManager.isActualTransactionActive());
}
}
//////////////////////////////////////////////////
interface MyInterface0 {
}
interface MyInterface1 {
//annotation won't be found because they aren't inherited from interfaces
@MyAnnotation("curry laksa")
public void firstMethod();
}
interface MyInterface2 {
public void secondMethod();
}
interface MyInterface3 {
public void thirdMethod();
}
/**
* Annotation that indicates which methods we want to find and call.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface MyAnnotation {
public String value();
}
//////////////////////////////////////////////////
/**
* Annotation that marks the classes of the beans we want to retrieve from the
* context to search for methods having MyAnnotation
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface MyAnnotatedBean {
;
}
//////////////////////////////////////////////////
//@EnableTransactionManagement(proxyTargetClass=true)
@EnableTransactionManagement
@Configuration
class Config {
@Bean
public AnnotatedProxyExample myBean() {
return new AnnotatedProxyExample();
}
@Bean
public PlatformTransactionManager transactionManager() {
DataSource ds = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL).build();
return new DataSourceTransactionManager(ds);
}
}
Declaring a transactional thirdMethod()
and not implementing an interface it gives the following output:
myBean is an AOP proxy Using myBean instance of class com.example.AnnotatedProxyExample$$EnhancerBySpringCGLIB$$367d5296 returned from Spring context ***Did not find any methods with MyAnnotation Using the instance of class com.example.AnnotatedProxyExample returned by AopProxyUtils.ultimateTargetClass(MyBean): Found MyAnnotation on secondMethod(), value=roti prata object to invoke method on is a CGLib proxy ***The object's class does not have the MyAnnotatedBean annotation Invoking secondMethod() on object using annotated Method from the target's class CALLED! secondMethod(), tx=false Invoking secondMethod() on object using a matching Method from object's class CALLED! secondMethod(), tx=false Found MyAnnotation on thirdMethod(), value=economy bee hoon object to invoke method on is a CGLib proxy ***The object's class does not have the MyAnnotatedBean annotation Invoking thirdMethod() on object using annotated Method from the target's class CALLED! thirdMethod(), tx=true Invoking thirdMethod() on object using a matching Method from object's class CALLED! thirdMethod(), tx=true
If you go and remove the @Transactional
from thirdMethod()
and then run the example again, you will notice that it no longer creates a proxy so the bean's class is the real one and not a proxy:
myBean is not an AOP proxy Using myBean instance of class com.example.AnnotatedProxyExample returned from Spring context Found MyAnnotation on secondMethod(), value=roti prata object to invoke method on is not an AOP proxy The object's class has the MyAnnotatedBean annotation Invoking secondMethod() on object using annotated Method from the target's class CALLED! secondMethod(), tx=false (The target and object methods are the same here) ...
You can try experimenting with implementing the interfaces to see how it affects the behaviour. For example, if you make AnnotatedProxyExample
implement MyInterface2
then Spring will use a JDK proxy but won't be able to call thirdMethod()
, whereas if you instead/also implement MyInterface3
which declares that method, then you can.
... Found MyAnnotation on thirdMethod(), value=economy bee hoon object to invoke method on is a JDK proxy ***The object's class does not have the MyAnnotatedBean annotation Invoking thirdMethod() on object using annotated Method from the target's class *** Couldn't call thirdMethod() on instance of class com.example.$Proxy17 because object is not an instance of declaring class Invoking thirdMethod() on object using a matching Method from object's class CALLED! thirdMethod(), tx=true
Upvotes: 5