NO END FOR LEARNING

Writing blog if you feel tired | 学海无涯 苦写博客

再次了解JDBC(上)- 从Class.forName到DataSource

| Comments

JDBC(Java Data Base Connectivity,Java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成。

API

先来了解JDBC中几个非常重要的API(以下内容来自JDBC API中文文档,但未完全拷贝,建议还是阅读一下):

java.sql.DriverManager(类):管理一组 JDBC 驱动程序的基本服务。(注:DataSource 接口是 JDBC 2.0 API 中的新增内容,它提供了连接到数据源的另一种方法。使用 DataSource 对象是连接到数据源的首选方法。)作为初始化的一部分,DriverManager 类会尝试加载在 “jdbc.drivers” 系统属性中引用的驱动程序类。这允许用户定制由他们的应用程序使用的 JDBC Driver。应用程序不再需要使用 Class.forName() 显式地加载 JDBC 驱动程序。当前使用 Class.forName() 加载 JDBC 驱动程序的现有程序将在不作修改的情况下继续工作。在调用getConnection方法时,DriverManager会试着从初始化时加载的那些驱动程序以及使用与当前applet或应用程序相同的类加载器显式加载的那些驱动程序中查找合适的驱动程序。

java.sql.Connection(接口):与特定数据库的连接(会话)。在连接上下文中执行 SQL 语句并返回结果。(注:在配置 Connection 时,JDBC 应用程序应该使用适当的Connection方法,比如setAutoCommit或setTransactionIsolation。在有可用的JDBC方法时,应用程序不能直接调用 SQL 命令更改连接的配置。默认情况下,Connection对象处于自动提交模式下,这意味着它在执行每个语句后都会自动提交更改。如果禁用了自动提交模式,那么要提交更改就必须显式调用commit方法;否则无法保存数据库更改。)

java.sql.Statement(接口):用于执行静态SQL语句并返回它所生成结果的对象。

java.sql.ResultSet(接口):表示数据库结果集的数据表,通常通过执行查询数据库的语句生成。ResultSet对象具有指向其当前数据行的光标。最初,光标被置于第一行之前。next方法将光标移动到下一行;因为该方法在ResultSet对象没有下一行时返回false,所以可以在while循环中使用它来迭代结果集。默认的ResultSet对象不可更新,仅有一个向前移动的光标。因此,只能迭代它一次,并且只能按从第一行到最后一行的顺序进行。ResultSet 接口提供用于从当前行获取列值的获取方法(getBoolean、getLong 等)。可以使用列的索引编号或列的名称获取值。一般情况下,使用列索引较为高效。列从1开始编号。为了获得最大的可移植性,应该按从左到右的顺序读取每行中的结果集列,每列只能读取一次。对于获取方法,JDBC 驱动程序尝试将底层数据转换为在获取方法中指定的Java类型,并返回适当的Java值。JDBC规范有一个表,显示允许的从SQL类型到ResultSet获取方法所使用的Java类型的映射关系。

java.sql.Driver(接口):每个驱动程序类必须实现的接口。Java SQL框架允许多个数据库驱动程序。每个驱动程序都应该提供一个实现 Driver 接口的类。DriverManager会试着加载尽可能多的它可以找到的驱动程序,然后,对于任何给定连接请求,它会让每个驱动程序依次试着连接到目标URL。强烈建议每个Driver类应该是小型的并且是单独的,这样就可以在不必引入大量支持代码的情况下加载和查询Driver类。在加载某一Driver类时,它应该创建自己的实例并向DriverManager注册该实例。这意味着用户可以通过调用以下程序加载和注册一个驱动程序Class.forName(“foo.bah.Driver”)。

来看一段比较老式风格的JDBC代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package me.zeph.jdbc.example.dao;

import me.zeph.jdbc.example.model.Book;

import java.sql.*;

public class BookDaoWithDM {

  private final String url;
  private final String username;
  private final String password;
  private final String driverName;

  public BookDaoWithDM(String url, String username, String password, String driverName) {
      this.url = url;
      this.username = username;
      this.password = password;
      this.driverName = driverName;
  }

  public Book findBookByISBN(int isbn) {
      Book book = null;
      Statement statement = null;
      Connection connection = null;
      try {
          connection = getConnection();
          statement = connection.createStatement();
          book = getBook(statement.executeQuery(getQuerySqlFor(isbn)));
      } catch (ClassNotFoundException e) {
          e.printStackTrace();
      } catch (SQLException e) {
          e.printStackTrace();
      } finally {
          try {
              if (statement != null) {
                  statement.close();
              }
              if (connection != null) {
                  connection.close();
              }
          } catch (SQLException e) {
              e.printStackTrace();
          }
      }
      return book;
  }

  private Connection getConnection() throws ClassNotFoundException, SQLException {
      Class.forName(driverName);
      return DriverManager.getConnection(url, username, password);
  }

  private String getQuerySqlFor(int isbn) {
      return "select * from book where isbn = " + isbn + ";";
  }

  private Book getBook(ResultSet resultSet) throws SQLException {
      Book book = null;
      while (resultSet.next()) {
          book = new Book();
          book.setIsbn(resultSet.getInt(1));
          book.setName(resultSet.getString(2));
          book.setPrice(resultSet.getDouble(3));
          book.setAuthor(resultSet.getString(4));
      }
      return book;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package me.zeph.jdbc.example.dao;

import me.zeph.jdbc.example.model.Book;
import org.junit.Before;
import org.junit.Test;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertThat;

public class BookDaoWithDMTest {

  private final String url = "jdbc:mysql://localhost:3306/bookshelf";
  private final String username = "username";
  private final String password = "";
  private final String driverName = "com.mysql.jdbc.Driver";

  private BookDaoWithDM bookDaoWithDM;

  @Before
  public void setUp() throws Exception {
      bookDaoWithDM = new BookDaoWithDM(url, username, password, driverName);
  }

  @Test
  public void shouldFindBookFromDatabaseWhenISBNIs12() {
      //when
      Book book = bookDaoWithDM.findBookByISBN(12);

      //then
      assertThat(book.getName(), is("benwei"));
  }

  @Test
  public void shouldNotFindBookFromDatabaseWhenISBNIs1() {
      //when
      Book book = bookDaoWithDM.findBookByISBN(1);

      //then
      assertThat(book, is(nullValue()));
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package me.zeph.jdbc.example.model;

public class Book {
  private int isbn;
  private String name;
  private double price;
  private String author;

  public void setIsbn(int isbn) {
      this.isbn = isbn;
  }

  public int getIsbn() {
      return isbn;
  }

  public void setName(String name) {
      this.name = name;
  }

  public String getName() {
      return name;
  }

  public void setPrice(double price) {
      this.price = price;
  }

  public double getPrice() {
      return price;
  }

  public void setAuthor(String author) {
      this.author = author;
  }

  public String getAuthor() {
      return author;
  }
}

Class.forName()

上面这个例子是很普通的JDBC编程,这里有一点要讲的,那就是DriverManager会加载注册到它自己的Driver。但是,在上面的例子中,好像没有看到哪里有显示的注册Driver到DriverManager。为什么DriverManager能够找到MySQL的Driver呢?

回头看API文档中的一句话:

“这意味着用户可以通过调用以下程序加载和注册一个驱动程序Class.forName(“foo.bah.Driver”)。”。

我们知道Class.forName()方法是将一个指定名字的Class加载到JVM中。这和注册Driver有什么关系呢?看下MySQL实现的Driver源码你就明白了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.mysql.jdbc

import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
  static {
      try {
          java.sql.DriverManager.registerDriver(new Driver());
      } catch (SQLException E) {
          throw new RuntimeException("Can't register driver!");
      }
  }
  public Driver() throws SQLException {
      
  }
}

原来在这个Driver实现类中一段静态代码块,在类加载时,会被执行,此时就实现了将自己注册到DriverManager。

DataSource

OK,继续,前面的API文档中提到,目前一种更好的连接到数据源也就是获得Connection的方式是使用DataSource。我们来看下DataSource是什么?同样,先来读一下DataSource的API文档。

javax.sql.DataSource(接口):该工厂用于提供到此DataSource对象所表示的物理数据源的连接(A factory for connections to the physical data source that this DataSource object represents.)。作为 DriverManager工具的替代项,DataSource对象是获取连接的首选方法。实现DataSource接口的对象通常在基于JavaTM Naming and Directory Interface(JNDI) API的命名服务中注册。

DataSource接口由驱动程序供应商实现。共有三种类型的实现:

基本实现 - 生成标准的Connection对象
连接池实现 - 生成自动参与连接池的Connection对象。此实现与中间层连接池管理器一起使用。
分布式事务实现 - 生成一个Connection对象,该对象可用于分布式事务,大多数情况下总是参与连接池。此实现与中间层事务管理器一起使用,大多数情况下总是与连接池管理器一起使用。

DataSource对象的属性在必要时可以修改。例如,如果将数据源移动到另一个服务器,则可更改与服务器相关的属性。其优点在于,由于可以更改数据源的属性,所以任何访问该数据源的代码都无需更改。

通过DataSource对象访问的驱动程序本身不会向DriverManager注册。通过查找操作获取DataSource对象,然后使用该对象创建Connection对象。使用基本的实现,通过DataSource对象获取的连接与通过DriverManager设施获取的连接相同。

——————————————————————————————————————————————————————

上面提到了几点:

1.使用JNDI方式注册DataSource,然后在使用时根据名字将DataSource取出。
2.通过DataSource对象访问的驱动程序本身不会向DriverManager注册。

JNDI(Java Naming and Directory Interface,Java命名和目录接口)是一组在Java应用中访问命名和目录服务的API。命名服务将名称和对象联系起来,使得我们可以用名称访问对象。不过在本文中暂时不去涉及这个内容。

DataSource是一个接口,那么它的实现者有哪些呢?有一个比较常用的,Apache Commons的DBCP(Database Connection Pool),它是Apache Commons提供的一种数据库连接池组件,也是tomcat的数据库连接池组件。(回头看一下API文档上说的三种实现类型的第二种)。

将上面例子中使用DriverManager创建连接的方式,换成使用DataSource非常简单,替换上面的getConnection方法。

1
2
3
4
5
6
7
private Connection getConnection() throws SQLException {
      BasicDataSource dataSource = new BasicDataSource();
      dataSource.setUrl(url);
      dataSource.setUsername(username);
      dataSource.setPassword(password);
      return dataSource.getConnection();
}

从这段代码可以看出,映射了上面我说的的第二点,不需要向DriverManager注册,所以这里没有传入MySQL的ClassName。

OK,到此,我们回顾了JDBC最基础的几个接口:DriverManager,Driver,Connection,Statement,ResultSet,以及JDBC推荐的数据库连接方式DataSource。在下一章,我们会继续了解与JDBC相关的核心技术和概念,包括事务,JNDI等。

logger(SLF4j和log4j)

| Comments

SLF4j

Simple Logging Facade for Java (SLF4J)

SLF4J,简单日志门面(Facade),是各个不同日志框架的抽象,它不是具体的日志解决方案,只服务于各种各样的日志系统,例如java.util.logging, log4j等,它允许最终用户在部署其应用时使用其所希望的日志系统。

什么意思?SLF4J就像一个接口,是一个门面,在写代码时,我们只需要根据这个接口提供的方法去记录日志。这个接口可以和不同的日志框架绑定,比如Java自带的java.util.logging,或者大家常听到的log4j。这样,你可以在不修改代码的情况下,去替换不同的日志框架。

基本概念明白之后,看下面这个例子:

1
2
3
4
5
6
7
8
9
import org.slf4j.logger;
import org.slf4j.loggerFactory;

public class HelloWorld {
  public static void main(String[] args) {
    logger logger = loggerFactory.getlogger(HelloWorld.class);
    logger.info("Hello World");
  }
}
1
2
3
4
5
6
7
8
9
10
apply plugin: 'idea'
apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.slf4j:slf4j-api:1.7.7'
}

输出的结果是:

SLF4J: Failed to load class “org.slf4j.impl.StaticloggerBinder”.
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticloggerBinder for further details.

原因是你没有将SLF4j和某一个具体的日志框架绑定。怎么绑定呢?不需要你做任何事情,简单的加一个依赖就可以了。

1
2
3
4
5
6
7
8
9
10
11
apply plugin: 'idea'
apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.slf4j:slf4j-api:1.7.7'
    compile 'org.slf4j:slf4j-simple:1.7.7'
}

输出是:[main] INFO log.HelloWorld - Hello World

SLF4J支持对很多不同框架的绑定:

slf4j-log4j12-1.7.7.jar
slf4j-jdk14-1.7.7.jar
slf4j-nop-1.7.7.jar
slf4j-simple-1.7.7.jar
slf4j-jcl-1.7.7.jar

那么有个问题,SLF4J支不支持对多个日志框架的绑定呢?答案是不支持,也感觉一般这是不必要需求。

1
2
3
4
5
6
7
8
9
10
11
12
apply plugin: 'idea'
apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.slf4j:slf4j-api:1.7.7'
    compile 'org.slf4j:slf4j-simple:1.7.7'
    compile 'org.slf4j:slf4j-log4j12:1.7.7'
}

输出:

SLF4J: Class path contains multiple SLF4J bindings.

在SLF4J的官方文档上也说明了这点:

“SLF4J does not rely on any special class loader machinery. In fact, each SLF4J binding is hardwired at compile time to use one and only one specific logging framework. For example, the slf4j-log4j12-1.7.7.jar binding is bound at compile time to use log4j. In your code, in addition to slf4j-api-1.7.7.jar, you simply drop one and only one binding of your choice onto the appropriate class path location. Do not place more than one binding on your class path.”

OK,关于SLF4J的内容就到这里,该进入重头戏–log4J。

log4j

log4j相对于SLF4j就复杂许多了,毕竟SLF4j只是接口,log4j则是具体的实现了。

log4j由三个重要的组件组成:

  1. 类型和级别(logger)
  2. 输出目的地(Appender)
  3. 输出格式(Layout)

logger

logger hierarchy(logger的层次级别)

Named Hierarchy(以名字分层)

logger的名字是大小写敏感的,并且根据名字的不同,logger之间存在父子层次关系。举例来说,com.foo是com.foo.bar的父logger,com是com.foo.bar的祖先logger。

在这些logger之间,存在一个root logger,位于这个层次的顶层,它是永久存在,并且不能够通过名字获取,要获取它,只能通过logger.getRootlogger()方法获取。而这样一个层次关系,也决定了logger和logger之间存在着一些继承关系。

Level Inheritance(级别继承)

子类继承父类的级别:如果子类级别没有显示的指定,子类的级别等于第一个级别非空的父类logger的级别,直到追溯到根logger。

前面提到logger的级别,它们分别是TRACE, DEBUG, INFO, WARN, ERROR和FATAL。

这些级别存在高低关系:DEBUG < INFO < WARN < ERROR < FATAL。

如果你以前有用过一点点log4j,这些级别映射到你的记忆中,一定是反映成logger的各个方法。没错,在logger这个对象中,你会看到很多的打印方法,debug, info, warn, error, fatal和log。

回过头来想想,如果你给一个logger指定了级别,但是你打印日志时,又不是调用对应级别的方法,会发生什么事情呢?

答案是:对这些方法的调用,不光是打印日志信息,也是对logger对象级别改变的请求。

但是,并不是你调用它,级别就一定会改变,对级别改变的请求还是取决于logger对象本身的级别(无论是被显示的指定,还是继承)。如果请求的级别低于该logger当前的级别,那么改变就不会成功。

Appender

Appender的作用是指出日志信息的输出位置,比如控制台,文件。而且好处是,你可以给一个logger指定多个appender,比如同时在控制台和文件中输出日志。

Appender Additivity(附加性)

每一个Appender都有一个属性,叫做additivity flag,默认是true。什么意思呢?

子类会继承它直接父类的appender(包括该父类自己继承的appender),如果设置为false,则不继承其父类的appender。

这句话应该好理解,那下面这种情况呢?

Logger Appender Additivity
root A1 null
appenderFather A2 false
appenderFather.appenderChild A3 true

appenderFather的flag设置为了false,所以他拥有的appender的结果应该比较明显,就是A2。

appenderFather.appenderChild的结果应该是什么?答案是A2和A3。因为它只是继承它直接父类的appender。

log4j提供的Appender有哪些呢?

org.apache.log4j.ConsoleAppender(控制台)

org.apache.log4j.FileAppender(文件)

org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件)

org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生新文件,用的较多,限制文件大小)

org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)

Layout

日志信息布局(格式)是log4j提供的另外一个非常有用的特性。

log4j提供四种Layout方式:

org.apache.log4j.HTMLLayout(以HTML表格形式布局),

org.apache.log4j.PatternLayout(可以灵活地指定布局模式,比较常用),

org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串),

org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等等信息)

log4j采用类似C语言中的printf函数的打印格式格式化日志信息,打印参数如下:

%m 输出代码中指定的消息  

%p 输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL   

%r 输出自应用启动到输出该log信息耗费的毫秒数   

%c 输出所属的类目,通常就是所在类的全名   

%t 输出产生该日志事件的线程名   

%n 输出一个回车换行符,Windows平台为“rn”,Unix平台为“n”   

%d 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyy MMM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921   

%l 输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数。举例:Testlog4.main(TestLog4.java:10)

%x: 输出和当前线程相关联的NDC(嵌套诊断环境),尤其用到像java servlets这样的多客户多线程的应用中。

%%: 输出一个”%”字符 %F: 输出日志消息产生时所在的文件名称

%L: 输出代码中的行号

%m: 输出代码中指定的消息,产生的日志具体信息

%n: 输出一个回车换行符,Windows平台为”\r\n”,Unix平台为”\n”输出日志信息换行 可以在%与模式字符之间加上修饰符来控制其最小宽度、最大宽度、和文本的对齐方式。

Ok,基本知识就到这里,看一个例子。

Example

1
2
3
4
5
6
7
8
9
10
11
apply plugin: 'idea'
apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.slf4j:slf4j-api:1.7.7'
    compile 'org.slf4j:slf4j-log4j12:1.7.7'
}

在配置依赖时,将SLF4J和log4j绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package log;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HelloWorld {

  private final Logger logger = LoggerFactory.getLogger(HelloWorld.class);

  public void say() {
      logger.info("start time");
      System.out.println("Hello, world!!!");
      System.out.println(logger.getName());
      logger.info("end time");
  }

  public static void main(String args[]) {
      HelloWorld helloWorld = new HelloWorld();
      helloWorld.say();
  }
}

通过SLF4J的LoggerFactory.getLogger()方法得到一个logger的实力。logger的名字就是类名(全限定的类名)。

1
2
3
4
5
6
7
8
9
10
log4j.rootLogger=info, console, file

log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%p %l %d{yyy MMM dd HH:mm:ss,SSS} %m %n

log4j.appender.file=org.apache.log4j.DailyRollingFileAppender
log4j.appender.file.File=logger.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%p %l %d{yyy MMM dd HH:mm:ss,SSS} %m %n

在log4j的properties文件中,指定了根logger的级别,以及它的两个appender,分别是console和file。然后定义这两个appender,并指定它们的layout使用PatternLayout,让它们输出:级别,日志输出的位置,时间,指定的信息和换行。

输出结果如下:

INFO log.HelloWorld.say(HelloWorld.java:11) 2014 Jun 01 16:57:54,807 start time
Hello, world!!!
INFO log.HelloWorld.say(HelloWorld.java:13) 2014 Jun 01 16:57:54,809 end time
log.HelloWorld

总结,log不算是功能代码,但是他对于开发人员和维护人员,非常有用了,往往是解决问题的唯一线索,特别是对于后台系统,往往只能通过log来分析问题。

参考资料:

1.http://logging.apache.org/log4j/1.2/manual.html
2.http://www.slf4j.org/manual.html
3.http://www.cnblogs.com/dennisit/archive/2013/01/01/2841603.html