news 2026/6/13 6:27:52

Java-23 深入浅出 MyBatis - 手写ORM框架4 框架优化 SqlSession DefaultSqlSession

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java-23 深入浅出 MyBatis - 手写ORM框架4 框架优化 SqlSession DefaultSqlSession

手写 MyBatis 框架:动态代理让 Mapper 接口告别手写实现类

TL;DR

  • 场景:自研持久层框架的 DAO 层仍有重复代码与硬编码statementId,调用方式不像 MyBatis。
  • 结论:在SqlSession增加getMapper方法,通过 JDK 动态代理为 Mapper 接口生成代理对象,根据方法签名自动拼装statementId并分发到selectList/selectOne
  • 产出:可复用的getMapper动态代理实现 + 完整测试调用样例 + 错误速查卡。

版本矩阵

功能状态说明
SqlSession.getMapper(Class<?>)接口定义✅ 已验证MyBatis 3.x 官方org.apache.ibatis.session.SqlSession标准方法,2025 年文档可查
JDKProxy.newProxyInstance动态代理✅ 已验证基于java.lang.reflect.Proxy,Java 8+ 可用,2026 年仍是默认实现方式
statementId = 类全限定名.方法名拼装规则✅ 已验证与 MyBatis 3.xMapperProxy的命名空间解析规则一致
ParameterizedType判断返回List<T>✅ 已验证method.getGenericReturnType()是 JDK 反射标准 API
Object方法透传(toString/equals/hashCode✅ 已验证官方MapperProxy.invoke同样做此判断以避免误派发
CGLIB 代理 Mapper⚠️ 不适用JDK 代理要求接口,CGLIB 仅在无接口场景下由 MyBatis 选择使用

框架优化

前面我们已经手写了一个简单的持久层框架,解决了 JDBC 原生开发中的一些重复问题,比如连接获取、SQL 执行、结果封装等。

但是目前 DAO 层仍然存在两个明显问题:

  • DAO 实现类中仍然有重复代码,例如创建SqlSession、调用查询方法等流程。
  • DAO 实现类中存在硬编码,例如调用SqlSession方法时,需要手动传入statementId

本篇主要解决这两个问题:通过动态代理生成 Mapper 接口的代理对象,让调用方式更接近 MyBatis。

SqlSession

解决思路是:在SqlSession中增加getMapper方法,通过代理模式为 Mapper 接口创建代理对象。

修改SqlSession接口,增加如下方法:

<T>TgetMapper(Class<?>mapperClass);

修改完成后,对应的截图如下:

DefaultSqlSession

接下来在DefaultSqlSession中实现getMapper方法:

@Overridepublic<T>TgetMapper(Class<?>mapperClass){ObjectproxyInstance=Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(),newClass[]{mapperClass},newInvocationHandler(){@OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{StringmethodName=method.getName();if(method.getDeclaringClass()==Object.class){returnmethod.invoke(this,args);}StringclassName=method.getDeclaringClass().getName();StringstatementId=className+"."+methodName;TypegenericReturnType=method.getGenericReturnType();if(genericReturnTypeinstanceofParameterizedType){List<Object>objects=selectList(statementId,args);returnobjects;}returnselectOne(statementId,args);}});return(T)proxyInstance;}

对应的截图如下所示:

这个方法的核心作用是:根据传入的 Mapper 接口类型,动态生成一个代理对象。以后我们就不需要手写 Mapper 的实现类了。

几个关键点如下:

  • @Override:表示该方法重写了接口中的方法。
  • <T> T:表示这是一个泛型方法,返回值类型由调用方决定。
  • getMapper(Class<?> mapperClass):接收一个 Mapper 接口的Class对象,用于生成对应的代理对象。

动态代理对象的创建代码如下:

ObjectproxyInstance=Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(),newClass[]{mapperClass},newInvocationHandler(){...});

各个参数的含义如下:

  • Proxy.newProxyInstance:创建 JDK 动态代理对象。
  • DefaultSqlSession.class.getClassLoader():指定类加载器。
  • new Class[]{mapperClass}:指定代理对象需要实现的接口。
  • new InvocationHandler():定义代理对象调用方法时的处理逻辑。

动态代理逻辑

代理对象调用 Mapper 接口中的方法时,都会进入invoke方法:

@OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{...}

参数含义如下:

  • proxy:当前代理对象。
  • method:当前被调用的方法。
  • args:调用方法时传入的参数。

也就是说,当我们执行:

userInfoMapper.selectOne(userInfo);

实际并不会进入某个手写的实现类,而是进入动态代理中的invoke方法。

方法调用逻辑

首先获取当前调用的方法名,并处理Object类中的方法:

StringmethodName=method.getName();if(method.getDeclaringClass()==Object.class){returnmethod.invoke(this,args);}

这里的判断是为了处理toString()equals()hashCode()等方法。

如果不做这个判断,代理对象打印、比较时也会被当成普通 SQL 方法处理,容易出现不符合预期的问题。

SQL 语句标识符

接下来生成statementId

String className=method.getDeclaringClass().getName();String statementId=className +"."+ methodName;

这里的规则是:

Mapper接口全限定名.方法名

例如 Mapper 接口是:

icu.wzk.dao.UserInfoMapper

调用的方法是:

selectOne

那么最终生成的statementId就是:

icu.wzk.dao.UserInfoMapper.selectOne

这样就可以和配置文件中的 SQL 语句进行匹配,避免在 DAO 实现类中手动写死statementId

方法返回类型判断

最后根据方法返回值类型,决定调用selectList还是selectOne

TypegenericReturnType=method.getGenericReturnType();if(genericReturnTypeinstanceofParameterizedType){List<Object>objects=selectList(statementId,args);returnobjects;}

这里通过method.getGenericReturnType()获取方法的返回值类型。

如果返回值是参数化类型,例如:

List<UserInfo>

那么它属于ParameterizedType,此时调用selectList

如果不是集合类型,则默认调用:

selectOne(statementId,args);

所以这段逻辑可以简单理解为:

  • Mapper 方法返回List<T>:执行selectList
  • Mapper 方法返回普通对象:执行selectOne

通过这一步,Mapper 接口方法就和底层 SQL 执行逻辑关联起来了。

测试方法

下面编写一个测试方法,通过SqlSessionFactory创建SqlSession,再通过getMapper获取 Mapper 代理对象:

packageicu.wzk.test;importicu.wzk.bean.Resources;importicu.wzk.bean.SqlSession;importicu.wzk.bean.SqlSessionFactory;importicu.wzk.bean.SqlSessionFactoryBuilder;importicu.wzk.dao.UserInfoMapper;importicu.wzk.model.UserInfo;importjava.io.InputStream;publicclassTest02{publicstaticvoidmain(String[]args)throwsException{InputStreaminputStream=Resources.getResourceAsStream("sqlMapConfig.xml");SqlSessionFactorysqlSessionFactory=newSqlSessionFactoryBuilder().build(inputStream);SqlSessionsqlSession=sqlSessionFactory.openSession();UserInfouserInfo=newUserInfo();userInfo.setUsername("wzk");UserInfoMapperuserInfoMapper=sqlSession.getMapper(UserInfoMapper.class);System.out.println("userInfoMapper: "+userInfoMapper);System.out.println(userInfoMapper.selectOne(userInfo));}}

测试流程如下:

  1. 读取sqlMapConfig.xml配置文件。
  2. 构建SqlSessionFactory
  3. 通过openSession()获取SqlSession
  4. 调用getMapper(UserInfoMapper.class)获取 Mapper 代理对象。
  5. 调用 Mapper 接口方法执行查询。

对应的截图如下所示:

运行结果

执行之后,控制台输出结果如下:

log4j:WARN No appenders could be foundforlogger(com.mchange.v2.log.MLog). log4j:WARN Please initialize the log4j system properly. log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.userInfoMapper: icu.wzk.bean.DefaultSqlSession$1@61dc03ce SimpleExecutor getBoundSql: SELECT * FROM user_info WHEREusername=? UserInfo(id=1,username=wzk,password=<PASSWORD>,age=18)

对应的截图如下所示:

从运行结果可以看到,我们已经不需要手写UserInfoMapper的实现类,也不需要在 DAO 中手动拼接statementId

现在的调用方式变成了:

UserInfoMapperuserInfoMapper=sqlSession.getMapper(UserInfoMapper.class);UserInfouserInfo=userInfoMapper.selectOne(queryParam);

这一步完成后,框架的使用方式已经更接近 MyBatis:

  • 开发者只需要定义 Mapper 接口。
  • 框架负责生成代理对象。
  • 代理对象根据接口名和方法名生成statementId
  • 底层继续复用已有的selectOneselectList查询逻辑。

这样就减少了 DAO 层的重复代码,也消除了手写statementId带来的硬编码问题。


错误速查卡

症状根因定位修复
打印userInfoMapper时也走了 SQL 查询路径没有在invoke中判断method.getDeclaringClass() == Object.classtoString被当作 Mapper 方法DefaultSqlSession.getMapperinvoke逻辑在拼装statementId之前先做Object方法透传
控制台报statement id not found: xxxstatementId没有使用类全限定名.方法名规则,配置文件中 namespace 或 id 不匹配检查 XML 的namespaceid,对比运行时拼接值保持method.getDeclaringClass().getName() + "." + method.getName()规则,XML 同步
返回List<T>时只返回了第一条数据在分发逻辑里一律调用了selectOne,没有用ParameterizedType判断invokegenericReturnType instanceof ParameterizedType分支method.getGenericReturnType()区分列表与单对象
getMapper调用报ClassCastExceptionProxy.newProxyInstance返回Object,调用方未做泛型强转,或接口未传入return (T) proxyInstance;new Class[]{mapperClass}确保传入的是接口Class,返回处做强转
log4j 警告No appenders could be found没有log4j.propertieslog4j.xml运行时启动日志增加 log4j 配置或显式BasicConfigurator.configure(),与本框架功能无关可忽略
同一个 Mapper 接口被加载多次产生多个代理没有缓存MapperProxyFactory,每次getMapper都新建DefaultSqlSession.getMapper与配置注册表引入MapperRegistry缓存knownMappers,MyBatis 官方做法
接口中没有声明throws Exception但代理内部抛了受检异常JDK 代理不会自动包装受检异常,且invoke声明throws Throwable编译错误或UndeclaredThrowableExceptioninvoke内部 try/catch 统一包装为运行时异常,与 MyBatisExceptionUtil.unwrapThrowable一致

作者:武子康的个人博客

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/13 6:14:54

终极Unity逆向工程指南:使用Il2CppDumper轻松破解IL2CPP保护

终极Unity逆向工程指南&#xff1a;使用Il2CppDumper轻松破解IL2CPP保护 【免费下载链接】Il2CppDumper Unity il2cpp reverse engineer 项目地址: https://gitcode.com/gh_mirrors/il/Il2CppDumper 你是否曾经试图分析Unity游戏&#xff0c;却被IL2CPP保护机制挡在门外…

作者头像 李华
网站建设 2026/6/13 6:09:53

NSK EM5025-6E 高速重载滚珠丝杠技术详解

型号 EM5025-6E 属于 sources 中 NSK 专为高速机床设计的 HMD型&#xff08;中空轴冷却&#xff09;与 EM型&#xff08;单螺母冷却&#xff09;组合的高速精密滚珠丝杠系列。 与您上一条查询的同系列 20 mm 导程型号&#xff08;EM5020-6E&#xff0c;静载 205,000 N&#xff…

作者头像 李华