spring整合mybatis原理分析,分为两章,第一章从零开始写一个spring整合mybatis的代码,带大家分析spring整合mybatis的思路。
1 原生mybatis中执行SQL我们先从原生mybatis执行SQL的例子。代码目录如下。
│ pom.xml
└─src
├─main
│ ├─java
│ │ └─com
│ │ │ Main.java
│ │ │
│ │ ├─entity
│ │ │ User.java
│ │ │
│ │ └─mapper
│ │ baseDao.java
│ │ UserDao.java
│ │
│ └─resources
│ │ configuration.xml
│ │
│ └─mapper
│ UserMapper.xml
│
└─test
└─java
实体类
public class User {
private Integer id;
private String userName;
private int age;
private String address;
.........
}
DAO类
public interface baseDao{ T findById(Integer id); }
public interface UserDao extends baseDao{ }
mapper文件UserMapper.xml
select id, user_name userName, age, address from user where id=#{id}
mybatis配置
表结构
CREATE TABLE `mybatis`.`Untitled` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, `age` int(11) NULL DEFAULT NULL, `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
maven配置
org.mybatis mybatis 3.5.9 mysql mysql-connector-java 5.1.49 org.springframework spring-context 5.2.19.RELEASE org.mybatis mybatis-spring 2.0.7
最后是执行方法。
public class Main {
public static void main(String[] args) throws IOException {
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(Resources
.getResourceAsReader("configuration.xml"));
SqlSession sqlSession = sessionFactory.openSession();
UserDao userMapper = sqlSession.getMapper(UserDao.class);
User user = userMapper.findById(1);
System.out.println(user.toString());
}
}
User{id=1, userName='zzl', age=18, address='法守法'}
从上面一个简单的mybatis的例子,大家对spring整合mybatis是否有灵感,我们以UserDao 来分析一下:
- 如果是UserDao 能通过@Autowired注入实例,那么首先需要把UserDao 放入spring容器中,但是UserDao 是一个接口,所以放入spring容器的是一个代理对象,这里我们可以使用FactoryBean。代理的逻辑有哪些?其实从最后的执行方法可以看到,只要我们通过sqlSession.getMapper(UserDao.class)拿到mybatis中UserDao的代理对象那么我们就可以通过这个代理对象执行方法。通过上面可以知道UserDao 其实是被代理了两次,第一次是在mybatis中被代理,第二次放入spring容器的时候。
按上面分析的思路,我们新建一个FactoryBean类。
@Component
public class UserDaoFactoryBean implements FactoryBean {
@Override
public Object getObject() throws Exception {
Object proxyInstance = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{UserDao.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(Resources
.getResourceAsReader("configuration.xml"));
SqlSession sqlSession = sessionFactory.openSession();
Object result = method.invoke(userMapper, args);
return result;
}
});
return proxyInstance;
}
@Override
public Class> getObjectType() {
return UserDao.class;
}
}
新建一个UserService并入住UserDao
@Service
public class UserService {
@Autowired
UserDao userDao;
public User getUser(int id){
return userDao.findById(id);
}
}
新建spring配置类
@ComponentScan("com")
public class AppConfig {
}
public class Main {
public static void main(String[] args) throws IOException {
// String resource = "configuration.xml";
//
// SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(Resources
// .getResourceAsReader(resource));
// SqlSession sqlSession = sessionFactory.openSession();
// UserDao userMapper = sqlSession.getMapper(UserDao.class);
// User user = userMapper.findById(1);
// System.out.println(user.toString());
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean(UserService.class);
System.out.println(userService.getUser(1));
}
}
执行结果
User{id=1, userName='zzl', age=18, address='法守法'}
从上结果可以看到我们已经把userDao整合到spring中。但是获取SqlSessionFactory 是公共代码我们把它抽取出来。重构后的代码如下。
@ComponentScan("com")
public class AppConfig {
@Bean
SqlSessionFactory getSessionFactory() throws IOException {
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(Resources
.getResourceAsReader("configuration.xml"));
return sessionFactory;
}
}
@Component
public class UserDaoFactoryBean implements FactoryBean {
private SqlSession sqlSession;
public UserDaoFactoryBean(SqlSessionFactory sessionFactory) {
this.sqlSession = sessionFactory.openSession();
}
@Override
public Object getObject() throws Exception {
Object proxyInstance = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{UserDao.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
UserDao userMapper = sqlSession.getMapper(UserDao.class);
Object result = method.invoke(userMapper, args);
return result;
}
});
return proxyInstance;
}
@Override
public Class> getObjectType() {
return UserDao.class;
}
}
重构后大家对整合mybatis是不是感觉有点熟悉了。
通过spirng 的bean工厂后置处理器BeanFactoryPostProcessor注册beanUserDaoFactoryBean 中的接口类是写死的,我们不可能为每一个接口都建一个FactoryBean ,所以我们FactoryBean需要变的更通用。重构UserDaoFactoryBean 。
构造函数改为传入接口类,新增一个setSqlSessionFactory用于注入SqlSessionFactory
public class MybatisFactoryBean implements FactoryBean {
private SqlSession sqlSession;
private Class mapperInterface;
public MybatisFactoryBean(Class mapperInterface) {
this.mapperInterface = mapperInterface;
}
@Override
public Object getObject() throws Exception {
Object proxyInstance = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{mapperInterface}, (proxy, method, args) -> {
UserDao userMapper = sqlSession.getMapper(UserDao.class);
Object result = method.invoke(userMapper, args);
return result;
});
return proxyInstance;
}
@Override
public Class> getObjectType() {
return mapperInterface;
}
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
this.sqlSession = sqlSessionFactory.openSession();
}
}
为了方便测试,我们在新增一个OrderDao
public interface OrderDao extends baseDao{ }
在bean工厂后置处理器中注册FactoryBean
@Component
public class MybatisBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
private Class extends MybatisFactoryBean> mapperFactoryBeanClass = MybatisFactoryBean.class;
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
AbstractBeanDefinition beanDefinition1 = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
beanDefinition1.setBeanClass(mapperFactoryBeanClass);
ConstructorArgumentValues constructorArgumentValues = beanDefinition1.getConstructorArgumentValues();
constructorArgumentValues.addGenericArgumentValue(UserDao.class);
//自动注入BY TYPE
beanDefinition1.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
registry.registerBeanDefinition("userDao",beanDefinition1);
AbstractBeanDefinition beanDefinition2 = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
beanDefinition2.setBeanClass(mapperFactoryBeanClass);
beanDefinition2.getConstructorArgumentValues().addGenericArgumentValue(OrderDao.class);
//自动注入BY TYPE
beanDefinition2.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
registry.registerBeanDefinition("orderDao",beanDefinition2);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
}
通过这种方式我们不用再为每一个接口都建一个FactoryBean,bean的注入模式为by type用于SqlSessionFactory的注入。
扫描通过上面的两个可以看出,其实除了构造函数传入的接口类型不一样,其他都是一致的,那么我们是否可以约定一个目录,我们扫描这个目录把需要代理的接口读进来即可。
新增一个扫描注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@import(MybatisimportBeanDefinitionRegistrar.class)
public @interface MybatisMapperScan {
String value();
}
在MybatisMapperScan 注解上通过@import导入MybatisimportBeanDefinitionRegistrar用于注册一个MybatisBeanDefinitionRegistryPostProcessor。
public class MybatisimportBeanDefinitionRegistrar implements importBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(Annotationmetadata importingClassmetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
Map annotationAttributes = importingClassmetadata.getAnnotationAttributes(MybatisMapperScan.class.getName());
String basePackage =(String) annotationAttributes.get("value");
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MybatisBeanDefinitionRegistryPostProcessor.class);
//传入要扫描的包路径
builder.addPropertyValue("basePackage", basePackage);
builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(generatebaseBeanName(importingClassmetadata,0), builder.getBeanDefinition());
}
private static String generatebaseBeanName(Annotationmetadata importingClassmetadata, int index) {
return importingClassmetadata.getClassName() + "#" + MybatisimportBeanDefinitionRegistrar.class.getSimpleName() + "#" + index;
}
}
MybatisBeanDefinitionRegistryPostProcessor 逻辑中关键是MybatisClassPathBeanDefinitionScanner类,
它是ClassPathBeanDefinitionScanner的子类。(ClassPathBeanDefinitionScanner是用来扫描@Component,@Service等注解的),另外它还实现BeanNameAware 接口用于注入bean名字。
public class MybatisBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor, BeanNameAware {
private String basePackage;
private String beanName;
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
BeanDefinition mapperScannerBean = registry.getBeanDefinition(beanName);
PropertyValues propertyValues = mapperScannerBean.getPropertyValues();
basePackage = propertyValues.getPropertyValue("basePackage").getValue().toString();
// mybatis接口扫描器
MybatisClassPathBeanDefinitionScanner scanner = new MybatisClassPathBeanDefinitionScanner(registry);
//这个过滤器在spring中是用来判断是否有@Component等注解的,覆盖默认值,所有情况都返回true,
scanner.addIncludeFilter((metadataReader, metadataReaderFactory) -> true);
scanner.scan(basePackage);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
public void setbasePackage(String basePackage) {
this.basePackage = basePackage;
}
@Override
public void setBeanName(String name) {
this.beanName = name;
}
}
MybatisClassPathBeanDefinitionScanner 实现如下
public class MybatisClassPathBeanDefinitionScanner extends ClassPathBeanDefinitionScanner {
private Class extends MybatisFactoryBean> mapperFactoryBeanClass = MybatisFactoryBean.class;
public MybatisClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry) {
super(registry);
}
@Override
protected Set doScan(String... basePackages) {
Set beanDefinitionHolders = super.doScan(basePackages);
for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
AbstractBeanDefinition beanDefinition =(AbstractBeanDefinition) beanDefinitionHolder.getBeanDefinition();
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
//类型注入
beanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
beanDefinition.setBeanClass(this.mapperFactoryBeanClass);
}
return beanDefinitionHolders;
}
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
//只扫描接口
return beanDefinition.getmetadata().isInterface();
}
}
在AppConfig中使用@MybatisMapperScan注解
@ComponentScan("com")
@MybatisMapperScan("com.mapper")
public class AppConfig {
@Bean
SqlSessionFactory getSessionFactory() throws IOException {
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(Resources
.getResourceAsReader("configuration.xml"));
return sessionFactory;
}
}
执行
public static void main(String[] args) throws IOException {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean(UserService.class);
System.out.println(userService.getUser(1));
}
测试结果如下
spring集成mybatis第一篇到这里就分析结束了,大家可能觉得有点奇怪,在MybatisimportBeanDefinitionRegistrar类中可以直接扫描,为什么还要注册一个MybatisBeanDefinitionRegistryPostProcessor,在mybatis中整合到spring的项目中有两个版本,第一个版本是在importBeanDefinitionRegistrar子类中实现扫描,第二个版本才改为通过实现BeanDefinitionRegistryPostProcessor在进行扫描,至于原因,我们在第二篇源码分析的时候在讨论。



