这次呢,大致介绍一些Mybatis的实现原理与总体设计。
Mybatis提供了方便的方式,直接通过注入一个interface,就可以实现方便的数据库查询工作。
但是仔细观察会发现,每一个interface其实并没有自己的实现类,那么mybatis是怎么让他实际去读写数据库的呢?
其实就是通过 动态代理, 动态代理在Mybatis中用的很多。
而要讲解这个代理的流程,需要先说一下Mybatis的一个核心类
SqlSession本身是一个接口,结构并不复杂
<T> T selectOne(String statement, Object parameter);
<E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds);
<K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds);
void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler);
int insert(String statement, Object parameter);
int update(String statement, Object parameter);
int delete(String statement, Object parameter);
void commit(boolean force);
void rollback(boolean force);
List<BatchResult> flushStatements();
void close();
void clearCache();
/**
* Retrieves current configuration
* @return Configuration
*/
Configuration getConfiguration();
/**
* Retrieves a mapper.
* @param <T> the mapper type
* @param type Mapper interface class
* @return a mapper bound to this SqlSession
*/
<T> T getMapper(Class<T> type);
/**
* Retrieves inner database connection
* @return Connection
*/
Connection getConnection();
}
从接口就可以看出来,SqlSesion的核心功能,就是实际的数据库操作。
并且,中间有getMapper方法,也就是说,Mapper的代理Proxy其实是由SqlSession提供
而数据库操作需要的几个东西:数据库连接和数据库操作的语句,在它的接口中并没有体现出来。
而主要通过一个参数
int insert(String statement, Object parameter);]]></ac:plain-text-body>
一个statement来传递,这个所谓的statement,比较好理解,就是通过mapper.xml们中的配置,加载过来的东西。
mapper接口和mapper.xml的大致关系就是:
一个Mapper.xml的一个sql方法会通过xml解析成一个MappedStatment, 一个MappedStatement就是一整个sql方法的整合类,它的属性大概有
public final class MappedStatement {
private String resource;
private Configuration configuration;
private String id;
private Integer fetchSize;
private Integer timeout;
private StatementType statementType;
private ResultSetType resultSetType;
private SqlSource sqlSource;
private Cache cache;
private ParameterMap parameterMap;
private List<ResultMap> resultMaps;
private boolean flushCacheRequired;
private boolean useCache;
private boolean resultOrdered;
private SqlCommandType sqlCommandType;
private KeyGenerator keyGenerator;
private String[] keyProperties;
private String[] keyColumns;
private boolean hasNestedResultMaps;
private String databaseId;
private Log statementLog;
private LanguageDriver lang;
private String[] resultSets;
}
其id就是它的对应的接口方法的全名称
并且以Map的形式统一存在了Configuration的属性里面
然后,MapperProxy也就是实现接口动态代理的类
public class MapperProxy<T> implements InvocationHandler, Serializable {
private static final long serialVersionUID = -6424540398559729838L;
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;
public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
private MapperMethod cachedMapperMethod(Method method) {...}
}
这里我们可以看出,动态代理对接口的绑定。
而我们在代码中实际注入的,就是这个MapperProxy代理类
它的产生
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
......
}
了解动态代理的同学能够明白,当执行被代理的interface的时候,如果执行的对象是一个代理对象,则就会运行到MapperProxy的invoke方法中。
而mapperMethod的execute方法当中,实际执行的是
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
if (SqlCommandType.INSERT == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
} else if (SqlCommandType.UPDATE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
} else if (SqlCommandType.DELETE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
} else if (SqlCommandType.SELECT == command.getType()) {
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 {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
} else {
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;
}
MapperMethod采用 命令模式 运行,根据上下文的条件的不同,可以跳转到sqlSession对应不同的方法当中。
至此,我们应该都知道为何Myabtis只用Mapper接口就可以运行SQL了,因为mapper.xml文件中的命名空间,就对应的是interface的全路径,然后通过路径和方法,就可以将对应的Sql找到并运行。
从上面可以看到,映射器其实是通过动态代理,进入到了MapperMethod的execute方法,然后根据简单的判断,就进入到了SqlSession的增删改查的方法当中,但是这些方法具体是怎么执行的呢?
其实SqlSession下又四个核心的对象
Executor是真正执行Java和数据库交互的东西。Mybatis存在三种执行器,可以在文件中配置选择
分别是
可以看一下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);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
其实这里就是根据哪种类型来创建一个新的Executor
每一个sqlSesiion都会创建一个全新的Executor
接下来以SimpleExecutor的查询方法为例
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);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
//prepare方法
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
可以简单的看出,configuration提供了StatementHandler的生产
然后通过调用StatementHandler的prepare方法来对进行一些预先的设置与编译
包括对数据库语句的预编译,防止SQL注入,以及一些超时时间,查询大小的设置等。
然后就进入到第二个重要对象,StatementHandler
这个是用来专门处理与数据库交互的。先看下Mybatis是怎么生成它的
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) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
实际创建的是RoutingStatementHandler。
而RoutingStatementHandler也只是一个代理对象,我们先看下其构造方法
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
}
可以看到,也有三种不同的Handler,并且作为代理存在于RoutingStatementHandler中。这三种不同的Handler其实也是对应着之前提到的三种不同的Executor
statment的执行就比较简单了
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
String sql = boundSql.getSql();
statement.execute(sql);
return resultSetHandler.<E>handleResultSets(statement);
}
简单的运行statment,然后将结果ResultSet交给Resulthandler去处理。
至此,我们可以看一下一整个SqlSession的查询过程的流程
我们之前有看到,四大对象在创建的时候,会调用一行代码
executor = (Executor) interceptorChain.pluginAll(executor);
这就是,将四大对象,与插件进行绑定。
这里使用了 责任链的设计模式
于是,我们可以无缝添加很多的插件在Mybatis的运行过程中,并且在四大对象调度的时候,寻找合适的时机运行我们的代码。这就是Mybatis的插件技术
Mybatis的插件是对Mybatis的底层的修改,所以是存在一定的危险性的
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
这里有三个方法
插件的代理用的是责任链模式,其就是一个对象,可以是Mybatis的Sqlsession的四大对象的任意一个,在多个角色中进行传递。在传递链条上任何一个插件都有可以处理它的权利。
以Executor为例子,前面说到过,创建的时候执行过
executor = (Executor) interceptorChain.pluginAll(executor);
pluginAll的实现是
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
比较好理解,只是将预先加载好的插件拿出来循环一次,然后依次调用其plugin方法,对新生成的executor进行代理设置。这里可以看出
一个target被代理一次之后,会被第二个插件进行再一次的代理,是一个递归的代理模式。
大致为:
生成代理的方式,Mybatis提供了一个现成的实现,可以直接调用
public class Plugin implements InvocationHandler {
//生成代理对象
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;
}
//代理对象的实际方法执行
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);
}
}
可以看出,提供了一个Plugin类,然后调用其warp方法,就会对指定的对象生成一个动态代理对象。
而动态代理对象的方法执行的时候,就会自动跳转到invoke方法
而invoke方法就会调用其intercept方法,将一个包装好的Invocation对象作为参数传给它。
然后插件再对这个方法的执行与否,进行自己的判断与逻辑
PageHelper是我们在使用Mybatis中经常使用的,分页工具
在mybatis中的配置
<plugins>
<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageHelper">
<!-- 4.0.0以后版本可以不设置该参数 -->
<property name="dialect" value="mysql"/>
<!-- 该参数默认为false -->
<!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 -->
<!-- 和startPage中的pageNum效果一样-->
<property name="offsetAsPageNum" value="false"/>
<!-- 该参数默认为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,orderBy,不配置映射的用默认值 -->
<!-- 不理解该含义的前提下,不要随便复制该配置 -->
<property name="params" value="pageNum=pageHelperStart;pageSize=pageHelperRows;"/>
<!-- 支持通过Mapper接口参数来传递分页参数 -->
<property name="supportMethodsArguments" value="false"/>
<!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page -->
<property name="returnPageInfo" value="none"/>
</plugin>
</plugins>
通过在mybatis-config.xml中配置之后,此插件PageHelper就会添加到Mybatis插件的责任链interceptorChain当中去。
对四大对象的加载过程中,就会依次生成对应的代理对象
public class PageHelper implements Interceptor {
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
public Object intercept(Invocation invocation) throws Throwable {
if (autoRuntimeDialect) {
SqlUtil sqlUtil = getSqlUtil(invocation);
return sqlUtil.processPage(invocation);
} else {
if (autoDialect) {
initSqlUtil(invocation);
}
return sqlUtil.processPage(invocation);
}
}
}
从上面能看得出来,PageHelper其实是对整个Executor进行来代理,也就是说整个执行过程就行了责任处理。之后的流程细节就不仔细看了,不过原理明白了,也很容易联想到,之后是对sql的参数进行了拦截,然后添加上了分页、排序、limit等信息。