Mybatis 相关文章:
Mybatis 拦截器
拦截器的作用,我们就不做过多解释了;
这篇文章梳理完,后面还会梳理一篇 Mybatis的设计模式,还有一个 执行链
该篇文章主要讲述下面几个内容:
- 1、Mybatis拦截器的实现
- 2、Mybatis拦截器的应用场景(主要将应用场景)
一、Mybatis拦截器的实现
1.1 配置拦截器节点
首先,拦截器是添加到 Mybatis配置文件的 节点中:
xml
<configuration>
<!--拦截器-->
<plugins>
<!--CustomDefineInterceptor 自定义拦截器类-->
<plugin interceptor="com.yogurt.example.interceptor.CustomDefineInterceptor">
<!--设置拦截器的属性-->
<property name="preInfo" value="本次查询记录的数目"/>
</plugin>
</plugins>
</configuration>
1.2 解析plugin节点信息
其实这个在源码执行流程文章中已经讲解过这一步,解析plugin节点只是解析整个mybatis的configuration配置文件的一个小小步骤。
有不懂源码执行流程的,可以看一下本人的:『Mybatis 源码执行流程』这篇文章。
我们再来大致跟一下:这里我们就直接从解析过程开始了,不在说注册bean信息这些了。
直接来到 解析plugin吧,简洁一点,这篇文章主要讲Mybatis拦截器的应用。
来到源码这个方法里面:org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration
java
public class XMLConfigBuilder extends BaseBuilder {
/**
* 这里是解析配置文件的起始方法,解析的所有信息都放入到了configuration中
* @param root
*/
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
// 这里解析 plugins 节点
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings); //所有的 settings 节点中的属性都放在这这里解析了
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
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> 节点
* @param parent <plugins> 节点
* @throws Exception
*/
private void pluginElement(XNode parent) throws Exception {
// <plugins> 节点 存在
if (parent != null) {
// 依次取出<plugins>节点下的 每一个<plugin>节点
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);
}
}
}
}
可以看到,interceptor 就是我们上面在配置文件中,配置的拦截器节点。
1.3 注册拦截器到拦截器链上
configuration.addInterceptor(interceptorInstance);
会将该拦截器注册到 configuration中的 拦截器链 interceptorChain
中;
java
public class Configuration {
// 拦截器链(用来支持插件的插入)
protected final InterceptorChain interceptorChain = new InterceptorChain();
// 注册
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
}
interceptorChain
类型就是 InterceptorChain;该类中 有一个List: interceptors + 利用for循环,实现责任链模式;
其实不是说实现责任链模式,而是利用责任链模式解决了 目标方法在被多个拦截器拦截的时候,能够顺序的利用责任链上的拦截器拦截目标方法,达到我们使用拦截器的效果。
java
public class InterceptorChain {
// 这里就是为了形成 拦截器链
private final List<Interceptor> interceptors = new ArrayList<>();
// target是支持拦截的几个类的实例。
// 该方法依次向所有拦截器插入这几个类的实例,如果某个插件真的需要发挥作用,则返回一个代理对象即可;
// 如果不需要发挥作用,则返回原对象即可。
/**
* 向所有的拦截器链提供目标对象,由拦截器链给出替换目标对象的对象
* @param target 目标对象,是MyBatis中支持拦截的几个类(ParameterHandler、ResultSetHandler、StatementHandler、Executor)的实例
* @return 用来替换目标对象的对象
*/
public Object pluginAll(Object target) {
// 依次交给每个拦截器完成目标对象的替换工作。
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
二、应用场景
2.1 应用场景一
拦截器目的:在获取到查询结果之后,打印结果的条数。
详细的demo不再写了,只要先正常将Mybatis配置好,可以利用Mapper接口方法访问数据库就行;之后配置该拦截器,注意在配置文件中加上 节点,添加上这个拦截器引用就行。
主要看下面 注释对拦截器方法的介绍和注解参数介绍。
java
/**
* 用户可以自定的拦截器;
* 这个拦截器的目的是为了,在获取到查询结果之后,打印结果的条数;
* 这里设置了拦截器,还需要在配置文件中添加plugin节点才可以生效。
* <!--拦截器-->
* <plugins>
* <plugin interceptor="com.yogurt.example.interceptor.CustomDefineInterceptor">
* <property name="preInfo" value="本次查询记录的数目"/>
* </plugin>
* </plugins>
*
* 拦截器写在这个位置其实是有问题的,应该写在项目中,如:mybatis-learning-demo中,而不是写在源码中!!
* 如果有需要就可以写在自己的项目中即可!
*
* 1、注解介绍:
* 拦截器类上有注解 Intercepts,Intercepts的参数是 Signature注解数组。每个Signature注解都声明了当前拦截器类要拦截的方法。
*
* Signature注解中参数的含义如下:
* ① type:拦截器要拦截的类型。CustomDefineInterceptor拦截器要拦截的类型是ResultSetHandler类型。
* ② method:拦截器要拦截的 type类型中的方法。
* CustomDefineInterceptor 拦截器要拦截的是ResultSetHandler类型中的 handleResultSets方法。
* ③ args:拦截器要拦截的type类型中method方法的参数类型列表。
* 在CustomDefineInterceptor拦截器中,ResultSetHandler类型中的 handleResultSets方法只有一个 Statement类型的参数。
*
* 当要拦截多个方法时,只需在 Intercepts数组中放入多个 Signature注解即可。
*
* 2、从 Interceptor继承过来的方法:
* ① intercept:
* 拦截器类必须实现该方法。
* 拦截器拦截到目标方法时,会将操作转接到该 intercept 方法上,其中的参数 Invocation 为拦截到的目标方法。
* ② plugin:
* 拦截器类可以选择实现该方法。该方法中可以输出一个对象来替换输入参数传入的目标对象。
* ③ setProperties:
* 拦截器类可以选择实现该方法。该方法用来为拦截器设置属性。
* 在CustomDefineInterceptor拦截器中,我们使用该方法为拦截器设置 info属性的值。
*/
@Intercepts(
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
)
public class CustomDefineInterceptor implements Interceptor {
private String info;
/**
* 拦截器这个方法是必须要实现的。
* @param invocation 该参数 Invocation 为拦截到的目标方法
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 执行原有方法
Object result = invocation.proceed();
// 打印原方法输出结果的数目
System.out.println(info + " : " + ((List)result).size());
// 返回原有结果
return result;
}
/**
* 这个就暂时不用了。
* 拦截器类可以选择实现该方法。该方法中可以输出一个对象来替换输入参数传入的目标对象
* @param target
* @return
*/
@Override
public Object plugin(Object target) {
return Interceptor.super.plugin(target);
}
/**
* 为拦截器设置属性
* @param properties
*/
@Override
public void setProperties(Properties properties) {
// Interceptor.super.setProperties(properties);
// 为拦截器设置属性
info = properties.get("preInfo").toString();
}
}
demo测试结果,可以看到 在执行玩方法执行,有打印条数信息:
2.2 应用场景二
该场景引用文章:guochenglai.com/article/14
拦截器目的:一个结果构建成一个HashMap返回
有时候,我们需要将一个结果构建成一个HashMap返回,便于我们后面处理,但是Mybatis对接结果集的返回,没有Map类型的;所以我们可以利用拦截器,对结果集进行特殊处理。
其实利用拦截器,只是花里胡哨点,因为返回的结果集有这种类型 : List<String,HashMap<String,Stirng>>
,拿着这种类型去处理结果也行。
这里目的就是为了学习一下 Mybatis拦截器。
一、拦截器
java
package com.yogurt.example.interceptor.map;
import org.apache.ibatis.executor.resultset.DefaultResultSetHandler;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.*;
@Intercepts(@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}))
public class MapInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget();
// 这里一定要注意 target 类型是 DefaultResultSetHandler;
// 要想 target 类型是 DefaultResultSetHandler!!!
// 必须将该 MapInterceptor 放在拦截器链的第一个,因为拦截器链是一个List;
// 然后顺序执行的,如果不在第一个,先被别的拦截器拦截,拦截器会返回一个被该拦截器处理过的代理对象Proxy,进而继续执行下面的拦截器;
// 这时候,如果来到该方法,那么 invocation.getTarget()就是一个代理对象类型,而不是 DefaultResultSetHandler类型,
// 没有办法将代理对象转为目标对象,就会报错,所以,这里要注意一下。
//
// 或者这里,我们可以获取代理对象,然后将代理对象转成一个目标对象也行;
// 怎么转,我还不知道怎么转;
// 还真不能转,为啥?
// 因为:JDK动态代理,只能代理接口,这里我们要将target转成 DefaultResultSetHandler类型,并获取该类的mappedStatement参数
// 而,DefaultResultSetHandler类型是 ResultSetHandler接口的实现类,所以不能转。
//
// 目前是,拦截器链里面只有这一 MapInterceptor拦截器,可以实现 将结果集处理成 HashMap类型,
// 主要是靠下面的方法: com.yogurt.example.interceptor.map.MapInterceptor.result2Map()
DefaultResultSetHandler defaultResultSetHandler = (DefaultResultSetHandler) target;
// 这里通过反射,根据类名称来获取内部类,如果出现变更,则需要修改,不过Mybatis的插件目前都是这样一种情况
MappedStatement mappedStatement =
(MappedStatement) ReflectUtil.getFieldValue(defaultResultSetHandler, "mappedStatement");
// mappedStatement.getId() 是mapper接口里面的方法的 全称限定名: com.yogurt.example.dao.TestMapperDao.interceptorBuildMap
// 那么:
// className: com.yogurt.example.dao.TestMapperDao
// methodName : interceptorBuildMap
String className = substringBeforeLast(mappedStatement.getId(), ".");
String methodName = substringAfterLast(mappedStatement.getId(), ".");
Method[] methods = Class.forName(className).getDeclaredMethods();
if (methods == null) {
return invocation.proceed();
}
// 找到需要执行的method 注意这里是根据方法名称来查找,如果出现方法重载,需要认真考虑
for (Method method : methods) {
if (methodName.equalsIgnoreCase(method.getName())) {
//如果添加了注解标识,就将结果转换成map
MapResult map = method.getAnnotation(MapResult.class);
if (map == null) {
return invocation.proceed();
}
//进行map的转换
Statement statement = (Statement) invocation.getArgs()[0];
return result2Map(statement);
}
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Interceptor.super.plugin(target);
}
@Override
public void setProperties(Properties properties) {
}
/**
* 将SQL返回的结果集,构建成一个HashMap返回
* @param statement
* @return
* @throws Throwable
*/
private Object result2Map(Statement statement) throws Throwable{
ResultSet resultSet = statement.getResultSet();
if (resultSet == null) {
return null;
}
List<Object> resultList = new ArrayList<Object>();
Map<Object, Object> map = new HashMap<Object, Object>();
while (resultSet.next()) {
map.put(resultSet.getObject(1), resultSet.getObject(2));
}
resultList.add(map);
return resultList;
}
private static String substringBeforeLast(String str, String separator){
final int pos = str.lastIndexOf(separator);
return str.substring(0, pos);
}
public static String substringAfterLast(final String str, final String separator) {
final int pos = str.lastIndexOf(separator);
return str.substring(pos + separator.length());
}
}
二、反射工具类,为了获取属性 mappedStatement
属性
java
package com.yogurt.example.interceptor.map;
//import org.slf4j.Logger;
//import org.slf4j.LoggerFactory;
import java.lang.reflect.Field;
public class ReflectUtil {
//private static final Logger LOGGER = LoggerFactory.getLogger(ReflectUtil.class);
/**
* 根据反射或者一个对象的方法
* @param obj
* @param fieldName
* @param <T>
* @return
*/
public static <T> T getFieldValue(Object obj, String fieldName) {
Object result = null;
Field field = ReflectUtil.getField(obj, fieldName);
if (field != null) {
field.setAccessible(true);
try {
result = field.get(obj);
} catch (IllegalArgumentException e) {
//LOGGER.info("ReflectUtil getFieldValue cause IllegalArgumentException",e);
System.out.println("ReflectUtil getFieldValue cause IllegalArgumentException : "+e);
} catch (IllegalAccessException e2) {
//LOGGER.info("ReflectUtil getFieldValue cause IllegalAccessException",e2);
System.out.println("ReflectUtil getFieldValue cause IllegalAccessException : "+e2);
}
}
return (T)result;
}
private static Field getField(Object obj, String fieldName) {
Field field = null;
for (Class<?> clazz=obj.getClass(); clazz != Object.class; clazz=clazz.getSuperclass()) {
try {
field = clazz.getDeclaredField(fieldName);
break;
} catch (NoSuchFieldException e) {
//LOGGER.info("ReflectUtil getField cause NoSuchFieldException",e);
System.out.println("ReflectUtil getField cause NoSuchFieldException : "+e);
}
}
return field;
}
}
三、处理HashMap结果集映射的 注解标志
java
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
@Inherited
public @interface MapResult {
String value();
}
public interface TestMapperDao {
// 注解打在 Mapper映射方法上,目的是为了让拦截器找到之后,对其特殊处理
@MapResult("interceptorBuildMap")
Map<Integer,Integer> interceptorBuildMap(Integer id);
}
四、加入到 plugin节点中
xml
<!--拦截器-->
<plugins>
<!--将结果集构建成map返回-->
<plugin interceptor="com.yogurt.example.interceptor.map.MapInterceptor">
</plugin>
</plugins>
五、执行结果:
OK,结束!!!
现在我们来说一下,在 map.MapInterceptor#intercept
方法,在转类型是时候出现的问题;
当 MapInterceptor
在 配置文件中配置 里面放在第一个,或者是只有一个 MapInterceptor
拦截器的话,
ini
Object target = invocation.getTarget();
DefaultResultSetHandler defaultResultSetHandler = (DefaultResultSetHandler) target;
这里可以正常转换,因为执行器链里面只有一个拦截器,不会有其他拦截器干扰。
我们可以看到,下面的 target类型就是 DefaultResultSetHandler类型的。
**但是,如果 `MapInterceptor`不在拦截器第一个位置的话,就会出现问题。**
这里加入我们场景一的拦截器,并将MapInterceptor拦截器放在第二位置:
xml
<!--拦截器-->
<plugins>
<plugin interceptor="com.yogurt.example.interceptor.CustomDefineInterceptor">
<property name="preInfo" value="本次查询记录的数目"/>
</plugin>
<!--将结果集构建成map返回-->
<plugin interceptor="com.yogurt.example.interceptor.map.MapInterceptor">
</plugin>
</plugins>
因为拦截器链是一个List;然后顺序执行的,如果不在第一个,先被别的拦截器拦截,拦截器会返回一个被该拦截器处理过的代理对象Proxy,进而继续执行下面的拦截器; 这时候,如果来到该方法,那么 invocation.getTarget() 就是一个代理对象类型,而不是 DefaultResultSetHandler类型,没有办法将代理对象转为目标对象,就会报错,所以,这里要注意一下。
可以看到 target是个代理类型, 因为:JDK动态代理,只能代理接口,这里我们要将target转成 DefaultResultSetHandler类型,并获取该类的mappedStatement参数;
而,DefaultResultSetHandler类型是 ResultSetHandler接口的实现类,所以不能转。
这里就会报错:
OK,彻底完结,撒花。