springboot mybatis如何打印出查询语句_阿里架构师实操讲解:SpringBoot集成MyBatis底层原理,源码太简单

0086f52ff6f140da92c912c046da12ee

MyBatis是可以说是目前最主流的Spring持久层框架了,本文主要探讨SpringBoot集成MyBatis的底层原理。完整代码可移步Github。

一、如何使用MyBatis

一般情况下,我们在SpringBoot项目中应该如何集成MyBatis呢?

  • 引入MyBatis依赖
org.mybatis.spring.boot    mybatis-spring-boot-starter    2.1.2
  • 配置数据源
  • 在启动类上添加@MapperScan注解,传入需要扫描的dao层的包路径
  • 在dao层中创建接口,在方法上传入对应的SQL语句,也可以使用Mapper.xml文件进行配置。例如:
public interface UserDao {    @Select("insert into user xxx")    void insert();}
  • 完成这些工作后,我们就可以调用new UserDao().insert()方法来实现对数据库的操作了。

那么问题来了,MyBatis是如何通过如此简单的配置完成完成与Spring的“无缝连接”和数据的持久化工作的呢?

4c70750f-ff44-456d-807e-b7aac3160fe7

二、Spring的BeanDefinition

众所周知,Spring的一大特性是IoC,既控制反转。当我们将一个对象交给Spring管理之后,我们就不需要手动地通过new关键字去创建对象,只需要通过@Autowired或者@Resource自动注入即可。那么这个过程是如何实现的呢?

简单来说,Spring会通过一个被声明为bean的类信息生成一个beanDefinition(后面简称BD)对象,然后通过这个BD对象创建一个单例(不声明为prototype的情况下),存入单例池,需要时进行调用。

在创建beadDefinition时,Spring会调用一系列的后置处理器(postProcessor)对BD加以处理,我们也可以自定义后置处理器,对BD的属性进行修改,只需要实现BeanFactoryPostProcessor接口即可,例如:

@Componentpublic class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {    @Override    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {        GenericBeanDefinition userDaoBD = (GenericBeanDefinition)beanFactory.getBeanDefinition("userDao");        userDaoBD.setBeanClassName("userDaoChanged");        userDaoBD.setAbstract(false);        // more...    }}

在这个postProcessor中,我们获取了UserDao的BD对象,并且将它的名字修改为"userDaoChanged",这样我们就可以通过调用ApplicationContext的getBean("userDaochanged")方法获取到原来的userDao的bean。

三、关于MyBatis

现在我们知道了,当我们将一个类交给Spring管理时,Spring通过beanDefinition构建bean单例。现在,我们又有了两个新的问题:

  • MyBatis如何将dao交给Spring管理的?
  • 我们编写的dao是一个接口,接口是如何实例化的?

四、MyBatis如何将dao交给Spring管理的?

在Spring中,将一个对象交给Spring管理主要有三种方式:

  • @Bean
  • beanFactory.registerSingleton(String beanName, Object singletonObject)
  • FactoryBean

其中MyBatis使用的是FactoryBean的方式,实现FactoryBean接口就可以将我们的userDao注入到Spring当中。

beanFactory管理着Spring所有的bean,是一个大工厂。FactoryBean也是一个bean,但它却有着Factory的功能,当我们调用Spring上下文的getBean()方法,并传入自定义的FactoryBean时,返回的bean并不是这个FactoryBean本身,而是重写的getObject()方法中所返回的对象。

如此看来,FactoryBean就是一个“小工厂”。

@Componentpublic class UserFactoryBean implements FactoryBean {        UserDao userDao;    @Override    public Object getObject() throws Exception {        return userDao;    }    @Override    public Class> getObjectType() {        return UserDao.class;    }}

只是这样写当然是不能满足我们的要求的,我们这时候调用getBean()方法会报错,我们无法传入一个userDao参数,因为UserDao不能被实例化。但是在MyBatis中,我们却可以通过sqlSession.getMapper(UserDao.class)方法获取到一个UserDao的实例化对象,MyBatis是如何做到这一点的?

d4d33478-b029-4fc7-b39f-c0c0907868f4

五、如何实例化一个接口?

为什么接口可以被实例化呢?查看MyBatis的源码我们可以得知,MyBatis通过动态代理(Proxy)的技术在接口的基础上包装出了一个对象,然后将这个对象交给了Spring。沿着getMapper方法追根溯源我们可以发现这样一个方法:

protected T newInstance(MapperProxy mapperProxy) {        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);    }

MyBatis可以,那我们也可以,我们改写一下UserFatoryBean:

@Componentpublic class UserFactoryBean implements FactoryBean {    @Override    public Object getObject() throws Exception {        Class[] classes = new Class[]{UserDao.class};        return Proxy.newProxyInstance(MyFactoryBean.class.getClassLoader(),classes, new MyInvokationHandler());    }    @Override    public Class> getObjectType() {        return UserDao.class;    }}

Proxy.newProxyInstance()方法接收三个参数,分别为:

  • ClassLoader loader:决定用哪个类加载器来加载生成的代理对象
  • Class>[] interfaces:决定这个代理对象要实现哪些接口,拥有哪些功能
  • InvocationHandler h:决定调用这个代理对象的某个方法时执行的具体逻辑

然后编写一个InvokationHandler类用于执行代理对象的具体方法逻辑:

public class MyInvokationHandler implements InvocationHandler {    @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        System.out.println("假装查询数据库:" + method.getAnnotation(Select.class).value()[0]);        return null;    }}

在这个handler中,我们获取到@Select注解中的信息,然后将它打印出来。

OK,现在我们运行一下:

@Testvoid contextLoads() {    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Appconfig.class);    UserDao userDao = (UserDao) ac.getBean("userFactoryBean");    userDao.insert();}

控制台输出:

假装查询数据库:insert into user xxx

至此,我们就完成了MyBatis的简易实现的一小部分。但是还有一个重要的问题:我们这个FactoryBean是写死的,只能返回UserDao的代理对象,实际情境下,我们如何根据用户传入的参数返回不同的代理对象呢?

六、动态生成代理对象

想要动态生成代理对象,首先我们需要修改UserFactoryBean的代码,让它能适配各种类型的dao,不妨直接改个名字叫MyFactoryBean:

public class MyFactoryBean implements FactoryBean {    Class mapperInterface;    // 为了支持XML配置,必须提供一个默认构造方法    public MyFactoryBean(){}    // 通过MapperScan方式    public MyFactoryBean(Class mapperInterface){        this.mapperInterface = mapperInterface;    }    @Override    public Object getObject() throws Exception {        Class[] classes = new Class[]{mapperInterface};        return Proxy.newProxyInstance(MyFactoryBean.class.getClassLoader(),classes, new MyInvokationHandler());    }    @Override    public Class> getObjectType() {        return mapperInterface;    }}

我们将UserDao.class改成了动态的mapperInterFace,那么我们如何向MyFactoryBean的构造方法传入这个参数呢?这就回到了我们一开始说到的beanDefinition,在Spring中,可以在bd期间修改bean的各种属性,这其中就包括构造方法的参数。我们修改我们一开始写的MyBeanFactoryPostProcessor:

@Componentpublic class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {    @Override    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {        List daos = new ArrayList<>();        // 获取所有dao        daos.add(UserDao.class);        daos.add(AnchorDao.class);        for(Class dao:daos){            // 获取一个空的beanDefinition            BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MyFactoryBean.class);            GenericBeanDefinition beanDefinition = (GenericBeanDefinition) builder.getBeanDefinition();            // 为构造方法添加参数            beanDefinition.setBeanClass(MyFactoryBean.class);            beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(dao);        }    }}

这样的话我们就构造出了我们想要的beanDefinition,现在要做的是把它加入到Spring容器中去。注意:是把beanDefinition加入到Spring容器中,而不是把bean加到Spring容器中。我们前面说的@Bean之内的方法是不适用的。

这里需要用到另两个知识点:@Import和ImportBeanDefinitionRegistrar

在应用中,有时没有把某个类注入到IOC容器中,但在运用的时候需要获取该类对应的bean,此时就需要用到@Import注解。

@Import最强大的地方在于,它提供了一个扩展点给用户。当我们用@Import导入的类实现了ImportBeanDefinitionRegistrar接口时,Spring不会直接将这个类包装成一个bean,而是执行其内部的registerBeanDefinitions方法。这有点像FactoryBean,可以在类中执行自己的逻辑。

我们编写这样一个registerBeanDefinitions:

public class ImportBeanDefinitionRegister implements ImportBeanDefinitionRegistrar {    @Override    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry){        // 获得包名,遍历获得所有类名        String packagePath = Appconfig.class.getAnnotation(MyScan.class).path();        List classNames = SelectClassName.selectByPackage(packagePath);        for(String className:classNames){            BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MyFactoryBean.class);            GenericBeanDefinition genericBeanDefinition = (GenericBeanDefinition) builder.getBeanDefinition();            registry.registerBeanDefinition(SelectClassName.getShortName(className),genericBeanDefinition);            // 添加构造方法参数            genericBeanDefinition.getConstructorArgumentValues().addGenericArgumentValue(className);        }    }}

并且编写一个MyScan注解类用户获取需要扫描的包名:

@Retention(RetentionPolicy.RUNTIME)public @interface MyScan {    String path();}

然后在AppConfig类上加入@MyScan注解,传入包名,最后编写一个工具类用来获取包下的所有类名。MyBeanFactoryPostProcessor类也可以删除了,ImportBeanDefinitionRegister替代了它的工作。

@Testvoid contextLoads() {    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Appconfig.class);    ac.getBean(AnchorDao.class).query();    ac.getBean(UserDao.class).insert();}

控制台输出:

假装查询数据库:select * from anchor limit 5假装查询数据库:insert into user xxx

大功告成!回头再看一下我们是如何使用MyBatis的:@MapperScan、编写dao接口、@Select——和我们现在的功能几乎完全一样。只需要在MyInvokationHandler中封装一下JDBC,实现具体的访问数据库逻辑,你就可以在项目中使用自己编写的“MyBatis”了。

七、总结

Spring提供了很好的环境用于第三方框架的开发,这也是Spring能发展出如今这样庞大且完善的生态的原因之一。知识都是触类旁通的,例如Spring的另一大特性:AOP,它就与本文谈到的后置处理器beanPostProcessor和动态代理有关,对SpringBoot集成MyBatis底层原理的学习和研究,让我对Spring和MyBatis都有了更深入的认识

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值