前段時間甲同學遇到了在mybatis中遇到了一個神奇的問題,PageHelper會自動加上了limit ??導致查詢數據不準,先還原現場,
mapper.xml中sql語句:
<select id="selectByExample" resultMap="BaseResultMap" parameterType="com.xxxx.xxxx.xxxx.domain.PartnerExample" >
select
<if test="distinct" >
distinct
</if>
'true' as QUERYID,
<include refid="Base_Column_List" />
from usr_partner
<if test="_parameter != null" >
<include refid="Example_Where_Clause" />
</if>
<if test="orderByClause != null" >
order by ${orderByClause}
</if>
</select>
Mapper.java代碼:
@Component
public interface PartnerMapper {
List<Partner> selectByExample(PartnerExample example);
}
service層代碼:
public List<Partner> getAllPartnerList() {
PartnerExample example = new PartnerExample();
return partnerMapper.selectByExample(example);
}
甲同學一頓操作,調用查詢所有商戶方法getAllPartnerList()每次返回的結果都不一樣,和預期返回所有的商戶不一致,甲同學苦惱了好久並沒有發現問題,後面我給他指點指點問題就找到了,首先解決問題的關鍵就是把mybatis執行的sql語句打印出來就能發現其中的奧祕,在mybatis配置文件中加入
<settings>
<setting name="logImpl" value="STDOUT_LOGGING" />
</settings>
執行方法後打印sql語句
select * from usr_partner limit 0,3000
看到結果發現原來在執行的sql語句上面自動加上了limit ,,查看我們xml配置並沒有發現sql中有limit,那limit來自何處?其實是分頁插件PageHelper自動加上了limit
解決辦法(作用是手動清理 ThreadLocal 存儲的分頁參):
- 5.0及以後版本,調用PageHelper.clearPage();
- 低於5.0版本,調用 SqlUtil.clearLocalPage();
Mybatis PageHelper原理分析
分頁插件依賴:
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.2.8</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>4.1.6</version>
</dependency>
mybatis-config配置
<configuration>
<plugins>
<!-- com.github.pagehelper爲PageHelper類所在包名 -->
<plugin interceptor="com.github.pagehelper.PageHelper">
<property name="dialect" value="mysql" />
</plugin>
</plugins>
</configuration>
使用分頁插件:
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = factory.openSession();
PageHelper.startPage(0, 3);
session.selectList("com.xxx.xxxx.xxx.dao.PartnerMapper.selectByExample");
通過build()入口分析
加載文件配置
public SqlSessionFactory build(InputStream inputStream) {
return this.build((InputStream)inputStream, (String)null, (Properties)null);
}
繼續看build方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
SqlSessionFactory var5;
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
var5 = this.build(parser.parse());
} catch (Exception var14) {
throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
} finally {
...................
}
return var5;
}
繼續分析XMLConfigBuilder的parse方法,看看config.xml文件是如何加載進去
public Configuration parse() {
if (this.parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
} else {
this.parsed = true;
//先解析config.xml的root節點configuration
this.parseConfiguration(this.parser.evalNode("/configuration"));
return this.configuration;
}
}
//獲取到configuration節點後再解析config.xml中的其他結點
private void parseConfiguration(XNode root) {
try {
this.propertiesElement(root.evalNode("properties"));
this.typeAliasesElement(root.evalNode("typeAliases"));
//解析插件
this.pluginElement(root.evalNode("plugins"));
this.objectFactoryElement(root.evalNode("objectFactory"));
this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
this.settingsElement(root.evalNode("settings"));
this.environmentsElement(root.evalNode("environments"));
this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
this.typeHandlerElement(root.evalNode("typeHandlers"));
this.mapperElement(root.evalNode("mappers"));
} catch (Exception var3) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
}
}
分頁插件使用需要配置mybatis-config.xml的plugins,這裏繼續分析如何加載我們配置的分頁插件
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
Iterator i$ = parent.getChildren().iterator();
while(i$.hasNext()) {
XNode child = (XNode)i$.next();
//我們在配置文件中添加了
//<plugin interceptor="com.github.pagehelper.PageHelper">
//<property name="dialect" value="mysql" />
//</plugin>配置,這裏可以讀取到
String interceptor = child.getStringAttribute("interceptor");
//獲取<property name="dialect" value="mysql" />等屬性
Properties properties = child.getChildrenAsProperties();
//通過反射創建Interceptor 實例對象
Interceptor interceptorInstance = (Interceptor)this.resolveClass(interceptor).newInstance();
//設置屬性
interceptorInstance.setProperties(properties);
//添加攔截器
this.configuration.addInterceptor(interceptorInstance);
}
}
}
繼續分析addInterceptor方法
public void addInterceptor(Interceptor interceptor) {
this.interceptorChain.addInterceptor(interceptor);
}
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList();
public InterceptorChain() {
}
public void addInterceptor(Interceptor interceptor) {
this.interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(this.interceptors);
}
}
到這裏就是把配置的分頁插件添加到攔截器鏈中
通過上面分析發現PageHelper是一個攔截器,查看一下源碼
Intercepts({@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)})
public class PageHelper implements Interceptor {
private SqlUtil sqlUtil;
private Properties properties;
private Boolean autoDialect;
public PageHelper() {
}
.......
}
可以發現攔截的主要是Executor類的query方法,query參數主要有MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class,Executor又來自何方呢,繼續分析SqlSession session = factory.openSession();
public SqlSession openSession() {
return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
DefaultSqlSession var8;
............只看核心,其餘代碼省略..............
Environment environment = this.configuration.getEnvironment();
TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//攔截的Executor 在這裏生成的
Executor executor = this.configuration.newExecutor(tx, execType);
var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
............只看核心,其餘代碼省略..............
}
進入newExecutor方法
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? this.defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Object executor;
............只看核心,其餘代碼省略..............
Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
return executor;
}
//這個方法其實在加載plugin的InterceptorChain 的方法,addInterceptor方法作用是加載攔截器到攔截器調用鏈,
//pluginAll方法是遍歷攔截器鏈中的攔截器,調用攔截器的plugin方法,這裏只關注PageHelper中的plugin方法
public Object pluginAll(Object target) {
Interceptor interceptor;
for(Iterator i$ = this.interceptors.iterator(); i$.hasNext(); target = interceptor.plugin(target)) {
interceptor = (Interceptor)i$.next();
}
return target;
}
繼續分析PageHelper中的plugin方法
public Object plugin(Object target) {
//如果是需要攔截的Executor對象實例則調用wrap方法
return target instanceof Executor ? Plugin.wrap(target, this) : target;
}
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
}
通過JDK的動態代理生成增強的Executor對象
瞭解動態代理的應該清楚當執行攔截的方法時就會調用InvocationHandler的invoke方法,Plugin實現了InvocationHandler的invoke方法,繼續查invoke方法會發現
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());
//攔截的方法執行前會調用interceptor.intercept方法也就是PageHelper的intercept()方法
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);
}
}
其實這裏就是一個攔截器,所以當執行Executor的query的方法並且參數是MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class類型時就會執行攔截方法intercept也就是PageHelper的intercept方法,繼續看PageHelper的intercept方法實現
public Object intercept(Invocation invocation) throws Throwable {
............只看核心,其餘代碼省略..............
return this.sqlUtil.processPage(invocation);
}
public Object processPage(Invocation invocation) throws Throwable {
Object var3;
try {
Object result = this._processPage(invocation);
var3 = result;
} finally {
clearLocalPage();
OrderByHelper.clear();
}
return var3;
}
先看看參數invocation的值
可以看到Invocation類的3個屬性值主要包括執行器爲SimpleExecutor、查詢方法query、Mapper.xml位置、Mapper.xml中定義的方法、sql語句等,繼續查看_processPage方法實現
private Object _processPage(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
Page page = null;
//判斷是否支持分頁
if (this.supportMethodsArguments) {
page = this.getPage(args);
}
//分頁
RowBounds rowBounds = (RowBounds)args[2];
//如果設置的page分頁參數爲空或者不支持分頁時跳過
if (this.supportMethodsArguments && page == null || !this.supportMethodsArguments && getLocalPage() == null && rowBounds == RowBounds.DEFAULT) {
return invocation.proceed();
} else {
if (!this.supportMethodsArguments && page == null) {
page = this.getPage(args);
}
return this.doProcessPage(invocation, page, args);
}
}
繼續跟蹤doProcessPage方法
private Page doProcessPage(Invocation invocation, Page page, Object[] args) throws Throwable {
............只看核心,其餘代碼省略..............
//判斷page的條件
if (page.getPageSize() > 0 && (rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0 || rowBounds != RowBounds.DEFAULT)) {
page.setCountSignal((Boolean)null);
//獲取sql
BoundSql boundSql = ms.getBoundSql(args[1]);
args[1] = this.parser.setPageParameter(ms, args[1], boundSql, page);
page.setCountSignal(Boolean.FALSE);
Object result = invocation.proceed();
page.addAll((List)result);
}
return page;
}
繼續跟蹤getBoundSql方法
public BoundSql getBoundSql(Object parameterObject) {
Boolean count = this.getCount();
if (count == null) {
return this.getDefaultBoundSql(parameterObject);
} else {
//如果查詢總數不爲0掉用此方法
return count ? this.getCountBoundSql(parameterObject) : this.getPageBoundSql(parameterObject);
}
}
跟蹤getPageBoundSql方法
protected BoundSql getPageBoundSql(Object parameterObject) {
............只看核心,其餘代碼省略..............
//這個就是去獲取執行的sql語句 tempSql爲mapper.xml中編寫的
tempSql = ((Parser)localParser.get()).getPageSql(tempSql);
return new BoundSql(this.configuration, tempSql, ((Parser)localParser.get()).getPageParameterMapping(this.configuration, this.original.getBoundSql(parameterObject)), parameterObject);
}
跟蹤getPageSql方法發現了新大陸,在待執行的語句後面加上了limit ??拼接新的sql語句通過jdbc執行獲取分頁結果
public String getPageSql(String sql) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
sqlBuilder.append(sql);
sqlBuilder.append(" limit ?,?");
return sqlBuilder.toString();
}
到此,PageHelper實現原理就分析完成,最新版本的源碼略有修改,可以自行閱讀。