何についての話か
SpringBoot+MyBatisを利用したプロジェクトで、TypeAliasRegistry#registerAliasesでパッケージ指定して、その下のエンティティのクラスのエイリアスを自動で丸ごと指定している場合、つまり、
src.main.kotlin
├── App.kt <- Springboot起動クラス
├── MyBatisConfig.kt <- @Configurationが指定されたMyBatis設定クラス
└── package
└── to
└── entities <- エンティティクラスのパッケージ: この下のエンティティクラス全部alias指定したい
├── Entity1.kt
├── Entity2.kt
└── Entity3.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作ってコマンドラインで実行しようとすると
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ファイル内にあるファイルたちにアクセスするためには素で書くと
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で書きます。
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で実行するよ!となると、
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()!!
}
}
とすると動くよ!っていう記事がたくさんあるのですが 私は動かなかったです。
以前はこれだけで動いた気がするのだけどなぁ。
ちなみに、上のコードでは、SqlSessionFactoryBeanでclasspath以下のSQLのXMLファイルを参照しようとしているときにVFSを経由しているので、
SqlSessonFactoryにVFSはSpringBootVFSだよ!って教えてあげている1行です。
解決策(最終版)
ソース追ってたら、どうやらTypeAliasRegistry#registerAliasesでパッケージの展開ができてない模様。
ん?これはもしや・・・と思い、もうちょっとソース追ってみたところ、
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クラス直接参照しているのですね、、、こっちは。。
ということで、ソースコードをこんな感じに修正。
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で書くとこんな感じです。
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つずつクラス指定すればいいんだよ!
つまり...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)
とすればエラー出ないよ!
という回答でした。
趣深い。