备战面试日记(4.3)- (框架.Mybatis)
本人本科毕业,21届毕业生,一年工作经验,简历专业技能如下,现根据简历,并根据所学知识复习准备面试。
记录日期:2022.1.15
大部分知识点只做大致介绍,具体内容根据推荐博文链接进行详细复习。
文章目录
- 框架原理 - Mybatis
- 概念
- 简介
- 作用
- 优点
- 缺点
- Mybatis配置文件【可阅】
- properties
- settings
- typeAliases
- typeHandlers
- 处理枚举类型
- objectFactory
- plugins
- environments
- 数据源(dataSource)
- UNPOOLED
- POOLED
- JNDI
- databaseIdProvider
- mappers
- Mybatis源码浅析
- 引入
- SqlSessionFactoryBuilder
- XMLConfigBuilder#parse()
- XMLConfigBuilder#parseConfiguration()
- 插件注册
- mapper扫描与解析
- SqlSession
- Mapper代理
- 执行查询语句
- 插件开发
- 引入
- 四大对象
- Mybatis插件接口 - Interceptor
- 实际运用举例
- 插件原理
- Mybatis面试问题
- #{} 和 ${} 的区别?
- Mybatis 一级缓存 和 二级缓存?
- 一级缓存
- 二级缓存
- 缓存失效
- 一级缓存
- 二级缓存
- Mybatis插件运行原理?
- 编写插件
- 插件运行原理
- 举例:PageHelper
- 配置过程
- 使用步骤
- 普通使用
- Service中使用
- 原理说明
- Xml映射文件和内部数据结构之间的映射?
- configuration
- resultMap
- mappedStatment
- Mybatis中用到了哪些设计模式?
框架原理 - Mybatis
推荐阅读面试文章【推荐】:MyBatis面试题总结
推荐阅读功能使用文章:MyBatis详解
推荐阅读解析源码文章:MyBatis系列之Mybatis源码解读
推荐囧辉的文章:面试题:mybatis 中的 DAO 接口和 XML 文件里的 SQL 是如何建立关系的?
其他推荐:Mybatis知识点整理
概念
Mybatis官网:mybatis – MyBatis 3 | 入门
简介
Mybatis
是一个半ORM框架,它内部封装了JDBC,开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。
MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。
通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。
作用
帮助程序员将数据存入到数据库中。
传统的JDBC代码太复杂了。简化、框架、自动化。
优点
- 基于SQL语句编程,不会对应用程序或者数据库的现有设计造成任何影响,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,重用性高。
- 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接;
- 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。
- 能够与Spring很好的集成。
- 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。
缺点
- SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。
- SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
Mybatis配置文件【可阅】
对配置文件不熟悉的可以阅读一下。
参考博客链接:【MyBatis】第二章:MyBatis xml配置文件详解
在使用mybatis框架时,首先导入其对应的jar包,并进行相应的配置,所以得对配置文件的每个参数都得了解。一个完全的mybatis配置文件结构如下:
<?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><!-- 属性:定义配置外在化 --><properties></properties><!-- 设置:定义mybatis的一些全局性设置 --><settings><!-- 具体的参数名和参数值 --><setting name="" value=""/> </settings><!-- 类型名称:为一些类定义别名 --><typeAliases></typeAliases><!-- 类型处理器:定义Java类型与数据库中的数据类型之间的转换关系 --><typeHandlers></typeHandlers><!-- 对象工厂 --><objectFactory type=""></objectFactory><!-- 插件:mybatis的插件,插件可以修改mybatis的内部运行规则 --><plugins><plugin interceptor=""></plugin></plugins><!-- 环境:配置mybatis的环境 --><environments default=""><!-- 环境变量:可以配置多个环境变量,比如使用多数据源时,就需要配置多个环境变量 --><environment id=""><!-- 事务管理器 --><transactionManager type=""></transactionManager><!-- 数据源 --><dataSource type=""></dataSource></environment> </environments><!-- 数据库厂商标识 --><databaseIdProvider type=""></databaseIdProvider><!-- 映射器:指定映射文件或者映射类 --><mappers></mappers>
</configuration>
properties
properties元素主要是用来定义配置外在化,比如数据库的连接属性等。这些属性都是可外部配置且可动态替换的,既可以在典型的Java属性文件中配置,亦可以通过properties元素的子元素来传递。例如:
<properties resource="org/mybatis/example/config.properties"><property name="username" value="root"/><property name="password" value="123456"/>
</properties>
其中的属性就可以在整个配置文件中使用来替换需要动态配置的属性值。比如在数据源中使用的例子:
<dataSource type="POOLED"><property name="driver" value="${driver}"/><property name="url" value="${url}"/><property name="username" value="${username}"/><property name="password" value="${password}"/>
</dataSource>
这个例子中的 username
和 password
将会由 properties
元素中设置的相应值来替换。driver
和 url
属性将会由config.properties
文件中对应的值来替换。这样就为配置提供了诸多灵活选择。属性也可以被传递到SqlSessionBuilder.build()
方法中。例如:
SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader, props);// or
SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader, environment, props);
但是,这也就涉及到了优先级的问题,如果属性不只在一个地方配置,那么mybatis将会按照下面的顺序来加载:
- 在properties元素体内指定的属性首先被读取。
- 然后根据properties元素中的resource属性读取类路径下属性文件或根据url属性指定的路径读取属性文件,并覆盖已读取的同名属性。
- 最后读取作为方法参数传递的属性,并覆盖已读取的同名属性。
因此,通过方法参数传递的属性具有最高优先级,resource/url
属性中指定的配置文件次之,最低优先级的是properties
属性中指定的属性。
settings
setting是指定MyBatis的一些全局配置属性,这是MyBatis中极为重要的调整设置,它们会改变MyBatis的运行时行为,所以我们需要清楚的知道这些属性的作用及默认值。
设置参数 | 描述 | 有效值 | 默认值 |
---|---|---|---|
cacheEnabled | 该配置影响的所有映射器中配置的缓存的全局开关 | true 、false | true |
lazyLoadingEnabled | 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态 | true、false | false |
aggressiveLazyLoading | 当启用时,对任意延迟属性的调用会使带有延迟加载属性的对象完整加载;反之,每种属性将会按需加载。 | true、false | true |
multipleResultSetsEnabled | 是否允许单一语句返回多结果集(需要兼容驱动)。 | true、flase | true |
useColumnLabel | 使用列标签代替列名。不同的驱动在这方面会有不同的表现, 具体可参考相关驱动文档或通过测试这两种不同的模式来观察所用驱动的结果。 | true、false | true |
useGeneratedKeys | 允许 JDBC 支持自动生成主键,需要驱动兼容。 如果设置为 true 则这个设置强制使用自动生成主键,尽管一些驱动不能兼容但仍可正常工作(比如 Derby)。 | true、false | false |
autoMappingBehavior | 指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示取消自动映射;PARTIAL 只会自动映射没有定义嵌套结果集映射的结果集。 FULL 会自动映射任意复杂的结果集(无论是否嵌套)。 | NONE, PARTIAL, FULL | PARTIAL |
defaultExecutorType | 配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新。 | SIMPLE REUSE BATCH | SIMPLE |
defaultStatementTimeout | 设置超时时间,它决定驱动等待数据库响应的秒数。 | Any positive integer | Not Set (null) |
defaultFetchSize | Sets the driver a hint as to control fetching size for return results. This parameter value can be override by a query setting. | Any positive integer | Not Set (null) |
safeRowBoundsEnabled | 允许在嵌套语句中使用分页(RowBounds)。 | true、false | false |
mapUnderscoreToCamelCase | 是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射。 | true、false | false |
localCacheScope | MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。 默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据。 | SESSION、STATEMENT | SESSION |
jdbcTypeForNull | 当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。 某些驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。 | JdbcType enumeration. Most common are: NULL, VARCHAR and OTHER | OTHER |
lazyLoadTriggerMethods | 指定哪个对象的方法触发一次延迟加载。 | A method name list separated by commas | equals,clone,hashCode,toString |
defaultScriptingLanguage | 指定动态 SQL 生成的默认语言。 | A type alias or fully qualified class name. | org.apache.ibatis.scripting.xmltags.XMLDynamicLanguageDriver |
callSettersOnNulls | 指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这对于有 Map.keySet() 依赖或 null 值初始化的时候是有用的。注意基本类型(int、boolean等)是不能设置成 null 的。 | true、false | false |
logPrefix | 指定 MyBatis 增加到日志名称的前缀。 | Any String | Not set |
logImpl | 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 | SLF4J、LOG4J、LOG4J2、JDK_LOGGING、COMMONS_LOGGING、STDOUT_LOGGING、NO_LOGGING | Not set |
proxyFactory | 指定 Mybatis 创建具有延迟加载能力的对象所用到的代理工具。 | CGLIB、JAVASSIST | JAVASSIST (MyBatis 3.3 or above) |
一个完整的settings元素示例如下:
<settings><setting name="cacheEnabled" value="true"/><setting name="lazyLoadingEnabled" value="true"/><setting name="multipleResultSetsEnabled" value="true"/><setting name="useColumnLabel" value="true"/><setting name="useGeneratedKeys" value="false"/><setting name="autoMappingBehavior" value="PARTIAL"/><setting name="defaultExecutorType" value="SIMPLE"/><setting name="defaultStatementTimeout" value="25"/><setting name="defaultFetchSize" value="100"/><setting name="safeRowBoundsEnabled" value="false"/><setting name="mapUnderscoreToCamelCase" value="false"/><setting name="localCacheScope" value="SESSION"/><setting name="jdbcTypeForNull" value="OTHER"/><setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>
typeAliases
类型别名是为Java类型设置一个短的名字。它只和xml配置有关,存在的意义仅在于用来减少类完全限定名的冗余,例如:
<typeAliases><typeAlias alias="Author" type="domain.blog.Author"/><typeAlias alias="Blog" type="domain.blog.Blog"/><typeAlias alias="Comment" type="domain.blog.Comment"/><typeAlias alias="Post" type="domain.blog.Post"/><typeAlias alias="Section" type="domain.blog.Section"/><typeAlias alias="Tag" type="domain.blog.Tag"/>
</typeAliases>
当这样配置时,Blog可以用在任何使用domain.blog.Blog的地方。
也可以指定一个包名,MyBatis会在包名下搜索需要的JavaBean,比如:
<typeAliases><package name="domain.blog"/>
</typeAliases>
每一个在包domain.blog中的JavaBean,在没有注解的情况下,会使用Bean的首字母小写的非限类名来作为它的别名。比如domain.blog.Author的别名为author;若有注解,则别名为注解值。看下面的例子:
@Alias("author")
public class Author {...
}
已经为许多常见的Java类型内建了相应的类型别名。它们都是大小写不敏感的,需要注意的是有基本类型名称重复导致的特殊处理。
别名 | 映射的类型 |
---|---|
_byte | byte |
_long | long |
_short | short |
_int | int |
_integer | int |
_double | double |
_float | float |
__boolean | boolean |
string | String |
byte Byte | String |
long | Long |
short | Short |
int | Integer |
integer | Integer |
double | Double |
float | Float |
boolean | Boolean |
date | Date |
decimal | BigDecimal |
bigdecimal | BigDecimal |
object | Object |
map | Map |
hashmap | HashMap |
list | List |
arraylist | ArrayList |
collection | Collection |
iterator | Iterator |
typeHandlers
无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时, 都会用类型处理器将获取的值以合适的方式转换成 Java 类型。下表描述了一些默认的类型处理器。
类型处理器 | Java 类型 | JDBC 类型 |
---|---|---|
BooleanTypeHandler | java.lang.Boolean, boolean | 数据库兼容的 BOOLEAN |
ByteTypeHandler | java.lang.Byte, byte | 数据库兼容的 NUMERIC 或 BYTE |
ShortTypeHandler | java.lang.Short, short | 数据库兼容的 NUMERIC 或 SHORT INTEGER |
IntegerTypeHandler | java.lang.Integer, int | 数据库兼容的 NUMERIC 或 INTEGER |
LongTypeHandler | java.lang.Long, long | 数据库兼容的 NUMERIC 或 LONG INTEGER |
FloatTypeHandler | java.lang.Float, float | 数据库兼容的 NUMERIC 或 FLOAT |
DoubleTypeHandler | java.lang.Double, double | 数据库兼容的 NUMERIC 或 DOUBLE |
BigDecimalTypeHandler | java.math.BigDecimal | 数据库兼容的 NUMERIC 或 DECIMAL |
StringTypeHandler | java.lang.String | CHAR, VARCHAR |
ClobTypeHandler | java.lang.String | CLOB, LONGVARCHAR |
NStringTypeHandler | java.lang.String | NVARCHAR, NCHAR |
NClobTypeHandler | java.lang.String | NCLOB |
ByteArrayTypeHandler | byte[] | 数据库兼容的字节流类型 |
BlobTypeHandler | byte[] | BLOB, LONGVARBINARY |
DateTypeHandler | java.util.Date | TIMESTAMP |
DateOnlyTypeHandler | java.util.Date | DATE |
TimeOnlyTypeHandler | java.util.Date | TIME |
SqlTimestampTypeHandler | java.sql.Timestamp | TIMESTAMP |
SqlDateTypeHandler | java.sql.Date | DATE |
SqlTimeTypeHandler | java.sql.Time | TIME |
ObjectTypeHandler | Any | OTHER 或未指定类型 |
EnumTypeHandler | Enumeration Type | VARCHAR-任何兼容的字符串类型,存储枚举的名称(而不是索引) |
EnumOrdinalTypeHandler | Enumeration Type | 任何兼容的 NUMERIC 或 DOUBLE 类型,存储枚举的索引(而不是名称)。 |
可以重写类型处理器或创建自己的类型处理器来处理不支持的或非标准的类型。具体的做法为:实现org.apache.ibatis.type.TypeHandler
接口,或继承一个很便利的类org.apache.ibatis.type.BaseTypeHandler
,然后可以选择性地将它映射到一个JDBC类型。比如:
// ExampleTypeHandler.java
@MappedJdbcTypes(JdbcType.VARCHAR)
public class ExampleTypeHandler extends BaseTypeHandler<String> {@Overridepublic void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {ps.setString(i, parameter);}@Overridepublic String getNullableResult(ResultSet rs, String columnName) throws SQLException{return rs.getString(columnName);}@Overridepublic String getNullableResult(ResultSet rs, int columnIndex) throws SQLException{return rs.getString(columnIndex);}@Overridepublic String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {return cs.getString(columnIndex);}}
并且还需要在配置文件里面加上:
<!-- mybatis-config.xml -->
<typeHandlers><typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>
使用这个的类型处理器将会覆盖已经存在的处理Java 的 String
类型属性和 VARCHAR
参数及结果的类型处理器。要注意MyBatis不会窥探数据库元信息来决定使用哪种类型,所以必须在参数和结果映射中指明是 VARCHAR
类型字段,以使其能绑定到正确的类型处理器上。这是因为,MyBatis直到语句被执行才清楚数据类型。
通过类型处理器的泛型,MyBatis可以得知该类型处理器的Java类型,不过这种行为可以通过两种方法来指定被关联的JDBC类型:
- 在类型处理器的元素(typeHandler element)上增加一个javaType属性(比如,javaType=“String”);
- 在类型处理器的类上(TypeHandler class)增加一个@MappedTypes注解来指定与其关联的Java类型列表。如果在javaType属性中也同时制定,则注解方式将被忽略。
最后,还可以让MyBatis查找类型处理器:
<!-- mybatis-config.xml -->
<typeHandlers><package name="org.mybatis.example"/>
</typeHandlers>
注意在使用自动检索(autodiscovery)功能的时候,只能通过注解的方式来指定JDBC类型。
你能创建一个泛型类型处理器,它可以处理多于一个类。为达到此目的,需要增加一个接收该类作为参数的构造器,这样在构造一个类型处理器的时候MyBatis就会传入一个具体的类。
//GenericTypeHandler.java
public class GenericTypeHandler<E extends MyObject> extends BaseTypeHandler<E> {private Class<E> type;public GenericTypeHandler(Class<E> type) {if (type == null) throw new IllegalArgumentException("Type argument cannot be null");this.type = type;}...
}
EnumTypeHandler
和EnumOrdinalTypeHandler
都是泛型处理器(generic TypeHandlers
),接下来的部分详细探讨。
处理枚举类型
若想映射枚举类型Enum
,则需要从EnumTypeHandler
或者 EnumOrdinalTypeHandler
中选一个来使用
比如说我们想存储近似值时用到的舍入模式。默认情况下,MyBatis会利用EnumTypeHandler
来把Enum
值转换成对应的名字。
注意 EnumTypeHandler
在某种意义上来说是比较特别的,其他的处理器只针对某个特定的类,而它不同,它会处理任意继承了Enum
的类。
不过,我们可能不想存储名字,相反我们的DBA会坚持使用整形值代码。那也一样轻而易举;在配置文件中把EnumOrdinalTypeHandler
加到typeHandlers
中即可,这样每个RoundingMode
将通过他们的序数值来映射成对应的整形。
<!-- mybatis-config.xml -->
<typeHandlers><typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="java.math.RoundingMode"/>
</typeHandlers>
但是怎么样能将同样的 Enum
既映射成字符串又映射成整形呢?
自动映射器( auto-mapper
)会自动选用 EnumOrdinalTypeHandler
来处理,所以如果我们想用普通的EnumTypeHandler
,就非要为那些SQL语句显示地设置要用到的类型处理器不可。
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="org.apache.ibatis.submitted.rounding.Mapper"><resultMap type="org.apache.ibatis.submitted.rounding.User" id="usermap"><id column="id" property="id"/><result column="name" property="name"/><result column="funkyNumber" property="funkyNumber"/><result column="roundingMode" property="roundingMode"/></resultMap><select id="getUser" resultMap="usermap">select * from users</select><insert id="insert">insert into users (id, name, funkyNumber, roundingMode)values(#{id},#{name},#{funkyNumber},#{roundingMode})</insert><resultMap type="org.apache.ibatis.submitted.rounding.User" id="usermap2"><id column="id" property="id"/><result column="name" property="name"/><result column="funkyNumber" property="funkyNumber"/><result column="roundingMode" property="roundingMode" typeHandler="org.apache.ibatis.type.EnumTypeHandler"/></resultMap><select id="getUser2" resultMap="usermap2">select * from users2</select><insert id="insert2">insert into users2 (id, name, funkyNumber, roundingMode) values(#{id}, #{name}, #{funkyNumber}, #{roundingMode, typeHandler=org.apache.ibatis.type.EnumTypeHandler})</insert></mapper>
注意,这里的select语句强制使用resultMap来代替resultType。
objectFactory
MyBatis每次创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory
)实例来完成。默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认构造方法,要么在参数映射存在的时候通过参数构造方法来实例化。如果想覆盖对象工厂的行为,则可以通过创建自己的对象工厂来实现,比如:
//ExampleObjectFactory.java
public class ExampleObjectFactory extends DefaultObjectFactory {public Object create(Class type) {return super.create(type);}public Object create(Class type, List<Class> constructorArgTypes, List<Object> constructorArgs) {return super.create(type, constructorArgTypes,constructorArgs);}public void setProperties(Properties properties) {super.setProperties(properties);}public <T> boolean isCollection(Class<T> type) {return Collection.class.isAssignableFrom(type);}
}
<!-- mybatis-config.xml -->
<objectFactory type="org.mybatis.example.ExampleObjectFactory"><property name="someProperty" value="100"/>
</objectFactory>
ObjectFactory接口很简单,它包含两个创建用的方法,一个是处理默认构造方法的,另外一个是处理带参数的构造方法。最后setProperties方法可以被用来配置ObjectFactory,初始化你的ObjectFactory实例后,objectFactory元素体内定义的属性会被传递给setProperties方法。
plugins
MyBatis允许你在已映射的语句执行过程中的某一点进行拦截调用。默认情况下,Mybatis允许使用插件来拦截的方法调用包括:
- Executor(update,query,flushStatements,commit,rollback,getTransaction,close,isClosed)
- ParameterHandler(getParameterObejct,setParameters)
- ResultSetHandler(handlerResultSets,handlerOutputParameters)
- StatementHandler(prepare,parameterize,batch,update,query)
这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看MyBatis的发行包中的源代码。假设你想做的不仅仅是方法的调用,那么你应该很好的了解正在重写的方法的行为。因为如果在视图修改或重写已有方法的行为的时候,你很有可能在破坏MyBatis的核心模块。这些都是更低层的类和方法,所以使用插件的时候要特别担心。
通过MyBatis提供强大的机制,使用插件是非常简单的,只需要实现Interceptor接口,并指定想要拦截的方法签名即可。
// ExamplePlugin.java
@Intercepts({@Signature(type= Executor.class,method = "update",args = {MappedStatement.class,Object.class})}
)
public class ExamplePlugin implements Interceptor {public Object intercept(Invocation invocation) throws Throwable {return invocation.proceed();}public Object plugin(Object target) {return Plugin.wrap(target,this);}public void setProperties(Properties properties) {}
}
<!-- mybatis-config.xml -->
<plugins><plugin interceptor="org.mybatis.example.ExamplePlugin"><property name="someProperty" value="100"/></plugin>
</plugins>
上面的插件将会拦截Executor
实例中所有的“update”方法调用,这里的Executor
是负责执行底层映射语句的内部对象。
覆盖配置类
除了用插件来修改MyBatis核心行为之外,还可以通过完全覆盖配置类来达到目的。只需继承后覆盖其中的每个方法,再把它传递到sqlSessionFactoryBuilder.build(myConfig)
方法即可。再次重申,这可能会严重影响Mybatis的行为,务请慎之又慎!
environments
MyBatis可以配置成适应多种环境,这种机制有助于将sql映射应用于多种数据库中,现实情况下有多种理由需要这么做。例如,开发、测试和生产环境需要有不同的配置;或者共享相同的Schema
的多个生产数据库,想使用相同的sql映射。许多类似的用例。
尽管可以配置多个环境,但是每个SqlSessionFactory
实例只能选择其一。
所以,如果想连接两个数据库,就需要创建两个SqlSessionFactory
实例,每个数据库对应一个。而如果是三个数据库,就需要三个实例,依此类推。
每个数据库对应一个SqlSessionFactory实例。
为了指定创建哪种环境,只要将它作为可选参数传递给SqlSessionFactoryBuilder即可。可以接受环境配置的两个方法签名是:
SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader, environment);
SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader, environment,properties);
如果忽略了环境参数,那么默认环境将会被加载,如下所示:
SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader);
SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader,properties);
环境元素定义了如何配置环境:
<environments default="development"><environment id="development"><transactionManager type="JDBC"><property name="..." value="..."/></transactionManager><dataSource type="POOLED"><property name="driver" value="${driver}"/><property name="url" value="${url}"/><property name="username" value="${username}"/><property name="password" value="${password}"/></dataSource></environment>
</environments>
注意这里的关键点:
- 默认环境的ID(比如:default=“development”)
- 每个environment元素定义的环境ID(比如:id=“development”)
- 事务管理器的配置(比如:type=“JDBC”)
- 数据源的配置(比如:type=“POOLED”)
默认的环境和环境ID是一目了然的。随你怎么命名,只要保证默认环境要匹配其中一个环境ID事务管理器(transactionManager)。
在MyBatis中有两种类型的事务管理器(也就是 type="[JDBC|MANAGED]")
-
JDBC:这个配置就是直接使用了JDBC的提交和回滚设置,它依赖于从数据源得到的连接来管理事务范围。
-
MANAGED:这个配置几乎没做什么。它从来不提交或回滚一个连接,而是让容器来管理事务的整个生命周期(比如JEE应用服务器上下文)。默认情况下它会关闭连接,然而一些容器并不希望这样,因此需要将closeConnection属性设置为false来阻止它默认的行为。例如:
<transactionManager type="MANAGED"><property name="closeConnection" value="false"/> </transactionManager>
如果正在使用Spring+MyBatis,则没有必要配置事务管理器,因为Spring模块会使用自带的管理器来覆盖前面的配置。
这两种事务管理器类型都不需要任何属性。它们只不过是类型别名,换句话说,你可以使用TransactionFactory
接口的实现类的完全限定名或类型别名替代它们。
public interface TransactionFactory {void setProperties(Properties props); Transaction newTransaction(Connection conn);Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit); }
任何在xml中配置的属性在实例化之后将会被传递给setProperties方法。你也需要创建一个Transaction接口的实现类,这个接口也很简单。
public interface Transaction{Connection getConnection() throws SQLException;void commit() throws SQLException;void rollback() throws SQLException;void close() throws SQLException;}
使用这两个接口,完全可以自定义MyBatis对事务的处理。
数据源(dataSource)
dataSource元素使用了标准的JDBC数据源接口来配置JDBC连接对象的资源。
许多MyBatis的应用程序将会按示例中的例子来配置数据源。然而它并不是必须的。要知道为了方便使用延迟加载,数据源才是必须的。
有三种内建的数据源类型(也就是 type="[UNPOOLED|POOLED|JNDI]"):
UNPOOLED
这个数据源的实现只是被请求时打开和关闭连接。虽然有一点慢,它对在及时可用连接方面没有性能要求的简单应用是一个很好的选择。不同的数据库在这方面表现也是不一样的,所以对某些数据库来说使用连接池并不重要,这个配置也是理想。UNPOOLED类型的数据源仅仅需要配置以下5种属性:
- driver – 这是JDBC驱动的Java类的完全限定名(并不是JDBC驱动中可能包含的数据源类)
- url – 这是数据库的JDBC URL 地址。
- username – 登录数据库的用户名。
- password – 登录数据库的密码。
- defaultTransactionIsolationLevel – 默认的连接事务隔离级别。
作为可选项,可以传递属性给数据库驱动。要这样做,属性的前缀为"driver.",例如:
- driver.encoding=UTF-8
这将通过DriverManager,getConnection(url,driverProperties)方法传递值为UTF-8的encoding属性给数据库驱动。
POOLED
这种数据源的实现利用“池”的概念将JDBC连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。这是一种使得并发web应用快速响应请求的流行处理方式。
除了上述提到UNPOOLED下的属性外,会有更多属性用来配置POOLED的数据源:
- poolMaximumActiveConnections – 在任意时间可以存在的活动(也就是正在使用)连接数量,默认值10。
- poolMaximumIdleConnections – 任意时间可能存在的空闲连接数。
- poolMaximumCheckoutTime – 在被强制返回之前,池中连接被检出(checked out)时间,默认值:20000毫秒(即20秒)。
- poolTimeToWait – 这是一个底层设置,如果获取连接花费的相当长的时间,它会给连接池打印状态日志并重新尝试获取一个连接(避免在误配置的情况下一直安静的失败),默认值20000毫秒(即20秒)。
- poolPingQuery – 发送到数据库的侦测查询,用来检验连接是否处在正常的工作秩序中,并且准备接受请求。默认是"NOT PING QUERY SET",这会导致多数数据库连接失败时带有一个恰当的错误信息。
- poolPingEnabled – 是否启用侦测。若开启,也必须使用一个可执行的SQL语句设置poolPingQuery属性(最好是一个非常快的SQL),默认值:false。
- poolPingConnectionsNotUsedFor – 配置poolPingQuery使用的频度。这可以被设置成匹配具体的数据库连接超时时间,来避免不必要的侦测,默认值:0(即所有连接每一时刻都被侦测 – 当然仅当 poolPingEnabled为true时适用)。
JNDI
这个数据源的实现是为了能在如EJB或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个JNDI上下文的引用。这种数据源配置只要两个属性:
- initial_context – 这个属性用来在InitialContext中寻找上下文(即,initialContext.lookup(initial_context))。这是个可选属性,如果忽略,那么data_source属性将会直接从InitialContext中寻找。
- data_source – 这是引用数据源实例位置的上下文的路径。提供了 initial_context配置时会在其返回的上下文中进行查找,没有提供时则直接在InitialContext中查找。
和其他数据源配置类似,可以通过添加前缀"env."直接把属性传递给初始上下文。比如:
- env.encoding=UTF-8
这会在初始上下文(InitialContext)实例化时往它的构造方法传递值为UTF-8的encoding属性。
通过需要实现接口 org.apache.ibatis.datasource.DataSourceFactory,也可使用任何第三方数据源:
public interface DataSourceFactory {void setProperties(Properties props);DataSource getDataSource();}
org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory 可被用作父类来构建新的数据源适配器,比如下面这段插入C3P0所必需的代码:
import org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory;
import com.mchange.v2.c3p0.ComboPooledDataSource;public class C3P0DataSourceFactory extends UnpooledDataSourceFactory {public C3P0DataSourceFactory() {this.dataSource = new ComboPooledDataSource();}
}
为了令其工作,为每个需要MyBatis调用的setter方法中增加一个属性。下面是一个可以连接到PostgreSQL数据库的例子:
<dataSource type="org.myproject.C3P0DataSourceFactory"><property name="driver" value="org.postgresql.Driver"/><property name="url" value="jdbc:postgresql:mydb"/><property name="username" value="postgres"/><property name="password" value="root"/>
</dataSource>
databaseIdProvider
MyBatis可以根据不同的数据库厂商执行不同的语句,这种多厂商的支持是基于映射语句中的databaseId
属性。MyBatis会加载不带databaseId
属性和带有匹配当前数据库databaseId
属性的所有语句。如果同时找到带有databaseId
和不带databaseId
的相同语句,则后者被舍弃。为支持多厂商特性,只要像下面这样在mybatis-config.xml
文件中加入databaseIdProvider
即可:
<databaseIdProvider type="DB_VENDOR" />
这里的DB_VENDOR
会通过DatabaseMetaData#getDatabaseProductName()
返回的字符串进行设置。由于通常情况下这个字符串都非常长而且相同产品的不同版本会返回不同的值,所以最好通过设置属性别名来使其变短,如下:
<databaseIdProvider type="DB_VENDOR"><property name="SQL Server" value="sqlserver"/><property name="DB2" value="db2"/> <property name="Oracle" value="oracle" />
</databaseIdProvider>
在有properties
时,DB_VENDOR databaseIdProvider
的将被设置为第一个能匹配数据库产品名称的属性键值对应的值,如果没有匹配的属性将会设置为”null“。在这个例子中,如果getDatabaseProductName()
返回”Oracle(DataDirect)“,databaseId将被设置为"oracle"。
可以通过实现接口org.apache.ibatis.mapping.DatabaseIdProvider
并在mybatis-config.xml
中注册来构建自己的DatabaseIdProvider
:
public interface DatabaseIdProvider {void setProperties(Properties p);String getDatabaseId(DataSource dataSource) throws SQLException;}
mappers
既然MyBatis的行为已经由上述元素配置完了,现在就要定义SQL映射语句了。但是首先需要告诉MyBatis到哪里去找到这些语句。Java在自动查找这方面没有提供一个很好的方法,所以最佳的方式是告诉MyBatis到哪里去找映射文件。可以使用相对于类路径的资源引用、或完全限定资源定位符(包括file:///的URL),或类名和包名等等。例如:
<!-- Using classpath relative resources -->
<mappers><mapper resource="org/mybatis/builder/AuthorMapper.xml"/><mapper resource="org/mybatis/builder/BlogMapper.xml"/><mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- Using url fully qualified paths -->
<mappers><mapper url="file:///var/mappers/AuthorMapper.xml"/><mapper url="file:///var/mappers/BlogMapper.xml"/><mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<!-- Using mapper interface classes -->
<mappers><mapper class="org.mybatis.builder.AuthorMapper"/><mapper class="org.mybatis.builder.BlogMapper"/><mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- Register all interfaces in a package as mappers -->
<mappers><package name="org.mybatis.builder"/>
</mappers>
这些配置会告诉了MyBatis去哪里找映射文件,剩下的细节就应该是每个SQL映射文件了。
Mybatis源码浅析
这部分内容仅涉及SqlSessionFactoryBuilder
、SqlSession
、 MapperProxy
、插件
等源码部分,对于日志
、cache
等仅提供博客链接参考。
本段落参考博客链接:Mybatis运行原理及源码解析
引入
项目的搭建我们可以参照官方文档。本次基于mybatis-3.5.3.jar版本搭建分析,不加入Spring的整合。参考官方文档,我们只需要创建mybatis-config.xml
和 mapper.xml
文件以及对应的 mapper
接口。为了方便大家阅读,搭建源码如下:
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configurationPUBLIC "-//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://127.0.0.1:3306/test"/><property name="username" value="root"/><property name="password" value="123456"/></dataSource></environment></environments><mappers><mapper resource="mybatis/mapper/UserMapper.xml"/></mappers>
</configuration>
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="mapper.UserMapper"><resultMap id="User" type="pojo.User"><result column="user_id" property="userId" jdbcType="INTEGER" javaType="Integer"/><result column="user_account" property="userAccount" jdbcType="VARCHAR" javaType="String"/><result column="user_createtime" property="userCreatetime" jdbcType="TIMESTAMP"javaType="java.time.LocalDateTime"/></resultMap><sql id="userColumn">user_id,user_account,user_createtime</sql><select id="getUserById" parameterType="java.lang.String" resultMap="User">SELECT<include refid="userColumn"/>FROM `user`whereuser_id = #{userId}</select>
</mapper>
UserMapper.java
public interface UserMapper {WxUser getUserById(String userId);
}
MybatisTest.java入口测似类
public static void main(String[] args) {try {String resource = "mybatis-config.xml";// 通过classLoader获取到配置文件InputStream inputStream = Resources.getResourceAsStream(resource);SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();// 把配置文件和mapper文件封装到Configuration实体类SqlSessionFactory sqlSessionFactory = builder.build(inputStream);SqlSession sqlSession = sqlSessionFactory.openSession();// 动态代理方式UserMapper mapper = sqlSession.getMapper(UserMapper.class);User user = mapper.getUserById("8");System.out.println("通过动态代理返回结果" + user.getUserAccount());// 不用动态代理的方式直接statement获取查询User u2 = sqlSession.selectOne("mapper.UserMapper.getUserById", "8");System.out.println("通过statement返回结果" + u2.getUserAccount());} catch (Exception e) {e.printStackTrace();}
}
SqlSessionFactoryBuilder
由于 SqlSessionFactory
初始化需要的参数比较多,所以Mybatis这里采用了构造者模式通过xml的方式实例化一个 SqlSessionFactory
对象。具体的属性和配置可以查看官方文档。通过查看 SqlSessionFactoryBuilder
的build()方法分析源码,主要逻辑看代码注释(最好结合源码对照查看)。
public class SqlSessionFactoryBuilder {public SqlSessionFactory build(Reader reader, String environment, Properties properties) {try {// 主要是把配置文件构造成XMLConfigBuilder对象 XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);return build(parser.parse());} catch (Exception e) {throw ExceptionFactory.wrapException("Error building SqlSession.", e);} finally {ErrorContext.instance().reset();try {reader.close();} catch (IOException e) {// Intentionally ignore. Prefer previous error.}}}public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {try {// 主要是把配置文件构造成XMLConfigBuilder对象 // 通俗的说就是拿到config.xml的inputStream,然后解析xml,把配置的信息封装到parser对象里面XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);return build(parser.parse());} catch (Exception e) {throw ExceptionFactory.wrapException("Error building SqlSession.", e);} finally {ErrorContext.instance().reset();try {inputStream.close();} catch (IOException e) {// Intentionally ignore. Prefer previous error.}}}public SqlSessionFactory build(Configuration config) {return new DefaultSqlSessionFactory(config);}
}
XMLConfigBuilder#parse()
// 解析config.xml和所有的mapper.xml 封装成Configuration对象返回
public Configuration parse() {if (parsed) {throw new BuilderException("Each XMLConfigBuilder can only be used once.");}parsed = true;parseConfiguration(parser.evalNode("/configuration"));return configuration;
}
XMLConfigBuilder#parseConfiguration()
private void parseConfiguration(XNode root) {try {//issue #117 read properties first/**解析配置文件中的各种属性*/propertiesElement(root.evalNode("properties"));/**解析mybatis的全局设置信息*/Properties settings = settingsAsProperties(root.evalNode("settings"));loadCustomVfs(settings);loadCustomLogImpl(settings);/**解析别名配置*/typeAliasesElement(root.evalNode("typeAliases"));/**解析插件配置*/pluginElement(root.evalNode("plugins"));/**解析对象工厂元素*/objectFactoryElement(root.evalNode("objectFactory"));objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));reflectorFactoryElement(root.evalNode("reflectorFactory"));settingsElement(settings);// read it after objectFactory and objectWrapperFactory issue #631/**解析mybatis的环境配置*/environmentsElement(root.evalNode("environments"));databaseIdProviderElement(root.evalNode("databaseIdProvider"));/**解析类型处理器配置信息*/typeHandlerElement(root.evalNode("typeHandlers"));/**解析mapper配置信息*/mapperElement(root.evalNode("mappers"));} catch (Exception e) {throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);}
}
实际上就是解析主配置文件中的各个节点,然后保存在 Configuration
当中,然后使用 Configuration
创建出一个 DefaultSqlsessionFactory
对象。
此处,我们可以重点关注如下两个地方,看看具体在做了什么动作:
pluginElement(root.evalNode("plugins"));
mapperElement(root.evalNode("mappers"));
插件注册
pluginElement(root.evalNode("plugins"));
点进去查看详细实现:
private void pluginElement(XNode parent) throws Exception {if (parent != null) {for (XNode child : parent.getChildren()) {// 获取到内容 比如com.github.pagehelper.PageHelperString interceptor = child.getStringAttribute("interceptor");// 获取配置的属性信息Properties properties = child.getChildrenAsProperties();// 创建的拦截器实例Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();// 将属性和拦截器绑定interceptorInstance.setProperties(properties);/** 将实例化的拦截器类放到configuration中的interceptorChain中 */configuration.addInterceptor(interceptorInstance);}}
}
实际上就是通过interceptor标签,解析出拦截器类,然后将其实例化并保存到Configuration类中的InterceptorChain中,以备后用。
public void addInterceptor(Interceptor interceptor) {// 将拦截器添加到了 拦截器链中 而拦截器链本质上就是一个List有序集合this.interceptorChain.addInterceptor(interceptor);
}
mapper扫描与解析
mapperElement(root.evalNode("mappers"));
点进去查看详细实现:
private void mapperElement(XNode parent) throws Exception {if (parent != null) {// 遍历config.xml所有的<mappers>// <mapper resource=""/>// </mappers>for (XNode child : parent.getChildren()) {/*如果子节点是配置的<package>,那么进行包自动扫描处理*/if ("package".equals(child.getName())) {String mapperPackage = child.getStringAttribute("name");configuration.addMappers(mapperPackage);} else {String resource = child.getStringAttribute("resource");String url = child.getStringAttribute("url");String mapperClass = child.getStringAttribute("class");/**如果子节点配置的是resource、url、mapperClass,本文我们使用的是resource*/if (resource != null && url == null && mapperClass == null) {ErrorContext.instance().resource(resource);// 获取到mapper.xml文件InputStream inputStream = Resources.getResourceAsStream(resource);// 把xml封装成对象XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());// 解析封装到Configuration对象中mapperParser.parse();} else if (resource == null && url != null && mapperClass == null) {ErrorContext.instance().resource(url);InputStream inputStream = Resources.getUrlAsStream(url);/**解析resource引入的另外一个xml文件*/XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());mapperParser.parse();} else if (resource == null && url == null && mapperClass != null) {Class<?> mapperInterface = Resources.classForName(mapperClass);configuration.addMapper(mapperInterface);} else {throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");}}}}
}
下面我们具体看一下他是如何解析另一个xml文件的:
// mapperParser.parse()
public void parse() {if (!configuration.isResourceLoaded(resource)) {/*解析sql语句*/configurationElement(parser.evalNode("/mapper"));configuration.addLoadedResource(resource);/*绑定mapper的namespace即解析名称空间,实际上就是对应绑定的接口类*/bindMapperForNamespace();}parsePendingResultMaps();parsePendingCacheRefs();parsePendingStatements();
}
下面我们来看一下 configurationElement(parser.evalNode("/mapper"))
到底做了什么:
private void configurationElement(XNode context) {try {// mapper映射文件的namespace字段String namespace = context.getStringAttribute("namespace");if (namespace == null || namespace.equals("")) {throw new BuilderException("Mapper's namespace cannot be empty");}builderAssistant.setCurrentNamespace(namespace);cacheRefElement(context.evalNode("cache-ref"));cacheElement(context.evalNode("cache"));parameterMapElement(context.evalNodes("/mapper/parameterMap"));resultMapElements(context.evalNodes("/mapper/resultMap"));sqlElement(context.evalNodes("/mapper/sql"));buildStatementFromContext(context.evalNodes("select|insert|update|delete"));} catch (Exception e) {throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);}
}
我们来看一下buildStatementFromContext(context.evalNodes("select|insert|update|delete"))
到底做了什么:
private void buildStatementFromContext(List<XNode> list) {if (configuration.getDatabaseId() != null) {buildStatementFromContext(list, configuration.getDatabaseId());}buildStatementFromContext(list, null);
}
我们来看一下buildStatementFromContext()
的重载方法:
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {for (XNode context : list) {final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);try {statementParser.parseStatementNode();} catch (IncompleteElementException e) {configuration.addIncompleteStatement(statementParser);}}}
来看一下statementParser.parseStatementNode()
方法里:
public void parseStatementNode() {String id = context.getStringAttribute("id");String databaseId = context.getStringAttribute("databaseId");if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {return;}String nodeName = context.getNode().getNodeName();SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));boolean isSelect = sqlCommandType == SqlCommandType.SELECT;boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);boolean useCache = context.getBooleanAttribute("useCache", isSelect);boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);// Include Fragments before parsingXMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);includeParser.applyIncludes(context.getNode());String parameterType = context.getStringAttribute("parameterType");Class<?> parameterTypeClass = resolveClass(parameterType);String lang = context.getStringAttribute("lang");LanguageDriver langDriver = getLanguageDriver(lang);// Parse selectKey after includes and remove them.processSelectKeyNodes(id, parameterTypeClass, langDriver);// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)KeyGenerator keyGenerator;String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);if (configuration.hasKeyGenerator(keyStatementId)) {keyGenerator = configuration.getKeyGenerator(keyStatementId);} else {keyGenerator = context.getBooleanAttribute("useGeneratedKeys",configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;}SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));Integer fetchSize = context.getIntAttribute("fetchSize");Integer timeout = context.getIntAttribute("timeout");String parameterMap = context.getStringAttribute("parameterMap");String resultType = context.getStringAttribute("resultType");Class<?> resultTypeClass = resolveClass(resultType);String resultMap = context.getStringAttribute("resultMap");String resultSetType = context.getStringAttribute("resultSetType");ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);if (resultSetTypeEnum == null) {resultSetTypeEnum = configuration.getDefaultResultSetType();}String keyProperty = context.getStringAttribute("keyProperty");String keyColumn = context.getStringAttribute("keyColumn");String resultSets = context.getStringAttribute("resultSets");builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,resultSetTypeEnum, flushCache, useCache, resultOrdered,keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
来看一下添加参数返回MappedStatement
的addMappedStatement(...)
方法:
public MappedStatement addMappedStatement(String id,SqlSource sqlSource,StatementType statementType,SqlCommandType sqlCommandType,Integer fetchSize,Integer timeout,String parameterMap,Class<?> parameterType,String resultMap,Class<?> resultType,ResultSetType resultSetType,boolean flushCache,boolean useCache,boolean resultOrdered,KeyGenerator keyGenerator,String keyProperty,String keyColumn,String databaseId,LanguageDriver lang,String resultSets) {if (unresolvedCacheRef) {throw new IncompleteElementException("Cache-ref not yet resolved");}id = applyCurrentNamespace(id, false);boolean isSelect = sqlCommandType == SqlCommandType.SELECT;MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType).resource(resource).fetchSize(fetchSize).timeout(timeout).statementType(statementType).keyGenerator(keyGenerator).keyProperty(keyProperty).keyColumn(keyColumn).databaseId(databaseId).lang(lang).resultOrdered(resultOrdered).resultSets(resultSets).resultMaps(getStatementResultMaps(resultMap, resultType, id)).resultSetType(resultSetType).flushCacheRequired(valueOrDefault(flushCache, !isSelect)).useCache(valueOrDefault(useCache, isSelect)).cache(currentCache);ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);if (statementParameterMap != null) {statementBuilder.parameterMap(statementParameterMap);}MappedStatement statement = statementBuilder.build();configuration.addMappedStatement(statement);return statement;
}
通过解析一个个的标签,最终将sql语句的所有信息封装成MappedStatement对象,然后存储在configuration对象中。
那么bindMapperForNamespace()
又做了什么呢?
private void bindMapperForNamespace() {String namespace = builderAssistant.getCurrentNamespace();if (namespace != null) {Class<?> boundType = null;try {boundType = Resources.classForName(namespace);} catch (ClassNotFoundException e) {//ignore, bound type is not required}if (boundType != null) {if (!configuration.hasMapper(boundType)) {// Spring may not know the real resource name so we set a flag// to prevent loading again this resource from the mapper interface// look at MapperAnnotationBuilder#loadXmlResourceconfiguration.addLoadedResource("namespace:" + namespace);configuration.addMapper(boundType);}}}
}
实际上就是解析该sql对应的class,并把该class放到configuration中的mapperRegistry中。实际上mybatis的所有配置信息以及运行时的配置参数全部都保存在configuration对象中。
所以整个流程可以用如下的时序图表示:
SqlSession
SqlSession
的获取主要是通过 SqlSessionFactory
的默认实现类 DefaultSqlSessionFactory
的 openSessionFromDataSource
封装一个 DefaultSqlSession
(实现 SqlSession
接口)返回。
当执行 openSession()
操作的时候,实际上执行的代码如下:
@Override
public SqlSession openSession() {return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}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);// 创建一个Executor对象,用于后面执行SQL脚本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();}
}
从代码可以知道,openSession()
操作会创建 Mybatis 四大对象之一的 Executor
对象,创建过程如下:
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);}/**如果开启了二级缓存,executor会被CachingExecutor包装一次*/if (cacheEnabled) {executor = new CachingExecutor(executor);}/*尝试将executor使用interceptorChain中的每个interceptor包装一次(根据配置),这里是对Mybatis强大的插件开发功能做支持*/executor = (Executor) interceptorChain.pluginAll(executor);return executor;
}
默认情况下会返回一个SimpleExecutor
对象。然后SimpleExecutor
被封装到DefaultSqlSession
。
这里我们需要注意一下,在Executor
创建完毕之后,会根据配置是否开启了二级缓存,来决定是否使用CachingExecutor
包装一次Executor
。最后尝试将executor
使用interceptorChain
中的每个interceptor
包装一次(根据配置),这里是对Mybatis强大的插件开发功能做支持。
Mapper代理
当我们使用如下代码:
UserMapper mapper = session.getMapper(UserMapper.class);
来获取 UserMapper
的时候,实际上是从 configuration
当中的 MapperRegistry
当中获取 UserMapper
的代理对象:
/*** 可以看到我们是从 Configuration 对象中的 MapperRegistry 对象通过类对象作为key获取* MapperProxyFactory 然后通过jdk的动态代理生成代理对象(这里也就解释了为什么我们要创建一个 Mapper 接口而不是实体类) * 里面的 addMapper() 方法是不是似曾相识。*/// DefaultSqlSession 289
@Override
public <T> T getMapper(Class<T> type) {return configuration.getMapper(type, this);
}// Configuration 778
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {return mapperRegistry.getMapper(type, sqlSession);
}// MapperRegistry
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();// MapperRegistry 44
@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);}
}
我们来看一下 MapperProxyFactory
的实现:
public class MapperProxyFactory<T> {private final Class<T> mapperInterface;private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();public MapperProxyFactory(Class<T> mapperInterface) {this.mapperInterface = mapperInterface;}public Class<T> getMapperInterface() {return mapperInterface;}public Map<Method, MapperMethod> getMethodCache() {return methodCache;}@SuppressWarnings("unchecked")protected T newInstance(MapperProxy<T> mapperProxy) {return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);}// 上方代码中,调用的是这个方法// knownMappers属性里面的值,实际上就是我们在mappers扫描与解析的时候放进去的。public T newInstance(SqlSession sqlSession) {final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);return newInstance(mapperProxy);}}
public class MapperProxy<T> implements InvocationHandler, Serializable {...
}
并且MapperProxy
实现了InvocationHandler
接口,从以上代码可以看出,实际上使用的就是jdk的动态代理,给UserMapper
接口生成一个代理对象。实际上就是MapperProxy
的一个对象,如下图调试信息所示:
所以整个代理对象生成过程可以用如下时序图表示:
执行查询语句
我们知道,我们获取到的UserMapper实际上是代理对象MapperProxy,所以我们执行查询语句的时候实际上执行的是MapperProxy的invoke方法:
// MapperProxy 78
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {/**如果调用的是Object原生的方法,则直接放行*/if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, args);} else if (method.isDefault()) {if (privateLookupInMethod == null) {return invokeDefaultMethodJava8(proxy, method, args);} else {return invokeDefaultMethodJava9(proxy, method, args);}}} catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);}final MapperMethod mapperMethod = cachedMapperMethod(method);return mapperMethod.execute(sqlSession, args);
}
我们再来看看cachedMapperMethod方法:
private MapperMethod cachedMapperMethod(Method method) {return methodCache.computeIfAbsent(method,k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));}
可以看到,先根据方法签名,从方法缓存中获取方法,如果为空,则生成一个MapperMethod放入缓存并返回。
所以最终执行查询的是MapperMethod的execute方法:
// MapperMethod 57
public Object execute(SqlSession sqlSession, Object[] args) {Object result;switch (command.getType()) {case INSERT: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.insert(command.getName(), param));break;}case UPDATE: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.update(command.getName(), param));break;}case DELETE: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.delete(command.getName(), param));break;}/**select查询语句*/case SELECT:/**当返回类型为空*/if (method.returnsVoid() && method.hasResultHandler()) {executeWithResultHandler(sqlSession, args);result = null;/**当返回many的时候*/} else if (method.returnsMany()) {result = executeForMany(sqlSession, args);/**当返回值类型为Map的时候*/} else if (method.returnsMap()) {result = executeForMap(sqlSession, args);} else if (method.returnsCursor()) {result = executeForCursor(sqlSession, args);} else {/**除去以上情况,执行这里的步骤*/Object param = method.convertArgsToSqlCommandParam(args);result = sqlSession.selectOne(command.getName(), param);if (method.returnsOptional()&& (result == null || !method.getReturnType().equals(result.getClass()))) {result = Optional.ofNullable(result);}}break;case FLUSH:result = sqlSession.flushStatements();break;default:throw new BindingException("Unknown execution method for: " + command.getName());}if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {throw new BindingException("Mapper method '" + command.getName()+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");}return result;
}
我们示例中执行的分支语句是:
Object param = method.convertArgsToSqlCommandParam(args);result = sqlSession.selectOne(command.getName(), param);
这里有一个查询参数的解析过程:
// MapperMethod 308
public Object convertArgsToSqlCommandParam(Object[] args) {return paramNameResolver.getNamedParams(args);
}// ParamNameResolver 110
public Object getNamedParams(Object[] args) {final int paramCount = names.size();if (args == null || paramCount == 0) {return null;/**参数没有标注@Param注解,并且参数个数为一个*/} else if (!hasParamAnnotation && paramCount == 1) {return args[names.firstKey()];/**否则执行这个分支*/} else {final Map<String, Object> param = new ParamMap<>();int i = 0;for (Map.Entry<Integer, String> entry : names.entrySet()) {param.put(entry.getValue(), args[entry.getKey()]);// add generic param names (param1, param2, ...)final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);// ensure not to overwrite parameter named with @Paramif (!names.containsValue(genericParamName)) {param.put(genericParamName, args[entry.getKey()]);}i++;}return param;}
}
这里代码的意思是:这里参数解析如果判断参数一个只有一个(一个单一参数或者是一个集合参数),并且没有标注@Param注解,那么直接返回这个参数的值,否则会被封装为一个Map,然后再返回。
封装的样式如下用几个示例解释:
例1:
/**接口为*/
User selectByNameSex(String name, int sex);
/**我们用如下格式调用*/
userMapper.selectByNameSex("张三",0);
/**参数会被封装为如下格式:*/
0 ---> 张三
1 ---> 0
param1 ---> 张三
param2 ---> 0
例2:
/**接口为*/
User selectByNameSex(@Param("name") String name, int sex);
/**我们用如下格式调用*/
userMapper.selectByNameSex("张三",0);
/**参数会被封装为如下格式:*/
name ---> 张三
1 ---> 0
param1 ---> 张三
param2 ---> 0
参数处理完接下来就是调用执行过程,最终调用执行的是DefaultSqlSession中的selectList方法:
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {try {MappedStatement ms = configuration.getMappedStatement(statement);return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);} catch (Exception e) {throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);} finally {ErrorContext.instance().reset();}
}
这里调用SimpleExecutor的query方法执行查询操作,接着调用doQuery方法:
// SimpleExecutor 57
@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();/**这里出现了Mybatis四大对象中的StatementHandler*/StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);stmt = prepareStatement(handler, ms.getStatementLog());return handler.query(stmt, resultHandler);} finally {closeStatement(stmt);}
}// Configuration 591
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);statementHandler = (StatementHandler) /**创建StatementHandler并应用到插件支持*/ interceptorChain.pluginAll(statementHandler);return statementHandler;
}
在创建StatementHandler的同时,应用插件功能,同时创建了Mybatis四大对象中的另外两个对象:
protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {………………/**Mybatis四大对象中的ParameterHandler*/ this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);/**Mybatis四大对象中的ResultSetHandler*/ this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
}public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {………………interceptorChain.pluginAll(parameterHandler);return parameterHandler;
}public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,ResultHandler resultHandler, BoundSql boundSql) {………………interceptorChain.pluginAll(resultSetHandler);return resultSetHandler;
}
接下来就是执行正常的JDB C查询功能了,参数设置由ParameterHandler操作,结果集处理由ResultSetHandler处理。至此,整个查询流程结束。
整个查询阶段的时序图可以用如下图表示:
整个查询流程,可以总结如下图:
插件开发
引入
首先我们知道一次查询的过程为:
-
根据配置文件(全局,sql映射)初始化出
Configuration
对象 -
创建一个
DefaultSqlSession
对象,它里面包含Configuration
以及Executor
(根据全局配置文件的defaultExecutorType创建出对应的Executor
) -
DefaultSqlSession.getMapper()
拿到Mapper
接口对应的MapperProxy
-
MapperProxy
里面有DefaultSqlSession
-
执行增删改查方法:
-
(代理对象)调用
DefaultSqlSession
的增删改查(Executor
) -
创建一个
StatementHandler
对象,同时也创建ParameterHandler
和ResultSetHandler
-
调用
StatementHandler
的预编译参数(使用ParameterHandler
设置参数值) -
调用
StatementHandler
的增删改查方法 -
ResultSetHandler
封装结果
-
四大对象
Mybatis四大对象指的是:Executor
、StatementHandler
、ParamaterHandler
、ResultSetHandler
。
- ParameterHandler:处理 SQL 的参数对象。
- ResultSetHandler:处理 SQL 的返回结果集。
- StatementHandler:数据库的处理对象,用于执行 SQL 语句。
- Executor:MyBatis 的执行器,用于执行增删改查操作。
Mybatis允许我们在四大对象执行的过程中对其指定方法进行拦截,这样就可以很方便了进行功能的增强,这个功能跟 Spring 的切面编程非常类似。上文我们都有提到过,在四大对象创建的时候,都进行了插件增强,下面我们就来讲解一下其实现原理。
Mybatis插件接口 - Interceptor
首先我们需要理解 Intercaptor
接口。
Intercept
方法,插件的核心方法。plugin
方法,生成target
的代理对象。setProperties
方法,传递插件所需参数。
看如下代码的方法实现以及参数注释得以理解:
/** 插件签名,告诉mybatis单钱插件用来拦截那个对象的哪个方法 **/
@Intercepts({@Signature(type = ResultSetHandler.class,method ="handleResultSets",args = Statement.class)})
public class MyFirstInterceptor implements Interceptor {/** @Description 拦截目标对象的目标方法 **/@Overridepublic Object intercept(Invocation invocation) throws Throwable {System.out.println("拦截的目标对象:"+invocation.getTarget());Object object = invocation.proceed();return object;}/*** @Description 包装目标对象 为目标对象创建代理对象* @Param target为要拦截的对象* @Return 代理对象*/@Overridepublic Object plugin(Object target) {System.out.println("将要包装的目标对象:"+target);return Plugin.wrap(target,this);}/** 获取配置文件的属性 **/@Overridepublic void setProperties(Properties properties) {System.out.println("插件配置的初始化参数:"+properties);}
}
然后再在 mybatis.xml
中配置插件。
<!-- 自定义插件 -->
<plugins><plugin interceptor="mybatis.interceptor.MyFirstInterceptor"><!--配置参数--><property name="name" value="Bob"/></plugin>
</plugins>
调用查询方法,查询方法会返回 ResultSet
。
public class MyBatisTest {public static SqlSessionFactory sqlSessionFactory = null;public static SqlSessionFactory getSqlSessionFactory() {if (sqlSessionFactory == null) {String resource = "mybatis-config.xml";try {Reader reader = Resources.getResourceAsReader(resource);sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);} catch (IOException e) {e.printStackTrace();}}return sqlSessionFactory;}public void testGetById() {SqlSession sqlSession = this.getSqlSessionFactory().openSession();PersonMapper personMapper = sqlSession.getMapper(PersonMapper.class);Person person=personMapper.getById(2001);System.out.println(person.toString());}public static void main(String[] args) {new MyBatisTest().testGetById();}
}
最后输出结果为:
插件配置的初始化参数:{name=Bob}
将要包装的目标对象:org.apache.ibatis.executor.CachingExecutor@754ba872
将要包装的目标对象:org.apache.ibatis.scripting.defaults.DefaultParameterHandler@192b07fd
将要包装的目标对象:org.apache.ibatis.executor.resultset.DefaultResultSetHandler@7e0b0338
将要包装的目标对象:org.apache.ibatis.executor.statement.RoutingStatementHandler@1e127982
拦截的目标对象:org.apache.ibatis.executor.resultset.DefaultResultSetHandler@7e0b0338
Person{id=2001, username='Tom', email='email@0', gender='F'}
实际运用举例
例如:我们希望在 sql 语句之前前后进行时间打印,计算出 sql 执行的时间。此功能我们就可以拦截StatementHandler
。这里我们需要时间Mybatis提供的 Intercaptor
接口。
/**该注解签名告诉此拦截器拦截四大对象中的哪个对象的哪个方法,以及方法的签名信息*/
@Intercepts({@Signature(type = StatementHandler.class,method = "query",args = {Statement.class,ResultHandler.class})}
)
public class SqlLogPlugin implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {long begin = System.currentTimeMillis();try {return invocation.proceed();} finally {long time = System.currentTimeMillis() - begin;System.out.println("sql 运行了 :" + time + " ms");}}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {}
}
接下来我们需要在mybatis-config.xml配置该拦截器:
<plugins><plugin interceptor="com.test.mybatis.intercaptor.SqlLogPlugin"><property name="参数1" value="root"/><property name="参数2" value="123456"/></plugin>
</plugins>
此时,拦截器的配置就完成了,运行结果如下:
DEBUG 11-24 17:51:34,877 ==> Preparing: select * from user where id = ? (BaseJdbcLogger.java:139)
DEBUG 11-24 17:51:34,940 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:139)
DEBUG 11-24 17:51:34,990 <== Total: 1 (BaseJdbcLogger.java:139)
sql 运行了 :51 ms
User{id=1, name='张三', age=42, sex=0}
插件原理
Mybatis的插件原理,即Mybatis的插件借助于责任链的模式进行对拦截的处理;使用动态代理对目标对象进行包装,达到拦截的目的;作用于Mybatis的作用域对象之上。
那么插件具体是如何拦截并附加额外的功能的呢?
我们先以 ParameterHandler
来说:
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object object, BoundSql sql, InterceptorChain interceptorChain){ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement,object,sql);parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);return parameterHandler;
}
其中的pluginAll
实现如下:
public Object pluginAll(Object target) {for (Interceptor interceptor : interceptors) {target = interceptor.plugin(target);}return target;
}
interceptorChain
保存了所有的拦截器(interceptors
),是 Mybatis 初始化的时候创建的。调用拦截器链中的拦截器依次的对目标进行拦截或增强。interceptor.plugin(target)
中的 target
就可以理解为 Mybatis 中的四大对象。返回的 target
是被重重代理后的对象。
那我们再从上面的实际运用示例里来看:
public Object plugin(Object target) {return Plugin.wrap(target, this);
}
该代码是对目标对象的包装,实际运行的时候,是使用的包装之后的类,运行的时候执行的是intercept
方法。那么现在我们来看下它是怎么进行包装的:
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) {// 返回的是JDK动态代理类,代理类增强了目标类return Proxy.newProxyInstance(type.getClassLoader(),interfaces,new Plugin(target, interceptor, signatureMap));}return target;
}
而Plugin
类实现了InvocationHanlder
接口:
public class Plugin implements InvocationHandler {// ...
}
显然这里使用的就是JDK的动态代理,对目标对象包装了一层,重写invoke()方法。
// Plugin.invoke
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {Set<Method> methods = signatureMap.get(method.getDeclaringClass());// 如果是定义的拦截的方法 就执行intercept方法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);}
}
假如一个对象被多个拦截器进行了多次包装,那么后包装的在最外层会先执行。
Mybatis面试问题
#{} 和 ${} 的区别?
${}
是properties
文件中的变量占位符
,它可以用于标签属性值和sql内部,属于静态文本替换。#{}
是sql
的参数占位符
,Mybatis会将 sql 中的 #{} 替换为 ? 号,在 sql 执行前会使用PreparedStatement
的参数设置方法,按序给 sql 的 ? 号占位符设置参数值。
举例说明:
# ${param} 传递的参数会被当成sql语句中的一部分,举例:
order by ${param}
# 则解析成的sql为:
order by id# #{param} 传入的数据都当成一个字符串,会对自动传入的数据加一个双引号,举例:
select * from table where name = #{param}
# 则解析成的sql为:
select * from table where name = "id"
所以一般都是使用 #{}
来进行变量替换。
Mybatis 一级缓存 和 二级缓存?
- 一级缓存:基于
PerpetualCache
的HashMap
本地缓存,其存储作用域为Session
,当Session
flush 或 close 之后,该Session
中的所有 Cache 就将清空,Mybatis默认打开一级缓存,一级缓存存放在BaseExecutor
的localCache
变量中。 - 二级缓存:机制与一级缓存相同,默认也是采用
PerpetualCache
,HashMap
存储,不同在于其存储作用域为Mapper(Namespace)
级别。Mybatis默认不打开二级缓存,可以在 config 文件中xml<settings><setting name="cacheEnabled" value="true"/></settings>
开启全局的二级缓存,但并不会为所有的 Mapper 设置二级缓存,每个 mapper.xml 文件中使用标签来开启当前mapper的二级缓存,二级缓存存放在MappedStatement
类cache
变量中。
一级缓存
代码如下:
public abstract class BaseExecutor implements Executor {// ...protected PerpetualCache localCache;// ...// 构造方法protected BaseExecutor(Configuration configuration, Transaction transaction) {// ...this.localCache = new PerpetualCache("LocalCache"); // 默认初始化// ...}// query查询方法@SuppressWarnings("unchecked")@Overridepublic <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()) {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 #601deferredLoads.clear();if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {// issue #482clearLocalCache();}}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;}
}
二级缓存
二级缓存默认是不开启的,需要手动开启二级缓存,实现二级缓存的时候,MyBatis要求返回的 POJO 必须是可序列化的。
开启二级缓存的条件也是比较简单,通过直接在 MyBatis 配置文件中设置:
<settings><setting name = "cacheEnabled" value = "true" />
</settings>
还需要在 Mapper.xml
配置文件中加入 <cache>
标签。
缓存失效
一级缓存
Mybatis 一级缓存(会话级别缓存:sqlSession)默认是开启的。
Mybatis 一级缓存失效的情况:
- 非同一个sqlSession(两个不同的sqlSession)
- 同一个sqlSession,查询条件不同(查询条件不同,相应的数据还没有放到缓存)
- 同一个sqlSession,两次查询之间做了更新操作(新增、删除、修改)
- 同一个sqlSession,两次查询之间做了缓存清除:sqlSession.clearCache()
二级缓存
当执行一条查询sql时,先从二级缓存进行查询,没有则进入一级缓存查询,再没有就执行sql进行数据库查询,在会话(sqlSession)结束前把缓存放到一级缓存,会话结束后放到二级缓存中(需要开启二级缓存)。
@Options(flushCache = FlushCachePolicy.TRUE, useCache = false)
/*
1. flushCache:是否清除缓存,默认是DEFAULT,可以直接设置FlushCachePolicy.TRUE1.1 FlushCachePolicy.DEFAULT:如果是查询(select)操作,则是false,其它更新(insert、update、delete)操作则是true
2. useCache:是否将查询结果放到二级缓存
*/
Mybatis插件运行原理?
上面应该讲过了吧,再稍微提一下。
编写插件
- 实现Interceptor接口方法
- 确定拦截的签名
- 在配置文件中配置插件
插件运行原理
在创建三个重要的Handler(StatementHandler、ParameterHandler、ResultSetHandler)时通过插件数组包装了三大Handler:
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
获取到所有的 Interceptor
(拦截器,即插件需要实现的接口),调用:
interceptor.plugin(target);
返回 target
包装后的对象,最后回调回自定义插件的 intercept()
方法执行插件内的代码逻辑。
可拦截的接口和方法一览:
- Executor(update、query 、 flushStatment 、 commit 、 rollback 、 getTransaction 、 close 、 isClose)
- StatementHandler(prepare 、 paramterize 、 batch 、 update 、 query)
- ParameterHandler( getParameterObject 、 setParameters )
- ResultSetHandler( handleResultSets 、 handleCursorResultSets 、 handleOutputParameters )
举例:PageHelper
配置过程
相关依赖如下:
<dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.2.8</version>
</dependency>
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper</artifactId><version>1.2.15</version>
</dependency>
首先要在myabtis.xml的全局文件上进行添加插件plugin,如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configurationPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration><plugins><!-- com.github.pagehelper为PageHelper类所在包名 --><plugin interceptor="com.github.pagehelper.PageHelper"><property name="dialect" value="mysql" /><!-- 该参数默认为false --><!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 --><!-- 和startPage中的pageNum效果一样 --><property name="offsetAsPageNum" value="true" /><!-- 该参数默认为false --><!-- 设置为true时,使用RowBounds分页会进行count查询 --><property name="rowBoundsWithCount" value="true" /><!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 --><!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型) --><property name="pageSizeZero" value="true" /><!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 --><!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 --><!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 --><property name="reasonable" value="false" /><!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 --><!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 --><!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值 --><!-- 不理解该含义的前提下,不要随便复制该配置 --><property name="params" value="pageNum=start;pageSize=limit;" /><!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page --><property name="returnPageInfo" value="check" /></plugin></plugins>
</configuration>
使用步骤
普通使用
// 获取配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis/mybatis-config.xml");
// 通过加载配置文件获取SqlSessionFactory对象
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取SqlSession对象
SqlSession session = factory.openSession();
PageHelper.startPage(1, 5);
session.selectList("com.bobo.UserMapper.query");
Service中使用
@Service("orderService")
public class OrderServiceImpl implements OrderService {@Autowiredprivate OrderDao orderDao;@Overridepublic List<Order> findAll(int page, int size) throws Exception {// pageNum 是页码值,pageSize是每页显示条数PageHelper.startPage(page, size);return orderDao.findAll();}
}
原理说明
根据上面在 SqlSessionFactoryBuilder
使用 XMLConfigBuilder#parseConfiguration()
解析配置文件的插件注册中,我们知道 PageHelper
会被封装成一个 Interceptor
注册进拦截器链。
那么我们这里只要关注它的具体注册的拦截信息就好了。
我们来看下 PageHelper
的源代码的头部定义:
@SuppressWarnings("rawtypes")
// 定义的是拦截 Executor对象中的
// 拦截的方法是:query(MappedStatement ms,Object o,RowBounds ob ResultHandler rh)
@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class PageHelper implements Interceptor {//sql工具类private SqlUtil sqlUtil;//属性参数信息private Properties properties;//配置对象方式private SqlUtilConfig sqlUtilConfig;//自动获取dialect,如果没有setProperties或setSqlUtilConfig,也可以正常进行private boolean autoDialect = true;//运行时自动获取dialectprivate boolean autoRuntimeDialect;//多数据源时,获取jdbcurl后是否关闭数据源private boolean closeConn = true;}
具体就不分析了。。PageHelper
分页的实现其实是在我们执行 SQL 语句之前动态的将 SQL 语句拼接了分页的语句,从而实现了从数据库中分页获取的过程。
Xml映射文件和内部数据结构之间的映射?
根据我在文首引入的第一篇文章拿的图,简单看一下吧。
configuration
resultMap
mappedStatment
Mybatis中用到了哪些设计模式?
- 日志模块:代理模式、适配器模式
- 数据源模块:代理模式、工厂模式
- 缓存模块:装饰器模式
- 初始化阶段:建造者模式
- 代理阶段:策略模式
- 数据读写阶段:模板模式
- 插件化开发:责任链模式
这块可以引申介绍设计模式,其实对设计模式熟悉的可以通过一种设计模式映射到很多源码中的实现。
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
相关文章
- docker之harbor仓库8
harbor仓库(最小化安装)的搭建及用法 harbor仓库作为企业级仓库:提供了镜像扫描、病毒扫描、签名 harbor仓库每次启动时会开启许多容器,可以提供web操作页面 docker-compose负责单个节点控制多个容器 软件下载位置:ht…...
2024/4/13 5:59:33 - C语言 实现 mine clearance(扫雷)
已传入github,可直接下载: GitHub - frankRenlf/c_programsContribute to frankRenlf/c_programs development by creating an account on GitHub.https://github.com/frankRenlf/c_programs.git 自定义炸弹个数 可选择清理区域,或者确定炸弹,或者取消确定 下面放几个片段...
2024/4/19 22:26:15 - Java基础--手把手教你如何从键盘录入信息
从键盘录入信息 文章目录一、前奏1、创建扫描仪对象2、导入包二、从键盘录入信息1、输入整数2、输入浮点数3、输入字符串三、释放资源四、补充说明1、输入与接收的数据类型不匹配2、输入带有空格的字符串五、全部代码一、前奏 1、创建扫描仪对象 Scanner有扫描仪的意思&#…...
2024/4/5 2:06:27 - 一文详解Mockmvc在实际工作如何使用
文章目录简介流程代码简介 对模块进行集成测试时,希望能够通过输入URL对Controller进行测试,如果通过启动服务器,建立http client进行测试,这样会使得测试变得很麻烦,比如,启动速度慢,测试验证…...
2024/4/17 9:09:06 - STM32——继电器控制灯的开关
STM32——继电器控制灯的开关 文章目录STM32——继电器控制灯的开关继电器控制灯的开关项目概述:环境概述:项目的开始:第一步:第二步:1、配置GPIOA时钟2、GPIOA3的结构体配置3、初始化项目代码:总结&#x…...
2024/4/13 5:59:13 - 【Ubuntu】-概述与安装配置操作笔记
一、Debian与Ubuntu简介 1.1:Debian简介: 是从1993 年由lan Murdockk(伊恩默多克) 发起的,受到当时 Linux 与 GNU 的鼓舞,目标是成为⼀个公开的发行版,经过二十几年的迭代更新 Debian 从⼀个小型紧密的自由软件骇客&…...
2024/4/18 13:20:48 - 国内CDN加速的背景和现状
sigcomm一篇很有意思的论文: https://conferences.sigcomm.org/events/apnet2021/papers/apnet2021-4.pdf 微观视角下的宏观状态。大致说的是: 虽然BBR压倒性的优势引导人们用BBR替换CUBIC,但随着BBR部署比例的增加,收益将越来越…...
2024/4/20 17:41:58 - pytest的快速使用
pytest是用python编写测试用例,有灵活的初始化机制,还可以灵活的挑选测试用例,可以生成报表 pytest的功能非常多,目前我只学习了常用的功能 1.关于安装 执行下面的命令 pip install pytest 2.还需要产生测试报表 pip install…...
2024/4/8 19:05:02 - static_cast<void>(0)与(void)0及在宏中的应用
1、static_cast<void>(0)和(void)0的意义: 1.1:作用: C中的的static_cast<void>(0)和C语言的(void)0作用一样,都表示将0强制转换为void类型,表示一个空语句。 1.2:原理: 任何表达式…...
2024/4/13 5:59:58 - 权限系统模型有哪些?
DAC——自主访问控制 用户可将自己拥有的权限分配给其他人 MAC——强制访问控制 用户和资源都有固定的安全属性值 用户可以访问安全属性值同级和更低别的资源 多用于对安全性要求比较高的系统,如多级安全军事系统 RBAC——基于角色的权限管理 RBAC0 RBAC基本…...
2024/4/20 8:27:53 - JavaScript和C的三个区别
JS面向对象,C面向过程。 面向过程可以理解为一个很全能的类什么活都包揽了,而面向对象是有很多各司其职的类,每个类都很专一。通常应用层的软件开发使用面向对象编程,因为老板时常会提出新的需求,如果是面向过程则要重…...
2024/4/13 6:00:08 - 【C进阶】文件操作(1)
目录 1、为什么使用文件 2、什么是文件 (1)程序文件 (2)数据文件 (3)文件名 3、文件的打开和关闭 (1)文件指针 (2)文件的打开和关闭 4、文件的顺序读写 &a…...
2024/4/13 6:00:53 - 接口和抽象类的深度详解
关键字 interface接口的深入解 析 先说结论,接口的出现是为了打破类(只能描述一类事物的特性,对于某些不属于任何一个类的,界定不清楚的行为无法正确描述,很难用类描述清楚的行为,如果强行用类去…...
2024/4/17 9:09:30 - 澳大利亚(DKASC)光伏数据集介绍和分享
http://dkasolarcentre.com.au/download?locationalice-springs 上述链接可以下载到光伏数据集,我也下载好了一个,分享到了百度网盘(免密) https://pan.baidu.com/s/18w9fwmmz6bbVlRpfep56KQ?pwdjwu9 注意光伏电站介绍中的Du…...
2024/4/27 19:34:45 - 二 数据类型
一 变量的基本类型 一 不可变型 ❤️无法对变量内的某个单一元素进行修改,增加,删除 1 字符串 - str - string 2 整型 - int - integer 3 浮点型 - float 4 元组 - tuple (1, 2, 3, "大学") 二 可变型 1 字典 - dict - dictionary {…...
2024/4/16 23:53:45 - LeetCode刷题笔记 字节每日打卡 正则表达式匹配
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 . 和 * 的正则表达式匹配。 . 匹配任意单个字符 * 匹配零个或多个前面的那一个元素 所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。 示例 1: 输入:s …...
2024/4/20 15:45:26 - SpringBoot(二)——整合视图层(FreeMarker、Thymeleaf、SpringBoot 整合 jsp)
SpringBoot(二)——整合视图层(FreeMarker、Thymeleaf、SpringBoot 整合 jsp 一、FreeMarker 1、页面模板介绍 前后端不分离开发,除了前面经常用的 jsp ,还有 Freemarker、Thymeleaf。前后端虽说基本不需要模板了&a…...
2024/4/14 15:20:43 - Qt学习笔记——创建第一个按钮和对象树
示例 代码 //创建第一个按钮QPushButton *btn new QPushButton;//btn->show();//show以顶层方式弹出窗口控件//让btn对象 依赖在Widget窗口中btn->setParent(this);//设置按钮的大小btn->resize(100,40);//显示文本btn->setText("第一个按钮");//创建第…...
2024/4/18 23:38:10 - Spark序列化
目录spark序列化方式SparkSql与序列化在spark中使用kryo使用kryo但不注册类kryo与java占用内存对比Encoders.kryo vs Encoders.javaSerialization参考spark序列化方式 分布式的程序存在着网络传输,无论是数据还是程序本身的序列化都是必不可少的。spark自身提供两种…...
2024/4/16 21:29:10 - 2022.2.12学习汇报
一、Velodyne Lidar仿真和3D建图: 1.安装gtsam(Georgia Tech Smoothing and Mapping library, 4.0.0-alpha2) 2.运行velodyne_simulator ROS包 3.在neor_mini上装置Velodyne-16雷达(此处上周出现的问题得以解决) 4.在Gazebo下运行neor_mini…...
2024/4/19 9:42:33
最新文章
- 用vim或gvim编辑程序
vim其实不难使用,学习一下就好了。简单功能很快学会。它有三种模式:命令模式,编辑模式,视模式。打开时在命令模式。在命令模式下按 i 进入编辑模式,在编辑模式下按<Esc>键退出编辑模式。在命令模式按 :wq 保存文…...
2024/5/3 13:07:44 - 梯度消失和梯度爆炸的一些处理方法
在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言,在此感激不尽。 权重和梯度的更新公式如下: w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...
2024/3/20 10:50:27 - 招投标系统简介 企业电子招投标采购系统源码之电子招投标系统 —降低企业采购成本
功能描述 1、门户管理:所有用户可在门户页面查看所有的公告信息及相关的通知信息。主要板块包含:招标公告、非招标公告、系统通知、政策法规。 2、立项管理:企业用户可对需要采购的项目进行立项申请,并提交审批,查看所…...
2024/5/2 20:46:00 - 是否有替代U盘,可安全交换的医院文件摆渡方案?
医院内部网络存储着大量的敏感医疗数据,包括患者的个人信息、病历记录、诊断结果等。网络隔离可以有效防止未经授权的访问和数据泄露,确保这些敏感信息的安全。随着法律法规的不断完善,如《网络安全法》、《个人信息保护法》等,医…...
2024/5/1 14:10:39 - 【外汇早评】美通胀数据走低,美元调整
原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...
2024/5/1 17:30:59 - 【原油贵金属周评】原油多头拥挤,价格调整
原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...
2024/5/2 16:16:39 - 【外汇周评】靓丽非农不及疲软通胀影响
原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...
2024/4/29 2:29:43 - 【原油贵金属早评】库存继续增加,油价收跌
原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...
2024/5/2 9:28:15 - 【外汇早评】日本央行会议纪要不改日元强势
原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...
2024/4/27 17:58:04 - 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响
原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...
2024/4/27 14:22:49 - 【外汇早评】美欲与伊朗重谈协议
原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...
2024/4/28 1:28:33 - 【原油贵金属早评】波动率飙升,市场情绪动荡
原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...
2024/4/30 9:43:09 - 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试
原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...
2024/4/27 17:59:30 - 【原油贵金属早评】市场情绪继续恶化,黄金上破
原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...
2024/5/2 15:04:34 - 【外汇早评】美伊僵持,风险情绪继续升温
原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...
2024/4/28 1:34:08 - 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势
原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...
2024/4/26 19:03:37 - 氧生福地 玩美北湖(上)——为时光守候两千年
原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...
2024/4/29 20:46:55 - 氧生福地 玩美北湖(中)——永春梯田里的美与鲜
原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...
2024/4/30 22:21:04 - 氧生福地 玩美北湖(下)——奔跑吧骚年!
原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...
2024/5/1 4:32:01 - 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!
原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...
2024/4/27 23:24:42 - 「发现」铁皮石斛仙草之神奇功效用于医用面膜
原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...
2024/4/28 5:48:52 - 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者
原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...
2024/4/30 9:42:22 - 广州械字号面膜生产厂家OEM/ODM4项须知!
原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...
2024/5/2 9:07:46 - 械字号医用眼膜缓解用眼过度到底有无作用?
原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...
2024/4/30 9:42:49 - 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...
解析如下:1、长按电脑电源键直至关机,然后再按一次电源健重启电脑,按F8健进入安全模式2、安全模式下进入Windows系统桌面后,按住“winR”打开运行窗口,输入“services.msc”打开服务设置3、在服务界面,选中…...
2022/11/19 21:17:18 - 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。
%读入6幅图像(每一幅图像的大小是564*564) f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...
2022/11/19 21:17:16 - 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...
win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面,在等待界面中我们需要等待操作结束才能关机,虽然这比较麻烦,但是对系统进行配置和升级…...
2022/11/19 21:17:15 - 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...
有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows,请勿关闭计算机”的提示,要过很久才能进入系统,有的用户甚至几个小时也无法进入,下面就教大家这个问题的解决方法。第一种方法:我们首先在左下角的“开始…...
2022/11/19 21:17:14 - win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...
置信有很多用户都跟小编一样遇到过这样的问题,电脑时发现开机屏幕显现“正在配置Windows Update,请勿关机”(如下图所示),而且还需求等大约5分钟才干进入系统。这是怎样回事呢?一切都是正常操作的,为什么开时机呈现“正…...
2022/11/19 21:17:13 - 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...
Win7系统开机启动时总是出现“配置Windows请勿关机”的提示,没过几秒后电脑自动重启,每次开机都这样无法进入系统,此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一:开机按下F8,在出现的Windows高级启动选…...
2022/11/19 21:17:12 - 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...
有不少windows10系统用户反映说碰到这样一个情况,就是电脑提示正在准备windows请勿关闭计算机,碰到这样的问题该怎么解决呢,现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法:1、2、依次…...
2022/11/19 21:17:11 - 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...
今天和大家分享一下win7系统重装了Win7旗舰版系统后,每次关机的时候桌面上都会显示一个“配置Windows Update的界面,提示请勿关闭计算机”,每次停留好几分钟才能正常关机,导致什么情况引起的呢?出现配置Windows Update…...
2022/11/19 21:17:10 - 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...
只能是等着,别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚,只能是考虑备份数据后重装系统了。解决来方案一:管理员运行cmd:net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...
2022/11/19 21:17:09 - 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?
原标题:电脑提示“配置Windows Update请勿关闭计算机”怎么办?win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢?一般的方…...
2022/11/19 21:17:08 - 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...
关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!关机提示 windows7 正在配…...
2022/11/19 21:17:05 - 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...
钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...
2022/11/19 21:17:05 - 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...
前几天班里有位学生电脑(windows 7系统)出问题了,具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面,长时间没反应,无法进入系统。这个问题原来帮其他同学也解决过,网上搜了不少资料&#x…...
2022/11/19 21:17:04 - 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...
本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法,并在最后教给你1种保护系统安全的好方法,一起来看看!电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中,添加了1个新功能在“磁…...
2022/11/19 21:17:03 - 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...
许多用户在长期不使用电脑的时候,开启电脑发现电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机。。.这要怎么办呢?下面小编就带着大家一起看看吧!如果能够正常进入系统,建议您暂时移…...
2022/11/19 21:17:02 - 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...
配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!配置windows update失败 还原更改 请勿关闭计算机&#x…...
2022/11/19 21:17:01 - 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...
不知道大家有没有遇到过这样的一个问题,就是我们的win7系统在关机的时候,总是喜欢显示“准备配置windows,请勿关机”这样的一个页面,没有什么大碍,但是如果一直等着的话就要两个小时甚至更久都关不了机,非常…...
2022/11/19 21:17:00 - 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...
当电脑出现正在准备配置windows请勿关闭计算机时,一般是您正对windows进行升级,但是这个要是长时间没有反应,我们不能再傻等下去了。可能是电脑出了别的问题了,来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...
2022/11/19 21:16:59 - 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...
我们使用电脑的过程中有时会遇到这种情况,当我们打开电脑之后,发现一直停留在一个界面:“配置Windows Update失败,还原更改请勿关闭计算机”,等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢࿰…...
2022/11/19 21:16:58 - 如何在iPhone上关闭“请勿打扰”
Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...
2022/11/19 21:16:57