前言
-
场景
mybatis中,如何实现在不侵入原有业务代码的情况下拦截sql,执行特定的某些逻辑。如:分页工具PageHelper、敏感信息加密或多数据库适配等。
-
基本介绍
在mybatis中也为开发者预留了拦截器接口Interceptor,通过实现自定义拦截器这一功能,可以实现我们自己的插件,允许用户在不改动mybatis的原有逻辑的条件下,实现自己的逻辑扩展。
使用说明
-
拦截接口
Executor、StatementHandler、ParameterHandler和ResultSetHandler接口所有方法。ParameterHandler主要解析参数,为Statement设置参数,ResultSetHandler主要是负责把ResultSet转换成Java对象。
-
内容说明
Interceptor接口、Invocation调用类、@Intercepts和@Signature注解。
Interceptor接口
-
源码分析
public interface Interceptor { // 该方法就是MyBatis在使用前面提到的四个接口中的方法时进行回调的方法。 Object intercept(Invocation invocation) throws Throwable; // 用来为目标对象创建代理对象的方法,通常不需要覆写。 default Object plugin(Object target) { return Plugin.wrap(target, this); } // 设置一些属性 default void setProperties(Properties properties) { } }
Invocation类
-
源码分析
public class Invocation { private final Object target; // 目标对象 private final Method method; // 执行方法 private final 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; } // 执行目标方法 public Object proceed() throws InvocationTargetException, IllegalAccessException { return method.invoke(target, args); } }
@Intercepts和@Signature注解
-
@Intercepts
@Intercepts注解只有一个属性,即value,其返回值类型是一个@Signature类型的数组,表示我们可以配置多个@Signature注解。
-
@Signature
@Signature注解其实就是一个方法签名,其共有三个属性,分别为:
type指接口的class,
method指接口中的方法名,
args指的是方法参数类型(该属性返回值是一个数组)。举例说明
比如:我们需要代理Executor的query方法,而查询方法有2个,一个有(CacheKey cacheKey, BoundSql boundSql)参数,另外一个没有,如果我们要代理的是没有这2个参数的方法,那么需要在配置中的args参数里就不写这2个参数
-
源码分析
public @interface Intercepts { /** * 方法签名 */ Signature[] value(); } public @interface Signature { /** * 用来指定接口类型 */ Class<?> type(); /** * 用来指定方法名 */ String method(); /** * 用来指定方法参数 */ Class<?>[] args(); }
代码示例
需求描述
-
敏感信息密文加密
对于新增和修改,如果有username和phone字段,则需要动态添加字段username_enc和phone_enc为对应字段的密文,并保存到数据库。
实现流程
-
引入依赖
<?xml version="1.0"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <!--<version>2.2.5.RELEASE</version>--> <version>1.5.5.RELEASE</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>test-shardingsphere</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <description>Demo project for Spring Boot</description> <dependencies> <!--swagger ui--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> <!--springboot--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> </dependency> <!--mybatis starter--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.41</version> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.10</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.8.13</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!--sql解析--> <dependency> <groupId>com.github.jsqlparser</groupId> <artifactId>jsqlparser</artifactId> <version>3.1</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.2</version> <configuration> <!--配置文件的位置--> <configurationFile>src/main/resources/generatorConfig.xml<configurationFile> <verbose>true</verbose> <overwrite>true</overwrite> </configuration> <executions> <execution> <id>Generate MyBatis Artifacts</id> <goals> <goal>generate</goal> </goals> </execution> </executions> <dependencies> <dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.2</version> </dependency> </dependencies> </plugin> </plugins> </build> </project>
-
自定义拦截器
/** * */ @Intercepts({ @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }), @Signature(type = Executor.class, method = "query", args={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) }) @Slf4j public class MyBatisInterceptor implements Interceptor{ @Override public Object intercept(Invocation invocation) throws Throwable { // 拦截sql //被代理对象的被代理方法参数,如Executor的update方法,那么args[0]为MappedStatement类型对象,args[1]为类型object对象 //args[1]具体为什么对象呢:当前执行查询的mapper方法对应的参数及其值 //如:当前我执行的是UserMapper的findUser(@Param("user")User oneUser),那么这里的args[1]为一个map,key为user,值为形参oneUser对应的的值 Object[] args = invocation.getArgs(); MappedStatement statement = (MappedStatement) args[0]; Object parameterObject = args[1]; BoundSql boundSql = statement.getBoundSql(parameterObject); String sql = boundSql.getSql(); if (sql==null||"".equals(sql)) { return invocation.proceed(); } // 重写sql resetSql2Invocation(invocation,sql); return invocation.proceed(); } private void resetSql2Invocation(Invocation invocation,String sql) throws SQLException { final Object[] args = invocation.getArgs(); MappedStatement statement = (MappedStatement) args[0]; MapperMethod.ParamMap parameterObject = (MapperMethod.ParamMap)args[1]; final BoundSql boundSql = statement.getBoundSql(parameterObject); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); //根据不同的sql类型重新构建新的sql语句 String newsql = ""; MyOperationType myOperationType = null; switch (statement.getSqlCommandType()) { case INSERT: myOperationType = new MyInsertOperationType(); newsql = myOperationType.handle(statement,parameterMappings, parameterObject, sql); break; case UPDATE: myOperationType = new MyUpdateOperationType(); newsql = myOperationType.handle(statement,parameterMappings, parameterObject, sql); break; case DELETE: myOperationType = new MyDeleteOperationType(); newsql = myOperationType.handle(statement,parameterMappings, parameterObject, sql); break; case SELECT: myOperationType = new MySelectOperationType(); newsql = myOperationType.handle(statement,parameterMappings, parameterObject, sql); break; default: break; } // 重新new一个查询语句对像 BoundSql newBoundSql = new BoundSql(statement.getConfiguration(), newsql, parameterMappings, parameterObject); // 把新的查询放到statement里 MappedStatement newStatement = copyFromMappedStatement(statement, new BoundSqlSqlSource(newBoundSql)); // 重新设置新的参数 args[0] = newStatement; System.out.println("sql语句:"+newsql); } //构造新的statement private MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource) { MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType()); builder.resource(ms.getResource()); builder.fetchSize(ms.getFetchSize()); builder.statementType(ms.getStatementType()); builder.keyGenerator(ms.getKeyGenerator()); if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) { StringBuilder keyProperties = new StringBuilder(); for (String keyProperty : ms.getKeyProperties()) { keyProperties.append(keyProperty).append(","); } keyProperties.delete(keyProperties.length() - 1, keyProperties.length()); builder.keyProperty(keyProperties.toString()); } builder.timeout(ms.getTimeout()); builder.parameterMap(ms.getParameterMap()); builder.resultMaps(ms.getResultMaps()); builder.resultSetType(ms.getResultSetType()); builder.cache(ms.getCache()); builder.flushCacheRequired(ms.isFlushCacheRequired()); builder.useCache(ms.isUseCache()); return builder.build(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { // TODO Auto-generated method stub } class BoundSqlSqlSource implements SqlSource { private BoundSql boundSql; public BoundSqlSqlSource(BoundSql boundSql) { this.boundSql = boundSql; } @Override public BoundSql getBoundSql(Object parameterObject) { return boundSql; } } }
-
MyBatis拦截器配置
@Configuration public class MyBatisConfig { @Autowired private SqlSessionFactory sqlSessionFactory; @PostConstruct public void addMySqlInterceptor() { MyBatisInterceptor interceptor = new MyBatisInterceptor(); sqlSessionFactory.getConfiguration().addInterceptor(interceptor); } }
-
jsqlparser重写sql
MyOperationType接口
public interface MyOperationType { String handle(MappedStatement statement,List<ParameterMapping> parameterMappings, MapperMethod.ParamMap parameterObject, String sql); default boolean isNotIn(ParameterMapping parameterMapping,List<ParameterMapping> parameterMappings){ if(parameterMappings!=null){ for(ParameterMapping it:parameterMappings){ if(it.getProperty().equals(parameterMapping.getProperty())){ return false; } } } return true; } default List<String> encList(String tableName,List<Column> columns){ List<String> result = new ArrayList<>(); List<String> list = getEncMap().get(tableName); if(list!=null&&columns!=null){ for(Column col:columns){ if(list.contains(col.getColumnName())){ result.add(col.getColumnName()); } } } return result; } default String getColumnValue(MapperMethod.ParamMap parameterObject,List<ParameterMapping> parameterMappings,List<Column> columns,String column){ if(columns!=null){ for(int i=0;i<columns.size();i++){ Column col = columns.get(i); if(column.equals(col.getColumnName())){ ParameterMapping parameterMapping = parameterMappings.get(i); String property = parameterMapping.getProperty(); Object result = parameterObject.get(property); return (String)result; } } } return null; } Map<String,List<String>> getEncMap(); }
MyDefaultOperationType抽象类
public abstract class MyDefaultOperationType implements MyOperationType{ Map<String,List<String>> encMaps = new HashMap<>(); /** * 假设只对t_user表的username和phone2个字段进行加密存储 */ public MyDefaultOperationType(){ encMaps.put("t_user", Arrays.asList("username","phone")); } @Override public Map<String, List<String>> getEncMap() { return encMaps; } }
MyInsertOperationType新增操作类型子类
public class MyInsertOperationType extends MyDefaultOperationType{ @Override public String handle(MappedStatement mystatement,List<ParameterMapping> parameterMappings, MapperMethod.ParamMap parameterObject, String sql) { try { //解析sql语句 Statement statement = CCJSqlParserUtil.parse(sql); //强制转换成insert对象 Insert insert = (Insert) statement; //从insert中获取表名 Table table = insert.getTable(); //从insert中获取字段名 List<Column> columns = insert.getColumns(); List<String> needEncs = encList(table.getName(), columns); if (!needEncs.isEmpty()) { ExpressionList list = (ExpressionList) insert.getItemsList(); for(String col:needEncs) { String newCol = col+"_enc"; Column miwen = new Column(newCol); //在insert里添加一个字段 columns.add(miwen); //在insert的value里添加一个占位符? list.getExpressions().add(new JdbcParameter()); //将新增的字段添加到mybatis框架的字段集合中 ParameterMapping parameterMapping = new ParameterMapping.Builder(mystatement.getConfiguration(),newCol,String.class).build(); if(isNotIn(parameterMapping,parameterMappings)) { parameterMappings.add(parameterMapping); } //添加参数值 parameterObject.put(newCol, getColumnValue(parameterObject,parameterMappings,columns,col)+"123"); } insert.setItemsList(list); } return insert.toString(); }catch (Exception e){ throw new RuntimeException("解析sql异常",e); } } }
MyUpdateOperationType修改操作类型子类
public class MyUpdateOperationType extends MyDefaultOperationType { @Override public String handle(MappedStatement mystatement,List<ParameterMapping> parameterMappings, MapperMethod.ParamMap parameterObject, String sql) { try { Statement statement = CCJSqlParserUtil.parse(sql); Update update = (Update)statement; Table table = update.getTable(); List<Column> columns = update.getColumns(); List<String> needEncs = encList(table.getName(), columns); if (!needEncs.isEmpty()) { List<Expression> expressions = update.getExpressions(); for(String col:needEncs) { String newCol = col+"_enc"; Column miwen = new Column(table,newCol); Expression expression = new JdbcParameter(); expressions.add(expression); //添加参数key ParameterMapping parameterMapping = new ParameterMapping.Builder(mystatement.getConfiguration(),newCol,String.class).build(); if(isNotIn(parameterMapping,parameterMappings)) { int index = parameterMappings.size()-(parameterMappings.size()- columns.size()); parameterMappings.add(index,parameterMapping); } columns.add(miwen); //添加参数值 parameterObject.put(newCol, getColumnValue(parameterObject,parameterMappings,columns,col)+"123"); } Expression where = update.getWhere(); EqualsTo status = new EqualsTo(); status.setLeftExpression(new Column(table, "status")); StringValue stringValue = new StringValue("0"); status.setRightExpression(stringValue); if(where!=null) { AndExpression lastwhere = new AndExpression(where, status); update.setWhere(lastwhere); }else{ update.setWhere(status); } update.setExpressions(expressions); } return update.toString(); }catch (Exception e){ throw new RuntimeException("解析sql异常",e); } } }
MyDeleteOperationType删除操作类型子类
public class MyDeleteOperationType extends MyDefaultOperationType { @Override public String handle(MappedStatement mystatement,List<ParameterMapping> parameterMappings, MapperMethod.ParamMap parameterObject, String sql) { try { Statement statement = CCJSqlParserUtil.parse(sql); Delete delete = (Delete) statement; Table table = delete.getTable(); Update update = new Update(); update.setTable(table); update.setColumns(Arrays.asList(new Column(table,"status"))); update.setExpressions(Arrays.asList(new StringValue("1"))); update.setWhere(delete.getWhere()); return update.toString(); }catch (Exception e){ throw new RuntimeException("解析sql异常",e); } } }
MySelectOperationType查询操作类型子类
public class MySelectOperationType extends MyDefaultOperationType { @Override public String handle(MappedStatement mystatement,List<ParameterMapping> parameterMappings, MapperMethod.ParamMap parameterObject, String sql) { try { Statement statement = CCJSqlParserUtil.parse(sql); Select select = (Select) statement; PlainSelect plain = (PlainSelect) select.getSelectBody(); FromItem fromItem = plain.getFromItem(); String tableName = fromItem.toString(); Table table = new Table(); table.setAlias(fromItem.getAlias()); table.setName(tableName); Expression where = plain.getWhere(); EqualsTo status = new EqualsTo(); status.setLeftExpression(new Column(table, "status")); StringValue stringValue = new StringValue("0"); status.setRightExpression(stringValue); if(where!=null) { AndExpression lastwhere = new AndExpression(where, status); plain.setWhere(lastwhere); }else{ plain.setWhere(status); } return select.toString(); }catch (Exception e){ throw new RuntimeException("解析sql异常",e); } } }
总结
-
流程说明
MyBatis中的拦截器机制其实就是基于JDK的动态代理实现,在创建完目标对象后,循环用户注册的所有拦截器实现,然后对每个拦截器的@Intercepts中@Signature注解中配置的类型来和目标对象所实现的接口进行匹配,如果匹配上了,则说明目标对象是当前拦截器所关注的。
在代理对象执行的时候,对目标方法进行拦截判断是不是当前拦截器所关注的方法,如果是则执行拦截器的intercept方法,否则直接执行目标方法。
其他
-
MappedStatement
SQL映射语句(Mapper.xml文件每一个方法对应一个MappedStatement对象)
参考链接
-
MyBatis中拦截器(Interceptor)实现原理分析
https://blog.csdn.net/m0_43448868/article/details/112184232
-
Mybatis执行SQL的4大基础组件详解
https://blog.csdn.net/prestigeding/article/details/90578125
-
mybatis拦截器,并使用jsqlparser重写sql
https://blog.csdn.net/zhaocuit/article/details/120913046
-
#私藏项目实操分享# Mybatis自定义拦截器与插件开发
https://blog.51cto.com/u_15363374/4838804