只会写 Mapper 就敢说会 MyBatis?面试官:原理都没懂

108 阅读3分钟

有次我去面试,我人都傻了

“用过 MyBatis 吗?”
“用过!”
“那说说它怎么把接口变成 SQL 的?”
“……” 我当场裂开。

1. 日常场景

大多数开发都是下面这个流程:

Controller → Service → Mapper → XML

写多了,就以为MyBatis只是:

  1. 写个接口
  2. 写个 XML
  3. 调个方法

直到面试官问:“接口里可没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. 接口咋变成对象?

答案:MyBatisJDK动态代理给你整了一个代理对象。

  • 启动时扫描 Mapper 接口
  • 给每个接口整一个 MapperProxy
  • 代理对象拦着你的调用,再去找XML里的SQL

代码片段:

UserMapper mapper = (UserMapper) Proxy.newProxyInstance(
    UserMapper.class.getClassLoader(),
    new Class[]{UserMapper.class},
    new MapperProxy(sqlSession)
);

2. SQL什么时候拼?

代理对象收到调用后,干了两件小事:

  1. 根据方法名+参数,找到XML里那条SQL
  2. #{}换成真正的?,再把参数塞进去

也就是SqlSourceBoundSql的过程。

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》

《别再if套if了!Java中return的9种优雅写法》

《别学23种了!Java项目中最常用的6个设计模式,附案例》

《写给小公司前端的 UI 规范:别让页面丑得自己都看不下去》

《Vue3+TS设计模式:5个真实场景让你代码更优雅》