【学习笔记】mybatis的dao层只有接口,没有具体实现,如何工作的?

本文介绍了MyBatis在开发中如何通过Dao接口与XML文件建立关系,从传统开发模式对比代理开发模式,详细阐述了SqlSessionFactoryBean的初始化过程,包括解析mapper.xml文件生成SqlSource和MappedStatement,以及Dao接口的代理实现,最后讨论了如何在Spring中扫描并注册Mapper接口。
摘要由CSDN通过智能技术生成

一、问题引入

mybatis在写dao层的时候只是写了个接口,并没有具体实现,如何正常工作的?

其实最初开发web的时候是需要写dao接口的实现,只是后面mybatis简化了我们的开发模式,将“dao层的实现”这部分重复代码给我们自动生成了,不需要手动写了。

我们先回顾下“需要写dao实现的传统开发模式” 和 “不需要写dao实现的代理开发模式”,再看看mybatis是如何做到这点的。

二、传统开发 VS 代理开发

为了方便比价,这里将“带dao实现”的开发方式称作“传统开发”模式,将“不带dao实现”的开发模式称作“代理开发”模式。

2.1 传统开发模式

step1: 定义UserDao接口

public interface UserDao {
    // 1、 根据用户ID查询用户信息
    public User findUserById(int id) throws IOException;// 2、 根据用户ID和用户名称查询用户信息
    public User findByUserIdAndName(User user) throws IOException;
    
	// 3、 返回所有符合条件的用户信息
    public List<User> getUserList(User user) throws IOException;
}

step2: 定义 UserDao 接口的实现类 UserDaoImpl

public class UserDaoImpl implements UserDao {
    /**  依赖注入,将工程在外面创建 */
    private SqlSessionFactory sqlSessionFactory;

    public UserDaoImpl(SqlSessionFactory sqlSessionFactory) {
        //将外面创建的工厂传递进来(以后spring)
        this.sqlSessionFactory = sqlSessionFactory;
    }

    @Override
    public User findUserById(int id) throws IOException {
        // 创建SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 调用SqlSession的增删改查方法
        User user = sqlSession.selectOne("userMapper.findByUserId", id);
        System.out.println(user);
        // 关闭资源
        sqlSession.close();
        return user;
    }

    @Override
    public User findByUserIdAndName(User user)  throws IOException {
        // 创建SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 调用SqlSession的增删改查方法
        User result = sqlSession.selectOne("userMapper.findByIdAndName", user);
        System.out.println(result);
        // 关闭资源
        sqlSession.close();
        return result;
    }

    @Override
    public List<User> getUserList(User user) throws IOException {
        // 创建SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 调用SqlSession的增删改查方法
        List<User> userList= sqlSession.selectList("userMapper.getUserList", user);
        System.out.println(user);
        // 关闭资源
        sqlSession.close();
        return userList;
    }
}

step3: 对应的 UserMapper.xml 文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.demo.dao.impl.UserDaoImpl" >

    <select id="findUserById" parameterType="int" resultType="User">
        select *from user
        where id = #{id}
    </select>

    <select id="findByUserIdAndName" parameterType="User"  resultType="User">
        select *from user
        where id = #{id} and name = #{name}
    </select>

    <select id="getUserList" parameterType="User"  resultType="User">
        select *from user
        where age = #{age}
    </select>
</mapper>

step4: 模拟业务层调用

public static void main(String [] args) throws Exception {
   InputStream inputStream = Resources.getResourceAsStream(resource);
   // 创建SqlSessionFactory
   SqlSessionFactory  sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
   UserDao dao = new UserDaoImpl(sqlSessionFactory);
   User user = dao.findUserById(1);
   System.out.println(user);
}

2.2 代理开发模式

为了简化开发,Mybatis 提供了一种代理开发的方式,这种方式是项目开发中主流,由Mybatis 实现 Dao 层的接口。

代理开发方式只需要程序员编写Dao 接口 和 对应的Mapper.xml文件,然后由 Mybatis 框架根据接口定义创建接口的动态代理对象,代理对象的方法体同上边Dao接口实现类方法。 **也就是说,不用写dao层的实现类了。**相应的开发流程如下:

step1: 定义UserDao接口

内容同2.1节

step2: 对应的 UserMapper.xml 文件

内容同2.1节
将mapper标签中的 nameSpace值由 UserDaoImpl 改为 UserDao 即可

step3:模拟业务层调用

public static void main(String [] args) throws Exception {
   InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
   // 创建SqlSessionFactory
   SqlSessionFactory  sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
   // 创建SqlSession
   SqlSession sqlSession = sqlSessionFactory.openSession();
   UserDao dao = sqlSession.getMapper(UserDao.class);
   User user = dao.findByUserId(1);
   System.out.println(user);
}

代理开发方式使用的是动态代理的 JDK 代码实现的,使用该代理方式需要遵循下面4个规范:

规范1. 映射文件mapper.xml中的mapper标签的namespace属性与 dao 接口的全限定名相同
在这里插入图片描述
规范2. 映射文件mapper.xml中的每条映射语句中id的属性值与 dao 接口中方法名相同
在这里插入图片描述
规范3. 映射文件mapper.xml中的每条映射语句的parameterType属性与 dao 接口中方法的形参相同
在这里插入图片描述
规范4. 映射文件mapper.xml中的每条映射语句的resultType属性与 dao 接口中方法的返回值类型相同
在这里插入图片描述
如果 Mapper 接口已经遵循上述规范,那么不需要创建 Dao 层的实现类了,可以直接进行使用。

三、Dao接口和XML文件里的SQL是如何建立关系的?

3.1 初始化SqlSessionFactoryBean

3.1.1 SqlSessionFactoryBean简介

每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为中心的,但SqlSessionFactory是一个接口,它里面其实就两个方法:

  • openSession方法 是为了获取一个SqlSession对象,完成必要数据库增删改查功能;
  • getConfiguration方法 可以返回一个Configuration类,可来配置mapper映射文件、SQL参数、返回值类型、缓存等属性;Configuration类可以看成是一个配置管家,MyBatis所有的配置信息都维持在Configuration对象之中,基本每个对象都会持有它的引用。
public interface SqlSessionFactory {
  SqlSession openSession();
  SqlSession openSession(boolean autoCommit);
  SqlSession openSession(Connection connection);
  SqlSession openSession(TransactionIsolationLevel level);
  SqlSession openSession(ExecutorType execType);
  SqlSession openSession(ExecutorType execType, boolean autoCommit);
  SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
  SqlSession openSession(ExecutorType execType, Connection connection);

  Configuration getConfiguration();
}

日常开发中都是将Mybatis与Spring一起使用的,所以把实例化工作交给Spring处理。通常用org.mybatis.spring.SqlSessionFactoryBean来获取SqlSessionFactory实例。

3.1.2 SqlSessionFactoryBean

SqlSessionFactoryBean的类图如下:
在这里插入图片描述
它实现了InitializingBean接口,该接口内只有一个afterPropertiesSet()方法。这说明,SqlSessionFactoryBean类被实例化之后会调用到该方法。

public interface InitializingBean {
    void afterPropertiesSet() throws Exception;
}

SqlSessionFactoryBean类对应的实现:

public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {

	······
	
  @Override
  public void afterPropertiesSet() throws Exception {
   	······
    this.sqlSessionFactory = buildSqlSessionFactory();
  }

  protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
 	1、从配置文件的property属性中加载各种组件,解析配置到configuration中
	2、加载mapper文件,解析SQL语句,封装成MappedStatement对象,配置到configuration中。
  }
}

SqlSessionFactoryBean.afterPropertiesSet()方法只有一个动作,就是buildSqlSessionFactory,这个动作的作用有两个:

  1. 从配置文件的property属性中加载各种组件,解析配置到configuration中
  2. 加载mapper.xml文件,解析SQL语句,封装成MappedStatement对象,配置到configuration中。

3.1.3 解析mapper.xml 文件的过程

从mapperLocations路径去解析里面所有的XML文件,获取SQL内容,可以分位两部分。

3.1.3.1 创建SqlSource

Mybatis会把每个SQL标签封装成SqlSource对象,然后根据SQL语句的不同,又分为动态SQL和静态SQL。其中,静态SQL包含一段String类型的sql语句;而动态SQL则是由一个个SqlNode组成。
在这里插入图片描述
假如我们有这样一个SQL:

<select id="getUserById" resultType="user">
    select * from user 
    <where>
        <if test="uid!=null">
            and uid=#{uid}
        </if>
    </where>
</select>

它对应的SqlSource对象看起来应该是这样的:
在这里插入图片描述

3.1.3.2 创建MappedStatement

XML文件中的每一个SQL标签就对应一个MappedStatement对象,这里面有两个属性很重要:

  • id:全限定类名+方法名组成的ID。
  • sqlSource:当前SQL标签对应的SqlSource对象。

创建完MappedStatement对象,将它缓存到Configuration#mappedStatements中。

Configuration对象就是Mybatis中的大管家,基本所有的配置信息都维护在这里。把所有的XML都解析完成之后,Configuration就包含了所有的SQL信息。
在这里插入图片描述
到目前为止,XML就解析完成了。当我们执行Mybatis方法的时候,就通过全限定类名+方法名找到MappedStatement对象,然后解析里面的SQL内容,执行即可。

3.2 Dao接口代理

3.2.1 配置需要扫描的基本包路径

通过注解的方式配置:

@MapperScan({"com.xxxx.UserDao"})

或者xml的方式配置:

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
	<property name="basePackage" value="com.xxx.UserDao" />
	<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
</bean>

3.2.2 扫描并注册bean

它们的作用是一样的。这个过程是通过 org.mybatis.spring.mapper.MapperScannerConfigurer这个类发挥作用的。
在这里插入图片描述
可以看到它实现了几个接口。其中的重点是BeanDefinitionRegistryPostProcessor。它可以动态的注册Bean信息,方法为postProcessBeanDefinitionRegistry()。

public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
	······
  @Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    if (this.processPropertyPlaceHolders) {
      processPropertyPlaceHolders();
    }
// 创建ClassPath扫描器,设置属性
    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    scanner.setAddToConfig(this.addToConfig);
    scanner.setAnnotationClass(this.annotationClass);
    scanner.setMarkerInterface(this.markerInterface);
    scanner.setSqlSessionFactory(this.sqlSessionFactory);
    scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
    scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
    scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
    scanner.setResourceLoader(this.applicationContext);
    scanner.setBeanNameGenerator(this.nameGenerator);
    scanner.registerFilters();
    
  //调用扫描方法
 scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }
  ······
}

ClassPathMapperScanner中的scan方法最终会调用本类重写的doScan方法:

public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
    ……
    public Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
        if (beanDefinitions.isEmpty()) {
            this.logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
        } else {
            this.processBeanDefinitions(beanDefinitions);
        }

        return beanDefinitions;
    }
    ……
}    

经过上面这些步骤,此时已经扫描到了所有的Mapper接口,并将其注册为BeanDefinition对象。

最终的效果是:将包路径下的所有dao类注册到Spring Bean中,并且将它们的beanClass设置为MapperFactoryBean。

这就相当于使用UserDao注册bean时:当前的dao接口在Spring容器中,beanName是userDao,beanClass是MapperFactoryBean.class。故在Spring的IOC初始化的时候,实例化的对象就是MapperFactoryBean对象。

3.3 使用bean

MapperFactoryBean实现了FactoryBean接口,俗称工厂Bean。那么,当我们通过@Autowired注入这个Dao接口的时候,返回的对象就是MapperFactoryBean这个工厂Bean中的getObject()方法对象。

这个getObject()方法干了些什么呢?
它就是通过JDK动态代理,返回了一个Dao接口的代理对象,这个代理对象的处理器是MapperProxy对象。所有,我们通过@Autowired注入Dao接口的时候,注入的就是这个代理对象,我们调用到Dao接口的方法时,则会调用到MapperProxy对象的invoke方法

使用bean:

@Service
public class UserServiceImpl implements UserService{

    @Autowired
    UserDao userDao1;

    public List<User> getUserList(Map<String,Object> map) {
        return userDao1.getUserList(map);
    }
}

我们调用Dao接口方法的时候,实际调用到代理对象的invoke方法。

public class MapperProxy<T> implements InvocationHandler, Serializable {
  ……
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }
  ……
}  

在这里,实际上调用的就是SqlSession里面的东西了。

public class DefaultSqlSession implements SqlSession {

    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        try {
            MappedStatement ms = configuration.getMappedStatement(statement);
            return executor.query(ms, 
                wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
        }
    }
}

通过statement全限定类型+方法名拿到MappedStatement 对象,然后通过执行器Executor去执行具体SQL并返回:
在这里插入图片描述

四、参考文档

ref1. MyBatis之Dao层实现
ref2. Mybatis中的Dao接口和XML文件里的SQL是如何建立关系的?
ref3. mybatis 的 dao 接口跟 xml 文件里面的 sql 是如何建立关系的?一步步解析
ref4. SqlSession与SqlSessionFactory到底是什么关系?
ref5.MyBatis常用对象SqlSessionFactory和SqlSession介绍和运用
ref6.全网最通俗易懂理清mybatis中SqlSession、SqlSessionTemplate、SessionFactory和SqlSessionFactoryBean之间的关系

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值