有次我去面试,我人都傻了
“用过 MyBatis 吗?”
“用过!”
“那说说它怎么把接口变成 SQL 的?”
“……”
我当场裂开。
1. 日常场景
大多数开发都是下面这个流程:
Controller → Service → Mapper → XML
写多了,就以为MyBatis只是:
- 写个接口
- 写个 XML
- 调个方法
直到面试官问:“接口里可没SQL,怎么就跑起来了?”,我才清醒的知道它没那么简单。
2. 案例
需求:查用户信息。
1. 先整接口
public interface UserMapper {
User selectById(Long id);
}
2. 再整XML
<select id="selectById" resultType="com.xxx.User">
SELECT * FROM t_user WHERE id = #{id}
</select>
3. Service里直接调
@Autowired
private UserMapper userMapper;
public User get(Long id) {
return userMapper.selectById(id);
}
一切看起来岁月静好,直到面试官问:“userMapper是个接口,Spring怎么给你塞了个能跑的对象?”,这话一出,直接懵了。
4. 答案在这
1. 接口咋变成对象?
答案:MyBatis用JDK动态代理给你整了一个代理对象。
- 启动时扫描 Mapper 接口
- 给每个接口整一个
MapperProxy - 代理对象拦着你的调用,再去找
XML里的SQL
代码片段:
UserMapper mapper = (UserMapper) Proxy.newProxyInstance(
UserMapper.class.getClassLoader(),
new Class[]{UserMapper.class},
new MapperProxy(sqlSession)
);
2. SQL什么时候拼?
代理对象收到调用后,干了两件小事:
- 根据方法名+参数,找到
XML里那条SQL - 把
#{}换成真正的?,再把参数塞进去
也就是SqlSource→BoundSql的过程。
3. 结果怎么塞进实体?
MyBatis偷偷调了:
ResultSet → 反射 → User 对象
全程靠ResultSetHandler干活,字段名对上就行,对不上就用@Results手动指。
5. 手写一个极简 MyBatis
1. 定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Select {
String value();
}
2. 接口
public interface UserMapper {
@Select("SELECT * FROM t_user WHERE id = #{id}")
User selectById(Long id);
}
3. 代理类
public class MyMapperProxy implements InvocationHandler {
private Connection conn;
public MyMapperProxy(Connection conn) {
this.conn = conn;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Select select = method.getAnnotation(Select.class);
String sql = select.value();
sql = sql.replace("#{id}", args[0].toString());
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
User u = new User();
u.setId(rs.getLong("id"));
u.setName(rs.getString("name"));
return u;
}
return null;
}
}
4. 启动器
Connection conn = DriverManager.getConnection("jdbc:mysql://...", "root", "root");
UserMapper mapper = (UserMapper) Proxy.newProxyInstance(
UserMapper.class.getClassLoader(),
new Class[]{UserMapper.class},
new MyMapperProxy(conn)
);
System.out.println(mapper.selectById(1L));
跑起来,控制台真的打出了用户信息。
6. 更多知识点
- 一级缓存:同一个
SqlSession,第二次查直接拿内存 - 二级缓存:跨
SqlSession,但要配<cache/> - 插件:分页、加密、脱敏全靠拦截器链
- 延迟加载:查用户不查部门,用到部门再发
SQL
若是面试能聊到这里,基本已经反杀了。
7. 总结
接口 + 代理 → 找SQL → 拼参数 → 反射塞结果。
08 彩蛋:面试现场原对话还原
面试官:“为啥#{}能防SQL注入?”
我:“因为它先占位,再设参数,JDBC会当字符串处理。”
面试官:“那${}呢?”
我:“直接拼,容易出事,一般不这样写。”
面试官:“行,过了。”
我:???
我是大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!
📌往期精彩
《Elasticsearch 太重?来看看这个轻量级的替代品 Manticore Search》
《别学23种了!Java项目中最常用的6个设计模式,附案例》