Springboot Mybatis 打包jar掃描bean與mapper問題研究與解決
最近在專案中整合以全註解的方式Mybatis,配置了自動bean包與mapper所在包
db.mybatis.mapperLocations=classpath*:com/cml/springboot/sample/db/resource/*
db.mybatis.typeAliasesPackage=com.cml.springboot.sample.bean
db.mybatis.typeHandlerPackage=com.cml.springboot.framework.mybatis.typehandler
這些配置直接在ide上執行都是ok的,但是經過打包成jar包後,就一直報錯提示找打不到對應的bean或者對應的mapper。主要錯誤如下:
Error resolving class. Cause: org.apache.ibatis.type.TypeException: Could not resolve type alias 'logbean'. Cause: java.lang.ClassNotFoundException: Cannot find class: logbean
***************************
APPLICATION FAILED TO START
***************************
Description:
Field logMapper in com.cml.springboot.sample.service.impl.LogServiceImpl required a bean of type 'com.cml.springboot.sample.db.LogMapper' that could not be found.
Action:
Consider defining a bean of type 'com.cml.springboot.sample.db.LogMapper' in your configuration.
針對以上問題,在新開的分支deploy_jar_bugfind上進行研究,找到了一個臨時解決辦法,就是配置mybat-configuration.xml將對應的bean都配置到xml上。
<configuration>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true" />
</settings>
<typeAliases>
<typeAlias type="com.cml.springboot.sample.bean.LogBean" alias="logbean"/>
<typeAlias type="com.cml.springboot.sample.bean.User" alias="user"/>
</typeAliases>
<typeHandlers>
<typeHandler javaType="org.joda.time.DateTime"
handler="com.cml.springboot.framework.mybatis.typehandler.JodaDateTimeTypeHandler" />
<typeHandler javaType="org.joda.time.LocalTime"
handler="com.cml.springboot.framework.mybatis.typehandler.JodaLocalTimeTypeHandler" />
</typeHandlers>
</configuration>
但是這個僅僅適合小專案並且bean少的情況,假如在打專案上,可能有幾十上百的bean,都要一一配置豈不是累死人了,而且容易出bug。
於是繼續研究,首先自定義DefaultSqlSessionFactoryBean取代預設的SqlSessionFactoryBean,並且在buildSqlSessionFactory()方法上對bean掃描的配置進行一步步log拆分,主要程式碼如下:
if (hasLength(this.typeAliasesPackage)) {
String[] typeAliasPackageArray = tokenizeToStringArray(this.typeAliasesPackage,
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
for (String packageToScan : typeAliasPackageArray) {
logger.debug("##################################################################################");
List<String> children = VFS.getInstance().list(packageToScan.replace(".", "/"));
logger.debug("findclass:" + children);
logger.debug("##################################################################################");
logger.debug("DefaultSqlSessionFactoryBean.buildSqlSessionFactory.registerAliases:" + packageToScan);
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.find(new ResolverUtil.IsA(Object.class), packageToScan);
Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
logger.debug(
"DefaultSqlSessionFactoryBean.buildSqlSessionFactory.typeAliasesPackage.scanClass:" + typeSet);
for (Class<?> type : typeSet) {
if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
String alias = type.getSimpleName();
String key = alias.toLowerCase(Locale.ENGLISH);
logger.debug(
"DefaultSqlSessionFactoryBean.buildSqlSessionFactory.typeAliasesPackage.scan:" + key);
}
}
configuration.getTypeAliasRegistry().registerAliases(packageToScan,
typeAliasesSuperType == null ? Object.class : typeAliasesSuperType);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Scanned package: '" + packageToScan + "' for aliases");
}
}
}
發現在ide上執行的時候是可以掃描到指定的bean,但是在jar上就掃描不到了。主要問題找到了,就是對程式碼的掃描問題,原本想自定義configuration的,但是發現好多類都是依賴configuration這個類,於是便放棄這個想法,轉而研究掃描不到bean的問題。
繼續研究原始碼,得知bean的掃描是通過ResolverUtil這個類進行的,並且ResolverUtil掃描是通過VFS進行掃描的,主要程式碼:
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);
}
return this;
}
結合log上的資訊,工程上預設使用的是Mybatis的DefaultVFS進行掃描。檢視這個類程式碼後也沒發現什麼問題,轉而求救於百度與谷歌,經過多方搜尋,找到了Mybatis官網issue (地址:https://github.com/mybatis/mybatis-3/issues/325)這裡有對這些問題進行說明。根據issue的描述與各大神的回答,定位到了問題是DefaultVFS在獲取jar上的class問題。
在mybat/spring-boot-starter工程上找到了SpringBootVFS,這個類重寫了class掃描功能,通過spring進行掃描。
於是將SpringBootVFS拷貝到工程上,並且新增到VFS實現上去,程式碼如下:
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(DataSource datasource, MybatisConfigurationProperties properties)
throws Exception {
log.info("*************************sqlSessionFactory:begin***********************" + properties);
VFS.addImplClass(SpringBootVFS.class);
DefaultSqlSessionFactoryBean sessionFactory = new DefaultSqlSessionFactoryBean();
sessionFactory.setDataSource(datasource);
sessionFactory.setTypeAliasesPackage(properties.typeAliasesPackage);
sessionFactory.setTypeHandlersPackage(properties.typeHandlerPackage);
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sessionFactory.setMapperLocations(resolver.getResources(properties.mapperLocations));
// sessionFactory
// .setConfigLocation(new PathMatchingResourcePatternResolver().getResource(properties.configLocation));
SqlSessionFactory resultSessionFactory = sessionFactory.getObject();
log.info("===typealias==>" + resultSessionFactory.getConfiguration().getTypeAliasRegistry().getTypeAliases());
log.info("*************************sqlSessionFactory:successs:" + resultSessionFactory
+ "***********************" + properties);
return resultSessionFactory;
}
這樣mybaits打包jar後掃描問題完美解決。
解決辦法:
- 通過mybatis-configuration.xml進行配置
- 通過SpringBootVFS進行自動掃描配置(推薦)