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

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

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

1. 项目需求

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

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

但运营方无法分析所有店面的汇总数据(如商品整体销售情况等), 因此需要将每个店面的数据定期同步至服务器的数据库中.

由于店内可能无网络(无网时不能受数据同步影响,系统需正常运行), 实时同步方案被排除.
为保证数据库安全性, 服务器数据库不能对外暴露, 使用数据库的同步机制方案被排除.
部分业务需要记录数据变化日志(数据从1到0又到1, 需记录过程), 增量同步方案被排除.
最终采用了将客户端所有更新(增,删,改)的SQL按照执行顺序保存至数据库中, 定期同步并在服务器的数据库按照顺序执行SQL, 以此来保证服务器数据库的数据是各客户端数据的汇总.

2. 解决方案

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

Mybatis中SQL的写法:

<insert id="insert">
INSERT INTO atd681_mybatis_test ( dv ) VALUES ( #{dv} )
</insert>
复制代码
需要同步至服务器执行的SQL:

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配置文件中, 声明拦截器并将其配置到 SqlSessionFactoryBean 中 plugins 属性中

// 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的操作在同一事务中, 必须同时成功或失败.

  1. 测试

在数据库中创建两张表:

atd681_mybatis_test
atd681_mybatis_sql
创建 DAO 和 Mapper , 创建增加, 删除, 修改的方法及SQL

// 数据DAObr/>@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.

执行DELETE操作
保存1中DELETE操作的SQL
执行INSERT SQL
保存3中INSERT操作的SQL
执行UPDATE SQL
保存5中UPDATE操作的SQL
上述6次数据库操作必须在同一事务中, 否则一旦出现业务操作成功但保存SQL失败的情况. 服务器端同步的数据就会与客户端本地不一致.

原文地址:http://blog.51cto.com/13952953/2171689

时间: 2024-08-24 17:11:36

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

mybatis - 基于拦截器修改执行中的SQL语句

拦截器介绍 mybatis提供了@Intercepts注解允许开发者对mybatis的执行器Executor进行拦截. Executor接口方法主要有update.query.commit.rollback等等. 主要思路为: 进入拦截器方法中 获取拦截器方法参数 获取解析参数及SQL 自定义生成自己的SQL语句 将自定义SQL设置进参数中 由mybatis处理后续问题 拦截器代码 import org.apache.ibatis.cache.CacheKey; import org.apach

Mybatis Interceptor 拦截器原理 源码分析

Mybatis采用责任链模式,通过动态代理组织多个拦截器(插件),通过这些拦截器可以改变Mybatis的默认行为(诸如SQL重写之类的),由于插件会深入到Mybatis的核心,因此在编写自己的插件前最好了解下它的原理,以便写出安全高效的插件. 代理链的生成 Mybatis支持对Executor.StatementHandler.PameterHandler和ResultSetHandler进行拦截,也就是说会对这4种对象进行代理. 通过查看Configuration类的源代码我们可以看到,每次都

SpringMVC多拦截器的执行

如果有多个拦截器,执行的顺序是在SpringMVC的配置文件里的前后顺序. <mvc:interceptors> <bean class="com.neuedu.interceptor.Inteceptor1"></bean> <mvc:interceptor> <mvc:mapping path="/test"/> <bean class="com.neuedu.interceptor.I

springmvc中自定义拦截器以及拦截器的执行过程

1.拦截器在一次请求中的执行流程 2.拦截器入门案例 2.1springMVC.xml的编写 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:m

mybatis拦截器实现查看sql执行效率

package cc.zeelan.common.utils; import java.lang.reflect.Field; import java.sql.Statement; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Properties; import org.apache.ibatis.executor.statement.StatementHan

MyBatis拦截器的执行顺序引发的MyBatis源码分析

你猜一下哪个先执行?反正不要按常规来. 1 <plugins> 2 <plugin interceptor="com.Interceptor1"></plugin> 3 <plugin interceptor="com.Interceptor2"></plugin> 4 </plugins> 之前看有的博客分析源码,都没提到这一点.之前我只是用一下而已,这个顺序测试一下其实结论也很容易获得,但是

MyBatis之拦截器分页

鲁春利的工作笔记,好记性不如烂笔头 数据库的分页主要有物理分页和逻辑分页.    物理分页:数据库本身提供的分页方式,如MySQL的limit.Oracle的rownum.SqlServer的top,好处是效率高,不好的地方就是不同数据库有不同的查询方式.    逻辑分页:从数据库将所有记录查询出来,存储到内存中,然后数据再直接从内存中获取并筛选分页,好处是能够统一查询方式,不好的地方是效率低,因为每次都要把全部数据查询出来再处理. 常用orm框架采用的分页技术:①:hibernate采用的是物

使用AOP拦截器获取一次请求流经方法的调用次数和调用耗时

引语 作为工程师,不能仅仅满足于实现了现有的功能逻辑,还必须深入认识系统.一次请求,流经了哪些方法,调用了多少次DB操作,多少次API操作,多少次IO操作,多少CPU操作,各耗时多少 ? 开发者必须知道这些运行时数据,才能对系统的运行有更深入的理解,更好滴提升系统的性能和稳定性. 完成一次订单导出任务,实际上是一个比较复杂的过程:需要访问ES 来查询订单,调用 API 及访问 Hbase 获取订单详情数据,写入和上传报表文件,更新数据库,上报日志数据等:在大流量导出的情形下,采用批量并发策略,多

拦截器的执行顺序

1.在请求到来的时候,拦截器会拦截,执行preHandle方法.如果该方法的返回值为true, 就继续往下执行,否则,就结束执行,往下就不在执行任何方法. 2.在preHandle的返回值为true的情况下,就继续执行请求的jsp页面或者controller. 3.执行完请求后,执行postHandle方法. 4.最后执行afterComplete方法.