JDBC代码实现之第二版:代码封装与参数提取
前言
由 代码实现之第一版:注册驱动与创建连接 可以发现,它的具体实现麻烦而啰嗦,但这是jdbc访问数据库的第一步的操作,想要执行sql语句,这也是很关键的一步。而在做项目时,我们要经常的,频繁的访问数据库,每次增删改查都敲一遍这段代码,显然这不现实,也不科学。所以需要想办法将其简化,封装,将注册驱动,创建连接以及关闭连接 这段代码封装好,做成一个工具类,每次通过调用工具类中的方法来管理连接。我们管理一段代码的最好方式,就是将它们封装起来,以后便于复用和阅读。
1.原理
- 有关jdbc连接的代码之所以麻烦,其主要原因是它得try-catch,而且处处都要try-catch,很不方便,再有一个是,数据库连接需要指定几个参数,比如驱动的类名,访问数据库的路径,账号和密码,而且这些参数都是字符串,开发工具没有提示信息,手写的话也很不方便。为解决这两个麻烦,简化代码,下面创建一个工具类叫DBTool,跟数据库相关的工具。并希望这个工具能被更多的人使用,最起码达到让自己项目组中的程序员都能用的这个目标来写(高级程序员)。
- 在DBTool这个工具类中,首先需要提供一个方法给调用者创建连接,叫getConnection(),这个方法会为调用者返回一个连接,即Connection接口的一个实例化对象,就是某个数据库驱动类的具体实现。为了调用者使用这个方法方便,并将其声明为静态的,这样的话,就不用实例化,可以直接调用。所以我们朴素的想法就是要写一个方法给别人用,但是再具体深入,我们创建连接无非就是把JDBC代码实现第一版中的那段逻辑的代码,贴到这个方法里来,就可以了。不过还有一个问题就是参数问题,我们需要写一个配置文件,将需要提取出来的参数提前写入配置文件,并由实施人员改好,这个配置文件里的参数是为getConnection()这个方法服务的。我们调用getConnection()方法创建连接时,要读取这些参数,那么我们程序在执行时,会频繁的对数据库执行增删改查操作,要经常获取连接,而每次建立连接都读一遍配置文件,获取这些参数,读文件时,I/O是比较耗效率的一个操作。所以应该减少读文件的次数,最好是只读一次,读完之后把数据存到内存里,方便反复读取。那么java中的对象和变量是存储在内存中的,把数据存储到变量上,实际就是存储到了内存里,以后就从内存里读取即可。
- 为了保证只读一次配置文件,需要将读取文件的代码写在静态块里面,因为static在类加载时,只执行一遍。另外,类加载时,在static块里对文件读取之后,读取的文件内容是要给getConnection()方法用,但是static里读到的内容是不能直接被getConnection()方法用的,所以还需要把读到的内容做成一个变量,这变量是全局的,那么getConnection()方法就能够直接取到这个变量的值。按照此方式最终封装成一个工具DBTool,解决冗杂的try-catch代码与参数问题。从而实现了参数灵活可配,且只读一次,效率较高,调用者,有任意多个调用者可以复用getConnetion方法,提高编程效率。
步骤如下:
- 先有配置文件db.properties,写好参数;
- 再写static静态块,读取参数;
- 将读取的参数存入全局变量;
- getConnection()方法通过全局变量对参数加以使用;
- 调用者通过getConnection()方法得到连接
2.参数问题
配置文件:
-
我们将代码(包括处理参数)提取出来进行封装,不仅为了简化开发步骤,更重要的一个原因是便于修改。因为这个参数,将来是要修改的,如果说你在这个方法中把它写死了,就不方便修改,比如,这个项目做完以后,由实施人员上线,在给客户安装的时,客户有没有可能说,我不用Oracle,我用Mysql,就即便用Oracle,那客户的IP地址和你的是不一样的。
-
另外,因为在客户现场部署上线工作的人员是实施工程师,那实施人员拿这个软件给客户安装,问客户说,你的数据库的ip地址是什么呀,账号密码是什么呀,客户说是什么,然后实施就得改,他改的话,改一个类,实施不会改,因为实施人员,他一般是不懂编程的,他只会用这个软件,他并不会写代码,他也不会编译,所以如果把参数写死在方法里,实施人员改不了,很不方便。最好的方法就是把这个参数,单独提取到一个配置文件里,让实施人员去改一个配置文件,这个他可以改,因为配置文件改完以后,不用编译,而java代码改完以后,必须得编译。
-
我们可以用任意的文件做配置文件,比如说普通的文本文件,word,Excel等,其实什么都可以。但是在工作中常用的配置文件只有两种:XML文件和properties文件。如果是复杂的参数,有相互的复杂关系,和层次结构,就用XML文件,如果是简单参数,没有复杂结构,就写到properties文件里。使用properties存储简单的参数,这是Java这个语言当中经典的用法,工作时会发现项目组的人都是这么干的。因为这里参数之间没有层次结构,就用properties文件,并取名为db.properties。
-
properties文件的语法很简单,就是key-value对的形式,但是注意不要随意的加空格,左右都没有空格,以换行结束,另起一行再写。这里需要4个参数,数据库的驱动driver,访问数据库的路径url,用户名user和密码pwd。文件内容如下:
driver=oracle.jdbc.driver.OracleDriver
url=jdbc:oracle:thin:@localhost:1521:orcl
user=SYSxxx
pwd=Oraclexxx这个db.properties文件因为是配置文件,所以放到src/main/resources目录下,另外,由于这文件是老外发明的,它发明之初就不支持中文,所以不能写中文字符,如果实在想写中文的话,得写那个unicode码,那unicode码谁能记得住,所以就用英文写。要写注释的话,也写英文的注释
注释行以#号开头即可。
java读取properties文件
-
Java为我们提供了一个工具类叫Properties,即java.util.Properties,这是Java自带的一个类,因其继承于Hashtable,所以它本质上是Map,但它的作用就是专门用来读properties文件的。因为Properties对象本身是个Map,而.properties文件中的也是以key-value键值对的方式存储的,Map的结构和这个.properties文件中的内容的结构是一致的,所以这个实例化的Properties对象的取值规则也是和Map是一样的,根据key取value。对已经写好的db.properties文件中key为driver进行取值,代码示例如下:
public class Test {
/**
* 如何读取properties文件
* 1.使用java.util.Properties类
* 2.它本质上就是Map
* 3.它专门用来读取properties文件
*/
@Test
public void test() {
//实例化Properties,调用它load方法来读取properties文件
Properties p = new Properties();
try {
//使用ClassLoader从classes下获取db.properties,这个文件的输入流,作为load方法的参数.
p.load(Test.class.getClassLoader().getResourceAsStream(“db.properties” ));
//调用Properties类的getProperties()方法,根据key获取文件中value的值.
String driver = p.getProperty(“driver”);
System.out.println(driver);
} catch (IOException e) {
e.printStackTrace();
}
}
}
解释:
- Properties类中有一个方法load(InputStream inStream),英文的加载,就是读取的意思,即通过Properties类中的load()方法读取properties文件。那么这个load方法,它要读取文件,需要我们传入一些参数。它要求我们传入一个输入流InputStream,当然,这个输入流是所要读取的,这个文件的输入流,我们可以new FileInputStream,可以那样做,但使用这种方式做,获得这个文件db.properties的输入流:
p.load(Test.class.getClassLoader().getResourceAsStream("db.properties"));
这句话的意思是,使用ClassLoader类加载器,从classes目录下去读这个db.properties文件。另外,这个load方法声明抛了异常,需要try-catch进行异常处理。
1.ClassLoader,英文的类加载器,就是能够加载类,加载class文件,读取class文件。
2.Test.class,调用Test类,这个类它能够获取到类加载器ClassLoader,类加载器就具有一个能力,它默认就从classes目录下读东西,其实,任何类都能取到,可以写Test.class.getClassLoader(),也可以写任何其它的类,但是通常,我们写当前的类,不要依赖于别人,万一别人那个类被删掉了,影响你,你就依赖于自己是最好的。
3.Test.class.getClassLoader().getResourceAsStream(“db.properties”),调用getClassLoader()方法,获取到类加载器ClassLoader以后,再调用ClassLoader的getResourceAsStream(String name)方法,获取资源的InputStream输入流,即db.properties文件的输入流,作为load方法的参数,从而读取到classes目录下db.properties这个文件。当然,它也可以读取到classes之下的其它文件,这个properties顺便也能读。 - load方法需要传入一个所要读取的文件输入流InputStream,这个输入流可以通过
new FileInputStream
获得,也可以通过类加载器的方式获得,这两者的区别是什么?
1.我们在正式的项目当中,我们读配置文件,并不像我们表面所看到的,就从这个resources下去读,其实这个properties文件在我们的项目中,有多个地方有存在这份文件,尽管我们只写了一份,但它存在多份,我们在读的时候,要读对哪一份,这个是关键,而类加载器ClassLoader所加载的那份一定就是我们所需要的那一份,而不用纠结所需要读取的文件具体在哪。
2.那这个db.properties配置文件目前在我们项目中,有两处有,一个是我们的源代码中,它在workspace里的**java源码目录**中,比如`jdbc/src/main/resources`下,另一个是在源代码编译后**字节码文件所处的位置**,比如`jdbc/target/classes`下。根据Eclipse的编译原理,在项目正式的运行环境,服务器只会保留classes目录下的代码和文件,我们读配置文件,一定从classes下面读,如果不从这里读,而从src源码目录下读,当前可以,但将来不行,以后到服务器端时不行。所以,我们就需要从classes下去读,这个万能的,一定可行的,别管是测试代码,还是正式代码都可行。 - Properties类中有两种可以读取配置文件中key-value结构的方法。
get(Object key) : Object - Hashtable - 6%
getProperty(String key) : String - Properties - 51%
1.一种方法是,就是get。传一个参数key,它给你返回一个value,这个方法是从Hashtable继承过来的。但是它返回的是Object,支持所有类型。
2.一个方法叫getProperty,key是String,返回的还是String,这是新加的方法,看它的使用百分比,51%的人用这个,说明这个还是好用的,因为就是,我们这个配置文件里配的这个参数都是字符串,所以我们按照字符串处理就可以了,它也没有别的类型,那就用这个就可以。
3.代码封装
- 为了将jdbc创建连接的代码进行简化,已经对参数问题做了相应的提取,并整理到了properties配置文件中,另外就是对那些繁杂的try-catch进行封装到DBTool这个工具类中,首先需要提供一个方法用来给调用者创建连接,叫getConnection(),这个方法会为调用者返回一个连接,即Connection接口的一个实例化对象,就是某个数据库驱动类的具体实现。为了调用者使用这个方法方便,并将其声明为静态的,这样的话,就不用实例化,可以直接调用。所以我们朴素的想法就是要写一个方法给别人用,那么创建连接无非就是把JDBC代码实现第一版中的那段逻辑的代码,贴到那方法里来,就可以了。
- 首先在src/main/java下建个包,写类之前,必须要有包,遵守规则,包里要放的是我们要封装的工具。包名叫util,包下建一个类,类名叫DBTool,(包名和类名依据个人习惯即可)。这个工具类有三部分内容,第一部分是变量,第二部分静态代码块,第三部分方法实现。代码结构:
package util;
import java.sql.Connection;
public class DBTool {
private static String driver;
private static String url;
private static String user;
private static String pwd;
static {
}
public static Connection getConnection() {
return null;
}
}
变量
-
先有配置文件db.properties,写好参数;配置文件db.properties:
driver=oracle.jdbc.driver.OracleDriver
url=jdbc:oracle:thin:@localhost:1521:orcl
user=SYSxxx
pwd=Oraclexxx -
在配置文件中有4个参数,我们需要有四个变量存储这4个参数,并在静态的getConnection()方法里去使用它们,所以这四个变量也得为静态的,因为我们在这个类中局部使用就可以,外界不需要用,所以把四个变量的访问权限指定为私有属性private。代码示例:
package util;
public class DBTool {
private static String driver;
private static String url;
private static String user;
private static String pwd;
}
静态块
静态代码块static,是在类加载时,只执行一次的内容,包括
-
读取配置文件,为变量赋值:首先实例化Properties,调用这个类的load方法,通过类加载器ClassLoader的getResourceAsStream获取db.properties文件的输入流作为load方法的参数。获取类加载器时,我们zuihao依赖于自己本身的类,不要依赖于别人,万一别人那个类被删了,会对我们有影响。同时,load方法声明抛了异常IOException,要对其进行try-catch,若捕获到异常,按照异常处理原则进行统一处理。
Properties p = new Properties();
p.load(DBTool.class.getClassLoader().getResourceAsStream(“db.properties”)); -
注册驱动:得到参数变量之后,getConnection方法会调用这些参数去创建连接,但创建连接之前还要注册驱动。因为创建连接,要调DriverManager,它帮我们创建连接的前提是,我得告诉他哪个驱动,我得注册驱动,而注册驱动就一句话Class.forName(driver);,而且也是只需要注册一次即可,所以写在static里,同时,forName方法声明抛了异常ClassNotFoundException,也要对其进行try-catch,若捕获到异常,按照异常处理原则进行统一处理。
方法
创建连接:getConnection()
-
这里的方法是要被调用者调用的,它必须得是共有的,为了别人调用方便,把它做成静态的static。因为调用者是要利用这个方法获取连接,所以这个方法有一个返回值Connection。方法名要做到见名知义,比较直观,一般叫getConnection()获取连接。最后先给它return null;,防止报错,后续再详细实现。
public static Connection getConnection() {
return null;
} -
方法的实现是这样的,在getConnection方法内部,我们调用DriverManager的getConnection方法,分别传入访问数据库的IP地址,账号和密码三个参数即可。另外,getConnection方法声明抛了异常SQLException,也得处理异常。但注意,这块处理异常,就不try-catch了,我直接往外抛。原则上是能处理就处理,但此处我把它抛出去,是希望强制调用者去try-catch。因为我们创建连接,执行完sql以后,最终一定要关闭连接。而且一定是在finally中关闭。无论报不报异常,最终都得关闭异常,那我强制调用者try-catch,调用者很容易会想起来,还得写finally,
关闭异常。起到一个提示的作用。如果说呢,调用者一看,不用处理异常,他可能会忘记try-catch,也会忘记写finally,为了防止这一点。所以我强制抛个异常让他try-catch,从而起到一个提醒的作用,是这么一个目的我们就抛出去。Add throws declaration,往外抛,那调用者自然就必须得处理了,不处理不行了。
关闭连接:getClose()
- 另外,我们在finally中关闭连接的时候,也得try-catch,这try-catch其实也挺麻烦的,所以把关闭连接的这段代码也封装一下。在DBTool工具类中,再多一个方法,为了让调用者方便调用,这个方法也得是公用的,也得是静态的,然后这个关闭连接,不需要返回值,把连接关掉即可,所以没有返回值,方法名叫close。为了帮助调用者关闭连接,调用者需要把连接传进来,所以方法有一个参数Connection conn。把参数传进来以后,在关闭连接之前最好做个判断,严谨一点,因为万一调用者传给我一个空值怎么办,所以判断一下,如果传入的连接确实不为空,就帮助调用者关闭连接,最后因为调用Connection的close方法时,也声明抛了异常,还得try-catch,而我们封装的主要目的就是减少调用者去try-catch,所以此处不直接往上抛,因为如果我们写的代码,封装的方法,到处都是还直接往上抛异常的话,调用者一调我们这方法,还是要各种try-catch,他还是很麻烦,而写那个代码,麻烦就麻烦在,到处都要try-catch。所以我们一定要把这个控制住,都不要到处都抛异常,那即便是抛,也抛个RuntimeException,调用者愿意catch就catch,不愿意catch也可以不catch,他就方便了。
/**
* 连接的关闭:
* 在工具类中定义公共的关闭连接的方法
* 所有访问数据库的应用,共享此方法
*/
public static void closeConnection(Connection conn) {
if(conn != null) {
try{
conn.close();
} catch(SQLException e) {
e.printStackTrace();
}
}
} - 所以对close加个try-catch,那catch到异常以后,我们还是处理不了,那记录日志,之后向上抛,那我们catch以后向上抛,和我直接向上抛,不都是抛,其区别就在于,如果没有try-catch直接抛出去,就是你什么都没干,什么都没处理,但我catch后,catch里,我也记日志,记完日志,我发现我处理不了,我把这个异常写个标题,再往上抛,起码有我的处理的一个方式,这个和我没处理,直接抛上去,是有本质区别的。一个是你处理过,一个是你没处理过,差在这,所以我们该catch还是要catch。这里往上抛一下,抛个RuntimeException,zhuyao是调用者可以不去非得处理它,这样的话,调用者方便一点。
代码示例
package util;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
public class DBTool {
private static String driver;
private static String url;
private static String user;
private static String pwd;
static {
Properties p = new Properties();
try {
//加载配置文件 (加载就是读取的意思,但我们习惯于说加载,显得专业。专业人士都这么讲。)
p.load(DBTool.class.getClassLoader().getResourceAsStream("db.properties"));
//读取连接参数
driver = p.getProperty("driver");
url = p.getProperty("url");
user = p.getProperty("user");
pwd = p.getProperty("pwd");
//注册驱动
Class.forName(driver);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("加载db.properties失败",e);
} catch (ClassNotFoundException e) {
e.printStackTrace();
throw new RuntimeException("找不到这个驱动",e);
}
}
public static Connection getConnection() throws SQLException {
//conn = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:orcl", "SYSxxx", "Oraclexxx");
return DriverManager.getConnection(url, user, pwd);
}
public static void close(Connection conn) {
if(conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException("关闭连接失败",e);
}
}
}
}
4.测试
- 现在DBTool这工具就有了创建连接getConnection()和关闭连接close(),这里通过JUnit测试工具对其进行测试,通过调用DBTool工具中的getConnection()方法创建连接,为了提醒调用者在finally中关闭连接,这个方法声明抛了异常,强制调用者catch,并在finally中调用DBTool的close()方法将连接关闭,否则,数据库很容易崩溃。
/**
* 测试DBTool.getConnection()
*/
@Test
public void test() {
Connection conn = null;
try {
conn = DBTool.getConnection();
System.out.println(conn);
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBTool.close(conn);
}
}
- 输出结果:
oracle.jdbc.driver.T4CConnection@1ff8b8f
5.异常分析:
- 编程时,最容易出现的问题就是,拼写错误,那么,拼写错误是出现在字符串里。我们写一个类名,接口名,方法名。他写错了,Java有编译,它有提示,明显的能看出来,你也执行不了。只有字符串儿写错了,没有编译,看不出来。所以要检查这些字符串。这是常出现问题的地方。再一个出现问题了,你也不用怀疑,是不是这个,Eclipse有问题啊。是不是jdk有bug啊,这基本上不可能的,就是那个bug,要让你这么轻易就发现了,他早就发现了,没那么明显,还是你自己的错儿,还得检查。
- @Test注解缺失;
- return DriverManager.getConnection(url, user, pwd);参数顺序颠倒或有错误;
- p.load(DBTool.class.getClassLoader().getResourceAsStream(“db.properties”));配置文件名称不对,或配置文件的内容有误;
参考文献(References)
文中如有侵权行为,请联系me。。。。。。。。。。。。。
文中的错误,理解不到位的地方在所难免,也请指教!在成长过程中,也将继续不断完善,不作为专业文章。不喜勿喷。