关于Mybatis拦截器的使用

上一篇记录了Mybatis拦截器的说明,对于其原理功能,有了初步的了解,本次记录一下Mybatis在日常中的应用场景

1 Mybatis拦截器的使用

上一篇给到了Mybatis官方对于拦截器的使用方法, 而在日常项目中使用拦截器,只需要分成两步.

  • 1 自定义拦截器
  • 2 注册拦截器

1 自定义拦截器

根据上一篇说明可知,自定义拦截器需要实现org.apache.ibatis.plugin.Interceptor接口, 并在接口上添加@Intercepts注解.

1 Interceptor接口

public interface Interceptor {
    /**
     *  拦截要执行的方法, 在这个方法中自定义的逻辑
     */
  Object intercept(Invocation invocation) throws Throwable;
    /**
     * 拦截器用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理
     * 	1 拦截  当返回的是代理的时候我们可以对其中的方法进行拦截来调用intercept方法 -- Plugin.wrap(target, this)
     * 	2 不拦截 当返回的是当前对象的时候 就不会调用intercept方法
     */
  Object plugin(Object target);
    /**
     *  用于指定属性,注册当前拦截器的时候可以设置一些属性
     */
  void setProperties(Properties properties);

}

2 @Intercepts注解

@Intercepts注解是通过一个@Signature注解(拦截点),来指定拦截那个对象里面的某个方法

Intercepts注解的参数列表是Signature注解数组

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
    // Signature注解数组
  Signature[] value();
}

3 @Signature注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
 /**
  * 定义拦截的类 Executor、ParameterHandler、StatementHandler、ResultSetHandler当中的一个
  */
  Class<?> type();
    
  /**
   * 在定义拦截类的基础之上,在定义拦截的方法
   */
  String method();
   /**
    * 在定义拦截方法的基础之上在定义拦截的方法对应的参数,
    * 因方法里面可能重载,不指定参数列表,不能确定是对应拦截的方法
    */
  Class<?>[] args();
}

2 注册拦截器

注册拦截器,就是一个普通的Bean对象注册并交由Spring管理.

/**
 * mybatis拦截器配置
 */
@Configuration
public class MybatisConfiguration {

    /**
     * 注册拦截器
     */
    @Bean
    public MybatisInterceptor mybatisInterceptor() {
        MybatisInterceptor mybatisInterceptor = new MybatisInterceptor();
        Properties properties = new Properties();
        // 可以调用properties.setProperty方法来给拦截器设置一些自定义参数
        mybatisInterceptor.setProperties(properties);
        return mybatisInterceptor;
    }
}

3 拦截器使用案列

1 日志打印

在项目基本增删改查功能完成的基础上, 需要添加日志打印或日志记录入库的需求,此时可使用Mybatis拦截器.

自定义拦截器

/**
 * 记录打印日志
 */
@Slf4j
@Intercepts({
        @Signature(
                type = Executor.class,
                method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
        ),
        @Signature(
                type = Executor.class,
                method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class,
                        CacheKey.class, BoundSql.class}
        ),
        @Signature(
                type = Executor.class,
                method = "update",
                args = {MappedStatement.class, Object.class}
        )
})
public class Loginterceptor implements Interceptor {

    // 是否开启记录日志
    private Boolean enable;

    public Loginterceptor(Boolean enable) {
        this.enable = enable;
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];

        Object param = invocation.getArgs()[1];
        BoundSql boundSql = mappedStatement.getBoundSql(param);
        long logStartTime = System.currentTimeMillis();
        if (enable == null || !enable) {
            return invocation.proceed();
        }
        // 执行sql语句
        Object result = invocation.proceed();
        long logEndTime = System.currentTimeMillis();

        // 组装sql语句
        String sql = formatSql(boundSql, mappedStatement.getConfiguration()).concat(";");

        // 构造日志对象
        LogStat sqlStat = LogStat.builder()
                .id(mappedStatement.getId())
                .sqlCostTime(String.valueOf(logEndTime - logStartTime).concat(" ms"))
                .sql(sql)
                .build();

        // 打印拦截sql日志, 也可以根据不同的要求保存到数据
        log.info("Mybatis拦截器执行了, 拦截的数据为: {}",JSON.toJSONString(sqlStat));
        return result;
    }

    /**
     * 拼接sql
     */
    private String formatSql(BoundSql boundSql, Configuration configuration) {
        String sql = boundSql.getSql();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        Object parameterObject = boundSql.getParameterObject();
        if (StringUtils.isBlank(sql)) {
            return "";
        }
        if (configuration == null) {
            return "";
        }
        TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
        sql = beautifySql(sql);
        if (parameterMappings != null) {
            for (ParameterMapping parameterMapping : parameterMappings) {
                if (parameterMapping.getMode() != ParameterMode.OUT) {
                    Object value;
                    String propertyName = parameterMapping.getProperty();
                    if (boundSql.hasAdditionalParameter(propertyName)) {
                        value = boundSql.getAdditionalParameter(propertyName);
                    } else if (parameterObject == null) {
                        value = null;
                    } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                        value = parameterObject;
                    } else {
                        MetaObject metaObject = configuration.newMetaObject(parameterObject);
                        value = metaObject.getValue(propertyName);
                    }
                    String paramValueStr = "";
                    if (value instanceof String) {
                        paramValueStr = "'" + value + "'";
                    } else if (value instanceof Date) {
                        paramValueStr = "'" + this.dateToStrTimeMill((Date) value) + "'";
                    } else {
                        paramValueStr = value + "";
                    }
                    sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(paramValueStr));
                }
            }
        }
        return sql;
    }

    private static String formatStr = "yyyy-MM-dd HH:mm:ss:SSS";

    /**
     * 时间转换
     */
    private String dateToStrTimeMill(Date currentDate) {
        if (currentDate == null || formatStr == null) {
            return null;
        } else {
            SimpleDateFormat sdf = new SimpleDateFormat(formatStr);
            return sdf.format(currentDate);
        }
    }

    /**
     * 格式化sql
     */
    private String beautifySql(String sql) {
        sql = sql.replaceAll("[\\s\n ]+", " ");
        return sql;
    }

    /**
     * 拦截对象属于Executor实例
     */
    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    /**
     * 可设置属性
     */
    @Override
    public void setProperties(Properties properties) {

    }

    /**
     * 定义日志记录内部类
     */
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    static class LogStat {

        // sql语句
        private String sql;
        // 执行耗时
        private String sqlCostTime;
        // 全限定名
        private String id;
    }
}

注册拦截器

@Configuration
public class SqlConfig {

    @Autowired
    private SqlSessionFactory sqlSessionFactory;

    // 是否开启日志记录 默认开启
    @Value("${sqlPrint:true}")
    private String enable;

    @PostConstruct
    public void init(){
        sqlSessionFactory.getConfiguration().addInterceptor(new Loginterceptor(Boolean.parseBoolean(enable)));
    }
}

2 数据隔离

在某些多租户数据隔离项目中, 一些数据需要根据不同的租户,展示对应的数据,在一些查询中,为了安全和方便,可以将这一部分功能放到Mybatis拦截中执行.

自定义拦截器

/**
 * 多租户查询拦截
 */
@Intercepts({
        @Signature(method = "prepare",
                type = StatementHandler.class,
                args = {Connection.class, Integer.class}
        ),
        @Signature(method = "query",
                type = Executor.class,
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class,
                        CacheKey.class, BoundSql.class}
        )
})
public class TenantInterceptor implements Interceptor {


    // 跳过拦截的mapper
    private final Set<String> ignore = new HashSet<>();

    public TenantInterceptor( String... ignore) {
        if (ignore != null && ignore.length > 0) {
            this.ignore.addAll(Arrays.asList(ignore));
        }
    }


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler handler = (StatementHandler) invocation.getTarget();

        // 由于mappedStatement中有我们需要的方法id,但却是protected的,所以要通过反射获取
        MetaObject statementHandler = SystemMetaObject.forObject(handler);
        MappedStatement mappedStatement = (MappedStatement) statementHandler
                .getValue("delegate.mappedStatement");

        // 不是查询类型,直接放过
        if (!SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) {
            return invocation.proceed();
        }
        String namespace = mappedStatement.getId();

        // 忽视拦截对象,直接放过
        if (ignore.contains(namespace)) {
            return invocation.proceed();
        }
        String className = namespace.substring(0, namespace.lastIndexOf("."));
        String methodName = namespace.substring(namespace.lastIndexOf(".") + 1);
        Class<?> clazz = Class.forName(className);
        Tenant tenant = null;
        Method[] methods = clazz.getMethods();
        for (Method method : methods) {
            if (methodName.equals(method.getName())) {
                tenant = method.getAnnotation(Tenant.class);
                break;
            }
        }
        // 方法没有租户注解,直接放过
        if (tenant == null) {
            return invocation.proceed();
        }
        BoundSql boundSql = handler.getBoundSql();
        // 获取sql
        String sql = boundSql.getSql();

        StringBuilder whereSql = new StringBuilder();
        CCJSqlParserManager parserManager = new CCJSqlParserManager();
        Select select = (Select) parserManager.parse(new StringReader(sql));
        PlainSelect plain = (PlainSelect) select.getSelectBody();
        // 获取当前查询条件
        Expression where = plain.getWhere();
        // 租户数据隔离
        // 从用户上下文域中获取用户id和租户id等信息
        if (tenant != null) {
            whereSql.append("( 1 = 0 OR ");
            whereSql.append(addAlias(plain, tenant.userField())).append(" = '")
                    .append(BaseContextHolder.getUserId()).append("'");
            whereSql.append(" AND ");
            whereSql.append(addAlias(plain, tenant.tenantField())).append(" = '")
                    .append(BaseContextHolder.getTenantId()).append("' )");
        }
        if (where == null) {
            if (tenant != null) {
                whereSql.append(")");
            }
            if (whereSql.length() > 0) {
                Expression whereExpression = CCJSqlParserUtil.parseCondExpression(whereSql.toString());
                plain.setWhere(whereExpression);
            }
        } else {
            if (whereSql.length() > 0) {
                whereSql.append(" and ( ").append(where.toString()).append(" )");
            } else {
                whereSql.append(where.toString());
            }
            if (tenant != null) {
                whereSql.append(")");
            }
            Expression expression = CCJSqlParserUtil.parseCondExpression(whereSql.toString());
            plain.setWhere(expression);
        }
        statementHandler.setValue("delegate.boundSql.sql", select.toString());
        return invocation.proceed();
    }

    /**
     * 拦截StatementHandler实例对象     
     * @param target
     * @return
     */
    @Override
    public Object plugin(Object target) {
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {

    }
    /**
     * 添加别名
     * @param plain
     * @param field
     * @return
     */
    private String addAlias(PlainSelect plain, String field) {
        if (plain.getFromItem().getAlias() != null) {
            return plain.getFromItem().getAlias() + "." + field;
        } else {
            return field;
        }
    }
}

额外使用到租户注解来进行开关控制. 租户注解用在需要添加租户隔离的查询方法上或类上.

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD, ElementType.TYPE})
public @interface Tenant {

    /**
     * 租户属性
     */
    String tenantField() default "tenant_id";

    /**
     * 用户信息
     */
    String userField() default "crt_user";
}

注册拦截器

@Configuration
public class TenantConfig {
    
    /**
     * 注册拦截器
     */
    @Bean
    public TenantInterceptor tenantInterceptor() {
        TenantInterceptor tenantInterceptor = new TenantInterceptor();
        return tenantInterceptor;
    }
}

3 结果翻译替换

@Intercepts({
        @Signature(method = "handleResultSets",
                type = ResultSetHandler.class,
                args = {Statement.class}
        )
})
public class TranslateInterceptor implements Interceptor {

	@Override
	public Object intercept(Invocation invocation) throws Throwable{
		Object result = invocation.proceed();
		if(ObjectUtils.isEmpty(result)){
			return result;
		}
		// 翻译数据
		
		return translate(reuslt);
	}
	
	// 模拟翻译接口
	public Object translate(Object object){
	 // xxx
	}

}

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MyBatis拦截器MyBatis框架提供的一种机制,用于在执行SQL语句前后进行拦截和处理。通过使用拦截器,我们可以在不修改原有代码的情况下,对SQL语句进行自定义的处理和增强。 使用MyBatis拦截器,需要实现`Interceptor`接口,并且重写`intercept()`方法和`plugin()`方法。`intercept()`方法用于自定义SQL语句的处理逻辑,`plugin()`方法用于生成代理对象。 下面是一个示例,展示如何使用MyBatis拦截器: 首先,创建一个类实现`Interceptor`接口: ```java public class MyInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 在执行SQL语句前的逻辑处理 System.out.println("Before executing SQL"); // 执行原始方法 Object result = invocation.proceed(); // 在执行SQL语句后的逻辑处理 System.out.println("After executing SQL"); return result; } @Override public Object plugin(Object target) { // 生成代理对象 return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { // 设置拦截器属性 } } ``` 然后,在配置文件中配置拦截器: ```xml <!-- 配置拦截器 --> <plugins> <plugin interceptor="com.example.MyInterceptor" /> </plugins> ``` 以上示例中,`MyInterceptor`类实现了`intercept()`方法,在其中可以编写自定义的SQL处理逻辑。`plugin()`方法生成了拦截器的代理对象。配置文件中的`<plugins>`标签用于配置拦截器

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值