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.分析当前分页的参数传递

  1. 前台传递到Controller需要的参数为pageSize,currentPageNo;分别表示每页显示的条数,以及当前的页码
  2. Controller层接收到这个参数并调用Service层中的内容,最后Service层一般都会调用Mapper层中的内容,最后执行对应的XML文件
  3. 分析分页的SQL:一般表现形式为:Select ... from table 条件 limit startNum,pageSize
  4. 所以出现一个问题需要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的创建和添加

springboot mybatis添加拦截器_Source

具体实现如下:

@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 条件,(我们直接使用这个就可以实现分页效果)

结果:

springboot mybatis添加拦截器_分页_02


springboot mybatis添加拦截器_sql_03

6.分析RowBounds的作用和用法

前面通过替换RowBounds的方式实现分页,发现SQL中没有LIMIT,所以可以发现当前的查询中可能出现先查询所有的数据然后跳过,导致的结果就是:数据量大跳过的数据量大,出现性能问题!

现在开始通过查看哪里使用了RowBounds

springboot mybatis添加拦截器_Source_04


springboot mybatis添加拦截器_sql_05

查看这个handleRowValues方法

springboot mybatis添加拦截器_Source_06


然后分别查看handleRowValuesForNestedResultMap和handleRowValuesForSimpleResultMap方法,发现相同的问题

springboot mybatis添加拦截器_分页_07


springboot mybatis添加拦截器_分页_08

发现具有相同的方法shipRows方法,这个因该就是按照当前的条数实现跳过的方法

springboot mybatis添加拦截器_sql_09


结果,就是这个样子,从返回的结果中获得指定的条数,通过跳过的方式实现,这个性能不好!会有问题

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); }
	 */
}

结果为:

springboot mybatis添加拦截器_sql_10


发现实现了分页效果!但是需要重新构建当前的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方式实现的分页效果好!