Mybatis之拦截器--获取执行SQL实现多客户端数据同步

0. 前言

最近的一个项目是将J2EE环境打包安装在客户端(使用nwjs+NSIS制做安装包)运行, 全部的业务操做在客户端完成, 数据存储在客户端数据库中. 服务器端数据库汇总各客户端的数据进行分析. 其中客户端ORM使用Mybatis. 经过Mybatis拦截器获取全部在执行的SQL语句, 按期同步至服务器.mysql

本文经过在客户端拦截SQL的操做介绍Mybatis拦截器的使用方法.git

1. 项目需求

客户分店较多且比较分散, 部分店内网络不稳定, 客户要求每一个分店在无网络的状况下也能正常使用系统, 同时全部店面数据须要进行汇总分析. 综合客户的需求, 项目架构以下:github

将WEB项目及其运行环境经过NSIS制做安装包在各分店进行安装, 每一个分店是一个独立的WEB服务, 这样就保证店内在无网络(有局域网,没法访问互联网)的状况下也能够正常使用系统. 此时每一个分店的数据库保存本身店内的运营数据, 各店之间的数据相互隔离.sql

但运营方没法分析全部店面的汇总数据(如商品总体销售状况等), 所以须要将每一个店面的数据按期同步至服务器的数据库中.数据库

  • 因为店内可能无网络(无网时不能受数据同步影响,系统需正常运行), 实时同步方案被排除.
  • 为保证数据库安全性, 服务器数据库不能对外暴露, 使用数据库的同步机制方案被排除.
  • 部分业务须要记录数据变化日志(数据从1到0又到1, 需记录过程), 增量同步方案被排除.

最终采用了将客户端全部更新(增,删,改)的SQL按照执行顺序保存至数据库中, 按期同步并在服务器的数据库按照顺序执行SQL, 以此来保证服务器数据库的数据是各客户端数据的汇总.apache

2. 解决方案

项目采用Mybatis, Mapper中定义SQL时可使用Mybatis的标签及参数标识符, Mybatis会解析标签替换参数生成最终的SQL在数据库中执行, 而咱们须要的是最终在数据库中执行的SQL.数组

Mybatis中SQL的写法:安全

<insert id="insert">
    INSERT INTO atd681_mybatis_test ( dv ) VALUES ( #{dv} )
</insert>
复制代码

须要同步至服务器执行的SQL:bash

INSERT INTO atd681_mybatis_test ( dv ) VALUES ( 'aaa' )
复制代码

3. 拦截器

3.1 什么是拦截器

想这样一个场景, 你作饭的时候可能须要如下步骤:服务器

买菜 >> 洗菜 >> 切菜 >> 作菜 >> 上菜 >> 洗碗

  • 开始洗菜前, 买菜操做已经完成, 能够知道买了什么菜.
  • 洗菜时还未开始作菜, 所以不知道菜是什么口味的.
  • 在上菜前(此时作菜已经完成), 能够知道菜的口味.
  • 在上菜时不知道有没有剩菜
  • 在洗碗时咱们能够知道有没有剩菜.

上面的作饭流程是按照步骤一步一步的进行, 咱们既能够在其中的某个步骤中获取前几步的成果, 也能够在某个步骤开始以前作些额外的事情, 好比: 切菜前对菜称重等.

Mybatis提供了这样一个组件: 他能够在某个步骤执行以前先执行自定义的操做. 这个组件叫作拦截器. 所谓拦截器, 顾名思义: 须要定义拦截哪一个操做步骤及拦截后作什么事情.

3.2 定义拦截器

拦截器须要实现org.apache.ibatis.plugin.Interceptor接口并指定拦截的方法.

// 拦截器
@Intercepts(@Signature(type = StatementHandler.class, 
                       method = "update", 
                       args = Statement.class)
            )
public class SQLInterceptor implements Interceptor {

    // 拦截方法后执行的逻辑
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 继续执行Mybatis原有的逻辑
        // proceed中经过反射执行被拦截的方法
        return invocation.proceed();
    }

    // 返回当前拦截的对象(StatementHandler)的动态代理
    // 当拦截对象的方法被执行时, 动态代理中执行拦截器intercept方法.
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

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

}

复制代码
  • @Intercepts为Mybatis提供的拦截器注解, @Signature指定拦截的方法.
  • 若是一个拦截器拦截多个方法时, 在@Intercepts中配置多个@Signature(数组)便可.
  • 因为JAVA的方法能够重载, 肯定惟一方法须要指定类(type), 方法(method), 参数(args).
  • 拦截器可拦截Executor,ParameterHandler,ResultSetHandler,StatementHandler下的方法.

3.3 配置拦截器

在Spring配置文件中, 声明拦截器并将其配置到SqlSessionFactoryBeanplugins属性中

// Mybatis拦截器
sqlInterceptor(SQLInterceptor)

// Mybatis配置
sqlSessionFactory(SqlSessionFactoryBean) {
    dataSource = ref("dataSource")
    mapperLocations = "classpath*:/com/atd681/mybatis/interceptor/*_mapper.xml"
    
    // 配置Mybatis拦截器
    plugins = [
        sqlInterceptor
    ] 
}
复制代码

4. 获取并保存SQL

Mybatis处理SQL的大体流程以下:

加载SQL >> 解析SQL >> 替换SQL参数 >> 执行SQL >> 获取返回结果

拦截[执行SQL]操做, 此时Mybatis已经完成SQL解析及替换参数, 所得的SQL即为发送数据库执行的SQL. 咱们只须要获取该SQL并保存至数据库便可.

// Mybatis拦截器:拦截全部的增删改SQL,将SQL保持至数据库
// 拦截StatementHandler.update方法
@Intercepts(@Signature(type = StatementHandler.class, 
                       method = "update", 
                       args = Statement.class)
           )
public class SQLInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        // invocation.getArgs()能够获取到被拦截方法的参数
        // StatementHandler.update(Statement s)的参数为Statement
        Statement s = (Statement) invocation.getArgs()[0];

        // 数据源为DRUID, Statement为DRUID的Statement
        Statement stmt = ((DruidPooledPreparedStatement) s).getStatement();

        // 配置druid链接时使用filters: stat配置
        if (stmt instanceof PreparedStatementProxyImpl) {
            stmt = ((PreparedStatementProxyImpl) stmt).getRawObject();
        }

        // 数据库提供的Statement可获取参数替换后的SQL(JDBC和DRUID获取的是带?的)
        // 数据库为MySQL,能够直接强制转换为MySQL的PreparedStatement获取SQL
        // SQL在书写时为了格式容器阅读会有换行符(多个空格)存在
        // 为了保存和查看方便去除SQL中的换行及多个空格
        String sql = ((com.mysql.jdbc.PreparedStatement) stmt).asSql().replaceAll("\\s+", " ");

        // 保存SQL的操做必须和当前执行的SQL在同一事务中
        // 使用当前SQL所在的数据库链接执行保存操做便可
        // 目标sql成功时保存sql的方法也同步成功
        Connection conn = stmt.getConnection();

        // 将SQL保存至数据库中
        PreparedStatement ps = null;

        try {
            ps = conn.prepareStatement("INSERT INTO atd681_mybatis_sql (v_sql) VALUES (?)");
            ps.setString(1, sql);

            // 由于和Mybatis的操做在同一事务中
            // 若是本次操做若是失败, 全部操做都回滚
            ps.execute();
        }
        finally {
            if (ps != null) {
                ps.close();
            }
        }

        // 继续执行StatementHandler.update方法
        return invocation.proceed();

    }

}

复制代码
  • 只有MySQL提供的PreparedStatement对象中能够获取到最终的SQL.
  • 保存SQL操做须要和Mybatis的操做在同一事务中, 必须同时成功或失败.

5. 测试

在数据库中建立两张表:

  • atd681_mybatis_test: 存储业务测试数据
  • atd681_mybatis_sql: 存储业务操做的SQL

建立DAOMapper, 建立增长, 删除, 修改的方法及SQL

// 数据DAO
@Repository
public interface DataDAO {

    // 添加数据
    void insert(String dv);

    // 更新数据
    void update(String dv);

    // 删除数据
    void delete();

}
复制代码
<mapper namespace="com.atd681.mybatis.interceptor.DataDAO">
	
	<!-- 添加数据,内容为参数i的值 -->
	<insert id="insert">
		INSERT INTO atd681_mybatis_test ( dv ) VALUES ( #{dv} )
	</insert>
	
	<!-- 更新数据,更新为参数u的值 -->
	<update id="update">
		UPDATE atd681_mybatis_test1 SET dv = #{dv}
	</update>
	
	<!-- 删除数据 -->
	<delete id="delete">
		DELETE FROM atd681_mybatis_test
	</delete>
	
</mapper> 
复制代码

控制器中添加方法, 依次调用删除, 添加, 更新. 保证三个操做在同一个事务中.

@RestController
public class DataController {

    // 注入DAO
    @Autowired
    private DataDAO dao;

    // 分别执行删除,插入,更新操做
    // 参数i: 插入时的字符串
    // 参数u: 更新时的字符串
    @GetMapping("/mybatis/test")
    @Transactional
    public String excuteSql(String i, String u) {

        // 删除数据后将参数i的内容插件数据库,将数据更新成参数u的内容
        // 该方法添加了事务,3次数据库操做会在同一个事务中执行.
        // Mybatis拦截器会捕获三次数据库SQL插入至数据库中(详见拦截器)
        dao.delete();
        dao.insert(i);
        dao.update(u);

        return "success";
    }

}
复制代码

启动服务, 访问http://localhost:3456/mybatis/test?i=insert&u=update

程序依次执行删除、添加(内容为"insert")、更新(内容为"update")三个操做, 执行完成后数据库中有一条记录(内容为"update"). 因为配置了拦截器, 在每一个操做执行前将SQL保持至数据库中, 所以三条SQL也被保存至数据库中.

上述过程当中除了3次业务操做, 还有3次保持SQL的操做, 所以数据库总共会执行6条SQL.

  1. 执行DELETE操做
  2. 保存1中DELETE操做的SQL
  3. 执行INSERT SQL
  4. 保存3中INSERT操做的SQL
  5. 执行UPDATE SQL
  6. 保存5中UPDATE操做的SQL

上述6次数据库操做必须在同一事务中, 不然一旦出现业务操做成功但保存SQL失败的状况. 服务器端同步的数据就会与客户端本地不一致.

6. 示例代码

虽然Mybatis没有Spring的扩展性强, 可是拦截器的出现也能够帮助咱们解决一些常见问题. 经过拦截器能够实现分页, 统一设置参数等常见的功能.

  • 示例代码地址: https://github.com/atd681/alldemo
  • 示例项目名称: atd681-mybatis-interceptor
相关文章
相关标签/搜索