Mybatis-Spring掃描路徑有重疊導致Invalid bound statement(not found)問題


背景

近日,某個系統的測試環境mybatis總是報Invalid bound statement(not found)異常,導致tomcat容器無法啟動。異常信息如下:

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天前已經上線了,線上表現一切正常。於是開發同學(我)開始介入排查問題。

注意:兩個環境的雲主機配置,OS版本,JDK版本,tomcat版本完全一致。

問題定位

首先,我嘗試本機復現,發現無論是develop分支還是master分支在本機都不會出現異常,甚至直接去測試環境scp war包到本地啟動都無法復現。這就比較抓瞎了,於是只能根據錯誤日志開始假設排除。

假設1:mybatis interface 和 xml 映射代碼錯誤

異常信息非常直觀,就是mybatis查找不到 com.xxx.management.dao.IssueDao.countByCid 這個statement了,第一反應檢查mybaits dao相關代碼。

由於系統使用的是interface + xml的映射方式,所以先檢查 xml 文件的namespace,沒問題;再檢查 <select> 的 id,也沒問題,parameterType、resultType等屬性均無問題。

google 上相關問題的解決方案還有人說改動一下maven的打包配置,但確認過xml和dao都已打進jar包,因此可以確定不是 dao interface 和 xml 的映射代碼有問題。

假設2:mybatis mapper scan 配置錯誤

系統使用mybatis-spring集成,mybaits MapperScan 的配置如下:

<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)后發現拋異常的代碼如下:

  • MapperMethod 227行:

  • MapperMethod 251行:

原因就在於 mapperInterface.equals(declaringClass),斷點觀察后發現兩者的值都是com.xxx.management.dao.IssueDao,因此可以斷定MapperScan的配置沒有問題,basePackage正常生效了。

假設3:mybatis mapper locations 配置錯誤

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包。

問題解決

知道原因在哪就很好辦了,有兩種辦法:

  1. classpath:改成classpath*:
  2. 改變xml文件路徑,讓兩個location不會有重疊路徑

經過實際驗證,兩種方法均有效。

擴展

問題到上面其實已經解決了,但是還有一個問題遺留着:同樣的代碼,甚至是同一個war包,在不同的機器上的表現為啥完全不一致?

因為已知機器的系統版本、JDK版本、tomcat版本都是完全相同的,所以我猜測這個是否和JVM的jar包加載順序有關呢?

google一番,參考 https://my.oschina.net/ericquan8/blog/1523496 這篇文章,可知 linux 系統下 jvm 其實是優先加載 inode 值更小的 jar ,去各環境實測發現確實如此。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM