jdbc数据库连接池

Posted Java笔记录

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了jdbc数据库连接池相关的知识,希望对你有一定的参考价值。

JDBC数据库连接池


为什么要使用数据库连接池?什么是数据库连接池?


数据库连接池其实就是多个数据库连接的集合,提前就建立了多规格数据库的连接,这样在很多人同时进行和数据库交互相关操作的时候就不用每个人都重新建立连接,大大节省了时间和资源,避免了资源的大量浪费,也避免了频繁的打开和关闭数据库连接造成的性能地下。


如何编写数据库连接池?多个连接如何保存?连接后如何在close之后再存入连接池中?


带着以上三个问题接着向下学习,首先思考使用什么保存多个连接?很显然,我们会想到集合类,那么问题来了,到底用哪一个集合类呢?创建多个数据库连接插进集合类中,连接之后要从集合类中取出,连接断掉之后也要添加进集合中,所以这里我们选用的是LinkedList,因为LinkedList底层结构是链表,对元素的增删操作比较快。


来看下具体是如何实现的


这里我们采用的是配置文件的形式设置基本信息


URL=jdbc:mysql://localhost:3306/test01
USER=root
PASS=ForMe=520
DV=com.mysql.jdbc.Driver
poolsize=15


多了一个连接池的数量:poolsize,设置最大连接池的数量为15。


编写实现数据库连接池的具体操作:要实现一个

DataSource接口,并实现其中的方法。


package com.demo.test;

import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.logging.Logger;

public class DBPoolTest implements DataSource {
    private static LinkedList<Connection> connlist = new LinkedList<>();

    static{
        InputStream inputStream = DBPoolTest.class.getResourceAsStream("/db.properties");
        Properties properties = new Properties();
        try {
            properties.load(inputStream);

            String USER = properties.getProperty("USER");
            String URL = properties.getProperty("URL");
            String PASS = properties.getProperty("PASS");
            String DRIVER = properties.getProperty("DV");
            int poolsize = Integer.parseInt(properties.getProperty("poolsize"));

            Class.forName(DRIVER);

            for(int i = 1; i <= poolsize; i++){
                Connection connection = DriverManager.getConnection(URL,USER,PASS);
                System.out.println("获取到第" + i + "个数据库连接:" + connection);
                connlist.add(connection);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    @Override
    public Connection getConnection() throws SQLException {
        if (connlist.size() > 0) {
            final Connection connection = connlist.removeFirst();
            System.out.println("connlist数据库连接池现在的大小为:" + connlist.size());
            return (Connection) Proxy.newProxyInstance(DBPoolTest.class.getClassLoader(), new Class[]{Connection.class}, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    if (!method.getName().equals("close"))
                        return method.invoke(connection, args);
                    else {
                        //如果调用的是Connection对象的close方法,就把conn还给数据库连接池
                        connlist.add(connection);
                        System.out.println(connection + "被还给了数据库连接池");
                        return null;
                    }
                }
            });
        }
        else
            throw new RuntimeException("数据库连接池繁忙");
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return null;
    }

    @Override
    public <T> unwrap(Class<T> iface) throws SQLException {
        return null;
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return false;
    }

    @Override
    public PrintWriter getLogWriter() throws SQLException {
        return null;
    }

    @Override
    public void setLogWriter(PrintWriter out) throws SQLException {

    }

    @Override
    public void setLoginTimeout(int seconds) throws SQLException {

    }

    @Override
    public int getLoginTimeout() throws SQLException {
        return 0;
    }

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        return null;
    }

}


说明:

静态代码块中的内容很好理解,就是获取连接。最重要的部分在getConnection方法中,使用了代理对象的方式,其中要注意的就是这个:


(Connection) Proxy.newProxyInstance(DBPoolTest.class.getClassLoader(), new Class[]{Connection.class}, new InvocationHandler(){}


(Connection) Proxy.newProxyInstance(DBPoolTest.class.getClassLoader(), new Class[]{Connection.class}, new InvocationHandler(){}

红色部分的参数有的时候也可以使用:

conn.getClass.getInterfaces() 

但是我之前测试的时候使用的就是 conn.getClass.getInterfaces() ,不过报了 $Proxy0 cannot be cast to java.sql.Connection  的错误,后来网上查了些资料,发现是在获取代理的时候由于数据库驱动不同造成的。

conn.getClass.getInterfaces()的结果与数据库驱动有关,驱动不同,其结果也就不同。比如用mysql的驱动可能这样做可以,但是你在oracle下就可能出问题。conn.getClassInterfaces()返回的是一个Class数组,它的第一个元素必须是一个Connection才能将创建的代理转化为Connection,所以用conn.getClass.getInterfaces()不行,具有太多的不稳定性。那么,既然需要的是一个Connection来转化,那么我们直接给一个Connection就好,这也是为什么我们使用new Class[]{Connection.class}有用的原因了。


接下来我们来测试一下


public class DBPoolTest2 {
    public static void main(String[] args) throws SQLException {
        DBPoolTest dbPoolTest = new DBPoolTest();
        List<Connection> list = new ArrayList<>();
        Connection connection1 = null;
        for(int i = 1; i <= 4; i++){
           connection1 =  dbPoolTest.getConnection();
           list.add(connection1);
        }
        for(int i = 0; i < list.size(); i++)
            list.get(i).close();
    }
}


看下测试结果



首先是创建了大小为15的数据库连接池,然后连接一次减少一个,断开连接之后就又放回了数据库连接池中。


开源数据库连接池


既然数据库连接池这么重要,那到底有没有人将其封装起来供开发人员直接进行连接呢?答案是肯定的,现在有很多服务器已经实现了数据库连接池,在项目的实际开发中就不需要编写连接数据库的代码了,直接从数据源获得数据库的连接。


开源的数据库连接池有两种

✦ DBCP

✦ C3P0


DBCP    


首先要导入包

commons-dbcp.jar  

commons-logging.jar (不导入此包也会报错)      

commons-pool.jar


看下properties文件的使用规范,必须遵守,否则会报错


driverClassName=com.mysql.jdbc.Driver  // 不多解释,这是基本的驱动加载

url=jdbc:mysql://localhost/db_student    // 驱动注册

username=root    //要连接的数据库用户名

password=root   // 要连接的数据库密码

defaultAutoCommit=true// 设置是否自动提交,默认为true

defaultReadOnly=false: // 是否为只读 默认为false

defaultTransactionIsolation=3// 设置数据库的事务隔离级别默认为1,READ_UNCOMMITTED,推荐设置为3

initialSize=10:  // 初始化数据池拥有的连接数量

maxActive=20:  /池中最多可容纳的活着的连接数量,当达到这个数量不在创建连接

maxIdle=20:  // 最大空闲等待,也就是连接等待队列超过这个值会自动回收未使用的连接,直到达到20

minIdle=5: // 最小空闲等待 ,数据池中最少保持的连接

maxWait=10000   // 最大等待时间,超过这个时间等待队列中的连接就会失效

testOnBorrow=true  //从池中取出连接时完成校验 ,验证不通过销毁这个connection,默认为true,

testOnReturn=false  //放入池中时完成校验,默认我fasle

validationQuery=select 1  // 校验语句,必须是查询语句,至少查询一列,设置了它onBorrow才会生效

validationQueryTimeout=1  // 校验查询时长,如果超过,认为校验失败

testWhileIdle=false   // 清除一个连接时是否需要校验

timeBetweenEvictionRunsMillis=1  // DBCP默认有个回收器Eviction,这个为设置他的回收时间周期

numTestsPerEvictionRun=3  // Eviction在运行时一次处理几个连接

poolPreparedStatements=true  //是否缓存PreparedStatements

maxOpenPreparedStatements=1 // 缓存PreparedStatements的最大个数


测试用例


package com.demo.test;

import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.commons.dbcp2.BasicDataSourceFactory;

import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

public class DBCPTest1 {
    private static DataSource ds = null;

    static {
        InputStream inputStream = DBCPTest1.class.getResourceAsStream("/db.properties");
        Properties properties = new Properties();
        try {
            properties.load(inputStream);
            BasicDataSourceFactory basicDataSourceFactory = new BasicDataSourceFactory();
            ds = basicDataSourceFactory.createDataSource(properties);

        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static Connection getConn() {
        try {
            return ds.getConnection();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }
   //release方法不是断开连接,断开连接的操作已经被封装了,这里只是为了把连接重新放入连接池中
    public static void release(Connection conn, Statement st, ResultSet rs) {

        if (rs != null) {
            try {
                rs.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            rs = null;
        }
        if (st != null) {
            try {
                st.close();
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
        if (conn != null) {
            try {
                conn.close();
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }
}


Test类


public class DBCPTest2 {
    public static void main(String[] args) throws SQLException {
        testDBCP();
    }

    public static void testDBCP() throws SQLException {
        Connection connection = DBCPTest1.getConn();
        String sql = "INSERT INTO things (name) VALUES (?)";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setString(1,"ForMes");
        preparedStatement.executeUpdate();
        DBCPTest1.release(connection,preparedStatement,null);
    }
}


执行两次观测结果


jdbc数据库连接池



C3P0


也需要导入包

c3p0-0.9.5.2.jar、mchange-commons-java-0.2.12.jar,如果操作的是Oracle数据库,那么还需要导入

c3p0-oracle-thin-extras-0.9.2-pre1.jar


https://sourceforge.net/projects/c3p0/files/latest/download?source=files


与DBCP相比c3p0有自动回收空闲连接功能。


C3P0也需要写配置文件,命名为c3p0-config.xml


<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>

    <!--默认配置-->
    <default-config>
        <property name="initialPoolSize">10</property>
        <property name="maxIdleTime">30</property>
        <property name="maxPoolSize">100</property>
        <property name="minPoolSize">10</property>
        <property name="maxStatements">200</property>
    </default-config>

    <named-config name="test">
        <property name="driverClass">com.mysql.jdbc.Driver</property>
        <property name="jdbcUrl">jdbc:mysql://localhost:3306/test01</property>
        <property name="user">root</property>
        <property name="password">ForMe=520</property>
        <property name="initialPoolSize">10</property>
        <property name="maxIdleTime">30</property>
        <property name="maxPoolSize">100</property>
        <property name="minPoolSize">10</property>
        <property name="maxStatements">200</property>
    </named-config>

    <!--<named-config name="test2"></named-config>  配置连接池二-->

    <!--……………………-->

    <!--<named-config name="testn"></named-config>  配置连接池n-->
</c3p0-config>


编写连接类


package com.demo.test;

import com.mchange.v2.c3p0.ComboPooledDataSource;

import javax.swing.plaf.nimbus.State;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class C3P0Test1 {
    private static ComboPooledDataSource cs = null;
    static{
        cs = new ComboPooledDataSource("test");
    }

    public static Connection getConn() throws SQLException {
        return cs.getConnection();
    }

    public static void release(Connection connection, Statement statement, ResultSet resultSet){
        if(connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if(statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if(resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}


在配置文件中如果不想写named-config,也可以在static块中使用set方法设置值,如下


//通过代码创建C3P0数据库连接池
ds = new ComboPooledDataSource();
ds.setDriverClass("com.mysql.jdbc.Driver");           ds.setJdbcUrl("jdbc:mysql://localhost:3306/test01");
ds.setUser("root");
ds.setPassword("ForMe=520");
ds.setInitialPoolSize(10);
ds.setMinPoolSize(5);
ds.setMaxPoolSize(20);


最后测试类和DBCP一样,看结果


jdbc数据库连接池


在IDEA中配置Tomcat数据源


参考:https://www.cnblogs.com/caryfang/p/5581463.html


Tomcat服务器创建的数据源是以JNDI资源的形式发布的,所以说在Tomat服务器中配置一个数据源实际上就是在配置一个JNDI资源。


具体的配置过程如下,看张图片


jdbc数据库连接池


1、打开module setting面板,找到facets 配置项,这个配置项非常重要,里面可配置tomcat加载的web.xml和context.xml文件所在的路径,部署的时候IDEA会自动读取。

2、若没有facets-web项,则自己添加一个,然后点击add application server specific descriptor,选择tomcat context descriptor和默认配置文件路径,保存即可。

3、在web面板里打开META-INF/context.xml文件(这个文件在第2步配置后,会自动创建。),直接把数据源的配置参数填入即可。


然后在META-INF目录下的context.xml中配置自己的数据源


<?xml version="1.0" encoding="UTF-8"?>
<Context path="/">
    <Resource
            name="jdbc/datasource"
            auth="Container"
            type="javax.sql.DataSource"
            username="root"
            password="ForMe=520"
            driverClassName="com.mysql.jdbc.Driver"
            url="jdbc:mysql://localhost:3306/test01"
            maxActive="8"
            maxIdle="4"/>

</Context>


之后把数据库驱动包放在Tomcat服务器的lib目录下



接着就是编写工具类获取数据源


package com.jnditests;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class JNDITest1 {
    private static DataSource ds = null;
    static{
        try {

            //初始化JNDI
            Context init = new InitialContext();

            //得到JNDI容器
            Context getJdni = (Context) init.lookup("java:comp/env");

            //从JNDI容器中检索name为jdbc/datasource的数据源
            ds = (DataSource)getJdni.lookup("jdbc/datasource");

        } catch (NamingException e) {
            e.printStackTrace();
        }
    }

    public static Connection getConnection() throws SQLException {
        //从数据源中获取数据库连接
        return ds.getConnection();
    }


    public static void release(Connection conn, Statement st, ResultSet rs){
        if(rs!=null){
            try{
                //关闭存储查询结果的ResultSet对象
                rs.close();
            }catch (Exception e) {
                e.printStackTrace();
            }
            rs = null;
        }
        if(st!=null){
            try{
                //关闭负责执行SQL命令的Statement对象
                st.close();
            }catch (Exception e) {
                e.printStackTrace();
            }
        }

        if(conn!=null){
            try{
                //将Connection连接对象还给数据库连接池
                conn.close();
            }catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

}



最后编写一个Servlet类来测试下


package com.jnditests;

import javax.naming.Context;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

@WebServlet(name = "JNDIServlet")
public class JNDIServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request,response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        try {
            Connection connection = JNDITest1.getConnection();
            String sql = "INSERT INTO things (name) VALUES (?)";
            PreparedStatement preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1,"JNDIname");
            preparedStatement.executeUpdate();
            JNDITest1.release(connection,preparedStatement,null);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}



看下数据库中的信息



发现插入成功。



总结:本篇学习了数据库连接池和两种开源的数据库连接池DBCP和C3P0两种都要熟练掌握,最后是在IDEA中配置Tomcat数据源,是最实用的一种,要着重学习。





以上是关于jdbc数据库连接池的主要内容,如果未能解决你的问题,请参考以下文章

数据库连接池的Java连接池

用Java手动封装JDBC连接池

JDBC 连接池错误

哪个更好:JDBC 连接池,还是使用 SIngleton 类进行 JDBC 连接?

MySql & JDBC & 连接池 & 总结

JDBC,连接池及CRUD操作