vlambda博客
学习文章列表

MyBatis 延迟加载、一二级缓存、架构设计的面试题(常问,重点了解)

MyBatis 的面试题整理快接近尾声了,我对着问题写了 6 个 Demo,把 MyBatis 的源码翻了几遍,收获满满。不知道你看的有没有收获?可以在文末参与讨论。Let's go on...


1、MyBatis 的 SQL 执行日志如何开启?

这是之前遗留的一个问题。
在配置文件的  <setting>   标签上设置 logImpl 参数值(SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING),指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 

如 MyBatis 实现的标准输出的配置

<settings>
    <setting name="logImpl" value="STDOUT_LOGGING"></setting>
</settings>



2、MyBatis 中注册 Mapper 有哪些方式?

方式一:在配置文件 mybatis-config.xml 中添加 及其子标签 ,编写对应的 Mapper 接口与 XML

<mappers>
    <mapper resource="constxiong/mapper/UserMapper.xml"/>
</mappers>


方式二、硬编码方式在 configuration 对象中注册 Mapper 接口

//配置
Configuration configuration = new Configuration(environment);
//注册
configuration.addMapper(UserMapper.class);



3、MyBatis 如何支持延迟加载?现实原理是什么?

支持延迟加载的配置:
在配置文件的 标签内设置参数

  • lazyLoadingEnabled:延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态

  • aggressiveLazyLoading:开启时,任一方法的调用都会加载该对象的所有延迟加载属性。否则,每个延迟加载属性会按需加载

  • lazyLoadTriggerMethods:指定对象的哪些方法触发一次延迟加载

resultMap 中配置 <association> 或 <collection>

配置与测试示例

//配置文件
<settings>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="aggressiveLazyLoading" value="false"/>
    <setting name="lazyLoadTriggerMethods" value=""/>
</settings>


//Mapper xml
<select id="selectUserWithLazyInfo" resultMap="UserWithLazyInfo">
    select * from user where id = 1
</select>

<resultMap id="UserWithLazyInfo" type="constxiong.po.User">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <association property="info" javaType="constxiong.po.Info" select="constxiong.mapper.InfoMapper.selectInfoByUserId" column="id"/>
</resultMap>


//InfoMapper
public interface InfoMapper {
    @Select("select * from info where user_id = #{userId}")
    @Results(value = {@Result(column="user_id", property = "userId")})
    Info selectInfoByUserId(int userId);
}


//测试代码
System.out.println("------ selectUserWithLazyInfo ------");
User user = userMapper.selectUserWithLazyInfo();
System.out.println(user);
System.out.println(user.getInfo());


//打印 User 对象里的 Info 为空,使用 getInfo 能够查询对应的值
------ selectUserWithLazyInfo ------
User{id=1, name='ConstXiong1', mc='null', info=null, articles=null}
Info{userId=1, name=大熊}


实现原理:

支持延迟加载是通过字节码增强实现的,MyBatis 3.3 及以上默认使用了 javassist,3.3 以前使用 cglib 实现。

我本地用的 MyBatis 3.5.5,使用了 javassist 增强,核心源码如下

//DefaultResultSetHandler getRowValue 获取每条的查询数据,resultMap 中如果包含懒加载 rowValue 在 createResultObject 方法通过 javassist 代理增强
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
    //对象数据,通过 javassist 代理增强
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
    if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
        final MetaObject metaObject = configuration.newMetaObject(rowValue);
        boolean foundValues = this.useConstructorMappings;
        if (shouldApplyAutomaticMappings(resultMap, false)) {
            foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
        }
        //根据从数据库查询到的 resultSet,根据 resultMap 通过反射设置 rowValue 的值
        foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
        foundValues = lazyLoader.size() > 0 || foundValues;
        rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
    }
    return rowValue;
}

private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
    this.useConstructorMappings = false// reset previous mapping result
    final List<Class<?>> constructorArgTypes = new ArrayList<>();
    final List<Object> constructorArgs = new ArrayList<>();
    Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
    if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
        final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
        for (ResultMapping propertyMapping : propertyMappings) {
            //如果返回对象的属性中包含懒加载,使用 javassist 代理增强,当设置属性值时被代理到 JavassistProxyFactory 的 invoke 方法
            if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
                resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
                break;
            }
        }
    }
    this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
    return resultObject;
}
//JavassistProxyFactory 的 invoke 方法
public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
    final String methodName = method.getName();
    try {
        synchronized (lazyLoader) {
            if (WRITE_REPLACE_METHOD.equals(methodName)) {
                ...
            } else {
                if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
                    if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
                        lazyLoader.loadAll();
                    } else if (PropertyNamer.isSetter(methodName)) {
                        final String property = PropertyNamer.methodToProperty(methodName);
                        lazyLoader.remove(property);
                    } else if (PropertyNamer.isGetter(methodName)) {
                        //测试代码中 user.getInfo() 方法的调用,在此执行懒加载查询关联 SQL 设置 info 属性
                        final String property = PropertyNamer.methodToProperty(methodName);
                        if (lazyLoader.hasLoader(property)) {
                            lazyLoader.load(property);
                        }
                    }
                }
            }
        }
        return methodProxy.invoke(enhanced, args);
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
}


完整 Demo:

https://javanav.com/val/973ded541e9244aa8b3169b9fb869d60.html



4、MyBatis 中的本地缓存和二级缓存的作用是什么?怎么实现的?

本地缓存

作用:

SqlSession 级别的缓存,默认开启,在 MyBatis 配置文件中可以修改 MyBatis 文件中 <setting> 标签 localCacheScope 参数值改变缓存的作用域。statementId、boundSql.getSql() 执行 sql、查询参数、RowBounds 都相同,即认为是同一次查询,返回缓存值。


实现原理:

每个 SqlSession 对象包含一个 Executor 对象,Executor 对象中 localCache 属性使用 PerpetualCache 对象缓存查询数据;从源码中看 DefaultSqlSession 的 close、commit、rollback、insert、delete、update 相关的方法都会触发 BaseExecutor 对象清掉缓存。


二级缓存

作用:
MappedStatement 级别的缓存,默认不开启,可以在 Mapper xml 中通过 <cache> 标签开启 或者 MyBatis 文件中 <setting> 标签设置 cacheEnabled 参数为 true 全局开启 或者 mapper xml 配置文件中的 select 节点需要加上属性 useCache,在 SqlSession 关闭或提交之后才会生效。


开启二级缓存的默认作用摘自官网

  • 映射语句文件中的所有 select 语句的结果将会被缓存。

  • 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。

  • 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。

  • 缓存不会定时进行刷新(也就是说,没有刷新间隔)。

  • 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。

  • 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。



实现原理:

  • XMLMappedBuilder 解析 Mapper xml 中的 <cache>、<cache-ref> 标签

  • 通过 builderAssistant 对象 addMappedStatement 方法,设置 cache 信息到 MappedStatement 对象内

  • CachingExecutor 对象的 query 方法先 MappedStatement 对象中 getCache() 获取缓存 Cache 对象,如果没有查到则到 BaseExecutor 中查询,走本地缓存逻辑



5、MyBatis 的源码中的核心类有哪些?如何实现框架功能的?

  • Configuration: 配置类

  • Environment: 环境信息

  • SqlSessionFactoryBuilder: SqlSessionFactory 构造者类

  • SqlSessionFactory: SqlSession 工厂类

  • SqlSession: 执行 SQL 的一次会话

  • XMLConfigBuilder: MyBatis xml 配置文件构造者类

  • XMLMapperBuilder: Mapper xml 配置文件构造者类

  • MapperBuilderAssistant: Mapped 匹配信息构造者类,如构造添加MappedStatement

  • XMLStatementBuilder: Mapper xml 配置文件中 SQL 标签的构造者类,构造 MappedStatement

  • MappedStatement: 通过 Mapper xml 或注解,生成的 select|update|delete|insert Statement 的封装

  • MapperProxy: Mapper 接口的代理类

  • MapperMethod: Mapper 接口的方法,包含匹配的 SQL 执行种类和具体方法签名等信息

  • Executor: 执行器,是 MyBatis 调度的核心,负责 SQL 语句的生成和查询缓存的维护

  • BaseExecutor: SqlSession 中的基本数据库的增删改查的执行器,涉及本地缓存与数据库查询

  • CachingExecutor: 带缓存的执行器,涉及二级缓存,未命中走本地缓存逻辑

  • ResultMap: 返回值类型匹配的类

  • SqlSource: 负责根据用户传递的 parameterObject,动态地生成 SQL 语句,将信息封装到 BoundSql 对象中,并返回该对象

  • BoundSql: 动态生成的 SQL 语句以及相应的参数信息

  • StatementHandler: Statement 处理接口,封装 JDBC Statement 操作

  • ParameterHandler: 参数处理接口,负责对用户传递的参数转换成 JDBC Statement 所需要的参数

  • ResultSethandler: 执行结果处理接口

  • TypeHandler: 返回类型处理接口

框架如何实现,这个问题的细节就特别多了,画了一张我个人理解的图


到此,还剩 3 道题目
  • MyBatis 插件的运行原理是什么?如何编写一个自定义插件?

  • MyBatis 是如何被 Spring 集成的?

  • Spring 中如何配置 MyBatis?

留着下回分解...



留言

ConstXiong 发起了一个读者讨论 这篇编号为3、4、5的问题不好问答,但面试常问,可以重点了解一波。