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.propertiesapplication.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/classestarget/classes の中にXMLがあるかを見れば即判定できます。Gradleの sourceSetssrc/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 に紐付いている可能性が高いです。@MapperScansqlSessionFactoryRef を確認してください。

系統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-locationsclasspath:mapper/**/*.xml
同上実行時namespace / idFQCN・メソッド名と一致させる
同上実行時XMLの場所・jar内のXMLresources側へ移動、除外設定を外す
Parameter ‘xxx’ not found実行時@Param複数引数に付与
同上実行時プレースホルダ名 / -parametersAvailable 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 をそのまま読むのが、最短の切り分けです。エラーが消えたら、動的SQLresultMapでの結合マッピング に進みましょう。接続が詰まる場合は HikariCPのチューニング記事 も参考になります。