目录
- maven依赖
- 配置部分
- 测试
假如我们想实现多租户,或者在某些SQL后面自动拼接查询条件。在开发过程中大部分场景可能都是一个查询写一个SQL去处理,我们如果想修改最终SQL可以通过修改各个mapper.xml中的SQL来处理。
但实际过程中我们可能穿插着ORM和SQL的混合使用,隐藏在代码中不容易被发现,还有假如项目中有很多很多的SQL我们不可能一一的去修改解决。
这个时候我们就需要通过mybatis拦截SQL并且最终修改SQL。
maven依赖
<dependency> | |
<groupId>org.mybatis</groupId> | |
<artifactId>mybatis-spring</artifactId> | |
<version>.0.4</version> | |
</dependency> | |
<dependency> | |
<groupId>org.springframework</groupId> | |
<artifactId>spring-core</artifactId> | |
<version>.2.4.RELEASE</version> | |
<scope>compile</scope> | |
</dependency> | |
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --> | |
<dependency> | |
<groupId>mysql</groupId> | |
<artifactId>mysql-connector-java</artifactId> | |
<version>.0.26</version> | |
</dependency> | |
<!-- 大家最好是使用jsqlparser去解析sql提供的功能很多 --> | |
<dependency> | |
<groupId>com.github.jsqlparser</groupId> | |
<artifactId>jsqlparser</artifactId> | |
<version>.2</version> | |
</dependency> | |
<dependency> | |
<groupId>org.mybatis</groupId> | |
<artifactId>mybatis-spring</artifactId> | |
<version>.0.4</version> | |
</dependency> | |
<dependency> | |
<groupId>com.baomidou</groupId> | |
<artifactId>mybatis-plus</artifactId> | |
<version>.3.3</version> | |
</dependency> | |
<!-- 核心包 --> | |
<dependency> | |
<groupId>org.mybatis</groupId> | |
<artifactId>mybatis</artifactId> | |
<version>.5.4</version> | |
</dependency> | |
<dependency> | |
<groupId>com.alibaba</groupId> | |
<artifactId>druid-spring-boot-starter</artifactId> | |
<version>.2.6</version> | |
</dependency> | |
<dependency> | |
<groupId>org.projectlombok</groupId> | |
<artifactId>lombok</artifactId> | |
</dependency> | |
<dependency> | |
<groupId>org.projectlombok</groupId> | |
<artifactId>lombok</artifactId> | |
</dependency> |
配置部分
这部分是传统mybatis的xml配置,如果是Springboot项目或者使用JavaConfig配置的请查看官方文档配置方式。
无非就是Springboot封装了mybatis-xxx-stater包将部分配置都转为了参数控制以及部分autoconfig,大同小异这里不做过多讨论。
<configuration> | |
<properties resource="db.properties"></properties> | |
<settings> | |
<!-- 打印查询语句 --> | |
<setting name="logImpl" value="STDOUT_LOGGING" /> | |
<!-- 控制全局缓存(二级缓存)--> | |
<setting name="cacheEnabled" value="false"/> | |
<!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false --> | |
<setting name="lazyLoadingEnabled" value="true"/> | |
<!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖--> | |
<setting name="aggressiveLazyLoading" value="false"/> | |
<!-- Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST --> | |
<!--<setting name="proxyFactory" value="CGLIB" />--> | |
<!-- STATEMENT级别的缓存,使一级缓存,只针对当前执行的这一statement有效 --> | |
<!-- <setting name="localCacheScope" value="STATEMENT"/>--> | |
<setting name="localCacheScope" value="SESSION"/> | |
</settings> | |
<!--<typeAliases>--> | |
<!-- <typeAlias alias="blog" type="com.allens.mybatis.model.model" />--> | |
<!--</typeAliases>--> | |
<!-- <typeHandlers> | |
<typeHandler handler="com.wuzz.type.MyTypeHandler"></typeHandler> | |
</typeHandlers>--> | |
<!-- 对象工厂 --> | |
<!-- <objectFactory type="com.wuzz.objectfactory.GPObjectFactory"> | |
<property name="wuzz" value=""/> | |
</objectFactory>--> | |
<!-- 配置拦截器,本文的重点 --> | |
<plugins> | |
<plugin interceptor="com.allens.mybatis.interceptor.SQLInterceptor"/> | |
<plugin interceptor="com.allens.mybatis.interceptor.SQLParamInterceptor"/> | |
</plugins> | |
<environments default="development"> | |
<environment id="development"> | |
<transactionManager type="JDBC"/><!-- 单独使用时配置成MANAGED没有事务 --> | |
<dataSource type="POOLED"> | |
<property name="driver" value="com.mysql.cj.jdbc.Driver"/> | |
<property name="url" value="jdbc:mysql://localhost:/test?useUnicode=true&characterEncoding=UTF-8"/> | |
<property name="username" value="root"/> | |
<property name="password" value=""/> | |
</dataSource> | |
</environment> | |
</environments> | |
<mappers> | |
<mapper resource="mappers/UserMapper.xml"/> | |
</mappers> | |
</configuration> |
整个拦截调用链路流程图:
① mybatis在这一层包装了StatementHandler返回代理对象,下一步调用prepare的时候会先调用增强拦截器。
- Configuration.newStatementHandler
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { | |
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); | |
// 对statementhandler进行代理 | |
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); | |
return statementHandler; | |
} |
- InterceptorChain.pluginAll
public Object pluginAll(Object target) { | |
for (Interceptor interceptor : interceptors) { | |
target = interceptor.plugin(target); | |
} | |
return target; | |
} |
- Interceptor.plugin
default Object plugin(Object target) { | |
return Plugin.wrap(target, this); | |
} |
包装成代理对象
- Plugin.wrap
public static Object wrap(Object target, Interceptor interceptor) { | |
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); | |
Class<?> type = target.getClass(); | |
// 获取target对象的所有接口类型 | |
Class<?>[] interfaces = getAllInterfaces(type, signatureMap); | |
if (interfaces.length >) { | |
// 学习过JDK动态代理的同学对这段代码肯定很熟,这里是生成一个代理对象 | |
return Proxy.newProxyInstance( | |
type.getClassLoader(), // 类加载器 | |
interfaces, // JDK动态代理必须要有接口 | |
new Plugin(target, interceptor, signatureMap)); | |
} | |
return target; | |
} |
如果大家觉得这篇文章写的还可以请关注我,我后续会出mybatis的源码解析。
调用代码
这部分代码负责调用mybatis,如果使用springboot这部分就是你的rest接口。
import com.allens.mybatis.model.User; | |
import org.apache.ibatis.io.Resources; | |
import org.apache.ibatis.session.SqlSession; | |
import org.apache.ibatis.session.SqlSessionFactory; | |
import org.apache.ibatis.session.SqlSessionFactoryBuilder; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Map; | |
public class TestMybatis { | |
public static void main(String[] args) throws IOException { | |
// 读取配置文件 | |
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml"); | |
// 通过SqlSessionFactoryBuilder创建SqlSessionFactory | |
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); | |
// 获取到SqlSession | |
SqlSession sqlSession = sqlSessionFactory.openSession(); | |
// 调用Mapper中的指定方法 com.wyh.mapper.UserMapper.queryAll是statementId | |
Map<String, Object> map = new HashMap<>(); | |
map.put("pageSize",); | |
map.put("desc", "desc"); | |
map.put("name", "Allens"); | |
List<User> userList = sqlSession.selectList("com.allens.mybatis.mappers.UserMapper.selectUsers", map); | |
System.out.println("++++++++++++++++++++++"); | |
userList.forEach(System.out::println); | |
} | |
} |
拦截器的代码实现
这使用了Druid的SQLParser进行解析SQL,如果不想使用druid可以使用sqlparser包进行sql解析。不管用什么样的工具把SQL修改掉就行了,形式不限。
如果想使用sqlparser进行解析sql可以看我的下一篇文章
JSqlparser 使用攻略(高效的SQL解析工具)
/** | |
* MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括: | |
* <p> | |
* Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) | |
* ParameterHandler (getParameterObject, setParameters) | |
* ResultSetHandler (handleResultSets, handleOutputParameters) | |
* StatementHandler (prepare, parameterize, batch, update, query) | |
* 这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。 | |
* <p> | |
* 通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可 | |
*/ | |
//@Intercepts({@Signature( | |
// type= Executor.class, | |
// method = "query", | |
// args = {StatementHandler.class, Object.class, RowBounds.class, ResultHandler.class})}) | |
type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) | ({ (|
public class SQLInterceptor implements Interceptor { | |
private Properties properties = new Properties(); | |
public List<String> getInsertPropertiesName(List<Map<String, Object>> properties) { | |
List<String> list = new ArrayList<>(); | |
properties.forEach(data -> data.keySet() | |
.stream() | |
.filter(e -> e.equals("columnName")) | |
.forEach(e -> list.add((String) data.get("columnName")))); | |
return list; | |
} | |
public void setInsertProperties(List<Map<String, Object>> properties, String columnName, Object value) { | |
properties.forEach(element -> element.forEach((k, v) -> { | |
if (k.equals("columnName") && element.get(k).equals(columnName)) { | |
element.put("columnValue", value); | |
element.put("columnName", columnName); | |
} | |
})); | |
} | |
public Object intercept(Invocation invocation) throws Throwable { | |
// implement pre processing if need | |
List<Map<String, Object>> insertProperties = new ArrayList<>(); | |
Map<String, Object> nameProperties = new HashMap<>(); | |
nameProperties.put("columnName", "name"); | |
nameProperties.put("columnValue", "baby"); | |
nameProperties.put("expr", "and"); | |
insertProperties.add(nameProperties); | |
Map<String, Object> namePropertiesOr = new HashMap<>(); | |
namePropertiesOr.put("columnName", "id"); | |
namePropertiesOr.put("columnValue", ""); | |
namePropertiesOr.put("expr", "or"); | |
insertProperties.add(namePropertiesOr); | |
// BoundSql boundSql = ((MappedStatement)invocation.getArgs()[]).getBoundSql(invocation.getArgs()[1]); | |
StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); | |
// 获取传入的参数 | |
Object parameterMappings = statementHandler.getBoundSql().getParameterObject(); | |
if (parameterMappings instanceof Map) { | |
Map parameterMappingsConvert = (Map) parameterMappings; | |
getInsertPropertiesName(insertProperties).forEach(e -> { | |
Object value = parameterMappingsConvert.get(e); | |
if (value != null) { // 如果传参值不为空就覆盖配置值 | |
setInsertProperties(insertProperties, e, value); | |
} | |
}); | |
} | |
MetaObject metaObject = MetaObject | |
.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, | |
new DefaultReflectorFactory()); | |
BoundSql boundSql = statementHandler.getBoundSql(); | |
SQLStatementParser sqlParser = SQLParserUtils.createSQLStatementParser(boundSql.getSql(), DbType.MYSQL.getValue()); | |
SQLStatement stmt = sqlParser.parseStatementList().get(); | |
System.out.println("Origin SQL is:" + boundSql.getSql()); | |
System.out.println("++++++++++++++++++++++"); | |
if (stmt instanceof SQLSelectStatement) { | |
// ((SQLSelectStatement) stmt).addWhere(sqlExpr); | |
// convert conditions to 'and' statement | |
StringBuilder constraintsBuffer = new StringBuilder(); | |
boolean first = true; | |
for (Map<String, Object> data : insertProperties) { | |
if (String.valueOf(data.get("expr")).equalsIgnoreCase("and")) { | |
if (!first) { | |
constraintsBuffer.append(" AND "); | |
} | |
constraintsBuffer.append(String.format(" %s = '%s' ", data.get("columnName"), data.get("columnValue"))); | |
} else if (String.valueOf(data.get("expr")).equalsIgnoreCase("or")) { | |
if (!first) { | |
constraintsBuffer.append(" OR "); | |
} | |
constraintsBuffer.append(String.format(" %s = '%s' ", data.get("columnName"), data.get("columnValue"))); | |
} | |
first = false; | |
} | |
SQLExprParser constraintsParser = SQLParserUtils.createExprParser(constraintsBuffer.toString(), JdbcUtils.MYSQL); | |
SQLExpr constraintsExpr = constraintsParser.expr(); | |
SQLSelectStatement selectStmt = (SQLSelectStatement) stmt; | |
// 拿到SQLSelect 通过在这里打断点看对象我们可以看出这是一个树的结构 | |
SQLSelect sqlselect = selectStmt.getSelect(); | |
SQLSelectQueryBlock query = (SQLSelectQueryBlock) sqlselect.getQuery(); | |
SQLExpr whereExpr = query.getWhere(); | |
// 修改where表达式 | |
if (whereExpr == null) { | |
query.setWhere(constraintsExpr); | |
} else { | |
SQLBinaryOpExpr newWhereExpr = new SQLBinaryOpExpr( | |
whereExpr, SQLBinaryOperator.BooleanAnd, constraintsExpr); | |
query.setWhere(newWhereExpr); | |
} | |
sqlselect.setQuery(query); | |
String sql = sqlselect.toString(); | |
//通过反射修改sql语句 | |
Field field = boundSql.getClass().getDeclaredField("sql"); | |
field.setAccessible(true); | |
field.set(boundSql, sql); | |
System.out.println("modify sql is:" + sql); | |
} | |
// implement post processing if need | |
return invocation.proceed(); | |
} | |
public Object plugin(Object target) { | |
return Interceptor.super.plugin(target); | |
} | |
public void setProperties(Properties properties) { | |
this.properties = properties; | |
} | |
} |
可以看到上面使用了反射去设置boundsql.sql,有可能有同学会问这样会不会触发JVM优化修改不了这个final String属性。
这里我下一个结论是可以的,为了解释这个问题我写了端代码帮助理解:
package base; | |
import java.lang.reflect.Field; | |
import java.lang.reflect.InvocationTargetException; | |
public class FinalPropertyModify { | |
private final String a = ""; | |
private final String ab; | |
public FinalPropertyModify(String ab) { | |
this.ab = ab; | |
} | |
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException { | |
// 直接赋值初始化 | |
FinalPropertyModify finalPropertyModify = new FinalPropertyModify(""); | |
Field a = FinalPropertyModify.class.getDeclaredField("a"); | |
a.setAccessible(true); | |
a.set(finalPropertyModify, ""); | |
// 构造函数初始化 | |
Field ab = FinalPropertyModify.class.getDeclaredField("ab"); | |
ab.setAccessible(true); | |
ab.set(finalPropertyModify, ""); | |
System.out.println("==================反射获取=================="); | |
System.out.println(a.get(finalPropertyModify)); | |
System.out.println(ab.get(finalPropertyModify)); | |
System.out.println("==================直接获取=================="); | |
System.out.println(finalPropertyModify.a); | |
System.out.println(finalPropertyModify.ab); | |
} | |
} |
最终输出结果为:
可以看到如果是直接赋值进行初始化final属性的话,会被JVM给优化掉,如果使用的是构造函数进行初始化属性是不是触发JVM优化的。
我们再看一下boundsql类的属性定义,很显然sql属性是在构造函数中进行初始化的。我们可以大胆的去modify sql,但一定要注意不能修改成错误的SQL和一定要考虑安全问题,mybatis没有提供sql的修改方法也是考虑这一点,可能会不安全。
public class BoundSql { | |
// 我们要修改的SQL属性 | |
private final String sql; | |
private final List<ParameterMapping> parameterMappings; | |
private final Object parameterObject; | |
private final Map<String, Object> additionalParameters; | |
private final MetaObject metaParameters; | |
public BoundSql(Configuration configuration, String sql, List<ParameterMapping> parameterMappings, Object parameterObject) { | |
this.sql = sql; | |
this.parameterMappings = parameterMappings; | |
this.parameterObject = parameterObject; | |
this.additionalParameters = new HashMap<>(); | |
this.metaParameters = configuration.newMetaObject(additionalParameters); | |
} | |
public String getSql() { | |
return sql; | |
} | |
public List<ParameterMapping> getParameterMappings() { | |
return parameterMappings; | |
} | |
public Object getParameterObject() { | |
return parameterObject; | |
} | |
public boolean hasAdditionalParameter(String name) { | |
String paramName = new PropertyTokenizer(name).getName(); | |
return additionalParameters.containsKey(paramName); | |
} | |
public void setAdditionalParameter(String name, Object value) { | |
metaParameters.setValue(name, value); | |
} | |
public Object getAdditionalParameter(String name) { | |
return metaParameters.getValue(name); | |
} | |
} |
UserMapper.xml
很简单的一个SQL
PUBLIC "-//mybatis.org//DTD Mapper.0//EN" | |
"http://mybatis.org/dtd/mybatis--mapper.dtd"> | |
<mapper namespace="com.allens.mybatis.mappers.UserMapper"> | |
<select id="selectUsers" parameterType="int" resultType="com.allens.mybatis.model.User"> | |
select * from user where id = limit ${pageSize} | |
</select> | |
</mapper> |
测试
运行成功: