近日,某個系統的測試環境mybatis老是報Invalid bound statement(not found)
異常,致使tomcat容器沒法啓動。異常信息以下:java
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.xxx.management.dao.IssueDao.countByCid at org.apache.ibatis.binding.MapperMethod$SqlCommand.<init>(MapperMethod.java:227) at org.apache.ibatis.binding.MapperMethod.<init>(MapperMethod.java:49) at org.apache.ibatis.binding.MapperProxy.cachedMapperMethod(MapperProxy.java:65) at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:58) at com.sun.proxy.$Proxy126.countByCid(Unknown Source) at com.xxx.management.service.issue.IssueService.countByCid(IssueService.java:81) at com.xxx.management.service.issue.IssueService$$FastClassBySpringCGLIB$$be57e1e9.invoke(<generated>) at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:746) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:294) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175) at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:93) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688)
QA同窗開始覺得是develop
分支有代碼改動致使,切到master
分支從新部署,仍是出現同樣的問題,但是詭異的是相同的代碼其實在2天前已經上線了,線上表現一切正常。因而開發同窗(我)開始介入排查問題。node
注意:兩個環境的雲主機配置,OS版本,JDK版本,tomcat版本徹底一致。linux
首先,我嘗試本機復現,發現不管是develop
分支仍是master
分支在本機都不會出現異常,甚至直接去測試環境scp war包到本地啓動都沒法復現。這就比較抓瞎了,因而只能根據錯誤日誌開始假設排除。spring
異常信息很是直觀,就是mybatis查找不到 com.xxx.management.dao.IssueDao.countByCid
這個statement了,第一反應檢查mybaits dao相關代碼。sql
因爲系統使用的是interface + xml的映射方式,因此先檢查 xml 文件的namespace,沒問題;再檢查 <select>
的 id,也沒問題,parameterType、resultType等屬性均無問題。apache
google 上相關問題的解決方案還有人說改動一下maven的打包配置,但確認過xml和dao都已打進jar包,所以能夠肯定不是 dao interface 和 xml 的映射代碼有問題。tomcat
系統使用mybatis-spring集成,mybaits MapperScan 的配置以下:mybatis
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> <property name="basePackage" value="com.xxx.common.dao,com.xxx.management.dao"/> </bean>
出問題的dao interface是 com.xxx.management.dao.IssueDao
,根據異常的堆棧信息遠程debug打斷點(系統使用的mybatis版本爲3.4.6)後發現拋異常的代碼以下:app
MapperMethod 227行:jvm
MapperMethod 251行:
緣由就在於 mapperInterface.equals(declaringClass)
,斷點觀察後發現二者的值都是com.xxx.management.dao.IssueDao
,所以能夠判定MapperScan的配置沒有問題,basePackage正常生效了。
mybatis-spring還有一項mapper locations的配置,系統中的配置以下:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="configLocation" value="classpath:mybatis-config.xml"/> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations"> <array> <value>classpath:/mybatis/common/*.xml</value> <value>classpath:/mybatis/*.xml</value> </array> </property> </bean>
先說明一下爲啥mapperLocations有兩個,且二者都是/mybatis
目錄下,是由於它們分別在兩個不一樣的jar包裏。classpath:/mybatis/common/*.xml
在依賴的一個二方庫lib-commons.jar
中,classpath:/mybatis/*.xml
則在自身工程的manage-moudle-dao.jar
中。
從配置文件可知,mapperLocations
被賦值給了SqlSessionFactoryBean.mapperLocations
:
查找一下this.mapperLocations
的調用,發現被 SqlSessionFactoryBean.buildSqlSessionFactory()
方法調用,調用代碼段以下:
代碼做用簡單來講就是解析 xml mapper,而且解析成功後會打印一條DEBUG日誌,因而去查 tomcat 啓動日誌,發現並無Parsed mapper file: '[mybatis/IssueDao.xml]'
的日誌,因而遠程debug在遍歷this.mapperLocations
處,發現並未加載到 /myabtis
目錄下的任何文件,僅加載到了/mybatis/common
目錄下的文件,而 this.mapperLoactions
並沒有其餘write調用,所以能夠判定問題出在mapperLoactions的spring屬性賦值上。
從上文SqlSessionFactoryBean
的代碼截圖能夠看到,mapperLocations
實際類型是Resource[]
,這個是被spring在解析BeanDefinition時作的轉換,經過的是ResourcePatternResolver
接口,具體到本例上是PathMatchingResourcePatternResolver
。
PathMatchingResourcePatternResolver.getResources
方法的代碼以下:
注:常量CLASSPATH_ALL_URL_PREFIX = "classpath*:"
。
從代碼中可知,classpath*:
會查找classpath下的全部符合條件的resource,而 classpath:
則只會查找第一個符合條件的resource, 本例中使用的classpath:
。
所以spring應當是先加載了lib-commons.jar
中的/mybatis/common/*.xml
,而後根據第二個location去加載/mybatis/*.xml
,由於第一個location的配置中也有/mybatis/
,因此根據classpath:
只查找第一個符合條件的原則,直接在已命中過的lib-commons.jar
中搜索/mybatis/*.xml
,而沒有再去搜索其餘jar包。
知道緣由在哪就很好辦了,有兩種辦法:
classpath:
改爲classpath*:
通過實際驗證,兩種方法均有效。
問題到上面其實已經解決了,可是還有一個問題遺留着:一樣的代碼,甚至是同一個war包,在不一樣的機器上的表現爲啥徹底不一致?
由於已知機器的系統版本、JDK版本、tomcat版本都是徹底相同的,因此我猜想這個是否和JVM的jar包加載順序有關呢?
google一番,參考 https://my.oschina.net/ericquan8/blog/1523496 這篇文章,可知 linux 系統下 jvm 實際上是優先加載 inode 值更小的 jar ,去各環境實測發現確實如此。