经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » Spring Boot » 查看文章
Mybatis插件的开发经验
来源:cnblogs  作者:冬眠的山谷  时间:2019/6/11 9:17:37  对本文有异议

前言

MyBatis开放用户实现自己的插件,从而对整个调用过程进行个性化扩展。

这是MyBatis整个调用流程的主要参与者。

我们可以对其中的一些过程进行拦截,添加自己的功能,比如重写Sql添加分页参数。

拦截的接口

MyBatis允许拦截的接口如下

Executor

  1. public interface Executor {
  2. ResultHandler NO_RESULT_HANDLER = null;
  3. int update(MappedStatement var1, Object var2) throws SQLException;
  4. <E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4, CacheKey var5, BoundSql var6) throws SQLException;
  5. <E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws SQLException;
  6. <E> Cursor<E> queryCursor(MappedStatement var1, Object var2, RowBounds var3) throws SQLException;
  7. List<BatchResult> flushStatements() throws SQLException;
  8. void commit(boolean var1) throws SQLException;
  9. void rollback(boolean var1) throws SQLException;
  10. CacheKey createCacheKey(MappedStatement var1, Object var2, RowBounds var3, BoundSql var4);
  11. boolean isCached(MappedStatement var1, CacheKey var2);
  12. void clearLocalCache();
  13. void deferLoad(MappedStatement var1, MetaObject var2, String var3, CacheKey var4, Class<?> var5);
  14. Transaction getTransaction();
  15. void close(boolean var1);
  16. boolean isClosed();
  17. void setExecutorWrapper(Executor var1);
  18. }

ParameterHandler

  1. public interface ParameterHandler {
  2. Object getParameterObject();
  3. void setParameters(PreparedStatement var1) throws SQLException;
  4. }

ResultSetHandler

  1. public interface ResultSetHandler {
  2. <E> List<E> handleResultSets(Statement var1) throws SQLException;
  3. <E> Cursor<E> handleCursorResultSets(Statement var1) throws SQLException;
  4. void handleOutputParameters(CallableStatement var1) throws SQLException;
  5. }

StatementHandler

  1. public interface StatementHandler {
  2. Statement prepare(Connection var1, Integer var2) throws SQLException;
  3. void parameterize(Statement var1) throws SQLException;
  4. void batch(Statement var1) throws SQLException;
  5. int update(Statement var1) throws SQLException;
  6. <E> List<E> query(Statement var1, ResultHandler var2) throws SQLException;
  7. <E> Cursor<E> queryCursor(Statement var1) throws SQLException;
  8. BoundSql getBoundSql();
  9. ParameterHandler getParameterHandler();
  10. }

只要拦截器定义了拦截的接口和方法,后续调用该方法时,将会被拦截。

拦截器实现

如果要实现自己的拦截器,需要实现接口Interceptor

  1. @Slf4j
  2. @Intercepts(@Signature(type = Executor.class,
  3. method ="update",
  4. args ={MappedStatement.class,Object.class} ))
  5. public class MyIntercetor implements Interceptor {
  6. @Override
  7. public Object intercept(Invocation invocation) throws Throwable {
  8. log.info("MyIntercetor ...");
  9. Object result = invocation.proceed();
  10. log.info("result = " + result);
  11. return result;
  12. }
  13. @Override
  14. public Object plugin(Object o) {
  15. return Plugin.wrap(o,this);
  16. }
  17. @Override
  18. public void setProperties(Properties properties) {
  19. }
  20. }

1. 拦截方法配置

Intercepts,Signature

  1. public @interface Intercepts {
  2. Signature[] value();
  3. }
  1. public @interface Signature {
    Class<?> type();

    String method();

    Class<?>[] args();
    }
  1.  

配置

  1. @Intercepts(@Signature(type = Executor.class,
  2. method ="update",
  3. args ={MappedStatement.class,Object.class} ))

我们知道Java中方法的签名包括所在的类,方法名称,入参。 

@Signature定义方法签名

type:拦截的接口,为上节定义的四个接口

method:拦截的接口方法

args:参数类型列表,需要和方法中定义的顺序一致。

 也可以配置多个

  1. @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}
    )})

2. intercept(Invocation invocation)

  1. public class Invocation {
    private final Object target;
    private final Method method;
    private final Object[] args;

    public Invocation(Object target, Method method, Object[] args) {
    this.target = target;
    this.method = method;
    this.args = args;
    }

    public Object getTarget() {
    return this.target;
    }

    public Method getMethod() {
    return this.method;
    }

    public Object[] getArgs() {
    return this.args;
    }

    public Object proceed() throws InvocationTargetException, IllegalAccessException {
    return this.method.invoke(this.target, this.args);
    }
    }

通过Invocation可以获取到被拦截的方法的调用对象,方法,参数。

proceed()用于继续执行并获得最终的结果。

这里使用了设计模式中的责任链模式。

3.这里不能返回null。

用于给被拦截的对象生成一个代理对象,并返回它。

  1. @Override
  2. public Object plugin(Object o) {
  3. return Plugin.wrap(o,this);
  4. }

 可以看下wrap方法,其实现了JDK的接口InvocationHandler,也就是为传入的target创建了一个代理对象。这里使用了JDK动态代理方式。也可以自己实现其他代理方式,比如cglib.

  1. public class Plugin implements InvocationHandler {
  2. private final Object target;
  3. private final Interceptor interceptor;
  4. private final Map<Class<?>, Set<Method>> signatureMap;
  5.    public static Object wrap(Object target, Interceptor interceptor) {
  6. Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
  7. Class<?> type = target.getClass();
  8. Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
  9. return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
  10. }

      
  1.   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
    Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());
    return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args);
    } catch (Exception var5) {
    throw ExceptionUtil.unwrapThrowable(var5);
    }
    }

  1. }

由于使用了动态代理,方法执行时,将会被调用invoke方法,会先判断是否设置了拦截器:methods != null && methods.contains(method),

如果设置了拦截器,则调用拦截器this.interceptor.intercept(new Invocation(this.target, method, args))

否则直接调用method.invoke(this.target, args);

4.拦截器在执行前输出"MyIntercetor ...",在数据库操作返回后输出"result =xxx"

  1. log.info("MyIntercetor ...");
  2. Object result = invocation.proceed();
  3. log.info("result = " + result);

插件实现完成!

测试

在Spring中引入很简单。

第一种方式:

创建拦截器的bean

  1. @Slf4j
  2. @Configuration
  3. public class IntercetorConfiguration {
  4. @Bean
  5. public MyIntercetor myIntercetor(){
  6. return new MyIntercetor();
  7. }
  8. }

注意第一种方式和第二种方式仅适用于SpringBoot应用,并且引入以下依赖

  1. <dependency>
  2. <groupId>org.mybatis.spring.boot</groupId>
  3. <artifactId>mybatis-spring-boot-starter</artifactId>
  4. <version>1.3.2</version>
  5. </dependency>

第二种方式

手动往Configuration中添加拦截器。

  1. @Slf4j
  2. @Configuration
  3. public class IntercetorConfiguration {
  4. @Autowired
  5. private List<SqlSessionFactory> sqlSessionFactoryList;
  6. @PostConstruct
  7. public void addPageInterceptor() {
  8. MyIntercetor interceptor = new MyIntercetor();
  9. Iterator var3 = this.sqlSessionFactoryList.iterator();
  10. while(var3.hasNext()) {
  11. SqlSessionFactory sqlSessionFactory = (SqlSessionFactory)var3.next();
  12. sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
  13. }
  14. }
  15. }

 第三种方式

如果是纯Spring应用,可在mybatis配置文件中配置

  1. <plugins>
  2. <plugin intercetor="xxx.xxx.MyIntercetor">
  3. <property name="xxx" value="xxx">
  4. </plugin>
  5. </plugins>

由于上面定义的拦截器是拦截Executor的update方法,所以在执行insert,update,delete的操作时,将会被拦截。

本例子使用insert来测试。具体代码查看:GitHub

  1. 2019-06-10 16:08:03.109 INFO 20410 --- [nio-8110-exec-1] c.m.user.dao.intercetor.MyIntercetor : MyIntercetor ...
  2. 2019-06-10 16:08:03.166 INFO 20410 --- [nio-8110-exec-1] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited
  3. 2019-06-10 16:08:03.267 DEBUG 20410 --- [nio-8110-exec-1] o.m.s.t.SpringManagedTransaction : JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5cb1c36e] will not be managed by Spring
  4. 2019-06-10 16:08:03.274 DEBUG 20410 --- [nio-8110-exec-1] c.m.u.dao.mapper.UserMapper.insertList : ==> Preparing: insert into user (name) values (?) , (?) , (?)
  5. 2019-06-10 16:08:03.307 DEBUG 20410 --- [nio-8110-exec-1] c.m.u.dao.mapper.UserMapper.insertList : ==> Parameters: name:58(String), name:64(String), name:69(String)
  6. 2019-06-10 16:08:03.355 DEBUG 20410 --- [nio-8110-exec-1] c.m.u.dao.mapper.UserMapper.insertList : <== Updates: 3
  7. 2019-06-10 16:08:03.358 DEBUG 20410 --- [nio-8110-exec-1] c.m.u.d.m.U.insertList!selectKey : ==> Preparing: SELECT LAST_INSERT_ID()
  8. 2019-06-10 16:08:03.358 DEBUG 20410 --- [nio-8110-exec-1] c.m.u.d.m.U.insertList!selectKey : ==> Parameters:
  9. 2019-06-10 16:08:03.380 DEBUG 20410 --- [nio-8110-exec-1] c.m.u.d.m.U.insertList!selectKey : <== Total: 1

  10. 2019-06-10 16:08:03.381 INFO 20410 --- [nio-8110-exec-1] c.m.user.dao.intercetor.MyIntercetor : result = 3

可以看到拦截器被调用了。

简单的分页插件实现

这里拦截StatementHandler的prepare方法,也就是SQL语句预编译之前进行SQL改写。

  1. @Slf4j
  2. @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
  3. public class PageIntercetor implements Interceptor {
  4. @Override
  5. public Object intercept(Invocation invocation) throws Throwable {
  6. log.info("StatementHandler prepare ...");
  7. StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
  8. ParameterHandler parameterHandler = statementHandler.getParameterHandler();
  9. BoundSql boundSql = statementHandler.getBoundSql();
  10. //获取到原始sql语句
  11. String sql = boundSql.getSql();
  12. String mSql = sql + " limit 0,1";
  13. //通过反射修改sql语句
  14. Field field = boundSql.getClass().getDeclaredField("sql");
  15. field.setAccessible(true);
  16. field.set(boundSql, mSql);
  17. return invocation.proceed();
  18. }
  19. @Override
  20. public Object plugin(Object target) {
  21. return Plugin.wrap(target, this);
  22. }
  23. @Override
  24. public void setProperties(Properties properties) {
  25. //此处可以接收到配置文件的property参数
  26. System.out.println(properties.getProperty("name"));
  27. }
  28. }

分页插件实现的难点在于当使用不同的Statement时,执行流程是不一样的。

Statement需要定义statementType="STATEMENT",这个时候SQL语句不需要进行预编译处理,参数是与xml中配饰的SQL语句拼接在一起的。

  1. <select id="select" resultMap="BaseResultMap" statementType="STATEMENT">
    select id, name
    from user
    where
    name = '${name}'
    </select>

而当使用PreparedStatement时需要定义statementType="PREPARED",这个时候SQL语句需要进行预编译处理。CallableStatement(用于调用存储过程)同理。

  1. <select id="select" resultMap="BaseResultMap" statementType="PREPARED">
  2. select id, name
  3. from user
  4. where
  5. name = #{name}
  6. </select>

因此需要考虑不同情况下的SQL改写。

虽然Mybatis给我们实现了分页,只要在接口上传入RowBounds参数,即可实现分页。

但是这个是内存分页。也就是把所有的数据都读到应用内存中,再进行分页。造成了许多无效的读取。

当然也没必要搞的这么复杂!可以在mapper.xml中直接添加limit.

需要注意的是limit的参数的数据量不同,那么效率是不一样的,需要进行相关的优化。

结束!!!!!

原文链接:http://www.cnblogs.com/lgjlife/p/10998363.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号