前言 
一般的 Jsp Container如 Tomcat,Weblogic都会提供对数据库连接池的支持。这就导致了一般的Java的Web应用的数据库连接池都回是部署在Jsp Container内部的。Web 应用程序通过上下文(Context)环境来访问这些已经被封装好的数据库连接。这种机制给我们带来了很大的方便。这不是我想说的。我想说的是,该Web Application中需要用到数据库连接的代码都必须放到特定Jsp Container中运行。本来这是很正常的要求,但是如过我想用Junit这样的单元测试工具对一个段到数据库连接的代码进行测试时问题就来了。如何在脱离Jsp Container的情况下得到Jsp Container提供的数据库连接池? 
  
方案 
上面的问题好像就时痴人说梦。既然要脱离Jsp Container,还要Jsp Container为我们提供服务。这怎么可能!就像不喜欢一个人,但偏偏又想要吃他做的饭。要么就是忍受他,要么就是不要吃他做的饭。只有两种选择。没办法,不要太亏待自己不该忍受的东西就不要忍受。但是也要吃饭啊(要用数据库连接啊!)。自己做吧。 
好了,说好自己做就动手了。先来分析一下有多少事情要做。 
1.         虚拟出JNDI的Context环境。 
2.         完成数据库连接池的实现。 
3.         把数据连接池挂到虚拟出来的Context环境中。 
大目标已经确立。下面的任务就是干活了。 
实现 
I.          虚拟出JNDI的Context环境 
JNDI(Java Naming and Directory Interface)是一种将Name和Server绑定和映射的机制。他主要是通过Context和InitialContextFactory接口配合工作来实现的。更具体内容可以参看The JNDI Tutorial(http://java.sun.com/products/jndi/tutorial/)。 
Context的任务是维护一个映射HashTabl。该HashTable保存了Name和Server的映射关系。 
维护的内容包括绑定(bind),重新绑定(rebind),释放绑定(unbind),查找(lookup)。 
InitialContextFactory是接口用来创建一个Context。简单实现的这个接口。代码如下: 
public class SimpleContextFactory implements InitialContextFactory 
{ 
  
    private static SimpleContext instance; 
    public Context getInitialContext( Hashtable environment ) 
    { 
       //保证每次创建的Context对象都是同一个对象。这样就可以保证每次得到的上下文 
//都相同 
        if ( instance == null ) 
        { 
            instance = new SimpleContext(); 
        } 
        return instance; 
    } 
} 
  
实现了这两个接口(interface)就可以用它们来虚拟出上下文环境了。 
Hashtable env = new Hashtable(); 
//指定特定的 InitialContextFactory实现对象为上下文工厂(Context Factory)  
env.put( 
                  Context.INITIAL_CONTEXT_FACTORY, 
                  "com.javaranch.unittest.helper.sql.pool.SimpleContextFactory" ); 
       System.setProperty( 
                  Context.INITIAL_CONTEXT_FACTORY, 
                  "com.javaranch.unittest.helper.sql.pool.SimpleContextFactory" ); 
    //用制定的工厂创建InitialContext对象。 
    Context ctx = new InitialContext( env ); 
    然后是对该Context进行一些绑定。 
这样我虚拟出我们自己的上下文环境了。 
II.       完成数据库连接池的实现 
实现数据库连接池,网上有很丰富的资源。我选取了其中的两个做为参考。最终写出了如下的数据库连接池。 
  
/* 
 * Created on 2004-10-13 
 * 
 * To change the template for this generated file go to 
 * Window>Preferences>Java>Code Generation>Code and Comments 
 */ 
package com.benq.database.connectionpool; 
  
import java.sql.Connection; 
import java.sql.DatabaseMetaData; 
import java.sql.Driver; 
import java.sql.DriverManager; 
import java.sql.SQLException; 
import java.sql.Statement; 
import java.util.Enumeration; 
import java.util.Vector; 
import java.lang.reflect.*; 
/**  
* ConnectionPool 类创建了一个对特定数据库指定大小的连接池。连接池对象  
* 允许客户端指定 JDBC 驱动程序,数据库,使用数据库的用户名和密码。而且,  
* 客户端能指定连接池的在初始创建是产生数据库连接的数量,和指定当连接  
* 不够时每次自动增加连接的数量及连接池最多的数据库连接的数量。  
*  
* 对外提供的方法有: ConnectionPool :构造函数  
* getInitialConnections: 返回连接池初始化大小  
* setInitialConnections: 设置连接池初始化大小  
* getIncrementalConnections: 返回连接池自动增加的增量  
* setIncrementalConnections: 设置连接池自动增加的大小  
* getMaxConnections :获得连接池的最大可允许的连接数  
* setMaxConnections :设置连接池的最大可允许的连接数  
* getTestTable :获得测试表的名字  
* setTestTable :设置测试表的名字  
* createPool: 创建连接池 , 线程己同步  
* getConnection: 从连接池中获得一个数据库连接  
* returnConnection: 返回一个连接到连接池中  
* refreshConnections: 刷新连接池  
* closeConnectionPool: 关闭连接池  
*   
 */ 
  
public class ConnectionPool { 
  
    private String jdbcDriver = ""; // 数据库驱动  
  
    private String dbUrl = ""; // 数据 URL  
  
    private String dbUsername = ""; // 数据库用户名  
  
    private String dbPassword = ""; // 数据库用户密码  
  
    private String testTable = ""; // 测试连接是否可用的测试表名,默认没有测试表  
  
    private int initialConnections = 2; // 连接池的初始大小  
  
    private int incrementalConnections = 2; // 连接池自动增加的大小  
  
    private int maxConnections = 50; // 连接池最大的大小  
  
    private Vector connections = null; // 存放连接池中数据库连接的向量 , 初始时为 null  
  
    // 它中存放的对象为 PooledConnection 型  
  
    /**  
     
    * 构造函数  
     
    *  
     
    * @param jdbcDriver String JDBC 驱动类串  
     
    * @param dbUrl String 数据库 URL  
     
    * @param dbUsername String 连接数据库用户名  
     
    * @param dbPassword String 连接数据库用户的密码  
     
    *  
     
    */ 
  
    public ConnectionPool( 
       String jdbcDriver, 
       String dbUrl, 
       String dbUsername, 
       String dbPassword) { 
  
       this.jdbcDriver = jdbcDriver; 
  
       this.dbUrl = dbUrl; 
  
       this.dbUsername = dbUsername; 
  
       this.dbPassword = dbPassword; 
  
    } 
     
    public ConnectionPool(ConnectionParam conParam){ 
       this.jdbcDriver = conParam.getDriver(); 
       this.dbUrl = conParam.getUrl(); 
       this.dbUsername = conParam.getUser(); 
       this.dbPassword = conParam.getPassword(); 
        
        maxConnections = Integer.parseInt(conParam.getMaxWait()); 
       //maxIdle  = Integer.parseInt(conParam.getMaxIdel()); 
    } 
  
    /**  
     
    * 返回连接池的初始大小  
     
    *  
     
    * @return 初始连接池中可获得的连接数量  
     
    */ 
  
    public int getInitialConnections() { 
  
       return this.initialConnections; 
  
    } 
  
    /**  
     
    * 设置连接池的初始大小  
     
    *  
     
    * @param 用于设置初始连接池中连接的数量  
     
    */ 
  
    public void setInitialConnections(int initialConnections) { 
  
       this.initialConnections = initialConnections; 
  
    } 
  
    /**  
     
    * 返回连接池自动增加的大小 、  
     
    *  
     
    * @return 连接池自动增加的大小  
     
    */ 
  
    public int getIncrementalConnections() { 
  
       return this.incrementalConnections; 
  
    } 
  
    /**  
     
    * 设置连接池自动增加的大小  
     
    * @param 连接池自动增加的大小  
     
    */ 
  
    public void setIncrementalConnections(int incrementalConnections) { 
  
       this.incrementalConnections = incrementalConnections; 
  
    } 
  
    /**  
     
    * 返回连接池中最大的可用连接数量  
     
    * @return 连接池中最大的可用连接数量  
     
    */ 
  
    public int getMaxConnections() { 
  
       return this.maxConnections; 
  
    } 
  
    /**  
     
    * 设置连接池中最大可用的连接数量  
     
    *  
     
    * @param 设置连接池中最大可用的连接数量值  
     
    */ 
  
    public void setMaxConnections(int maxConnections) { 
  
       this.maxConnections = maxConnections; 
  
    } 
  
    /**  
     
    * 获取测试数据库表的名字  
     
    *  
     
    * @return 测试数据库表的名字  
     
    */ 
  
    public String getTestTable() { 
  
       return this.testTable; 
  
    } 
  
    /**  
     
    * 设置测试表的名字  
     
    * @param testTable String 测试表的名字  
     
    */ 
  
    public void setTestTable(String testTable) { 
  
       this.testTable = testTable; 
  
    } 
  
    /**  
     
    *  
     
    * 创建一个数据库连接池,连接池中的可用连接的数量采用类成员  
     
    * initialConnections 中设置的值  
     
    */ 
  
    public synchronized void createPool() throws Exception { 
  
       // 确保连接池没有创建  
  
       // 如果连接池己经创建了,保存连接的向量 connections 不会为空  
  
       if (connections != null) { 
  
           return; // 如果己经创建,则返回  
  
       } 
  
       // 实例化 JDBC Driver 中指定的驱动类实例  
  
       Driver driver = (Driver) (Class.forName(this.jdbcDriver).newInstance()); 
  
       DriverManager.registerDriver(driver); // 注册 JDBC 驱动程序  
  
       // 创建保存连接的向量 , 初始时有 0 个元素  
  
       connections = new Vector(); 
  
       // 根据 initialConnections 中设置的值,创建连接。  
  
       createConnections(this.initialConnections); 
  
       System.out.println(" 数据库连接池创建成功! "); 
  
    } 
  
    /**  
     
    * 创建由 numConnections 指定数目的数据库连接 , 并把这些连接  
     
    * 放入 connections 向量中  
     
    *  
     
    * @param numConnections 要创建的数据库连接的数目  
     
    */ 
  
    private void createConnections(int numConnections) throws SQLException { 
  
       // 循环创建指定数目的数据库连接  
  
       for (int x = 0; x < numConnections; x++) { 
  
           // 是否连接池中的数据库连接的数量己经达到最大?最大值由类成员 maxConnections  
  
           // 指出,如果 maxConnections 为 0 或负数,表示连接数量没有限制。  
  
           // 如果连接数己经达到最大,即退出。  
  
           if (this.maxConnections > 0 
              && this.connections.size() >= this.maxConnections) { 
  
              break; 
  
           } 
  
           //add a new PooledConnection object to connections vector  
  
           // 增加一个连接到连接池中(向量 connections 中)  
  
           try { 
  
              connections.addElement(new PooledConnection(newConnection(),this)); 
  
           } catch (SQLException e) { 
  
              System.out.println(" 创建数据库连接失败! " + e.getMessage()); 
  
              throw new SQLException(); 
  
           } 
  
           System.out.println(" 数据库连接己创建 ......"); 
  
       } 
  
    } 
  
    /**  
     
    * 创建一个新的数据库连接并返回它  
     
    *  
     
    * @return 返回一个新创建的数据库连接  
     
    */ 
  
    private Connection newConnection() throws SQLException { 
  
       // 创建一个数据库连接  
  
       Connection conn = 
           DriverManager.getConnection(dbUrl, dbUsername, dbPassword); 
  
       // 如果这是第一次创建数据库连接,即检查数据库,获得此数据库允许支持的  
  
       // 最大客户连接数目  
  
       //connections.size()==0 表示目前没有连接己被创建  
  
       if (connections.size() == 0) { 
  
           DatabaseMetaData metaData = conn.getMetaData(); 
  
           int driverMaxConnections = metaData.getMaxConnections(); 
  
           // 数据库返回的 driverMaxConnections 若为 0 ,表示此数据库没有最大  
  
           // 连接限制,或数据库的最大连接限制不知道  
  
           //driverMaxConnections 为返回的一个整数,表示此数据库允许客户连接的数目  
  
           // 如果连接池中设置的最大连接数量大于数据库允许的连接数目 , 则置连接池的最大  
  
           // 连接数目为数据库允许的最大数目  
  
           if (driverMaxConnections > 0 
              && this.maxConnections > driverMaxConnections) { 
  
              this.maxConnections = driverMaxConnections; 
  
           } 
  
       } 
  
       return conn; // 返回创建的新的数据库连接  
  
    } 
  
    /**  
     
    * 通过调用 getFreeConnection() 函数返回一个可用的数据库连接 ,  
     
    * 如果当前没有可用的数据库连接,并且更多的数据库连接不能创  
     
    * 建(如连接池大小的限制),此函数等待一会再尝试获取。  
     
    *  
     
    * @return 返回一个可用的数据库连接对象  
     
    */ 
  
    public synchronized Connection getConnection() throws SQLException { 
  
       // 确保连接池己被创建  
  
       if (connections == null) { 
  
           return null; // 连接池还没创建,则返回 null  
  
       } 
  
       Connection conn = getFreeConnection(); // 获得一个可用的数据库连接  
  
       // 如果目前没有可以使用的连接,即所有的连接都在使用中  
  
       while (conn == null) { 
  
           // 等一会再试  
  
           wait(250); 
  
           conn = getFreeConnection(); // 重新再试,直到获得可用的连接,如果  
  
           //getFreeConnection() 返回的为 null  
  
           // 则表明创建一批连接后也不可获得可用连接  
  
       } 
  
       return conn; // 返回获得的可用的连接  
  
    } 
  
    /**  
     
    * 本函数从连接池向量 connections 中返回一个可用的的数据库连接,如果  
     
    * 当前没有可用的数据库连接,本函数则根据 incrementalConnections 设置  
     
    * 的值创建几个数据库连接,并放入连接池中。  
     
    * 如果创建后,所有的连接仍都在使用中,则返回 null  
     
    * @return 返回一个可用的数据库连接  
     
    */ 
  
    private Connection getFreeConnection() throws SQLException { 
  
       // 从连接池中获得一个可用的数据库连接  
  
       Connection conn = findFreeConnection(); 
  
       if (conn == null) { 
  
           // 如果目前连接池中没有可用的连接  
  
           // 创建一些连接  
  
           createConnections(incrementalConnections); 
  
           // 重新从池中查找是否有可用连接  
  
           conn = findFreeConnection(); 
  
           if (conn == null) { 
  
              // 如果创建连接后仍获得不到可用的连接,则返回 null  
  
              return null; 
  
           } 
  
       } 
  
       return conn; 
  
    } 
  
    /**  
     
    * 查找连接池中所有的连接,查找一个可用的数据库连接,  
     
    * 如果没有可用的连接,返回 null  
     
    *  
     
    * @return 返回一个可用的数据库连接  
     
    */ 
  
    private Connection findFreeConnection() throws SQLException { 
  
       Connection conn = null; 
  
       PooledConnection pConn = null; 
  
       // 获得连接池向量中所有的对象  
  
       Enumeration enum = connections.elements(); 
  
       // 遍历所有的对象,看是否有可用的连接  
  
       while (enum.hasMoreElements()) { 
  
           pConn = (PooledConnection) enum.nextElement(); 
  
           if (!pConn.isBusy()) { 
  
              // 如果此对象不忙,则获得它的数据库连接并把它设为忙  
  
              conn = pConn.getConnection(); 
  
              pConn.setBusy(true); 
  
              // 测试此连接是否可用  
  
              if (!testConnection(conn)) { 
  
                  // 如果此连接不可再用了,则创建一个新的连接,  
  
                  // 并替换此不可用的连接对象,如果创建失败,返回 null  
  
                  try { 
  
                     conn = newConnection(); 
  
                  } catch (SQLException e) { 
  
                     System.out.println(" 创建数据库连接失败! " + e.getMessage()); 
  
                     return null; 
  
                  } 
  
                  pConn.setConnection(conn); 
  
              } 
  
              break; // 己经找到一个可用的连接,退出  
  
           } 
  
       } 
  
       return conn; // 返回找到到的可用连接  
  
    } 
  
    /**  
     
    * 测试一个连接是否可用,如果不可用,关掉它并返回 false  
     
    * 否则可用返回 true  
     
    *  
     
    * @param conn 需要测试的数据库连接  
     
    * @return 返回 true 表示此连接可用, false 表示不可用  
     
    */ 
  
    private boolean testConnection(Connection conn) { 
  
       try { 
  
           // 判断测试表是否存在  
  
           if (testTable.equals("")) { 
  
              // 如果测试表为空,试着使用此连接的 setAutoCommit() 方法  
  
              // 来判断连接否可用(此方法只在部分数据库可用,如果不可用 ,  
  
              // 抛出异常)。注意:使用测试表的方法更可靠  
  
              conn.setAutoCommit(true); 
  
           } else { // 有测试表的时候使用测试表测试  
  
              //check if this connection is valid  
  
              Statement stmt = conn.createStatement(); 
  
              stmt.execute("select count(*) from " + testTable); 
  
           } 
  
       } catch (SQLException e) { 
  
           // 上面抛出异常,此连接己不可用,关闭它,并返回 false;  
  
           closeConnection(conn); 
  
           return false; 
  
       } 
  
       // 连接可用,返回 true  
  
       return true; 
  
    } 
  
    /**  
     
    * 此函数返回一个数据库连接到连接池中,并把此连接置为空闲。  
     
    * 所有使用连接池获得的数据库连接均应在不使用此连接时返回它。  
     
    *  
     
    * @param 需返回到连接池中的连接对象  
     
    */ 
  
    public void returnConnection(Connection conn) { 
  
       // 确保连接池存在,如果连接没有创建(不存在),直接返回  
  
       System.out.println("Invoce the return Connection method!"); 
       if (connections == null) { 
  
           System.out.println(" 连接池不存在,无法返回此连接到连接池中 !"); 
  
           return; 
  
       } 
  
       PooledConnection pConn = null; 
  
       Enumeration enum = connections.elements(); 
  
       // 遍历连接池中的所有连接,找到这个要返回的连接对象  
  
       while (enum.hasMoreElements()) { 
  
           pConn = (PooledConnection) enum.nextElement(); 
  
           // 先找到连接池中的要返回的连接对象  
  
           if (conn == pConn.getOConnection()) { 
  
              // 找到了 , 设置此连接为空闲状态  
  
              pConn.setBusy(false); 
  
              break; 
  
           } 
  
       } 
  
    } 
  
    /**  
     
    * 刷新连接池中所有的连接对象  
     
    *  
     
    */ 
  
    public synchronized void refreshConnections() throws SQLException { 
  
       // 确保连接池己创新存在  
  
       if (connections == null) { 
  
           System.out.println(" 连接池不存在,无法刷新 !"); 
  
           return; 
  
       } 
  
       PooledConnection pConn = null; 
  
       Enumeration enum = connections.elements(); 
  
       while (enum.hasMoreElements()) { 
  
           // 获得一个连接对象  
  
           pConn = (PooledConnection) enum.nextElement(); 
  
           // 如果对象忙则等 5 秒 ,5 秒后直接刷新  
  
           if (pConn.isBusy()) { 
  
              wait(5000); // 等 5 秒  
  
           } 
  
           // 关闭此连接,用一个新的连接代替它。  
  
           closeConnection(pConn.getConnection()); 
  
           pConn.setConnection(newConnection()); 
  
           pConn.setBusy(false); 
  
       } 
  
    } 
  
    /**  
     
    * 关闭连接池中所有的连接,并清空连接池。  
     
    */ 
  
    public synchronized void closeConnectionPool() throws SQLException { 
  
       // 确保连接池存在,如果不存在,返回  
  
       if (connections == null) { 
  
           System.out.println(" 连接池不存在,无法关闭 !"); 
  
           return; 
  
       } 
  
       PooledConnection pConn = null; 
  
       Enumeration enum = connections.elements(); 
  
       while (enum.hasMoreElements()) { 
  
           pConn = (PooledConnection) enum.nextElement(); 
  
           // 如果忙,等 5 秒  
  
           if (pConn.isBusy()) { 
  
              wait(5000); // 等 5 秒  
  
           } 
  
           //5 秒后直接关闭它  
  
           closeConnection(pConn.getConnection()); 
  
           // 从连接池向量中删除它  
  
           connections.removeElement(pConn); 
  
       } 
  
       // 置连接池为空  
  
       connections = null; 
  
    } 
  
    /**  
    * 关闭一个数据库连接  
    *  
    * @param 需要关闭的数据库连接  
    */ 
  
    private void closeConnection(Connection conn) { 
  
       try { 
  
           conn.close(); 
  
       } catch (SQLException e) { 
  
           System.out.println(" 关闭数据库连接出错: " + e.getMessage()); 
  
       } 
  
    } 
  
    /**  
    * 使程序等待给定的毫秒数  
    *  
    * @param 给定的毫秒数  
     
    */ 
    private void wait(int mSeconds) { 
  
       try { 
  
           Thread.sleep(mSeconds); 
  
       } catch (InterruptedException e) { 
  
       } 
  
    } 
  
    /**  
    *  
    * 内部使用的用于保存连接池中连接对象的类  
    * 此类中有两个成员,一个是数据库的连接,另一个是指示此连接是否  
    * 正在使用的标志。  
    */ 
  
    class PooledConnection implements InvocationHandler { 
  
       Connection connection = null, 
                 newConnection = null; // 数据库连接  
       ConnectionPool connPool = null; 
  
       boolean busy = false; // 此连接是否正在使用的标志,默认没有正在使用  
  
       // 构造函数,根据一个 Connection 构告一个 PooledConnection 对象  
  
       public PooledConnection(Connection connection) { 
  
           this.connection = connection; 
  
       } 
       public PooledConnection(Connection connection ,ConnectionPool connPool ) { 
  
                  this.connPool = connPool; 
                  this.connection = connection; 
                  Class[] interfaces = {Connection.class}; 
                   
                  newConnection = (Connection)Proxy.newProxyInstance( 
                                connection.getClass().getClassLoader(), 
                                interfaces,this); 
              /*Class[] interfaces = {java.sql.Connection.class}; 
            this.connection = (Connection) Proxy.newProxyInstance( 
                con.getClass().getClassLoader(), 
                interfaces, this); 
            m_originConnection = con; 
              */ 
  
  
       } 
       // 返回此对象中的连接  
  
       public Connection getConnection() { 
  
           return newConnection; 
  
       } 
       public Connection getOConnection(){ 
           return connection; 
       } 
  
       // 设置此对象的,连接  
  
       public void setConnection(Connection connection) { 
  
           this.connection = connection; 
  
       } 
  
       // 获得对象连接是否忙  
  
       public boolean isBusy() { 
  
           return busy; 
  
       } 
  
        // 设置对象的连接正在忙  
  
       public void setBusy(boolean busy) { 
  
           this.busy = busy; 
  
       } 
               
       public Object invoke(Object proxy, Method method, Object[] args) 
           throws Throwable { 
           // TODO Auto-generated method stub 
           Object obj = null; 
           System.out.println("invoke in :" + method.getName() ); 
           if("close".equalsIgnoreCase(method.getName())){ 
              connPool.returnConnection(this.connection); 
           }else{ 
              obj = method.invoke(connection,args); 
           } 
            
           return obj; 
       } 
  
    } 
    } 
该数据库连接池第一个原形是abnerchai完成,在此向abnerchai表示感谢。我在他的基础上加入了“close”方法的一个托管。当客户程序调用Connection实例(这种说法时错误的,关于其中细节可以参考Java Dynamic Proxies: One Step from Aspect-oriented Programming一文。可以在http://www.devx.com/Java/Article/21463找到该文 向该文作者Lara D'Abreo表示感谢)的“close”方法时,我将会把它拦截下来,把该数据库连接返回给数据库连接池。以备下次使用。 
III.     把数据连接池挂到虚拟出来的Context环境中 
为了虚拟的足够想,我还将该数据库连接池封装到一个DataSource实现对象中。 
ConnectionPool connectionPool = null; 
  
SimpleDataSource( 
    String jdbcDriver, 
    String dbUrl, 
    String dbUsername, 
    String dbPassword 
    ) throws Exception 
    { 
       super( SimpleDataSource.class.getName() ); 
        
       dbDriver = jdbcDriver; 
       dbServer = dbUrl; 
       dbLogin  = dbUsername; 
       this.dbPassword = dbPassword; 
        //创建该数据连接池 
       connectionPool = new ConnectionPool( 
                         jdbcDriver, 
                         dbUrl, 
                         dbUsername, 
                         dbPassword); 
       //初始化数据库连接池。 
       connectionPool.createPool(); 
} 
  
public Connection getConnection() 
    throws java.sql.SQLException 
    { 
       Connection currentConnection = null; 
if( connectionPool != null ) 
//从数据库连接池中获取数据库连接。 
       currentConnection = connectionPool.getConnection(); 
        
       return currentConnection; 
} 
  
这样我就完成了预定的所有任务。并通过了,Junit的单元测试。哈哈,看到项目中的DB类运行在Tomcat之外,好一个“爽”字了得!! 
   
 
  |