MyBatisを入れてMapperを書き、いざ起動して呼び出したら BindingException で落ちる。導入直後にほぼ全員が一度は通る道ですよね。原因はほぼ「設定」「XMLの置き場所」「@Param」「@Mapper/@MapperScan」のどれかなので、スタックトレースを読めば数分で直せます。
この記事では Spring Boot 3.x + mybatis-spring-boot-starter 3.0.x(Java 17+)を対象に、エラー文から逆引きで原因を潰していきます。なお starter の 2.x 系は javax ベースなので Spring Boot 3.x では動きません。必ず 3.0.x を使ってください。導入の正常手順は MyBatisのMapper実装ガイド に譲り、JPAから乗り換えてきた人 も含めて、ここではエラー解消に集中しましょう。
まず自分のエラーがどの系統か確認する
スタックトレースの先頭付近を、次の3つと照らし合わせてください。
# 系統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.
| 発生タイミング | エラー文 | 見るべき場所 |
|---|---|---|
| 実行時 | Invalid bound statement (not found) | 系統1へ。mapper-locations、namespace、id、XMLの置き場所 |
| 実行時 | Parameter ‘xxx’ not found | 系統2へ。@Param、-parameters |
| 起動時 | required a bean of type ’…Mapper’ | 系統3へ。@Mapper、@MapperScan |
系統3はBindingExceptionではありませんが、MyBatis導入直後の定番なので一緒に扱います。
系統1 Invalid bound statement が出る原因
MyBatisはXMLの namespace + "." + id をキーにしてSQLを登録し、Mapperメソッドが呼ばれると インターフェースのFQCN.メソッド名 でそのキーを探します。エラー文末尾の com.example.demo.mapper.UserMapper.findById が「探したキー」で、これが見つからなかった、というのがこのエラーの正体です。つまり原因は「XMLが読まれていない」か「キーが一致しない」のどちらかしかありません。以降の再現には次の最小構成を使います(importは省略)。
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>
原因(a) mybatis.mapper-locations が未設定かパターン違い
一番多いのがこれです。starterは mapper-locations を推測してくれないので、未設定だと src/main/resources/mapper/ に置いたXMLは一切読まれません。
mybatis:
# NG: 未設定、または classpath:mapper/*.xml だとサブディレクトリが対象外
# NG: classpath:mappers/**/*.xml のようにディレクトリ名が違う
mapper-locations: classpath:mapper/**/*.xml
サブディレクトリを掘るなら ** を使いましょう。application.properties と application.yml の両方に書いて片方が上書きしているケースも地味に多いです。
ひとつ例外があって、XMLをMapperインターフェースと同じパッケージ階層(src/main/resources/com/example/demo/mapper/UserMapper.xml)に置くと、mapper-locations なしでも自動で読まれます。「前のプロジェクトでは設定なしで動いた」と感じるなら、この暗黙動作が理由です。
原因(b) namespace がインターフェースのFQCNと違う
<mapper namespace="..."> はインターフェースの完全修飾名と完全一致が必要です。パッケージを移動したのにXMLが旧パッケージのまま、クラス名を UserRepository に変えた、別MapperのXMLをコピーしてnamespaceを直し忘れた、あたりが典型です。
エラー文に出るFQCNとnamespaceをそのまま文字列比較するのが最速です。IntelliJにMyBatisXなどのMyBatis系プラグインを入れていれば、namespaceからクラスへジャンプできるので、ジャンプできなければ不一致だと分かります。
原因(c) id とメソッド名が違う
<select id="findById"> の id はメソッド名と大文字小文字まで一致が必要です。findByID と書いた、メソッド名を selectUser から selectUsers に変えてXMLを直していない、といったタイポ系です。なお同じnamespace内でidが重複すると、今度は IllegalArgumentException: Mapped Statements collection already contains value for ... という別のエラーになります。
原因(d) XMLを src/main/java 側に置いている
インターフェースの隣にXMLを置きたくなりますが、GradleもMavenもデフォルトでは src/main/java 配下の非Javaファイルをclassesにコピーしません。
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 ← 同パッケージなら設定不要
build/classes や target/classes の中にXMLがあるかを見れば即判定できます。Gradleの sourceSets で src/main/java をリソースに追加して読ませることもできますが、指定を誤ると .java まで成果物に混ざるので、素直にresources側へ移すのがおすすめです。
原因(e) ビルド設定でXMLがjarに入っていない
IDEから起動すると動くのに java -jar だと落ちる、という場合はここを疑ってください。Mavenで <resources> を自分で書いて *.yml だけをincludeしている例が典型です。
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.yml</include>
<include>**/*.xml</include> <!-- これが抜けるとXMLがjarに入らない -->
</includes>
</resource>
</resources>
Gradleの processResources { exclude '**/*.xml' } や、マルチモジュールでMapperとXMLが別モジュールにあって依存に含めていないケースも同じ症状です。確認方法は後述のjarの中身チェックを使ってください。
アノテーション方式なら mapper-locations は不要
@Select などで書くMapperはXMLを使わないので mapper-locations は不要です(設定があっても害はありません)。同じメソッドにXMLとアノテーションの両方を書くと重複エラーになるので、どちらかに寄せましょう。@Select を書いたのに Invalid bound statement が出るなら、複数の DataSource / SqlSessionFactory を定義していて、その Mapper が意図と違う SqlSessionFactory に紐付いている可能性が高いです。@MapperScan の sqlSessionFactoryRef を確認してください。
系統2 Parameter not found が出る原因
複数引数のメソッドに @Param を付けていないのが主因です。なお Available parameters are [...] の並び順は環境によって [arg1, arg0, param1, param2] のように前後するので、順番は気にせず中身だけ見てください。
// 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);
エラー文の Available parameters are [arg0, arg1, param1, param2] は「今この名前なら使える」というリストです。#{arg0} と書けば応急処置にはなりますが、引数の順番を変えた瞬間に壊れるのでやめておきましょう。
単一引数のときは @Param なしで動く
引数が1つなら話は変わります。
- オブジェクト1つなら、プロパティ名で直接
#{name}#{status}と書けます。ここに@Param("user")を付けると#{user.name}とネストが必要になります - Map1つなら、キー名でそのまま参照できます
Long idのようなプリミティブやString1つなら、#{id}でも#{value}でも名前に関係なく解決されます
逆パターンにも注意です。findById(@Param("id") Long id) に対してXMLで #{userId} と書くと、Parameter 'userId' not found. Available parameters are [id, param1] になります。
-parameters フラグで挙動が変わる理由
javacの -parameters が有効だと引数名がclassファイルに残るので、MyBatisは @Param なしでも name status という実引数名で解決できます。Spring BootのGradleプラグインは2.0以降ずっと JavaCompile タスクに -parameters を付けているので、Gradleなら何もしなくても有効です。
Mavenは事情が違います。spring-boot-starter-parent が maven-compiler-plugin に <parameters>true</parameters> を設定するようになったのは Spring Boot 3.2 からで、starter-parent を使わずBOMをimportしているだけの構成では今も自分で設定が必要です。「ローカルでは動くのにCIでは arg0 になる」という食い違いは、だいたい次のどちらかです。
- Mavenでstarter-parentを使っておらず(またはBoot 3.1以前で)、maven-compiler-pluginに
-parametersを渡していない - IntelliJがGradle/Mavenに委譲せず自前のjavacでビルドしていて、フラグが渡っていない
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
GradleはSpring Bootプラグインを当てていれば不要です。素の java プラグインだけで組んでいる場合は tasks.withType(JavaCompile) { options.compilerArgs << '-parameters' } を足してください。
Available parametersが [name, status, param1, param2] なのにエラーになるなら、単にXML側のプレースホルダ名が違うだけです。とはいえビルド環境に左右されない @Param 明示を推奨します。
系統3 Mapper が注入できない
起動時に required a bean of type '...UserMapper' that could not be found で落ちる場合、Mapperがbean登録されていません。原因はほぼ次の3つです。
- インターフェースに
@Mapperがなく、@MapperScanも書いていない。起動ログにNo MyBatis mapper was found in '[com.example.demo]' package. Please check your configuration.が出ていればこれです @MapperScan("com.example.demo.mappers")のようにパッケージ名のタイポや階層違い- Mapperを
com.example.mapperのようにメインクラスより上のパッケージに置いていて、starterの自動スキャン範囲(メインクラスのパッケージ以下)から外れている
// パターンA: 各インターフェースに @Mapper(メインクラス配下なら自動スキャンされる)
@Mapper
public interface UserMapper { ... }
// パターンB: メインクラスに @MapperScan(@Mapper は不要になる)
@SpringBootApplication
@MapperScan("com.example.demo.mapper")
public class DemoApplication { ... }
@Mapper と @MapperScan の使い分け
どちらか一方で十分です。両方書いても二重登録にはなりませんが、@MapperScan を書いた時点でstarterの自動スキャンは無効になるので、@MapperScan の範囲外にある @Mapper は登録されなくなります。「@MapperScanを足したら別パッケージのMapperが消えた」はこれが原因です。
Mapperが少ないなら @Mapper、数が多い・パッケージが分かれるなら @MapperScan でbasePackagesを明示、というのが実務的な落としどころです。@MapperScan は指定パッケージ内のインターフェースを全部拾うので、Mapper専用パッケージに分けておきましょう。Bean未定義の一般論(@ComponentScanの範囲など)は 起動失敗のトラブルシューティング記事 を参照してください。
設定が本当に読まれているか確認する
推測で直すより、ログとjarの中身で事実を見たほうが早いです。
logging:
level:
com.example.demo.mapper: debug # ==> Preparing: SELECT ... が出ればOK
org.mybatis.spring: debug # org.mybatis.spring.SqlSessionFactoryBean が Parsed mapper file: ... を出す
SQLログが出ればMapperの登録とXMLの読込は両方成功しています。map-underscore-to-camel-case などの設定が効いているかも、同じログと返ってきたフィールドで確認できます。
java -jar で落ちるならjarの中身も見ましょう。
./gradlew bootJar
jar tf build/libs/demo-0.0.1-SNAPSHOT.jar | grep -i mapper
# BOOT-INF/classes/mapper/UserMapper.xml が出なければ原因(d)(e)
エラーメッセージ別チェックリスト
| エラー文(抜粋) | 発生 | 確認項目 | 修正 |
|---|---|---|---|
| Invalid bound statement | 実行時 | mapper-locations | classpath:mapper/**/*.xml |
| 同上 | 実行時 | namespace / id | FQCN・メソッド名と一致させる |
| 同上 | 実行時 | XMLの場所・jar内のXML | resources側へ移動、除外設定を外す |
| Parameter ‘xxx’ not found | 実行時 | @Param | 複数引数に付与 |
| 同上 | 実行時 | プレースホルダ名 / -parameters | Available parametersと合わせる |
| required a bean of type ’…Mapper’ | 起動時 | @Mapper / @MapperScan | どちらかを付ける |
| 同上 | 起動時 | basePackages・パッケージ階層 | メインクラス配下に置く |
本記事では扱いませんが、TooManyResultsException は1件想定のメソッドが複数行を返したとき、Mapped Statements collection already contains value はid重複やXMLとアノテーションの二重定義が原因です。
正しい最小構成をまとめて確認する
最後に、エラーが出ないひな形を1セット置いておきます。依存はstarterとDBドライバだけです。
dependencies {
// バージョンは 3.0.x 系の最新を Maven Central で確認する
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.4'
runtimeOnly 'org.postgresql:postgresql'
}
Mavenなら org.mybatis.spring.boot:mybatis-spring-boot-starter を同じバージョンで <dependency> に書けば同等です。
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"
type-aliases-package を設定しているので、XMLの resultType はFQCNではなく User と書けます。この構成で起動し、先ほどのdebugログでSQLが流れれば完了です。
まとめ
- 実行時の Invalid bound statement は、XMLが読まれていないか namespace/id の不一致
- 実行時の Parameter not found は、複数引数への @Param 漏れ
- 起動時のbean未検出は、@Mapper か @MapperScan の漏れ・範囲ミス
エラー文に含まれるFQCNや Available parameters をそのまま読むのが、最短の切り分けです。エラーが消えたら、動的SQL や resultMapでの結合マッピング に進みましょう。接続が詰まる場合は HikariCPのチューニング記事 も参考になります。