完整的项目代码见 https://codechina.csdn.net/dreaming_coder/mybatis-spring
1. 引言
以前使用 Spring 整合 Mybatis 时,应该都用过下面的方式将 Mapper 接口加入到 Spring 中:
<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>
可以看到,类型并不是 XXXMapper,而是 MapperFactoryBean,你知道为什么这么配吗?
我们新建一个 Maven 项目来看看:
【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) {
// 启动 Spring
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.register(IceApplication.class);
applicationContext.refresh();
// 获取 bean
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.test();
}
}
现在执行 main 方法可以正常执行吗?答案是不行!
容器中并没有名为 userService 的 bean, 因为 UserMapper 是接口,无法实例化,更别提创建 bean 了!那么它就无法注入到 userService 的属性中,自然,名为 userService 的 bean 并没有创建成功(注意对象和 bean 的区别)。
2. UserMapper 的依赖注入
显然,有两种方式可以选择:
- 写一个实现类注入
- 代理对象
当然是代理对象方便点,因为可以用动态代理节省代码量
那么, 这个代理对象是由谁来产生的, spring V.S. mybatis?答案是 mybatis,不然为啥离开 spring 它也能使用呢?
3. 创建代理对象 bean
创建 bean 有两种方式:
- 声明式,@Bean,@Component
- 编程式,BeanDefination
声明式的应该用的很多,这里看看编程式的。
例如我们有个类 User:
public class User {
}
然后,在 main 方法里这样写:
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));
}
可以运行成功吗?No!
因为光定义一个 bean 还不行,还要注册到容器中。
public static void main(String[] args) {
// 启动 Spring
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.register(IceApplication.class);
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
beanDefinition.setBeanClass(User.class);
// 注册到容器中,给 bean 设置 name 为 user
applicationContext.registerBeanDefinition("user", beanDefinition);
applicationContext.refresh();
System.out.println(applicationContext.getBean("user", User.class));
}
不过,还是不能用这种方法直接注册 UserMapper:
public static void main(String[] args) {
// 启动 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));
}
毕竟 UserMapper 是接口,没有构造函数,所以我们可以通过 FactoryBean 来创建代理对象。
先看看 FactoryBean 怎么用的:
【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) {
// 启动 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"));
}
}
因为我们只能指定一个名字,就是 FactoryBean 创建的 bean 的名字 user,FactoryBean 其本身也是一个 bean,名字默认是 &user
所以我们可以在 IceFactoryBean 的 getObject() 方法中返回 UserMapper 的代理对象。
【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;
}
}
此时再执行一开始的 main 方法:
public static void main(String[] args) {
// 启动 Spring
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.register(IceApplication.class);
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
beanDefinition.setBeanClass(IceFactoryBean.class);
applicationContext.registerBeanDefinition("userMapper",beanDefinition);
applicationContext.refresh();
// 获取 bean
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.test();
}
可以发现不报错了:
为什么是 null 呢?因为执行 test() 方法会调用 selectById() 方法,会通过 invoke() 方法执行,而 invoke() 方法返回的是 null,所以结果为 null。
此时还有一个问题,IceFactoryBean 不可能仅用于 UserMapper 接口,那太奢侈了,如果我还有 OrderMapper,MemberMapper 呢?
我们修改一下 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;
}
}
现在问题,是怎么用构造方法给属性赋值。
Spring 提供了传参的方法:
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class);
现在我们修改的 main 方法如下:
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) {
// 启动 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();
// 获取 bean
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.test();
}
}
输出结果如下:
这种方式在 mybatis 中有体现:
但是,每增加一个 XXXMapper 接口,都要在 main 方法增加一段代码,太麻烦了
4. ImportBeanDefinitionRegistrar 改写
Spring 提供了 ImportBeanDefinitionRegistrar 接口,方便我们注册 bean,我们利用它来改写之前在 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);
}
}
启动类改动如下:
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) {
// 启动 Spring
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.register(IceApplication.class);
applicationContext.refresh();
// 获取 bean
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.test();
}
}
执行结果:·
5. 扫描改写
前面的方法虽然启动的时候清爽了,但是还是有些繁琐,因为 ImportBeanDefinitionRegistrar 也应该是中间件的内容,不能耦合业务类,所以应该用扫描来注册。
【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
// 我们只关心接口,所以重写时要覆盖Spring的默认规则
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return beanDefinition.getMetadata().isInterface();
}
}
【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;
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);
// 这里是因为 Spring 默认会排除一些文件,我们希望所有的都被扫描
iceMapperScanner.addIncludeFilter(new TypeFilter() {
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
return true;
}
});
iceMapperScanner.scan(path);
}
}
此时我们再添加一个 XXXMapper 的情况下,输出结果为:
目前,还有几个问题:
- 扫描路径是写死的
- XXXMapper 的代理对象目前你是我们自己生成的,我们需要 mybatis 生成的代理对象
Mybatis 是怎么生成代理对象的呢?
sqlSession.getMapper(XXMapper.class);
下面我们改写使其产生 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;
}
}
然后需要配置 bean
@Bean
public SqlSessionFactory sqlSessionFactory() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
return sqlSessionFactory;
}
这样,我们再执行时,就会调用真的 SQL 查数据库了,将刚刚 test() 方法中的语句结果 print 可以看到:
下面解决扫描路径的问题,因为现在路径还是写死的。
我们定义一个注解:
【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();
}
然后在启动类基于该注解指定扫描路径:
@ComponentScan("com.ice")
@IceScan("com.ice.mapper")
public class IceApplication {
// ...
}
注意,@Import 的位置移动到注解上了,这是因为 Spring 启动会扫描配置类的注解,这样 IceScan 接口的值就被扫描到了,同时 Spring 还会继续看注解上是否有 @Import 注解,如果有,则会执行注册 bean 的方法,此时,IceScan 注解的值就传进去了。
此时,IceImportBeanDefinitionRegistrar 类应该改成下面这样:
【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);
// 这里是因为 Spring 默认会排除一些文件,我们希望所有的都被扫描
iceMapperScanner.addIncludeFilter(new TypeFilter() {
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
return true;
}
});
iceMapperScanner.scan(path);
}
}
此时仍然可以运行:
6. 替换所有 Spring 注解
作为中间件,不太适合用一些其他包的注解,那如何进行 set 注入呢?
首先,删除 IceFactoryBean 中 的 @Autowired
注解,然后,进行下面这样的修改:
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());
// 配置注入模型
beanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
return beanDefinitionHolders;
}
// ...
}