Mybatis到底帮我们做了什么

Mybatis到底解放了程序开发者什么工作,它的底层原理到底是什么,接下来做一个简单总结。

一:没有Mybatis时如何操作数据库?

从代码中可以看到在与数据库建立连接之后,需要手动的执行java代码进行查询和结果收集

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class JdbcExample {
    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        try {
            // 加载驱动
            Class.forName("com.mysql.cj.jdbc.Driver");
            // 建立连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "password");
            // 创建语句对象
            stmt = conn.createStatement();
            // 执行查询
            rs = stmt.executeQuery("SELECT * FROM users");
            // 处理结果集
            while (rs.next()) {
                System.out.println(rs.getString("username"));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 关闭资源
            try {
                if (rs != null) rs.close();
                if (stmt != null) stmt.close();
                if (conn != null) conn.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

如果用了Mybatis,该如何实现查询呢?

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.InputStream;

public class MyBatisExample {
    public static void main(String[] args) throws Exception {
        // 加载 MyBatis 配置文件
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        // 创建 SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        // 创建 SqlSession
        try (SqlSession session = sqlSessionFactory.openSession()) {
            // 执行数据库操作
            session.selectList("xxMapper.queryUsers");
        }
    }
}

 从中可以看到,似乎SqlSession是执行数据库操作的核心,具体的查询是通过该接口的一系列方法执行的,也就是说有了Mybatis,开发者不用每次使用java底层提供的基本能力,而是直接使用SqlSession封装的一系列方法,这些方法会让开发者聚焦于sql本身的编写,不需要关注参数的转换,底层如建立连接,回收链接等,通过抽象和面向对象的思维,大大简化了开发的复杂性。

疑问:SqlSession和Connection有什么关系?

可以从源码中看出,SqlSession还是用的java的Connection,也就是说Connection才是操作数据库连接的基本单元,然后Connection是在连接池中,每次取出后需要放回。

 

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.apache.ibatis.session;

import java.io.Closeable;
import java.sql.Connection;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.cursor.Cursor;
import org.apache.ibatis.executor.BatchResult;

public interface SqlSession extends Closeable {
   
    Configuration getConfiguration();

    <T> T getMapper(Class<T> type);

    Connection getConnection();
}

二:Mybatis进阶使用

虽然上文列举了Mybatis对比传统jdbc确实有诸多好处,但是使用起来似乎也很复杂,有没有更优雅的使用方式呢?答案是:当然有。

为了简化使用复杂性,Mybatis提供了模版方法,通过提供操作模版,进一步简化开发者感知sqlsession,那就是SqlSessionTemplate

import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

@Repository
public class UserDao {

    @Autowired
    private SqlSessionTemplate sqlSessionTemplate;

    public User selectUserById(int id) {
        return sqlSessionTemplate.selectOne("UserMapper.selectUserById", id);
    }
}

 sqlSessionTemplate是单例的,全局只有一个实例,可以看做是一个操作模版,对sqlsession进行了友好的增强,当执行具体方法时,比如上文中selectOne时,会调用内部的sqlSessionProxy代理对象进行真正的执行,代理对象在invoke时先要获取到SqlSession(如何获取?两种方式:如果开启事务如@Transactional,会在方法真正执行前,从SqlSessionFactory中取出一个SqlSession放入ThreadLocal中,这样一个线程在执行事务内的sql的时候用的都是一个SqlSession,就达到了事务控制的目的,如果不在事务中那么每次都从工厂中获取SqlSession即可)获取到SqlSession之后事情就变得简单了,直接执行sql即可。所以本质上sqlSessionTemplate只是一个工具箱,只是内部封装了一些功能,比如代理对象等,具体的执行还是靠SqlSession,只是因为事务的有无,SqlSession的获取方式也不一样,不过最后都是执行sql,下面是详细源码,可以看到代理对象的invoke具体过程。

public class SqlSessionTemplate implements SqlSession, DisposableBean {

  private final SqlSessionFactory sqlSessionFactory;

  private final ExecutorType executorType;

  private final SqlSession sqlSessionProxy;//代理对象,可以对方法增强,减少代码冗余

  private final PersistenceExceptionTranslator exceptionTranslator;

  /**
   * Constructs a Spring managed SqlSession with the {@code SqlSessionFactory} provided as an argument.
   *
   * @param sqlSessionFactory
   *          a factory of SqlSession
   */
  public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    this(sqlSessionFactory, sqlSessionFactory.getConfiguration().getDefaultExecutorType());
  }

  /**
   * Constructs a Spring managed SqlSession with the {@code SqlSessionFactory} provided as an argument and the given
   * {@code ExecutorType} {@code ExecutorType} cannot be changed once the {@code SqlSessionTemplate} is constructed.
   *
   * @param sqlSessionFactory
   *          a factory of SqlSession
   * @param executorType
   *          an executor type on session
   */
  public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType) {
    this(sqlSessionFactory, executorType,
        new MyBatisExceptionTranslator(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), true));
  }
    
    private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
      try {
        Object result = method.invoke(sqlSession, args);
          //是否在事务中
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          //如果不是,直接提交
          sqlSession.commit(true);
        }
          //否则返回结果,等待统一提交
        return result;
      } catch (Throwable t) {
        Throwable unwrapped = unwrapThrowable(t);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
          // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
          sqlSession = null;
          Throwable translated = SqlSessionTemplate.this.exceptionTranslator
              .translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {
            unwrapped = translated;
          }
        }
        throw unwrapped;
      } finally {
        if (sqlSession != null) {
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }
}

 

三:Mybatis与Spring集成实战使用

 在实际spring项目中,通常不会直接使用sqlSessionTemplate去操作sql,因为直接使用的话,代码的简洁性、可读性、复用性都不高,同时也不利于代码的抽象和维护,所以在实际使用的时候通常用Mapper的方式。

那么到底是如何做到的呢?接下来一步步分析:

 第一步:配置数据源

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DataSourceConfig {

    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/your_database_name?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC");
        config.setUsername("your_username");
        config.setPassword("your_password");
        config.setDriverClassName("com.mysql.cj.jdbc.Driver");
        return new HikariDataSource(config);
    }
}

第二步:创建SqlSessionFactory,同时注入数据源,指定生效的mapper位置(mapper可以是xml文件或者直接注解)

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;

@Configuration
@MapperScan("com.example.demo.mapper")//配置生效的mapper范围
public class MyBatisConfig {

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        // 设置 mapper.xml 文件的位置
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
        return factoryBean.getObject();
    }

   @Bean //这里无需显式的实例化,只是为了看起来方便
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

 第三步:创建xxMapper接口,加@Mapper注解

import org.apache.ibatis.annotations.Mapper;
import com.example.entity.User;

@Mapper
public interface UserMapper {
    
    @Select("SELECT * FROM user WHERE id = #{id}")
    User getUserById(int id);
}

 第四步:直接注入xxMapper接口即可

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;//通过注入Mapper,达到直接调用方法的目的,编码方式优雅、简单、复用性强

    public User getUserById(int id) {
        return userMapper.getUserById(id);
    }
}

底层原理分析:为什么不用显式的使用SqlSessionTemplate就可以直接操作sql呢?

MyBatis-Spring 框架利用 Java 的动态代理机制为 Mapper 接口创建代理对象。在应用启动时,Spring 会扫描指定包下的 Mapper 接口,然后使用 MapperFactoryBean 为这些接口生成代理实例。这些代理对象会自动处理 SqlSession 的获取、使用和关闭等操作,从而将开发者从繁琐的 SqlSessionTemplate 操作中解放出来。
例如,当我们调用 Mapper 接口的方法时,实际上是调用代理对象的对应方法。代理对象会在内部创建 SqlSession,并使用该 SqlSession 执行相应的 SQL 语句,执行完毕后还会负责关闭 SqlSession。整个过程对开发者是透明的,开发者只需关注 Mapper 接口的定义和方法调用。
@Mapper 注解能让 Spring 识别并管理这些 Mapper 接口。Spring 会将这些代理对象注册为 Bean,从而可以在其他组件(如 Service 层)中使用 @Autowired 注解进行依赖注入。
源码分析:
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.support.SqlSessionDaoSupport;
import org.springframework.beans.factory.FactoryBean;
//spring识别到@Mapper注解后,在加载bean的时候,会为Mapper接口创建jdk动态代理对象
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {

    private Class<T> mapperInterface;

    public MapperFactoryBean(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    @Override
    public T getObject() throws Exception {
        //getSqlSession()获取到的是sqlSessionTemplate
        return getSqlSession().getMapper(this.mapperInterface);//这里就是生成Mapper代理对象的入口
    }

    @Override
    public Class<T> getObjectType() {
        return this.mapperInterface;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

接下来会触发代理对象的具体创建

// MapperRegistry.java
public class MapperRegistry {
    private final Configuration config;
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
        if (mapperProxyFactory == null) {
            throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        }
        try {
            return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
            throw new BindingException("Error getting mapper instance. Cause: " + e, e);
        }
    }
}

// MapperProxyFactory.java
public class MapperProxyFactory<T> {
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();

    public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    public T newInstance(SqlSession sqlSession) {
        //可以看到这里代理对象被注入了sqlSessionTemplate,后面所有的数据库操作都是用这个sqlSessionTemplate
        final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
    }
}

代理对象的执行

// MapperProxy.java
public class MapperProxy<T> implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -6424540398559729838L;
  private final SqlSession sqlSession;//这里有了SqlSession就可以操作数据库连接了
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethodInvoker> methodCache;

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethodInvoker> methodCache) {
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }

  @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 {
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

  private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    try {
      return methodCache.computeIfAbsent(method, m -> {
        if (m.isDefault()) {
          try {
            if (privateLookupInMethod == null) {
              return new DefaultMethodInvoker(getMethodHandleJava8(method));
            } else {
              return new DefaultMethodInvoker(getMethodHandleJava9(method));
            }
          } catch (IllegalAccessException | InstantiationException | InvocationTargetException
              | NoSuchMethodException e) {
            throw new RuntimeException(e);
          }
        } else {
          return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
        }
      });
    } catch (RuntimeException re) {
      Throwable cause = re.getCause();
      throw cause == null ? re : cause;
    }
  }
}

 所以流程就是:一旦定义的@Mapper接口被Spring扫描到,就会将该接口标记为应生成代理对象,在初始化完成时候进行对象代理,之后执行具体的sql时候实际是代理对象通过封装的SqlSessionTemplate执行具体的调用。

 

posted @ 2025-04-23 01:39  反内耗先锋  阅读(31)  评论(0)    收藏  举报