MyBatis拦截器及分页插件

更新时间:2024-05-10 00:20:02 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

Mybatis拦截器介绍

1.1 目录

1.1 目录 1.2 前言

1.3 Interceptor接口 1.4 注册拦截器

1.5 Mybatis可拦截的方法 1.6 利用拦截器进行分页

1.2 前言

拦截器的一个作用就是我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时执行自己的逻辑而不再执行被拦截的方法。Mybatis拦截器设计的一个初衷就是为了供用户在某些时候可以实现自己的逻辑而不必去动Mybatis固有的逻辑。打个比方,对于Executor,Mybatis中有几种实现:BatchExecutor、ReuseExecutor、SimpleExecutor和CachingExecutor。这个时候如果你觉得这几种实现对于Executor接口的query方法都不能满足你的要求,那怎么办呢?是要去改源码吗?当然不。我们可以建立一个Mybatis拦截器用于拦截Executor接口的query方法,在拦截之后实现自己的query方法逻辑,之后可以选择是否继续执行原来的query方法。

1.3 Interceptor接口

对于拦截器Mybatis为我们提供了一个Interceptor接口,通过实现该接口就可以定义我们自己的拦截器。我们先来看一下这个接口的定义: Java代码

1. package org.apache.ibatis.plugin; 2.

3. import java.util.Properties; 4.

5. public interface Interceptor { 6.

7. Object intercept(Invocation invocation) throws Throwable; 8.

9. Object plugin(Object target); 10.

11. void setProperties(Properties properties); 12. 13. }

我们可以看到在该接口中一共定义有三个方法,intercept、plugin和setProperties。plugin方法是拦截器用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理。当返回的是代理的时候我们可以对其中的方法进行拦截来调用intercept方法,当然也可以调用其他方法,这点将在后文讲解。setProperties方法是用于在Mybatis配置文件中指定一些属性的。

定义自己的Interceptor最重要的是要实现plugin方法和intercept方法,在plugin方法中我们可以决定是否要进行拦截进而决定要返回一个什么样的目标对象。而intercept方法就是要进行拦截的时候要执行的方法。

对于plugin方法而言,其实Mybatis已经为我们提供了一个实现。Mybatis中有一个叫做Plugin的类,里面有一个静态方法wrap(Object target,Interceptor interceptor),通过该方法可以决定要返回的对象是目标对象还是对应的代理。这里我们先来看一下Plugin的源码: Java代码

1. package org.apache.ibatis.plugin; 2.

3. import java.lang.reflect.InvocationHandler; 4. import java.lang.reflect.Method; 5. import java.lang.reflect.Proxy; 6. import java.util.HashMap; 7. import java.util.HashSet; 8. import java.util.Map; 9. import java.util.Set;

10.

11. import org.apache.ibatis.reflection.ExceptionUtil; 12.

13. public class Plugin implements InvocationHandler { 14.

15. private Object target;

16. private Interceptor interceptor;

17. private Map, Set> signatureMap; 18.

19. private Plugin(Object target, Interceptor interceptor, Map, Set

od>> signatureMap) { 20. this.target = target;

21. this.interceptor = interceptor; 22. this.signatureMap = signatureMap; 23. } 24.

25. public static Object wrap(Object target, Interceptor interceptor) {

26. Map, Set> signatureMap = getSignatureMap(interceptor)

;

27. Class type = target.getClass();

28. Class[] interfaces = getAllInterfaces(type, signatureMap); 29. if (interfaces.length > 0) { 30. return Proxy.newProxyInstance( 31. type.getClassLoader(), 32. interfaces,

33. new Plugin(target, interceptor, signatureMap)); 34. }

35. return target; 36. } 37.

38. public Object invoke(Object proxy, Method method, Object[] args) throws Thro

wable { 39. try {

40. Set methods = signatureMap.get(method.getDeclaringClass()); 41. if (methods != null && methods.contains(method)) {

42. return interceptor.intercept(new Invocation(target, method, args)); 43. }

44. return method.invoke(target, args); 45. } catch (Exception e) {

46. throw ExceptionUtil.unwrapThrowable(e); 47. } 48. } 49.

50. private static Map, Set> getSignatureMap(Interceptor inte

rceptor) {

51. Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Interce

pts.class);

52. if (interceptsAnnotation == null) { // issue #251

53. throw new PluginException(\

ptor \54. }

55. Signature[] sigs = interceptsAnnotation.value();

56. Map, Set> signatureMap = new HashMap, Se

t>();

57. for (Signature sig : sigs) {

58. Set methods = signatureMap.get(sig.type()); 59. if (methods == null) {

60. methods = new HashSet(); 61. signatureMap.put(sig.type(), methods); 62. } 63. try {

64. Method method = sig.type().getMethod(sig.method(), sig.args()); 65. methods.add(method);

66. } catch (NoSuchMethodException e) {

67. throw new PluginException(\

med \68. } 69. }

70. return signatureMap; 71. } 72.

73. private static Class[] getAllInterfaces(Class type, Map, Set<

Method>> signatureMap) {

74. Set> interfaces = new HashSet>();

75. while (type != null) {

76. for (Class c : type.getInterfaces()) { 77. if (signatureMap.containsKey(c)) { 78. interfaces.add(c); 79. } 80. }

81. type = type.getSuperclass(); 82. }

83. return interfaces.toArray(new Class[interfaces.size()]); 84. } 85. 86. }

我们先看一下Plugin的wrap方法,它根据当前的Interceptor上面的注解定义哪些接口需要拦截,然后判断当前目标对象是否有实现对应需要拦截的接口,如果没有则返回目标对象本身,如果有则返回一个代理对象。而这个代理对象的InvocationHandler正是一个Plugin。所以当目标对象在执行接口方法时,如果是通过代理对象执行的,则会调用对应InvocationHandler的invoke方法,也就是Plugin的invoke方法。所以接着我们来看一下该invoke方法的内容。这里invoke方法的逻辑是:如果当前执行的方法是定义好的需要拦截的方法,则把目标对象、要执行的方法以及方法参数封装成一个Invocation对象,再把封装好的Invocation作为参数传递给当前拦截器的intercept方法。如果不需要拦截,则直接调用当前的方法。Invocation中定义了定义了一个proceed方法,其逻辑就是调用当前方法,所以如果在intercept中需要继续调用当前方法的话可以调用invocation的procced方法。

这就是Mybatis中实现Interceptor拦截的一个思想,如果用户觉得这个思想有问题或者不能完全满足你的要求的话可以通过实现自己的Plugin来决定什么时候需要代理什么时候需要拦截。以下讲解的内容都是基于Mybatis的默认实现即通过Plugin来管理Interceptor来讲解的。

对于实现自己的Interceptor而言有两个很重要的注解,一个是@Intercepts,其值是一个@Signature数组。@Intercepts用于表明当前的对象是一个Interceptor,而@Signature则表明要拦截的接口、方法以及对应的参数类型。来看一个自定义的简单Interceptor: Java代码

1. package com.tiantian.mybatis.interceptor; 2.

3. import java.sql.Connection; 4. import java.util.Properties; 5.

6. import org.apache.ibatis.executor.Executor;

7. import org.apache.ibatis.executor.statement.StatementHandler; 8. import org.apache.ibatis.mapping.MappedStatement; 9. import org.apache.ibatis.plugin.Interceptor; 10. import org.apache.ibatis.plugin.Intercepts; 11. import org.apache.ibatis.plugin.Invocation; 12. import org.apache.ibatis.plugin.Plugin; 13. import org.apache.ibatis.plugin.Signature; 14. import org.apache.ibatis.session.ResultHandler; 15. import org.apache.ibatis.session.RowBounds; 16.

17. @Intercepts( {

18. @Signature(method = \19. MappedStatement.class, Object.class, RowBounds.class, 20. ResultHandler.class }),

21. @Signature(method = \

onnection.class }) })

22. public class MyInterceptor implements Interceptor { 23.

24. public Object intercept(Invocation invocation) throws Throwable { 25. Object result = invocation.proceed(); 26. System.out.println(\27. return result; 28. } 29.

30. public Object plugin(Object target) { 31. return Plugin.wrap(target, this); 32. } 33.

34. public void setProperties(Properties properties) { 35. String prop1 = properties.getProperty(\36. String prop2 = properties.getProperty(\

37. System.out.println(prop1 + \38. } 39. 40. }

首先看setProperties方法,这个方法在Configuration初始化当前的Interceptor时就会执行,这里只是简单的取两个属性进行打印。

其次看plugin方法中我们是用的Plugin的逻辑来实现Mybatis的逻辑的。 接着看MyInterceptor类上我们用@Intercepts标记了这是一个Interceptor,然后在@Intercepts中定义了两个@Signature,即两个拦截点。第一个@Signature我们定义了该Interceptor将拦截Executor接口中参数类型为MappedStatement、Object、RowBounds和ResultHandler的query方法;第二个@Signature我们定义了该Interceptor将拦截StatementHandler中参数类型为Connection的prepare方法。

最后再来看一下intercept方法,这里我们只是简单的打印了一句话,然后调用invocation的proceed方法,使当前方法正常的调用。

对于这个拦截器,Mybatis在注册该拦截器的时候就会利用定义好的n个property作为参数调用该拦截器的setProperties方法。之后在新建可拦截对象的时候会调用该拦截器的plugin方法来决定是返回目标对象本身还是代理对象。对于这个拦截器而言,当Mybatis是要Executor或StatementHandler对象的时候就会返回一个代理对象,其他都是原目标对象本身。然后当Executor代理对象在执行参数类型为MappedStatement、Object、RowBounds和ResultHandler的query方法或StatementHandler代理对象在执行参数类型为Connection的prepare方法时就会触发当前的拦截器的intercept方法进行拦截,而执行这两个接口对象的其他方法时都只是做一个简单的代理。

1.4 注册拦截器

注册拦截器是通过在Mybatis配置文件中plugins元素下的plugin元素来进行的。一个plugin对应着一个拦截器,在plugin元素下面我们可以指定若干个property子元素。Mybatis在注册定义的拦截器时会先把对应拦截器下面的所有property通过Interceptor的setProperties方法注入给对应的拦截器。所以,我们可以这样来注册我们在前面定义的MyInterceptor:

Xml代码

1.

3. PUBLIC \4. \5.

6.

8. 10.

11. 15.

16. 20.

22. 25. 26. 27.

28. 30.

1.5 Mybatis可拦截的方法

Mybatis拦截器只能拦截四种类型的接口:Executor、StatementHandler、

ParameterHandler和ResultSetHandler。这是在Mybatis的Configuration中写死了的,如果要支持拦截其他接口就需要我们重写Mybatis的Configuration。Mybatis可以对这四个接口中所有的方法进行拦截。

1.6 利用拦截器进行分页

下面将介绍一个Mybatis拦截器的实际应用。Mybatis拦截器常常会被用来进行分页处理。我们知道要利用JDBC对数据库进行操作就必须要有一个对应的Statement对象,Mybatis在执行Sql语句前也会产生一个包含Sql语句的Statement对象,而且对应的Sql语句是在Statement之前产生的,所以我们就可以在它成Statement之前对用来生成Statement的Sql语句下手。在Mybatis中Statement语句是通过RoutingStatementHandler对象的prepare方法生成的。所以利用拦截器实现Mybatis分页的一个思路就是拦截StatementHandler接口的prepare方法,然后在拦截器方法中把Sql语句改成对应的分页查询Sql语句,之后再调用StatementHandler对象的prepare方法,即调用

invocation.proceed()。更改Sql语句这个看起来很简单,而事实上来说的话就没那么直观,因为包括sql等其他属性在内的多个属性都没有对应的方法可以直接取到,它们对外部都是封闭的,是对象的私有属性,所以这里就需要引入反射机制来获取或者更改对象的私有属性的值了。对于分页而言,在拦截器里面我们常常还需要做的一个操作就是统计满足当前条件的记录一共有多少,这是通过获取到了原始的Sql语句后,把它改为对应的统计语句再利用Mybatis封装好的参数和设置参数的功能把Sql语句中的参数进行替换,之后再执行查询记录数的Sql语句进行总记录数的统计。先来看一个我们对分页操作封装的一个实体类Page: Java代码

1. import java.util.HashMap; 2. import java.util.List; 3. import java.util.Map; 4. 5. /**

6. * 对分页的基本数据进行一个简单的封装 7. */

8. public class Page { 9.

10. private int pageNo = 1;//页码,默认是第一页

11. private int pageSize = 15;//每页显示的记录数,默认是15 12. private int totalRecord;//总记录数 13. private int totalPage;//总页数

14. private List results;//对应的当前页记录

15. private Map params = new HashMap();//其

他的参数我们把它分装成一个Map对象 16.

17. public int getPageNo() { 18. return pageNo; 19. } 20.

21. public void setPageNo(int pageNo) { 22. this.pageNo = pageNo; 23. } 24.

25. public int getPageSize() { 26. return pageSize; 27. } 28.

29. public void setPageSize(int pageSize) { 30. this.pageSize = pageSize; 31. } 32.

33. public int getTotalRecord() { 34. return totalRecord; 35. } 36.

37. public void setTotalRecord(int totalRecord) { 38. this.totalRecord = totalRecord;

39. //在设置总页数的时候计算出对应的总页数,在下面的三目运算中加法拥有更

高的优先级,所以最后可以不加括号。

40. int totalPage = totalRecord%pageSize==0 ? totalRecord/pageSize : totalRec

ord/pageSize + 1;

41. this.setTotalPage(totalPage); 42. } 43.

44. public int getTotalPage() {

45. return totalPage; 46. } 47.

48. public void setTotalPage(int totalPage) { 49. this.totalPage = totalPage; 50. } 51.

52. public List getResults() { 53. return results; 54. } 55.

56. public void setResults(List results) { 57. this.results = results; 58. } 59.

60. public Map getParams() { 61. return params; 62. } 63.

64. public void setParams(Map params) { 65. this.params = params; 66. } 67.

68. @Override

69. public String toString() {

70. StringBuilder builder = new StringBuilder();

71. builder.append(\72. .append(pageSize).append(\73. \74. \75. return builder.toString(); 76. } 77. 78. }

对于需要进行分页的Mapper映射,我们会给它传一个Page对象作为参数,我们可以看到Page对象里面包括了一些分页的基本信息,这些信息我们可以在拦截器里面用到,然后我们把除分页的基本信息以外的其他参数用一个Map对象进行包装,这样在Mapper映射语句中的其他参数就可以从Map中取值了。接着来看一下我们的PageInterceptor的定义,对于PageInterceptor我就不做过多的说明,代码里面附有很详细的注释信息: Java代码

1. package com.tiantian.mybatis.interceptor; 2.

3. import java.lang.reflect.Field; 4. import java.sql.Connection; 5. import java.sql.PreparedStatement; 6. import java.sql.ResultSet; 7. import java.sql.SQLException; 8. import java.util.List; 9. import java.util.Properties; 10.

11. import org.apache.ibatis.executor.parameter.ParameterHandler; 12. import org.apache.ibatis.executor.statement.RoutingStatementHandler; 13. import org.apache.ibatis.executor.statement.StatementHandler; 14. import org.apache.ibatis.mapping.BoundSql; 15. import org.apache.ibatis.mapping.MappedStatement; 16. import org.apache.ibatis.mapping.ParameterMapping; 17. import org.apache.ibatis.plugin.Interceptor; 18. import org.apache.ibatis.plugin.Intercepts; 19. import org.apache.ibatis.plugin.Invocation; 20. import org.apache.ibatis.plugin.Plugin; 21. import org.apache.ibatis.plugin.Signature;

22. import org.apache.ibatis.scripting.defaults.DefaultParameterHandler; 23.

24. import com.tiantian.mybatis.model.Page; 25. 26. /** 27. *

28. * 分页拦截器,用于拦截需要进行分页查询的操作,然后对其进行分页处理。 29. * 利用拦截器实现Mybatis分页的原理:

30. * 要利用JDBC对数据库进行操作就必须要有一个对应的Statement对象,Mybatis

在执行Sql语句前就会产生一个包含Sql语句的Statement对象,而且对应的Sql语句

31. * 是在Statement之前产生的,所以我们就可以在它生成Statement之前对用来生

成Statement的Sql语句下手。在Mybatis中Statement语句是通过RoutingStatementHandler对象的

32. * prepare方法生成的。所以利用拦截器实现Mybatis分页的一个思路就是拦截

StatementHandler接口的prepare方法,然后在拦截器方法中把Sql语句改成对应的分页查询Sql语句,之后再调用

33. * StatementHandler对象的prepare方法,即调用invocation.proceed()。 34. * 对于分页而言,在拦截器里面我们还需要做的一个操作就是统计满足当前条件的

记录一共有多少,这是通过获取到了原始的Sql语句后,把它改为对应的统计语句再利用Mybatis封装好的参数和设

35. * 置参数的功能把Sql语句中的参数进行替换,之后再执行查询记录数的Sql语句

进行总记录数的统计。 36. * 37. */

38. @Intercepts( {

39. @Signature(method = \

onnection.class}) })

40. public class PageInterceptor implements Interceptor { 41.

42. private String databaseType;//数据库类型,不同的数据库有不同的分页方法 43. 44. /**

45. * 拦截后要执行的方法 46. */

47. public Object intercept(Invocation invocation) throws Throwable { 48. //对于StatementHandler其实只有两个实现类,一个是

RoutingStatementHandler,另一个是抽象类BaseStatementHandler, 49. //BaseStatementHandler有三个子类,分别是SimpleStatementHandler,

PreparedStatementHandler和CallableStatementHandler, 50. //SimpleStatementHandler是用于处理Statement的,

PreparedStatementHandler是处理PreparedStatement的,而CallableStatementHandler是

51. //处理CallableStatement的。Mybatis在进行Sql语句处理的时候都是建立的

RoutingStatementHandler,而在RoutingStatementHandler里面拥有一个

52. //StatementHandler类型的delegate属性,RoutingStatementHandler会依据

Statement的不同建立对应的BaseStatementHandler,即SimpleStatementHandler、

53. //PreparedStatementHandler或CallableStatementHandler,在

RoutingStatementHandler里面所有StatementHandler接口方法的实现都是调用的delegate对应的方法。

54. //我们在PageInterceptor类上已经用@Signature标记了该Interceptor只拦截

StatementHandler接口的prepare方法,又因为Mybatis只有在建立RoutingStatementHandler的时候

55. //是通过Interceptor的plugin方法进行包裹的,所以我们这里拦截到的目标对

象肯定是RoutingStatementHandler对象。

56. RoutingStatementHandler handler = (RoutingStatementHandler) invocation.

getTarget();

57. //通过反射获取到当前RoutingStatementHandler对象的delegate属性 58. StatementHandler delegate = (StatementHandler)ReflectUtil.getFieldValue(h

andler, \

59. //获取到当前StatementHandler的 boundSql,这里不管是调用

handler.getBoundSql()还是直接调用delegate.getBoundSql()结果是一样的,因为之前已经说过了

60. //RoutingStatementHandler实现的所有StatementHandler接口方法里面都是

调用的delegate对应的方法。

61. BoundSql boundSql = delegate.getBoundSql();

62. //拿到当前绑定Sql的参数对象,就是我们在调用对应的Mapper映射语句时所

传入的参数对象

63. Object obj = boundSql.getParameterObject();

64. //这里我们简单的通过传入的是Page对象就认定它是需要进行分页操作的。 65. if (obj instanceof Page) { 66. Page page = (Page) obj;

67. //通过反射获取delegate父类BaseStatementHandler的mappedStatement

属性

68. MappedStatement mappedStatement = (MappedStatement)ReflectUtil.ge

tFieldValue(delegate, \

69. //拦截到的prepare方法参数是一个Connection对象

70. Connection connection = (Connection)invocation.getArgs()[0]; 71. //获取当前要执行的Sql语句,也就是我们直接在Mapper映射语句中写的

Sql语句

72. String sql = boundSql.getSql();

73. //给当前的page参数对象设置总记录数 74. this.setTotalRecord(page,

75. mappedStatement, connection); 76. //获取分页Sql语句

77. String pageSql = this.getPageSql(page, sql);

78. //利用反射设置当前BoundSql对应的sql属性为我们建立好的分页Sql语

79. ReflectUtil.setFieldValue(boundSql, \80. }

81. return invocation.proceed(); 82. } 83. 84. 85. /**

86. * 拦截器对应的封装原始对象的方法 87. */

88. public Object plugin(Object target) { 89. return Plugin.wrap(target, this); 90. } 91. 92. /**

93. * 设置注册拦截器时设定的属性 94. */

95. public void setProperties(Properties properties) {

96. this.databaseType = properties.getProperty(\97. } 98. 99. /**

100. * 根据page对象获取对应的分页查询Sql语句,这里只做了两种数据库类型,

Mysql和Oracle

101. * 其它的数据库都 没有进行分页 102. *

103. * @param page 分页对象 104. * @param sql 原sql语句 105. * @return 106. */

107. private String getPageSql(Page page, String sql) {

278. }

接着我们在Mybatis的配置文件里面注册该拦截器: Xml代码

1.

3. PUBLIC \4. \5.

6.

8. 10.

11. 14.

15. 19.

21. 24. 25. 26.

27.

这样我们的拦截器就已经定义并且配置好了,接下来我们就来测试一下。假设在我们的UserMapper.xml中有如下这样一个Mapper映射信息: Xml代码

1.