Issue
I am making my own ORM and I'm at a point where I want to to eliminate the need to create repository classes. They currently look like this :
@Repository
public class CustomerDao extends AbstractDao<Customer, Long>
{
}
Without going too deep into the inner workings of my ORM, the AbstractDao class uses a bean which is a map containing the entity class as a key and the dao class as a value. Without bringing ByteBuddy in the mix, the declaration of that bean looks like this :
@Bean
public Map<Class<?>, AbstractDao<?, ?>> daoMap()
{
context.getBeansWithAnnotation(Repository.class).forEach((s, c) ->
{
if (c instanceof AbstractDao<?, ?>) map.put(ClassUtils.getSuperClassTypeArgument(c.getClass(), 0), (AbstractDao<?, ?>) c);
});
return map;
}
Again this works great but it's very annoying to have to create these empty classes. I am trying to use the Reflections and ByteBuddy libraries to generate these classes at runtime and dynamically inject them into the Spring context as repository beans. Here is my modified method :
@Bean
public Map<Class<?>, AbstractDao<?, ?>> daoMap() throws InstantiationException, IllegalAccessException, IllegalArgumentException,
InvocationTargetException, ConstructorMissingException, AnnotationMissingException, NoSuchMethodException, SecurityException
{
var map = new HashMap<Class<?>, AbstractDao<?, ?>>();
var byteBuddy = new ByteBuddy();
for (var entityClass : new Reflections("com.somepackage.entity").getSubTypesOf(Entity.class))
{
var daoClass = byteBuddy
.subclass(TypeDescription.Generic.Builder.parameterizedType(AbstractDao.class, entityClass,
ReflectionUtils.getIdField(entityClass).getType()).build())
.annotateType(AnnotationDescription.Builder.ofType(Repository.class).build())
.make();
var clazz = daoClass.load(getClass().getClassLoader()).getLoaded();
((GenericApplicationContext) context).registerBean(clazz, clazz.getConstructor().newInstance());
}
context.getBeansWithAnnotation(Repository.class).forEach((s, c) ->
{
if (c instanceof AbstractDao<?, ?>) map.put(ClassUtils.getSuperClassTypeArgument(c.getClass(), 0), (AbstractDao<?, ?>) c);
});
return map;
}
The loop that creates the classes and injects them as beans seems to work fine. It doesn't throw exceptions and affects the right classes. I am however getting an exception at the getBeansWithAnnotation line :
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'com.mdenis.carbon.carbon_orm.dao.AbstractDao$ByteBuddy$jzMtXq5b': Could not resolve matching constructor (hint: specify index/type/name arguments for simple parameters to avoid type ambiguities)
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:278) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1358) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1204) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:557) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:517) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:323) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:226) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:321) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeansWithAnnotation(DefaultListableBeanFactory.java:672) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.getBeansWithAnnotation(AbstractApplicationContext.java:1264) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at com.mdenis.carbon.carbon_orm.config.ORMConfig.daoMap(ORMConfig.java:55) ~[classes/:na]
at com.mdenis.carbon.carbon_orm.config.ORMConfig$$EnhancerBySpringCGLIB$$8fd9cb34.CGLIB$daoMap$1(<generated>) ~[classes/:na]
at com.mdenis.carbon.carbon_orm.config.ORMConfig$$EnhancerBySpringCGLIB$$8fd9cb34$$FastClassBySpringCGLIB$$a25eec90.invoke(<generated>) ~[classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:244) ~[spring-core-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:331) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
at com.mdenis.carbon.carbon_orm.config.ORMConfig$$EnhancerBySpringCGLIB$$8fd9cb34.daoMap(<generated>) ~[classes/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:564) ~[na:na]
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
... 85 common frames omitted
This seems to be related to Spring failing to autowire the constructor even though it technically doesn't have to. Any idea how to resolve this?
Solution
Your problem is not related to byte-buddy. The error also occurs if you try to register a preexisting class.
You need to register the new beans in an earlier phase of the context startup. One way of doing that is to use a BeanFactoryPostProcessor
. Split your map creation into two sections. The one with the class generation into the BeanFactoryPostProcessor
. The one with the Map
generation you can keep in the @Bean
factory method
e.g.:
@Configuration
public class RepositoryConfigurationBean implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
try {
// Replace X1.class with the one from bytebuddy
// The name should be unique
configurableListableBeanFactory.registerSingleton("abc", X1.class.getConstructor().newInstance());
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
e.printStackTrace();
}
}
Answered By - k5_