Mybatis 使用与深入解析
Mybatis 使用
解决问题思路
- 查看异常栈,是否可以从异常描述找到问题原因
- 如果异常描述找不到具体原因,可以尝试定位报错的源码行,断点debug查看报错原因
排查经历 之 类型转换
- 接口
List<PrmPromotionEntity> getPromotionOverViewList(String tenantCode, Date startTime, Date endTime);
- XML
<select id="getPromotionOverViewList" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
select
<include refid="Base_Column_List" />
from PRM_PROMOTION p
where <![CDATA[ p.start_time > #{startTime, jdbcType = TIMESTAMP} ]]>
<![CDATA[ and p.end_time <= #{endTime, jdbcType = TIMESTAMP} ]]>
and p.tenant_code = #{tenantCode, jdbcType=VARCHAR}
and p.lifecycle = 'PUBLISH'
</select>
- ERROR 日志
错误日志很明显:java.util.Date cannot be cast to java.lang.Integer,但是怎么解决确不知道。
如果对Mybatis源码比较清楚,这一堆东西是可以解决问题的,至少能知道它是在说什么。
Could not set parameters for mapping: ParameterMapping{property=‘startTime’, mode=IN, javaType=class java.lang.Integer, jdbcType=TIMESTAMP, numericScale=null, resultMapId=‘null’, jdbcTypeName=‘null’, expression=‘null’}.
很明显,我是没看懂,所以选择debug去了。将以下几行代码依次打上断点。
分析会得知,入参:startTime,javaType 是 Integer,但是jdbcType是TIMESTAMP,所以我看了一下入参和xml,发现我入参是String tenantCode, Date startTime, Date endTime,但是xml设置是parameterType=“java.lang.Integer”,Mybatis会将三个入参都当Integer来解析。
尝试去掉parameterType=“java.lang.Integer”,Mybatis会将三个参数当做object解析,报错解决。
- 尝试不是用xml,用Mybatis注解
- ERROR日志
一看,Sql有问题,理论上不应该有问题呀,对比了一下,注解和xml的SQL。哦哦哦,面试中常见的# 和 $ 符号的问题,我这里用的$,是直接替换的,所以 canadaGoose_001连引号都没有。改成# 就可以了。 - 那么,到此就没有问题了么?跑一下发现还有问题,debug日志看一下,发现数据有的,但是封装java对象的时候只有id是有值的。很明显咯,数据库字段名用的下划线分割而java用的驼峰法。
怎么解决呢?没有去百度,因为可以确定是结果解析有问题,同时可以确定有下划线转驼峰法逻辑,所以ResultHandler相关的类打了一些断点,开发分析,在DefaultResultSetHandler中找到以下代码:final String property = metaObject.findProperty(propertyName, configuration.isMapUnderscoreToCamelCase());
所以咯!设置isMapUnderscoreToCamelCase为true就可以了。去官网找,
当然官网还有很多其他配置。https://mybatis.org/mybatis-3/zh/configuration.html
- 使用的java配置
@Configuration
public class MyBatisConfig {
@Bean
ConfigurationCustomizer mybatisConfigurationCustomizer() {
return configuration -> configuration.setMapUnderscoreToCamelCase(true);
}
}
排查经历 之 时间类型转换
Mybatis 深入解析
SqlSessionFactoryBuilder
SqlSessionFactory
可以看出,DefaultSessionFactory暴露出的接口具体实现是openSessionFromConnection和openSessionFromDataSource 两个方法。
openSessionFromConnection和openSessionFromDataSource 两个方法区别在于事务的开启参数和自动提交参数来源不一样。
一个是与connection保持一致,一个是与数据库连接信息中保持一致。
会发现一个问题,openSession方法中没有太多复杂操作。
配置、执行器以及是否自动提交,下面我们重点看一下Executor。
Executor
可以看出Executor有三种基本类型:简单、复用和批处理
另外还有一个包装类CachingExecutor.
几种执行器的作用显而易见,那么这些都是怎么实现的呢?让我们继续往下看。
无论是哪个都继承了BaseExecutor,而BaseExecutor实现了一些通用操作。
类 | 结构 |
---|---|
SimpleExecutor | ![]() |
ReuseExecutor | ![]() |
BatchExecutor | ![]() |
- Executor 中枢作用
从Executor的入参就可以看出,Executor搜集了执行一个SQL的所有条件,如:MappedStatement【statement信息】,parameter【参数】,rowBounds【返回数据条数限制】,ResultHandler【结果集处理】,BoundSql【执行的SQL】
其实,此刻,基本流程已经完了。但是中间有很多细节,前面各个参数已经初始化好,Executor只是调用各模块实现,下面可以逐个分析。
-
另外,对比一下SimpleExecutor 和 ReuseExecutor 的 doUpdate 和 doQuery 方法。
SimpleExecutor 在doUpdate 和doQuery执行后,加了closeStatement。根据经验猜测,
- SimpleExecutor每次调用都会创建一个Statement,用完立刻close;
- ReuseExecutor会复用Statement。
而获取Statement方式都是使用的prepareStatement方法。
继续撸代码,你会发现,StatementHandler有两个实现类,进哪个类呢? BaseStatementHandler,因为RoutingStatementHandler是包装类,具体相关请看Statement章节。
-
其实,看着这里已经蒙了,继续看doUpdate,doQuery的具体实现,发现会涉及到StatementHandler,BoundSql等不清楚干了些什么。先看一下Statement。
StatementHandler
发现StatementHandler其实也只是对JDBC的Statement获取做了一些封装。
-
instantiateStatement:Statement实例化,不同handler实现创建Statement参数不一样,可以看一下JDBC的Connection创建Statement接口。
-
query、update方法封装实现也比较简单
SimpleStatementHandler: @Override public void batch(Statement statement) throws SQLException { String sql = boundSql.getSql(); statement.addBatch(sql); } @Override public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { String sql = boundSql.getSql(); statement.execute(sql); return resultSetHandler.handleResultSets(statement); }
理解:StatementHandler 简化JDBC的statement操作,并做了一个简单分类。
ParameterHandler
不难看出,ParameterHandler是提供给CallableStatement和PreparedStatement使用的。
且,实现只有DefaultParameterHandler一个。那么DefaultParameterHandler.setParameters都干了什么呢?
-- DefaultParameterHandler:
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException | SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
ResultSetHandler
Plugin 结合实际场景分析
项目中的包结构依赖
实力打脸了。IDEA的《External Libraries》 包含了项目中所有的模块的jar,如果各个模块独立运行,即使使用的jar包版本不一样,也没有问题,恰巧,我们现在就是这样,有一个模块开发,使用了tkMybatis,没有影响其他模块,因为其他模块没有引用此模块。从项目管理角度,这种问题是不可以发生的,应该将依赖放入dependencyManager中管理。
Plugin 实践到原理
0 使用Mybatis插件实现审计字段自动注入。
@Component
@Intercepts({
@Signature(
type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class}),
@Signature(
type = StatementHandler.class,
method = "parameterize",
args = {Statement.class})})
public class MybatisEntityPluginInterceptor implements Interceptor{
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
Object[] args = invocation.getArgs();
if (args[0] instanceof MappedStatement) {
// 映射的各种信息,SQL信息、接口方法对应的参数、接口方法的全名称等等
MappedStatement mappedStatement = (MappedStatement) args[0];
// 获取执行语句的类型
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
if (args[1] instanceof HashMap<?, ?>) {
HashMap<?, ?> hashMap = (HashMap<?, ?>) args[1];
// 这里分别处理每个参数的实体注入
// mappedStatement.getId() -> 获取方法的全路径; 例如: cn.xxx.xxx.xxx.ClassName.methodName
String id = mappedStatement.getId();
Optional<Method> methodInMapperOpt = getMethodInMapper(id);
if(!methodInMapperOpt.isPresent()){
return invocation.proceed();
}
String[] paramKeys = parseParamKeysByMethod(methodInMapperOpt.get());
for (String key : paramKeys) {
Object o = hashMap.get(key);
if (o instanceof List) {
List<?> list = (List<?>) o;
for (Object entity : list) {
Field[] allAuditFields = FieldAccess.get(entity.getClass()).getFields();
// Field[] allAuditFields = MyReflectUtils.getAllAuditFields(entity.getClass());
setAuditField(sqlCommandType, entity, allAuditFields);
}
} else if (o instanceof BaseEntity) {
// 这里处理不是集合的情况,通过继承 cn.dmahz.entity.Base,可证明为Java Bean
Field[] allAuditFields = FieldAccess.get(o.getClass()).getFields();
// Field[] allAuditFields = MyReflectUtils.getAllAuditFields(o.getClass());
setAuditField(sqlCommandType, o, allAuditFields);
}
}
} else if (args[1] instanceof BaseEntity) {
Object o = args[1];
// 这里处理不是集合的情况,通过继承 cn.dmahz.entity.Base,可证明为Java Bean
Field[] allAuditFields = FieldAccess.get(o.getClass()).getFields();
// Field[] allAuditFields = MyReflectUtils.getAllAuditFields(o.getClass());
setAuditField(sqlCommandType, o, allAuditFields);
}
}
} catch (Exception e) {
log.warn("Generator Audit Field has error.", e);
}
// 让拦截器继续处理剩余的操作
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
private Optional<Method> getMethodInMapper(String id) throws ClassNotFoundException {
String className = id.substring(0, id.lastIndexOf("."));
String methodName = id.substring(id.lastIndexOf("."));
return Arrays.stream(Class.forName(className).getMethods()).filter(one -> one.getName().equals(methodName)).findFirst();
}
/**
* 解析方法的形参的key值,key值用于在 ParamMap中查找值,进行填充审计字段
*/
private String[] parseParamKeysByMethod(Method method) {
ArrayList<String> keyList = new ArrayList<>();
Parameter[] parameters = method.getParameters();
for (Parameter parameter : parameters) {
Param parameterAnnotation = parameter.getAnnotation(Param.class);
if (parameterAnnotation != null) {
keyList.add(parameterAnnotation.value());
} else {
// 形参名称
String name = parameter.getName();
// 类型的简写名称
// String simpleName = parameter.getType().getSimpleName();
if (StringUtils.isNotBlank(name)) {
keyList.add(name);
}
}
}
return keyList.toArray(new String[0]);
}
/**
* 包装一下重复的代码,方便调用
*/
private void setAuditField(SqlCommandType sqlCommandType, Object o, Field[] fields) throws IllegalAccessException {
for (Field field : fields) {
setAuditField(sqlCommandType, o, field);
}
}
/**
* 简化,创建、更新时间由数据库自动操作,创建人、修改人简化为:操作人(operator)
*/
private void setAuditField(SqlCommandType sqlCommandType, Object o, Field field) throws IllegalAccessException {
if ((sqlCommandType == SqlCommandType.INSERT || sqlCommandType == SqlCommandType.UPDATE)
&& field.isAnnotationPresent(AutoOperator.class)) {
String currentCode;
try {
currentCode = CommonBizUtil.getCurrentUserCode();
} catch (Exception e) {
//这里仅作测试,忽略空指针异常
currentCode = "system"; //TODO: 非Web环境,当前用户ID测试值(创建值),这里暂时使用默认值:system
}
field.setAccessible(true);
field.set(o, currentCode);
}
// if (sqlCommandType == SqlCommandType.INSERT) {
// if (field.isAnnotationPresent(CreatedBy.class)) {
// String currentUserId;
// try {
// currentUserId = CommonUtil.getCurrentUserCode();
// } catch (NullPointerException e) {
// //这里仅作测试,忽略空指针异常
// currentUserId = "非Web环境,当前用户ID测试值(创建值)";
// }
// field.set(o, currentUserId);
// } else if (field.isAnnotationPresent(CreatedDate.class)) {
// field.set(o, System.currentTimeMillis());
// } else if (field.isAnnotationPresent(Id.class)) {
// String uuId = UUID.randomUUID().toString();
// field.set(o, uuId.replace("-", ""));
// }
// } else if (sqlCommandType == SqlCommandType.UPDATE) {
// if (field.isAnnotationPresent(LastModifiedBy.class)) {
// String currentUserId;
// try {
// currentUserId = CommonUtil.getCurrentUserCode();
// } catch (NullPointerException e) {
// //这里仅作测试,忽略空指针异常
// currentUserId = "非Web环境,当前用户ID测试值(更新值)";
// }
// field.set(o, currentUserId);
// } else if (field.isAnnotationPresent(LastModifiedDate.class)) {
// field.set(o, System.currentTimeMillis());
// }
// }
}
}
大致看了一下,
1 Mybatis 配置类Configuration 维护一个interceptorChain,各个入口将Interceptor实现类添加到interceptorChain中
1.1 Mybatis自身通过XML解析处Interceptor实现类
1.2 Spring通过注解解析
1.3 而PageHelper 则是通过自动装载自动添加,不错的思路
2 我们在创建四大对象(ParameterHandler、ResultSetHandler、StatementHandler、Executor)的时候,会通过InterceptorChain依次遍历插件,调用插件的plugin方法。
2 -> 3 通常plugin方法实现是这样的,将插件自己嵌入四大对象中。
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
3 Plugin 代码大致是一个代理,贴出来先。
public class Plugin implements InvocationHandler {
private final Object target;
private final Interceptor interceptor;
private final 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,
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);
}
}
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
// issue #251
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<>();
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
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<>();
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()]);
}
}
4 …
Spring 集成Mybatis
在上一节Plugin有提到,Spring通过注解解析添加Interceptor,那么,这块是怎么实现的呢?
1.mybatis-spring-boot-autoconfigure-2.0.1.jar 包装MybatisAutoConfiguration类会ObjectProvider加载Interceptor的SpringBean的数据。
2.mybatis-spring-2.0.1.jar包中SqlSessionFactoryBean implements FactoryBean, InitializingBean, ApplicationListener 通过afterPropertiesSet构造sqlSessionFactory,此间会遍历第一步加载的Interceptor,调用addInterceptor方法。
通过MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware 实现动态注册Mybatis的相关的Mapper类
总结
架构
基本缓存
- PerpetualCache使用HashMap实现
装饰器
- BlockingCache:阻塞包装器
从缓存中getObject时,首先回去尝试获取锁,当一个线程getObject时,没有从缓存中拿到值,就会一直持有锁,直到
putObject将锁释放;这个保证了多个线程同时访问且缓存没有时,只需要一个线程A去访问数据库,其他线程阻塞,直到A线程从数据库拿到数据并维护到缓存中。
@Override
public void putObject(Object key, Object value) {
try {
delegate.putObject(key, value);
} finally {
releaseLock(key);
}
}
@Override
public Object getObject(Object key) {
acquireLock(key);
Object value = delegate.getObject(key);
if (value != null) {
releaseLock(key);
}
return value;
}
- FifoCache:缓存大小为1024的先进先出队列包装器
可以看一下基本缓存PerpetualCache类,只是对HashMap进行了简单的包装,没有大小限制。如果长时间运行,缓存的数据会越来越多,导致内存溢出,FifoCache包装类就是为了解决以上问题。程序启动时,delegate和keyList都为空,随着时间推移,缓存的sql结果越来越多,当达到1024后,如果有第1025个SQL结果调用putObject方法,将会把第1个SQL结果删除,然后把1025入缓存,这样后续内存中将一直保持缓存1024个SQL结果。
private final Cache delegate; // 被包装的缓存
private final Deque<Object> keyList; // 队列
private int size; // 缓存大小,默认1024
@Override
public void putObject(Object key, Object value) {
cycleKeyList(key);
delegate.putObject(key, value);
}
private void cycleKeyList(Object key) {
keyList.addLast(key);
if (keyList.size() > size) {
Object oldestKey = keyList.removeFirst();
delegate.removeObject(oldestKey);
}
}
- LoggingCache:日志及统计
private final Log log;
private final Cache delegate;
protected int requests = 0; // 请求次数
protected int hits = 0; // 命中次数
@Override
public Object getObject(Object key) {
requests++;
final Object value = delegate.getObject(key);
if (value != null) {
hits++;
}
if (log.isDebugEnabled()) {
log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio()); // 日志打印命中率
}
return value;
}
private double getHitRatio() {
return (double) hits / (double) requests;
}
- LruCache:最近最少使用队列包装器
没看懂,慢慢研究
/**
* Copyright 2009-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ibatis.cache.decorators;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.ibatis.cache.Cache;
/**
* Lru (least recently used) cache decorator.
*
* @author Clinton Begin
*/
public class LruCache implements Cache {
private final Cache delegate;
private Map<Object, Object> keyMap;
private Object eldestKey;
public LruCache(Cache delegate) {
this.delegate = delegate;
setSize(1024);
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
return delegate.getSize();
}
public void setSize(final int size) {
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
eldestKey = eldest.getKey();
}
return tooBig;
}
};
}
@Override
public void putObject(Object key, Object value) {
delegate.putObject(key, value);
cycleKeyList(key);
}
@Override
public Object getObject(Object key) {
keyMap.get(key); //touch
return delegate.getObject(key);
}
@Override
public Object removeObject(Object key) {
return delegate.removeObject(key);
}
@Override
public void clear() {
delegate.clear();
keyMap.clear();
}
private void cycleKeyList(Object key) {
keyMap.put(key, key);
if (eldestKey != null) {
delegate.removeObject(eldestKey);
eldestKey = null;
}
}
}
- ScheduledCache:缓存超时包装器
缓存超时控制,默认1小时。
private final Cache delegate;
protected long clearInterval;
protected long lastClear;
public ScheduledCache(Cache delegate) {
this.delegate = delegate;
this.clearInterval = TimeUnit.HOURS.toMillis(1);
this.lastClear = System.currentTimeMillis();
}
private boolean clearWhenStale() {
if (System.currentTimeMillis() - lastClear > clearInterval) {
clear();
return true;
}
return false;
}