MyBatis Development: XML vs Annotations and Underlying Implementation

MyBatis supports two development approaches: XML-based and annotation-based. The annotation approach further splits into two variants, resulting in three distinct writing styles.

Three Implementation Approaches

1. XML-Based Approach

Define the mapper interface:

public interface UserMapper {
    Integer queryAgeByName(String name);
}

Corresponding XML configuration:

<mapper namespace="com.example.mapper.UserMapper">
    <select id="queryAgeByName" resultType="java.lang.Integer">
        SELECT age FROM user_info WHERE name = #{name}
    </select>
</mapper>

2. @Select Annotation Approach

@Select("SELECT age FROM user_info WHERE name = #{name}")
Integer annotationQueryAgeByName(String name);

3. @SelectProvider Annotation Approach

@SelectProvider(type = UserInfoSql.class, method = "buildQueryAgeSql")
Integer classQueryAgeByName(String name);

The SQL provider class:

public class UserInfoSql {
    public String buildQueryAgeSql() {
        return "SELECT age FROM user_info WHERE name = #{name}";
    }
}

Underlying Implementation Analysis

Entry Point

Spring Boot loads MyBatis through the spring.factories file in mybatis-spring-boot-autoconfigure. The sqlSessionFactory method serves as the entry point, where you can add breakpoints to begin debugging.

XML Parsing Process

For XML-based configurations, the parsing flows through:

  1. org.apache.ibatis.builder.xml.XMLMapperBuilder#parse - parses the XML file
  2. org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode - extracts statement nodes
  3. org.apache.ibatis.scripting.xmltags.XMLLanguageDriver#createSqlSource - creates the SqlSource object
  4. org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode - strips out the complete SQL statement

Annotation Parsing Process

For annotation-based configurations, the org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parse method handles processing. It iterates through methods in the mapper class and calls getSqlSourceFromAnnotations.

@Select Processing

When encountering @Select, the framework:

  1. Detects sqlAnnotationType as Select
  2. Uses reflecsion to extract the SQL string from the annotation
  3. Delegates to XMLLanguageDriver#createSqlSource with the SQL string

Key insight: Internally, @Select still leverages XML parsing logic. The SQL can include <script> tags for complex conditions:

@Select("<script>SELECT * FROM user_info <when test='startPage != null and pageSize != null'>LIMIT #{startPage},#{pageSize}</when></script>")

This mirrors XML development patterns exactly.

@SelectProvider Processing

When encountering @SelectProvider:

  1. Creates a ProviderSqlSource instance with the annotated class and method
  2. Stores the method reference rather than the SQL itself
  3. At execution time, invokes the method via reflection to obtain the SQL

SqlSource Types

Regardless of the approach, MyBatis ultimately produces a SqlSource object:

Approach SqlSource Type
XML RawSqlSource
@Select RawSqlSource
@SelectProvider ProviderSqlSource
  • RawSqlSource: Contains a StaticSqlSource holding the pre-parsed SQL string (variables and conditions remain unreplaced)
  • ProviderSqlSource: Holds a method reference that gets invoked at execution time

SQL Execution Flow

At runtime, the SqlSource#getBoundSql method generates the final SQL. The flow differs slightly between implementations:

RawSqlSource path:

StaticSqlSource#getBoundSql
  ↓
prepareStatement (via PreparedStatement)
  ↓
Execute query

ProviderSqlSource path:

ProviderSqlSource#getBoundSql
  ↓
Invoke providerMethod via reflection
  ↓
SqlSourceBuilder#parse
  ↓
StaticSqlSource#getBoundSql
  ↓
Execute query

Common Pitfall: Primitive vs Wrapper Return Types

Consdier this scenario with a mapper method:

// Return type: primitive int
int annotationQueryAgeByName(String name);

When querying an existing user, this works fine. However, when querying a non-existent user:

int age = userMapper.annotationQueryAgeByName("nonExistent");

A BindingException occurs:

Could not set property 'age' of 'test.service.UserService' 
  with value 'null' Cause: java.lang.IllegalArgumentException: 
  Cannot set property age of primitive type

The source code reveals the check in org.apache.ibatis.executor.resultset.DefaultResultSetHandler#validateColumnName:

if (value == null && !method.getReturnType().isPrimitive() 
    && !method.getReturnType().equals(Void.TYPE)) {
    throw new BindingException(...);
}

This validation prevents NullPointerException from auto-unboxing, but the real issue occurs when:

// Method declares wrapper type
Integer annotationQueryAgeByName(String name);

// But caller uses primitive
int age = userMapper.annotationQueryAgeByName("nonExistent");

The result is null being assigned to a primitive int, triggering auto-unboxing and thrwoing NullPointerException.

Elegant Implementation Pattern

The selectOne method in DefaultSqlSession demonstrates an elegant pattern:

public <T> T selectOne(String statement, Object parameter) {
    List<T> list = this.selectList(statement, parameter);
    if (list.size() == 1) {
        return list.get(0);
    } else if (list.size() > 1) {
        throw new TooManyResultsException(...);
    }
    return null;
}

Rather than duplicating logic, selectOne reuses selectList and handles the size validation. This avoids code duplication while maintaining clear separation of concerns.

Practical Recommendations

  • Simple CRUD operations: Annotations work well and improve readability
  • Complex dynamic SQL with conditional logic: XML configuration remains cleaner and more maintainable
  • Hybrid approach: Use annotations for straightforward queries, XML for dynamic or complex ones
  • Avoid over-engineering: Don't abandon XML entirely just because annotations exist

Tags: MyBatis java Annotation XML ORM

Posted on Sun, 06 Sep 2026 16:12:39 +0000 by nadeauz