NO END FOR LEARNING

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

再次了解JDBC(中)- 引入JNDI

| Comments

什么是命名服务?什么是目录服务?什么是命名服务的上下文?

命名服务NS(Naming Service)是命名系统提供的服务功能,通过名字访问命名系统中的对象。

目录服务DS(Directory Service)是命名服务的扩展,它不仅把名字与对象对应在一起,并且把名字与对象属性(Attribute) 联系在一起,因此不仅可以通过名字还可以通过属性来搜索对象。

对象名与对应的对象构成的集合叫对象上下文(Context)。 例如,在文件命名系统中,一个目录就是一个Context,其内容是文件名(名)和对应的文件的集合。

那什么是JNDI?

Java Naming and Dirctory Interface - Java命名和目录接口,它是sun公司提出的方便应用程序访问命名和目录服务的的API。

和其他设计一样,JNDI是接口API,它独立于任何命名或者目录服务的具体实现。这样,你就可以用相同的API去访问多种不同类型的命名和目录服务。

根据它们作用的不同,典型应用场景也就分为两个部分:

(1)将Java应用程序连接到外部的目录服务。

(2)允许Java的Servlet在web容器中寻找定义配置信息。

JNDI的架构是有一套API和一套SPI(Service Provider Interface)接口组成。Java应用程序使用JNDI API来访问不同的命名和目录服务。SPI则让不同的命名和目录服务可以透明和无缝插入,这样Java应用程序才能使用JNDI API来访问它们的服务。

JNDI是包含在Java SE平台中。要使用JNDI,你必须有JNDI的类库和一个到多个服务提供商。JDK本身包含一些服务提供商:

Lightweight Directory Access Protocol (LDAP)
Common Object Request Broker Architecture (CORBA) Common Object Services (COS) name service
Java Remote Method Invocation (RMI) Registry
Domain Name Service (DNS)

JNDI API是访问任何命名或者目录服务的通用API。实际访问一个命名或目录服务需要在JNDI下插入一个服务提供商。

服务提供商是一个映射到JNDI API能实际调用命名或目录服务器的软件。一般情况,服务提供商的角色和命名或目录服务器的角色是不一样的。从C/S软件角度说,JNDI和服务器提供商是JNDI的客户端,命名或者目录服务器是服务端。

客户端和服务器端交互的方式有很多。一种比较常用的方式是,使用网络协议。而服务器通常支持不同的客户端,不仅仅是JNDI的客户端,只是它们要遵循不同协议。JNDI也不规定JNDI客户端和服务端交互的方式。

JDNI的Context(上下文)

上下文这个概念在Java的开发中经常出现,比如,ServletContext,Spring的Context,Android中也有Context,JNDI也不例外。

JNDI的上下文,依赖于一个重要的接口Context和一个重要的类InitialContext

Context接口 它表示一个命名上下文,由一组名称到对象的绑定组成。它提供了查找,绑定,重命名,创建和销毁子上下文的接口。

InitialContext类 所有命名操作都相对于某一上下文,它是JNDI提供的,执行命名和目录操作的初始上下文,是根上下文,为命名和目录服务提供起点。一旦你有了初始化上下文,你就可以去查找其他的上下文和对象。

JNDI的环境变量

JNDI需要定义许多环境变量来说明访问什么样的命名和目录服务。

而为了简化设置JNDI应用需要的环境变量,应用程序组件和服务提供商会和资源文件一起发布。JNDI的资源文件就是常用的properties文件格式,包含的是键值对。

JNDI的资源文件有两种类型:provider和application

每个服务商都有一个可选的资源文件:[prefix/]jndiprovider.properties,这个prefix前缀是context实现类的包名。例如:

com.sun.jndi.ldap.LdapCtx对应的资源文件是com/sun/jndi/ldap/jndiprovider.properties

JNDI会使用ClassLoader.getResources()方法在应用程序的所有资源文件中查找一个叫jndi.properties的文件。该文件中定义的所有属性都会放到InitialContext里面,而其他的Context会继承自该InitialContext。

当InitialContext被构建时,它的环境会被初始化,要么通过传递进来的HashMap参数,要么通过定义的Java应用properties文件。而IntialContext的具体实现是在运行时决定的,默认的策略是使用环境变量“java.naming.factory.initial”定义的InitContextFactory(工厂类)。

回到我们的例子当中:

那么在上一篇中,DataSource API文档里提到的“实现DataSource接口的对象通常在基于JavaTM Naming and Directory Interface(JNDI) API的命名服务中注册。”就是JNDI的第二种应用方式。

我们使用的Apache Common的DBCP作为DataSource的实现,而DBCP也是Tomcat的数据库连接池组建,所以针对它,我么可以使用Tomcat实现的JNDI服务。

首先需要把上一篇中的例子改为一个Java Web应用。

定义Context.xml,位置在webapp/META-INF/context.xml

1
2
3
4
5
6
7
<?xml version='1.0' encoding='utf-8'?>
<Context>
    <Resource name="jdbc/mysql/bookshelf" auth="Container" type="javax.sql.DataSource"
              maxActive="100" maxIdle="30" maxWait="10000"
              username="root" password="" driverClassName="com.mysql.jdbc.Driver"
              url="jdbc:mysql://localhost:3306/bookshelf"/>
</Context>

在JDNI中,对象是一种资源,Tomcat指定资源的入口出在元素中,有两个位置可以定义,一个是在$CATALINA_BASE/conf/server.xml,一个是在每个web应用需的META-INF/context.xml中。前一种方式,Tomcat容器中所有的Web应用都可以使用,算是一种全局的资源。不过一般第二种方式会更好。

我在使用第一种方式的时候,遇到了MySQL的Driver文件找不到的问题,应该是需要将MySQL的驱动拷贝的Tomcat的lib下。

定义web.xml文件,位置在webapp/WEB-INF/web.xml

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
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4"
         xmlns="http://java.sun.com/xml/ns/j2ee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
        http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

    <servlet>
        <servlet-name>helloDataSource</servlet-name>
        <servlet-class>me.zeph.jdbc.example.servlet.HelloDataSourceServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>helloDataSource</servlet-name>
        <url-pattern>/helloDataSource</url-pattern>
    </servlet-mapping>

    <resource-ref>
        <description>DB Connection</description>
        <res-ref-name>jdbc/mysql/bookshelf</res-ref-name>
        <res-type>javax.sql.DataSource</res-type>
        <res-auth>Application</res-auth>
    </resource-ref>

</web-app>

光定义资源还不行,web应用必须要有个办法知道资源有哪些?所以需要在web.xml定义资源的引用。

此时,获取Connection的方式就可以换成JNDI的方式了。

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 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 BookDaoWithDS {

  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 (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 SQLException {
      DataSource dataSource = null;
      try {
          InitialContext initialContext = new InitialContext();
          Context envContext = (Context) initialContext.lookup("java:/comp/env");
          dataSource = (DataSource) envContext.lookup("jdbc/mysql/bookshelf");
      } catch (NamingException e) {
          e.printStackTrace();
      }
      return dataSource.getConnection();
  }

  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;
  }
}

这里你可以将java.naming.factory.initial和java.naming.factory.url.pkgs打印出来,结果是:org.apache.naming.java.javaURLContextFactory和org.apache.naming。

java.naming.factory.url.pkgs的作用是告诉JNDI去哪个包下面,找满足java.javaURLContextFactory的类。

你应该还看到一点有点疑惑,我的资源名字命名就是:jdbc/mysql/bookshelf,为什么前面还有java:/comp/env。

java:/comp/env是环境命名上下文,是针对Java EE组件中使用JNDI引入的,目的是为了防止冲突。Java EE环境下,被访问的系统或者用户定义的对象都是存储在java:comp/env的环境命名上下文中。

到此,我们实现了JNDI的引入,可以通过JNDI来配置DataSource,此时,如果我们希望从MySQL迁移到Oracle就不需要修改Java代码,只需要更改一下配置文件即可,这也就是JNDI的好处。

再下一篇,我们继续讨论,JDBC的事务。

参考资料:

http://docs.oracle.com/javase/tutorial/jndi/overview/index.html

http://tomcat.apache.org/tomcat-7.0-doc/jndi-resources-howto.html

http://docs.oracle.com/javase/jndi/tutorial/

http://docs.oracle.com/javase/8/docs/api/javax/naming/Context.html

再次了解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等。