1.声明
当前的内容中主要是为了本人更加深度的学习并了解MyBatis中的内容,以及部分的SpringBoot中的内容!
1.实现目标创建自己的分页插件,分析使用Aspect方式实现和当前基于Mybatis的区别和优缺点
2.由于本人最近正在手动创建项目,所以只有部分代码!
1.1 pom文件
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>SystemModel</groupId>
<artifactId>SystemModel</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>SystemModel</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
<scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
</dependencies>
</project>
2.分析当前分页的参数传递
- 前台传递到Controller需要的参数为
pageSize
,currentPageNo
;分别表示每页显示的条数,以及当前的页码 - Controller层接收到这个参数并调用Service层中的内容,最后Service层一般都会调用Mapper层中的内容,
最后执行对应的XML文件
- 分析分页的SQL:一般表现形式为:
Select ... from table 条件 limit startNum,pageSize
- 所以出现一个问题需要startNum参数,
如果采用@Param方式注入参数需要写入两个参数
,所以需要在传递参数的时候计算这个值
总结
1.我们可以通过拦截Controller层或者Service层以及Mapper层执行操作实现分页,但是需要实现分页的很多,有的需要条件,有的不需要条件,所以优点复杂,所以本人默认实现Map<String,Object>作为参数
3.本人的解决方案
1.由于Spring提供了AspectJ(切面),通过创建需要分页的注解@Paged,来定制特定的方法拦截(本人拦截Controller方法),通过拦截获取的分页参数实现通用的参数注入,通过计算方式实现该分页结果(本人已经实现成功)
2.由于本人整合的是Mybatis,所以可以通过实现特定的拦截器方式实现分页操作,不需要特定的分页参数
4.基于本人解决方案一实现的分页结果
1.创建注解@Paged
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
/**
* @description
* 当前注解用在那些需要被分页的数据中,
* 提供参数currentPageNo 表示当前的页码
* 提供参数pageSize 表示当前每页显示的条数
* 提供参数startNum 表示起始行
* @author hy
* @date 2020-03-11
*/
public @interface Paged {
}
2.在需要的Controller上添加注解@Paged
3.创建AOP切面类
@Slf4j
@Configuration
@Aspect
@Order(value = 3)
public class PagedAspectConfig {
private static final String PAGED_ANNOTATAION_STRING = "com.hy.commons.systemmodel.annotataion.Paged";
@Pointcut("@annotation(" + PAGED_ANNOTATAION_STRING + ")")
void pagePointCut() {
}
@Autowired
ServletRequest request;
@Around("pagePointCut()")
Object handlerBeforePaged(ProceedingJoinPoint pointcut) throws Throwable {
// 主要通过request获取和处理分页参数和设定分页参数,pageSize,currentPageNo、statNum
return pointcut.proceed();
}
}
4.编写分页操作以及分页的sql
结果确实是实现的,但是缺点就是:每个SQL中还是要写#{startNum},#{pageSize},而且当前的Mapper参数中还是需要@Param注解,并且在执行的时候,也会执行Count方法!缺点太多,不灵活,这个不推荐使用!
5.基于实现MyBatis的拦截器方式的参数(推荐使用这个)
1.创建需要配置的拦截器类,查看当前的MyBatis官方文档:MyBatis中Plugs的创建和添加
具体实现如下:
@Slf4j
@Intercepts({ @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class }) })
public class PageInterceptor implements Interceptor {
@Autowired
ServletRequest request;
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement mappedStatement = (MappedStatement) args[0];
String fullExecutorString = mappedStatement.getId();
if (fullExecutorString.contains("Page")) {
// 我们就认为这个方法是一个需要被分页的方法
args[2] = getCurrentRowBounds();
}
Object result = invocation.proceed();// 这里是让程序继续执行下去,并返回结果
log.info("<== Result:" + result);
return result;
}
private RowBounds getCurrentRowBounds() {
// 这里返回startNum和pageSize
return new RowBounds((currentPageNo - 1) * pageSize, pageSize);
}
这里我拦截的是Executor类中的query方法并且参数为:MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class
2.接下来配置这个拦截器,(主要在配置类中实现)
@Configuration
@PropertySource(value = { "db.properties" })
@MapperScan(basePackages = { "com.hy.commons.systemmodel.mapper" })
@EnableTransactionManagement
@ServletComponentScan
public class SystemModelAppConfig {
@Value("${url}")
private String url;
@Value("${dbname}")
private String dbname;
@Value("${dbpwd}")
private String dbpwd;
@Value("${driverClassName}")
private String driverClassName;
@Bean
public DataSource configDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(url);
dataSource.setUsername(dbname);
dataSource.setPassword(dbpwd);
return dataSource;
}
@Value("${mybatisTypeAliasesPackage}")
private String mybatisTypeAliasesPackage;
// 配置当前sql会话工厂
@Bean
public SqlSessionFactoryBean configSqlSessionFactoryBean() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setPlugins(configPageInterceptor());// 添加分页的插件
sqlSessionFactoryBean.setDataSource(configDataSource());
sqlSessionFactoryBean.setTypeAliasesPackage(mybatisTypeAliasesPackage);
return sqlSessionFactoryBean;
}
@Bean
public PageInterceptor configPageInterceptor() {
return new PageInterceptor();
}
}
修改SQL:Select … from table 条件,(我们直接使用这个就可以实现分页效果)
结果:
6.分析RowBounds的作用和用法
前面通过替换RowBounds的方式实现分页,发现SQL中没有LIMIT,所以可以发现当前的查询中可能出现先查询所有的数据然后跳过
,导致的结果就是:数据量大跳过的数据量大,出现性能问题!
现在开始通过查看哪里使用了RowBounds
查看这个handleRowValues方法
然后分别查看handleRowValuesForNestedResultMap和handleRowValuesForSimpleResultMap方法,发现相同的问题
发现具有相同的方法shipRows方法,这个因该就是按照当前的条数实现跳过的方法
结果,就是这个样子,从返回的结果中获得指定的条数,通过跳过的方式实现,这个性能不好!会有问题
!
7.替换MappedStatement来实现分页
@Slf4j
@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 })
*/
public class PageInterceptor implements Interceptor {
@Autowired
ServletRequest request;
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement mappedStatement = (MappedStatement) args[0];
String fullExecutorString = mappedStatement.getId();
if (!fullExecutorString.contains("Page")) {
return invocation.proceed();
}
// 处理不是select的查询或者是
if (SqlCommandType.SELECT != mappedStatement.getSqlCommandType()
|| StatementType.CALLABLE == mappedStatement.getStatementType()) {
return invocation.proceed();
}
Object result;
// 我们就认为这个方法是一个需要被分页的方法
/* args[2] = getCurrentRowBounds(); */
// 修改当前的执行的statement
args[0] = createNewMappedStatement(mappedStatement, args[1]);
result = invocation.proceed();
// 这里是让程序继续执行下去,并返回结果
// 方法
/* Object result = replaceWay(invocation); */
log.info("<== Result:" + result);
return result;
}
private BoundSql createNewBoundSql(MappedStatement mappedStatement, BoundSql oldBoundSql, Object parameterObject) {
String oldSql = oldBoundSql.getSql();
List<ParameterMapping> parameterMappings = oldBoundSql.getParameterMappings();
return createNewBoundSql(mappedStatement, oldSql, parameterMappings, parameterObject);
}
private BoundSql createNewBoundSql(MappedStatement mappedStatement, String oldSql,
List<ParameterMapping> parameterMappings, Object parameterObject) {
String createNewSql = concatPageSql(oldSql);
return new BoundSql(mappedStatement.getConfiguration(), createNewSql, parameterMappings, parameterObject);
}
// 创建新的sql
private String createNewSql(BoundSql boundSql) {
String sqlString = boundSql.getSql();
String pageSqlString = concatPageSql(sqlString);
return pageSqlString;
}
private MappedStatement createNewMappedStatement(MappedStatement mappedStatement, Object parameterObject) {
SqlSource sqlSource = mappedStatement.getSqlSource();
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
String pageSqlString = createNewSql(boundSql);
/* String pageSqlString = concatPageSql(sqlString); */
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
// sqlSource一旦创建不能修改
SqlSource newStaticSqlSource = new StaticSqlSource(mappedStatement.getConfiguration(), pageSqlString,
parameterMappings);
MappedStatement newMappedStatement = new MappedStatement.Builder(mappedStatement.getConfiguration(),
mappedStatement.getId(), newStaticSqlSource, mappedStatement.getSqlCommandType())
.resultMaps(mappedStatement.getResultMaps()).build();
setMappedStatementProperty(mappedStatement, newMappedStatement);// 需要添加的配置
return newMappedStatement;
}
private void setMappedStatementProperty(MappedStatement oldStatement, MappedStatement newStatement) {
// current None properties
}
private String concatPageSql(String sql) {
if (sql.contains(";")) {
sql = sql.replace(";", "");
}
Map<String, Integer> pageMap = PageUtils.getPageMap(request);
Integer pageSize = pageMap.get(PageUtils.PAGESIZE_STRING);
Integer currentPageNo = pageMap.get(PageUtils.CURRENTPAGENO_STRING);
log.info("==> PageParameters: " + PageUtils.PAGESIZE_STRING + "=" + pageSize + ","
+ PageUtils.CURRENTPAGENO_STRING + "=" + currentPageNo);
return new StringBuffer(sql).append("\n LIMIT " + ((currentPageNo - 1) * pageSize) + "," + pageSize).toString();
}
/*
* private RowBounds getCurrentRowBounds() { Map<String, Integer> pageMap =
* PageUtils.getPageMap(request); Integer pageSize =
* pageMap.get(PageUtils.PAGESIZE_STRING); Integer currentPageNo =
* pageMap.get(PageUtils.CURRENTPAGENO_STRING); log.info("==> PageParameters: "
* + PageUtils.PAGESIZE_STRING + "=" + pageSize + "," +
* PageUtils.CURRENTPAGENO_STRING + "=" + currentPageNo); return new
* RowBounds((currentPageNo - 1) * pageSize, pageSize); }
*/
}
结果为:
发现实现了分页效果!但是需要重新构建当前的SqlSource(一旦构建就不能修改)和MappedStatement
,其中MappedStatement中可能需要配置其他的参数,具有其他的可变性
8.使用executor方式实现分页效果
@Slf4j
@Intercepts(
{
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
}
)
public class PageInterceptor implements Interceptor {
@Autowired
ServletRequest request;
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement mappedStatement = (MappedStatement) args[0];
String fullExecutorString = mappedStatement.getId();
if (!fullExecutorString.contains("Page")) {
return invocation.proceed();
}
// 处理不是select的查询或者是
if (SqlCommandType.SELECT != mappedStatement.getSqlCommandType()
|| StatementType.CALLABLE == mappedStatement.getStatementType()) {
return invocation.proceed();
}
Object parameterObject = args[1];
RowBounds rowBounds = (RowBounds) args[2];
@SuppressWarnings("rawtypes")
ResultHandler resultHandler = (ResultHandler) args[3];
Executor executor = (Executor) invocation.getTarget();
Object result;
BoundSql boundSql = mappedStatement.getBoundSql(parameterObject);
CacheKey cacheKey = executor.createCacheKey(mappedStatement, parameterObject, rowBounds, boundSql);
// 使用替换方式执行
/*
* args[0] = createNewMappedStatement(mappedStatement, args[1]); result =
* invocation.proceed();
*/
BoundSql newBoundSql = createNewBoundSql(mappedStatement, boundSql, parameterObject);
result = executor.query(mappedStatement, parameterObject, rowBounds, resultHandler, cacheKey, newBoundSql);
log.info("<== Result:" + result);
return result;
}
private BoundSql createNewBoundSql(MappedStatement mappedStatement, BoundSql oldBoundSql, Object parameterObject) {
String oldSql = oldBoundSql.getSql();
List<ParameterMapping> parameterMappings = oldBoundSql.getParameterMappings();
return createNewBoundSql(mappedStatement, oldSql, parameterMappings, parameterObject);
}
private BoundSql createNewBoundSql(MappedStatement mappedStatement, String oldSql,
List<ParameterMapping> parameterMappings, Object parameterObject) {
String createNewSql = concatPageSql(oldSql);
log.debug(createNewSql);
return new BoundSql(mappedStatement.getConfiguration(), createNewSql, parameterMappings, parameterObject);
}
private String concatPageSql(String sql) {
if (sql.contains(";")) {
sql = sql.replace(";", "");
}
Map<String, Integer> pageMap = PageUtils.getPageMap(request);
Integer pageSize = pageMap.get(PageUtils.PAGESIZE_STRING);
Integer currentPageNo = pageMap.get(PageUtils.CURRENTPAGENO_STRING);
log.info("==> PageParameters: " + PageUtils.PAGESIZE_STRING + "=" + pageSize + ","
+ PageUtils.CURRENTPAGENO_STRING + "=" + currentPageNo);
return new StringBuffer(sql).append("\n LIMIT " + ((currentPageNo - 1) * pageSize) + "," + pageSize).toString();
}
/*
* private RowBounds getCurrentRowBounds() { Map<String, Integer> pageMap =
* PageUtils.getPageMap(request); Integer pageSize =
* pageMap.get(PageUtils.PAGESIZE_STRING); Integer currentPageNo =
* pageMap.get(PageUtils.CURRENTPAGENO_STRING); log.info("==> PageParameters: "
* + PageUtils.PAGESIZE_STRING + "=" + pageSize + "," +
* PageUtils.CURRENTPAGENO_STRING + "=" + currentPageNo); return new
* RowBounds((currentPageNo - 1) * pageSize, pageSize); }
*/
}
结果与上面的一致,但是这个方法比上面的要简单而且方便(本人建议使用这个方式)
9.总结
1.通过这个基本的分页操作发现,使用的时候应该尽量向使用的数据库工具靠近,可以很容易的达到自己的要求(例如使用MyBatis拦截器),不要轻易地使用AOP编程(可能会导致其他的结果导致代码冗余)
2.通过编写自己的MyBatis拦截器,发现实现起来还是很容易的,一定要注意低耦合
3.编写程序的时候一定要先思考,会采用哪几种解决方案,最后再测试,发现各个方案的不足之处和各自的优点
4.尽量不要使用RowBounds方式实现分页,使用executor方式实现的分页效果好!