當前位置:
首頁 > 知識 > mybatis精通之路之插件分頁(攔截器)進階

mybatis精通之路之插件分頁(攔截器)進階

這裡詳細講解了幾種分頁方式的原理和優缺點,適合於初學者,很容易理解,不清楚的同學可以回去瞟上幾眼。。

任務分析:當然,這並不是我們這篇博客講解的重點。記得在上一篇中,我們只是實現了最簡單的插件分頁實現,還非常簡陋,功能也還不夠完善,日常使用起來也還不夠簡便。所以在這裡,我們對插件分頁的實現原理進行一下詳細的介紹,並且實現一個功能完善的分頁插件。

原理剖析:

//註解攔截器並且簽名

@Intercepts(@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}))

1

2

和StatementHandler服務類中prepare方法相對應。

public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException

1

自定義的插件類,都需要使用@Intercepts註解,@Signature是對插件需要攔截的對象進行簽名,type表示要攔截的類型,method表示攔截類中的方法,args是需要的參數,這裡的參數在後面也可以獲取到。

StatementHandler:資料庫會話器,專門用於處理資料庫會話,statement的執行操作,是一個介面。

MetaObject:mybatis工具類,可以有效的讀取或修改一些重要對象的屬性,基本思想是通過反射去獲取和設置對象的屬性值,只是MetaObject類不需要我們自己去實現具體反射的方法,已經封裝好了。

通過MetaObject.getValue()和MetaObject.setValue(name,value)方法去獲取對象屬性值和設置對象屬性值。

通過MetaObject屬性的獲取流程:

MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement")

1

上面代碼是怎麼獲取到MappedStatement對象的??這裡的metaStatementHandler是一個MetaObject對象。

首先通過metaStatementHandler.getValue(「delegate」)拿到真正實現StatementHandler介面的服務對象。

public class RoutingStatementHandler implements StatementHandler {

//delegate屬性來自這裡,是一個實現了StatementHandler介面的類

private final StatementHandler delegate;

//通過這裡給delegate屬性賦值

public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

switch(RoutingStatementHandler.SyntheticClass_1.$SwitchMap$org$apache$ibatis$mapping$StatementType[ms.getStatementType().ordinal()]) {

case 1:

this.delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);

break;

case 2:

this.delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);

break;

case 3:

this.delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);

break;

default:

throw new ExecutorException("Unknown statement type: " + ms.getStatementType());

}

}

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

拿到具體的服務對象(處理邏輯的StatementHandler實現類)後,再獲取mappedStatement屬性,我們再來看mappedStatement屬性的定義:

public abstract class BaseStatementHandler implements StatementHandler {

protected final Configuration configuration;

protected final ObjectFactory objectFactory;

protected final TypeHandlerRegistry typeHandlerRegistry;

protected final ResultSetHandler resultSetHandler;

protected final ParameterHandler parameterHandler;

protected final Executor executor;

//定義在這裡

protected final MappedStatement mappedStatement;

protected final RowBounds rowBounds;

protected BoundSql boundSql;

}

1

2

3

4

5

6

7

8

9

10

11

12

可以看出是定義在BaseStatementHandler中的屬性,三個具體的服務對象都會繼承BaseStatementHandler。這裡有很多和執行資料庫操作相關的屬性,如果我們需要的話,都可以通過上述方式獲取,如果相獲取下層對象的屬性,按照這個寫法一次獲取也可以拿到。

RoutingStatementHandler:不是真正的服務對象,它通過適配器模式找到正確的StatementHandler去執行操作。通過invocation.getTarget()獲取到的是一個RoutingStatementHandler代理對象,再通過MappedStatement中不同的類型,找到具體的處理類。

真正實現StatementHandler介面的服務對象有:SimpleStatementHandler,PreparedStatementHandler,CallableStatementHandler都繼承BaseStatementHandler,它們分別對應是三種不同的執行器:SIMPLE:默認的簡單執行器;REUSE:重用預處理語句的執行期;BATCH:重用語句和批量更新處理器。

BoundSql: 用於組裝SQL和參數,使用插件時需要通過它拿到當前運行的SQL和參數及參數規則。它有如下幾個重要屬性:

public class BoundSql {

private String sql;

private List<ParameterMapping> parameterMappings;

private Object parameterObject;

private Map<String, Object> additionalParameters;

private MetaObject metaParameters;

}

1

2

3

4

5

6

7

parameterObject:是參數本身,調用方法時傳遞進來的參數。可以是pojo,map或@param註解的參數等。

parameterMappings:它是一個List,存儲了許多ParameterMapping對象。這個對象會描述我們的參數,參數包括屬性、名稱、表達式、javaType、jdbcType等。

sql:我們書寫在mapper.xml文件中的一條sql語句。

MappedStatement:存儲mapper.xml文件中一條sql語句配置的所有信息。

Connection:連接對象,在插件中會依賴它去進行一些資料庫操作。

Configuration:包含mybatis所有的配置信息。

ParameterHandler:介面,對預編譯語句進行參數設置。即將參數設置到sql語句中。它有兩個重要方法:getParameterObject()用於獲取參數對象和

setParameters(PreparedStatement var1)用於設置參數對象。

在對自定義分頁插件中會使用到的各個參數有了理解後,我們就來具體實現這個分頁插件。

在插件中我們使用了一個輔助類,來封裝分頁時會用到的一些參數,定義如下:

package com.cbg.interceptor;

/**

* Created by chenboge on 2017/5/14.

* <p>

* Email:baigegechen@gmail.com

* <p>

* description:實現分頁的輔助類,用於封裝用於分頁的一些參數

*/

public class PageParam {

private Integer defaultPage;

// 默認每頁顯示條數

private Integer defaultPageSize;

// 是否啟用分頁功能

private Boolean defaultUseFlag;

// 是否檢測當前頁碼的合法性(大於最大頁碼或小於最小頁碼都不合法)

private Boolean defaultCheckFlag;

//當前sql查詢的總記錄數,回填

private Integer totle;

// 當前sql查詢實現分頁後的總頁數,回填

private Integer totlePage;

public Integer getDefaultPage() {

return defaultPage;

}

public void setDefaultPage(Integer defaultPage) {

this.defaultPage = defaultPage;

}

public Integer getDefaultPageSize() {

return defaultPageSize;

}

public void setDefaultPageSize(Integer defaultPageSize) {

this.defaultPageSize = defaultPageSize;

}

public Boolean isDefaultUseFlag() {

return defaultUseFlag;

}

public void setDefaultUseFlag(Boolean defaultUseFlag) {

this.defaultUseFlag = defaultUseFlag;

}

public Boolean isDefaultCheckFlag() {

return defaultCheckFlag;

}

public void setDefaultCheckFlag(Boolean defaultCheckFlag) {

this.defaultCheckFlag = defaultCheckFlag;

}

public Integer getTotle() {

return totle;

}

public void setTotle(Integer totle) {

this.totle = totle;

}

public Integer getTotlePage() {

return totlePage;

}

public void setTotlePage(Integer totlePage) {

this.totlePage = totlePage;

}

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

當需要使用到分頁功能時,我們只需要將分頁參數封裝到PageParam對象中,並且作為參數傳遞到查詢方法中,插件中就會自動獲取到這些參數,並且動態組分頁的Sql查詢語句。下面就是我們自定義的分頁插件類實現:

package com.cbg.interceptor;

import org.apache.ibatis.executor.parameter.ParameterHandler;

import org.apache.ibatis.executor.statement.RoutingStatementHandler;

import org.apache.ibatis.executor.statement.StatementHandler;

import org.apache.ibatis.mapping.BoundSql;

import org.apache.ibatis.mapping.MappedStatement;

import org.apache.ibatis.plugin.*;

import org.apache.ibatis.reflection.MetaObject;

import org.apache.ibatis.reflection.SystemMetaObject;

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

import javax.security.auth.login.Configuration;

import java.lang.reflect.InvocationTargetException;

import java.sql.Connection;

import java.sql.PreparedStatement;

import java.sql.ResultSet;

import java.sql.SQLException;

import java.util.Map;

import java.util.Properties;

/**

* Created by chenboge on 2017/5/14.

* <p>

* Email:baigegechen@gmail.com

* <p>

* description:插件分頁

*/

@Intercepts(@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}))

public class PageInterceptor implements Interceptor {

// 默認頁碼

private Integer defaultPage;

// 默認每頁顯示條數

private Integer defaultPageSize;

// 是否啟用分頁功能

private boolean defaultUseFlag;

// 檢測當前頁碼的合法性(大於最大頁碼或小於最小頁碼都不合法)

private boolean defaultCheckFlag;

@Override

public Object intercept(Invocation invocation) throws Throwable {

StatementHandler statementHandler = getActuralHandlerObject(invocation);

MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);

String sql = statementHandler.getBoundSql().getSql();

// 檢測未通過,不是select語句

if (!checkIsSelectFalg(sql)) {

return invocation.proceed();

}

BoundSql boundSql = statementHandler.getBoundSql();

Object paramObject = boundSql.getParameterObject();

PageParam pageParam = getPageParam(paramObject);

if (pageParam == null)

return invocation.proceed();

Integer pageNum = pageParam.getDefaultPage() == null ? defaultPage : pageParam.getDefaultPage();

Integer pageSize = pageParam.getDefaultPageSize() == null ? defaultPageSize : pageParam.getDefaultPageSize();

Boolean useFlag = pageParam.isDefaultUseFlag() == null ? defaultUseFlag : pageParam.isDefaultUseFlag();

Boolean checkFlag = pageParam.isDefaultCheckFlag() == null ? defaultCheckFlag : pageParam.isDefaultCheckFlag();

//不使用分頁功能

if (!useFlag) {

return invocation.proceed();

}

int totle = getTotle(invocation, metaStatementHandler, boundSql);

//將動態獲取到的分頁參數回填到pageParam中

setTotltToParam(pageParam, totle, pageSize);

//檢查當前頁碼的有效性

checkPage(checkFlag, pageNum, pageParam.getTotlePage());

//修改sql

return updateSql2Limit(invocation, metaStatementHandler, boundSql, pageNum, pageSize);

}

@Override

public Object plugin(Object o) {

return Plugin.wrap(o, this);

}

// 在配置插件的時候配置默認參數

@Override

public void setProperties(Properties properties) {

String strDefaultPage = properties.getProperty("default.page");

String strDefaultPageSize = properties.getProperty("default.pageSize");

String strDefaultUseFlag = properties.getProperty("default.useFlag");

String strDefaultCheckFlag = properties.getProperty("default.checkFlag");

defaultPage = Integer.valueOf(strDefaultPage);

defaultPageSize = Integer.valueOf(strDefaultPageSize);

defaultUseFlag = Boolean.valueOf(strDefaultUseFlag);

defaultCheckFlag = Boolean.valueOf(strDefaultCheckFlag);

}

// 從代理對象中分離出真實statementHandler對象,非代理對象

private StatementHandler getActuralHandlerObject(Invocation invocation) {

StatementHandler statementHandler = (StatementHandler) invocation.getTarget();

MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);

Object object = null;

// 分離代理對象鏈,目標可能被多個攔截器攔截,分離出最原始的目標類

while (metaStatementHandler.hasGetter("h")) {

object = metaStatementHandler.getValue("h");

metaStatementHandler = SystemMetaObject.forObject(object);

}

if (object == null) {

return statementHandler;

}

return (StatementHandler) object;

}

// 判斷是否是select語句,只有select語句,才會用到分頁

private boolean checkIsSelectFalg(String sql) {

String trimSql = sql.trim();

int index = trimSql.toLowerCase().indexOf("select");

return index == 0;

}

/*

獲取分頁的參數

參數可以通過map,@param註解進行參數傳遞。或者請求pojo繼承自PageParam 將PageParam中的分頁數據放進去

*/

private PageParam getPageParam(Object paramerObject) {

if (paramerObject == null) {

return null;

}

PageParam pageParam = null;

//通過map和@param註解將PageParam參數傳遞進來,pojo繼承自PageParam不推薦使用 這裡從參數中提取出傳遞進來的pojo繼承自PageParam

// 首先處理傳遞進來的是map對象和通過註解方式傳值的情況,從中提取出PageParam,循環獲取map中的鍵值對,取出PageParam對象

if (paramerObject instanceof Map) {

Map<String, Object> params = (Map<String, Object>) paramerObject;

for (Map.Entry<String, Object> entry : params.entrySet()) {

if (entry.getValue() instanceof PageParam) {

return (PageParam) entry.getValue();

}

}

} else if (paramerObject instanceof PageParam) {

// 繼承方式 pojo繼承自PageParam 只取出我們希望得到的分頁參數

pageParam = (PageParam) paramerObject;

}

return pageParam;

}

// 獲取當前sql查詢的記錄總數

private int getTotle(Invocation invocation, MetaObject metaStatementHandler, BoundSql boundSql) {

// 獲取mapper文件中當前查詢語句的配置信息

MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");

//獲取所有配置Configuration

org.apache.ibatis.session.Configuration configuration = mappedStatement.getConfiguration();

// 獲取當前查詢語句的sql

String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");

// 將sql改寫成統計記錄數的sql語句,這裡是mysql的改寫語句,將第一次查詢結果作為第二次查詢的表

String countSql = "select count(*) as totle from (" + sql + ") $_paging";

// 獲取connection連接對象,用於執行countsql語句

Connection conn = (Connection) invocation.getArgs()[0];

PreparedStatement ps = null;

int totle = 0;

try {

// 預編譯統計總記錄數的sql

ps = conn.prepareStatement(countSql);

//構建統計總記錄數的BoundSql

BoundSql countBoundSql = new BoundSql(configuration, countSql, boundSql.getParameterMappings(), boundSql.getParameterObject());

//構建ParameterHandler,用於設置統計sql的參數

ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, boundSql.getParameterObject(), countBoundSql);

//設置總數sql的參數

parameterHandler.setParameters(ps);

//執行查詢語句

ResultSet rs = ps.executeQuery();

while (rs.next()) {

// 與countSql中設置的別名對應

totle = rs.getInt("totle");

}

} catch (SQLException e) {

e.printStackTrace();

} finally {

if (ps != null)

try {

ps.close();

} catch (SQLException e) {

e.printStackTrace();

}

}

return totle;

}

// 設置條數參數到pageparam對象

private void setTotltToParam(PageParam param, int totle, int pageSize) {

param.setTotle(totle);

param.setTotlePage(totle % pageSize == 0 ? totle / pageSize : (totle / pageSize) + 1);

}

// 修改原始sql語句為分頁sql語句

private Object updateSql2Limit(Invocation invocation, MetaObject metaStatementHandler, BoundSql boundSql, int page, int pageSize) throws InvocationTargetException, IllegalAccessException, SQLException {

String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");

//構建新的分頁sql語句

String limitSql = "select * from (" + sql + ") $_paging_table limit ?,?";

//修改當前要執行的sql語句

metaStatementHandler.setValue("delegate.boundSql.sql", limitSql);

//相當於調用prepare方法,預編譯sql並且加入參數,但是少了分頁的兩個參數,它返回一個PreparedStatement對象

PreparedStatement ps = (PreparedStatement) invocation.proceed();

//獲取sql總的參數總數

int count = ps.getParameterMetaData().getParameterCount();

//設置與分頁相關的兩個參數

ps.setInt(count - 1, (page - 1) * pageSize);

ps.setInt(count, pageSize);

return ps;

}

// 驗證當前頁碼的有效性

private void checkPage(boolean checkFlag, Integer pageNumber, Integer pageTotle) throws Exception {

if (checkFlag) {

if (pageNumber > pageTotle) {

throw new Exception("查詢失敗,查詢頁碼" + pageNumber + "大於總頁數" + pageTotle);

}

}

}

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

這次的分頁插件的實現相比上一遍講到的簡單攔截器來說,進行了更強一步的封裝,功能性也變得更強,使用起來也更加方便,完全適用於日常資料庫常見的分頁場景。

總結:現在幾乎所有的互聯網項目都會用到插件分頁的技術,這裡只是提出了一些簡單的實現思路和邏輯,如果你有更多的想法或者更好的方式,不妨提出來打架一起探討下。最後,在實現這個分頁插件的時候,順便整理了一下ssm開發中需要的基礎框架(項目結構、配置和需要的包整理),可以直接在上面進行二次開發,也實現了數據的簡單增刪改查邏輯和分頁插件。對分頁插件有不清楚的,或者初學者都可以借鑒一下,可能會有所幫助:項目地址ssm基礎配置和分頁插件

mybatis精通之路之插件分頁(攔截器)進階

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 程序員小新人學習 的精彩文章:

python腳本監控-用python監控python腳本執行情況(日誌記錄)
Docker-Window環境下安裝,使用入門hello-world示例

TAG:程序員小新人學習 |