Reputation: 2769
I have a situation where I want to look for interfaces annotated with a given annotation, then check if a matching implementation is available. If not, I want to make a bean available that is actually a JDK proxy of the interface, essentially exactly what:
@ConditionalOnMissingBean
would do, except without writing a factory method for each of those.
I have code that is working "sometimes" - it seems extremely sensitive to the classloader structure, specifically, wether classes are loaded from a springboot fat jar (works) or wether some part is loaded from a separate classpath entry (doesnt work, mostly).
here is what I am doing:
@Service
@Order(value = Ordered.LOWEST_PRECEDENCE)
public class RemotingImportService implements BeanFactoryPostProcessor {
private static Log log = LogFactory.getLog(RemotingExportService.class);
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// scan classpath with classgraph, then:
for (ClassInfo classInfo : scanResult.getClassesWithAnnotation(MyAnnotation.class.getCanonicalName())) {
Class c = Class.forName(classInfo.getName());
if(beanFactory.getBeanNamesForType(c).length > 0) {
implFound = true;
log.info(c.getName()+" already has an implementation ... skipping");
continue;
}
// create proxy, then:
GenericBeanDefinition bdService = new GenericBeanDefinition();
bdService.setBeanClassName(classInfo.getName());
bdService.setInstanceSupplier(new ProxySupplier(proxy));
bdService.setLazyInit(true);
((DefaultListableBeanFactory) beanFactory).registerBeanDefinition(classInfo.getName(), bdService);
log.info(c.getName()+" has NO implementation ... proxy registerd");
}
in some cases, it seems that the beanfactory isn't finished, and
beanFactory.getBeanNamesForType()
returns an empty list, although it does find beans for that type later. i am aware that messing with this is probably not ideal - but it would be nice to find a solution that plays nice with spring boot.
any suggestions on how to solve this? a way to mark a bean definition as "ConditionalOnMissingBean" would also be great.
Upvotes: 3
Views: 2521
Reputation: 333
You should use BeanPostProcessor
instead of BeanFactoryPostProcessor
BeanPostProcessor
is operating by assembled beans, while BeanFactoryPostProcessor
uses raw bean definitions.
Read more here
public class ConditionalOnMissingProcessor implements BeanPostProcessor, Ordered, ApplicationContextAware
{
private static final Logger LOG = Logger.getLogger(ConditionalOnMissingProcessor .class);
private ApplicationContext applicationContext;
// Ordering to last in chain.
@Override
public int getOrder()
{
return Ordered.LOWEST_PRECEDENCE;
}
@Override
public void setApplicationContext(final ApplicationContext applicationContext) throws BeansException
{
this.applicationContext = applicationContext;
}
/**
* Process each bean and inject proxy objects in fields marked with: {@ConditionalOnMissingBean}
*/
@Override
public Object postProcessBeforeInitialization(final Object bean, final String beanName) throws BeansException
{
LOG.debug("Processing bean: " + beanName);
final Class clazz = bean.getClass();
ReflectionUtils.doWithFields(clazz, new ReflectionUtils.FieldCallback()
{
@Override
public void doWith(final Field field)
{
try
{
if(field.isAnnotationPresent(ConditionalOnMissingBean.class))
{
ReflectionUtils.makeAccessible(field);
final String beanFieldName = field.getName();
...
// Find proper implementation in application context
// Or create a proxy.
// Inject proxy or impl into a bean
field.set(bean, <value>));
}
}
catch(IllegalAccessException e)
{
LOG.error("Cannot set " + field.getName() + " in " + beanName, e);
}
}
});
return bean;
}
UPD:
First of all, I have to say about disadvantages of your impl (IMHO):
- you are trying to scan classpath to find all interfaces with @RemoteEndpoint
annotation, and, if current application context doesn't contain a bean that implemented this interface - create a proxy bean.
But what if I say, that not all interfaces marked with @RemoteEndpoint
should be taken into account? Developer should explicitly mark those interfaces, and then you can create all needed beans (for example, developer makes a common-services library).
- probably you are specifying redudnant information, when marking impl class with @RemotingEndpoint(value=RandomService.class)
annotation. You are already mentioned that when you implemented an interface.
There are multiple ways for implementing your idea
Using custom annotation on bean fields instead of @Autowired
.
pros:
BeanPostProcessor
.cons:
Using regular @Autowired
and @Value
annotations for injecting remote service proxy
In this case you should use BeanFactoryPostProcessor
(as you already tried). You'll have to iterate over all bean definitions, collect a map of field names and interfaces of remote service proxies you'll have to register (dependencies meta info). And next step is creating and registering beans using dependencies meta info (creating new ones only if there is no implementation bean in context). Spring will autowire those fields later. But, those dependencies should be singleton beans.
pros:
cons:
Still using regular @Autowired
and @Value
annotations and BeanFactoryPostProcessor
, but instead of registering new beans, you should register a FactoryBean
for each interface @RemoteEndpoint
.
pros:
Upvotes: 1