[JAVA][學習·練手·挖坑] 作個數據庫幫助庫雛形 · 二

這是作個數據庫幫助庫雛形 的當晚的再一次嘗試 ORZjava

在乎識到原來的 ConnectionProvider 提供的只是一個普通(實現了AutoCloseable接口)的 Connection,這在 RepositoryInvocationHandler.handleFind中使用 try-with-resource 的狀況下就至關於 ConnectionProvier沒啥卵用...mysql

所以,今天晚上進行了一些大改:sql

注:寫到最後我仍是想配個日誌了... 不過鑑於這麼晚了,仍是明天再搞吧 : P數據庫

ConnectionProvier

/**
 * Created by krun on 2017/9/22.
 */
public class ConnectionProvider {

    public static ConnectionProvider configure (Configuration configuration) {
        return new ConnectionProvider(configuration);
    }

    private Class driverClass;
    private Configuration configuration;
    
    // 大改的核心之處
    private volatile MysqlPooledConnection pooledConnection;

    private ConnectionProvider (Configuration configuration) {
        this.configuration = configuration;
        try {
            this.driverClass = Class.forName(this.configuration.getDriverClass( ));
            System.out.println("加載驅動完畢");
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("沒法加載 JDBC 驅動: " + this.configuration.getDriverClass( ));
        }
    }

    private synchronized MysqlPooledConnection create ( ) {
        if (driverClass == null) {
            throw new RuntimeException("還沒有加載 JDBC 驅動.");
        } else {
            try {
                System.out.println("建立新的 MysqlPooledConnection");
                return new MysqlPooledConnection((com.mysql.jdbc.Connection)
                        DriverManager.getConnection(
                                this.configuration.getConnectionURL( ),
                                this.configuration.getUsername( ),
                                this.configuration.getPassword( )));
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public synchronized Connection provide ( ) throws SQLException {
        if (pooledConnection == null) {
            System.out.println("初始化 pooledConnection");
            pooledConnection = create( );
        } else if (pooledConnection.getConnection().isClosed()) {
            System.out.println("從新獲取 pooledConnection");
            pooledConnection = create( );
        } else {
            System.out.println("使用緩存 pooledConnection");
        }
        return pooledConnection.getConnection();
    }

}

能夠發現,最大的改變之處在於 create方法返回的是 MysqlPooledConnection 了。
我用記憶中殘存的 PooledConnection 做爲關鍵字搜索後,發現了這篇文章segmentfault

這東西事實上並不算完整的鏈接池實現,有興趣能夠用 MysqlPooledConnection 做爲關鍵字進行搜索 : P緩存

改用這個東西后,後面其實沒啥改的,由於這個東西所建立的 PreparedStatement 是一個裝飾器。app

Connection pooledConnection = ConnectionProvider.provide();
//這裏返回的實際類型是 JDBC42PreparedStatementWrapper
PreparedStatement ps = pooledConnection.prepareStatement(...);

JDBC42PreparedStatementWrapper 這個東西包裝了 com.mysql.jdbc.PreparedStatement
事實上,JDBC42PreparedStatementWrapper 實現了 java.sql.PreparedStatement接口,而com.mysql.jdbc.PreparedStatement也實現了 java.sql.PreparedStatement接口。ide

那麼來看看修改了一些 connection 獲取邏輯的 RepositoryInvocationHandler:this

RepositoryInvocationHandler

/**
 * Created by krun on 2017/9/22.
 */
public class RepositoryInvocationHandler implements InvocationHandler {

    //緩存表名,避免屢次從方法註解獲取該信息
    private final String entityName;
    private RepositoryFactory factory;
    private Class<? extends Repository> invokeRepositoryClass;
    //對 PreparedStatement 作一層緩存,避免每次調用方法都建立一個 statement
    private LinkedHashMap<String, PreparedStatement> preparedStatementMap;
    //緩存鏈接,主要是批量建立`statement`時避免頻繁調用 ConnectionProvider.provide()
    private Connection connection;

    public RepositoryInvocationHandler (RepositoryFactory factory, Class<? extends Repository> invokeRepositoryClass) {

        this.factory = factory;
        this.invokeRepositoryClass = invokeRepositoryClass;
        this.preparedStatementMap = new LinkedHashMap<>( );
        this.entityName = getEntityName( );
        this.connection = getConnection();

        try {
            PreparedStatement preparedStatementWrapper;
            Query query;
            //批量建立 statement,替換表名佔位符,存入緩存
            for (Method method : invokeRepositoryClass.getMethods( )) {
                query = method.getAnnotation(Query.class);
                if (query == null) continue;
                preparedStatementWrapper = createPreparedStatementWrapper(String.format(query.value( ), entityName));
                System.out.println("爲方法 [" + method.getName() + "] 緩存 preparedStatement" );
                this.preparedStatementMap.put(method.getName( ), preparedStatementWrapper);
            }
        } catch (SQLException e) {
            e.printStackTrace( );
        }

    }

    public Object invoke (Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName( );
        if (methodName.startsWith("find")) {
            return handleFind(method, args);
        } else if (methodName.startsWith("save")) {

        } else if (methodName.startsWith("delete")) {

        } else if (methodName.startsWith("exist")) {

        } else if ("close".equals(methodName)) {
            // 暴露 Repository.close 接口來釋放一些資源
            for (String key : this.preparedStatementMap.keySet( )) {
                this.preparedStatementMap.get(key).close( );
            }
            this.connection.close();
            
            //註釋掉這句,避免 close 後再次調用方法時拋出 Statement 已被關閉的錯誤。
            //this.preparedStatementMap.clear();
            System.out.println("釋放 " + invokeRepositoryClass.getSimpleName() + " 的資源");
        }
        return null;
    }

    // 由於獲取鏈接的動做被抽出來給類裏的方法公用,因此要作一層緩存處理
    private Connection getConnection() {
        try {
            synchronized ( this ) {
                if (this.connection == null) {
                    System.out.println("第一次從 provider 獲取鏈接");
                    this.connection = this.factory.getConnectionProvider().provide();
                } else if (this.connection.isClosed()) {
                    System.out.println("從 provider 獲取新的鏈接");
                    this.connection = this.factory.getConnectionProvider().provide();
                } else {
                    System.out.println("使用緩存鏈接");
                }
            }
            return this.connection;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    //把建立 statement 的動做抽出來,配合 getConnection
    private PreparedStatement createPreparedStatementWrapper(String preparedSql) throws SQLException {
        System.out.println("爲 [" + preparedSql + "] 建立PreparedStatemen");
        return getConnection()
                .prepareStatement(preparedSql);
    }

    private String getEntityName ( ) {
        if (! Repository.class.isAssignableFrom(this.invokeRepositoryClass)) {
            throw new RuntimeException(String.format("接口 [%s] 並無繼承 Repository", this.invokeRepositoryClass.getName( )));
        }
        System.out.println("獲取表名");
        ParameterizedType parameterizedType = (ParameterizedType) this.invokeRepositoryClass.getGenericInterfaces( )[0];
        return ((Class) parameterizedType.getActualTypeArguments( )[0]).getSimpleName( ).toLowerCase( );
    }

    //因爲緩存了 statement,須要對其持有的 connection 進行有效性檢查
    private PreparedStatement keepAlive (PreparedStatement preparedStatementWrapper, Method method) {
        try {
            try {
                // 這裏有個坑,詳情見代碼塊下的說明
                boolean isClosed = preparedStatementWrapper.isClosed( );
                if (! isClosed) {
                    System.out.println("使用緩存 [" + method.getName() + "] 的 PreparedStatemen");
                    return preparedStatementWrapper;
                }
                System.out.println("[" + method.getName() + "] 的緩存PreparedStatemen已被關閉,建立新的");
                preparedStatementWrapper = createPreparedStatementWrapper(String.format(method.getAnnotation(Query.class).value( ), entityName));
                this.preparedStatementMap.put(method.getName( ), preparedStatementWrapper);
            } catch (SQLException ignore) {
                System.out.println("[" + method.getName() + "] 的緩存PreparedStatemen的stm已被關閉,建立新的");
                preparedStatementWrapper = createPreparedStatementWrapper(String.format(method.getAnnotation(Query.class).value( ), entityName));
                this.preparedStatementMap.put(method.getName( ), preparedStatementWrapper);
            }
            return preparedStatementWrapper;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    @SuppressWarnings ("unchecked")
    private Object handleFind (Method method, Object... args) {
        PreparedStatement preparedStatementWrapper = this.preparedStatementMap.get(method.getName( ));
        if (preparedStatementWrapper == null) {
            throw new IllegalArgumentException("也許你忘了爲 " + method.getDeclaringClass( ).getSimpleName( ) + "." + method.getName( ) + "() 設置 @Query 註解");
        }
        try {
            System.out.println("檢查 [" + method.getName() + "] 的 preparedStatement 是否有效");
            preparedStatementWrapper = this.keepAlive(preparedStatementWrapper, method);
            System.out.println("填充參數...");
            for (int i = 1; i <= args.length; i++) {
                preparedStatementWrapper.setObject(i, args[i - 1]);
            }
            System.out.println(preparedStatementWrapper.toString( ));
            ResultSet resultSet = preparedStatementWrapper.executeQuery( );
            ResultSetMetaData metaData = resultSet.getMetaData( );
            while (resultSet.next( )) {
                for (int i = 1; i <= metaData.getColumnCount( ); i++) {
                    System.out.print(String.valueOf(resultSet.getObject(i)) + "\t");
                }
                System.out.println();
            }
            resultSet.close( );
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        try {
            return method.getReturnType( ).newInstance( );
        } catch (InstantiationException | IllegalAccessException e) {
            e.printStackTrace( );
        }
        return null;
    }
}

代碼塊中有個地方要單獨拿出來講一下:.net

0        try {
1            try {
2                boolean isClosed = preparedStatementWrapper.isClosed( );
3                if (! isClosed) {
4                    System.out.println("使用緩存 [" + method.getName() + "] 的 PreparedStatemen");
5                    return preparedStatementWrapper;
6                }
7                System.out.println("[" + method.getName() + "] 的緩存PreparedStatemen已被關閉,建立新的");
8                preparedStatementWrapper = createPreparedStatementWrapper(String.format(method.getAnnotation(Query.class).value( ), entityName));
9                this.preparedStatementMap.put(method.getName( ), preparedStatementWrapper);
10            } catch (SQLException ignore) {
11                System.out.println("[" + method.getName() + "] 的緩存PreparedStatemen的stm已被關閉,建立新的");
12                preparedStatementWrapper = createPreparedStatementWrapper(String.format(method.getAnnotation(Query.class).value( ), entityName));
13                this.preparedStatementMap.put(method.getName( ), preparedStatementWrapper);
14            }
15            return preparedStatementWrapper;
16        } catch (SQLException e) {
17            throw new RuntimeException(e);
18        }

重點在於第二行的 preparedStatementWrapper.isClosed()

還記得以前說的,使用 MysqlPooledConnection以後,其返回的PreparedStatement其實是一個裝飾器JDBC43PreparedStatementWrapper嗎。

而在 invoke() 中咱們對 close方法進行了一個釋放資源的操做,調用的是 statement.close()

那麼這個 statement 是裝飾器的話,它的 close 操做究竟是關得誰呢?看看源碼吧:

//JDBC42PreparedStatementWrapper的close實現是由JDBC4PreparedStatementWrapper作的。
public class JDBC4PreparedStatementWrapper extends PreparedStatementWrapper {
    public synchronized void close() throws SQLException {
        if (this.pooledConnection == null) {
            // no-op
            return;
        }

        MysqlPooledConnection con = this.pooledConnection; // we need this later...

        try {
            super.close();
        } finally {
            try {
                StatementEvent e = new StatementEvent(con, this);
                // todo: pull this all up into base classes when we support *only* JDK6 or newer
                if (con instanceof JDBC4MysqlPooledConnection) {
                    ((JDBC4MysqlPooledConnection) con).fireStatementEvent(e);
                } else if (con instanceof JDBC4MysqlXAConnection) {
                    ((JDBC4MysqlXAConnection) con).fireStatementEvent(e);
                } else if (con instanceof JDBC4SuspendableXAConnection) {
                    ((JDBC4SuspendableXAConnection) con).fireStatementEvent(e);
                }
            } finally {
                this.unwrappedInterfaces = null;
            }
        }
    }
}

嗯,看來調用的是 super.close(),那麼咱們須要再往上看 StatementWrapper:

public void close() throws SQLException {
        try {
            if (this.wrappedStmt != null) {
                this.wrappedStmt.close();
            }
        } catch (SQLException sqlEx) {
            checkAndFireConnectionError(sqlEx);
        } finally {
            this.wrappedStmt = null;
            this.pooledConnection = null;
        }
    }

問題就在 this.wrappedStmt = null;這一句,它把所裝飾的 statement 實例置空了,再來看看 JDBC4PreparedStatementWrapperisClosed實現:

public boolean isClosed() throws SQLException {
        try {
            if (this.wrappedStmt != null) {
                return this.wrappedStmt.isClosed();
            } else {
                throw SQLError.createSQLException("Statement already closed", SQLError.SQL_STATE_ILLEGAL_ARGUMENT, this.exceptionInterceptor);
            }
        } catch (SQLException sqlEx) {
            checkAndFireConnectionError(sqlEx);
        }

        return false; // never get here - compiler can't tell
    }

wrappedStmt 爲空時,會直接拋出錯誤 ORZ

這就意味着,在調用 JDBC4PreparedStatementWrapper.close() 後,再調用 JDBC4PreparedStatementWrapper.isClosed() 必定會拋出錯誤 ORZ

這就致使咱們必須嘗試獲取一下 isClosed()的結果,在獲取成功(雖然我不知道這在什麼狀況下才會出現)後,在 isClosed == true 的狀況下從新建立 JDBC4PreparedStatementWrapper;而 catch 塊裏也須要作一個從新建立的操做。

看起來是重複語句,但實際上不能直接把 重建操做放在 finally 塊中,那樣會致使每次調用 keepAlive 時都重建 PreparedStatement

RepositoryFactory

寫完上篇筆記時,我就想起來沒有作一個檢查:

用戶建立一個給定名稱的Repository時,確保這個給定名稱不是 GLOBAL,由於這是全局工廠的名稱。

因此修改以下:

private static boolean isSelfCall (StackTraceElement[] stackTraceElements) {
        return stackTraceElements[1].getClassName( ).equals(RepositoryFactory.class.getName( ));
    }

    public static RepositoryFactory configure (String name, Configuration configure) {
        if (! isSelfCall(new Exception( ).getStackTrace( )) &&
                FACTORY_GLOBAL.equals(name)) {
            throw new RuntimeException("GLOBAL 是默認全局工廠名稱,請使用別的名稱.");
        }
        RepositoryFactory factory;
        synchronized ( RepositoryFactory.factoryMap ) {
            factory = RepositoryFactory.factoryMap.get(name);
            if (factory != null) {
                throw new RuntimeException(name + " 工廠已經配置完成,請不要重複配置。");
            }
            System.out.println("建立新的工廠: " + name);
            factory = new RepositoryFactory(ConnectionProvider.configure(configure));
            RepositoryFactory.factoryMap.put(name, factory);
        }
        return factory;
    }
相關文章
相關標籤/搜索