本文会讲到mybatis的使用和核心原理分析、spring-mybatis的整合原理,目的是真正搞明白mybatis是如何使用spring的扩展点的
我们通常都是在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 中的相关方法
上述调用过程看似很简单的,其实关键在于第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(); }
就这么一个简单的过程,就能完成对接口的代理,使得接口中的方法可以正常执行
这个过程相信大家都很熟悉了,为了文章的完整性还是在总结一下
@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定义的对象。
@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的整合,其实官方的整合代码中有很多的细节,有时间单独分析官方的整合代码