MySQL Fabric和MyBatis的整合過程中遇到的問題


這是我昨天在整合MySQL Fabric和MyBatis時遇到的問題,花了大半天才解決的問題,解決的過程中在網上查找了很久,都沒有找到解決的方案。現在記下來,希望能夠幫助有同樣問題的朋友。如果各位朋友有更好的解決方案,也請告訴我。

1. 問題描述

這個問題是在整合MySQL和MyBatis的時候遇到的。
首先說一下我使用的jar包的版本,MySQL Connector用的是5.1.36,myBatis用的是3.2.8。我也試過將MySQL Connector升級到5.1.40,然並卵。

看看MyBatis中數據源的設置如下:

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"  
        destroy-method="close">  
        <property name="driverClassName" value="${mybatis.driverClassName}" />  
        <property name="url" value="${mybatis.url}" />  
        <property name="username" value="${mybatis.username}" />  
        <property name="password" value="${mybatis.password}" />  
        <property name="minIdle" value="${mybatis.minIdle}" />        <!-- 隊列中的最小等待數 -->  
        <property name="maxIdle" value="${mybatis.maxIdle}" />        <!-- 隊列中的最大等待數 -->  
        <property name="maxWait" value="${mybatis.maxWait}" />        <!-- 最長等待時間,單位毫秒 -->  
        <property name="maxActive" value="${mybatis.maxActive}" />    <!-- 最大活躍數 -->  
        <property name="initialSize" value="${mybatis.initialSize}" /><!-- 初始大小 -->  
        <property name="validationQuery" value="${mybatis.validationQuery}" />
        <property name="testWhileIdle" value="${mybatis.testWhileIdle}" />
        <property name="timeBetweenEvictionRunsMillis" value="${mybatis.timeBetweenEvictionRunsMillis}" />
        <property name="numTestsPerEvictionRun" value="${mybatis.numTestsPerEvictionRun}" />
        <property name="minEvictableIdleTimeMillis" value="${mybatis.minEvictableIdleTimeMillis}" />
    </bean>    

對應的屬性值定義如下:

mybatis.driverClassName=com.mysql.fabric.jdbc.FabricMySQLDriver
mybatis.url=jdbc:mysql:fabric://10.8.48.230:32274/basicservice?fabricServerGroup=spfood&fabricUsername=admin&fabricPassword=admin123#
mybatis.username=spfood
mybatis.password=spfood123#
mybatis.initialSize=5
mybatis.minIdle=1
mybatis.maxIdle=10
mybatis.maxWait=3000
mybatis.maxActive=50
#SQL查詢,用來驗證從連接池取出的連接
mybatis.validationQuery=SELECT 1
#指明連接是否被空閑連接回收器(如果有)進行檢驗,如果檢測失敗,則連接將被從池中去除
mybatis.testWhileIdle=true
#在空閑連接回收器線程運行期間休眠的時間值,以毫秒為單位,一般比minEvictableIdleTimeMillis小
mybatis.timeBetweenEvictionRunsMillis=300000
#在每次空閑連接回收器線程(如果有)運行時檢查的連接數量,最好和maxActive一致
mybatis.numTestsPerEvictionRun=50
#連接池中連接,在時間段內一直空閑,被逐出連接池的時間(1000*60*60 = 1 hour),以毫秒為單位
mybatis.minEvictableIdleTimeMillis=3600000  

在查詢的時候,一直遇到如下的錯誤信息:

org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException: 
### Error updating database.  Cause: java.lang.NullPointerException
### The error may involve com.spfood.basicservice.idgeneration.domain.IDGenIDInfo.updateByPrimaryKey-Inline
### The error occurred while setting parameters
### SQL: update IDGen_IDInfo     set Next_Value = ?,       Step_Length = ?     where Id_Type = ?
### Cause: java.lang.NullPointerException
	at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:76)
	at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:421)
	at com.sun.proxy.$Proxy19.update(Unknown Source)
	at org.mybatis.spring.SqlSessionTemplate.update(SqlSessionTemplate.java:270)
	at com.spfood.kernel.dao.impl.BaseDaoImpl.updateById(BaseDaoImpl.java:273)
	... 43 more
Caused by: org.apache.ibatis.exceptions.PersistenceException: 
### Error updating database.  Cause: java.lang.NullPointerException
### The error may involve com.spfood.basicservice.idgeneration.domain.IDGenIDInfo.updateByPrimaryKey-Inline
### The error occurred while setting parameters
### SQL: update IDGen_IDInfo     set Next_Value = ?,       Step_Length = ?     where Id_Type = ?
### Cause: java.lang.NullPointerException
	at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:26)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:154)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.lang.reflect.Method.invoke(Unknown Source)
	at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:408)
	... 46 more
Caused by: java.lang.NullPointerException
	at com.mysql.jdbc.StatementImpl$CancelTask.<init>(StatementImpl.java:86)
	at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1893)
	at com.mysql.jdbc.PreparedStatement.execute(PreparedStatement.java:1193)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.lang.reflect.Method.invoke(Unknown Source)
	at com.mysql.jdbc.MultiHostConnectionProxy$JdbcInterfaceProxy.invoke(MultiHostConnectionProxy.java:91)
	at com.sun.proxy.$Proxy27.execute(Unknown Source)
	at org.apache.commons.dbcp.DelegatingPreparedStatement.execute(DelegatingPreparedStatement.java:172)
	at org.apache.commons.dbcp.DelegatingPreparedStatement.execute(DelegatingPreparedStatement.java:172)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.lang.reflect.Method.invoke(Unknown Source)
	at org.apache.ibatis.logging.jdbc.PreparedStatementLogger.invoke(PreparedStatementLogger.java:62)
	at com.sun.proxy.$Proxy28.execute(Unknown Source)
	at org.apache.ibatis.executor.statement.PreparedStatementHandler.update(PreparedStatementHandler.java:44)
	at org.apache.ibatis.executor.statement.RoutingStatementHandler.update(RoutingStatementHandler.java:69)
	at org.apache.ibatis.executor.ReuseExecutor.doUpdate(ReuseExecutor.java:50)
	at org.apache.ibatis.executor.BaseExecutor.update(BaseExecutor.java:105)
	at org.apache.ibatis.executor.CachingExecutor.update(CachingExecutor.java:71)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:152)
	... 51 more

2. 查找問題根源

可以看到,具體的錯誤來自PreparedStatement.java:1193 和 StatementImpl.java:86, 查找到這兩個地方的代碼如下:

StatementImpl.java:84-86行

            Properties props = StatementImpl.this.connection.getProperties();

            Enumeration<?> keys = props.propertyNames();

PreparedStatement.java:1192-1895

            if (locallyScopedConnection.getEnableQueryTimeouts() && this.timeoutInMillis != 0 && locallyScopedConnection.versionMeetsMinimum(5, 0, 0)) {
               timeoutTask = new CancelTask(this);
               locallyScopedConnection.getCancelTimer().schedule(timeoutTask, this.timeoutInMillis);
            }

Debug一下,可以看到,在StatementImpl.java中的第84行,getProperties()返回的值為null,跟蹤進去,可以看到connection的具體類是FabricMySQLConnectionProxy,該類的getProperties方法實現如下:

    public Properties getProperties() {
        return null;
    }

這個錯誤就是在創建CancelTask的時候失敗。關於CancelTask,網上的資料大把,請自行搜索。

3. 解決方法

那么,有兩種方式解決這個問題。

3.1 修改myBatis配置

我們可以看到在 PreparedStatement.java:1192這一行中的條件,如果this.timeoutInMillis為0,那么就不會創建CancelTask。我們只需要在MyBatis的配置中將這個值設置為0就可以了。

<configuration>  
    <settings>  
        <setting name="cacheEnabled" value="true" />  
        <setting name="lazyLoadingEnabled" value="false" />  
        <setting name="multipleResultSetsEnabled" value="true" />  
        <setting name="useColumnLabel" value="true" />  
        <setting name="defaultExecutorType" value="REUSE" />  
        <setting name="defaultStatementTimeout" value="0" />  
        <setting name="logImpl" value="LOG4J"/>
    </settings>  
      
</configuration>   

就是上面的配置中的defaultStatementTimeout這一項。

3.2 擴展MySQL Connector的代碼

如果還想繼續使用CancelTask的功能,那就只有自行擴展MySQL Connector的代碼了。
首先想到的是修改FabricMySQLConnectionProxy。我們編寫一個自己的ConnectionProxy,繼承FabricMySQLConnectionProxy,覆蓋它的getProperties和getCancelTimer方法就可以了,如下:

public class AfragFabricMySQLConnectionProxy extends FabricMySQLConnectionProxy 
      implements FabricMySQLConnection, FabricMySQLConnectionProperties{
    /**
     * 
     */
    private static final long serialVersionUID = 8626818655234189033L;

    private transient Timer cancelTimer;

    public AfragFabricMySQLConnectionProxy(Properties props) throws SQLException {
        super(props);
    }

    public Timer getCancelTimer() {
        synchronized (getConnectionMutex()) {
            if (this.cancelTimer == null) {
                boolean createdNamedTimer = false;
                // Use reflection magic to try this on JDK's 1.5 and newer, fallback to non-named timer on older VMs.
                try {
                    Constructor<Timer> ctr = Timer.class.getConstructor(new Class[] { String.class, Boolean.TYPE });
                    this.cancelTimer = ctr.newInstance(new Object[] { "MySQL Statement Cancellation Timer", Boolean.TRUE });
                    createdNamedTimer = true;
                } catch (Throwable t) {
                    createdNamedTimer = false;
                }
                if (!createdNamedTimer) {
                    this.cancelTimer = new Timer(true);
                }
            }
            return this.cancelTimer;
        }
    }  
    
    public Properties getProperties() {
        return new Properties();
    }
} 

那么,怎么讓driver知道要使用我們的ConnectionProxy呢?那就需要修改FabricMySQLDriver類了。同樣的,我們創建自己的Driver類,繼承FabricMySQLDriver類。如下,主要修改的地方有兩個:

  1. 修改connection方法,使其返回我們的AfragFabricMySQLConnectionProxy,而不是原來的FabricMySQLConnectionProxy。
  2. 在類的靜態塊中,首先Deregister FabricMySQLDriver,同時注冊自己。不然DriverManager在解析url的時候,還是會使用老的FabricMySQLDriver類。

這樣修改后,應該沒有問題了吧?但是有時候還是會報錯,在跟蹤一下,可以看到在connection方法中,有時候會返回JDBC4FabricMySQLConnectionProxy,該類繼承了FabricMySQLConnectionProxy。因此,我們需要創建自己的JDBC4 ConnectionProxy,實現getCancelTimer和getProperties方法。

實現后的Driver類和JDBC4 ConnectionProxy類如下:
Driver類:

public class AfragFabricMySQLDriver extends FabricMySQLDriver{
    
    // Deregister FabricMySQLDriver and Register ourselves with the DriverManager
    static {
        try {
            deregisterFabricMySQLDriver();
            
            DriverManager.registerDriver(new AfragFabricMySQLDriver());
        } catch (SQLException ex) {
            throw new RuntimeException("Can't register driver", ex);
        }
    }
    
    public static void deregisterFabricMySQLDriver() throws SQLException{
        Enumeration<Driver> drivers = DriverManager.getDrivers();
        while (drivers.hasMoreElements()){
            Driver driver = drivers.nextElement();
            if (driver instanceof FabricMySQLDriver){
                DriverManager.deregisterDriver(driver);
            }
        }
    }
    /**
     * @throws SQLException
     */
    public AfragFabricMySQLDriver() throws SQLException {
        super();
    }
    
    public Connection connect(String url, Properties info) throws SQLException {
        Properties parsedProps = parseFabricURL(url, info);
        if (parsedProps == null) {
            return null;
        }
        parsedProps.setProperty(FABRIC_PROTOCOL_PROPERTY_KEY, "http");
        if (com.mysql.jdbc.Util.isJdbc4()) {
            try {
                Constructor<?> jdbc4proxy = Class.forName("personal.afrag.AfragJDBC4FabricMySQLConnectionProxy").getConstructor(
                        new Class[] { Properties.class });
                return (Connection) com.mysql.jdbc.Util.handleNewInstance(jdbc4proxy, new Object[] { parsedProps }, null);
            } catch (Exception e) {
                throw (SQLException) new SQLException(e.getMessage()).initCause(e);
            }
        }
        return new AfragFabricMySQLConnectionProxy(parsedProps);
    }
    
    Properties parseFabricURL(String url, Properties defaults) throws SQLException {
        if (!url.startsWith("jdbc:mysql:fabric://")) {
            return null;
        }
        // We have to fudge the URL here to get NonRegisteringDriver.parseURL() to parse it for us.
        // It actually checks the prefix and bails if it's not recognized.
        // jdbc:mysql:fabric:// => jdbc:mysql://
        return super.parseURL(url.replaceAll("fabric:", ""), defaults);
    }
}  

JDBC4 Connection Proxy:

public class AfragJDBC4FabricMySQLConnectionProxy extends JDBC4FabricMySQLConnectionProxy implements JDBC4FabricMySQLConnection, FabricMySQLConnectionProperties {
    /**
     * 
     */
    private static final long serialVersionUID = 6404998348296596764L;
    /**
     * @param props
     * @throws SQLException
     */
    public AfragJDBC4FabricMySQLConnectionProxy (Properties props) throws SQLException {
        super(props);
    }
    
    private transient Timer cancelTimer;
    public Timer getCancelTimer() {
        synchronized (getConnectionMutex()) {
            if (this.cancelTimer == null) {
                boolean createdNamedTimer = false;
                // Use reflection magic to try this on JDK's 1.5 and newer, fallback to non-named timer on older VMs.
                try {
                    Constructor<Timer> ctr = Timer.class.getConstructor(new Class[] { String.class, Boolean.TYPE });
                    this.cancelTimer = ctr.newInstance(new Object[] { "MySQL Statement Cancellation Timer", Boolean.TRUE });
                    createdNamedTimer = true;
                } catch (Throwable t) {
                    createdNamedTimer = false;
                }
                if (!createdNamedTimer) {
                    this.cancelTimer = new Timer(true);
                }
            }
            return this.cancelTimer;
        }
    }
}


免責聲明!

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



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