Java9 秘籍(五)
十一、调试和单元测试
调试是软件开发的一大部分。为了有效地进行调试,您必须能够像计算机一样“思考”并深入代码,解构导致您正在努力解决的逻辑错误的每一步。在计算机编程的初期,没有很多工具可以帮助调试。大多数情况下,调试包括查看您的代码并找出不一致的地方;然后重新提交代码再次编译。今天,每个 IDE 都提供了使用断点和检查内存变量的能力,这使得调试变得更加容易。在 ide 之外,还有其他工具可以帮助您进行项目的日常调试、构建和测试;这些工具确保您的代码不断地被测试,以发现编程时可能引入的错误。在这一章中,您将探索有助于调试、分析和测试 Java 软件的不同工具。
本章涵盖了一些调试和单元测试的基础知识。您将学习如何使用 Apache Ant 和 JUnit 从命令行或终端执行单元测试。您还将了解如何利用 NetBeans Profiler 以及其他工具来分析和监视您的应用。
11-1.了解异常
问题
您捕获并记录了一个异常,您需要确定其原因。
解决办法
分析异常的 printStackTrace()方法的输出:
public class Recipe11_1 {
public static void main (String[] args) {
Recipe11_1 recipe = new Recipe11_1();
recipe.startProcess();
}
private void startProcess() {
try {
int a = 5/0;
} catch (Exception e) {
e.printStackTrace();
}
}
}
结果:
java.lang.ArithmeticException: / by zero
at org.java8recipes.chapter11.recipe11_01.Recipe11_1.start(Recipe11_1.java:18)
at org.java8recipes.chapter11.recipe11_01.Recipe11_1.main(Recipe11_1.java:13)
它是如何工作的
在编程行话中,栈指的是被调用以到达程序中某一点的函数列表,通常从即时函数(System.out.println())开始到更一般的函数(public static void main)。每个程序都跟踪哪个代码被执行,以便到达代码的特定部分。栈跟踪的输出是指发生错误时内存中的栈。Java 中抛出的异常跟踪它们发生的位置,以及抛出异常时执行的代码路径。栈跟踪显示从发生异常的最具体的地方(发生异常的那一行)到出错代码的顶级调用程序(以及中间的所有内容)。然后,这些信息允许您查明执行了哪些方法调用,并可能有助于了解引发异常的原因。
在本例中,被零除异常发生在 Recipe11_1.java 的第 18 行,是由 main()方法的调用引起的(在第 13 行)。有时,当查看栈跟踪的输出时,您会看到不属于项目的方法。这是自然发生的,因为有时方法调用是在工作系统的其他部分生成的。例如,当出现异常时,在 Swing 应用中看到抽象窗口工具包(AWT)方法是很常见的(由于 EventQueue 的性质)。如果您查看更具体的函数调用(最早),您最终将使用项目自己的代码运行,然后可以尝试确定引发异常的原因。
注意
如果用“调试”信息编译程序,栈跟踪输出将包含行号信息。默认情况下,大多数 ide 在调试配置中运行时都会包含这些信息。通常,IDE 还会生成一个直接链接,将您带到有问题的代码行,从而使错误的行号变得易于访问。如果使用命令行,请使用–g 选项来编译和生成调试信息。
11-2.锁定类的行为
问题
您需要锁定您的类的行为,并希望创建将用于验证您的应用中的特定行为的单元测试。
解决办法
使用 JUnit 创建单元测试来验证类中的行为。要使用这个解决方案,您需要在您的类路径中包含 JUnit 依赖项。JUnit 可以从www.junit.org下载,或者你可以简单地将 Maven 依赖项添加到你的项目中。如果您选择下载它,那么您将需要加载 junit.jar 和 hamcrest.jar。在撰写本文时,Maven 依赖关系如下,请相应地更改版本:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
当 JUnit 成为项目的一部分时,您将能够包含 org.junit 和 junit.framework 名称空间。在这个例子中,为 MathAdder 类创建了两个单元测试。MathAdder 类包含两个方法:addNumber (int,int)和 subtract number(int,int)。这两个方法返回它们传递的参数的加法(或减法)(一个简单的类)。单元测试(由@Test 注释标记)验证 MathAdder 类实际上是将两个数相加和/或相减。
package org.java8recipes.chapter11;
import junit.framework.Assert;
import org.junit.Test;
public class Recipe11_2_MathAdderTest {
@Test
public void testAddBehavior() {
Recipe_11_2_MathAdder adder = new Recipe_11_2_MathAdder();
for (int i =0;i < 100;i++) {
for (int j =0;j < 100;j++) {
Assert.assertEquals(i+j,adder.addNumbers(i,j));
}
}
}
@Test
public void testSubstractBehavior() {
Recipe_11_2_MathAdder adder = new Recipe_11_2_MathAdder();
for (int i =0;i < 100;i++) {
for (int j =0;j < 100;j++) {
Assert.assertEquals(i-j,adder.substractNumber(i,j));
}
}
}
}
要执行此测试,请使用您的 IDE 来运行测试类。例如,在 NetBeans 中,您必须通过右键单击 test 类并将其移动到 NetBeans 项目中的“Test Packages”模块来重构它。一旦您将测试类移动到“Test Packages”中所需的包中,右键单击并运行文件来执行测试。
注意
在撰写本文时,JUnit 5 库正在积极开发中。它是 JUnit 的下一代,包括许多新的功能,利用了较新的 JVM 语言结构,比如 lambdas。这个方法主要关注 JUnit 4,因为它是一个成熟的测试套件。有关 JUnit 5 的更多信息,请参考以下网站:junit.org/junit5/
它是如何工作的
单元测试对于测试你的代码以确保预期的行为发生在你的类中是有用的。在项目中包含单元测试可以减少添加或重构代码时破坏功能的可能性。当您创建单元测试时,您正在指定一个对象应该如何行为(这被称为它的契约)。单元测试确保预期的行为发生(他们通过验证方法的结果和使用不同的 JUnit 来做到这一点。断言方法)。
编写单元测试的第一步是创建一个新的类,描述您想要验证的行为。一个通用的单元测试命名约定是创建一个与被测试类同名的类,后缀为 Test;在这个菜谱的例子中,主类称为 Recipe11_2_MathAdder,而测试类称为 Recipe11_2_MathAdderTest。
单元测试类(MathAdderTest)将包含检查和验证类行为的方法。为此,对方法名进行了注释。注释是元数据的形式,开发者可以“注释”代码的指定部分,从而将信息添加到注释的代码中。程序不使用这些额外的信息,而是由编译器/构建器(或外部工具)来指导代码的编译、构建和/或测试。出于单元测试的目的,您可以通过在每个方法名之前指定 @Test 来注释作为单元测试一部分的方法。在每个方法中,使用 Assert.assertEquals(或任何其他 Assert 静态方法)来验证行为。
Assert.assertEquals 方法指示单元测试框架验证您正在测试的类的方法调用的预期值与其方法调用返回的实际值是否相同。在配方示例中,Assert.assertEquals 验证 MathAdder 是否正确地将两个整数相加。虽然这个类的范围很小,但是它显示了进行全功能单元测试的最低要求。
如果断言调用成功,它在单元测试框架中被报告为“通过”测试;如果 Assert 调用失败,那么单元测试框架将停止并显示一条消息,显示单元测试失败的地方。大多数现代的 ide 都有运行单元测试类的能力,只需右击名字并选择 Run/Debug(这是运行 Chapter_11_2_MathAdderTest 方法的预期方式)。
诚然,ide 可以在开发的同时运行单元测试,但它们是为了自动运行而创建的(通常由预定的构建或版本控制系统的签入来触发),这就是方法 11-3 所说的。
11-3.编写单元测试脚本
问题
您希望自动运行单元测试,而不是手动调用它们。
解决办法
使用和配置 JUnit 和 Ant。为此,请按照下列步骤操作:
-
下载 Apache Ant(位于
ant.apache.org/)。 -
将 Apache Ant 解压缩到一个文件夹中(例如,对于 Windows 系统为 c:\ant,对于 OS X 系统为/Development)。
-
确保 Apache Ant 可以从命令行或终端执行。在 Windows 中,这意味着将 apache-ant/bin 文件夹添加到路径,如下所示:
-
转到控制面板➤系统。
-
单击高级系统设置。
-
单击环境变量。
-
在系统变量列表中,双击变量名路径。
-
在字符串的末尾,添加;C:\apache-ant-1.8.2\bin(或您解压缩 Apache Ant 的文件夹)。
-
单击 OK(在之前打开的每个弹出框上)接受更改。
注意 Apache Ant 预装在 OS X 上,因此您不必安装或配置它。要验证这一点,请打开终端窗口并键入 ant–version,以查看系统上安装的是哪个版本。
确保定义了 JAVA_HOME 环境变量。在 Windows 中,这意味着添加一个名为 JAVA_HOME 的新环境变量。例如:
转到控制面板➤系统。
-
-
单击高级系统设置。
-
单击环境变量。在系统变量列表中,检查是否有名为 JAVA_HOME 的变量,以及该值是否指向您的 JDK 发行版。如果 JAVA_HOME 不存在,请单击新建。将变量名设置为 JAVA_HOME,将变量值设置为 C:\Program Files\Java\jdk1.9.0 或 JDK 9 安装的根目录。
在 OS X 上,环境变量是在。bash 概要文件,驻留在用户主目录中。要添加 JAVA_HOME,请在。bash_profile:
export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.9.0.jdk/Contents/Home测试您是否可以联系到 Ant,并且 Ant 可以找到您的 JDK 安装。要测试更改是否生效,请执行以下操作:
-
打开命令窗口或终端。
-
类型 ant 。
如果您收到消息“Ant 未被识别为内部或外部命令”,请重复设置 PATH 变量的第一步(第一组指令)。如果您收到消息“无法定位 tools.jar”,您需要为您的安装创建和/或更新 JAVA_HOME 路径(第二组指令)。
消息“Buildfile: build.xml 不存在!”意味着您的设置已经可以使用 Ant 构建了。恭喜你!
注意
在 Microsoft Windows 或 OS X 中更改环境变量时,必须关闭以前的命令行或终端窗口,然后重新打开它们,因为更改只应用于新的命令窗口。
在项目的根目录下创建 build.xml,并将下面的基本 Ant 脚本作为 build.xml 文件的内容。这个特定的 build.xml 文件包含 Ant 将用来编译和测试这个菜谱的信息。
<project default="test" name="Chapter11Project" basedir=".">
<property name="src" location="src"/>
<property name="build" location="build/"/>
<property name="src.tests" location="src/"/>
<property name="reports.tests" location="report/" />
<path id="build.path">
<fileset dir="dep">
<include name="**/*.jar" />
</fileset>
<pathelement path="build" />
</path>
<target name="build">
<mkdir dir="${build}" />
<javac srcdir="${src}" destdir="${build}">
<classpath refid="build.path" />
</javac>
</target>
<target name="test" depends="build">
<mkdir dir="${reports.tests}" />
<junit fork="yes" printsummary="yes" haltonfailure="yes">
<classpath refid="build.path" />
<formatter type="plain"/>
<batchtest fork="yes" todir="${reports.tests}">
<fileset dir="${src.tests}">
<include name="**/*Test*.java"/>
</fileset>
</batchtest>
</junit>
</target>
</project>
注意
要执行此配方,请打开命令行窗口或终端,导航到 Chapter 11 文件夹,键入 ant,然后按 Enter 键。
它是如何工作的
Apache Ant(或简称 Ant)是一个允许您编写项目构建和单元测试脚本的程序。通过配置 Ant,您可以使用命令行构建、测试和部署您的应用。(反过来,它可以被安排由操作系统自动运行。)Ant 可以自动运行单元测试并报告这些测试的结果。这些结果可以在每次运行后进行分析,以查明行为的变化。
由于 Ant 的复杂性,它有一个很大的学习曲线,但它允许在编译、构建和编织代码方面有很大的灵活性。通过使用 Ant,有可能在如何构建项目上实现最大限度的配置。
注意
访问ant.apache.org/manual/index.html获得更深入的 Ant 教程。
build.xml 文件包含有关如何编译项目、使用哪个类路径以及运行哪些单元测试的说明。每个 build.xml 包含一个标签,它封装了构建项目的步骤。在每个中有目标,它们是构建过程中的“步骤”。一个可以依赖于其他目标,允许您在项目中建立依赖关系(在这个菜谱的例子中,目标“test”依赖于目标“build”,这意味着要运行测试目标,Ant 将首先运行构建目标)。
每个目标都包含任务。这些任务是可扩展的,并且有一组核心任务可以开箱即用。任务将编译 src 属性中指定的一组 Java 文件,并将输出写入 dest 属性。作为任务的一部分,您可以指定使用哪个类路径。在本例中,类路径是通过引用先前定义的路径(称为 build.path)来指定的。在这个配方中,类路径被定义为任何具有。jar 扩展位于 dep 文件夹中。
构建目标中的另一个任务是。此任务将找到在其任务中指定的单元测试并运行它。单元测试在属性中定义。通过使用属性,可以告诉 JUnit 查找名称中包含单词 Test 并以。java 扩展。一旦 JUnit 运行了每个测试,它将向控制台写出一个摘要,并向 reports.tests 文件夹写入一个关于单元测试结果的报告。
注意
您可以使用标签在 build.xml 文件中定义变量。定义属性后,可以使用${propertyName}语法将其作为另一个任务的一部分进行访问。这允许您快速更改构建脚本以响应结构变化(例如,切换目标/源文件夹)。
11-4.尽早发现漏洞
问题
您希望确保能够在设计时找到最大数量的 bug。
解决办法
使用 FindBugs 来扫描您的软件的问题。使用包含 FindBugs 的 Ant 构建文件进行报告。
以下是添加 FindBugs 报告的新 build.xml 文件:
<project default="test" name="Chapter11Project" basedir=".">
<property name="src" location="src"/>
<property name="build" location="build/"/>
<property name="reports.tests" location="report/" />
<property name="classpath" location="dep/" />
<!-- Findbugs Static Analyzer Info -->
<property name="findbugs.dir" value="dep/findbugs" />
<property name="findbugs.report" value="findbugs" />
<path id="findbugs.lib" >
<fileset dir="${findbugs.dir}" includes="*.jar"/>
</path>
<taskdef name="findbugs" classpathref="findbugs.lib" classname="edu.umd.cs.findbugs.anttask.FindBugsTask"/>
<path id="build.path">
<fileset dir="dep">
<include name="**/*.jar" />
</fileset>
</path>
<target name="clean">
<delete dir="${build}" />
<delete dir="${reports.tests}" />
<delete dir="${coverage.dir}" />
<delete dir="${instrumented}" />
<mkdir dir="${build}" />
<mkdir dir="${reports.tests}" />
<mkdir dir="${coverage.dir}" />
</target>
<target name="build">
<javac srcdir="${src}" destdir="${build}" debug="${debug}">
<classpath refid="build.path" />
</javac>
</target>
<target name="test" depends="clean,build">
<junit fork="yes" printsummary="yes" haltonfailure="yes">
<classpath refid="build.path" />
<formatter type="plain"/>
<batchtest fork="yes" todir="${reports.tests}">
<fileset dir="${build}">
<include name="**/*Test*.class"/>
</fileset>
</batchtest>
<jvmarg value="-XX:-UseSplitVerifier" />
</junit>
</target>
<target name="findbugs" depends="clean">
<antcall target="build">
<param name="debug" value="true" />
</antcall>
<mkdir dir="${findbugs.report}" />
<findbugs home="${findbugs.dir}"
output="html"
outputFile="${findbugs.report}/index.html"
reportLevel="low"
>
<class location="${build}/" />
<auxClasspath refid="build.path" />
<sourcePath path="${src}" />
</findbugs>
</target>
</project>
要运行这个食谱,下载 FindBugs(findbugs.sourceforge.net/downloads.html)。解压缩到您计算机上的一个文件夹中,然后复制。/lib/ folder 到项目的/dep/findbugs 文件夹中(如果需要,创建/dep/findbugs 文件夹)。确保/dep/findbugs/findbugs.jar 和/dep/findbugs/findbugs-ant.jar 存在。
它是如何工作的
FindBugs 是一个静态代码分析器(SCA) 。它将分析你的程序的编译文件,并找出编码中常见的错误(不是语法错误,而是某些类型的逻辑错误)。例如,FindBugs 将发现的一个错误是使用==而不是 String.equals()比较两个字符串。然后将分析写成 HTML(或文本),可以用浏览器查看。从 FindBugs 中捕捉错误很容易,将它作为持续集成过程的一部分是非常有益的。
在 build.xml 的开头,您定义了 FindBugs 任务。本节指定。jar 文件定义了新任务(dep\findbugs ),并决定了完成后将报告放在哪里。
build.xml 还有一个名为“findbugs”的新目标项目。findbugs 目标编译包含调试信息的源文件(包含调试信息有助于 FindBugs 报告,因为它会在报告错误时包含行号),然后继续分析错误的字节码。在 findbugs 任务中,指定编译的。类文件(这是属性)、项目依赖项的位置(属性),以及源代码的位置(属性)。
在 findbugs 目标中,有一个任务。任务只是运行在任务中指定的目标。就在任务之前,您将调试赋值为 true。这又以 debug="${debug} "的形式传递给任务。当 debug 设置为 true 时,任务将把调试信息包含到 Java 源文件的编译中。在编译后的文件中包含调试信息将有助于生成可读性更好的 FindBugs 报告,因为它将包含问题所在的行号。在整个 build.xml 文件中使用从 Ant 目标中分配属性的技巧,以便在遍历特定的构建目标时有选择地启用某些行为。如果您要构建常规的构建目标,构建的结果将不包含调试信息。相反,如果您要构建 findbugs 目标,因为 findbugs 目标将 debug 替换为 true,那么构建的结果将包含调试信息。
小费
要调用 ant 来运行默认的“目标”(如 build.xml 中所指定的),只需键入 Ant。指定另一个。xml 文件(而不是 build.xml),键入 ant–f name of other file . XML。要更改要运行的默认目标,请在末尾键入目标的名称(例如, ant clean )。要运行此示例,请键入 ant–f FindBugs build . XML FindBugs。这将要求 Ant 使用 findbugsbuild.xml 文件并运行 findbugs 目标。
11-5.监控应用中的垃圾收集
问题
您注意到您的应用似乎变慢了,并怀疑正在进行垃圾收集。
解决方案 1
启动 Java 程序时,将-Xloggc:GC . log-XX:+PrintGCDetails-XX:+printgcstimestamps 作为参数。这些参数允许您将垃圾收集信息记录到 gc.log 文件中,包括垃圾收集发生的时间以及详细信息(是次要的还是主要的垃圾收集,以及花费了多长时间)。
Ant target that executes Recipe 11_5 with garbage logging on.
<target name="Recipe11_5" depends="build">
<java classname="org.java9recipes.chapter11.Recipe11_5" fork="true">
<classpath refid="build.path" />
<jvmarg value="-Xloggc:gc.log" />
<jvmarg value="-XX:+PrintGCDetails" />
<jvmarg value="-XX:+PrintGCTimeStamps" />
</java>
</target>
在这个 build.xml 文件中,Java 任务用于在启动应用之前向编译器添加垃圾收集日志记录的参数。要在整个 Ant 中运行这个示例,请键入 ant Recipe11_5 。
解决方案 2
使用 NetBeans“Profiler”工具分析程序的内存消耗等。要运行 profiler,请选择要对其执行性能分析的文件或项目,然后从 NetBeans“性能分析”菜单中选择“性能分析项目”或“性能分析文件”命令。您也可以用鼠标右键单击项目或文件,以访问上下文菜单配置文件选项。
Profiler 对话框(图 11-1 )将会打开,允许您选择和配置选项。在这个解决方案中,只需选择 Run 按钮,用默认设置执行概要分析。
图 11-1。NetBeans 探查器
一旦 profiler 开始运行,它将一直运行,直到您使用“控制”面板上的“停止”按钮将其停止。生成的输出应该如图 11-2 所示。
图 11-2。NetBeans 探查器结果
它是如何工作的
在解决方案 1 中为日志垃圾收集添加标志将导致您的 Java 应用将次要和主要垃圾收集信息写入一个日志文件。这允许您及时“重建”应用发生的情况,并发现可能的内存泄漏(或者至少是其他与内存相关的问题)。这是生产系统的首选故障排除方法,因为它通常是轻量级的,可以在垃圾收集发生后进行分析。
相反,解决方案 2 涉及到使用 NetBeans IDE 附带的开源工具。该工具允许您在代码运行时对其进行分析。这是一个很好的工具,可以原位了解您的应用中发生了什么,因为您可以看到实时 CPU 消耗、垃圾收集、创建的线程和加载的类。
这个方法仅仅触及了 NetBeans Profiler 的皮毛。更多信息,请参见位于profiler.netbeans.org/的在线文档。
注意
在使用 NetBeans Profiler 之前,必须校准目标 JVM。为此,请在 NetBeans 中打开“管理校准数据”对话框,并选择要校准的 JVM。通过打开配置文件菜单,然后选择高级命令,可以找到管理校准数据选项。
11-6.获取线程转储
问题
您的程序似乎什么也没做就“挂起”了,您怀疑可能出现了死锁。
解决办法
使用 JStack 获取线程转储,然后分析线程转储中的死锁。下面的 JStack 是来自 org . Java 9 recipes . chapter 11 . recipe 11 _ 06 类的线程转储。配方 11_6,它创建了一个死锁。Recipe11_6.java 的代码如下:
public class Recipe11_6 {
Lock firstLock = new ReentrantLock();
Lock secondLock = new ReentrantLock();
public static void main (String[] args) {
Recipe11_6 recipe = new Recipe11_6();
recipe.start();
}
private void start() {
firstLock.lock();
Thread secondThread = new Thread(() -> {
secondLock.lock();
firstLock.lock();
});
secondThread.start();
try {
Thread.sleep(250);
} catch (InterruptedException e) {
e.printStackTrace();
}
secondLock.lock();
secondLock.unlock();
firstLock.unlock();
}
}
从命令行或 IDE 执行代码,然后使用操作系统实用工具(如任务管理器)检查进程 ID。从下面的命令中可以看出,示例代码正在进程 ID 为 19705 的情况下运行:
**jstack -l 19705**
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.66-b17 mixed mode):
"Attach Listener" #11 daemon prio=9 os_prio=31 tid=0x00007f95c5818000 nid=0x380b waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"Thread-0" #10 prio=5 os_prio=31 tid=0x00007f95c41ba000 nid=0x5503 waiting on condition [0x000000012afba000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x000000076ab76698> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209)
at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
at org.java9recipes.chapter11.recipe11_06.Recipe11_6.lambda$start$0(Recipe11_6.java:25)
at org.java9recipes.chapter11.recipe11_06.Recipe11_6$$Lambda$1/1418481495.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
Locked ownable synchronizers:
- <0x000000076ab766c8> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
"Service Thread" #9 daemon prio=9 os_prio=31 tid=0x00007f95c4051000 nid=0x5103 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"C1 CompilerThread3" #8 daemon prio=9 os_prio=31 tid=0x00007f95c4031800 nid=0x4f03 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"C2 CompilerThread2" #7 daemon prio=9 os_prio=31 tid=0x00007f95c4031000 nid=0x4d03 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"C2 CompilerThread1" #6 daemon prio=9 os_prio=31 tid=0x00007f95c4030000 nid=0x4b03 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"C2 CompilerThread0" #5 daemon prio=9 os_prio=31 tid=0x00007f95c402e800 nid=0x4903 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"Signal Dispatcher" #4 daemon prio=9 os_prio=31 tid=0x00007f95c401a000 nid=0x3c17 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"Finalizer" #3 daemon prio=8 os_prio=31 tid=0x00007f95c283a800 nid=0x3503 in Object.wait() [0x0000000128e91000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076ab070b8> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
- locked <0x000000076ab070b8> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)
Locked ownable synchronizers:
- None
"Reference Handler" #2 daemon prio=10 os_prio=31 tid=0x00007f95c4003800 nid=0x3303 in Object.wait() [0x0000000128d8e000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076ab06af8> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:157)
- locked <0x000000076ab06af8> (a java.lang.ref.Reference$Lock)
Locked ownable synchronizers:
- None
"main" #1 prio=5 os_prio=31 tid=0x00007f95c280d800 nid=0x1303 waiting on condition [0x000000010d286000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x000000076ab766c8> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209)
at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
at org.java9recipes.chapter11.recipe11_06.Recipe11_6.start(Recipe11_6.java:34)
at org.java9recipes.chapter11.recipe11_06.Recipe11_6.main(Recipe11_6.java:18)
Locked ownable synchronizers:
- <0x000000076ab76698> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
"VM Thread" os_prio=31 tid=0x00007f95c3830800 nid=0x3103 runnable
"GC task thread#0 (ParallelGC)" os_prio=31 tid=0x00007f95c3005000 nid=0x2103 runnable
"GC task thread#1 (ParallelGC)" os_prio=31 tid=0x00007f95c3005800 nid=0x2303 runnable
"GC task thread#2 (ParallelGC)" os_prio=31 tid=0x00007f95c3006000 nid=0x2503 runnable
"GC task thread#3 (ParallelGC)" os_prio=31 tid=0x00007f95c4000000 nid=0x2703 runnable
"GC task thread#4 (ParallelGC)" os_prio=31 tid=0x00007f95c4001000 nid=0x2903 runnable
"GC task thread#5 (ParallelGC)" os_prio=31 tid=0x00007f95c3007000 nid=0x2b03 runnable
"GC task thread#6 (ParallelGC)" os_prio=31 tid=0x00007f95c3007800 nid=0x2d03 runnable
"GC task thread#7 (ParallelGC)" os_prio=31 tid=0x00007f95c3807000 nid=0x2f03 runnable
"VM Periodic Task Thread" os_prio=31 tid=0x00007f95c401b000 nid=0x5303 waiting on condition
JNI global references: 308
Found one Java-level deadlock:
=============================
"Thread-0":
waiting for ownable synchronizer 0x000000076ab76698, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
which is held by "main"
"main":
waiting for ownable synchronizer 0x000000076ab766c8, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
which is held by "Thread-0"
Java stack information for the preceding threads:
===================================================
"Thread-0":
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x000000076ab76698> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209)
at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
at org.java9recipes.chapter11.recipe11_06.Recipe11_6.lambda$start$0(Recipe11_6.java:25)
at org.java9recipes.chapter11.recipe11_06.Recipe11_6$$Lambda$1/1418481495.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
"main":
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x000000076ab766c8> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209)
at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
at org.java9recipes.chapter11.recipe11_06.Recipe11_6.start(Recipe11_6.java:34)
at org.java9recipes.chapter11.recipe11_06.Recipe11_6.main(Recipe11_6.java:18)
Found 1 deadlock.
要使这个方法在 Windows 上正常运行,您必须将 JDK 的 bin 文件夹作为 PATH 环境变量的一部分(例如 C:\ Program Files \ Java \ JDK 1 . 9 . 0 \ bin)。如果有这个路径,可以运行 JStack、JPS 之类的工具。JStack 预装在 OS X 上,所以你应该可以开箱即用。
JStack 命令使用–L(破折号和字母 L)作为参数,它指定了一个很长的列表(它做了额外的工作来获得关于正在运行的线程的更多信息)。JStack 还需要知道目标虚拟机的 PID。列出所有正在运行的 JVM 的一个快速方法是键入 JPS 并按 Enter 键。这将列出正在运行的虚拟机及其 PID。图 11-3 显示了一个 JStack 在 OS X 机器上发现配方 11-6 死锁的截图。
图 11-3。JStack 结果
注意
在这个例子中,j.u.c.l 代表 java.util.concurrent.locks,aqs 代表 AbstractQueuedSynchronizer。
它是如何工作的
JStack 允许您查看当前正在运行的线程的所有栈跟踪。JStack 还会试图找到可能会让系统停滞的死锁(锁的循环依赖)。JStack 不会发现其他问题,例如活锁(当线程总是在旋转时,比如 while(true))或饥饿(当线程因为优先级太低或有太多线程争用资源而无法执行时),但它会帮助您了解程序中的每个线程正在做什么。
死锁的发生是因为一个线程正在等待另一个线程拥有的资源,而第二个线程正在等待第一个线程拥有的资源。在这种情况下,两个线程都无法继续,因为两个线程都在等待对方释放各自拥有的资源。死锁不仅发生在两个线程之间,还可能涉及一串线程,因此线程 A 正在等待线程 B 正在等待线程 C 正在等待线程 D 正在等待原始线程 A。了解转储以找到罪魁祸首资源非常重要。
在这个配方的示例中,Thread-0 想要获取名为 0x000000076ab76698 的锁;它在线程转储中被描述为“等待可拥有的同步器”Thread-0 无法获取锁,因为它由主线程持有。另一方面,主线程想要获取锁 0x000000076ab766c8(注意它们是不同的;第一个锁以 98 结尾,而第二个锁以 c8 结尾,由 Thread-0 持有。这是一个教科书式的死锁定义,每个线程都在等待对方释放另一个线程拥有的锁。
除了死锁之外,查看线程转储可以让您了解您的程序实时在做什么。特别是在多线程系统中,使用线程转储将有助于澄清线程在哪里休眠,或者它在等待什么条件。
注意
JStack 通常是轻量级的,足以在实时系统中运行,所以如果您需要对实时问题进行故障排除,您可以安全地使用 JStack。
摘要
在这一章中,我们看了一些最容易被忽视,但却是最重要的软件开发部分。为了确保交付可靠的软件,调试、单元测试和应用性能评估是必须执行的关键任务。有许多有用的实用程序可以完成这些任务,本章简要介绍了其中的一些。
十二、Unicode、国际化和货币代码
Java 平台提供了一组丰富的国际化特性来帮助您创建可以在世界范围内使用的应用。该平台提供了本地化您的应用的方法,以各种文化上适当的格式格式化日期和数字,并显示在许多书写系统中使用的字符。
本章只描述了程序员在开发国际化应用时必须执行的一些最常见的任务。因为 Java 语言在语言和区域的抽象方面增加了新的特性,所以本章描述了一些使用 Locale 类的新方法。其他新功能对开发人员来说是透明的,例如更新以符合较新的 Unicode 标准,但更新提供了合规性,因此 JDK 9 将在未来几年保持相关性。Java 9 支持 Unicode 7.0,增加了 3000 个字符和 20 多个脚本。
注意
本章示例的源代码可以在 org.java9recipes.chapter12 包中找到。
12-1.将 Unicode 字符转换为数字
问题
您希望将 Unicode 数字字符转换为其各自的整数值。例如,您有一个包含值 8 的泰国数字的字符串,并且您想用该值生成一个整数。
解决办法
java.lang.Character 类有几个静态方法将字符转换为整数数值:
-
公共静态 intdigit(char ch,int radix)
-
公共静态整数(整数,整数基数)
下面的代码片段遍历从 0x0000 到 0x10FFFF 的整个 Unicode 码位范围。对于每个也是数字的码位,它显示字符及其数字值 0 到 9。你可以在 org . Java 9 recipes . chapter 12 . recipe 12 _ 1 中找到这个例子。Recipe12_1 类。
int x = 0;
for (int c=0; c <= 0x10FFFF; c++) {
if (Character.isDigit(c)) {
++x;
System.out.printf("Codepoint: 0x%04X\tCharacter: %c\tDigit: %d\tName: %s\n", c, c,
Character.digit(c, 10), Character.getName(c));
}
}
System.out.printf("Total digits: %d\n", x);
一些输出如下:
Codepoint: 0x0030 Character: 0 Digit: 0 Name: DIGIT ZERO
Codepoint: 0x0031 Character: 1 Digit: 1 Name: DIGIT ONE
Codepoint: 0x0032 Character: 2 Digit: 2 Name: DIGIT TWO
Codepoint: 0x0033 Character: 3 Digit: 3 Name: DIGIT THREE
Codepoint: 0x0034 Character: 4 Digit: 4 Name: DIGIT FOUR
Codepoint: 0x0035 Character: 5 Digit: 5 Name: DIGIT FIVE
Codepoint: 0x0036 Character: 6 Digit: 6 Name: DIGIT SIX
Codepoint: 0x0037 Character: 7 Digit: 7 Name: DIGIT SEVEN
Codepoint: 0x0038 Character: 8 Digit: 8 Name: DIGIT EIGHT
Codepoint: 0x0039 Character: 9 Digit: 9 Name: DIGIT NINE
Codepoint: 0x0660 Character: ٠ Digit: 0 Name: ARABIC-INDIC DIGIT ZERO
Codepoint: 0x0661 Character: ١ Digit: 1 Name: ARABIC-INDIC DIGIT ONE
Codepoint: 0x0662 Character: ٢ Digit: 2 Name: ARABIC-INDIC DIGIT TWO
Codepoint: 0x0663 Character: ٣ Digit: 3 Name: ARABIC-INDIC DIGIT THREE
Codepoint: 0x0664 Character: ٤ Digit: 4 Name: ARABIC-INDIC DIGIT FOUR
Codepoint: 0x0665 Character: ٥ Digit: 5 Name: ARABIC-INDIC DIGIT FIVE
Codepoint: 0x0666 Character: ٦ Digit: 6 Name: ARABIC-INDIC DIGIT SIX
Codepoint: 0x0667 Character: ٧ Digit: 7 Name: ARABIC-INDIC DIGIT SEVEN
Codepoint: 0x0668 Character: ٨ Digit: 8 Name: ARABIC-INDIC DIGIT EIGHT
Codepoint: 0x0669 Character: ٩ Digit: 9 Name: ARABIC-INDIC DIGIT NINE
...
Codepoint: 0x0E50 Character: ๐ Digit: 0 Name: THAI DIGIT ZERO
Codepoint: 0x0E51 Character: ๑ Digit: 1 Name: THAI DIGIT ONE
Codepoint: 0x0E52 Character: ๒ Digit: 2 Name: THAI DIGIT TWO
Codepoint: 0x0E53 Character: ๓ Digit: 3 Name: THAI DIGIT THREE
Codepoint: 0x0E54 Character: ๔ Digit: 4 Name: THAI DIGIT FOUR
Codepoint: 0x0E55 Character: ๕ Digit: 5 Name: THAI DIGIT FIVE
Codepoint: 0x0E56 Character: ๖ Digit: 6 Name: THAI DIGIT SIX
Codepoint: 0x0E57 Character: ๗ Digit: 7 Name: THAI DIGIT SEVEN
Codepoint: 0x0E58 Character: ๘ Digit: 8 Name: THAI DIGIT EIGHT
Codepoint: 0x0E59 Character: ๙ Digit: 9 Name: THAI DIGIT NINE
...
注意
示例代码打印到控制台。由于字体或平台的差异,您的控制台可能不会打印此示例中显示的所有字符标志符号。但是,这些字符将被正确地转换为整数。
它是如何工作的
Unicode 字符集很大,包含一百多万个唯一的码位,其整数值范围从 0x0000 到 0x10FFFF。每个字符值都有一组属性。其中一个属性是 isDigit。如果该属性为 true,则该字符表示从 0 到 9 的数字。例如,代码点值为 0x30 到 0x39 的字符具有字符标志符号 0、1、2、3、4、5、6、7、8 和 9。如果您简单地将这些代码值转换成它们对应的整数值,您将得到从 0x30 到 0x39 的十六进制值。对应的十进制值是 48 到 57。但是,这些字符也表示数字。当在计算中使用它们时,这些字符代表从 0 到 9 的值。
当字符具有 digit 属性时,使用 Character.digit()静态方法将其转换为相应的整数数值。请注意,digit()方法被重载以接受 char 或 int 参数。此外,该方法需要基数。基数的常用值是 2、10 和 16。有趣的是,虽然字符 A–F 和 A–F 没有 digit 属性,但它们可以用作基数为 16 的数字。对于这些字符,digit()方法返回 10 到 15 之间的预期整数值。
要完全理解 Unicode 字符集和 Java 的实现,需要熟悉几个新术语:字符、码位、字符、编码、序列化编码、UTF-8 和 UTF-16。这些术语超出了本菜谱的范围,但是您可以从位于 unicode.org 的 Unicode 网站或字符类 Java API 文档中了解更多关于这些和其他 Unicode 概念的信息。
12-2.创建和使用语言环境
问题
您希望以符合客户语言和文化期望的用户友好方式显示数字、日期和时间。
解决办法
数字、日期和时间的显示格式因世界而异,这取决于用户的语言和文化区域。此外,文本排序规则因语言而异。java.util.Locale 类表示世界上特定的语言和地区。通过确定和使用客户的区域设置,您可以将该区域设置应用于各种格式类,这些格式类可用于以预期的形式创建用户可见的数据。使用 Locale 实例来修改特定语言或地区的行为的类称为 locale-sensitive 类。你可以在第四章中了解更多关于区域敏感类的知识。该章向您展示了如何在 NumberFormat 和 DateFormat 类中使用 Locale 实例。然而,在本菜谱中,您将学习创建这些区域实例的不同选项。
您可以通过以下任何方式创建区域设置实例:
-
使用区域设置。用于配置和构建区域设置对象的生成器类。
-
使用静态 Locale.forLanguageTag()方法。
-
使用区域设置构造函数创建对象。
-
使用预先配置的静态语言环境对象。
Java 语言环境。构建器类具有 setter 方法,允许您创建可以转换为格式良好的最佳通用实践(BCP) 47 语言标记的区域设置。“它是如何工作的”一节更详细地描述了 BCP 47 标准。现在,您应该简单地理解构建器创建符合该标准的场所实例。
以下代码片段来自 org . Java 9 recipes . chapter 12 . recipe 12 _ 2。Recipe12_2 类演示了如何创建生成器和区域设置实例。您可以在区分区域设置的类中使用所创建的区域设置来生成区域性正确的显示格式:
private static final long number = 123456789L;
private static final Date now = new Date();
private void createFromBuilder() {
System.out.printf("Creating from Builder...\n\n");
String[][] langRegions = {{"fr", "FR"}, {"ja", "JP"}, {"en", "US"}};
Builder builder = new Builder();
Locale l = null;
NumberFormat nf = null;
DateFormat df = null;
for (String[] lr: langRegions) {
builder.clear();
builder.setLanguage(lr[0]).setRegion(lr[1]);
l = builder.build();
nf = NumberFormat.getInstance(l);
df = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, l);
System.out.printf("Locale: %s\nNumber: %s\nDate: %s\n\n",
l.getDisplayName(),
nf.format(number),
df.format(now));
}
前面的代码将以下内容打印到标准控制台:
Creating from Builder...
Locale: French (France)
Number: 123 456 789
Date: 14 septembre 2016 00:08:06 PDT
Locale: Japanese (Japan)
Number: 123,456,789
Date: 2016/09/14 0:08:06 PDT
Locale: English (United States)
Number: 123,456,789
Date: September 14, 2016 12:08:06 AM PDT
创建 Locale 实例的另一种方法是使用静态 Locale.forLanguageTag()方法。此方法允许您使用 BCP 47 语言标记参数。以下代码使用 forLanguageTag()方法从相应的语言标记创建三个区域设置:
...
System.out.printf("Creating from BCP 47 language tags...\n\n");
String[] bcp47LangTags= {"fr-FR", "ja-JP", "en-US"};
Locale l = null;
NumberFormat nf = null;
DateFormat df = null;
for (String langTag: bcp47LangTags) {
l = Locale.forLanguageTag(langTag);
nf = NumberFormat.getInstance(l);
df = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, l);
System.out.printf("Locale: %s\nNumber: %s\nDate: %s\n\n",
l.getDisplayName(),
nf.format(number),
df.format(now));
}
...
输出类似于从生成器生成的区域设置实例创建的结果:
Creating from BCP 47 language tags...
Locale: French (France)
Number: 123 456 789
Date: 14 septembre 2016 01:07:22 PDT
...
还可以使用构造函数来创建实例。以下代码显示了如何做到这一点:
Locale l = new Locale("fr", "FR");
其他构造函数允许您传递更少或更多的参数。参数可以包括语言、地区和可选的变量代码。
最后,Locale 类有许多针对一些常用情况的预定义静态实例。因为实例是预定义的,所以您的代码只需要引用静态实例。例如,以下示例显示了如何引用表示 fr-FR、ja-JP 和 en-US 语言环境的现有静态实例:
Locale frenchInFrance = Locale.FRANCE;
Locale japaneseInJapan = Locale.JAPAN;
Locale englishInUS = Locale.US;
有关其他静态实例的示例,请参考 locale Java API 文档。
它是如何工作的
Locale 类为区分区域设置的类提供了执行符合区域性的数据格式化和解析所需的上下文。一些区分区域设置的类包括:
-
Java . text . number 格式
-
java.text.DateFormat
-
java.util.Calendar
一个 Locale 实例标识一种特定的语言,可以对其进行微调,以标识用特定脚本编写的语言或在特定地区使用的语言。对于创造任何依赖于语言或地域影响的事物来说,语言环境是一个重要且必要的元素。
Java Locale 类一直在增强,以便为现代 BCP 47 语言标签提供更好的支持。BCP 47 定义了在语言、地区、文字和变体标识符方面使用 ISO 标准的最佳实践。尽管现有的语言环境构造函数继续与 Java 平台的早期版本兼容,但是这些构造函数不支持额外的脚本标记。例如,只有最近添加的区域设置。Builder 类和 Locale.forLanguageTag()方法支持标识脚本的新功能。因为区域设置构造函数不强制严格遵守 BCP 47,所以您应该在任何新代码中避免使用这些构造函数。相反,开发人员应该使用 Builder 类和 forLanguageTag()方法。
一个地点。构建器实例具有多种 setter 方法,可帮助您对其进行配置,以创建有效的、符合 BCP 47 标准的区域设置实例:
-
公共区域设置。BuildersetLanguage(字符串语言)
-
公共区域设置。BuildersetRegion(字符串区域)
-
当地观众。buildersetscript(字符串脚本)
如果这些方法的参数不是 BCP 47 标准的格式良好的元素,则每个方法都会引发 Java . util . illformedlocaleexception。language 参数必须是有效的两个或三个字母的 ISO 639 语言标识符。region 参数必须是有效的两个字母的 ISO 3166 地区代码或三个数字的 M.49 联合国“区域”代码。最后,脚本参数必须是有效的四字母 ISO 15924 脚本代码。
构建器允许您对其进行配置,以创建特定的符合 BCP 47 的语言环境。一旦设置了所有配置,build()方法就会创建并返回一个 Locale 实例。请注意,所有的 setters 可以被链接在一起形成一条语句。构建器模式的工作原理是让每个配置方法返回一个对当前实例的引用,在该实例上可以调用更多的配置方法。
Locale aLocale = new Builder().setLanguage("fr").setRegion("FR").build();
BCP 47 文件及其包含的标准可在以下位置找到:
-
47(语言标签):
www.rfc-editor.org/rfc/bcp/bcp47.txt -
ISO 3166(地区标识符):
www . iso . org/iso/country _ codes/iso _ 3166 _ code _ lists/country _ names _ and _ code _ elements . htm -
联合国 M.49(地区标识符):
unstats.un.org/unsd/methods/m49/m49.htm
12-3.设置默认区域设置
问题
您希望为所有区分区域设置的类设置默认区域设置。
解决办法
使用 Locale.setDefault()方法设置所有区分区域设置的类默认情况下将使用的区域设置实例。此方法由以下两种形式重载:
-
locale . set default(alocale locale)
-
locale . setdefault(本地)。c 类(locale alocale)
此示例代码演示了如何为所有区分区域设置的类设置默认区域设置:
Locale.setDefault(Locale.FRANCE);
您还可以为另外两个区域设置类别设置默认值,即显示和格式:
Locale.setDefault(Locale.Category.DISPLAY, Locale.US);
Locale.setDefault(Locale.Category.FORMAT, Locale.FR);
您可以在应用中创建使用这些特定区域设置类别的代码,以混合不同用途的区域设置选择。例如,您可以选择对 ResourceBundle 文本使用显示区域设置,而对日期和时间格式使用格式区域设置。org . Java 9 recipes . chapter 12 . recipe 12 _ 3 中的示例代码。Recipe12_3 类演示了这种更复杂的用法:
public class Recipe12_3 {
private static final Date NOW = new Date();
public void run() {
// Set ALL locales to fr-FR
Locale.setDefault(Locale.FRANCE);
demoDefaultLocaleSettings();
// System default is still fr-FR
// DISPLAY default is es-MX
// FORMAT default is en-US
Locale.setDefault(Locale.Category.DISPLAY, Locale.forLanguageTag("es-MX"));
Locale.setDefault(Locale.Category.FORMAT, Locale.US);
demoDefaultLocaleSettings();
// System default is still fr-FR
// DISPLAY default is en-US
// FORMAT default is es-MX
Locale.setDefault(Locale.Category.DISPLAY, Locale.US);
Locale.setDefault(Locale.Category.FORMAT, Locale.forLanguageTag("es-MX"));
demoDefaultLocaleSettings();
// System default is Locale.US
// Resets both DISPLAY and FORMAT locales to en-US as well.
Locale.setDefault(Locale.US);
demoDefaultLocaleSettings();
}
public void demoDefaultLocaleSettings() {
DateFormat df =
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
ResourceBundle resource =
ResourceBundle.getBundle("SimpleResources",
Locale.getDefault(Locale.Category.DISPLAY));
String greeting = resource.getString("GOOD_MORNING");
String date = df.format(NOW);
System.out.printf("DEFAULT LOCALE: %s\n", Locale.getDefault());
System.out.printf("DISPLAY LOCALE: %s\n", Locale.getDefault(Locale.Category.DISPLAY));
System.out.printf("FORMAT LOCALE: %s\n", Locale.getDefault(Locale.Category.FORMAT));
System.out.printf("%s, %s\n\n", greeting, date );
}
public static void main(String[] args) {
Recipe12_3 app = new Recipe12_3();
app.run();
}
}
该代码产生以下输出:
DEFAULT LOCALE: fr_FR
DISPLAY LOCALE: fr_FR
FORMAT LOCALE: fr_FR
Bonjour!, 19/09/16 20:31
DEFAULT LOCALE: fr_FR
DISPLAY LOCALE: es_MX
FORMAT LOCALE: en_US
¡Buenos días!, 9/19/16 8:31 PM
DEFAULT LOCALE: fr_FR
DISPLAY LOCALE: en_US
FORMAT LOCALE: es_MX
Good morning!, 19/09/16 08:31 PM
DEFAULT LOCALE: en_US
DISPLAY LOCALE: en_US
FORMAT LOCALE: en_US
Good morning!, 9/19/16 8:31 PM
它是如何工作的
Locale 类允许您为两个不同的类别设置默认的区域设置。类别由地区表示。类别枚举:
-
区域设置。类别。显示
-
区域设置。类别.格式
使用应用用户界面的显示类别。设置默认显示区域设置意味着 ResourceBundle 类可以独立于格式区域设置为该特定区域设置加载用户界面资源。设置格式默认区域设置会影响各种格式子类的行为。例如,默认的 DateFormat 实例将使用 Format 默认区域设置来创建区分区域设置的输出格式。同样,这两个类别是独立的,因此您可以针对不同的需求使用不同的区域设置实例。
在这个菜谱的示例代码中,Locale.setDefault(Locale。法国)方法调用将默认的系统、显示和格式区域设置设置为 fr-FR(法国的法语)。此方法总是重置显示和格式区域设置,以匹配系统区域设置。创建新的资源包时,resource bundle 类默认使用系统区域设置。但是,通过提供一个 locale 实例参数,您可以告诉 bundle 为特定的 Locale 加载资源。例如,即使系统区域设置是 locale。法国,您可以指定显示默认区域设置,并在 ResourceBundle.getBundle()方法调用中使用该显示区域设置。例如,这段代码试图为 es-MX 加载一个语言包,即使系统语言环境仍然是 locale。法国:
Locale.setDefault(Locale.Category.DISPLAY, Locale.forLanguageTag("es-MX"));
Locale.setDefault(Locale.Category.FORMAT, Locale.US);
DateFormat df = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
ResourceBundle resource =
ResourceBundle.getBundle("org.java9recipes.chapter12.resource.SimpleResources",
Locale.getDefault(Locale.Category.DISPLAY));
String greeting = resource.getString("GOOD_MORNING");
在这种情况下,它找到一个带有“Buenos días!”的早安资源值,因为显示默认区域设置是一个参数。资源包是一个文件,其中包含各种语言环境的已翻译属性字符串。名为 SimpleResources_en.properties(英语)的文件有一个 GOOD_MORNING 属性,写为“Good morning!”请注意,资源包中每个属性的翻译必须存在于特定于区域设置的资源文件中才能显示。Java 代码不翻译这些字符串。相反,它只是根据所选的语言环境选择所需属性的适当翻译。
注意
虽然如果您不在 DateFormat 和 NumberFormat 类的创建方法中提供区域设置参数,它们将自动使用默认的格式区域设置,但 ResourceBundle.getBundle()方法在默认情况下始终使用系统区域设置。要在 ResourceBundle()中使用显示默认区域设置,必须将其作为参数显式提供。
12-4.匹配和过滤区域设置
问题
您希望匹配或过滤区域设置列表,并只返回符合指定条件的区域设置。
解决办法
利用 Java 8 的 java.util.Locale 类中引入的新的语言环境匹配和过滤方法。如果您得到一个字符串格式的逗号分隔的区域设置列表,您可以对该字符串应用过滤器或“优先级列表”,以便只返回字符串中符合过滤器的区域设置。在以下示例中,使用 java.util.Locale filterTag 方法筛选语言标记列表,以字符串格式返回匹配的标记:
List<Locale.LanguageRange> list1 = Locale.LanguageRange.parse("ja-JP, en-US");
list1.stream().forEach((range) -> {
System.out.println("Range:" + range.getRange());
});
ArrayList localeList = new ArrayList();
localeList.add("en-US");
localeList.add("en-JP");
List<String> tags1 = Locale.filterTags(list1, localeList);
System.out.println("The following is the filtered list of locales:");
tags1.stream().forEach((tag) -> {
System.out.println(tag);
});
结果:
Range:ja-jp
Range:en-us
The following is the filtered list of Locales:
en-us
Locale 类的 filter()方法允许您返回匹配的 Locale 实例的列表。在下面的示例中,区域语言标记列表用于从区域列表中筛选区域类。
String localeTags = Locale.ENGLISH.toLanguageTag() + "," +
Locale.CANADA.toLanguageTag();
List<Locale.LanguageRange> list1 = Locale.LanguageRange.parse(localeTags);
list1.stream().forEach((range) -> {
System.out.println("Range:" + range.getRange());
});
ArrayList<Locale> localeList = new ArrayList();
localeList.add(new Locale("en"));
localeList.add(new Locale("en-JP"));
List<Locale> tags1 = Locale.filter(list1, localeList);
System.out.println("The following is the matching list of Locales:");
tags1.stream().forEach((tag) -> {
System.out.println(tag);
});
结果如下:
Range:en
Range:en-ca
The following is the matching list of locales:
en
它是如何工作的
Java 8 中的 java.util.Locale 类中添加了一些方法,允许您基于 List <locale.languagerange>格式的优先级列表过滤语言环境实例或语言标签。过滤机制基于 RFC 4647。以下列表包含这些过滤方法的简短摘要:</locale.languagerange>
-
过滤器(列表<locale.languagerange>,集合</locale.languagerange>
filter(List<Locale.LanguageRange>, Collection<Locale>, Locale.FilteringMode)(返回区域设置实例的匹配列表)
-
filterTags(列表<locale.languagerange>,集合</locale.languagerange>
filterTags(List<Locale.LanguageRange>, Collection<String>, Locale.FilteringMode)(返回匹配的语言标签列表)
要使用每种方法,应该将排序后的优先级顺序作为第一个参数发送。这个优先级顺序是一个区域列表。LanguageRange 对象,并且应该根据优先级或权重按降序排序。filter()方法中的第二个参数是区域设置的集合。此集合包含将被筛选的区域设置。可选的第三个参数包含一个 Locale.FilteringMode。表 12-1 列出了不同的过滤模式。
表 12-1。区域设置。过滤模式值
|模式
|
描述
| | --- | --- | | 自动选择 _ 过滤 | 指定基于给定优先级语言列表的筛选模式。 | | 扩展 _ 过滤 | 指定扩展筛选。 | | 忽略 _ 扩展 _ 范围 | 指定基本筛选。 | | 地图 _ 扩展 _ 范围 | 指定基本筛选,如果语言优先级列表中包含任何扩展语言,它们将被映射到基本语言范围。 | | 拒绝 _ 扩展 _ 范围 | 指定基本筛选,如果语言优先级列表中包含任何扩展语言,该列表将被拒绝并引发 IllegalArgumentException。 |
12-5.使用正则表达式搜索 Unicode
问题
您希望在字符串中查找或匹配 Unicode 字符。您希望使用正则表达式语法来实现这一点。
解决方案 1
查找或匹配字符的最简单方法是使用 String 类本身。字符串实例存储 Unicode 字符序列,并使用正则表达式提供相对简单的查找、替换和标记字符的操作。
若要确定字符串是否匹配正则表达式,请使用 matches()方法。如果整个字符串与正则表达式完全匹配,matches()方法返回 true。
以下代码来自 org . Java 9 recipes . chapter 12 . recipe 12 _ 4。Recipe12_4 类使用两个不同的表达式和两个字符串。正则表达式匹配只是确认字符串匹配在变量 enRegEx 和 jaRegEx 中定义的特定模式。
private String enText = "The fat cat sat on the mat with a brown rat.";
private String jaText = "Fight 文字化け!";
boolean found = false;
String enRegEx = "^The \\w+ cat.*";
String jaRegEx = ".*文字.*";
String jaRegExEscaped = ".*\u6587\u5B57.*";
found = enText.matches(enRegEx);
if (found) {
System.out.printf("Matches %s.\n", enRegEx);
}
found = jaText.matches(jaRegEx);
if (found) {
System.out.printf("Matches %s.\n", jaRegEx);
}
found = jaText.matches(jaRegExEscaped);
if (found) {
System.out.printf("Matches %s.\n", jaRegExEscaped);
}
该代码打印以下内容:
Matches ^The \w+ cat.*.
Matches .*文字.*.
Matches .*文字.*.
使用 replaceFirst()方法创建一个新的 String 实例,其中目标文本中正则表达式的第一个匹配项被替换为替换文本。该代码演示了如何使用此方法:
String replaced = jaText.replaceFirst("文字化け", "mojibake");
System.out.printf("Replaced: %s\n", replaced);
输出中显示了替换文本:
Replaced: Fight mojibake!
replaceAll()方法用替换文本替换所有出现的表达式。
最后,split()方法创建一个 String[],其中包含由匹配表达式分隔的文本。换句话说,它返回由表达式分隔的文本。或者,您可以提供一个 limit 参数来限制在源文本中应用分隔符的次数。以下代码演示了 split()方法对空格字符进行拆分:
String[] matches = enText.split("\\s", 3);
for(String match: matches) {
System.out.printf("Split: %s\n",match);
}
代码的输出如下:
Split: The
Split: fat
Split: cat sat on the mat with a brown rat.
解决方案 2
当简单的字符串方法不够时,可以使用更强大的 java.util.regex 包来处理正则表达式。使用 Pattern 类创建正则表达式。匹配器使用模式处理字符串实例。所有匹配器操作都使用模式和字符串实例来执行它们的功能。
下面的代码演示了如何在两个单独的字符串中搜索 ASCII 和非 ASCII 文本。见 org . Java 9 recipes . chapter 12 . recipe 12 _ 4。Recipe12_4 类的完整源代码。demoSimple()方法查找后面跟有任何字符的文本。在”。demoComplex()方法在字符串中查找两个日语符号:
public void demoSimple() {
Pattern p = Pattern.compile(".at");
Matcher m = p.matcher(enText);
while(m.find()) {
System.out.printf("%s\n", m.group());
}
}
public void demoComplex() {
Pattern p = Pattern.compile("文字");
Matcher m = p.matcher(jaText);
if (m.find()) {
System.out.println(m.group());
}
}
对先前定义的英语和日语文本运行这两种方法会显示以下内容:
fat
cat
sat
mat
rat
文字
它是如何工作的
使用正则表达式的字符串方法如下:
-
公共布尔匹配(字符串正则表达式)
-
public String replaceFirst(字符串正则表达式,字符串替换)
-
public String replaceAll(字符串正则表达式,字符串替换)
-
公共 String[] split(String regex,int limit)
-
公共字符串[]拆分(字符串正则表达式)
字符串方法是 java.util.regex 类更强大功能的有限且相对简单的包装:
-
java.util.regex .模式
-
java.util.regex.Matcher
-
Java . util . regex . patternantxeexception
Java 正则表达式类似于 Perl 语言中使用的那些表达式。虽然关于 Java 正则表达式还有很多要学的,但是从这个食谱中最重要的几点可能是:
-
正则表达式肯定可以包含所有 Unicode 字符中的非 ASCII 字符。
-
由于 Java 语言编译器理解反斜杠字符的特性,您将不得不在代码中使用两个反斜杠,而不是一个用于预定义的字符类表达式。
在正则表达式中使用非 ASCII 字符最方便、最易读的方法是使用键盘输入法将它们直接输入到源文件中。操作系统和编辑器允许您在 ASCII 之外输入复杂文本的方式有所不同。不管什么操作系统,如果你的编辑允许,你都应该用 UTF-8 编码保存文件。作为使用非 ASCII 正则表达式的另一种更困难的方法,您可以使用\uXXXX 符号对字符进行编码。使用这种表示法,您可以输入 \u 或 \U ,后跟 Unicode 码位的十六进制表示,而不是使用键盘直接键入字符。这个菜谱的代码示例使用了日语单词“文字”(发音墨姬)。如示例所示,您可以在正则表达式中使用实际字符,也可以查找 Unicode 码位值。对于这个特定的日语单词,编码将是\u6587\u5B57。
Java 语言的正则表达式支持包括特殊的字符类。例如,\d 和\w 分别是正则表达式[0-9]和[a-zA-z0-9]的快捷表示法。但是,由于 Java 编译器对反斜杠字符的特殊处理,在使用预定义的字符类如\d(数字)、\w(单词字符)和\s(空格字符)时,必须使用额外的反斜杠。例如,要在源代码中使用它们,您可以分别输入 \d 、 \w 和 \s 。示例代码在解决方案 1 中使用双反斜杠来表示\w 字符类:
String enRegEx = "^The **\\w** + cat.*";
12-6.覆盖默认货币
问题
您希望使用与默认区域设置无关的货币来显示数值。
解决办法
通过显式设置 NumberFormat 实例中使用的货币,控制使用格式化货币值打印哪种货币。以下示例假定默认区域设置为 Locale.JAPAN。它通过调用 NumberFormat 实例的 setCurrency(Currency c)方法来更改货币。这个例子来自 org . Java 9 recipes . chapter 12 . recipe 12 _ 6。Recipe12_6 类。
BigDecimal value = new BigDecimal(12345);
System.out.printf("Default locale: %s\n", Locale.getDefault().getDisplayName());
NumberFormat nf = NumberFormat.getCurrencyInstance();
String formattedCurrency = nf.format(value);
System.out.printf("%s\n", formattedCurrency);
Currency c = Currency.getInstance(Locale.US);
nf.setCurrency(c);
formattedCurrency = nf.format(value);
System.out.printf("%s\n\n", formattedCurrency);
前面的代码打印出以下内容:
Default locale: 日本語 (日本)
¥12,345
USD12,345
它是如何工作的
使用 NumberFormat 实例来格式化货币值。您应该显式调用 getCurrencyInstance()方法来创建货币格式化程序:
NumberFormat nf = NumberFormat.getCurrencyInstance();
前一个格式化程序将使用您的默认区域设置首选项将数字格式化为货币值。此外,它将使用与地区相关联的货币符号。然而,一个非常常见的用例涉及到为不同地区的货币设置值的格式。
使用 setCurrency()方法在数字格式化程序中显式设置货币:
nf.setCurrency(aCurrencyInstance); // requires a Currency instance
请注意,java.util.Currency 类是一个工厂。它允许您以两种方式创建货币对象:
-
Currency.getInstance(本地端)
-
Currency.getInstance(字符串货币代码)
第一个 getInstance 调用使用一个 Locale 实例来检索货币对象。Java 平台将默认货币与地区相关联。在这种情况下,当前与美国相关联的默认货币是美元:
Currency c1 = Currency.getInstance(Locale.US);
第二个 getInstance 调用使用有效的 ISO 4217 货币代码。美元的货币代码是 USD:
Currency c2 = Currency.getInstance("USD");
一旦有了货币实例,只需在格式化程序中使用该实例:
nf.setCurrency(c2);
这个格式化程序现在被配置为使用默认区域设置的数字格式符号和模式来格式化数字值,但是它会将目标货币代码显示为可显示文本的一部分。这允许您将默认的数字格式模式与其他货币代码混合使用。
注意
货币既有符号又有代码。货币代码总是指三个字母的 ISO 4217 代码。货币符号通常不同于代码。例如,美元的代码为 USD,符号为$。货币格式化程序在使用默认区域设置中的地区货币格式化数字时,通常会使用符号。但是,当您显式更改格式化程序的货币时,格式化程序并不总是知道目标货币的本地化符号。在这种情况下,format 实例通常会在显示的文本中使用货币代码。
12-7.将字节数组与字符串相互转换
问题
您需要将字节数组中的字符从传统字符集编码转换为 Unicode 字符串。
解决办法
使用 String 类将传统字符编码从字节数组转换为 Unicode 字符串。以下代码片段来自 org . Java 9 recipes . chapter 12 . recipe 12 _ 7。Recipe12_7 类演示了如何将传统的移位 JIS 编码的字节数组转换为字符串。在同一示例的后面,代码演示了如何从 Unicode 转换回移位 JIS 字节数组。
byte[] legacySJIS = {(byte)0x82,(byte)0xB1,(byte)0x82,(byte)0xF1,
(byte)0x82,(byte)0xC9,(byte)0x82,(byte)0xBF,
(byte)0x82,(byte)0xCD,(byte)0x81,(byte)0x41,
(byte)0x90,(byte)0xA2,(byte)0x8A,(byte)0x45,
(byte)0x81,(byte)0x49};
// Convert a byte[] to a String
Charset cs =Charset.forName("SJIS");
String greeting = new String(legacySJIS, cs);
System.out.printf("Greeting: %s\n", greeting);
这段代码打印出转换后的文本,即“Hello,world!”在日语中:
Greeting: こんにちは、世界!
使用 getBytes()方法将字符从字符串转换为字节数组。在前面代码的基础上,使用以下代码转换回原始编码,并比较结果:
// Convert a String to a byte[]
byte[] toSJIS = greeting.getBytes(cs);
// Confirm that the original array and newly converted array are same
Boolean same = false;
if (legacySJIS.length == toSJIS.length) {
for (int x=0; x< legacySJIS.length; x++) {
if(legacySJIS[x] != toSJIS[x]) break;
}
same = true;
}
System.out.printf("Same: %s\n", same.toString());
正如预期的那样,输出表明返回到遗留编码的往返转换是成功的。原始字节数组和转换后的字节数组包含相同的字节:
Same: true
它是如何工作的
Java 平台为许多传统字符集编码提供了转换支持。当从字节数组创建字符串实例时,必须向字符串构造函数提供一个 charset 参数,以便平台知道如何执行从传统编码到 Unicode 的映射。所有 Java 字符串都使用 Unicode 作为本地编码。
原始数组中的字节数通常不等于结果字符串中的字符数。在这个配方的例子中,原始数组包含 18 个字节。移位 JIS 编码需要 18 个字节来表示日语文本。但是,转换后,结果字符串包含九个字符。字节和字符之间没有 1:1 的关系。在这个例子中,在原始的移位 JIS 编码中,每个字符需要两个字节。
实际上有数百种不同的字符集编码。编码的数量取决于您的 Java 平台实现。但是,您可以保证支持几种最常见的编码,并且您的平台很可能包含比这个最小集合更多的编码:
-
美国-阿斯凯
-
ISO-8859-1
-
UTF-8
-
UTF-16BE
-
UTF-16LE 编码
-
UTF-16
构建字符集时,您应该准备好处理字符集不受支持时可能出现的异常:
-
当字符集名称不合法时抛出
-
当字符集名称为空时抛出
-
当你的 JVM 不支持目标字符集时抛出
12-8.转换字符流和缓冲区
问题
您需要在大块 Unicode 字符文本和任意面向字节的编码之间进行转换。大块文本可能来自流或文件。
解决方案 1
使用 java.io.InputStreamReader 将字节流解码为 Unicode 字符。使用 java.io.OutputStreamWriter 将 Unicode 字符编码为字节流。
下面的代码使用 InputStreamReader 从类路径中的文件读取并转换可能很大的文本字节块。org . Java 9 recipes . chapter 12 . recipe 12 _ 8。StreamConversion 类为此示例提供了完整的代码:
public String readStream() throws IOException {
InputStream is = getClass().getResourceAsStream("resource/helloworld.sjis.txt");
StringBuilder sb = new StringBuilder();
if (is != null) {
try (InputStreamReader reader =
new InputStreamReader(is, Charset.forName("SJIS"))) {
int ch = reader.read();
while (ch != -1) {
sb.append((char) ch);
ch = reader.read();
}
}
}
return sb.toString();
}
类似地,可以使用 OutputStreamWriter 将文本写入字节流。下面的代码将一个字符串写入 UTF 8 编码的字节流:
public void writeStream(String text) throws IOException {
FileOutputStream fos = new FileOutputStream("helloworld.utf8.txt");
try (OutputStreamWriter writer
= new OutputStreamWriter(fos, Charset.forName("UTF-8"))) {
writer.write(text);
}
}
解决方案 2
使用 Java . nio . charset . charset encoder 和 Java . nio . charset . charset decoder 在 Unicode 字符缓冲区和字节缓冲区之间进行转换。使用 newEncoder()或 newDecoder()方法从 charset 实例中检索编码器或解码器。然后使用编码器的 encode()方法创建字节缓冲区。使用解码器的 decode()方法创建字符缓冲区。以下代码来自 org . Java 9 recipes . chapter 12 . recipe 12 _ 8。BufferConversion 类对缓冲区中的字符集进行编码和解码:
public ByteBuffer encodeBuffer(String charsetName, CharBuffer charBuffer)
throws CharacterCodingException {
Charset charset = Charset.forName(charsetName);
CharsetEncoder encoder = charset.newEncoder();
ByteBuffer targetBuffer = encoder.encode(charBuffer);
return targetBuffer;
}
public CharBuffer decodeBuffer(String charsetName, ByteBuffer srcBuffer)
throws CharacterCodingException {
Charset charset = Charset.forName(charsetName);
CharsetDecoder decoder = charset.newDecoder();
CharBuffer charBuffer = decoder.decode(srcBuffer);
return charBuffer;
}
它是如何工作的
java.io 和 java.nio.charset 包包含几个类,可以帮助您对大型文本流或缓冲区执行编码转换。流是方便的抽象,可以帮助您使用各种源和目标来转换文本。流可以表示 HTTP 连接中的传入或传出文本,甚至可以表示文件。
如果您使用 InputStream 来表示基础源文本,您将在 InputStreamReader 中包装该流,以执行从字节流的转换。读取器实例执行从字节到 Unicode 字符的转换。
使用 OutputStream 实例表示目标文本,将流包装在 OutputStreamWriter 中。编写器会在目标流中将您的 Unicode 文本转换为面向字节的编码。
为了有效地使用 OutputStreamWriter 或 InputStreamReader,您必须知道目标或源文本的字符编码。当您使用 OutputStreamWriter 时,源文本总是 Unicode,并且您必须提供一个 charset 参数来告诉编写器如何转换为目标面向字节的文本编码。使用 InputStreamReader 时,目标编码始终是 Unicode。您必须提供源文本编码作为参数,以便读者理解如何转换文本。
注意
Java 平台的字符串表示 Unicode 的 UTF-16 编码中的字符。Unicode 可以有几种编码,包括 UTF-16、UTF-8,甚至 UTF-32。在本讨论中,转换为 Unicode 始终意味着转换为 UTF-16。转换成面向字节的编码通常意味着转换成传统的非 Unicode 字符集编码。然而,一种常见的面向字节的编码是 UTF-8,使用 InputStreamReader 或 OutputStreamWriter 类将 Java 的“本机”UTF-16 Unicode 字符转换为 UTF-8 或从-8 转换是完全合理的。
执行编码转换的另一种方式是使用 CharsetEncoder 和 CharsetDecoder 类。CharsetEncoder 会将您的 Unicode CharBuffer 实例编码为 ByteBuffer 实例。CharsetDecoder 将 ByteBuffer 实例解码成 CharBuffer 实例。无论哪种情况,都必须提供一个字符集参数。
字符集表示在互联网签名号码管理机构(IANA)字符集注册表中定义的字符集编码。创建字符集实例时,应该使用注册表定义的字符集的规范名称或别名。你可以在www.iana.org/assignments/character-sets找到注册表。
请记住,您的 Java 实现不一定支持所有的 IANA 字符集名称。然而,所有的实现都需要至少支持本章的方法 12-7 中显示的那些。
12-9.设置区分区域设置的服务的搜索顺序
问题
您希望在 Java 运行时环境中为语言环境敏感的服务指定一个指定的搜索顺序。
解决办法
使用 java.locale.providers 属性为区分区域设置的服务指定所需的顺序。在下面的示例中,SPI 和 CLDR 提供程序是在属性中指定的。
java.locale.providers=SPI,CLDR
它是如何工作的
自 Java 8 发布以来,设置 java.locale.providers 属性指定了对语言环境敏感的服务的搜索顺序。该属性在 Java 运行时启动时读取。要设置服务顺序,请指定缩写,用逗号分隔。以下服务可供使用:
-
SPI:由 SPI(服务提供者接口)提供者表示的对地区敏感的服务
-
JRE:Java 运行时环境中的区域敏感服务
-
CLDR:基于 Unicode Consortium 的 CLDR 项目的提供商
-
主机:反映底层操作系统中用户自定义设置的提供者
摘要
国际化是开发文化响应应用的关键。它允许更改应用文本,以符合应用使用的文化和语言。本章提供了一些例子,说明如何利用国际化技术来克服跨文化开发的细微差别。本章还介绍了有关 Unicode 转换的主题。
十三、使用数据库
几乎所有重要的应用都包含某种数据库。一些应用使用内存数据库,而其他应用使用传统的关系数据库管理系统(RDBMSs)。无论是哪种情况,每个 Java 开发人员都必须掌握一些使用数据库的技能。多年来,Java 数据库连接(JDBC) API 已经有了很大的发展,在过去的几个版本中已经有了一些重大的进步。
本章讲述了使用 JDBC 处理数据库的基础知识。您将学习如何执行所有标准的数据库操作,以及一些操作数据的高级技术。您还将了解如何使用 API 中的一些最新进展来创建安全的数据库应用并节省开发时间。最终,您将能够开发与 Oracle 数据库、PostgreSQL 和 MySQL 等传统 RDBMSs 一起工作的 Java 应用。
注意
要遵循本章中的示例,请运行 create_user.sql 脚本来创建数据库用户模式。然后,在刚刚创建的数据库模式中运行 create_database.sql 脚本。
本书中的数据库示例是为 Apache Derby 或 Oracle 数据库量身定制的,但是它们可以修改为适用于任何关系数据库。
13-1.连接到数据库
问题
您希望从桌面 Java 应用中创建一个到数据库的连接。
解决方案 1
使用 JDBC 连接对象来获取连接。为此,创建一个新的连接对象,然后加载您需要用于特定数据库供应商的驱动程序。一旦连接对象准备就绪,就调用它的 getConnection()方法。下面的代码演示了如何根据指定的驱动程序获得到 Oracle 或 Apache Derby 数据库的连接。
public Connection getConnection() throws SQLException {
Connection conn = null;
String jdbcUrl;
if(driver.equals("derby")){
jdbcUrl = "jdbc:derby://" + this.hostname + ":" +
this.port + "/" + this.database;
} else {
jdbcUrl = "jdbc:oracle:thin:@" + this.hostname + ":" +
this.port + ":" + this.database;
}
System.out.println(jdbcUrl);
conn = DriverManager.getConnection(jdbcUrl, username, password);
System.out.println("Successfully connected");
return conn;
}
本例中描述的方法返回一个准备好用于数据库访问的连接对象。
解决方案 2
使用数据源创建连接池。DataSource 对象必须已经正确实现并部署到应用服务器环境中。在实现和部署 DataSource 对象之后,应用可以使用它来获得到数据库的连接。以下代码显示了可用于通过 DataSource 对象获取数据库连接的代码:
public Connection getDSConnection() {
Connection conn = null;
try {
Context ctx = new InitialContext();
DataSource ds = (DataSource)ctx.lookup("jdbc/myOracleDS");
conn = ds.getConnection();
} catch (NamingException | SQLException ex) {
ex.printStackTrace();
}
return conn;
}
注意,DataSource 实现中唯一需要的信息是有效 DataSource 对象的名称。获得数据库连接所需的所有信息都在应用服务器中管理。
它是如何工作的
在 Java 应用中,有几种不同的方法可以创建到数据库的连接。如何做到这一点取决于您正在编写的应用的类型。如果一个应用是独立的或者是一个桌面应用,那么经常使用 DriverManager。基于 Web 和 intranet 的应用通常依靠应用服务器通过 DataSource 对象为应用提供连接。
创建 JDBC 连接需要几个步骤。首先,您需要确定您将需要哪个数据库驱动程序。在确定了需要哪个驱动程序之后,下载包含该驱动程序的 JAR 文件,并将其放入类路径中。对于这个菜谱,要么建立 Oracle 数据库连接,要么建立 Apache Derby 连接。每个数据库供应商都将提供不同的 JDBC 驱动程序,这些驱动程序打包在具有不同名称的 JAR 文件中;有关更多信息,请参考特定数据库的文档。一旦获得了适合您的数据库的 JAR 文件,就将它包含在您的应用类路径中。接下来,使用 JDBC 驱动程序管理器获得到数据库的连接。从 4.0 版开始,类路径中包含的驱动程序被自动加载到 DriverManager 对象中。如果您使用的是 4.0 之前的 JDBC 版本,则必须手动加载驱动程序。
要使用 DriverManager 获得到数据库的连接,需要向它传递一个包含 JDBC URL 的字符串。JDBC URL 由数据库供应商名称、托管数据库的服务器名称、数据库名称、数据库端口号以及可以访问您要使用的模式或数据库对象的有效数据库用户名和口令组成。很多时候,用于创建 JDBC URL 的值都是从属性文件中获取的,因此如果需要的话,可以很容易地对它们进行更改。要了解更多关于使用属性文件存储连接值的信息,请参阅配方 13-5。用于为解决方案 1 创建 Oracle 数据库 JDBC URL 的代码如下所示:
String jdbcUrl = "jdbc:oracle:thin:@" + this.hostname + ":" +
this.port + ":" + this.database;
一旦所有的变量都被替换到字符串中,它将看起来像下面这样:
jdbc:oracle:thin:@hostname:1521:database
类似地,Apache Derby URL 字符串如下所示:
jdbc:derby://hostname:1521/database
一旦创建了 JDBC URL,就可以将它传递给 DriverManager.getConnection()方法以获得 java.sql.Connection 对象。如果向 getConnection()方法传递了不正确的信息,将引发 Java . SQL . sqlexception;否则,将返回有效的连接对象。
获得数据库连接的首选方法是在应用服务器上运行时使用数据源,或者访问 Java 命名和目录接口(JNDI)服务。要使用 DataSource 对象,您需要有一个应用服务器将它部署到。任何兼容的 Java 应用服务器如 GlassFish、Oracle Weblogic、Payara 或 WildFly 都可以工作。大多数应用服务器都包含一个 web 接口,可以用来轻松部署 DataSource 对象。但是,您可以使用类似如下的代码来手动部署 DataSource 对象:
org.java9recipes.chapter13.recipe13_01.FakeDataSourceDriver ds =
new org.java9recipes.chapter13.recipe13_1.FakeDataSourceDriver();
ds.setServerName("my-server");
ds.setDatabaseName("JavaRecipes");
ds.setDescription("Database connection for Java 9 Recipes");
这段代码实例化一个新的 DataSource 驱动程序类,然后根据您想要注册的数据库设置属性。在应用服务器中注册数据源或访问 JNDI 服务器时,通常会使用这里演示的数据源代码。如果您使用基于 web 的管理工具来部署数据源,应用服务器通常在幕后完成这项工作。大多数数据库供应商都会提供一个数据源驱动程序以及他们的 JDBC 驱动程序,所以如果正确的 JAR 驻留在应用或服务器类路径中,它应该可以被识别并可供使用。一旦实例化和配置了数据源,下一步就是向 JNDI 命名服务注册数据源。
以下代码演示了向 JNDI 注册数据源的过程:
try {
Context ctx = new InitialContext();
DataSource ds =
(DataSource) ctx.bind("java9recipesDB");
} catch (NamingException ex) {
ex.printStackTrace();
}
一旦部署了数据源,部署到同一应用服务器的任何应用都可以访问它。使用 DataSource 对象的美妙之处在于,您的应用代码不需要了解数据库的任何信息;它只需要知道数据源的名称。通常,数据源的名称以 jdbc/前缀开头,后面跟一个标识符。为了查找 DataSource 对象,使用了 InitialContext。InitialContext 查看应用服务器中所有可用的数据源,如果找到,则返回有效的数据源;否则会抛出 java.naming.NamingException 异常。在解决方案 2 中,您可以看到 InitialContext 返回一个必须转换为 DataSource 的对象。
Context ctx = new InitialContext();
DataSource ds = (DataSource)ctx.lookup("jdbc/myOracleDS");
如果数据源是连接池缓存,当应用请求时,它将发送连接池中的一个可用连接。以下代码行从数据源返回一个连接对象:
conn = ds.getConnection();
当然,如果不能获得有效的连接,就会抛出 java.sql.SQLException。DataSource 技术优于 DriverManager,因为数据库连接信息只存储在一个地方:应用服务器。一旦部署了有效的数据源,它就可以被许多应用使用。
在您的应用获得一个有效的连接之后,就可以使用它来处理数据库了。要了解有关使用连接对象处理数据库的更多信息,请参见方法 13-2 和 13-4。
13-2.处理连接和 SQL 异常
问题
应用中的数据库活动引发了异常。您需要处理 SQL 异常,以便您的应用不会崩溃。
解决办法
使用 try-catch 块来捕获和处理由 JDBC 连接或 SQL 查询引发的任何 SQL 异常。下面的代码演示了如何实现 try-catch 块来捕获 SQL 异常:
try {
// perform database tasks
} catch (java.sql.SQLException){
// perform exception handling
}
它是如何工作的
标准的 try-catch 块可用于捕获 java.sql.Connection 或 java.sql.SQLException 异常。如果不处理这些异常,您的代码将无法编译,为了防止您的应用在这些异常之一被抛出时崩溃,适当地处理它们是一个好主意。几乎所有针对 java.sql.Connection 对象执行的工作都需要包含错误处理,以确保正确处理数据库异常。事实上,通常需要嵌套的 try-catch 块来处理所有可能的异常。您需要确保一旦完成工作并且不再使用连接对象,就关闭连接。同样,关闭 java.sql.Statement 对象来清理内存分配也是一个好主意。
因为需要关闭语句和连接对象,所以经常会看到使用 try-catch-finally 块来确保所有资源都被按需使用。您很可能会看到类似以下样式的旧 JDBC 代码:
try {
// perform database tasks
} catch (java.sql.SQLException ex) {
// perform exception handling
} finally {
try {
// close Connection and Statement objects
} catch (java.sql.SQLException ex){
// perform exception handling
}
}
应该编写新的代码来利用 try-with-resources 语句,该语句允许将资源管理卸载到 Java,而不是执行手动关闭。下面的代码演示如何使用 try-with-resources 打开连接,创建语句,然后在完成后关闭连接和语句。
注意
示例中的 createConn 对象抽象出了获取数据库连接的细节,这些细节可以通过调用 getConnection()方法返回。
try (Connection conn = createConn.getConnection();
Statement stmt = conn.createStatement();) {
ResultSet rs = stmt.executeQuery(qry);
while (rs.next()) {
// PERFORM SOME WORK
}
} catch (SQLException e) {
e.printStackTrace();
}
如前面的伪代码所示,为了清理未使用的资源,经常需要嵌套的 try-catch 块。适当的异常处理有时会使 JDBC 代码编写起来相当费力,但它也将确保需要数据库访问的应用不会失败,从而导致数据丢失。
13-3.查询数据库和检索结果
问题
应用中的一个进程需要查询数据库表中的数据。
解决办法
使用方法 13-1 中描述的技术之一获得一个 JDBC 连接,然后使用 java.sql.Connection 对象创建一个语句对象。java.sql.Statement 对象包含 executeQuery()方法,该方法解析文本字符串并使用它来查询数据库。一旦执行了查询,就可以将查询结果检索到 ResultSet 对象中。以下示例查询名为 RECIPES 的数据库表并打印结果:
String qry = "select recipe_num, name, description from recipes";
try (Connection conn = createConn.getConnection();
Statement stmt = conn.createStatement();) {
ResultSet rs = stmt.executeQuery(qry);
while (rs.next()) {
String recipe = rs.getString("RECIPE_NUM");
String name = rs.getString("NAME");
String desc = rs.getString("DESCRIPTION");
System.out.println(recipe + "\t" + name + "\t" + desc);
}
} catch (SQLException e) {
e.printStackTrace();
}
如果您使用本章中包含的数据库脚本执行此代码,您将收到以下结果:
13-1 Connecting to a Database DriverManager and DataSource Implementations
13-2 Querying a Database and Retrieving Results Obtaining and Using Data from a DBMS
13-3 Handling SQL Exceptions Using SQLException
它是如何工作的
对数据库最常执行的操作之一是查询。使用 JDBC 执行数据库查询非常容易,尽管每次执行查询时都需要使用一些样板代码。首先,您需要为您想要运行查询的数据库和模式获取一个连接对象。你可以通过使用配方 13-1 中的一个解决方案来完成。接下来,您需要形成一个查询并以字符串格式存储它。然后,连接对象用于创建语句。您的查询字符串将被传递给语句对象的 executeQuery()方法,以便实际查询数据库。在这里,您可以看到不使用 try-with-resources 进行资源管理时的情况。
String qry = "select recipe_num, name, description from recipes";
Connection conn;
Statement stmt = null;
try {
conn = createConn.getConnection()
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(qry);
...
相同的代码可以更有效地编写如下:
try (Connection conn = createConn.getConnection();
Statement stmt = conn.createStatement();) {
ResultSet rs = stmt.executeQuery(qry);
...
如您所见,语句对象的 executeQuery()方法接受一个字符串并返回一个 ResultSet 对象。ResultSet 对象使处理查询结果变得容易,因此您可以按任何顺序获得所需的信息。如果您看一下示例中的下一行代码,就会发现在 ResultSet 对象上创建了一个 while 循环。这个循环将继续调用 ResultSet 对象的 next()方法,获得每次迭代从查询中返回的下一行。在这种情况下,ResultSet 对象被命名为 rs,因此当 rs.next()返回 true 时,循环将继续被处理。一旦处理完所有返回的行,rs.next()将返回一个 false,表示没有要处理的行了。
在 while 循环中,处理每个返回的行。对 ResultSet 对象进行解析,以获得每次传递的给定列名的值。请注意,如果希望列返回一个字符串,则必须调用 ResultSet getString()方法,以字符串格式传递列名。类似地,如果希望该列返回一个 int,您可以调用 ResultSet getInt()方法,以字符串格式传递列名。其他数据类型也是如此。这些方法将返回相应的列值。在这个配方的解决方案的例子中,这些值被存储到局部变量中。
String recipe = rs.getString("RECIPE_NUM");
String name = rs.getString("NAME");
String desc = rs.getString("DESCRIPTION");
一旦获得了列值,您就可以对存储在局部变量中的值做您想做的事情。在这种情况下,它们是使用 System.out()方法打印出来的。
System.out.println(recipe + "\t" + name + "\t" + desc);
尝试查询数据库时可能会引发 java.sql.SQLException(例如,如果没有正确获取连接对象,或者如果您尝试查询的数据库表不存在)。在这些情况下,您必须提供异常处理来处理错误。因此,所有数据库处理代码都应该放在 try 块中。catch 块然后处理一个 SQLException,因此如果抛出一个,将使用 catch 块中的代码处理该异常。听起来很简单,对吧?是的,但是每次执行数据库查询时都必须这样做。很多样板代码。
如果语句和连接是打开的,关闭它们总是一个好主意。使用 try-with-resources 构造是最有效的资源管理解决方案。完成后关闭资源将有助于确保系统可以根据需要重新分配资源,并尊重数据库。尽快关闭连接以便其他进程可以使用它们是很重要的。
13-4.执行 CRUD 操作
问题
您需要能够在应用中执行标准的数据库操作。也就是说,您需要创建、检索、更新和删除(CRUD)数据库记录的能力。
解决办法
使用配方 13-1 中提供的解决方案之一创建一个连接对象并获得一个数据库连接;然后使用从 java.sql.Connection 对象获得的 java.sql.Statement 对象执行 CRUD 操作。将用于这些操作的数据库表具有以下格式:
RECIPES (
id int not null,
recipe_number varchar(10) not null,
recipe_name varchar(100) not null,
description varchar(500),
text clob,
constraint recipes_pk primary key (id) enable
);
以下代码摘录演示了如何使用 JDBC 执行每个 CRUD 操作:
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import org.java9recipes.chapter13.recipe13_01.CreateConnection;
public class CrudOperations {
static CreateConnection createConn;
public static void main(String[] args) {
createConn = new CreateConnection();
performCreate();
performRead();
performUpdate();
performDelete();
System.out.println("-- Final State --");
performRead();
}
private static void performCreate(){
String sql = "INSERT INTO RECIPES VALUES(" +
"next value for recipes_seq, " +
"'13-4', " +
"'Performing CRUD Operations', " +
"'How to perform create, read, update, delete functions', " +
"'Recipe Text')";
try (Connection conn = createConn.getConnection();
Statement stmt = conn.createStatement();) {
// Returns row-count or 0 if not successful
int result = stmt.executeUpdate(sql);
if (result == 1{
System.out.println("-- Record created --");
} else {
System.err.println("!! Record NOT Created !!");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
private static void performRead(){
String qry = "select recipe_number, recipe_name, description from recipes";
try (Connection conn = createConn.getConnection();
Statement stmt = conn.createStatement();) {
ResultSet rs = stmt.executeQuery(qry);
while (rs.next()) {
String recipe = rs.getString("RECIPE_NUMBER");
String name = rs.getString("RECIPE_NAME");
String desc = rs.getString("DESCRIPTION");
System.out.println(recipe + "\t" + name + "\t" + desc);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
private static void performUpdate(){
String sql = "UPDATE RECIPES " +
"SET RECIPE_NUMBER = '13-5' " +
"WHERE RECIPE_NUMBER = '13-4'";
try (Connection conn = createConn.getConnection();
Statement stmt = conn.createStatement();) {
int result = stmt.executeUpdate(sql);
if (result > 0){
System.out.println("-- Record Updated --");
} else {
System.out.println("!! Record NOT Updated !!");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
private static void performDelete(){
String sql = "DELETE FROM RECIPES WHERE RECIPE_NUMBER = '13-5'";
try (Connection conn = createConn.getConnection();
Statement stmt = conn.createStatement();) {
int result = stmt.executeUpdate(sql);
if (result > 0){
System.out.println("-- Record Deleted --");
} else {
System.out.println("!! Record NOT Deleted!!");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
下面是运行代码的结果:
Successfully connected
-- Record created --
13-1 Connecting to a Database―DriverManager and DataSource Implementations
13-2 Querying a Database and Retrieving Results Obtaining and Using Data from a DBMS
13-3 Handling SQL Exceptions Using SQLException
13-4 Performing CRUD Operations How to Perform Create, Read, Update, Delete Functions
-- Record Updated --
-- Record Deleted --
-- Final State --
13-1 Connecting to a Database DriverManager and DataSource Implementations
13-2 Querying a Database and Retrieving Results Obtaining and Using Data from a DBMS
13-3 Handling SQL Exceptions Using SQLException
它是如何工作的
几乎每个数据库任务都使用相同的基本代码格式。格式如下:
-
获取到数据库的连接。
-
根据连接创建一个语句。
-
使用语句执行数据库任务。
-
对数据库任务的结果做一些事情。
-
关闭语句(如果使用完了,还要关闭数据库连接)。
使用 JDBC 执行查询和使用数据操作语言(DML)执行查询的主要区别在于,根据要执行的操作,您将对语句对象调用不同的方法。要执行查询,需要调用语句 executeQuery()方法。为了执行插入、更新和删除等 DML 任务,请调用 executeUpdate()方法。
这个配方的解决方案中的 performCreate()方法演示了将记录插入数据库的操作。要在数据库中插入记录,请构造一个字符串格式的 SQL INSERT 语句。要执行插入,请将 SQL 字符串传递给语句对象的 executeUpdate()方法。如果执行插入,将返回一个 int 值,指定已插入的行数。如果插入操作未成功执行,将返回零或引发 SQLException,表明语句或数据库连接有问题。
这个配方的解决方案中的 performRead()方法演示了查询数据库的操作。要执行查询,请调用语句对象的 executeQuery()方法,以字符串格式传递 SQL 语句。结果将是一个 ResultSet 对象,然后可以用它来处理返回的数据。有关执行查询的更多信息,请参见配方 13-3。
这个配方的解决方案中的 performUpdate()方法演示了在数据库表中更新记录的操作。首先,构造一个字符串格式的 SQL UPDATE 语句。接下来,为了执行更新操作,将 SQL 字符串传递给语句对象的 executeUpdate()方法。如果更新成功执行,将返回一个 int 值,该值指定更新的记录数。如果更新操作没有成功执行,将返回零或引发 SQLException,表明语句或数据库连接有问题。
需要介绍的最后一个数据库操作是删除操作。这个配方的解决方案中的 performDelete()方法演示了从数据库中删除记录的操作。首先,构造一个字符串格式的 SQL DELETE 语句。接下来,为了执行删除,将 SQL 字符串传递给语句对象的 executeUpdate()方法。如果删除成功,将返回一个指定删除行数的 int 值。否则,如果删除失败,将返回零或引发 SQLException,表明语句或数据库连接有问题。
几乎每个数据库应用都会在某个时候使用至少一个 CRUD 操作。如果您在 Java 应用中使用数据库,这是需要知道的基本 JDBC。即使您不会直接使用 JDBC API,了解这些基础知识也是有好处的。
13-5.简化连接管理
问题
您的应用需要使用数据库,为了使用数据库,您需要为每个交互打开一个连接。您不需要在每次需要访问数据库时编写逻辑来打开数据库连接,而是希望使用单个类来执行该任务。
解决办法
编写一个类来处理应用中的所有连接管理。这样做将允许您调用该类来获得连接,而不是在每次需要访问数据库时设置一个新的连接对象。执行以下步骤为您的 JDBC 应用设置连接管理环境:
-
创建一个名为 CreateConnection.java 的类,它将封装应用的所有连接逻辑。
-
创建一个属性文件来存储连接信息。将该文件放在类路径中的某个位置,以便 CreateConnection 类可以加载它。
-
使用 CreateConnection 类获取数据库连接。
以下代码列出了可用于集中式连接管理的 CreateConnection 类:
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
public class CreateConnection {
static Properties props = new Properties();
String hostname = null;
String port = null;
String database = null;
String username = null;
String password = null;
String driver = null;
String jndi = null;
public CreateConnection() {
// Looks for properties file in the root of the src directory in Netbeans project
try (InputStream in = Files.newInputStream(FileSystems.getDefault().
getPath(System.getProperty("user.dir") + File.separator + "db_props.properties"));) {
props.load(in);
in.close();
} catch (IOException ex) {
ex.printStackTrace();
}
loadProperties();
}
public final void loadProperties() {
hostname = props.getProperty("host_name");
port = props.getProperty("port_number");
database = props.getProperty("db_name");
username = props.getProperty("username");
password = props.getProperty("password");
driver = props.getProperty("driver");
jndi = props.getProperty("jndi");
}
/**
* Demonstrates obtaining a connection via DriverManager
*
* @return
* @throws SQLException
*/
public Connection getConnection() throws SQLException {
Connection conn = null;
String jdbcUrl;
if (driver.equals("derby")) {
jdbcUrl = "jdbc:derby://" + this.hostname + ":"
+ this.port + "/" + this.database;
} else {
jdbcUrl = "jdbc:oracle:thin:@" + this.hostname + ":"
+ this.port + ":" + this.database;
}
conn = DriverManager.getConnection(jdbcUrl, username, password);
System.out.println("Successfully connected");
return conn;
}
/**
* Demonstrates obtaining a connection via a DataSource object
*
* @return
*/
public Connection getDSConnection() {
Connection conn = null;
try {
Context ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup(this.jndi);
conn = ds.getConnection();
} catch (NamingException | SQLException ex) {
ex.printStackTrace();
}
return conn;
}
}
接下来,下面几行文本是属性文件中应该包含的内容的示例,该属性文件用于获取到数据库的连接。对于本例,属性文件被命名为 db_props.properties:
host_name=your_db_server_name
db_name=your_db_name
username=db_username
password=db_username_password
port_number=db_port_number
#driver = derby or oracle
driver=db_driver
jndi=jndi_connection_String
最后,使用 CreateConnection 类获取应用的连接。下面的代码演示了这个概念:
CreateConnection createConn = new CreateConnection();
try(Connection conn = createConn.getConnection()) {
performDbTask();
} catch (java.sql.SQLException ex) {
ex.printStackTrace();
}
这段代码使用 try-with-resources 在完成数据库任务后自动关闭连接。
它是如何工作的
在数据库应用中获取连接可能需要大量代码。此外,如果每次需要获得连接时都要重新键入代码,这个过程很容易出错。通过将数据库连接逻辑封装在单个类中,您可以在每次需要连接到数据库时重用相同的连接代码。这提高了您的工作效率,减少了输入错误的机会,也增强了可管理性,因为如果您必须进行更改,它可以在一个地方而不是在几个不同的位置发生。
创建一个战略连接方法对你和其他将来可能需要维护你的代码的人是有益的。虽然在使用应用服务器或 JNDI 时,数据源是管理数据库连接的首选技术,但是这个方法的解决方案演示了使用标准的 JDBC 驱动程序管理器连接。使用 DriverManager 的一个安全问题是,您需要将数据库凭证存储在某个地方,供应用使用。将这些凭证以纯文本的形式存储在任何地方都是不安全的,将它们嵌入到应用代码中也是不安全的,因为应用代码将来可能会被反编译。如解决方案所示,磁盘上的属性文件用于存储数据库凭证。假设这个属性文件在部署到服务器之前会被加密,并且应用能够处理解密。
如解决方案所示,代码从属性文件中读取数据库凭证、主机名、数据库名和端口号。然后将这些信息拼凑起来形成一个 JDBC URL,DriverManager 可以使用它来获得到数据库的连接。一旦获得,该连接可以在任何地方使用,然后关闭。类似地,如果使用已经部署到应用服务器的数据源,属性文件可以用来存储 JNDI 连接。这是使用数据源连接到数据库所需的唯一信息。对于使用 connection 类的开发人员来说,这两种类型的连接之间的唯一区别是为了获得 Connection 对象而调用的方法名。
您可以开发一个 JDBC 应用,这样用于获得连接的代码就需要从头到尾都是硬编码的。相反,这种解决方案使获取连接的所有代码都被一个类封装起来,这样开发人员就不需要担心了。这种技术还允许代码变得更易于维护。例如,如果应用最初是使用 DriverManager 部署的,但是后来有了使用数据源的能力,那么只需要修改很少的代码。
13-6.防范 SQL 注入
问题
您的应用执行数据库任务。为了减少 SQL 注入攻击的机会,您需要确保没有未经过滤的文本字符串被附加到 SQL 语句中并针对数据库执行。
小费
尽管准备好的语句是解决这一问题的方法,但它们不仅仅可以用来防范 SQL 注入病毒。它们还提供了集中和更好地控制应用中使用的 SQL 的方法。例如,您可以将查询创建一次,作为一个准备好的语句,然后从代码中的许多不同位置调用它,而不是创建同一个查询的多个可能不同的版本。对查询逻辑的任何更改只需要在准备语句时进行。
解决办法
使用 PreparedStatements 执行数据库任务。PreparedStatements 将预编译的 SQL 语句而不是字符串发送到 DBMS。以下代码演示如何使用 java.sql.PreparedStatement 对象执行数据库查询和数据库更新。
在下面的代码示例中,PreparedStatement 用于查询数据库中的给定记录。假设配方编号的字符串[]作为一个变量被传递给这个代码。
private static void queryDbRecipe(String[] recipeNumbers) {
String sql = "SELECT ID, RECIPE_NUMBER, RECIPE_NAME, DESCRIPTION "
+ "FROM RECIPES "
+ "WHERE RECIPE_NUMBER = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
for (String recipeNumber : recipeNumbers) {
pstmt.setString(1, recipeNumber);
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
System.out.println(rs.getString(2) + ": " + rs.getString(3)
+ " - " + rs.getString(4));
}
}
} catch (SQLException ex) {
ex.printStackTrace();
}
}
下一个示例演示了如何使用 PreparedStatement 将记录插入数据库。假设 recipeNumber、title、description 和 text 字符串作为变量传递给了这段代码。
String sql = "INSERT INTO RECIPES VALUES(" +
"NEXT VALUE FOR RECIPES_SEQ, ?,?,?,?)";
try(PreparedStatement pstmt = conn.prepareStatement(sql);) {
pstmt.setString(1, recipeNumber);
pstmt.setString(2, title);
pstmt.setString(3, description);
pstmt.setString(4, text);
pstmt.executeUpdate();
System.out.println("Record successfully inserted.");
} catch (SQLException ex){
ex.printStackTrace();
}
在最后一个示例中,PreparedStatement 用于从数据库中删除记录。同样,假设 recipeNumber 字符串作为变量传递给这段代码。
String sql = "DELETE FROM RECIPES WHERE " +
"RECIPE_NUMBER = ?";
try(PreparedStatement pstmt = conn.prepareStatement(sql);) {
pstmt.setString(1, recipeNumber);
pstmt.executeUpdate();
System.out.println("Recipe " + recipeNumber + " successfully deleted.");
} catch (SQLException ex){
ex.printStackTrace();
}
如您所见,PreparedStatement 与标准的 JDBC 语句对象非常相似,但它将预编译的 SQL 而不是文本字符串发送到 DBMS。
它是如何工作的
虽然标准的 JDBC 语句可以完成工作,但残酷的现实是,它们有时不安全,使用起来很麻烦。例如,如果使用动态 SQL 语句来查询数据库,并且将用户接受的字符串赋给变量并与预期的 SQL 字符串连接,就会发生不好的事情。在大多数情况下,用户接受的字符串将被连接起来,SQL 字符串将按预期用于查询数据库。然而,攻击者可以决定将恶意代码放入字符串中(也称为 SQL 注入),然后使用标准语句对象将恶意代码无意中发送到数据库。使用 PreparedStatements 可以防止此类恶意字符串连接成 SQL 字符串并传递到 DBMS,因为它们使用不同的方法。PreparedStatements 使用替代变量而不是串联来使 SQL 字符串动态化。它们也是预编译的,这意味着在 SQL 被发送到 DBMS 之前就形成了有效的 SQL 字符串。此外,PreparedStatements 可以帮助您的应用更好地执行,因为如果同一个 SQL 必须运行多次,它只需编译一次。之后,替代变量是可互换的,但是整个 SQL 可以由 PreparedStatement 非常快速地执行。
让我们看看 PreparedStatement 在实践中是如何工作的。如果您查看这个配方的解决方案中的第一个示例,您可以看到数据库表 RECIPES 正在被查询,传递一个 RECIPE_NUMBER 并检索匹配记录的结果。SQL 字符串如下所示:
String sql = "SELECT ID, RECIPE_NUMBER, RECIPE_NAME, DESCRIPTION " +
"FROM RECIPES " +
"WHERE RECIPE_NUM = ?";
除了问号(?)在字符串的末尾。在 SQL 字符串中放置一个问号表示在执行 SQL 时将使用一个替代变量来代替这个问号。使用 PreparedStatement 的下一步是声明 PreparedStatement 类型的变量。这可以从下面一行代码中看出:
PreparedStatement pstmt = null;
PreparedStatement 实现 AutoCloseable,因此可以在 try-with-resources 块的上下文中使用它。一旦声明了 PreparedStatement,就可以使用它了。但是,使用 PreparedStatement 可能不会导致引发异常。因此,在不使用 try-with-resources 的情况下,应该在 try-catch 块中出现 PreparedStatement,以便可以优雅地处理任何异常。例如,如果数据库连接由于某种原因不可用,或者 SQL 字符串无效,就会出现异常。最好在 catch 块中明智地处理异常,而不是因为这些问题而导致应用崩溃。下面的 try-catch 块包含将 SQL 字符串发送到数据库并检索结果所需的代码:
try(PreparedStatement pstmt = conn.prepareStatement(sql);) {
pstmt.setString(1, recipeNumber);
ResultSet rs = pstmt.executeQuery();
while(rs.next()){
System.out.println(rs.getString(2) + ": " + rs.getString(3) +
" - " + rs.getString(4));
}
} catch (SQLException ex) {
ex.printStackTrace();
}
首先,可以看到 Connection 对象用于实例化一个 PreparedStatement 对象。SQL 字符串在创建时被传递给 PreparedStatement 对象的构造函数。由于 PreparedStatement 是在 try-with-resources 构造中实例化的,因此当它不再使用时将自动关闭。接下来,PreparedStatement 对象用于为已经放入 SQL 字符串中的任何替代变量设置值。如您所见,示例中使用了 PreparedStatement setString()方法将位置 1 处的替换变量设置为 recipeNumber 变量的内容。替代变量的位置与问号(?)放在 SQL 字符串中。字符串中的第一个问号被分配给第一个位置,第二个问号被分配给第二个位置,依此类推。如果要分配多个替代变量,将会有多个针对 PreparedStatement 的调用,分配每个变量,直到每个变量都被考虑在内。PreparedStatements 可以接受许多不同数据类型的替代变量。例如,如果一个 int 值被赋给一个替代变量,调用 setInt(position,variable)方法是合适的。有关可用于使用 PreparedStatement 对象分配替代变量的完整方法集,请参见联机文档或 IDE 的代码完成。
一旦所有变量都被赋值,就可以执行 SQL 字符串了。PreparedStatement 对象包含一个 executeQuery()方法,该方法用于执行表示查询的 SQL 字符串。executeQuery()方法返回一个 ResultSet 对象,该对象包含为特定 SQL 查询从数据库中获取的结果。接下来,可以遍历 ResultSet 以获取从数据库中检索的值。同样,通过调用 ResultSet 对象的相应 getter 方法并传递您想要获取的列值的位置,位置赋值用于检索结果。位置由列名在 SQL 字符串中出现的顺序决定。在该示例中,第一个位置对应于 RECIPE_NUMBER 列,第二个位置对应于 RECIPE_NAME 列,依此类推。如果 recipeNumber 字符串变量等于“13-1”,则在示例中执行查询的结果将如下所示:
13-1: Connecting to a Database - DriverManager and DataSource Implementations
当然,如果替代变量设置不正确或者 SQL 字符串有问题,就会抛出异常。这将导致包含在 catch 块中的代码被执行。您还应该确保在使用 PreparedStatements 后进行清理,方法是在使用完语句后关闭该语句。如果没有使用 try-with-resources 构造,最好将所有清理代码放在 finally 块中,以确保即使抛出异常,PreparedStatement 也能正确关闭。在该示例中,finally 块如下所示:
finally {
if (pstmt != null){
try {
pstmt.close();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
}
可以看到,已实例化的 PreparedStatement 对象 pstmt 被检查是否为 NULL。如果没有,则通过调用 close()方法关闭它。
通过研究这个配方的解决方案中的代码,您可以看到类似的代码用于处理数据库插入、更新和删除语句。这两种情况的唯一区别是调用了 PreparedStatement executeUpdate()方法,而不是 executeQuery()方法。executeUpdate()方法将返回一个 int 值,表示受 SQL 语句影响的行数。
PreparedStatement 对象的使用优于 JDBC 语句对象。这是因为它们更安全,性能更好。它们还可以使您的代码更容易遵循和维护。
13-7.执行交易
问题
构建应用的方式需要任务的顺序处理。一个任务依赖于另一个任务,每个进程执行不同的数据库操作。如果其中一个任务失败,已经发生的数据库处理需要被逆转。
解决办法
将连接对象自动提交设置为 false,然后执行要完成的事务。一旦成功地执行了每个事务,手动提交连接对象;否则,回滚已发生的每个事务。下面的代码示例演示事务管理。如果您查看 TransactionExample 类的 main()方法,您将看到 Connection 对象的 autoCommit()首选项已被设置为 false,因此数据库语句被组合在一起形成一个事务。如果事务内的所有语句都成功,则通过调用 commit()方法手动提交连接对象;否则,通过调用 rollback()方法回滚所有语句。默认情况下,autoCommit 设置为 true,这将自动将每个语句视为单个事务。
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.java9recipes.chapter13.recipe13_01.CreateConnection;
public class TransactionExample {
public static Connection conn = null;
public static void main(String[] args) {
boolean successFlag = false;
try {
CreateConnection createConn = new CreateConnection();
conn = createConn.getConnection();
conn.setAutoCommit(false);
queryDbRecipes();
successFlag = insertRecord(
"13-6",
"Simplifying and Adding Security with Prepared Statements",
"Working with Prepared Statements",
"Recipe Text");
if (successFlag == true){
successFlag = insertRecord(
"13-6B",
"Simplifying and Adding Security with Prepared Statements",
"Working with Prepared Statements",
"Recipe Text");
}
// Commit Transactions
if (successFlag == true)
conn.commit();
else
conn.rollback();
conn.setAutoCommit(true);
queryDbRecipes();
} catch (java.sql.SQLException ex) {
System.out.println(ex);
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
}
}
private static void queryDbRecipes(){
String sql = "SELECT ID, RECIPE_NUMBER, RECIPE_NAME, DESCRIPTION " +
"FROM RECIPES";
try(PreparedStatement pstmt = conn.prepareStatement(sql);) {
ResultSet rs = pstmt.executeQuery();
while(rs.next()){
System.out.println(rs.getString(2) + ": " + rs.getString(3) +
" - " + rs.getString(4));
}
} catch (SQLException ex) {
ex.printStackTrace();
}
}
private static boolean insertRecord(String recipeNumber,
String title,
String description,
String text){
String sql = "INSERT INTO RECIPES VALUES(" +
"NEXT VALUE FOR RECIPES_SEQ, ?,?,?,?)";
boolean success = false;
try(PreparedStatement pstmt = conn.prepareStatement(sql);) {
pstmt.setString(1, recipeNumber);
pstmt.setString(2, title);
pstmt.setString(3, description);
pstmt.setString(4, text);
pstmt.executeUpdate();
System.out.println("Record successfully inserted.");
success = true;
} catch (SQLException ex){
success = false;
ex.printStackTrace();
}
return success;
}
}
最后,如果任何语句失败,所有事务都将回滚。但是,如果所有语句都正确执行,所有内容都将被提交。
它是如何工作的
事务管理在应用中扮演着重要的角色。对于执行相互依赖的不同任务的应用来说尤其如此。在许多情况下,如果在一个事务中执行的任务之一失败,那么整个事务失败要比只完成一部分要好。例如,假设您正在向应用数据库添加数据库用户记录。现在,假设为您的应用添加一个用户需要修改几个不同的数据库表,可能是一个角色表,等等。如果第一个表修改正确,第二个表修改失败,会发生什么?您将得到一个部分完成的应用用户添加,并且您的用户很可能无法像预期的那样访问应用。在这种情况下,如果其中一个更新失败,最好回滚所有已经完成的数据库修改,这样数据库就处于干净的状态,可以再次尝试事务。
默认情况下,会设置一个连接对象,以便打开自动提交。这意味着每个数据库插入、更新或删除语句都会被立即提交。通常,这是您希望应用运行的方式。但是,在您有许多相互依赖的数据库语句的情况下,关闭自动提交以便可以一次提交所有语句是很重要的。为此,调用连接对象的 setAutoCommit()方法并传递一个 false 值。正如您在这个配方的解决方案中所看到的,setAutoCommit()方法被称为传递假值,数据库语句被执行。这样做将导致所有数据库语句更改都是临时的,直到调用连接对象的 commit()方法。这为您提供了在发出 commit()之前确保所有语句正确执行的能力。看一下这个配方的解决方案中 TransactionExample 类的 main()方法中包含的事务管理代码:
boolean successFlag = false;
...
CreateConnection createConn = new CreateConnection();
conn = createConn.getConnection();
conn.setAutoCommit(false);
queryDbRecipes();
successFlag = insertRecord(
"13-6",
"Simplifying and Adding Security with Prepared Statements",
"Working with Prepared Statements",
"Recipe Text");
if (successFlag == true){
successFlag = insertRecord(
null,
"Simplifying and Adding Security with Prepared Statements",
"Working with Prepared Statements",
"Recipe Text");
}
// Commit Transactions
if (successFlag == true)
conn.commit();
else
conn.rollback();
conn.setAutoCommit(true);
请注意,只有在成功处理了所有事务语句的情况下,才会调用 commit()方法。如果其中任何一个失败,successFlag 等于 false,这将导致调用 rollback()方法。在这个配方的解决方案中,对 insertRecord()的第二次调用试图向配方中插入一个空值。ID 列,这是不允许的。因此,该插入会失败,所有内容(包括前一次插入)都会回滚。
13-8.创建可滚动的结果集
问题
您已经查询了数据库并获得了一些结果。您希望将这些结果存储在一个对象中,该对象将允许您在结果中向前和向后遍历,并根据需要更新值。
解决办法
创建一个可滚动的 ResultSet 对象,然后您将能够读取下一条、第一条记录、最后一条和上一条记录。使用可滚动的 ResultSet 允许从任何方向获取查询结果,以便可以根据需要检索数据。下面的示例方法演示了如何创建可滚动的 ResultSet 对象:
private static void queryDbRecipes(){
String sql = "SELECT ID, RECIPE_NUMBER, RECIPE_NAME, DESCRIPTION " +
"FROM RECIPES";
try(PreparedStatement pstmt =conn.prepareStatement(sql,
ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
ResultSet rs = pstmt.executeQuery()) {
rs.first();
System.out.println(rs.getString(2) + ": " + rs.getString(3) +
" - " + rs.getString(4));
rs.next();
System.out.println(rs.getString(2) + ": " + rs.getString(3) +
" - " + rs.getString(4));
rs.previous();
System.out.println(rs.getString(2) + ": " + rs.getString(3) +
" - " + rs.getString(4));
rs.last();
System.out.println(rs.getString(2) + ": " + rs.getString(3) +
" - " + rs.getString(4));
} catch (SQLException ex) {
ex.printStackTrace();
}
}
使用最初为本章加载的数据,执行此方法将产生以下输出:
Successfully connected
13-1: Connecting to a Database - DriverManager and DataSource Implementations - More to Come
13-2: Querying a Database and Retrieving Results - Obtaining and Using Data from a DBMS
13-1: Connecting to a Database - DriverManager and DataSource Implementations - More to Come
13-3: Handling SQL Exceptions - Using SQLException
它是如何工作的
普通的 ResultSet 对象允许向前提取结果。也就是说,应用可以从检索到的第一条记录到最后一条记录处理默认的 ResultSet 对象。有时候,在遍历结果集时,应用需要更多的功能。例如,假设您想编写一个应用,允许某人显示检索到的第一条或最后一条记录,或者在结果中向前或向后翻页。使用一个标准的结果集,你不可能很容易地做到这一点。但是,通过创建一个可滚动的结果集,您可以轻松地在结果中前后移动。
若要创建可滚动的结果集,必须首先创建能够创建可滚动结果集的语句或 PreparedStatement 的实例。也就是说,在创建语句时,必须将 ResultSet 滚动类型常量值传递给 Connection 对象的 createStatement()方法。同样,在使用 PreparedStatement 时,必须将滚动类型常量值传递给连接对象的 prepareStatement()方法。有三种滚动类型常量可供使用。表 13-1 显示了这三个常数。
表 13-1。结果集滚动类型常量
|常数
|
描述
| | --- | --- | | 结果集。仅转发类型 | 默认类型,仅允许向前移动。 | | 结果集。TYPE _ SCROLL _ 不敏感 | 允许向前和向后移动。对结果集更新不敏感。 | | 结果集。类型 _ 滚动 _ 敏感 | 允许向前和向后移动。对结果集更新敏感。 |
您还必须传递一个 ResultSet 并发常量,以告知 ResultSet 是否是可更新的。默认值为 ResultSet。CONCUR_READ_ONLY,这意味着结果集不可更新。另一种并发类型是 ResultSet。CONCUR_UPDATABLE,表示可更新的结果集对象。
在该配方的解决方案中,使用了一个 PreparedStatement 对象,创建一个能够生成可滚动结果集的 PreparedStatement 对象的代码如下所示:
pstmt = conn.prepareStatement(sql, ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY);
这样创建 PreparedStatement 后,将返回一个可滚动的 ResultSet。您可以使用可滚动的 ResultSet 在多个方向上遍历,方法是调用 ResultSet 方法来指示您想要移动的方向或想要的位置。以下代码行将检索结果集中的第一条记录:
ResultSet rs = pstmt.executeQuery();
rs.first();
这个配方的解决方案演示了几种不同的滚动方向。具体来说,您可以看到调用 ResultSet first()、next()、last()和 previous()方法是为了移动到 ResultSet 中的不同位置。有关 ResultSet 对象的完整参考,请参见位于docs . Oracle . com/javase/8/docs/API/Java/SQL/ResultSet . html的在线文档。
可滚动的 ResultSet 对象在应用开发中有一席之地。当你需要它们的时候,它们是那些美好事物中的一种,但它们也是你可能不经常需要的东西。
13-9.创建可更新的结果集
问题
一个应用任务查询了数据库并获得了结果。您已经将这些结果存储到一个 ResultSet 对象中,并且希望更新 ResultSet 中的一些值,并将它们提交回数据库。
解决办法
使 ResultSet 对象可更新,然后在迭代结果时根据需要更新行。以下示例方法演示了如何使结果集可更新,然后如何更新该结果集中的内容,最终将其保存在数据库中:
private static void queryAndUpdateDbRecipes(String recipeNumber){
String sql = "SELECT ID, RECIPE_NUMBER, RECIPE_NAME, DESCRIPTION " +
"FROM RECIPES " +
"WHERE RECIPE_NUMBER = ?";
ResultSet rs = null;
try (PreparedStatement pstmt =
conn.prepareStatement(sql, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);){
pstmt.setString(1, recipeNumber);
rs = pstmt.executeQuery();
while(rs.next()){
String desc = rs.getString(4);
System.out.println("Updating row" + desc);
rs.updateString(4, desc + " -- More to come");
rs.updateRow();
}
} catch (SQLException ex) {
ex.printStackTrace();
} finally {
if (rs != null){
try {
rs.close();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
}
这个方法可以称为传递包含配方号的字符串值。假设配方号“13-1”被传递给了这个方法;结果将是以下输出:
Successfully connected
13-1: Connecting to a Database - DriverManager and DataSource Implementations
13-2: Querying a Database and Retrieving Results - Obtaining and Using Data from a DBMS
13-3: Handling SQL Exceptions - Using SQLException
Updating rowDriverManager and DataSource Implementations
13-1: Connecting to a Database - DriverManager and DataSource Implementations - More to come
13-2: Querying a Database and Retrieving Results - Obtaining and Using Data from a DBMS
13-3: Handling SQL Exceptions - Using SQLException
它是如何工作的
有时您需要在解析数据时更新数据。通常这种技术包括测试从数据库返回的值,并在与另一个值比较后更新它们。最简单的方法是通过传递 ResultSet 使 ResultSet 对象可更新。连接对象的 createStatement()或 prepareStatement()方法的 CONCUR_UPDATABLE 常量。这样做会导致语句或 PreparedStatement 生成可更新的结果集。
注意
一些数据库 JDBC 驱动程序不支持可更新的结果集。有关详细信息,请参阅您的 JDBC 驱动程序文档。这段代码是在 Oracle 数据库 11.2 版上使用 Oracle 的 ojdbc6.jar JDBC 驱动程序运行的。
创建将产生可更新结果集的语句的格式是将结果集类型作为第一个参数传递,将结果集并发性作为第二个参数传递。滚动类型必须是 TYPE_SCROLL_SENSITIVE,以确保结果集对所做的任何更新敏感。下面的代码通过创建一个语句对象来演示这种技术,该对象将产生一个可滚动和可更新的 ResultSet 对象:
Statement stmt = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,
ResultSet.CONCUR_UPDATABLE);
创建将生成可更新结果集的 PreparedStatement 的格式是将 SQL 字符串作为第一个参数传递,将结果集类型作为第二个参数传递,将结果集并发性作为第三个参数传递。该配方的解决方案使用以下代码行演示了这种技术:
pstmt = conn.prepareStatement(sql, ResultSet.TYPE_SCROLL_SENSITIVE,
ResultSet.CONCUR_UPDATABLE);
本节中讨论的两行代码都将产生可滚动和可更新的 ResultSet 对象。一旦获得了可更新的 ResultSet,就可以像使用普通的 ResultSet 一样获取从数据库中检索到的值。此外,您可以调用 ResultSet 对象的 updateXXX()方法之一来更新 ResultSet 中的任何值。在这个配方的解决方案中,调用了 updateString()方法,将查询值的位置作为第一个参数传递,将更新后的文本作为第二个参数传递。在这种情况下,SQL 查询中列出的第四个元素列将被更新。
rs.updateString(4, desc + " -- More to come");
最后,要持久化您已经更改的值,调用 ResultSet updateRow()方法,如这个配方的解决方案所示:
rs.updateRow();
创建可更新的结果集并不是您每天都需要做的事情。事实上,您可能永远不需要创建可更新的结果集。然而,对于需要这种策略的情况,这种技术会非常方便。
13-10.缓存数据以便在断开连接时使用
问题
当处于断开状态时,您希望使用 DBMS 中的数据。也就是说,您正在一台没有连接到数据库的设备上工作,并且您仍然希望能够像连接到数据库一样处理一组数据。例如,您正在便携式设备上处理数据,并且您不在办公室,没有连接。您希望能够查询、插入、更新和删除数据,即使没有连接。一旦连接可用,您希望让您的设备同步断开连接时所做的任何数据库更改。
解决办法
使用 CachedRowSet 对象存储要在脱机时使用的数据。这将使您的应用能够像连接到数据库一样处理数据。连接恢复或连接回数据库后,将 CachedRowSet 中已更改的数据与数据库存储库同步。下面的示例类演示 CachedRowSet 的用法。在这种情况下,main()方法执行示例。但是,假设没有 main()方法,便携设备上的另一个应用将调用该类的方法。遵循示例中的代码,并考虑在未连接到数据库的情况下使用存储在 CachedRowSet 中的结果的可能性。例如,假设您在连接到网络的情况下开始在办公室工作,而现在在办公室之外,网络不稳定,您无法保持与数据库的持续连接:
package org.java9recipes.chapter13.recipe13_10;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.rowset.CachedRowSet;
import javax.sql.rowset.RowSetFactory;
import javax.sql.rowset.RowSetProvider;
import javax.sql.rowset.spi.SyncProviderException;
import org.java9recipes.chapter13.recipe13_01.CreateConnection;
public class CachedRowSetExample {
public static Connection conn = null;
public static CreateConnection createConn;
public static CachedRowSet crs = null;
public static void main(String[] args) {
boolean successFlag = false;
try {
createConn = new CreateConnection();
conn = createConn.getConnection();
// Perform Scrollable Query
queryWithRowSet();
// Update the CachedRowSet
updateData();
// Synchronize changes
syncWithDatabase();
} catch (java.sql.SQLException ex) {
System.out.println(ex);
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
}
}
/**
* Call this method to synchronize the data that has been used in the
* CachedRowSet with the database
*/
public static void syncWithDatabase() {
try {
crs.acceptChanges(conn);
} catch (SyncProviderException ex) {
// If there is a conflict while synchronizing, this exception
// will be thrown.
ex.printStackTrace();
} finally {
// Clean up resources by closing CachedRowSet
if (crs != null) {
try {
crs.close();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
}
}
public static void queryWithRowSet() {
RowSetFactory factory;
try {
// Create a new RowSetFactory
factory = RowSetProvider.newFactory();
// Create a CachedRowSet object using the factory
crs = factory.createCachedRowSet();
// Alternatively populate the CachedRowSet connection settings
// crs.setUsername(createConn.getUsername());
// crs.setPassword(createConn.getPassword());
// crs.setUrl(createConn.getJdbcUrl());
// Populate a query that will obtain the data that will be used
crs.setCommand("select id, recipe_number, recipe_name, description from recipes");
// Set key columns
int[] keys = {1};
crs.setKeyColumns(keys);
crs.execute(conn);
// You can now work with the object contents in a disconnected state
while (crs.next()) {
System.out.println(crs.getString(2) + ": " + crs.getString(3)
+ " - " + crs.getString(4));
}
} catch (SQLException ex) {
ex.printStackTrace();
}
}
public static boolean updateData() {
boolean returnValue = false;
try {
// Move to the position before the first row in the result set
crs.beforeFirst();
// traverse result set
while (crs.next()) {
// If the recipe_num equals 11-2 then update
if (crs.getString("RECIPE_NUMBER").equals("13-2")) {
System.out.println("updating recipe 13-2");
crs.updateString("description", "Subject to change");
crs.updateRow();
}
}
returnValue = true;
// Move to the position before the first row in the result set
crs.beforeFirst();
// traverse result set to see changes
while (crs.next()) {
System.out.println(crs.getString(2) + ": " + crs.getString(3)
+ " - " + crs.getString(4));
}
} catch (SQLException ex) {
returnValue = false;
ex.printStackTrace();
}
return returnValue;
}
}
运行此示例代码将显示类似于以下代码的输出,尽管文本可能会因数据库中的值而异。请注意,在更新 CachedRowSet 后,配方 13-2 的数据库记录有一个更改的描述。
Successfully connected
13-1: Connecting to a Database - DriverManager and DataSource Implementations - More to Come
13-2: Querying a Database and Retrieving Results - Subject to Change
13-3: Handling SQL Exceptions - Using SQLException
Updating Recipe 13-2
13-1: Connecting to a Database - DriverManager and DataSource Implementations - More to Come
13-2: Querying a Database and Retrieving Results - Obtaining and Using Data from a DBMS
13-3: Handling SQL Exceptions - Using SQLException
它是如何工作的
如果您在移动设备上工作或旅行,不可能一直保持与互联网的连接。现在有一些设备可以让你在旅途中完成大量的工作,即使你没有直接连接到数据库。在这种情况下,像 CachedRowSet 对象这样的解决方案就可以发挥作用了。CachedRowSet 与常规的 ResultSet 对象相同,只是它不必为了保持可用而保持与数据库的连接。可以查询数据库,获取结果,放入 CachedRowSet 对象中;然后在没有连接到数据库的情况下使用它们。如果在任何时候对数据进行了更改,这些更改可以在以后与数据库同步。
有几种方法可以创建 CachedRowSet。这个配方的解决方案使用 RowSetFactory 来实例化 CachedRowSet。但是,您也可以使用 CachedRowSet 默认构造函数来创建新的实例。这样做将类似于下面的代码行:
CachedRowSet crs = new CachedRowSetImpl();
一旦实例化,您需要建立到数据库的连接。还有几种方法可以做到这一点。可以为将要使用的连接设置属性,这个方法的解决方案在注释中演示了这种技术。以下解决方案摘录使用 CachedRowSet 对象的 setUsername()、setPassword()和 setUrl()方法设置连接属性。它们每个都接受一个字符串值,在本例中,该字符串是从 CreateConnection 类获得的:
// Alternatively populate the CachedRowSet connection settings
// crs.setUsername(createConn.getUsername());
// crs.setPassword(createConn.getPassword());
// crs.setUrl(createConn.getJdbcUrl());
建立连接的另一种方法是等待查询执行完毕,并将连接对象传递给 executeQuery()方法。这是在解决这个配方时使用的技术。但是在执行查询之前,必须使用 setCommand()方法设置它,该方法接受一个字符串值。在这种情况下,字符串是您需要执行的 SQL 查询:
crs.setCommand("select id, recipe_number, recipe_name, description from recipes");
接下来,如果 CachedRowSet 将用于更新,则应使用 setKeys()方法记录主键值。该方法接受一个包含键列位置索引的 int 数组。这些键用于标识唯一的列。在这种情况下,查询中列出的第一列 ID 是主键:
int[] keys = {1};
crs.setKeyColumns(keys);
最后,执行查询并使用 execute()方法填充 CachedRowSet。如前所述,execute()方法可选地接受一个连接对象,这允许 CachedRowSet 获得一个数据库连接。
crs.execute(conn);
一旦执行了查询并填充了 CachedRowSet,就可以像使用任何其他结果集一样使用它。您可以使用它向前和向后获取记录,或者通过指定您想要检索的行的绝对位置来获取记录。该配方的解决方案仅演示了其中的几种获取方法,但最常用的方法在表 13-2 中列出。
表 13-2。CachedRowSet 提取方法
|方法
|
描述
| | --- | --- | | 首先() | 移动到集合中的第一行。 | | beforeFirst() | 移动到集合中第一行之前的位置。 | | 最后一次 | 移动到集合中最后一行之后的位置。 | | 下一个() | 移动到集合中的下一个位置。 | | 最后() | 移动到集合的最后一个位置。 |
可以在 CachedRowSet 中插入和更新行。若要插入行,请使用 moveToInsertRow()方法移动到新的行位置。然后使用与您在行中填充的列的数据类型相对应的各种方法[CachedRowSet、updateString()、updateInt()等]来填充行。一旦在行中填充了每个必需的列,就调用 insertRow()方法,然后调用 moveToCurrentRow()方法。以下代码行演示了如何将记录插入到 RECIPES 表中:
crs.moveToInsertRow();
crs.updateInt(1, sequenceValue); // obtain current sequence values with a prior query
crs.updateString(2, “13-x”);
crs.updateString(3, “This is a new recipe title”);
crs.insertRow();
crs.moveToCurrentRow();
更新行类似于使用可更新的结果集。只需使用 CachedRowSet 对象的方法[updateString()、updateInt()等]来更新值,这些方法对应于您在行内要更新的列的数据类型。一旦更新了行中的一列或多列,就调用 updateRow()方法。这种技术在这个配方的解决方案中得到了演示。
crs.updateString("description", "Subject to change");
crs.updateRow();
若要将任何更新或插入传播到数据库,必须调用 acceptChanges()方法。该方法可以接受一个可选的连接参数,以便连接到数据库。一旦被调用,所有的更改都会被刷新到数据库中。不幸的是,由于自上次为 CachedRowSet 检索数据以来可能已经过了一段时间,因此可能会出现冲突。如果出现这种冲突,将引发 SyncProviderException。您可以捕获这些异常,并使用 SyncResolver 对象手动处理冲突。但是,解决冲突超出了本方法的范围,因此要了解更多信息,请参阅在线文档,该文档可以在 http://download . Oracle . com/javase/tutorial/JDBC/basics/cachedrowset . html 找到。
CachedRowSet 对象为处理数据提供了极大的灵活性,尤其是当您使用的设备并不总是连接到数据库时。然而,在您可以简单地使用标准结果集甚至可滚动结果集的情况下,它们也可能是多余的。
13-11.未连接到数据源时联接行集对象
问题
您希望在未连接到数据库的情况下联接两个或多个行集。也许您的应用被加载到一个并不总是连接到数据库的移动设备上。在这种情况下,您正在寻找一个允许您连接两个或更多查询结果的解决方案。
解决办法
使用 JoinRowSet 从两个关系数据库表中获取数据并连接它们。应该将每个要联接的表中的数据提取到一个行集中,然后可以使用 JoinRowSet 根据这些行集中包含的相关元素来联接每个行集对象。例如,假设数据库中有两个相关的表。其中一个表存储作者列表,另一个表包含这些作者撰写的章节列表。这两个表可以使用 SQL 通过主键和外键关系来连接。
注意
主键是数据库表的每个记录中的唯一标识符,外键是两个表之间的引用约束。
但是,应用不会连接到数据库来进行连接查询,因此必须使用 JoinRowSet 来完成。下面的类清单演示了一种可以使用的策略。在这个场景中,数据库表 BOOK_AUTHOR 的设置如下:
BOOK_AUTHOR(
id int primary key,
last varchar(30),
first varchar(30));
author_work(
id int primary key,
author_id int not null,
chapter_number int not null,
chapter_title varchar(100) not null,
constraint author_work_fk
foreign key(author_id) references book_author(id));
book(
id int primary key,
title varchar(150),
image varchar(150),
description clob);
使用该表的 Java 代码如下:
package org.java9recipes.chapter13.recipe13_11;
import com.sun.rowset.JoinRowSetImpl;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.rowset.CachedRowSet;
import javax.sql.rowset.JoinRowSet;
import javax.sql.rowset.RowSetFactory;
import javax.sql.rowset.RowSetProvider;
import org.java9recipes.chapter13.recipe13_01.CreateConnection;
public class JoinRowSetExample {
public static Connection conn = null;
public static CreateConnection createConn;
public static CachedRowSet bookAuthors = null;
public static CachedRowSet authorWork = null;
public static JoinRowSet jrs = null;
public static void main(String[] args) {
boolean successFlag = false;
try {
createConn = new CreateConnection();
conn = createConn.getConnection();
// Perform Scrollable Query
queryBookAuthor();
queryAuthorWork();
joinRowQuery();
} catch (java.sql.SQLException ex) {
System.out.println(ex);
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
if (bookAuthors != null) {
try {
bookAuthors.close();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
if (authorWork != null) {
try {
authorWork.close();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
if (jrs != null) {
try {
jrs.close();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
}
}
public static void queryBookAuthor() {
RowSetFactory factory;
try {
// Create a new RowSetFactory
factory = RowSetProvider.newFactory();
// Create a CachedRowSet object using the factory
bookAuthors = factory.createCachedRowSet();
// Alternatively populate the CachedRowSet connection settings
// crs.setUsername(createConn.getUsername());
// crs.setPassword(createConn.getPassword());
// crs.setUrl(createConn.getJdbcUrl());
// Populate a query that will obtain the data that will be used
bookAuthors.setCommand("SELECT ID, LASTNAME, FIRSTNAME FROM BOOK_AUTHOR");
bookAuthors.execute(conn);
// You can now work with the object contents in a disconnected state
while (bookAuthors.next()) {
System.out.println(bookAuthors.getString(1) + ": " + bookAuthors.getString(2)
+ ", " + bookAuthors.getString(3));
}
} catch (SQLException ex) {
ex.printStackTrace();
}
}
public static void queryAuthorWork() {
RowSetFactory factory;
try {
// Create a new RowSetFactory
factory = RowSetProvider.newFactory();
// Create a CachedRowSet object using the factory
authorWork = factory.createCachedRowSet();
// Alternatively populate the CachedRowSet connection settings
// crs.setUsername(createConn.getUsername());
// crs.setPassword(createConn.getPassword());
// crs.setUrl(createConn.getJdbcUrl());
// Populate a query that will obtain the data that will be used
authorWork.setCommand("SELECT AW.ID, AUTHOR_ID, B.TITLE FROM AUTHOR_WORK AW, " +
"BOOK B " +
"WHERE B.ID = AW.BOOK_ID");
authorWork.execute(conn);
// You can now work with the object contents in a disconnected state
while (authorWork.next()) {
System.out.println(authorWork.getString(1) + ": " + authorWork.getString(2)
+ " - " + authorWork.getString(3));
}
} catch (SQLException ex) {
ex.printStackTrace();
}
}
public static void joinRowQuery() {
try {
// Create JoinRowSet
jrs = new JoinRowSetImpl();
// Add RowSet & Corresponding Keys
jrs.addRowSet(bookAuthors, 1);
jrs.addRowSet(authorWork, 2);
// Alternatively use join-column name
// jrs.addRowSet(authorWork, "AUTHOR_ID");
// Traverse Results
while(jrs.next()){
System.out.println(jrs.getInt("ID") + ": " +
jrs.getString("TITLE") + " - " +
jrs.getString("FIRSTNAME") + " " +
jrs.getString("LASTNAME"));
}
} catch (SQLException ex) {
ex.printStackTrace();
}
}
}
运行该类将产生类似如下的输出:
Successfully connected
100: JUNEAU, JOSH
101: DEA, CARL
102: BEATY, MARK
103: GUIME, FREDDY
104: JOHN, OCONNER
105: TESTER, JOE
110: TESTER, JOE
111: OCONNER, JOHN
1: 100 - Java 8 Recipes
2: 100 - Java 7 Recipes
3: 100 - Java EE 7 Recipes
4: 100 - Introducing Java EE 7
5: 103 - Java 7 Recipes
6: 101 - Java 7 Recipes
7: 111 - Java 7 Recipes
8: 102 - Java 7 Recipes
9: 101 - Java FX 2.0 - Introduction by Example
111: Java 7 Recipes - JOHN OCONNER
103: Java 7 Recipes - FREDDY GUIME
102: Java 7 Recipes - MARK BEATY
101: Java FX 2.0 - Introduction by Example - CARL DEA
101: Java 7 Recipes - CARL DEA
100: Introducing Java EE 7 - JOSH JUNEAU
100: Java EE 7 Recipes - JOSH JUNEAU
100: Java 7 Recipes - JOSH JUNEAU
100: Java 8 Recipes - JOSH JUNEAU
它是如何工作的
JoinRowSet 是两个或多个已填充的行集对象的组合。它可用于根据键/值关系联接两个行集对象,就像 SQL 联接查询一样。为了创建 JoinRowSet,必须首先用相关数据填充两个或多个 RowSet 对象,然后可以将它们分别添加到 JoinRowSet 中以创建组合结果。
在这个菜谱的解决方案中,被查询的表被命名为 BOOK_AUTHOR、BOOK 和 AUTHOR_WORK。BOOK_AUTHOR 表包含作者姓名列表,而 AUTHOR_WORK 表包含书籍列表以及相应的 AUTHOR_ID。图书表包含图书细节。按照 main()方法,首先查询 BOOK_AUTHOR 表,并使用 queryBookAuthor()方法将其结果提取到 CachedRowSet 中。有关使用 CachedRowSet 对象的更多详细信息,请参见配方 13-10。
接下来,调用 queryAuthorBook()方法时,用查询 AUTHOR_WORK 和 BOOK 表的结果填充另一个 CachedRowSet。此时,有两个填充的 CacheRowSet 对象,现在可以使用 JoinRowSet 将它们组合起来。为此,每个查询必须包含一个或多个与另一个表相关的列。在这种情况下,BOOK_AUTHOR。ID 列与 AUTHOR_WORK 相关。AUTHOR_ID 列,因此行集对象必须在这些列值上联接。
main()中调用的最后一个方法是 joinRowQuery()。此方法是所有 JoinRowSet 工作发生的地方。首先,通过实例化 JoinRowSetImpl()对象来创建新的 JoinRowSet:
jrs = new JoinRowSetImpl();
注意
使用 JoinRowSetImpl 时,您将收到一个编译时警告,因为它是一个内部的 SUN 专有 API。但是,Oracle 版本是 OracleJoinRowSet,它没有那么通用。
接下来,通过调用其 addRowSet()方法,将两个 CachedRowSet 对象添加到新创建的 JoinRowSet 中。addRowSet()方法接受两个参数。第一个是要添加到 JoinRowSet 的行集对象的名称,第二个是一个 int 值,指示在 CachedRowSet 中的位置,该值包含将用于实现联接的键值。在这个配方的解决方案中,对 addRowSet()的第一次调用传递 bookAuthors CachedRowSet 和数字 1,因为 bookAuthors CachedRowSet 第一个位置的元素对应于 BOOK_AUTHOR。ID 列。对 addRowSet()的第二次调用传递 authorWork CachedRowSet 和编号 2,因为 authorWork CachedRowSet 第二个位置的元素对应于 AUTHOR_WORK。作者 ID 列。
// Add RowSet & Corresponding Keys
jrs.addRowSet(bookAuthors, 1);
jrs.addRowSet(authorWork, 2);
// Alternatively specify the join-column name
jrs.addRowSet(authorWork, "AUTHOR_ID");
JoinRowSet 现在可以用来获取连接的结果,就像它是一个普通的行集一样。当调用 JoinRowSet 的相应方法[getString()、getInt()等]时,传递与要存储的数据对应的数据库列的名称:
while(jrs.next()){
System.out.println(jrs.getInt("ID") + ": " +
jrs.getString("TITLE") + " - " +
jrs.getString("FIRSTNAME") + " " +
jrs.getString("LASTNAME"));
}
虽然 JoinRowSet 不是每天都需要的,但在对两个相关数据集执行操作时,它会很方便。尤其是当应用没有一直连接到数据库时,或者当您试图使用尽可能少的连接对象时。
13-12.筛选行集中的数据
问题
您的应用查询数据库并返回大量的行。缓存结果集中的行数太大,用户无法一次处理。您希望限制可见的行数,以便可以使用从表中查询的不同数据集执行不同的活动。
解决办法
使用 FilteredRowSet 查询数据库并存储内容。FilteredRowSet 可以配置为筛选查询返回的结果,以便仅显示您想要查看的行。在下面的示例中,创建了一个 filter 类,该类稍后将用于筛选数据库查询返回的结果。示例中的过滤器用于根据作者的姓氏限制可见的行数。下面的类包含过滤器的实现:
package org.java9recipes.chapter13.recipe13_12;
import java.sql.SQLException;
import javax.sql.RowSet;
import javax.sql.rowset.Predicate;
public class AuthorFilter implements Predicate {
private String[] authors;
private String colName = null;
private int colNumber = -1;
public AuthorFilter(String[] authors, String colName) {
this.authors = authors;
this.colNumber = -1;
this.colName = colName;
}
public AuthorFilter(String[] authors, int colNumber) {
this.authors = authors;
this.colNumber = colNumber;
this.colName = null;
}
@Override
public boolean evaluate(Object value, String colName) {
if (colName.equalsIgnoreCase(this.colName)) {
for (String author : this.authors) {
if (author.equalsIgnoreCase((String)value)) {
return true;
}
}
}
return false;
}
@Override
public boolean evaluate(Object value, int colNumber) {
if (colNumber == this.colNumber) {
for (String author : this.authors) {
if (author.equalsIgnoreCase((String)value)) {
return true;
}
}
}
return false;
}
@Override
public boolean evaluate(RowSet rs) {
if (rs == null)
return false;
try {
for (int i = 0; i < this.authors.length; i++) {
String authorLast = null;
if (this.colNumber > 0) {
authorLast = (String)rs.getObject(this.colNumber);
} else if (this.colName != null) {
authorLast = (String)rs.getObject(this.colName);
} else {
return false;
}
if (authorLast.equalsIgnoreCase(authors[i])) {
return true;
}
}
} catch (SQLException e) {
return false;
}
return false;
}
}
FilteredRowSet 使用该筛选器来限制查询的可见结果。正如您将看到的,利用 FilteredRowSet 可以在应用级别以面向对象的方式过滤数据,而不是在 SQL 数据库级别。好处是,您可以实现一系列过滤器,并将它们应用于同一个结果集,返回所需的结果。使用这样的选项消除了执行返回不同数据集的多个数据库查询的需求。
下面的类演示如何实现 FilteredRowSet。main()方法调用一个名为 implement FilteredRowSet()的方法,它包含用于筛选 BOOK_AUTHOR 和 AUTHOR_WORK 表的查询结果的代码,以便只返回来自姓 DEA 和 JUNEAU 的作者的结果:
package org.java9recipes.chapter13.recipe13_12;
import com.sun.rowset.FilteredRowSetImpl;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.RowSet;
import javax.sql.rowset.FilteredRowSet;
import org.java9recipes.chapter13.recipe13_01.CreateConnection;
public class FilteredRowSetExample {
public static Connection conn = null;
public static CreateConnection createConn;
public static FilteredRowSet frs = null;
public static void main(String[] args) {
boolean successFlag = false;
try {
createConn = new CreateConnection();
conn = createConn.getConnection();
implementFilteredRowSet();
} catch (java.sql.SQLException ex) {
System.out.println(ex);
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
if (frs != null) {
try {
frs.close();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
}
}
public static void implementFilteredRowSet() {
String[] authorArray = {"DEA", "JUNEAU"};
AuthorFilter authorFilter = new AuthorFilter(authorArray, 2);
try {
frs = new FilteredRowSetImpl();
frs.setCommand("SELECT TITLE, LASTNAME "
+ "FROM BOOK_AUTHOR BA, "
+ " AUTHOR_WORK AW, "
+ " BOOK B "
+ "WHERE AW.AUTHOR_ID = BA.ID "
+ "AND B.ID = AW.BOOK_ID");
frs.execute(conn);
System.out.println("Prior to adding filter:");
viewRowSet(frs);
System.out.println("Adding author filter:");
frs.beforeFirst();
frs.setFilter(authorFilter);
viewRowSet(frs);
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void viewRowSet(RowSet rs) {
try {
while (rs.next()) {
System.out.println(rs.getString(1) + " - "
+ rs.getString(2));
}
} catch (SQLException ex) {
ex.printStackTrace();
}
}
}
运行这段代码的结果将类似于下面几行。请注意,FilteredRowSet 只返回与筛选器中列出的作者对应的数据行。
Successfully connected
Prior to adding filter:
Java 7 Recipes - JUNEAU
Java 7 Recipes - BEATY
Java 7 Recipes - DEA
Java 7 Recipes - GUIME
Java 7 Recipes - OCONNER
Java EE 7 Recipes - JUNEAU
Java FX 2.0 - Introduction by Example - DEA
Adding author filter:
Java 7 Recipes - JUNEAU
Java 7 Recipes - DEA
Java EE 7 Recipes - JUNEAU
Java FX 2.0 - Introduction by Example – DEA
它是如何工作的
通常,数据库查询返回的结果包含大量的行。正如您可能知道的,太多的行会在可视化处理数据时产生问题。通过在 SQL 语句中使用 WHERE 子句来限制查询返回的行数,以便只返回相关的数据,这通常会有所帮助。但是,如果应用将数据检索到内存中的行集中,然后需要在没有其他数据库请求的情况下根据各种条件筛选数据,则需要使用查询以外的方法。FilteredRowSet 可用于筛选已填充行集中显示的数据,以便更易于管理。
使用 FilteredRowSet 有两个部分。首先,需要创建一个过滤器,用于指定应该如何过滤数据。过滤器类应该实现谓词接口。可能有多个构造函数,每个构造函数接受一组不同的参数,过滤器可能包含多个 evaluate()方法,每个方法接受不同的参数并包含不同的实现。构造函数应该接受可用于筛选行集的内容数组。它们还应该接受第二个参数,或者是筛选器应该针对的列名,或者是筛选器应该针对的列的位置。在这个配方的解决方案中,过滤器类被命名为 AuthorFilter,它用于根据作者姓名数组过滤数据。它的每个构造函数都接受一个数组,该数组包含要过滤的作者姓名,以及列名或位置。每个 evaluate()方法的任务是确定给定的数据行是否与指定的过滤器匹配;在这种情况下,通过数组传递的作者姓名。如果将列名而不是位置传递给过滤器,则调用第一个 evaluate()方法,如果传递了列位置,则调用第二个 evaluate()方法。最后一个 evaluate()方法接受行集本身,它执行遍历数据并返回一个布尔值的工作,以指示相应的列名/位置值是否与筛选数据匹配。
FilteredRowSet 实现的第二部分是 FilteredRowSet 的工作。这可以在 FilteredRowSetExample 类的 implementFilteredRowSet()方法中看到。FilteredRowSet 实际上将使用您编写的 filter 类来确定要显示哪些行。您可以看到,将传递给 filter 类的值数组是该方法中的第一个声明。第二个声明是过滤器类 AuthorFilter 的实例化。当然,过滤器值的数组和对应于过滤器值的列位置被传递到过滤器构造函数中。
String[] authorArray = {"DEA", "JUNEAU"};
// Creates a filter using the array of authors
AuthorFilter authorFilter = new AuthorFilter(authorArray, 2);
若要实例化 FilteredRowSet,请创建 FilteredRowSetImpl 类的新实例。实例化后,只需使用 setCommand()方法设置用于获取结果的 SQL 查询,然后通过调用 executeQuery()方法来执行它。
// Instantiate a new FilteredRowSet
frs = new FilteredRowSetImpl();
// Set the query
frs.setCommand("SELECT TITLE, LASTNAME "
+ "FROM BOOK_AUTHOR BA, "
+ " AUTHOR_WORK AW, "
+ " BOOK B "
+ "WHERE AW.AUTHOR_ID = BA.ID "
+ "AND B.ID = AW.BOOK_ID");
// Execute the query
frs.execute(conn);
注意
使用 FilteredRowSetImpl 时,您将收到一个编译时警告,因为它是 Sun Microsystems 生产的一个较旧的内部专有 API。
请注意,过滤器尚未应用。实际上,此时您拥有的是一个可滚动的行集,其中填充了来自查询的所有结果。该示例在应用筛选器之前显示这些结果。若要应用过滤器,请使用 setFilter()方法,将过滤器作为参数传递。完成后,FilteredResultSet 将只显示那些与筛选器指定的条件相匹配的行。
同样,FilteredRowSet 技术也有其用武之地,尤其是当您使用的应用可能不总是连接到数据库时。它是一个强大的工具,可以用来过滤数据、处理数据,然后应用不同的过滤器并处理新的结果。这类似于在不查询数据库的情况下将 WHERE 子句应用于查询。
13-13.查询和存储大型对象
问题
您正在开发的应用需要存储可以包含无限数量字符的文本字符串。
解决办法
当需要存储的字符串非常大时,最好使用字符大对象(CLOB)数据类型来存储文本。RECIPE_TEXT 表的数据库图表如下:
RECIPE_TEXT (
id int primary key,
recipe_id int not null,
text clob,
constraint recipe_text_fk
foreign key (recipe_id)
references recipes(id))
以下示例中的代码演示了如何将 CLOB 加载到数据库中以及如何查询它:
package org.java9recipes.chapter13.recipe13_13;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.java9recipes.chapter13.recipe13_01.CreateConnection;
public class LobExamples {
public static Connection conn = null;
public static CreateConnection createConn;
public static void main(String[] args) {
boolean successFlag = false;
try {
createConn = new CreateConnection();
conn = createConn.getConnection();
loadClob();
readClob();
} catch (java.sql.SQLException ex) {
System.out.println(ex);
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
}
}
public static void loadClob() {
Clob textClob = null;
String sql = "INSERT INTO RECIPE_TEXT VALUES("
+ "next value for recipe_text_seq, "
+ "(select id from recipes where recipe_number = '13-1'), "
+ "?)";
try (PreparedStatement pstmt = conn.prepareStatement(sql);) {
textClob = conn.createClob();
textClob.setString(1, "This will be the recipe text in clob format");
// obtain the sequence number in real world
// set the clob value
pstmt.setClob(1, textClob);
pstmt.executeUpdate();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
public static void readClob() {
String qry = "select text from recipe_text";
Clob theClob = null;
try(PreparedStatement pstmt = conn.prepareStatement(qry);
ResultSet rs = pstmt.executeQuery();) {
while (rs.next()) {
theClob = rs.getClob(1);
System.out.println("Clob length: " + theClob.length());
System.out.println(theClob.toString());
}
System.out.println(theClob.toString());
} catch (SQLException ex) {
ex.printStackTrace();
}
}
}
它是如何工作的
如果您的应用需要存储字符串值,您需要知道这些字符串可能会有多大。对于 VARCHAR 字段的存储大小,大多数数据库都有一个上限。例如,Oracle 数据库的上限为 2,000 个字符,超过这个长度的部分将被删除。如果需要存储大量文本,请在数据库中使用 CLOB 字段。
在 Java 代码中,CLOB 的处理方式与字符串略有不同。事实上,在最初几次使用它时,实际上有点奇怪,因为您必须从一个连接创建 CLOB。
注意
实际上,CLOBs 和 BLOBs(二进制大型对象)并不存储在定义它们的 Oracle 表中。相反,大型对象(LOB)定位器存储在表列中。Oracle 可能会将 CLOB 放在数据库服务器上的一个单独文件中。当 Java 创建 Clob 对象时,它可以用来保存更新到数据库中特定 lob 位置的数据,或者从数据库中特定 LOB 位置检索数据。
让我们来看看这个菜谱的解决方案中包含的 loadClob()方法。如您所见,Clob 对象是使用 Connection createClob()方法创建的。一旦创建了 Clob,就可以使用 setString()方法设置它的内容,方法是传递指示放置字符串的位置和文本字符串本身:
textClob = conn.createClob();
textClob.setString(1, "This will be the recipe text in clob format");
一旦创建并填充了 Clob,只需使用 PreparedStatement setClob()方法将其传递给数据库。在本例中,PreparedStatement 像往常一样通过调用 executeUpdate()方法在 RECIPE_TEXT 表中执行数据库插入。
查询 Clob 也相当简单。正如您在这个配方的解决方案中包含的 readClob()方法中看到的,建立了一个 PreparedStatement 查询,并将结果检索到一个 ResultSet 中。使用 Clob 和字符串之间的唯一区别是,您必须将 Clob 加载到 Clob 类型中。
注意
调用 Clob getString()方法将传递一个看起来很有趣的文本字符串,它表示一个 Clob 对象。因此,调用 Clob 对象的 getAsciiStream()方法将返回存储在 Clob 中的实际数据。
虽然 Clobs 很容易使用,但是需要额外的几个步骤来准备。最好相应地规划您的应用,并尝试估计您使用的数据库字段是否由于大小限制而需要 CLOBs。适当的规划将防止您回头修改标准的基于字符串的代码来使用 Clobs。
13-14.调用存储过程
问题
应用所需的一些逻辑被编写为数据库存储过程。您需要能够从应用中调用存储过程。
解决办法
下面的代码块显示了创建 Java 将调用的存储过程所需的 PL/SQL。这个存储过程的功能非常小;它只是接受一个值,并将该值赋给一个 OUT 参数,以便程序可以显示它:
create or replace procedure dummy_proc (text IN VARCHAR2,
msg OUT VARCHAR2) as
begin
-- Do something, in this case the IN parameter value is assigned to the OUT parameter
msg :=text;
end;
下面代码中的 CallableStatement 执行数据库中包含的这个存储过程,并传递必要的参数。然后,OUT 参数的结果会显示给用户。
try(CallableStatement cs = conn.prepareCall("{call DUMMY_PROC(?,?)}");) {
cs.setString(1, "This is a test");
cs.registerOutParameter(2, Types.VARCHAR);
cs.executeQuery();
System.out.println(cs.getString(2));
} catch (SQLException ex){
ex.printStackTrace();
}
运行这个菜谱的示例类将显示以下输出,它与输入相同。这是因为 DUMMY_PROC 过程只是将 IN 参数的内容分配给 OUT 参数。
Successfully connected
This is a test
它是如何工作的
对于应用来说,将数据库存储过程用于可以在数据库中直接执行的逻辑并不少见。为了从 Java 调用数据库存储过程,必须创建一个 CallableStatement 对象,而不是使用 PreparedStatement。在这个配方的解决方案中,一个 CallableStatement 调用一个名为 DUMMY_PROC 的存储过程。实例化 CallableStatement 的语法类似于使用 PreparedStatement 的语法。使用 Connection 对象的 prepareCall()方法,将调用传递给存储过程。存储过程调用必须用花括号{}括起来,否则应用将引发异常。
cs = conn.prepareCall("{call DUMMY_PROC(?,?)}");
一旦实例化了 CallableStatement,就可以像 PreparedStatement 一样使用它来设置参数值。但是,如果某个参数在数据库存储过程中注册为 OUT 参数,则必须调用一个特殊的方法 registerOutParameter(),传递要注册的 OUT 参数的参数位置和数据库类型。在这个配方的解决方案中,OUT 参数在第二个位置,它的类型是 VARCHAR。
cs.registerOutParameter(2, Types.VARCHAR);
若要执行存储过程,请对 CallableStatement 调用 executeQuery()方法。完成后,您可以通过调用对应于数据类型的 CallableStatement getXXX()方法来查看 OUT 参数的值:
System.out.println(cs.getString(2));
关于存储函数的一个注记
调用存储数据库函数本质上与调用存储过程相同。但是,prepareCall()的语法略有修改。若要调用存储函数,请将花括号中的调用改为使用?性格。例如,假设一个名为 DUMMY_FUNC 的函数接受一个参数并返回一个值。下面的代码将用于进行调用并返回值:
cs = conn.prepareCall("{? = call DUMMY_FUNC(?)}");
cs.registerOutParameter(1, Types.VARCHAR);
cs.setString(2, "This is a test");
cs.execute();
对 cs.getString(1)的调用将检索返回值。
13-15.获取数据库使用的日期
问题
您希望正确转换 LocalDate,以便将其插入到数据库记录中。
解决办法
利用静态 java.sql.Date . value of(LocalDate)方法将 local date 对象转换为 Java . SQL . date 对象,JDBC 可以利用该对象插入或查询数据库。在下面的示例中,当前日期被插入到日期类型的数据库列中。
private static void insertRecord(
String title,
String publisher) {
String sql = "INSERT INTO PUBLICATION VALUES("
+ "NEXT VALUE FOR PUBLICATION_SEQ, ?,?,?,?)";
LocalDate pubDate = LocalDate.now();
try (Connection conn = createConn.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);) {
pstmt.setInt(1, 100);
pstmt.setString(2, title);
pstmt.setDate(3, **java.sql.Date.valueOf(pubDate)**);
pstmt.setString(4, publisher);
pstmt.executeUpdate();
System.out.println("Record successfully inserted.");
} catch (SQLException ex) {
ex.printStackTrace();
}
}
它是如何工作的
在 Java 8 中,新的日期时间 API(第四章第四章)是处理日期和时间的首选 API。因此,当处理日期值和数据库时,JDBC API 必须在 SQL 日期和新的日期时间 LocalDate 对象之间进行转换。这个配方的解决方案演示了如何从 LocalDate 对象获取 java.sql.Date 的实例,只需调用静态 java.sql.Date.valueOf()方法,传递相关的 LocalDate 对象。
13-16.自动关闭资源
问题
与其在每次数据库调用时手动打开和关闭资源,不如让应用为您处理这些样板代码。
解决办法
使用 try-with-resources 语法自动关闭您打开的资源。下面的代码块使用这种策略,在使用完连接、语句和结果集资源后自动关闭它们:
String qry = "select recipe_number, recipename, description from recipes";
try (Connection conn = createConn.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(qry);) {
while (rs.next()) {
String recipe = rs.getString("RECIPE_NUMBER");
String name = rs.getString("RECIPE_NAME");
String desc = rs.getString("DESCRIPTION");
System.out.println(recipe + "\t" + name + "\t" + desc);
}
} catch (SQLException e) {
e.printStackTrace();
}
运行此代码产生的输出应该类似于以下内容:
Successfully connected
13-1 Connecting to a Database DriverManager and DataSource Implementations - More to Come
13-2 Querying a Database and Retrieving Results Subject to Change
13-3 Handling SQL Exceptions Using SQLException
它是如何工作的
管理 JDBC 资源一直是件令人头疼的事情。当不再需要资源时,关闭资源需要大量的样板代码。自从 Java SE 7 发布以来,情况就不是这样了。Java 7 引入了使用 try-with-resources 的自动资源管理。通过使用这种技术,开发人员不再需要手动关闭每个资源,这种改变可以减少许多行代码。
为了使用这种技术,您必须实例化您希望在 try 子句后的一组括号中启用自动处理的所有资源。在这个配方的解决方案中,声明的资源是连接、语句和结果集。
try (Connection conn = createConn.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(qry);) {
一旦这些资源超出范围,它们就会自动关闭。这意味着不再需要编写 finally 块来确保关闭资源。自动资源处理不仅适用于数据库工作,还适用于任何符合新的 java.lang.Autocloseable API 的资源。文件 I/O 等其他操作也遵循新的 API。java.lang.Autoclosable 中有一个 close()方法管理资源的关闭。实现 java.io.Closeable 接口的类可以遵守 API。
摘要
在许多应用中,数据库对于存储重要信息已经变得必不可少。因此,很好地理解如何在应用中使用数据库是很重要的。本章从头开始,涵盖了数据库访问入门的诀窍。然后讨论了一些重要的主题,比如如何安全地访问和修改数据、事务管理以及在没有连接到网络时的数据访问。现在,您应该对 Java 解决方案中的一些数据处理技术有了很好的理解。请记住,有许多数据访问解决方案,本章中的方法只是解决信息管理这个大问题的一些方法。