Search code examples
javaspringspring-data-jpabyte-buddy

How to register custom JPA interface using bytebuddy


I need to register a custom interface that extended from JpaRepository. I found a way using bytebuddy, how to create an interface that extended from JpaRepository, but I didn't find, how to register him as a Bean.

  AnnotationDescription name = AnnotationDescription.Builder.ofType(Column.class)
            .define("name", "type")
            .build();

    AnnotationDescription id = AnnotationDescription.Builder.ofType(Id.class)
            .build();

    AnnotationDescription entity = AnnotationDescription.Builder.ofType(Entity.class)
            .build();
    AnnotationDescription table = AnnotationDescription.Builder.ofType(Table.class)
            .define("name", "generated_view")
            .build();

    Class<?> type = new ByteBuddy()
            .subclass(Object.class)
            .name("GeneratedModelX")
            .annotateType(entity)
            .annotateType(table)
            .defineField("id", Integer.class, Visibility.PRIVATE)
            .annotateField(id)
            .defineField("name", String.class, Visibility.PRIVATE)
            .annotateField(name)
            .defineMethod("getId", Integer.class, Visibility.PUBLIC)
            .intercept(FieldAccessor.ofBeanProperty())
            .defineMethod("setId", void.class, Visibility.PUBLIC)
            .withParameter(Integer.class)
            .intercept(FieldAccessor.ofBeanProperty())
            .defineMethod("getName", String.class, Visibility.PUBLIC)
            .intercept(FieldAccessor.ofBeanProperty())
            .defineMethod("setName", void.class, Visibility.PUBLIC)
            .withParameter(String.class)
            .intercept(FieldAccessor.ofBeanProperty())
            .make()
            .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
            .getLoaded();

    AnnotationDescription repositoryAnnotation = AnnotationDescription.Builder.ofType(Repository.class)
            .build();
    
    TypeDescription.Generic genericType = TypeDescription.Generic.Builder
            .parameterizedType(JpaRepository.class, type, Long.class)
            .annotate(repositoryAnnotation)
            .build();

    Class<? extends Object> repository = new ByteBuddy()
            .makeInterface()
            .implement(genericType)
            .name("CustomRepository")
            .make()
            .load(Controller.class.getClassLoader())
            .getLoaded();

Solution

  • Once you have properly created your custom entity and corresponding custom JPA repository - use ByteBuddy to transform the service class autowiring the newly loaded entity.

    builder = builder.defineField("customRepository", repo, Visibility.PUBLIC)
                     .annotateField(AnnotationDescription.Builder.ofType(Autowired.class)
                            .build());
    

    Now, since this is an interface with us not providing any concrete implementation [ Spring will provide proper implementation ], we will need a BeanFactoryPostProcessor.

    @Slf4j
    @Configuration
    public class DynamicJpaBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
            BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
                    .rootBeanDefinition(JpaRepositoryFactoryBean.class).addConstructorArgValue("your.custom.repository.classname");
            ((DefaultListableBeanFactory) configurableListableBeanFactory).registerBeanDefinition("your.custom.repository.classname", beanDefinitionBuilder.getBeanDefinition());
    
            log.info("Registered the {} bean for {} successfully", JpaRepositoryFactoryBean.class.getSimpleName(),
                    "your.custom.repository.classname");
        }
    }
    

    Hopefully this helps you. Of-course you will need to transform your service class to add methods utilizing injected repository bean.