文章目录
- 1. 如何自定义插件
- 1.1 创建接口Interceptor的实现类
- 1.2 配置拦截器
- 1.3 运行程序
- 2. 插件原理
- 2.1 解析过程
- 2.2 创建代理对象
- 2.2.1 Executor
- 2.2.2 StatementHandler
- 2.2. 3ParameterHandler
- 2.2.4 ResultSetHandler
- 2.3 执行流程
- 2.4 多拦截器的执行顺序
- 3. PageHelper
- 3.1 配置和代码
- 3.2 原理解析
- 4. 拦截器应用场景
1. 如何自定义插件
1.1 创建接口Interceptor的实现类
/*** @author Clinton Begin*/
public interface Interceptor {// 执行拦截逻辑的方法Object intercept(Invocation invocation) throws Throwable;// 决定是否触发 intercept()方法default Object plugin(Object target) {return Plugin.wrap(target, this);}// 根据配置 初始化 Intercept 对象default void setProperties(Properties properties) {// NOP}}
mybatis运行拦截的内容包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
定义一个实现类。
/*** MyBatis中的自定义的拦截器** @Signature 表示一个方法签名,唯一确定一个方法*/
@Intercepts({@Signature(type = Executor.class, // 拦截类型method = "query", // 拦截方法// args 中指定 被拦截方法的 参数列表args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),@Signature(type = Executor.class,method = "close",args = {boolean.class})})
public class MyInterceptor implements Interceptor {private String interceptorName;public String getInterceptorName() {return interceptorName;}/*** 执行拦截的方法*/@Overridepublic Object intercept(Invocation invocation) throws Throwable {System.out.println("------MyInterceptor before---------");Object proceed = invocation.proceed();System.out.println("------MyInterceptor after---------");return proceed;}@Overridepublic Object plugin(Object target) {return Interceptor.super.plugin(target);}@Overridepublic void setProperties(Properties properties) {System.out.println("setProperties : " + properties.getProperty("interceptorName"));this.interceptorName = properties.getProperty("interceptorName");}
}
1.2 配置拦截器
<plugins><plugin interceptor="com.boge.interceptor.MyInterceptor"><property name="interceptorName" value="myInterceptor"/></plugin></plugins>
1.3 运行程序
@Testpublic void test2() throws Exception{// 1.获取配置文件InputStream in = Resources.getResourceAsStream("mybatis-config.xml");// 2.加载解析配置文件并获取SqlSessionFactory对象SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);// 3.根据SqlSessionFactory对象获取SqlSession对象SqlSession sqlSession = factory.openSession();// 4.通过SqlSession中提供的 API方法来操作数据库UserMapper mapper = sqlSession.getMapper(UserMapper.class);Integer param = 1;User user = mapper.selectUserById(param);System.out.println(user);
}
拦截的query方法和close方法的源码位置在如下:
2. 插件原理
2.1 解析过程
解析全局配置文件过程中,查看XMLConfigBuilder类的方法parseConfiguration。
private void pluginElement(XNode parent) throws Exception {if (parent != null) {for (XNode child : parent.getChildren()) {// 获取<plugin> 节点的 interceptor 属性的值String interceptor = child.getStringAttribute("interceptor");// 获取<plugin> 下的所有的properties子节点Properties properties = child.getChildrenAsProperties();// 获取 Interceptor 对象Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();// 设置 interceptor的 属性interceptorInstance.setProperties(properties);// Configuration中记录 Interceptorconfiguration.addInterceptor(interceptorInstance);}}}
该方法主要创建Interceptor 对象,并设置属性,最终放在configuration对象的InterceptorChain里面
来看InterceptorChain的源码。
/*** InterceptorChain 记录所有的拦截器* @author Clinton Begin*/
public class InterceptorChain {// 保存所有的 Interceptor 也就我所有的插件是保存在 Interceptors 这个List集合中的private final List<Interceptor> interceptors = new ArrayList<>();// 现在我们定义的有一个 Interceptor MyInterceptorpublic 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);}}
可以看到拦截器放在这个list变量interceptors 。
2.2 创建代理对象
2.1步骤创建了拦截器,并且保存在InterceptorChain,**那拦截器如何与目标对象关联?**拦截器拦截对象包括:Executor,ParameterHandler,ResultSetHandler,StatementHandler. 这些对象创建的时候需要注意什么?
2.2.1 Executor
在创建SqlSession的过程中,会创建执行器Executor。可以看到Executor植入插件
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {// 获取执行器的类型executorType = executorType == null ? defaultExecutorType : executorType;executorType = executorType == null ? ExecutorType.SIMPLE : executorType;Executor executor;// 根据对应的类型创建执行器if (ExecutorType.BATCH == executorType) {executor = new BatchExecutor(this, transaction);} else if (ExecutorType.REUSE == executorType) { // 针对 Statement 对象做缓存executor = new ReuseExecutor(this, transaction);} else {// 默认 SimpleExecutor 每一次只是SQL操作都创建一个新的Statement对象executor = new SimpleExecutor(this, transaction);}// 二级缓存开关,settings 中的 cacheEnabled 默认是 true// 映射文件中 <cache> 标签 --> 创建 Cache对象// settings 中的 cacheEnabled = true 真正的对 Executor 做了缓存的增强if (cacheEnabled) {// 穿衣服的事情 --> 装饰器模式executor = new CachingExecutor(executor);}// 植入插件的逻辑,至此,四大对象已经全部拦截完毕executor = (Executor) interceptorChain.pluginAll(executor);return executor;}
进入pluginAll方法:
// 现在我们定义的有一个 Interceptor MyInterceptorpublic Object pluginAll(Object target) {for (Interceptor interceptor : interceptors) { // 获取拦截器链中的所有拦截器target = interceptor.plugin(target); // 创建对应的拦截器的代理对象}return target;}
再进入plugin方法
再查看Plugin工具类的实现 wrap方法。
/*** 创建目标对象的代理对象* 目标对象 Executor ParameterHandler ResultSetHandler StatementHandler* @param target 目标对象* @param interceptor 拦截器* @return*/public static Object wrap(Object target, Interceptor interceptor) {// 获取用户自定义 Interceptor中@Signature注解的信息// getSignatureMap 负责处理@Signature 注解 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;}
getSignatureMap方法
再来看Plugin的源码。
/*** @author Clinton Begin*/
public class Plugin implements InvocationHandler {private final Object target; // 目标对象private final Interceptor interceptor; // 拦截器private final Map<Class<?>, Set<Method>> signatureMap; // 记录 @Signature 注解的信息private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {this.target = target;this.interceptor = interceptor;this.signatureMap = signatureMap;}/*** 创建目标对象的代理对象* 目标对象 Executor ParameterHandler ResultSetHandler StatementHandler* @param target 目标对象* @param interceptor 拦截器* @return*/public static Object wrap(Object target, Interceptor interceptor) {// 获取用户自定义 Interceptor中@Signature注解的信息// getSignatureMap 负责处理@Signature 注解 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;}/*** 代理对象方法被调用时执行的代码* @param proxy* @param method* @param args* @return* @throws Throwable*/@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {// 获取当前方法所在类或接口中,可被当前Interceptor拦截的方法 Executor querySet<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);}}/*** 获取拦截器中的 @Intercepts 注解中的相关内容* @param interceptor* @return*/private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {// 获取 @Intercepts 注解Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);// issue #251if (interceptsAnnotation == null) {throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());}// 获取 @Signature 注解中的内Signature[] sigs = interceptsAnnotation.value();Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();for (Signature sig : sigs) {Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());try {Method method = sig.type().getMethod(sig.method(), sig.args());methods.add(method);} catch (NoSuchMethodException e) {throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);}}return signatureMap;}private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {Set<Class<?>> interfaces = new HashSet<>();while (type != null) {for (Class<?> c : type.getInterfaces()) {// 判断 目标对象的 接口类型是否在 @Signature 注解中声明的有if (signatureMap.containsKey(c)) {interfaces.add(c);}}// 继续获取父类type = type.getSuperclass();}return interfaces.toArray(new Class<?>[interfaces.size()]);}}
2.2.2 StatementHandler
2.2. 3ParameterHandler
2.2.4 ResultSetHandler
2.3 执行流程
以Executor的query方法为例,实际执行的是代理对象。
然后会执行Plugin的invoke方法。
然后进入interceptor.intercept,进入自定义拦截器
2.4 多拦截器的执行顺序
总结:
对象 | 作用 |
---|---|
Interceptor | 自定义插件需要实现接口,实现4个方法 |
InterceptChain | 配置的插件解析后会保存在Configuration的InterceptChain中 |
Plugin | 触发管理类,还可以用来创建代理对象 |
Invocation | 对被代理类进行包装,可以调用proceed()调用到被拦截的方法 |
3. PageHelper
3.1 配置和代码
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper</artifactId><version>4.1.6</version>
</dependency>
<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageHelper"><property name="dialect" value="mysql" /><!-- 该参数默认为false --><!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 --><!-- 和startPage中的pageNum效果一样 --><property name="offsetAsPageNum" value="true" /><!-- 该参数默认为false --><!-- 设置为true时,使用RowBounds分页会进行count查询 --><property name="rowBoundsWithCount" value="true" /><!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 --><!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型) --><property name="pageSizeZero" value="true" /><!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 --><!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 --><!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 --><property name="reasonable" value="false" /><!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 --><!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 --><!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值 --><!-- 不理解该含义的前提下,不要随便复制该配置 --><property name="params" value="pageNum=start;pageSize=limit;" /><!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page --><property name="returnPageInfo" value="check" />
</plugin>
代码:
@Testpublic void test5() throws Exception{// 1.获取配置文件InputStream in = Resources.getResourceAsStream("mybatis-config.xml");// 2.加载解析配置文件并获取SqlSessionFactory对象SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);// 3.根据SqlSessionFactory对象获取SqlSession对象SqlSession sqlSession = factory.openSession();// 4.通过SqlSession中提供的 API方法来操作数据库UserMapper mapper = sqlSession.getMapper(UserMapper.class);// 分页PageHelper.startPage(1, 10);List<User> users = mapper.selectUserList();System.out.println(users);// 5.关闭会话sqlSession.close();}
执行结果:
3.2 原理解析
PageHelper实现了Interceptor接口,拦截接口Executor的query方法。也就是说SqlSession的executor创建之后,经过类Plugin的wrap方法处理之后,变成代理对象。
接下里的 PageHelper.startPage(1, 10)做了什么? Let’s get into it.
F7跟踪到这里:
这里主要设置页码和分页大小。SqlUtil类来面有个TreadLocal变量LOCAL_PAGE,绑定当前线程的page对象。
接着往下走:
接着到PageHelper的intercept(Invocation invocation)方法:
最终在doProcessPage方法里面实现分页查询。
/*** Mybatis拦截器方法** @param invocation 拦截器入参* @return 返回执行结果* @throws Throwable 抛出异常*/private Page doProcessPage(Invocation invocation, Page page, Object[] args) throws Throwable {//保存RowBounds状态RowBounds rowBounds = (RowBounds) args[2];//获取原始的msMappedStatement ms = (MappedStatement) args[0];//判断并处理为PageSqlSourceif (!isPageSqlSource(ms)) {processMappedStatement(ms);}//设置当前的parser,后面每次使用前都会set,ThreadLocal的值不会产生不良影响((PageSqlSource)ms.getSqlSource()).setParser(parser);try {//忽略RowBounds-否则会进行Mybatis自带的内存分页args[2] = RowBounds.DEFAULT;//如果只进行排序 或 pageSizeZero的判断if (isQueryOnly(page)) {return doQueryOnly(page, invocation);}//简单的通过total的值来判断是否进行count查询if (page.isCount()) {page.setCountSignal(Boolean.TRUE);//替换MSargs[0] = msCountMap.get(ms.getId());//查询总数Object result = invocation.proceed();//还原msargs[0] = ms;//设置总数page.setTotal((Integer) ((List) result).get(0));if (page.getTotal() == 0) {return page;}} else {page.setTotal(-1l);}//pageSize>0的时候执行分页查询,pageSize<=0的时候不执行相当于可能只返回了一个countif (page.getPageSize() > 0 &&((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)|| rowBounds != RowBounds.DEFAULT)) {//将参数中的MappedStatement替换为新的qspage.setCountSignal(null);BoundSql boundSql = ms.getBoundSql(args[1]);args[1] = parser.setPageParameter(ms, args[1], boundSql, page);page.setCountSignal(Boolean.FALSE);//执行分页查询Object result = invocation.proceed();//得到处理结果page.addAll((List) result);}} finally {((PageSqlSource)ms.getSqlSource()).removeParser();}//返回结果return page;}
// todo 待仔细研究
4. 拦截器应用场景
作用 | 描述 | 实现方式 |
---|---|---|
水平分表 | 一张费用表按月度拆分为12张表。fee_202001-202012。当查询条件出现月度(tran_month)时,把select语句中的逻辑表名修改为对应的月份表。 | 对query update方法进行拦截在接口上添加注解,通过反射获取接口注解,根据注解上配置的参数进行分表,修改原SQL,例如id取模,按月分表 |
数据脱敏 | 手机号和身份证在数据库完整存储。但是返回给用户,屏蔽手机号的中间四位。屏蔽身份证号中的出生日期。 | query——对结果集脱敏 |
菜单权限控制 | 不同的用户登录,查询菜单权限表时获得不同的结果,在前端展示不同的菜单 | 对query方法进行拦截在方法上添加注解,根据权限配置,以及用户登录信息,在SQL上加上权限过滤条件 |
黑白名单 | 有些SQL语句在生产环境中是不允许执行的,比如like %% | 对Executor的update和query方法进行拦截,将拦截的SQL语句和黑白名单进行比较,控制SQL语句的执行 |
全局唯一ID | 在高并发的环境下传统的生成ID的方式不太适用,这时我们就需要考虑其他方式了 | 创建插件拦截Executor的insert方法,通过UUID或者雪花算法来生成ID,并修改SQL中的插入信息 |