See for complete project code https://codechina.csdn.net/dreaming_coder/mybatis-spring
1. Introduction
In the past, when using Spring to integrate Mybatis, the Mapper interface should be added to Spring in the following way:
<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean"> <property name="mapperInterface" value="org.mybatis.spring.sample.mapper.UserMapper" /> <property name="sqlSessionFactory" ref="sqlSessionFactory" /> </bean>
You can see that the type is not XXXMapper, but MapperFactoryBean. Do you know why it matches so well?
Let's create a Maven project to see:
[pom.xml]
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.ice</groupId> <artifactId>demo</artifactId> <version>1.0</version> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.8</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.7</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.25</version> </dependency> </dependencies> </project>
[UserMapper.java]
package com.ice.mapper; import org.apache.ibatis.annotations.Select; public interface UserMapper { @Select("select 'user'") String selectById(); }
[UserService.java]
package com.ice.service; import com.ice.mapper.UserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class UserService { @Autowired private UserMapper userMapper; public void test() { System.out.println(userMapper.selectById()); } }
[IceApplication.java]
import com.ice.service.UserService; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @ComponentScan("com.ice") public class IceApplication { public static void main(String[] args) { // Start Spring AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); applicationContext.register(IceApplication.class); applicationContext.refresh(); // Get bean UserService userService = applicationContext.getBean("userService", UserService.class); userService.test(); } }
Can the main method be executed normally now? The answer is no!
There is no bean named userService in the container, because UserMapper is an interface and cannot be instantiated, let alone create a bean! Then it cannot be injected into the properties of userService. Naturally, the bean named userService was not created successfully (pay attention to the difference between object and bean).
2. Dependency injection of usermapper
Obviously, there are two options:
- Write an implementation class injection
- Proxy object
Of course, the proxy object is more convenient, because you can use dynamic proxy to save code
So, who generates this proxy object, spring V.S. mybatis? The answer is mybatis, otherwise why can it be used without spring?
3. Create proxy object bean
There are two ways to create bean s:
- Declarative, @ Bean, @ Component
- Beandefinition
Declarative should be used a lot. Here's a look at programmatic.
For example, we have a User class:
public class User { }
Then, write this in the main method:
public static void main(String[] args) { AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); applicationContext.register(IceApplication.class); AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition(); beanDefinition.setBeanClass(User.class); applicationContext.refresh(); System.out.println(applicationContext.getBean("user", User.class)); }
Can it run successfully? No!
Because just defining a bean is not enough. You have to register it in the container.
public static void main(String[] args) { // Start Spring AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); applicationContext.register(IceApplication.class); AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition(); beanDefinition.setBeanClass(User.class); // Register in the container and set the bean name to user applicationContext.registerBeanDefinition("user", beanDefinition); applicationContext.refresh(); System.out.println(applicationContext.getBean("user", User.class)); }
However, you cannot register UserMapper directly in this way:
public static void main(String[] args) { // Start Spring AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); applicationContext.register(IceApplication.class); AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition(); beanDefinition.setBeanClass(UserMapper.class); applicationContext.registerBeanDefinition("userMapper",beanDefinition); applicationContext.refresh(); System.out.println(applicationContext.getBean("userMapper", UserMapper.class)); }
After all, UserMapper is an interface without a constructor, so we can create a proxy object through FactoryBean.
Let's see how FactoryBean works first:
[IceFactoryBean.java]
package org.mybatis.spring; import com.ice.service.User; import org.springframework.beans.factory.FactoryBean; public class IceFactoryBean implements FactoryBean { @Override public Object getObject() throws Exception { User user = new User(); return user; } @Override public Class<?> getObjectType() { return User.class; } }
[IceApplication.java]
import org.mybatis.spring.IceFactoryBean; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.ComponentScan; @ComponentScan("com.ice") public class IceApplication { public static void main(String[] args) { // Start Spring AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); applicationContext.register(IceApplication.class); AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition(); beanDefinition.setBeanClass(IceFactoryBean.class); applicationContext.registerBeanDefinition("user",beanDefinition); applicationContext.refresh(); System.out.println(applicationContext.getBean("user")); System.out.println(applicationContext.getBean("&user")); } }
Because we can only specify one name, that is, the name user of the bean created by FactoryBean. FactoryBean itself is also a bean, and the default name is & user
So we can return the proxy object of UserMapper in the getObject() method of IceFactoryBean.
[IceFactoryBean.java]
package org.mybatis.spring; import com.ice.mapper.UserMapper; import org.springframework.beans.factory.FactoryBean; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class IceFactoryBean implements FactoryBean { @Override public Object getObject() throws Exception { Object o = Proxy.newProxyInstance(IceFactoryBean.class.getClassLoader(), new Class[]{UserMapper.class}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return null; } }); return o; } @Override public Class<?> getObjectType() { return UserMapper.class; } }
At this point, execute the main method at the beginning:
public static void main(String[] args) { // Start Spring AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); applicationContext.register(IceApplication.class); AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition(); beanDefinition.setBeanClass(IceFactoryBean.class); applicationContext.registerBeanDefinition("userMapper",beanDefinition); applicationContext.refresh(); // Get bean UserService userService = applicationContext.getBean("userService", UserService.class); userService.test(); }
It can be found that no error is reported:
Why null? Because executing the test() method will call the selectById() method, which will be executed through the invoke() method, and the invoke() method returns null, the result is null.
At this time, there is another problem. IceFactoryBean cannot only be used for UserMapper interface, which is too extravagant. What if I have OrderMapper and MemberMapper?
Let's modify IceFactoryBean
package org.mybatis.spring; import org.springframework.beans.factory.FactoryBean; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class IceFactoryBean implements FactoryBean { private Class mapperClass; public IceFactoryBean(Class mapperClass) { this.mapperClass = mapperClass; } @Override public Object getObject() throws Exception { Object o = Proxy.newProxyInstance(IceFactoryBean.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(method.getName() + "--------" + mapperClass); return null; } }); return o; } @Override public Class<?> getObjectType() { return mapperClass; } }
Now the question is how to assign a value to an attribute with a construction method.
Spring provides methods to pass parameters:
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class);
Now we modify the main method as follows:
import com.ice.mapper.MemberMapper; import com.ice.mapper.OrderMapper; import com.ice.mapper.UserMapper; import com.ice.service.UserService; import org.mybatis.spring.IceFactoryBean; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.ComponentScan; @ComponentScan("com.ice") public class IceApplication { public static void main(String[] args) { // Start Spring AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); applicationContext.register(IceApplication.class); AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition(); beanDefinition.setBeanClass(IceFactoryBean.class); beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class); applicationContext.registerBeanDefinition("userMapper",beanDefinition); AbstractBeanDefinition beanDefinition1 = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition(); beanDefinition1.setBeanClass(IceFactoryBean.class); beanDefinition1.getConstructorArgumentValues().addGenericArgumentValue(OrderMapper.class); applicationContext.registerBeanDefinition("orderMapper",beanDefinition1); AbstractBeanDefinition beanDefinition2 = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition(); beanDefinition2.setBeanClass(IceFactoryBean.class); beanDefinition2.getConstructorArgumentValues().addGenericArgumentValue(MemberMapper.class); applicationContext.registerBeanDefinition("memberMapper",beanDefinition2); applicationContext.refresh(); // Get bean UserService userService = applicationContext.getBean("userService", UserService.class); userService.test(); } }
The output results are as follows:
This method is reflected in mybatis:
However, every time you add an XXXMapper interface, you need to add a piece of code in the main method. It's too troublesome
4. ImportBeanDefinitionRegistrar rewriting
Spring provides the ImportBeanDefinitionRegistrar interface to facilitate us to register beans. We use it to rewrite the cumbersome code in main:
[IceImportBeanDefinitionRegistrar.java]
package org.mybatis.spring; import com.ice.mapper.MemberMapper; import com.ice.mapper.OrderMapper; import com.ice.mapper.UserMapper; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.type.AnnotationMetadata; public class IceImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) { AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition(); beanDefinition.setBeanClass(IceFactoryBean.class); beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class); registry.registerBeanDefinition("userMapper", beanDefinition); AbstractBeanDefinition beanDefinition1 = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition(); beanDefinition1.setBeanClass(IceFactoryBean.class); beanDefinition1.getConstructorArgumentValues().addGenericArgumentValue(OrderMapper.class); registry.registerBeanDefinition("orderMapper", beanDefinition1); AbstractBeanDefinition beanDefinition2 = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition(); beanDefinition2.setBeanClass(IceFactoryBean.class); beanDefinition2.getConstructorArgumentValues().addGenericArgumentValue(MemberMapper.class); registry.registerBeanDefinition("memberMapper", beanDefinition2); } }
The changes of startup class are as follows:
import com.ice.service.UserService; import org.mybatis.spring.IceImportBeanDefinitionRegistrar; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Import; @ComponentScan("com.ice") @Import(IceImportBeanDefinitionRegistrar.class) public class IceApplication { public static void main(String[] args) { // Start Spring AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); applicationContext.register(IceApplication.class); applicationContext.refresh(); // Get bean UserService userService = applicationContext.getBean("userService", UserService.class); userService.test(); } }
Execution result:·
5. Scan rewrite
Although the previous method is refreshing when starting, it is still a little cumbersome, because ImportBeanDefinitionRegistrar should also be the content of middleware and cannot couple business classes, so it should be registered by scanning.
[IceMapperScanner.java]
package org.mybatis.spring; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.context.annotation.ClassPathBeanDefinitionScanner; import java.util.Set; public class IceMapperScanner extends ClassPathBeanDefinitionScanner { public IceMapperScanner(BeanDefinitionRegistry registry) { super(registry); } @Override protected Set<BeanDefinitionHolder> doScan(String... basePackages) { Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages); for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) { BeanDefinition beanDefinition = beanDefinitionHolder.getBeanDefinition(); beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName()); beanDefinition.setBeanClassName(IceFactoryBean.class.getName()); } return beanDefinitionHolders; } @Override // We only care about interfaces, so we need to override Spring's default rules when rewriting protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { return beanDefinition.getMetadata().isInterface(); } }
[IceImportBeanDefinitionRegistrar.java] some changes need to be made:
package org.mybatis.spring; import com.ice.mapper.MemberMapper; import com.ice.mapper.OrderMapper; import com.ice.mapper.UserMapper; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.filter.TypeFilter; import java.io.IOException; public class IceImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) { String path = "com.ice.mapper"; IceMapperScanner iceMapperScanner = new IceMapperScanner(registry); // This is because Spring excludes some files by default. We want all files to be scanned iceMapperScanner.addIncludeFilter(new TypeFilter() { @Override public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException { return true; } }); iceMapperScanner.scan(path); } }
When we add another XXXMapper, the output result is:
At present, there are several problems:
- The scan path is dead
- The proxy object of XXXMapper is currently generated by ourselves. We need the proxy object generated by mybatis
How does Mybatis generate proxy objects?
sqlSession.getMapper(XXMapper.class);
Let's rewrite it to generate the proxy object generated by Mybatis:
[IceFactoryBean.java]
package org.mybatis.spring; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.annotation.Autowired; public class IceFactoryBean implements FactoryBean { private Class mapperClass; private SqlSession sqlSession; @Autowired public void setSqlSession(SqlSessionFactory sqlSessionFactory) { sqlSessionFactory.getConfiguration().addMapper(mapperClass); this.sqlSession = sqlSessionFactory.openSession(); } public IceFactoryBean(Class mapperClass) { this.mapperClass = mapperClass; } @Override public Object getObject() throws Exception { Object mapper = sqlSession.getMapper(mapperClass); return mapper; } @Override public Class<?> getObjectType() { return mapperClass; } }
Then you need to configure the bean
@Bean public SqlSessionFactory sqlSessionFactory() throws IOException { InputStream inputStream = Resources.getResourceAsStream("mybatis.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); return sqlSessionFactory; }
In this way, when we execute again, we will call real SQL to query the database. The statement result print in the test() method just now can be seen:
Next, solve the problem of scanning path, because the path is still dead.
We define an annotation:
[IceScan.java]
package org.mybatis.spring; import org.springframework.context.annotation.Import; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Import(IceImportBeanDefinitionRegistrar.class) public @interface IceScan { String value(); }
Then specify the scanning path based on the annotation in the startup class:
@ComponentScan("com.ice") @IceScan("com.ice.mapper") public class IceApplication { // ... }
Note that the @ Import position is moved to the annotation, because Spring startup will scan the annotation of the configuration class, so that the value of IceScan interface will be scanned. At the same time, Spring will continue to see whether there is @ Import annotation on the annotation. If so, it will execute the method of registering bean s. At this time, the value of IceScan annotation will be passed in.
At this point, the IceImportBeanDefinitionRegistrar class should be changed as follows:
[IceImportBeanDefinitionRegistrar.java]
package org.mybatis.spring; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.filter.TypeFilter; import java.io.IOException; import java.util.Map; public class IceImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) { Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(IceScan.class.getName()); String path = (String) annotationAttributes.get("value"); IceMapperScanner iceMapperScanner = new IceMapperScanner(registry); // This is because Spring excludes some files by default. We want all files to be scanned iceMapperScanner.addIncludeFilter(new TypeFilter() { @Override public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException { return true; } }); iceMapperScanner.scan(path); } }
You can still run:
6. Replace all Spring annotations
As a middleware, it is not suitable to use some annotations of other packages. How to inject set?
First, delete the @ Autowired annotation in IceFactoryBean, and then make the following modifications:
public class IceMapperScanner extends ClassPathBeanDefinitionScanner { // ... @Override protected Set<BeanDefinitionHolder> doScan(String... basePackages) { Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages); for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) { GenericBeanDefinition beanDefinition = (GenericBeanDefinition) beanDefinitionHolder.getBeanDefinition(); beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName()); beanDefinition.setBeanClassName(IceFactoryBean.class.getName()); // Configuration injection model beanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); } return beanDefinitionHolders; } // ... }