【Java EE】插件

插件

有机会在四大对象调度时插入自定义的代码来执行一些特殊的要求以满足特殊的场景需求,这便是MyBatis的插件技术。

插件接口

在MyBatis中使用插件,就必须实现接口Interceptor,它的定义和各个方法的含义为:

public interface Interceptor{
	Object intercept(Invocation invocation) throws Throwable;
	Object plugin(Object target);
	void setProperties(Properties properties);
}	

在接口中,定义了3个方法:
-intercept方法:它将直接覆盖拦截对象原有的方法,是插件的核心方法。intercept里面有个参数Invocation对象,通过它可以反射调度原来对象的方法。
plugin方法:target是被拦截对象,它的作用的给被拦截对象生成一个代理对象,并返回它。在MyBatis中,它提供了org.apache.ibatis.plugin.Plugin中的wrap静态(static)方法生成代理对象,一般情况下都会使用它来生成代理对象。
setProperties方法:允许在plugin元素中配置所需参数,方法在插件初始化时被调用了一次,然后把插件对象存入到配置中,以便后面再取出。
这是插件的骨架,这样的模式被称为模板(template)模式

插件的初始化

插件的初始化是在MyBatis初始化时完成的。在解析配置文件时,在MyBatis的上下文初始化过程中,就开始读入插件节点和配置的参数,同时使用反射技术生成对应的插件实例,然后调用插件方法中的setProperties方法,设置我们配置的参数,将插件实例保存到配置对象中,以便读取和使用它。所以插件的实例对象是一开始就被初始化的,而不是用到时才初始化。使用时,直接拿出来用就可以了。
完成初始化的插件保存在List对象里边等待将其取出使用。

插件的代理和反射设计

插件用的是责任链模式,MyBatis的责任链是由interceptorChain去定义的。创建执行器时用到过如下代码:

executor = (Executor) interceptorChain.pluginAll(executor)

其中pluginAll()方法的实现如下:

public Object pluginAll(Object target){
	for (Interceptor interceptor: interceptors){
		target =interceptor.plugin(target);
	}
	return target;
}

plugin方法是生成代理对象的方法,它是从Configuration对象中取出插件的。有多少个拦截器就生成多少个代理对象。每一个插件都可以拦截到真实的对象。
MyBatis中提供了一个常用的工具类,可以用来生成代理对象,它便是Plugin类。Plugin类实现了InvocationHandler接口,采用的JDK的动态代理。这个类有两个十分重要的方法:wrap()方法和invoke()方法。其中wrap方法生成这个对象的动态代理对象。如果使用这个类为插件生成代理对象,那么代理对象在调用方法时就会进入到invoke方法中。在invoke方法中,如果存在签名的拦截方法,插件的intercept方法就会在这里调用,然后返回结果。如果不存在签名方法,那么将直接反射调度要执行的方法。
这里MyBatis把被代理对象、反射方法及其参数都传递给了Invocation类的构造方法,用以生成一个Invocation类对象,Invocation类中有一个proceed()方法,这个就是通过反射的方式调度被代理对象的真实方法的。

常用的工具类——MetaObject

MyBatis的工具类——MetaObject,可以有效读取或者修改一些重要对象的属性。在MyBatis中,四大对象提供的public设置参数的方法很少,难以通过其自身得到相关的属性信息,但是有了MetaObject这个工具类就可以通过其他的技术手段来读取或者修改这些重要对象的属性。
工具类MetaObject有3个方法:

  • MetaObject forObject(Object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory)方法用于包装对象。这个方法已经不再使用了,而是用MyBatis提供的SystemMetaObject.forObject(Object obj)。
  • Object getValue(String name) 方法用于获取对象属性值,支持OGNL。
  • void setValue(String name, Object value)方法用于修改对象属性值,支持OGNL。
    MyBatis对象,包括四大对象大量使用了这个类进行包装,因此可以通过它来给四大对象的某些属性赋值从而满足我们的需要。

插件的开发过程和实例

确定需要拦截的签名

MyBatis允许拦截四大对象中的任意一个对象,而通过Plugin源码可知需要先注册签名才能使用插件。因此首先要确定需要拦截的对象,才能进一步确定需要配置什么样的签名,进而完成拦截的方法逻辑。

1. 确定需要拦截的对象

首先要根据功能来确定需要拦截什么对象。

  • Executor是执行SQL的全过程,包括组装参数、组装结果集返回和执行SQL过程,都可以拦截。一般用的不算太多。根据是否启动缓存参数,决定它是否使用CachingExecutor进行封装,这是拦截执行器时需要注意的地方。
  • StatementHandler是执行SQL的过程,是最常用的拦截对象。
  • ParameterHandler主要拦截执行SQL的参数组装,可以重写组装参数规则。
  • ResultSetHandler用于拦截执行结果的组装,可以重写组装结果的规则。
    要拦截的是StatementHandler对象,应该在预编译SQL之前修改SQL,使得结果返回数量被限制。

2. 拦截方法和参数

确定了需要拦截什么对象,接下来就要确定需要拦截什么方法及方法的参数,这些都是在理解了MyBatis四大对象运作的基础上才能确定的。
查询的过程是通过Executor调度StatementHandler来完成的。调度StatementHandler的prepare方法预编译SQL,于是要拦截的方法便是prepare方法,在此之前完成SQL的重新编写。 先看StatementHandler接口的定义:

public interface StatementHandler {

  Statement prepare(Connection connection, Integer transactionTimeout)
      throws SQLException;

  void parameterize(Statement statement)
      throws SQLException;

  void batch(Statement statement)
      throws SQLException;

  int update(Statement statement)
      throws SQLException;

  <E> List<E> query(Statement statement, ResultHandler resultHandler)
      throws SQLException;

  <E> Cursor<E> queryCursor(Statement statement)
      throws SQLException;

  BoundSql getBoundSql();

  ParameterHandler getParameterHandler();

}

以上的任何方法都可以拦截。从接口定义而言,prepare方法有一个参数Connection对象,因此,以如下代码设计拦截器:

@Intercepts({
	@Signature(type=StatementHandler.class,
		method="prepare",
		args={Connection.class, Integer}
	)
})
public class MyPlugin implements Interceptor{
	......
}

其中,@Intercepts说明它是一个拦截器。@Signature是注册拦截器签名的地方,只有签名满足条件才能拦截,type可以是四大对象中的一个,这里是StatementHandler。method代表要拦截四大对象的某一种接口方法,而args则表示该方法的参数。要根据拦截对象的方法参数进行设置。

实现拦截方法

import java.sql.Connection;
import java.util.Properties;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.log4j.Logger;

@Intercepts({
		@Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class }) })
public class MyPlugin implements Interceptor {
	private Logger log = Logger.getLogger(MyPlugin.class);
	private Properties props = null;

	/**
	 * 插件方法,它将代替StatementHandler的prepare方法
	 *
	 * @param invocation
	 *            入参
	 * @return 返回预编译后的PreparedStatement.
	 * @throws Throwable
	 *             异常.
	 */
	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
		// 进行绑定
		MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
		Object object = null;
		/*
		 * 分离代理对象链(由于目标类可能被多个拦截器[插件]拦截, 从而形成多次代理,通过循环可以分离出最原始的目标类)
		 */
		while (metaStatementHandler.hasGetter("h")) {
			object = metaStatementHandler.getValue("h");
			metaStatementHandler = SystemMetaObject.forObject(object);
		}
		statementHandler = (StatementHandler) object;
		String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
		Long parameterObject = (Long) metaStatementHandler.getValue("delegate.boundSql.parameterObject");
		log.info("执行的SQL:【" + sql + "】");
		log.info("参数:【" + parameterObject + "】");
		log.info("before ......");
		// 如果当前代理的是一个非代理对象,那么它就回调用真实拦截对象的方法,
		// 如果不是,那么它会调度下个插件代理对象的invoke方法
		Object obj = invocation.proceed();
		log.info("after ......");
		return obj;
	}

	/**
	 * 生成代理对象
	 *
	 * @param target
	 *            -- 被拦截对象.
	 * @return 代理对象
	 */
	@Override
	public Object plugin(Object target) {
		// 采用系统默认的Plugin.wrap方法生成
		return Plugin.wrap(target, this);
	}

	/**
	 * 设置参数,MyBatis初始化时,就会生成插件实例,并且调用这个方法.
	 *
	 * @param props
	 *            配置参数
	 */
	@Override
	public void setProperties(Properties props) {
		this.props = props;
		log.info("dbType = " + this.props.get("dbType"));
	}
}

插件首先分离代理对象,然后通过MetaObject获取了执行的SQL参数,并且在反射方法之前和之后分别打印before和after,这就意味着使用可以在方法前或方法后执行特殊的代码,以满足特殊要求。

配置和运行

最后要在MyBatis配置文件里配置才能够使用插件。注意plugins元素的配置顺序,配错顺序系统会报错。

<plugins>
	<plugin interceptor="com.ssm.chapter8.plugin.MyPlugin">
		<property name="dbType" value="mysql" />
	</plugin>
</plugins>

使用MyBatis执行一条SQL

<select id="getRole" parameterType="long" resultType="com.ssm.chapter8.pojo.Role">
	select id, role_name as roleName, note from t_role where id = #{id}
</select>

插件实例——分页插件

首先定义一个分页参数的POJO,通过它可以设置分页的各种参数。

public class PageParams {
	// 当前页码
	private Integer page;
	// 每页限制条数
	private Integer pageSize;
	// 是否启动插件,如果不启动,则不作分页
	private Boolean useFlag;
	// 是否检测页码的有效性,如果为true,而页码大于最大页数,则抛出异常
	private Boolean checkFlag;
	// 是否清除最后order by后面的语句
	private Boolean cleanOrderBy;
	// 总条数,插件会回填这个值
	private Integer total;
	// 总页数,插件会回填这个值.
	private Integer totalPage;
	/*setter and getter*/
}

在MyBatis中传递参数可以是单个参数,也可以是多个,或者使用Map。有了这些规则,为了使用方便,定义只要满足下列条件之一,就可以启用分页参数(PageParams)。

  • 传递单个PageParams或者其子对象
  • map中存在一个值为PageParams或者其子对象的参数
  • 在MyBatis中传递多个参数,但其中之一为PageParams或者其子对象
  • 传递多个POJO参数,这个POJO有一个属性为PageParams或者其子对象,且提供了setter和getter方法。
    为了在编译SQL之前修改SQL,需要增加分页参数并计算出查询总条数。依据MyBatis运行原理,选择拦截StatementHandler的prepare方法,拦截方法签名:
@Intercepts({
	@Signature(type=StatementHandler.class,
		method="prepare",
		args={Connection.class, Integer.class}
	)
})

在插件中有3个方法需要自己完成,其中plugin方法中,我们使用的是Plugin类的静态方法wrap生产代理对象。当Parameters的useFlag属性设置为false时,也就是禁用此分类参数时,就没有必要生成代理对象了。

@Intercepts({
		@Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class }) })
public class PagePlugin implements Interceptor {
	/**
	 * 插件默认参数,可配置默认值.
	 */
	private Integer defaultPage; // 默认页码
	private Integer defaultPageSize;// 默认每页条数
	private Boolean defaultUseFlag; // 默认是否启用插件
	private Boolean defaultCheckFlag; // 默认是否检测页码参数
	private Boolean defaultCleanOrderBy; // 默认是否清除最后一个order by 后的语句

	@Override
	public Object intercept(Invocation invocation) throws Throwable {
	...
	}
	@Override
	public Object plugin(Object target) {
		// 生成代理对象.
		return Plugin.wrap(target, this);
	}

	/**
	 * 设置插件配置参数。
	 *
	 * @param props
	 *            配置参数
	 */
	@Override
	public void setProperties(Properties props) {
		// 从配置中获取参数
		String strDefaultPage = props.getProperty("default.page", "1");
		String strDefaultPageSize = props.getProperty("default.pageSize", "50");
		String strDefaultUseFlag = props.getProperty("default.useFlag", "false");
		String strDefaultCheckFlag = props.getProperty("default.checkFlag", "false");
		String StringDefaultCleanOrderBy = props.getProperty("default.cleanOrderBy", "false");
		// 设置默认参数.
		this.defaultPage = Integer.parseInt(strDefaultPage);
		this.defaultPageSize = Integer.parseInt(strDefaultPageSize);
		this.defaultUseFlag = Boolean.parseBoolean(strDefaultUseFlag);
		this.defaultCheckFlag = Boolean.parseBoolean(strDefaultCheckFlag);
		this.defaultCleanOrderBy = Boolean.parseBoolean(StringDefaultCleanOrderBy);
	}
}

plugin方法使用了MyBatis提供生成代理对象的方法来生成,只要符合签名的规则,StatementHandler在运行时就会进入到intercept方法里。setProperties方法用于给插件设置默认值。intercept方法如下:

@Override
	public Object intercept(Invocation invocation) throws Throwable {
		StatementHandler stmtHandler = (StatementHandler) getUnProxyObject(invocation.getTarget());
		MetaObject metaStatementHandler = SystemMetaObject.forObject(stmtHandler);
		String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
		MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
		// 不是select语句
		if (!checkSelect(sql)) {
			return invocation.proceed();
		}
		BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
		Object parameterObject = boundSql.getParameterObject();
		PageParams pageParams = getPageParamsForParamObj(parameterObject);
		if (pageParams == null) { // 无法获取分页参数,不进行分页
			return invocation.proceed();
		}

		// 获取配置中是否启用分页功能
		Boolean useFlag = pageParams.getUseFlag() == null ? this.defaultUseFlag : pageParams.getUseFlag();
		if (!useFlag) { // 不使用分页插件
			return invocation.proceed();
		}
		// 获取相关配置的参数
		Integer pageNum = pageParams.getPage() == null ? defaultPage : pageParams.getPage();
		Integer pageSize = pageParams.getPageSize() == null ? defaultPageSize : pageParams.getPageSize();
		Boolean checkFlag = pageParams.getCheckFlag() == null ? defaultCheckFlag : pageParams.getCheckFlag();
		Boolean cleanOrderBy = pageParams.getCleanOrderBy() == null ? defaultCleanOrderBy
				: pageParams.getCleanOrderBy();
		// 计算总条数
		int total = getTotal(invocation, metaStatementHandler, boundSql, cleanOrderBy);
		// 回填总条数到分页参数
		pageParams.setTotal(total);
		// 计算总页数.
		int totalPage = total % pageSize == 0 ? total / pageSize : total / pageSize + 1;
		// 回填总页数到分页参数
		pageParams.setTotalPage(totalPage);
		// 检查当前页码的有效性
		checkPage(checkFlag, pageNum, totalPage);
		// 修改sql
		return preparedSQL(invocation, metaStatementHandler, boundSql, pageNum, pageSize);
	}

首先从责任链中分离出最原始的StatementHandler对象,使用的方法是getUnProxyObject,代码如下:

/**
	 * 从代理对象中分离出真实对象
	 * 
	 * @param ivt
	 *            --Invocation
	 * @return 非代理StatementHandler对象
	 */
	private Object getUnProxyObject(Object target) {
		MetaObject metaStatementHandler = SystemMetaObject.forObject(target);
		// 分离代理对象链(由于目标类可能被多个拦截器拦截,从而形成多次代理,通过循环可以分离出最原始的目标类)
		Object object = null;
		
		// 可以分离出最原始的的目标类)  
        while (metaStatementHandler.hasGetter("h")) {  
            object = metaStatementHandler.getValue("h");  
            metaStatementHandler = SystemMetaObject.forObject(object);  
        }  
        
		if (object == null) {
			return target;
		}
		return object;
	}

通过这个方法就可以把JDK动态代理链上原始的StatementHandler分离出来,然后通过MetaObject对象进行绑定,这样就可以为后续通过它分离出当前执行的SQL和参数做准备,由于拦截了所有的SQL,对于非查询(select)语句是不需要拦截的,所以需要一个判断是否是查询语句的方法checkSelect,代码如下:

/**
	 * 判断是否sql语句
	 *
	 * @param sql
	 *            --当前执行SQL
	 * @return 是否查询语句
	 */
	private boolean checkSelect(String sql) {
		String trimSql = sql.trim();
		int idx = trimSql.toLowerCase().indexOf("select");
		return idx == 0;
	}

如果是查询语句,则拦截这条SQL进入下一步——分离出分页参数PageParams,代码如下:

/**
	 * * 分离出分页参数
	 * 
	 * @param parameterObject
	 *            --执行参数
	 * @return 分页参数
	 * @throws Exception
	 */
	public PageParams getPageParamsForParamObj(Object parameterObject) throws Exception {
		PageParams pageParams = null;
		if (parameterObject == null) {
			return null;
		}
		// 处理map参数,多个匿名参数和@Param注解参数,都是map
		if (parameterObject instanceof Map) {
			@SuppressWarnings("unchecked")
			Map<String, Object> paramMap = (Map<String, Object>) parameterObject;
			Set<String> keySet = paramMap.keySet();
			Iterator<String> iterator = keySet.iterator();
			while (iterator.hasNext()) {
				String key = iterator.next();
				Object value = paramMap.get(key);
				if (value instanceof PageParams) {
					return (PageParams) value;
				}
			}
		} else if (parameterObject instanceof PageParams) { // 参数是或者继承PageParams
			return (PageParams) parameterObject;
		} else { // 从POJO属性尝试读取分页参数
			Field[] fields = parameterObject.getClass().getDeclaredFields();
			// 尝试从POJO中获得类型为PageParams的属性
			for (Field field : fields) {
				if (field.getType() == PageParams.class) {
					PropertyDescriptor pd = new PropertyDescriptor(field.getName(), parameterObject.getClass());
					Method method = pd.getReadMethod();
					return (PageParams) method.invoke(parameterObject);
				}
			}
		}
		return pageParams;
	}

计算出这条SQL能返回多少条记录,这是插件的难点之一。首先,修改为总数的SQL。其次,要为总数SQL设置参数,代码如下:

/**
	 * 获取总条数.
	 * 
	 * @param ivt
	 *            Invocation 入参
	 * @param metaStatementHandler
	 *            statementHandler
	 * @param boundSql
	 *            sql
	 * @param cleanOrderBy
	 *            是否清除order by语句
	 * @return sql查询总数.
	 * @throws Throwable
	 *             异常.
	 */
	private int getTotal(Invocation ivt, MetaObject metaStatementHandler, BoundSql boundSql, Boolean cleanOrderBy)
			throws Throwable {
		// 获取当前的mappedStatement
		MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
		// 配置对象
		Configuration cfg = mappedStatement.getConfiguration();
		// 当前需要执行的SQL
		String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
		// 去掉最后的order by语句
		if (cleanOrderBy) {
			sql = this.cleanOrderByForSql(sql);
		}
		// 改写为统计总数的SQL
		String countSql = "select count(*) as total from (" + sql + ") $_paging";
		// 获取拦截方法参数,根据插件签名,知道是Connection对象
		Connection connection = (Connection) ivt.getArgs()[0];
		PreparedStatement ps = null;
		int total = 0;
		try {
			// 预编译统计总数SQL
			ps = connection.prepareStatement(countSql);
			// 构建统计总数BoundSql
			BoundSql countBoundSql = new BoundSql(cfg, countSql, boundSql.getParameterMappings(),
					boundSql.getParameterObject());
			// 构建MyBatis的ParameterHandler用来设置总数Sql的参数
			ParameterHandler handler = new DefaultParameterHandler(mappedStatement, boundSql.getParameterObject(),
					countBoundSql);
			// 设置总数SQL参数
			handler.setParameters(ps);
			// 执行查询.
			ResultSet rs = ps.executeQuery();
			while (rs.next()) {
				total = rs.getInt("total");
			}
		} finally {
			// 这里不能关闭Connection,否则后续的SQL就没法继续了
			if (ps != null) {
				ps.close();
			}
		}
		return total;
	}

	private String cleanOrderByForSql(String sql) {
		StringBuilder sb = new StringBuilder(sql);
		String newSql = sql.toLowerCase();
		// 如果没有order语句,直接返回
		if (newSql.indexOf("order") == -1) {
			return sql;
		}
		int idx = newSql.lastIndexOf("order");
		return sb.substring(0, idx).toString();
	}

首先,从BoundSql中分离出SQL,通过分页参数判断是否需要去掉order by语句,如果需要,则删除,这样是为了避免影响执行性能。然后通过StatementHandler的参数获取数据库连接资源,使用JDBC获取总数,但是难点在于如何给总数SQL预编译参数。原本SQL设置参数是通过ParameterHandler进行处理的,因此首先使用总数的SQL创建BoundSql和ParameterHandler对象,然后就可以通过总数的ParameterHandler对象设置总数SQL的参数,这样就能够执行总数SQL,求出这条SQL所能查询出的总数,然后将其返回。
求出了总数,就可以通过每页条数的限制求出总页数,并与总数一起回填到分页总数中,然后判断当前页面是否合法,这需要判断一个分页参数的属性——checkFlag,当true时才回去判断:

/**
	 * 检查当前页码的有效性
	 *
	 * @param checkFlag
	 *            检测标志
	 * @param pageNum
	 *            当前页码
	 * @param pageTotal
	 *            最大页码
	 * @throws Throwable
	 */
	private void checkPage(Boolean checkFlag, Integer pageNum, Integer pageTotal) throws Throwable {
		if (checkFlag) {
			// 检查页码page是否合法
			if (pageNum > pageTotal) {
				throw new Exception("查询失败,查询页码【" + pageNum + "】大于总页数【" + pageTotal + "】!!");
			}
		}
	}

查询当前页就需要修改原有的SQL,并且加入SQL的分页参数,这样就容易查询到分页数据:

/**
	 * 预编译改写后的SQL,并设置分页参数
	 *
	 * @param invocation
	 *            入参
	 * @param metaStatementHandler
	 *            MetaObject绑定的StatementHandler
	 * @param boundSql
	 *            boundSql对象
	 * @param pageNum
	 *            当前页
	 * @param pageSize
	 *            最大页
	 * @throws IllegalAccessException
	 *             异常
	 * @throws InvocationTargetException
	 *             异常
	 */
	private Object preparedSQL(Invocation invocation, MetaObject metaStatementHandler, BoundSql boundSql, int pageNum,
			int pageSize) throws Exception {
		// 获取当前需要执行的SQL
		String sql = boundSql.getSql();
		String newSql = "select * from (" + sql + ") $_paging_table limit ?, ?";
		// 修改当前需要执行的SQL
		metaStatementHandler.setValue("delegate.boundSql.sql", newSql);
		// 执行编译,相当于StatementHandler执行了prepared()方法,这个时候,就剩下两个分页参数没有设置
		Object statementObj = invocation.proceed();
		// 设置两个分页参数
		this.preparePageDataParams((PreparedStatement) statementObj, pageNum, pageSize);
		return statementObj;
	}

	/**
	 * 使用PreparedStatement预编译两个分页参数,如果数据库的规则不一样,需要改写设置的参数规则
	 *
	 * @throws SQLException
	 * @throws NotSupportedException
	 *
	 */
	private void preparePageDataParams(PreparedStatement ps, int pageNum, int pageSize) throws Exception {
		// prepared()方法编译SQL,由于MyBatis上下文没有分页参数的信息,所以这里需要设置这两个参数
		// 获取需要设置的参数个数,由于参数是最后的两个,所以很容易得到其位置
		int idx = ps.getParameterMetaData().getParameterCount();
		// 最后两个是我们的分页参数
		ps.setInt(idx - 1, (pageNum - 1) * pageSize);// 开始行
		ps.setInt(idx, pageSize); // 限制条数
	}

取出原有的SQL,改写为分页SQL。分页SQL存在两个参数:偏移量和限制条数。它们实际上调用了StatementHandler的prepare方法,这个方法的作用就是预编译SQL的参数。由于加入了两个新参数,所以调用了这个方法,并没有把它们预编译在内,因此这里的preparePageDataParams方法就是为了完成这个任务,在执行完preparedSQL后,就完成了查询总条数、总页数和分页SQL的编译,最后返回已编译好的PreparedStatement对象,这样就完成了分页插件的逻辑。
MyBatis配置如下:

<plugins>
		<plugin interceptor="com.ssm.chapter8.plugin.PagePlugin">
			<property name="default.page" value="1" />
			<property name="default.pageSize" value="20" />
			<property name="default.useFlag" value="true" />
			<property name="default.checkFlag" value="false" />
			<property name="default.cleanOrderBy" value="false" />
		</plugin>
	</plugins>

映射器的配置如下:

<select id="findRole" parameterType="string" resultType="com.ssm.chapter8.pojo.Role">
        select id, role_name as roleName, note from t_role
        <where>
            <if test = "roleName != null">
                role_name like concat('%', #{roleName}, '%')
            </if>
        </where>
</select>

总结

注意以下6点:

  • 插件是MyBatis最强大的组件。也是最危险的组件,能不用尽量少用它。
  • 插件生成的是层层代理对象的责任链模式,通过反射方法运行,性能不高,所以减少插件就能减少代理,提高系统性能
  • 插件的基础是SqlSession下的四大对象和它们的写作,需要对四大对象的方法有较深入的理解,才能明确拦截什么对象、拦截什么方法及其参数,从而确定插件的签名
  • 在插件中往往需要读取和修改MyBatis映射器中的对象属性
  • 插件的代码编写要考虑全面,特别是多个插件层层代理时,注意其执行顺序及保证前后逻辑的正确性
  • 大部分插件都应该尽量少改动MyBatis的底层内容,才能减少错误的发生。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章