编写插件
编写拦截器类,以PageHelper为例
1)实现Interceptor接口
public class PageInterceptor implements Interceptor {
2)实现方法。intercept就是拦截方法,增强代码写里面。
3)在拦截器类上加上参数。注解签名注明拦截对象、拦截方法、拦截方法参数。
下面拦截Executor中的两个query方法。
@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}),
}
)
插件配置
在mybatis-config.xml注册插件,配置属性
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<property name="offsetAsPageNum" value="true"/>
<property name="rowBoundsWithCount" value="true"/>
<property name="pageSizeZero" value="true"/>
<property name="reasonable" value="true"/>
<property name="params" value="pageNum=start;pageSize=limit;"/>
<property name="supportMethodsArguments" value="true"/>
<property name="returnPageInfo" value="check"/>
plugin>
plugins>
解析注册插件
myBatis启动时扫描
XMLConfigBuilder类
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
解析时将所有插件存到Configuration的InterceptorChain中,它是list类型。
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
不修改代码怎么增强功能?
代理模式,myBatis插件实现原理
多个插件怎么拦截?
责任链模式,链路执行,层层拦截。
什么对象可以被拦截?
有哪些对象和方法可以被拦截?
https://mybatis.org/mybatis-3/zh/configuration.html#plugins
下面两张图:
Executor有可能被二级缓存装饰。
Executor会拦截CachingExecutor或BaseExecutor。
DefaultSqlSessionFactory.openSessionFromDataSource():
先创建基本类型,再二级缓存装饰,最后插件拦截。
所以拦截的是CachingExecutor。
代理模式,需要解决的问题:
1.代理类怎么创建?
2.什么时候创建?
3.调用流程什么样?
代理类什么时候创建?
Executor拦截代理类是openSession时创建
怎么创建?
遍历InterceptorChain,使用Interceptor实现类的plugin方法,对目标对象进行代理。
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
这个plugin方法是自己实现的,返回一个代理对象。
JDK动态代理,需要实现InvocationHandler接口触发管理类。
用Proxy.nexProxyInstance创建对象。
myBatis插件机制将这些类封装好了,提供了一个触发管理类Plugin,
实现了InvocationHandler。
创建代理对象的newProxyInstance方法也进行了封装,就是wrap。
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
被代理后的调用流程
先触发管理类Plugin的invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
如果被拦截方法不为空,进入Plugin的invoke方法,调用interceptor的intercept方法,到我们自己实现的拦截逻辑。
return interceptor.intercept(new Invocation(target, method, args));
new Invocation(target, method, args) 对象是对被拦截对象、方法、参数的封装。
被代理对象执行它的方法从Invocation对象拿。
总结:
配置顺序与执行顺序?
配置与执行顺序是相反的。
InterceptorChain从上往下添加,执行从最后开始。
总结
翻页
使用RowBounds翻页,在内存中筛选数据。
使用
public String getEmps(@RequestParam(value = "pn", defaultValue = "1") Integer pn, Model model) {
PageHelper.startPage(pn, 10);
List<Employee> emps = employeeService.getAll();
PageInfo page = new PageInfo(emps, 10);
//连续显示的页数是10页
//包装查出来的结果,只需要将pageInfo交给页面,封装了详细的分页信息
//包括查询出来的数据
model.addAttribute("pageInfo", page);
return "list";
}
原理
拦截器类PageInterceptor。
先判断是否需要count获得总数,默认true。
获得count后,判断是否需要分页,如果pageSize>0,就分页。
下面通过getPageSql方法生成新BoundSql:
getPageSql对不同数据库有不同实现
实际是添加了LIMIT语句,加上了起始与结束。
插件是如何获取页码和每页数量?
PageHelper.startPage方法,调用了PageMethod的setLocalPage方法,包装了一个Page对象,并且把对象放到ThreadLocal中。
AbstractHelperDialect中,Page对象中的翻页信息是通过getLocalPage()取出的:
调的就是PageHelper的getLocalPage,从ThreadLocal中获取到
每次查询(每个线程)都有线程私有Page对象,里面有页码和每页数量。
关键类
pom依赖
出了mybatis依赖,还需要mybais和spring整合包。
叫mybatis-spring。版本要对应。
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatis-springartifactId>
<version>${mybatis-spring.version}version>
dependency>
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatisartifactId>
<version>${mybatis.version}version>
dependency>
SqlSessionFactoryBean
applicationContext.xml配置这个类。
这个Bean会初始化SqlSessionFactory,用来创建SqlSession。
属性要指定mybatis-config.xml和Mapper映射器文件。
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="configLocation" value="classpath:mybatis-config.xml">property>
<property name="mapperLocations" value="classpath:mapper/*.xml">property>
<property name="dataSource" ref="dataSource"/>
bean>
MapperScannerConfigurer
applicationContext.xml配置扫描Mapper接口路径。
方法一:
<mybatis-spring:scan #base-package="com.gupaoedu.crud.dao"/>
方法二:
<bean id="mapperScanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.gupaoedu.crud.dao"/>
bean>
方法三:
Spring集成mybatis原理:
1)SqlSessionFactory在哪创建?
2)SqlSession在哪创建?
3)代理类在哪创建?
实现了三个接口:InitializingBean、FactoryBean、ApplicationListener
InitializingBean
实现AfterPropertiesSet方法,在bean属性值设置完后调用
调用了buildSqlSesssionFactory方法。
创建Configuration对象。
创建解析全局配置文件XMLConfigBuilder。
FactoryBean接口
让用户自定义实例化Bean逻辑。
获取SqlSessionFactoryBean,就会调用它的getObject方法。
getObject方法调用了afterPropertiesSet方法,做mybatis解析配置,返回DefaultSqlSessionFactory。
ApplicationListener
监听ContextRefreshedEven(上下文刷新实践),会在SPring容器加载完后执行。
检查ms是否加载完毕。
SqlSessionFactoryBean用到的Spring扩展点
DefaultSqlSession是线程不安全的。
mybatis-spring包,提供了线程安全的SqlSession包装类,SqlSessionTemplate。
可以在所有DAO层共享实例(默认单例)
SqlSessionTemplate,增删改查都是调用代理对象的方法。
代理对象在构造方法通过JDK动态代理创建:
怎么拿到一个SqlSessionTemplate?
提供抽象支持类SqlSessionDaoSupport,持有一个SqlSessionTemplate对象,提供getSqlSession方法。
在实现类得方法里,可以直接调用父类封装的selectOne方法,
最终会调用sqlSessionTemplate的selectOne方法。
MapperScannerConfigurer用来扫描Mapper接口的。
MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor接口。
BeanDefinitionRegistryPostProcessor是BeanFactoryPostProcessor子类,里面有一个postProcessBeanDefintionRegistry方法。
MapperScannerConfigurer重写了postProcessBeanDefintionRegistry方法。
创建了scanner对象,设置属性。
Spring如何把 mybatis集成进去?
1.提供SqlSession替代品SqlSessionTemplate,里面有一个实现InvocationHandler的内部SqlSessionInterceptor,本质是对SqlSession的代理.
2.提供获取SqlSessionTemplate的抽象类SqlSessionDaoSupport
3.扫描Mapper接口,注册到容器中的是MapperFactoryBean
4.把Mapper注入使用的时候,调用的是getObject方法
5.执行Mapper接口任意方法,会走到触发管理类MapperProxy,进去SQL处理流程
学到了?
1.为组件预留扩展接口
2.利用Spring扩展机制,把组件集成到mybatis中
设计模式总结:
参考资料:
1.咕泡学院·MyBatis插件原理与Spring集成·青山