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:
org.apache.ibatis.builder.xml.XMLMapperBuilder#parse- parses the XML fileorg.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode- extracts statement nodesorg.apache.ibatis.scripting.xmltags.XMLLanguageDriver#createSqlSource- creates the SqlSource objectorg.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:
- Detects
sqlAnnotationTypeasSelect - Uses reflecsion to extract the SQL string from the annotation
- Delegates to
XMLLanguageDriver#createSqlSourcewith 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:
- Creates a
ProviderSqlSourceinstance with the annotated class and method - Stores the method reference rather than the SQL itself
- 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
StaticSqlSourceholding 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