说明:把表名直接定义成 order 是要报错的
1. 逻辑分页和物理分页的区别
逻辑分页
又称内存分页 :mapper 接口传入 RowBounds,SQL 没有任何分页标识,取表总满足条件的数据, 到内存中去分页,和 ArrayList的 ,subList(start,end)很像,只适合数据量小的场景,如果数据量大的话要用物理分页,所以他不能算是真正的分页,只能算逻辑上的分页
// offset,从第几行开始查询
int start = 10;
// limit,查询多少条
int pageSize = 5;
RowBounds rb = new RowBounds(start, pageSize);
List<Student> list = mapper.selectStudentList(rb);
for(Student s :list){
System.out.println(s);
}
xml 里的sql
select * from student
RowBouds 的底层其实是对ResultSet 的处理。它会舍弃掉前面offset 条数据,然后再取剩下的数据的limit 条(所以RowBounds的实现逻辑是 先取全量数据,然后在内存中截取分页的数据)
DefaultResultSetHandler.java
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap,
ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws
SQLException {
DefaultResultContext<Object> resultContext = new DefaultResultContext();
ResultSet resultSet = rsw.getResultSet();
this.skipRows(resultSet, rowBounds);
while(this.shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() &&
resultSet.next()) {
ResultMap discriminatedResultMap = this.resolveDiscriminatedResultMap(resultSet,
resultMap, (String)null);
Object rowValue = this.getRowValue(rsw, discriminatedResultMap, (String)null);
this.storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}
}
物理分页(limit)
具体可以参考如下文章:
https://blog.csdn.net/Leon_Jinhai_Sun/article/details/110732995
mybatis 中文官网:
http://www.mybatis.cn/
http://www.mybatis.cn/archives/789.html
1.看代码部分:
为什么只能拦截 四大天王?Configuration 类里创建 四大天王的时候,把拦截器链里的内容给四大天王(rownum 599,Wrap.plugin(target,this)
2. 多个自定义拦截器的执行顺序如何处理的?
1.
DefaultSQLSession 进行增删改查?
Executor
StatementHandler 封装对数据库的增删改查
ParameterHandler 设置值的
ResultSetHandler
MappedProxy 找到映射器
1.插件的原理与自定义插件的编写
问题:
有哪些对象可以被代理,对象里的哪些方法可以被代理?四大天王:
Executor 顶层的接口(update,query,closed: 判断一个会话是否结束......),注意用的update 代替insert
ParameterHandler(getParameterObject,setParameters):SQL 语句组装,拼接参数
StatementHandler(prepare,parameterize,batch,update,query)
ResultSetHandler(handlerResultSets.,handlerOutputParameters)
多个插件如何注册?(这个实际开发中遇到过,多个的话会报错)
多个插件的话,代理能不能被代理?可以
假如 对Executor 对象的某个方法进行代理,定义了三个拦截器 a ,b ,c (链接器链里存的是 a,b,c)
调用目标方法时候,执行顺序是怎么样的? 先执行c的,然后是b,然后是a,最后是 Executor 对象的被代理方法
(c(b(a(Executor)a)b)c)
拦截器链如何形成,如何做到层层拦截的?
怎么创建代理?Intercepter 接口 的plugin 方法(Wrap.plugin(target,this))代码待看??
什么时候创建代理对象,启动,创建会话,执行SQL?
被代理后,调用的是什么方法?怎么调用到被代理对象的方法?还是调用 statementHandler.query(or invocation.proceed) ,因为已经改写了要执行的 SQL了
Intercepter 接口的 intercept 方法
①插件原理
②自定义插件编写(1.要打印会被执行的SQL,除了mybatisplugin还可以自己写个简单的插件,自己用)
说明一个场景:现在基本用appo 配置,有的人直接将线上的applo 配置copy到本地,修改 日志级别为debug,但是发现 项目启动的时候
特别慢,然后考虑用插件),sql 的执行时间 etc ;2.返回数据的脱敏
常用案例:
水平分表(因为 interceptor的intercept(Invocation invocation )
可以通过invocation 获得 方法上面的注解,信息,,哪个表etc)
invocation.getArgs();invocation.getTarget()
可以参考链接 :https://www.cnblogs.com/mmzs/p/11174551.html
权限控制:(eg: 拼接SQL,shiro和security 是直接从表里拿出所有用户权限数据,然后根据注解值去判断是否在用户权限列表中;在mybatis 插件来实现,直接过滤此用户有没有这个权限,有就返回数据,没有就返回null )
修改 mybatis 原来的内存分页(RowBands) 变为 物理分页(limit)
特殊情况下:打印SQL,统计SQL执行时间(这个开发阶段,调试用的多;查看慢查询(数据库级别的很浪费时间的))
对从库里查出的数据脱敏(手机号,电话号码,身份证等,当然这个也可在SQL中实现)
拦截器相关的接口:
/**
* @author Clinton Begin
*/
public interface Interceptor {
//在此方法中实现自己需要的功能,最后执行invocation.proceed()方法,实际就是调method.invoke(target, args)方法,调用代理类
Object intercept(Invocation invocation) throws Throwable;
// target 可以是四个对象,也可以是代理四大对象的代理对象
Object plugin(Object target);
void setProperties(Properties properties);
}
拦截器链
/**
* @author Clinton Begin
*/
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
// 传入四大对象,用拦截器链里的每个拦截器去包装目标对象,生成代理对象,代理对象作为目标对象,再
// 被其他拦截器 去代理(所以说目标对象和代理对象都是可以被代理的)
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
// 这儿也包括自定义拦截器(因为实现了 Interceptor 接口)
// plugin 是 自定义拦截器的 plugin 方法,用来生成 代理对象的
target = interceptor.plugin(target);
}
// 最后返回代理对象
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
接下来看看哪些方法调用了 pluginAll 和 interceptor.plugin(target) 的逻辑
然后看下 interceptor.plugin(target)
plugin 是interceptor的方法
import java.util.Properties;
/**
* @author Clinton Begin
*/
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
选择一个实现类点进去:
看看 invokeHandler 的invoke方法怎么重写的?
如何知道哪个拦截器走完了,那个拦截器没有执行完呢?
Invocation
package org.apache.ibatis.plugin;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* @author Clinton Begin
*/
public class Invocation {
private Object target;
private Method method;
private Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
public Object getTarget() {
return target;
}
public Method getMethod() {
return method;
}
public Object[] getArgs() {
return args;
}
//mybatis的Interceptor最终还是调用的method.invoke方法
// eg 在自定义的拦截器要实现 Interceptor 接口,并重写 intercept方法 ,在intercept 写完要
// 增强的逻辑后,还要 调用 invocation.proceed() 方法去调用真正的目标方法
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
}
Plugin 类:
/**
* @author Clinton Begin
*/
//这个类是Mybatis拦截器的核心,大家可以看到该类继承了InvocationHandler
//又是JDK动态代理机制
public class Plugin implements InvocationHandler {
//目标对象
private Object target;
//拦截器
private Interceptor interceptor;
//记录需要被拦截的类与方法
private Map<Class<?>, Set<Method>> signatureMap;
private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
//一个静态方法,对一个目标对象进行包装,生成代理类。
public static Object wrap(Object target, Interceptor interceptor) {
//首先根据interceptor上面定义的注解 获取需要拦截的信息
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
//如果长度为>0 则返回代理类 否则不做处理
if (interfaces.length > 0) {
//创建JDK动态代理对象
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
//在执行Executor、ParameterHandler、ResultSetHandler和StatementHandler的实现类的方法时会调用这个方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//通过method参数定义的类 去signatureMap当中查询需要拦截的方法集合
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
//判断是否是需要拦截的方法,如果需要拦截的话就执行实现的Interceptor的intercept方法,执行完之后还是会执行method.invoke方法,不过是放到interceptor实现类中去实现了
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);
}
}
//根据拦截器接口(Interceptor)实现类上面的注解获取相关信息
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
//获取注解信息
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
// issue #251
//为空则抛出异常
if (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<Class<?>, Set<Method>>();
//循环注解信息
for (Signature sig : sigs) {
//根据Signature注解定义的type信息去signatureMap当中查询需要拦截方法的集合
Set<Method> methods = signatureMap.get(sig.type());
//第一次肯定为null 就创建一个并放入signatureMap
if (methods == null) {
methods = new HashSet<Method>();
signatureMap.put(sig.type(), methods);
}
try {
//找到sig.type当中定义的方法 并加入到集合
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;
}
//根据对象类型与signatureMap获取接口信息
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<Class<?>>();
//循环type类型的接口信息 如果该类型存在与signatureMap当中则加入到set当中去
while (type != null) {
for (Class<?> c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
//转换为数组返回
return interfaces.toArray(new Class<?>[interfaces.size()]);
}
}
给两个例子,通过mybatis 插件给insert方法和update 方法增加默认值(创建时间,创建人etc)
① 对 StatementHandler拦截
package com.example.demo.mybatisplugin;
import com.example.demo.entity.businessenum.CreatedInfoEnum;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import java.lang.reflect.Field;
import java.sql.PreparedStatement;
import java.sql.Statement;
import java.time.LocalDateTime;
import java.util.Properties;
/**
* @program: 插入修改信息补全创建人创建时间,修改人修改时间
* @description: StatementHandler 接口 update 方法的定义如下 (用方法定义完成 @Signature 里注解内容)
* int update(Statement statement) throws SQLException;
* @author: guoyiguang
* @create: 2021-02-27 16:18
**/
//@Intercepts({
// @Signature(type = StatementHandler.class, method = "update", args = {Statement.class})
//})
@Intercepts({
@Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}),
@Signature(type = StatementHandler.class, method = "update", args = {Statement.class})
})
//@Component
public class InsertOrUpdatePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("执行目标方法前 InsertOrUpdatePlugin 拦截的目标对象:"+invocation.getTarget()+"拦截的方法 "+ invocation.getMethod().toString() + " 拦截的参数: "+ invocation.getArgs().toString());
/**
***********************************************************执行目标方法前的逻辑***********************************
*/
if (invocation.getTarget() instanceof ParameterHandler) {
return invokeSetParameter(invocation);
}
/**
***********************************************************执行目标方法***********************************
*/
Object proceed = invocation.proceed();
/**
***********************************************************执行目标方法后的逻辑***********************************
*/
System.out.println("InsertOrUpdatePlugin 行目标方法后 逻辑 ");
return proceed;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target,this);
}
@Override
public void setProperties(Properties properties) {
}
private Object invokeSetParameter(Invocation invocation) throws Exception {
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0];
// 反射获取 参数对像
// 虽然上面拿到的 是 ParameterHandler,但是 getClass后 的是 ParameterHandler 的实现类 DefaultParameterHandler,内部有 parameterObject 属性,
// 通过反射获得某个 属性值(or 属性对象)(此处是Order 对象)
Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");
parameterField.setAccessible(true);
// 此处是Order对象
Object parameterObject = parameterField.get(parameterHandler);
// 改写参数
parameterObject = processParam(parameterObject);
// 改写的参数设置到原parameterHandler对象
parameterField.set(parameterHandler, parameterObject);
parameterHandler.setParameters(ps);
return null;
}
private Object processParam(Object parameterObject) throws IllegalAccessException {
// Order.class
Class<?> aClass = parameterObject.getClass();
Field[] declaredFields = aClass.getDeclaredFields();
for (Field field:declaredFields){
field.setAccessible(true);
if(field.getName().equals(CreatedInfoEnum.createdTime.toString()) || field.getName().equals(CreatedInfoEnum.updatedTime.toString())){
LocalDateTime now = LocalDateTime.now();
field.set(parameterObject,now);
}
// 从内存获取 用户信息
String user = "123456783456789";
if (field.getName().equals(CreatedInfoEnum.createdGUID.toString()) && null == field.get(parameterObject)){
field.set(parameterObject,user);
}
if (field.getName().equals(CreatedInfoEnum.updatedGUID.toString()) && null == field.get(parameterObject)){
field.set(parameterObject,user);
}
}
return parameterObject;
}
}
② 对 Executor 拦截:
package com.example.demo.mybatisplugin;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.sql.PreparedStatement;
import java.time.LocalDateTime;
import java.util.Properties;
/** 参考链接:https://www.cnblogs.com/qingshan-tang/p/13299701.html
* @program: 插入修改信息补全创建人创建时间,修改人修改时间
* @description: Executor 接口 update 方法的定义如下 (用方法定义完成 @Signature 里注解内容)
* iint update(MappedStatement ms, Object parameter) throws SQLException;
*
* 第一个参数 MappedStatement ; 第二个参数 Order对象
* @author: guoyiguang
* @create: 2021-02-27 16:18
**/
@Intercepts({
@Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class,Object.class})
})
@Component
public class InsertOrUpdatePlugin2 implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("执行目标方法前 InsertOrUpdatePlugin2 拦截的目标对象:"+invocation.getTarget()+"拦截的方法 "+ invocation.getMethod().toString() + " 拦截的参数: "+ invocation.getArgs().toString());
/**
***********************************************************执行目标方法前的逻辑***********************************
*/
fillField(invocation);
/**
***********************************************************执行目标方法***********************************
*/
Object proceed = invocation.proceed();
/**
***********************************************************执行目标方法后的逻辑***********************************
*/
System.out.println("InsertOrUpdatePlugin2 行目标方法后 逻辑 ");
return proceed;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target,this);
}
@Override
public void setProperties(Properties properties) {
}
private void fillField(Invocation invocation) throws IllegalAccessException {
Object[] args = invocation.getArgs();
SqlCommandType sqlCommandType = null;
for (int i = 0; i < args.length; i++) {
Object arg = args[i];
// 0 参数类型:org.apache.ibatis.mapping.MappedStatement
// 1 参数类型:com.example.demo.entity.Order
System.out.println(i+" 参数类型:" + arg.getClass().getName());
//arg.getClass().getName() 和 arg.getClass().toString() 这两个方法等效
//第一个参数 MappedStatement ,从他内部获取 是增加还是删除的操作
// arg.getClass().toString() ------> org.apache.ibatis.mapping.MappedStatement
if (arg instanceof MappedStatement) {
MappedStatement ms = (MappedStatement) arg;
sqlCommandType = ms.getSqlCommandType();
System.out.println("操作类型:" + sqlCommandType);
//如果是“增加”或“更新”操作,则继续进行默认操作信息赋值。否则,则退出
if (sqlCommandType == SqlCommandType.INSERT || sqlCommandType == SqlCommandType.UPDATE) {
// continue:不再执行循环体中continue语句之后的代码,直接进行下一次循环
// 此处是 进入 第二个参数循环
continue;
} else {
// break:直接跳出当前循环体(while、for、do while)或程序块(switch)
break;
}
}
// 第二个参数才走这儿
if (sqlCommandType == SqlCommandType.INSERT) {
for (Field f : arg.getClass().getDeclaredFields()
) {
f.setAccessible(true);
switch (f.getName()) {
case "createdGUID":
f.set(arg,"111");
break;
case "createdTime":
f.set(arg, LocalDateTime.now());
break;
case "updatedGUID":
f.set(arg, "111");
break;
case "updatedTime":
f.set(arg, LocalDateTime.now());
break;
case "delFlag":
f.set(arg, "0");
break;
}
}
} else if (sqlCommandType == SqlCommandType.UPDATE) {
for (Field f : arg.getClass().getDeclaredFields()
) {
f.setAccessible(true);
switch (f.getName()) {
case "updatedGUID":
f.set(arg, "111");
break;
case "updatedTime":
f.set(arg, LocalDateTime.now());
break;
}
}
}
}
}
}
③记录sql的执行时间
package com.example.demo.mybatisplugin;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.springframework.stereotype.Component;
import java.sql.Statement;
import java.util.Properties;
/**
* @program: 拦截所以数据库sql执行时长超过1毫秒的方法,并记录sql
* @description: @Intercepts以及@Signature配置需要拦截的对象,其中type是需要拦截的对象Class,method是对象里面的方法,args是方法参数类型。
* @author: guoyiguang
*
* 遇到的问题: 之前也是 拦截的 StatementHandler 的prepare 方法,args: Statement prepare(Connection connection, Integer transactionTimeout)
* 但是发现 了 和分页插件一起使用,limit没有出现,于是考虑 拼接完sql之后,访问数据库之前的 query 方法
*
* query:
* 在 Statement 接口的定义:
* <E> List<E> query(Statement statement, ResultHandler resultHandler)
* 下图的 type 是 四大对象的一个,method 是四大对象接口里的 方法,args 是 方法对应的形参:
*
* @create: 2021-02-26 23:39
**/
@Intercepts({
@Signature(type = StatementHandler.class, method = "query", args = {Statement.class,ResultHandler.class})
})
@Component
public class RecordTimePlugin implements Interceptor {
private long time;
/**
* 拦截的 前逻辑 + 执行目标方法 invocation.proceed() + 执行完目标方法后的逻辑
*
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println(" RecordTimePlugin 拦截的目标对象:"+invocation.getTarget()+"拦截的方法 "+ invocation.getMethod().toString() + " 拦截的参数: "+ invocation.getArgs().toString());
/**
***************************************************************** 执行目标方法前的逻辑 **************************************************************************************
*
*/
//通过StatementHandler获取执行的sql
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
long start = System.currentTimeMillis();
/**
***************************************************************** 调用 invocation.proceed() 执行目标方法 **************************************************************************************
*
*/
// 底层调用 method.invoke 执行目标方法
Object proceed = invocation.proceed();
/**
***************************************************************** 执行完目标方法后的逻辑 **************************************************************************************
*
*/
// 执行完目标方法后的逻辑
long end = System.currentTimeMillis();
if ((end - start) > time) {
System.out.println("本次数据库操作是慢查询,sql是:" + sql);
}else{
System.out.println("本次数据库操作是查询,sql是:" + sql);
}
return proceed;
}
/**
* @Description: 传入目标对象或者一个代理对象的,返回新的代理对象
* 底层JDK的动态代理
*/
@Override
public Object plugin(Object target) {
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
}
return Plugin.wrap(target, this);
}
/**
* @Description: 获取设置的阈值等参数
*/
@Override
public void setProperties(Properties properties) {
this.time = Long.parseLong(properties.getProperty("time"));
}
}
mybatis插件的注册有两种方法:
① 直接在 拦截器上加 @Component 注解
② 新增一个配置类:用 @Configuration + @Bean
eg:
@Configuration
public class MyBatisConfig {
@Bean
public CustomePagePlugin customePagePlugin() {
CustomePagePlugin customePagePlugin = new CustomePagePlugin();
return customePagePlugin;
}
}
参考链接:
https://blog.csdn.net/chinabestchina/article/details/102559207