📌 异常处理:Java开发基于Spring Boot的异常处理框架设计:电商系统业务异常建模与全局统一响应实现
第2题:MyBatis 动态代理?
📚 回答:
- 核心考点: MyBatis 动态代理不是简单的"JDK 动态代理生成实现类"一句话能概括的。大厂面试中,面试官期望你深入理解 MapperProxy 的 invoke 拦截链路(为什么只拦截接口方法、Object 方法的过滤逻辑)、MapperMethod 的缓存设计(ConcurrentHashMap 缓存避免重复反射解析)、MapperRegistry 的注册与绑定机制,以及 MyBatis 为何选择 JDK 动态代理而非 CGLIB。面试官真正想判断的是:你是否能从框架设计、性能优化、设计模式三个维度,给出体系化的源码级分析。
1. 为什么需要动态代理?——从传统 DAO 到 Mapper 接口
- 1.1 传统 JDBC 的痛点 在没有 ORM 框架的时代,数据访问层需要大量样板代码:
// ❌ 传统 JDBC:大量重复代码
public class UserDaoImpl implements UserDao {
public User selectById(int id) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
ps.setInt(1, id);
rs = ps.executeQuery();
if (rs.next()) {
User user = new User();
user.setId(rs.getInt("id"));
user.setName(rs.getString("name"));
// … 十几个字段的映射
return user;
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
// 关闭资源… 极易出错
}
return null;
}
}
痛点:连接管理、SQL 编写、参数绑定、结果集映射、资源释放——每个 DAO 方法都要重复。
- 1.2 MyBatis 的解决方案:接口 + 动态代理 MyBatis 的核心设计哲学是:开发者只定义接口和 SQL,框架负责生成实现。
// ✅ MyBatis:只需接口 + XML/注解
public interface UserMapper {
User selectById(int id);
}
// 使用:直接调用接口方法,无需实现类
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectById(1); // 自动执行 SQL 并映射结果
关键问题:UserMapper 是接口,没有实现类,mapper.selectById(1) 为什么能执行?答案就是动态代理。
2. 动态代理的完整源码链路
- 2.1 核心组件关系图
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
│
▼
SqlSession.getMapper(Class<T>)
│
▼
Configuration.getMapper(Class<T>, SqlSession)
│
▼
MapperRegistry.getMapper(Class<T>, SqlSession)
│
├─ 1. 从 knownMappers 获取 MapperProxyFactory
│
▼
MapperProxyFactory.newInstance(SqlSession)
│
├─ 2. 创建 MapperProxy(InvocationHandler)
│
▼
Proxy.newProxyInstance(ClassLoader, Class[], InvocationHandler)
│
▼
返回代理对象(实现了 UserMapper 接口)
- 2.2 MapperRegistry:Mapper 接口的注册中心 MapperRegistry 是 Configuration 的内部组件,负责管理所有 Mapper 接口与代理工厂的映射:
public class MapperRegistry {
// 核心:接口 → 代理工厂的映射
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
knownMappers.put(type, new MapperProxyFactory<>(type));
}
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
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);
}
}
}
注册时机:MyBatis 启动时解析 mybatis-config.xml 中的 <mappers> 或扫描注解,调用 addMapper() 注册。
- 2.3 MapperProxyFactory:代理工厂 为每个 Mapper 接口创建代理对象的工厂:
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
// 缓存:Method → MapperMethod,避免重复解析
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
protected T newInstance(MapperProxy<T> mapperProxy) {
// JDK 动态代理:传入接口类加载器、接口数组、InvocationHandler
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(),
new Class[] { mapperInterface },
mapperProxy
);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
关键设计:methodCache 使用 ConcurrentHashMap 缓存 MapperMethod,避免每次调用都反射解析方法签名。
- 2.4 MapperProxy:InvocationHandler 实现 代理对象的方法调用都由 MapperProxy.invoke() 拦截:
public class MapperProxy<T> implements InvocationHandler, Serializable {
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 1. 过滤 Object 类的方法(toString/hashCode/equals等)
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
// 2. 获取缓存的 MapperMethod(或创建并缓存)
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 3. 执行 SQL
return mapperMethod.execute(sqlSession, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
// 首次调用:反射解析方法签名,创建 MapperMethod
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
}
为什么过滤 Object 方法? toString()/hashCode()/equals() 等 Object 方法不需要映射到 SQL,直接调用 InvocationHandler 自身的方法即可。
- 2.5 MapperMethod:SQL 执行的分发器 MapperMethod 封装了 SQL 指令和执行逻辑,是动态代理的核心执行单元:
public class MapperMethod {
private final SqlCommand command; // SQL 类型 + MappedStatement ID
private final MethodSignature methodSignature; // 方法签名(返回类型、参数等)
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.methodSignature = new MethodSignature(config, mapperInterface, method);
}
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
result = rowCountResult(sqlSession.insert(command.getName(), param(args)));
break;
}
case UPDATE: {
result = rowCountResult(sqlSession.update(command.getName(), param(args)));
break;
}
case DELETE: {
result = rowCountResult(sqlSession.delete(command.getName(), param(args)));
break;
}
case SELECT:
if (methodSignature.returnsVoid() && methodSignature.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (methodSignature.returnsMany()) {
result = sqlSession.selectList(command.getName(), param(args));
} else if (methodSignature.returnsMap()) {
result = sqlSession.selectMap(command.getName(), param(args), methodSignature.getMapKey());
} else if (methodSignature.returnsCursor()) {
result = sqlSession.selectCursor(command.getName(), param(args));
} else {
result = sqlSession.selectOne(command.getName(), param(args));
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
return result;
}
}
SqlCommand 的构造:通过 mapperInterface.getName() + "." + method.getName() 拼接出 MappedStatement 的唯一 ID(如 com.example.mapper.UserMapper.selectById),去 Configuration 中查找对应的 SQL 配置。
3. 为什么 MyBatis 选择 JDK 动态代理而非 CGLIB?
| 实现方式 | 基于接口生成代理 | 基于继承生成子类 |
| 要求 | 必须有接口 | 无需接口,但不能代理 final 类 |
| 性能 | 反射调用稍慢 | 字节码生成,调用更快 |
| 依赖 | JDK 内置,无额外依赖 | 需引入 CGLIB 库 |
| 代理范围 | 只能代理接口方法 | 可代理所有非 final 方法 |
| 启动开销 | 较小 | 较大(需生成字节码) |
MyBatis 选择 JDK 动态代理的原因:
CGLIB 的适用场景:Spring AOP 中需要代理类(非接口)时使用 CGLIB,但 MyBatis 的 Mapper 层不需要。
4. 方法缓存机制:为什么用 ConcurrentHashMap?
- 4.1 缓存设计动机 如果每次调用 Mapper 方法都通过反射解析方法签名(返回类型、参数类型、注解等),性能开销巨大。methodCache 的设计是用空间换时间:
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();
| 首次调用 | 反射解析 + 创建 MapperMethod | 反射解析 + 创建 + 放入缓存 |
| 后续调用 | 再次反射解析 | 直接从缓存获取,O(1) |
| 线程安全 | 无需考虑 | ConcurrentHashMap 保证 |
-
4.2 为什么是 ConcurrentHashMap 而非 HashMap? Mapper 代理对象可能在多线程环境下使用(如 Spring 的 Singleton Bean),ConcurrentHashMap 保证线程安全的同时提供高并发读取性能。
-
4.3 缓存的生命周期 缓存绑定在 MapperProxyFactory 实例上,而 MapperProxyFactory 绑定在 MapperRegistry 中,随 Configuration 生命周期存在。应用重启后缓存失效,重新构建。
5. 与 Spring 集成时的动态代理
- 5.1 Spring 管理 Mapper 的生命周期 在 Spring + MyBatis 项目中,通常使用 MapperScannerConfigurer 或 @MapperScan 自动扫描 Mapper 接口:
@Configuration
@MapperScan("com.example.mapper")
public class MyBatisConfig {
// Spring 会为每个 Mapper 接口生成 BeanDefinition
// 实际注入的是 MyBatis 的动态代理对象
}
- 5.2 Spring 注入的是代理对象 当 Service 层 @Autowired 注入 Mapper 时,实际注入的是 MapperProxy 生成的 JDK 动态代理对象:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper; // 实际类型:com.sun.proxy.$ProxyXX
public User getUser(int id) {
return userMapper.selectById(id); // 走动态代理 → MapperProxy.invoke()
}
}
- 5.3 事务与 SqlSession 的绑定 Spring 通过 SqlSessionTemplate 管理 SqlSession 生命周期,保证事务内多个 Mapper 调用共享同一个 SqlSession(一级缓存生效):
@Transactional
public void updateUser(User user) {
userMapper.update(user); // 使用同一个 SqlSession
logMapper.insertLog("update"); // 同一个 SqlSession,同一事务
// 事务提交时统一 flush
}
6. 生产环境避坑指南
- 6.1 严禁在 Mapper 接口中写业务逻辑 Mapper 接口只应定义数据访问方法,业务逻辑应在 Service 层:
// ❌ 错误:Mapper 中写业务逻辑
public interface UserMapper {
default User getUserWithOrders(int id) {
User user = selectById(id);
// 查询订单… 业务逻辑不应在 Mapper 中
return user;
}
}
// ✅ 正确:Mapper 只定义数据访问
public interface UserMapper {
User selectById(int id);
List<Order> selectOrdersByUserId(int userId);
}
// Service 层处理业务逻辑
@Service
public class UserService {
public UserDTO getUserDetail(int id) {
User user = userMapper.selectById(id);
List<Order> orders = orderMapper.selectOrdersByUserId(id);
return new UserDTO(user, orders);
}
}
- 6.2 注意 Mapper 接口与 XML 的绑定一致性 namespace 和 id 必须严格匹配,否则抛出 Invalid bound statement:
<!– ❌ 错误:namespace 或 id 不匹配 –>
<mapper namespace="com.example.mapper.UserMapper">
<select id="selectById" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
</mapper>
<!– 接口方法名必须是 selectById,不能是 selectByID 或 select_user_by_id –>
- 6.3 多参数必须使用 @Param 多参数时,MyBatis 无法通过参数名映射(JDK 8 编译参数名需开启 -parameters),必须使用 @Param:
// ❌ 错误:多参数无 @Param,XML 中 #{id} 无法映射
User selectByIdAndName(int id, String name);
// ✅ 正确:使用 @Param 指定参数名
User selectByIdAndName(@Param("id") int id, @Param("name") String name);
- 6.4 避免循环依赖 Mapper 接口之间不要相互注入,防止 Spring 循环依赖:
// ❌ 错误:循环依赖
public interface UserMapper {
@Autowired
OrderMapper orderMapper; // Mapper 注入 Mapper,循环依赖风险
}
- 6.5 接口方法名与 XML id 大小写敏感 MyBatis 的 MappedStatement ID 是大小写敏感的,方法名 selectById 和 XML 中的 selectByid 会被视为不同语句。
7. 面试官追问与高分回答模板
-
追问 1:“MyBatis 动态代理的底层原理是什么?”
低分回答:“通过 JDK 动态代理生成 Mapper 接口的实现类。”(太浅,没有触及源码链路)
高分回答:
"MyBatis 动态代理的完整链路分为五个阶段:
- 注册阶段:MyBatis 启动时解析 mybatis-config.xml 中的 <mappers>,通过 MapperRegistry.addMapper() 将接口与 MapperProxyFactory 绑定到 knownMappers 中。
- 获取阶段:调用 sqlSession.getMapper(UserMapper.class) 时,MapperRegistry 从 knownMappers 获取 MapperProxyFactory,调用 newInstance(sqlSession) 创建代理。
- 代理创建:MapperProxyFactory 创建 MapperProxy(实现 InvocationHandler),通过 Proxy.newProxyInstance() 生成代理对象。
- 方法拦截:调用 mapper.selectById(1) 时,MapperProxy.invoke() 拦截,过滤 Object 方法,从 methodCache 获取 MapperMethod。
- SQL 执行:MapperMethod.execute() 根据 SqlCommand 的类型(SELECT/INSERT/UPDATE/DELETE)和方法签名(返回类型、参数),调用 SqlSession 的对应方法执行 SQL。
-
追问 2:“为什么 MyBatis 选择 JDK 动态代理而不是 CGLIB?”
高分回答:
"MyBatis 选择 JDK 动态代理基于三个核心原因:
- Mapper 本身就是接口:MyBatis 的设计哲学是接口 + XML/注解,JDK 动态代理天然适合代理接口。CGLIB 基于继承,需要代理类,而 Mapper 层没有类需要代理。
- 无额外依赖:JDK 动态代理是 Java 内置能力,无需引入 CGLIB 等第三方库,减少依赖冲突和包体积。
- 代理范围精准:只需要代理接口中定义的方法,Object 方法(toString/hashCode/equals)直接过滤。CGLIB 会代理所有非 final 方法,范围过大。
-
追问 3:“MapperProxy 的 invoke 方法为什么要过滤 Object 类的方法?”
高分回答:
"MapperProxy.invoke() 中过滤 Object 方法的原因是:
- 语义不符:toString()/hashCode()/equals() 等 Object 方法是 Java 对象的基础方法,不应映射到 SQL 执行。如果代理这些方法,调用 mapper.toString() 会触发数据库查询,毫无意义。
- 实现简单:Object 方法直接调用 method.invoke(this, args),在 MapperProxy 实例上执行,无需经过 SQL 解析和执行链路。
- 避免意外:如果不过滤,代理对象的 equals() 可能被误用,导致意外的 SQL 执行或比较逻辑错误。
-
追问 4:“MapperMethod 的缓存机制是怎么设计的?为什么用 ConcurrentHashMap?”
高分回答:
"MapperMethod 的缓存设计是用空间换时间的经典案例:
- 缓存位置:MapperProxyFactory 中维护 Map<Method, MapperMethod> methodCache,每个 Mapper 接口有一个独立的缓存。
- 缓存时机:首次调用某个方法时,通过反射解析方法签名(返回类型、参数类型、注解等)创建 MapperMethod,放入缓存。后续调用直接从缓存获取,O(1) 时间复杂度。
- 线程安全:使用 ConcurrentHashMap 而非 HashMap,因为 Mapper 代理对象在 Spring 中通常是 Singleton,多线程并发访问时需要线程安全。ConcurrentHashMap 的读操作无锁,性能高。
- 缓存生命周期:随 Configuration 生命周期存在,应用重启后失效。由于 Mapper 接口和方法在运行期不变,缓存不会过期,也无需清理。
-
追问 5:“Spring 集成 MyBatis 时,注入的 Mapper 是什么对象?”
高分回答:
"Spring 注入的 Mapper 是 MyBatis 生成的 JDK 动态代理对象,具体类型是 com.sun.proxy.$ProxyXX(JDK 代理类的命名规则)。
具体流程:
- Spring 启动时,@MapperScan 或 MapperScannerConfigurer 扫描 Mapper 接口,为每个接口创建 BeanDefinition。
- BeanDefinition 的 beanClass 设置为 MapperFactoryBean,实际创建 Bean 时调用 MapperFactoryBean.getObject()。
- MapperFactoryBean 内部调用 SqlSession.getMapper(),走 MyBatis 的动态代理链路生成代理对象。
- Service 层 @Autowired 注入的就是这个代理对象。
-
追问 6:“如果 Mapper 接口和 XML 的 namespace 不匹配会怎样?”
高分回答:
"如果 namespace 不匹配或 id 不匹配,MyBatis 会抛出 Invalid bound statement (not found) 异常。
具体匹配规则:
- namespace 必须等于 Mapper 接口的全限定名(如 com.example.mapper.UserMapper)。
- <select>/<insert> 等标签的 id 必须等于接口方法名(如 selectById)。
- MappedStatement 的唯一 ID 是 namespace + "." + id(如 com.example.mapper.UserMapper.selectById)。
- namespace 写错包名或类名
- 方法名大小写不匹配(如 XML 中 selectByid,接口中 selectById)
- XML 文件未被扫描(mybatis-config.xml 中未配置 <mappers> 或路径错误)
- 多参数未加 @Param,导致参数绑定失败
核心设计亮点是 methodCache 用 ConcurrentHashMap 缓存 MapperMethod,避免每次调用都反射解析方法签名。"
性能方面,虽然 CGLIB 的字节码生成在调用时更快,但 MyBatis 通过 MapperMethod 缓存避免了重复反射,实际瓶颈在数据库 I/O 而非代理层。JDK 动态代理的性能足够且更简单。"
源码中的判断逻辑是 if (Object.class.equals(method.getDeclaringClass())),通过方法声明类是否为 Object 来精准过滤。"
这个设计避免了每次调用都反射解析,大幅提升频繁调用的性能。"
事务方面,Spring 通过 SqlSessionTemplate 管理 SqlSession,保证 @Transactional 方法内多个 Mapper 调用共享同一个 SqlSession,一级缓存生效,事务统一提交/回滚。"
常见错误:
排查方法:检查 target/classes 下 XML 是否编译进去,检查日志中的 Parsed mapper file 记录。"
8. 方案选型速查表
| 纯 MyBatis 项目 | JDK 动态代理(原生) | 无额外依赖,接口设计 |
| Spring + MyBatis | @MapperScan + 动态代理 | Spring 管理生命周期,事务集成 |
| MyBatis-Plus | 继承 BaseMapper + 动态代理 | 增强 CRUD,仍走动态代理 |
| 需要代理类(非接口) | CGLIB(Spring AOP) | MyBatis 不支持,需 Spring AOP |
| 高并发频繁调用 | 确保 methodCache 生效 | 缓存避免重复反射 |
| 多数据源 | 多个 SqlSessionFactory | 每个数据源独立 Configuration |
💡 面试官想要的满分总结:
MyBatis 动态代理的核心设计在于将接口方法调用自动映射为 SQL 执行,开发者只需定义接口和 SQL 配置,框架负责生成实现。完整链路是:MapperRegistry 注册 → MapperProxyFactory 创建代理 → MapperProxy.invoke() 拦截 → MapperMethod.execute() 分发执行。
理解动态代理必须抓住三个关键点:
与 Spring 集成时,注入的 Mapper 是 JDK 动态代理对象,Spring 通过 SqlSessionTemplate 管理 SqlSession 生命周期,保证事务内一级缓存生效。生产环境中最常见的坑是 namespace/id 不匹配、多参数未加 @Param、Mapper 接口中写业务逻辑——这些都会导致 Invalid bound statement 或代码耦合。
觉得对您有帮助,麻烦点点关注啦,您的关注是我创作的最大动力~ 🎯


