Mybatis动态代理解析
1、引言
Mybatis 作为一种持久层框架,它支持自定义 SQL、存储过程以及高级映射。里面大量使用了代理技术,这篇文章将从以下几个问题对 Mybatis 中的代理进行解析。
Mybatis Mapper 接口在无实现类的情况下,如何实现的动态代理。
JDK 动态代理为什么不能对非接口类进行代理。 2、JDK 动态代理2.1、基于接口的动态代理
•代码实例 public interface StudentDao { public void sayHello(); } public class StudentDaoImpl implements StudentDao{ @Override public void sayHello() { System.out.println("你好"); } } public class ProxyHandler implements InvocationHandler { private Object target; public ProxyHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("调用代理类"); return method.invoke(target, args); } public static void main(String[] args) { StudentDao studentDao = new StudentDaoImpl(); StudentDao proxy = (StudentDao) Proxy.newProxyInstance(studentDao.getClass().getClassLoader(), studentDao.getClass().getInterfaces(), new ProxyHandler(studentDao)); proxy.sayHello(); } }
上述代码运行后,可以发现对应 proxy 对象有一个特殊标记(Proxy拼接一个数字),这代表该对象的类型是一个代理对象。那么这个代理类是如何生成的,这些标记是如何构建的,下面我们走入 Proxy.newProxyInstance 里面去一瞅真相。
• newProxyInstance
该方法主要是生成代理类,其内部主要的流程为 getProxyClass0 与 newInstance ,其时序图如下所示:
1.getProxyClass0 会创建代理类
2.newInstance 会绑定代理类和增强类的关系
3、Mybatis 中的动态代理
Mybatis 未出现之前,开发人员需要在每个 mapper 层的实现类中重复进行如下操作,对数据库进行增删查改。 // 装载Mysql驱动 Class.forName(driveName); // 获取连接 con = DriverManager.getConnection(url, user, pass); // 创建Statement Statement state = con.createStatement(); // 构建SQL语句 String stuQuerySqlStr = "SELECT * FROM STUDENT"; // 执行SQL返回结果 ResultSet result = state.executeQuery(stuQuerySqlStr);
Mybatis 针对该痛点,利用 JDK 动态代理对以上逻辑进行封装,使用者只需要自定义 Mapper 和 xml 文件,利用注解或者 xml 的形式定义 SQL 语句,项目启动时通过解析器解析 SQL 语句组装为 Java 中的对象。
但是日常使用 Mybatis 过程中我们都是直接定义的 mapper 接口,没有定义其实现类,那么 JDK 动态代理是如何代理未实现的接口呐,和上文具有实现类的接口代理有什么区别?
•未实现类接口的动态代理 public interface Subject { String sayHello(); } public class ProxyInvocationHandler implements InvocationHandler { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("进入代理调用处理器 "); return "success"; } }
可以发现对于未实现类接口的动态代理,其 InvocationHandler 并未执行代理类的方法,而是执行完增强方法后就返回InvocationHandler 的返回值;而具有实现类接口的动态代理则会执行代理类方法并且返回其执行结果。
那么,Mybatis 对于 maaper 接口的代理方法是怎样的呐,其 InvocationHandler 的具体实现细节是什么?我们接着分析。
•MapperProxyFactory
该类是通过工厂模式去生成 mapper 代理类,其中 mapperInterface 是等待生成代理对象的接口。 public class MapperProxyFactory { private final Class mapperInterface; private final Map methodCache = new ConcurrentHashMap<>(); public MapperProxyFactory(Class mapperInterface) { this.mapperInterface = mapperInterface; } public Class getMapperInterface() { return mapperInterface; } public Map getMethodCache() { return methodCache; } @SuppressWarnings("unchecked") protected T newInstance(MapperProxy mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } public T newInstance(SqlSession sqlSession) { final MapperProxy mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } }
这里我们重点关注该类中的两个 newInstance 方法,由于 mapper 是一个接口,最终代理类的调用是执行的 InvocationHandler 中的 invoke 方法,所以最终的 mybatis 封装 mapper 的操作放在 MapperProxy 中。我们接着看里面的细节。
•MapperProxy
由于该类代码较多,我们这里只关注封装部分的方法逻辑,该部分逻辑获取对应 mapper 中的方法的增强实现,并调用其 invoke 方法。 MapperMethod类是整个代理机制的核心类,对SqlSession中的操作进行了封装使用。 该类里有两个内部类SqlCommand和MethodSignature。 SqlCommand用来封装CRUD操作,也就是我们在xml中配置的操作的节点。每个节点都会生成一个MappedStatement类。MethodSignature用来封装方法的参数以及返回类型 private MapperMethodInvoker cachedInvoker(Method method) throws Throwable { try { // A workaround for https://bugs.openjdk.java.net/browse/JDK-8161372 // It should be removed once the fix is backported to Java 8 or // MyBatis drops Java 8 support. See gh-1929 MapperMethodInvoker invoker = methodCache.get(method); if (invoker != null) { return invoker; } return methodCache.computeIfAbsent(method, m -> { if (m.isDefault()) { try { if (privateLookupInMethod == null) { return new DefaultMethodInvoker(getMethodHandleJava8(method)); } else { return new DefaultMethodInvoker(getMethodHandleJava9(method)); } } catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) { throw new RuntimeException(e); } } else { return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())); } }); } catch (RuntimeException re) { Throwable cause = re.getCause(); throw cause == null ? re : cause; } }
•MapperMethod
该类是整个代理机制的核心类,对SqlSession中的操作进行了封装使用。 从而实现了 CRUD,其最后还是回到 SqlSession 中进行调用数据库。 ublic 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; } case SELECT: if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } 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); if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) { result = Optional.ofNullable(result); } } 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; }