在mybatis环境中我们定义的是接口,但是最终可以通过getMapper(classType)方法获取到该接口的实现类,然后调用数据库的查询方法,很显然是mybatis帮我们做了jdk的动态代理,本文的环境仍然与上一篇博客一致。
很显然是jdk的动态代理,下面我们就来看看mybatis如何实现代理的过程,首先调用getMapper方法,这是个空壳方法,传入的参数type就是我们需要代理的接口的class类型,this通过SqlSessionFactory的openSession方法获取的SqlSession。
这里的knownMappers在mybatis解析mapper配置文件的时候就已经添加完成了,这里仅仅贴出部分代码
可以看到在解析mapper的时候往knowMappers集合中存放了key为mapper接口的class类型,value为MapperProxyFactory的实力对象,所以这里就通过我们传入的mapper接口的class类型去获取mapperProxyFactory用来判断传入的接口是否经过了解析注册到了mybatis中,如果解析完毕的mapper那么一定是存在这样的键值对的,判断mapperProxyFactory不为空就去调用newInstance方法去获取动态代理对象。
但是这里不知道为什么mybatis要这样设计,在笔者看来因为每一个mapper接口的代理工厂对象都是同一个完全可以定义一个全局变量来生成动态代理对象
jdk动态代理需要实现InvocationHandler接口,而实现这个接口的实现类就是MapperProxy,这里的methodCache是一个缓存集合,因为如果每次mapper都要经过一次动态代理比较耗费性能,所以在当前的session中,被代理后的mapper会被缓存到集合中
jdk动态代理实现mapper接口的代理过程
当我们在调用UserMapper的findAll方法,其实调用的是代理对象的invoke方法
- proxy - 是代理对象
- method - 原方法
- args - 调用方法时所传入的参数
Object.class.equals(method.getDeclaringClass()
这里要判断当前所调用的方法时候属于Object类,因为如果我们调用的方法比如hashcode()、equals等这些事继承于Object类的方法,那么mybatis就不需要进行动态代理了,直接执行父类的逻辑即可,这里也很容易理解,因为mybatis无法帮我们完成对这些方法的逻辑代理。
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
// A workaround for https://bugs.openjdk.java.net/browse/JDK-8161372
// It should be removed once the fix is backported to Java 8 or
// MyBatis drops Java 8 support. See gh-1929
MapperMethodInvoker invoker = methodCache.get(method); //判断该方法在当前的session中是否已经代理过,代理过的方法会缓存在map中
if (invoker != null) {
return invoker;
}
return methodCache.computeIfAbsent(method, m -> {
//判断是不是接口的默认方法,是默认方法就不做代理
if (m.isDefault()) {
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
} else {
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
调用cacheInvoker方法实现数据的封装,首先判断在缓存中是否有当前方法的代理,如果已经封装过操作数据库所需要的数据就不需要重复执行这个过程,直接返回调用invoke即可,不存在的话就先判断我们当前调用的方法是不是接口中的default方法,是的话也不需要执行额外的操作,因为mybatis事先也不可能得知程序员要在default中执行的逻辑。
这里就开始封装操作数据库所需要的数据,来自两方面的内容,sql语句相关的内容以及接口方法的内容
这个时候调用的invoke方法与InvocationHandler的invoke方法没有什么关系,是PlainMethodInvoker的方法,而PlainMethodInvoker对象封装了所有我们所需要的数据,程序运行到这里,数据已经都准备好了,接着调用execute方法
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;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} 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;
}
通过switch-case根据操作类型执行不同的逻辑,我们这里调用的findAll方法返回值是List<User>
,所以最终调用的是
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
//获取方法参数的签名
Object param = method.convertArgsToSqlCommandParam(args);
if (method.hasRowBounds()) {
//判断是否需要通过mybatis提供的方式做分页查询,一般不会用,因为mybatis是查出数据后然后在内存中做了分页操作
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.selectList(command.getName(), param, rowBounds);
} else {
result = sqlSession.selectList(command.getName(), param);
}
// issue #510 Collections & arrays support
if (!method.getReturnType().isAssignableFrom(result.getClass())) {
if (method.getReturnType().isArray()) {
return convertToArray(result);
} else {
return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
}
}
return result;
}
先来看看参数解析的方法:
public Object getNamedParams(Object[] args) {
//使用@Param注解可以自定义参数名
//获取参数的个数,如果没有使用@Param注解那么在多个参数的时候通过jdk获取到的参数名就是arg0,arg1,arg2或者是param1,param2,param3,否则mybatis为sql语句赋值会报错
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
//方法上面没有加注解,且方法只有一个参数就不做改动,直接返回
} else if (!hasParamAnnotation && paramCount == 1) {
Object value = args[names.firstKey()];
return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
} else {
//方法含有多个参数比如,User findByCondition(String username,String address,String sex)
final Map<String, Object> param = new ParamMap<>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
//好像是jdk的问题,获取方法参数名的时候获取到的就是arg0、arg1、arg2...
param.put(entry.getValue(), args[entry.getKey()]);
// add generic param names (param1, param2, ...)
final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
// ensure not to overwrite parameter named with @Param
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
args是我们传入的参数,如果我们传入的参数值只有一个,在学应用的时候我们就知道,这种情况下#{}内的占位符字符串不管写什么都可以正常使用,原理就在这里,如果只传入了一个参数就直接返回出去,不做任何处理。如果传入的参数有多个那么占位符中只能填arg0、arg1、arg2…或者param1、param2、param3…否则会报错,因为这源自于jdk的一个问题,通过反射获取方法的参数名的时候在jdk1.8以前是无法获取真实参数名的,在jdk1.8中也需要增加一些配置才可以获取到,所以如果想要在占位符中使用自定义的参数名那么就需要通过@Param注解来实现。
method.hasRowBounds()
是判断我们是否使用了mybatis内置的分页方法,但是mybatis原生的分页其实是不合理的,它是查询出数据后在内存中分页,一般也用不到。如果没有使用那么在调用selectList方法的时候传入的RowBouds中的offset参数是0,limit参数是Integer的最大值
MappedStatement在解析mapper文件完成的时候就会将数据封装到这个对象中,存放MappedStatement的集合会保存在Configuration全局配置对象中
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//对于${}情况。进行解析sql,并且设置值,但是对于#{},这里只会用?进行替换占位符,还没有设置值
BoundSql boundSql = ms.getBoundSql(parameterObject);
//创建一个session级别的缓存
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
首先通过MappedStatement的getBoundSql
方法去解析sql,因为前文说过如果是${}在解析mapper文件的sql语句的时候不会解析,只有#{}会在解析sql语句的时候解析,这里调用getBoundSql方法就体现了面向接口编程的好处,代码只需要一份,在解析mapper文件的时候,如果判断sql语句中存在 ${}不管sql语句中是否存在#{},那么都不会去解析直接返回DynamicSqlSource
在RowSqlSource中这里就不需要做处理了直接传入参数构建BoundSql对象返回即可
@Override
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
rootSqlNode.apply(context);//解析${},${'param1'} -> ${我们传入的参数} 仅仅做了个字符串的替换
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); //将#{parameterName} 解析成 占位符?
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
}
调用apply方法解析sql,解析方法主要是操作字符串比较琐碎就列举一部分
解析之前的sql语句
解析之后的sql语句
这里就可以发现${}不能防止sql注入攻击的原因,因为处理的时候是做字符串的拼接,而#{}最终是通过PrepareStatement的set方法来设置会有转义符来保证不会将条件参数变成查询命令的一部分。
解析#{}
解析#{}与${}的方法一模一样,也是字符串的拼接:
解析之前的sql语句:
解析之后的sql语句:
可以发现解析${}与解析#{}的区别在于前者会将占位符替换然后拼接我们传入的参数值,但是后者并没有拼接上参数,而且是后面通过PrepareStatement的set方法来设置参数,这也就是为什么可以避免sql注入攻击的原因所在。
解析完sql以后会创建一个缓存对象,缓存本次查询的参数,这是一个session级别的缓存,有关缓存的问题再下一篇博客中再详细探讨。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//这个cache是二级缓存
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
执行操作前会先去判断二级缓存是否存在,如果有缓存就不会去执行查询,这里就不探讨缓存的问题,直接进入query方法
public <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++; //localCache 一级缓存
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 #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
同样在调用查询方法之前会判断一级缓存(session)中是否有数据
这里是之前调用过findAll方法查询过的记录,可以看到已经在session缓存中了,缓存中没有数据就会调用queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
//将一级缓存保存在当前的session中,现在还没有执行查询结果,所以放一个空的类似于占位符
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;
}
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();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//初始化(获取链接对象,获取jdbc中的Statement对象) 给参数赋值
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
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) { //String对应varchar,mybatis会有默认的一些类型对应关系,一般很少用
jdbcType = configuration.getJdbcTypeForNull();
}
try {
typeHandler.setParameter(ps, i + 1, value, jdbcType); //调用jdbc的方法设置参数值
} catch (TypeException | SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
下面我们再跟踪到最终的赋值操作
可以发现底层处理#{},就是通过PreparedStatement的set方法来实现,查询出结果后就返回查询结果的数据,然后移除之前保存在session中的当前查询语句的缓存结果,更近记录以后再put到缓存数据的集合中。