4.1 Mybatis动态代理
在使用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",不再做过多分析。