热爱技术,追求卓越
不断求索,精益求精

使用mybatis拦截器Interceptor实现运行时sql打印及数据库CRUD后的缓存清理

定义数据库CRUD后的处理接口

有时候我们在数据库的CRUD操作后,需要做一些缓存的清理或缓存的重置;比如select操作后添加到缓存,update、insert、delete操作后需要清理缓存。我们定义一个接口AfterCrud用于实现CRUD后的操作。

import org.apache.ibatis.mapping.SqlCommandType;

/**
 * 数据库crud后的一些操作
 *
 */
public interface AfterCrud {

    /**插入操作*/
    String INSERT = "insert";
    /**更新操作*/
    String UPDATE = "update";
    /**删除操作*/
    String DELETE = "delete";

    /**
     * 数据库crud后的操作
     * @param sqlId 数据库sqlId
     * @param sqlCommandType 操作类型
     * @param args 参数,一般第二个参数为传给sql的参数,第一个是MappedStatement
     */
    void action(String sqlId, SqlCommandType sqlCommandType, Object[] args);
}

实现mybatis拦截器Interceptor

基于mybatis拦截器Interceptor可以做很多有趣的事情,著名的开源项目PageHelper就是基于mybatis拦截器Interceptor和ThreadLocal实现的。我们这里实现的拦截器主要有两个功能,一个是打印运行时的sql便于监控耗时信息,另一个是拦截数据库写操作后做一些缓存清理的工作。

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Properties;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * 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 SqlInterceptor implements Interceptor {

    private static final Logger log = LoggerFactory.getLogger("db");

    private boolean printSql;
    /**crud后的一些操作*/
    private AfterCrud afterCrud = null;

    public Object intercept(Invocation invocation) throws Throwable {

        Object returnValue = null;
        long time = 0;
        try {
            long start = System.currentTimeMillis();
            returnValue = invocation.proceed();
            long end = System.currentTimeMillis();
            time = (end - start);//计算调用耗时毫秒
        } catch (Throwable e) {
            throw e;
        } finally{
            MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
            String sqlId = mappedStatement.getId();
            if( printSql ) {//出错也打印sql
                Object parameter = null;
                if (invocation.getArgs().length > 1) {
                    parameter = invocation.getArgs()[1];
                }
                String runtimeSql = null;
                BoundSql boundSql = mappedStatement.getBoundSql(parameter);
                Configuration configuration = mappedStatement.getConfiguration();
                List<Object> params = getRuntimeParams(configuration, boundSql);//查询参数
                runtimeSql = getRuntimeSql(boundSql, params);
                print(sqlId, runtimeSql, time);//输入SQL
            }
            //crud后的一些操作
            if(afterCrud != null){
                afterCrud.action(sqlId, mappedStatement.getSqlCommandType(), invocation.getArgs());
            }
        }
        return returnValue;
    }

    /**
     * 打印SQL日志
     * @param configuration
     * @param boundSql
     * @param sqlId
     * @param time
     * @return
     */
    private void print(String sqlId, String sql, long time) {
        StringBuilder info = new StringBuilder(sql.length() + 128);
        DateFormat format = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
        info.append( format.format(new Date()) + " [" + sqlId + "] cost " + time + " ms\n");
        info.append( sql + "\n");
        log.info(info.toString());
    }

    /**
     * 取得运行时的SQL 替换对应的参数
     * @param boundSql 
     * @param params
     * @return
     */
    public static String getRuntimeSql(BoundSql boundSql, List<Object> params) {
        String sql = boundSql.getSql();
        for (Object object : params) {
            sql = sql.replaceFirst("\\?", getParameterValue(object));
        }
        return sql;
    }

    /**
     * 取得运行时的参数
     * @param configuration 
     * @param boundSql
     * @return
     */
    public static List<Object> getRuntimeParams(Configuration configuration, BoundSql boundSql) {
        List<Object> params = new ArrayList<Object>();
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings.size() > 0 && parameterObject != null) {
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                params.add(parameterObject);
            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    if (metaObject.hasGetter(propertyName)) {
                        Object obj = metaObject.getValue(propertyName);
                        params.add(obj);
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        params.add(obj);
                    }
                }
            }
        }
        return params;
    }

    /**
     * 取得参数值
     * @param obj
     * @return
     */
    private static String getParameterValue(Object obj) {
        String value = "";
        if (obj instanceof String) {
            value = "'" + obj+ "'";
        } else if (obj instanceof Date) {
            SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            value = "'" + formatter.format((Date)obj) + "'";
        } else if (obj != null) {
            value = obj.toString();
        }
        value = value.replace("$", "\\$");
        return value;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }


    @Override
    public void setProperties(Properties properties) {
    }

    /**允许输出SQL*/
    public void setPrintSql(String printSql) {
        this.printSql = "true".equals(printSql);
    }

    public SqlInterceptor(boolean printSql, AfterCrud afterCrud) {
        super();
        this.printSql = printSql;
        this.afterCrud = afterCrud;
    }


}

SqlInterceptor实现了Interceptor接口,构造方法可以根据传入的参数决定是否开启打印日志功能,是否支持CRUD后的一些额外的操作比如清理缓存等。

public SqlInterceptor(boolean printSql, AfterCrud afterCrud) {
    super();
    this.printSql = printSql;
    this.afterCrud = afterCrud;
}

实现AfterCrud接口,做缓存清理操作

AfterCrudImpl实现AfterCrud接口负责处理一些缓存清理的操作:

@Component("afterCrudImpl")
public class AfterCrudImpl implements AfterCrud {

    //private static final Logger log = LoggerFactory.getLogger("run");

    @Override
    public void action(String sqlId, SqlCommandType sqlCommandType,
            Object[] args) {
        String type = sqlCommandType.name().toLowerCase();
        // 有插入、修改、删除操作的时候触发
        if (type.equals(INSERT) || type.equals(UPDATE) || type.equals(DELETE)) {
            // 参数如果小于2个,则不处理
            if (args.length < 2) {
                return;
            }
            //获取缓存管理器处理各自的缓存处理逻辑
            Object obj = args[1];
            ICacheManager manager = CacheManagerFactory.getCacheManager(obj);
            manager.onCrud(obj);
        }
    }

}

上面的代码CacheManagerFactory的核心代码片段:

public class CacheManagerFactory {

    /** 数据模型对应的缓存处理器 */
    public final static Map<Class<?>, Class<?>> CACHE_MANAGER_MAP = Maps
            .newHashMap();

    static {
        // 产品基本信息缓存管理
        CACHE_MANAGER_MAP.put(
                cn.lovecto.model.ProductBase.class,
                cn.lovecto.cache.ProductBaseCache.class);
        // 产品常量缓存管理
        CACHE_MANAGER_MAP.put(
                cn.lovecto.model.ProductConstant.class,
                cn.lovecto.cache.ProductConstantCache.class);
        // 产品资源缓存管理
        CACHE_MANAGER_MAP.put(
                cn.lovecto.model.ProductResource.class,
                cn.lovecto.cache.ProductResourceCache.class);
    }

    /**
     * 缓存管理工具
     * 
     * @param object
     * @return
     */
    @SuppressWarnings({ "rawtypes" })
    public static ICacheManager getCacheManager(Object object) {
        Class<?> clazz = null;
        // 如果是集合类型,则需要获取元素的类型
        if (object instanceof Collection) {
            if (((Collection) object).size() > 0) {
                clazz = CACHE_MANAGER_MAP.get(((Collection) object).iterator()
                        .next().getClass());
            }
        } else {
            clazz = CACHE_MANAGER_MAP.get(object.getClass());
        }
        if (clazz == null) {
            return new DefaultCacheManager();
        }
        return (ICacheManager) SpringUtil.getBean(clazz);
    }

    /**
     * 默认的缓存处理器,不做任何处理
     * 
     *
     */
    public static class DefaultCacheManager implements ICacheManager {
        @Override
        public void onCrud(Object object) {
        }
    }
}

mybatis拦截器Interceptor其实是责任链模式,由于项目使用过程中使用了PageHelper,导致向SqlSessionFactory中加入拦截器时不生效,解决办法是在springboot的启动类上排除掉PageHelperAutoConfiguration:

@EnableAutoConfiguration(exclude={PageHelperAutoConfiguration.class})

同时自定义一个InterceptorConfig并把PageHelperAutoConfiguration中的代码拷贝出来改造如下:

@Configuration
@EnableConfigurationProperties(PageHelperProperties.class)
public class InterceptorConfig {

    @Autowired
    private List<SqlSessionFactory> sqlSessionFactoryList;

    @Autowired
    private PageHelperProperties properties;

    @Autowired
    private AfterCrud afterCrud;

    /**
     * 接受分页插件额外的属性
     *
     * @return
     */
    @Bean
    @ConfigurationProperties(prefix = PageHelperProperties.PAGEHELPER_PREFIX)
    public Properties pageHelperProperties() {
        return new Properties();
    }

    /**
     * 需要把自定义的拦截器放到page helper的前面
     */
    @PostConstruct
    public void addPageInterceptor() {
        PageInterceptor interceptor = new PageInterceptor();
        Properties properties = new Properties();
        // 先把一般方式配置的属性放进去
        properties.putAll(pageHelperProperties());
        // 在把特殊配置放进去,由于close-conn 利用上面方式时,属性名就是 close-conn 而不是
        // closeConn,所以需要额外的一步
        properties.putAll(this.properties.getProperties());
        interceptor.setProperties(properties);
        SqlInterceptor sqlInterceptor = new SqlInterceptor(true, afterCrud);
        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
            sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
            sqlSessionFactory.getConfiguration().addInterceptor(sqlInterceptor);
        }
    }

}

我们的拦截器要加到PageInterceptor的后面,否则不生效,具体原因请参考PageHelper的PageInterceptor的实现。

赞(6)
未经允许不得转载:LoveCTO » 使用mybatis拦截器Interceptor实现运行时sql打印及数据库CRUD后的缓存清理

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

热爱技术 追求卓越 精益求精