SpringBootのExecutable JARでMyBatisのtypeAliasが解決できない件。

## 何についての話か

SpringBoot+MyBatisを利用したプロジェクトで、TypeAliasRegistry#registerAliasesでパッケージ指定して、その下のエンティティのクラスのエイリアスを自動で丸ごと指定している場合、つまり、

```:package想定
src.main.kotlin
├── App.kt <- Springboot起動クラス
├── MyBatisConfig.kt <- @Configurationが指定されたMyBatis設定クラス
└── package
    └── to
        └── entities <- エンティティクラスのパッケージ: この下のエンティティクラス全部alias指定したい
            ├── Entity1.kt
            ├── Entity2.kt
            └── Entity3.kt
```
```kotlin:registerAliasesで丸ごと.kt
    @Bean
    fun sqlSessionFactory(dataSource: DataSource): SqlSessionFactory {
        val config = org.apache.ibatis.session.Configuration().apply {
            isMapUnderscoreToCamelCase = true
            autoMappingBehavior = AutoMappingBehavior.FULL
            typeAliasRegistry.registerAliases("package.to.entities")
        }
        /* そのあといろいろ */
    }

```
こんな場合で、EclipseやIntelliJで開発時に動かすと動くのに、実行可能JAR作ってコマンドラインで実行しようとすると

```java:Exceptionログ(抜粋)
Caused by: org.apache.ibatis.type.TypeException: Could not resolve type alias 'hogehoge'.  Cause: java.lang.ClassNotFoundException: Cannot find class: hogehoge
	at org.apache.ibatis.type.TypeAliasRegistry.resolveAlias(TypeAliasRegistry.java:120)
	at org.apache.ibatis.builder.BaseBuilder.resolveAlias(BaseBuilder.java:149)
	at org.apache.ibatis.builder.BaseBuilder.resolveClass(BaseBuilder.java:116)
```
とかなることありませんか？？

本記事はその解決策になります。

### 環境
- SpringBoot 2.4.3
- MyBatis 3.5.6
- mybatis-spring-boot-starter 2.1.4
- 開発環境IntelliJ IDEA Community(ごめんなさい) 2020.3.2
- Kotlin 1.x(現時点で最新)
- Java 11

では始めます。

## 原因
JARファイルにまとめてしまうとJARファイル内にあるファイルたちにアクセスするためには素で書くと

```java:resource
   getClass().getResource("classpath:hogehoge")
```
などとしてアクセスする必要があります。
MyBatisでは仮想ファイルシステム(VFS)を経由することでJARファイルの中にアクセスするようになっているようです。

このVFSはJARごとに用意されているのですが、デフォルトではSpringBoot pluginで生成するJAR用のVFSが**セットされていません**。
**※用意はされている。**

このため、IDE上で実行する＝実FS上の実ファイルにアクセスできる、が、JARを実行する＝SpringBoot仮想FS上のクラスファイルにアクセスできない(見つからない)、となってClassNotFoundExceptionが起きます。

## 解決策

SpringBoot+MyBatisでは、MyBatisの設定をbeanで行えます。
最近Kotlin覚えたのでKotlinで書きます。

```kotlin:MyBatisConfig.kt
import org.apache.ibatis.session.AutoMappingBehavior
import org.apache.ibatis.session.SqlSessionFactory
import org.mybatis.spring.SqlSessionFactoryBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
import org.springframework.jdbc.datasource.DataSourceTransactionManager
import org.springframework.transaction.TransactionManager
import javax.sql.DataSource

@Configuration
class MyBatisConfig {

    @Bean
    fun transactionManager(dataSource: DataSource): TransactionManager =
        DataSourceTransactionManager().apply {
            this.dataSource = dataSource
        }

    @Bean
    fun sqlSessionFactory(dataSource: DataSource): SqlSessionFactory {
        val config = org.apache.ibatis.session.Configuration().apply {
            isMapUnderscoreToCamelCase = true
            autoMappingBehavior = AutoMappingBehavior.FULL
            typeAliasRegistry.registerAliases("package.to.entities")
        }

        val sessionFactory = SqlSessionFactoryBean().apply {
            setDataSource(dataSource)
            setConfiguration(config)

            val resolver = PathMatchingResourcePatternResolver()
            setMapperLocations(* resolver.getResources("classpath:/sql/**/*.xml"))
        }

        return sessionFactory.getObject()!!
    }

}
```
テンプレです。

WARにパッケージングしたり、IDE上で動かしたり、build/binディレクトリごと持っていくなら上で問題ないですが、JARで実行するよ！となると、

```kotlin:MyBatisConfig修正版.kt
import org.apache.ibatis.session.AutoMappingBehavior
import org.apache.ibatis.session.SqlSessionFactory
import org.mybatis.spring.SqlSessionFactoryBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
import org.springframework.jdbc.datasource.DataSourceTransactionManager
import org.springframework.transaction.TransactionManager
import javax.sql.DataSource
import org.mybatis.spring.boot.autoconfigure.SpringBootVFS

@Configuration
class MyBatisConfig {

    @Bean
    fun transactionManager(dataSource: DataSource): TransactionManager =
        DataSourceTransactionManager().apply {
            this.dataSource = dataSource
        }

    @Bean
    fun sqlSessionFactory(dataSource: DataSource): SqlSessionFactory {
        val config = org.apache.ibatis.session.Configuration().apply {
            isMapUnderscoreToCamelCase = true
            autoMappingBehavior = AutoMappingBehavior.FULL
            typeAliasRegistry.registerAliases("package.to.entities")
        }

        val sessionFactory = SqlSessionFactoryBean().apply {
            vfs = SpringBootVFS::class.java // <- これが要る！！
            setDataSource(dataSource)
            setConfiguration(config)

            val resolver = PathMatchingResourcePatternResolver()
            setMapperLocations(* resolver.getResources("classpath:/sql/**/*.xml"))
        }

        return sessionFactory.getObject()!!
    }

}
```
とすると動くよ！っていう記事がたくさんあるのですが <font color="red">私は動かなかった</font>です。
以前はこれだけで動いた気がするのだけどなぁ。

ちなみに、上のコードでは、SqlSessionFactoryBeanでclasspath以下のSQLのXMLファイルを参照しようとしているときにVFSを経由しているので、
SqlSessonFactoryにVFSはSpringBootVFSだよ！って教えてあげている1行です。

## 解決策(最終版)

ソース追ってたら、どうやらTypeAliasRegistry#registerAliasesでパッケージの展開ができてない模様。
ん？これはもしや・・・と思い、もうちょっとソース追ってみたところ、

```java:org.apache.ibatis.io.ResolverUtil.java
  public ResolverUtil<T> find(Test test, String packageName) {
    String path = getPackagePath(packageName);

    try {
      List<String> children = VFS.getInstance().list(path); // <- ここ！！
      for (String child : children) {
        if (child.endsWith(".class")) {
          addIfMatching(test, child);
        }
      }
    } catch (IOException ioe) {
      log.error("Could not read package: " + packageName, ioe);
    }
```
うおい！！
5行目でVFSクラス直接参照しているのですね、、、こっちは。。

ということで、ソースコードをこんな感じに修正。

```kotlin:MyBatisConfig最終版.kt
import org.apache.ibatis.session.AutoMappingBehavior
import org.apache.ibatis.session.SqlSessionFactory
import org.mybatis.spring.SqlSessionFactoryBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
import org.springframework.jdbc.datasource.DataSourceTransactionManager
import org.springframework.transaction.TransactionManager
import javax.sql.DataSource
import org.apache.ibatis.io.VFS
import org.mybatis.spring.boot.autoconfigure.SpringBootVFS

@Configuration
class MyBatisConfig {

    @Bean
    fun transactionManager(dataSource: DataSource): TransactionManager =
        DataSourceTransactionManager().apply {
            this.dataSource = dataSource
        }

    @Bean
    fun sqlSessionFactory(dataSource: DataSource): SqlSessionFactory {
        val config = org.apache.ibatis.session.Configuration().apply {
            isMapUnderscoreToCamelCase = true
            autoMappingBehavior = AutoMappingBehavior.FULL
            
            VFS.addImplClass(SpringBoot::class.java) // <= これも要る！！
            typeAliasRegistry.registerAliases("package.to.entities")
        }

        val sessionFactory = SqlSessionFactoryBean().apply {
            vfs = SpringBootVFS::class.java // <- これが要る！！
            setDataSource(dataSource)
            setConfiguration(config)

            val resolver = PathMatchingResourcePatternResolver()
            setMapperLocations(* resolver.getResources("classpath:/sql/**/*.xml"))
        }

        return sessionFactory.getObject()!!
    }

}
```

てな感じで、SqlSessionFactoryだけじゃなく、TypeAliasRegistryから呼ばれるResolverUtilにもVFSを教えてあげる必要がありました。

これで、IDE上でも実行可能JARででも両方とも動きました！

お悩みの方々、ご参考になれば幸いです。

### おまけにJavaのコード

上のコードはKotlinのapplyとか使ってますが、Javaで書くとこんな感じです。

```java:MyBatisConfig.java
import org.apache.ibatis.io.VFS;
import org.apache.ibatis.session.AutoMappingBehavior;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.sql.DataSource;

@Configuration
public class MyBatisConfig {

	@Bean
	public DataSourceTransactionManager transactionManager(DataSource dataSource) {
		DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
		transactionManager.setDataSource(dataSource);

		return transactionManager;
	}

	@Bean
	public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
		SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
		sessionFactory.setDataSource(dataSource);
		sessionFactory.setVfs(SpringBootVFS.class);

		org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration();
		config.setMapUnderscoreToCamelCase(true);
		config.setAutoMappingBehavior(AutoMappingBehavior.FULL);
		VFS.addImplClass(SpringBootVFS.class);
		config.getTypeAliasRegistry().registerAliases("package.to.entities");
		sessionFactory.setConfiguration(config);

		PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
		sessionFactory.setMapperLocations(resolver.getResources("classpath:/sql/**/*.xml"));

		return sessionFactory.getObject();
	}

}

```

## つぶやき
ClassNotFoundExceptionの対応策一生懸命探してたんですがなかなかなくてソースを見る羽目になりました。
ただ、ちょっと趣深いなーと思ったのが、同様のエラーが発生している人に対して、

> registerAliasesでパッケージ指定せずに、**1つずつクラス指定すればいいんだよ**！
>
> ```kotlin:つまり...
> typeAliasRegistry.registerAlias(packages.to.entity.Entity1::class.java)
> typeAliasRegistry.registerAlias(packages.to.entity.Entity2::class.java)
> typeAliasRegistry.registerAlias(packages.to.entity.Entity3::class.java)
> typeAliasRegistry.registerAlias(packages.to.entity.Entity4::class.java)
> ```
> とすればエラー出ないよ！

という回答でした。
趣深い。