资讯专栏INFORMATION COLUMN

[JAVA][学习·练手·挖坑] 做个数据库帮助库雏形 · 二

Eric / 571人阅读

摘要:事实上,实现了接口,而也实现了接口。还记得之前说的,使用之后,其返回的实际上是一个装饰器吗。所以修改如下是默认全局工厂名称,请使用别的名称工厂已经配置完成,请不要重复配置。

这是做个数据库帮助库雏形 的当晚的再一次尝试 ORZ

在意识到原来的 ConnectionProvider 提供的只是一个普通(实现了AutoCloseable接口)的 Connection,这在 RepositoryInvocationHandler.handleFind中使用 try-with-resource 的情况下就相当于 ConnectionProvier没啥卵用...

因此,今天晚上进行了一些大改:

注:写到最后我还是想配个日志了... 不过鉴于这么晚了,还是明天再搞吧 : 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 作为关键字搜索后,发现了这篇文章。

这东西事实上并不算完整的连接池实现,有兴趣可以用 MysqlPooledConnection 作为关键字进行搜索 : P

改用这个东西后,后面其实没啥改的,因为这个东西所创建的 PreparedStatement 是一个装饰器。

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接口。

那么来看看修改了一些 connection 获取逻辑的 RepositoryInvocationHandler:

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

    //缓存表名,避免多次从方法注解获取该信息
    private final String entityName;
    private RepositoryFactory factory;
    private Class invokeRepositoryClass;
    //对 PreparedStatement 做一层缓存,避免每次调用方法都创建一个 statement
    private LinkedHashMap preparedStatementMap;
    //缓存连接,主要是批量创建`statement`时避免频繁调用 ConnectionProvider.provide()
    private Connection connection;

    public RepositoryInvocationHandler (RepositoryFactory factory, Class 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)) + "	");
                }
                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;
    }
}

代码块中有个地方要多带带拿出来说一下:

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;
    }

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/70523.html

相关文章

  • [JAVA][学习·练手·挖坑] 做个数据帮助雏形

    摘要:前者是数据库驱动,由于这是个挖坑性质的东西,所以只针对做功能了后者是代码生成框架,挺好用的,强烈推荐也就是说,并不使用常见的数据库连接池,比如。的工厂已经被初始化了,不能再对其进行配置。 在以往的编码中,使用过 spring-data-jpa,也用过 hibernate 和 mybatis。在简单的数据库操作中,spring-data-jpa 是用起来最爽的,毕竟在 IntelliJ ...

    rickchen 评论0 收藏0
  • [Java][数据工具][坑] Juice README

    摘要:注意供应器只会在仓库工厂第一次创建工厂时调用,而参数处理器和结果解析器将在每次仓库方法被调用时调用。解析器接收一个语句表模型的类声明触发解析器的仓库方法声明。因此当您配置了一个结果解析器,语句的执行时机将推迟到这里。 Juice 这是我自己做的一个小项目,也可能会弃坑... 留作纪念吧。GitHub 地址 简介 Juice 是一个简易的、尚不完善的基于 Java 的SQL数据库工具,它...

    CoXie 评论0 收藏0
  • 全栈最后一公里 - Node.js 项目的线上服务器部署与发布

    摘要:没有耐心阅读的同学,可以直接前往学习全栈最后一公里。我下面会罗列一些,我自己录制过的一些项目,或者其他的我觉得可以按照这个路线继续深入学习的项目资源。 showImg(https://segmentfault.com/img/bVMlke?w=833&h=410); 本文技术软文,阅读需谨慎,长约 7000 字,通读需 5 分钟 大家好,我是 Scott,本文通过提供给大家学习的方法,...

    Nosee 评论0 收藏0
  • 我,27岁,程序员,10月无情被辞:想给学python的人提个醒......

    摘要:就在最新的指数中,数据科学和机器学习项目的首选语言,现在排名仅次于语言,排在第二位,将打落到第三位。特别是在深度学习机器学习等领域的广泛使用,让一跃成为人工智能时代的网红语言。 ...

    ZweiZhao 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<