1 Mybatis核心类和组件
1.1 核心类组件
1.1.1 配置对象Configuration
mybatis-config.xml
文件可以加载多个映射文件,每个文件对应数据库中的一个表。MyBatis所有的配置信息都保存在Configuration对象之中,配置文件中的大部分配置都会存储到该类中。
1.1.2 会话工厂 SqlSessionFactory
每个基于 MyBatis 的应用都是以一个 SqlSessionFactory
的实例为核心。
有两种方式获取SqlSessionFactory:
(1) 从 mybatis-config.xml
文件构建 SqlSessionFactory
示例
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
(2) 配置类构建 SqlSessionFactory
示例
DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment); configuration.addMapper(BlogMapper.class);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
1.1.3 会话对象SqlSession
SqlSession是MyBatis工作的主要顶层API,表示和数据库交互时的会话。
SqlSession
提供了在数据库执行 SQL 所需的所有方法,通过 SqlSession
实例直接执行已映射的SQL语句
try (SqlSession session = sqlSessionFactory.openSession()) {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);
}
1.1.4 执行器Executor
原生JDBC执行 SQL 语句的是 Statement
类,Executor
可以理解是Mybatis 对 JDBC Statement
类的封装,Executor 根据 SqlSession 传递的参数动态的生成需要执行的SQL语句,同时负责查询缓存的维护。
1.1.5 MappedStatement对象
MappedStatement是维护一条SQL节点的封装, 用于存储SQL语句的id,参数等信息
每一个 MappedStatement
实例对应 mapper.xml 中配置的一个 SQL 语句。
1.1.6 输入参数映射
输入参数类型可以是 Map
,List
等集合类型,也可以是基本数据类型和 POJO
类型,输入参数映射过程类似于 JDBC 对 perparedStatement
对象设置参数的过程。
1.1.7 输出结果映射
输出结果类型可以是 Map
,List
等集合类型,也可以是基本数据类型和 POJO 类型,输出结果映射过程类似于 JDBC对结果集的解析过程。
1.2 SqlSession下的四大核心组件
MyBatis 封装了对数据库的访问,把对数据库的会话和事务控制放到了 SqlSession 对象中。SqlSession 是 MyBatis 的顶层接口,它提供了所有执行语句,获取映射器和管理事务等方法。
Mybatis中SqlSession下有四大核心组件:ParameterHandler 、ResultSetHandler 、StatementHandler 、Executor 。
1.2.1 Executor
对SqlSession方法的访问最终都会落到Executor相应方法上去。Executor负责生成动态 SQL 以及管理缓存。
普通Executor分三类
-
SimpleExecutor:简单类型的执行器,也是默认的执行器,每次执行update或者select操作,都会创建一个Statement对象,执行结束后关闭Statement对象。
-
ReuseExecutor:可重用的执行器,重用的是Statement对象,第一次执行一条sql,会将这条sql的Statement对象缓存在key-value结构的map缓存中。下一次执行,就可以从缓存中取出Statement对象,减少了重复编译的次数,从而提高了性能。每个SqlSession对象都有一个Executor对象,因此这个缓存是SqlSession级别的,所以当SqlSession销毁时,缓存也会销毁。
-
BatchExecutor:批量执行器,默认情况是每次执行一条sql,MyBatis都会发送一条sql。而批量执行器的操作是,每次执行一条sql,不会立马发送到数据库,而是批量一次性发送多条sql。
1.2.2 StatementHandler
StatementHandler 对象使用数据库中的Statement(PrepareStatement)执行操作,即底层是封装好的PrepareStatement。StatementHandler负责设置 Statement 对象中的查询参数、处理 JDBC 返回的 resultSet,将 resultSet 加工为 List 集合返回。
1.2.3 ParameterHandler
ParameterHandler 处理SQL参数,负责将传入的 Java 对象转换 JDBC 类型对象,并为 PreparedStatement 的动态 SQL 填充数值。
1.2.4 ResultSetHandler
ResultSetHandler负责结果集ResultSet封装处理返回。
1.3 组件生命周期
1.3.1 SqlSessionFactory
SqlSessionFactory在MyBatis应用的整个生命周期中,每个数据库只对应一个SqlSessionFactory,因此 SqlSessionFactory 的最佳作用域是应用作用域,以单例模式获取该对象。
1.3.2 SqlSession
SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。一个SqlSession可以执行多条SQL,保证事务的一致性。确保 SqlSession 使用后关闭。
try (SqlSession session = sqlSessionFactory.openSession()) {
// 应用逻辑代码
}
1.3.3 Mapper
Mapper的作用是发送SQL,然后返回需要的结果。任何映射器Mapper实例的最大作用域与请求它们的 SqlSession 相同。但方法作用域才是映射器实例的最合适的作用域。
try (SqlSession session = sqlSessionFactory.openSession()) {
BlogMapper mapper = session.getMapper(BlogMapper.class);
// 应用逻辑代码
}
2 拦截器
拦截器主要是为开发者提供了一些额外的操作,可以让我们管理事务,操作SQL等。 自定义拦截器需要实现Interceptor接口, 并在接口上添加@Intercepts注解。
2.1 @Intercepts注解
@Intercepts:标识该类是一个拦截器。下面是Mybatis官网给的一个例子
@Intercepts({
@Signature(
type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class})
})
2.1.1 拦截注解@Signature
@Signature拦截器相关属性设置
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
/**
* 定义拦截的类 Executor、ParameterHandler、StatementHandler、ResultSetHandler当中的一个
*/
Class<?> type();
/**
* 在定义拦截类的基础之上,在定义拦截的方法
*/
String method();
/**
* 在定义拦截方法的基础之上在定义拦截的方法对应的参数,
* 因方法里面可能重载,不指定参数列表,不能确定是对应拦截的方法
*/
Class<?>[] args();
}
2.1.2 拦截类型type
拦截器的类型,总共有4种,按照执行的先后顺序为:
Executor
:拦截执行器的方法ParameterHandler
:拦截参数的处理ResultHandler
:拦截结果集的处理StatementHandler
:拦截Sql语法构建的处理
2.1.3 拦截方法method
基于4种拦截器类型type
,拦截方法method
有下列
-
Executor
- update - 更新操作
- query - 查询操作
- flushStatements - flush操作
- commit - 提交事务
- rollback - 回滚事务
- getTransaction - 获取事务管理对象
- close - 关闭
- isClosed - 链接是否已关闭
-
ParameterHandler
- getParameterObject - 获取参数对象
- setParameters - 设置参数
-
ResultSetHandler
- handleResultSets - 处理结果集
- handleOutputParameters - 处理输出参数
-
StatementHandler
- prepare - 预编译
- parameterize - 参数化设置
- batch - 批量操作
- update - 更新
- query - 查询
2.1.4 参数类型args
Executor
中 query
方法因为重载原因有多个,args 就是指明参数类型,从而确定是哪一个方法。
2.2 Interceptor 接口
public interface Interceptor {
Object intercept(Invocation var1) throws Throwable;
Object plugin(Object var1);
void setProperties(Properties var1);
}
2.2.1 intercept方法
interceptor能够拦截的四种类型对象,此处入参invocation
便是指拦截到的对象。
Object intercept(Invocation var1) throws Throwable;
2.2.2 plugin方法
让mybatis判断,是否要进行拦截,然后做出决定是否生成一个代理
@Override
public Object plugin(Object target) {
//判断是否拦截这个类型对象(根据 @Intercepts注解筛选),然后决定是返回一个代理对象还是返回原对象。
//如果是插件要拦截的对象时才执行Plugin.wrap方法,否则的话,直接返回目标本身。
return Plugin.wrap(target, this);
}
根据@Intercepts注解来决定是否进行拦截处理。
2.2.3 setProperties方法
拦截器需要一些变量对象,而且这个对象是支持可配置的。
2.3 原理
2.3.1 InterceptorChain拦截器链类
Mybatis中的InterceptorChain
类用来创建拦截器链,内部持有一个interceptors的List,拦截器的顺序就是在配置文件中配置的拦截器的顺序
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
//创建拦截器链,target为mybatis中的4大对象中的某一个(ParameterHandler,StatementHandler,ResultSetHandler,Executor)
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
//通过jdk动态代理为目标创建代理对象,如果有多个拦截器,会出现代理对象再次被代理的情况,通过层层代理,构建拦截器链
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
pluginAll(target)方法
是用来构建拦截器链的,以target对象是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);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
每次调用newExecutor()方法时将目标对象executor传入interceptorChain.pluginAll(),返回executor的代理对象,其实在这个代理对象的内部,拦截器链已经形成了。
2.3.2 Plugin类
要想搞清楚拦截器链怎样构建的,必须需要深入interceptor.plugin(target)
方法。
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
Plugin
类实现了InvocationHandler
接口,返回了一个JDK自身提供的动态代理类。
public class Plugin implements InvocationHandler {
//目标对象,可能是一个代理对象,在第一次调用interceptor.plugin(target)时,target不是代理类,
//之后调用interceptor.plugin(target)时,这里的target就是代理对象了
private Object target;
//拦截器对象,因为之后要在invoke方法里面调用拦截器的拦截方法,所以这里需要持有引用
private Interceptor interceptor;
//拦截器方法签名map,在invoke方法内部判断如果调用的是拦截器支持拦截的方法,否则,直接调用目标对象的方法
private Map<Class<?>, Set<Method>> signatureMap;
private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
//自己写的拦截器需要调用该方法对目标对象进行代理
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,//注意这里第三个参数创建了一个当前类对象,并将目标对象、拦截器对象和方法签名的map传入
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)) {
//注意方法参数,创建一个Invocation并将目标对象、方法和参数传进去,所以在Invocation对象内部可以通过反射调用目标对象的方法
return interceptor.intercept(new Invocation(target, method, args));
}
//直接调用目标对象的方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
if (interceptsAnnotation == null) {
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
Signature[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.get(sig.type());
if (methods == null) {
methods = new HashSet<Method>();
signatureMap.put(sig.type(), methods);
}
try {
Method method = sig.type().getMethod(sig.method(), sig.args());
methods.add(method);
} catch (NoSuchMethodException e) {
throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
}
}
return signatureMap;
}
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<Class<?>>();
while (type != null) {
for (Class<?> c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
return interfaces.toArray(new Class<?>[interfaces.size()]);
}
}
Plugin
这个类的作用就是根据 @Interceptors
注解,得到这个注解的属性 @Signature
数组,然后根据每个 @Signature
注解的type,method,args
属性使用反射找到对应的Method
。最终根据调用的target对象实现的接口决定是否返回一个代理对象
替代原先的target对象。
比如MyBatis官网的例子
@Intercepts({
@Signature(
type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class})
})
由于Executor
接口的update(MappedStatement ms, Object parameter)
方法被拦截器被截获。因此最终返回的是一个代理类Plugin
,而不是Executor
。调用方法的时候,那么会执行代理类Plugin
的invoke方法:
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);
}
}
Invocation
的定义如下,它的proceed
方法也就是调用原先方法(不走代理)。
public class Invocation {
private final Object target;
private final Method method;
private final Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
public Object getTarget() {
return target;
}
public Method getMethod() {
return method;
}
public Object[] getArgs() {
return args;
}
//调用原先方法(不走代理)
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
}
3 利用拦截器开发SQL延迟指标
接下来,利用Mybatis拦截器,我们开发一个SQL获取延迟指标,监控到的指标可以实时上报到监控平台,方便开发和运维关注应用的慢sql。
3.1 @Intercepts定义
对Executor
的query
和update
方法进行拦截::
@Slf4j
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class,
Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class,
Object.class})
})
3.2 Interceptor实现
public class PerfInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement statement = (MappedStatement)invocation.getArgs()[0];
// method:sql执行的 [Mapper接口]#[mapper方法]
String method = this.parseMethod(statement);
long start = System.currentTimeMillis();
Object res = invocation.proceed();
// latency:sql执行耗时
long latency = System.currentTimeMillis() - start;
// type:sql类型:如update, query
String type = statement.getSqlCommandType().name();
log.info("The latency of method {} is {}, type = {}", method, latency, type);
return res;
}
private String parseMethod(MappedStatement mappedStatement){
String[] splits = StringUtils.split(mappedStatement.getId(), '.');
return splits[splits.length - 2] + "#" + splits[splits.length - 1];
}
}
3.3 加入Spring容器
第一步:在Spring中定义bean perfInterceptor
;
第二步:通过代码配置或者xml配置的方式将perfInterceptor
配置到SqlSessionFactoryBean
。
下面是xml配置的示例:
<!-- myBatis文件 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
...
<property name="plugins">
<list>
<ref bean="perfInterceptor"/>
</list>
</property>
</bean>
3.4 完整代码
package com.artemis.xm.mybatis;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;Ø
import java.util.Properties;
@Slf4j
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class,
Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class,
Object.class})
})
public class PerfInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement statement = (MappedStatement)invocation.getArgs()[0];
// method:sql执行的 [Mapper接口]#[mapper方法]
String method = this.parseMethod(statement);
long start = System.currentTimeMillis();
Object res = invocation.proceed();
// latency:sql执行耗时
long latency = System.currentTimeMillis() - start;
// type:sql类型:如update, query
String type = statement.getSqlCommandType().name();
log.info("The latency of method {} is {}, type = {}", method, latency, type);
return res;
}
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
public void setProperties(Properties properties) {
// NOP
}
private String parseMethod(MappedStatement mappedStatement){
String[] splits = StringUtils.split(mappedStatement.getId(), '.');
return splits[splits.length - 2] + "#" + splits[splits.length - 1];
}
}