PageHelper在SpringBoot中的使用和原理分析
在SpringBoot项目中使用Mybatis的PageHelper分页插件进行分页查询
1、导入相关依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
2、添加相关配置信息
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
url: jdbc:mysql://localhost:3306/ssm?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
pagehelper:
reasonable: true
defaultCount: true # 分页插件默认参数支持 default-count 形式,自定义扩展的参数,必须大小写一致
helperDialect: mysql
mybatis:
mapper-locations: classpath:/mapper/*.xml
3、编写mapper接口
@Repository
public interface UserMapper {
List<User> getAllUser();
}
4、编写service,其中getAllUser()是普通查询,getAllUser1()和getAllUser2()是分页查询,使用PageHelper需要传入pageNum和pageSize
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public List<User> getAllUser(){
return userMapper.getAllUser();
}
public List<User> getAllUser1(int pageNum,int pageSize){
PageHelper.startPage(pageNum,pageSize);
List<User> allUser = userMapper.getAllUser();
return allUser;
}
public PageInfo<User> getAllUser2(int pageNum,int pageSize){
PageHelper.startPage(pageNum,pageSize);
List<User> allUser = userMapper.getAllUser();
PageInfo<User> pageInfo = new PageInfo<>();
pageInfo.setList(allUser);
return pageInfo;
}
}
5、编写Controller
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users")
public List<User> getAllUser(){
return userService.getAllUser();
}
@GetMapping("/users1")
public List<User> getAllUser1(){
return userService.getAllUser1(1,3);
}
@GetMapping("/users2")
public PageInfo<User> getAllUser2(){
return userService.getAllUser2(1,3);
}
}
6、编写Mybatis的mapper文件,其中namespace要与接口方法所在的包绑定,MappedStatement的id要与接口方法绑定
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.example.demo.pojo.User">
<id column="id" property="id" />
<result column="name" property="name" />
<result column="phone" property="phone" />
</resultMap>
<select id="getAllUser" resultType="com.example.demo.pojo.User">
select * from user
</select>
</mapper>
7、测试程序,测试接口
接口:http://localhost:8080/users
[{"id":"1","name":"zlw","phone":"17860397215"},{"id":"2","name":"zyx","phone":"18865986031"},{"id":"3","name":"zq","phone":"17860397216"},{"id":"4","name":"zxj","phone":"17860398476"},{"id":"5","name":"zmj","phone":"15021469872"},{"id":"6","name":"zqq","phone":"17652369421"}]
接口:http://localhost:8080/users1
[{"id":"1","name":"zlw","phone":"17860397215"},{"id":"2","name":"zyx","phone":"18865986031"},{"id":"3","name":"zq","phone":"17860397216"}]
接口:http://localhost:8080/users2
{"total":0,"list":[{"id":"1","name":"zlw","phone":"17860397215"},{"id":"2","name":"zyx","phone":"18865986031"},{"id":"3","name":"zq","phone":"17860397216"}],"pageNum":0,"pageSize":0,"size":0,"startRow":0,"endRow":0,"pages":0,"prePage":0,"nextPage":0,"isFirstPage":false,"isLastPage":false,"hasPreviousPage":false,"hasNextPage":false,"navigatePages":0,"navigatepageNums":null,"navigateFirstPage":0,"navigateLastPage":0}
接下来从代码的角度看看PageHelper是怎么实现分页的。
当导入pagehelper-spring-boot-starter依赖后,pagehelper已经自定了一个自动配置类PageHelperAutoConfiguration,可以在maven包下找到对应的spring.factories文件看一下。
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.github.pagehelper.autoconfigure.PageHelperAutoConfiguration
进入这个类看一下
@Configuration
@ConditionalOnBean({SqlSessionFactory.class})
@EnableConfigurationProperties({PageHelperProperties.class, PageHelperStandardProperties.class})
@AutoConfigureAfter({MybatisAutoConfiguration.class})
@Lazy(false)
public class PageHelperAutoConfiguration implements InitializingBean {
private final List<SqlSessionFactory> sqlSessionFactoryList;
private final PageHelperProperties properties;
public PageHelperAutoConfiguration(List<SqlSessionFactory> sqlSessionFactoryList, PageHelperStandardProperties standardProperties) {
this.sqlSessionFactoryList = sqlSessionFactoryList;
this.properties = standardProperties.getProperties();
}
public void afterPropertiesSet() throws Exception {
PageInterceptor interceptor = new PageInterceptor();
interceptor.setProperties(this.properties);
Iterator var2 = this.sqlSessionFactoryList.iterator();
while(var2.hasNext()) {
SqlSessionFactory sqlSessionFactory = (SqlSessionFactory)var2.next();
org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();
if (!this.containsInterceptor(configuration, interceptor)) {
configuration.addInterceptor(interceptor);
}
}
}
private boolean containsInterceptor(org.apache.ibatis.session.Configuration configuration, Interceptor interceptor) {
try {
return configuration.getInterceptors().stream().anyMatch((config) -> {
return interceptor.getClass().isAssignableFrom(config.getClass());
});
} catch (Exception var4) {
return false;
}
}
}
可以看到这个自动配置类PageHelperAutoConfiguration实现了InitializingBean接口并且重写了afterPropertiesSet()方法,所以当spring在初始化PageHelperAutoConfiguration时会执行afterPropertiesSet()进行初始化。这里创建了一个PageInterceptor分页拦截器,然后将PageInterceptor配置到Mybatis中。
这个PageInterceptor是实现分页的关键,点进去看看,先看一下类上的注解。
@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}
)})
@Intercepts表明PageInterceptor是一个拦截器,@Signature定义了这了拦截器要拦截的类型,method = "query"也就是这个拦截器会拦截所有的sql查询方法。
回到UserService.java,在调用mapper查询的代码上打个断点看一下PageInterceptor是怎么执行的。
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public List<User> getAllUser(){
return userMapper.getAllUser();
}
public List<User> getAllUser1(int pageNum,int pageSize){
PageHelper.startPage(pageNum, pageSize);
List<User> allUser = userMapper.getAllUser();
return allUser;
}
public PageInfo<User> getAllUser2(int pageNum,int pageSize){
PageHelper.startPage(pageNum,pageSize);
List<User> allUser = userMapper.getAllUser();
PageInfo<User> pageInfo = new PageInfo<>();
pageInfo.setList(allUser);
return pageInfo;
}
}
程序首先会执行PageInterceptor的plugin()方法,它返回了Mybatis执行器的代理对象,跟进wrap()方法看看。
public class PageInterceptor implements Interceptor {
//...
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
//...
}
public class Plugin implements InvocationHandler {
private final Object target;
private final Interceptor interceptor;
private final Map<Class<?>, Set<Method>> signatureMap;
private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
public static Object wrap(Object target, Interceptor interceptor) {
//map的key是Mybatis的执行器,value是要执行拦截的查询方法
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
//target是一个cachingExcutor类,这是Mybatis的执行器,负责执行查询操作和缓存机制
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
//为cachingExcutor生成一个代理对象,看第三个参数,Plugin类自身实现了Invocation接口,这里把自身传进去了
return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
}
//Plugin类实现Invocation接口,并重写invoke方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());
//如果方法需要拦截,就执行PageIntercptor的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);
}
}
}
Plugin的wrap()方法最终返回了一个Mybatis执行器的代理对象,在执行分页查询方法时,通过invoke方法进行调用,这时会执行PageInterceptor的intercept方法。
//PageInterceptor.class
public Object intercept(Invocation invocation) throws Throwable {
try {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement)args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds)args[2];
ResultHandler resultHandler = (ResultHandler)args[3];
Executor executor = (Executor)invocation.getTarget();
CacheKey cacheKey;
BoundSql boundSql;
if (args.length == 4) {
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
cacheKey = (CacheKey)args[4];
boundSql = (BoundSql)args[5];
}
this.checkDialectExists();
if (this.dialect instanceof BoundSqlInterceptor.Chain) {
boundSql = ((BoundSqlInterceptor.Chain)this.dialect).doBoundSql(Type.ORIGINAL, boundSql, cacheKey);
}
List resultList;
if (!this.dialect.skip(ms, parameter, rowBounds)) {
this.debugStackTraceLog();
Future<Long> countFuture = null;
Long count;
if (this.dialect.beforeCount(ms, parameter, rowBounds)) {
if (this.dialect.isAsyncCount()) {
countFuture = this.asyncCount(ms, boundSql, parameter, rowBounds);
} else {
count = this.count(executor, ms, parameter, rowBounds, (ResultHandler)null, boundSql);
if (!this.dialect.afterCount(count, parameter, rowBounds)) {
Object var13 = this.dialect.afterPage(new ArrayList(), parameter, rowBounds);
return var13;
}
}
}
//分页的重点
resultList = ExecutorUtil.pageQuery(this.dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
if (countFuture != null) {
count = (Long)countFuture.get();
this.dialect.afterCount(count, parameter, rowBounds);
}
} else {
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
Object var17 = this.dialect.afterPage(resultList, parameter, rowBounds);
return var17;
} finally {
if (this.dialect != null) {
this.dialect.afterAll();
}
}
}
ExecutorUtil.pageQuery()方法是分页逻辑的重点部分,进去看看。
//ExecutorUtil.class
public static <E> List<E> pageQuery(Dialect dialect, Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql, CacheKey cacheKey) throws SQLException {
if (!dialect.beforePage(ms, parameter, rowBounds)) {
return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
} else {
parameter = dialect.processParameterObject(ms, parameter, boundSql, cacheKey);
//从mysql方言中获取分页sql,在原始sql上拼接limit
String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, cacheKey);
//创建一个BoundSql对象,包含了分页sql以及其他的参数
BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
Iterator var12 = additionalParameters.keySet().iterator();
while(var12.hasNext()) {
String key = (String)var12.next();
pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
if (dialect instanceof BoundSqlInterceptor.Chain) {
pageBoundSql = ((BoundSqlInterceptor.Chain)dialect).doBoundSql(Type.PAGE_SQL, pageBoundSql, cacheKey);
}
//执行器去查询
return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, pageBoundSql);
}
}
从dialect.getPageSql()方法一直跟进可以进行mysql方言的getPageSql()方法,可以看到这个方法在原始的sql上拼接上了limit语句
//MySqlDialect.class
public String getPageSql(String sql, Page page, CacheKey pageKey) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
sqlBuilder.append(sql);
if (page.getStartRow() == 0L) {
sqlBuilder.append("\n LIMIT ? ");
} else {
sqlBuilder.append("\n LIMIT ?, ? ");
}
return sqlBuilder.toString();
}
接下来会封装一个BoundSql对象,包含分页sql和一些参数。
取哪些数据是根据pageNum和pageSize来计算的,这些参数是在调用mapper接口的方法之前通过PageHelper的startPage方法传入的。
PageHelper.startPage(pageNum, pageSize);
最后调用执行器的query()方法从数据库中查询结果并将其返回。
标签:dialect,return,SpringBoot,Object,public,原理,PageHelper,parameter,class From: https://www.cnblogs.com/Linwei33/p/18417484