前段时间甲同学遇到了在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实现原理就分析完成,最新版本的源码略有修改,可以自行阅读。