本文会讲到mybatis的使用和核心原理分析、spring-mybatis的整合原理,目的是真正搞明白mybatis是如何使用spring的扩展点的
Mybatis部分
mybatis的简单使用
我们通常都是在spring环境下使用mybatis,那么mybatis在非spring的环境下mybatis是如何使用呢?下面将会采用注解的方式来使用mybatis
DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(BlogMapper.class);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
// 数据查询阶段
SqlSession sqlSession = sqlSessionFactory.openSession();
// 这一步完成了代理
BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog();
log.info("blog {}", blog);
总结一下,mybatis的使用为下面几个步骤:
1、创建数据源和事务工厂
2、创建Environment 对象,并把数据源和事务工厂传入该对象
3、创建SqlSessionFactory, 并把Environment 传入该对象
4、获取SqlSession
5、调用sqlSession.getMapper获取BlogMapper 对象
6、执行BlogMapper 中的相关方法
mybatis核心原理
上述调用过程看似很简单的,其实关键在于第5步,我们可以看到第五步传入了一个接口,返回也是用接口去接的,那么为什么在第6步就可以直接调用这个方法呢,这里直接说结论,第五步是使用代理完成的,获取的是接口的代理对象,这个对象已经对接口中的方法进行了增强,才能使我们能在第六步顺利完成调用。
在这里,我们模拟下这一过程,如果你对jdk代理的使用还不了解,请完成了解后再看下面步骤
public class CustomSqlSession {
public <T> T getMapper(Class<T> clazz) {
Object proxy = Proxy.newProxyInstance(CustomSqlSession.class.getClassLoader(), new Class[]{clazz}, new CustomInvocationHandler());
return (T) proxy;
}
class CustomInvocationHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.isAnnotationPresent(Select.class)) {
Select select = method.getAnnotation(Select.class);
String sql = select.value()[0];
log.info("sql:{}", sql);
}
if (method.getName().equals("toString")) {
return proxy.getClass().getName();
}
return null;
}
}
}
public static void main(String[] args) {
CustomSqlSession customSqlSession = new CustomSqlSession();
BlogMapper mapper = customSqlSession.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog();
}
就这么一个简单的过程,就能完成对接口的代理,使得接口中的方法可以正常执行
mybatis-spring部分
mybatis整合
这个过程相信大家都很熟悉了,为了文章的完整性还是在总结一下
@Configuration
@MapperScan("haoxu.wang.mybatis.common")
public class MybatisConfig {
@Bean()
public DataSource dataSource() {
DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
driverManagerDataSource.setPassword("123456");
driverManagerDataSource.setUsername("root");
driverManagerDataSource.setUrl("jdbc:mysql://39.105.156.80:3306/demo?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC");
return driverManagerDataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource());
return factoryBean.getObject();
}
}
public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MybatisConfig.class);
Blog blog = ac.getBean(BlogMapper.class).selectBlog();
log.info("blog {}", blog);
}
可以看的出来,一个@MapperScan注解就完成了把mapper接口注入到spring容器中,这里是如何完成的呢?
根据spring给用户提供的接口以及扩展点,有一下方式可以完成对对象的注入
1.xml中的bean标签
这种方法需要提供一个类,然后交给spring去初始化,奈何mybatis的mapper是mybatis通过代理创建的,所以无法使用这种方式。
其实这里还可以配置工厂方法进行注入,但是为每个mapper都写一个工厂方法显然不大合适
2.@bean注解
为每个mapper都写一个@bean方法,可以实现,但是也不合适
3.@Componse注解扫描
需要交给spring去实例化,无法实现
4.registerBean、registerBeanDefinition、registerSingleton
registerBean、registerBeanDefinition需要交给spring去实例化,无法实现;
registerSingleton可以实现但是spring没这么做,为什么没这么做?我认为是因为registerSingleton这种方式属于给用户提供的api,不是属于扩展点,如果mybatis这么做,其实是需要用户手动去调用这个方法的,如何能通过扩展点注入才是mybatis的出路
这里如果使用BeanFactoryPostProcessor中调用registerSingleton确实可以属于扩展点,也可以优雅的完成mapper的注入,但是mybatis没这么做
5.FactoryBean方法
mybatis就是选择了这种方法,至于为什么选择这种办法,其实这跟spring给用户提供的扩展点有关系,spring给用户提供了很多的扩展点,要不就是使用第4中方式中的办法,但是除了registerSingleton,其余两个都需要把类交给spring,如何能把类交给spring,但是又能自己处理初始化?spring提供了FactoryBean这种方式。简单概括这种方式,就是在FactoryBean中定义类的初始化方式,然后把FactoryBean注入到容器,当容器进行初始化时,会注入两个对象。一个是FactoryBean,一个是FactoryBean定义的对象。
mybatis整合原理
@Retention(RetentionPolicy.RUNTIME)
@Import(CustomImportBeanDefinitionRegistrar.class)
public @interface CustomScan {
String value() default "com.sahdo";
}
public class CustomFactoryBean implements FactoryBean{
Class mapperInterface;
public CustomFactoryBean(){
System.out.println("xxxx");
}
public CustomFactoryBean(Class mapperInterface){
this.mapperInterface=mapperInterface;
}
@Override
public Object getObject() throws Exception {
CustomSqlSession customSqlSession = new CustomSqlSession();
Object mapper = customSqlSession.getMapper(mapperInterface);
return mapper;
}
@Override
public Class<?> getObjectType() {
return mapperInterface;
}
public void setMapperInterface(Class mapperInterface) {
this.mapperInterface = mapperInterface;
}
}
public class CustomImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//customBeanFactory -bd
MergedAnnotations annotations = importingClassMetadata.getAnnotations();
//spring scan
List<Class> list = new ArrayList<>();
list.add(BlogMapper.class);
for (Class aClass : list) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(CustomFactoryBean.class);
AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
beanDefinition.getPropertyValues().add("mapperInterface",aClass);
String beanName=(aClass.getSimpleName().substring(0,1).toLowerCase())+aClass.getSimpleName().substring(1);
registry.registerBeanDefinition(beanName,beanDefinition);
}
}
}
就这样完成了mapper的注入,调用非常简单
@Configuration
@CustomScan("haoxu.wang.mybatis.common")
public class CustomMybatisConfig {
}
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(CustomMybatisConfig.class);
BlogMapper bean = ac.getBean(BlogMapper.class);
bean.selectBlog();
}
}
上述就模拟了spring-mybatis的整合,其实官方的整合代码中有很多的细节,有时间单独分析官方的整合代码