用 Apache Derby 构建脱机 Ajax

先决条件和系统要求

本文将使用 Apache Derby 作为客户端数据库。Derby 可以单独下载,但是也被绑定到 Java™ 6 中并被称为 Java DB。在本文中,我们将把 Derby V10.4.1.3 与 Java 5 和 Java 6 结合使用。我们将利用 Java Applet 在浏览器中启用 Derby 并且使用 JavaScript 访问 Applet。因此,强烈建议熟悉 Java Applet 和 JavaScript。Derby 允许使用普通 JDBC 和 SQL,因此需要熟悉这些内容(请参阅 参考资料)。


--------------------------------------------------------------------------------
回页首
Apache Derby

Apache Derby 是任何一个 Java 应用程序都可以使用的嵌入式数据库。它是非常有用的工具,因此绑定在 Java Platform, Standard Edition (Java SE) V6 中。虽然嵌入式数据库的应用不计其数,但是许多人都不知道用 Derby 可以实现的一些客户端功能。我们将通过构建一个简单的地址本应用程序研究其中一些应用。我们将从利用 Apache Derby 的 Java Applet 开始,最终实现一个使用 Derby 作为缓存的基于 Ajax 的应用程序。

数据访问

对于一篇有关数据库技术的文章,应当首先从数据库代码开始讨论。首先,让我们定义用于存储联系人的简单的表模式,如下所示:


图 1. Contact 表



您可以设想更复杂的联系人模式,如添加多个电话号码、地址等。但是,对于我们的应用程序来说,使用目前的这种模式刚刚好。当然,存在一个与 Contact 表相对应的 Java 类。在本例中,我们将遵循 Active Record 模式并用能够执行所有数据库操作的类封装数据库行。其代码如下所示:


清单 1. Contact 类

public class Contact {
   
    private Integer id;
    private String firstName;
    private String lastName;
    private String email;
   
    public static List<Contact> getContacts(String clause){
        if (clause == null)
            clause = "";
        String sql = SELECT_SQL + clause;
        Connection conn = DbManager.getConnection();
        List<Contact> contacts = new ArrayList<Contact>();
        try {
            ResultSet cursor = conn.createStatement().executeQuery(sql);
            while (cursor.next()){
                Contact c = new Contact();
                c.setId(cursor.getInt(1));
                c.setFirstName(cursor.getString(2));
                c.setLastName(cursor.getString(3));
                c.setEmail(cursor.getString(4));
                contacts.add(c);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return contacts;
    }
   
    public static List<Contact> getAllContacts(){
        return Contact.getContacts(null);
    }
   
    public static Contact getContact(String clause){
        List<Contact> results = Contact.getContacts(clause);
        if (results == null || results.size() != 1){
            return null;
        }
        else return results.get(0);   
    }
   
    public void save(){
        if (id == null)
            insert();
        else
            update();
    }
   
    public void delete(){
        Connection conn = DbManager.getConnection();
        String sql = "delete from Contact where id=?";
        try{
            PreparedStatement ps = conn.prepareStatement(sql);
            ps.setInt(1, id);
            ps.executeUpdate();
            System.out.println("Deleted contact id="+id);
        } catch (SQLException e){
            e.printStackTrace();
        }
    }
   
    private void insert() {
        Connection conn = DbManager.getConnection();
        try {
            PreparedStatement ps = conn.prepareStatement(INSERT_SQL,
                    PreparedStatement.RETURN_GENERATED_KEYS);
            ps.setString(1, firstName);
            ps.setString(2, lastName);
            ps.setString(3, email);
            ps.executeUpdate();
            ResultSet autoRs = ps.getGeneratedKeys();
            if (autoRs.next()){
                id = autoRs.getInt(1);
            }
            System.out.println("Contact saved new id = " + id);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
   
    private void update(){
        Connection conn = DbManager.getConnection();
        try{
            PreparedStatement ps = conn.prepareStatement(UPDATE_SQL);
            ps.setString(1, firstName);
            ps.setString(2, lastName);
            ps.setString(3, email);
            ps.setInt(4, id);
            ps.executeUpdate();
            System.out.println("Contact updated with id="+id);
        } catch (SQLException e){
            e.printStackTrace();
        }
    }



该类将完成很多任务,但是所有内容都非常简单。它有与数据库列对应的字段及用于每个字段的常用存取器(getter 和 setter)。它拥有执行所有创建/更新/删除(CReate Update Delete,CRUD)操作及其附带 SQL 查询的方法。例如,查询联系人的方法是静态方法,因此可以执行类似 Contact.getAllContacts() 的操作。保存和删除操作是实例方法,因此对个别联系人调用这些方法。此处没有显示查询,因为查询是标准的 SQL。该类还有常用的 getter 和 setter,但是为了简短起见并未显示(请参阅 下载 以获得完整的代码清单)。该类将用 Derby 构造基本的客户端存储。首先,我们将使用它作为 Applet UI 的一部分,但是稍后该 Applet 将把它用于 JavaScript。注意,对于每个方法,我们将调用实用程序类 DbManager 以获得连接。


清单 2. DbManager 类

package org.developerworks.addressbook;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DbManager {
    static{
        try {
            Class.forName("org.apache.derby.jdbc.EmbeddedDriver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
   
    public static Connection getConnection(){
        try {
            Connection conn = DriverManager.getConnection("jdbc:derby:contacts;
create=true");
            return conn;
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }
}



这是所有特定于 Derby 的代码所在的位置。实际上,并不是特别特定于 Derby。我们使用的是嵌入式数据库,但是我们处理它的方式和通过 JDBC 访问的其他数据库一样。我们将在类的静态初始化器中把 EmbeddedDriver 用于驱动程序类。另外对于 getConnection 方法,我们将添加额外的参数 create=true 来告诉 Derby 在数据库尚不存在时创建数据库。注意,不需要像使用 JDBC 那样用到用户名或密码 — 这是使用嵌入式数据库的优点之一。您看到了所有的数据访问代码。它看起来非常类似于在任何一个应用程序中都可以看到的数据库代码;而数据库刚好是嵌入式 Derby 数据库。您可以设想为应用程序创建其他模型,这样可以存储专用于应用程序的数据,但是位于客户端。让我们查看一个利用清单 2 中所示的数据访问代码的简单应用程序。

Applet UI

首先使用一个非常简单的 Applet,该 Applet 将使用数据访问代码。


清单 3. Applet UI 代码

public class AddressBookApplet extends JApplet {
    private static final long serialVersionUID = 1L;   
    private static final String[] columns = { "First Name", "Last Name", "Email", "Id"};
   
    public AddressBookApplet() {
        this.setLayout(new GridLayout(1,0));
        JPanel panel = buildUi();
        this.add(panel);
    }

    public Contact addContact(String firstName, String lastName, String email) {
        Contact c = new Contact();
        c.setFirstName(firstName);
        c.setLastName(lastName);
        c.setEmail(email);
        c.save();
        return c;
    }
   
    public void deleteContact(Integer id){
        Contact c= new Contact();
        c.setId(id);
        c.delete();
    }

    public Object[][] loadContacts() {
        List<Contact> book = Contact.getAllContacts();
        Object[][] contacts = new Object[book.size()][4];
        int cnt = 0;
        for (Contact contact : book){
            contacts[cnt++] = contact.toArray();
        }
        return contacts;
    }

    private JPanel buildUi() {
        JPanel panel = new JPanel();
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
       
        final DefaultTableModel dataModel = createDataModel();
        final JTable table = createTable(dataModel);
       
        //Lots of Swing/UI Code omitted for brevity   
      }
   
    private JTable createTable(final DefaultTableModel dataModel) {
        final JTable table = new JTable(dataModel);
        table.setPreferredScrollableViewportSize(new Dimension(500, 70));
        table.setFillsViewportHeight(true);
        return table;
    }

    private DefaultTableModel createDataModel() {
        Object[][] contacts = loadContacts();
        final DefaultTableModel dataModel = new DefaultTableModel(contacts, columns);
        return dataModel;
    }
}



这段代码大部分是构建 UI 的典型 Swing 代码。所有 UI 代码都是在位于类底部的私有方法中完成的。buildUi 方法处理 Swing 组件的创建,但是为了简短起见而省略了大部分内容。更有趣的是三个公共方法(除了构造函数之外):addContact、deleteContact 和 loadContacts。这三个方法实质上都是先前开发的数据访问代码的包装器。实际上,我们不需要将 Applet 用于最终应用程序的 UI,但是它提供了测试代码的简单方法。如果使用的是 Eclipse,则只需在 Applet 类上右键单击并选择 Run As > Java Applet。


图 2. 在 Eclipse 中作为 Java Applet 运行



Eclipse 在这里并不神秘,它只是使用 JDK 的 Applet Viewer。如果使用的不是 Eclipse,则可以在命令行中调用此程序。不管采用哪种方法,您都应当会得到类似图 3 的内容。


图 3. 使用 Applet Viewer 运行 Applet



添加和删除联系人以测试应用程序。这是开发和测试稍后使用 JavaScript 访问的客户端数据库代码的简单方法。Applet UI 是一个近乎完美的单元测试。我们几乎可以获取它并将其添加到 Web 页面中,但并不完全如此。首先,有一些安全注意事项需要处理。

安全性

我们将为 Applet 中使用的 JAR 设置签名。如果这里继续遵循计划,有一件事好到令人难以置信。Derby 将给我们提供嵌入到客户机中的持久数据库(所有内容都存储在客户机中)。它有几分像 HTTP Cookie,但是众所周知,那些 Cookie 在每个域中不可以超过 4 KB。客户机中的 Derby 数据库的限制是什么?答案是要么很多,要么很少。

默认情况下,Applet 无法访问本地文件系统,因此 Derby 无法在客户机中存储任何数据。那么使用 Derby 是做白日梦么?幸运的是,它不是。关键是您必须给 Applet 设置数字签名。有签名的 Applet 将获得本地文件系统的访问权,这样如果数据来自有签名的 Applet,则 Derby 可以持久存储这些数据。我们只需给 Applet 设置签名。


清单 4. 给 Applet 设置签名

$ keytool -genkey -alias sigs -keystore sigstore -keypass password -storepass password
What is your first and last name?
  [Unknown]:  Michael
What is the name of your organizational unit?
  [Unknown]:  developerWorks
What is the name of your organization?
  [Unknown]:  IBM
What is the name of your City or Locality?
  [Unknown]:  San Jose
What is the name of your State or Province?
  [Unknown]:  CA
What is the two-letter country code for this unit?
  [Unknown]:  US
Is CN=Michael, OU=developerWorks, O=IBM, L=San Jose, ST=CA, C=US correct?
  [no]:  yes

$ jarsigner -keystore sigstore -storepass password -keypass password -signedjar
addrbook.jar derby.jar sigs

Warning: The signer certificate will expire within six months.



正如您所见,我们使用两个 JDK 工具给 Applet(技术上是包含 Applet 的 JAR)设置签名。首先,使用 keytool 创建用于保存生成的加密密钥的密钥库。还可以使用它执行创建 SSL 证书之类的任务。在拥有密钥后,结合使用该密钥与 jarsigner 工具来给 JAR 设置签名。注意,我们包括了 Derby JAR,以及包含自定义代码的 addrbook JAR。最终获得了一个自签名 Applet 的示例。这对于开发来说没问题,但是通常不适用于任何面向用户的代码。在这种情况下,您将需要来自受信任提供商(如 VeriSign)的密钥/证书。关键原因是因为需要在客户机中存储数据,我们需要执行这些附加步骤才能符合 Java 语言的客户端安全模型。牢记这些安全事项并且拥有一颗可以正常工作的 Applet 后,我们现在已经准备好从 JavaScript 使用 Applet。


--------------------------------------------------------------------------------
回页首
带有 JavaScript 的 Applet

可以将 Applet 用于 Web 应用程序中的所有内容,但是在 Web 应用程序的 UI 中使用标准的 HTML 和 JavaScript 更加常见。我们仍然可以这样做并且使用 Applet 作为访问嵌入式 Derby 数据库的方法。只需在 JavaScript 与 Applet 之间完成一些集成。

集成 Applet

创建代码以实现 JavaScript 与 Java Applet 之间的通信听起来可能十分棘手,或者听上去像是某种新技术。实际上,这类集成从 Netscape Navigator 时代起就一直在进行,并且在那时使用的技术仍然起作用。首先,让我们查看仅用于将 Applet 嵌入到页面中的 HTML 代码。


清单 5. 嵌入 Applet 的代码

<applet alt="Address Book Applet" name="addrBookApplet"
    code="org.developerworks.addressbook.AddressBookApplet"
    width="400" height="200" archive="addrbook.jar, derby.jar">
</applet>



这是非常标准的 Applet 嵌入代码。它将应用不推荐使用的 <applet> 标记。它仍然受跨浏览器支持。它是可以与 Microsoft® 和 Mozilla 浏览器结合使用的惟一标记。因此如果不需要使用它,则需要使用 JavaScript 来判断浏览器(确定用户在使用哪种浏览器)。如果是 Internet Explorer,则将使用 <object> 标记。否则,将 <embed> 标记与嵌套的 <param> 标记结合用于 height、width 和 archive 属性之类的内容。此外,如果使用 <object> 标记,则还需要一个可编写脚本的参数以允许 JavaScript 调用 Applet 的 Java 方法。如果使用 <applet> 标记,则不需要这样做。

您应当知道的另一个属性是 MAYSCRIPT。此属性用于授予 Applet 权限以在页面中执行 JavaScript。此属性会非常有用,但是在本例中不需要它。我们将使用 JavaScript 访问 Applet(JavaScript 将调用 Applet 的 Java 方法)。但是,该 Applet 将不会调用 JavaScript,因此不需要 MAYSCRIPT 属性。那么如何从 JavaScript 中调用那些 Java 方法?


清单 6. 从 JavaScript 中调用 Java

function saveContact(firstName, lastName, email){
    var applet = document.addrBookApplet;
    var newContact = applet.addContact(firstName, lastName, email);
    addContactToUi(newContact);
}



在 saveContact 函数中执行的第一个操作是获得 Applet 中的句柄,方法是使用 Applet 的名称(清单 5 中的 name 属性)。我们将在其中直接调用 addContact 方法,然后它将在清单 6 的代码中返回一个新 Contact 对象。我们将把该对象传递给另一个 JavaScript 函数以用新联系人更新 UI。不需要更多内容。就是这样简单。可以将 Applet 仅用于持久性,并且可以将 JavaScript 用于所有其他内容。

使用没有头文件的 Applet

现在可以使 Applet 没有头文件(就是一个没有 UI 的代码库)。为此,只需删除清单 3 中的所有 UI 并且只在顶部保留那些公共方法。需要略微调整 UI 嵌入代码。


清单 7. 没有头文件的 UI HTML

<applet alt="Headless Applet" name="headlessApplet"
    code="org.developerworks.addressbook.HeadlesApplet"
    width="1" height="1" archive="addrbook.jar, derby.jar">
</applet>



此处惟一有趣的是把宽度和高度都设为 1。这实际上将使 Applet 在页面中不可见。因此,最终用户根本不知道页面中有一个 Applet。它将是一个不可见的 helper。当然,如果更改 Applet 的名称,则还需要调整 JavaScript,这是另外一处需要修改的内容。我们已经准备好使用此客户端持久化工具来进一步增强 Web 应用程序。


--------------------------------------------------------------------------------
回页首
创建 Ajax 缓存

Applet 能够执行用 Ajax 可以完成的许多相同操作,并且如我们所见,Applet 可以完成更多操作。在本例中,我们只对 “多出来” 的操作感兴趣。Applet 可以与服务器进行通信,但是我们将坚持用 Ajax 完成该操作。使用 Derby 作为嵌入式客户端数据库能够使我们完成单独用 Ajax 无法完成的操作。更为重要的是:可以将 Derby 用作存储来自服务器的数据的强大客户端缓存。

使用 Derby 作为缓存

下面是我们的想法:在服务器中保存联系人,而所有添加和删除之类的操作都通过 Ajax 调用完成。但是,将在客户机中的 Derby 中保存相同信息并将其用作缓存。因此,可以从 Derby 中载入所有联系人。


清单 8. 从 Derby 缓存中载入联系人

function init(){
    var book = document.headlessApplet.loadContacts();
    var i = 0;
    var contact = {};
    for (i=0;i<book.length;i++){
        contact = {firstName:book[i][0], lastName:book[i][1],
                    email:book[i][2], id:book[i][3]};
        addContactToUi(contact);
    }
}



该函数将从 Applet 中载入联系人,然后进行遍历以将每个联系人添加到 UI 中。这是在页面首次装入时调用的代码。通常,我们将从服务器中获得联系人,并且等待服务器响应会导致产生延迟。使用 Derby 缓存就不会产生延迟。但是,需要确保服务器与 Derby 缓存中的联系人之间保持同步。

保持同步

必须确保缓存是准确的,因此需要确保它与服务器同步。完成此操作的最简单方法是向服务器发送异步更新并且在处理期间,更新缓存和应用程序的 UI。


清单 9. 添加联系人

function addContact(){
    var contact = {};
    var firstName = document.getElementById("firstName").value;
    var lastName = document.getElementById("lastName").value;
    var email = document.getElementById("email").value;
    var req = getXhr(); // get the browser specific XMLHttpRequest object
    var params = "firstName="+username + ",lastName="+lastName+",email="+email;
    req.open("POST", "/contact/create", true);
    req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    req.setRequestHeader("Content-length", params.length);
    req.setRequestHeader("Connection", "close");
    req.onreadystatechange = handleResponse;   
    req.send(params);   
    // update cache
    saveContact(firstName, lastName, email);
}



这段代码大部分是使用 XMLHttpRequest 对象的典型 Ajax 代码。只需照常发送 XMLHttpRequest。这将是异步的,因此 req.send() 调用将立即返回。然后可以调用清单 6 中的 saveContact() 函数。注意,我们注册了一个处理程序(通过设置 req.onreadystatechange 属性)。使用这种方法,代码需要处理调用服务器失败的情况。这可能会在服务器临时关闭或者在用户遇到网络问题时发生。这里可以变得复杂一些并将更新放入队列,这样可以在稍后服务器或网络恢复时重新执行更新。此外,我们可以把 saveContact 调用移到处理程序中。该 UI 的响应能力并不十分强大,但是用这种方法,我们只需在成功更新服务器时更新 UI 和缓存。


--------------------------------------------------------------------------------
回页首
结束语

我们已经看到使用 Derby 作为嵌入式数据库有多么简单,也看到了如何把这个数据库和将数据持久化到客户机的 Java Applet 结合使用。这里有一些必须考虑的安全因素 — 只有经过签名的 Applet 才可以将数据写入客户机的文件系统 — 但是我们查看了给 Applet 设置签名的过程和工具。牢记这些内容后,我们可以轻松地从 JavaScript 中访问 Applet 并把 Derby 转换为支持 Ajax 的 Web 应用程序的缓存。即使服务器临时不可用,也可以访问数据。因而,Derby 可以成为任何 “脱机” 策略的关键部分,从而在服务器不可用时访问数据。

猜你喜欢

转载自zhangqi007.iteye.com/blog/1826247