mybatis的拦截器实现分页(动态代理)
拦截sql语句来实现分页
1.拦截什么样的对象(以page作为参数传入;page对象)
2.拦截对象什么行为
3.什么时候拦截 (在prepareStatement的时候拦截)
代人买票
mybatis获取statement其实是在statementHandler中,这是一个处理接口,有个prepare方法,返回Statement,这个方法是在BaseStatementHandler中实现的,statement是在instantiateStatement这个方法中获取的,这个方法是一个抽象方法,看它的PrepareStatementHandler实现,在这里边看到了connection.prepareStatement(sql,PreparedStatement.),也就是和JDBC类似的代码了,这就是分页拦截器要拦截的位置了。如何实现拦截呢?mybatis提供了相应的注解:
@Intercepts({@Signature(type=StatementHandler.class,method="prepare",args={Connection.class})})
//成立代购公司 ---- implements Interceptor
public class PageInterceptor implements Interceptor {
①type指向要连接的接口class,这里指向StatementHandler.class, ②Method指向要拦截的方法,这里是prepare
③args[]拦截的方法的参数类型,这里是Connection.class
这样就准确描述了要拦截StatementHandler接口下的prepare方法。目标确定,接下来就可以做手脚了,在PrepareStatementHandler拿到sql语句之前将这个sql语句改装成我们的分页sql,然后在塞回去,让程序继续执行,这样就成功了。
注意:过早过迟的拦截都不合适。所以在PreparedStatement pstmt=conn.prepareStatement(sql.toString());之前拦截即可(把SQL语句处理再放进去提交)
注册公司 ------ plugin
申报资产 ----- property
<plugins>
<plugin interceptor="com.imooc.interceptor.PageInterceptor">
<property name="test" value="abc"/>
</plugin>
</plugins>
使用资产
public class PageInterceptor implements Interceptor {
private String test;
// 执行顺序 1
@Override
public void setProperties(Properties properties) {
this.test = properties.getProperty("test");
}
识别哪些是去买票的人
(并不一定就是需要找代购的人,在正式代购的时候会更精确的定位客户群体)
// 执行顺序 2
@Override
public Object plugin(Object target) {
System.out.println(this.test);
return Plugin.wrap(target, this);
}
plugin(Object target)方法参数就是被拦截的对象target,返回的就是满足条件的代理类,Plugin.wrap(target,this):this也就是自定义拦截器实例,通过获取注解得到要拦截的类型,比较target的类型与this获取的要拦截的类型是不是一致,如果满足条件就获取代理对象,并执行intercept方法,没有获取代理对象的将直接返回,不会经过intercept方法。
开始代购
// 执行顺序 3
@Override
public Object intercept(Invocation invocation) throws Throwable {
//拦截器的参数(Invocation)中保存了拦截器所拦截的所有对象,根据方法签名,这里仅仅只是对statementHandler中的关键信息进行处理,原理就是使用分页的sql替换拦截到的原始sql,拦截对象类型是StatementHandler,由方法签名决定的
StatementHandler statementHandler = (StatementHandler)invocation.getTarget();
//StatementHandler将对配置文件中的sql语句进行处理(sql语句在MappedStatement中),但是在StatementHandler中,所有的对象属性均为受保护的以及私有的,首先想到的是通过反射读写信息,幸好Mybatis已经有一个类MetaObject,有个方法 MetaObject.forObject(statementHandler,__,__)可以对注解的拦截方法签名所对应的对象进行包装,这样我们得到的是被包装的statementHandler
MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY);
MappedStatement mappedStatement = (MappedStatement)metaObject.getValue("delegate.mappedStatement");
// 配置文件中SQL语句的ID
String id = mappedStatement.getId();
//定位更精准的用户群体(嫌麻烦不愿排队的)
if(id.matches(".+ByPage$")) {
BoundSql boundSql = statementHandler.getBoundSql();
通过BoundSql获得原始的sql语句之后,再次使用的是BoundSql的getParameterObject()来获取配置文件中的参数,因为得到的参数是一个map,调用对象的get方法得到Page对象,得到page对象之后就可以拼接分页sql了。metaObject.setValue(“delegate.boundSql.sql”,pageSql)修改原本不可以修改的值,修改原来的属性值为新的sql。
mybatis通过Invocation这个参数的proceed()方法交回主权,这个方法的源码 return method.invoke(target,args)
// 原始的SQL语句
String sql = boundSql.getSql();
// 查询总条数的SQL语句
//这里的问题在于sql是否能执行以及如何执行,需要connection对象,而此对象就是方法签名的参数,可以通过invocation.getArgs()[0]获得,然后通过connection.prepareStatement(countSql)将拼接好的sql语句进行预编译,并执行,就可以获得结果,由于此结果是统计总数的,只有一条记录,将此记录转换为int类型,并赋值给page对象。
String countSql = "select count(*) from (" + sql + ")a";
Connection connection = (Connection)invocation.getArgs()[0];
PreparedStatement countStatement = connection.prepareStatement(countSql);
ParameterHandler parameterHandler = (ParameterHandler)metaObject.getValue("delegate.parameterHandler");
parameterHandler.setParameters(countStatement);
ResultSet rs = countStatement.executeQuery();
Map<?,?> parameter = (Map<?,?>)boundSql.getParameterObject();
//获取购票信息
Page page = (Page)parameter.get("page");
if(rs.next()) {
page.setTotalNumber(rs.getInt(1));
}
// 改造后带分页查询的SQL语句
//代购公司 开始购票
String pageSql = sql + " limit " + page.getDbIndex() + "," + page.getDbNumber();
metaObject.setValue("delegate.boundSql.sql", pageSql);
}
return invocation.proceed();
}
代购过程总结:
1.RoutingStatementHandler
2.通过RoutingStatementHandler对象的属性delegate找到statement实现类BaseStatementHandler
3.通过BaseStatementHandler类的反射得到对象的MappedStatement对象
4.通过MappedStatement的属性getID得到配置文件sql语句的ID
5.通过BaseStatementHandler属性的到原始sql语句
6.拼接分页sql(
1.需要查询总数的sql
2.通过拦截Connection对象得到PrepareStatement对象
3.得到对应的参数
4.把参数设到prepareStatement对象里的?(该?号在配置文件以#{}形式存在,mybatis会把它转为?号)
5.执行sql语句
6.得到总数
)
7.把属性值为新的sql