Mybatis的使用和原理

目录

入门示例

全局配置

properties

settings

typeAlias

typeHandlers

objectFactory

plugins

environments

mappers

mapper映射

select

update

sql

参数

结果集映射

二级缓存

日志

动态sql

if

choose/when/otherwise

where

foreach

script

原理

创建SqlSessionFactory

创建SqlSession

执行过程

缓存原理

一级缓存

二级缓存

数据源

日志

事务

设计模式


官网:MyBatis中文网

入门示例

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.mybatis.test.mapper.UserMapper">
    <!--<select id="selectUsers" resultType="map">
        select * from user;
    </select>-->
</mapper>

UserMapper.java
package com.mybatis.test.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.Map;
@Mapper
public interface UserMapper {
    @Select("select * from user")
    Map<String, Object> selectUser();
}

mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://10.211.55.7:3306/users?useSSL=false&amp;serverTimezone=Asia/Shanghai&amp;allowPublicKeyRetrieval=true"/>
                <property name="username" value="root"/>
                <property name="password" value="Gz2021.."/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper/UserMapper.xml"/>
    </mappers>
</configuration>

测试类:
package com.mybatis.test;

import com.mybatis.test.mapper.UserMapper;
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;
import java.util.List;

public class GetSqlSessionTest {

    public static void main(String[] args) throws Exception {

        String config = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(config);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession sqlSession = sqlSessionFactory.openSession();

        //通过xml定义mapper
        //List<Object> list = sqlSession.selectList("com.mybatis.test.mapper.UserMapper.selectUsers");
        //System.out.println(list);

        //通过接口定义mapper
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        System.out.println(userMapper.selectUser());
        sqlSession.close();
    }
}

SqlSession是非线程安全的,只能存在于当前请求作用于内,不能作为共享变量使用。

在最原始的Mybatis使用方式中,接口和mapper.xml的是sql的两种类型的容器,但使用接口封装sql时,还是需要通过namespace关联mapper.xml,然后导入mapper.xml,这样mybatis才能够识别接口中的方法。

全局配置

properties

<properties>
    <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://10.211.55.7:3306/users?useSSL=false&amp;serverTimezone=Asia/Shanghai&amp;allowPublicKeyRetrieval=true"/>
    <property name="username" value="root"/>
<properties/>

通过#{name}的方式在配置文件中使用。

settings

设置mybatis的一些内部行为。

配置_MyBatis中文网

<settings>
    <!-- 打印mybatis日志 -->
    <setting name="logImpl" value="STDOUT_LOGGING" />
    <!-- 是否开启二级缓存 -->
    <setting name="cacheEnabled" value="false"/>
    <!-- 设置执行器类型 -->
    <setting name="defaultExecutorType" value="SIMPLE"/>
</settings>

typeAlias

设置别名,一边用在自定义类上,方便其他地方声明类型。mybatis对一内部的类型提供了默认的别名。

配置_MyBatis中文网

如果指定类型,别名就是给定的名称,如果指定包名,别名就是去掉包名的类名。

<typeAliases>
    <package name="com.mybatis.test.domain"/>
</typeAliases>

//使用com.mybatis.test.domain包下的User时就不用使用全限定名了
<select id="selectUsers" resultType="User">
    select * from user;
</select>

typeHandlers

类型处理器,当向sql中设置参数或从ResultSet获取结果集封成Bean时,会通过类型处理器进行类型转换。

mybatis提供了很多内置的类型处理器,如果有特殊需求,可以自定义类型处理器,然后通过该注解进行声明。

@MappedJdbcTypes(JdbcType.VARCHAR)
public class MyTypeHandler extends BaseTypeHandler<String> {
    
    //设置参数
    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement, int i, String s, JdbcType jdbcType) throws SQLException {
        System.out.println("setNonNullParameter");
        preparedStatement.setString(i, s);
    }

    //获取结果集
    @Override
    public String getNullableResult(ResultSet resultSet, String s) throws SQLException {
        System.out.println("getNullableResult");
        return resultSet.getString(s);
    }

    @Override
    public String getNullableResult(ResultSet resultSet, int i) throws SQLException {
        System.out.println("getNullableResult");
        return resultSet.getString(i);
    }

    @Override
    public String getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
        System.out.println("getNullableResult");
        return callableStatement.getString(i);
    }
}
<typeHandlers>
    <typeHandler handler="com.mybatis.test.typehandler.MyTypeHandler"/>
</typeHandlers>

objectFactory

对象工厂的作用是处理结果及时创建返回值的对象,如果想改变默认的行为,可以通过继承DefaultObjectFactory来实现。

plugins

插件的作用是对某些组件的执行过程进行拦截,主要针对以下四个组件:

  • Executor
  • ParameterHandler
  • ResultSetHandler
  • StatementHandler

自定义插件通过实现Interceptor以及@Interceptor注解来实现。

environments

environment → transactionManager(JDBC/Managed) → dataSource(POOLED、UNPOOLED、JNDI)

mappers

引入mapper.xml文件,有四种方式:

  • resource
  • url
  • class
  • package

mapper映射

cache、ref-cache、resultMap、sql、insert、update、delete、select

select

<select
  #唯一标识符,用来引用这个sql,基于接口的使用方式中,这个id是方法名
  id="selectPerson"
  #参数类型
  parameterType="int"
  #已经废弃了
  parameterMap="deprecated"
  #结果类型,即用什么来封装数据库中的没一条记录
  resultType="hashmap"
  #结果类型映射,能够进行一些转换和一对多 多对一 多对多的配置,能够进行字段名和属性名的匹配配置
  resultMap="personResultMap"
  #是否刷新二级缓存
  flushCache="false"
  #是否使用二级缓存,默认为true,如果开启了二级缓存就会使用
  useCache="true"
  #驱动等待数据库返回数据的超时时间,是否支持 依赖于数据库的实现,很少使用
  timeout="10"
  #给数据库驱动一个建议,每次返回多少条,依赖于驱动的实现,很少使用
  fetchSize="256"
  #STATEMENT → Statement,PREPARED → PreparedStatement CALLABLE → CallableStatement
  statementType="PREPARED"
  #很少使用
  resultSetType="FORWARD_ONLY">

update

<insert
  id="insertAuthor"
  parameterType="domain.blog.Author"
  #是否刷新二级缓存,默认为true
  flushCache="true"
  statementType="PREPARED"
  #指定主键的属性名,通过与getGeneratedKeys或selectKey配合,给主键属性自动生成一个值
  #selectKey是insert标签中的一个子标签,用于定义生成key值的规则
  keyProperty=""  
  keyColumn=""
  #生成key值,默认为false
  useGeneratedKeys="false"
  timeout="20">

<update
  id="updateAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">

<delete
  id="deleteAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">

sql

定义sql片段。

<sql id="">
  ...
<sql/>
<include refid="">

参数

#{prop} 与 ${prop}

#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}

结果集映射

结果集映射一般有以下几种形式:

  • 通过resultType指定一个bean,属性名与sql返回的列名完全一直,mybatis自动映射。
  • 通过resultType指定结果集类型为map,mybatis通过一个map来封装一条数据,key是列名。
  • 通过resultMap指定一个自定义的映射,在复杂sql中比较常用,能够配置一对多、多对一等关系。

二级缓存

使用cache时,是为当前mapper创建一个单独的cache实例,使用cache-ref时,是与其他mapper共享一个缓存实例

<cache
  #淘汰策略,LRU、FIFO、SOFT、WEAK
  eviction="FIFO"
  #刷新频率
  flushInterval="60000"
  #缓存对象的个数
  size="512"
  #是否为只读
  readOnly="true"/>

日志

支持的日志框架类型:

  • SLF4J
  • Apache Commons Logging
  • Log4j 2
  • Log4j
  • JDK logging
<setting name="logImpl" value="LOG4J"/>
#标准输出
<setting name="logImpl" value="STDOUT_LOGGING" />

可选的值有:SLF4J、LOG4J、LOG4J2、JDK_LOGGING、COMMONS_LOGGING、STDOUT_LOGGING、NO_LOGGING,或者是实现了 org.apache.ibatis.logging.Log 接口。

动态sql

if

<if test="author != null and author.name != null">
    AND author_name like #{author.name}
  </if>

choose/when/otherwise

相当于switch、case、defualt

<choose>
    <when test="title != null">
      AND title like #{title}
    </when>
    <when test="author != null and author.name != null">
      AND author_name like #{author.name}
    </when>
    <otherwise>
      AND featured = 1
    </otherwise>
</choose>

where

通过where能够自动处理一些sql错误。

<where>
    <if test="state != null">
         state = #{state}
    </if>
    <if test="title != null">
        AND title like #{title}
    </if>
    <if test="author != null and author.name != null">
        AND author_name like #{author.name}
    </if>
  </where>

foreach

<foreach item="item" index="index" collection="list"
      open="(" separator="," close=")">
        #{item}
  </foreach>

script

在基于接口的mapper中定义一些复杂的sql

@Update({"<script>",
      "update Author",
      "  <set>",
      "    <if test='username != null'>username=#{username},</if>",
      "    <if test='password != null'>password=#{password},</if>",
      "    <if test='email != null'>email=#{email},</if>",
      "    <if test='bio != null'>bio=#{bio}</if>",
      "  </set>",
      "where id=#{id}",
      "</script>"})
    void updateAuthorValues(Author author);

原理

SqlSessionFactoryBuilder、SqlSessionFactory、SqlSession、Exeuctor、MappedStatement、StatementHandler、SqlSource、BoundSql、ParameterHandler、ResultHandler、TypeHandler。

关键点:数据源、一级缓存、二级缓存、日志、事务

mybatis流行的两点原因:

  • ①对jdbc模板代码的封装
  • ②提供了丰富的扩展功能
  • ③提供了非常好的扩展性

创建SqlSessionFactory

//SqlSessionFactoryBuilder
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
  try {
    //读取xml配置文件  
    XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
    //创建SqlSessionFactory
    return build(parser.parse());
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  } finally {
    ErrorContext.instance().reset();
    try {
     if (reader != null) {
       reader.close();
     }
    } catch (IOException e) {
      // Intentionally ignore. Prefer previous error.
    }
  }
}

public SqlSessionFactory build(Configuration config) {
  return new DefaultSqlSessionFactory(config);
}

创建SqlSession

mybatis-config.xml → SqlSessionFactoryBuilder → SqlSessionFactory → SqlSessionFactory.open

→ Configuration → Environment

→ DataSource → Transaction

→ Transaction → Executor → SqlSession

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  Transaction tx = null;
  try {
    final Environment environment = configuration.getEnvironment();
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    final Executor executor = configuration.newExecutor(tx, execType);
    return new DefaultSqlSession(configuration, executor, autoCommit);
  } catch (Exception e) {
    closeTransaction(tx); // may have fetched a connection so lets call close()
    throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

执行过程

Mapper.xml中的sql语句在解析过程中会被封装成MappedStatement,以Map的形式存放在Configuration中,statementId = namespace + sqlId。

→ SqlSession → 调用select/update/insert/delete → 传入statementId引用对应的sql语句要执行的sql对应的MappedStatement

→ 将MappedStatement交给Exeuctor去执行 → Executor内部提供了query/update等方法,SqlSession中的方法都是通过Executor支持的

→ 从MappedStatement中拿到BoundSql,即包含了参数的sql

→ 根据sql、参数、分页参数创建一个一级缓存(当前连接内的)的CacheKey

→ 根据CacheKey从本地缓存中获取数据,如果没有获取到再查询数据库

→ 根据各种参数创建一个StatementHandler,由StatementHandler全权负责sql语句的执行

→ 预处理:获取数据库连接(transaction.getConnection())、设置参数(ParameterHandler/TypeHandler)

→ 执行:通过PreparedStatement的execute方法向数据库发送请求,并获取结果

→ 处理结果集:ResultHandler/TypeHandler

→ 如果执行update,本地缓存会被清空,即使后续还有select操作,本地缓存也会被清空

//DefaultSqlSession.java
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
  try {
    MappedStatement ms = configuration.getMappedStatement(statement);
    return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

//BaseExecutor
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  //获取BoundSql,包含了sql和参数
  BoundSql boundSql = ms.getBoundSql(parameter);
  //创建一级缓存的cache key
  CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
  return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  if (queryStack == 0 && ms.isFlushCacheRequired()) {
    //如果当前sql语句需要刷新缓存,就清空本地缓存      
    clearLocalCache();
  }
  List<E> list;
  try {
    queryStack++;
    //尝试从本地缓存中获取
    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    if (list != null) {
      handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    } else {
      //如果本地缓存没有就尝试从数据库查找  
      list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
  } finally {
    queryStack--;
  }
  if (queryStack == 0) {
    for (DeferredLoad deferredLoad : deferredLoads) {
      deferredLoad.load();
    }
    // issue #601
    deferredLoads.clear();
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
      // issue #482
      clearLocalCache();
    }
  }
  return list;
}

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  List<E> list;
  localCache.putObject(key, EXECUTION_PLACEHOLDER);
  try {
    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
  } finally {
    localCache.removeObject(key);
  }
  localCache.putObject(key, list);
  if (ms.getStatementType() == StatementType.CALLABLE) {
    localOutputParameterCache.putObject(key, parameter);
  }
  return list;
}

//SimpleExecutor
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
  Statement stmt = null;
  try {
    Configuration configuration = ms.getConfiguration();
    //创建StatementHandler
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    //执行prepre,获取连接、设置参数
    stmt = prepareStatement(handler, ms.getStatementLog());
    //执行
    return handler.query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
  Statement stmt;
  Connection connection = getConnection(statementLog);
  stmt = handler.prepare(connection, transaction.getTimeout());
  handler.parameterize(stmt);
  return stmt;
}

缓存原理

mybatis缓存分为一级缓存和二级缓存,一级缓存的作用范围是SqlSession,二级缓存的作用范围是Application。

一级缓存

作用范围、存储结构、刷新时机、清空时机、CacheKey包含的内容。

一级缓存默认开启,包含了Executor内部。

public abstract class BaseExecutor implements Executor {
    protected PerpetualCache localCache;
    protected BaseExecutor(Configuration configuration, Transaction transaction) {
        this.localCache = new PerpetualCache("LocalCache");   
    }
}

PerPetualCache实际上就是一个HashMap,由于SqlSession本身就不是线程安全的,所以PerPetualCache也不需要考虑线程安全问题。

public class PerpetualCache implements Cache {
    private final String id;
    private final Map<Object, Object> cache = new HashMap<>();
}

在执行查询时,如果本地缓存没有,就会从数据库查询,然后放入本地缓存,但当前sql上如果配置了flushCache=true,那么每次执行查询时都会先清除本地缓存。

  • 配置了flushCache=true
  • SqlSession被SqlSession.close
  • 执行更新操作
  • 执行SqlSession.clearCache()

CacheKey对象是缓存的key值,CacheKey中包含了:

  • sql
  • 分页参数
  • 普通参数
  • statementId
  • environmentId
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  CacheKey cacheKey = new CacheKey();
  //statement Id
  cacheKey.update(ms.getId());
  //分页参数
  cacheKey.update(rowBounds.getOffset());
  cacheKey.update(rowBounds.getLimit());
  //sql语句
  cacheKey.update(boundSql.getSql());
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
  // mimic DefaultParameterHandler logic
  //具体的查询参数
  for (ParameterMapping parameterMapping : parameterMappings) {
    if (parameterMapping.getMode() != ParameterMode.OUT) {
      Object value;
      String propertyName = parameterMapping.getProperty();
      if (boundSql.hasAdditionalParameter(propertyName)) {
        value = boundSql.getAdditionalParameter(propertyName);
      } else if (parameterObject == null) {
        value = null;
      } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
        value = parameterObject;
      } else {
        MetaObject metaObject = configuration.newMetaObject(parameterObject);
        value = metaObject.getValue(propertyName);
      }
      cacheKey.update(value);
    }
  }
  //environmentId
  if (configuration.getEnvironment() != null) {
    // issue #176
    cacheKey.update(configuration.getEnvironment().getId());
  }
  return cacheKey;
}

CacheKey的update方法是将该值作为计算CacheKey校验和的一部分。

public void update(Object object) {
  //得到传入的对象的hashCode  
  int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
  //参与计算校验和的对象的个数
  count++;
  //累加计算CacheKey的校验和
  checksum += baseHashCode;
  baseHashCode *= count;
  hashcode = multiplier * hashcode + baseHashCode;
  updateList.add(object);
}

二级缓存

开启条件:

<seting cacheEnable="true">
<cache/>  <cache-ref namespace=""/>  <cache class="XxxClass">   
<select useCache="true"/>
<cache
  #淘汰策略,LRU、FIFO、SOFT、WEAK
  eviction="FIFO"
  #刷新频率
  flushInterval="60000"
  #缓存对象的个数
  size="512"
  #是否为只读
  readOnly="true"/>

原理和实现类:

二级缓存是应用级别的,mybatis内部实现了四个种类的缓存实现:LRU、FIFO、SOFT、WEAK,也可以基于第三方的缓存工具来作为二级缓存。

二级缓存的实现基础是被装饰的Executor → CachingExecutor 和 Cache接口

→ CachingExecutor 内部持有了一个Cache接口的实现,查询数据时先从这个Cache中找,如果找不到在交给BaseExecutor

→ Cache接口是mybatis的总的Cache接口,一级缓存和二级缓存都实现了该接口,对于二级缓存,有以下几种实现:

→ FifoCache FIFO淘汰策略

→ LruCache LRU淘汰策略

→ SoftCache 类似jvm的软引用淘汰策略,即当jvm内存不够用导致垃圾回收时,会被清除

→ WeakCache 类似jvm的若引用淘汰策略,只要发生jvm垃圾回收时,会被清除

→ BlockingCache 多个线程同时查询同一个key时,如果缓存中没有,则只能由一个线程去查询数据库,其余等待,类似于可以防止缓存穿透。

→ LogingCache 实现了在Debug级别下输出缓存命中日志

→ ScheduledCache 增加了基于刷新时间的缓存清除功能,flushInterval

→ SerializedCache 实现了对value的序列化

→ SynchronizedCache 通过在方法上增加synchronized关键字实现了同步功能

→ TransactionalCache 实现了事务功能,commit时加入缓存,rollback时从缓存中移除。

二级缓存最底层实际上还是PerpetualCache,只是在其基础上增加了上面的一系列的装饰类。

默认用的驱逐方式是LruCache,然后再根据配置,再次基础上增加一系列的装饰类。

//XMLMapperBuilder
private void cacheElement(XNode context) {
  if (context != null) {
    String type = context.getStringAttribute("type", "PERPETUAL");
    Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
    String eviction = context.getStringAttribute("eviction", "LRU");
    Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
    Long flushInterval = context.getLongAttribute("flushInterval");
    Integer size = context.getIntAttribute("size");
    boolean readWrite = !context.getBooleanAttribute("readOnly", false);
    boolean blocking = context.getBooleanAttribute("blocking", false);
    Properties props = context.getChildrenAsProperties();
    builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
  }
}

public Cache useNewCache(Class<? extends Cache> typeClass,
    Class<? extends Cache> evictionClass,
    Long flushInterval,
    Integer size,
    boolean readWrite,
    boolean blocking,
    Properties props) {
  Cache cache = new CacheBuilder(currentNamespace)
      .implementation(valueOrDefault(typeClass, PerpetualCache.class))
      .addDecorator(valueOrDefault(evictionClass, LruCache.class))
      .clearInterval(flushInterval)
      .size(size)
      .readWrite(readWrite)
      .blocking(blocking)
      .properties(props)
      .build();
  configuration.addCache(cache);
  currentCache = cache;
  return cache;
}

public Cache build() {
  setDefaultImplementations();
  Cache cache = newBaseCacheInstance(implementation, id);
  setCacheProperties(cache);
  // issue #352, do not apply decorators to custom caches
  if (PerpetualCache.class.equals(cache.getClass())) {
    for (Class<? extends Cache> decorator : decorators) {
      cache = newCacheDecoratorInstance(decorator, cache);
      setCacheProperties(cache);
    }
    //设置标准的装饰器
    cache = setStandardDecorators(cache);
  } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
    cache = new LoggingCache(cache);
  }
  return cache;
}

//根据配置的属性来设置装饰器
private Cache setStandardDecorators(Cache cache) {
  try {
    MetaObject metaCache = SystemMetaObject.forObject(cache);
    if (size != null && metaCache.hasSetter("size")) {
      metaCache.setValue("size", size);
    }
    //设置了定时清除
    if (clearInterval != null) {
      cache = new ScheduledCache(cache);
      ((ScheduledCache) cache).setClearInterval(clearInterval);
    }
    
    //设置成了读写缓存,需要对操作进行同步
    if (readWrite) {
      cache = new SerializedCache(cache);
    }
    //增加日志输出装饰
    cache = new LoggingCache(cache);
    //增加序列化装饰
    cache = new SynchronizedCache(cache);
    //设置blocking=true,增加BlockingCache,此时多个线程对同一个key进行查询时,只会有一个
    //线程区查询数据库,其余的会进行等待
    if (blocking) {
      cache = new BlockingCache(cache);
    }
    return cache;
  } catch (Exception e) {
    throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
  }
}

实例:

//启动5个线程执行同一个sql
for (int i = 0;i < 5;i ++) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            SqlSession sqlSession = sqlSessionFactory.openSession();
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            System.out.println(userMapper.selectUser(1));
            System.out.println(users);
            sqlSession.close();
        }
    }).start();
}


<mapper namespace="com.mybatis.test.mapper.UserMapper2">
    <cache blocking="true"/>
    <select id="selectUsers" resultType="User" parameterType="int">
        select * from user where id = #{id};
    </select>
</mapper>

//从结果中看,只进行了一次数据库查询,如果不设置blocking=true,会进行5次数据库查询,
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6d8ae5f8]
==>  Preparing: select * from user where id = ?
==> Parameters: 1(Integer)
==> Parameters: 1(Integer)
==> Parameters: 1(Integer)
==> Parameters: 1(Integer)
==> Parameters: 1(Integer)
<==    Columns: id, name, age, sex
<==    Columns: id, name, age, sex
<==    Columns: id, name, age, sex
<==    Columns: id, name, age, sex
<==    Columns: id, name, age, sex
<==        Row: 1, zhangsan, 10, male
<==        Row: 1, zhangsan, 10, male
<==        Row: 1, zhangsan, 10, male
<==        Row: 1, zhangsan, 10, male
<==        Row: 1, zhangsan, 10, male
getNullableResult
getNullableResult
getNullableResult
getNullableResult
getNullableResult
<==      Total: 1
<==      Total: 1
<==      Total: 1
<==      Total: 1
<==      Total: 1
{sex=male, name=zhangsan, id=1, age=10}
{sex=male, name=zhangsan, id=1, age=10}
{sex=male, name=zhangsan, id=1, age=10}
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2e00b060]
{sex=male, name=zhangsan, id=1, age=10}
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4993cff4]
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@19f1af7b]
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6d8ae5f8]
{sex=male, name=zhangsan, id=1, age=10}
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@21be90cd]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2e00b060]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@19f1af7b]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4993cff4]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6d8ae5f8]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@21be90cd]

没有配置bocking=true时的结果,执行了5次查询

Created connection 1028948052.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3d548054]
Created connection 734471668.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2bc725f4]
==>  Preparing: select * from user where id = ?
==>  Preparing: select * from user where id = ?
Created connection 1520525687.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5aa16177]
==>  Preparing: select * from user where id = ?
Created connection 1857298393.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6eb41fd9]
==>  Preparing: select * from user where id = ?
Created connection 910917581.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@364b7fcd]
==>  Preparing: select * from user where id = ?
==> Parameters: 1(Integer)
==> Parameters: 1(Integer)
==> Parameters: 1(Integer)
==> Parameters: 1(Integer)
==> Parameters: 1(Integer)
<==    Columns: id, name, age, sex
<==    Columns: id, name, age, sex
<==    Columns: id, name, age, sex
<==    Columns: id, name, age, sex
<==    Columns: id, name, age, sex
<==        Row: 1, zhangsan, 10, male
<==        Row: 1, zhangsan, 10, male
<==        Row: 1, zhangsan, 10, male
<==        Row: 1, zhangsan, 10, male
<==        Row: 1, zhangsan, 10, male
getNullableResult
getNullableResult
getNullableResult
getNullableResult
getNullableResult
<==      Total: 1
<==      Total: 1
<==      Total: 1
<==      Total: 1
<==      Total: 1
{sex=male, name=zhangsan, id=1, age=10}
{sex=male, name=zhangsan, id=1, age=10}
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5aa16177]
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2bc725f4]
{sex=male, name=zhangsan, id=1, age=10}
{sex=male, name=zhangsan, id=1, age=10}
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6eb41fd9]
{sex=male, name=zhangsan, id=1, age=10}
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@364b7fcd]
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3d548054]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3d548054]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2bc725f4]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6eb41fd9]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@364b7fcd]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5aa16177]

数据源

mybatis的数据源在原生模式下通过environment配置,有三种选择

  • POOLED 池化连接池
  • UNPOOLED 非池化连接池,类似jdbc
  • JNDI 基于jndi的连接池

DataSourceFactory

→ JndiDataSourceFactory

→ PooledDataSourceFactory

→ PooledDataSource

→ UnpooledDataSourceFactory

→ UnpooledDataSource

但一般在使用mybatis的时候,会把连接池和事务管理托管给spring,这里就涉及到spring和mybatis的集成包 mybatis-spring:2.0.6,在这个包中提供了一个SqlSessionFactoryBean,内部包含了Mybatis的组件,spring通过注册SqlSessionFactoryBean集成mybatis。

public SqlSessionFactoryBean writeSqlSessionFactoryBean() throws Exception {
    SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    sqlSessionFactoryBean.setDataSource(dataSource());
    sqlSessionFactoryBean.setMapperLocations(new FileSystemResource("/Users/dongzhang/develop/workspace/echo20222022/open_src/spring_cloud_demo/cloud-user/src/main/resources/mappers/UserMapper.xml"));
    return sqlSessionFactoryBean;
}

SqlSessionFactoryBean实现了InitializingBean,当spring容器启动时会调用afterPropertiesSet方法,该方法在spring体系下读取配置,然后构造mybatis的Configuration,用这个Configuration对象创建一个SqlSessionFactory。

同时,这个包还提供了@MapperScanner,在spring启动的时候将指定包下的类的BeanDefination注册到了Spring中。

//SqlSessionFactoryBean
@Override
public void afterPropertiesSet() throws Exception {
  notNull(dataSource, "Property 'dataSource' is required");
  notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
  state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
      "Property 'configuration' and 'configLocation' can not specified with together");

  this.sqlSessionFactory = buildSqlSessionFactory();
}

protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
    return this.sqlSessionFactoryBuilder.build(targetConfiguration);
}

public class MapperScannerConfigurer
    implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
        @Override
        public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
          if (this.processPropertyPlaceHolders) {
            processPropertyPlaceHolders();
          }
        
          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.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
          if (StringUtils.hasText(lazyInitialization)) {
            scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
          }
          if (StringUtils.hasText(defaultScope)) {
            scanner.setDefaultScope(defaultScope);
          }
          scanner.registerFilters();
          //扫描指定的包,向spring中注册对应接口的BeanDefinition
          scanner.scan(
              StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
        }       
 }

另外,在springboot环境下使用mybatis时,有一个mybatis-spring-boot-starter,引入了MybatisAutoConfiguration自动装配类,在这个类中注册了SqlSessionFactoryBean,会自动注入spring环境中的DataSource,然后SqlSessionFactoryBean同时还会自动扫描Mapper并注册到spring中。

@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
    factory.setDataSource(dataSource);
    return factory.getObject();
}

mybatis的解析environment标签的逻辑:

//XMLConfigBuilder
//解析environment标签
private void environmentsElement(XNode context) throws Exception {
  if (context != null) {
    if (environment == null) {
      environment = context.getStringAttribute("default");
    }
    for (XNode child : context.getChildren()) {
      String id = child.getStringAttribute("id");
      if (isSpecifiedEnvironment(id)) {
        TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
        //解析数据源类型,并根据类型返回一个工厂对象
        DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
        DataSource dataSource = dsFactory.getDataSource();
        Environment.Builder environmentBuilder = new Environment.Builder(id)
            .transactionFactory(txFactory)
            .dataSource(dataSource);
        configuration.setEnvironment(environmentBuilder.build());
        break;
      }
    }
  }
}

private DataSourceFactory dataSourceElement(XNode context) throws Exception {
  if (context != null) {
    String type = context.getStringAttribute("type");
    Properties props = context.getChildrenAsProperties();
    DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
    factory.setProperties(props);
    return factory;
  }
  throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}

日志

mybatis为了适配不同的日志框架,在内部引入了一个统一的Log接口和适配各种日志框架的适配器(适配器设计模式),可以通过setting进行配置

<setting name="logImpl" value="SLF4J" />

可选择值:SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING

统一接口:Log、LogFactory

具体实现:Slf4jLoggerImpl、StdOutImpl、BaseJdbcLogger、JakartaCommonsLoggingImpl等。

事务

mybatis的事务通过enverment配置,有两种类型,JDBC和MANAGED,JDBC类型时通过jdbc的Connection来管理事务,MANAGED类型不会做任何事情,一般用在和其他框架集成。

public class ManagedTransaction implements Transaction { 
    @Override
    public Connection getConnection() throws SQLException {
      if (this.connection == null) {
        openConnection();
      }
      return this.connection;
    }
    
    //提交和回滚没有做任何事情
    @Override
    public void commit() throws SQLException {
      // Does nothing
    }
    @Override
    public void rollback() throws SQLException {
      // Does nothing
    }
}


public class JdbcTransaction implements Transaction {
    @Override
    public Connection getConnection() throws SQLException {
      if (connection == null) {
        openConnection();
      }
      return connection;
    }
    //通过connection进行提交
    @Override
    public void commit() throws SQLException {
      if (connection != null && !connection.getAutoCommit()) {
        if (log.isDebugEnabled()) {
          log.debug("Committing JDBC Connection [" + connection + "]");
        }
        connection.commit();
      }
    }
    //通过connection进行回滚    
    @Override
    public void rollback() throws SQLException {
      if (connection != null && !connection.getAutoCommit()) {
        if (log.isDebugEnabled()) {
          log.debug("Rolling back JDBC Connection [" + connection + "]");
        }
        connection.rollback();
      }
    }
    protected void openConnection() throws SQLException {
        this.connection = this.dataSource.getConnection();
        if (this.level != null) {
            this.connection.setTransactionIsolation(this.level.getLevel());
        }
        this.setDesiredAutoCommit(this.autoCommit);
    }
}

当跟Spring进行集成时,创建SqlSessionFactoryBean时创建的是:

public class SpringManagedTransactionFactory implements TransactionFactory {
  @Override
  public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
    return new SpringManagedTransaction(dataSource);
  }
  @Override
  public Transaction newTransaction(Connection conn) {
    throw new UnsupportedOperationException("New Spring transactions require a DataSource");
  }
  @Override
  public void setProperties(Properties props) {
    // not needed in this version
  }
}

public class SpringManagedTransaction implements Transaction {
    @Override
    public Connection getConnection() throws SQLException {
      if (this.connection == null) {
        openConnection();
      }
      return this.connection;
    }
    @Override
    public void commit() throws SQLException {
      if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
        LOGGER.debug(() -> "Committing JDBC Connection [" + this.connection + "]");
        this.connection.commit();
      }
    }
    @Override
    public void rollback() throws SQLException {
      if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
        LOGGER.debug(() -> "Rolling back JDBC Connection [" + this.connection + "]");
        this.connection.rollback();
      }
    }
    
    private void openConnection() throws SQLException {
      //从线程池中获取连接  
      this.connection = DataSourceUtils.getConnection(this.dataSource);
      this.autoCommit = this.connection.getAutoCommit();
      this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
    
      LOGGER.debug(() -> "JDBC Connection [" + this.connection + "] will"
          + (this.isConnectionTransactional ? " " : " not ") + "be managed by Spring");
    }
}

设计模式

  • 创建者模式
  • 工厂方法模式
  • 装饰模式
  • 适配器模式
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

echo20222022

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值