4.1 Mybatis动态代理

2017-06-10 10:43:06 10,249 6

在使用Mybatis的时候,我们可以只定义一个XxxMaper接口,然后直接利用这个接口定义的抽象方法来进行增删改查操作,Mybatis内部实际上利用了动态代理技术帮我们生成了这个接口的代理类。

举例来说,假设有映射文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 
<mapper namespace="com.tianshouzhi.mybatis.quickstart.UserMapper">
 
    <insert id="insert" parameterType="com.tianshouzhi.mybatis.quickstart.User">
        INSERT INTO user(name,age) VALUES (#{name},#{age})
    </insert>
 
    <select id="selectById" parameterType="int" resultType="com.tianshouzhi.mybatis.quickstart.User">
        select id,name,age from user where id= #{id}
    </select>
 
    <update id="updateById" parameterType="com.tianshouzhi.mybatis.quickstart.User">
        UPDATE user SET name=#{name},age=#{age} WHERE id=#{id}
    </update>
 
    <delete id="deleteById" parameterType="int">
        DELETE FROM user WHERE id=#{id}
    </delete>
 
</mapper>

我们只要定义一个UserMapper接口,注意:

1、这个接口的全路径就是映射文件的namespace属性值

2、然后在这个接口中定义几个方法,方法名分别于映射文件中定义的<insert>、<select>等元素的id属性值相同

如下:

package com.tianshouzhi.mybatis.quickstart;

public interface UserMapper {
    public int insert(User user);
    public User selectById(int id);
    public int updateById(User user);
    public int deleteById(int id);
}

之后我们就可以直接使用UserMapper类来进行增删改查,使用方式如下:

SqlSession  sqlSession = sqlSessionFactory.openSession();
try{
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    //增加
    User user=...
    int insertCount = userMapper.insert(user);
    assert insertCount==1;
    //查询
    user=userMapper.selectById(1);
    //更新
    userMapper.updateById(user);
    //删除
     userMapper.deleteById(1);
} finally {
    sqlSession.close();
}

之所以可以这样使用,是因为Mybatis对生成了UserMapper接口的动态代理类,当执行某个方法时,代理类内部会首选获取调用的方法的全路径,例如当我们调用UserMapper的insert方法时,其对应的全路径是com.tianshouzhi.mybatis.quickstart.UserMapper.insert。而这个值刚好对应着namespace属性值为com.tianshouzhi.mybatis.quickstart.UserMapper的mapper映射文件的id="insert"的insert元素,从而执行相应的sql。真正在执行时,还是利用SqlSession的insert方法来执行的,只不过这个过程对于用于来说屏蔽了。

我们带着2个问题来进行源码分析:

1、动态代理类是如何生成的

2、动态代理类是如何对方法进行拦截的

一、动态代理类的生成时机

每次当我们调用sqlSession的getMapper方法时,都会创建一个新的动态代理类实例,如:

sqlSession.getMapper(UserMapper.class);

也就是说,生成的动态代理类不是唯一的,而是每次都创建一个新的。

而SqlSession对象又将getMapper方法委给了Configuration对象执行,如下所示:

public class DefaultSqlSession implements SqlSession {

  private Configuration configuration;
...
 
  @Override
  public <T> T getMapper(Class<T> type) {
    return configuration.<T>getMapper(type, this);
  }
...  
}

Configuration类里面通过MapperRegistry对象维护了所有要生成动态代理类的XxxMapper接口信息。

public class Configuration {
...
protected MapperRegistry mapperRegistry = new MapperRegistry(this);
...
public <T> void addMapper(Class<T> type) {
  mapperRegistry.addMapper(type);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  return mapperRegistry.getMapper(type, sqlSession);
}
...
}

其中,getMapper方法就是用于创建接口的动态类。而addMapper方法是mybatis在解析配置文件时,会将需要生成动态代理类的接口注册到其中。目前我们主要介绍的getMapper方法,addMapper方法我们将会在之后进行介绍。

可以看到Configuration类的addMapper和getMapper方法最终又都是委派给MapperRegistry的addMapper和getMapper方法执行的。

MapperRegistry类的getMapper方法源码如下所示:

public class MapperRegistry {

  private final Configuration config;
  //用于维护所有要生成动态代理类XxxMapper映射关系,key就是要生成动态代理类的Class对象,value是真正生成动态代理的工厂类
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
...

  @SuppressWarnings("unchecked")
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
      //获取创建动态代理的工厂对象MapperProxyFactory
    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);
    }
  }
...
}

可以看到,创建动态代理类的核心代码都位于MapperProxyFactory的newInstance方法中。目前对这个方法不做分析,放到后面分析生成的代理类是如何对接口的方法进行拦截一起说。目前只需要知道,每次调用SqlSession的getMapper方法,都会创建一个新的代理类即可。

现在我们考虑,Mybatis是如何知道要生成一个类的动态代理类的,这个过程是在mybatis解析xml配置文件的时候就确定了。

具体逻辑是,根据<mapper namespace="....">的namespace属性值,判断有没有这样一个接口的全路径与namespace属性值完全相同,如果有,就生成这个接口的动态代理类。

相关解析代码位于XMLMapperBuilder的 parse方法中:

public void parse() {
  if (!configuration.isResourceLoaded(resource)) {
    configurationElement(parser.evalNode("/mapper"));//解析映射文件的根节点mapper元素
    configuration.addLoadedResource(resource);
    bindMapperForNamespace();//这个方法内部会根据namespace属性值,生成动态代理类。
  }

  parsePendingResultMaps();
  parsePendingChacheRefs();
  parsePendingStatements();
}

bindMapperForNamespace方法源码如下所示:

private void bindMapperForNamespace() {
  String namespace = builderAssistant.getCurrentNamespace();//获得mapper元素的namespace属性值。
  if (namespace != null) {
    Class<?> boundType = null;
    try {
      boundType = Resources.classForName(namespace);//获取namespace属性值对应的Class对象。
    } catch (ClassNotFoundException e) {
      //如果没有这个类,则直接忽略。这是因为namespace属性值只需要是唯一的即可,并不一定需要对应一个XxxMapper接口。
      //没有XxxMapper接口的时候,我们可以直接使用SqlSession来进行增删改查操作。
    }
    if (boundType != null) {
      if (!configuration.hasMapper(boundType)) {
        // Spring may not know the real resource name so we set a flag
        // to prevent loading again this resource from the mapper interface
        // look at MapperAnnotationBuilder#loadXmlResource
        configuration.addLoadedResource("namespace:" + namespace);
        //如果namespace属性值有对应的java类,调用Configuration的addMapper方法,将其添加到MapperRegistry中。
        configuration.addMapper(boundType);
      }
    }
  }
}

在前面的代码中,我们已经看到,Configuration的addMapper方法是委派给MapperRegistry的addMapper进行的,源码如下所示:

org.apache.ibatis.binding.MapperRegistry#addMapper

public <T> void addMapper(Class<T> type) {//type是根据namespace属性值解析出来的class对象
  if (type.isInterface()) {//这个class一定要是一个接口,否则不会针对其生成动态代理
    if (hasMapper(type)) {
      throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
    }
    boolean loadCompleted = false;
    try {
      //针对这个接口,生成一个MapperProxyFactoy,用于之后生成动态代理类。
      knownMappers.put(type, new MapperProxyFactory<T>(type));
      //以下代码片段用于解析我们定义的XxxMapper接口里面使用的注解,这主要是处理不使用xml映射文件,
      //而是直接通过相关注解如@Select、@Insert等把sql定义在接口的方法上面的情况,这里不做讨论
      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      parser.parse();
      loadCompleted = true;
    } finally {
      if (!loadCompleted) {
        knownMappers.remove(type);
      }
    }
  }
}

二、动态代理类是如何对方法进行拦截的

通过前面的分析,我们知道当调用SqlSession的getMapper方法时,通过一层一层的委派,最终会通过MapperProxyFactory的newInstance(sqlSession)方法,来创建动态代理类,MapperProxyFactory类源码如下所示

public class MapperProxyFactory<T> {

  private final Class<T> mapperInterface;
  ...

  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

  ...

  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

}

可以看到,MapperProxyFactory的newInstance(sqlSession)方法中,首先会创建一个MapperProxy对象,然后将其当做参数传递给newInstance(mapperProxy)方法,这个方法内部通过JDK提供的Proxy.newProxyInstance方法生成动态代理类。Proxy.newProxyInstance方法声明如下所示:

java.lang.reflect.Proxy#newProxyInstance

public static Object newProxyInstance(ClassLoader loader,//类加载器
                  Class<?>[] interfaces,//生成哪些接口的动态代理
                 InvocationHandler h)...//当接口中的方法被调用时,会JVM会回调InvocationHandler的invoke方法

InvocationHandler接口定义如下所示:

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

当接口中的任何一个方法被调用时,JVM都会回调InvocationHandler接口实现类的invoke方法。并会传递三个回调参数:

Object proxy:被代理的类

Method method:表示当前被调用的接口的方法对象

Object[] args:表示接口方法被调用时,传递的参数。

MapperProxy类实现了InvocationHandler接口,因此我们只要从其实现的invoke方法入手进行分析

public class MapperProxy<T> implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -6424540398559729838L;
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache;

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  //如果调用的是Object类中定义的方法,直接通过反射调用即可。
  //我们知道,在Java中,任何对象都是Object对象的子类,所以会继承Object对象定义的公共方法。
  //当这些方法被调用时,我们不需要做任何特殊处理,直接进行即可。
    if (Object.class.equals(method.getDeclaringClass())) {
      try {
        return method.invoke(this, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
    //如果进行到这一步,表示调用的是我们在XxxMapper接口中自定义的方法,因此需要进行代理。
    //首先将当前被调用的方法Method构造成一个MapperMethod对象,然后调用其execute方法真正的开始执行。
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

  private MapperMethod cachedMapperMethod(Method method) {
    MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) {
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
  }

}

现在我们定义为到,最终的拦截代码位于MapperMethod类的execute方法中,当把这个方法的代码分析完成,本小节也就分析完成了。MapperMethod类源码如下所示:

public class MapperMethod {

  private final SqlCommand command;
  private final MethodSignature method;

  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, mapperInterface, method);
  }

  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    if (SqlCommandType.INSERT == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
    } else if (SqlCommandType.UPDATE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
    } else if (SqlCommandType.DELETE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
    } else if (SqlCommandType.SELECT == command.getType()) {//select语句的处理逻辑
        //根据调用的XxxMapper接口定义的抽象方法的返回值类型,选择SqlSession的不同的方法进行执行。
      if (method.returnsVoid() && method.hasResultHandler()) {
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {//如果方法的返回值是一个集合,调用selectList方法
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {//如果方法的返回值是一个Map,调用selectMap方法
        result = executeForMap(sqlSession, args);
      } else if (method.returnsCursor()) {//如果方法的返回值,调用selectCursor方法
        result = executeForCursor(sqlSession, args);
      } else {//否则调用sqlSession.selectOne方法
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
      }
    } else if (SqlCommandType.FLUSH == command.getType()) {
        result = sqlSession.flushStatements();
    } else {
      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;
  }
...
}

在MapperMethod的构造方法中,例如参数构造了两个对象SqlCommand、MethodSignature,这两个类都是MapperMethod的内部类。

而execute方法中,会首选调用SqlCommand的getType方法,判断要执行的sql类型INSERT、UPDATE、DELETE、SELECT、FLUSH。然后分别调用SqlSession的insert、update、delete、selectOne、selectMap、selectList等方法来执行。

这说明,我们通过SqlSession的getMapper方法获得接口代理来进行CRUD操作,其底层还是依赖于最原始的SqlSession的使用方法。

关于SqlCommand,我们不做过多介绍,这里只提一点,SqlCommand.getType中是如何确定当前要执行的sql类型的。因为不同的Sql类型意味着我们要对sql操作的结果做不同的处理,例如:

对于insert、update和delete,这样的sql,其返回值是影响的结果集的行数,因此我们看到上述相关代码在执行的时候,都调用了一个rowCountResult方法。

对于select:要对查询的结果集ResultSet进行封装。由于SqlSession提供了众多方法对查询结果集进行处理,例如selectOne,selectMap、selectList等,因此根据接口中定义的方法的返回值的类型,来选择执行不同的方法,对ResultSet进行封装。

由于我们在SqlCommand对象构造的时候,将当前代理的接口和当执行被调用的方法method对象传递过去,其内部会通过以下方式查找对应的MappedStatement对象

String statementName = mapperInterface.getName() + "." + method.getName();
MappedStatement ms = configuration.getMappedStatement(statementName);

MappedStatement是我们在定义mapper映射文件时,内部的<insert>、<update>、<delete>、<select>元素的解析结果,每个这样的元素都会被解析成一个MappedStatement对象,并保存到Configuration类中的mappedStatements 属性中。

 protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>

其中key就是就是"namespace.id",由于我们定义的映射文件中,namespace属性就是XxxMapper的全路径,而<insert>等元素的id属性就是方法名,因此可以通过这种方式找到对应的解析后的MappedStatement。

由于这些标签(<insert>、<update>...)本身就代表了自己的sql类型,这些信息也会被保存到MappedStatement对象中,因此我们就可以通过SqlCommand的getType方法获取当前要执行的sql类型。

当调用SqlSession的相关方法时,第一个参数都是传入的都是command.getName方法,这个方法返回值,也是"namespace.id",不再做过多分析。