Loading...
墨滴

jrh

2021/11/03  阅读:49  主题:橙心

Java 面试八股文之中间件篇(一)

前言

这是系列文章【 Java 面试八股文】中间件篇的第一期。

【 Java 面试八股文】系列会陆续更新 Java 面试中的高频问题,旨在从问题出发,带你理解 Java 基础,数据结构与算法,数据库,常用框架等。该系列前几期文章可以通过下方给出的链接进行查看~

按照惯例——首先要做几点说明:

  1. 【 Java 面试八股文】中的面试题来源于社区论坛,书籍等资源;感谢使我读到这些宝贵面经的作者们。
  2. 对于【 Java 面试八股文】中的每个问题,我都会尽可能地写出我自己认为的“完美解答”。但是毕竟我的身份不是一个“真理持有者”,只是一个秉承着开源分享精神的 “knowledge transmitter” & 菜鸡,所以,如果这些答案出现了错误,可以留言写出你认为更好的解答,并指正我。非常感谢您的分享。
  3. 知识在于“融释贯通”,而非“死记硬背”;现在市面上固然有很多类似于“Java 面试必考 300 题” 这类的文章,但是普遍上都是糟粕,仅讲述其果,而不追其源;希望我的【 Java 面试八股文】可以让你知其然,且知其所以然~

那么,废话不多说,我们正式开始吧!

往期文章

MyBatis 篇

1、什么是 ORM 框架?


1. 从 JDBC 到 ORM

我们开发的 Web 应用不可避免地会涉及到数据的管理操作,也就不可避免地会使用到数据库。

目前流行的数据库有很多,譬如 MySQL,Oracle,Postgre SQL 等等,为了让 Java 可以操作不同的数据库,SUN 提供了一套访问数据库的标准接口,然后让各个数据库厂商来遵循并实现这套规范,而这套规范就是 JDBC(Java Database Connectivity)。

传统的 JDBC 开发存在着一些问题:

  1. 严重影响了开发效率

    使用过 JDBC 编程的童鞋就知道,JDBC 开发的套路几乎是固定的:我们需要使用 DriverManager 来加载驱动,使用 Connection 创建与数据库的连接,然后再创建 Statement 语句执行 sql,最后返回 ResultSet 结果集。并且我们还需要依次关闭这些资源。虽然我们可以将创建连接,关闭资源这些步骤封装成一个工具类,但我们仍然希望有一种工具可以帮助我们做掉这些业务代码之外的事情。

  2. Sql 语句与 Java 代码产生的耦合问题

    使用 JDBC 编程还有一个令人诟病之处,那就是 sql 语句与 Java 代码产生了耦合。在我们的实际项目中,sql 语句一旦要进行修改,那我们就需要重新修改 Java 代码,然后重新编译,重新发布,维护起来十分困难。

  3. 数据库的移植性

    如果我们想从 MySQL 数据库迁移到 Oracle 上,那么之前使用的 sql 语句就要全部进行修改。

为了解决传统的 JDBC 编程带来的问题,ORM 框架应运而生。首先解释一下,什么是 ORM?

ORM(Object Relational Mapping)即:对象关系映射。它是一种将数据库映射为 Java 对象的思想与概念:

简单来说,ORM 通过描述对象与数据库之间的关系,将程序中的对象自动持久化到关系型数据库中。对于数据库的操作,我们将无需再去编写原生的 sql 语句,取而代之的是使用基于面向对象的思想去编写类,调用的方法等等,ORM 框架会将其转换为 sql 语句,交给数据库执行。

2. MyBatis,半自动的 ORM?

在上文中,我提到了,ORM 框架的一个特点是自动生成 sql 语句。在这一点上,ORM 框架解决了 JDBC 数据库迁移需要重写 sql 的问题。

而 MyBatis 还是需要手动编写 sql 的,所以我们通常把 MyBatis 称作为一个半自动的 ORM 框架。

而 Hibernate 才是一个真正的 ORM 框架,或者说它是一个全自动的 ORM 框架。Hibernate 是 JPA 规范的一种实现。它对 JDBC 进行了轻量级的封装,可以自动生成 SQL 语句,并自动执行。使 Java 程序员可以随心所欲地使用面向对象编程的思想来操作数据库。

不过相比于 Hibernate,MyBatis 也有它的优点。Hibernate 属于一个重量级框架,学习的成本效高。如果我们编写一些复杂 sql,Hibernate 则丧失了一定的灵活性,并且如果我们对 sql 的性能优化方面有很大的要求,通常也会使用 MyBatis 框架。

2、MyBatis 的工作原理?


1. MyBatis 的核心组件

我们先来简单地认识一下 MyBatis 中的核心组件:

1. SqlSessionFactoryBuilder

通过名字,我们就知道这是一个使用了 Builder 模式的类,其作用为创建一个 SqlSessionFactory。

2. Configuration

SqlSessionFactoryBuilder 会创建一个 XMLConfigBuilder 对象,XMLConfigBuilder 则会对 mybatis-config.xml 文件进行解析,然后将解析后的信息存放在 Configuration 对象中。Configuration 用于描述 MyBatis 的主配置信息。当其他组件想要获取到配置信息时,将直接通过 Configuration 对象进行获取。

3. MappedStatement

MappedStatement 对象是对 Mapper 中映射信息的一个封装,它可以是对 XML 文件中 select/update/delete/insert 等标签的信息的封装,也可以是对 @Select/@Update/@Delete/@Insert 注解的信息的封装。

4. SqlSessionFactory

创建 SqlSession 的工厂类。

5. SqlSession

SqlSession 是 MyBatis 中应用程序与持久层之间进行交互操作的会话对象,用于完成数据库的 CRUD 操作。

6. Executor

Executor 是 MyBatis 的 sql 执行器,每一个 SqlSession 对象都拥有一个 Executor,MyBatis 中对数据库所有的增删改查操作都是由该组件完成的。

7. StatementHandler

StatementHandler 封装了对 JDBC Statement 对象的操作,譬如为 Statement 对象设置参数,调用 Statement 接口提供的方法与数据库进行交互,等等。

8. ParameterHandler

当 MyBatis 使用的 Statement 类型为 CallableStatement 或 PreparedStatement 时,ParameterHandler 会为 Statement 对象的参数占位符设置值。

9. ResultSetHandler

ResultSetHandler 封装了对 JDBC 中的 ResultSet 对象的操作,它的作用就是将 Statement 执行后的结果集,按照 Mapper 文件中配置的 resultType 或 resultMap 来封装成对应的 Java 对象。

10. TypeHandler

TypeHandler 是 MyBatis 中的类型处理器,类型处理器是做什么的呢?我们需要将数据库中的类型和 Java 中的字段类型进行转换,在 MyBatis 中就是使用 TypeHandler 来实现的。举个比较简单的例子,我创建了一个博客表,表中的创建时间和修改时间用 VARCHAR 类型,但是在我的 POJO 对象中,创建时间和修改时间的类型是 Date,这样我在向数据库插入数据时,需要将日期类型转换成 VARCHAR,而从数据库中查询出的结果又需要将 VARCHAR 类型转换成 Date。TypeHandler 的作用就是用来处理 javaType 和 jdbcType 之间的映射。

2. MyBatis 的工作原理

看过 MyBatis 的这些重要组件后,我们来看一下 MyBatis 的工作原理,或者说是 MyBatis 主要的工作流程:

  1. 读取 mybatis-config.xml 全局配置文件,将配置文件中的信息封装为 Configuration 全局对象
  2. 加载 Mapper,将 Mapper 里面的 select/update/delete/insert 标签封装为一个一个的 MappedStatement 对象,并存储到 Configuration 中
  3. 通过 SqlSessionFactoryBuilder 构建 SqlSessionFactory
  4. 通过 SqlSessionFactory 创建与数据库持久层的会话 SqlSession
  5. SqlSession 对象通过调用 Executor 执行器执行 sql 语句
  6. Executor 执行器会拿着对应的 MappedStatement 对象,调用 StatementHandler 的 update/query/batch/prepare 等方法,而 StatementHandler 则是对 MyBatis 内置 JDBC 的 Statement 对象的调用
  7. 如果需要执行一条预编译的 sql,StatementHandler 会调用 prepare 方法对这条 sql 语句进行预编译
  8. ParameterHandler 会根据 TypeHandler 提供的参数设置规则,调用 setParameters 方法为 Statement 的参数占位符设置值
  9. 设置值以后,StatemenHandler 便可以执行这条 Statement 语句了,执行完毕后,JDBC 会产生一个 ResultSet 结果集,ResultSetHandler 会根据 TypeHandler 提供的结果类型的转换规则,调用 handleResult 方法,将 ResultSet 封装为一个 Java 对象,并返回

以上内容便是 MyBatis 主要的工作流程,大家可以将本问回答中,我给出的那张图与这个流程步骤相结合,加深一下理解~

3、MyBatis 使用相关


1. #{}${} 有什么区别?

#{}${} 语法都是为了在我们的 sql 语句中动态传递参数而存在的。

<select id="getUserById" parameterType="int" resultType="com.github.test.entity.User">
    select * from user where id = #{id}
</select>

不同之处在于,MyBatis 在处理 #{} 时,会将 sql 中的 #{} 替换为 ?,然后调用底层 JDBC 的 PreparedStatement 的 setXXX 方法来进行赋值,这样做可以有效地防止 sql 注入,提供系统的安全性;而 ${} 方式则相当于是字符串替换,没有 PreparedStatement 的预编译处理,无法防御 sql 注入。

2. 如何在 mapper 中传递多个参数?

常见的做法有以下三种:

  1. @Param 注解传参
  2. 传递一个 map
  3. 传递一个 Java Bean 实体

第一种:@Param 注解传参

public User selectUser(@Param("userName") String name, @Param("deptId") int deptId);
<select id="selectUser" resultMap="UserResultMap">
    select * from user
    where user_name = #{userName} and dept_id = #{deptId}
</select>

第二种:传递一个 map

public User selectUser(Map<String, Object> params);
<select id="selectUser" parameterType="java.util.Map" resultMap="UserResultMap">
    select * from user
    where user_name = #{userName} and dept_id = #{deptId}
</select>

此时,#{} 里的名称为传入 map 的 key 名称。

第三种:传递一个 Java Bean 实体

public User selectuser(User user);
<select id="selectUser" parameterType="com.github.test.entity.User" resultMap="UserResultMap">
    select * from user
    where user_name = #{name} and dept_id = #{deptId}
</select>

此时,#{} 里的名称对应的是 User 类的成员属性名称。

3. 使用过哪些 MyBatis 动态 sql 标签?它们的作用?

动态 sql 是 MyBatis 最强大的功能之一,常用的标签有以下几种:

  • <if>
  • <choose><when><otherwise>
  • <foreach>

if

<select id="findActiveBlogWithTitleLike"
     resultType="Blog">

  SELECT * FROM BLOG
  WHERE state = ‘ACTIVE’
  <if test="title != null">
    AND title like #{title}
  </if>
</select>

choose,when,otherwise

有时候,我们不想使用所有的条件,只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了 <choose> 标签,它的作用类似于 Java 的 Switch 语句:

<select id="findActiveBlogLike"
     resultType="Blog">

  SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  <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>
</select>

foreach

动态 sql 另一个常见的使用场景是对集合进行遍历(尤其是在构建 IN 条件语句的时候),此时我们可以使用 <foreach> 标签构建我们的动态 sql:

<select id="selectPostIn" resultType="domain.blog.Post">
  SELECT *
  FROM POST P
  WHERE ID in
  <foreach item="item" index="index" collection="list"
      open="(" separator="," close=")">

        #{item}
  </foreach>
</select>

<foreach> 标签中,item 为本次迭代获取的元素,如果 collection 为 map,则代表 map 中的 value;index 表示当前迭代的索引,如果 collection 为 map,则代表 map 中的key;collection 为迭代集合的名称,它可以是一个 list,set,map,或者是数组;open 表示 <foreach> 标签中的语句以什么作为开始,通常为左括弧 (;close 表示 <foreach> 标签中的语句以什么作为结束,通常为右括弧 );separator 表示每次迭代后给 sql 语句加上的指定字符,通常为逗号 ,

4. 如何解决 MyBatis 中实体类和数据库字段名不匹配的问题?

假设我们数据库中 User 表的字段名如下:

user_id
user_name
hashed_password

对应的 POJO User 类如下:

package com.github.test.entity;

public class User {
    private int id;
    private String name;
    private String password;
    // getter,setter ... ...
}

当实体类和数据库字段名不匹配时,有两种做法:

  1. 在 sql 语句中定义字段名的别名
  2. 使用 <resultMap> 来映射字段名和实体类属性名的对应关系

第一种做法:在 sql 语句中定义字段名的别名

<select id="selectUsers" resultType="com.github.test.entity.User">
  select
    user_id             as "id",
    user_name           as "name",
    hashed_password     as "password"
  from user
  where id = #{id}
</select>

第二种做法:使用 <resultMap> 来映射字段名和实体类属性名的对应关系

<select id="selectUsers" resultMap="userResultMap">
  select user_id, user_name, hashed_password
  from user
  where id = #{id}
</select>
<resultMap id="userResultMap" type="com.github.test.entity.User">
  <id property="id" column="user_id" />
  <result property="name" column="user_name"/>
  <result property="password" column="hashed_password"/>
</resultMap>

5. MyBatis 中如何实现一对一,一对多查询?

举一个比较经典的例子:用户表与订单表。

我们知道,一个商品订单只能对应一个用户,当我们以订单表为主表关联用户表进行查询时,是典型的一对一查询:

<resultMap type="Order" id="orderList">
  <id property="id" column="id"/>
  <result property="number" column="number"/>
  <association property="user" javaType="User">  
    <id property="id" column="id"/>
    <result property="username" column="user_name"/>
  </association>  
</resultMap>
<select id="getOrderList" resultMap="orderList">
  select
    o.id,
    o.user_id,
    o.number,
    u.user_name
  from order o
  left join user u
  on o.user_id = u.id
</select>  

我们可以通过在 resultMap 里配置 association 节点的方式完成一对一的联合查询。

而一个用户则可以对应多个商品订单,所以当我们以用户表为主表关联订单表进行查询时,是典型的一对多查询:

<resultMap type="User" id="listUser">
  <id property="id" column="user_id"/>
  <result property="username" column="user_name"/>
  <collection property="order" ofType="Order">
    <id property="id" column="id"/>
    <result property="number" column="number"/>
  </collection>  
</resultMap>
<select id="getUserList" resultMap="listUser">
  select 
    o.id,
    o.user_id,
    o.number,
    u.user_name
  from user u
  left join order o
  on o.user_id = u.id
</select>

我们可以通过在 resultMap 里配置 collection 节点的方式完成一对多的联合查询。

4、说一下 MyBatis 的缓存机制?


MyBatis 设有一级缓存与二级缓存,其目的就是为了提升查询效率与减少对数据库的压力。

一级缓存

MyBatis 中有一个 Cache 接口,该接口仅有一个默认的实现类——PerpetualCache。在 PerpetualCache 中有一个 map 用来缓存:

private Map<Object, Object> cache = new HashMap<Object, Object>();

MyBatis 的一级缓存是在会话(SqlSession)层面进行缓存的,它是一个本地缓存,且默认是开启的。

当我们使用 MyBatis 开启一次和数据库的会话时,便会在 SqlSession 对象中建立一个缓存,将每次查询到的结果缓存起来。如果在下一次查询时,发现之前有一个相同的查询,那么 MyBatis 便会直接从 SqlSession 的缓存中将结果返回给用户。

一级缓存的生命周期:

  • MyBatis 开启了一个数据库会话时,会创建一个 SqlSession 对象,SqlSession 对象持有一个 Executor 对象,Executor 对象则会持有一个 PerpetualCache 对象;当一个会话结束时,这些对象则全部都会被释放掉
  • SqlSession 调用了 close 方法时,会释放掉一级缓存 PerpetualCache,一级缓存将不可用
  • SqlSession 调用了 clearCache 方法时,会清空缓存,但是该对象仍然可用
  • SqlSession 执行更新操作时,会清空缓存,但是该对象仍然可用

二级缓存

一级缓存的作用域为会话,在有多个会话的情况下,会话之间的缓存不能共享。而 MyBatis 的二级缓存则是用来解决一级缓存不能跨会话共享的问题。

MyBatis 二级缓存默认也是采用 PerpetualCache 类的 map 进行存储,不过二级缓存的作用域为 namespace,这样就意味着二级缓存的作用范围更广,只要是同一个命名空间的查询,就可以用到二级缓存,且二级缓存可以被多个 SqlSession 所共享。

如上图所示,当用户执行一个查询时,MyBatis 首先会从二级缓存中进行获取,如果没有则再从一级缓存中获取。

MyBatis 使用了一个装饰器模式的类:CachingExecutor,在 CachingExecutor 的 query 方法中,首先会判断获取到的二级缓存是否为空,如果不为空再去判断从二级缓存中是否可以获取到结果,如果获取到了则直接返回给用户;如果没有则调用委托对象 delegate(真正的 Executor,譬如 SimpleExecutor) 来执行查询。接着就会再次走一级缓存的查询流程,最后将结果缓存起来,并返回给用户。

MyBatis 的二级缓存默认是没有开启的,我们需要在配置文件 application.properties 中指定 mybatis.configuration.cache-enabled=true,并且在 Mapper.xml 文件中,使用 <cache> 标签来开启二级缓存。MyBatis 的二级缓存一旦开启,其生命周期将伴随到整个应用结束,一旦我们对表进行更新操作时,都会刷新二级缓存,导致之前的缓存内容失效。所以,二级缓存适用于查询为主的应用,对于增删改比较频繁的应用来说,二级缓存的意义并不大,反而会导致性能下降。

总结

在今天的文章中,我总结了 MyBatis 这个半自动 ORM 框架的常考面试题。

我认为,MyBatis 主要考查的还是应用方面,对于原理性的内容面试官应该不会问的太深,只要记住 MyBatis 的一些常用组件,以及 MyBatis 的工作原理即可~

好啦,至此为止,这篇文章就到这里了,感谢您的阅读与支持~~欢迎大家关注我的公众号,在这里希望你可以收获更多的知识,我们下一期再见!

jrh

2021/11/03  阅读:49  主题:橙心

作者介绍

jrh