1、MyBatis扩展点plugins
mybatis的扩展是通过拦截器Interceptor来实现的,本质上就是JDK的动态代理,所以它只能对接口进行拦截,我们一步步看一下MyBatis是如何将这些扩展暴露给我们开发者使用的。
SqlSession的创建过程【重点】:
mybatis中的SQL都是通过DefaultSqlSession去执行的。
MyBatis 是怎么构造 DefaultSqlSession 的?:【重点】
通过查看源码,得知 MyBatis 是通过 DefaultSqlSessionFactory 来构造 DefaultSqlSession 的。
DefaultSqlSessionFactory#openSessionFromDataSource(ExecutorType, TransactionIsolationLevel, boolean)
/**
* @param ExecutorType 执行器的类型。MyBatis中提供了三种执行器:SIMPLE, REUSE, BATCH。默认的是 SIMPLE
* @param TransactionIsolationLevel 事务隔离级别
* @param autoCommit 是否自动提交事务
*/
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); // 通过 Environment 获取事务工厂 TransactionFactory。没有指定Environment,则使用 ManagedTransactionFactory
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); // 从 TransactionFactory 中获取一个 Transaction
final Executor executor = configuration.newExecutor(tx, execType); // 从 Configuration 中获取一个新的 Executor。(Configuration 对应的是 mybatis-config.xml 中的配置)
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();
}
}
重点看一下 Configuration#newExecutor(Transaction transaction, ExecutorType executorType)
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType; // 默认使用 SimpleExecutor
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction); // 使用 BatchExecutor
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction); // 使用 ReuseExecutor
} else {
executor = new SimpleExecutor(this, transaction); // 默认使用 SimpleExecutor
}
if (cacheEnabled) {
executor = new CachingExecutor(executor); // 使用 CachingExecutor
}
executor = (Executor) interceptorChain.pluginAll(executor); // 执行所有的MyBatis拦截器,并返回 Executor
return executor;
}
至此,我们找到了MyBatis的一个扩展点——拦截器interceptor。
MyBatis Inteceptor是使用JDK的动态代理来实现的,所以它只能对接口进行拦截。
里面两个很重要的注解是:@Intercepts、@Signature
@Intercepts : 标记要拦截的方法签名
@Signature : 方法签名,唯一的标记一个接口的方法
通过查看源码,我们还可以知道,MyBatis所有的代理拦截都是通过 InterceptorChain.pluginAll(Object target) 来实现的。
至此,我们得到下图:
通过上图可知,Mybatis支持对 Executor 、 StatementHandler 、 ResultSetHandler 和 PameterHandler 进行拦截,也就是说会对这4种对象进行代理。
Executor : 作用是执行SQL语句(所有的sql),并且对事务、缓存等提供统一接口。(在这一层上做拦截的权限会更大)
StatementHandler : 作用是对 statement 进行预处理,并且提供统一的原子的增、删、改、查接口。(如果要在SQL执行前进行拦截的话,拦截这里就可以了)
ResultSetHandler : 作用是对返回结果ResultSet进行处理。
PameterHandler : 作用是对参数进行赋值。
2、源码解读具体实现(以Executor接口为例)
2.1、创建SqlSession时,SqlSessionFactroy会解析mybatis.xml配置文件中的plugins标签,并将Interceptor属性定义的Interceptor放到interceptorChain中;
// SqlSessionFactoryBuilder.java
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
// 解析mybatis.xml配置文件,并创建DefaultSqlSessionFactory
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.
}
}
}
// XMLConfigBuilder.java
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
// 解析mybatis.xml中的各个标签
private void parseConfiguration(XNode root) {
try {
propertiesElement(root.evalNode("properties")); //issue #117 read properties first
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
settingsElement(root.evalNode("settings"));
environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
// 解析plugins标签,并把Interceptor放到interceptorChain中
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
// Configuration,mybatis文件的抽象类
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
2.2、DefaultSqlSessionFactory.openSession()时使用JDK动态代理生成@Signature注解指定的被代理类(包含代理的方法以及方法参数)
// DefaultSqlSessionFactory.java
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);
// 使用Configuration创建Executor
final Executor executor = configuration.newExecutor(tx, execType, autoCommit);
return new DefaultSqlSession(configuration, executor);
} 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();
}
}
2.3、InterceptorChain生成的具体过程
// InterceptorChain.java
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
// Interceptor的具体实现类(即我们业务上要实现的功能)
@Override
public Object plugin(Object arg0) {
return Plugin.wrap(arg0, this);
}
// Plugin.java
public static Object wrap(Object target, Interceptor interceptor) {
// getSignatureMap获取Interceptor类上的@Intercepts(@Signature)内容
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// 生成目标类target(Executor.class)的代理类,实现我们需要的plugin功能
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
// 解析实现Interceptor接口的类上定义的@Intercepts(@Signature)内容,获取需要拦截的类和方法。
// 例如:@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
if (interceptsAnnotation == null) { // issue #251
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 {
// sig.type()即Executor.class
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;
}
3、demo关键步骤
3.1、实现自定义的Interceptor
// 自定义拦截器
@Intercepts({@Signature(type = Executor.class, method = "query",
args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class MyTestInterceptor implements Interceptor {
private static final String MSG = "octopus route table info is not exit!";
@Override
public Object intercept(Invocation arg0) throws Throwable {
Object obj = null;
try {
obj = arg0.proceed();
} catch (Throwable e) {
if (e.getCause() instanceof MySQLSyntaxErrorException) {
MySQLSyntaxErrorException ex = (MySQLSyntaxErrorException) e.getCause();
System.out.println("====" + ex.getErrorCode());
System.out.println("====" + ex.getSQLState());
System.out.println("====" + ex.getMessage());
System.out.println("====" + ex.getCause());
if (MSG.equals(ex.getMessage())) {
throw new RouteTableNoExistException();
}
}
}
return obj;
}
@Override
public Object plugin(Object arg0) {
return Plugin.wrap(arg0, this);
}
@Override
public void setProperties(Properties arg0) {
System.out.println("env value: " + arg0.getProperty("names"));
}
}
3.2、在mybatis.xml中配置plugins
<configuration>
<plugins>
<plugin interceptor="com.pinganfu.interceptor.MyTestInterceptor" />
</plugins>
<environments default="development">
<environment id="development">
<transactionManager type="MANAGED">
<property name="closeConnection" value="false" />
</transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${driver}" />
<property name="url" value="${jdbcUrl}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mappers/TBATMapper.xml" />
</mappers>
</configuration>
3.3、获取SqlSession
Properties pro = new Properties();
try {
pro.load(Resources.getResourceAsStream("jdbc.properties"));
// 加载mybatis.xml中的plugins
InputStream in = Resources.getResourceAsStream("mybatis.xml");
sqlSession = new SqlSessionFactoryBuilder().build(in, "development", pro).openSession();
} catch (IOException e) {
e.printStackTrace();
}
4、mybatis针对各种异常的处理
mybatis通过DefaultSqlSession执行时,会将发生的所有异常统一包装成PersistenceException再抛出,我们可以通过PersistenceException.getCause()获取具体的异常。
// DefaultSqlSession.java
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
return result;
} catch (Exception e) {
// 对执行发生的所有Exception进行wrap之后再抛出
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
// ExceptionFactory.java
public static RuntimeException wrapException(String message, Exception e) {
// 将Exception进行统一包装成PersistenceException
return new PersistenceException(ErrorContext.instance().message(message).cause(e).toString(), e);
}