最近的一个项目是将J2EE环境打包安装在客户端(使用nwjs
+NSIS
制做安装包)运行, 全部的业务操做在客户端完成, 数据存储在客户端数据库中. 服务器端数据库汇总各客户端的数据进行分析. 其中客户端ORM使用Mybatis. 经过Mybatis拦截器获取全部在执行的SQL语句, 按期同步至服务器.mysql
本文经过在客户端拦截SQL的操做介绍Mybatis拦截器的使用方法.git
客户分店较多且比较分散, 部分店内网络不稳定, 客户要求每一个分店在无网络的状况下也能正常使用系统, 同时全部店面数据须要进行汇总分析. 综合客户的需求, 项目架构以下:github
将WEB项目及其运行环境经过NSIS制做安装包在各分店进行安装, 每一个分店是一个独立的WEB服务, 这样就保证店内在无网络(有局域网,没法访问互联网)的状况下也能够正常使用系统. 此时每一个分店的数据库保存本身店内的运营数据, 各店之间的数据相互隔离.sql
但运营方没法分析全部店面的汇总数据(如商品总体销售状况等), 所以须要将每一个店面的数据按期同步至服务器的数据库中.数据库
最终采用了将客户端全部更新(增,删,改)的SQL按照执行顺序保存至数据库中, 按期同步并在服务器的数据库按照顺序执行SQL, 以此来保证服务器数据库的数据是各客户端数据的汇总.apache
项目采用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' )
复制代码
想这样一个场景, 你作饭的时候可能须要如下步骤:服务器
买菜 >> 洗菜 >> 切菜 >> 作菜 >> 上菜 >> 洗碗
上面的作饭流程是按照步骤一步一步的进行, 咱们既能够在其中的某个步骤中获取前几步的成果, 也能够在某个步骤开始以前作些额外的事情, 好比: 切菜前对菜称重等.
Mybatis提供了这样一个组件: 他能够在某个步骤执行以前先执行自定义的操做. 这个组件叫作拦截器. 所谓拦截器, 顾名思义: 须要定义拦截哪一个操做步骤及拦截后作什么事情.
拦截器须要实现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
(数组)便可.Executor
,ParameterHandler
,ResultSetHandler
,StatementHandler
下的方法.在Spring配置文件中, 声明拦截器并将其配置到SqlSessionFactoryBean
中plugins
属性中
// Mybatis拦截器
sqlInterceptor(SQLInterceptor)
// Mybatis配置
sqlSessionFactory(SqlSessionFactoryBean) {
dataSource = ref("dataSource")
mapperLocations = "classpath*:/com/atd681/mybatis/interceptor/*_mapper.xml"
// 配置Mybatis拦截器
plugins = [
sqlInterceptor
]
}
复制代码
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();
}
}
复制代码
在数据库中建立两张表:
atd681_mybatis_test
: 存储业务测试数据atd681_mybatis_sql
: 存储业务操做的SQL建立DAO
和Mapper
, 建立增长, 删除, 修改的方法及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.
- 执行DELETE操做
- 保存1中DELETE操做的SQL
- 执行INSERT SQL
- 保存3中INSERT操做的SQL
- 执行UPDATE SQL
- 保存5中UPDATE操做的SQL
上述6次数据库操做必须在同一事务中, 不然一旦出现业务操做成功但保存SQL失败的状况. 服务器端同步的数据就会与客户端本地不一致.
虽然Mybatis没有Spring的扩展性强, 可是拦截器的出现也能够帮助咱们解决一些常见问题. 经过拦截器能够实现分页, 统一设置参数等常见的功能.
https://github.com/atd681/alldemo
atd681-mybatis-interceptor