【mybatis】

news2024/10/6 0:44:37

对象关系映射(Object Relational Mapping,简称ORM)模式是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术。
ORM框架是连接数据库的桥梁,只要提供了持久化类与表的映射关系,ORM框架在运行时就能参照映射文件的信息,把对象持久化到数据库中。

ORM解决的问题

1、避免了大量重复代码(创建连接、构建语句集、执行、结果集、流关闭)
2、资源统一管理(数据库信息)
3、表与对象的映射、表字段映射问题、表字段类型映射问题
4、SQL 耦合

mybatis配置

全局配置文件 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>
<!-- 定义外部配置,${jdbc.url}引用 -->
    <properties resource="db.properties"></properties>	
    <settings>
<!-- 打印查询语句 -->
        <setting name="logImpl" value="STDOUT_LOGGING" />

        <!-- 控制全局缓存(二级缓存),默认 true-->
        <setting name="cacheEnabled" value="true"/>

        <!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false  -->
        <setting name="lazyLoadingEnabled" value="true"/>
        <!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
        <setting name="aggressiveLazyLoading" value="true"/>
        <!--  Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST -->
        <!--<setting name="proxyFactory" value="CGLIB" />-->
        <!-- STATEMENT级别的缓存,使一级缓存,只针对当前执行的这一statement有效 -->
        <!--
                <setting name="localCacheScope" value="STATEMENT"/>
        -->
        <setting name="localCacheScope" value="SESSION"/>
    </settings>

<!-- 简化拼写,将type的对象用alias代替:parameterType="blog" -->
    <typeAliases>
        <typeAlias alias="blog" type="com.mybatis.domain.Blog" />
    </typeAliases>

<!--    <typeHandlers>
        <typeHandler handler="com.mybatis.type.MyTypeHandler"></typeHandler>
    </typeHandlers>-->

    <!-- 对象工厂 -->
<!--    <objectFactory type="com.mybatis.objectfactory.ObjectFactory">
        <property name="objName" value="666"/>
    </objectFactory>-->

<!--    <plugins>
        <plugin interceptor="com.mybatis.interceptor.SQLInterceptor">
            <property name="objName" value="betterme" />
        </plugin>
        <plugin interceptor="com.mybatis.interceptor.MyPageInterceptor">
        </plugin>
    </plugins>-->

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/><!-- 单独使用时配置成MANAGED没有事务 -->
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="BlogMapper.xml"/>
        <mapper resource="BlogMapperExt.xml"/>
    </mappers>

</configuration>

properties

可通过properties属性来实现引用配置文件
如果两个配置文件有同一个字段,优先使用外部配置文件的,${}引用

<properties resource="db.properties" />

<environments default="development">
    <environment id="development">
	<!-- 单独使用时配置成MANAGED没有事务 -->
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="${jdbc.driver}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
        </dataSource>
    </environment>
</environments>

可以直接引入外部配置文件,properties子元素中可以增加一些属性配置

<properties resource="db.properties">
	<property name="" value="" />
	<property name="" value="" />
</properties>

settings 全局参数设置

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

<!--执行器改成批量执行(攒够条数后,一次性执行)-->
<setting name="defaultExecutorType" value="BATCH"/>	

typeAliases

将type的对象用alias代替,实际应用时:parameterType=“blog”

<typeAliases>
	<typeAlias alias="blog" type="com.mybatis.domain.Blog" />
</typeAliases>

指定一个包名,将包中bean的首字母小写的非限定类名作为bean的别名

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

typeHandlers 类型处理器

TypeHandlerRegistry中预先设置字段类型转换:
this.register((Class)String.class, (TypeHandler)(new StringTypeHandler()));
this.register((Class)String.class, JdbcType.CHAR, (TypeHandler)(new StringTypeHandler()));
this.register((Class)String.class, JdbcType.CLOB, (TypeHandler)(new ClobTypeHandler()));
this.register((Class)String.class, JdbcType.VARCHAR, (TypeHandler)(new StringTypeHandler()));
this.register((Class)String.class, JdbcType.LONGVARCHAR, (TypeHandler)(new ClobTypeHandler()));
this.register((Class)String.class, JdbcType.NVARCHAR, (TypeHandler)(new NStringTypeHandler()));
this.register((Class)String.class, JdbcType.NCHAR, (TypeHandler)(new NStringTypeHandler()));
this.register((Class)String.class, JdbcType.NCLOB, (TypeHandler)(new NClobTypeHandler()));

objectFactory 对象工厂

DefaultObjectFactory类基于反射,获取对象实例

<objectFactory type="com.mybatis.objectfactory.ObjectFactory">
	<property name="" value=""/>
</objectFactory>

plugins 插件

<plugins>
        <plugin interceptor="com.mybaits.interceptor.SQLInterceptor">
            <property name="" value="" />
        </plugin>
        <plugin interceptor="com.mybaits.interceptor.MyPageInterceptor">
        </plugin>
</plugins>

在映射语句执行之前进行拦截

Environments 环境配置

用来管理数据库环境,nvironments可以包含多个,来适应多个环境的数据库

<environments default="development">
    <environment id="development">
		<!-- 单独使用时配置成MANAGED没有事务 -->
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="${jdbc.driver}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
        </dataSource>
    </environment>
</environments>
transactionManager 事务管理

JDBC:用JDBC管理事务
MANAGED:基于集成的容器来管理事务

dataSource 数据源

默认是带有连接池的数据源,同type=“POOLED”。type=“UNPOOLED”,不带有连接池,type=“JNDI”,采用服务器提供的JNDI技术实现。

注意:如果spring中也有定义,则覆盖mybatis。所以一般直接在spring中定义,除非单独使用mybatis。

Mappers 映射器

<mappers>
	<mapper resource="com/ferao/mapper/UserMapper.xml"/>
</mappers>
<!--该方式绑定注册时,接口和它的Mapper配置文件必须同名,且它的Mapper配置文件必须在同一个包下-->
<mappers>
	<mapper class="com.ferao.mapper.UserMapper" />
</mappers>
<!--该方式注入绑定时,接口和它的Mapper配置文件必须同名,且它的Mapper配置文件必须在同一个包下-->
<mappers>
	<package name="com.ferao.mapper"></package>
</mappers>

mybatis应用

mapper中参数传递

parameterType(输入类型)
resultType(输出类型)
resultMap(映射实体类)
顺序传参法

#{}里面的数字代表传入参数的顺序
这种方法sql表达不直观,一旦顺序调整容易出错,不建议使用

public User selectUser(String name, int deptId);

<select id="selectUser" resultMap="UserResultMap">
    select * from user
    where user_name = #{0} and dept_id = #{1}
</select>
@Param注解传参法

#{}里的名称对应注解@Param里的名称。
这种方法在参数不多的情况还是比较直观的,推荐使用

public User selectUser(@Param("userName") String name, int @Param("deptId") deptId);

<select id="selectUser" resultMap="UserResultMap">
    select * from user
    where user_name = #{userName} and dept_id = #{deptId}
</select>
Map传参法

#{}里的名称对应Map里的key
这种方法适合传递多个参数,且参数易变,需要灵活传递的情况,推荐使用

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>
Java Bean传参法

#{}里的名称对应User类的成员属性
这种方法直观,代码可读性强,业务逻辑处理方便,推荐使用

public User selectUser(User user);

<select id="selectUser" parameterType="com.jourwon.pojo.User" resultMap="UserResultMap">
    select * from user
    where user_name = #{userName} and dept_id = #{deptId}
</select>
useGeneratedKeys 插入后自动获取id
<insert id="insertSelective" parameterType="……" useGeneratedKeys="true" keyProperty="id">

resultType 输出类型

一般数据类型(单条)
<select id="getStuNameById" resultType="string">
	select username from t_student where id = #{id}
</select>
JavaBean 类型(单条)
public Student getStuById(Integer id);

<select id="getStuById" resultType="student">
	select * from t_student where id = #{id}
</select>	
List类型(多条)
public List<Student> getAllStus();

<!-- 注意这里的 resultType 返回值类型是集合内存储数据的类型,不是 'list' -->
<select id="getAllStus" resultType="student">
	select * from t_student
</select>
Map类型(单条key+value)
public Map<String, Object> getStuAsMapById(Integer id);

<select id="getStuAsMapById" resultType="map">
	select * from t_student where id = #{id}
</select>
Map类型(单条key+obj)
<!-- 注意:id作为key -->
@MapKey("id")
Map<Integer, Student> getAllStusAsMap();

<!-- 注意:返回的是 student对象 -->
<select id="getAllStusAsMap" resultType="student">
	select * from t_student  where id = #{id}
</select>
Map类型(多条key+obj)
List<Map<String, Object>> getAllStuAsMapById(Integer id);

<select id="getAllStuAsMapById"  parameterType="int" resultType="map">
	select * from t_student
</select>

resultMap

<select id="getOrder" parameterType="int" resultType="com.jourwon.pojo.Order">
       select order_id id, order_no orderno ,order_price price form orders where order_id=#{id};
</select>
select * from orders where order_id=#{id}
<resultMap type="com.jourwon.pojo.Order" id="orderResultMap">
    <!–用id属性来映射主键字段–>
    <id property="id" column="order_id">
    <!–用result属性来映射非主键字段,property为实体类属性名,column为数据库表中的属性–>
	<result property ="orderno" column ="order_no"/>
	<result property="price" column="order_price" />
</reslutMap>
association 嵌套结果集
<resultMap id="" type="">
		<!--设置主键时使用,使用此标签配置映射关系(可能不止一个) -->
	<id column="" jdbcType="" property="" />
	<result column="" jdbcType="" property=""/>
	
		<!-- 集合中的property须为oftype定义的pojo对象的属性-->
	<collection property="pojo的集合属性" ofType="集合中的pojo对象">
	  	<id column="表主键字段" jdbcType="字段类型" property="pojo对象的主键属性" />
	  	<result column="表字段" jdbcType="字段类型" property="pojo对象的属性" />
	</collection>
</resultMap>	
collection 嵌套结果集
<select id="findSportsInfoByEmpId" resultMap="empmap">
		select e.*, s.* 
		from employee as e,sport as s
		where e.eid=s.eid
		and e.eid=#{eid}
	</select>
	
	<resultMap type="Employee" id="empmap">
		<id property="eid" column="eid"/>
		<result property="ename" column="ename"/>
		<result property="epwd" column="epwd"/>
		<result property="address" column="address"/>
		<result property="tel" column="tel"/>
		
		<!-- collection描述一对多的关系,ofType是集合所包含的类型,可以写完整Java类名或别名  -->
		<collection property="sports" ofType="Sport">
			<id property="sportId" column="sportid"/>
			<result property="sportName" column="sportname"/>
			<result property="sportScore" column="sportscore"/>
		</collection>
	</resultMap>

association 嵌套查询
<resultMap id="BlogWithAuthorQueryMap"  type="com.mybatis.domain.associate.BlogAndAuthor">
    <id column="bid" property="bid" jdbcType="INTEGER"/>
    <result column="name" property="name" jdbcType="VARCHAR"/>
    <association property="author" javaType="com.mybatis.domain.Author"
                 column="author_id" select="selectAuthor"/> 
</resultMap>

<select id="selectAuthor" parameterType="int" 		resultType="com.mybatis.domain.Author">
    select author_id authorId, author_name authorName
    from author where author_id = #{authorId}
</select>

动态SQL

if
<if test="subscribeOrderSource != null">
choose、when、otherwise
<choose>
    <when test="criterion.noValue">
        and ${criterion.condition}
    </when>
    <when test="criterion.singleValue">
        and ${criterion.condition} #{criterion.value}
    </when>
     <otherwise>
    </otherwise>
</choose>
trim、where、set
<trim prefix="(" suffix=")" suffixOverrides=",">
    <if test="bid != null">
        bid,
    </if>
    <if test="name != null">
        name,
    </if>
</trim>
foreach
<foreach collection="list" item="blogs" index="index"  separator=",">
     ( #{blogs.bid},#{blogs.name},#{blogs.authorId} )
</foreach>

批量操作

批量操作时,如果数据量过大,发送的数据包太大,mybatis会报错。
Show VARIABLES like ‘%max_allowed_packet%’ 查看,默认4MB多的大小。

动态SQL批量操作

批量插入

<insert id="insertBlogList" parameterType="java.util.List">
    insert into blog (bid, name, author_id)
    values
    <foreach collection="list" item="blogs" index="index"  separator=",">
        ( #{blogs.bid},#{blogs.name},#{blogs.authorId} )
    </foreach>
</insert>

批量修改

<update id="updateBlogList">
    update blog set
    name =
    <foreach collection="list" item="blogs" index="index" separator=" " open="case bid" close="end">
        when #{blogs.bid} then #{blogs.name}
    </foreach>
    ,author_id =
    <foreach collection="list" item="blogs" index="index" separator=" " open="case bid" close="end">
        when #{blogs.bid} then #{blogs.authorId}
    </foreach>
    where bid in
    <foreach collection="list" item="item" open="(" separator="," close=")">
        #{item.bid,jdbcType=INTEGER}
    </foreach>
</update>
修改执行器类型

修改执行器类型,设置批量执行

 <setting name="defaultExecutorType" value="BATCH"/>
JDBC批量操作的方式
public void testJdbcBatch() throws IOException {
    Connection conn = null;
    PreparedStatement ps = null;
    try {
        conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mybatis?useUnicode=true&……, "root", "123456"); // 打开连接
        ps = conn.prepareStatement("INSERT into blog values (?, ?, ?)");
        for (int i = 1000; i < 101000; i++) {
            Blog blog = new Blog();
            ps.setInt(1, i);
            ps.setString(2, String.valueOf(i)+"");
            ps.setInt(3, 1001);
            ps.addBatch(); 	// 放入客户端缓存
        }
        ps.executeBatch(); // 批量执行
        conn.commit();
    } catch (Exception se) {
        se.printStackTrace();
    } finally {
        ps.close();
        conn.close();
		……
    }
}

#、$

$方式不会对符号转义,不能防止SQL注入
$方式没有预编译,不会缓存

PrepareStatement(ps)、Statement

1.都是接口,ps继承statement。
2.Statement执行静态SQL,ps可以执行带入参的SQL
3.Ps的addBatch() 实现批量操作
4.Ps对于相似的SQL只编译一次(语句相同,入参不同),减少编译的次数。
5.Ps可以防止SQL注入。

模糊查询

like %

直接用%有SQL注入的风险。

like concat()
order_num like concat("%", #{orderNum}, "%")
bind标签
<select id="id" resultType="……">
  <!-- bind:可以将OGNL表达式的值绑定到一个变量中,方便后来引用这个变量的值 -->
    <bind name="bindName" value="'%'+eName+'%'"/>    
          
SELECT * FROM emp
    <if test="param!=null">
         where ename like #{bindName}
    </if>
/select>

自定义TypeHandler

声明MyTypeHandler类
<typeHandlers>
    <typeHandler handler="com.mybatis.type.MyTypeHandler" />
</typeHandlers>
实现MyTypeHandler类
public class MyTypeHandler extends BaseTypeHandler<String> {
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
        // 设置 String 类型的参数的时候调用,Java类型到JDBC类型
        // 注意只有在字段上添加typeHandler属性才会生效
        // insertBlog name字段
        System.out.println("---------------setNonNullParameter1:"+parameter);
        ps.setString(i, parameter);
    }

    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        // 根据列名获取 String 类型的参数的时候调用,JDBC类型到java类型
        // 注意只有在字段上添加typeHandler属性才会生效
        System.out.println("---------------getNullableResult1:"+columnName);
        return rs.getString(columnName);
    }

    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        // 根据下标获取 String 类型的参数的时候调用
        System.out.println("---------------getNullableResult2:"+columnIndex);
        return rs.getString(columnIndex);
    }

    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        System.out.println("---------------getNullableResult3:");
        return cs.getString(columnIndex);
    }
}
xml中引用MyTypeHandler
#{name,jdbcType=VARCHAR,typeHandler=com.mybatis.type.MyTypeHandler}

自定义对象工厂

声明ObjectFactory
<objectFactory type="com.mybatis.objectfactory.ObjectFactory">
    <property name="objname" value="666"/>
</objectFactory>
实现ObjectFactory
public class ObjectFactory extends DefaultObjectFactory {
    @Override
    public Object create(Class type) {
        System.out.println("创建对象方法:" + type);
        if (type.equals(Blog.class)) {
            Blog blog = (Blog) super.create(type);
            blog.setName("object factory");
            blog.setBid(1111);
            blog.setAuthorId(2222);
            return blog;
        }
        Object result = super.create(type);
        return result;
    }
}

Mapper映射器的继承

public interface BlogMapperExt extends BlogMapper

<mapper namespace="com.mybatis.mapper.BlogMapperExt" 
extends = 'com.mybatis.mapper.BlogMapper'>

mybatis体系结构与工作原理

工作流程

工作流程

解析配置文件

Mybatis启动时,解析全局配置文件、映射器文件,解析成configuration对象。

提供操作接口

mybatis在应用程序和数据库中间。与数据库之间的一次连接(会话),就是一个SqlSession对象。
SqlSession对象由SqlSessionFactory会话工厂创建。
Builder负责创建SqlSessionFactory。
Mybatis是对JDBC的封装,其底层一定会有JDBC的核心对象,比如执行SQL的Statement,结果集ResultSet。

SQL执行

SqlSession持有一个Executor对象,用来封装对数据库的操作。
在执行器Executor执行query或者update操作的时候我们创建一系列的对象,来处理参数、执行SQL、处理结果集,这里我们把它简化成一个对象: StatementHandler,可以把它理解为对Statement的封装,在阅读源码的时候我们再去了解还有什么其他的对象。

包结构(21个包)

包结构

架构模型

架构模型

接口层

接口层是我们打交道最多的,核心对象是sqlsession,它是上层应用于mybatis之间的桥梁。Sqlsession定义了很多对数据库的操作方法。
接口层负责接收请求,并调用核心层的响应模块来完成具体的数据库操作。

核心层

与数据库操作相关的动作都在这一层完成。
1.将接口入参解析、映射成JDBC类型。
2.解析XML文件中的SQL语句(插入参数、生产动态SQL)
3.执行SQL语句。
4.处理结果集,映射成Java对象。

基础层

mybatis缓存

ORM框架一般都会提供缓存,从而提高查询效率,减少数据库压力。
Mybatis和Hibernate一样,也有一级缓存、二级缓存,并且预留了集成第三方缓存的接口。

Cache

Mybatis的缓存类都在cache包里,一个Cache接口,PrepetualCache实现类。
cache

PrepetualCache

PrepetualCache是用HashMap实现的。
PrepetualCache是由SqlSession的Executor(执行器)维护的。
PrepetualCache

BaseExecutor

在这里插入图片描述

DefaultSqlSession

在这里插入图片描述

装饰器类

PrepetualCache是装饰器模式中的基础实现类,又叫基础缓存。
Decorators包内都是缓存装饰类,可以提供很多额外的功能,比如:回收策略、日志记录、定时刷新等等。
实际场景中,基础缓存会被装饰四五层。
在这里插入图片描述

缓存分类

可分为三类:基本缓存、淘汰算法缓存、装饰器缓存。
在这里插入图片描述

一级缓存(本地缓存)

Mybatis的一级缓存是在会话(sqlsession)层面进行的,由Executor执行。
在这里插入图片描述

一级缓存的开关

默认开启

<!-- 关闭一级缓存 -->
	<setting name="localCacheScope" value="STATEMENT"/>

STATEMENT:将一级缓存的作用域限于单次执行的单个SQL,等价于关闭。

一级缓存适用范围

同一个会话中,多次执行相同的SQL,会直接从内存中取缓存结果,不访问数据库。
不同的会话中,即使SQL相同,也不会使用一级缓存。

一级缓存验证

首先必须关闭二级缓存:localCacheScope 设置为SESSION

	<setting name="localCacheScope" value="SESSION"/>
@Test
public void testCache() throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = 
			new SqlSessionFactoryBuilder().build(inputStream);

    SqlSession session1 = sqlSessionFactory.openSession();
    SqlSession session2 = sqlSessionFactory.openSession();
    try {
        BlogMapper mapper0 = session1.getMapper(BlogMapper.class);
        BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
        System.out.println(mapper0.selectBlogById(1));

        System.out.println("第二次查询,相同会话,获取到缓存了吗?");
        System.out.println(mapper1.selectBlogById(1));

        System.out.println("第三次查询,不同会话,获取到缓存了吗?");
        BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
        System.out.println(mapper2.selectBlogById(1));

    } finally {
        session1.close();
    }
}

结论:
1.“mapper0.selectBlogById(1)”进入数据库查询,mapper1 和mapper0 源于同一个session1,所以“mapper1.selectBlogById(1)”直接从缓存中取值。
2.mapper2 源于session2所以哪怕SQL一样,“mapper2.selectBlogById(1)”不会从缓存取值。

源码分析

get
在这里插入图片描述
put

在这里插入图片描述

update(delete)
在这里插入图片描述
query方法中(见上 get),flushCache = true 也会清空缓存。
Mapper文件中,的属性flushCache 默认true,所以会清空缓存。但是默认false,所以不请客缓存。

二级缓存

一级缓存不能跨会话共享,不同的会话之间对于相同数据肯能有不一样的缓存,以至于在多个会话、分布式场景下,会存在直接返回缓存中过时数据的问题,所以就需要工作范围更广的二级缓存了。
二级缓存的范围是namespace级别(每个mapper有自己的namespace),可以被多个SqlSession共享,生命周期和应用同步。
二级缓存取不到时,才去一级缓存取。

CachingExecutor

二级缓存由CachingExecutor维护,CachingExecutor是Executor的装饰器类。
在这里插入图片描述

二级缓存的开启方式

mybatis-config.xml(默认开启)

<!-- 控制全局缓存(二级缓存),默认 true,false可以关闭-->
<setting name="cacheEnabled" value="true"/>

Mapper.xml文件中配置标签

<cache 
	type="org.apache.ibatis.cache.impl.PerpetualCache"
	size="1024"     	<!--最多缓存对象个数,默认1024-->
	eviction="LRU"		<!--回收策略-->
	flushInterval="120000"	<!--自动刷新时间(ms),默认调用时刷新-->
	readOnly="false"/>	<!--改为true,可读写(对象必须支持序列化),默认false(安全)-->

注意:cache标签如果没有的话,虽然会进入CachingExecutor类,但在判断if (ms.isUseCache() && resultHandler == null) 时,false,从而导致二级缓存不生效。

useCache=”false”关闭缓存

<select id="" resuleMap="" userCache="false">
二级缓存验证

二级缓存绑定事务

@Test
public void testCache() throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = 
			new SqlSessionFactoryBuilder().build(inputStream);

    SqlSession session1 = sqlSessionFactory.openSession();
    SqlSession session2 = sqlSessionFactory.openSession();
    try {
        BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
        System.out.println(mapper1.selectBlogById(1));
        // 事务不提交的情况下,二级缓存会写入吗?
        session1.commit();	

        System.out.println("第二次查询");
        BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
        System.out.println(mapper2.selectBlogById(1));
    } finally {
        session1.close();
    }
}

注意:如果没有session1.commit();“mapper2.selectBlogById(1)”不会从二级缓存取数据。事务提交之后,才会把结果写入二级缓存。

多个namespace共享一个二级缓存
<cache-ref namespace="com.mybatis.mapper.BlogMapperExt"/>
依赖Redis二级缓存

将type替换成第三方的引用
redis会自动的将Sql+条件+Hash等当做key值,而将查询结果作为value。

<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-redis</artifactId>
    <version>1.0.0-beta2</version>
</dependency>
<cache type="org.mybatis.caches.redis.RedisCache"
       eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
适用场景

1.查询为主,比如历史交易、历史订单的查询。(所有的增删改都会刷新二级缓存,导致二级缓存失效)。
2.多个namespace针对同一个表操作的场景,如果一个namespace刷新的缓存,其他的没有,会出现读到脏数据的情况,所以只适用于一个Mapper操作单表的情况。

源码分析

在这里插入图片描述

mybatis插件

自定义插件

实现Interceptor接口

(1)实现Interceptor接口,
(2)重写intercept、plugin、setProperties方法,其中intercept是最关键的,包含拦截的逻辑、需要增强的功能
(3)拦截器上添加注解。指定需要拦截的对象、方法、参数。

@Intercepts({ @Signature(
   	type = StatementHandler.class, 
   	method = "query", 
   	args = { Statement.class, ResultHandler.class}) })
public class SQLInterceptor implements Interceptor {
   @Override
   public Object intercept(Invocation invocation) throws Throwable {
       long startTime = System.currentTimeMillis();
       StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
       BoundSql boundSql = statementHandler.getBoundSql();
       String sql = boundSql.getSql();
       System.out.println("获取到SQL语句:"+sql);

       try {
           return invocation.proceed();
       }finally {
           long endTime = System.currentTimeMillis();
           System.out.println("SQL执行耗时:" + (endTime-startTime) +"ms");
       }

   }

   @Override
   public Object plugin(Object target) {
       return Plugin.wrap(target, this);
   }

   @Override
   public void setProperties(Properties properties) {
       String columnName= properties.getProperty("columnName");
       // System.out.println("获取到的参数:"+ columnName);
   }
}
插件注册

Mybatis-config.xml文件中注册插件,配置属性
在这里插入图片描述

解析注册

Mybatis启动时,扫描标签,把所有插件注册到configuration对象的InterceptorChain

XMLConfigBuilder解析
在这里插入图片描述
InterceptorChain 保存
在这里插入图片描述

架构体系

在这里插入图片描述
注意:多个插件时,插件的配置顺序()与执行顺序相反。
在这里插入图片描述

关键对象-作用

Interceptor接口 自定义拦截
InterceptorChain 存放插件的容器
Plugin 对象,提供创建代理类的方法
Invocation 对被代理对象的封装
在这里插入图片描述

源码分析

先二级缓存装饰?还是先插件代理?
Executor对象可以被二级缓存装饰,再插件中有需要被代理,那么这两者的先后关系是怎样的?

DefaultSqlSessionFactory.openSessionFromDataSource()

在这里插入图片描述

构建executor Configuration.newExecutor()

executor在构建的过程中,先创建基本类型,再二级缓存装饰,最后插件拦截CachingExecutor。

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
 executorType = executorType == null ? defaultExecutorType : executorType;
 executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
 Executor executor;
 if (ExecutorType.BATCH == executorType) {	// 创建基本类型
   executor = new BatchExecutor(this, transaction);
 } else if (ExecutorType.REUSE == executorType) {
   executor = new ReuseExecutor(this, transaction);
 } else {
   executor = new SimpleExecutor(this, transaction);
 }
 if (cacheEnabled) {
   executor = new CachingExecutor(executor);	// 二级缓存装饰	
 }
 executor = (Executor) interceptorChain.pluginAll(executor);// 插件拦截,创建代理类
 return executor;
}
创建代理类 InterceptorChain.pluginAll

pluginAll方法中,通过遍历interceptors (插件集),基于interceptor.plugin(target)实现对target的代理(递归)

public class InterceptorChain {
 private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
 public Object pluginAll(Object target) {
   for (Interceptor interceptor : interceptors) {
     target = interceptor.plugin(target);	// 创建代理对象
   }
   return target;
 }
 public void addInterceptor(Interceptor interceptor) {
   interceptors.add(interceptor);
 }
 public List<Interceptor> getInterceptors() {
   return Collections.unmodifiableList(interceptors);
 }
}

interceptor.plugin

public Object plugin(Object target) {
   return Plugin.wrap(target, this);
}

public static Object wrap(Object target, Interceptor interceptor) {
 Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
 Class<?> type = target.getClass();
 Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
 if (interfaces.length > 0) {
   return Proxy.newProxyInstance(	// 代理
       type.getClassLoader(),
       interfaces,
       new Plugin(target, interceptor, signatureMap));
 }
 return target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
 try {
   Set<Method> methods = signatureMap.get(method.getDeclaringClass());
   if (methods != null && methods.contains(method)) {
     return interceptor.intercept(new Invocation(target, method, args));	
   			// 调用自己定义的插件类,实现自己的拦截逻辑
   }
   return method.invoke(target, args);	// 实现已有的逻辑
 } catch (Exception e) {
   throw ExceptionUtil.unwrapThrowable(e);
 }
}

分页插件 PageInterceptor

针对不同数据库,有不同的实现
在这里插入图片描述

PageHelper.startPage

PageHelper是PageMethod子类
PageMethod.startPage()中setLocalPage(page);
PageMethod.setLocalPage(page)
在当前线程中创建LOCAL_PAGE对象,并将page信息存放在LOCAL_PAGE中
在这里插入图片描述

PageHelper.getLocalPage()

dialect.getPageSql中,通过getLocalPage()从LOCAL_PAGE中获取page对象。

在这里插入图片描述
在这里插入图片描述

插件适用场景

在这里插入图片描述

spring、mybatis整合

步骤

Pom依赖
<!--mybatis 和Spring整合 -->
<dependency>
   <groupId>org.mybatis</groupId>
   <artifactId>mybatis-spring</artifactId>
   <version>2.0.4</version>
</dependency>

<!-- mybatis -->
<dependency>
   <groupId>org.mybatis</groupId>
   <artifactId>mybatis</artifactId>
   <version>${mybatis.version}</version>
</dependency>
applicatioContext.xml配置sqlsessionFactorybean
<!-- 在Spring启动时创建 sqlSessionFactory -->
<bean id="sqlSessionFactory" 
			class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="configLocation" 
			value="classpath:mybatis-config.xml"></property>
    <property name="mapperLocations" 
			value="classpath:mapper/*.xml"></property>
    <property name="dataSource" ref="dataSource"/>
</bean>
配置扫描Mapper接口的路径
<mybatis-spring:scan #base-package="com.mybatis.crud.dao"/>

<bean id="mapperScanner" 
		class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.mybatis.crud.dao"/>
</bean>

@MapperScan("com.t3.ts.maintain.biz.center.mappers")

创建会话工厂 sqlSessionFactory

在这里插入图片描述

InitializingBean接口

实现InitializingBean接口,重写afterPropertiesSet()方法,创建sqlSessionFactory 实例。

@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();
}
FactoryBean接口

实现FactoryBean接口,重新getObject()方法,用户自定义实例化Bean的逻辑。

@Override
public SqlSessionFactory getObject() throws Exception {
  if (this.sqlSessionFactory == null) {
    afterPropertiesSet();
  }

  return this.sqlSessionFactory;
}
ApplicationListener接口

实现ApplicationListener接口,让sqlSessionFactory有能力监控应用发出的事件通知。
监听ContextRefreshedEvent(上下文刷新事件),会在spring容器加载完之后执行。

@Override
public void onApplicationEvent(ApplicationEvent event) {
  if (failFast && event instanceof ContextRefreshedEvent) {
    // fail-fast -> check all statements are completed
    this.sqlSessionFactory.getConfiguration().getMappedStatementNames();
  }
}

创建会话

DefaultSqlSession不能直接拿来用,非线程安全。
在这里插入图片描述

SqlSessionTemplate

线程安全,可以在所有DAO层共享一个实例(默认单例)。
基于代理对象sqlSessionProxy 实现调用
SqlSessionTemplate

public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {

  notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
  notNull(executorType, "Property 'executorType' is required");

  this.sqlSessionFactory = sqlSessionFactory;
  this.executorType = executorType;
  this.exceptionTranslator = exceptionTranslator;
  this.sqlSessionProxy = (SqlSession) newProxyInstance(	// 反射
      SqlSessionFactory.class.getClassLoader(),
      new Class[] { SqlSession.class },
      new SqlSessionInterceptor());
}

SqlSessionInterceptor

private class SqlSessionInterceptor implements InvocationHandler {
  @Override
  public Object invoke(Object proxy, 
			Method method, Object[] args) throws Throwable {
	/* 每次获取一个新的sqlSession ,保证线程安全 */
    SqlSession sqlSession = getSqlSession(	        
    	SqlSessionTemplate.this.sqlSessionFactory,
        SqlSessionTemplate.this.executorType,	// 执行器类型
        SqlSessionTemplate.this.exceptionTranslator);	// 异常解析器
    try {
      Object result = method.invoke(sqlSession, args);// 基于sqlSession 调用增删改查
      if (!isSqlSessionTransactional(sqlSession, 		
			SqlSessionTemplate.this.sqlSessionFactory)) {
        // force commit even on non-dirty sessions because some databases require
        // a commit/rollback before calling close()
        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);
      }
    }
  }
}
SqlSessionTemplate的实例获取方式

继承SqlSessionDaoSupport类,getSqlSession()获取SqlSessionTemplate的实例。

SqlSessionDaoSupport.class

public abstract class SqlSessionDaoSupport extends DaoSupport {

  private SqlSession sqlSession;

  private boolean externalSqlSession;

  public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
    if (!this.externalSqlSession) {
      this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
    }
  }

  public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
    this.sqlSession = sqlSessionTemplate;
    this.externalSqlSession = true;
  }

  public SqlSession getSqlSession() {
    return this.sqlSession;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected void checkDaoConfig() {
    notNull(this.sqlSession, "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required");
  }

}

应用

public class BaseDao extends SqlSessionDaoSupport {
    public Object selectOne(String statement) {
        return getSqlSession().selectOne(statement);
    }

Mapper接口扫描注册

然而在实际应用中,只声明了一个Mapper接口,并将其注入到DAO层,那么很显然,当Mapper接口在spring容器中注册时,必然做了一些其他的操作。

ClassPathMapperScanner.class
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
		// 扫出所有的mapper class文件
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

	……
		// beanClass 设置为MapperBeanClass.class
definition.setBeanClass(this.mapperFactoryBean.getClass()); 
	……
}
MapperFactoryBean.class
public class MapperFactoryBean<T> 
		extends SqlSessionDaoSupport implements FactoryBean<T> {
……
@Override
public T getObject() throws Exception {	// 代理类
//获取SqlSessionTemplate实例
        return this.getSqlSession().getMapper(this.mapperInterface);
    }

@Override
public Class<T> getObjectType() {
  return this.mapperInterface; // 获取Mapper接口:比如com.t3.ts.maintain.biz.center.mappers.MaintenanceAccidentPersonInjuredMapper
}
}
public class MapperRegistry {
	……
   @SuppressWarnings("unchecked")
  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);
    }
  }
}

MapperFactoryBean继承了SqlSessionDaoSupport接口。也就是说,所有的Mapper接口在容器中都被注册成一个支持泛型的MapperFactoryBean。

getObject()最终调用了
Configuration.getMapper(Class type, SqlSession sqlSession)

mapperProxy.invoke()
protected T newInstance(MapperProxy<T> mapperProxy) {
  return (T) Proxy.newProxyInstance(
	mapperInterface.getClassLoader(), 
	new Class[] { mapperInterface }, 
	mapperProxy);

总结

1.SqlSessionTemplate作为SqlSession接口的实现类,其本质是个代理类。内部类SqlSessionInterceptor实现InvocationHandler接口,构建属性SqlSession sqlSessionProxy。
2.SqlSessionDaoSupport获取SqlSessionTemplate的实例。
3.MapperFactoryBean继承SqlSessionDaoSupport,并实现FactoryBean,在Mapper注入的时候,通过调用SqlSessionTemplate的getMapper()方法,获取JDK代理对象并注入。
4.执行Mapper接口的任意方法,会触发管理类MapperProxy,进入SQL处理的流程。

在这里插入图片描述

设计模式

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/363147.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Yolov5】深度模型进行训练-CPU版

yolo5模型训练1.yolov5自己的模型训练1.1 git下载对应的源码到服务器1.2 从最新的 YOLOv5版本自动下载模型。1.3 detect.py 推断1.4 train.py进行训练1.5 yolo的原理2.训练自己的模型2.1 Data下面新建如图目录Mydata2.1 lableimg进行数据标注的图形2.2 数据集2.3 train.py调整d…

IP 协议

1.IP协议报头如下图:版本号 代表的是当前的IP协议的版本,此处的版本一共有两个取值:v4和v6.本文着重针对v4版本进行解析.首部长度 代表的是整个IP报头的长度,这个报头长度是可变长的,可变长的原因在于报头中的选项,这个属性是一个可有可无的属性,会改变报头长度,它的单位是32bi…

5个原因告诉您为什么要拥有个人IP

在数字时代中&#xff0c;信息每分每秒都在飞速地增长&#xff0c;对于企业和个人来说&#xff0c;获得关注变得越来越具有挑战性。如果您希望在网上创造某种形式的存在感&#xff0c;保持内容的原创性和新鲜度是您通往成功的路上不可避免的挑战。尽管如此&#xff0c;从竞争对…

电商项目之Feign与Dubbo技术选型

文章目录1 问题背景2 前言3 思路4 Feign与Dubbo的区别5 总结6 真实案例1 问题背景 电商项目&#xff0c;B端以consul作为注册中心。重构了一个营销服务&#xff0c;以Nacos作为注册中心。B端需要调用营销服务。关于远程调用框架&#xff0c;营销服务用了Dubbo&#xff0c;而B端…

吉卜力风格水彩画怎么画?

著名的水彩艺术家陈坚曾说&#xff1a;“水彩是用水润调和形成的饱和度极高的艺术画面&#xff0c;在纸上晕染的画面面积、强度等具有许多随意性&#xff0c;天空的颜色乌云密布&#xff0c;都是很随意的&#xff0c;难以模仿。” 是的&#xff0c;水彩画的妙处就在于不确定的…

C语言数据结构初阶(1)----时空复杂度

目录 1. 数据结构&#xff0c;算法的概念 2. 算法的效率 2.1 算法复杂度 3. 时间复杂度 3.1 时间复杂度的概念 3.2 大O的渐进表示法 3.3 小试牛刀 4. 算法的空间复杂度 4.1 小试牛刀 1. 数据结构&#xff0c;算法的概念 数据结构(Data Structure)是计算机存储、组织数据…

2023从0开始学性能(3) —— 探索性能测试流程2

性能测试计划 性能测试的计划中应该由项目描述、项目的业务模型和性能指标&#xff08;指标来源于调研需求中预期指标&#xff09;&#xff0c;性能测试环境说明&#xff08;前期调研的配置信息&#xff09;以及测试的场景设计&#xff0c;具体的我们后面再慢慢说。 性能测试场…

4123版驱动最新支持《霍格沃茨之遗》,英特尔锐炫显卡带你畅游魔法世界

2023年开年最火的3A大作&#xff0c;那一定是近期上架steam平台的《霍格沃茨之遗》&#xff0c;这款游戏在2020年9月份曝光&#xff0c;游戏根据《哈利波特》系列书籍内容改编&#xff0c;作为一款开放式的3A大作&#xff0c;《霍格沃兹之遗》目前在steam上的实时在线人数已经突…

客户体验:客户对企业产生的所有交互与感知!

“客户体验是一种纯主观的在用户使用产品过程中建立起来的感受。良好的用户体验有助于公司不断完善产品或服务。正是这所有用户接触的感受差异&#xff0c;构成了用户对一家公司独特的体验认知。”说白了&#xff0c;客户体验就是客户和公司接触产生的感受。客户体验主要关注客…

JS的事件循环

文章目录写在前面1.浏览器的进程模型1.1 何为进程1.2 何为线程1.3 浏览器有哪些线程和进程2.渲染主线程是如何工作的任务队列的优先级面试题如何理解JS异步JS中的计时器能做到精确计时吗&#xff1f;为什么&#xff1f;写在前面 此处的文字为自己的理解 1.浏览器的进程模型 1.…

git的安装及git的使用方法和指令

Git的使用什么是版本控制&#xff1f;什么是 Git&#xff1f;人工版本控制器版本控制工具常见版本控制工具怎么工作的&#xff1f;git 文件生命周期状态区域安装配置-- global检查配置创建仓库 - repository工作流与基本操作查看工作区的文件状态如果显示乱码的解决方式git sta…

JUC并发编程(下)

✨作者&#xff1a;猫十二懿 ❤️‍&#x1f525;账号&#xff1a;CSDN 、掘金 、个人博客 、Github &#x1f389;公众号&#xff1a;猫十二懿 学习地址 写在最前 JUC并发编程&#xff08;上&#xff09; JUC&#xff08;Java Util Concurrent&#xff09;学习内容框架&…

Leetcode第530题二叉搜索树的最小绝对差|C语言

刚开始看到这道题&#xff0c;习惯性的找了搜索树的最左下和最右下的结点进行计算。结果发现要求返回的是“任意两个结点”的差的绝对值中最小的值。 思路&#xff1a;看了答案&#xff0c;因为二叉搜索树的中序遍历是有序数组&#xff0c;因此可以采用中序遍历对相邻结点之间进…

合约检测之slither(草稿)

一、Slither 介绍及运行流程 Slither 是一个静态分析框架&#xff0c;它通过将 Solidity 智能合约转换为称为 SlithIR 的中间表示来工作。 SlithIR 使用静态单一赋值 (SSA) 形式和精简指令集来减慢分析执行速度&#xff0c;同时保留在 Solidity 转换为字节码时可用的语义信息。…

GEE学习笔记 六十一:颜值就是战斗力

引用网络名人局座的一句话&#xff1a;“颜值越高的武器&#xff0c;战斗力越强”。对于我们做开发也是&#xff0c;使用的工具颜值越高&#xff0c;我们越喜欢使用。Google Earth Engine在线编辑器&#xff08;https://code.earthengine.google.com/&#xff09;虽然目前还不是…

【Proteus仿真】【STM32单片机】环境监测系统设计

文章目录一、功能简介二、软件设计三、实验现象联系作者一、功能简介 本项目使用Proteus8仿真STM32单片机控制器&#xff0c;使用LCD1602显示模块、蜂鸣器、按键模块、DHT11温湿度传感器、继电器加热祛湿加湿模块等。 主要功能&#xff1a; 系统运行后&#xff0c;LCD1602显示…

实现达梦数据库 disql 工具在 Linux 系统里上下翻动查看和编辑历史命令

本文内容已在如下环境上测试&#xff1a; 操作系统&#xff1a;银河麒麟10 数据库版本&#xff1a;达梦8 一、默认情况下&#xff0c;在 Linux 系统中使用 disql 命令时&#xff0c;若使用键盘的上下键查找历史命令复用&#xff0c;会出现乱码&#xff0c;导致在 Linux 上编写…

Scala的简单使用

文章目录Scala的简单使用&#xff08;一&#xff09;交互模式1、命令行方式2、文件方式&#xff08;二&#xff09;编译模式1、创建源程序2、编译成字节码3、解释执行对象Scala的简单使用 Scala可以在交互模式和编译模式两种方式下运行 &#xff08;一&#xff09;交互模式 在…

到了35岁,软件测试职业发展之困惑如何解?

35岁&#xff0c;从工作时间看&#xff0c;工作超过10年&#xff0c;过了7年之痒&#xff0c;多数IT人都已经跳槽几次。 35岁&#xff0c;发展比较好的软件测试人&#xff0c;已经在管理岗位&#xff08;测试经理甚至测试总监&#xff09;或已经成为测试专家或测试架构师。发展…

超280人靠这份软件测试面试题进入大厂,堪称金三银四最强,建议收藏

个人感觉目前各个大中厂面试不好的地方是&#xff0c;都在朝着背面试八股文的方向发展&#xff08;曾经面试某二线厂&#xff0c;对着面试官纯背了 40 分钟概念&#xff09;&#xff0c;但不得不承认这也是企业在短时间内筛选面试者的一个重要手段。 今天为了帮助大家&#xff…