玖叶教程网

前端编程开发入门

MyBatis3的Plugins功能使用(mybatislogplugin)

这是什么?

根据官方介绍,这个功能可以让你在已映射语句执行过程中的某一点进行拦截调用。其实就是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语句的功能来进行演示。

通过Plugin实现打印SQL语句

  • 创建表并初始化数据
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', '男');
  • 构建项目

创建一个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>

创建包及类


  • 类介绍

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>
  • 执行效果

执行我们的main方法,效果如下:


真正执行的sql打印出来了

可以看出,通过一个切入,我们可以把MyBatis真正执行的sql打印出来。

详细解析

我们从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);
  }

根据代码,知道通过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的动态代理,为需要拦截的点,创建一个代理类,当执行指定拦截的方法时,通过代理类执行自己的业务逻辑,而且可以选择是否执行默认的后续流程,可以选择自己执行后续流程。

tips:第一次写这种博客,如有错误请评论指出。谢谢。

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言