mybatis3.4.6 批量更新 foreach 遍历map 的正确姿势详解
好久没编码了!最近开始编码遇到一个问题 !一个批量修改的问题,就是mybatis foreach 的使用。
当时使用的场景 ,前端 传逗号拼接的字符串id, 修改id对应数据的数据顺序 ,顺序 就是id 的顺序.
就是一个条件(单个id值) 修改一个值(传入的id的顺序) ,
1、 把条件作为Map 的key 修改值是value,用map入参
2、用List<Object> 或者数组 ,把条件和值封装成对象放进list集合或者array数组
3、代码使用for循环调用mapper方法 穿两个参数。
因为考虑到第二种用法,需要不断创建对象 放进数组在 遍历数组获取对象取值。从虚拟机的堆内存考虑,放弃------------------------
第三种方法,会循环多少次就执行多少条sql语句,放弃-----------------------
于是使用Map,
可是在mybatis中参数是map的foreach使用,对于很久没编码的我,实在是忘记得很干净。于是百度一堆,一致性 就是报错:
把打印出的sql语句放到navicat 执行 可以执行不会报错。那问题是什么(这里想来我1个小时),最后没办法 直接看mybatis的官网,把sql改成如下,正确执行。
下面给出正确的mybatis中foreach的map的姿势,避免大家以后在这上面浪费时间 ,直接上代码,涂改部分因公司保密协议问题(大家都知道是表名):
mapper
一定要加@Param注解
mapper.xml
最后 大家遇到此问题的时候 看到我的这篇文章 少耽误时间!
补充知识:MyBatis3的Plugins功能使用
1、这是什么?
根据官方介绍,这个功能可以让你在已映射语句执行过程中的某一点进行拦截调用。其实就是MyBatis给用户留了几个切入点,通过这些切入点,用户可以自己实现的功能。根据官方介绍,这些切入点(方法)包括以下几个:
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
这是四个接口以及接口的方法,这些接口是MyBatis在执行sql语句时会执行的方法。用户就可以通过这些方法进行切入,实现自己的逻辑。
接下来,我们通过实现一个打印sql语句的功能来进行演示。
2、通过Plugin实现打印SQL语句
2.1 创建表并初始化数据
CREATE TABLE `t_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `age` int(3) DEFAULT NULL, `address` varchar(255) DEFAULT NULL, `sex` varchar(10) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
插入测试数据
INSERT INTO `t_user` VALUES (1, '张三', 4, 'OUT', '男'); INSERT INTO `t_user` VALUES (2, '李四', 5, 'OUT', '男'); INSERT INTO `t_user` VALUES (3, '王五', 5, 'OUT', '男');
2.2、构建项目
创建一个maven项目,并引入相关jar包,pom.xml文件如下
<?xml version="1.0" encoding="UTF-8"?> <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> <groupId>com.mybatis.plugins</groupId> <artifactId>mybatis-plugins-study</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.6</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>8</source> <target>8</target> <encoding>utf-8</encoding> </configuration> </plugin> </plugins> </build> </project>
创建包及类
2.3、类介绍
User类,与数据库表对应的实体类
package com.mybatis.plugins.study.entity; /** * @author guandezhi * @date 2019/7/2 17:40 */ public class User { private int id; private String name; private int age; private String address; private String sex; @Override public String toString() { final StringBuffer sb = new StringBuffer("User{"); sb.append("id=").append(id); sb.append(", name='").append(name).append('\''); sb.append(", age=").append(age); sb.append(", address=").append(address); sb.append(", sex=").append(sex); sb.append('}'); return sb.toString(); } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } }
UserMapper接口,实现具体的sql语句,这里为了方便,我们使用MyBatis的注解方式
package com.mybatis.plugins.study.mapper; import com.mybatis.plugins.study.entity.User; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.type.Alias; import java.util.List; /** * @author guandezhi * @date 2019/7/2 17:49 */ @Alias(value = "userMapper") public interface UserMapper { /** * 查询方法 * @param id 用户id * @param name 用户姓名 * @return 结果集合 */ @Select("select * from t_user where id = #{param1} and name = #{param2}") List<User> select(@Param("idParam") int id, @Param("nameParam") String name); }
在这里简单说明一下注解sql里的参数问题,一般情况下,如果只有一个参数,我们不需要写@Param注解也是可以的,如果有多个参数,则需要对每个参数使用@Param注解指定参数名字,或者也可以使用MyBatis提供的默认参数方式,就是示例中使用的方式,按照参数的顺序分别为param1、param2…。
接下来,就是Plugins的核心代码了,LogInterceptor类,按照官方文档,如果我们需要使用Plugins的功能,需实现 Interceptor 接口
package com.mybatis.plugins.study.interceptor; import org.apache.ibatis.binding.MapperMethod; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.plugin.*; import java.sql.Connection; import java.util.List; import java.util.Properties; /** * @author guandezhi * @date 2019/7/2 17:46 */ @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) public class LogInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); BoundSql boundSql = statementHandler.getBoundSql(); // 原始sql String sql = boundSql.getSql(); System.out.println("打印原始sql===>" + sql); // 参数集合 MapperMethod.ParamMap parameterObject = (MapperMethod.ParamMap) boundSql.getParameterObject(); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); for (ParameterMapping parameterMapping : parameterMappings) { Object param = parameterObject.get(parameterMapping.getProperty()); if (param.getClass() == Integer.class) { sql = sql.replaceFirst("\\?", param.toString()); } else if (param.getClass() == String.class) { sql = sql.replaceFirst("\\?", "'".concat(param.toString()).concat("'")); } } // 替换占位符的sql System.out.println("打印执行sql===>" + sql); return invocation.proceed(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } }
实现接口的三个方法 intercept(Invocation invocation)、plugin(Object target)、setProperties(Properties properties)。
通过注解Intercepts告诉MyBatis,我们需要在哪个切入点实现自己的功能,在这里我们切入的是StatementHandler的prepare方法,即这是我们的切入点,当MyBatis执行一个sql语句到这个方法的时候,会先执行我们自己的逻辑,执行哪个逻辑呢?就是 intercept(Invocation invocation)里面的逻辑。具体原理我们稍后再讲。
最后是我们的主体类,这里为了方便,我直接使用一个main方法。
package com.mybatis.plugins.study; import com.mybatis.plugins.study.entity.User; import com.mybatis.plugins.study.mapper.UserMapper; 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.InputStream; import java.util.List; /** * @author guandezhi * @date 2019/7/2 17:45 */ public class MainClass { public static void main(String[] args) throws Exception { String resource = "mybatis-config.xml"; InputStream stream = Resources.getResourceAsStream(resource); SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(stream); SqlSession session = factory.openSession(); try { UserMapper mapper = session.getMapper(UserMapper.class); List<User> userList = mapper.select(1, "张三"); userList.forEach(System.out::println); } finally { session.close(); } } }
基于我们的需求,只做了简单的配置,配置文件如下
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <properties> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/test"/> <property name="username" value="root"/> <property name="password" value="baofeng"/> </properties> <plugins> <plugin interceptor="com.mybatis.plugins.study.interceptor.LogInterceptor"/> </plugins> <environments default="dev"> <environment id="dev"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> </dataSource> </environment> </environments> <mappers> <mapper class="com.mybatis.plugins.study.mapper.UserMapper" /> </mappers> </configuration>
2.4、执行效果
执行我们的main方法,效果如下:
可以看出,通过一个切入,我们可以把MyBatis真正执行的sql打印出来。
3、详细解析
我们从main方法开始一步步的去分析。
public static void main(String[] args) throws Exception { String resource = "mybatis-config.xml"; InputStream stream = Resources.getResourceAsStream(resource); SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(stream); SqlSession session = factory.openSession(); try { UserMapper mapper = session.getMapper(UserMapper.class); List<User> userList = mapper.select(1, "张三"); userList.forEach(System.out::println); } finally { session.close(); } }
我们在配置文件中增加了LogInterceptor的配置,那么在SqlSessionFactory初始化的时候做了什么呢?
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { // 根据配置文件构造一个XMLConfigBuilder对象 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); // 通过parse()方法解析配置文件,构造一个Configuration对象,然后根据Configuration对象构造一个默认的DefaultSqlSessionFactory对象 return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } }
跟踪这个对象的parse()方法,找到解析plugins节点的方法
private void pluginElement(XNode parent) throws Exception { // parent就是plugins节点 if (parent != null) { // 解析每个plugin节点 for (XNode child : parent.getChildren()) { // 获取plugin节点的interceptor参数 String interceptor = child.getStringAttribute("interceptor"); // 获取plugin节点的property参数(我们的配置文件未配置该参数) Properties properties = child.getChildrenAsProperties(); // 调用resolveClass方法,加载我们配置的interceptor参数指定的类并返回这个类的实例 Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance(); // 设置配置的属性 interceptorInstance.setProperties(properties); // 把实例添加到配置对象的私有属性interceptorChain中,它只有一个私有属性,就是所有Interceptor的集合 configuration.addInterceptor(interceptorInstance); } } }
所以构建SqlSessionFactory的时候,就是为我们配置的interceptor创建了一个实例并保存在了一个集合中。
继续回到main方法,构建完SqlSessionFactory之后,想要执行sql,需要通过SqlSessionFactory返回一个SqlSession,跟踪factory.openSession()方法
/** * @param execType 执行器,不配置默认是SIMPLE * @param level 事物隔离级别,这里为null * @param autoCommit 自动提交,这里默认为false */ private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { // 数据库连接的包装类 Transaction tx = null; try { // 从配置对象中获取环境信息,构建数据库连接 final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); // 调用newExecutor方法,返回一个执行器 final Executor executor = configuration.newExecutor(tx, execType); // 返回一个默认的SqlSession return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
看看如何构造一个执行器
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) { executor = new ReuseExecutor(this, transaction); } else { executor = new SimpleExecutor(this, transaction); } // 如果开启缓存功能,返回一个CachingExecutor if (cacheEnabled) { executor = new CachingExecutor(executor); } // 重点!!! executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
之前说过,构建SqlSessionFactory的时候,interceptor被初始化到configuration的interceptorChain属性中,查看代码,看看这里做了什么
public class InterceptorChain { private final List<Interceptor> interceptors = new ArrayList<Interceptor>(); public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { // 遍历所有的interceptor,调用plugin方法,这个plugin方法就是我们实现Interceptor接口需要实现的三个方法之一 target = interceptor.plugin(target); } return target; } 省略其他代码...... }
看看我们实现的方法中做了什么
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) public class LogInterceptor implements Interceptor { 省略其他代码...... @Override public Object plugin(Object target) { // 因为我们的这个Interceptor切入的是StatementHandler,所以,如果target是一个StatementHandler,我们就调用Plugin的wrap方法返回一个代理类,否则就直接返回target // 因为如果我们不切入这个target,就不需要返回代理类。 Class<?>[] interfaces = target.getClass().getInterfaces(); for (Class<?> i : interfaces) { if (i == StatementHandler.class) { return Plugin.wrap(target, this); } } return target; } 省略其他代码...... }
从上边我们知道,刚刚是从的newExecutor调用的,所以target是Executor不是我们切入的StatementHandler,所以这里其实什么都没做,直接又返回了Executor。
到这里,openSession也分析完了。就是构建了数据库连接及执行器。
然后,分析session.getMapper()方法,这个方法返回的是一个代理类,既然是代理类,那么我们就需要知道使用的是什么代理,找到执行的入口。
// 跟踪代码,直到MapperRegistry的getMapper方法 /** *@param type 就是我们getMapper要获取的那个Mapper对象 *@param sqlSession 就是上一步创建的sqlSession对象 */ @SuppressWarnings("unchecked") public <T> T getMapper(Class<T> type, SqlSession sqlSession) { // 先从knownMappers中获取Mapper,这个knownMappers是在构建SqlSessionFactory的时候,解析的mappers节点配置的所有Mapper final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } try { // 创建实例 return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } }
查看创建实例的代码
@SuppressWarnings("unchecked") protected T newInstance(MapperProxy<T> mapperProxy) { // 根据MapperProxy创建代理类实例 return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } public T newInstance(SqlSession sqlSession) { // 直接构造了一个MapperProxy的代理类,mapperInterface就是我们需要创建的mapper,这个是在解析配置文件的时候,为每个配置的Mapper创建了一个MapperProxyFactory,这里保存了对应的mapper final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); }
根据代码,知道通过session.getMapper(UserMapper.class)返回的Mapper是一个MapperProxy的代理类,所以,main方法里的userMapper.select()实际上执行的是MapperProxy的invoke方法
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 判断是不是Object声明的方法 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); // 判断是不是public方法或者接口方法 } else if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } // 在methodCache中获取这个Mapper方法的MapperMethod对象,如果没有就构造一个 final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); }
cachedMapperMethod方法
private MapperMethod cachedMapperMethod(Method method) { // 先从methodCache中拿,因为没有执行过,所以这里是空 MapperMethod mapperMethod = methodCache.get(method); if (mapperMethod == null) { // 根据mapper类和执行的方法(select)构建一个MapperMethod对象,然后放到缓存中 mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()); methodCache.put(method, mapperMethod); } return mapperMethod; }
构造MapperMethod
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) { this.command = new SqlCommand(config, mapperInterface, method); this.method = new MethodSignature(config, mapperInterface, method); }
构造SqlCommond和MethodSignature
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) { final String methodName = method.getName(); final Class<?> declaringClass = method.getDeclaringClass(); // 根据Mapper和方法名,构造一个MappedStatement对象 MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration); if (ms == null) { if (method.getAnnotation(Flush.class) != null) { name = null; type = SqlCommandType.FLUSH; } else { throw new BindingException("Invalid bound statement (not found): " + mapperInterface.getName() + "." + methodName); } } else { name = ms.getId(); type = ms.getSqlCommandType(); if (type == SqlCommandType.UNKNOWN) { throw new BindingException("Unknown execution method for: " + name); } } }
// 方法签名,执行的方法是否有返回值,返回值类型等等信息 public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) { Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface); if (resolvedReturnType instanceof Class<?>) { this.returnType = (Class<?>) resolvedReturnType; } else if (resolvedReturnType instanceof ParameterizedType) { this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType(); } else { this.returnType = method.getReturnType(); } this.returnsVoid = void.class.equals(this.returnType); this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray(); this.returnsCursor = Cursor.class.equals(this.returnType); this.mapKey = getMapKey(method); this.returnsMap = this.mapKey != null; this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class); this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class); this.paramNameResolver = new ParamNameResolver(configuration, method); }
继续invoke方法,执行MapperMethod.execute()方法
public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case INSERT: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); break; } case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); break; } // 我们执行的就是select方法 case SELECT: if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; // 并且返回的是个List } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); } else { Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); } break; case FLUSH: result = sqlSession.flushStatements(); break; default: throw new BindingException("Unknown execution method for: " + command.getName()); } if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { throw new BindingException("Mapper method '" + command.getName() + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); } return result; }
执行的是executeForMany方法,这里调用sqlSession的selectList方法,在selectList中调用了执行器(构建SqlSession时的那个执行器)的query方法,因为默认我们没有配置缓存,默认是开启的,所以会先执行CachingExecutor的query方法,在这里,会调用配置文件中配置的执行器(我们的没有配置,默认是SIMPLE类型执行器)的query方法,因为SimpleExecutor没有覆盖query方法,所以实际上执行的是BaseExecutor的query方法,在这里又调用了queryFromDatabase方法,然后又调用了doQuery方法,这个方法是抽象方法,由子类实现,即SimpleExecutor的doQuery方法
@Override public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); // 这个方法,类似之前提过的newExecutor方法 StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); stmt = prepareStatement(handler, ms.getStatementLog()); return handler.<E>query(stmt, resultHandler); } finally { closeStatement(stmt); } }
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); // 在这里,又调用了pluginAll方法, 之前讲过,这个方法其实就是遍历每个Interceptor,调用它们的plugin方法 statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; }
我们的LogInterceptor的plugin方法
@Override public Object plugin(Object target) { Class<?>[] interfaces = target.getClass().getInterfaces(); for (Class<?> i : interfaces) { if (i == StatementHandler.class) { return Plugin.wrap(target, this); } } return target; }
其实就是调用了一下Plugin的wrap方法。首先看一下Plugin的声明,他就是一个InvocationHandler。
public class Plugin implements InvocationHandler { // 省略部分代码...... /** * @param target 在这里就是StatementHandler * @param interceptor 在这里就是我们的LogInterceptor * / public static Object wrap(Object target, Interceptor interceptor) { // 解析注解配置,也就是我们要切入的类及方法 Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); Class<?> type = target.getClass(); // 如果我们的注解包含这个type,就把type的接口返回,在这里会返回StatementHandler。 Class<?>[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length > 0) { // 创建一个代理类,代理类为Plugin return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } return target; } 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[] sigs = interceptsAnnotation.value(); Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>(); for (Signature sig : sigs) { Set<Method> methods = signatureMap.get(sig.type()); if (methods == null) { methods = new HashSet<Method>(); signatureMap.put(sig.type(), methods); } 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; } // 省略部分代码...... }
也就是说,因为我们的切入,在SimpleExecutor的doQuery方法中调用newStatementHandler返回的是一个StatementHandler的代理类。接下来就调用了内部方法prepareStatement方法
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { Statement stmt; Connection connection = getConnection(statementLog); // 重点!!!因为handler是代理类,所以执行的都是代理类的invoke方法 stmt = handler.prepare(connection, transaction.getTimeout()); handler.parameterize(stmt); return stmt; }
代理类Plugin的invoke方法
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 从signatureMap中找一下,这个map的key是切入的类,所以调用getDeclaringClass()根据当前方法的声明类查找对应的方法集合。 Set<Method> methods = signatureMap.get(method.getDeclaringClass()); // 如果集合不为空或者包含当前方法, 也就是说我们切入这个类的这个方法了 if (methods != null && methods.contains(method)) { // 就会调用interceptor的interceptor方法了。注意,这里是return,也就是说不会执行默认的后续流程了。如果要执行后续流程,可以再interceptor方法的最后调用invocation的proceed()方法。 return interceptor.intercept(new Invocation(target, method, args)); } return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } }
这时候,轮到我们真正切入的逻辑了,这里为了演示,我们只是简单的做了替换。实现效果。
@Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); BoundSql boundSql = statementHandler.getBoundSql(); // 原始sql String sql = boundSql.getSql(); System.out.println("打印原始sql===>" + sql); // 参数集合 MapperMethod.ParamMap parameterObject = (MapperMethod.ParamMap) boundSql.getParameterObject(); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); for (ParameterMapping parameterMapping : parameterMappings) { Object param = parameterObject.get(parameterMapping.getProperty()); if (param.getClass() == Integer.class) { sql = sql.replaceFirst("\\?", param.toString()); } else if (param.getClass() == String.class) { sql = sql.replaceFirst("\\?", "'".concat(param.toString()).concat("'")); } } // 替换占位符的sql System.out.println("打印执行sql===>" + sql); // 最后调用一下这个方法,这个方法其实就是默认的后续方法。当然,按照实际业务,如果不需要的话,也可以不调用。 return invocation.proceed(); }
之后的流程就是默认的流程了。至此,其实整个的plugin拦截就分析完毕了。
其实,就是通过jdk的动态代理,为需要拦截的点,创建一个代理类,当执行指定拦截的方法时,通过代理类执行自己的业务逻辑,而且可以选择是否执行默认的后续流程,可以选择自己执行后续流程。
以上这篇mybatis3.4.6 批量更新 foreach 遍历map 的正确姿势详解就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持脚本之家。
相关文章
skywalking源码解析javaAgent工具ByteBuddy应用
这篇文章主要为大家介绍了skywalking源码解析javaAgent工具ByteBuddy应用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助2022-03-03如何为 Spring Boot 项目配置 Logback 日志
由于 Spring Boot 的默认日志框架选用的 Logback,再加上 Log4j2 之前爆过严重的漏洞,所以我们这次就只关注 Logback,本文重点给大家介绍如何为 Spring Boot 项目配置 Logback 日志,感兴趣的朋友跟随小编一起看看吧2024-07-07
最新评论