在Spring中我们经常会使用到拦截器,在登录验证、日志记录、性能监控等场景中,通过使用拦截器允许我们在不改动业务代码的情况下,执行拦截器的方法来增强现有的逻辑。在mybatis中,同样也有这样的业务场景,有时候需要我们在不侵入原有业务代码的情况下拦截sql,执行特定的某些逻辑。那么这个过程应该怎么实现呢,同样,在mybatis中也为开发者预留了拦截器接口,通过实现自定义拦截器这一功能,可以实现我们自己的插件,允许用户在不改动mybatis的原有逻辑的条件下,实现自己的逻辑扩展。

那么,在实现拦截器之前,首先看一下拦截器的拦截目标对象是什么,以及拦截器的工作流程是怎样的?

拦截器核心对象

mybatis拦截器可以对下面4种对象进行拦截:

1、Executor:mybatis的内部执行器,作为调度核心负责调用StatementHandler操作数据库,并把结果集通过ResultSetHandler进行自动映射

2、StatementHandler: 封装了JDBC Statement操作,是sql语法的构建器,负责和数据库进行交互执行sql语句

3、ParameterHandler:作为处理sql参数设置的对象,主要实现读取参数和对PreparedStatement的参数进行赋值

4、ResultSetHandler:处理Statement执行完成后返回结果集的接口对象,mybatis通过它把ResultSet集合映射成实体对象

工作流程

在mybatis中提供了一个Interceptor接口,通过实现该接口就能够自定义拦截器,接口中定义了3个方法:

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
}
}
  • intercept:在拦截目标对象的方法时,实际执行的增强逻辑,我们一般在该方法中实现自定义逻辑

  • plugin:用于返回原生目标对象或它的代理对象,当返回的是代理对象的时候,会调用intercept方法

  • setProperties:可以用于读取配置文件中通过property标签配置的一些属性,设置一些属性变量

看一下plugin方法中的wrap方法源码:

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;
}

可以看到,在wrap方法中,通过使用jdk动态代理的方式,生成了目标对象的代理对象,在执行实际方法前,先执行代理对象中的逻辑,来实现的逻辑增强。以拦截Executorquery方法为例,在实际执行前会执行拦截器中的intercept方法:

在mybatis中,不同类型的拦截器按照下面的顺序执行:

Executor -> StatementHandler -> ParameterHandler -> ResultSetHandler

以执行query 方法为例对流程进行梳理,整体流程如下:

1、Executor执行query()方法,创建一个StatementHandler对象

2、StatementHandler 调用ParameterHandler对象的setParameters()方法

3、StatementHandler 调用 Statement对象的execute()方法

4、StatementHandler 调用ResultSetHandler对象的handleResultSets()方法,返回最终结果

拦截器能实现什么

在对mybatis拦截器有了初步的认识后,来看一下拦截器被普遍应用在哪些方面:

  • sql 语句执行监控

    可以拦截执行的sql方法,可以打印执行的sql语句、参数等信息,并且还能够记录执行的总耗时,可供后期的sql分析时使用

  • sql 分页查询

    mybatis中使用的RowBounds使用的内存分页,在分页前会查询所有符合条件的数据,在数据量大的情况下性能较差。通过拦截器,可以做到在查询前修改sql语句,提前加上需要的分页参数

  • 公共字段的赋值

    在数据库中通常会有createTimeupdateTime等公共字段,这类字段可以通过拦截统一对参数进行的赋值,从而省去手工通过set方法赋值的繁琐过程

  • 数据权限过滤

    在很多系统中,不同的用户可能拥有不同的数据访问权限,例如在多租户的系统中,要做到租户间的数据隔离,每个租户只能访问到自己的数据,通过拦截器改写sql语句及参数,能够实现对数据的自动过滤

除此之外,拦截器通过对上述的4个阶段的介入,结合我们的实际业务场景,还能够实现很多其他功能。

插件定义与注册

在我们自定义的拦截器类实现了Interceptor接口后,还需要在类上添加@Intercepts 注解,标识该类是一个拦截器类。注解中的内容是一个@Signature对象的数组,指明自定义拦截器要拦截哪一个类型的哪一个具体方法。其中type指明拦截对象的类型,method是拦截的方法,argsmethod执行的参数。通过这里可以了解到 mybatis 拦截器的作用目标是在方法级别上进行拦截,例如要拦截Executorquery方法,就在类上添加:

@Intercepts({
@Signature(type = Executor.class,method = "query", args = { MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class })
})

如果要拦截多个方法,可以继续以数组的形式往后追加。这里通过添加参数可以确定唯一的拦截方法,例如在Executor中存在两个query方法,通过上面的参数可以确定要拦截的是下面的第2个方法:

<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql);
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler);

当编写完成我们自己的插件后,需要向mybatis中注册插件,有两种方式可以使用,第一种直接在SqlSessionFactory中配置:

@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setPlugins(new Interceptor[]{new ExecutorPlugin()});
return sqlSessionFactoryBean.getObject();
}

第2种是在mybatis-config.xml中对自定义插件进行注册:

<configuration>
<plugins>
<plugin interceptor="com.cn.plugin.interceptor.MyPlugin">
<property name="text" value="hello"/>
</plugin>
<plugin interceptor="com.cn.plugin.interceptor.MyPlugin2"></plugin>
<plugin interceptor="com.cn.plugin.interceptor.MyPlugin3"></plugin>
</plugins>
</configuration>

在前面我们了解了不同类型拦截器执行的固定顺序,那么对于同样类型的多个自定义拦截器,它们的执行顺序是怎样的呢?分别在plugin方法和intercept中添加输出语句,运行结果如下:

从结果可以看到,拦截顺序是按照注册顺序执行的,但代理逻辑的执行顺序正好相反,最后注册的会被最先执行。这是因为在mybatis中有一个类InterceptorChain,在它的pluginAll()方法中,会对原生对象target进行代理,如果有多个拦截器的话,会对代理类再次进行代理,最终实现一层层的增强target对象,因此靠后被注册的拦截器的增强逻辑会被优先执行。从下面的图中可以直观的看出代理的嵌套关系:

xml中注册完成后,在application.yml中启用配置文件,这样插件就可以正常运行了:

mybatis:
config-location: classpath:mybatis-config.xml

在了解了插件的基础概念与运行流程之后,通过代码看一下应用不同的拦截器能够实现什么功能。

拦截器使用示例

Executor

通过拦截Executorqueryupdate方法实现对sql的监控,在拦截方法中,打印sql语句、执行参数、实际执行时间:

@Intercepts({
@Signature(type = Executor.class,method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class,method = "query", args = { MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class })})
public class ExecutorPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("Executor Plugin 拦截 :"+invocation.getMethod());
Object[] queryArgs = invocation.getArgs();
MappedStatement mappedStatement = (MappedStatement) queryArgs[0];
//获取 ParamMap
MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap) queryArgs[1];
// 获取SQL
BoundSql boundSql = mappedStatement.getBoundSql(paramMap);
String sql = boundSql.getSql();
log.info("==> ORIGIN SQL: "+sql);
long startTime = System.currentTimeMillis();
Configuration configuration = mappedStatement.getConfiguration();
String sqlId = mappedStatement.getId(); Object proceed = invocation.proceed();
long endTime=System.currentTimeMillis();
long time = endTime - startTime;
printSqlLog(configuration,boundSql,sqlId,time);
return proceed;
} public static void printSqlLog(Configuration configuration, BoundSql boundSql, String sqlId, long time){
Object parameterObject = boundSql.getParameterObject();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
String sql= boundSql.getSql().replaceAll("[\\s]+", " ");
StringBuffer sb=new StringBuffer("==> PARAM:");
if (parameterMappings.size()>0 && parameterObject!=null){
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
sql = sql.replaceFirst("\\?", parameterObject.toString());
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
for (ParameterMapping parameterMapping : parameterMappings) {
String propertyName = parameterMapping.getProperty();
if (metaObject.hasGetter(propertyName)) {
Object obj = metaObject.getValue(propertyName);
String parameterValue = obj.toString();
sql = sql.replaceFirst("\\?", parameterValue);
sb.append(parameterValue).append("(").append(obj.getClass().getSimpleName()).append("),");
} else if (boundSql.hasAdditionalParameter(propertyName)) {
Object obj = boundSql.getAdditionalParameter(propertyName);
String parameterValue = obj.toString();
sql = sql.replaceFirst("\\?", parameterValue);
sb.append(parameterValue).append("(").append(obj.getClass().getSimpleName()).append("),");
}
}
}
sb.deleteCharAt(sb.length()-1);
}
log.info("==> SQL:"+sql);
log.info(sb.toString());
log.info("==> SQL TIME:"+time+" ms");
}
}

执行代码,日志输出如下:

在上面的代码中,通过Executor拦截器获取到了BoundSql对象,进一步获取到sql的执行参数,从而实现了对sql执行的监控与统计。

StatementHandler

下面的例子中,通过改变StatementHandler对象的属性,动态修改sql语句的分页:

@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class StatementPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
metaObject.setValue("delegate.rowBounds.offset", 0);
metaObject.setValue("delegate.rowBounds.limit", 2);
return invocation.proceed();
}
}

MetaObject是mybatis提供的一个用于方便、优雅访问对象属性的对象,通过将实例对象作为参数传递给它,就可以通过属性名称获取对应的属性值。虽然说我们也可以通过反射拿到属性的值,但是反射过程中需要对各种异常做出处理,会使代码中堆满难看的try/catch,通过MetaObject可以在很大程度上简化我们的代码,并且它支持对BeanCollectionMap三种类型对象的操作。

对比执行前后:

可以看到这里通过改变了分页对象RowBounds的属性,动态的修改了分页参数。

ResultSetHandler

ResultSetHandler 会负责映射sql语句查询得到的结果集,如果在生产环境中存在一些保密数据,不想在外部系统中展示,那么可能就需要在查询到结果后做一下数据的脱敏处理,这时候就可以使用ResultSetHandler对结果集进行改写。

@Intercepts({
@Signature(type= ResultSetHandler.class,method = "handleResultSets",args = {Statement.class})})
public class ResultSetPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("Result Plugin 拦截 :"+invocation.getMethod());
Object result = invocation.proceed();
if (result instanceof Collection) {
Collection<Object> objList= (Collection) result;
List<Object> resultList=new ArrayList<>();
for (Object obj : objList) {
resultList.add(desensitize(obj));
}
return resultList;
}else {
return desensitize(result);
}
}
//脱敏方法,将加密字段变为星号
private Object desensitize(Object object) throws InvocationTargetException, IllegalAccessException {
Field[] fields = object.getClass().getDeclaredFields();
for (Field field : fields) {
Confidential confidential = field.getAnnotation(Confidential.class);
if (confidential==null){
continue;
}
PropertyDescriptor ps = BeanUtils.getPropertyDescriptor(object.getClass(), field.getName());
if (ps.getReadMethod() == null || ps.getWriteMethod() == null) {
continue;
}
Object value = ps.getReadMethod().invoke(object);
if (value != null) {
ps.getWriteMethod().invoke(object, "***");
}
}
return object;
}
}

运行上面的代码,查看执行结果:

{"id":1358041517788299266,"orderNumber":"***","money":122.0,"status":3,"tenantId":2}

在上面的例子中,在执行完sql语句得到结果对象后,通过反射扫描结果对象中的属性,如果实体的属性上带有自定义的@Confidential注解,那么在脱敏方法中将它转化为星号再返回结果,从而实现了数据的脱敏处理。

ParameterHandler

mybatis可以拦截ParameterHandler注入参数,下面的例子中我们将结合前面介绍的其他种类的对象,通过组合拦截器的方式,实现一个简单的多租户拦截器插件,实现多租户下的查询逻辑。

@Intercepts({
@Signature(type = Executor.class,method = "query", args = { MappedStatement.class, Object.class,RowBounds.class, ResultHandler.class }),
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
@Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
})
public class TenantPlugin implements Interceptor {
private static final String TENANT_ID = "tenantId"; @Override
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget();
String methodName = invocation.getMethod().getName();
if (target instanceof Executor && methodName.equals("query") && invocation.getArgs().length==4) {
return doQuery(invocation);
}
if (target instanceof StatementHandler){
return changeBoundSql(invocation);
}
if (target instanceof ParameterHandler){
return doSetParameter(invocation);
}
return null;
} private Object doQuery(Invocation invocation) throws Exception{
Executor executor = (Executor) invocation.getTarget();
MappedStatement ms= (MappedStatement) invocation.getArgs()[0];
Object paramObj = invocation.getArgs()[1];
RowBounds rowBounds = (RowBounds) invocation.getArgs()[2]; if (paramObj instanceof Map){
MapperMethod.ParamMap paramMap= (MapperMethod.ParamMap) paramObj;
if (!paramMap.containsKey(TENANT_ID)){
Long tenantId=1L;
paramMap.put("param"+(paramMap.size()/2+1),tenantId);
paramMap.put(TENANT_ID,tenantId);
paramObj=paramMap;
}
}
//直接执行query,不用proceed()方法
return executor.query(ms, paramObj,rowBounds,null);
} private Object changeBoundSql(Invocation invocation) throws Exception {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
PreparedStatementHandler preparedStatementHandler = (PreparedStatementHandler) metaObject.getValue("delegate");
String originalSql = (String) metaObject.getValue("delegate.boundSql.sql");
metaObject.setValue("delegate.boundSql.sql",originalSql+ " and tenant_id=?");
return invocation.proceed();
} private Object doSetParameter(Invocation invocation) throws Exception {
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0];
MetaObject metaObject = SystemMetaObject.forObject(parameterHandler);
BoundSql boundSql= (BoundSql) metaObject.getValue("boundSql"); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
boolean hasTenantId=false;
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getProperty().equals(TENANT_ID)) {
hasTenantId=true;
}
}
//添加参数
if (!hasTenantId){
Configuration conf= (Configuration) metaObject.getValue("configuration");
ParameterMapping parameterMapping= new ParameterMapping.Builder(conf,TENANT_ID,Long.class).build();
parameterMappings.add(parameterMapping);
}
parameterHandler.setParameters(ps);
return null;
}
}

在上面的过程中,拦截了sql执行的三个阶段,来实现多租户的逻辑,逻辑分工如下:

  • 拦截Executorquery方法,在查询的参数Map中添加租户的属性值,这里只是简单的对Map的情况作了判断,没有对Bean的情况进行设置
  • 拦截StatementHandlerprepare方法,改写sql语句对象BoundSql,在sql语句中拼接租户字段的查询条件
  • 拦截ParameterHandlersetParameters方法,动态设置参数,将租户id添加到要设置到参数列表中

最终通过拦截不同执行阶段的组合,实现了基于租户的条件拦截。

总结

总的来说,mybatis拦截器通过对ExecutorStatementHandlerParameterHandlerResultSetHandler 这4种接口中的方法进行拦截,并生成代理对象,在执行方法前先执行代理对象的逻辑,来实现我们自定义的逻辑增强。从上面的例子中,可以看到通过灵活使用mybatis拦截器开发插件能够帮助我们解决很多问题,但是同样它也是一把双刃剑,在实际工作中也不要滥用插件、定义过多的拦截器,因为通过学习我们知道mybatis插件在执行中使用到了代理模式和责任链模式,在执行sql语句前会经过层层代理,如果代理次数过多将会消耗额外的性能,并增加响应时间。

如果文章对您有所帮助,欢迎关注公众号 码农参上

Mybatis自定义拦截器与插件开发的更多相关文章

  1. spring mvc <mvc:annotation-driven/> 自定义拦截器不走

    <mvc:annotation-driven/> 这个便签会注册2个自定义拦截器,所以导致请求过来就会自己去走注册的这2个拦截器和定义的一堆bean 但是这个便签是必须得定义的 直接贴代码 ...

  2. 12.Struts2自定义拦截器

    12.自定义拦截器        拦截器是Struts2的一个重要特性.因为Struts2的大多数核心功能都是通过拦截器实现的. 拦截器之所以称之为“拦截器”,是因为它可以拦截Action方法的执行, ...

  3. 【Java EE 学习 35 下】【struts2】【struts2文件上传】【struts2自定义拦截器】【struts2手动验证】

    一.struts2文件上传 1.上传文件的时候要求必须使得表单的enctype属性设置为multipart/form-data,把它的method属性设置为post 2.上传单个文件的时候需要在Act ...

  4. SpringMVC——自定义拦截器、异常处理以及父子容器配置

    自定义拦截器: 一.若想实现自定义拦截器,需要实现 org.springframework.web.servlet.HandlerInterceptor 接口. 二.HandlerIntercepto ...

  5. Struts2 自定义拦截器

    自定义拦截器(权限管理),包含了对ajax和表单请求的拦截 package com.interceptor; import java.io.IOException; import java.io.Pr ...

  6. Struts2自定义拦截器

    1. 需求 自定义拦截器实现,用户登录的访问控制. 2. 定义拦截器类 public class LoginInterceptor extends AbstractInterceptor { @Ove ...

  7. SpringMvc自定义拦截器

    SpringMvc也可以使用拦截器对请求进行拦截处理,用户可以自定义拦截器来实现特定的功能,自定义拦截器必须实现HandlerInterceptor接口 -preHandle():这个方法在业务处理器 ...

  8. struts2基础——自定义拦截器

    一.自定义拦截器 默认的拦截器能实现的功能是有限的,Struts2 支持自定义拦截器. 二.拦截器类 1.实现 Interceptor 接口 2.继承 AbstractInterceptor 抽象类, ...

  9. Apache CXF自定义拦截器

    为什么设计拦截器?1.为了在webservice请求过程中,能动态操作请求和响应数据,CXF设计了拦截器 拦截器分类: 1.按所处的位置分:服务器端拦截器,客户端拦截器. 2.按消息的方向分:入拦截器 ...

随机推荐

  1. Web 实时通信方案 All In One

    Web 实时通信方案 All In One HTTP 轮询, 单向通信,开销大 HTTP 长轮询, 单向通信,开销较小 WebSocket,双向通信,开销小 (TCP 高延迟,保证数据完整性) Ser ...

  2. 图解 HTTP, 图解 HTTPS, 图解 HTTP/2, 图解 HTTP/3, 图解 QUIC

    图解 HTTP, 图解 HTTPS, 图解 HTTP/2, 图解 HTTP/3, 图解 QUIC HTTP https://en.wikipedia.org/wiki/Hypertext_Transf ...

  3. Redis in Action

    Redis in Action Redis REmote DIctionary Server(Redis) Redis 是一种开放源代码(BSD许可)的内存中数据结构存储,用作数据库,缓存和消息代理. ...

  4. ES-Next classes static properties

    ES-Next classes static properties https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/ ...

  5. js webpack打包时保留指定注释

    optimization: { minimizer: [ new TerserJSPlugin({ terserOptions: { format: { comments: /(\s*#if)|(\s ...

  6. 【Python核心编程笔记】一、Python中一切皆对象

    Python中一切皆对象 本章节首先对比静态语言以及动态语言,然后介绍 python 中最底层也是面向对象最重要的几个概念-object.type和class之间的关系,以此来引出在python如何做 ...

  7. 高性能环形队列框架 Disruptor 核心概念

    高性能环形队列框架 Disruptor Disruptor 是英国外汇交易公司LMAX开发的一款高吞吐低延迟内存队列框架,其充分考虑了底层CPU等运行模式来进行数据结构设计 (mechanical s ...

  8. 聊聊CacheLine

    本文转载自聊聊CacheLine 导语 文章聊聊缓存一致性协议中我们提到过,缓存里面最小的单位是缓存行/缓存条目,但是缓存中的具体存储结构是什么样的,缓存行中有存放的是什么?在缓存中是如何寻找指定是还 ...

  9. 翻译:《实用的Python编程》03_02_More_functions

    目录 | 上一节 (3.1 脚本) | 下一节 (3.3 错误检查) 3.2 深入函数 尽管函数在早先时候介绍了,但有关函数在更深层次上是如何工作的细节却很少提供.本节旨在填补这些空白,并讨论函数调用 ...

  10. 腾讯云容器服务 TKE 拿下新加坡 MTCS 最高级别安全认证

    近日,腾讯云容器服务 TKE 荣获新加坡 MTCS 最高级安全认证,标志着腾讯云 TKE 在为用户提供可靠.易部署.灵活扩展等基础服务上,已经全面满足了新加坡监管机构以及多个行业客户对服务安全的要求. ...