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呢?
@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执行具体的调用。

浙公网安备 33010602011771号