kazuki43zoo / mybatis-spring-native

The experimental project that the MyBatis integration with Spring Native feature
Other
4 stars 1 forks source link

Fix early init of MapperFactoryBean #32

Closed snicoll closed 2 years ago

snicoll commented 2 years ago

This commit post-processes the bean factory so that the target type produced by the factory bean is exposed. While this is not a problem with a regular runtime, AOT uses instance supplier and without a proper bound for the generic type that it produces, the only solution is to create it early.

An alternative would be to change ClassPathMapperScanner in mybatis so that it exposes the type that the factory bean produces.

kazuki43zoo commented 2 years ago

@snicoll Thanks for your work!! I try this change in my local. The AOT mode on JVM work fine, but native mode does not work...

p.s. Following is result that I've applied the Spring Boot 2.6.3 and spring-native 0.11.2-SNAPSHOT latest version.

...
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'runner': Unsatisfied dependency expressed through method 'runner' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'cityMapper': Error setting property values; nested exception is org.springframework.beans.NotWritablePropertyException: Invalid property 'mapperInterface' of bean class [org.mybatis.spring.mapper.MapperFactoryBean]: Bean property 'mapperInterface' is not writable or has an invalid setter method. Does the parameter type of the setter match the return type of the getter?
        at org.springframework.aot.beans.factory.InjectedConstructionResolver.resolve(InjectedConstructionResolver.java:88) ~[na:na]
        at org.springframework.aot.beans.factory.InjectedElementResolver.resolve(InjectedElementResolver.java:19) ~[na:na]
        at org.springframework.aot.beans.factory.InjectedElementResolver.create(InjectedElementResolver.java:50) ~[na:na]
        at org.springframework.aot.beans.factory.BeanDefinitionRegistrar$InstanceSupplierContext.create(BeanDefinitionRegistrar.java:193) ~[na:na]
        at org.mybatis.spring.nativex.sample.simple.ContextBootstrapInitializer.lambda$registerMybatisSpringNativeSampleApplication_runner$1(ContextBootstrapInitializer.java:11) ~[na:na]
        at org.springframework.aot.beans.factory.ThrowableFunction.apply(ThrowableFunction.java:18) ~[na:na]
        at org.springframework.aot.beans.factory.BeanDefinitionRegistrar.lambda$instanceSupplier$0(BeanDefinitionRegistrar.java:97) ~[na:na]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1249) ~[na:na]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1191) ~[na:na]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) ~[na:na]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[na:na]
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[na:na]
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[na:na]
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[na:na]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[na:na]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:953) ~[na:na]
        at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918) ~[na:na]
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) ~[na:na]
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:732) ~[mybatis-spring-native-sample-simple:2.6.3]
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:414) ~[mybatis-spring-native-sample-simple:2.6.3]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:302) ~[mybatis-spring-native-sample-simple:2.6.3]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) ~[mybatis-spring-native-sample-simple:2.6.3]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) ~[mybatis-spring-native-sample-simple:2.6.3]
        at org.mybatis.spring.nativex.sample.simple.MybatisSpringNativeSampleApplication.main(MybatisSpringNativeSampleApplication.java:32) ~[mybatis-spring-native-sample-simple:0.0.1-SNAPSHOT]
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'cityMapper': Error setting property values; nested exception is org.springframework.beans.NotWritablePropertyException: Invalid property 'mapperInterface' of bean class [org.mybatis.spring.mapper.MapperFactoryBean]: Bean property 'mapperInterface' is not writable or has an invalid setter method. Does the parameter type of the setter match the return type of the getter?
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1744) ~[na:na]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1452) ~[na:na]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:619) ~[na:na]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[na:na]
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[na:na]
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[na:na]
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[na:na]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[na:na]
        at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) ~[na:na]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1389) ~[na:na]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1309) ~[na:na]
        at org.springframework.aot.beans.factory.InjectedConstructionResolver.lambda$resolve$0(InjectedConstructionResolver.java:83) ~[na:na]
        at org.springframework.aot.beans.factory.InjectedConstructionResolver.resolveDependency(InjectedConstructionResolver.java:97) ~[na:na]
        at org.springframework.aot.beans.factory.InjectedConstructionResolver.resolve(InjectedConstructionResolver.java:83) ~[na:na]
        ... 23 common frames omitted
Caused by: org.springframework.beans.NotWritablePropertyException: Invalid property 'mapperInterface' of bean class [org.mybatis.spring.mapper.MapperFactoryBean]: Bean property 'mapperInterface' is not writable or has an invalid setter method. Does the parameter type of the setter match the return type of the getter?
        at org.springframework.beans.BeanWrapperImpl.createNotWritablePropertyException(BeanWrapperImpl.java:243) ~[na:na]
        at org.springframework.beans.AbstractNestablePropertyAccessor.processLocalProperty(AbstractNestablePropertyAccessor.java:432) ~[na:na]
        at org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue(AbstractNestablePropertyAccessor.java:278) ~[na:na]
        at org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue(AbstractNestablePropertyAccessor.java:266) ~[na:na]
        at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:104) ~[na:na]
        at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:79) ~[na:na]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1740) ~[na:na]
        ... 36 common frames omitted
snicoll commented 2 years ago

Thanks for trying. It is so easy to forget what the AOT engine does for us automatically. Yeah, writing custom code means you need to know about manual hints. I'll revisit this shortly.

snicoll commented 2 years ago

OK so the problem now is that the type of the bean is CityMapper and no longer MapperFactoryBean. When the engine goes through the type to register hints automatically, it searches for a mapperInterface property on CityMapper (since that type is exposed). It doesn't find it and therefore give up.

One way to fix this would be to rework this PR so that the type exposed is ResolvableType.forClassWithGenerics(MapperFactoryBean.class, CityMapper.class). The problem with that approach as we've seen in Spring Data is that the factory bean could be a sub-class, or it could have different generic types. Us blindly rewriting the type assuming it only has one type is dangerous, which is why I didn't act on it.

Another solution would be to introduce the concept of FactoryBean as a first class citizen in BeanInstanceDescriptor. This would let us keep the simple type and expose the factory bean for further inspection.

What do you think?

kazuki43zoo commented 2 years ago

@snicoll Thanks for your comment! Which method would you recommend? I've tried ResolvableType.forClassWithGenerics(MapperFactoryBean.class, CityMapper.class), it work fine in my local.

before:

beanDefinition.setTargetType(mapperInterface);

after:

beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(beanDefinition.getBeanClass(), mapperInterface));

Another solution would be to introduce the concept of FactoryBean as a first class citizen in BeanInstanceDescriptor.

What should I do specifically for this?

snicoll commented 2 years ago

Which method would you recommend?

I don't know. Is sub-classing MapperFactoryBean happening frequently? the Spring integration has a way to specify the factorybean to use so it is possible to subclass it. If we can then the problem I've tried to describe in my previous comment could apply. I don't know enough about mybatis to know.

What should I do specifically for this?

Nothing, that would be done completely in Spring Native. I doubt I would have the time to work on that short term though.

kazuki43zoo commented 2 years ago

@snicoll Thanks for quick response!!

happening frequently?

Probably, I think it's rare. Therefore I want to apply to first solution(using ResolvableType.forClassWithGenerics). At first release, we will announce as limitation for using sub-class of MapperFactoryBean.

snicoll commented 2 years ago

OK let me update the PR in that direction