I have been trying to create a Spring project that uses MyBatis for the data access layer as a proof of concept for my team. I really want to avoid XML configuration if at all possible, so I'm attempting to wire everything together using annotated @Configuration classes.
Everything seems to be wired correctly, but my mapper beans are not being AutoWired into my service layer.
In my example I'm trying to wire together a UserDao, User entity, and a UserService.
public interface UserDao {
@Select("SELECT * FROM users WHERE id = #{userId}")
User get(@Param("userId") Integer userId);
}
@Component("User")
public class User implements Entity {
public Integer userId;
public String username;
/** ... getters/setters ommitted **/
}
@Service("UserService")
public class UserServiceImpl {
private UserDao userDao = null;
public User getUserDetails(Integer userId) {
return userDao.get(userId);
}
@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}
I'm wiring these together using two configuration classes.
@Configuration
@EnableLoadTimeWeaving(aspectjWeaving=AspectJWeaving.ENABLED)
@Import(DefaultDataAccessConfig.class) // I'm importing this because I thought ordering might be important, otherwise I was hoping to just let the component scanning pull in additional configuration files
@ComponentScan(basePackages="com.example.gwtspringpoc.server",
excludeFilters=@Filter(type=FilterType.ANNOTATION,
value=Controller.class))
public class ApplicationContextConfig {
/** No bean definitions needed here **/
}
@Configuration
@EnableTransactionManagement
public class DefaultDataAccessConfig implements TransactionManagementConfigurer {
@Bean
public DataSource dataSource() {
OracleDataSource ods = null;
try {
ods = new OracleDataSource();
} catch (SQLException e) {
throw new RuntimeException(e);
}
ods.setURL("jdbc:oracle:thin:@//localhost:9601/sid");
ods.setUser("user");
ods.setPassword("pass");
return ods;
}
@Override
@Bean(name="transactionManager")
public PlatformTransactionManager annotationDrivenTransactionManager() {
return new DataSourceTransactionManager(dataSource());
}
@Bean
public SqlSessionFactory sqlSessionFactory() {
SqlSessionFactoryBean sf = new SqlSessionFactoryBean();
sf.setDataSource(dataSource());
try {
return (SqlSessionFactory) sf.getObject();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Bean
public SqlSession sqlSessionTemplate() {
return new SqlSessionTemplate(sqlSessionFactory());
}
/*
* This did not work at all. It seems to be configured correctly, but the UserDao bean never
* got created at any stage, which was very disappointing as I was hoping not to have to
* create a bean definition for each DAO manually
*/
/*@Bean
public static MapperScannerConfigurer mapperScannerConfig() {
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("com.ca.spna.gwtspringpoc.server.model.dao");
msc.setAnnotationClass(Repository.class);
return msc;
}*/
/*
* Because the above code did not work, I decided to create the mapping manually.
* This is most likely my problem - something about this setup. My understanding
* is that the MapperFactoryBean once instantiated by Spring, will create a proxy
* object of type UserDao with the name "userDao" that can be injected elsewhere.
*/
@Bean
public MapperFactoryBean<UserDao> userDao() {
MapperFactoryBean<UserDao> mfb = new MapperFactoryBean<UserDao>();
mfb.setMapperInterface(UserDao.class);
return mfb;
}
}
You can read the comments above the last two methods in the above code snippet to gain more insight into how I'm creating the UserDao bean.
Once I got all the configuration setup, I created a unit test to try to test the UserService using the AnnotationConfigContextLoader, but was immediately hit with the following exception when trying to run the test:
Caused by: org.springframework.beans.factory.BeanCreationException: Could not autowire method: public void com.example.gwtspringpoc.server.service.UserServiceImpl.setUserDao(com.example.gwtspringpoc.server.model.dao.UserDao); nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No matching bean of type [com.example.gwtspringpoc.server.model.dao.UserDao] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {}
After seeing that, I commented out the @Autowired in the UserService and went back to my unit test and injected the ApplicationContext so I could inspect it, and the bean named "userDao" is in fact a MapperProxy instance.
So, is my understanding of how the MapperFactoryBean works off track or is it just not very compatible with annotation driven configuration? Additionally, if anyone has any idea how to make the MapperScannerConfigurer work correctly, I would appreciate it greatly!
In addition to being able to reference any particular bean definition as seen above, one @Configuration class may reference the instance of any other @Configuration class using @Autowired . This works because the @Configuration classes themselves are instantiated and managed as individual Spring beans.
Spring simplifies the configuration for MyBatis. The only required elements are javax. sql. Datasource, org.
To use MyBatis with Spring you need at least two things defined in the Spring application context: an SqlSessionFactory and at least one mapper interface. Notice that the SqlSessionFactory requires a DataSource . This can be any DataSource and should be configured just like any other Spring database connection.
Mapper XML is an important file in MyBatis, which contains a set of statements to configure various SQL statements such as select, insert, update, and delete. These statements are known as Mapped Statements or Mapped SQL Statements. All the statements have unique id.
After some time I was able to figure things out, so I'll answer my own question in case others run into something similar as there wasn't a whole lot of information available out there and it took some searching.
The problem comes down to the fact that MapperScannerConfigurer is a BeanDefinitionRegistryPostProcessor. As it turns out, this is the same mechanism used to process the @Configuration files and register the @Bean annotated methods. Unfortunately, one BeanDefinitionRegistryPostProcessor cannot make use of another, according to this Spring Jira ticket: https://jira.springsource.org/browse/SPR-7868
The suggestion here was to create an XML configuration for the processor and then include an @ImportResource annotation in the Java based configuration to pull it in. Well, that suggestion isn't fully accurate. You can't simply create an XML file with the configuration and pull it into the Java based configuration if you are still planning to have your configuration bootstrapped via an AnnotationConfigContextLoader. Instead, you have to revert back to loading your configuration via XML first and then creating a bean for your configuration file(s) the "old-fashion" way. For me this, was pretty trivial.
New Application Context
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.1.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd">
<!--
Because MapperScannerConfigurer is a BeanDefinitionRegistryPostProcessor, it cannot be
configured via @Configuration files with a @Bean annotaiton, because those files are
themselves configured via a BeanDefinitionRegistryPostProcessor which cannot spawn
another one.
-->
<bean id="myBatisMapperScanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.example.gwtspringpoc.server.model.dao"/>
<property name="annotationClass" value="org.springframework.stereotype.Repository"/>
</bean>
<!--
Load the rest of our configuration via our base configuration class
-->
<bean class="com.example.gwtspringpoc.server.spring.config.ApplicationContextConfig" />
</beans>
I then bootstrap the context container the traditional way, by providing a ContextConfigLocation. This works for me because the ApplicationContextConfig that I reference in the above XML handles everything else - including component scanning which will pick up all of my other @Configuration files.
Once I did this, all of my problems went away. I was able to @Autowire the UserDao as I expected and all was wonderful.
Note:
When I tried manually defining UserDao by creating a MapperFactoryBean, like in my original question's code example, there was a UserDao bean created but it was of type MapperProxy and would not @Autowire. However, I could get it to load by name using @Repository("userDao"), for what that's worth. I believe that the MapperFactoryBean suffers from a similar problem as the MapperScannerConfigurer and is simply not compatible with @Configuration files, alas.
From mybatis.3.2.0 and mybatis-spring.1.2.0, instead of MapperFactoryBean, you can use MapperScan for this.
@Configuration
@MapperScan("org.mybatis.spring.sample.mapper")
public class AppConfig
{
@Bean
public DataSource dataSource()
{
return new EmbeddedDatabaseBuilder().addScript("schema.sql").build();
}
@Bean
public DataSourceTransactionManager transactionManager()
{
return new DataSourceTransactionManager(dataSource());
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception
{
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource());
return sessionFactory.getObject();
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With