Why MyBatis Mapper Interfaces Don't Need Implementation Classes

MyBatis generates mapper implementations at runtime using JDK dynamic proxies; the proxy intercepts interface calls, builds a statementId from the interface’s fully‑qualified name and method name, looks up the corresponding MappedStatement via the namespace‑id convention, and delegates execution to SqlSession, eliminating the need for a concrete class.

Java Architect Handbook
Java Architect Handbook
Java Architect Handbook
Why MyBatis Mapper Interfaces Don't Need Implementation Classes

Proxy Creation

When sqlSession.getMapper(UserMapper.class) is called, MyBatis looks up the MapperProxyFactory in the Configuration 's MapperRegistry, creates a JDK dynamic proxy that implements UserMapper, and stores a MapperProxy (which implements InvocationHandler) as the invocation handler.

Method Invocation

Calling mapper.selectById(1L) triggers MapperProxy.invoke(). The method first checks whether the invoked method belongs to java.lang.Object; if so it is executed directly. Otherwise the java.lang.reflect.Method is wrapped into a MapperMethod (cached in methodCache). MapperMethod parses the method signature, determines the SQL command type, builds a statementId composed of the interface's fully‑qualified name and the method name, and delegates to SqlSession.

public class MapperProxy<T> implements InvocationHandler {
    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 {
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        }
        MapperMethod mapperMethod = cachedMapperMethod(method);
        return mapperMethod.execute(sqlSession, args);
    }
    // ...
}
public class MapperMethod {
    private final SqlCommand command; // SQL type + statementId
    private final MethodSignature method; // method signature info

    public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
        this.command = new SqlCommand(config, mapperInterface, method);
        this.method = new MethodSignature(config, method);
    }

    public Object execute(SqlSession sqlSession, Object[] args) {
        switch (command.getType()) {
            case SELECT:
                return method.returnsMany()
                    ? sqlSession.selectList(command.getName(), args)
                    : sqlSession.selectOne(command.getName(), args);
            case INSERT:
                return sqlSession.insert(command.getName(), args);
            case UPDATE:
                return sqlSession.update(command.getName(), args);
            case DELETE:
                return sqlSession.delete(command.getName(), args);
            default:
                throw new BindingException("Unknown execution method for: " + command.getName());
        }
    }
    // ...
}
SqlSession

uses the statementId as a key to retrieve the corresponding MappedStatement from Configuration.mappedStatements. The MappedStatement contains the SQL text, parameter mappings, result mappings, etc. Depending on the command type (SELECT, INSERT, UPDATE, DELETE), SqlSession calls selectOne / selectList / insert / update / delete.

Naming Convention

The link between the mapper interface and the XML is established by a strict naming convention:

The XML <mapper> element's namespace must equal the interface's fully‑qualified name.

Each SQL tag's id must match the corresponding method name.

The concatenation namespace + "." + id forms the statementId used as the lookup key.

<mapper namespace="com.example.mapper.UserMapper">
    <select id="selectById" resultType="User">
        SELECT * FROM t_user WHERE id = #{id}
    </select>
</mapper>

package com.example.mapper;
public interface UserMapper {
    User selectById(Long id);
}

If the namespace or id does not match, MyBatis throws a BindingException at call time, e.g.

Invalid bound statement (not found): com.example.mapper.UserMapper.selectById

.

Core Classes

MapperProxy

– implements InvocationHandler, intercepts mapper method calls. MapperProxyFactory – creates the JDK dynamic proxy instance. MapperMethod – encapsulates method metadata, builds SqlCommand, routes to SqlSession. MapperRegistry – registers Class<T>MapperProxyFactory<T> mappings. MappedStatement – holds complete SQL metadata (SQL text, parameter and result mappings).

Why JDK Dynamic Proxy Instead of CGLIB

Requirement : JDK dynamic proxy works only with interfaces; CGLIB can generate subclasses for concrete classes.

Mapper Interface : The mapper is an interface, so JDK dynamic proxy fits naturally (✅). CGLIB cannot subclass an interface (❌).

Performance : JDK dynamic proxy is slightly slower before JDK 8, but the gap becomes negligible after JDK 8; CGLIB is marginally faster.

Dependency : JDK dynamic proxy is built into the JDK, requiring no extra libraries; CGLIB adds an external dependency.

Because the mapper is an interface, generating a subclass (CGLIB) is impossible, making JDK dynamic proxy the only practical choice.

Common Pitfalls

The generated proxy class ( $Proxy0) exists only in memory unless the JVM option -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true is enabled.

Mismatched namespace or id results in a BindingException at runtime.

Mixing annotation‑based and XML‑based mappings for the same method causes conflicts; a method can use either annotations or XML, but not both simultaneously.

Typical Interview Follow‑up Questions

When is the mapper registered? – During MyBatis initialization by XMLMapperBuilder or via @MapperScan (Spring).

Can an interface use both annotations and XML? – Yes, but a single method cannot have both an annotation and an XML definition.

What happens if the method name and XML id differ? – A BindingException is thrown at call time.

Call Chain Overview

The full call chain can be divided into two phases:

Proxy Creation Phase (steps ①‑④) : sqlSession.getMapper → lookup MapperProxyFactory in MapperRegistry → create JDK dynamic proxy implementing the mapper interface → proxy holds a MapperProxy instance.

Method Invocation Phase (steps ⑤‑⑨) : Application calls mapper.selectByIdMapperProxy.invoke wraps the method into a cached MapperMethodMapperMethod builds statementId → delegates to SqlSession which executes the appropriate SQL operation.

Diagram
Diagram
Call chain diagram
Call chain diagram
JavaMyBatisSQL MappingJDK Dynamic ProxyMapperMethodMapperProxy
Java Architect Handbook
Written by

Java Architect Handbook

Focused on Java interview questions and practical article sharing, covering algorithms, databases, Spring Boot, microservices, high concurrency, JVM, Docker containers, and ELK-related knowledge. Looking forward to progressing together with you.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.