vlambda博客
学习文章列表

大白话聊懂Java中的连接池,用包装模式实现标准的DataSource数据源连接池

一、首先简单谈谈为什么要用连接池?

大家知道,我们平常连接数据库的时候,首先需要获取到数据库的连接,在Java中对应的是 Connection,建立获取数据库连接是比较消耗资源的,而且每次建立获取连接也比较浪费时间,可以试想,如果每次请求过来,需要访问数据库时,都去重新建立并获取新的连接,会浪费大量的资源和时间,此时客户端的响应时间肯定会较长,这并不是我们想看到的。因此这时候我们就要想办法避免这种现象,所以这时候就可以用连接池来解决。


其实简单的说,连接池实现的主要目的就是,获取连接的时候从池中获取已经初始化好的连接,这样做就不用每次都建立新的连接,就能节省资源的消耗,也能提高效率。


然后在用完连接以后,调用conn.close( ); 关闭连接的时候并不是真去把这个连接关闭掉,而是应该把这个连接放回到池中,下次请求过来了,可以继续使用。


Java中连接池具体的实现一般有两种方式,一种是用包装类来实现,另一种是用动态代理来实现


现在常见的开源的Java连接池有DBCP和C3P0等,其中DBCP就是上边说的利用包装类来实现的,而C3P0是动态代理实现的。


今天讲一下使用包装类实现一个简单的连接池


刚才也过说过了,连接池的核心功能就是在用完conn资源后,需要关闭释放资源,但是我们并不想真正的把这个连接资源关闭,而应该放回池中,供其他请求使用,或者供下次继续使用。


说到这儿,肯定很多人会想到,如果是这样的话,那肯定就得在conn对象的close方法上做点文章啊,那就改一下conn中的close方法啊,在close方法中撸点代码实现刚才说的不去真正关闭连接资源,而是放回池中,这样不就好了吗?

这么想貌似是没什么问题,但是你细品就会发现一个大问题,那就是:Connection是JDBC规范中的一个接口啊。具体的怎么获取到数据库的连接(也就是上边一直说的conn对象)那是具体不同的数据库厂商来实现的,也就是咱们平常项目中导入的数据库驱动jar包中,oracle驱动jar包中有oracle的实现方式,mysql驱动jar包中有mysql的实现方式。这些数据库厂商们他们都实现了JDBC的接口规范,也就是说数据库厂商肯定实现了Connection这个接口(因为这是JDBC的规范),具体到底怎么获取到数据库连接,他们在实现类里肯定有实现。就等于说如果你用的是oracle数据库,那么这个conn对象就是oracle驱动jar包给你返回的;如果你用的是mysql数据库,那么这个conn对象就是mysql的驱动jar包给你返回的。


所以说这个conn对象是数据库驱动包里的,如果是像上边说的直接去改一下conn中的close方法来实现把连接放回池中,那岂不是去修改数据库驱动jar包中的源码吗?这简直是天方夜谭啊兄dei,肯定行不通的!


那写到到这儿估计有小伙伴估计会有另外一个疑问:那数据库厂商们直接把这个close方法给写成咱们想要的不就好了,也免的咱们去改了,多麻烦(而且关键是jar包里的内容咱也改不了啊。。。)


咱们继续思考:各个数据库厂商实现的是JDBC规范,而JDBC规范中的close方法就是要求立即关闭连接,并不是把连接放回池中。


所以数据库厂商的实现类中这么实现close方法并没有问题,数据库厂商负责实现的close就应该是立即释放资源。


下面的截图是JDK API中官方对Connection接口中close方法的解释

大白话聊懂Java中的连接池,用包装模式实现标准的DataSource数据源连接池


所以数据库厂商本身就不应该关心连接池的问题,如果它close方法的做法是把连接放回池中,那它放回到哪个池中呢,它是不是还要写个连接池什么的?这肯定不科学啊,对吧。


关于怎么更好、更高效率的去使用这些连接,这不是数据库厂商要考虑的事情,而是连接池需要考虑的,所以就出现了DBCP、C3P0这些连接池。


好了,问题及分析就先说到这儿。


接下来就该考虑:既然上边说的方法行不通,那该怎么办呢?


第一种比较能想到的方式就是利用Java中的包装设计模式来解决


第二种就是用动态代理返回conn的代理对象来解决


今天就来讲一下第一种用包装来实现。


撸码之前先简单的讲一下利用包装实现的原理:


其实很简单,咱们实现的目标不就是在调用close方法的时候做一下处理吗,其他的方法该调原有对象还调原有对象的方法。


所以咱们就自定义一个自己的类也实现Connection接口,然后把原有的conn对象通过构造方法注入进来,为什么要注入原有的conn对象呢?因为调用其他方法的时候咱们必须得用原有的conn对象来调用啊,所以原有的对象必须得注入进来。最后在使用用conn对象的地方,把咱们自定义的conn对象返回去就好了。

二、具体代码实现

第一步:写一个自定义的conn类

/**
 *
 * @author caoju
 *
 */

public class MyConnection implements Connection{
  
  //原有的数据库连接
  private Connection conn;
  //数据源中的池
  private LinkedList<Connection> pool;
  //通过构造函数注入
  public MyConnection(Connection conn,LinkedList<Connection> pool){
    this.conn = conn;
    this.pool = pool;
  }
  
  @Override
  public void close() throws SQLException {
    //close方法做咱们需要的处理,也就是放回池中
    pool.add(conn);
  }
  
  @Override
  public PreparedStatement prepareStatement(String sql) throws SQLException {
    //其他的方法什么改变都不做,还是用原来的conn对象来调用
    //所以除了close方法咱们特殊处理一下,其他的方法没一点变化,保持原有的功能
    return conn.prepareStatement(sql);
  }
  @Override
  public Statement createStatement() throws SQLException {
    //其他的方法什么改变都不做,还是用原来的conn对象来调用
    //所以除了close方法咱们特殊处理一下,其他的方法没一点变化,保持原有的功能
    return conn.createStatement();
  }
  
  @Override
  public <T> T unwrap(Class<T> iface) throws SQLException {
    // TODO Auto-generated method stub
    //下边的方法一样都用原有的对象把调用保持下去
    //由于Connection接口中方法很多,在这儿就不一一搞了
    return null;
  }
 
  @Override
  public boolean isWrapperFor(Class<?> iface) throws SQLException {
    // TODO Auto-generated method stub
    return false;
  }
 
  
  @Override
  public CallableStatement prepareCall(String sql) throws SQLException {
    // TODO Auto-generated method stub
    return null;
  }
  .......
}


这里需要注意一个问题就是,由于自定义的MyConnection类也实现了Connection接口,所以需要实现Connection接口里所有的方法,里边有几十个方法,代码太长了,贴上来影响阅读,我就省略号了。。。大家自己写的时候需要全部实现。


第二步:自定义一个DataSource


/**
 *
 * @author caoju
 *
 */

public class MyDataSource implements DataSource{
 
  private static LinkedList<Connection> pool = new LinkedList<Connection>();
  private static final String name = "com.mysql.jdbc.Driver";
  private static final String url = "jdbc:mysql://192.168.199.188:3306/cj";
  private static final String user = "root";
  private static final String password = "123456";
    
    static{//利用静态代码块儿在类一加载的时候就初始化10个连接到池中
    try {
      Class.forName(name);
      for(int i=0;i<10;i++){
        Connection conn = DriverManager.getConnection(url, user, password);
        pool.add(conn);
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
    
  @Override
  public Connection getConnection() throws SQLException {
    if(pool.size()>0){
      Connection conn = pool.remove();
      //此处返回的已经不是原有的conn对象了,而是咱们自定义的包装类对象
      return new MyConnection(conn,pool);
    }else{
      throw new RuntimeException("对不起,服务器忙...");
    }
  }
  
  
  
  @Override
  public PrintWriter getLogWriter() throws SQLException {
    return null;
  }
 
  @Override
  public void setLogWriter(PrintWriter out) throws SQLException {
    // TODO Auto-generated method stub
    
  }
 
  @Override
  public void setLoginTimeout(int seconds) throws SQLException {
    // TODO Auto-generated method stub
    
  }
 
  @Override
  public int getLoginTimeout() throws SQLException {
    // TODO Auto-generated method stub
    return 0;
  }
 
  @Override
  public Logger getParentLogger() throws SQLFeatureNotSupportedException {
    // TODO Auto-generated method stub
    return null;
  }
 
  @Override
  public <T> T unwrap(Class<T> iface) throws SQLException {
    // TODO Auto-generated method stub
    return null;
  }
 
  @Override
  public boolean isWrapperFor(Class<?> iface) throws SQLException {
    // TODO Auto-generated method stub
    return false;
  }
 
 
  @Override
  public Connection getConnection(String username, String password)
      throws SQLException
{
    // TODO Auto-generated method stub
    return null;
  }
  
}


第三步:写个调用端测试一下


/**
 *
 * @author caoju
 *
 */

public class Client {
 
  public static void main(String[] args) {
    DataSource ds = new MyDataSource();
 
    Connection conn = null;
    PreparedStatement ps = null;
    try {
      conn = ds.getConnection();
      ps = conn.prepareStatement("select * from student");
      ResultSet rs = ps.executeQuery();
      List<Student> stuList = new ArrayList<Student>();
      while(rs.next()){
        Student student = new Student();
        String id = rs.getString("id");
        String name = rs.getString("name");
        student.setId(id);
        student.setName(name);
        stuList.add(student);
      }
      for (Student student : stuList) {
        System.out.println(student);
      }
      
    } catch (Exception e) {
      e.printStackTrace();
    }finally{
      if(ps!=null){
        try {
          ps.close();
        } catch (SQLException e) {
          e.printStackTrace();
        }
      }
      if(conn!=null){
        try {
          conn.close();
        } catch (SQLException e) {
          e.printStackTrace();
        }
      }
    }
  
  }
 
}


最后,打个断点瞅一眼

可以看到,返回的对象是咱们自定义的MyConnection类,而不是原有的conn对象。

最后的运行结果:


到这儿用包装设计模式来实现连接池就搞定啦,代码还是比较简单,大家可以动手试一试。

供大家参考,若有错误的地方希望大家包涵并及时指出

铁子们,如果觉得文章对你有所帮助,可以点关注,点赞