1.mybatis拦截器介绍
拦截器可在 mybatis 进行sql底层处理的时候执行额外的逻辑,最常见的就是分页逻辑、对结果集进行处理过滤敏感信息等。
public ParameterHandler newParameterHandler(Mapped statement mappedStatement, Object parameterObject, BoundSql boundSql) { | |
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); | |
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); | |
return parameterHandler; | |
} | |
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, | |
ResultHandler resultHandler, BoundSql boundSql) { | |
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds); | |
resultSetHandler = (ResultSetHandler) interceptorChain. Plugin All(resultSetHandler); | |
return resultSetHandler; | |
} | |
public StatementHandler new Statement Handler(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; | |
} | |
public Executor newExecutor( transaction transaction) { | |
return newExecutor(transaction, defaultExecutorType); | |
} | |
public Executor newExecutor(Transaction transaction, executor Type 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; | |
}折叠 |
从上面的代码可以看到mybatis支持的拦截类型只有四种(按拦截顺序)
1.Executor 执行器接口
2.StatementHandler sql构建处理器
3.ParameterHandler 参数处理器
4.ResultSetHandler 结果集处理器
2.拦截器原理
public class InterceptorChain { | |
private final List<Interceptor> interceptors = new ArrayList<>(); | |
// 遍历定义的拦截器,对拦截的对象进行包装 | |
public Object pluginAll(Object target) { | |
for (Interceptor interceptor : interceptors) { | |
target = interceptor.plugin(target); | |
} | |
return target; | |
} | |
public void addInterceptor(Interceptor interceptor) { | |
intercept ors.add(interceptor); | |
} | |
public List<Interceptor> getInterceptors() { | |
return Collections.unmodifiableList(interceptors); | |
} | |
} | |
#Interceptor | |
public interface Interceptor { | |
Object intercept(Invocation invocation) throws Throwable; | |
default Object plugin(Object target) { | |
return Plugin.wrap(target, this); | |
} | |
default void setProperties(Properties properties) { | |
// NOP | |
} | |
} |
mybatis拦截器本质上使用了 jdk 动态代理,interceptorChain拦截器链中存储了用户定义的拦截器,会遍历进行对目标对象代理包装。
用户自定义拦截器类需要实现Interceptor接口,以及实现intercept方法, plugin 和setProperties方法可重写,plugin方法一般不会改动,该方法调用了Plugin的 静态方法 wrap实现了对目标对象的代理
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>> signature map ) { | |
this.target = target; | |
this.interceptor = interceptor; | |
this.signatureMap = signatureMap; | |
} | |
// 获取jdk代理对象 | |
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 >) { | |
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)) { | |
// Invocation存储了目标对象、拦截方法以及方法参数 | |
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注解值不能为空 | |
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); | |
// issue # | |
if (interceptsAnnotation == null) { | |
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); | |
} | |
Signature[] sigs = interceptsAnnotation.value(); | |
// key 拦截的类型 | |
Map<Class<?>, Set<Method>> signatureMap = new HashMap<>(); | |
for (Signature sig : sigs) { | |
Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new Hash Set<>()); | |
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()]); | |
} | |
}折叠 | |
RetentionPolicy.RUNTIME) | (|
ElementType.TYPE) | (|
public Intercepts { | |
/** | |
* Returns method signatures to intercept. | |
* | |
* @return method signatures | |
*/ Signature[] value(); | |
} | |
RetentionPolicy.RUNTIME) | (|
({}) | |
public Signature { | |
/** | |
* Returns the java type. | |
* | |
* @return the java type | |
*/ Class<?> type(); | |
/** | |
* Returns the method name. | |
* | |
* @return the method name | |
*/ string method(); | |
/** | |
* Returns java types for method argument. | |
* @return java types for method argument | |
*/ Class<?>[] args(); | |
}折叠 |
可以看到,当被拦截的方法被执行时主要调用自定义拦截器的intercept方法,把拦截对象、方法以及方法参数封装成Invocation对象传递过去。
在getSignatureMap方法中可以看到,自定义的拦截器类上需要添加Intercepts注解并且Signature需要有值,Signature注解中的type为需要拦截对象的接口(Executor.class/StatementHandler/ParameterHandler/ResultSetHandler),method为需要拦截的方法的方法名,args为拦截方法的方法参数类型。
3.参考例子
接下来举一个拦截器实现对结果集下划线转驼峰的例子来简要说明
/** | |
* @author dxu | |
* @date/7/14 | |
* map结果转驼峰 | |
*/ (value = { (type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})}) | |
public class MyInterceptor implements Interceptor { | |
"unchecked") | (|
public Object intercept(Invocation invocation) throws Throwable { | |
// 调用目标方法 | |
List<Object> result = (List<Object>) invocation.proceed(); | |
for (Object o : result) { | |
if (o instanceof Map) { | |
processMap((Map<String, Object>) o); | |
} else { | |
break; | |
} | |
} | |
return result; | |
} | |
public Object plugin(Object target) { | |
return Plugin.wrap(target, this); | |
} | |
public void setProperties(Properties properties) { | |
} | |
private void processMap(Map<String, Object> map) { | |
Set<String> keySet = new HashSet<>(map.keySet()); | |
for (String key : keySet) { | |
if ((key.charAt() >= 'A' && key.charAt(0) <= 'Z') || key.indexOf("_") > 0) { | |
Object value = map.get(key); | |
map.remove(key); | |
map.put( camel (key), value); | |
} | |
} | |
} | |
// 下划线转驼峰 | |
private String camel(String fieldName) { | |
StringBuffer stringBuffer = new stringBuffer (); | |
boolean flag = false; | |
for (int i =; i < fieldName.length(); i++) { | |
if (fieldName.charAt(i) == '_') { | |
if (stringBuffer.length() >) { | |
flag = true; | |
} | |
} else { | |
if (flag) { | |
stringBuffer.append(Character.toUpperCase(fieldName.charAt(i))); | |
flag = false; | |
} else { | |
stringBuffer.append(Character.toLowerCase(fieldName.charAt(i))); | |
} | |
} | |
} | |
return stringBuffer.toString(); | |
} | |
}折叠 |
这个例子拦截的是ResultSetHandler的handleResultSets方法,这个方法是用来对结果集处理的,看intercept方法首先调用了目标对象的方法接着强转为List<Object>类型,这里为什么可以强转呢,因为我们可以看到handleResultSets方法定义 <E> List<E> handleResultSets(Statement stmt) throws SQLException; 返回的是List类型,然后遍历列表,若元素是map类型的再进行处理把key值转化为驼峰形式重新put到map中。
最后不要忘了把自定义的拦截器添加到配置中,这边是使用xml配置的,添加完后接着运行测试代码,可以看到列user_id已经转换成驼峰形式了。
<plugins> | |
<plugin interceptor="org.apache.ibatis.study.interceptor.MyInterceptor"> | |
</plugin> | |
</plugins> | |
List<Map> selectAllUsers(); | |
<select id="selectAllUsers" resultType="map"> | |
select user_id, username, password, nickname | |
from user | |
</select> | |
public class Test { | |
public static void main(String[] args) throws IOException { | |
try ( InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml")) { | |
// 构建 session 工厂 DefaultSqlSessionFactory | |
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); | |
SqlSession sqlSession = sqlSessionFactory.openSession(); | |
UserMapper userMapper = sqlSession.getMapper(UserMapper.class); | |
System.out.println(userMapper.selectAllUsers()); | |
} | |
} | |
} |