Here is the translated article body.
You add MyBatis, write a Mapper, start the app, call it, and it crashes with a BindingException. Almost everyone hits this at least once right after adopting MyBatis. The cause is nearly always one of “configuration”, “XML location”, “@Param”, or “@Mapper/@MapperScan”, so if you read the stack trace you can fix it in a few minutes.
This article targets Spring Boot 3.x + mybatis-spring-boot-starter 3.0.x (Java 17+) and works backward from the error message to eliminate the cause. Note that the 2.x line of the starter is javax-based and will not work on Spring Boot 3.x. Always use 3.0.x. The normal setup procedure is covered in the MyBatis Mapper implementation guide, and whether you are coming over from JPA or not, here we will focus purely on resolving the errors.
First, identify which category your error belongs to
Compare the top of your stack trace against the following three.
# 系統1: 実行時(Mapperメソッド呼び出し時)
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.example.demo.mapper.UserMapper.findById
# 系統2: 実行時(SQLのパラメータ解決時)
org.apache.ibatis.binding.BindingException: Parameter 'name' not found. Available parameters are [arg0, arg1, param1, param2]
# 系統3: 起動時
Parameter 0 of constructor in com.example.demo.service.UserService required a bean of type 'com.example.demo.mapper.UserMapper' that could not be found.
Action:
Consider defining a bean of type 'com.example.demo.mapper.UserMapper' in your configuration.
| When it occurs | Error message | Where to look |
|---|---|---|
| Runtime | Invalid bound statement (not found) | Go to Category 1. mapper-locations, namespace, id, XML location |
| Runtime | Parameter ‘xxx’ not found | Go to Category 2. @Param, -parameters |
| Startup | required a bean of type ’…Mapper’ | Go to Category 3. @Mapper, @MapperScan |
Category 3 is not a BindingException, but it is such a common issue right after adopting MyBatis that we cover it together here.
Category 1 Causes of Invalid bound statement
MyBatis registers SQL statements using namespace + "." + id from the XML as the key, and when a Mapper method is called, it looks up that key using fully qualified interface name.method name. The com.example.demo.mapper.UserMapper.findById at the end of the error message is the “key it looked for”, and this error means that key was not found. In other words, the cause can only be one of two things: “the XML was not loaded” or “the keys do not match”. The following minimal setup is used for reproduction from here on (imports omitted).
package com.example.demo.mapper;
@Mapper
public interface UserMapper {
User findById(Long id);
User findByNameAndStatus(String name, String status);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
<select id="findById" resultType="com.example.demo.entity.User">
SELECT * FROM users WHERE id = #{id}
</select>
</mapper>
Cause (a) mybatis.mapper-locations is unset or the pattern is wrong
This is by far the most common one. The starter does not guess mapper-locations for you, so if it is unset, XML files placed under src/main/resources/mapper/ are never read at all.
mybatis:
# NG: 未設定、または classpath:mapper/*.xml だとサブディレクトリが対象外
# NG: classpath:mappers/**/*.xml のようにディレクトリ名が違う
mapper-locations: classpath:mapper/**/*.xml
If you want to descend into subdirectories, use **. Another surprisingly common case is having the setting in both application.properties and application.yml, with one overriding the other.
There is one exception. If you place the XML in the same package hierarchy as the Mapper interface (src/main/resources/com/example/demo/mapper/UserMapper.xml), it is loaded automatically even without mapper-locations. If you feel like “it worked without any configuration in my previous project”, this implicit behavior is the reason.
Cause (b) namespace differs from the interface’s FQCN
<mapper namespace="..."> must exactly match the fully qualified name of the interface. Typical cases include moving the package while the XML still references the old package, renaming the class to UserRepository, or copying another Mapper’s XML and forgetting to update the namespace.
The fastest check is a direct string comparison between the FQCN in the error message and the namespace. If you have a MyBatis plugin such as MyBatisX installed in IntelliJ, you can jump from the namespace to the class, so if the jump fails, you know they do not match.
Cause (c) id differs from the method name
The id in <select id="findById"> must match the method name exactly, including case. These are typo-class issues: writing findByID, or renaming the method from selectUser to selectUsers without updating the XML. Note that if an id is duplicated within the same namespace, you get a different error instead: IllegalArgumentException: Mapped Statements collection already contains value for ....
Cause (d) The XML is placed under src/main/java
It is tempting to put the XML right next to the interface, but by default neither Gradle nor Maven copies non-Java files under src/main/java into the classes output.
NG: src/main/java/com/example/demo/mapper/UserMapper.xml ← ビルドに含まれない
OK: src/main/resources/mapper/UserMapper.xml ← mapper-locations で指定
OK: src/main/resources/com/example/demo/mapper/UserMapper.xml ← 同パッケージなら設定不要
You can tell immediately by checking whether the XML exists inside build/classes or target/classes. You can also add src/main/java as a resource directory via Gradle’s sourceSets so it gets read, but if you get the configuration wrong, .java files end up in the artifact too, so simply moving the XML to the resources side is the recommended approach.
Cause (e) The build configuration leaves the XML out of the jar
If it works when launched from the IDE but crashes with java -jar, suspect this. A typical example is writing your own <resources> in Maven and only including *.yml.
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.yml</include>
<include>**/*.xml</include> <!-- これが抜けるとXMLがjarに入らない -->
</includes>
</resource>
</resources>
Gradle’s processResources { exclude '**/*.xml' }, or a multi-module project where the Mapper and XML live in a separate module that is not included as a dependency, produce the same symptom. To verify, use the jar contents check described later.
With annotation-based Mappers, mapper-locations is not needed
Mappers written with @Select and similar annotations do not use XML, so mapper-locations is unnecessary (having it set does no harm). If you write both XML and annotations for the same method, you get a duplicate error, so settle on one or the other. If you wrote @Select and still get Invalid bound statement, it is very likely that you have defined multiple DataSources / SqlSessionFactories and that Mapper is bound to a different SqlSessionFactory than intended. Check sqlSessionFactoryRef on @MapperScan.
Category 2 Causes of Parameter not found
The main cause is not adding @Param to methods with multiple arguments. Note that the order in Available parameters are [...] varies by environment and may appear as [arg1, arg0, param1, param2], so ignore the order and look only at the contents.
// NG: MyBatisは引数名を知らないので arg0/arg1 か param1/param2 でしか参照できない
User findByNameAndStatus(String name, String status);
// OK: XML側の #{name} #{status} と名前を揃える
User findByNameAndStatus(@Param("name") String name, @Param("status") String status);
The Available parameters are [arg0, arg1, param1, param2] in the error message is the list of “names you can use right now”. Writing #{arg0} works as a stopgap, but it breaks the moment you change the argument order, so avoid it.
A single argument works without @Param
With a single argument, the story changes.
- With a single object, you can reference its properties directly as
#{name}and#{status}. If you add@Param("user")here, you then need to nest as#{user.name} - With a single Map, you can reference keys directly by name
- With a single primitive or String such as
Long id, both#{id}and#{value}resolve regardless of the name
Watch out for the reverse pattern too. If you write #{userId} in the XML for findById(@Param("id") Long id), you get Parameter 'userId' not found. Available parameters are [id, param1].
Why the -parameters flag changes the behavior
When javac’s -parameters flag is enabled, argument names are retained in the class file, so MyBatis can resolve them using the actual argument names name and status even without @Param. Spring Boot’s Gradle plugin has added -parameters to the JavaCompile task ever since 2.0, so with Gradle it is enabled without doing anything.
Maven is a different story. spring-boot-starter-parent only started setting <parameters>true</parameters> on maven-compiler-plugin from Spring Boot 3.2, and setups that only import the BOM without using starter-parent still need to configure it themselves. The “it works locally but becomes arg0 in CI” discrepancy is usually one of the following.
- Using Maven without starter-parent (or on Boot 3.1 or earlier), and not passing
-parametersto maven-compiler-plugin - IntelliJ building with its own javac instead of delegating to Gradle/Maven, so the flag is not passed through
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
With Gradle, this is unnecessary as long as the Spring Boot plugin is applied. If you are building with only the plain java plugin, add tasks.withType(JavaCompile) { options.compilerArgs << '-parameters' }.
If Available parameters shows [name, status, param1, param2] and you still get the error, the placeholder name on the XML side is simply wrong. That said, we recommend explicitly using @Param, which does not depend on the build environment.
Category 3 Mapper cannot be injected
If the app crashes at startup with required a bean of type '...UserMapper' that could not be found, the Mapper has not been registered as a bean. The cause is almost always one of these three.
- The interface has no
@Mapperand no@MapperScanis written either. If the startup log showsNo MyBatis mapper was found in '[com.example.demo]' package. Please check your configuration., this is it - A typo or wrong hierarchy in the package name, such as
@MapperScan("com.example.demo.mappers") - The Mapper is placed in a package above the main class, such as
com.example.mapper, putting it outside the starter’s automatic scan range (the main class’s package and below)
// パターンA: 各インターフェースに @Mapper(メインクラス配下なら自動スキャンされる)
@Mapper
public interface UserMapper { ... }
// パターンB: メインクラスに @MapperScan(@Mapper は不要になる)
@SpringBootApplication
@MapperScan("com.example.demo.mapper")
public class DemoApplication { ... }
Choosing between @Mapper and @MapperScan
Either one is sufficient. Writing both does not cause double registration, but the moment you write @MapperScan, the starter’s automatic scan is disabled, so any @Mapper outside the @MapperScan range will no longer be registered. “I added @MapperScan and the Mappers in another package disappeared” is caused by this.
The practical compromise is @Mapper when you have few Mappers, and @MapperScan with explicit basePackages when you have many or they are spread across packages. Since @MapperScan picks up every interface in the specified package, keep Mappers in a dedicated package. For general discussion of undefined beans (such as @ComponentScan range), see the startup failure troubleshooting article.
Verify that the configuration is actually being read
Rather than fixing by guesswork, it is faster to look at the facts in the logs and the jar contents.
logging:
level:
com.example.demo.mapper: debug # ==> Preparing: SELECT ... が出ればOK
org.mybatis.spring: debug # org.mybatis.spring.SqlSessionFactoryBean が Parsed mapper file: ... を出す
If SQL logs appear, both Mapper registration and XML loading have succeeded. You can also confirm whether settings such as map-underscore-to-camel-case are in effect from the same logs and the returned fields.
If it crashes with java -jar, also inspect the jar contents.
./gradlew bootJar
jar tf build/libs/demo-0.0.1-SNAPSHOT.jar | grep -i mapper
# BOOT-INF/classes/mapper/UserMapper.xml が出なければ原因(d)(e)
Checklist by error message
| Error message (excerpt) | When | What to check | Fix |
|---|---|---|---|
| Invalid bound statement | Runtime | mapper-locations | classpath:mapper/**/*.xml |
| Same as above | Runtime | namespace / id | Match the FQCN and method name |
| Same as above | Runtime | XML location / XML inside the jar | Move to the resources side, remove exclusion settings |
| Parameter ‘xxx’ not found | Runtime | @Param | Add to multi-argument methods |
| Same as above | Runtime | Placeholder name / -parameters | Match the Available parameters |
| required a bean of type ’…Mapper’ | Startup | @Mapper / @MapperScan | Add one of them |
| Same as above | Startup | basePackages / package hierarchy | Place under the main class |
Not covered in this article, but TooManyResultsException occurs when a method expecting a single row returns multiple rows, and Mapped Statements collection already contains value is caused by duplicate ids or defining the same statement in both XML and annotations.
Review a correct minimal setup all at once
Finally, here is one complete template that produces no errors. The only dependencies are the starter and the DB driver.
dependencies {
// バージョンは 3.0.x 系の最新を Maven Central で確認する
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.4'
runtimeOnly 'org.postgresql:postgresql'
}
With Maven, writing org.mybatis.spring.boot:mybatis-spring-boot-starter at the same version in a <dependency> is equivalent.
mybatis:
mapper-locations: classpath:mapper/**/*.xml
type-aliases-package: com.example.demo.entity
configuration:
map-underscore-to-camel-case: true
src/main
├── java/com/example/demo
│ ├── DemoApplication.java
│ ├── entity/User.java
│ └── mapper/UserMapper.java ← @Mapper 付き、複数引数は @Param 付き
└── resources
├── application.yml
└── mapper/UserMapper.xml ← namespace=FQCN、id=メソッド名、resultType="User"
Because type-aliases-package is configured, the resultType in the XML can be written as User instead of the FQCN. Start the app with this setup, and if SQL flows through the debug log shown earlier, you are done.
Summary
- Invalid bound statement at runtime means the XML was not loaded or the namespace/id does not match
- Parameter not found at runtime means a missing @Param on a multi-argument method
- Bean not found at startup means a missing @Mapper or @MapperScan, or a wrong scan range
Reading the FQCN or Available parameters contained in the error message as-is is the shortest path to isolating the problem. Once the errors are gone, move on to dynamic SQL or join mapping with resultMap. If connections get stuck, the HikariCP tuning article is also a useful reference.