1. mybatis自定义拦截器实现步骤:
实现
org.apache.ibatis.plugin.Interceptor
接口。添加拦截器注解
org.apache.ibatis.plugin.Intercepts
。配置文件中添加拦截器。
2. 在mybatis中可被拦截的类型有四种(按照拦截顺序):
Executor:拦截执行器的方法。
ParameterHandler:拦截参数的处理。
ResultHandler:拦截结果集的处理。
StatementHandler:拦截Sql语法构建的处理。
1. 不同拦截类型执行顺序:
com.galax.configuration.Aa#plugin打印拦截器对象顺序.png
2. 多个插件拦截的顺序?
image.png
需要注意的是,因为拦截器Aa和拦截器Bb均是拦截的StatementHandler对象,所以拦截器B在此获取StatementHandler的时候,获取的是代理对象。
拦截器对象的处理过程.png
3. 多个插件plugin()和intercept()方法的执行顺序
先执行每个插件的plugin方法,若是@Intercepts注解标明需要拦截该对象,那么生成类型对象的代理对象。(即使该插件需要拦截该类型对象,但是依旧会执行下一个插件的plugin方法)。知道执行完毕所有的plugin方法。在执行每个Intercept方法。
3. 拦截器注解的作用:
自定义拦截器必须使用mybatis提供的注解来声明我们要拦截的类型对象。
Mybatis插件都要有Intercepts [in特赛婆斯]
注解来指定要拦截哪个对象哪个方法。我们知道,Plugin.wrap方法会返回四大接口对象的代理对象,会拦截所有的方法。在代理对象执行对应方法的时候,会调用InvocationHandler处理器的invoke方法。
4. 拦截器注解的规则:
具体规则如下:
@Intercepts({ @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}), @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}), @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})})
@Intercepts:标识该类是一个拦截器;
@Signature:指明自定义拦截器需要拦截哪一个类型,哪一个方法;
2.1 type:对应四种类型中的一种;
2.2 method:对应接口中的哪类方法(因为可能存在重载方法);
2.3 args:对应哪一个方法;
5. 拦截器可拦截的方法:
拦截的类 | 拦截的方法 |
---|---|
Executor | update, query, flushStatements, commit, rollback,getTransaction, close, isClosed |
ParameterHandler | getParameterObject, setParameters |
StatementHandler | prepare, parameterize, batch, update, query |
ResultSetHandler | handleResultSets, handleOutputParameters |
2. 拦截器方法
2.1 官方插件开发方式
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})public class TestInterceptor implements Interceptor { public Object intercept(Invocation invocation) throws Throwable { Object target = invocation.getTarget(); //被代理对象 Method method = invocation.getMethod(); //代理方法 Object[] args = invocation.getArgs(); //方法参数 // do something ...... 方法拦截前执行代码块 Object result = invocation.proceed(); // do something .......方法拦截后执行代码块 return result; } public Object plugin(Object target) { return Plugin.wrap(target, this); }}
2.2 拦截器的方法
public interface Interceptor { Object intercept(Invocation invocation) throws Throwable; Object plugin(Object target); void setProperties(Properties properties);}
2.2.1 setProperties方法
如果我们的拦截器需要一些变量对象,而且这个对象是支持可配置的。
类似于Spring中的@Value("${}")从application.properties文件中获取。
使用方法:
<plugin interceptor="com.plugin.mybatis.MyInterceptor"> <property name="username" value="xxx"/> <property name="password" value="xxx"/></plugin>
方法中获取参数:properties.getProperty("username");
问题:但是为什么不直接使用@Value("${}") 获取变量?
解答:因为mybatis框架本身就是一个可以独立使用的框架,没有像Spring这种做了很多的依赖注入。
2.2.2 plugin方法
这个方法的作用是就是让mybatis判断,是否要进行拦截,然后做出决定是否生成一个代理。
@Override public Object plugin(Object target) { if (target instanceof StatementHandler) { return Plugin.wrap(target, this); } return target; }
需要注意的是:每经过一个拦截器对象都会调用插件的plugin方法,也就是说,该方法会调用4次。根据@Intercepts注解来决定是否进行拦截处理。
问题1:
Plugin.wrap(target, this)
方法的作用?
解答:判断是否拦截这个类型对象(根据@Intercepts注解决定),然后决定是返回一个代理对象还是返回原对象。
故我们在实现plugin方法时,要判断一下目标类型,是本插件要拦截的对象时才执行Plugin.wrap方法,否则的话,直接返回目标本身。
问题2:拦截器代理对象可能经过多层代理,如何获取到真实的拦截器对象?
/** * <p> * 获得真正的处理对象,可能多层代理. * </p> */ @SuppressWarnings("unchecked") public static <T> T realTarget(Object target) { if (Proxy.isProxyClass(target.getClass())) { MetaObject metaObject = SystemMetaObject.forObject(target); return realTarget(metaObject.getValue("h.target")); } return (T) target; }
2.2.3 intercept(Invocation invocation)方法
我们知道,mybatis只能拦截四种类型的对象。而intercept
方法便是处理拦截到的对象。比如我们要拦截StatementHandler#query(Statement st,ResultHandler rh)
方法,那么Invocation
就是这个对象,Invocation
中有三个参数。
target:StatementHandler;
method :query;
args[]:Statement st,ResultHandler rh
org.apache.ibatis.reflection.SystemMetaObject#forObject
:方便的获取对象中的值。
案例:将参数拼接到sql语句。
因为已经执行了ParameterHandler拦截器,故Statement对象已经是完全拼接好的SQL语句。
@Intercepts({ @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}), @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}), @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})})@Slf4jpublic class Aa implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { Statement statement; //获取方法参数 Object firstArg = invocation.getArgs()[0]; if (Proxy.isProxyClass(firstArg.getClass())) { statement = (Statement) SystemMetaObject.forObject(firstArg).getValue("h.statement"); } else { statement = (Statement) firstArg; } MetaObject stmtMetaObj = SystemMetaObject.forObject(statement); //获取Statement对象(sql语法已经构建完毕) statement = (Statement) stmtMetaObj.getValue("stmt.statement"); //获取sql语句 String originalSql = statement.toString(); //将原始sql中的空白字符(\s包括换行符,制表符,空格符)替换为" " originalSql = originalSql.replaceAll("[\\s]+", " "); //只获取sql的select/update/insert/delete开头的sql int index = indexOfSqlStart(originalSql); if (index > 0) { originalSql = originalSql.substring(index); } // 计算执行 SQL 耗时 long start = System.currentTimeMillis(); Object result = invocation.proceed(); long timing = System.currentTimeMillis()- start; //获取MapperStatement对象,获取到sql的详细信息 Object realTarget = realTarget(invocation.getTarget()); //获取metaObject对象 MetaObject metaObject = SystemMetaObject.forObject(realTarget); //获取MappedStatement对象 MappedStatement ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); StringBuilder formatSql = new StringBuilder() .append(" Time:").append(timing) //获取Mapper信息和方法信息 .append(" ms - ID:").append(ms.getId()) .append("Execute SQL:") .append(originalSql); //打印sql信息 log.info(formatSql.toString()); return result; } @Override public Object plugin(Object target) { if (target instanceof StatementHandler) { return Plugin.wrap(target, this); } return target; } @Override public void setProperties(Properties prop) { } /** * 获取sql语句开头部分 * * @param sql * @return */ private int indexOfSqlStart(String sql) { String upperCaseSql = sql.toUpperCase(); Set<Integer> set = new HashSet<>(); set.add(upperCaseSql.indexOf("SELECT ")); set.add(upperCaseSql.indexOf("UPDATE ")); set.add(upperCaseSql.indexOf("INSERT ")); set.add(upperCaseSql.indexOf("DELETE ")); set.remove(-1); if (CollectionUtils.isEmpty(set)) { return -1; } List<Integer> list = new ArrayList<>(set); list.sort(Comparator.naturalOrder()); return list.get(0); } /** * <p> * 获得真正的处理对象,可能多层代理. * </p> */ @SuppressWarnings("unchecked") public static <T> T realTarget(Object target) { if (Proxy.isProxyClass(target.getClass())) { MetaObject metaObject = SystemMetaObject.forObject(target); return realTarget(metaObject.getValue("h.target")); } return (T) target; }}
附录
1. 完整版拼接sql语句
@Intercepts({ @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}), @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}), @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})})@Slf4jpublic class PerformanceInterceptor implements Interceptor { // private static final Log logger = LogFactory.getLog(PerformanceInterceptor.class); private static final String DruidPooledPreparedStatement = "com.alibaba.druid.pool.DruidPooledPreparedStatement"; private static final String T4CPreparedStatement = "oracle.jdbc.driver.T4CPreparedStatement"; private static final String OraclePreparedStatementWrapper = "oracle.jdbc.driver.OraclePreparedStatementWrapper"; /** * SQL 执行最大时长,超过自动停止运行,有助于发现问题。 */ @Setter @Getter @Accessors(chain = true) private long maxTime = 0; /** * 是否写入日志文件<br> * true 写入日志文件,不阻断程序执行!<br> * 超过设定的最大执行时长异常提示! */ @Setter @Getter @Accessors(chain = true) private boolean writeInLog = false; private Method oracleGetOriginalSqlMethod; private Method druidGetSQLMethod; @Override public Object intercept(Invocation invocation) throws Throwable { Object result = invocation.proceed(); return result; } @Override public Object plugin(Object target) { if (target instanceof StatementHandler) { return Plugin.wrap(target, this); } return target; } @Override public void setProperties(Properties prop) { String maxTime = prop.getProperty("maxTime"); if (StringUtils.isNotEmpty(maxTime)) { this.maxTime = Long.parseLong(maxTime); } } /** * 获取此方法名的具体 Method * * @param clazz class 对象 * @param methodName 方法名 * @return 方法 */ public Method getMethodRegular(Class<?> clazz, String methodName) { if (Object.class.equals(clazz)) { return null; } for (Method method : clazz.getDeclaredMethods()) { if (method.getName().equals(methodName)) { return method; } } return getMethodRegular(clazz.getSuperclass(), methodName); } /** * 获取sql语句开头部分 * * @param sql * @return */ private int indexOfSqlStart(String sql) { String upperCaseSql = sql.toUpperCase(); Set<Integer> set = new HashSet<>(); set.add(upperCaseSql.indexOf("SELECT ")); set.add(upperCaseSql.indexOf("UPDATE ")); set.add(upperCaseSql.indexOf("INSERT ")); set.add(upperCaseSql.indexOf("DELETE ")); set.remove(-1); if (CollectionUtils.isEmpty(set)) { return -1; } List<Integer> list = new ArrayList<>(set); list.sort(Comparator.naturalOrder()); return list.get(0); }}
2. MappedStatement.class
一个MappedStatement对象对应Mapper配置文件中的一个select/update/insert/delete节点,主要描述的是一条sql语句。其属性为:
//节点中的id属性加要命名空间 private String id; //直接从节点属性中取 private Integer fetchSize; //直接从节点属性中取 private Integer timeout; private StatementType statementType; private ResultSetType resultSetType; //对应一条SQL语句 private SqlSource sqlSource; //每条语句都对就一个缓存,如果有的话。 private Cache cache; //这个已经过时了 private ParameterMap parameterMap; private List<ResultMap> resultMaps; private boolean flushCacheRequired; private boolean useCache; private boolean resultOrdered; //SQL的类型,select/update/insert/detete 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;