Java-中文官方教程-2022-版-四-

龙哥盟 / 2025-02-21 / 原文

Java 中文官方教程 2022 版(四)

原文:docs.oracle.com/javase/tutorial/reallybigindex.html

自定义格式

原文:docs.oracle.com/javase/tutorial/i18n/format/decimalFormat.html

你可以使用DecimalFormat类将十进制数格式化为特定于区域设置的字符串。这个类允许你控制前导和尾随零的显示,前缀和后缀,分组(千位)分隔符和小数分隔符。如果你想要更改格式化符号,比如小数分隔符,你可以与DecimalFormat类一起使用DecimalFormatSymbols。这些类在数字格式化方面提供了很大的灵活性,但可能会使你的代码变得更加复杂。

以下文本使用示例演示了DecimalFormatDecimalFormatSymbols类。本材料中的代码示例来自一个名为DecimalFormatDemo的示例程序。

构建模式

你可以通过模式String指定DecimalFormat的格式属性。模式决定了格式化后的数字是什么样子的。有关模式语法的完整描述,请参见数字格式模式语法。

接下来的示例通过将模式String传递给DecimalFormat构造函数来创建一个格式化器。format方法接受一个double值作为参数,并以String形式返回格式化后的数字:

DecimalFormat myFormatter = new DecimalFormat(pattern);
String output = myFormatter.format(value);
System.out.println(value + " " + pattern + " " + output);

前面代码的输出描述在以下表中。value是要格式化的数字,一个doublepattern是指定格式属性的Stringoutput是一个String,表示格式化后的数字。

DecimalFormatDemo程序的输出

value pattern output 解释
123456.789 ###,###.### 123,456.789 井号(#)表示一个数字,逗号是分组分隔符的占位符,句点是小数分隔符的占位符。
123456.789 ###.## 123456.79 value小数点右侧有三位数字,但pattern只有两位。format方法通过四舍五入处理这个问题。
123.78 000000.000 000123.780 pattern指定了前导和尾随零,因为使用 0 字符而不是井号(#)。
12345.67 $###,###.### | $12,345.67 pattern中的第一个字符是美元符号($)。请注意,它紧跟在格式化的output中最左边的数字之前。
12345.67 \u00A5###,###.### ¥12,345.67 pattern指定了日元(¥)的货币符号,Unicode 值为 00A5。

区域敏感格式化

前面的示例创建了一个默认LocaleDecimalFormat对象。如果你想要一个非默认LocaleDecimalFormat对象,你可以实例化一个NumberFormat,然后将其转换为DecimalFormat。以下是一个示例:

NumberFormat nf = NumberFormat.getNumberInstance(loc);
DecimalFormat df = (DecimalFormat)nf;
df.applyPattern(pattern);
String output = df.format(value);
System.out.println(pattern + " " + output + " " + loc.toString());

运行上一个代码示例会产生以下输出。格式化的数字,位于第二列,随Locale而变化:

###,###.###      123,456.789     en_US
###,###.###      123.456,789     de_DE
###,###.###      123 456,789     fr_FR

到目前为止,这里讨论的格式化模式遵循美国英语的惯例。例如,在模式###,###.##中,逗号是千位分隔符,句点代表小数点。这种惯例是可以接受的,前提是您的最终用户不会接触到它。然而,一些应用程序,如电子表格和报表生成器,允许最终用户定义自己的格式化模式。对于这些应用程序,最终用户指定的格式化模式应使用本地化符号。在这些情况下,您需要在DecimalFormat对象上调用applyLocalizedPattern方法。

更改格式化符号

您可以使用DecimalFormatSymbols类来更改format方法生成的格式化数字中出现的符号。这些符号包括小数分隔符、分组分隔符、减号和百分号等。

下一个示例演示了DecimalFormatSymbols类,通过对数字应用奇怪的格式来实现。这种不寻常的格式是通过调用setDecimalSeparatorsetGroupingSeparatorsetGroupingSize方法得到的。

DecimalFormatSymbols unusualSymbols = new DecimalFormatSymbols(currentLocale);
unusualSymbols.setDecimalSeparator('|');
unusualSymbols.setGroupingSeparator('^');

String strange = "#,##0.###";
DecimalFormat weirdFormatter = new DecimalFormat(strange, unusualSymbols);
weirdFormatter.setGroupingSize(4);

String bizarre = weirdFormatter.format(12345.678);
System.out.println(bizarre);

运行时,此示例会以奇怪的格式打印数字:

1²³⁴⁵|678

数字格式模式语法

您可以按照以下 BNF 图表指定的规则设计自己的数字格式模式:

pattern    := subpattern{;subpattern}
subpattern := {prefix}integer{.fraction}{suffix}
prefix     := '\\u0000'..'\\uFFFD' - specialCharacters
suffix     := '\\u0000'..'\\uFFFD' - specialCharacters
integer    := '#'* '0'* '0'
fraction   := '0'* '#'*

在前面的图表中使用的符号在以下表格中有解释:

符号 描述
X* X 的 0 或多个实例
(X | Y) X 或 Y 中的任意一个
X..Y 从 X 到 Y 的任意字符,包括 X 和 Y
S - T S 中的字符,但不包括 T 中的字符
{X} X 是可选的

在前面的 BNF 图表中,第一个子模式指定了正数的格式。第二个子模式是可选的,指定了负数的格式。

尽管在 BNF 图表中没有说明,但逗号可能出现在整数部分内。

在子模式中,您可以使用特殊符号指定格式。这些符号在以下表格中描述:

符号 描述
0 一个数字
# 一个数字,零表示不存在
. 小数分隔符的占位符
, 用于分组分隔符的占位符
E 用于指数格式中的尾数和指数的分隔符
; 分隔格式
- 默认负数前缀
% 乘以 100 并显示为百分比
? 乘以 1000 并显示为千分数
¤ 货币符号;替换为货币符号;如果双倍,则替换为国际货币符号;如果在模式中存在,则使用货币小数分隔符而不是小数分隔符
X 前缀或后缀中可以使用任何其他字符
' 用于引用前缀或后缀中的特殊字符

日期和时间

原文:docs.oracle.com/javase/tutorial/i18n/format/dateintro.html


版本说明: 此日期和时间部分使用java.util包中的日期和时间 API。JDK 8 发布的java.time API 提供了一个全面的日期和时间模型,相比java.util类有显著改进。java.time API 在日期时间教程中有描述。旧版日期时间代码页面可能会引起特别关注。


Date对象表示日期和时间。您无法显示或打印Date对象,而不先将其转换为适当格式的String。什么是“适当”的格式呢?首先,格式应符合最终用户的Locale的约定。例如,德国人认为20.4.09是一个有效的日期,但美国人期望同一日期显示为4/20/09。其次,格式应包含必要的信息。例如,一个测量网络性能的程序可能报告经过的毫秒数。在线约会日历可能不会显示毫秒,但会显示星期几。

本节介绍如何以各种方式和符合区域设置的方式格式化日期和时间。如果您遵循这些技术,您的程序将以适当的Locale显示日期和时间,但您的源代码将保持独立于任何特定的Locale

使用预定义格式

DateFormat类提供了具有区域特定性和易于使用的预定义格式样式。

自定义格式

使用SimpleDateFormat类,您可以创建定制的、区域特定的格式。

更改日期格式符号

使用DateFormatSymbols类,您可以更改表示月份名称、星期几和其他格式化元素的符号。

使用预定义格式

原文:docs.oracle.com/javase/tutorial/i18n/format/dateFormat.html


版本说明: 本日期和时间部分使用了java.util包中的日期和时间 API。在 JDK 8 发布的java.timeAPI 中,提供了一个全面的日期和时间模型,相比java.util类有显著的改进。java.timeAPI 在日期时间教程中有详细描述。旧日期时间代码页面可能会引起特别关注。


DateFormat类允许您以区域敏感的方式使用预定义样式格式化日期和时间。接下来的部分演示了如何使用名为DateFormatDemo.java的程序与DateFormat类一起使用。

日期

使用DateFormat类格式化日期是一个两步过程。首先,使用getDateInstance方法创建格式化器。其次,调用format方法,返回包含格式化日期的String。以下示例通过调用这两个方法格式化今天的日期:

Date today;
String dateOut;
DateFormat dateFormatter;

dateFormatter = DateFormat.getDateInstance(DateFormat.DEFAULT, currentLocale);
today = new Date();
dateOut = dateFormatter.format(today);

System.out.println(dateOut + " " + currentLocale.toString());

以下是此代码生成的输出。请注意日期的格式会随着Locale的不同而变化。由于DateFormat是区域敏感的,它会为每个Locale处理格式化细节。

30 juin 2009     fr_FR
30.06.2009       de_DE
Jun 30, 2009     en_US

前面的代码示例指定了默认格式样式。默认样式只是DateFormat类提供的预定义格式样式之一,如下所示:

  • 默认

  • 中等

  • 完整

以下表显示了美国和法国区域的每种样式的日期格式:

示例日期格式

样式 美国区域 法国区域
默认 2009 年 6 月 30 日 2009 年 6 月 30 日
6/30/09 30/06/09
中等 2009 年 6 月 30 日 2009 年 6 月 30 日
2009 年 6 月 30 日 2009 年 6 月 30 日
完整 2009 年 6 月 30 日星期二 2009 年 6 月 30 日星期二

时间

Date对象代表日期和时间。使用DateFormat类格式化时间与格式化日期类似,只是你需要使用getTimeInstance方法创建格式化器,如下所示:

DateFormat timeFormatter =
    DateFormat.getTimeInstance(DateFormat.DEFAULT, currentLocale);

下表显示了美国和德国区域的各种预定义格式样式:

示例时间格式

样式 美国区域 德国区域
默认 上午 7:03:47 7:03:47
上午 7:03 07:03
中等 上午 7:03:47 07:03:07
上午 7:03:47 PDT 07:03:45 PDT
完整 上午 7:03:47 PDT 下午 7.03 PDT

日期和时间都包括

要在同一String中显示日期和时间,使用getDateTimeInstance方法创建格式化器。第一个参数是日期样式,第二个是时间样式。第三个参数是Locale。以下是一个快速示例:

DateFormat formatter = DateFormat.getDateTimeInstance(
                           DateFormat.LONG, 
                           DateFormat.LONG, 
                           currentLocale);

以下表显示了美国和法国区域的日期和时间格式样式:

日期和时间格式示例

样式 美国区域设置 法国区域设置
DEFAULT Jun 30, 2009 7:03:47 AM 30 juin 2009 07:03:47
SHORT 6/30/09 7:03 AM 30/06/09 07:03
MEDIUM Jun 30, 2009 7:03:47 AM 30 juin 2009 07:03:47
LONG June 30, 2009 7:03:47 AM PDT 30 juin 2009 07:03:47 PDT
FULL Tuesday, June 30, 2009 7:03:47 AM PDT mardi 30 juin 2009 07 h 03 PDT

自定义格式

原文:docs.oracle.com/javase/tutorial/i18n/format/simpleDateFormat.html


版本说明: 此日期和时间部分使用java.util包中的日期和时间 API。在 JDK 8 发布的java.timeAPI 提供了一个全面的日期和时间模型,相比java.util类有显著改进。java.timeAPI 在日期时间教程中有详细描述。旧版日期时间代码页面可能会引起特别关注。


前一节,使用预定义格式,描述了DateFormat类提供的格式样式。在大多数情况下,这些预定义格式是足够的。但是,如果你想创建自定义格式,可以使用SimpleDateFormat类。

接下来的代码示例演示了SimpleDateFormat类的方法。你可以在名为SimpleDateFormatDemo的文件中找到示例的完整源代码。

关于模式

当你创建一个SimpleDateFormat对象时,你指定一个模式String。模式String的内容决定了日期和时间的格式。有关模式语法的完整描述,请参见日期格式模式语法中的表格。

以下代码根据传递给SimpleDateFormat构造函数的模式String格式化日期和时间。format方法返回的String包含要显示的格式化日期和时间。

Date today;
String output;
SimpleDateFormat formatter;

formatter = new SimpleDateFormat(pattern, currentLocale);
today = new Date();
output = formatter.format(today);
System.out.println(pattern + " " + output);

以下表格显示了在指定美国Locale时前面代码示例生成的输出:

自定义日期和时间格式

模式 输出
dd.MM.yy 30.06.09
yyyy.MM.dd G 'at' hh:mm:ss z 2009.06.30 公元 at 08:29:36 PDT
EEE, MMM d, ''yy 周二, 六月 30, '09
h:mm a 8:29 PM
H:mm 8:29
H:mm:ss:SSS 8:28:36:249
K:mm a,z 8:29 AM,PDT
yyyy.MMMMM.dd GGG hh:mm aaa 2009.六月.30 公元 08:29 AM

模式和区域设置

SimpleDateFormat类是区域敏感的。如果实例化SimpleDateFormat而不带Locale参数,它将根据默认Locale格式化日期和时间。模式和Locale都决定了格式。对于相同的模式,如果Locale不同,SimpleDateFormat可能会以不同方式格式化日期和时间。

在接下来的示例代码中,模式是硬编码在创建SimpleDateFormat对象的语句中的:

Date today;
String result;
SimpleDateFormat formatter;

formatter = new SimpleDateFormat("EEE d MMM yy", currentLocale);
today = new Date();
result = formatter.format(today);
System.out.println("Locale: " + currentLocale.toString());
System.out.println("Result: " + result);

currentLocale设置为不同值时,前面的代码示例生成以下输出:

Locale: fr_FR
Result: mar. 30 juin 09
Locale: de_DE
Result: Di 30 Jun 09
Locale: en_US
Result: Tue 30 Jun 09

日期格式模式语法

你可以根据以下表格中的符号列表设计自己的日期和时间格式模式:

符号 含义 显示 示例
G 时代指示符 文本 公元
y 年份 数字 2009
M 年份中的月份 文本 & 数字 七月 & 07
d 月中的日期 数字 10
h 上午/下午的小时数 (1-12) 数字 12
H 一天中的小时数 (0-23) 数字 0
m 小时中的分钟数 数字 30
s 分钟中的秒数 数字 55
S 毫秒 数字 978
E 星期几 文本 星期二
D 一年中的日期 数字 189
F 月中的星期几 数字 2 (七月第二个星期三)
w 年中周数 数字 27
W 月中周数 数字 2
a 上午/下午标记 文本 下午
k 一天中的小时数 (1-24) 数字 24
K 上午/下午的小时数 (0-11) 数字 0
z 时区 文本 太平洋标准时间
' 转义文本 分隔符 (无)
' 单引号 字面值 '

非字母字符被视为引用文本。也就是说,即使它们没有被单引号括起来,它们也会出现在格式化文本中。

您指定的符号字母数量还决定了格式。例如,如果“zz”模式结果为“PDT”,那么“zzzz”模式会生成“太平洋夏令时间”。以下表总结了这些规则:

展示 符号数量 结果
文本 1 - 3 缩写形式,如果存在的话
文本 >= 4 完整形式
数字 需要的最小数字位数 较短的数字用零填充(例如,如果'y'的计数为 2,则年份将被截断为 2 位数)
文本 & 数字 1 - 2 数字形式
文本 & 数字 3 文本形式

更改日期格式符号

原文:docs.oracle.com/javase/tutorial/i18n/format/dateFormatSymbols.html


版本说明: 此日期和时间部分使用java.util包中的日期和时间 API。在 JDK 8 发布的java.time API 提供了一个全面的日期和时间模型,相比java.util类有显著改进。java.time API 在日期时间教程中有详细描述。传统日期时间代码页面可能会引起特别关注。


SimpleDateFormat类的format方法返回由数字和符号组成的String。例如,在String "Friday, April 10, 2009"中,符号是"Friday"和"April"。如果SimpleDateFormat封装的符号不符合您的需求,您可以使用DateFormatSymbols进行更改。您可以更改代表月份、星期几和时区等的符号。以下表列出了允许您修改符号的DateFormatSymbols方法:

DateFormatSymbol 方法

设置方法 方法修改的符号示例
setAmPmStrings 下午
setEras 公元
setMonths 十二月
setShortMonths 十二月
setShortWeekdays 星期二
setWeekdays 星期二
setZoneStrings PST

以下示例调用setShortWeekdays将星期几的简称从小写更改为大写字符。此示例的完整源代码在DateFormatSymbolsDemo中。setShortWeekdays的数组参数中的第一个元素是一个空String。因此,数组是基于一的而不是零的。SimpleDateFormat构造函数接受修改后的DateFormatSymbols对象作为参数。以下是源代码:

Date today;
String result;
SimpleDateFormat formatter;
DateFormatSymbols symbols;
String[] defaultDays;
String[] modifiedDays;

symbols = new DateFormatSymbols( new Locale("en", "US"));
defaultDays = symbols.getShortWeekdays();

for (int i = 0; i < defaultDays.length; i++) {
    System.out.print(defaultDays[i] + " ");
}
System.out.println();

String[] capitalDays = {
    "", "SUN", "MON",
    "TUE", "WED", "THU",
    "FRI", "SAT"
};
symbols.setShortWeekdays(capitalDays);

modifiedDays = symbols.getShortWeekdays();
for (int i = 0; i < modifiedDays.length; i++) {
    System.out.print(modifiedDays[i] + " ");
}
System.out.println();
System.out.println();

formatter = new SimpleDateFormat("E", symbols);
today = new Date();
result = formatter.format(today);
System.out.println("Today's day of the week: " + result);

以上代码生成以下输出:

 Sun Mon Tue Wed Thu Fri Sat 
 SUN MON TUE WED THU FRI SAT 

Today's day of the week: MON

消息

原文:docs.oracle.com/javase/tutorial/i18n/format/messageintro.html

我们都喜欢使用让我们了解正在发生什么的程序。通常,让我们保持了解的程序通过显示状态和错误消息来实现。当然,这些消息需要被翻译,以便全球的最终用户能够理解。隔离特定区域数据部分讨论了可翻译的文本消息。通常,在将消息String移入ResourceBundle后,你就完成了。然而,如果在消息中嵌入了变量数据,你需要采取一些额外的步骤来准备翻译。

复合消息包含变量数据。在下面的复合消息列表中,变量数据被划线标出:

The disk named MyDisk contains 300 files.
The current balance of account #34-09-222 is $2,745.72.
405,390 people have visited your website since January 1, 2009.
Delete all files older than 120 days.

你可能会尝试通过连接短语和变量来构建前述列表中的最后一条消息,如下所示:

double numDays;
ResourceBundle msgBundle;
// ...
String message = msgBundle.getString(
                     "deleteolder" +
                     numDays.toString() +
                     msgBundle.getString("days"));

这种方法在英语中效果很好,但对于动词出现在句子末尾的语言来说,这种方法不适用。因为这条消息的词序是硬编码的,你的本地化人员将无法为所有语言创建语法正确的翻译。

如果需要使用复合消息,如何使你的程序可本地化?你可以通过使用MessageFormat类来实现,这是本节的主题。


注意:

复合消息很难翻译,因为消息文本是分散的。如果使用复合消息,本地化将需要更长的时间和更多的成本。因此,只有在必要时才应使用复合消息。


处理复合消息

复合消息可能包含多种类型的变量:日期、时间、字符串、数字、货币和百分比。为了以与区域无关的方式格式化复合消息,你需要构建一个模式,然后应用到MessageFormat对象上。

处理复数形式

如果可能同时存在复数和单数形式的词汇,消息中的词汇通常会有所变化。通过ChoiceFormat类,你可以将数字映射到一个词或短语,从而构建语法正确的消息。

处理复合消息

原文:docs.oracle.com/javase/tutorial/i18n/format/messageFormat.html

复合消息可能包含几种类型的变量:日期、时间、字符串、数字、货币和百分比。为了以与语言环境无关的方式格式化复合消息,您构造一个模式,将其应用于 MessageFormat 对象,并将此模式存储在 ResourceBundle 中。

通过逐步执行示例程序,本节演示了如何国际化复合消息。示例程序使用了 MessageFormat 类。此程序的完整源代码在名为 MessageFormatDemo.java 的文件中。德语区域设置属性在名为 MessageBundle_de_DE.properties 的文件中。

1. 识别消息中的变量

假设您想要国际化以下消息:

以下一行文本:1998 年 4 月 13 日 1:15,在火星上我们探测到 7 艘飞船。变量数据(1:15,1998 年 4 月 13 日,7,和火星)已被划线。

请注意,我们已经划线标出了变量数据,并确定了将表示这些数据的对象的类型。

2. 在 ResourceBundle 中隔离消息模式

将消息存储在名为 MessageBundleResourceBundle 中,如下所示:

ResourceBundle messages =
   ResourceBundle.getBundle("MessageBundle", currentLocale);

这个 ResourceBundle 是由每个 Locale 的属性文件支持的。由于 ResourceBundle 名称为 MessageBundle,因此美国英语的属性文件名为 MessageBundle_en_US.properties。此文件的内容如下:

template = At {2,time,short} on {2,date,long}, \
    we detected {1,number,integer} spaceships on \
    the planet {0}.
planet = Mars

属性文件的第一行包含消息模式。如果您将此模式与步骤 1 中显示的消息文本进行比较,您将看到在消息文本中的每个变量都由大括号括起的参数替换。每个参数以称为参数编号的数字开头,该数字与保存参数值的 Object 数组中的元素的索引相匹配。请注意,在模式中,参数编号没有特定顺序。您可以将参数放置在模式的任何位置。唯一的要求是参数编号在参数值数组中有一个匹配的元素。

下一步讨论了参数值数组,但首先让我们看一下模式中的每个参数。以下表格提供了有关参数的一些详细信息:

MessageBundle_en_US.properties 中为 template 参数提供参数

参数 描述
{2,time,short} 一个 Date 对象的时间部分。short 样式指定了 DateFormat.SHORT 格式化样式。
{2,date,long} 一个Date对象的日期部分。相同的Date对象用于日期和时间变量。在参数的Object数组中,保存Date对象的元素的索引为 2。 (这在下一步中描述。)
{1,number,integer} 一个带有integer数字样式的Number对象。
{0} planet键对应的ResourceBundle中的String

对于参数语法的完整描述,请参阅MessageFormat类的 API 文档。

3. 设置消息参数

以下代码行为模式中的每个参数分配值。messageArguments数组中元素的索引与模式中的参数编号相匹配。例如,索引为 1 的Integer元素对应于模式中的{1,number,integer}参数。因为必须进行翻译,所以元素 0 处的String对象将使用getString方法从ResourceBundle中获取。以下是定义消息参数数组的代码:

Object[] messageArguments = {
    messages.getString("planet"),
    new Integer(7),
    new Date()
};

4. 创建格式化程序

接下来,创建一个MessageFormat对象。您设置Locale,因为消息包含应以区域敏感的方式格式化的DateNumber对象。

MessageFormat formatter = new MessageFormat("");
formatter.setLocale(currentLocale);

5. 使用模式和参数格式化消息

这一步展示了模式、消息参数和格式化程序如何协同工作。首先,使用getString方法从ResourceBundle中获取模式String。模式的关键是template。使用applyPattern方法将模式String传递给格式化程序。然后通过调用format方法使用消息参数的数组格式化消息。format方法返回的String已经准备好显示。所有这些只需两行代码就可以完成:

formatter.applyPattern(messages.getString("template"));
String output = formatter.format(messageArguments);

6. 运行演示程序

演示程序打印了英语和德语区域设置的翻译消息,并正确格式化了日期和时间变量。请注意,英语和德语动词("detected"和"entdeckt")相对于变量的位置不同:

currentLocale = en_US
At 10:16 AM on July 31, 2009, we detected 7
spaceships on the planet Mars.
currentLocale = de_DE
Um 10:16 am 31\. Juli 2009 haben wir 7 Raumschiffe
auf dem Planeten Mars entdeckt.

处理复数形式

原文:docs.oracle.com/javase/tutorial/i18n/format/choiceFormat.html

如果消息中的单词既可能是复数形式又可能是单数形式,则可能会有所变化。使用ChoiceFormat类,您可以将数字映射到一个单词或短语,从而构造语法正确的消息。

在英语中,单词的复数和单数形式通常是不同的。当您构造涉及数量的消息时,这可能会带来问题。例如,如果您的消息报告磁盘上的文件数量,则可能存在以下变化:

There are no files on XDisk.
There is one file on XDisk.
There are 2 files on XDisk.

解决这个问题的最快方法是创建一个像这样的MessageFormat模式:

There are {0,number} file(s) on {1}.

不幸的是,前面的模式导致了不正确的语法:

There are 1 file(s) on XDisk.

只要使用ChoiceFormat类,你就可以做得更好。在本节中,您将通过一个名为ChoiceFormatDemo的示例程序逐步学习如何处理消息中的复数。该程序还使用了在前一节中讨论的MessageFormat类,即处理复合消息。

1. 定义消息模式

首先,识别消息中的变量:

三行文本,每行中的变量都突出显示。

然后,用参数替换消息中的变量,创建一个可以应用于MessageFormat对象的模式:

There {0} on {1}.

磁盘名称的参数,由{1}表示,处理起来相当简单。您只需像处理MessageFormat模式中的任何其他String变量一样对待它。此参数匹配参数值数组中索引为 1 的元素。(参见步骤 7。)

处理参数{0}更加复杂,原因有几个:

  • 此参数替换的短语随文件数量的变化而变化。为了在运行时构造这个短语,您需要将文件数量映射到特定的String。例如,数字 1 将映射到包含短语is one fileStringChoiceFormat类允许您执行必要的映射。

  • 如果磁盘包含多个文件,则短语中包含一个整数。MessageFormat类允许您将数字插入到短语中。

2. 创建一个 ResourceBundle

因为消息文本必须被翻译,所以将其隔离在一个ResourceBundle中:

ResourceBundle bundle = ResourceBundle.getBundle(
    "ChoiceBundle", currentLocale);

示例程序使用属性文件支持ResourceBundleChoiceBundle_en_US.properties包含以下内容:

pattern = There {0} on {1}.
noFiles = are no files
oneFile = is one file
multipleFiles = are {2} files

此属性文件的内容显示了消息将如何构建和格式化。第一行包含了MessageFormat的模式。(参见步骤 1。)其他行包含了将替换模式中参数{0}的短语。multipleFiles 键的短语包含了参数{2},该参数将被一个数字替换。

这是属性文件的法语版本,ChoiceBundle_fr_FR.properties

pattern = Il {0} sur {1}.
noFiles = n'y a pas de fichiers
oneFile = y a un fichier
multipleFiles = y a {2} fichiers

3. 创建消息格式化器

在此步骤中,您实例化MessageFormat并设置其Locale

MessageFormat messageForm = new MessageFormat("");
messageForm.setLocale(currentLocale);

4. 创建选择格式化器

ChoiceFormat对象允许您根据double数字选择特定的Stringdouble数字的范围,以及它们映射到的String对象,都在数组中指定:

double[] fileLimits = {0,1,2};
String [] fileStrings = {
    bundle.getString("noFiles"),
    bundle.getString("oneFile"),
    bundle.getString("multipleFiles")
};

ChoiceFormatdouble数组中的每个元素映射到具有相同索引的String数组中的元素。在示例代码中,0 映射到调用bundle.getString("noFiles")返回的String。巧合的是,索引与fileLimits数组中的值相同。如果代码将fileLimits[0]设置为七,ChoiceFormat将把数字 7 映射到fileStrings[0]

在实例化ChoiceFormat时,您需要指定doubleString数组:

ChoiceFormat choiceForm = new ChoiceFormat(fileLimits, fileStrings);

5. 应用模式

还记得您在步骤 1 中构建的模式吗?现在是从ResourceBundle中检索模式并应用到MessageFormat对象的时候了:

String pattern = bundle.getString("pattern");
messageForm.applyPattern(pattern);

6. 分配格式

在此步骤中,您将在步骤 4 中创建的ChoiceFormat对象分配给MessageFormat对象:

Format[] formats = {choiceForm, null, NumberFormat.getInstance()};
messageForm.setFormats(formats);

setFormats方法将Format对象分配给消息模式中的参数。在调用setFormats方法之前,必须调用applyPattern方法。以下表格显示了Format数组的元素如何对应于消息模式中的参数:

ChoiceFormatDemo 程序的Format数组

数组元素 模式参数
choiceForm {0}
null {1}
NumberFormat.getInstance() {2}

7. 设置参数并格式化消息

在运行时,程序将变量分配给传递给MessageFormat对象的参数数组。数组中的元素对应于模式中的参数。例如,messageArgument[1]映射到模式参数{1},其中包含磁盘名称的String。在上一步中,程序将ChoiceFormat对象分配给模式的参数{0}。因此,分配给messageArgument[0]的数字确定ChoiceFormat对象选择哪个String。如果messageArgument[0]大于或等于 2,则包含短语are {2} filesString将替换模式中的参数{0}。分配给messageArgument[2]的数字将替换模式参数{2}的位置。以下是尝试此操作的代码:

Object[] messageArguments = {null, "XDisk", null};

for (int numFiles = 0; numFiles < 4; numFiles++) {
    messageArguments[0] = new Integer(numFiles);
    messageArguments[2] = new Integer(numFiles);
    String result = messageForm.format(messageArguments);
    System.out.println(result);
}

8. 运行演示程序

将程序显示的消息与第 2 步的ResourceBundle中的短语进行比较。注意ChoiceFormat对象选择了正确的短语,MessageFormat对象用于构建适当的消息。ChoiceFormatDemo程序的输出如下:

currentLocale = en_US
There are no files on XDisk.
There is one file on XDisk.
There are 2 files on XDisk.
There are 3 files on XDisk.

currentLocale = fr_FR
Il n'y a pas des fichiers sur XDisk.
Il y a un fichier sur XDisk.
Il y a 2 fichiers sur XDisk.
Il y a 3 fichiers sur XDisk.

教程:处理文本

原文:docs.oracle.com/javase/tutorial/i18n/text/index.html

几乎所有带有用户界面的程序都会操作文本。在国际市场上,您的程序显示的文本必须符合世界各地语言的规则。Java 编程语言提供了许多类来帮助您以与区域设置无关的方式处理文本。

检查字符属性

本节解释了如何使用Character比较方法来检查所有主要语言的字符属性。

比较字符串

在本节中,您将学习如何使用Collator类执行与区域设置无关的字符串比较。

检测文本边界

本节展示了BreakIterator类如何检测字符、单词、句子和行边界。

转换非 Unicode 文本

世界各地的不同计算机系统使用各种编码方案存储文本。本节描述了帮助您在 Unicode 和其他编码之间转换文本的类。

规范化器的 API

本节解释了如何使用规范化器的 API 来应用不同的规范化形式转换文本。

使用 JTextComponent 类处理双向文本

本节讨论了如何处理双向文本,即包含从左到右和从右到左两个方向的文本。

检查字符属性

原文:docs.oracle.com/javase/tutorial/i18n/text/charintro.html

您可以根据字符的属性对字符进行分类。例如,X 是大写字母,4 是十进制数字。检查字符属性是验证最终用户输入的数据的常见方法。例如,如果您在网上销售书籍,订单输入屏幕应验证数量字段中的字符是否都是数字。

不习惯编写全球软件的开发人员可能会通过将其与字符常量进行比较来确定字符的属性。例如,他们可能会编写如下代码:

char ch;
//...

// This code is WRONG!

// check if ch is a letter
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'))
    // ...

// check if ch is a digit
if (ch >= '0' && ch <= '9')
    // ...

// check if ch is a whitespace
if ((ch == ' ') || (ch =='\n') || (ch == '\t'))
    // ...

前面的代码是错误的,因为它只适用于英语和少数其他语言。要使前面的示例国际化,将其替换为以下语句:

char ch;
// ...

// This code is OK!

if (Character.isLetter(ch))
    // ...

if (Character.isDigit(ch))
    // ...

if (Character.isSpaceChar(ch))
    // ...

Character方法依赖于 Unicode 标准来确定字符的属性。Unicode 是一种支持世界主要语言的 16 位字符编码。在 Java 编程语言中,char值表示 Unicode 字符。如果您使用适当的Character方法检查char的属性,则您的代码将适用于所有主要语言。例如,如果字符是中文、德文、阿拉伯文或其他语言中的字母,则Character.isLetter方法返回true

以下列表列出了一些最有用的Character比较方法。Character API 文档完全指定了这些方法。

  • isDigit

  • isLetter

  • isLetterOrDigit

  • isLowerCase

  • isUpperCase

  • isSpaceChar

  • isDefined

Character.getType方法返回字符的 Unicode 类别。每个类别对应于Character类中定义的常量。例如,对于字符 A,getType返回Character.UPPERCASE_LETTER常量。有关getType返回的类别常量的完整列表,请参阅Character API 文档。以下示例显示了如何使用getTypeCharacter类别常量。这些if语句中的所有表达式都为true

if (Character.getType('a') == Character.LOWERCASE_LETTER)
    // ...

if (Character.getType('R') == Character.UPPERCASE_LETTER)
    // ...

if (Character.getType('>') == Character.MATH_SYMBOL)
    // ...

if (Character.getType('_') == Character.CONNECTOR_PUNCTUATION)
    // ...

比较字符串

原文:docs.oracle.com/javase/tutorial/i18n/text/collationintro.html

应用程序对文本进行排序时会执行频繁的字符串比较。例如,报告生成器在对字符串列表按字母顺序排序时执行字符串比较。

如果您的应用程序受众仅限于讲英语的人,您可能可以使用String.compareTo方法执行字符串比较。String.compareTo方法对两个字符串中的 Unicode 字符执行二进制比较。然而,对于大多数语言,这种二进制比较不能被依赖来排序字符串,因为 Unicode 值不对应字符的相对顺序。

幸运的是,Collator类允许您的应用程序为不同语言执行字符串比较。在本节中,您将学习如何在排序文本时使用Collator类。

执行与语言环境无关的比较

排序规则定义字符串的排序顺序。这些规则因语言环境而异,因为各种自然语言对单词的排序方式不同。使用Collator类提供的预定义排序规则,您可以以与语言环境无关的方式对字符串进行排序。

自定义排序规则

在某些情况下,Collator类提供的预定义排序规则可能不适用于您。例如,您可能希望对不受Collator支持的语言的字符串进行排序。在这种情况下,您可以定义自己的排序规则,并将其分配给RuleBasedCollator对象。

提高排序性能

使用CollationKey类,您可以提高字符串比较的效率。该类将String对象转换为遵循给定Collator规则的排序键。

执行与 Locale 无关的比较

原文:docs.oracle.com/javase/tutorial/i18n/text/locale.html

排序规则定义字符串的排序顺序。这些规则因区域而异,因为各种自然语言对单词的排序方式不同。您可以使用Collator类提供的预定义排序规则以与区域设置无关的方式对字符串进行排序。

要实例化Collator类,请调用getInstance方法。通常,您为默认的Locale创建一个Collator,如下例所示:

Collator myDefaultCollator = Collator.getInstance();

创建Collator时,您还可以指定特定的Locale,如下所示:

Collator myFrenchCollator = Collator.getInstance(Locale.FRENCH);

getInstance方法返回一个RuleBasedCollator,它是Collator的具体子类。RuleBasedCollator包含一组规则,这些规则确定您指定的区域设置的字符串排序顺序。这些规则对于每个区域设置都是预定义的。由于规则封装在RuleBasedCollator中,您的程序不需要特殊的例程来处理排序规则随语言变化的方式。

您调用Collator.compare方法执行与 Locale 无关的字符串比较。当第一个字符串参数小于、等于或大于第二个字符串参数时,compare方法返回小于零、等于零或大于零的整数。以下表格包含一些对Collator.compare的示例调用:

示例 返回值 解释
myCollator.compare("abc", "def") -1 "abc"小于"def"
myCollator.compare("rtf", "rtf") 0 两个字符串相等
myCollator.compare("xyz", "abc") 1 "xyz"大于"abc"

在执行排序操作时,您使用compare方法。名为CollatorDemo的示例程序使用compare方法对英语和法语单词数组进行排序。该程序展示了使用两个不同的排序器对相同单词列表进行排序时可能发生的情况:

Collator fr_FRCollator = Collator.getInstance(new Locale("fr","FR"));
Collator en_USCollator = Collator.getInstance(new Locale("en","US"));

排序方法称为sortStrings,可以与任何Collator一起使用。请注意,sortStrings方法调用compare方法:

public static void sortStrings(Collator collator, String[] words) {
    String tmp;
    for (int i = 0; i < words.length; i++) {
        for (int j = i + 1; j < words.length; j++) { 
            if (collator.compare(words[i], words[j]) > 0) {
                tmp = words[i];
                words[i] = words[j];
                words[j] = tmp;
            }
        }
    }
}

英语Collator按以下方式对单词进行排序:

peach
péché
pêche
sin

根据法语语言的排序规则,前述列表的顺序是错误的。在法语中,"péché"应该在排序列表中跟在"pêche"之后。法语Collator正确地对单词数组进行排序,如下所示:

peach
pêche
péché
sin

定制整理规则

原文:docs.oracle.com/javase/tutorial/i18n/text/rule.html

前一节讨论了如何使用区域设置的预定义规则来比较字符串。这些整理规则确定字符串的排序顺序。如果预定义的整理规则不符合您的需求,您可以设计自己的规则并将其分配给RuleBasedCollator对象。

定制的整理规则包含在传递给RuleBasedCollator构造函数的String对象中。这里是一个简单的例子:

String simpleRule = "< a < b < c < d";
RuleBasedCollator simpleCollator =  new RuleBasedCollator(simpleRule);

对于前面示例中的simpleCollator对象,a小于bb小于c,依此类推。当比较字符串时,simpleCollator.compare方法引用这些规则。用于构造整理规则的完整语法比这个简单示例更灵活和复杂。有关语法的完整描述,请参考RuleBasedCollator类的 API 文档。

接下来的示例使用两个整理器对一组西班牙语单词进行排序。此示例的完整源代码在RulesDemo.java中。

RulesDemo程序首先定义了英语和西班牙语的整理规则。该程序将按传统方式对西班牙语单词进行排序。按照传统规则排序时,字母 ch 和 ll 及其大写形式各自在排序顺序中有自己的位置。这些字符对比较就好像它们是一个字符一样。例如,ch 按照一个字母排序,在排序顺序中紧随 cz。请注意两个整理器的规则如何不同:

String englishRules = (
    "< a,A < b,B < c,C < d,D < e,E < f,F " +
    "< g,G < h,H < i,I < j,J < k,K < l,L " +
    "< m,M < n,N < o,O < p,P < q,Q < r,R " +
    "< s,S < t,T < u,U < v,V < w,W < x,X " +
    "< y,Y < z,Z");

String smallnTilde = new String("\u00F1");    // ñ
String capitalNTilde = new String("\u00D1");  // Ñ

String traditionalSpanishRules = (
    "< a,A < b,B < c,C " +
    "< ch, cH, Ch, CH " +
    "< d,D < e,E < f,F " +
    "< g,G < h,H < i,I < j,J < k,K < l,L " +
    "< ll, lL, Ll, LL " +
    "< m,M < n,N " +
    "< " + smallnTilde + "," + capitalNTilde + " " +
    "< o,O < p,P < q,Q < r,R " +
    "< s,S < t,T < u,U < v,V < w,W < x,X " +
    "< y,Y < z,Z");

以下代码行创建整理器并调用排序例程:

try {
    RuleBasedCollator enCollator = new RuleBasedCollator(englishRules);
    RuleBasedCollator spCollator =
        new RuleBasedCollator(traditionalSpanishRules);

    sortStrings(enCollator, words);
    printStrings(words);
    System.out.println();

    sortStrings(spCollator, words);
    printStrings(words);
} catch (ParseException pe) {
    System.out.println("Parse exception for rules");
}

名为sortStrings的排序例程是通用的。它将根据任何Collator对象的规则对任何单词数组进行排序:

public static void sortStrings(Collator collator, String[] words) {
    String tmp;
    for (int i = 0; i < words.length; i++) {
        for (int j = i + 1; j < words.length; j++) {
            if (collator.compare(words[i], words[j]) > 0) {
                tmp = words[i];
                words[i] = words[j];
                words[j] = tmp;
            }
        }
    }
}

使用英语整理规则排序时,单词数组如下:

chalina
curioso
llama
luz

将前面的列表与以下按照传统西班牙整理规则排序的列表进行比较:

curioso
chalina
luz
llama

提高排序性能

原文:docs.oracle.com/javase/tutorial/i18n/text/perform.html

对长字符串列表进行排序通常是耗时的。如果您的排序算法重复比较字符串,可以通过使用CollationKey类加快该过程。

一个CollationKey对象代表给定StringCollator的排序键。比较两个CollationKey对象涉及对排序键进行按位比较,比使用Collator.compare方法比较String对象更快。然而,生成CollationKey对象需要时间。因此,如果一个String只需比较一次,Collator.compare提供更好的性能。

接下来的示例使用CollationKey对象对单词数组进行排序。此示例的源代码在KeysDemo.java中。

KeysDemo程序在main方法中创建一个CollationKey对象数组。要创建CollationKey,需要在Collator对象上调用getCollationKey方法。除非两个CollationKey对象来自同一个Collator,否则无法比较它们。main方法如下:

static public void main(String[] args) {
    Collator enUSCollator = Collator.getInstance(new Locale("en","US"));
    String [] words = {
        "peach",
        "apricot",
        "grape",
        "lemon"
    };

    CollationKey[] keys = new CollationKey[words.length];

    for (int k = 0; k < keys.length; k ++) {
        keys[k] = enUSCollator. getCollationKey(words[k]);
    }

    sortArray(keys);
    printArray(keys);
}

sortArray方法调用CollationKey.compareTo方法。compareTo方法返回一个整数,如果keys[i]对象小于、等于或大于keys[j]对象,则返回小于零、等于零或大于零。请注意,程序比较的是CollationKey对象,而不是原始单词数组中的String对象。以下是sortArray方法的代码:

public static void sortArray(CollationKey[] keys) {
    CollationKey tmp;

    for (int i = 0; i < keys.length; i++) {
        for (int j = i + 1; j < keys.length; j++) {
            if (keys[i].compareTo(keys[j]) > 0) {
                tmp = keys[i];
                keys[i] = keys[j];
                keys[j] = tmp; 
            }
        }
    }
}

KeysDemo程序对CollationKey对象数组进行排序,但最初的目标是对String对象数组进行排序。为了检索每个CollationKeyString表示形式,程序在displayWords方法中调用getSourceString,如下所示:

static void displayWords(CollationKey[] keys) {
    for (int i = 0; i < keys.length; i++) {
        System.out.println(keys[i].getSourceString());
    }
}

displayWords方法打印以下行:

apricot
grape
lemon
peach

Unicode

原文:docs.oracle.com/javase/tutorial/i18n/text/unicode.html

Unicode 是一个计算行业标准,旨在一致且独特地编码世界各地书面语言中使用的字符。Unicode 标准使用十六进制表示字符。例如,值 0x0041 表示拉丁字符 A。Unicode 标准最初设计时使用 16 位来编码字符,因为主要的计算机是 16 位的个人电脑。

在创建 Java 语言规范时,接受了 Unicode 标准,并将 char 原始数据类型定义为 16 位数据类型,字符的十六进制范围为 0x0000 到 0xFFFF。

由于 16 位编码支持 2¹⁶(65,536)个字符,这不足以定义世界各地使用的所有字符,因此将 Unicode 标准扩展到 0x10FFFF,支持超过一百万个字符。在 Java 编程语言中,字符的定义无法从 16 位更改为 32 位,否则会导致数百万个 Java 应用程序无法正常运行。为了纠正这一定义,开发了一种方案来处理无法用 16 位编码的字符。

值在 16 位范围之外,且在 0x10000 到 0x10FFFF 范围内的字符被称为补充字符,并被定义为一对 char 值。

本课程包括以下部分:

  • 术语 – 解释了代码点和其他术语。

  • 补充字符作为代理 – 使用 16 位代理实现无法作为单个原始 char 数据类型实现的补充字符。

  • 字符和字符串 API – 列出了与 CharacterString 和相关类相关的 API。

  • 示例用法 – 提供了几个有用的代码片段。

  • 设计考虑 – 要牢记的设计考虑,以确保您的应用程序能够与任何语言脚本一起工作。

  • 更多信息 – 提供了更多资源列表。

术语

原文:docs.oracle.com/javase/tutorial/i18n/text/terminology.html

字符是具有语义值的最小文本单位。

字符集是一组可能被多种语言使用的字符集合。例如,拉丁字符集被英语和大多数欧洲语言使用,而希腊字符集仅被希腊语使用。

编码字符集是一个字符集,其中每个字符被分配一个唯一的数字。

代码点是可以在编码字符集中使用的值。代码点是一个 32 位的int数据类型,其中低 21 位表示有效的代码点值,而高 11 位为 0。

Unicode 的代码单元是一个 16 位的char值。例如,想象一个包含字母"abc"后跟 Deseret 长 I 的String,它用两个char值表示。该字符串包含四个字符,四个代码点,但五个代码单元。

要在 Unicode 中表示一个字符,十六进制值前缀为字符串 U+。Unicode 标准的有效代码点范围是 U+0000 到 U+10FFFF,包括两端。拉丁字符 A 的代码点值为 U+0041。代表欧元货币的字符€,其代码点值为 U+20AC。Deseret 字母表中的第一个字母,长 I,其代码点值为 U+10400。

以下表格显示了几个字符的代码点值:

字符 Unicode 代码点 符号
拉丁 A U+0041 拉丁字符 A
拉丁尖 S U+00DF 拉丁小写尖 S
东方汉字 U+6771 表示东方、东部或向东的汉字
Deseret, LONG I U+10400 长 I 的 Deseret 大写字母

如前所述,范围在 U+10000 到 U+10FFFF 之间的字符称为补充字符。从 U+0000 到 U+FFFF 的字符集有时被称为基本多语言平面(BMP)

更多术语可以在更多信息页面上列出的Unicode 术语词汇表中找到。

补充字符作为代理

原文:docs.oracle.com/javase/tutorial/i18n/text/supplementaryChars.html

为了支持补充字符而不改变char原始数据类型并导致与之前的 Java 程序不兼容,补充字符被定义为一对称为代理的代码点值。 第一个代码点来自U+D800U+DBFF高代理范围,第二个代码点来自U+DC00U+DFFF低代理范围。 例如,Deseret 字符 LONG I,U+10400,是用这对代理值定义的:U+D801U+DC00

字符和字符串 API

原文:docs.oracle.com/javase/tutorial/i18n/text/characterClass.html

Character 类封装了 char 数据类型。对于 J2SE 发布的第 5 版,许多方法被添加到 Character 类中以支持补充字符。这个 API 分为两类:将 char 和代码点值之间进行转换的方法以及验证或映射代码点的方法。

本节描述了 Character 类中可用方法的一个子集。有关可用 API 的完整列表,请参阅 Character 类规范。

转换方法和 Character 类

以下表格包括了 Character 类中最有用的转换方法或便于转换的方法。codePointAtcodePointBefore 方法包含在此列表中,因为文本通常以序列的形式出现,比如一个 String,这些方法可以用来提取所需的子字符串。

方法 描述
toChars(int codePoint, char[] dst, int dstIndex) toChars(int codePoint) 将指定的 Unicode 代码点转换为其 UTF-16 表示,并将其放入一个 char 数组中。示例用法:Character.toChars(0x10400)
toCodePoint(char high, char low) 将指定的代理对转换为其补充代码点值。

| codePointAt(char[] a, int index) codePointAt(char[] a, int index, int limit)

codePointAt(CharSequence seq, int index) | 返回指定索引处的 Unicode 代码点。第三个方法接受一个 CharSequence,第二个方法对索引施加了上限。 |

| codePointBefore(char[] a, int index) codePointBefore(char[] a, int index, int start)

codePointBefore(CharSequence seq, int index)

codePointBefore(char[], int, int) | 返回指定索引之前的 Unicode 代码点。第三个方法接受CharSequence,其他方法接受char数组。第二个方法对索引强制执行下限。 |

charCount(int codePoint) 对于可以由单个char表示的字符,返回值为 1。对于需要两个char表示的补充字符,返回值为 2。

字符类中的验证和映射方法

以前使用char原始数据类型的一些方法,如isLowerCase(char)isDigit(char),已被支持补充字符的方法所取代,如isLowerCase(int)isDigit(int)。以前的方法得到支持,但不适用于补充字符。为了创建一个全球应用程序并确保您的代码与任何语言无缝配合,建议您使用这些方法的新形式。

请注意,出于性能原因,大多数接受代码点的方法不会验证代码点参数的有效性。您可以使用isValidCodePoint方法进行验证。

以下表格列出了Character类中的一些验证和映射方法。

方法 描述
isValidCodePoint(int codePoint) 如果代码点在 0x0000 到 0x10FFFF(包括)范围内,则返回 true。
isSupplementaryCodePoint(int codePoint) 如果代码点在 0x10000 到 0x10FFFF(包括)范围内,则返回 true。
isHighSurrogate(char) 如果指定的char在\uD800 到\uDBFF(包括)的高代理范围内,则返回 true。
isLowSurrogate(char) 如果指定的char在\uDC00 到\uDFFF(包括)的低代理范围内,则返回 true。
isSurrogatePair(char high, char low) 如果指定的高代理和低代理代码值表示有效的代理对,则返回 true。
codePointCount(CharSequence, int, int) codePointCount(char[], int, int) 返回CharSequencechar数组中的 Unicode 代码点数。
isLowerCase(int) isUpperCase(int) 如果指定的 Unicode 代码点是小写或大写字符,则返回 true。
isDefined(int) 如果指定的 Unicode 代码点在 Unicode 标准中定义,则返回 true。
isJavaIdentifierStart(char) isJavaIdentifierStart(int) 如果指定的字符或 Unicode 代码点可作为 Java 标识符中的第一个字符,则返回 true。

| isLetter(int) isDigit(int)

isLetterOrDigit(int) | 如果指定的 Unicode 代码点是字母、数字或字母数字,则返回 true。 |

getDirectionality(int) 返回给定 Unicode 代码点的 Unicode 方向性属性。
Character.UnicodeBlock.of(int codePoint) 返回表示包含给定 Unicode 代码点的 Unicode 块的对象,如果代码点不是已定义块的成员,则返回null

字符串类中的方法

StringStringBufferStringBuilder 类也有构造函数和方法,可以处理补充字符。以下表格列出了一些常用方法。要查看可用 API 的完整列表,请参阅 StringStringBufferStringBuilder 类的 javadoc。

构造函数或方法 描述
String(int[] codePoints, int offset, int count) 分配一个新的 String 实例,其中包含来自 Unicode 代码点数组的子数组的字符。

| String.codePointAt(int index) StringBuffer.codePointAt(int index)

StringBuilder.codePointAt(int index) | 返回指定索引处的 Unicode 代码点。 |

| String.codePointBefore(int index) StringBuffer.codePointBefore(int index)

StringBuilder.codePointBefore(int index) | 返回指定索引之前的 Unicode 代码点。 |

| String.codePointCount(int beginIndex, int endIndex) StringBuffer.codePointCount(int beginIndex, int endIndex)

StringBuilder.codePointCount(int beginIndex, int endIndex) | 返回指定范围内的 Unicode 代码点数。 |

StringBuffer.appendCodePoint(int codePoint) StringBuilder.appendCodePoint(int codePoint) 将指定代码点的字符串表示附加到序列。

| String.offsetByCodePoints(int index, int codePointOffset) StringBuffer.offsetByCodePoints(int index, int codePointOffset)

StringBuilder.offsetByCodePoints(int index, int codePointOffset) | 返回给定索引偏移给定代码点数量后的索引。 |

示例用法

原文:docs.oracle.com/javase/tutorial/i18n/text/usage.html

本页面包含一些代码片段,展示了几种常见场景。

从代码点创建 String

String newString(int codePoint) {
    return new String(Character.toChars(codePoint));
}

从代码点创建 String - 为 BMP 字符进行优化

Character.toChars 方法创建一个临时数组,仅使用一次然后丢弃。如果这对性能产生负面影响,你可以使用以下针对 BMP 字符(由单个 char 值表示的字符)进行优化的方法。在这种方法中,toChars 仅用于补充字符。

String newString(int codePoint) {
    if (Character.charCount(codePoint) == 1) {
        return String.valueOf(codePoint);
    } else {
        return new String(Character.toChars(codePoint));
    }
}

批量创建 String 对象

要创建大量字符串,前面代码片段的批量版本重用了 toChars 方法使用的数组。该方法为每个代码点创建一个单独的 String 实例,并针对 BMP 字符进行了优化。

String[] newStrings(int[] codePoints) {
    String[] result = new String[codePoints.length];
    char[] codeUnits = new char[2];
    for (int i = 0; i < codePoints.length; i++) {
        int count = Character.toChars(codePoints[i], codeUnits, 0);
        result[i] = new String(codeUnits, 0, count);
    }
    return result;
}

生成消息

格式化 API 支持补充字符。以下示例是生成消息的简单方法。

// recommended
System.out.printf("Character %c is invalid.%n", codePoint);

以下方法简单且避免了连接,这使得文本更难本地化,因为并非所有语言都按照英语的顺序将数字值插入字符串中。

// not recommended
System.out.println("Character " + String.valueOf(char) + " is invalid.");

设计考虑事项

原文:docs.oracle.com/javase/tutorial/i18n/text/design.html

要编写能够无缝运行于任何语言和任何脚本的代码,有几点需要牢记。

考虑事项 原因
避免使用char数据类型的方法。 避免使用char原始数据类型或使用char数据类型的方法,因为使用该数据类型的代码对补充字符不起作用。对于需要char类型参数的方法,尽可能使用相应的int方法。例如,使用Character.isDigit(int)方法而不是Character.isDigit(char)方法。
使用isValidCodePoint方法验证代码点值。 代码点被定义为int数据类型,允许值超出从 0x0000 到 0x10FFFF 的有效代码点值范围。出于性能原因,接受代码点值作为参数的方法不会检查参数的有效性,但您可以使用isValidCodePoint方法检查该值。
使用codePointCount方法计算字符数。 String.length()方法返回字符串中代码单元或 16 位char值的数量。如果字符串包含补充字符,则计数可能会误导,因为它不会反映真实的代码点数量。要准确计算字符数(包括补充字符),请使用codePointCount方法。
使用String.toUpperCase(int codePoint)String.toLowerCase(int codePoint)方法而不是Character.toUpperCase(int codePoint)Character.toLowerCase(int codePoint)方法。 虽然Character.toUpperCase(int)Character.toLowerCase(int)方法可以处理代码点值,但有些字符无法进行一对一转换。例如,德语小写字符ß在转换为大写时变为两个字符 SS。同样,希腊语小写 Sigma 字符在字符串中的位置不同而有所不同。Character.toUpperCase(int)Character.toLowerCase(int)方法无法处理这些情况;然而,String.toUpperCaseString.toLowerCase方法可以正确处理这些情况。
删除字符时要小心。 在调用StringBuilder.deleteCharAt(int index)StringBuffer.deleteCharAt(int index)方法时,索引指向补充字符时,只会删除该字符的前半部分(第一个char值)。首先,调用Character.charCount方法对字符进行检查,以确定必须删除一个或两个char值。
在对序列中的字符进行反转时要小心。当在包含补充字符的文本上调用StringBuffer.reverse()StringBuilder.reverse()方法时,高低代理对会被反转,导致不正确甚至可能无效的代理对。

更多信息

原文:docs.oracle.com/javase/tutorial/i18n/text/info.html

有关补充字符的更多信息,请参考以下资源。

  • Java 平台中的补充字符

  • Unicode Consoritum

  • Unicode 术语表

检测文本边界

原文:docs.oracle.com/javase/tutorial/i18n/text/boundaryintro.html

操纵文本的应用程序需要定位文本内的边界。例如,考虑一些文字处理器的常见功能:突出显示一个字符,剪切一个单词,将光标移动到下一个句子,以及在行尾换行一个单词。为了执行这些功能,文字处理器必须能够检测文本中的逻辑边界。幸运的是,您不必编写自己的例程来执行边界分析。相反,您可以利用BreakIterator类提供的方法。

关于 BreakIterator 类

本节讨论了BreakIterator类的实例化方法和虚拟光标。

字符边界

在本节中,您将了解用户字符和 Unicode 字符之间的区别,以及如何使用BreakIterator定位用户字符。

词边界

如果您的应用程序需要在文本中选择或定位单词,使用BreakIterator会很有帮助。

句子边界

确定句子边界可能会有问题,因为许多书面语言中句子终止符的使用是模棱两可的。本节将讨论您可能遇到的一些问题,以及BreakIterator如何处理这些问题。

行边界

本节描述了如何使用BreakIterator在文本字符串中定位潜在的换行符。

关于 BreakIterator 类

原文:docs.oracle.com/javase/tutorial/i18n/text/about.html

BreakIterator类是区域敏感的,因为文本边界随语言而变化。例如,换行的语法规则并非所有语言都相同。要确定BreakIterator类支持哪些区域设置,请调用getAvailableLocales方法,如下所示:

Locale[] locales = BreakIterator.getAvailableLocales();

您可以使用BreakIterator类分析四种边界类型:字符、单词、句子和潜在的换行符。在实例化BreakIterator时,调用适当的工厂方法:

  • getCharacterInstance

  • getWordInstance

  • getSentenceInstance

  • getLineInstance

每个BreakIterator实例只能检测一种类型的边界。例如,如果您想定位字符和单词边界,您需要创建两个单独的实例。

BreakIterator具有一个想象的光标,指向文本字符串中的当前边界。您可以使用previousnext方法在文本中移动此光标。例如,如果您使用getWordInstance创建了一个BreakIterator,每次调用next方法时,光标都会移动到文本中的下一个单词边界。光标移动方法返回一个整数,指示边界的位置。此位置是文本字符串中将跟随边界的字符的索引。与字符串索引一样,边界是从零开始的。第一个边界在 0 处,最后一个边界是字符串的长度。以下图显示了nextprevious方法在文本行中检测到的单词边界:

带有边界指示的文本“希望是羽毛的东西”。

*此图已经缩小以适应页面。

点击图像以查看其自然大小。*

您应该仅将BreakIterator类与自然语言文本一起使用。要对编程语言进行标记化,请使用StreamTokenizer类。

接下来的部分为每种边界分析类型提供示例。编码示例来自名为BreakIteratorDemo.java的源代码文件。

字符边界

原文:docs.oracle.com/javase/tutorial/i18n/text/char.html

如果您的应用程序允许最终用户突出显示单个字符或逐个字符地移动光标穿过文本,则需要定位字符边界。要创建一个定位字符边界的BreakIterator,您可以调用getCharacterInstance方法,如下所示:

BreakIterator characterIterator =
    BreakIterator.getCharacterInstance(currentLocale);

这种类型的BreakIterator检测用户字符之间的边界,而不仅仅是 Unicode 字符。

用户字符可能由多个 Unicode 字符组成。例如,用户字符ü可以由组合 Unicode 字符\u0075(u)和\u00a8(¨)组成。然而,这并不是最好的例子,因为字符ü也可以用单个 Unicode 字符\u00fc 表示。我们将借助阿拉伯语言来举一个更现实的例子。

在阿拉伯语中,房子的单词是:

房子的阿拉伯象形文字

这个词包含三个用户字符,但它由以下六个 Unicode 字符组成:

String house = "\u0628" + "\u064e" + "\u064a" + "\u0652" + "\u067a" + "\u064f";

字符串house中位置 1、3 和 5 的 Unicode 字符是变音符号。阿拉伯语需要变音符号,因为它们可以改变单词的含义。示例中的变音符号是非间隔字符,因为它们出现在基本字符的上方。在阿拉伯语文字处理器中,您不能在屏幕上每个 Unicode 字符移动一次光标。相反,您必须为每个用户字符移动一次光标,这可能由多个 Unicode 字符组成。因此,您必须使用BreakIterator来扫描字符串中的用户字符。

示例程序BreakIteratorDemo创建一个BreakIterator来扫描阿拉伯字符。程序将这个BreakIterator与先前创建的String对象一起传递给名为listPositions的方法:

BreakIterator arCharIterator = BreakIterator.getCharacterInstance(
                                   new Locale ("ar","SA"));
listPositions (house, arCharIterator);

listPositions方法使用BreakIterator来定位字符串中的字符边界。请注意,BreakIteratorDemo使用setText方法将特定字符串分配给BreakIterator。程序使用first方法检索第一个字符边界,然后调用next方法,直到返回常量BreakIterator.DONE。此例程的代码如下:

static void listPositions(String target, BreakIterator iterator) {

    iterator.setText(target);
    int boundary = iterator.first();

    while (boundary != BreakIterator.DONE) {
        System.out.println (boundary);
        boundary = iterator.next();
    }
}

listPositions方法打印出字符串house中用户字符的以下边界位置。请注意,变音符号的位置(1、3、5)没有列出:

0
2
4
6

单词边界

原文:docs.oracle.com/javase/tutorial/i18n/text/word.html

您调用getWordIterator方法来实例化一个检测单词边界的BreakIterator

BreakIterator wordIterator =
    BreakIterator.getWordInstance(currentLocale);

当您的应用程序需要对单词执行操作时,您会想要创建这样一个BreakIterator。这些操作可能是常见的单词处理功能,如选择、剪切、粘贴和复制。或者,您的应用程序可能会搜索单词,并且必须能够区分整个单词和简单字符串。

BreakIterator分析单词边界时,它区分单词和不属于单词的字符。这些字符包括空格、制表符、标点符号和大多数符号,在两侧都有单词边界。

接下来的示例来自程序BreakIteratorDemo,标记了一些文本中的单词边界。该程序创建了BreakIterator,然后调用markBoundaries方法:

Locale currentLocale = new Locale ("en","US");

BreakIterator wordIterator =
    BreakIterator.getWordInstance(currentLocale);

String someText = "She stopped. " +
    "She said, \"Hello there,\" and then went " +
    "on.";

markBoundaries(someText, wordIterator);

markBoundaries方法在BreakIteratorDemo.java中定义。该方法通过在目标字符串下方打印插入符号(^)来标记边界。在接下来的代码中,请注意while循环,其中markBoundaries通过调用next方法扫描字符串:

static void markBoundaries(String target, BreakIterator iterator) {

    StringBuffer markers = new StringBuffer();
    markers.setLength(target.length() + 1);
    for (int k = 0; k < markers.length(); k++) {
        markers.setCharAt(k,' ');
    }

    iterator.setText(target);
    int boundary = iterator.first();

    while (boundary != BreakIterator.DONE) {
        markers.setCharAt(boundary,'^');
        boundary = iterator.next();
    }

    System.out.println(target);
    System.out.println(markers);
}

markBoundaries方法的输出如下。请注意插入符号(^)相对于标点符号和空格的位置:

She stopped.  She said, "Hello there," and then
^  ^^      ^^ ^  ^^   ^^^^    ^^    ^^^^  ^^   ^

went on.
^   ^^ ^^

BreakIterator类使得从文本中选择单词变得容易。您不必编写自己的处理各种语言标点规则的例程;BreakIterator类会为您处理这些。

以下示例中的extractWords方法提取并打印给定字符串的单词。请注意,该方法使用Character.isLetterOrDigit来避免打印包含空格字符的“单词”。

static void extractWords(String target, BreakIterator wordIterator) {

    wordIterator.setText(target);
    int start = wordIterator.first();
    int end = wordIterator.next();

    while (end != BreakIterator.DONE) {
        String word = target.substring(start,end);
        if (Character.isLetterOrDigit(word.charAt(0))) {
            System.out.println(word);
        }
        start = end;
        end = wordIterator.next();
    }
}

BreakIteratorDemo程序调用extractWords,将其传递给前面示例中使用的相同目标字符串。extractWords方法打印出以下单词列表:

She
stopped
She
said
Hello
there
and
then
went
on

句子边界

原文:docs.oracle.com/javase/tutorial/i18n/text/sentence.html

你可以使用BreakIterator来确定句子边界。首先通过getSentenceInstance方法创建一个BreakIterator

BreakIterator sentenceIterator =
    BreakIterator.getSentenceInstance(currentLocale);

为了显示句子边界,程序使用了markBoundaries方法,该方法在单词边界一节中有讨论。markBoundaries方法在字符串下方打印插入符号(^)来指示边界位置。以下是一些示例:

She stopped.  She said, "Hello there," and then went on.
^             ^                                         ^

He's vanished!  What will we do?  It's up to us.
^               ^                 ^             ^

Please add 1.5 liters to the tank.
^                                 ^

行边界

原文:docs.oracle.com/javase/tutorial/i18n/text/line.html

应用程序格式化文本或执行换行操作必须找到潜在的换行位置。您可以使用使用getLineInstance方法创建的BreakIterator来找到这些换行位置或边界:

BreakIterator lineIterator =
    BreakIterator.getLineInstance(currentLocale);

这个BreakIterator确定字符串中文本可以断开以继续到下一行的位置。BreakIterator检测到的位置是潜在的换行位置。在屏幕上显示的实际换行可能不同。

接下来的两个示例使用BreakIteratorDemo.javamarkBoundaries方法来显示BreakIterator检测到的行边界。markBoundaries方法通过在目标字符串下方打印插入符号(^)来指示行边界。

根据BreakIterator,在一系列空格字符(空格、制表符、换行符)的终止后发生行边界。在下面的示例中,请注意您可以在检测到的任何边界处断开行:

She stopped.  She said, "Hello there," and then went on.
^   ^         ^   ^     ^      ^     ^ ^   ^    ^    ^  ^

潜在的换行位置也会在连字符后立即发生:

There are twenty-four hours in a day.
^     ^   ^      ^    ^     ^  ^ ^   ^

下一个示例将长文本字符串分成固定长度的行,使用名为formatLines的方法。该方法使用BreakIterator来定位潜在的换行位置。formatLines方法简短、简单,并且由于使用了BreakIterator,与语言环境无关。以下是源代码:

static void formatLines(
    String target, int maxLength,
    Locale currentLocale) {

    BreakIterator boundary = BreakIterator.
        getLineInstance(currentLocale);
    boundary.setText(target);
    int start = boundary.first();
    int end = boundary.next();
    int lineLength = 0;

    while (end != BreakIterator.DONE) {
        String word = target.substring(start,end);
        lineLength = lineLength + word.length();
        if (lineLength >= maxLength) {
            System.out.println();
            lineLength = word.length();
        }
        System.out.print(word);
        start = end;
        end = boundary.next();
    }
}

BreakIteratorDemo程序调用formatLines方法如下:

String moreText =
    "She said, \"Hello there,\" and then " +
    "went on down the street. When she stopped " +
    "to look at the fur coats in a shop + "
    "window, her dog growled. \"Sorry Jake,\" " +
    "she said. \"I didn't know you would take " +
    "it personally.\"";

formatLines(moreText, 30, currentLocale);

调用formatLines的输出为:

She said, "Hello there," and
then went on down the
street. When she stopped to
look at the fur coats in a
shop window, her dog
growled. "Sorry Jake," she
said. "I didn't know you
would take it personally."

将拉丁数字转换为其他 Unicode 数字

原文:docs.oracle.com/javase/tutorial/i18n/text/shapedDigits.html

默认情况下,当文本包含数字值时,这些值将使用拉丁(欧洲)数字显示。如果希望使用其他 Unicode 数字形状,请使用java.awt.font.NumericShaper类。NumericShaper API 使您能够以任何 Unicode 数字形状显示内部表示为 ASCII 值的数字值。

下面的代码片段来自ArabicDigits示例,展示了如何使用NumericShaper实例将拉丁数字转换为阿拉伯数字。确定整形操作的行已加粗。

ArabicDigitsPanel(String fontname) {
    HashMap map = new HashMap();
    Font font = new Font(fontname, Font.PLAIN, 60);
    map.put(TextAttribute.FONT, font);
    map.put(TextAttribute.NUMERIC_SHAPING,
        NumericShaper.getShaper(NumericShaper.ARABIC));

    FontRenderContext frc = new FontRenderContext(null, false, false);
    layout = new TextLayout(text, map, frc);
}

// ...

public void paint(Graphics g) {
    Graphics2D g2d = (Graphics2D)g;
    layout.draw(g2d, 10, 50);
}

获取阿拉伯数字的NumericShaper实例,并将其放入HashMap中,用于TextLayout.NUMERIC_SHAPING属性键。哈希映射传递给TextLayout实例。在paint方法中呈现文本后,数字以所需脚本显示。在此示例中,拉丁数字 0 到 9 以阿拉伯数字形式显示。

ArabicDigits 示例输出显示从 0 到 9 的阿拉伯数字

前面的示例使用NumericShaper.ARABIC常量来检索所需的整形器,但NumericShaper类为许多语言提供了常量。这些常量被定义为位掩码,并称为NumericShaper 基于位掩码的常量

基于枚举的范围常量

指定特定数字集的另一种方法是使用NumericShaper.Range枚举类型(enum)。这个枚举在 Java SE 7 版本中引入,还提供了一组常量。尽管这些常量是使用不同的机制定义的,但NumericShaper.ARABIC位掩码在功能上等同于NumericShaper.Range.ARABIC枚举,并且每种常量类型都有相应的getShaper方法:

  • getShaper(int singleRange)

  • getShaper(NumericShaper.Range singleRange)

ArabicDigitsEnum示例与 ArabicDigits 示例相同,只是使用NumericShaper.Range枚举来指定语言脚本:

ArabicDigitsEnumPanel(String fontname) {
    HashMap map = new HashMap();
    Font font = new Font(fontname, Font.PLAIN, 60);
    map.put(TextAttribute.FONT, font);
    map.put(TextAttribute.NUMERIC_SHAPING,
        NumericShaper.getShaper(NumericShaper.Range.ARABIC));
    FontRenderContext frc = new FontRenderContext(null, false, false);
    layout = new TextLayout(text, map, frc);
}

两个getShaper方法都接受singleRange参数。使用任一常量类型,你都可以指定一组特定于脚本的数字范围。基于位掩码的常量可以使用OR操作符组合,或者你可以创建一组NumericShaper.Range枚举。以下显示了如何使用每种常量类型定义范围:

NumericShaper.MONGOLIAN | NumericShaper.THAI |
NumericShaper.TIBETAN
EnumSet.of(
    NumericShaper.Range.MONGOLIAN,
    NumericShaper.Range.THAI,
    NumericShaper.Range.TIBETAN)

你可以查询NumericShaper对象,以确定它支持哪些范围,使用基于位掩码的整形器的getRanges方法或基于枚举的整形器的getRangeSet方法。


注意:

你可以使用传统的基于位掩码的常量或Range枚举常量。在决定使用哪种时,有一些考虑因素:

  • Range API 需要 JDK 7 或更高版本。

  • Range API 覆盖的 Unicode 范围比位掩码 API 多。

  • 位掩码 API 比Range API 快一点。


根据语言环境渲染数字

ArabicDigits 示例旨在使用特定语言的整形器,但有时必须根据语言环境渲染数字。例如,如果数字之前的文本使用泰语书写,则优先使用泰语数字。如果文本显示为藏文,则优先使用藏文数字。

你可以使用其中一个getContextualShaper方法来实现这一点。

  • getContextualShaper(int ranges)

  • getContextualShaper(int ranges, int defaultContext)

  • getContextualShaper(Set<NumericShaper.Range> ranges)

  • getContextualShaper(Set<NumericShaper.Range> ranges, NumericShaper.Range defaultContext)

前两种方法使用位掩码常量,后两种使用枚举常量。接受defaultContext参数的方法允许你指定在数字值显示在文本之前时使用的初始整形器。当没有定义默认上下文时,任何前导数字都将使用拉丁形状显示。

ShapedDigits 示例展示了整形器的工作原理。显示了五种文本布局:

  1. 第一个布局不使用整形器;所有数字都显示为拉丁文。

  2. 第二个布局将所有数字形状为阿拉伯数字,不考虑语言环境。

  3. 第三个布局使用了一个使用阿拉伯数字的上下文整形器。默认上下文被定义为阿拉伯文。

  4. 第四个布局使用了一个使用阿拉伯数字的上下文形状器,但该形状器没有指定默认上下文。

  5. 第五个布局使用了一个使用ALL_RANGES位掩码的上下文形状器,但该形状器没有指定默认上下文。

ShapedDigits 示例输出,展示上下文形状器的工作原理

以下代码行展示了如果使用形状器,则如何定义:

  1. 没有使用形状器。

  2. NumericShaper arabic = NumericShaper.getShaper(NumericShaper.ARABIC);

  3. NumericShaper contextualArabic = NumericShaper.getContextualShaper(NumericShaper.ARABIC, NumericShaper.ARABIC);

  4. NumericShaper contextualArabicASCII = NumericShaper.getContextualShaper(NumericShaper.ARABIC);

  5. NumericShaper contextualAll = NumericShaper.getContextualShaper(NumericShaper.ALL_RANGES);

查看ShapedDigits.java示例以获取更多实现细节。

转换非 Unicode 文本

原文:docs.oracle.com/javase/tutorial/i18n/text/convertintro.html

在 Java 编程语言中,char值表示 Unicode 字符。Unicode 是一种支持世界主要语言的 16 位字符编码。您可以在Unicode 联盟网站了解有关 Unicode 标准的更多信息。

目前很少有文本编辑器支持 Unicode 文本输入。我们用来编写本节代码示例的文本编辑器仅支持 ASCII 字符,这些字符仅限于 7 位。为了表示 ASCII 无法表示的 Unicode 字符,例如ö,我们使用\uXXXX转义序列。转义序列中的每个X都是一个十六进制数字。以下示例显示了如何使用转义序列表示ö字符:

String str = "\u00F6";
char c = '\u00F6';
Character letter = new Character('\u00F6');

世界各地的系统使用各种字符编码。目前,很少有这些编码符合 Unicode 标准。因为您的程序期望字符为 Unicode 格式,所以从系统获取的文本数据必须转换为 Unicode 格式,反之亦然。当文本文件的编码与 Java 虚拟机的默认文件编码匹配时,文本数据会自动转换为 Unicode 格式。您可以通过创建一个OutputStreamWriter并询问其规范名称来确定默认文件编码:

OutputStreamWriter out = new OutputStreamWriter(new ByteArrayOutputStream());
System.out.println(out.getEncoding());

如果默认文件编码与您要处理的文本数据的编码不同,则必须自行进行转换。当处理来自其他国家或计算平台的文本时,您可能需要这样做。

本节讨论了用于将非 Unicode 文本转换为 Unicode 的 API。在使用这些 API 之前,您应该验证要转换为 Unicode 的字符编码是否受支持。支持的字符编码列表不是 Java 编程语言规范的一部分。因此,API 支持的字符编码可能因平台而异。要查看 Java 开发工具包支持的编码,请参阅支持的编码文档。

接下来的内容描述了将非 Unicode 文本转换为 Unicode 的两种技术。您可以将非 Unicode 字节数组转换为String对象,反之亦然。或者您可以在 Unicode 字符流和非 Unicode 文本的字节流之间进行转换。

字节编码和字符串

本节向您展示如何将非 Unicode 字节数组转换为String对象,反之亦然。

字符流和字节流

在本节中,您将学习如何在 Unicode 字符流和非 Unicode 文本的字节流之间进行转换。

字节编码和字符串

原文:docs.oracle.com/javase/tutorial/i18n/text/string.html

如果字节数组包含非 Unicode 文本,您可以使用 String 构造方法之一将文本转换为 Unicode。反之,您可以使用 String.getBytes 方法将 String 对象转换为非 Unicode 字符的字节数组。在调用这些方法时,您需要将编码标识符作为参数之一指定。

接下来的示例将字符在 UTF-8 和 Unicode 之间进行转换。UTF-8 是一种对 UNIX 文件系统安全的 Unicode 传输格式。示例的完整源代码在文件 StringConverter.java 中。

StringConverter 程序首先创建一个包含 Unicode 字符的 String

String original = new String("A" + "\u00ea" + "\u00f1" + "\u00fc" + "C");

当打印时,名为 originalString 如下所示:

AêñüC

要将 String 对象转换为 UTF-8,调用 getBytes 方法并指定适当的编码标识符作为参数。getBytes 方法以 UTF-8 格式返回一个字节数组。要从非 Unicode 字节的数组创建 String 对象,请使用带有编码参数的 String 构造方法。执行这些调用的代码被封装在一个 try 块中,以防指定的编码不受支持:

try {
    byte[] utf8Bytes = original.getBytes("UTF8");
    byte[] defaultBytes = original.getBytes();

    String roundTrip = new String(utf8Bytes, "UTF8");
    System.out.println("roundTrip = " + roundTrip);
    System.out.println();
    printBytes(utf8Bytes, "utf8Bytes");
    System.out.println();
    printBytes(defaultBytes, "defaultBytes");
} 
catch (UnsupportedEncodingException e) {
    e.printStackTrace();
}

StringConverter 程序打印出 utf8BytesdefaultBytes 数组中的值,以演示一个重要的观点:转换后的文本长度可能与源文本长度不同。一些 Unicode 字符转换为单个字节,而另一些转换为一对或三元组字节。

printBytes 方法通过调用在源文件 UnicodeFormatter.java 中定义的 byteToHex 方法来显示字节数组。以下是 printBytes 方法:

public static void printBytes(byte[] array, String name) {
    for (int k = 0; k < array.length; k++) {
        System.out.println(name + "[" + k + "] = " + "0x" +
            UnicodeFormatter.byteToHex(array[k]));
    }
}

printBytes 方法的输出如下。请注意,只有第一个和最后一个字节,即 A 和 C 字符,在两个数组中是相同的:

utf8Bytes[0] = 0x41
utf8Bytes[1] = 0xc3
utf8Bytes[2] = 0xaa
utf8Bytes[3] = 0xc3
utf8Bytes[4] = 0xb1
utf8Bytes[5] = 0xc3
utf8Bytes[6] = 0xbc
utf8Bytes[7] = 0x43
defaultBytes[0] = 0x41
defaultBytes[1] = 0xea
defaultBytes[2] = 0xf1
defaultBytes[3] = 0xfc
defaultBytes[4] = 0x43

字符流和字节流

原文:docs.oracle.com/javase/tutorial/i18n/text/stream.html

java.io包提供了允许您在 Unicode 字符流和非 Unicode 文本的字节流之间进行转换的类。使用InputStreamReader类,您可以将字节流转换为字符流。您可以使用OutputStreamWriter类将字符流转换为字节流。以下图示说明了转换过程:

此图表示转换过程

当您创建InputStreamReaderOutputStreamWriter对象时,您需要指定要转换的字节编码。例如,要将 UTF-8 编码的文本文件转换为 Unicode,您可以创建一个InputStreamReader如下:

FileInputStream fis = new FileInputStream("test.txt");
InputStreamReader isr = new InputStreamReader(fis, "UTF8");

如果省略编码标识符,InputStreamReaderOutputStreamWriter将依赖于默认编码。您可以通过调用getEncoding方法来确定InputStreamReaderOutputStreamWriter使用的编码,如下所示:

InputStreamReader defaultReader = new InputStreamReader(fis);
String defaultEncoding = defaultReader.getEncoding();

下面的示例向您展示如何使用InputStreamReaderOutputStreamWriter类执行字符集转换。此示例的完整源代码在StreamConverter.java中。此程序显示日文字符。在尝试之前,请验证系统上是否已安装适当的字体。如果您使用与版本 1.1 兼容的 JDK 软件,请复制font.properties文件,然后用font.properties.ja文件替换它。

StreamConverter程序将String对象中的一系列 Unicode 字符转换为以 UTF-8 编码的字节的FileOutputStream。执行转换的方法称为writeOutput

static void writeOutput(String str) {
    try {
        FileOutputStream fos = new FileOutputStream("test.txt");
        Writer out = new OutputStreamWriter(fos, "UTF8");
        out.write(str);
        out.close();
    } 
    catch (IOException e) {
        e.printStackTrace();
    }
}

readInput方法从由writeOutput方法创建的文件中读取以 UTF-8 编码的字节。InputStreamReader对象将 UTF-8 编码的字节转换为 Unicode,并以String形式返回结果。readInput方法如下:

static String readInput() {
    StringBuffer buffer = new StringBuffer();
    try {
        FileInputStream fis = new FileInputStream("test.txt");
        InputStreamReader isr = new InputStreamReader(fis, "UTF8");
        Reader in = new BufferedReader(isr);
        int ch;
        while ((ch = in.read()) > -1) {
            buffer.append((char)ch);
        }
        in.close();
        return buffer.toString();
    } 
    catch (IOException e) {
        e.printStackTrace();
        return null;
    }
}

StreamConverter程序的main方法调用writeOutput方法创建一个以 UTF-8 编码的字节文件。readInput方法读取相同的文件,将字节转换回 Unicode。以下是main方法的源代码:

public static void main(String[] args) {
    String jaString = new String("\u65e5\u672c\u8a9e\u6587\u5b57\u5217");
    writeOutput(jaString); 
    String inputString = readInput();
    String displayString = jaString + " " + inputString;
    new ShowString(displayString, "Conversion Demo");
}

原始字符串(jaString)应与新创建的字符串(inputString)完全相同。为了显示这两个字符串相同,程序将它们连接起来,并使用 ShowString 对象显示它们。ShowString 类使用 Graphics.drawString 方法显示字符串。此类的源代码在 ShowString.java 中。当 StreamConverter 程序实例化 ShowString 时,会出现以下窗口。显示的字符重复验证了这两个字符串是相同的:

这是 StreamConverter 程序的屏幕截图

规范化文本

原文:docs.oracle.com/javase/tutorial/i18n/text/normalizerapi.html

规范化是一种过程,通过该过程您可以对文本执行某些转换,使其在以前可能无法协调的方式中协调。比如说,您想要搜索或排序文本,在这种情况下,您需要将该文本规范化以考虑应表示为相同文本的代码点。

什么可以被规范化?当您需要转换带有变音符号的字符,更改所有字母大小写,分解连字,或将半角片假名字符转换为全角字符等时,规范化是适用的。

根据Unicode 标准附录#15,规范化器的 API 支持java.text.Normalizer.Form中定义的以下四种 Unicode 文本规范化形式:

  • 规范化形式 D(NFD):规范分解

  • 规范化形式 C(NFC):规范分解,然后规范组合

  • 规范化形式 KD(NFKD):兼容性分解

  • 规范化形式 KC(NFKC):兼容性分解,然后规范组合

让我们看看拉丁小写字母"o"带分音符号如何通过使用这些规范化形式进行规范化:

原始单词 NFC NFD NFKC NFKD
"schön" "schön" "scho\u0308n" "schön" "scho\u0308n"

您可以注意到在 NFC 和 NFKC 中原始单词保持不变。这是因为在 NFD 和 NFKD 中,复合字符被映射到它们的规范分解形式。但是在 NFC 和 NFKC 中,如果可能的话,组合字符序列被映射到复合字符。对于分音符号,没有复合字符,因此在 NFC 和 NFKC 中保持分解状态。

在稍后表示的代码示例NormSample.java中,您还可以注意到另一个规范化特性。半角和全角片假名字符将具有相同的兼容性分解,因此它们是兼容性等效的。但是,它们不是规范等效的。

要确保您确实需要规范化文本,您可以使用isNormalized方法来确定给定的字符序列是否已规范化。如果此方法返回 false,则意味着您需要规范化此序列,并且应使用normalize方法,该方法根据指定的规范化形式规范化char值。例如,要将文本转换为规范分解形式,您将需要使用以下normalize方法:

normalized_string = Normalizer.normalize(target_chars, Normalizer.Form.NFD);

此外,规范化方法会将重音符号重新排列为正确的规范顺序,因此您无需担心自行重新排列重音符号。

以下示例表示一个应用程序,使您能够选择规范化形式和模板进行规范化:


注意: 如果您看不到 applet 运行,请安装至少 Java SE Development Kit (JDK) 7 版本。


这个 applet 的完整代码在 NormSample.java 中。

使用 JTextComponent 类处理双向文本

原文:docs.oracle.com/javase/tutorial/i18n/text/bidi.html

本节讨论如何使用JTextComponent类处理双向文本。双向文本是包含从左到右和从右到左两个方向运行的文本。双向文本的一个示例是包含从右到左运行的阿拉伯文本(包含从左到右运行的数字)。显示和管理双向文本更加困难;但是JTextComponent会为您处理这些问题。

下面涵盖了以下主题:

  • 确定双向文本的方向性

  • 显示和移动插入符

  • 命中测试

  • 高亮选择

  • 设置组件方向

欲了解更多信息,或者如果您想更好地控制处理这些问题,请参阅处理双向文本在 2D 图形教程中。

确定双向文本的方向性

示例BidiTextComponentDemo.java,基于TextComponentDemo.java,在JTextPane对象中显示双向文本。在大多数情况下,Java 平台可以确定双向 Unicode 文本的方向性:

BidiTextComponentDemo.java

在 JTextComponent 对象中明确指定文本运行方向

您可以指定JTextComponent对象的Document对象的运行方向。例如,以下语句指定JTextPane对象textPane中的文本从右到左运行:

textPane.getDocument().putProperty(
    TextAttribute.RUN_DIRECTION,
    TextAttribute.RUN_DIRECTION_RTL);

或者,您可以根据语言环境指定特定 Swing 组件的组件方向。例如,以下语句指定对象textPane的组件方向基于 ar-SA 语言环境:

Locale arabicSaudiArabia = 
    new Locale.Builder().setLanguage("ar").setRegion("SA").build();

textPane.setComponentOrientation(
    ComponentOrientation.getOrientation(arabicSaudiArabia));

因为阿拉伯语的运行方向是从右到左,所以textPane对象中包含的文本的运行方向也是从右到左。

有关更多信息,请参阅设置组件方向。

显示和移动插入符

在可编辑文本中,插入符用于图形表示当前插入点,即文本中新字符将插入的位置。在BidiTextComponentDemo.java示例中,插入符包含一个小三角形,指向插入字符将显示的方向。

默认情况下,JTextComponent对象创建一个键映射(类型为Keymap),该键映射作为所有JTextComponent实例共享的默认键映射。键映射允许应用程序将按键绑定到操作。默认键映射(用于支持插入符移动的JTextComponent对象)包括将插入符向前和向后移动与左右箭头键绑定,从而支持通过双向文本移动插入符。

点击测试

通常,设备空间中的位置必须转换为文本偏移量。例如,当用户在可选择文本上单击鼠标时,鼠标的位置将被转换为文本偏移量,并用作选择范围的一端。从逻辑上讲,这是定位插入符的逆过程。

您可以将插入符监听器附加到JTextComponent的实例上。插入符监听器使您能够处理插入符事件,这些事件发生在插入符移动或文本组件中的选择更改时。您可以使用addCaretListener方法附加插入符监听器。有关更多信息,请参见如何编写插入符监听器。

高亮选择

一段选定的字符范围在图形上由一个高亮区域表示,该区域是以反色或不同背景颜色显示字形的区域。

JTextComponent对象实现了逻辑高亮。这意味着选定的字符在文本模型中始终是连续的,而高亮区域可以是不连续的。以下是逻辑高亮的示例:

BidiTextComponentDemo:逻辑高亮

设置组件方向

Swing 的布局管理器了解区域设置如何影响用户界面;不需要为每个区域设置创建新的布局。例如,在文本从右到左流动的区域,布局管理器将以相同的方向排列组件。

示例 InternationalizedMortgageCalculator.java 已本地化为英语,美国;英语,英国;法语,法国;法语,加拿大;以及阿拉伯语,沙特阿拉伯。

以下示例使用 en-US 区域设置:

按揭计算器,en-US 区域设置

以下示例使用 ar-SA 区域设置:

按揭计算器,ar-SA 区域设置

请注意,组件的布局与相应区域设置的方向相同:en-US 为从左到右,ar-SA 为从右到左。示例 InternationalizedMortgageCalculator.java 调用方法 applyComponentOrientationgetOrientation 来指定其组件的方向:

private static JFrame frame;

// ...

private static void createAndShowGUI(Locale currentLocale) {

    // Create and set up the window.
    // ...
    // Add contents to the window.
    // ...
    frame.applyComponentOrientation(
        ComponentOrientation.getOrientation(currentLocale));
    // ...
  }

示例 InternationalizedMortgageCalculator.java 需要以下资源文件:

  • resources/Resources.properties

  • resources/Resources_ar.properties

  • resources/Resources_fr.properties

课程:网络资源的国际化

原文:docs.oracle.com/javase/tutorial/i18n/network/index.html

在现代互联网社区中,许多用户不再满足于仅使用 ASCII 符号来识别域名或网络资源。例如,他们希望能够注册一个新的域名,使用阿拉伯语或中文中的本地字符。这就是为什么网络资源的国际化是拓宽万维网视野的基石。

本课程描述了网络域名资源的国际化。

国际化域名

国际化域名

原文:docs.oracle.com/javase/tutorial/i18n/network/idn.html

从历史上看,互联网域名仅包含 ASCII 符号。随着互联网的普及和在全球范围内的采用,支持域名的国际化变得必要,特别是支持包含 Unicode 字符的域名。

应用程序中的国际化域名(IDNA)机制被采用为将 Unicode 字符转换为标准 ASCII 域名的标准,从而保持域名系统的稳定性。该系统执行查找服务,将用户友好的名称转换为网络地址。

国际化域名的示例:

  • http://清华大学.cn

  • http://www.транспорт.com

如果您点击这些链接,您会看到地址栏中表示的 Unicode 域名被替换为 ASCII 字符串。

要在应用程序中实现类似的功能,java.net.IDN类提供了方法,用于在 ASCII 和非 ASCII 格式之间转换域名。

方法 目的
toASCII(String) toASCII(String, flag) 在将 IDN 发送到域名解析系统或写入期望 ASCII 字符的文件(例如 DNS 主文件)之前使用。如果输入字符串不符合RFC 3490,这些方法会抛出IllegalArgumentException异常。
toUnicode(String) toUnicode(String, flag) 在向用户显示名称时使用,例如从 DNS 区域获取的名称。此方法将字符串从 ASCII 兼容编码(ACE)转换为 Unicode 代码点。此方法永远不会失败;在出现错误时,输入字符串保持不变并原样返回。

可选的flag参数指定转换过程的行为。ALLOW_UNASSIGNED标志允许包含 Unicode 3.2 中未分配的代码点。USE_STD3_ASCII_RULES标志确保遵守 STD-3 ASCII 规则。您可以单独使用这些标志或逻辑上合并它们。如果不需要任何标志,请使用该方法的单参数版本。

教程:国际化的服务提供者

原文:docs.oracle.com/javase/tutorial/i18n/serviceproviders/index.html

国际化的服务提供者使得可以插入区域相关的数据和服务。由于可以插入区域相关的数据和服务,第三方能够提供java.textjava.util包中大多数区域敏感类的实现。

服务是一组提供对特定应用程序功能或特性访问的编程接口和类。服务提供者接口(SPI)是服务定义的一组公共接口和抽象类。服务提供者实现了 SPI。服务提供者使您能够创建可扩展的应用程序,可以在不修改其原始代码库的情况下进行扩展。您可以通过新的插件或模块增强其功能。有关服务提供者和可扩展应用程序的更多信息,请参阅创建可扩展应用程序。

您可以使用国际化的服务提供者来提供以下区域敏感类的自定义实现:

  • BreakIterator 对象

  • Collator 对象

  • Locale 类的语言代码、国家代码和变体名称

  • 时区名称

  • 货币符号

  • DateFormat 对象

  • DateFormatSymbols 对象

  • NumberFormat 对象

  • DecimalFormatSymbols 对象

相应的 SPI 包含在java.text.spijava.util.spi包中:

java.util.spi java.text.spi

|

  • CurrencyNameProvider

  • LocaleServiceProvider

  • TimeZoneNameProvider

|

  • BreakIteratorProvider

  • CollatorProvider

  • DateFormatProvider

  • DateFormatSymbolsProvider

  • DecimalFormatSymbolsProvider

  • NumberFormatProvider

|

例如,如果您想为新的区域设置提供一个NumberFormat对象,请实现java.text.spi.NumberFormatProvider类并实现以下方法:

  • getCurrencyInstance(Locale locale)

  • getIntegerInstance(Locale locale)

  • getNumberInstance(Locale locale)

  • getPercentInstance(Locale locale)

Locale loc = new Locale("da", "DK");
NumberFormat nf = NumberFormatProvider.getNumberInstance(loc);

这些方法首先检查 Java 运行时环境是否支持请求的区域设置;如果支持,则使用该支持。否则,方法会调用已安装提供者的适当接口的getAvailableLocales方法,以找到支持请求的区域设置的提供者。

要深入了解如何使用国际化服务提供者的示例,请参阅将自定义资源包作为扩展安装。本节展示了如何实现ResourceBundleControlProvider接口,使您能够在不对应用程序源代码进行任何其他更改的情况下使用任何自定义ResourceBundle.Control类。

安装自定义资源包作为扩展

原文:docs.oracle.com/javase/tutorial/i18n/serviceproviders/resourcebundlecontrolprovider.html

自定义资源包加载部分向您展示如何更改资源包的加载方式。这涉及从类ResourceBundle.Control派生一个新类,然后通过调用以下方法检索资源包:

ResourceBundle getBundle(
  String baseName,
  Locale targetLocale,
  ResourceBundle.Control control)

参数control是您对ResourceBundle.Control的实现。

java.util.spi.ResourceBundleControlProvider接口使您能够更改以下方法加载资源包的方式:

ResourceBundle getBundle(
  String baseName,
  Locale targetLocale)

请注意,此版本的ResourceBundle.getBundle方法不需要ResourceBundle.Control类的实例。ResourceBundleControlProvider是一个服务提供者接口(SPI)。SPI 使您能够创建可扩展的应用程序,即可以在不修改其原始代码库的情况下轻松扩展的应用程序。有关更多信息,请参阅创建可扩展应用程序。

要使用 SPI,首先通过实现类似ResourceBundleControlProvider的 SPI 来创建一个服务提供者。当您实现一个 SPI 时,您指定它将如何提供服务。ResourceBundleControlProvider SPI 提供的服务是在应用程序调用方法ResourceBundle.getBundle(String baseName, Locale targetLocale)时获取适当的ResourceBundle.Control实例。您将服务提供者与 Java 扩展机制一起打包为已安装的扩展。运行应用程序时,不需要在类路径中命名您的扩展;运行时环境会找到并加载这些扩展。

已安装的ResourceBundleControlProvider SPI 实现将替换默认的ResourceBundle.Control类(定义了默认的包加载过程)。因此,ResourceBundleControlProvider接口使您可以使用任何自定义的ResourceBundle.Control类,而无需对应用程序的源代码进行任何其他更改。此外,该接口使您能够编写应用程序,而无需引用任何自定义的ResourceBundle.Control类。

RBCPTest.java示例展示了如何实现ResourceBundleControlProvider接口以及如何将其打包为已安装的扩展。这个示例被打包在 zip 文件RBCPTest.zip中,包含以下文件:

  • src

    • java.util.spi.ResourceBundleControlProvider

    • RBCPTest.java

    • rbcp

      • PropertiesResourceBundleControl.java

      • PropertiesResourceBundleControlProvider.java

      • XMLResourceBundleControl.java

      • XMLResourceBundleControlProvider.java

    • resources

      • RBControl.properties

      • RBControl_zh.properties

      • RBControl_zh_CN.properties

      • RBControl_zh_HK.properties

      • RBControl_zh_TW.properties

      • XmlRB.xml

      • XmlRB_ja.xml

  • lib

    • rbcontrolprovider.jar
  • build:包含所有打包在rbcontrolprovider.jar中的文件以及类文件RBCPTest.class

  • build.xml

以下步骤展示了如何重新创建文件RBCPTest.zip的内容,RBCPTest示例的工作原理以及如何运行它:

  1. 创建 ResourceBundle.Control 类的实现。

  2. 实现 ResourceBundleControlProvider 接口。

  3. 在应用程序中调用 ResourceBundle.getBundle 方法。

  4. 通过创建配置文件注册服务提供者。

  5. 将提供者、其所需的类以及配置文件打包到一个 JAR 文件中。

  6. 运行 RBCPTest 程序。

1. 创建 ResourceBundle.Control 类的实现。

RBCPTest.java 示例使用了两种ResourseBundle.Control的实现:

  • PropertiesResourceBundleControlProvider.java:这是在自定义资源包加载中定义的相同ResourceBundle.Control实现。

  • XMLResourceBundleControl.java:这个ResourceBundle.Control实现使用Properties.loadFromXML方法加载基于 XML 的包。

XML 属性文件

如在 使用属性文件支持 ResourceBundle 部分所述,属性文件是简单的文本文件。它们每行包含一个键值对。XML 属性文件与属性文件类似:它们包含键值对,只是具有 XML 结构。以下是 XML 属性文件 XmlRB.xml

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE properties [
<!ELEMENT properties ( comment?, entry* ) >
<!ATTLIST properties version CDATA #FIXED "1.0">
<!ELEMENT comment (#PCDATA) >
<!ELEMENT entry (#PCDATA) >
<!ATTLIST entry key CDATA #REQUIRED>
]>

<properties>
    <comment>Test data for RBCPTest.java</comment>
    <entry key="type">XML</entry>
</properties>

以下是等效的属性文本文件:

# Test data for RBCPTest.java
type = XML

所有 XML 属性文本文件具有相同的结构:

  • 指定文档类型定义(DTD)的 DOCTYPE 声明:DTD 定义了 XML 文件的结构。注意:在 XML 属性文件中,你可以使用以下 DOCTYPE 声明:

    <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">  
    
    

    系统 URI (http://java.sun.com/dtd/properties.dtd) 在导出或导入属性时不会被访问;它是一个唯一标识 XML 属性文件的 DTD 的字符串。

  • 根元素 <properties>:此元素包含所有其他元素。

  • 任意数量的 <comment> 元素:用于注释。

  • 任意数量的 <entry> 元素:使用属性 key 指定键;在 <entry> 标签之间指定键的值。

查看 Properties 类以获取有关 XML 属性文件的更多信息。

2. 实现 ResourceBundleControlProvider 接口。

这个接口包含一个方法,ResourceBundle.Control getControl(String baseName) 方法。参数 baseName 是资源包的名称。在 getBundle 方法的定义中,指定应返回的 ResourceBundle.Control 实例,给定资源包的名称。

RBCPTest 示例包含 ResourceBundleControlProvider 接口的两个实现,PropertiesResourceBundleControlProvider.javaXMLResourceBundleControlProvider.java。如果资源包的基本名称以 resources.RBControl 开头,则方法 PropertiesResourceBundleControlProvider.getBundle 返回 PropertiesResourceBundleControl 的实例(在此示例中,所有资源文件都包含在 resources 包中):


package rbcp;

import java.util.ResourceBundle;
import java.util.spi.ResourceBundleControlProvider;

public class PropertiesResourceBundleControlProvider
    implements ResourceBundleControlProvider {
    static final ResourceBundle.Control PROPERTIESCONTROL =
        new PropertiesResourceBundleControl();

    public ResourceBundle.Control getControl(String baseName) {
        System.out.println("Class: " + getClass().getName() + ".getControl");
        System.out.println("    called for " + baseName);

        // Throws a NPE if baseName is null.
        if (baseName.startsWith("resources.RBControl")) {
            System.out.println("    returns " + PROPERTIESCONTROL);
            return PROPERTIESCONTROL;
        }
        System.out.println("    returns null");
        System.out.println();
        return null;
    }
}

类似地,如果资源包的基本名称以 resources.Xml 开头,则方法 XMLResourceBundleControlProvider.getControl 返回 XMLResourceBundleControl 的实例。

注意:你可以创建一个实现 ResourceBundleControlProvider 接口的实现,根据基本名称返回 PropertiesResourceBundleControlXMLResourceBundleControl 的实例。

3. 在你的应用程序中,调用方法 ResourceBundle.getBundle。

RBCPTest使用方法ResourceBundle.getBundle检索资源包:


import java.io.*;
import java.net.*;
import java.util.*;

public class RBCPTest {
    public static void main(String[] args) {
        ResourceBundle rb = ResourceBundle.getBundle(
            "resources.XmlRB", Locale.ROOT);
        String type = rb.getString("type");
        System.out.println("Root locale. Key, type: " + type);
        System.out.println();

        rb = ResourceBundle.getBundle("resources.XmlRB", Locale.JAPAN);
        type = rb.getString("type");
        System.out.println("Japan locale. Key, type: " + type);
        System.out.println();

        test(Locale.CHINA);
        test(new Locale("zh", "HK"));
        test(Locale.TAIWAN);
        test(Locale.CANADA);
    }

    private static void test(Locale locale) {
        ResourceBundle rb = ResourceBundle.getBundle(
            "resources.RBControl", locale);
        System.out.println("locale: " + locale);
        System.out.println("    region: " + rb.getString("region"));
        System.out.println("    language: " + rb.getString("language"));
        System.out.println();
    }
}

请注意,此类中没有ResourceBundle.ControlResourceBundleControlProvider的实现。因为ResourceBundleControlProvider接口使用 Java 扩展机制,运行时环境会找到并加载这些实现。但是,使用 Java 扩展机制安装的ResourceBundleControlProvider实现和其他服务提供程序是使用ServiceLoader类加载的。使用此类意味着您必须使用配置文件注册服务提供程序,下一步将对此进行描述。

4. 通过创建配置文件注册服务提供程序。

配置文件的名称是提供程序实现的接口或类的完全限定名称。配置文件包含您的提供程序的完全限定类名。文件java.util.spi.ResourceBundleControlProvider包含PropertiesResourceBundleControlProviderXMLResourceBundleControlProvider的完全限定名称,每行一个名称:

rbcp.XMLResourceBundleControlProvider
rbcp.PropertiesResourceBundleControlProvider

5. 将提供程序、其所需类和配置文件打包到 JAR 文件中。

编译源文件。从包含build.xml文件的目录中,运行以下命令:

javac -d build src/java.* src/rbcp/*.java

此命令将编译src目录中包含的源文件,并将类文件放在build目录中。在 Windows 上,请确保使用反斜杠(\)来分隔目录和文件名。

创建一个 JAR 文件,其中包含编译的类文件、资源文件和配置文件,目录结构如下:

  • META-INF

    • services

      • java.util.spi.ResourceBundleControlProvider
  • rbcp

    • PropertiesResourceBundleControl.class

    • PropertiesResourceBundleControlProvider.class

    • XMLResourceBundleControl.class

    • XMLResourceBundleControlProvider.class

  • 资源

    • RBControl.properties

    • RBControl_zh.properties

    • RBControl_zh_CN.properties

    • RBControl_zh_HK.properties

    • RBControl_zh_TW.properties

    • XmlRB.xml

    • XmlRB_ja.xml

注意,配置文件java.util.spi.ResourceBundleControlProvider必须打包在目录/META-INF/services中。此示例将这些文件打包在lib目录中的 JAR 文件rbcontrolprovider.jar中。

查看在 JAR 文件中打包程序以获取有关创建 JAR 文件的更多信息。

或者,下载并安装Apache Ant,这是一个可以帮助您自动化构建过程的工具,例如编译 Java 文件和创建 JAR 文件。确保 Apache Ant 可执行文件在您的PATH环境变量中,以便您可以从任何目录运行它。安装 Apache Ant 后,请按照以下步骤操作:

  1. 编辑文件build.xml,将${JAVAC}更改为您的 Java 编译器javac的完整路径名,并将${JAVA}更改为您的 Java 运行时可执行文件java的完整路径名。

  2. 从包含文件build.xml的相同目录运行以下命令:

    ant jar
    

    这个命令编译 Java 源文件并将它们与所需的资源和配置文件打包到lib目录中的 JAR 文件rbcontrolprovider.jar中。

6. 运行 RBCPTest 程序。

在命令提示符下,从包含build.xml文件的目录运行以下命令:

java -Djava.ext.dirs=lib -cp build RBCPTest

此命令假定以下情况:

  • 包含 RBCPTest 示例编译代码的 JAR 文件位于lib目录中。

  • 编译后的类RBCPTest.class位于build目录中。

或者,使用 Apache Ant,并从包含build.xml文件的目录运行以下命令:

ant run

当您安装 Java 扩展时,通常将扩展的 JAR 文件放在 JRE 的lib/ext目录中。但是,此命令使用系统属性java.ext.dirs指定包含 Java 扩展的目录。

RBCPTest程序首先尝试使用基本名称为resources.XmlRB和区域设置为Locale.ROOTLocal.JAPAN的资源包。程序检索这些资源包的输出类似于以下内容:

Class: rbcp.XMLResourceBundleControlProvider.getControl
    called for resources.XmlRB
    returns rbcp.XMLResourceBundleControl@16c1857
Root locale. Key, type: XML

Class: rbcp.XMLResourceBundleControlProvider.getControl
    called for resources.XmlRB
    returns rbcp.XMLResourceBundleControl@16c1857
Japan locale. Key, type: Value from Japan locale

该程序成功获取XMLResourceBundleControl的实例并访问属性文件XmlRB.xmlXmlRB_ja.xml

RBCPTest程序尝试检索资源包时,它调用配置文件java.util.spi.ResourceBundleControlProvider中定义的所有类。例如,当程序检索基本名称为resources.RBControl和区域设置为Locale.CHINA的资源包时,它打印以下输出:

Class: rbcp.XMLResourceBundleControlProvider.getControl
    called for resources.RBControl
    returns null

Class: rbcp.PropertiesResourceBundleControlProvider.getControl
    called for resources.RBControl
    returns rbcp.PropertiesResourceBundleControl@1ad2911
locale: zh_CN
    region: China
    language: Simplified Chinese

教程:2D 图形

原文:docs.oracle.com/javase/tutorial/2d/index.html

本教程向你介绍了 Java 2D™ API,并展示了如何在 Java 程序中显示和打印 2D 图形。本教程适用于希望丰富其对 Java 2D API 知识的开发人员,以及计算机图形初学者。几乎每个部分都包含相关示例来说明特定功能。Java 2D API 可以轻松执行以下任务:

  • 绘制线条、矩形和任何其他几何形状。

  • 用纯色、渐变和纹理填充这些形状。

  • 使用选项绘制文本,对字体和渲染过程进行精细控制。

  • 绘制图像,可选择应用滤镜操作。

  • 在上述任何渲染操作期间应用诸如合成和变换等操作。

本章还解释了较不熟悉的概念,如合成。

这幅图代表了不同的图形功能使用 2D 图形 API 显示复杂图表

这幅图代表了滤镜操作的使用使用图像滤镜操作

本章描述了在屏幕和离屏图像、表面和打印机设备上绘制的概念。本教程涵盖了 Java 2D API 的最常见用法,并简要描述了一些更高级功能。

介绍了关键的 Java 2D 概念,并描述了 Java 2D 渲染模型。这节课比本教程的其他课程更具概念性,它让你深入了解基本概念和类描述。

使用一个开发的示例向你展示如何获取 Graphics 对象并将其用于常见的图形渲染任务。

教你如何使用 API 绘制图形基元和任意形状,以及如何应用花哨的描边和填充样式。

展示了如何有效地使用文本 API,包括如何创建具有所需属性的Font对象,测量文本,并确定系统中可用字体的名称。

解释了如何创建 BufferedImage 对象,执行图像滤镜操作,并在图像上绘制。

教你如何将 2D 图形渲染到打印机,打印复杂文档,并使用打印服务。

解释了如何执行变换,裁剪绘图区域,合成重叠图形,指定渲染偏好,并控制渲染质量。

课程:Java 2D API 概念概述

原文:docs.oracle.com/javase/tutorial/2d/overview/index.html

Java 2D API 通过对抽象窗口工具包(AWT)的扩展,为 Java 程序提供了二维图形、文本和图像功能。这个全面的渲染包支持线条艺术、文本和图像,提供了一个灵活、功能齐全的框架,用于开发更丰富的用户界面、复杂的绘图程序和图像编辑器。Java 2D 对象存在于一个称为用户坐标空间或用户空间的平面上。当对象在屏幕或打印机上呈现时,用户空间坐标被转换为设备空间坐标。以下链接对开始学习 Java 2D API 很有帮助:

  • Graphics

  • Graphics2D

Java 2D API 提供以下功能:

  • 用于显示设备和打印机的统一渲染模型

  • 一系列几何基元,如曲线、矩形和椭圆,以及一种渲染几乎任何几何形状的机制

  • 用于在形状、文本和图像上执行命中检测的机制

  • 一个合成模型,提供对重叠对象如何呈现的控制

  • 增强的颜色支持,有助于颜色管理

  • 支持打印复杂文档

  • 通过使用渲染提示控制渲染质量

这些主题在以下章节中讨论:

  • Java 2D 渲染

  • 几何基元

  • 文本

  • 图像

  • 打印

坐标

原文:docs.oracle.com/javase/tutorial/2d/overview/coordinate.html

Java 2D API 维护两个坐标空间:

  • 用户空间 – 指定图形基元的空间

  • 设备空间 – 输出设备(如屏幕、窗口或打印机)的坐标系统

用户空间是一个设备无关的逻辑坐标系统,即您的程序使用的坐标空间。传递给 Java 2D 渲染例程的所有几何图形都是使用用户空间坐标指定的。

当使用从用户空间到设备空间的默认转换时,用户空间的原点是组件绘图区域的左上角。x坐标向右增加,y坐标向下增加,如下图所示。窗口的左上角是 0,0。所有坐标都使用整数指定,通常足够。但是,有些情况需要浮点数甚至双精度,这也是支持的。

这个图表示的是空间

设备空间是一个依赖设备的坐标系统,根据目标渲染设备而变化。虽然窗口或屏幕的坐标系统可能与打印机的坐标系统非常不同,但这些差异对 Java 程序是不可见的。在渲染过程中,用户空间和设备空间之间的必要转换是自动执行的。

Java 2D 渲染

原文:docs.oracle.com/javase/tutorial/2d/overview/rendering.html

Java 2D API 在不同类型设备上提供统一的渲染模型。在应用程序级别,无论目标渲染设备是屏幕还是打印机,渲染过程都是相同的。当需要显示组件时,其paintupdate方法会自动使用适当的Graphics上下文调用。

Java 2D API 包括java.awt.Graphics2D类,该类扩展了Graphics类,以提供对 Java 2D API 增强图形和渲染功能的访问。这些功能包括:

  • 渲染任何几何原语的轮廓,使用笔画和填充属性(draw 方法)。

  • 通过使用颜色或图案指定的填充属性(fill 方法)来填充任何几何原语的内部。

  • 渲染任何文本字符串(drawString 方法)。字体属性用于将字符串转换为字形,然后用颜色或图案指定的填充。

  • 渲染指定的图像(drawImage 方法)。

此外,Graphics2D类支持特定形状的drawOvalfillRectGraphics渲染方法。上面列出的所有方法可以分为两组:

  1. 绘制形状的方法

  2. 影响渲染的方法

第二组方法使用形成Graphics2D上下文的状态属性,用于以下目的:

  • 变化笔画宽度

  • 更改如何连接笔画

  • 设置裁剪路径以限制渲染区域

  • 在渲染对象时平移、旋转、缩放或倾斜对象

  • 定义颜色和图案以填充形状

  • 指定如何组合多个图形对象

在应用程序中使用 Java 2D API 功能,将传递给组件渲染方法的Graphics对象转换为Graphics2D对象。例如:

public void paint (Graphics g) {
    Graphics2D g2 = (Graphics2D) g;
    ...
}

如下图所示,Graphics2D类的渲染上下文包含几个属性。

此图表示用笔画描绘形状的轮廓 笔属性应用于形状的轮廓。此笔画属性使您能够用任何点大小和虚线模式绘制线条,并对线条应用端点和连接装饰。
此图显示如何用纯色填充形状 填充属性应用于形状的内部。此填充属性使您能够用纯色、渐变和图案填充形状。
这个图展示了如何合成现有图像和图形基元 compositing attribute 在渲染对象重叠现有对象时使用。
这个图表示剪切变换 transform 属性在渲染过程中应用,将渲染对象从用户空间转换为设备空间坐标。通过该属性还可以应用可选的平移、旋转、缩放或剪切变换。
这个图展示了如何使用 Shape 对象定义剪切路径 clip 类型将渲染限制在用于定义剪切路径的 Shape 对象轮廓内的区域。任何用于定义剪切的 Shape 对象。
这个图展示了字形的示例 font 属性用于将文本字符串转换为字形。
这个图表示抗锯齿 Rendering hints 指定在速度和质量之间的权衡偏好。例如,您可以指定是否应使用抗锯齿,如果该功能可用的话。另请参阅 控制渲染质量。

要了解更多关于变换和合成的内容,请参阅 Java2D 中的高级主题。

当设置属性时,会传递适当的属性对象。如下例所示,要将绘画属性更改为蓝绿色渐变填充,您需要构造一个 GradientPaint 对象,然后调用 setPaint 方法。

gp = new GradientPaint(0f,0f,blue,0f,30f,green);
g2.setPaint(gp);

几何基元

原文:docs.oracle.com/javase/tutorial/2d/overview/primitives.html

Java 2D API 提供了一组有用的标准形状,如点、线、矩形、弧、椭圆和曲线。定义常见几何基元的最重要的包是java.awt.geom包。任意形状可以由直线几何基元的组合表示。

Shape接口表示具有轮廓和内部的几何形状。此接口提供了一组用于描述和检查二维几何对象的常见方法,并支持曲线线段和多个子形状。Graphics类仅支持直线段。Shape接口可以支持曲线段。

关于如何绘制和填充形状的更多详细信息,请参阅几何处理课程。

Point2D类定义了表示(x, y)坐标空间中位置的点。Java 2D API 中的术语“点”与像素不同。点没有面积,不包含颜色,也不能呈现。

点用于创建其他形状。Point2D类还包括一种计算两点之间距离的方法。

线

Line2D类是表示一条线的抽象类。线的坐标可以作为双精度数检索。Line2D类包括几种设置线端点的方法。

您还可以使用下面描述的GeneralPath类创建直线段。

矩形形状

Rectangle2DRoundRectangle2DArc2DEllipse2D基元都是从RectangularShape类派生的。此类定义了可以由矩形边界框描述的Shape对象的方法。RectangularShape对象的几何形状可以从完全包围Shape轮廓的矩形中推断出来。

矩形形状

二次和三次曲线

QuadCurve2D使您能够创建二次参数曲线段。二次曲线由两个端点和一个控制点定义。

CubicCurve2D类使您能够创建三次参数曲线段。三次曲线由两个端点和两个控制点定义。以下是二次和三次曲线的示例。请参阅描边和填充图形基元以获取三次和二次曲线的实现。

此图表示一个二次曲线。

二次参数曲线

此图表示一个三次曲线。

三次参数曲线

任意形状

GeneralPath类使您能够通过指定沿着形状边界的一系列位置来构造任意形状。这些位置可以通过线段、二次曲线或三次(贝塞尔)曲线连接。以下形状可以通过三条线段和一条三次曲线创建。有关此形状实现的更多信息,请参见描边和填充图形基元。

这个图表示通过使用 GeneralPath 类创建的多边形

区域

使用Area类,您可以对任意两个Shape对象执行布尔运算,如并集、交集和差集。这种技术通常被称为构造性区域几何,使您能够快速创建复杂的Shape对象,而无需描述每条线段或曲线。

文本

原文:docs.oracle.com/javase/tutorial/2d/overview/text.html

Java 2D API 具有各种文本渲染功能,包括用于呈现字符串的方法以及用于设置字体属性和执行文本布局的整个类。

如果您只想绘制静态文本字符串,最直接的方法是通过Graphics类使用drawString方法直接呈现它。要指定字体,您可以使用Graphics类的setFont方法。

如果您想要实现自己的文本编辑例程或需要比文本组件提供的更多对文本布局的控制,您可以使用java.awt.font包中的 Java 2D 文本布局类。

字体

字体用于表示字符串中字符的形状称为字形。特定字符或字符组合可能表示为一个或多个字形。例如,á可能由两个字形表示,而连字fi可能由一个字形表示。

字体可以被视为一组字形。单个字体可能有许多面孔,如斜体和常规。字体中的所有面孔具有类似的印刷特征,并且可以被识别为同一字体系列的成员。换句话说,具有特定样式的一组字形形成一个字体面孔。一组字体面孔形成一个字体系列。字体系列的集合形成了系统中可用的字体集。

当您使用 Java 2D API 时,您可以通过使用Font的实例来指定字体。您可以通过调用静态方法GraphicsEnvironment.getLocalGraphicsEnvironment并查询返回的GraphicsEnvironment来确定可用的字体。getAllFonts方法返回一个包含系统上所有可用字体的Font实例数组。getAvailableFontFamilyNames方法返回可用字体系列的列表。

文本布局

在文本可以显示之前,必须对其进行布局,以便字符以适当的位置表示为适当的字形。以下是两种用于管理文本布局的 Java 2D 机制:

  • TextLayout类管理文本布局、高亮显示和命中检测。TextLayout提供的功能处理最常见的情况,包括具有混合字体、混合语言和双向文本的字符串。

  • 您可以通过使用Font类创建自己的GlyphVector对象,然后通过Graphics2D类渲染每个GlyphVector对象。因此,您可以完全控制文本的形状和位置。

文本的渲染提示

Java 2D API 使您能够通过使用渲染提示来控制形状和文本渲染的质量。渲染提示由java.awt.RenderingHints类封装。

当应用于文本时,这种能力用于抗锯齿(也称为平滑边缘)。例如,KEY_TEXT_ANTIALIASING提示可以让您单独控制文本的抗锯齿效果,而不影响其他形状的抗锯齿效果。要了解更多关于渲染提示的信息,请参阅控制渲染质量课程。

图像

原文:docs.oracle.com/javase/tutorial/2d/overview/images.html

在 Java 2D API 中,图像通常是一个矩形的二维像素数组,其中每个像素表示图像该位置的颜色,而维度表示图像显示时的水平范围(宽度)和垂直范围(高度)。

表示此类图像最重要的图像类是java.awt.image.BufferedImage类。Java 2D API 将这些图像的内容存储在内存中,以便可以直接访问。

应用程序可以直接创建一个BufferedImage对象,或从外部图像格式(如 PNG 或 GIF)获取图像。

在任一情况下,应用程序可以通过使用 Java 2D API 图形调用在图像上绘制。因此,图像不仅限于显示照片类型的图像。不同的对象,如线条艺术、文本和其他图形,甚至其他图像都可以绘制到图像上(如下图所示)。

这个图表示图像作为一个绘图表面

Java 2D API 允许您对BufferedImage应用图像过滤操作,并包括几个内置过滤器。例如,ConvolveOp过滤器可用于模糊或锐化图像。

然后生成的图像可以绘制到屏幕上,发送到打印机上,或保存为 PNG、GIF 等图形格式。要了解更多关于图像的信息,请参阅使用图像课程。

打印

原文:docs.oracle.com/javase/tutorial/2d/overview/printing.html

所有的 Swing 和 Java 2D 图形,包括合成图形和图像,都可以通过使用 Java 2D Printing API 渲染到打印机上。该 API 还提供文档组合功能,使您能够执行诸如更改打印页面顺序之类的操作。

渲染到打印机就像渲染到屏幕一样。打印系统控制何时渲染页面,就像绘图系统控制组件何时在屏幕上绘制一样。

Java 2D Printing API 基于回调模型,其中打印系统而不是应用程序控制打印页面的时间。应用程序向打印系统提供要打印的文档信息,打印系统确定何时需要成像每一页。

以下两个功能对支持打印很重要:

  • 作业控制 – 启动和管理打印作业,包括显示标准打印和设置对话框

  • 分页 – 在打印系统请求时呈现每一页

当需要成像页面时,打印系统使用适当的Graphics上下文调用应用程序的print方法。要在打印时使用 Java 2D API 功能,您将Graphics对象转换为Graphics2D类,就像在渲染到屏幕时一样。

教程:开始使用图形

原文:docs.oracle.com/javase/tutorial/2d/basic2d/index.html

Java 2D API 强大而复杂。然而,Java 2D API 的绝大多数用途利用了java.awt.Graphics类中封装的其功能的一个小子集。本课程涵盖了应用程序开发人员最常见的需求。较少常见的需求稍后在 Java 2D API 的高级主题中描述。

Graphics类的大多数方法可以分为两个基本组:

  • 绘制和填充方法,使您能够渲染基本形状、文本和图像

  • 设置属性的方法,影响绘制和填充的外观

诸如setFontsetColor之类的方法定义了如何渲染绘制和填充方法。

本图说明了这些方法与图形对象的关系:

此图表示类的基本方法

绘制方法包括:

  • drawString – 用于绘制文本

    g.drawString("Hello", 10, 10);
    
    
  • drawImage – 用于绘制图像

    g.drawImage(img, 
                0, 0, width, height,
                0, 0, imageWidth, imageHeight,
                null);                    
    
    
  • drawLinedrawArcdrawRectdrawOvaldrawPolygon – 用于绘制几何形状

    g2.draw(new Line2D.Double(0, 0, 30, 40));
    
    

根据您当前的需求,您可以根据以下标准在Graphics类中选择几种方法之一:

  • 无论您想要在指定位置以原始大小渲染图像还是将其缩放以适应给定矩形

  • 无论您喜欢用颜色填充图像的透明区域还是保持其透明

填充方法适用于几何形状,包括fillArcfillRectfillOvalfillPolygon

无论您绘制文本还是图像,记住在 2D 图形中,每个点由其 x 和 y 坐标确定。所有绘制和填充方法都需要这些信息,这些信息确定了文本或图像应该渲染在哪里。

例如,要绘制一条线,应用程序调用以下代码:

java.awt.Graphics.drawLine(int x1, int y1, int x2, int y2)

在此代码中(x1, y1)是线的起点,(x2, y2)是线的终点。

因此,绘制水平线的代码如下:

Graphics.drawLine(20, 100, 120, 100);

下面的演示累积了所有提到的技术。移动滑块以显示各种天气类型。


注意: 如果您看不到小程序运行,您需要安装至少Java SE Development Kit (JDK) 7版本。


WeatherWizard 演示使用 JSlider 组件以及各种图形功能来生成和显示指定的天气类型。有关 JSlider 类的更多信息,请参阅 Swing 教程的 How to Use Sliders 部分。

WeatherPainter 类的 paint 方法实现了图形特性。以下代码使用 setupWeatherReport() 方法绘制了一个图像。

...
origComposite = g2.getComposite();
if (alpha0 != null) g2.setComposite(alpha0);
g2.drawImage(img0,
             0, 0, size.width, size.height,
             0, 0, img0.getWidth(null),
             img0.getHeight(null), null);
if (img1 != null) {
    if (alpha1 != null) g2.setComposite(alpha1);
    g2.drawImage(img1,
                 0, 0, size.width, size.height,
                 0, 0, img1.getWidth(null),
                 img1.getHeight(null), null);
} 
...

setFontdrawString 方法渲染温度和天气状况。

...
// Freezing, Cold, Cool, Warm, Hot,
// Blue, Green, Yellow, Orange, Red
Font font = new Font("Serif", Font.PLAIN, 36);
g.setFont(font);

String tempString = feels + " " + temperature+"F";
FontRenderContext frc =
           ((Graphics2D)g).getFontRenderContext();
...
g.setColor(textColor);
int xTextTemp = rX-(int)boundsTemp.getX();
int yTextTemp = rY-(int)boundsTemp.getY();
g.drawString(tempString, xTextTemp, yTextTemp);

int xTextCond = rX-(int)boundsCond.getX();
int yTextCond = rY-(int)boundsCond.getY()
                   + (int)boundsTemp.getHeight();
g.drawString(condStr, xTextCond, yTextCond);

fillRect 方法允许您绘制一个填充了指定颜色的矩形。

...
Rectangle2D boundsTemp 
          = font.getStringBounds(tempString, frc);
Rectangle2D boundsCond 
          = font.getStringBounds(condStr, frc);
int wText = Math.max((int)boundsTemp.getWidth(),
            (int)boundsCond.getWidth());
int hText = (int)boundsTemp.getHeight()
            + (int)boundsCond.getHeight();
int rX = (size.width-wText)/2;
int rY = (size.height-hText)/2;

g.setColor(Color.LIGHT_GRAY);
g2.fillRect(rX, rY, wText, hText);
...

尝试修改 WeatherWizard 演示以更改图形内容。例如,使用 fillRoundRect 方法代替 fillRect 或在 setFont 方法中应用另一个字体大小。在 WeatherWizard.java 文件中找到此小程序的完整代码。演示还需要以下图像:weather-cloud.png, weather-rain.png, weather-snow.pngweather-sun.png,这些图像位于 images 目录中。

教程:与几何形状一起工作

原文:docs.oracle.com/javase/tutorial/2d/geometry/index.html

在之前的课程中,您已经学习了关于图形概念的知识,包括关于坐标系和图形对象创建的基本信息。现在,您将进一步学习有关 2D 图形类的详细课程。本课程将向您展示如何使用Graphics2D类绘制图形基元以及任意形状,以及如何使用花哨的轮廓和填充样式显示图形。这些主题将在以下部分讨论。

绘制几何基元

这一部分解释了如何创建标准形状,如点、线、曲线、弧线、矩形和椭圆。

绘制任意形状

这一部分解释了如何使用GeneralPath类绘制由直线几何基元组合表示的形状。

填充和描边

这一部分解释了如何设置描边和填充属性,以控制应用于Shape对象和文本的轮廓和填充样式。

绘制几何图元

原文:docs.oracle.com/javase/tutorial/2d/geometry/primitives.html

Java 2D API 提供了几个定义常见几何对象(如点、线、曲线和矩形)的类。这些几何类是java.awt.geom包的一部分。

PathIterator接口定义了从路径中检索元素的方法。

Shape接口提供了一组描述和检查几何路径对象的方法。该接口由GeneralPath类和其他几何类实现。

本节中的所有示例都是通过使用java.awt.geom包创建几何图形,然后通过使用Graphics2D类来呈现它们。首先,您需要获取一个Graphics2D对象,例如通过将paint()方法的Graphics参数进行强制转换。

public void paint (Graphics g) {
    Graphics2D g2 = (Graphics2D) g;
    ...
}

Point类创建一个表示(x,y)坐标空间中位置的点。子类Point2D.FloatPoint2D.Double分别提供存储点坐标的浮点和双精度。

//Create Point2D.Double
Point2D.Double point = new Point2D.Double(x, y);

要创建坐标为 0,0 的点,您可以使用默认构造函数Point2D.Double()

您可以使用setLocation方法设置点的位置如下:

  • setLocation(double x, double y) – 以双精度值设置点的位置-定义坐标。

  • setLocation(Point2D p) – 使用另一个点的坐标设置点的位置。

此外,Point2D类有方法来计算当前点与给定坐标点之间的距离,或者两点之间的距离。

线

Line2D类表示(x, y)坐标空间中的线段。Line2D.FloatLine2D.Double子类指定了浮点和双精度的线。例如:

// draw Line2D.Double
g2.draw(new Line2D.Double(x1, y1, x2, y2));

Line

该类包括几个setLine()方法来定义线的端点。

或者,可以使用Line2D.Float类的构造函数指定线的端点如下:

  • Line2D.Float(float X1, float Y1, float X2, float Y2)

  • Line2D.Float(Point2D p1, Point2D p2)

Graphics2D类中使用 Stroke 对象定义线路径的笔画。

曲线

java.awt.geom包使您能够创建二次或三次曲线段。

二次曲线段

QuadCurve2D类实现了Shape接口。该类表示(x, y)坐标空间中的二次参数曲线段。QuadCurve2D.FloatQuadCurve2D.Double子类指定了浮点和双精度的二次曲线。

几个setCurve方法用于指定曲线的两个端点和控制点,其坐标可以直接定义,通过其他点的坐标定义,或使用给定数组。

一个非常有用的方法,setCurve(QuadCurve2D),设置具有与提供的曲线相同端点和控制点的二次曲线。例如:

// create new QuadCurve2D.Float
QuadCurve2D q = new QuadCurve2D.Float();
// draw QuadCurve2D.Float with set coordinates
q.setCurve(x1, y1, ctrlx, ctrly, x2, y2);
g2.draw(q);

二次参数曲线段

三次曲线段

CubicCurve2D类还实现了Shape接口。该类表示(x, y)坐标空间中的三次参数曲线段。CubicCurve2D.FloatCubicCurve2D.Double子类指定了浮点和双精度的三次曲线。

CubicCurve2D类具有设置曲线的类似方法,与QuadraticCurve2D类相同,只是有第二个控制点。例如:

// create new CubicCurve2D.Double
CubicCurve2D c = new CubicCurve2D.Double();
// draw CubicCurve2D.Double with set coordinates
c.setCurve(x1, y1, ctrlx1,
           ctrly1, ctrlx2, ctrly2, x2, y2);
g2.draw(c);

三次曲线段

矩形

在下面的示例中表示的原语的类扩展了RectangularShape类,该类实现了Shape接口并添加了一些自己的方法。

这些方法使您能够获取有关形状位置和大小的信息,检查矩形的中心点,并设置形状的边界。

Rectangle2D类表示由位置(x, y)和尺寸(w x h)定义的矩形。Rectangle2D.FloatRectangle2D.Double子类指定了浮点和双精度的矩形。例如:

// draw Rectangle2D.Double
g2.draw(new Rectangle2D.Double(x, y,
                               rectwidth,
                               rectheight));

矩形

RoundRectangle2D类表示具有圆角的矩形,由位置(x, y)、尺寸(w x h)和圆角的宽度和高度定义。RoundRectangle2D.FloatRoundRectangle2D.Double子类指定了浮点和双精度的圆角矩形。

圆角矩形由以下参数指定:

  • 位置

  • 宽度

  • 高度

  • 圆角弧的宽度

  • 圆角弧的高度

要设置RoundRectangle2D对象的位置、大小和弧度,请使用方法setRoundRect(double a, double y, double w, double h, double arcWidth, double arcHeight)。例如:

// draw RoundRectangle2D.Double
g2.draw(new RoundRectangle2D.Double(x, y,
                                   rectwidth,
                                   rectheight,
                                   10, 10));

圆角矩形

椭圆

Ellipse2D类表示由边界矩形定义的椭圆。Ellipse2D.FloatEllipse2D.Double子类指定了以浮点和双精度表示的椭圆。

椭圆由位置、宽度和高度完全定义。例如:

// draw Ellipse2D.Double
g2.draw(new Ellipse2D.Double(x, y,
                             rectwidth,
                             rectheight));

椭圆

要绘制椭圆的一部分,您可以使用Arc2D类。这个类表示由边界矩形、起始角度、角度范围和闭合类型定义的弧。Arc2D.FloatArc2D.Double子类指定了以浮点和双精度表示的弧。

Arc2D类定义了这个类中对应常量表示的三种弧形:OPEN、PIE 和 CHORD。

弧

有几种方法可以设置弧的大小和参数:

  • 直接,通过坐标

  • 通过提供的Point2DDimension2D

  • 通过复制现有的Arc2D

此外,您可以使用setArcByCenter方法来指定从中心点开始的弧,给定其坐标和半径。

// draw Arc2D.Double
g2.draw(new Arc2D.Double(x, y,
                         rectwidth,
                         rectheight,
                         90, 135,
                         Arc2D.OPEN));

弧

ShapesDemo2D.java代码示例包含了所有描述的几何原语的实现。有关本节中所代表的类和方法的更多信息,请参阅java.awt.geom规范。

绘制任意形状

原文:docs.oracle.com/javase/tutorial/2d/geometry/arbitrary.html

您已经学会了如何绘制java.awt.geom包中表示的大多数形状。要创建更复杂的几何图形,如多边形、折线或星形,您可以使用此包中的另一个类GeneralPath

此类实现了Shape接口,表示由线段、二次曲线和三次曲线构成的几何路径。此类中的三个构造函数可以使用默认绕组规则(WIND_NON_ZERO)、给定的绕组规则(WIND_NON_ZEROWIND_EVEN_ODD)或指定的初始坐标容量创建GeneralPath对象。绕组规则指定了如何确定路径的内部。

public void paint (Graphics g) {
    Graphics2D g2 = (Graphics2D) g;
    ...
}

要创建一个空的GeneralPath实例,请调用new GeneralPath(),然后使用以下方法向形状添加线段:

  • moveTo(float x, float y) – 将路径的当前点移动到给定点

  • lineTo(float x, float y) – 向当前路径添加一个直线段

  • quadTo(float ctrlx, float ctrly, float x2, floaty2) – 向当前路径添加一个二次曲线段

  • curveTo(float ctrlx1, float ctrly1, float ctrlx2, float ctrly2, float x3, floaty3) – 向当前路径添加一个三次曲线段

  • closePath() – 关闭当前路径

以下示例说明了如何使用GeneralPath绘制折线:

|

// draw GeneralPath (polyline)
int x2Points[] = {0, 100, 0, 100};
int y2Points[] = {0, 50, 50, 0};
GeneralPath polyline = 
        new GeneralPath(GeneralPath.WIND_EVEN_ODD, x2Points.length);

polyline.moveTo (x2Points[0], y2Points[0]);

for (int index = 1; index < x2Points.length; index++) {
         polyline.lineTo(x2Points[index], y2Points[index]);
};

g2.draw(polyline);

此图表示一个折线

此示例说明了如何使用GeneralPath绘制多边形:

|

// draw GeneralPath (polygon)
int x1Points[] = {0, 100, 0, 100};
int y1Points[] = {0, 50, 50, 0};
GeneralPath polygon = 
        new GeneralPath(GeneralPath.WIND_EVEN_ODD,
                        x1Points.length);
polygon.moveTo(x1Points[0], y1Points[0]);

for (int index = 1; index < x1Points.length; index++) {
        polygon.lineTo(x1Points[index], y1Points[index]);
};

polygon.closePath();
g2.draw(polygon);

此图表示一个多边形

请注意,最后两个代码示例之间唯一的区别是closePath()方法。此方法通过向上一次moveTo的坐标绘制一条直线,从而将折线变成多边形。

要将特定路径添加到您的GeneralPath对象的末尾,您可以使用append()方法之一。ShapesDemo2D.java代码示例包含了任意形状的额外实现。

描边和填充图形原语

原文:docs.oracle.com/javase/tutorial/2d/geometry/strokeandfill.html

您已经知道如何创建不同的几何原语和更复杂的形状。本课程教授如何为图形添加一些颜色和花哨的轮廓,并表示填充和描边:

  • 填充 - 是用纯色、颜色渐变或纹理图案绘制形状内部的过程

  • 描边 - 是绘制形状轮廓的过程,应用描边宽度、线条样式和颜色属性

要将花哨的线条样式和填充图案应用于几何原语,需在呈现之前更改Graphics2D上下文中的描边和绘制属性。例如,通过创建适当的Stroke对象来绘制虚线。在呈现线条之前,将此描边添加到Graphics2D上下文中,调用setStroke方法。同样,通过创建GradientPaint对象并将其添加到Graphics2D上下文中,可以将渐变填充应用于Shape对象。

以下代码行丰富了几何原语的填充和描边上下文:

// draw RoundRectangle2D.Double

final static float dash1[] = {10.0f};
    final static BasicStroke dashed =
        new BasicStroke(1.0f,
                        BasicStroke.CAP_BUTT,
                        BasicStroke.JOIN_MITER,
                        10.0f, dash1, 0.0f);
g2.setStroke(dashed);
g2.draw(new RoundRectangle2D.Double(x, y,
                                   rectWidth,
                                   rectHeight,
                                   10, 10));

虚线圆角矩形

// fill Ellipse2D.Double
redtowhite = new GradientPaint(0,0,color.RED,100, 0,color.WHITE);
g2.setPaint(redtowhite);
g2.fill (new Ellipse2D.Double(0, 0, 100, 50));

填充渐变颜色的多边形

ShapesDemo2D.java代码示例代表了描边和填充的额外实现。

定义花哨的线条样式和填充图案

使用 Java 2D 的StrokePaint类,可以定义花哨的线条样式和填充图案。

线条样式

线条样式由Graphics2D呈现上下文中的描边属性定义。要设置描边属性,需创建一个BasicStroke对象并将其传递给Graphics2DsetStroke方法。

一个BasicStroke对象保存有关线宽、连接样式、端点样式和虚线样式的信息。当使用draw方法呈现Shape时,将使用此信息。

线宽是线条垂直于其轨迹的厚度。线宽以用户坐标单位的float值指定,当使用默认变换时,这些单位大致相当于 1/72 英寸。

连接样式是应用在两条线段相遇处的装饰。BasicStroke支持以下三种连接样式:

连接斜角笔触样式 JOIN_BEVEL

连接斜角笔触样式 JOIN_MITER

连接圆形笔触样式 JOIN_ROUND

端点样式是应用在线段结束处的装饰。BasicStroke支持以下三种端点样式:

平头端点样式 CAP_BUTT

圆形端点样式 CAP_ROUND

方形端点样式 CAP_SQUARE

虚线样式定义了沿着线长度应用的不透明和透明部分的模式。虚线样式由一个虚线数组和一个虚线相位定义。虚线数组定义了虚线模式。数组中的交替元素表示用户坐标单位中的虚线长度和虚线之间的空间长度。元素 0 表示第一个虚线,元素 1 表示第一个空格,依此类推。虚线相位是虚线模式中的偏移量,也以用户坐标单位指定。虚线相位指示应用于线的开头的虚线模式的哪个部分。

填充图案

填充图案由Graphics2D渲染上下文中的paint属性定义。要设置paint属性,您需要创建一个实现Paint接口的对象实例,并将其传递给Graphics2DsetPaint方法。

以下三个类实现了Paint接口:ColorGradientPaintTexturePaint

要创建一个GradientPaint,您需要指定起始位置和颜色以及结束位置和颜色。渐变沿着连接两个位置的线从一种颜色变化到另一种颜色。例如:

渐变填充

TexturePaint类的图案由BufferedImage类定义。要创建一个TexturePaint对象,您需要指定包含图案的图像和用于复制和锚定图案的矩形。以下图像表示了这个特性:

使用纹理填充矩形

课程:使用文本 APIs

原文:docs.oracle.com/javase/tutorial/2d/text/index.html

本课程向您介绍了使用文本 API 的概念,以应用文本渲染功能。在本教程中,您已经使用了基本的 Java 2D 文本 API,并知道如何设置字体和位置,以及如何绘制文本。

本课程扩展了该材料,帮助您了解如何使用这些 API,并进一步了解 Java 2D 文本显示的功能。

这些主题将在以下部分中讨论。

物理和逻辑字体

这一部分解释了如何使用Font类的方法来确定系统上可用的字体,创建Font对象,并获取有关字体系列的信息。

测量文本

这一部分解释了如何通过使用FontMetrics类的实例来正确测量文本。

高级文本显示

这一部分解释了如何定位和渲染一段样式文本,显示抗锯齿文本,使用文本属性来设置文本样式,并处理双向文本。

字体概念

原文:docs.oracle.com/javase/tutorial/2d/text/fontconcepts.html

本节介绍了Font类,支持详细字体信息的规范和复杂排版功能的使用。

一个Font对象表示系统上可用的字体集合中的字体面实例。常见字体面的示例包括 Helvetica Bold 和 Courier Bold Italic。一个Font对象关联三个名称:其逻辑名称、族名称和字体面名称:

  • 一个Font对象的逻辑名称是映射到系统上可用的特定字体之一的名称。在 Java 中指定Font时,请使用字体面名称而不是逻辑名称。您可以通过调用getName方法从Font中获取逻辑名称。要获取映射到系统上可用的特定字体的逻辑名称列表,请调用java.awt.GraphicsEnvironment.getAvailableFontFamilyNames方法。

    查看物理和逻辑字体以获取更多信息。

  • 一个Font对象的族名称是确定跨多个字体面的排版设计的字体族名称,如 Helvetica。通过getFamily方法检索族名称。

  • 一个Font对象的字体面名称指的是系统上安装的实际字体。这是您在指定字体时应该使用的名称。通常被称为字体名称。通过调用getFontName检索字体名称。要确定系统上可用的字体面,请调用java.awt.GraphicsEnvironment.getAllFonts方法。

您可以通过getAttributes方法访问有关Font的信息。Font对象的属性包括其名称、大小、变换和字体特征,如粗细和姿势。

一个LineMetrics对象封装了与Font相关的测量信息,如其上升、下降和行间距:

  • 上升是基线到上升线的距离。这个距离代表大写字母的典型高度,但有些字符可能会延伸到上升线以上。

  • 下降是基线到下行线的距离。大多数字符的最低点将落在下降线内,但有些字符可能会延伸到下行线以下。

  • 行间距是推荐的从下行线底部到下一行顶部的距离。

以下图显示了上升线、基线和下行线的位置:

上升线、基线和下行线的位置

这些信息用于正确定位字符沿着一行,以及相对于彼此定位行。您可以通过getAscentgetDescentgetLeading方法访问这些行度量。您还可以通过LineMetrics类访问有关Font对象的高度、基线以及下划线和删除线特性的信息。

文本布局概念

原文:docs.oracle.com/javase/tutorial/2d/text/textlayoutconcepts.html

在显示一段文本之前,必须使用适当的字形和连字对其进行正确的形状和定位。这个过程被称为文本布局。文本布局过程涉及以下内容:

  • 使用适当的字形和连字来形状文本

  • 正确排序文本

  • 测量和定位文本

用于排列文本的信息也对执行文本操作(如插入符定位、点击检测和高亮显示)是必要的。查看处理双向文本以获取有关这些文本操作的更多信息。

要开发可以部署在国际市场的软件,文本必须以符合适当书写系统规则的方式排列在不同语言中。

本节涵盖以下主题:

  • 形状文本

  • 排序文本

  • 测量和定位文本

形状文本

字形是一个或多个字符的视觉表示。字形的形状、大小和位置取决于其上下文。根据字体和样式,可以使用许多不同的字形来表示单个字符或字符组合。

例如,在手写草书文本中,特定字符的形状可能会因其与相邻字符的连接方式而异。

在某些书写系统中,特别是阿拉伯文中,必须始终考虑字形的上下文。与英语不同,阿拉伯文中的草书形式是强制性的;在阿拉伯文中,不能不使用草书形式呈现文本。

根据上下文,这些草书形式在形状上可能有很大差异。例如,阿拉伯字母heh有以下图中显示的四种草书形式:

阿拉伯文中未连接、右侧连接、两侧连接和左侧连接草书形式的插图

尽管这四种形式彼此非常不同,但这种草书形式的变化与英语中的草书写作并无根本区别。

在某些情况下,两个字形的形状甚至可以发生更大变化,合并成一个单一的字形。这种合并的字形称为连字。例如,大多数英文字体包含以下图中显示的连字fi

fi 的连字

合并的字形考虑了字母f的悬挑,并以一种自然的方式组合字符,而不是简单地让字母相撞。

阿拉伯文中也使用连字,有些连字的使用是强制性的;在不使用适当的连字的情况下呈现某些字符组合是不可接受的。当从阿拉伯字符形成连字时,形状甚至比在英文中更根本地改变。例如,下图说明了当两个阿拉伯字符在一起时如何组合成单个连字。

当两个阿拉伯字符在一起时如何组合成单个连字

文本排序

在 Java 编程语言中,文本使用 Unicode 字符编码进行编码。使用 Unicode 字符编码的文本以逻辑顺序存储在内存中。逻辑顺序是字符和单词被读取和写入的顺序。逻辑顺序不一定与视觉顺序相同,即对应字形显示的顺序。

特定书写系统(脚本)中字形的视觉顺序称为脚本顺序。例如,罗马文本的脚本顺序是从左到右,而阿拉伯文和希伯来文的脚本顺序是从右到左。

一些书写系统除了脚本顺序外,还有规则来排列文本行上的字形和单词。例如,阿拉伯文和希伯来文的数字是从左到右排列的,即使字母是从右到左排列的。这意味着阿拉伯文和希伯来文,即使没有嵌入英文文本,也是真正的双向文本。更多信息请参见处理双向文本。

测量和定位文本

除非您使用等宽字体,否则字体中的不同字符具有不同的宽度。这意味着所有文本的定位和测量都必须考虑到确切使用了哪些字符,而不仅仅是数量。例如,要在比例字体中右对齐显示的数字列,您不能简单地使用额外的空格来定位文本。为了正确对齐列,您需要知道每个数字的确切宽度,以便相应调整。

文本通常使用多种字体和样式显示,如粗体或斜体。在这种情况下,即使是相同的字符也可能有不同的形状和宽度,这取决于其样式。为了正确定位、测量和呈现文本,您需要跟踪每个单独的字符应用于该字符的样式。幸运的是,TextLayout类可以为您完成这些工作。

要正确显示希伯来文和阿拉伯文等语言的文本,需要测量和定位每个单独的字符,并将其放置在相邻字符的上下文中。由于字符的形状和位置可能会根据上下文而变化,因此在不考虑上下文的情况下测量和定位此类文本会产生不可接受的结果。

此外,Java SE 为您提供了FontMetrics类,它使您能够获取由Font对象渲染的文本的测量值,比如字体中一行文本的高度。您可以利用这些信息在 Java 图形应用程序中精确定位文本。更多信息请参见测量文本。

物理和逻辑字体

原文:docs.oracle.com/javase/tutorial/2d/text/fonts.html

有两种字体:物理字体和逻辑字体。物理字体是实际的字体库,包括 TrueType 或 PostScript Type 1 字体。物理字体可以是 Time、Helvetica、Courier 或任何其他字体,包括国际字体。逻辑字体是以下五个字体系列:Serif、SansSerif、Monospaced、Dialog 和 DialogInput。这些逻辑字体不是实际的字体库。相反,Java 运行时环境通过逻辑字体名称将其映射到物理字体。

本节帮助您确定在应用程序中使用哪种字体。它涵盖以下主题:

  • 物理字体

    • Lucidia 字体

    • 将物理字体与您的应用程序捆绑

  • 逻辑字体

  • 使用物理和逻辑字体的优缺点

  • 字体配置文件

物理字体

物理字体是包含字形数据和表的实际字体库,使用 TrueType 或 PostScript Type 1 等字体技术,以将字符序列映射到字形序列。要获取系统中安装的所有可用字体系列的名称,请调用以下内容:

GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
String []fontFamilies = ge.getAvailableFontFamilyNames();

FontSelector 示例程序(在 FontSelector.java 中可用)演示了如何定位和选择这些字体。


注意: 应用程序不应假设任何特定的物理字体存在。然而,逻辑字体是一个安全的选择,因为它们始终存在。有关更多信息,请参阅逻辑字体。



注意: 如果您看不到 applet 运行,请至少安装Java SE Development Kit (JDK) 7 版本。


Lucidia 字体

Oracle 的 JRE 包含这一系列物理字体,也被许可用于其他 Java 平台的实现。这些字体是物理字体,但不依赖于主机操作系统。

使用这些字体的应用程序可以在这些字体可用的任何地方实现相同的外观。此外,这些字体涵盖了大量的语言(特别是欧洲和中东地区),因此您可以为支持的语言创建完全多语言的应用程序。然而,这些字体可能不在所有 JRE 中可用。此外,它们目前不涵盖完整的 Unicode 字符集;特别是,不支持中文、日文和韩文。

将物理字体与您的应用程序捆绑

有时,应用程序不能依赖于系统上安装的字体,通常是因为该字体是一种不可用的自定义字体。在这种情况下,必须将字体文件与应用程序捆绑在一起。

使用以下方法之一从现有物理字体创建 Font 对象:

Font java.awt.Font.createFont(int fontFormat, InputStream in);
Font java.awt.Font.createFont(int fontFormat, File fontFile);

要从 TrueType 字体创建 Font 对象,形式参数 fontFormat 必须是常量 Font.TRUETYPE_FONT。以下示例从 TrueType 字体文件 A.ttf 创建 Font 对象:

Font font = Font.createFont(Font.TRUETYPE_FONT, new File("A.ttf"));

直接从文件访问字体更简单、更方便。但是,如果您的代码无法访问文件系统资源,或者字体与应用程序或小程序的其余部分一起打包在 Java 存档(JAR)文件中,则可能需要一个 InputStream 对象。

createFont 方法创建一个具有点大小为 1 和样式 PLAIN 的新 Font 对象。然后可以使用 Font.deriveFont 方法将此基础字体用于派生具有不同大小、样式、变换和字体特性的新 Font 对象。例如:

try {
     //Returned font is of pt size 1
     Font font = Font.createFont(Font.TRUETYPE_FONT, new File("A.ttf"));

     //Derive and return a 12 pt version:
     //Need to use float otherwise
     //it would be interpreted as style

     return font.deriveFont(12f);

} catch (IOException|FontFormatException e) {
     // Handle exception
}

使用 deriveFont 方法很重要,因为应用程序创建的字体不属于底层字体系统所知的字体集。由于 deriveFont 方法是从最初创建的字体工作的,因此它没有这种限制。

解决此问题的方法是将创建的字体注册到图形环境中。例如:

try {
     GraphicsEnvironment ge = 
         GraphicsEnvironment.getLocalGraphicsEnvironment();
     ge.registerFont(Font.createFont(Font.TRUETYPE_FONT, new File("A.ttf"));
} catch (IOException|FontFormatException e) {
     //Handle exception
}

在将字体注册到图形环境后,该字体可以在调用 getAvailableFontFamilyNames() 时使用,并且可以在字体构造函数中使用。

逻辑字体

Java SE 定义了以下五个逻辑字体系列:

  • Dialog

  • DialogInput

  • Monospaced

  • Serif

  • SansSerif

这些字体在任何 Java 平台上都可用,并且可以被视为某些具有其名称所暗示属性的基础字体的别名。Serif 字体类似于 Times New Roman,通常用于印刷。Sans Serif 字体更适合屏幕使用。

这些字体可以根据用户的语言环境进行定制。此外,这些字体支持最广泛的代码点(Unicode 字符)范围。

除了字体系列,字体还具有其他属性,其中最重要的是 样式大小。样式有 BoldItalic

Java 2D API 使用的默认字体是 12 磅的 Dialog。这种字体是在普通 72-120 DPI 显示设备上阅读文本的典型字号。应用程序可以通过指定以下内容直接创建此字体的实例:

Font font = new Font("Dialog", Font.PLAIN, 12);

使用物理和逻辑字体的优缺点

物理字体使应用程序能够充分利用所有可用字体,实现不同的文本外观和最大的语言覆盖范围。然而,创建使用物理字体的应用程序要困难得多。

将物理字体与您的应用程序捆绑在一起,可以使您创建的应用程序在任何地方看起来都一样,并且可以完全控制您想要支持的应用程序。然而,捆绑的字体可能会很大,特别是如果您希望您的应用程序支持中文、日文和韩文。此外,您可能需要解决许可问题。

逻辑字体名称保证在任何地方都能正常工作,并且它们至少能够在主机操作系统本地化的语言中进行文本呈现(通常支持更广泛的语言范围)。然而,用于呈现文本的物理字体在不同的实现、主机操作系统和区域设置之间会有所不同,因此应用程序无法在任何地方实现相同的外观。此外,映射机制有时会限制可以呈现的字符范围。这在 JRE 版本 5.0 之前曾经是一个大问题:例如,日文文本只能在日本本地化的主机操作系统上呈现,而在其他本地化系统上即使安装了日文字体也无法呈现。对于使用 2D 字体呈现的应用程序,在 JRE 版本 5.0 及更高版本中,这个问题要少得多,因为映射机制现在通常会识别并使用所有支持的书写系统的字体(如果已安装)。

字体配置文件

Java SE 运行时环境使用字体配置文件将逻辑字体名称映射到物理字体。根据主机操作系统版本的不同映射,有几个文件支持不同的映射。这些文件位于 JRE 安装的 lib 目录中。您可以编辑或创建自己的字体配置文件,以调整映射到您特定系统设置的映射。有关更多信息,请参阅字体配置文件。

测量文本

原文:docs.oracle.com/javase/tutorial/2d/text/measuringtext.html

要正确测量文本,您需要学习一些方法和一些要避免的错误。字体度量是由Font对象呈现的文本的测量,例如字体中一行文本的高度。测量文本最常见的方法是使用封装了这些度量信息的FontMetrics实例。例如:

// get metrics from the graphics
FontMetrics metrics = graphics.getFontMetrics(font);
// get the height of a line of text in this
// font and render context
int hgt = metrics.getHeight();
// get the advance of my text in this font
// and render context
int adv = metrics.stringWidth(text);
// calculate the size of a box to hold the
// text with some padding.
Dimension size = new Dimension(adv+2, hgt+2);

对于许多应用程序来说,这种方式足以均匀间隔文本行或调整 Swing 组件的大小。

注意以下内容:

  • 这些度量是从Graphics类中获取的,因为这个类封装了FontRenderContext,这是准确测量文本所需的。在屏幕分辨率下,字体会根据易读性进行调整。随着文本大小的增加,这种调整并不是线性缩放的。因此,在 20 pt 时,字体显示的文本长度不会正好是在 10 pt 时的两倍。除了文本本身和字体之外,用于测量文本的另一个重要信息是FontRenderContext。该方法包括从用户空间到设备像素的变换,用于测量文本。

  • 高度报告时没有参考任何特定文本字符串。例如,在文本编辑器中,您希望每行文本之间具有相同的行间距时,这是有用的。

  • stringWidth()返回文本的前进宽度。前进宽度是从文本原点到随后呈现的字符串位置的距离。

在使用这些方法测量文本时,请注意文本可以向字体高度和字符串前进的矩形定义之外的任何方向延伸。

此图显示了如何使用字体度量来测量文本

通常,最简单的解决方案是确保文本不被裁剪,例如,由围绕文本的组件。在可能导致文本被裁剪的情况下添加填充。

如果此解决方案不足够,Java 2D 软件中的其他文本测量 API 可以返回矩形边界框。这些框考虑了要测量的特定文本的高度和像素化效果。

高级文本显示

原文:docs.oracle.com/javase/tutorial/2d/text/advanced.html

Java 2D API 提供了支持复杂文本布局的机制。本节描述了高级文本显示的以下特性。

使用渲染提示显示抗锯齿文本

本节介绍如何通过使用渲染提示来控制渲染质量。

使用文本属性来设置文本样式

本节解释了如何使用TextAttribute类来给文本添加下划线或删除线。

绘制多行文本

本节解释了如何使用TextLayoutLineBreakMeasurer类来定位和渲染一段样式化文本。

处理双向文本

本节讨论如何使用java.awtjava.awt.font包中的类处理双向文本。

使用渲染提示显示抗锯齿文本

原文:docs.oracle.com/javase/tutorial/2d/text/renderinghints.html

Java 2D 文本渲染可能受渲染提示的影响。

请记住最重要的文本绘制方法如下:

Graphics.drawString(String s, int x, int y);

通常,该方法会使用纯色绘制文本字符串中的每个字形,而该字形中的每个“开启”像素都会设置为该颜色。这种绘制方式产生了最高对比度的文本,但有时会出现锯齿状(锯齿状)边缘。

文本抗锯齿是一种用于平滑屏幕上文本边缘的技术。Java 2D API 使应用程序能够指定是否应使用此技术以及通过将文本渲染提示应用于Graphics来使用哪种算法。

最常见的渲染提示会将前景(文本)颜色与文本边缘的屏幕背景像素混合。要请求此提示,应用程序必须调用以下方法:

graphics2D.setRenderingHint(
        RenderingHints.KEY_TEXT_ANTIALIASING,
        RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

以下图示说明了抗锯齿功能。

此图表示“Hello World”字符串的抗锯齿提示。

如果不当使用,此方法可能使文本显得过于模糊。在这种情况下,更好的提示是以下提示:

graphics2D.setRenderingHint(
        RenderingHints.KEY_TEXT_ANTIALIASING,
        RenderingHints.VALUE_TEXT_ANTIALIAS_GASP);

这种方法会自动使用字体本身的信息来决定是使用抗锯齿还是使用纯色。

LCD 显示器具有 Java 2D API 可以利用的属性,以产生不像典型抗锯齿那样模糊的文本,但在小尺寸下更易读的文本。要求使用典型 LCD 显示器的子像素 LCD 文本模式绘制文本,应用程序必须调用以下方法:

graphics2D.setRenderingHint(
        RenderingHints.KEY_TEXT_ANTIALIASING,
        RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);

下面所示的代码示例说明了抗锯齿功能的顺序:

  1. 抗锯齿已关闭。

  2. 抗锯齿已开启。

  3. 使用TEXT_ANTIALIAS_GASP提示进行抗锯齿。


    注意: 因此,GASP 表指定仅在这些大小上使用提示,而不是“平滑”。因此,在许多情况下,结果文本显示等同于VALUE_TEXT_ANTIALIAS_OFF


  4. 使用TEXT_ANTIALIAS_LCD_HRGB提示进行抗锯齿。


注意: 如果看不到 applet 运行,请至少安装Java SE Development Kit (JDK) 7版本。


此 applet 的完整代码在AntialiasedText.java中。

使用文本属性来设置文本样式

原文:docs.oracle.com/javase/tutorial/2d/text/textattributes.html

应用程序通常需要能够应用以下文本属性:

  • 下划线 – 在文本下方绘制的线

  • 删除线 – 通过文本绘制的水平线

  • 上标下标 – 文本或字母略高于一行或相应地低于一行

  • 字间距 – 调整字符之间的空间

这些以及其他文本属性可以通过使用 Java 2D TextAttribute 类来应用。

要应用这些文本属性,请将它们添加到 Font 对象中。例如:

Map<TextAttribute, Object> map =
    new Hashtable<TextAttribute, Object>();
map.put(TextAttribute.KERNING,
    TextAttribute.KERNING_ON);
font = font.deriveFont(map);
graphics.setFont(font);

下面的代码示例显示了按以下顺序应用文本属性:

  1. 示例字符串(未应用文本属性)

  2. 字间距

  3. 字间距和下划线

  4. 字间距、下划线和删除线

  5. 字间距、下划线、删除线和颜色


注意: 如果您看不到 applet 运行,您需要安装至少 Java SE Development Kit (JDK) 7 版本。


此 applet 的完整代码在 AttributedText.java 中。

绘制多行文本

原文:docs.oracle.com/javase/tutorial/2d/text/drawmulstring.html

如果您有一段带样式的文本,希望将其适应特定宽度,可以使用 LineBreakMeasurer 类。这个类使得带样式的文本可以被分成行,以便它们适应特定的视觉前进。每一行作为一个 TextLayout 对象返回,代表不可改变的、带样式的字符数据。然而,这个类也使得可以访问布局信息。TextLayoutgetAscentgetDescent 方法返回有关用于在组件中定位行的字体的信息。文本被存储为 AttributedCharacterIterator 对象,以便字体和点大小属性可以与文本一起存储。

以下小程序使用 LineBreakMeasurerTextLayoutAttributedCharacterIterator 在组件中定位一段带样式的文本。


注意: 如果您看不到小程序运行,您需要安装至少 Java SE Development Kit (JDK) 7 版本。


这个小程序的完整代码在 LineBreakSample.java 中。

以下代码创建一个包含字符串 vanGogh 的迭代器。检索迭代器的开始和结束,并从迭代器创建一个新的 LineBreakMeasurer

    AttributedCharacterIterator paragraph = vanGogh.getIterator();
    paragraphStart = paragraph.getBeginIndex();
    paragraphEnd = paragraph.getEndIndex();
    FontRenderContext frc = g2d.getFontRenderContext();
    lineMeasurer = new LineBreakMeasurer(paragraph, frc);

窗口的大小用于确定何处应该断行。同时为段落中的每一行创建一个 TextLayout 对象。

// Set break width to width of Component.
float breakWidth = (float)getSize().width;
float drawPosY = 0;
// Set position to the index of the first
// character in the paragraph.
lineMeasurer.setPosition(paragraphStart);

// Get lines from until the entire paragraph
// has been displayed.
while (lineMeasurer.getPosition() < paragraphEnd) {

    TextLayout layout = lineMeasurer.nextLayout(breakWidth);

    // Compute pen x position. If the paragraph
    // is right-to-left we will align the
    // TextLayouts to the right edge of the panel.
    float drawPosX = layout.isLeftToRight()
        ? 0 : breakWidth - layout.getAdvance();

    // Move y-coordinate by the ascent of the
    // layout.
    drawPosY += layout.getAscent();

    // Draw the TextLayout at (drawPosX,drawPosY).
    layout.draw(g2d, drawPosX, drawPosY);

    // Move y-coordinate in preparation for next
    // layout.
    drawPosY += layout.getDescent() + layout.getLeading();
}

TextLayout 类通常不会被应用程序直接创建。然而,当应用程序需要直接处理在文本中特定位置应用了样式(文本属性)的文本时,这个类就非常有用。例如,要在段落中画一个单词斜体,应用程序需要为每个子字符串执行测量并设置字体。如果文本是双向的,这个任务就不那么容易正确完成。通过从 AttributedString 对象创建一个 TextLayout 对象来处理这个问题。请参考 Java SE 规范了解更多关于 TextLayout 的信息。

处理双向文本

译文:docs.oracle.com/javase/tutorial/2d/text/textlayoutbidirectionaltext.html

这一部分讨论如何使用java.awtjava.awt.font包中的类处理双向文本。这些类允许您以任何语言或脚本绘制样式化文本,这些语言或脚本受到 Unicode 标准的支持:这是一个全球字符编码系统,用于处理各种现代、古典和历史语言。在绘制文本时,必须考虑文本的阅读方向,以便字符串中的所有单词都能正确显示。这些类维护文本的方向,并正确绘制它,无论字符串是从左到右、从右到左还是双向运行。双向文本对于正确定位插入符、准确定位选择以及正确显示多行文本提出了有趣的问题。另外,双向和从右到左的文本对于根据右箭头和左箭头键的按压正确移动插入符也存在类似问题。

下列主题包括:

  • 文本排序

  • 操作双向文本

    • 显示插入符

    • 移动插入符

    • 点击测试

    • 突出显示选择

  • 在 Java 应用程序中执行文本布局

  • 使用 TextLayout 类管理文本布局

    • 使用 TextLayout 类布局文本

    • 使用 TextLayout 类显示双插入符

    • 使用 TextLayout 类移动插入符

    • 使用 TextLayout 类进行点击测试

    • 使用 TextLayout 类突出显示选择

如果您计划使用 Swing 组件,请参阅使用 JTextComponent 类处理双向文本和使用文本组件获取更多信息。

文本排序

Java SE 在内存中以逻辑顺序存储文本,这是字符和单词读取和写入的顺序。逻辑顺序不一定与视觉顺序相同,后者是显示相应字形的顺序。

即使混合使用多种语言,双向文本中必须保持书写系统的视觉顺序。下图展示了一个嵌入在英语句子中的阿拉伯短语。

注意: 在这个和后续的示例中,阿拉伯语和希伯来语文本由大写字母表示,空格由下划线表示。每个示例包含两部分:存储在内存中的字符表示(逻辑顺序的字符)后跟这些字符如何显示的表示(视觉顺序的字符)。字符框下面的数字表示插入偏移量。

嵌入在英语句子中的阿拉伯短语

尽管它们是英语句子的一部分,但阿拉伯语单词以阿拉伯语书写顺序从右到左显示。因为斜体的阿拉伯语单词在逻辑上位于普通文本的阿拉伯语单词之后,所以在视觉上位于普通文本的左侧。

当显示混合左到右和右到左文本的行时,基本方向很重要。基本方向是主要书写系统的书写顺序。例如,如果文本主要是英语并带有一些嵌入的阿拉伯语,则基本方向是从左到右。如果文本主要是阿拉伯语并带有一些嵌入的英语或数字,则基本方向是从右到左。

基本方向确定具有共同方向的文本段的显示顺序。在前面图中显示的示例中,基本方向是从左到右。在这个示例中有三个方向性运行:句子开头的英语文本从左到右运行,阿拉伯文本从右到左运行,句号从左到右运行。

图形通常嵌入在文本流中。这些内联图形在影响文本流和换行方式方面类似于字形。这样的内联图形需要使用相同的双向布局算法定位,以便它们出现在字符流的适当位置。

Java SE 使用Unicode 双向算法,这是一种用于在一行内对字形进行排序的算法,从而确定双向文本的方向性。在大多数情况下,您无需包含任何额外信息,以便该算法获取正确的显示顺序。

操纵双向文本

为了允许用户编辑双向文本,您必须能够执行以下操作:

  • 显示插入符

  • 移动插入符

  • 点击测试

  • 突出显示选择

显示插入符

在可编辑文本中,插入符用于图形化表示当前插入点,即文本中新字符将被插入的位置。通常,插入符显示为两个字形之间闪烁的垂直条。新字符被插入并显示在插入符的位置。

计算插入符位置可能会很复杂,特别是对于双向文本。在方向边界上的插入偏移量有两个可能的插入符位置,因为对应于字符偏移量的两个字形不会相邻显示。如下图所示。在此图中,插入符显示为方括号,表示插入符对应的字形。

双插入符

字符偏移量 8 对应于下划线后和A之前的位置。如果用户输入阿拉伯字符,其字形将显示在A的右侧;如果用户输入英文字符,其字形将显示在下划线的右侧。

为了处理这种情况,一些系统显示双插入符,一个强(主要)插入符和一个弱(次要)插入符。强插入符指示插入的字符的方向与文本基础方向相同时将显示在何处。弱插入符显示插入的字符的方向与基础方向相反时将显示在何处TextLayout自动支持双插入符。

当处理双向文本时,不能简单地将字符偏移量之前的字形宽度相加以计算插入符位置。如果这样做,插入符将被绘制在错误的位置,如下图所示:

插入符绘制错误

为了正确定位插入符,需要将偏移量左侧的字形宽度相加,并考虑当前上下文。除非考虑上下文,否则字形度量可能不会与显示匹配。(上下文可能会影响使用哪些字形。)

移动插入符

所有文本编辑器都允许用户使用箭头键移动插入符。用户期望插入符沿按下的箭头键方向移动。在从左到右的文本中,移动插入偏移量很简单:右箭头键将插入偏移量增加一,左箭头键将其减少一。在双向文本或带有连字的文本中,此行为会导致插入符跨越方向边界的字形并在不同方向运行内部反向移动。

要在双向文本中平滑移动插入符,需要考虑文本运行的方向。当按下右箭头键时不能简单地增加插入偏移量,当按下左箭头键时减少它。如果当前插入偏移量位于从右到左字符的运行内,右箭头键应减少插入偏移量,左箭头键应增加它。

在跨越方向边界时移动插入符更加复杂。下图说明了当用户使用箭头键导航时,当穿越方向边界时会发生什么。在显示的文本中向右移动三个位置对应于移动到字符偏移 7、19,然后是 18。

插入符移动

某些字形之间永远不应该有插入符;相反,插入符应该移动得像这些字形代表一个单一字符一样。例如,如果一个 o 和一个变音符由两个单独的字符表示,那么它们之间永远不应该有插入符。

TextLayout 类提供了方法(getNextRightHitgetNextLeftHit),使您能够轻松地在双向文本中平滑地移动插入符。

命中测试

通常,设备空间中的位置必须转换为文本偏移量。例如,当用户在可选择文本上单击鼠标时,鼠标位置将转换为文本偏移量,并用作选择范围的一端。从逻辑上讲,这是放置插入符的逆过程。

当处理双向文本时,显示中的单个视觉位置可以对应源文本中的两个不同偏移量,如下图所示:

命中测试双向文本

因为单个视觉位置可以对应两个不同的偏移量,所以命中测试双向文本不仅仅是测量字形宽度直到找到正确位置的字形,然后将该位置映射回字符偏移量。检测命中位置所在的一侧有助于区分这两种选择。

您可以使用 TextLayout.hitTestChar 进行命中测试。命中信息封装在 TextHitInfo 对象中,并包括有关命中位置所在一侧的信息。

突出显示选择

一段选定的字符范围通过一个高亮区域图形化表示,其中字形以反色或不同背景颜色显示。

高亮区域,就像插入符一样,在双向文本中比单向文本更复杂。在双向文本中,一段连续的字符范围在显示时可能没有连续的高亮区域。相反,显示为视觉上连续的一系列字形的高亮区域可能不对应单一、连续的字符范围。

这导致双向文本中突出选择的两种策略:

  • 逻辑高亮:使用逻辑高亮,所选字符在文本模型中始终是连续的,而高亮区域允许是不连续的。以下是逻辑高亮的示例:

    逻辑高亮示例(连续字符)

  • 可视高亮:使用可视高亮,可能会有多个选定字符范围,但高亮区域始终是连续的。以下是可视高亮的示例:

    可视高亮示例(连续高亮区域)

逻辑高亮更容易实现,因为所选字符在文本中始终是连续的。

示例SelectionSample.java演示了逻辑高亮:

选择示例;逻辑高亮演示

在 Java 应用程序中执行文本布局

根据您使用的 Java API,您可以根据需要对文本布局进行精细或粗略的控制:

  • 如果您只想显示一块文本或需要一个可编辑的文本控件,您可以使用JTextComponent,它将为您执行文本布局。JTextComponent旨在处理大多数国际应用程序的需求,并支持双向文本。有关JTextComponent的更多信息,请参见使用 JTextComponent 类处理双向文本和使用文本组件。

  • 如果您想显示一个简单的文本字符串,您可以调用方法Graphics2D.drawString,让 Java 2D 为您布局字符串。您还可以使用Graphics2D.drawString来呈现带样式的字符串和包含双向文本的字符串。有关通过Graphics2D呈现文本的更多信息,请参见使用文本 API。

  • 如果您想实现自己的文本编辑例程,可以使用 TextLayout 来管理文本布局、高亮显示和点击检测。TextLayout 提供的功能处理了大多数常见情况,包括具有混合字体、混合语言和双向文本的文本字符串。有关使用 TextLayout 的更多信息,请参见管理文本布局。

  • 如果您想完全控制文本的形状和位置,可以使用 Font 类构建自己的 GlyphVector 实例,然后通过 Graphics2D 类进行渲染。

通常,您不需要自己执行文本布局操作。对于大多数应用程序,JTextComponent 是显示静态和可编辑文本的最佳解决方案。但是,JTextComponent 不支持双插入符或双向文本中的不连续选择的显示。如果您的应用程序需要这些功能,或者您更喜欢实现自己的文本编辑例程,可以使用 Java 2D 文本布局 API。

使用 TextLayout 类管理文本布局

TextLayout 类支持包含多种样式和来自不同书写系统(包括阿拉伯文和希伯来文)的字符的文本。(阿拉伯文和希伯来文特别难以显示,因为您必须重新排列和重新排序文本以获得可接受的表示。)

即使您只处理英文文本,TextLayout 也简化了显示和测量文本的过程。通过使用 TextLayout,您可以在不费额外努力的情况下实现高质量的排版。

TextLayout 被设计为在显示简单的单向文本时不会产生显著的性能影响。当使用 TextLayout 显示阿拉伯文或希伯来文时,会有一些额外的处理开销。但是,这通常是每个字符的微秒数量级,并且被正常绘图代码的执行所主导。

TextLayout 类为您管理字形的定位和排序。您可以使用TextLayout 来执行以下操作:

  • 使用 TextLayout 类进行文本布局

  • 使用 TextLayout 类显示双光标

  • 使用 TextLayout 类移动光标

  • 使用 TextLayout 类进行点击测试

  • 使用 TextLayout 类突出显示选择内容

使用 TextLayout 类进行文本布局

TextLayout 自动布局文本,包括双向文本,具有正确的形状和顺序。为了正确形状和排序表示一行文本的字形,TextLayout 必须了解文本的完整上下文:

  • 如果文本适合单行,例如按钮的单词标签或对话框中的一行文本,您可以直接从文本构造一个TextLayout

  • 如果您有更多文本无法适合单行或想要在单行文本上分隔制表符段,您不能直接构造一个TextLayout。您必须使用LineBreakMeasurer 提供足够的上下文。有关更多信息,请参见绘制多行文本。

文本的基本方向通常由文本上的属性(样式)设置。如果该属性缺失,TextLayout 将遵循 Unicode 双向算法,并从段落中的初始字符推导基本方向。

使用 TextLayout 类显示双光标

TextLayout 保留光标信息,如光标Shape、位置和角度。您可以使用此信息轻松地在单向和双向文本中显示光标。在为双向文本绘制光标时,使用TextLayout 可确保光标位置正确。

TextLayout提供默认插入符Shapes并自动支持双插入符。对于斜体和倾斜字形,TextLayout会产生倾斜插入符,如下图所示。这些插入符位置也用作高亮和命中测试之间的字形边界,有助于产生一致的用户体验。

倾斜插入符

给定插入偏移量,getCaretShapes方法返回一个包含两个Shape对象的数组:元素 0 包含强插入符,元素 1 包含弱插入符(如果存在)。要显示双插入符,只需绘制两个插入符Shape对象;插入符将自动呈现在正确的位置。

如果您想使用自定义插入符,可以从TextLayout中检索插入符的位置和角度,并自行绘制它们。

示例HitTestSample.java演示了双插入符。

点击希伯来文本旁边的o会记录用户在o之后点击的位置,这部分属于英文文本。这将使弱(黑色)插入符位于o旁边,而强插入符(红色)位于H之前:

点击希伯来文本旁边的'o'的命中测试示例

点击o右侧的空格会记录用户点击了空格,这部分属于希伯来文本。这将使强(红色)插入符位于o旁边,而弱插入符(黑色)位于H之前:

点击'o'右侧的空格的命中测试示例

使用 TextLayout 类移动插入符

您还可以使用TextLayout类确定用户按下左箭头或右箭头键时的插入偏移量。给定表示当前插入偏移量的TextHitInfo对象,getNextRightHit方法返回一个表示正确插入偏移量的TextHitInfo对象,如果按下右箭头键。getNextLeftHit方法为左箭头键提供相同的信息。

来自示例ArrowKeySample.java的以下摘录演示了确定用户按下左箭头或右箭头键时的插入偏移量的方法:

public class ArrowKeySample extends JPanel implements KeyListener {

  // ...

  private static void createAndShowGUI() {
    // Create and set up the window.
    ArrowKey demo = new ArrowKey();
    frame = new JFrame("Arrow Key Sample");
    frame.addKeyListener(demo);
    // ...
  }

  private void handleArrowKey(boolean rightArrow) {
    TextHitInfo newPosition;
    if (rightArrow) {
      newPosition = textLayout.getNextRightHit(insertionIndex);
    } else {
      newPosition = textLayout.getNextLeftHit(insertionIndex);
    }

    // getNextRightHit() / getNextLeftHit() will return null if
    // there is not a caret position to the right (left) of the
    // current position.
    if (newPosition != null) {
      // Update insertionIndex.
      insertionIndex = newPosition.getInsertionIndex();
      // Repaint the Component so the new caret(s) will be displayed.
      frame.repaint();
    }
  }

  // ...

  @Override
  public void keyPressed(KeyEvent e) {
    int keyCode = e.getKeyCode();
    if (keyCode == KeyEvent.VK_LEFT || keyCode == KeyEvent.VK_RIGHT) {
      handleArrowKey(keyCode == KeyEvent.VK_RIGHT);
    }
  }
}

使用 TextLayout 类进行命中测试

TextLayout类提供了一个简单的文本命中测试机制。hitTextChar方法以鼠标的xy坐标作为参数,并返回一个TextHitInfo对象。TextHitInfo包含指定位置的插入偏移量和命中位置的侧面。插入偏移量是最接近命中位置的偏移量:如果命中位置超过行尾,将返回行尾的偏移量。

来自HitTestSample.java的以下摘录从鼠标点击中检索偏移量:

private class HitTestMouseListener extends MouseAdapter {
    public void mouseClicked(MouseEvent e) {
      Point2D origin = computeLayoutOrigin();
      // Compute the mouse click location relative to
      // textLayout's origin.
      float clickX = (float) (e.getX() - origin.getX());
      float clickY = (float) (e.getY() - origin.getY());
      // Get the character position of the mouse click.
      TextHitInfo currentHit = textLayout.hitTestChar(clickX, clickY);
      insertionIndex = currentHit.getInsertionIndex();
      // Repaint the Component so the new caret(s) will be displayed.
      repaint();
    }
  }

使用 TextLayout 类进行高亮选择

你可以从TextLayout获取代表高亮区域的Shape。在计算高亮区域的尺寸时,TextLayout会自动考虑上下文。TextLayout支持逻辑和视觉高亮。

来自SelectionSample.java的以下摘录演示了显示高亮文本的一种方法:

public void paint(Graphics g) {

    // ...

    boolean haveCaret = anchorEnd == activeEnd;

    if (!haveCaret) {
      // Retrieve highlight region for selection range.
      Shape highlight = 
          textLayout.getLogicalHighlightShape(anchorEnd, activeEnd);
      // Fill the highlight region with the highlight color.
      graphics2D.setColor(HIGHLIGHT_COLOR);
      graphics2D.fill(highlight);
    }

    // ...

  }

  // ...

  private class SelectionMouseMotionListener extends MouseMotionAdapter {
    public void mouseDragged(MouseEvent e) {
      Point2D origin = computeLayoutOrigin();
      // Compute the mouse location relative to
      // textLayout's origin.
      float clickX = (float) (e.getX() - origin.getX());
      float clickY = (float) (e.getY() - origin.getY());
      // Get the character position of the mouse location.
      TextHitInfo position = textLayout.hitTestChar(clickX, clickY);
      int newActiveEnd = position.getInsertionIndex();
      // If newActiveEnd is different from activeEnd, update activeEnd
      // and repaint the Panel so the new selection will be displayed.
      if (activeEnd != newActiveEnd) {
        activeEnd = newActiveEnd;
        frame.repaint();
      }
    }
  }

  private class SelectionMouseListener extends MouseAdapter {
    public void mousePressed(MouseEvent e) {
      Point2D origin = computeLayoutOrigin();
      // Compute the mouse location relative to
      // TextLayout's origin.
      float clickX = (float) (e.getX() - origin.getX());
      float clickY = (float) (e.getY() - origin.getY());
      // Set the anchor and active ends of the selection
      // to the character position of the mouse location.
      TextHitInfo position = textLayout.hitTestChar(clickX, clickY);
      anchorEnd = position.getInsertionIndex();
      activeEnd = anchorEnd;
      // Repaint the Panel so the new selection will be displayed.
      frame.repaint();
    }
  }

方法SelectionMouseListener.mousePressed指定了变量anchorEnd,它是鼠标点击的文本位置。方法SelectionMouseMotionListener.mouseDragged指定了变量activeEnd,它是鼠标拖动到的文本位置。paint方法检索一个代表所选文本的Shape对象(即anchorEndactiveEnd位置之间的文本)。然后paint方法用高亮颜色填充Shape对象。

教程:处理图像

原文:docs.oracle.com/javase/tutorial/2d/images/index.html

正如您已经从 图像 教程中了解的那样,Image 由以像素为单位的宽度和高度描述,并且具有与绘图表面无关的坐标系。

处理图像时有许多常见任务。

  • 将外部 GIF、PNG JPEG 图像格式文件加载到 Java 2D 使用的内部图像表示中。

  • 直接创建 Java 2D 图像并对其进行渲染。

  • 将 Java 2D 图像的内容绘制到绘图表面上。

  • 将 Java 2D 图像的内容保存到外部 GIF、PNG 或 JPEG 图像文件中。

本课程教授如何加载、显示和保存图像的基础知识。

您必须了解的两个主要类来处理图像:

  • java.awt.Image 类是表示图形图像的像素矩形数组的超类。

  • java.awt.image.BufferedImage 类扩展了 Image 类,允许应用程序直接操作图像数据(例如,检索或设置像素颜色)。应用程序可以直接构造此类的实例。

BufferedImage 类是 Java 2D 立即模式成像 API 的基石。它管理内存中的图像,并提供存储、解释和获取像素数据的方法。由于 BufferedImageImage 的子类,因此可以通过接受 Image 参数的 GraphicsGraphics2D 方法来呈现它。

BufferedImage 本质上是具有可访问数据缓冲区的 Image。因此,直接使用 BufferedImage 更有效。BufferedImage 具有 ColorModel 和图像数据的 Raster。ColorModel 提供图像像素数据的颜色解释。

Raster 执行以下功能:

  • 表示图像的矩形坐标

  • 在内存中维护图像数据

  • 提供从单个图像数据缓冲区创建多个子图像的机制

  • 提供访问图像内特定像素的方法

图像的基本操作在以下部分中表示:

读取/加载图像

本节解释了如何使用 Image I/O API 将外部图像格式的图像加载到 Java 应用程序中。

绘制图像

本节教授如何使用 GraphicsGraphics2D 类的 drawImage 方法显示图像。

创建和绘制到图像

本节描述了如何创建图像以及如何将图像本身用作绘图表面。

写入/保存图像

本节解释了如何以适当的格式保存创建的图像。

读取/加载图像

原文:docs.oracle.com/javase/tutorial/2d/images/loadimage.html

当你想到数字图像时,你可能会想到采样图像格式,比如数字摄影中使用的 JPEG 图像格式,或者网页常用的 GIF 图像。所有可以使用这些图像的程序都必须首先将它们从外部格式转换为内部格式。

Java 2D 支持使用其 Image I/O API 将这些外部图像格式加载到其 BufferedImage 格式中,该 API 位于 javax.imageio 包中。Image I/O 内置支持 GIF、PNG、JPEG、BMP 和 WBMP。Image I/O 也是可扩展的,因此开发人员或管理员可以为其他格式“插入”支持。例如,TIFF 和 JPEG 2000 的插件是单独可用的。

要从特定文件加载图像,请使用以下代码,这段代码来自 LoadImageApp.java

BufferedImage img = null;
try {
    img = ImageIO.read(new File("strawberry.jpg"));
} catch (IOException e) {
}

Image I/O 将文件的内容识别为 JPEG 格式图像,并将其解码为可以被 Java 2D 直接使用的 BufferedImage

LoadImageApp.java 展示了如何显示这个图像。

如果代码在小程序中运行,那么从小程序代码库获取图像就像轻而易举一样。以下摘录来自 LoadImageApplet.java

try {
    URL url = new URL(getCodeBase(), "examples/strawberry.jpg");
    img = ImageIO.read(url);
} catch (IOException e) {
}

这个示例中使用的 getCodeBase 方法返回部署在 Web 服务器上时包含这个小程序的目录的 URL。如果小程序是本地部署的,getCodeBase 返回 null,小程序将无法运行。

以下示例展示了如何使用 getCodeBase 方法加载 strawberry.jpg 文件。


注意: 如果你看不到小程序运行,你需要至少安装 Java SE Development Kit (JDK) 7 版本。


LoadImageApplet.java 包含了这个示例的完整代码,这个小程序需要 strawberry.jpg 图像文件。

除了从文件或 URLS 读取外,Image I/O 还可以从其他来源读取,比如 InputStreamImageIO.read() 对于大多数应用程序来说是最直接的便利 API,但 javax.imageio.ImageIO 类提供了更多静态方法,用于更高级的 Image I/O API 的用法。这个类上的方法集仅代表了用于发现关于图像信息和控制图像解码(读取)过程的丰富 API 集合的一部分。

我们将在 写入/保存图像 部分中进一步探讨 Image I/O 的其他功能。

绘制图像

原文:docs.oracle.com/javase/tutorial/2d/images/drawimage.html

正如您已经了解的那样,Graphics.drawImage方法在特定位置绘制图像:

boolean Graphics.drawImage(Image img,
                   int x, int y,
                   ImageObserver observer);

x,y位置指定了图像左上角的位置。observer参数通知应用程序异步加载的图像更新。observer参数通常不直接使用,对于BufferedImage类来说,通常为 null。

描述的方法仅适用于整个图像要绘制的情况,将图像像素映射到用户空间坐标 1:1。有时应用程序需要绘制图像的一部分(子图像),或者缩放图像以覆盖绘图表面的特定区域,或在绘制之前对图像进行变换或过滤。

drawImage()方法的重载执行这些操作。例如,drawImage()方法的以下重载使您可以绘制指定图像的指定区域的尽可能多的部分,将其缩放以适合目标可绘制表面的指定区域:

boolean Graphics.drawImage(Image img,
       int dstx1, int dsty1, int dstx2, int dsty2,
       int srcx1, int srcy1, int srcx2, int srcy2,
       ImageObserver observer);

src参数表示要复制和绘制的图像区域。dst参数显示要由源区域覆盖的目标区域。dstx1, dsty1坐标定义了绘制图像的位置。目标区域的宽度和高度维度由以下表达式计算:(dstx2-dstx1), (dsty2-dsty1)。如果源区域和目标区域的尺寸不同,Java 2D API 将根据需要进行放大或缩小。

以下代码示例将图像分成四个象限,并随机将源图像的每个象限绘制到目标的不同象限。


注意: 如果您看不到小程序运行,您需要安装至少Java SE Development Kit (JDK) 7版本。


此小程序的完整代码在JumbledImageApplet.java中。

此示例使用以下代码绘制混乱的duke_skateboard.jpg图像。它迭代源图像的四个子图像,依次将每个子图像绘制到随机选择的目标象限中。

/* divide the image 'bi' into four rectangular
 * areas and draw each of these areas in to a
 * different part of the image, so as to jumble
 * up the image.  'cells' is an array which has
 * been populated with values which redirect
 * drawing of one subarea to another subarea.
 */
int cellWidth = bi.getWidth(null)/2;
int cellHeight = bi.getHeight(null)/2;
for (int x=0; x<2; x++) {
    int sx = x*cellWidth;
    for (int y=0; y<2; y++) {
        int sy = y*cellHeight;
        int cell = cells[x*2+y];
        int dx = (cell / 2) * cellWidth;
        int dy = (cell % 2) * cellHeight;
        g.drawImage(bi,
                    dx, dy, 
                    x+cellWidth, dy+cellHeight,
                    sx, sy,
                    sx+cellWidth, sy+cellHeight,
                    null);
    }
}

图像过滤

除了复制和缩放图像外,Java 2D API 还可以对图像进行滤镜处理。滤镜是通过将算法应用于源图像的像素来绘制或生成新图像。可以使用以下方法应用图像滤镜:

void Graphics2D.drawImage(BufferedImage img,
                          BufferedImageOp op,
                          int x, int y)

BufferedImageOp 参数实现了滤镜。以下小程序代表了一个在文本上方绘制的图像。拖动滑块以通过图像显示更多或更少的文本,并使图像更加透明。


注意: 如果您看不到小程序运行,请至少安装Java SE Development Kit (JDK) 7版本。


以下代码显示了如何通过使用RescaleOp对象对带有alpha通道的BufferedImage对象执行滤镜操作,并通过该对象重新调整 alpha 通道。alpha 通道确定每个像素的透明度。它还指定了此图像覆盖的程度。

/* Create an ARGB BufferedImage */
BufferedImage img = ImageIO.read(imageSrc);
int w = img.getWidth(null);
int h = img.getHeight(null);
BufferedImage bi = new
    BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics g = bi.getGraphics();
g.drawImage(img, 0, 0, null);

/*
 * Create a rescale filter op that makes the image
 * 50% opaque.
 */
float[] scales = { 1f, 1f, 1f, 0.5f };
float[] offsets = new float[4];
RescaleOp rop = new RescaleOp(scales, offsets, null);

/* Draw the image, applying the filter */
g2d.drawImage(bi, rop, 0, 0);

完整示例在SeeThroughImageApplet.java中包含了使用滑块调整透明度从初始 50%的代码。此示例还需要 duke_skateboard.jpg 图像。

RescaleOp 对象只是可以创建的许多滤镜之一。Java 2D API 具有几种内置滤镜,包括以下内容:

  • ConvolveOp。每个输出像素都是从源图像中周围像素计算出来的。可用于模糊或锐化图像。

  • AffineTransformOp。此滤镜通过在像素位置上应用变换将源中的像素映射到目标中的不同位置。

  • LookupOp。此滤镜使用应用提供的查找表重新映射像素颜色。

  • RescaleOp。此滤镜将颜色乘以某个因子。可用于使图像变亮或变暗,增加或减少其不透明度等。

以下示例使用了描述的每个滤镜以及缩放:


注意: 如果您看不到小程序运行,请至少安装Java SE Development Kit (JDK) 7版本。


此小程序的完整代码在ImageDrawingApplet.java中,此小程序需要 bld.jpg 图像。

使用下拉菜单选择图像缩放或滤镜操作。

创建和绘制图像

原文:docs.oracle.com/javase/tutorial/2d/images/drawonimage.html

我们已经知道如何加载现有图像,该图像在您的系统中创建并存储,或者在任何网络位置。但是,您可能还想创建一个新图像作为像素数据缓冲区。

在这种情况下,您可以手动创建一个 BufferedImage 对象,使用该类的三个构造函数:

  • new BufferedImage(width, height, type) - 构造一个预定义图像类型的 BufferedImage

  • new BufferedImage(width, height, type, colorModel) - 构造一个预定义图像类型的 BufferedImageTYPE_BYTE_BINARYTYPE_BYTE_INDEXED

  • new BufferedImage(colorModel, raster, premultiplied, properties) - 使用指定的 ColorModelRaster 构造一个新的 BufferedImage

另一方面,我们可以使用 Component 类的方法。这些方法可以分析给定 ComponentGraphicsConfiguration 的显示分辨率,并创建一个适当类型的图像。

  • Component.createImage(width, height)

  • GraphicsConfiguration.createCompatibleImage(width, height)

  • GraphicsConfiguration.createCompatibleImage(width, height, transparency)

GraphicsConfiguration 返回一个 BufferedImage 类型的对象,但 Component 返回一个 Image 类型的对象,如果您需要一个 BufferedImage 对象,那么您可以在代码中执行 instanceof 并转换为 BufferedImage。

正如在之前的课程中已经提到的,我们不仅可以在屏幕上渲染图像。图像本身可以被视为一个绘图表面。您可以使用 BufferedImage 类的 createGraphics() 方法来实现这一目的:

...

BufferedImage off_Image =
  new BufferedImage(100, 50,
                    BufferedImage.TYPE_INT_ARGB);

Graphics2D g2 = off_Image.createGraphics();

另一个有趣的离屏图像的用途是自动双缓冲。这个功能允许通过将图像绘制到后备缓冲区,然后将该缓冲区复制到屏幕上,而不是直接绘制到屏幕上,从而避免动画图像的闪烁。

Java 2D 还允许访问用于离屏图像的硬件加速,这可以提供更好的渲染性能以及从这些图像复制。您可以通过使用 Image 类的以下方法来获得此功能的好处:

  • getCapabilities 方法允许您确定图像当前是否加速。

  • setAccelerationPriority 方法允许您设置有关图像加速重要性的提示。

  • getAccelerationPriority 方法获取有关加速重要性的提示。

写入/保存图像

原文:docs.oracle.com/javase/tutorial/2d/images/saveimage.html

本课程从解释如何使用javax.imageio包开始,将图像从外部图像格式加载到 Java 2D 使用的内部BufferedImage格式。然后解释如何使用Graphics.drawImage()来绘制该图像,可选择进行滤镜处理。

最后一阶段是将BufferedImage对象保存为外部图像格式。这可能是最初由Image I/O类从外部图像格式加载并可能使用 Java 2D API 修改的图像,也可能是由 Java 2D 创建的图像。

Image I/O类提供了一种简单的方法,在以下示例中以各种图像格式保存图像:

static boolean ImageIO.write(RenderedImage im, 
                             String formatName,
                             File output)  throws IOException


注意: BufferedImage类实现了RenderedImage接口。


formatName参数选择要保存BufferedImage的图像格式。

try {
    // retrieve image
    BufferedImage bi = getMyImage();
    File outputfile = new File("saved.png");
    ImageIO.write(bi, "png", outputfile);
} catch (IOException e) {
    ...
}

ImageIO.write方法调用实现 PNG 写入的代码,即“PNG 写入插件”。术语插件用于Image I/O是可扩展的,可以支持各种格式。

但以下标准图像格式插件:JPEG、PNG、GIF、BMP 和 WBMP 始终存在。

每种图像格式都有其优点和缺点:

格式 优点 缺点
GIF 支持动画和透明像素 仅支持 256 种颜色和不支持半透明
PNG 比 GIF 或 JPG 更好的选择,用于高色彩无损图像,支持半透明 不支持动画
JPG 适用于摄影图像 压缩损失大,不适合文本、屏幕截图或需要完全保留原始图像的任何应用程序

对于大多数应用程序来说,使用这些标准插件就足够了。它们的优点是易于获取。Image I/O类提供了一种插入对额外格式的支持的方式,可以使用许多这样的插件。如果您想知道系统中可加载或保存的文件格式,可以使用ImageIO类的getReaderFormatNamesgetWriterFormatNames方法。这些方法返回一个字符串数组,列出了此 JRE 支持的所有格式。

String writerNames[] = ImageIO.getWriterFormatNames();

返回的名称数组将包括任何已安装的附加插件,这些名称中的任何一个都可以用作格式名称来选择图像写入器。以下代码示例是一个简单版本的完整图像编辑/修饰程序,使用了修订版本的ImageDrawingApplet.java示例程序,可以按以下方式使用:

  • 图像首先通过 Image I/O 加载

  • 用户从下拉列表中选择一个滤镜,然后绘制一个新的更新图像

  • 用户从下拉列表中选择保存格式

  • 接下来会出现文件选择器,用户选择保存图像的位置

  • 修改后的图像现在可以被其他桌面应用程序查看。

这个示例的完整代码在SaveImage.java中表示。

在本课程中,您仅学习了Image I/O的基础知识,该知识提供了广泛的支持,包括直接使用ImageWriter插件来实现对编码过程的更精细控制。 ImageIO 可以写入多个图像、图像元数据,并确定质量与大小之间的权衡。更多信息请参见Java Image I/O API 指南。

课程:打印

原文:docs.oracle.com/javase/tutorial/2d/printing/index.html

由于 Java 2D API 使您能够在任何表面上绘制,因此其自然扩展是能够打印 Java 2D 图形。打印机可以被视为与显示器一样的图形设备。

Java 2D 打印 API 不仅限于打印图形。它还使您能够打印应用程序用户界面的内容。内容可以通过将原始数据发送到打印机并在 Java 2D 打印 API 的格式控制下进行打印,或者使用 Java 2D 图形 API 来打印。

在这节课中,您将探索 Java 2D 打印 API 的打印机和作业控制功能,这些功能是对渲染元素的补充。您将学习如何查找系统或网络上配置的打印机,并发现有关这些打印机的信息,例如支持的纸张尺寸,并选择这些属性进行打印和用户对话框。

参与打印的主要类和接口在java.awt.printjavax.print包中(最后一个包允许您访问打印服务)。

基本的打印操作在以下部分中表示:

  • 一个基本的打印程序 – 本节描述了Printable接口并呈现了一个基本的打印程序。

  • 使用打印设置对话框– 本节解释了如何显示打印设置对话框。

  • 打印多页文档 – 本节解释了如何使用分页打印多页文档。

  • 使用打印服务和属性 – 本节教您有关打印服务的知识,如何指定打印数据格式,以及如何使用javax.print包创建打印作业。

  • 打印用户界面的内容 – 本节解释了如何打印窗口或框架的内容。

  • Swing 组件中的打印支持 – 本节简要描述了Swing中相关打印功能,并引用了特定的Swing类和接口。

一个基本的打印程序

原文:docs.oracle.com/javase/tutorial/2d/printing/printable.html

本节解释了如何创建一个基本的打印程序,显示打印对话框,并将文本“Hello World”打印到所选打印机。

打印任务通常由两部分组成:

  • 作业控制 — 创建打印作业,将其与打印机关联,指定副本数量,并与用户打印对话框交互。

  • 页面成像 — 将内容绘制到页面上,并管理跨页的内容(分页)。

首先创建打印作业。表示打印作业和大多数其他相关类的类位于java.awt.print包中。

import java.awt.print.*;

PrinterJob job = PrinterJob.getPrinterJob();

接下来提供代码,通过实现Printable接口将内容呈现到页面上。

class HelloWorldPrinter
              implements Printable { ... }
...
job.setPrintable(new HelloWorldPrinter());

应用程序通常会显示打印对话框,以便用户可以调整各种选项,如副本数量、页面方向或目标打印机。

boolean doPrint = job.printDialog();

此对话框会一直显示,直到用户批准或取消打印。如果doPrint变量为 true,则用户已经下达打印命令。如果doPrint变量为 false,则用户取消了打印作业。由于显示对话框是可选的,返回的值纯粹是信息性的。

如果doPrint变量为 true,则应用程序将通过调用PrinterJob.print方法请求打印作业。

if (doPrint) {
    try {
        job.print();
    } catch (PrinterException e) {
        // The job did not successfully
        // complete
    }
}

如果发送作业到打印机时出现问题,将抛出PrinterException。然而,由于PrinterJob.print方法一旦作业发送到打印机就会返回,用户应用程序无法检测到纸张卡住或缺纸等问题。这个作业控制样板对于基本的打印使用已经足够。

Printable接口只有一个方法:

public int print(Graphics graphics,
           PageFormat pf, int page)
           throws PrinterException;

PageFormat类描述了页面方向(纵向或横向)及其大小和可成像区域,单位为 1/72 英寸。可成像区域考虑了大多数打印机的边距限制(硬件边距)。可成像区域是这些边距内的空间,在实践中通常进一步限制以留出页眉或页脚的空间。

page参数是将要呈现的基于零的页码。

以下代码表示完整的Printable实现:

import java.awt.print.*;
import java.awt.*;

public class HelloWorldPrinter
    implements Printable {

  public int print(Graphics g, PageFormat pf, int page)
      throws PrinterException {

    // We have only one page, and 'page'
    // is zero-based
    if (page > 0) {
         return NO_SUCH_PAGE;
    }

    // User (0,0) is typically outside the
    // imageable area, so we must translate
    // by the X and Y values in the PageFormat
    // to avoid clipping.
    Graphics2D g2d = (Graphics2D)g;
    g2d.translate(pf.getImageableX(), pf.getImageableY());

    // Now we perform our rendering
    g.drawString("Hello world!", 100, 100);

    // tell the caller that this page is part
    // of the printed document
    return PAGE_EXISTS;
  }
}

此示例的完整代码在HelloWorldPrinter.java中。

Graphics实例发送到打印机基本上与将其呈现到屏幕相同。在这两种情况下,您需要执行以下步骤:

  • 绘制测试字符串与描述绘制到Graphics2D的其它操作一样简单。

  • 打印机图形具有更高的分辨率,这对大多数代码来说应该是透明的。

  • Printable.print() 方法由打印系统调用,就像 Component.paint() 方法被调用来在显示器上绘制组件一样。打印系统会在页面 0、1、..等等调用 Printable.print() 方法,直到 print() 方法返回 NO_SUCH_PAGE

  • print() 方法可能会在文档完成之前多次以相同的页面索引被调用。当用户指定了诸如多份拷贝和逐份选项等属性时,会应用此功能。

  • PageFormat 的可打印区域决定了剪切区域。可打印区域在计算分页或者如何跨打印页面展示内容时也很重要,因为页面断点是由每页能容纳多少内容决定的。


    注意: 如果用户指定了不涉及特定页面索引的不同页面范围,那么对于某些页面索引,可能会跳过对 print() 方法的调用。


使用打印设置对话框

原文:docs.oracle.com/javase/tutorial/2d/printing/dialog.html

传统上,用户希望看到页面设置和打印对话框。从打印对话框中,您可以选择打印机,指定要打印的页面,并设置副本数量。

这个图表示一个打印对话框

当用户按下与打印命令相关的按钮或从打印菜单中选择项目时,应用程序会显示打印对话框。要显示此对话框,请调用PrinterJob类的printDialog方法:

PrinterJob pj = PrinterJob.getPrinterJob();
...
    if (pj.printDialog()) {
        try {pj.print();}
        catch (PrinterException exc) {
            System.out.println(exc);
         }
     }   
...    

如果用户点击确定按钮离开对话框,则此方法返回true,否则返回false。对话框中用户的选择受限于已设置到PrinterJob的页面的数量和格式。

上述代码片段中的printDialog方法打开一个本机打印对话框。PrintDialogExample.java代码示例展示了如何显示跨平台打印对话框。

你可以通过使用页面设置对话框来更改包含在PageFormat对象中的页面设置信息。

这个图表示一个页面设置对话框窗口

要显示页面设置对话框,请调用PrinterJob类的pageDialog方法。

PrinterJob pj = PrinterJob.getPrinterJob();
PageFormat pf = pj.pageDialog(pj.defaultPage());

页面设置对话框使用传递给pageDialog的参数进行初始化。如果用户在对话框中点击确定按钮,则将根据用户的选择创建PageFormat实例,然后返回。如果用户取消对话框,则pageDialog将返回原始未更改的PageFormat

通常,Java 2D 打印 API 要求应用程序显示打印对话框,但有时可能可以在不显示任何对话框的情况下打印。这种类型的打印称为静默打印。在特定情况下可能会很有用,例如,当您需要打印特定数据库每周报告时。在其他情况下,始终建议在打印过程开始时通知用户。

打印多页文档

原文:docs.oracle.com/javase/tutorial/2d/printing/set.html

您已经学会了如何使用Printable接口打印单页文档。但是,文档通常不止一页。分页是识别文档中分页位置并相应打印的过程。

如果要打印多个图形图像,每页一个,使用页面索引来遍历这些页面,并在每页上打印一个。例如,如果几个图像在以下数组中表示:

BufferedImage[] images = new BufferedImage[10];

然后使用以下代码片段中显示的print()方法:

public int print(Graphics graphics,
           PageFormat pageFormat, int pageIndex)
           throws PrinterException {

    if (pageIndex < images.length) {
        graphics.drawImage(images[pageIndex], 100, 100, null);
        return PAGE_EXISTS;
    } else {
        return NO_SUCH_PAGE:
    }
}

如果文档是连续的,应用程序必须计算每页可以容纳多少内容,并在该点分页。如果文本文档由许多行组成,则应用程序必须计算这些行中有多少可以完全适合一页。Point类创建一个表示位置的点(x,y)

要计算单行文本的高度,请使用FontMetrics类。

Font font = new Font("Serif", Font.PLAIN, 10);
FontMetrics metrics = graphics.getFontMetrics(font);
int lineHeight = metrics.getHeight();

PageFormat参数描述了页面的可打印区域。特别是,要找到页面的垂直跨度,请使用以下代码片段:

double pageHeight = pageFormat.getImageableHeight();

使用以下代码片段计算一页上适合的行数和分页数:

int linesPerPage = ((int)pageHeight)/lineHeight);
int numBreaks = (textLines.length-1)/linesPerPage;
int[] pageBreaks = new int[numBreaks];
for (int b=0; b < numBreaks; b++) {
    pageBreaks[b] = (b+1)*linesPerPage; 
}

使用print()方法计算以下原因的可打印区域:

  • 文本测量取决于FontRenderContext,这在打印机图形返回的FontMetrics对象中是隐含的,除了在print()方法内部不可用。

  • 直到打印发生,页面格式可能不会被揭示。因为如果用户在打印对话框中选择了横向模式,则需要考虑此设置。传递给print()方法的PageFormat对象提供了此信息。

分页位置如下代码片段所示:

/* Draw each line that is on this page.
 * Increment 'y' position by lineHeight
 * for each line.
 */
int y = 0; 
int start = (pageIndex == 0) ? 0 : pageBreaks[pageIndex-1];
int end   = (pageIndex == pageBreaks.length)
                 ? textLines.length : pageBreaks[pageIndex];
for (int line=start; line<end; line++) {
    y += lineHeight;
    g.drawString(textLines[line], 0, y);
}

如果一个文档包含 100 行,每页只能容纳 48 行,则应用程序将打印 3 页,每页在第 48 行和第 96 行之后分页。剩余的 4 行将打印在最后一页上。此示例的完整代码在PaginationExample.java中。

PaginationExample代码中使用了以下简化因素:

  • 每页具有相同的高度。

  • 使用相同的字体。

使用打印服务和属性

原文:docs.oracle.com/javase/tutorial/2d/printing/services.html

从之前的课程中,您已经了解到 Java 2D 打印 API 支持页面成像,显示打印和页面设置对话框,并指定打印属性。 打印服务是任何打印子系统的另一个关键组件。

Java 打印服务(JPS)API扩展了当前的 Java 2D 打印功能,提供以下功能:

  • 应用程序通过动态查询打印机功能来发现满足其需求的打印机。

  • 应用程序扩展了包含在 JPS API 中的属性。

  • 第三方可以通过服务提供者接口插入其自己的打印服务,打印不同格式,包括 Postscript、PDF 和 SVG。

Java 打印服务 API 由四个包组成:

此图表示打印所需的四个包

javax.print包为 Java 打印服务 API 提供了主要的类和接口。 它使客户端和服务器应用程序能够:

  • 根据其功能发现和选择打印服务。

  • 指定打印数据的格式。

  • 将打印作业提交给支持要打印的文档类型的服务。

文档类型规范

DocFlavor类表示打印数据的格式,例如 JPEG 或 PostScript。DocFlavor格式由两部分组成:一个是 MIME 类型,另一个是表示类名称。 MIME 类型描述了格式,文档表示类名称指示文档如何传递给打印机或输出流。 应用程序使用DocFlavor和属性集来查找具有属性集指定功能的打印机。 此代码示例演示了获取能够将 GIF 图像转换为 PostScript 的StreamPrintService对象的StreamPrintServiceFactory对象数组:

DocFlavor flavor  = DocFlavor.INPUT_STREAM.GIF;
String psMimeType = DocFlavor.BYTE_ARRAY.
                    POSTSCRIPT.getMimeType();
StreamPrintServiceFactory[] psfactories =
              StreamPrintServiceFactory.
              lookupStreamPrintServiceFactories(
              flavor, psMimeType);

属性定义

javax.print.attributejavax.print.attribute.standard包定义了描述打印服务功能、指定打印作业要求以及跟踪打印作业进度的打印属性。

例如,如果您想使用 A4 纸张格式并打印文档的三份副本,则必须创建一组实现PrintRequestAttributeSet接口的以下属性:

PrintRequestAttributeSet attr_set =
    new HashPrintRequestAttributeSet();
attr_set.add(MediaSize.ISO_A4); 
attr_set.add(new Copies(3)); 

然后,您必须将属性集传递给打印作业的print方法,以及DocFlavor

打印服务发现

应用程序调用抽象类PrintServiceLookup的静态方法来定位具有满足应用程序打印请求能力的打印服务。例如,为了打印一份双面文档的两份副本,应用程序首先需要找到具有双面打印能力的打印机:

DocFlavor doc_flavor = DocFlavor.INPUT_STREAM.PDF;
PrintRequestAttributeSet attr_set =
    new HashPrintRequestAttributeSet();
attr_set.add(new Copies(2));
attr_set.add(Sides.DUPLEX);
PrintService[] service = PrintServiceLookup.
              lookupPrintServices(doc_flavor,
              attr_set);

API 的常见用法

总之,Java 打印服务 API 执行以下步骤来处理打印请求:

  1. 选择一个DocFlavor

  2. 创建一组属性。

  3. 定位一个可以处理由DocFlavor和属性集指定的打印请求的打印服务。

  4. 创建一个封装了DocFlavor和实际打印数据的Doc对象。

  5. 从打印服务获取由DocPrintJob表示的打印作业。

  6. 调用打印作业的print方法。

有关 Java 打印服务的更多信息,请参阅Java 2D 打印服务 API 用户指南。

打印用户界面的内容

原文:docs.oracle.com/javase/tutorial/2d/printing/gui.html

另一个常见的打印任务是打印窗口或框架的内容,可以是全部内容,也可以是部分内容。窗口可能包含以下组件:工具栏、按钮、滑块、文本标签、可滚动文本区域、图像和其他图形内容。所有这些组件都是使用 Java 2D 打印 API 的以下方法打印的:

java.awt.Component.print(Graphics g);
java.awt.Component.printAll(Graphics g);

以下图表示一个简单的用户界面。

在窗口中打印 12 行

创建此用户界面的代码位于示例程序PrintUIWindow.java中。

要打印此窗口,请修改之前打印文本或图像的示例中的代码。结果代码应如下所示:

public int print(Graphics g, PageFormat pf, int page)
    throws PrinterException {
    if (page > 0) {
        return NO_SUCH_PAGE;
    }

    Graphics2D g2d = (Graphics2D)g;
    g2d.translate(pf.getImageableX(), pf.getImageableY());

    // Print the entire visible contents of a
    // java.awt.Frame.
    frame.printAll(g);

    return PAGE_EXISTS;
}


注意: printAll方法的调用是此示例与打印文本或图像示例之间的唯一区别。print(Graphics g)方法反映了用于屏幕渲染的java.awt.Component.paint(Graphics g)方法。使用print()方法而不是paint()方法,因为Components类可能已经重写了print()方法以不同方式处理打印情况。


printAll(Graphics g)方法打印组件及其所有子组件。通常使用此方法打印对象,例如完整窗口,而不是单个组件。

Swing 组件中的打印支持

原文:docs.oracle.com/javase/tutorial/2d/printing/swing.html

在前一节中展示的 PrintUIWindow.java 示例表明打印出的内容与屏幕上看到的完全相同。这种外观看起来是合理的。然而,如果一个窗口是可滚动的,那么当前滚动出视图的内容不会包含在打印输出中。这会在打印机上产生一个倾倒效果。当打印大型组件(如 Swing 表格或文本组件)时,这就成为一个特殊问题。组件可能包含许多行文本,这些文本在屏幕上并不都能完全可见。在这种情况下,以与屏幕显示一致的方式打印组件显示的内容。

要解决这个问题,Swing 表格和所有文本组件都具有打印功能。以下方法直接提供了 Java 2D 打印的使用:

  • javax.swing.JTable.print();

  • javax.swing.text.JTextComponent.print();

这些方法为它们的内容提供了完整的打印实现。应用程序不需要直接创建 PrinterJob 对象并实现 Printable 接口。调用这些方法会显示打印对话框,并根据用户的选择打印组件的数据。还有其他提供更多选项的方法。

课程:Java2D 高级主题

原文:docs.oracle.com/javase/tutorial/2d/advanced/index.html

本课程向您展示如何使用Graphics2D显示具有花哨轮廓和填充样式的图形,当它们被渲染时如何转换图形,将渲染限制在特定区域,并一般控制图形在被渲染时的外观。您还将学习如何通过组合简单形状来创建复杂的Shape对象,以及如何检测用户何时点击显示的图形基元。这些主题在以下部分中讨论:

变换形状、文本和图像

本节向您展示如何修改默认变换,以便在渲染时对象被平移、旋转、缩放或倾斜。

裁剪绘图区域

你可以使用任何形状作为裁剪路径—即渲染发生的区域。

合成图形

本节介绍了AlphaComposite支持的各种合成样式,并向您展示如何在Graphics2D渲染上下文中设置合成样式。

控制渲染质量

本节描述了Graphics2D支持的渲染提示,并向您展示如何在渲染质量和速度之间的权衡中指定您的偏好。

从几何原语构建复杂形状

本节向您展示如何使用Area类在Shape对象上执行布尔运算。

支持用户交互

本节向您展示如何在图形基元上执行点击检测。

变换形状、文本和图像

原文:docs.oracle.com/javase/tutorial/2d/advanced/transforming.html

您可以在Graphics2D上下文中修改变换属性,以在渲染时移动、旋转、缩放和剪切图形基元。变换属性由AffineTransform类的实例定义。仿射变换是一种变换,如平移、旋转、缩放或剪切,在变换后平行线仍保持平行。

Graphics2D类提供了几种方法来更改变换属性。您可以构造一个新的AffineTransform并通过调用transform来更改Graphics2D的变换属性。

AffineTransform定义了以下工厂方法,以便更容易构造新的变换:

  • getRotateInstance

  • getScaleInstance

  • getShearInstance

  • getTranslateInstance

或者,您可以使用Graphics2D的一个变换方法来修改当前变换。当您调用这些便利方法之一时,生成的变换将与当前变换连接,并在渲染期间应用:

  • rotate — 用于指定以弧度为单位的旋转角度

  • scale — 用于指定xy方向的缩放因子

  • shear — 用于指定xy方向的剪切因子

  • translate — 用于指定xy方向的平移偏移量

您还可以直接构造一个AffineTransform对象,并通过调用transform方法将其与当前变换连接。

drawImage方法也被重载,允许您指定一个在渲染时应用于图像的AffineTransform。在调用drawImage时指定变换不会影响Graphics2D的变换属性。

示例:变换

下面的程序与StrokeandFill相同,但还允许用户在渲染选定对象时选择要应用的变换。


注意: 如果您看不到小程序运行,则需要安装至少Java SE Development Kit (JDK) 7版本。


Transform.java 包含此小程序的完整代码。

当从变换菜单中选择一个变换时,该变换将连接到AffineTransform at上:

public void setTrans(int transIndex) {
    // Sets the AffineTransform.
    switch ( transIndex ) {
    case 0 :
        at.setToIdentity();
        at.translate(w/2, h/2);
        break;
    case 1 :
        at.rotate(Math.toRadians(45));
        break;
    case 2 :
        at.scale(0.5, 0.5);
        break;
    case 3 :
        at.shear(0.5, 0.0);
        break;
    }
}

在显示与菜单选择对应的形状之前,应用程序首先从Graphics2D对象中检索当前变换:

AffineTransform saveXform = g2.getTransform();

此变换将在渲染后恢复到Graphics2D中。

在检索当前变换后,创建另一个AffineTransformtoCenterAt,使形状在面板中心渲染。at AffineTransform被连接到toCenterAt上:

AffineTransform toCenterAt = new AffineTransform();
toCenterAt.concatenate(at);
toCenterAt.translate(-(r.width/2), -(r.height/2));

使用transform方法将toCenterAt变换连接到Graphics2D变换上:

g2.transform(toCenterAt);

渲染完成后,使用setTransform方法恢复原始变换:

g2.setTransform(saveXform);


注意:永远不要使用setTransform方法将坐标变换连接到现有的变换上。setTransform方法会覆盖Graphics2D对象的当前变换,这可能会因其他原因而需要,比如在窗口中定位 Swing 和轻量级组件。执行变换的步骤如下:

  1. 使用getTransform方法获取当前变换。

  2. 使用transformtranslatescaleshearrotate来连接一个变换。

  3. 执行渲染。

  4. 使用setTransform方法恢复原始变换。


绘制区域剪切

原文:docs.oracle.com/javase/tutorial/2d/advanced/clipping.html

任何Shape对象都可以用作限制将呈现的绘图区域的剪切路径。剪切路径是Graphics2D上下文的一部分;要设置剪切属性,调用Graphics2D.setClip并传入定义要使用的剪切路径的Shape。可以通过调用clip方法并传入另一个Shape来缩小剪切路径;剪切设置为当前剪切和指定Shape的交集。

示例:ClipImage

该示例通过动画剪切路径来显示图像的不同部分。


注意: 如果看不到小程序运行,请至少安装Java SE Development Kit (JDK) 7版本。


ClipImage.java包含此小程序的完整代码。小程序需要clouds.jpg图像文件。

剪切路径由椭圆和随机设置尺寸的矩形的交集定义。将椭圆传递给setClip方法,然后调用clip将剪切路径设置为椭圆和矩形的交集。

private Ellipse2D ellipse = new Ellipse2D.Float();
private Rectangle2D rect = new Rectangle2D.Float();
...
ellipse.setFrame(x, y, ew, eh);
g2.setClip(ellipse);
rect.setRect(x+5, y+5, ew-10, eh-10);
g2.clip(rect);

示例:Starry

也可以从文本字符串创建一个剪切区域。以下示例使用字符串The Starry Night创建一个TextLayout。然后,获取TextLayout的轮廓。TextLayout.getOutline方法返回一个Shape对象,并从该Shape对象的边界创建一个Rectangle。边界包含布局可以绘制的所有像素。将图形上下文中的颜色设置为蓝色,并绘制轮廓形状,如下图和代码片段所示。

The Starry Night text (outline)

FontRenderContext frc = g2.getFontRenderContext();
Font f = new Font("Helvetica", 1, w/10);
String s = new String("The Starry Night");
TextLayout textTl = new TextLayout(s, f, frc);
AffineTransform transform = new AffineTransform();
Shape outline = textTl.getOutline(null);
Rectangle r = outline.getBounds();
transform = g2.getTransform();
transform.translate(w/2-(r.width/2), h/2+(r.height/2));
g2.transform(transform);
g2.setColor(Color.blue);
g2.draw(outline);   

接下来,使用从getOutline创建的Shape对象在图形上下文中设置一个剪切区域。将梵高的著名绘画作品The Starry Nightstarry.gif图像绘制到从Rectangle对象的左下角开始的剪切区域中。

g2.setClip(outline);
g2.drawImage(img, r.x, r.y, r.width, r.height, this);


注意: 如果你看不到小程序在运行,你需要安装至少 Java SE Development Kit (JDK) 7 版本。


Starry.java 包含了这个程序的完整代码。这个小程序需要 Starry.gif 图像文件。

合成图形

原文:docs.oracle.com/javase/tutorial/2d/advanced/compositing.html

AlphaComposite 类封装了各种合成样式,确定重叠对象如何被渲染。AlphaComposite 还可以具有指定透明度的 alpha 值:alpha = 1.0 完全不透明,alpha = 0.0 完全透明(清除)。AlphaComposite 支持下表中显示的大多数标准 Porter-Duff 合成规则。

合成规则 描述
源覆盖 (SRC_OVER) 源覆盖合成 如果正在渲染的对象(源)中的像素与先前渲染的像素(目标)位于相同位置,则源像素将覆盖目标像素。
源中 (SRC_IN) 源中合成 如果源和目标重叠,只有源区域的像素被渲染。
源减去 (SRC_OUT) 源减去合成 如果源和目标重叠,只有源区域外的像素被渲染。重叠区域的像素被清除。
目标覆盖 (DST_OVER) 目标覆盖合成 如果源和目标重叠,只有源区域外的像素被渲染。重叠区域的像素不会改变。
目标中 (DST_IN) 目标中合成 如果源和目标重叠,源的 alpha 值将应用于重叠区域的目标像素。如果 alpha = 1.0,则重叠区域的像素保持不变;如果 alpha 为 0.0,则清除重叠区域的像素。
目标减去 (DST_OUT) 目标减去合成 如果源和目标重叠,源的 alpha 值将应用于重叠区域的目标像素。如果 alpha = 1.0,则清除重叠区域的像素;如果 alpha 为 0.0,则重叠区域的像素保持不变。
清除 (CLEAR) 清除重叠合成 如果源和目标重叠,清除重叠区域的像素。

要更改 Graphics2D 类使用的合成样式,请创建一个 AlphaComposite 对象并将其传递给 setComposite 方法。

示例:合成

该程序演示了各种合成样式和 alpha 组合的效果。


注意: 如果您看不到 applet 运行,请至少安装Java SE Development Kit (JDK) 7版本。


Composite.java 包含了此 applet 的完整代码。

通过调用 AlphaComposite.getInstance 并指定所需的合成规则来构造一个新的 AlphaComposite 对象 ac

AlphaComposite ac =
  AlphaComposite.getInstance(AlphaComposite.SRC);

当选择不同的合成规则或 alpha 值时,会再次调用 AlphaComposite.getInstance,并将新的 AlphaComposite 分配给 ac。所选的 alpha 值除了每个像素的 alpha 值外还会应用,并作为第二个参数传递给 AlphaComposite.getInstance

ac = AlphaComposite.getInstance(getRule(rule), alpha);

通过将 AlphaComposite 对象传递给 Graphics 2DsetComposite 方法来修改合成属性。对象被渲染到 BufferedImage 中,然后复制到屏幕上,因此合成属性被设置在 BufferedImageGraphics2D 上下文中:

BufferedImage buffImg = new BufferedImage(w, h,
                        BufferedImage.TYPE_INT_ARGB);
Graphics2D gbi = buffImg.createGraphics();
...
gbi.setComposite(ac);

控制渲染质量

原文:docs.oracle.com/javase/tutorial/2d/advanced/quality.html

使用Graphics2D类的渲染提示属性来指定您是希望对象尽可能快地呈现还是您更喜欢呈现质量尽可能高。

要设置或更改Graphics2D上下文中的渲染提示属性,请构造一个RenderingHints对象,并通过使用setRenderingHints方法将其传递给Graphics2D。如果您只想设置一个提示,可以调用Graphics2DsetRenderingHint并指定要设置的提示的键值对。(键值对在RenderingHints类中定义。)

例如,要设置抗锯齿首选项(如果可能的话),您可以使用setRenderingHint

public void paint (graphics g){
    Graphics2D g2 = (Graphics2D)g;
    RenderingHints rh = new RenderingHints(
             RenderingHints.KEY_TEXT_ANTIALIASING,
             RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
    g2.setRenderingHints(rh);
...
}


注意:并非所有平台都支持修改渲染模式,因此指定渲染提示并不保证它们会被使用。


RenderingHints支持以下类型的提示:

提示

| 抗锯齿 | KEY_ANTIALIASING | VALUE_ANTIALIAS_ON VALUE_ANTIALIAS_OFF

VALUE_ANTIALIAS_DEFAULT |

| Alpha 插值 | KEY_ALPHA_INTERPOLATION | VALUE_ALPHA_INTERPOLATION_QUALITY VALUE_ALPHA_INTERPOLATION_SPEED

VALUE_ALPHA_INTERPOLATION_DEFAULT |

| 颜色渲染 | KEY_COLOR_RENDERING | VALUE_COLOR_RENDER_QUALITY VALUE_COLOR_RENDER_SPEED

VALUE_COLOR_RENDER_DEFAULT |

| 抖动 | KEY_DITHERING | VALUE_DITHER_DISABLE VALUE_DITHER_ENABLE

VALUE_DITHER_DEFAULT |

| 分数文本度量 | KEY_FRACTIONALMETRICS | VALUE_FRACTIONALMETRICS_ON VALUE_FRACTIONALMETRICS_OFF

VALUE_FRACTIONALMETRICS_DEFAULT |

| 图像插值 | KEY_INTERPOLATION | VALUE_INTERPOLATION_BICUBIC VALUE_INTERPOLATION_BILINEAR

VALUE_INTERPOLATION_NEAREST_NEIGHBOR |

| 渲染 | KEY_RENDERING | VALUE_RENDER_QUALITY VALUE_RENDER_SPEED

VALUE_RENDER_DEFAULT |

| 笔画规范化控制 | KEY_STROKE_CONTROL | VALUE_STROKE_NORMALIZE VALUE_STROKE_DEFAULT

VALUE_STROKE_PURE |

| 文本抗锯齿 | KEY_TEXT_ANTIALIASING | VALUE_TEXT_ANTIALIAS_ON VALUE_TEXT_ANTIALIAS_OFF

VALUE_TEXT_ANTIALIAS_DEFAULT

VALUE_TEXT_ANTIALIAS_GASP

VALUE_TEXT_ANTIALIAS_LCD_HRGB

VALUE_TEXT_ANTIALIAS_LCD_HBGR

VALUE_TEXT_ANTIALIAS_LCD_VRGB

VALUE_TEXT_ANTIALIAS_LCD_VBGR

|

LCD 文本对比度 KEY_TEXT_LCD_CONTRAST 值应为范围在 100 到 250 之间的正整数。较低的值(如 100)对应于在浅色背景上显示深色文本时更高的对比度。较高的值(如 200)对应于在浅色背景上显示深色文本时较低的对比度。一个典型有用的值在 140-180 的狭窄范围内。如果未指定值,则将应用系统或实现默认值。

当提示设置为默认时,使用平台渲染默认值。

从几何原语构建复杂形状

原文:docs.oracle.com/javase/tutorial/2d/advanced/complexshapes.html

构造区域几何(CAG)是通过对现有几何形状执行布尔运算来创建新几何形状的过程。在 Java 2D API 中,Area 类实现了 Shape 接口,并支持以下布尔运算。

范图显示并集 并集 范图显示减法 减法
范图显示交集 交集 范图显示异或操作 异或 (XOR)

例子:区域

在这个例子中,Area对象从几个椭圆构建了一个梨形。


注意: 如果你看不到小程序运行,你需要至少安装 Java SE Development Kit (JDK) 7 版本。


Pear.java 包含了这个小程序的完整代码。

每片叶子是通过在两个重叠圆上执行交集操作创建的。

leaf = new Ellipse2D.Double();
...
leaf1 = new Area(leaf);
leaf2 = new Area(leaf);
...
leaf.setFrame(ew-16, eh-29, 15.0, 15.0);
leaf1 = new Area(leaf);
leaf.setFrame(ew-14, eh-47, 30.0, 30.0);
leaf2 = new Area(leaf);
leaf1.intersect(leaf2);
g2.fill(leaf1);
...
leaf.setFrame(ew+1, eh-29, 15.0, 15.0);
leaf1 = new Area(leaf);
leaf2.intersect(leaf1);
g2.fill(leaf2);

重叠的圆也被用来通过减法操作构建茎。

stem = new Ellipse2D.Double();
...
stem.setFrame(ew, eh-42, 40.0, 40.0);
st1 = new Area(stem);
stem.setFrame(ew+3, eh-47, 50.0, 50.0);
st2 = new Area(stem);
st1.subtract(st2);
g2.fill(st1);

梨的主体是通过在一个圆和一个椭圆上执行并集操作构建的。

circle = new Ellipse2D.Double();
oval = new Ellipse2D.Double();
circ = new Area(circle);
ov = new Area(oval);
...
circle.setFrame(ew-25, eh, 50.0, 50.0);
oval.setFrame(ew-19, eh-20, 40.0, 70.0);
circ = new Area(circle);
ov = new Area(oval);
circ.add(ov);
g2.fill(circ);

支持用户交互

原文:docs.oracle.com/javase/tutorial/2d/advanced/user.html

为了让用户与您显示的图形进行交互,您需要能够确定用户何时点击其中一个图形。Graphics2D 类的 hit 方法提供了一种简单确定鼠标点击是否发生在特定 Shape 对象上的方法。或者,您可以获取鼠标点击的位置并在 Shape 上调用 contains 方法来确定点击是否在 Shape 的边界内。

如果您使用基本文本,可以通过获取与文本对应的轮廓 Shape,然后使用该 Shape 调用 hitcontains 方法来执行简单的点击测试。支持文本编辑需要更复杂的点击测试。如果要允许用户编辑文本,通常应使用 Swing 可编辑文本组件之一。如果您使用基本文本并使用 TextLayout 类来管理文本的形状和位置,则还可以使用 TextLayout 来执行文本编辑的点击测试。有关更多信息,请参阅 Java 2D 程序员指南 中的文本和字体章节,或查看下面的 HitTestSample 示例,该示例使用 TextLayout 执行简单的点击测试。

示例:ShapeMover

此小程序允许用户在小程序窗口内拖动 ShapeShape 在每个鼠标位置重新绘制,以提供用户拖动时的反馈。


注意: 如果您看不到小程序运行,请至少安装 Java SE Development Kit (JDK) 7 版本。


ShapeMover.java 包含了此小程序的完整代码。

当鼠标按下时,调用 contains 方法来确定光标是否在矩形的边界内。如果是,则更新矩形的位置。

public void mousePressed(MouseEvent e){
    last_x = rect.x - e.getX();
    last_y = rect.y - e.getY();
    if(rect.contains(e.getX(),
        e.getY())) updateLocation(e);
    ...

public void updateLocation(MouseEvent e){
    rect.setLocation(last_x + e.getX(),
        last_y + e.getY());
    ...
    repaint();

您可能会注意到,在每个鼠标位置重新绘制 Shape 是很慢的,因为填充的矩形每次移动时都会重新渲染。使用双缓冲可以消除这个问题。如果使用 Swing,绘图将自动双缓冲;您无需更改渲染代码。这个程序的 Swing 版本的代码是 SwingShapeMover.java

示例:HitTestSample

这个应用程序通过在用户点击 TextLayout 上的位置绘制默认插入符来说明点击测试,如下图所示。


注意: 如果你看不到小程序在运行,你需要至少安装 Java SE Development Kit (JDK) 7 版本。


HitTestSample.java 包含了这个小程序的完整代码。

mouseClicked 方法使用 TextLayout.hitTestChar 返回一个包含鼠标点击位置(插入索引)的 java.awt.font.TextHitInfo 对象在 TextLayout 对象中。

TextLayoutgetAscentgetDescentgetAdvance 方法返回的信息被用来计算 TextLayout 对象的原点位置,使其水平和垂直居中。

...

private Point2D computeLayoutOrigin() {
  Dimension size = getPreferredSize();
  Point2D.Float origin = new Point2D.Float();

  origin.x = (float) (size.width -
             textLayout.getAdvance()) / 2;   
  origin.y = 
    (float) (size.height -
             textLayout.getDescent() +
             textLayout.getAscent())/2;
  return origin;
}

...

public void paintComponent(Graphics g) {
  super.paintComponent(g);
  setBackground(Color.white);
  Graphics2D graphics2D = (Graphics2D) g;                
  Point2D origin = computeLayoutOrigin();
  graphics2D.translate(origin.getX(),
                       origin.getY());

  // Draw textLayout.
  textLayout.draw(graphics2D, 0, 0);

  // Retrieve caret Shapes for insertionIndex.
  Shape[] carets =
      textLayout.getCaretShapes(insertionIndex);

  // Draw the carets.  carets[0] is the strong
  // caret and carets[1] is the weak caret.   
  graphics2D.setColor(STRONG_CARET_COLOR);
  graphics2D.draw(carets[0]);                
  if (carets[1] != null) {
    graphics2D.setColor(WEAK_CARET_COLOR);
    graphics2D.draw(carets[1]);
  }       
}

...

private class HitTestMouseListener
              extends MouseAdapter {

    /**
     * Compute the character position of the
     * mouse click.
     */     
    public void mouseClicked(MouseEvent e) {

      Point2D origin = computeLayoutOrigin();

      // Compute the mouse click location
      // relative to textLayout's origin.
      float clickX =
          (float) (e.getX() - origin.getX());
      float clickY =
          (float) (e.getY() - origin.getY());

      // Get the character position of the
      // mouse click.
      TextHitInfo currentHit =
          textLayout.hitTestChar(clickX, clickY);
      insertionIndex =
          currentHit.getInsertionIndex();

      // Repaint the Component so the new
      // caret(s) will be displayed.
      hitPane.repaint();
    }

路径:声音

原文:docs.oracle.com/javase/tutorial/sound/index.html

Java Sound API 是一个低级 API,用于影响和控制声音媒体的输入和输出,包括音频和 MIDI(Musical Instrument Digital Interface)数据。 Java Sound API 提供了对通常需要的声音输入和输出功能的明确控制,以促进可扩展性和灵活性。

Java Sound API 满足了各种应用程序开发人员的需求。潜在的应用领域包括:

  • 通信框架,如会议和电话会议

  • 最终用户内容传递系统,如使用流媒体内容的媒体播放器和音乐

  • 交互式应用程序,如使用动态内容的游戏和网站

  • 内容创建和编辑

  • 工具、工具包和实用程序

Java Sound API 在 Java 平台上提供了最低级别的声音支持。它为应用程序提供了对声音操作的大量控制,并且是可扩展的。例如,Java Sound API 提供了安装、访问和操作系统资源的机制,如音频混音器、MIDI 合成器、其他音频或 MIDI 设备、文件读取器和写入器以及声音格式转换器。 Java Sound API 不包括复杂的声音编辑器或图形工具,但它提供了可以构建此类程序的功能。它强调低于最终用户通常期望的低级别控制。

Java Sound API 包括对数字音频和 MIDI 数据的支持。这两个主要功能模块分别在不同的包中提供:

  • javax.sound.sampled – 该包指定了用于捕获、混合和播放数字(采样)音频的接口。

  • javax.sound.midi – 该包提供了用于 MIDI 合成、序列化和事件传输的接口。

另外两个包允许服务提供商(而不是应用程序开发人员)创建自定义软件组件,以扩展 Java Sound API 的实现的功能:

  • javax.sound.sampled.spi

  • javax.sound.midi.spi

本页介绍了采样音频系统、MIDI 系统和 SPI 包。每个包稍后在教程中进行了更详细的讨论。


注意:

还有其他 Java 平台 API 也涉及声音相关元素。Java 媒体框架 API (JMF) 是当前作为 Java 平台标准扩展可用的更高级 API。JMF 指定了用于捕获和播放基于时间的媒体的统一架构、消息协议和编程接口。JMF 为基本媒体播放器应用程序提供了更简单的解决方案,并且它实现了不同媒体类型之间的同步,例如音频和视频。另一方面,专注于声音的程序可以从 Java Sound API 中受益,特别是如果它们需要更高级的功能,例如精细控制缓冲音频播放或直接操作 MIDI 合成器。其他具有声音方面的 Java API 包括 Java 3D 和用于电话和语音的 API。这些 API 的任何实现可能在内部使用 Java Sound API 的实现,但不是必须的。


什么是采样音频?

javax.sound.sampled 包处理数字音频数据,Java Sound API 将其称为采样音频。样本 是信号的连续快照。在音频的情况下,信号是声波。麦克风将声学信号转换为相应的模拟电信号,模拟-数字转换器将该模拟信号转换为采样数字形式。以下图显示了声音录制中的一个瞬间。

一个采样声波

采样声波

这个图表将声压(振幅)绘制在垂直轴上,时间绘制在水平轴上。模拟声波的振幅以一定速率周期性地进行测量,导致离散样本(图中的红色数据点)构成数字音频信号。中心水平线表示零振幅;线上的点为正值样本,线下的点为负值。模拟信号的数字近似精度取决于其时间分辨率(采样率)和振幅分辨率(量化),即用于表示每个样本的位数。作为参考,用于存储在光盘上的音频每秒采样 44,100 次,并以每个样本 16 位表示。

这里稍微宽松地使用了“采样音频”这个术语。声波可以在被保留为模拟形式的同时以离散间隔进行采样。然而,对于 Java Sound API 来说,“采样音频”等同于“数字音频”。

通常,计算机上的采样音频来自声音录制,但声音也可以被合成生成(例如,创建触摸电话的声音)。术语“采样音频”指的是数据类型,而不是其来源。

Java Sound API 不假设特定的音频硬件配置;它设计为允许在系统上安装不同类型的音频组件,并通过 API 访问。Java Sound API 支持常见功能,例如从声卡输入和输出(例如,用于录制和播放声音文件)以及混合多个音频流。以下是一个典型音频架构的示例:

以下上下文描述这幅图

典型音频架构

在这个例子中,像声卡这样的设备具有各种输入和输出端口,并且混音是在软件中提供的。混音器可能接收从文件中读取的数据,从网络流式传输的数据,应用程序动态生成的数据,或者由 MIDI 合成器产生的数据。混音器将所有音频输入组合成一个流,可以发送到输出设备进行渲染。

什么是 MIDI?

javax.sound.midi包含用于传输和排序 MIDI 事件以及从这些事件中合成声音的 API。

而采样音频是声音本身的直接表示,MIDI 数据可以被视为创建声音的配方,特别是音乐声音的配方。与音频数据不同,MIDI 数据不直接描述声音。相反,它描述影响由 MIDI 启用的设备或乐器执行的声音(或动作)的事件,例如合成器。 MIDI 数据类似于图形用户界面的键盘和鼠标事件。在 MIDI 的情况下,这些事件可以被视为对音乐键盘以及乐器上的各种踏板、滑块、开关和旋钮的动作。这些事件不一定实际起源于硬件乐器;它们可以在软件中模拟,并且可以存储在 MIDI 文件中。一个可以创建、编辑和执行这些文件的程序被称为序列器。许多计算机声卡包括可以通过序列器发送其 MIDI 事件的 MIDI 可控音乐合成器芯片。合成器也可以完全在软件中实现。合成器解释它们接收到的 MIDI 事件并产生音频输出。通常,从 MIDI 数据合成的声音是音乐声音(例如,与语音相对)。MIDI 合成器还能够生成各种声音效果。

一些声卡包括 MIDI 输入和输出端口,可以连接外部 MIDI 硬件设备(如键盘合成器或其他乐器)。从 MIDI 输入端口,应用程序可以接收外部 MIDI 设备生成的事件。程序可以使用计算机的内部合成器演奏音乐表演,将其保存为 MIDI 文件,或将其渲染为音乐符号。程序可以使用 MIDI 输出端口来演奏外部乐器,或控制其他外部设备,如录音设备。

以下图示了基于 Java Sound API 的可能 MIDI 配置中主要组件之间的功能关系。(与音频一样,Java Sound API 允许安装和连接各种 MIDI 软件设备。此处显示的系统只是一个潜在的场景。)组件之间的数据流由箭头表示。数据可以是标准文件格式,或者(如图示右下角的关键所示),可以是音频、原始 MIDI 字节或时间标记 MIDI 消息。

以下上下文描述了这个图。

可能的 MIDI 配置

在此示例中,应用程序通过加载存储在磁盘上的标准 MIDI 文件的音乐乐谱(图中的左侧)来准备音乐表演。标准 MIDI 文件包含轨道,每个轨道都是一个时间标记 MIDI 事件列表。大多数事件代表音乐音符(音高和节奏)。这个 MIDI 文件被读取,然后由软件音序器“演奏”。音序器通过向其他设备发送 MIDI 消息来演奏音乐,例如内部或外部合成器。合成器本身可能会读取包含模拟某些乐器声音指令的声音库文件。如果没有,合成器将使用已加载的任何乐器声音来播放 MIDI 文件中存储的音符。

如图所示,MIDI 事件必须在通过 MIDI 输出端口发送到外部 MIDI 乐器之前被转换为原始(非时间标记)MIDI。同样,从外部 MIDI 源(图中的键盘乐器)进入计算机的原始 MIDI 数据被转换为可以控制合成器的时间标记 MIDI 消息,或者可以由音序器存储以供以后使用。

服务提供者接口

javax.sound.sampled.spijavax.sound.midi.spi 包含的 API 允许软件开发人员创建新的音频或 MIDI 资源,可以单独提供给用户并“插入”到 Java Sound API 的现有实现中。以下是可以以这种方式添加的一些服务(资源)的示例:

  • 音频混音器

  • MIDI 合成器

  • 一个可以读取或写入新类型音频或 MIDI 文件的文件解析器

  • 在不同声音数据格式之间进行转换的转换器

在某些情况下,服务是软件接口,用于访问硬件设备的功能,比如声卡,而服务提供者可能与硬件供应商相同。在其他情况下,服务纯粹存在于软件中。例如,合成器或混音器可以是声卡上的芯片的接口,也可以在没有任何硬件支持的情况下实现。

Java Sound API 的实现包含一组基本服务,但服务提供者接口(SPI)包允许第三方创建新服务。这些第三方服务以与内置服务相同的方式集成到系统中。AudioSystem类和MidiSystem类充当协调器,让应用程序明确或隐式地访问服务。对于使用它的应用程序来说,服务的存在通常对其完全透明。服务提供者机制使基于 Java Sound API 的应用程序的用户受益,因为可以向程序添加新的声音功能,而无需新版本的 JDK 或运行时环境,甚至在许多情况下,甚至无需新版本的应用程序本身。

采样包概述

原文:docs.oracle.com/javase/tutorial/sound/sampled-overview.html

javax.sound.sampled 包主要涉及音频传输 - 换句话说,Java Sound API 专注于播放和捕获。 Java Sound API 解决的核心任务是如何将格式化音频数据的字节移入和移出系统。 这项任务涉及打开音频输入和输出设备以及管理填充实时音频数据的缓冲区。 它还可以涉及将多个音频流混合成一个流(无论是用于输入还是输出)。 当用户请求启动、暂停、恢复或停止声音流时,系统内部的声音传输必须得到正确处理。

为了支持对基本音频输入和输出的关注,Java Sound API 提供了在各种音频数据格式之间转换的方法,并且提供了读取和写入常见类型的声音文件的方法。 但是,它并不试图成为一个全面的声音文件工具包。 Java Sound API 的特定实现不必支持广泛的文件类型或数据格式转换。 第三方服务提供商可以提供模块,这些模块可以“插入”到现有实现中,以支持额外的文件类型和转换。

Java Sound API 可以以流式缓冲方式和内存中非缓冲方式处理音频传输。 这里所说的“流式”是指实时处理音频字节; 它并不是指以某种特定格式通过互联网发送音频的众所周知的情况。 换句话说,音频流只是一组连续的音频字节,它们以更多或更少的相同速率到达,以便处理(播放、录制等)。 操作在所有数据到达之前开始。 在流式模型中,特别是在音频输入而不是音频输出的情况下,您不一定事先知道声音的持续时间以及何时会完成到达。 您只需一次处理一个音频数据缓冲区,直到操作停止。 在音频输出(播放)的情况下,如果要播放的声音太大而无法一次性放入内存中,则还需要缓冲数据。 换句话说,您以块的形式将音频字节传递给声音引擎,它会在正确的时间播放每个样本。 提供了机制,使得很容易知道每个块中要传递多少数据。

Java Sound API 还允许在仅播放的情况下进行无缓冲传输,假设您已经拥有所有音频数据并且数据量不太大以适应内存。在这种情况下,应用程序无需缓冲音频,尽管如果需要,仍然可以使用缓冲的实时方法。相反,整个声音可以一次性预加载到内存中以供后续播放。由于所有声音数据都是预先加载的,因此播放可以立即开始,例如,用户点击“开始”按钮时。与缓冲模型相比,这可能是一个优势,因为在缓冲填满之前,播放必须等待第一个缓冲区。此外,内存中的无缓冲模型允许轻松循环(循环)声音或将其设置为数据中的任意位置。

使用 Java Sound API 播放或捕获声音时,您至少需要三样东西:格式化的音频数据,混音器和线路。以下是这些概念的概述。

什么是格式化音频数据?

格式化的音频数据指的是任何一种标准格式的声音。Java Sound API 区分数据格式文件格式

数据格式

数据格式告诉您如何解释一系列“原始”采样音频数据的字节,例如已从声音文件中读取的样本,或者已从麦克风输入捕获的样本。例如,您可能需要知道一个样本包含多少比特(表示声音的最短瞬间的表示),同样您可能需要知道声音的采样率(样本应该多快地跟随彼此)。在设置播放或捕获时,您指定正在捕获或播放的声音的数据格式。

在 Java Sound API 中,数据格式由一个AudioFormat对象表示,其中包括以下属性:

  • 编码技术,通常是脉冲编码调制(PCM)

  • 通道数(单声道为 1,立体声为 2,等等)

  • 采样率(每秒每个通道的样本数)

  • 每个样本(每个通道)的比特数

  • 帧率

  • 每帧大小(以字节为单位)

  • 字节顺序(大端或小端)

PCM 是声波的一种编码方式。Java Sound API 包括两种使用线性幅度量化的 PCM 编码,以及带符号或无符号整数值。线性量化意味着每个样本中存储的数字与该瞬间的原始声压(除了任何失真)成正比,类似地与振动声音的扬声器或鼓膜的位移成正比,该振动声音在该瞬间发生。例如,CD 使用线性 PCM 编码的声音。 mu-law 编码和 a-law 编码是常见的非线性编码,它们提供音频数据的更紧缩版本;这些编码通常用于电话或语音录音。非线性编码将原始声音的幅度通过非线性函数映射到存储值,该函数可以设计为给予安静声音比响亮声音更多的幅度分辨率。

一个帧包含特定时间所有通道的数据。对于 PCM 编码的数据,帧只是所有通道在给定时间点的同时样本集,没有任何附加信息。在这种情况下,帧速率等于采样率,帧大小以字节为单位是通道数乘以位采样大小,再除以字节的位数。

对于其他类型的编码,一个帧可能包含除样本之外的附加信息,并且帧速率可能与采样率完全不同。例如,考虑 MP3(MPEG-1 音频第三层)编码,它在当前版本的 Java Sound API 中没有明确提到,但可以由 Java Sound API 的实现或第三方服务提供商支持。在 MP3 中,每个帧包含一系列样本的压缩数据包,而不仅仅是每个通道的一个样本。由于每个帧封装了一整个系列的样本,因此帧速率比采样率慢。帧还包含一个头部。尽管有头部,但帧的字节大小比等量的 PCM 帧的字节大小要小。(毕竟,MP3 的目的是比 PCM 数据更紧凑。)对于这种编码,采样率和采样大小指的是编码后的声音最终将被转换成的 PCM 数据,然后传递给数字模拟转换器(DAC)。

文件格式

文件格式指定了声音文件的结构,包括文件中原始音频数据的格式,以及可以存储在文件中的其他信息。声音文件有各种标准品种,如 WAVE(也称为 WAV,通常与 PC 关联)、AIFF(通常与 Macintosh 关联)和 AU(通常与 UNIX 系统关联)。不同类型的声音文件具有不同的结构。例如,它们可能在文件的“头部”中具有不同的数据排列。头部包含描述性信息,通常在文件的实际音频样本之前,尽管一些文件格式允许连续的描述性和音频数据“块”。头部包括规范用于存储声音文件中音频的数据格式。任何这些类型的声音文件都可以包含各种数据格式(尽管通常在给定文件中只有一个数据格式),并且相同的数据格式可以在具有不同文件格式的文件中使用。

在 Java Sound API 中,文件格式由一个AudioFileFormat对象表示,其中包含:

  • 文件类型(WAVE、AIFF 等)

  • 文件的字节长度

  • 文件中包含的音频数据的帧数长度

  • 一个指定文件中包含的音频数据的数据格式的 AudioFormat 对象

AudioSystem类提供了用于读取和写入不同文件格式的声音以及在不同数据格式之间转换的方法。其中一些方法允许您通过一种称为AudioInputStream的流来访问文件的内容。AudioInputStreamInputStream类的子类,封装了可以按顺序读取的一系列字节。AudioInputStream类添加了有关字节音频数据格式的知识(由AudioFormat对象表示)到其超类。通过将声音文件作为AudioInputStream读取,您可以立即访问样本,而无需担心声音文件的结构(其头部、块等)。单个方法调用将为您提供有关数据格式和文件类型的所有信息。

什么是混音器?

许多声音应用程序编程接口(API)使用音频设备的概念。设备通常是对物理输入/输出设备的软件接口。例如,声音输入设备可能代表声卡的输入功能,包括麦克风输入、线路级模拟输入,以及可能的数字音频输入。

在 Java Sound API 中,设备由Mixer对象表示。混音器的目的是处理一个或多个音频输入流和一个或多个音频输出流。在典型情况下,它实际上将多个传入流混合成一个传出流。一个Mixer对象可以表示物理设备(如声卡)的声音混合功能,该设备可能需要混合从各种输入到计算机的声音,或者从应用程序到输出的声音。

或者,一个Mixer对象可以表示完全在软件中实现的声音混合功能,而没有与物理设备的固有接口。

在 Java Sound API 中,诸如声卡上的麦克风输入之类的组件本身并不被视为设备——也就是混音器——而是混音器内或外的端口。一个端口通常提供一个音频流进入或离开混音器(尽管该流可以是多声道的,比如立体声)。混音器可能有几个这样的端口。例如,代表声卡输出功能的混音器可能将几个音频流混合在一起,然后将混合信号发送到连接到混音器的任何或所有各种输出端口。这些输出端口可以是(例如)耳机插孔、内置扬声器或线路级输出。

要理解 Java Sound API 中混音器的概念,有助于想象一个物理混音控制台,比如在现场音乐会和录音室中使用的那种。

以下上下文描述这幅图。

物理混音控制台

物理混音器有“条带”(也称为“切片”),每个条带代表一个音频信号通过混音器进行处理的路径。该条带有旋钮和其他控件,通过这些控件可以控制该条带中信号的音量和声像(在立体声图像中的位置)。此外,混音器可能有一个用于混响等效果的单独总线,该总线可以连接到内部或外部混响单元。每个条带都有一个电位器,用于控制该条带信号的多少进入混响混合中。混响(“湿”)混合然后与来自条带的“干”信号混合。物理混音器将这个最终混合发送到一个输出总线,通常连接到磁带录音机(或基于磁盘的录音系统)和/或扬声器。

想象一场正在立体录制的现场音乐会。来自舞台上许多麦克风和电子乐器的电缆(或无线连接)插入混音台的输入。每个输入都进入混音器的一个单独条道,如图所示。音响工程师决定增益、声像和混响控件的设置。所有条道和混响单元的输出混合成两个声道。这两个声道进入混音器的两个输出,插入连接到立体磁带录音机输入的电缆中。这两个声道可能也通过放大器发送到大厅的扬声器,这取决于音乐类型和大厅的大小。

现在想象一个录音室,在录音室中,每个乐器或歌手都被录制到多轨磁带录音机的单独轨道上。在所有乐器和歌手都被录制后,录音工程师执行“混音”操作,将所有录制的轨道组合成可以分发到 CD 上的两声道(立体声)录音。在这种情况下,混音器条的每个输入不是麦克风,而是多轨录音的一个轨道。工程师可以再次使用条上的控件来决定每个轨道的音量、声像和混响量。混音器的输出再次进入立体录音机和立体扬声器,就像现场音乐会的例子一样。

这两个例子说明了混音器的两种不同用途:捕获多个输入通道,将它们组合成较少的轨道并保存混合物,或者播放多个轨道同时将它们混合成较少的轨道。

在 Java Sound API 中,混音器可以类似地用于输入(捕获音频)或输出(播放音频)。在输入的情况下,混音器获取音频进行混音的是一个或多个输入端口。混音器将捕获和混合的音频流发送到其目标,这是一个带有缓冲区的对象,应用程序可以从中检索这些混合音频数据。在音频输出的情况下,情况则相反。混音器的音频源是一个或多个包含缓冲区的对象,其中一个或多个应用程序将其声音数据写入其中;混音器的目标是一个或多个输出端口。

什么是一条线?

一个物理混音台的隐喻也有助于理解 Java Sound API 对线路概念的理解。

一条线是数字音频“管道”的一个元素,即将音频移入或移出系统的路径。通常,该线路是进入或离开混音器的路径(尽管从技术上讲,混音器本身也是一种线路)。

音频输入和输出端口是线路。这些类似于连接到物理混音台的麦克风和扬声器。另一种线路是应用程序可以通过其中获取输入音频或将输出音频发送到混音器的数据路径。这些数据路径类似于连接到物理混音台的多轨录音机的轨道。

Java Sound API 中的线路与物理混音器的一个区别是,通过 Java Sound API 中的线路流动的音频数据可以是单声道或多声道(例如,立体声)。相比之下,物理混音器的每个输入和输出通常是单声道的声音。要从物理混音器获得两个或更多声道的输出,通常会使用两个或更多个物理输出(至少在模拟声音的情况下;数字输出插孔通常是多声道的)。在 Java Sound API 中,线路中的声道数由当前流经线路的数据的AudioFormat指定。

现在让我们来看一些特定类型的线路和混音器。以下图表显示了 Java Sound API 实现的简单音频输出系统中不同类型的线路:

以下内容描述此图。

音频输出的可能配置线路

在这个例子中,一个应用程序已经获得了音频输入混音器的一些可用输入:一个或多个片段源数据线路。片段是一个混音器输入(一种线路),你可以在播放之前将音频数据加载到其中;源数据线路是一个接受实时音频数据流的混音器输入。应用程序将音频数据从声音文件预加载到片段中。然后,它将其他音频数据一次一个缓冲区地推送到源数据线路中。混音器从所有这些线路中读取数据,每个线路可能有自己的混响、增益和声像控制,并将干净的音频信号与湿润(混响)混合。混音器将最终输出传送到一个或多个输出端口,例如扬声器、耳机插孔和线路输出插孔。

尽管在图中各个线路被描绘为单独的矩形,但它们都是混音器的“所有权”,可以被视为混音器的组成部分。混响、增益和声像矩形代表混音器可以应用于流经线路的数据的处理控制(而不是线路)。

请注意,这只是 API 支持的可能混音器的一个示例。并非所有音频配置都具有所示的所有功能。个别源数据线路可能不支持声像控制,混音器可能不实现混响等。

一个简单的音频输入系统可能类似:

以下内容描述此图

音频输入线路的可能配置

在这里,数据从一个或多个输入端口流入混音器,通常是麦克风或线路输入插孔。增益和声像被应用,混音器通过混音器的目标数据线将捕获的数据传递给应用程序。目标数据线是混音器的输出,包含流式输入声音的混合物。最简单的混音器只有一个目标数据线,但有些混音器可以同时将捕获的数据传递给多个目标数据线。

线接口层次结构

现在我们已经看到了一些关于线路和混音器的功能图片,让我们从稍微更具编程视角的角度来讨论它们。通过基本Line接口的子接口定义了几种类型的线路。接口层次结构如下所示。

以下内容描述了这幅图

线接口层次结构

基本接口Line描述了所有线路共有的最小功能:

  • 控件 - 数据线和端口通常具有一组控件,影响通过线路传递的音频信号。Java Sound API 指定了可以用于操纵声音方面的控件类,例如:增益(影响信号的分贝音量)、声像(影响声音的左右定位)、混响(为声音添加混响以模拟不同种类的房间声学)和采样率(影响播放速率以及声音的音调)。

  • 打开或关闭状态 - 成功打开线路保证已为线路分配了资源。混音器具有有限数量的线路,因此在某些时候,多个应用程序(或同一个应用程序)可能会竞争使用混音器的线路。关闭线路表示线路使用的任何资源现在可以被释放。

  • 事件 - 当线路打开或关闭时,线路会生成事件。Line的子接口可以引入其他类型的事件。当线路生成事件时,事件会发送给所有已注册在该线路上“监听”事件的对象。应用程序可以创建这些对象,将它们注册为监听线路事件,并根据需要对事件做出反应。

现在我们将检查Line接口的子接口。

端口是用于音频输入或输出到音频设备的简单线路。如前所述,一些常见类型的端口是麦克风、线路输入、CD-ROM 驱动器、扬声器、耳机和线路输出。

Mixer接口代表一个混音器,当然,正如我们所见,它代表一个硬件或软件设备。Mixer接口提供了获取混音器线的方法。这些包括源线,将音频馈送到混音器,以及目标线,混音器将其混合音频传递给的线。对于音频输入混音器,源线是输入端口,如麦克风输入,目标线是TargetDataLines(下文描述),它会将音频传递给应用程序。另一方面,对于音频输出混音器,源线是ClipsSourceDataLines(下文描述),应用程序向其馈送音频数据,目标线是输出端口,如扬声器。

一个Mixer被定义为具有一个或多个源线和一个或多个目标线。请注意,这个定义意味着一个混音器不一定实际混合数据;它可能只有一个单一的源线。Mixer API 旨在涵盖各种设备,但典型情况下支持混音。

Mixer接口支持同步;也就是说,您可以指定一个混音器的两个或多个线被视为同步组。然后,您可以通过向组中的任何线发送单个消息来启动、停止或关闭所有这些数据线,而不必单独控制每条线。使用支持此功能的混音器,您可以在线之间获得样本精确的同步。

通用的Line接口不提供启动和停止播放或录制的方法。为此,您需要一个数据线。DataLine接口提供了以下额外的与媒体相关的功能,超出了Line的功能:

  • 音频格式 – 每个数据线都有与其数据流相关联的音频格式。

  • 媒体位置 – 数据线可以报告其在媒体中的当前位置,以采样帧表示。这代表自数据线打开以来捕获或渲染的采样帧数量。

  • 缓冲区大小 – 这是数据线内部缓冲区的大小,以字节为单位。对于源数据线,内部缓冲区是可以写入数据的,对于目标数据线,它是可以读取数据的。

  • 音量(音频信号的当前振幅)

  • 启动和停止播放或捕获

  • 暂停和恢复播放或捕获

  • 刷新(丢弃队列中的未处理数据)

  • 排空(阻塞直到队列中的所有未处理数据都被排空,并且数据线的缓冲区变为空)

  • 活动状态 – 如果数据线参与从混音器捕获音频数据或向混音器捕获音频数据,则被视为活动状态。

  • 事件 – STARTSTOP 事件在从数据线开始或停止活动演示或捕获数据时产生。

TargetDataLine 从混音器接收音频数据。通常,混音器从诸如麦克风之类的端口捕获音频数据;在将数据放入目标数据线缓冲区之前,它可能会处理或混合此捕获的音频。TargetDataLine 接口提供了从目标数据线缓冲区读取数据的方法,并确定当前可用于读取的数据量。

SourceDataLine 接收用于播放的音频数据。它提供了将数据写入源数据线缓冲区以进行播放的方法,并确定数据线准备接收多少数据而不会阻塞。

Clip 是一个数据线,可以在播放之前加载音频数据。由于数据是预加载而不是流式传输,因此在播放之前可以知道剪辑的持续时间,并且可以选择媒体中的任何起始位置。剪辑可以循环播放,意味着在播放时,两个指定循环点之间的所有数据将重复指定次数,或者无限循环。

本节介绍了采样音频 API 中大部分重要的接口和类。后续章节将展示如何在应用程序中访问和使用这些对象。

访问音频系统资源

原文:docs.oracle.com/javase/tutorial/sound/accessing.html

Java Sound API 对系统配置采取了灵活的方法。计算机上可以安装不同类型的音频设备(混音器)。该 API 对已安装的设备及其功能能力几乎不做任何假设。相反,它提供了系统报告可用音频组件的方法,以及您的程序访问它们的方法。

以下部分展示了您的程序如何了解计算机上已安装的采样音频资源以及如何访问可用资源。资源包括混音器和混音器拥有的各种类型的线路等。

AudioSystem

AudioSystem 类充当音频组件的集散地,包括来自第三方提供商的内置服务和单独安装的服务。 AudioSystem 作为应用程序访问这些已安装的采样音频资源的入口点。您可以查询 AudioSystem 以了解已安装了哪些资源,然后可以访问这些资源。例如,应用程序可能首先询问 AudioSystem 类是否有具有特定配置的混音器,例如在前面讨论线路时所示的输入或输出配置之一。然后,程序将从混音器获取数据线路,依此类推。

以下是应用程序可以从 AudioSystem 获取的一些资源:

  • 混音器 — 系统通常安装了多个混音器。通常至少有一个用于音频输入和一个用于音频输出。还可能有一些混音器没有 I/O 端口,而是接受应用程序的音频并将混合后的音频传递回程序。 AudioSystem 类提供了所有已安装混音器的列表。

  • 线路 — 即使每条线路都与混音器相关联,应用程序也可以直接从 AudioSystem 获取线路,而无需明确处理混音器。

  • 格式转换 — 应用程序可以使用格式转换将音频数据从一种格式转换为另一种格式。

  • 文件和流 — AudioSystem 类提供了在音频文件和音频流之间进行转换的方法。它还可以报告声音文件的文件格式,并且可以以不同格式写入文件。

信息对象

Java Sound API 中的几个类提供有关相关接口的有用信息。例如,Mixer.Info提供有关已安装混音器的详细信息,如混音器的供应商、名称、描述和版本。Line.Info获取特定线路的类。Line.Info的子类包括Port.InfoDataLine.Info,分别获取与特定端口和数据线相关的详细信息。这些类中的每一个在下面的适当部分中进一步描述。重要的是不要混淆Info对象与其描述的混音器或线路对象。

获取混音器

通常,使用 Java Sound API 的程序需要做的第一件事情之一是获取一个混音器,或者至少获取一个混音器的一条线路,以便将声音输入或输出计算机。您的程序可能需要特定类型的混音器,或者您可能希望显示所有可用混音器的列表,以便用户可以选择一个。在任何情况下,您需要了解安装了哪些类型的混音器。AudioSystem提供以下方法:

static Mixer.Info[] getMixerInfo()

由此方法返回的每个Mixer.Info对象标识安装的一种混音器类型。(通常系统最多只有一个给定类型的混音器。如果恰好有多个给定类型的混音器,则返回的数组仍然只有一个该类型的Mixer.Info。)应用程序可以遍历Mixer.Info对象,根据自身需求找到合适的混音器。Mixer.Info包括以下字符串来标识混音器的类型:

  • 名称

  • 版本

  • 供应商

  • 描述

这些都是任意字符串,因此需要特定混音器的应用程序必须知道可以期望什么以及将字符串与什么进行比较。提供混音器的公司应在其文档中包含此信息。或者,也许更典型的是,应用程序将向用户显示所有Mixer.Info对象的字符串,让用户选择相应的混音器。

一旦找到合适的混音器,应用程序调用以下AudioSystem方法来获取所需的混音器:

static Mixer getMixer(Mixer.Info info)

如果您的程序需要具有某些功能的混音器,但不需要特定供应商制造的特定混音器怎么办?如果您不能依赖用户知道应选择哪个混音器怎么办?在这种情况下,Mixer.Info对象中的信息将没有太大用处。相反,您可以通过调用getMixer为每个Mixer.Info对象返回的所有Mixer.Info对象迭代,获取每个混音器,并查询每个混音器的功能。例如,您可能需要一个可以将其混合音频数据同时写入一定数量的目标数据线的混音器。在这种情况下,您将使用此Mixer方法查询每个混音器:

int getMaxLines(Line.Info info)

在这里,Line.Info将指定一个TargetDataLineLine.Info类将在下一节中讨论。

获取所需类型的线

有两种方法可以获取一条线:

  • 直接从AudioSystem对象

  • 从您已从AudioSystem对象获取的混音器获取

直接从AudioSystem获取一条线

假设您尚未获得混音器,并且您的程序是一个真正只需要某种类型的线的简单程序;混音器的细节对您并不重要。您可以使用AudioSystem方法:

static Line getLine(Line.Info info)

这类似于先前讨论的getMixer方法。与Mixer.Info不同,作为参数使用的Line.Info不存储文本信息以指定所需的线。相反,它存储有关所需线类的信息。

Line.Info是一个抽象类,因此您可以使用其子类(Port.InfoDataLine.Info)来获取一条线。以下代码摘录使用DataLine.Info子类来获取和打开目标数据线:

TargetDataLine line;
DataLine.Info info = new DataLine.Info(TargetDataLine.class, 
    format); // format is an AudioFormat object
if (!AudioSystem.isLineSupported(info)) {
    // Handle the error.
    }
    // Obtain and open the line.
try {
    line = (TargetDataLine) AudioSystem.getLine(info);
    line.open(format);
} catch (LineUnavailableException ex) {
        // Handle the error.
    //... 
}

此代码获取一个TargetDataLine对象,除了其类和音频格式之外,没有指定任何属性。您可以使用类似的代码来获取其他类型的线。对于SourceDataLineClip,只需将该类替换为TargetDataLine作为线变量的类,并且在DataLine.Info构造函数的第一个参数中也进行替换。

对于Port,您可以在以下代码中使用Port.Info的静态实例:

if (AudioSystem.isLineSupported(Port.Info.MICROPHONE)) {
    try {
        line = (Port) AudioSystem.getLine(
            Port.Info.MICROPHONE);
    }
}

请注意使用isLineSupported方法来查看混音器是否具有所需类型的线。

请记住,源线是混音器的输入,即,如果混音器代表音频输入设备,则是一个Port对象,如果混音器代表音频输出设备,则是一个SourceDataLineClip对象。同样,目标线是混音器的输出:对于音频输出混音器,是一个Port对象,对于音频输入混音器,是一个TargetDataLine对象。如果一个混音器根本没有连接到任何外部硬件设备怎么办?例如,考虑一个仅从应用程序获取音频并将混合音频传递回程序的内部或仅软件混音器。这种混音器的输入线有SourceDataLineClip对象,输出线有TargetDataLine对象。

您还可以使用以下AudioSystem方法了解任何已安装混音器支持的指定类型的源和目标线路:

static Line.Info[] getSourceLineInfo(Line.Info info)
static Line.Info[] getTargetLineInfo(Line.Info info)

请注意,每个方法返回的数组表示唯一类型的线路,不一定是所有线路。例如,如果一个混音器的两条线路,或者两个不同混音器的两条线路,具有相同的Line.Info对象,则返回的数组中只会表示一个Line.Info

从混音器获取线路

Mixer接口包括上述AudioSystem访问方法的变体,用于源和目标线路。这些Mixer方法包括接受Line.Info参数的方法,就像AudioSystem的方法一样。但是,Mixer还包括这些不带参数的变体:

Line.Info[] getSourceLineInfo()
Line.Info[] getTargetLineInfo()

这些方法返回特定混音器的所有Line.Info对象的数组。一旦获得数组,您可以遍历它们,调用MixergetLine方法获取每条线路,然后调用Lineopen方法为您的程序保留每条线路的使用权。

选择输入和输出端口

关于如何获取所需类型的线路的上一节也适用于端口以及其他类型的线路。您可以通过将Port.Info对象传递给接受Line.Info参数的AudioSystem(或Mixer)方法getSourceLineInfogetTargetLineInfo来获取所有源(即输入)和目标(即输出)端口。然后,您遍历返回的对象数组并调用 Mixer 的getLine方法以获取每个端口。

然后,通过调用Lineopen方法打开每个Port。打开端口意味着您打开它 - 也就是说,您允许声音进出端口。同样,您可以关闭您不希望声音通过的端口,因为有些端口可能在您获取它们之前已经打开。一些平台默认打开所有端口;或者用户或系统管理员可能已经选择使用另一个应用程序或操作系统软件打开或关闭某些端口。

警告: 如果您想选择特定端口并确保声音实际上进出该端口,可以按照描述打开端口。但是,这可能被视为对用户不友好的行为!例如,用户可能已关闭扬声器端口以免打扰同事。如果您的程序突然推翻了她的意愿并开始播放音乐,她会感到非常沮丧。另一个例子,用户可能希望确保计算机的麦克风在没有他知情的情况下永远不会打开,以避免窃听。一般来说,建议不要打开或关闭端口,除非您的程序响应用户通过用户界面表达的意图。相反,尊重用户或操作系统已选择的设置。

在连接到混音器之前,不需要打开或关闭端口才能使其正常工作。例如,即使所有输出端口关闭,您也可以开始将声音播放到音频输出混音器中。数据仍然流入混音器;播放不会被阻止。用户只是听不到任何声音。一旦用户打开输出端口,声音将通过该端口听到,从媒体播放已经到达的任何点开始。

此外,您不需要访问端口来了解混音器是否具有某些端口。例如,要了解混音器是否实际上是音频输出混音器,可以调用getTargetLineInfo来查看它是否具有输出端口。除非您想更改其设置(例如打开或关闭状态或它们可能具有的任何控件的设置),否则没有理由访问端口本身。

使用音频资源的权限

Java Sound API 包括一个AudioPermission类,指示 applet(或在安全管理器下运行的应用程序)对采样音频系统可以具有哪些访问权限。录制声音的权限是单独控制的。应谨慎授予此权限,以帮助防止未经授权的窃听等安全风险。默认情况下,applet 和应用程序被授予以下权限:

  • 使用 applet 安全管理器运行的applet可以播放音频,但不能录制。

  • 没有安全管理器运行的应用程序可以播放和录制音频。

  • 使用默认安全管理器运行的应用程序可以播放音频,但不能录制。

一般来说,applet 在安全管理器的监督下运行,不允许录制声音。另一方面,应用程序不会自动安装安全管理器,并且可以录制声音。(但是,如果为应用程序显式调用默认安全管理器,则不允许应用程序录制声音。)

即使在安全管理器下运行,只要已被明确授予权限,applet 和应用程序都可以录制声音。

如果您的程序没有录制(或播放)声音的权限,在尝试打开线路时会抛出异常。在程序中,除了捕获异常并向用户报告问题外,您无法做任何事情,因为权限无法通过 API 更改。(如果可以的话,它们将毫无意义,因为没有任何东西是安全的!)通常,权限是在一个或多个策略配置文件中设置的,用户或系统管理员可以使用文本编辑器或策略工具程序进行编辑。

播放音频

原文:docs.oracle.com/javase/tutorial/sound/playing.html

播放有时被称为演示渲染。这些是适用于声音以外的其他媒体的通用术语。其关键特征是一系列数据被传送到某个地方,最终由用户感知。如果数据是基于时间的,如声音,它必须以正确的速率传送。与视频相比,对于声音来说,数据流速率的维持更为重要,因为声音播放中断通常会产生响亮的点击声或刺耳的失真。Java Sound API 旨在帮助应用程序平稳连续地播放声音,即使是非常长的声音。

之前您已经看到如何从音频系统或混音器中获取线路。在这里,您将学习如何通过线路播放声音。

如您所知,有两种可以用于播放声音的线路:ClipSourceDataLine。两者之间的主要区别在于,使用Clip时,您在播放之前一次性指定所有声音数据,而使用SourceDataLine时,在播放过程中持续写入新的数据缓冲区。虽然有许多情况可以使用ClipSourceDataLine,但以下标准有助于确定哪种线路更适合特定情况:

  • 当您有非实时声音数据可以预加载到内存中时,请使用Clip

    例如,您可以将短声音文件读入剪辑中。如果您希望声音重复播放多次,则ClipSourceDataLine更方便,特别是如果您希望播放循环(重复通过声音的全部或部分)。如果您需要在声音中的任意位置开始播放,Clip接口提供了一种轻松实现的方法。最后,与从SourceDataLine缓冲播放相比,从Clip播放通常具有更少的延迟。换句话说,因为声音已经预加载到剪辑中,播放可以立即开始,而不必等待缓冲区填充。

  • 使用SourceDataLine来流式传输数据,比如无法一次性全部放入内存的长声音文件,或者在播放之前无法预先知道数据的声音。

    作为后一种情况的示例,假设您正在监视声音输入,即在捕获声音时播放声音。如果您没有一个可以将输入音频发送回输出端口的混音器,您的应用程序将不得不获取捕获的数据并将其发送到音频输出混音器。在这种情况下,使用SourceDataLine比使用Clip更合适。另一个无法事先知道的声音示例是当您根据用户的输入合成或操作声音数据时。例如,想象一个游戏通过在用户移动鼠标时从一个声音“变形”到另一个声音来提供听觉反馈。声音转换的动态性要求应用程序在播放过程中持续更新声音数据,而不是在播放开始之前提供所有数据。

使用 Clip

如前所述,在获取所需类型的行下获取一个Clip;用Clip.class作为第一个参数构造一个DataLine.Info对象,并将此DataLine.Info作为参数传递给AudioSystemMixergetLine方法。

获取一条线只是意味着您已经找到了一个引用它的方法;getLine实际上并没有为您保留该线路。因为混音器可能只有有限数量的所需类型的线路可用,所以在您调用getLine获取剪辑后,可能会发生另一个应用程序在您准备开始播放之前抢走剪辑的情况。要实际使用剪辑,您需要通过调用以下Clip方法之一来为您的程序独占地保留它:

void open(AudioInputStream stream)
void open(AudioFormat format, byte[] data, int offset, int bufferSize)

尽管上述第二个open方法中有bufferSize参数,但Clip(不像SourceDataLine)不包括用于向缓冲区写入新数据的方法。这里的bufferSize参数只指定要加载到剪辑中的字节数组的大小。它不是一个可以随后加载更多数据的缓冲区,就像您可以使用SourceDataLine的缓冲区一样。

打开剪辑后,您可以使用ClipsetFramePositionsetMicroSecondPosition方法指定数据中应开始播放的位置。否则,它将从开头开始。您还可以使用setLoopPoints方法配置循环播放。

当您准备开始播放时,只需调用start方法。要停止或暂停剪辑,请调用stop方法,要恢复播放,请再次调用start。剪辑会记住停止播放的媒体位置,因此不需要显式的暂停和恢复方法。如果您不希望它在停止播放的位置继续播放,可以使用上述提到的帧或微秒定位方法将剪辑“倒带”到开头(或任何其他位置)。

可以通过调用DataLine方法getLevelisActive来监视Clip的音量级别和活动状态(活动与非活动)。活动的Clip是当前正在播放声音的Clip

使用 SourceDataLine

获取SourceDataLine类似于获取Clip。打开SourceDataLine也类似于打开Clip,因为目的再次是为了保留该线路。但是,您使用从DataLine继承的不同方法:

void open(AudioFormat format)

请注意,当您打开SourceDataLine时,尚未将任何声音数据与该线路关联,与打开Clip不同。相反,您只需指定要播放的音频数据的格式。系统会选择默认的缓冲区长度。

您还可以使用以下变体指定特定的字节缓冲区长度:

void open(AudioFormat format, int bufferSize)

为了与类似方法保持一致,bufferSize参数以字节表示,但必须对应于整数帧数。

除了使用上述描述的 open 方法,还可以使用Lineopen()方法打开SourceDataLine,而无需参数。在这种情况下,该线路将以其默认音频格式和缓冲区大小打开。但是,您以后无法更改这些。如果您想知道线路的默认音频格式和缓冲区大小,甚至在线路尚未打开之前,可以调用DataLinegetFormatgetBufferSize方法。

一旦SourceDataLine打开,您就可以开始播放声音。您可以通过调用DataLine的 start 方法来实现这一点,然后将数据重复写入线路的播放缓冲区。

start 方法允许线路在其缓冲区中有任何数据时开始播放声音。您可以通过以下方法将数据放入缓冲区:

int write(byte[] b, int offset, int length)

数组中的偏移量以字节表示,数组的长度也是以字节表示。

该线路尽快将数据发送到其混音器。当混音器将数据传递给其目标时,SourceDataLine会生成START事件。 (在 Java Sound API 的典型实现中,源线将数据传递给混音器的时间延迟与混音器将数据传递给其目标的时间延迟可以忽略不计,即远小于一个样本的时间。)此START事件将发送给线路的侦听器,如下所述监视线路状态。现在该线路被视为活动的,因此DataLineisActive方法将返回true。请注意,所有这些仅在缓冲区包含要播放的数据时才会发生,不一定在调用 start 方法时立即发生。如果您在新的SourceDataLine上调用了start但从未向缓冲区写入数据,则该线路永远不会处于活动状态,并且START事件永远不会发送。(但是,在这种情况下,DataLineisRunning方法将返回true。)

那么你如何知道要向缓冲区写入多少数据,以及何时发送第二批数据呢?幸运的是,你不需要计时第二次调用 write 以与第一个缓冲区的结束同步!相反,你可以利用write方法的阻塞行为:

  • 该方法在数据被写入缓冲区后立即返回。它不会等到缓冲区中的所有数据都播放完毕。(如果等待的话,你可能没有时间写入下一个缓冲区,从而导致音频中断。)

  • 尝试写入的数据量超过缓冲区容量是可以的。在这种情况下,该方法会阻塞(不返回),直到你请求的所有数据实际上都被放入缓冲区中。换句话说,每次只会写入一个缓冲区的数据并进行播放,直到剩余数据全部适应缓冲区为止,此时该方法才会返回。无论该方法是否阻塞,它都会在此次调用中最后一个缓冲区的数据被写入时立即返回。这意味着你的代码很可能在最后一个缓冲区的数据播放完成之前就重新获得了控制权。

  • 在许多情况下,写入的数据量超过缓冲区容量是可以的,但如果你想确保下一个写入不会阻塞,你可以将写入的字节数限制为DataLineavailable方法返回的数量。

下面是一个示例,迭代从流中读取的数据块,一次将一个数据块写入SourceDataLine进行播放:

// read chunks from a stream and write them to a source data 
line 
line.start();
while (total < totalToRead && !stopped)}
    numBytesRead = stream.read(myData, 0, numBytesToRead);
    if (numBytesRead == -1) break;
    total += numBytesRead; 
    line.write(myData, 0, numBytesRead);

}

如果你不希望write方法阻塞,你可以首先在循环内调用available方法来查找可以无阻塞写入的字节数,然后在从流中读取之前将numBytesToRead变量限制为这个数字。然而,在给定的示例中,阻塞并不重要,因为 write 方法是在一个循环内调用的,直到最后一个缓冲区在最后一个循环迭代中被写入。无论你是否使用阻塞技术,你可能会希望在应用程序的其余部分之外的一个单独线程中调用这个播放循环,这样当播放长声音时,你的程序不会出现冻结的情况。在循环的每次迭代中,你可以测试用户是否请求停止播放。这样的请求需要将上面代码中使用的stopped布尔值设置为true

由于write在所有数据完成播放之前返回,那么你如何知道播放实际上已经完成了呢?一种方法是在写入最后一个缓冲区数据后调用DataLinedrain方法。该方法会阻塞,直到所有数据都已经播放完毕。当控制返回到你的程序时,如果需要的话,你可以释放该线路,而不必担心会过早中断任何音频样本的播放:

line.write(b, offset, numBytesToWrite); 
//this is the final invocation of write
line.drain();
line.stop();
line.close();
line = null;

你可以有意提前停止播放,当然。例如,应用程序可能会为用户提供一个停止按钮。调用DataLinestop方法可以立即停止播放,即使在缓冲区的中间。这会使缓冲区中的任何未播放数据保留下来,因此如果随后调用start,播放将从停止的地方恢复。如果这不是你想要发生的事情,你可以通过调用flush来丢弃缓冲区中剩余的数据。

当数据流的流动已停止时,SourceDataLine会生成一个STOP事件,无论此停止是由drain方法、stop方法、flush方法引起的,还是因为在应用程序调用write提供新数据之前已到达播放缓冲区的末尾。STOP事件并不一定意味着已调用stop方法,并且也不一定意味着随后调用isRunning将返回false。但是,它确实意味着isActive将返回false。(当调用start方法时,即使生成STOP事件,isRunning方法也将返回true,并且只有在调用stop方法后才会开始返回false。)重要的是要意识到STARTSTOP事件对应于isActive,而不是isRunning

监控线路的状态

一旦开始播放声音,如何确定何时完成?我们在上面看到了一种解决方案,在写入最后一个数据缓冲区后调用drain方法,但这种方法仅适用于SourceDataLine。另一种途径,适用于SourceDataLinesClips,是注册以接收线路在改变其状态时发出的通知。这些通知以LineEvent对象的形式生成,其中有四种类型:OPENCLOSESTARTSTOP

在程序中实现LineListener接口的任何对象都可以注册以接收此类通知。要实现LineListener接口,对象只需要一个接受LineEvent参数的update方法。要将此对象注册为线路的侦听器之一,您需要调用以下Line方法:

public void addLineListener(LineListener listener)

每当线路打开、关闭、启动或停止时,它会向所有侦听器发送一个update消息。您的对象可以查询接收到的LineEvent。首先,您可能会调用LineEvent.getLine来确保停止的线路是您关心的线路。在我们讨论的情况下,您想知道声音是否已完成,因此您可以查看LineEvent是否为STOP类型。如果是,您可以检查声音的当前位置,该位置也存储在LineEvent对象中,并将其与声音的长度(如果已知)进行比较,以查看是否已达到结束并且没有被其他方式停止(例如用户点击停止按钮,尽管您可能能够在代码的其他地方确定该原因)。

同样,如果您需要知道何时打开、关闭或启动线路,可以使用相同的机制。LineEvents由不同类型的线路生成,不仅仅是ClipsSourceDataLines。但是,在Port的情况下,您不能指望获得事件来了解线路的打开或关闭状态。例如,当创建Port时,可能会最初打开Port,因此您不会调用open方法,而Port也不会生成OPEN事件。 (请参阅之前关于选择输入和输出端口的讨论。)

同步多条线路的播放

如果您同时播放多个音轨,您可能希望它们都在完全相同的时间开始和停止。一些混音器通过其synchronize方法促进此行为,该方法允许您对一组数据线应用操作,如openclosestartstop,而不是必须单独控制每条线路。此外,可以控制对线路应用操作的精度程度。

要了解特定数据线组的特定混音器是否提供此功能,调用Mixer接口的isSynchronizationSupported方法:

boolean isSynchronizationSupported(Line[] lines, boolean  maintainSync)

第一个参数指定了一组特定的数据线,第二个参数表示必须保持同步的精度。如果第二个参数是true,则查询是在询问混音器是否能够始终在所有时间内保持对指定线路的样本精确控制;否则,精确同步仅在启动和停止操作期间需要,而不是在整个播放过程中。

处理传出音频

一些源数据线具有信号处理控件,如增益、声像、混响和采样率控件。类似的控件,尤其是增益控件,也可能存在于输出端口上。有关如何确定一条线路是否具有此类控件以及如何在有此类控件的情况下使用它们的更多信息,请参阅使用控件处理音频。

音频捕获

原文:docs.oracle.com/javase/tutorial/sound/capturing.html

捕获指的是从计算机外部获取信号的过程。音频捕获的常见应用是录音,比如将麦克风输入录制到声音文件中。然而,捕获并不等同于录制,因为录制意味着应用程序始终保存正在输入的声音数据。捕获音频的应用程序不一定存储音频。相反,它可能在音频到达时对数据进行处理,比如将语音转录为文本,但在完成对每个缓冲区的处理后立即丢弃每个缓冲区的音频。

如示例包概述中所讨论的,在 Java Sound API 的实现中,典型的音频输入系统包括:

  1. 一个输入端口,比如麦克风端口或线路输入端口,将其传入的音频数据输入到:

  2. 一个混音器,将输入数据放入其中:

  3. 一个或多个目标数据线,应用程序可以从中检索数据。

通常情况下,一次只能打开一个输入端口,但也可以有一个混音器,从多个端口混合音频。另一种情况是一个没有端口的混音器,而是通过网络获取音频输入。

TargetDataLine接口在线接口层次结构下简要介绍过。TargetDataLineSourceDataLine接口直接类似,后者在播放音频中有详细讨论。回想一下,SourceDataLine接口包括:

  • 一个write方法将音频发送到混音器

  • 一个available方法,用于确定可以向缓冲区写入多少数据而不会阻塞

同样,TargetDataLine包括:

  • 一个read方法从混音器获取音频

  • 一个available方法,用于确定可以从缓冲区读取多少数据而不会阻塞

设置 TargetDataLine

获取目标数据线的过程在访问音频系统资源中已经描述,但为了方便起见,我们在这里重复一遍:

TargetDataLine line;
DataLine.Info info = new DataLine.Info(TargetDataLine.class, 
    format); // format is an AudioFormat object
if (!AudioSystem.isLineSupported(info)) {
    // Handle the error ... 

}
// Obtain and open the line.
try {
    line = (TargetDataLine) AudioSystem.getLine(info);
    line.open(format);
} catch (LineUnavailableException ex) {
    // Handle the error ... 
}

你可以调用MixergetLine方法,而不是AudioSystem的。

正如本例所示,一旦获得目标数据线,您可以通过调用SourceDataLine方法open来为应用程序保留它的使用权,就像在播放音频中描述的那样。open方法的单参数版本使线的缓冲区具有默认大小。您可以通过调用两个参数版本根据应用程序的需要设置缓冲区大小:

void open(AudioFormat format, int bufferSize)

从 TargetDataLine 读取数据

一旦线路打开,它就准备开始捕获数据,但它还没有激活。要实际开始音频捕获,使用DataLinestart方法。这将开始将输入音频数据传递到线路的缓冲区,以供您的应用程序读取。只有当应用程序准备好从线路读取时,应用程序才应该调用 start;否则,将浪费大量处理时间来填充捕获缓冲区,只会导致溢出(即,丢弃数据)。

要开始从缓冲区检索数据,请调用TargetDataLineread方法:

int read(byte[] b, int offset, int length)

这种方法尝试将length字节的数据读入数组b,从数组中的字节位置offset开始。该方法返回实际读取的字节数。

SourceDataLinewrite方法一样,您可以请求比缓冲区实际容量更多的数据,因为该方法会阻塞,直到请求的数据量已经传递,即使您请求了许多缓冲区的数据量。

为了避免在录制过程中使您的应用程序挂起,您可以在循环中调用 read 方法,直到检索到所有音频输入,就像这个例子中所示的那样:

// Assume that the TargetDataLine, line, has already
// been obtained and opened.
ByteArrayOutputStream out  = new ByteArrayOutputStream();
int numBytesRead;
byte[] data = new byte[line.getBufferSize() / 5];

// Begin audio capture.
line.start();

// Here, stopped is a global boolean set by another thread.
while (!stopped) {
   // Read the next chunk of data from the TargetDataLine.
   numBytesRead =  line.read(data, 0, data.length);
   // Save this chunk of data.
   out.write(data, 0, numBytesRead);
}     

请注意,在这个例子中,将数据读入的字节数组的大小设置为线路缓冲区大小的五分之一。如果您将其设置为与线路缓冲区一样大,并尝试读取整个缓冲区,您需要在时间上非常准确,因为如果混音器需要在您从中读取数据时向线路传递数据,数据将被丢弃。通过使用线路缓冲区大小的一部分,如此处所示,您的应用程序将更成功地与混音器共享对线路缓冲区的访问。

TargetDataLineread方法接受三个参数:一个字节数组,一个数组中的偏移量,以及您想要读取的输入数据的字节数。在这个例子中,第三个参数就是你的字节数组的长度。read方法返回实际读取到数组中的字节数。

通常,您会像这个例子中一样在循环中从线路中读取数据。在while循环中,每个检索到的数据块都会根据应用程序的适当方式进行处理——在这里,它被写入ByteArrayOutputStream。这里没有显示的是使用单独的线程来设置布尔值stopped,该值在循环终止时终止。当用户点击停止按钮时,该布尔值的值可能被设置为true,并且当监听器从线路接收到CLOSESTOP事件时也是如此。监听器对于CLOSE事件是必需的,对于STOP事件是推荐的。否则,如果线路在没有将stopped设置为true的情况下以某种方式停止,while循环将在每次迭代中捕获零字节,运行速度快,浪费 CPU 周期。一个更全面的代码示例将展示如果捕获再次激活,则重新进入循环。

与源数据线一样,可以排空或清空目标数据线。例如,如果您正在将输入录制到文件中,当用户点击停止按钮时,您可能希望调用drain方法。drain方法将导致混音器的剩余数据传递到目标数据线的缓冲区。如果您不排空数据,捕获的声音可能在末尾被截断。

也许有一些情况下,您希望清空数据。无论如何,如果您既不清空也不排空数据,数据将保留在混音器中。这意味着当重新开始捕获时,新录音的开头会有一些残留声音,这可能是不希望的。因此,在重新开始捕获之前清空目标数据线可能是有用的。

监控线路状态

因为TargetDataLine接口扩展了DataLine,目标数据线生成事件的方式与源数据线相同。您可以注册一个对象,以便在目标数据线打开、关闭、启动或停止时接收事件。有关更多信息,请参阅之前关于监控线路状态的讨论。

处理传入音频

像一些源数据线一样,一些混音器的目标数据线具有信号处理控件,例如增益、声像、混响或采样率控件。输入端口可能具有类似的控件,特别是增益控件。在下一节中,您将学习如何确定一条线是否具有这些控件,以及如何在有这些控件的情况下使用它们。

使用控件处理音频

原文:docs.oracle.com/javase/tutorial/sound/controls.html

先前的部分已经讨论了如何播放或捕获音频样本。隐含的目标是尽可能忠实地传递样本,而不进行修改(除了可能将样本与其他音频线路的样本混合)。然而,有时您可能希望能够修改信号。用户可能希望声音听起来更响亮、更安静、更丰满、更具混响、音高更高或更低等。本页讨论了提供这些类型信号处理的 Java Sound API 功能。

有两种应用信号处理的方式:

  • 您可以通过查询Control对象并根据用户的需求设置控件来使用混音器或其组件线路支持的任何处理。混音器和线路通常支持的控件包括增益、声像和混响控件。

  • 如果混音器或其线路提供的处理方式不符合您的需求,您的程序可以直接操作音频字节,根据需要进行操作。

本页更详细地讨论了第一种技术,因为第二种技术没有专门的 API。

控件介绍

混音器的某些或所有线路上可以有各种信号处理控件。例如,用于音频捕获的混音器可能具有带增益控制的输入端口,以及带增益和声像控制的目标数据线。用于音频播放的混音器可能在其源数据线上具有采样率控制。在每种情况下,所有控件都通过Line接口的方法访问。

因为Mixer接口扩展了Line,混音器本身可以具有自己的一组控件。这些控件可能作为主控件,影响所有混音器的源或目标线路。例如,混音器可能具有主增益控制,其分贝值会添加到其目标线路上各个增益控制的值中。

混音器的其他控件可能会影响一个特殊的线路,既不是源也不是目标,混音器在内部用于其处理。例如,全局混响控制可能选择应用于输入信号混合的混响类型,这个“湿”(混响)信号会在传递到混音器的目标线路之前混合回“干”信号中。

如果混音器或其任何线路具有控件,则您可能希望通过程序用户界面中的图形对象公开控件,以便用户可以根据需要调整音频特性。这些控件本身并不是图形化的;它们只是允许您检索和更改其设置。由您决定在程序中使用何种图形表示(滑块、按钮等)。

所有控件都作为抽象类Control的具体子类实现。许多典型的音频处理控件可以通过基于数据类型(如布尔值、枚举值或浮点数)的Control的抽象子类来描述。例如,布尔控件代表二进制状态控件,如静音或混响的开/关控件。另一方面,浮点控件非常适合表示连续可变控件,如声道、平衡或音量。

Java 音频 API 指定了以下Control的抽象子类:

  • BooleanControl — 代表二进制状态(真或假)控件。例如,静音、独奏和开/关开关都是BooleanControls的良好候选。

  • FloatControl — 提供对一系列浮点值的控制的数据模型。例如,音量和声道是可以通过旋钮或滑块操作的FloatControls

  • EnumControl — 提供从一组对象中进行选择的选项。例如,您可以将用户界面中的一组按钮与EnumControl关联起来,以选择几个预设混响设置中的一个。

  • CompoundControl — 提供对一组相关项目的访问,其中每个项目本身都是Control子类的实例。CompoundControls代表多控件模块,如图形均衡器。(图形均衡器通常由一组滑块表示,每个滑块影响一个FloatControl。)

上述每个Control的子类都有适用于其基础数据类型的方法。大多数类包括设置和获取控件当前值的方法,获取控件标签等。

当然,每个类都有特定于它和类所代表的数据模型的方法。例如,EnumControl有一个方法让您获取其可能值的集合,而FloatControl允许您获取其最小和最大值,以及控件的精度(增量或步长)。

每个Control的子类都有一个对应的Control.Type子类,其中包括标识特定控件的静态实例。

以下表格显示了每个Control子类、其对应的Control.Type子类以及指示特定控件类型的静态实例:

Control Control.Type Control.Type 实例
BooleanControl BooleanControl.Type MUTE – 线路静音状态 APPLY_REVERB – 混响开/关
CompoundControl CompoundControl.Type (无)
EnumControl EnumControl.Type REVERB – 访问混响设置(每个都是 ReverbType 的实例)

| FloatControl | FloatControl.Type | AUX_RETURN – 线路上的辅助返回增益 AUX_SEND – 线路上的辅助发送增益

BALANCE – 左右音量平衡

MASTER_GAIN – 线路上的总增益

PAN – 左右位置

REVERB_RETURN – 线路上的后混响增益

REVERB_SEND – 线路上的前混响增益

SAMPLE_RATE – 播放采样率

VOLUME – 线路上的音量 |

Java Sound API 的实现可以在其混音器和线路上提供任何或所有这些控件类型。它还可以提供 Java Sound API 中未定义的其他控件类型。这些控件类型可以通过这四个抽象子类的具体子类或不继承这四个抽象子类的其他 Control 子类来实现。应用程序可以查询每条线路以查找它支持的控件。

获取具有所需控件的线路

在许多情况下,应用程序将简单地显示该线路支持的任何控件。如果线路没有任何控件,那就算了。但是如果重要的是找到具有特定控件的线路呢?在这种情况下,您可以使用 Line.Info 来获取具有正确特征的线路,如之前在 获取所需类型的线路 中描述的。

例如,假设您希望有一个输入端口,让用户设置声音输入的音量。以下代码摘录显示了如何查询默认混音器以确定它是否具有所需的端口和控件:

Port lineIn;
FloatControl volCtrl;
try {
  mixer = AudioSystem.getMixer(null);
  lineIn = (Port)mixer.getLine(Port.Info.LINE_IN);
  lineIn.open();
  volCtrl = (FloatControl) lineIn.getControl(

      FloatControl.Type.VOLUME);

  // Assuming getControl call succeeds, 
  // we now have our LINE_IN VOLUME control.
} catch (Exception e) {
  System.out.println("Failed trying to find LINE_IN"
    + " VOLUME control: exception = " + e);
}
if (volCtrl != null)
  // ...

从线路获取控件

一个需要在其用户界面中公开控件的应用程序可能只需查询可用的线路和控件,然后为感兴趣的每条线路上的每个控件显示适当的用户界面元素。在这种情况下,程序的唯一任务是为用户提供对控件的“处理”,而不是知道这些控件对音频信号做了什么。只要程序知道如何将线路的控件映射到用户界面元素,Java Sound API 的 MixerLineControl 架构通常会处理其余部分。

例如,假设您的程序播放声音。您正在使用一个 SourceDataLine,如之前在 获取所需类型的线路 中描述的那样获取。您可以通过调用以下 Line 方法来访问线路的控件:

Control[] getControls()

然后,对于返回的数组中的每个控件,您可以使用以下 Control 方法来获取控件的类型:

Control.Type getType()

知道特定的Control.Type实例后,您的程序可以显示相应的用户界面元素。当然,为特定的Control.Type选择“相应的用户界面元素”取决于您的程序采取的方法。一方面,您可能会使用相同类型的元素来表示同一类别的所有Control.Type实例。这将需要您使用例如Object.getClass方法查询Control.Type实例的。假设结果匹配了BooleanControl.Type。在这种情况下,您的程序可能会显示一个通用的复选框或切换按钮,但如果其类匹配了FloatControl.Type,那么您可能会显示一个图形滑块。

另一方面,您的程序可能会区分不同类型的控件,甚至是同一类别的控件,并为每个控件使用不同的用户界面元素。这将需要您测试Control's getType方法返回的实例。然后,例如,如果类型匹配了BooleanControl.Type.APPLY_REVERB,您的程序可能会显示一个复选框;而如果类型匹配了BooleanControl.Type.MUTE,您可能会显示一个切换按钮。

使用控件更改音频信号

现在您知道如何访问控件并确定其类型,本节将描述如何使用Controls来更改音频信号的各个方面。本节不涵盖所有可用的控件;相反,它提供了一些示例,以展示如何入门。这些示例包括:

  • 控制线的静音状态

  • 更改线路的音量

  • 在各种混响预设中进行选择

假设您的程序已经访问了所有的混音器、它们的线路以及这些线路上的控件,并且它有一个数据结构来管理控件与其对应的用户界面元素之间的逻辑关联。那么,将用户对这些控件的操作转换为相应的Control方法就变得相当简单。

以下各小节描述了必须调用的一些方法,以影响特定控件的更改。

控制线的静音状态

控制任何线的静音状态只需调用以下BooleanControl方法:

void setValue(boolean value)

(可以假设程序通过引用其控件管理数据结构知道,静音是BooleanControl的一个实例。)为了使通过线路传递的信号静音,程序调用上述方法,指定值为true。要关闭静音,允许信号通过线路流动,程序调用该方法并将参数设置为false

更改线路的音量

假设您的程序将特定的图形滑块与特定线路的音量控制相关联。音量控制的值(即FloatControl.Type.VOLUME)是使用以下FloatControl方法设置的:

void setValue(float newValue)

检测到用户移动滑块后,程序会获取滑块的当前值,并将其作为参数newValue传递给上述方法。这会改变通过拥有该控件的线路流动的信号的音量。

在各种混响预设中进行选择

假设我们的程序有一个带有类型为EnumControl.Type.REVERB的控件的混音器。调用EnumControl方法:

java.lang.Objects[] getValues()

在该控件上会生成一个ReverbType对象数组。如果需要,可以使用以下ReverbType方法访问每个对象的特定参数设置:

int getDecayTime() 
int getEarlyReflectionDelay() 
float getEarlyReflectionIntensity() 
int getLateReflectionDelay() 
float getLateReflectionIntensity() 

例如,如果一个程序只想要一个听起来像洞穴的混响设置,它可以迭代ReverbType对象,直到找到一个getDecayTime返回大于 2000 的值。有关这些方法的详细解释,包括代表性返回值表,请参阅javax.sound.sampled.ReverbType的 API 参考文档。

通常,程序会为getValues方法返回的数组中的每个ReverbType对象创建一个用户界面元素,例如单选按钮。当用户点击其中一个单选按钮时,程序会调用EnumControl方法。

void setValue(java.lang.Object value) 

其中value设置为与新选择的按钮对应的ReverbType。通过拥有此EnumControl的线路发送的音频信号将根据构成控件当前ReverbType(即setValue方法的value参数中指定的特定ReverbType)的参数设置而产生混响效果。

因此,从我们应用程序的角度来看,让用户从一个混响预设(即 ReverbType)移动到另一个只是将getValues返回的数组的每个元素连接到不同的单选按钮。

直接操作音频数据

Control API 允许 Java Sound API 的实现或混音器的第三方提供商通过控件提供任意类型的信号处理。但是如果没有混音器提供所需类型的信号处理怎么办?这需要更多的工作,但您可能可以在程序中实现信号处理。因为 Java Sound API 允许您将音频数据作为字节数组访问,您可以以任何您选择的方式修改这些字节。

如果您正在处理传入的声音,您可以从TargetDataLine中读取字节,然后对其进行操作。一个算法上微不足道但可以产生引人入胜结果的例子是通过将其帧按相反顺序排列来倒放声音。这个微不足道的例子可能对您的程序没有太大用处,但有许多复杂的数字信号处理(DSP)技术可能更合适。一些例子包括均衡、动态范围压缩、峰值限制、时间拉伸或压缩,以及延迟、合唱、混响、失真等特效。

要播放处理过的声音,您可以将处理后的字节数组放入SourceDataLineClip中。当然,字节数组不一定需要来自现有的声音。您可以从头开始合成声音,尽管这需要一些声学知识或者访问声音合成函数。无论是处理还是合成,您可能需要查阅音频 DSP 教科书以获取您感兴趣的算法,或者将第三方信号处理函数库导入到您的程序中。对于合成声音的播放,考虑一下javax.sound.midi包中的Synthesizer API 是否符合您的需求。稍后在合成声音下您将了解更多关于javax.sound.midi的内容。

使用文件和格式转换器

原文:docs.oracle.com/javase/tutorial/sound/converters.html

大多数处理声音的应用程序需要读取声音文件或音频流。这是常见的功能,无论程序随后对读取的数据做什么(如播放、混合或处理)。同样,许多程序需要写入声音文件(或流)。在某些情况下,已读取的数据(或将要写入的数据)需要转换为不同的格式。

正如在访问音频系统资源中简要提到的,Java Sound API 为应用程序开发人员提供了各种设施,用于文件输入/输出和格式转换。应用程序可以读取、写入和在各种声音文件格式和音频数据格式之间进行转换。

采样包概述介绍了与声音文件和音频数据格式相关的主要类。作为回顾:

  • 从文件中读取或写入的音频数据流由AudioInputStream对象表示。(AudioInputStream继承自java.io.InputStream。)

  • 此音频数据的格式由AudioFormat对象表示。

    此格式指定了音频样本本身的排列方式,但不指定它们可能存储在的文件的结构。换句话说,AudioFormat描述了“原始”音频数据,例如系统在从麦克风输入捕获或从声音文件解析后可能传递给程序的数据。AudioFormat包括编码、字节顺序、通道数、采样率和每个样本的位数等信息。

  • 有几种众所周知的标准声音文件格式,如 WAV、AIFF 或 AU。不同类型的声音文件具有不同的存储音频数据以及存储有关音频数据的描述信息的结构。Java Sound API 中通过AudioFileFormat对象表示声音文件格式。AudioFileFormat包括一个AudioFormat对象来描述文件中存储的音频数据的格式,还包括有关文件类型和文件中数据长度的信息。

  • AudioSystem类提供了方法,用于将来自AudioInputStream的音频数据流存储到特定类型的音频文件中(换句话说,写入文件),从音频文件中提取音频字节流(AudioInputStream)(换句话说,读取文件),以及将音频数据从一种数据格式转换为另一种数据格式。本页分为三个部分,解释了这三种活动。


注意:

Java Sound API 的实现不一定提供全面的设施来读取、写入和转换不同数据和文件格式的音频。它可能仅支持最常见的数据和文件格式。然而,服务提供者可以开发和分发扩展这一集合的转换服务,正如你稍后将在提供采样音频服务中看到的那样。AudioSystem类提供了允许应用程序了解可用转换的方法,稍后在转换文件和数据格式下描述。


读取声音文件

AudioSystem类提供了两种类型的文件读取服务:

  • 存储在声音文件中的音频数据格式的信息

  • 可从声音文件中读取的格式化音频数据流

第一个是getAudioFileFormat方法的三个变体:

    static AudioFileFormat getAudioFileFormat (java.io.File file)
    static AudioFileFormat getAudioFileFormat(java.io.InputStream stream)
    static AudioFileFormat getAudioFileFormat (java.net.URL url)

如上所述,返回的AudioFileFormat对象告诉你文件类型、文件中数据的长度、编码、字节顺序、通道数、采样率和每个样本的位数。

第二种文件读取功能由这些AudioSystem方法提供。

    static AudioInputStream getAudioInputStream (java.io.File file)
    static AudioInputStream getAudioInputStream (java.net.URL url)
    static AudioInputStream getAudioInputStream (java.io.InputStream stream)

这些方法给你一个对象(一个AudioInputStream),让你使用AudioInputStream的读取方法读取文件的音频数据。我们马上会看到一个例子。

假设你正在编写一个声音编辑应用程序,允许用户从文件中加载声音数据,显示相应的波形图或频谱图,编辑声音,播放编辑后的数据,并将结果保存在新文件中。或者你的程序将读取文件中存储的数据,应用某种信号处理(例如减慢声音而不改变音调的算法),然后播放处理后的音频。在任何情况下,你需要访问音频文件中包含的数据。假设你的程序提供了一些方式让用户选择或指定输入声音文件,读取该文件的音频数据涉及三个步骤:

  1. 从文件中获取一个AudioInputStream对象。

  2. 创建一个字节数组,用于存储文件中连续的数据块。

  3. 重复从音频输入流中读取字节到数组中。在每次迭代中,对数组中的字节执行一些有用的操作(例如,你可以播放它们、过滤它们、分析它们、显示它们或将它们写入另一个文件)。

以下代码片段概述了这些步骤:

int totalFramesRead = 0;
File fileIn = new File(somePathName);
// somePathName is a pre-existing string whose value was
// based on a user selection.
try {
  AudioInputStream audioInputStream = 
    AudioSystem.getAudioInputStream(fileIn);
  int bytesPerFrame = 
    audioInputStream.getFormat().getFrameSize();
    if (bytesPerFrame == AudioSystem.NOT_SPECIFIED) {
    // some audio formats may have unspecified frame size
    // in that case we may read any amount of bytes
    bytesPerFrame = 1;
  } 
  // Set an arbitrary buffer size of 1024 frames.
  int numBytes = 1024 * bytesPerFrame; 
  byte[] audioBytes = new byte[numBytes];
  try {
    int numBytesRead = 0;
    int numFramesRead = 0;
    // Try to read numBytes bytes from the file.
    while ((numBytesRead = 
      audioInputStream.read(audioBytes)) != -1) {
      // Calculate the number of frames actually read.
      numFramesRead = numBytesRead / bytesPerFrame;
      totalFramesRead += numFramesRead;
      // Here, do something useful with the audio data that's 
      // now in the audioBytes array...
    }
  } catch (Exception ex) { 
    // Handle the error...
  }
} catch (Exception e) {
  // Handle the error...
}

让我们看看上面代码示例中发生了什么。首先,外部的try子句通过调用AudioSystem.getAudioInputStream(File)方法实例化了一个AudioInputStream对象。此方法透明地执行了所有必要的测试,以确定指定的文件实际上是 Java Sound API 支持的声音文件类型。如果正在检查的文件(例如此示例中的fileIn)不是声音文件,或者是某种不受支持的声音文件类型,将抛出UnsupportedAudioFileException异常。这种行为很方便,因为应用程序员不需要测试文件属性,也不需要遵守任何文件命名约定。相反,getAudioInputStream方法负责处理验证输入文件所需的所有底层解析和验证。然后,外部的try子句创建了一个名为audioBytes的字节数组,长度是任意固定的。我们确保其字节长度等于整数帧数,这样我们就不会最终只读取部分帧或更糟糕的是只读取部分样本。这个字节数组将作为一个缓冲区,临时保存从流中读取的一块音频数据。如果我们知道我们将只读取非常短的声音文件,我们可以使此数组与文件中的数据长度相同,通过从AudioInputStreamgetFrameLength方法返回的帧数长度来推导其字节长度。(实际上,我们可能会使用Clip对象。)但为了避免在一般情况下耗尽内存,我们会一次读取一个缓冲区的文件。

内部的try子句包含一个while循环,在这里我们将音频数据从AudioInputStream读入字节数组中。您应该在此循环中添加代码,以适当处理此数组中的音频数据,以满足程序的需求。如果您对数据应用某种信号处理,您可能需要进一步查询AudioInputStreamAudioFormat,以了解每个样本的位数等信息。

注意,方法AudioInputStream.read(byte[])返回读取的字节数,而不是样本数或帧数。当没有更多数据可读取时,此方法返回-1。一旦检测到这种情况,我们就会跳出while循环。

写入声音文件

前一节描述了读取声音文件的基础知识,使用了AudioSystemAudioInputStream类的特定方法。本节描述了如何将音频数据写入新文件。

以下AudioSystem方法创建指定文件类型的磁盘文件。该文件将包含指定AudioInputStream中的音频数据:

static int write(AudioInputStream in, 
  AudioFileFormat.Type fileType, File out)

请注意,第二个参数必须是系统支持的文件类型之一(例如,AU、AIFF 或 WAV),否则write方法将抛出IllegalArgumentException。为了避免这种情况,您可以通过调用此AudioSystem方法来测试特定AudioInputStream是否可以写入特定类型的文件:

static boolean isFileTypeSupported
  (AudioFileFormat.Type fileType, AudioInputStream stream)

只有在支持特定组合时才会返回true

更一般地,您可以通过调用这些AudioSystem方法来了解系统可以写入哪些类型的文件:

static AudioFileFormat.Type[] getAudioFileTypes() 
static AudioFileFormat.Type[] getAudioFileTypes(AudioInputStream stream) 

其中第一个返回系统可以写入的所有文件类型,第二个仅返回系统可以从给定音频输入流写入的文件类型。

以下摘录演示了使用上述提到的write方法从AudioInputStream创建输出文件的一种技术。

File fileOut = new File(someNewPathName);
AudioFileFormat.Type fileType = fileFormat.getType();
if (AudioSystem.isFileTypeSupported(fileType, 
    audioInputStream)) {
  AudioSystem.write(audioInputStream, fileType, fileOut);
}

上面的第一个语句创建了一个新的File对象fileOut,带有用户或程序指定的路径名。第二个语句从名为fileFormat的预先存在的AudioFileFormat对象中获取文件类型,该对象可能已从其他声音文件(例如,在上面的读取声音文件中读取的文件)中获取。 (您可以选择提供您想要的任何支持的文件类型,而不是从其他地方获取文件类型。例如,您可以删除第二个语句,并将上面代码中的其他两个fileType出现替换为AudioFileFormat.Type.WAVE。)

第三个语句测试指定类型的文件是否可以从所需的AudioInputStream写入。与文件格式一样,此流可能是先前读取的声音文件派生而来的。(如果是这样,那么您可能以某种方式处理或更改了其数据,否则有更简单的方法可以简单地复制文件。)或者也许该流包含从麦克风输入中新捕获的字节。

最后,流、文件类型和输出文件被传递给AudioSystem.write方法,以实现写入文件的目标。

转换文件和数据格式

请回想一下什么是格式化音频数据?,Java Sound API 区分音频文件格式和音频数据格式。这两者或多或少是独立的。粗略地说,数据格式指的是计算机表示每个原始数据点(样本)的方式,而文件格式指的是存储在磁盘上的声音文件的组织方式。每种声音文件格式都有一个定义文件头中存储信息的特定结构。在某些情况下,文件格式还包括包含某种形式元数据的结构,除了实际的“原始”音频样本。本页的其余部分将探讨 Java Sound API 的方法,这些方法使得可以进行各种文件格式和数据格式的转换。

从一种文件格式转换为另一种

本节介绍了在 Java Sound API 中转换音频文件类型的基础知识。再次,我们提出一个假设的程序,其目的是从任意输入文件中读取音频数据并将其写入类型为 AIFF 的文件中。当然,输入文件必须是系统能够读取的类型,输出文件必须是系统能够写入的类型。(在此示例中,我们假设系统能够写入 AIFF 文件。)示例程序不执行任何数据格式转换。如果输入文件的数据格式无法表示为 AIFF 文件,则程序简单地通知用户存在问题。另一方面,如果输入音频文件已经是 AIFF 文件,则程序会通知用户无需转换。

以下函数实现了刚才描述的逻辑:

public void ConvertFileToAIFF(String inputPath, 
  String outputPath) {
  AudioFileFormat inFileFormat;
  File inFile;
  File outFile;
  try {
    inFile = new File(inputPath);
    outFile = new File(outputPath);     
  } catch (NullPointerException ex) {
    System.out.println("Error: one of the 
      ConvertFileToAIFF" +" parameters is null!");
    return;
  }
  try {
    // query file type
    inFileFormat = AudioSystem.getAudioFileFormat(inFile);
    if (inFileFormat.getType() != AudioFileFormat.Type.AIFF) 
    {
      // inFile is not AIFF, so let's try to convert it.
      AudioInputStream inFileAIS = 
        AudioSystem.getAudioInputStream(inFile);
      inFileAIS.reset(); // rewind
      if (AudioSystem.isFileTypeSupported(
             AudioFileFormat.Type.AIFF, inFileAIS)) {
         // inFileAIS can be converted to AIFF. 
         // so write the AudioInputStream to the
         // output file.
         AudioSystem.write(inFileAIS,
           AudioFileFormat.Type.AIFF, outFile);
         System.out.println("Successfully made AIFF file, "
           + outFile.getPath() + ", from "
           + inFileFormat.getType() + " file, " +
           inFile.getPath() + ".");
         inFileAIS.close();
         return; // All done now
       } else
         System.out.println("Warning: AIFF conversion of " 
           + inFile.getPath()
           + " is not currently supported by AudioSystem.");
    } else
      System.out.println("Input file " + inFile.getPath() +
          " is AIFF." + " Conversion is unnecessary.");
  } catch (UnsupportedAudioFileException e) {
    System.out.println("Error: " + inFile.getPath()
        + " is not a supported audio file type!");
    return;
  } catch (IOException e) {
    System.out.println("Error: failure attempting to read " 
      + inFile.getPath() + "!");
    return;
  }
}

如前所述,此示例函数ConvertFileToAIFF的目的是查询输入文件,以确定它是否是 AIFF 音频文件,如果不是,则尝试将其转换为 AIFF 格式,生成一个新的副本,其路径名由第二个参数指定。(作为练习,您可以尝试使此函数更通用,以便不总是转换为 AIFF,而是根据新的函数参数指定的文件类型进行转换。)请注意,副本的音频数据格式——即新文件模仿原始输入文件的音频数据格式。

大部分此函数是不言自明的,并且与 Java Sound API 无关。然而,例程中使用了一些 Java Sound API 方法,这些方法对于音频文件类型转换至关重要。这些方法调用都在上面的第二个try子句中找到,包括以下内容:

  • AudioSystem.getAudioFileFormat:在此处用于确定输入文件是否已经是 AIFF 类型。如果是,则函数会快速返回;否则,转换尝试继续进行。

  • AudioSystem.isFileTypeSupported:指示系统是否可以写入包含来自指定AudioInputStream的音频数据的指定类型文件。在我们的示例中,如果指定的音频输入文件可以转换为 AIFF 音频文件格式,则此方法返回true。如果不支持AudioFileFormat.Type.AIFFConvertFileToAIFF会发出警告,指出无法转换输入文件,然后返回。

  • AudioSystem.write:在此处用于将inFileAIS的音频数据从AudioInputStream写入到输出文件outFile中。

这些方法中的第二个isFileTypeSupported方法有助于在写入之前确定特定输入音频文件是否可以转换为特定输出音频文件类型。在下一节中,我们将看到如何通过对ConvertFileToAIFF示例例程进行一些修改,可以转换音频数据格式以及音频文件类型。

在不同数据格式之间转换音频

前一节展示了如何使用 Java Sound API 将文件从一种文件格式(即一种声音文件类型)转换为另一种。本节探讨了一些方法,这些方法使音频数据格式转换成为可能。

在前一节中,我们从一个任意类型的文件中读取数据,并将其保存在一个 AIFF 文件中。请注意,尽管我们改变了用于存储数据的文件类型,但我们并没有改变音频数据本身的格式。(大多数常见的音频文件类型,包括 AIFF,可以包含各种格式的音频数据。)因此,如果原始文件包含 CD 音质音频数据(16 位样本大小、44.1kHz 采样率和两个声道),那么我们的输出 AIFF 文件也会包含相同的数据。

现在假设我们想要指定输出文件的数据格式,以及文件类型。例如,也许我们正在保存许多长文件以供在互联网上使用,并且担心我们的文件所需的磁盘空间和下载时间。我们可能选择创建包含低分辨率数据的较小的 AIFF 文件-例如,具有 8 位样本大小、8kHz 采样率和单声道的数据。

不像之前那样详细地进行编码,让我们探讨一些用于数据格式转换的方法,并考虑我们需要对ConvertFileToAIFF函数进行的修改以实现新目标。

音频数据转换的主要方法再次在AudioSystem类中找到。这个方法是getAudioInputStream的一个变体:

AudioInputStream getAudioInputStream(AudioFormat
    format, AudioInputStream stream)

此函数返回一个AudioInputStream,该流是使用指定的AudioFormat``format转换stream的结果。如果AudioSystem不支持转换,此函数会抛出IllegalArgumentException

为了避免这种情况,我们可以首先通过调用这个AudioSystem方法来检查系统是否可以执行所需的转换:

boolean isConversionSupported(AudioFormat targetFormat,
    AudioFormat sourceFormat)

在这种情况下,我们将stream.getFormat()作为第二个参数传递。

要创建一个特定的AudioFormat对象,我们使用下面显示的两个AudioFormat构造函数之一,要么:

AudioFormat(float sampleRate, int sampleSizeInBits,
    int channels, boolean signed, boolean bigEndian)

使用线性 PCM 编码和给定参数构造一个AudioFormat,或者:

AudioFormat(AudioFormat.Encoding encoding, 
    float sampleRate, int sampleSizeInBits, int channels,
    int frameSize, float frameRate, boolean bigEndian) 

还构造了一个AudioFormat,但允许您指定编码、帧大小和帧速率,除了其他参数。

现在,有了上面的方法,让我们看看如何扩展我们的ConvertFileToAIFF函数以执行所需的“低分辨率”音频数据格式转换。首先,我们将构造一个描述所需输出音频数据格式的AudioFormat对象。以下语句就足够了,并且可以插入到函数的顶部附近:

AudioFormat outDataFormat = new AudioFormat((float) 8000.0,
(int) 8, (int) 1, true, false);

由于上面的AudioFormat构造函数描述了一个具有 8 位样本的格式,因此构造函数的最后一个参数,指定样本是大端还是小端,是无关紧要的。(大端与小端只有在样本大小大于一个字节时才是一个问题。)

以下示例展示了我们如何使用这个新的AudioFormat来转换我们从输入文件创建的AudioInputStreaminFileAIS

AudioInputStream lowResAIS;         
  if (AudioSystem.isConversionSupported(outDataFormat,   
    inFileAIS.getFormat())) {
    lowResAIS = AudioSystem.getAudioInputStream
      (outDataFormat, inFileAIS);
  }

不管我们在何处插入这段代码,只要它在构建inFileAIS之后即可。如果没有isConversionSupported测试,如果请求的特定转换不受支持,调用将失败并抛出IllegalArgumentException。(在这种情况下,控制将转移到我们函数中适当的catch子句。)

因此,在这个过程中,我们将产生一个新的AudioInputStream,这是通过将原始输入文件(以其AudioInputStream形式)转换为由outDataFormat定义的所需低分辨率音频数据格式而产生的。

生成所需的低分辨率 AIFF 声音文件的最后一步是将AudioSystem.write调用中的AudioInputStream参数(即第一个参数)替换为我们转换后的流lowResAIS,如下所示:

AudioSystem.write(lowResAIS, AudioFileFormat.Type.AIFF, 
  outFile);

对我们之前的函数进行这几处修改,可以产生一个能够转换任何指定输入文件的音频数据和文件格式的东西,前提是系统支持转换。

学习可用的转换方式

几个AudioSystem方法会测试它们的参数,以确定系统是否支持特定的数据格式转换或文件写入操作。(通常,每个方法都与另一个方法配对,执行数据转换或写入文件。)其中一个查询方法AudioSystem.isFileTypeSupported在我们的示例函数ConvertFileToAIFF中被使用,以确定系统是否能够将音频数据写入 AIFF 文件。相关的AudioSystem方法getAudioFileTypes(AudioInputStream)返回给定流支持的所有文件类型的完整列表,作为AudioFileFormat.Type实例的数组。该方法:BEGINCODE boolean isConversionSupported(AudioFormat.Encoding encoding,

AudioFormat 格式)

用于确定是否可以从具有指定音频格式的音频输入流中获取具有指定编码的音频输入流。类似地,该方法:

boolean isConversionSupported(AudioFormat newFormat,
                              AudioFormat oldFormat) 

告诉我们是否可以通过将具有音频格式oldFormatAudioInputStream转换为具有指定音频格式newFormatAudioInputStream来获得AudioInputStream。(这个方法在前一节代码片段中被调用,用于创建低分辨率音频输入流lowResAIS。)

这些与格式相关的查询有助于在尝试使用 Java Sound API 执行格式转换时防止错误。

MIDI 包概述

原文:docs.oracle.com/javase/tutorial/sound/overview-MIDI.html

介绍简要介绍了 Java Sound API 的 MIDI 功能。接下来的讨论更详细地介绍了通过javax.sound.midi包访问的 Java Sound API 的 MIDI 架构。解释了一些 MIDI 本身的基本特性,作为一个复习或介绍,以便将 Java Sound API 的 MIDI 功能放入上下文中。然后继续讨论 Java Sound API 对 MIDI 的处理,为后续部分中解释的编程任务做准备。对 MIDI API 的讨论分为两个主要领域:数据和设备。

MIDI 复习:线缆和文件

音乐器数字接口(MIDI)标准定义了一种用于电子音乐设备(如电子键盘乐器和个人计算机)的通信协议。MIDI 数据可以在现场表演期间通过特殊电缆传输,并且还可以存储在标准类型的文件中以供以后播放或编辑。

本节回顾了一些 MIDI 基础知识,不涉及 Java Sound API。这个讨论旨在为熟悉 MIDI 的读者提供一个复习,为不熟悉的读者提供一个简要介绍,为随后讨论 Java Sound API 的 MIDI 包提供背景。如果您对 MIDI 有深入了解,可以安全地跳过本节。在编写大量 MIDI 应用程序之前,不熟悉 MIDI 的程序员可能需要比本教程中包含的更详细的 MIDI 描述。请参阅仅在硬拷贝中提供的完整 MIDI 1.0 详细规范,可从www.midi.org获取(尽管您可能会在网上找到改写或摘要版本)。

MIDI 既是硬件规范,也是软件规范。要理解 MIDI 的设计,了解其历史是有帮助的。MIDI 最初是为了在电子键盘乐器(如合成器)之间传递音乐事件(如按键)而设计的。被称为音序器的硬件设备存储了可以控制合成器的音符序列,允许音乐表演被录制并随后播放。后来,开发了连接 MIDI 乐器与计算机串口的硬件接口,允许音序器在软件中实现。最近,计算机声卡已经整合了用于 MIDI I/O 和合成音乐声音的硬件。如今,许多 MIDI 用户只使用声卡,从不连接外部 MIDI 设备。CPU 已经足够快,以至于合成器也可以在软件中实现。只有在音频 I/O 和在某些应用中与外部 MIDI 设备通信时才需要声卡。

MIDI 规范的简要硬件部分规定了 MIDI 电缆的引脚排列方式以及这些电缆插入的插孔。这部分内容不需要我们关心。因为最初需要硬件的设备,如音序器和合成器,现在可以在软件中实现,也许大多数程序员需要了解 MIDI 硬件设备的唯一原因只是为了理解 MIDI 中的隐喻。然而,外部 MIDI 硬件设备对于一些重要的音乐应用仍然至关重要,因此 Java Sound API 支持 MIDI 数据的输入和输出。

MIDI 规范的软件部分非常广泛。这部分内容涉及 MIDI 数据的结构以及合成器等设备应如何响应该数据。重要的是要理解 MIDI 数据可以流式传输序列化。这种二元性反映了 Complete MIDI 1.0 详细规范的两个不同部分:

  • MIDI 1.0

  • 标准 MIDI 文件

通过检查 MIDI 规范的这两个部分的目的,我们将解释流式传输和序列化的含义。

MIDI 线协议中的流数据

MIDI 规范的这两个部分中的第一个描述了非正式称为“MIDI 线协议”的内容。 MIDI 线协议,即原始 MIDI 协议,基于这样一个假设,即 MIDI 数据正在通过 MIDI 电缆(“线”)发送。电缆将数字数据从一个 MIDI 设备传输到另一个 MIDI 设备。每个 MIDI 设备可能是乐器或类似设备,也可能是配备有 MIDI 功能的声卡或 MIDI 到串口接口的通用计算机。

MIDI 数据,根据 MIDI 线协议定义,被组织成消息。不同类型的消息由消息中的第一个字节区分,称为状态字节。(状态字节是唯一一个最高位设置为 1 的字节。)在消息中跟随状态字节的字节称为数据字节。某些 MIDI 消息,称为通道消息,具有包含四位用于指定通道消息类型和另外四位用于指定通道号的状态字节。因此有 16 个 MIDI 通道;接收 MIDI 消息的设备可以设置为响应所有或仅一个这些虚拟通道上的通道消息。通常,每个 MIDI 通道(不应与音频通道混淆)用于发送不同乐器的音符。例如,两个常见的通道消息是 Note On 和 Note Off,分别启动音符发声并停止它。这两个消息各自需要两个数据字节:第一个指定音符的音高,第二个指定其“速度”(假设键盘乐器正在演奏音符时按下或释放键的速度)。

MIDI 传输协议为 MIDI 数据定义了一个流模型。该协议的一个核心特点是 MIDI 数据的字节是实时传递的,换句话说,它们是流式传输的。数据本身不包含时间信息;每个事件在接收时被处理,并假定它在正确的时间到达。如果音符是由现场音乐家生成的,那么这种模型是可以接受的,但如果您想要存储音符以便以后播放,或者想要在实时之外进行组合,那么这是不够的。当您意识到 MIDI 最初是为音乐表演而设计的,作为键盘音乐家在许多音乐家使用计算机之前控制多个合成器的一种方式时,这种限制是可以理解的。(规范的第一个版本于 1984 年发布。)

标准 MIDI 文件中的序列化数据

MIDI 规范的标准 MIDI 文件部分解决了 MIDI 传输协议中的时间限制。标准 MIDI 文件是一个包含 MIDI 事件 的数字文件。一个事件简单地是一个 MIDI 消息,如 MIDI 传输协议中定义的,但附加了一个指定事件时间的额外信息。(还有一些事件不对应 MIDI 传输协议消息,我们将在下一节中看到。)额外的时间信息是一系列字节,指示何时执行消息描述的操作。换句话说,标准 MIDI 文件不仅指定要播放哪些音符,而且确切地指定何时播放每一个音符。这有点像一个乐谱。

标准 MIDI 文件中的信息被称为序列。标准 MIDI 文件包含一个或多个轨道。每个轨道通常包含一个乐器会演奏的音符,如果音乐由现场音乐家演奏。一个序列器是一个可以读取序列并在正确时间传递其中包含的 MIDI 消息的软件或硬件设备。一个序列器有点像一个管弦乐队指挥:它拥有所有音符的信息,包括它们的时间,然后告诉其他实体何时演奏这些音符。

Java Sound API 对 MIDI 数据的表示

现在我们已经勾勒出 MIDI 规范对流式和序列化音乐数据的处理方式,让我们来看看 Java Sound API 如何表示这些数据。

MIDI 消息

MidiMessage 是表示“原始” MIDI 消息的抽象类。一个“原始” MIDI 消息通常是由 MIDI 传输协议定义的消息。它也可以是标准 MIDI 文件规范中定义的事件之一,但没有事件的时间信息。在 Java Sound API 中,有三类原始 MIDI 消息,分别由这三个相应的 MidiMessage 子类表示:

  • ShortMessages是最常见的消息,状态字节后最多有两个数据字节。通道消息,如 Note On 和 Note Off,都是短消息,还有一些其他消息也是短消息。

  • SysexMessages包含系统专用的 MIDI 消息。它们可能有许多字节,并且通常包含制造商特定的指令。

  • MetaMessages出现在 MIDI 文件中,但不出现在 MIDI 线协议中。元消息包含数据,例如歌词或速度设置,这对于音序器可能很有用,但通常对合成器没有意义。

MIDI 事件

正如我们所见,标准 MIDI 文件包含用于包装“原始”MIDI 消息以及时间信息的事件。Java Sound API 的MidiEvent类的实例代表了一个类似于标准 MIDI 文件中存储的事件。

MidiEvent的 API 包括设置和获取事件的时间值的方法。还有一个方法用于检索其嵌入的原始 MIDI 消息,这是MidiMessage子类的实例,接下来会讨论。(嵌入的原始 MIDI 消息只能在构造MidiEvent时设置。)

序列和轨道

如前所述,标准 MIDI 文件存储被安排到轨道中的事件。通常文件代表一个音乐作品,通常每个轨道代表一个部分,例如可能由单个乐器演奏。乐器演奏的每个音符至少由两个事件表示:开始音符的 Note On 和结束音符的 Note Off。轨道还可能包含不对应音符的事件,例如元事件(如上所述)。

Java Sound API 将 MIDI 数据组织成三部分层次结构:

  • Sequence

  • Track

  • MidiEvent

TrackMidiEvents的集合,SequenceTracks的集合。这种层次结构反映了标准 MIDI 文件规范中的文件、轨道和事件。(注意:这是一种包含和拥有的层次结构;这不是一种继承的类层次结构。这三个类直接继承自java.lang.Object。)

Sequences可以从 MIDI 文件中读取,也可以从头开始创建并通过向Sequence添加Tracks(或删除它们)进行编辑。同样,MidiEvents可以添加到序列中的轨道中,也可以从中删除。

Java Sound API 对 MIDI 设备的表示

前一节解释了 MIDI 消息在 Java Sound API 中的表示方式。然而,MIDI 消息并不是独立存在的。它们通常从一个设备发送到另一个设备。使用 Java Sound API 的程序可以从头开始生成 MIDI 消息,但更常见的情况是这些消息是由软件设备(如序列器)创建的,或者通过 MIDI 输入端口从计算机外部接收。这样的设备通常会将这些消息发送到另一个设备,比如合成器或 MIDI 输出端口。

MidiDevice 接口

在外部 MIDI 硬件设备的世界中,许多设备可以将 MIDI 消息传输到其他设备,并从其他设备接收消息。同样,在 Java Sound API 中,实现MidiDevice接口的软件对象可以传输和接收消息。这样的对象可以纯粹在软件中实现,也可以作为硬件的接口,比如声卡的 MIDI 功能。基本的MidiDevice接口提供了 MIDI 输入或输出端口通常所需的所有功能。然而,合成器和序列器进一步实现了MidiDevice的子接口之一:SynthesizerSequencer

MidiDevice接口包括用于打开和关闭设备的 API。它还包括一个名为MidiDevice.Info的内部类,提供设备的文本描述,包括其名称、供应商和版本。如果您已经阅读了本教程的采样音频部分,那么这个 API 可能会听起来很熟悉,因为其设计类似于javax.sampled.Mixer接口,代表音频设备,并且具有类似的内部类Mixer.Info

发送器和接收器

大多数 MIDI 设备都能够发送MidiMessages、接收它们,或两者兼而有之。设备发送数据的方式是通过它“拥有”的一个或多个发送器对象。同样,设备接收数据的方式是通过一个或多个接收器对象。发送器对象实现了Transmitter接口,而接收器实现了Receiver接口。

每个发送器一次只能连接到一个接收器,反之亦然。一个设备如果同时向多个其他设备发送其 MIDI 消息,则通过拥有多个发送器,每个发送器连接到不同设备的接收器来实现。同样,一个设备如果要同时从多个来源接收 MIDI 消息,则必须通过多个接收器来实现。

序列器

一个序列器是一种捕获和播放 MIDI 事件序列的设备。它具有发射器,因为它通常将存储在序列中的 MIDI 消息发送到另一个设备,例如合成器或 MIDI 输出端口。它还具有接收器,因为它可以捕获 MIDI 消息并将其存储在序列中。在其超接口MidiDevice中,Sequencer添加了用于基本 MIDI 序列操作的方法。序列器可以从 MIDI 文件加载序列,查询和设置序列的速度,并将其他设备与其同步。应用程序可以注册一个对象,以便在序列器处理某些类型的事件时收到通知。

合成器

Synthesizer是一种产生声音的设备。它是javax.sound.midi包中唯一产生音频数据的对象。合成器设备控制一组 MIDI 通道对象 - 通常是 16 个,因为 MIDI 规范要求有 16 个 MIDI 通道。这些 MIDI 通道对象是实现MidiChannel接口的类的实例,其方法代表 MIDI 规范的“通道音频消息”和“通道模式消息”。

应用程序可以通过直接调用合成器的 MIDI 通道对象的方法来生成声音。然而,更常见的情况是,合成器响应发送到其一个或多个接收器的消息而生成声音。例如,这些消息可能是由序列器或 MIDI 输入端口发送的。合成器解析其接收器接收到的每条消息,并通常根据事件中指定的 MIDI 通道号将相应的命令(如noteOncontrolChange)发送到其一个MidiChannel对象。

MidiChannel使用这些消息中的音符信息来合成音乐。例如,noteOn消息指定了音符的音高和“速度”(音量)。然而,音符信息是不够的;合成器还需要关于如何为每个音符创建音频信号的精确指令。这些指令由一个Instrument表示。每个Instrument通常模拟不同的真实乐器或音效。Instruments可能作为合成器的预设提供,也可能从声音库文件中加载。在合成器中,Instruments按照银行号(可以将其视为行)和程序号(列)进行排列。

本节为理解 MIDI 数据提供了背景,并介绍了与 Java Sound API 中的 MIDI 相关的一些重要接口和类。后续章节将展示如何在应用程序中访问和使用这些对象。

访问 MIDI 系统资源

原文:docs.oracle.com/javase/tutorial/sound/accessing-MIDI.html

Java Sound API 为 MIDI 系统配置提供了灵活的模型,就像为采样音频系统配置一样。Java Sound API 的实现本身可以提供不同类型的 MIDI 设备,并且服务提供者和用户可以提供并安装其他设备。您可以编写程序,使其对计算机上安装的具体 MIDI 设备做出少量假设。相反,程序可以利用 MIDI 系统的默认设置,或者允许用户从可用设备中选择。

本节展示了您的程序如何了解已安装的 MIDI 资源,以及如何访问所需的资源。在访问并打开设备之后,您可以将它们连接在一起,如后面的 传输和接收 MIDI 消息 中所讨论的。

MidiSystem 类

Java Sound API 的 MIDI 包中的 MidiSystem 类的作用与采样音频包中的 AudioSystem 类的作用直接类似。MidiSystem 充当访问已安装 MIDI 资源的中转站。

您可以查询MidiSystem以了解安装了什么类型的设备,然后可以遍历可用设备并访问所需设备。例如,一个应用程序可能首先询问MidiSystem有哪些合成器可用,然后显示一个列表,用户可以从中选择一个。一个更简单的应用程序可能只使用系统的默认合成器。

MidiSystem 类还提供了在 MIDI 文件和Sequences之间进行转换的方法。它可以报告 MIDI 文件的文件格式,并且可以写入不同类型的文件。

应用程序可以从MidiSystem获取以下资源:

  • 顺序器

  • 合成器

  • 发射器(例如与 MIDI 输入端口相关联的发射器)

  • 接收器(例如与 MIDI 输出端口相关联的接收器)

  • 来自标准 MIDI 文件的数据

  • 来自声音库文件的数据

本页重点介绍了这些类型资源中的前四种。其他类型将在本教程的后面讨论。

获取默认设备

使用 Java Sound API 的典型 MIDI 应用程序程序首先获取所需的设备,这些设备可以包括一个或多个顺序器、合成器、输入端口或输出端口。

有一个默认的合成器设备,一个默认的定序器设备,一个默认的传输设备和一个默认的接收设备。后两个设备通常代表系统上可用的 MIDI 输入和输出端口,如果有的话。(在这里很容易混淆方向性。将端口的传输或接收视为与软件相关,而不是与连接到物理端口的任何外部物理设备相关。MIDI 输入端口传输来自外部设备的数据到 Java Sound API 的Receiver,同样,MIDI 输出端口接收来自软件对象的数据并将数据中继到外部设备。)

一个简单的应用程序可能只使用默认设备而不探索所有安装的设备。MidiSystem类包括以下方法来检索默认资源:

static Sequencer getSequencer()
static Synthesizer getSynthesizer()
static Receiver getReceiver()
static Transmitter getTransmitter()

前两种方法获取系统的默认排序和合成资源,这些资源可以代表物理设备或完全在软件中实现。getReceiver方法获取一个Receiver对象,该对象接收发送给它的 MIDI 消息并将其中继到默认接收设备。类似地,getTransmitter方法获取一个 Transmitter 对象,该对象可以代表默认传输设备向某个接收设备发送 MIDI 消息。

学习安装了哪些设备

与使用默认设备不同,更彻底的方法是从系统上安装的所有设备中选择所需的设备。应用程序可以通过编程方式选择所需的设备,或者可以显示可用设备列表,让用户选择要使用的设备。MidiSystem类提供了一个方法来了解安装了哪些设备,以及一个相应的方法来获取给定类型的设备。

以下是学习已安装设备的方法:

 static MidiDevice.Info[] getMidiDeviceInfo()

如您所见,它返回一个信息对象数组。每个返回的MidiDevice.Info对象标识已安装的一种类型的定序器、合成器、端口或其他设备。(通常系统最多只有一个给定类型的实例。例如,来自某个供应商的特定型号的合成器只会安装一次。)MidiDevice.Info包括以下字符串来描述设备:

  • 名称

  • 版本号

  • 厂商(创建设备的公司)

  • 设备的描述

您可以在用户界面中显示这些字符串,让用户从设备列表中进行选择。

然而,要在程序中使用字符串来选择设备(而不是向用户显示字符串),你需要事先知道可能的字符串是什么。每个提供设备的公司应该在其文档中包含这些信息。需要或偏好特定设备的应用程序可以利用这些信息来定位该设备。这种方法的缺点是将程序限制在事先知道的设备实现上。

另一种更一般的方法是继续遍历MidiDevice.Info对象,获取每个相应的设备,并以编程方式确定是否适合使用(或至少适合包含在用户可以选择的列表中)。下一节将介绍如何执行此操作。

获取所需设备

一旦找到适当设备的信息对象,应用程序调用以下MidiSystem方法来获取相应的设备本身:

static MidiDevice getMidiDevice(MidiDevice.Info info)

如果您已经找到描述所需设备的信息对象,可以使用此方法。但是,如果无法解释getMidiDeviceInfo返回的信息对象以确定所需设备,且不想向用户显示所有设备的信息,您可以尝试以下操作:遍历getMidiDeviceInfo返回的所有MidiDevice.Info对象,使用上述方法获取相应设备,并测试每个设备以查看其是否合适。换句话说,您可以在将设备包含在向用户显示的列表中之前,查询每个设备的类和功能,或者以编程方式决定设备而不涉及用户。例如,如果您的程序需要合成器,可以获取每个已安装的设备,查看哪些是实现Synthesizer接口的类的实例,然后将它们显示在用户可以选择的列表中,如下所示:

// Obtain information about all the installed synthesizers.
Vector synthInfos;
MidiDevice device;
MidiDevice.Info[] infos = MidiSystem.getMidiDeviceInfo();
for (int i = 0; i < infos.length; i++) {
    try {
        device = MidiSystem.getMidiDevice(infos[i]);
    } catch (MidiUnavailableException e) {
          // Handle or throw exception...
    }
    if (device instanceof Synthesizer) {
        synthInfos.add(infos[i]);
    }
}
// Now, display strings from synthInfos list in GUI.    

作为另一个示例,您可以以编程方式选择设备,而不涉及用户。假设您想获取可以同时播放最多音符的合成器。您遍历所有MidiDevice.Info对象,如上所述,但在确定设备是合成器后,通过调用SynthesizergetMaxPolyphony方法查询其功能。您保留具有最大音色的合成器,如下一节所述。即使您不要求用户选择合成器,您可能仍然显示所选MidiDevice.Info对象的字符串,仅供用户参考。

打开设备

前一节展示了如何获取已安装的设备。然而,设备可能已安装但不可用。例如,另一个应用程序可能独占使用它。要为您的程序实际保留设备,您需要使用MidiDevice方法open

if (!(device.isOpen())) {
    try {
      device.open();
  } catch (MidiUnavailableException e) {
          // Handle or throw exception...
  }
}

一旦您访问了设备并通过打开它来预留了它,您可能希望将其连接到一个或多个其他设备,以便让 MIDI 数据在它们之间流动。这个过程在传输和接收 MIDI 消息中有描述。

当完成对设备的操作后,通过调用MidiDeviceclose方法释放它,以便其他程序可以使用。

传输和接收 MIDI 消息

原文:docs.oracle.com/javase/tutorial/sound/MIDI-messages.html

理解设备、发射器和接收器

Java Sound API 为 MIDI 数据指定了一种灵活且易于使用的消息路由架构,一旦理解其工作原理,就会变得灵活且易于使用。该系统基于模块连接设计:不同的模块,每个模块执行特定任务,可以相互连接(组网),使数据能够从一个模块流向另一个模块。

Java Sound API 的消息系统中的基本模块是MidiDevice接口。MidiDevices包括序列器(记录、播放、加载和编辑时间戳 MIDI 消息序列)、合成器(触发 MIDI 消息时生成声音)以及 MIDI 输入和输出端口,通过这些端口数据来自外部 MIDI 设备并传输到外部 MIDI 设备。通常所需的 MIDI 端口功能由基本的MidiDevice接口描述。SequencerSynthesizer接口扩展了MidiDevice接口,分别描述了 MIDI 序列器和合成器的附加功能。作为序列器或合成器的具体类应实现这些接口。

一个MidiDevice通常拥有一个或多个实现ReceiverTransmitter接口的辅助对象。这些接口代表连接设备的“插头”或“门户”,允许数据流入和流出。通过将一个MidiDeviceTransmitter连接到另一个MidiDeviceReceiver,可以创建一个模块网络,其中数据从一个模块流向另一个模块。

MidiDevice接口包括用于确定设备可以同时支持多少个发射器和接收器对象的方法,以及用于访问这些对象的其他方法。MIDI 输出端口通常至少有一个Receiver,通过该接收器可以接收传出消息;同样,合成器通常会响应发送到其ReceiverReceivers的消息。MIDI 输入端口通常至少有一个Transmitter,用于传播传入消息。功能齐全的序列器支持在录制过程中接收消息的Receivers和在播放过程中发送消息的Transmitters

Transmitter接口包括用于设置和查询发射器发送其MidiMessages的接收器的方法。设置接收器建立了两者之间的连接。Receiver接口包含一个将MidiMessage发送到接收器的方法。通常,此方法由Transmitter调用。TransmitterReceiver接口都包括一个close方法,用于释放先前连接的发射器或接收器,使其可用于不同的连接。

现在我们将讨论如何使用发射器和接收器。在涉及连接两个设备的典型情况之前(例如将一个音序器连接到合成器),我们将研究一个更简单的情况,即直接从应用程序向设备发送 MIDI 消息。研究这种简单的情况应该会更容易理解 Java Sound API 如何安排在两个设备之间发送 MIDI 消息。

发送消息到接收器而不使用发射器

假设你想从头开始创建一个 MIDI 消息,然后将其发送到某个接收器。你可以创建一个新的空白ShortMessage,然后使用以下ShortMessage方法填充它的 MIDI 数据:

void setMessage(int command, int channel, int data1,
         int data2) 

一旦您准备好发送消息,您可以使用这个Receiver方法将其发送到一个Receiver对象:

void send(MidiMessage message, long timeStamp)

时间戳参数将在稍后解释。现在,我们只会提到,如果您不关心指定精确时间,则其值可以设置为-1。在这种情况下,接收消息的设备将尽快响应消息。

应用程序可以通过调用设备的getReceiver方法来获取MidiDevice的接收器。如果设备无法向程序提供接收器(通常是因为设备的所有接收器已经在使用中),则会抛出MidiUnavailableException。否则,此方法返回的接收器可立即供程序使用。当程序使用完接收器后,应调用接收器的close方法。如果程序在调用close后尝试在接收器上调用方法,则可能会抛出IllegalStateException

作为一个发送消息而不使用发射器的具体简单示例,让我们向默认接收器发送一个 Note On 消息,通常与设备(如 MIDI 输出端口或合成器)相关联。我们通过创建一个合适的ShortMessage并将其作为参数传递给Receiversend方法来实现这一点:

  ShortMessage myMsg = new ShortMessage();
  // Start playing the note Middle C (60), 
  // moderately loud (velocity = 93).
  myMsg.setMessage(ShortMessage.NOTE_ON, 0, 60, 93);
  long timeStamp = -1;
  Receiver       rcvr = MidiSystem.getReceiver();
  rcvr.send(myMsg, timeStamp);

此代码使用ShortMessage的静态整数字段,即NOTE_ON,用作 MIDI 消息的状态字节。 MIDI 消息的其他部分作为参数给出了setMessage方法的显式数值。零表示音符将使用 MIDI 通道号 1 播放;60 表示中央 C 音符;93 是一个任意的按键按下速度值,通常表示最终播放音符的合成器应该以相当大的音量播放它。(MIDI 规范将速度的确切解释留给合成器对其当前乐器的实现。)然后将此 MIDI 消息以时间戳 -1 发送到接收器。现在我们需要仔细研究时间戳参数的确切含义,这是下一节的主题。

理解时间戳

正如您已经知道的,MIDI 规范有不同的部分。一部分描述了 MIDI "线"协议(实时设备之间发送的消息),另一部分描述了标准 MIDI 文件(作为"序列"中事件存储的消息)。在规范的后半部分,存储在标准 MIDI 文件中的每个事件都标记有指示何时播放该事件的时间值。相比之下,MIDI 传输协议中的消息总是应立即处理,一旦被设备接收,因此它们没有附带的时间值。

Java Sound API 添加了一个额外的变化。毫不奇怪,MidiEvent 对象中存在时间值,这些对象存储在序列中(可能是从 MIDI 文件中读取的),就像标准 MIDI 文件规范中一样。但是在 Java Sound API 中,甚至在设备之间发送的消息——换句话说,对应 MIDI 传输协议的消息——也可以被赋予时间值,称为时间戳。我们关心的是这些时间戳。

发送到设备的消息上的时间戳

Java Sound API 中在设备之间发送的消息上可选附带的时间戳与标准 MIDI 文件中的时间值有很大不同。 MIDI 文件中的时间值通常基于音乐概念,如拍子和速度,每个事件的时间度量了自上一个事件以来经过的时间。相比之下,发送到设备的Receiver对象的消息上的时间戳始终以微秒为单位测量绝对时间。具体来说,它测量了自拥有接收器的设备打开以来经过的微秒数。

这种时间戳旨在帮助补偿操作系统或应用程序引入的延迟。重要的是要意识到,这些时间戳用于对时间进行微小调整,而不是实现可以在完全任意时间安排事件的复杂队列(就像MidiEvent的时间值那样)。

发送到设备(通过Receiver)的消息上的时间戳可以为设备提供精确的时间信息。设备在处理消息时可能会使用这些信息。例如,它可能通过几毫秒来调整事件的时间以匹配时间戳中的信息。另一方面,并非所有设备都支持时间戳,因此设备可能会完全忽略消息的时间戳。

即使设备支持时间戳,也可能不会按照您请求的时间安排事件。您不能期望发送一个时间戳在未来很远处的消息,并让设备按照您的意图处理它,当然也不能期望设备正确安排一个时间戳在过去的消息!设备决定如何处理时间戳偏离太远或在过去的消息。发送方不知道设备认为什么是太远,或者设备是否对时间戳有任何问题。这种无知模仿了外部 MIDI 硬件设备的行为,它们发送消息而从不知道是否被正确接收。(MIDI 线协议是单向的。)

一些设备通过Transmitter发送带有时间戳的消息。例如,MIDI 输入端口发送的消息可能会标记上消息到达端口的时间。在某些系统中,事件处理机制会导致在对消息进行后续处理过程中丢失一定量的时间精度。消息的时间戳允许保留原始的时间信息。

要了解设备是否支持时间戳,请调用MidiDevice的以下方法:

    long getMicrosecondPosition()

如果设备忽略时间戳,则此方法返回-1。否则,它将返回设备当前的时间概念,您作为发送方可以在确定随后发送的消息的时间戳时使用它作为偏移量。例如,如果您想发送一个时间戳为未来五毫秒的消息,您可以获取设备当前的微秒位置,加上 5000 微秒,并将其用作时间戳。请记住,MidiDevice对时间的概念总是将时间零放在设备打开时的时间。

现在,有了时间戳的背景解释,让我们回到Receiversend方法:

void send(MidiMessage message, long timeStamp)

timeStamp 参数以微秒为单位表示,根据接收设备对时间的概念。如果设备不支持时间戳,它会简单地忽略 timeStamp 参数。您不需要为发送给接收器的消息加时间戳。您可以使用 -1 作为 timeStamp 参数,表示您不关心调整确切的时间;您只是让接收设备尽快处理消息。然而,不建议在发送给同一接收器的某些消息中使用 -1,而在其他消息中使用显式时间戳。这样做可能会导致结果时间的不规则性。

连接发射器到接收器

我们已经看到您可以直接向接收器发送 MIDI 消息,而不使用发射器。现在让我们看看更常见的情况,即您不是从头开始创建 MIDI 消息,而是简单地连接设备在一起,以便其中一个可以向另一个发送 MIDI 消息。

连接到单个设备

我们将以连接序列器到合成器作为我们的第一个示例。一旦建立了这种连接,启动序列器将导致合成器从序列器当前序列中的事件生成音频。现在,我们将忽略将序列从 MIDI 文件加载到序列器中的过程。此外,我们不会涉及播放序列的机制。加载和播放序列在播放、录制和编辑 MIDI 序列中有详细讨论。加载乐器到合成器在合成声音中有讨论。现在,我们只关心如何连接序列器和合成器。这将作为连接一个设备的发射器到另一个设备的接收器的更一般过程的示例。

为简单起见,我们将使用默认的序列器和默认的合成器。

    Sequencer           seq;
    Transmitter         seqTrans;
    Synthesizer         synth;
    Receiver         synthRcvr;
    try {
          seq     = MidiSystem.getSequencer();
          seqTrans = seq.getTransmitter();
          synth   = MidiSystem.getSynthesizer();
          synthRcvr = synth.getReceiver(); 
          seqTrans.setReceiver(synthRcvr);      
    } catch (MidiUnavailableException e) {
          // handle or throw exception
    }

一个实现实际上可能有一个既是默认序列器又是默认合成器的单个对象。换句话说,实现可能使用一个同时实现 Sequencer 接口和 Synthesizer 接口的类。在这种情况下,可能不需要像上面的代码中所做的显式连接。但出于可移植性考虑,最好不要假设这样的配置。如果需要,当然可以测试这种情况:

if (seq instanceof Synthesizer)

尽管上面的显式连接应该在任何情况下都能工作。

连接到多个设备

前面的代码示例说明了发射器和接收器之间的一对一连接。但是,如果您需要将相同的 MIDI 消息发送到多个接收器怎么办?例如,假设您希望从外部设备捕获 MIDI 数据以驱动内部合成器,同时将数据录制到序列中。这种连接形式有时被称为“分流”或“分配器”,很简单。以下语句显示了如何创建一个分流连接,通过该连接,到达 MIDI 输入端口的 MIDI 消息被发送到一个Synthesizer对象和一个Sequencer对象。我们假设您已经获取并打开了三个设备:输入端口、序列器和合成器。(要获取输入端口,您需要遍历MidiSystem.getMidiDeviceInfo返回的所有项目。)

    Synthesizer  synth;
    Sequencer    seq;
    MidiDevice   inputPort;
    // [obtain and open the three devices...]
    Transmitter   inPortTrans1, inPortTrans2;
    Receiver            synthRcvr;
    Receiver            seqRcvr;
    try {
          inPortTrans1 = inputPort.getTransmitter();
          synthRcvr = synth.getReceiver(); 
          inPortTrans1.setReceiver(synthRcvr);
          inPortTrans2 = inputPort.getTransmitter();
          seqRcvr = seq.getReceiver(); 
          inPortTrans2.setReceiver(seqRcvr);
    } catch (MidiUnavailableException e) {
          // handle or throw exception
    }

此代码介绍了MidiDevice.getTransmitter方法的双重调用,将结果分配给inPortTrans1inPortTrans2。如前所述,设备可以拥有多个发射器和接收器。每次为给定设备调用MidiDevice.getTransmitter()时,都会返回另一个发射器,直到没有更多可用为止,此时将抛出异常。

要了解设备支持多少个发射器和接收器,您可以使用以下MidiDevice方法:

    int getMaxTransmitters()
    int getMaxReceivers()

这些方法返回设备拥有的总数,而不是当前可用的数量。

发射器一次只能向一个接收器传输 MIDI 消息。(每次调用TransmittersetReceiver方法时,如果存在现有的Receiver,则会被新指定的替换。您可以通过调用Transmitter.getReceiver来判断发射器当前是否有接收器。)但是,如果设备有多个发射器,它可以同时向多个设备发送数据,通过将每个发射器连接到不同的接收器,就像我们在上面的输入端口的情况中看到的那样。

同样,设备可以使用其多个接收器同时从多个设备接收。所需的多接收器代码很简单,直接类似于上面的多发射器代码。一个单一接收器也可以同时从多个发射器接收消息。

关闭连接

一旦完成连接,您可以通过调用每个已获取的发射器和接收器的close方法来释放其资源。TransmitterReceiver接口各自都有一个close方法。请注意,调用Transmitter.setReceiver不会关闭发射器当前的接收器。接收器保持打开状态,仍然可以接收来自任何连接到它的其他发射器的消息。

如果您也完成了设备的使用,可以通过调用MidiDevice.close()将它们提供给其他应用程序。关闭设备会自动关闭其所有发射器和接收器。

介绍 Sequencers

原文:docs.oracle.com/javase/tutorial/sound/MIDI-seq-intro.html

在 MIDI 世界中,sequencer是任何能精确播放或记录一系列时间戳 MIDI 消息的硬件或软件设备。同样,在 Java Sound API 中,Sequencer抽象接口定义了可以播放和记录MidiEvent对象序列的对象的属性。Sequencer通常从标准 MIDI 文件加载这些MidiEvent序列或将它们保存到这样的文件中。序列也可以进行编辑。以下页面解释了如何使用Sequencer对象以及相关的类和接口来完成这些任务。

要对Sequencer是什么有直观的理解,可以将其类比为磁带录音机,在许多方面Sequencer与磁带录音机相似。磁带录音机播放音频,而Sequencer播放 MIDI 数据。一个序列是多轨、线性、按时间顺序记录的 MIDI 音乐数据,Sequencer可以以不同速度播放,倒带,定位到特定点,录制或复制到文件进行存储。

传输和接收 MIDI 消息解释了设备通常具有Receiver对象、Transmitter对象或两者。为了播放音乐,设备通常通过Receiver接收MidiMessages,而Receiver通常是从属于SequencerTransmitter接收这些消息。拥有这个Receiver的设备可能是一个Synthesizer,它将直接生成音频,或者可能是一个 MIDI 输出端口,通过物理电缆将 MIDI 数据传输到外部设备。类似地,为了录制音乐,一系列带有时间戳的MidiMessages通常被发送到Sequencer拥有的Receiver中,然后将它们放入Sequence对象中。通常发送消息的对象是与硬件输入端口相关联的Transmitter,端口通过物理电缆中继从外部设备获取的 MIDI 数据。然而,负责发送消息的设备可能是其他Sequencer,或者任何具有Transmitter的设备。此外,如前所述,程序可以在完全不使用任何Transmitter的情况下发送消息。

一个Sequencer本身既有Receivers又有Transmitters。在录制时,它实际上通过其Receivers获取MidiMessages。在播放时,它使用其Transmitters发送存储在其记录的Sequence中的MidiMessages(或从文件中加载)。

在 Java Sound API 中,将Sequencer的角色视为MidiMessages的聚合器和“解聚器”的一种方式。一系列独立的MidiMessages被发送到Sequencer,每个MidiMessages都有自己的时间戳,标记了音乐事件的时间。这些MidiMessages被封装在MidiEvent对象中,并通过Sequencer.record方法在Sequence对象中收集。Sequence是一个包含MidiEvents聚合的数据结构,通常代表一系列音符,通常是整首歌曲或作品。在播放时,Sequencer再次从Sequence中的MidiEvent对象中提取MidiMessages,然后将它们传输到一个或多个设备,这些设备将把它们渲染成声音,保存它们,修改它们,或将它们传递给其他设备。

一些音序器可能既没有发射器也没有接收器。例如,它们可能会根据键盘或鼠标事件从头开始创建MidiEvents,而不是通过Receivers接收MidiMessages。同样,它们可能通过直接与内部合成器(实际上可能是与音序器相同的对象)通信来演奏音乐,而不是将MidiMessages发送到与另一个对象关联的Receiver

何时使用音序器

应用程序可以直接向设备发送 MIDI 消息,而不使用音序器,就像在传输和接收 MIDI 消息中描述的那样。程序只需在想要发送消息时调用Receiver.send方法。这是一种直接的方法,当程序本身实时创建消息时非常有用。例如,考虑一个程序,让用户通过点击屏幕上的钢琴键盘来演奏音符。当程序接收到鼠标按下事件时,立即向合成器发送适当的 Note On 消息。

如前所述,程序可以在发送到设备的接收器的每个 MIDI 消息中包含时间戳。然而,这些时间戳仅用于微调时间,以纠正处理延迟。调用者通常不能设置任意时间戳;传递给Receiver.send的时间值必须接近当前时间,否则接收设备可能无法正确安排消息。这意味着,如果应用程序想要提前为整首音乐创建一个 MIDI 消息队列(而不是对实时事件做出响应时创建每个消息),它必须非常小心地安排每次调用Receiver.send几乎正确的时间。

幸运的是,大多数应用程序不必担心这种调度问题。程序可以使用Sequencer对象来管理 MIDI 消息队列,而不是自己调用Receiver.sendSequencer负责调度和发送消息,换句话说,以正确的时间播放音乐。通常,在需要将非实时 MIDI 消息序列转换为实时序列(如播放)或反之(如录制)时,使用Sequencer是有利的。Sequencer最常用于播放来自 MIDI 文件的数据和从 MIDI 输入端口录制数据。

理解序列数据

在查看SequencerAPI 之前,了解存储在序列中的数据类型是有帮助的。

序列和轨道

在 Java Sound API 中,Sequencer在组织记录的 MIDI 数据方面紧密遵循标准 MIDI 文件规范。如上所述,Sequence是按时间组织的MidiEvents的聚合。但Sequence比仅仅是线性MidiEvents序列更具结构:Sequence实际上包含全局时间信息以及一组Tracks,而Tracks本身保存MidiEvent数据。因此,由Sequencer播放的数据由三级对象层次结构组成:SequencerTrackMidiEvent

在这些对象的常规使用中,Sequence代表完整的音乐作品或作品的一部分,每个Track对应于合奏中的一个声音或演奏者。在这个模型中,特定Track上的所有数据也因此被编码到为该声音或演奏者保留的特定 MIDI 通道中。

这种数据组织方式便于编辑序列,但请注意,这只是一种使用Tracks的常规方式。Track类的定义中没有任何限制它只能包含不同 MIDI 通道上的MidiEvents的内容。例如,整个多通道 MIDI 作品可以混合录制到一个Track上。此外,Type 0 标准 MIDI 文件(与 Type 1 和 Type 2 相对)根据定义只包含一个轨道;因此,从这样的文件中读取的Sequence必然只有一个Track对象。

MidiEvents 和 Ticks

如 Java Sound API 概述中所讨论的,Java Sound API 包括与组成大多数标准 MIDI 消息的原始两个或三字节序列对应的MidiMessage对象。MidiEvent只是一个MidiMessage的打包,以及指定事件发生时间的伴随时间值。(我们可能会说序列实际上包含四或五级数据层次,而不是三级,因为表面上最低级别的MidiEvent实际上包含一个更低级别的MidiMessage,同样MidiMessage对象包含一个组成标准 MIDI 消息的字节数组。)

在 Java Sound API 中,MidiMessages可以与定时值关联的另一种方式有两种。 其中一种是上面提到的“何时使用 Sequencer”。 这种技术在不使用 Transmitter 向接收器发送消息和理解时间戳下有详细描述。 在那里,我们看到Receiversend方法接受一个MidiMessage参数和一个时间戳参数。 那种时间戳只能用微秒表示。

MidiMessage可以指定其定时的另一种方式是通过封装在MidiEvent中。 在这种情况下,定时以稍微更抽象的单位称为ticks来表示。

一个 tick 的持续时间是多少? 它可以在序列之间变化(但不会在序列内部变化),其值存储在标准 MIDI 文件的头部中。 一个 tick 的大小以两种类型的单位给出:

  • 每四分音符的脉冲(ticks),缩写为 PPQ

  • 每帧的 ticks,也称为 SMPTE 时间码(由电影和电视工程师协会采用的标准)

如果单位是 PPQ,一个 tick 的大小被表示为四分音符的一部分,这是一个相对的,而不是绝对的时间值。 四分音符是一个音乐持续时间值,通常对应于音乐中的一个节拍(4/4 拍子中的四分之一)。 四分音符的持续时间取决于速度,如果序列包含速度变化事件,则音乐中的四分音符的持续时间可能会在音乐过程中变化。 因此,如果序列的定时增量(ticks)发生,比如每四分音符发生 96 次,那么每个事件的定时值都以音乐术语来衡量该事件的位置,而不是绝对时间值。

另一方面,在 SMPTE 的情况下,单位度量绝对时间,而速度的概念不适用。 实际上有四种不同的 SMPTE 约定可用,它们指的是每秒的电影帧数。 每秒的帧数可以是 24、25、29.97 或 30。 使用 SMPTE 时间码,一个 tick 的大小被表示为帧的一部分。

在 Java Sound API 中,您可以调用Sequence.getDivisionType来了解特定序列中使用的单位类型,即 PPQ 或 SMPTE 单位之一。 然后在调用Sequence.getResolution之后可以计算一个 tick 的大小。 如果分割类型是 PPQ,则后一种方法返回每四分音符的 ticks 数,或者如果分割类型是 SMPTE 约定之一,则返回每个 SMPTE 帧的 ticks 数。 在 PPQ 的情况下,可以使用以下公式来获取一个 tick 的大小:

ticksPerSecond =  
    resolution * (currentTempoInBeatsPerMinute / 60.0);
tickSize = 1.0 / ticksPerSecond;

以及在 SMPTE 的情况下的这个公式:

framesPerSecond = 
  (divisionType == Sequence.SMPTE_24 ? 24
    : (divisionType == Sequence.SMPTE_25 ? 25
      : (divisionType == Sequence.SMPTE_30 ? 30
        : (divisionType == Sequence.SMPTE_30DROP ?

            29.97))));
ticksPerSecond = resolution * framesPerSecond;
tickSize = 1.0 / ticksPerSecond;

Java Sound API 中对序列中时间的定义与标准 MIDI 文件规范相似。然而,有一个重要的区别。MidiEvents中包含的 tick 值衡量的是累积时间,而不是增量时间。在标准 MIDI 文件中,每个事件的时间信息衡量的是自上一个事件开始以来经过的时间。这被称为增量时间。但在 Java Sound API 中,ticks 不是增量值;它们是前一个事件的时间值加上增量值。换句话说,在 Java Sound API 中,每个事件的时间值始终大于序列中前一个事件的时间值(或者相等,如果事件应该同时发生)。每个事件的时间值衡量的是自序列开始以来经过的时间。

总结一下,Java Sound API 以 MIDI ticks 或微秒表示时间信息。MidiEvents以 MIDI ticks 形式存储时间信息。一个 tick 的持续时间可以从Sequence的全局时间信息以及(如果序列使用基于速度的时间)当前的音乐速度计算出来。另一方面,发送给ReceiverMidiMessage的时间戳总是以微秒表示。

这种设计的一个目标是避免时间概念的冲突。Sequencer的工作是解释其MidiEvents中的时间单位,这些单位可能是 PPQ 单位,并将其转换为以微秒为单位的绝对时间,考虑到当前的速度。序列器还必须相对于接收消息的设备打开时的时间表达微秒。请注意,一个序列器可以有多个发射器,每个发射器将消息传递给一个可能与完全不同设备关联的不同接收器。因此,您可以看到,序列器必须能够同时执行多个转换,确保每个设备接收适合其时间概念的时间戳。

更复杂的是,不同设备可能基于不同来源(如操作系统的时钟或声卡维护的时钟)更新其时间概念。这意味着它们的时间可能相对于序列器的时间会有偏移。为了与序列器保持同步,一些设备允许自己成为序列器时间概念的“从属”。设置主从关系将在稍后的MidiEvent中讨论。

使用Sequencer方法

原文:docs.oracle.com/javase/tutorial/sound/MIDI-seq-methods.html

Sequencer接口提供了几个类别的方法:

  • 从 MIDI 文件或Sequence对象加载序列数据,并将当前加载的序列数据保存到 MIDI 文件。

  • 类似于磁带录音机的传输功能的方法,用于停止和开始播放和录制,启用和禁用特定轨道上的录制,并在Sequence中快进/快退当前播放或录制位置。

  • 高级方法用于查询和设置对象的同步和定时参数。Sequencer可以以不同的速度播放,一些Tracks静音,并且与其他对象处于各种同步状态。

  • 高级方法用于注册“监听器”对象,当Sequencer处理某些类型的 MIDI 事件时通知它们。

无论您将调用哪些Sequencer方法,第一步都是从系统获取Sequencer设备并为程序使用保留它。

获取一个Sequencer

应用程序不会实例化Sequencer;毕竟,Sequencer只是一个接口。相反,像 Java Sound API 的 MIDI 包中的所有设备一样,Sequencer通过静态的MidiSystem对象访问。如前面在访问 MIDI 系统资源中提到的,可以使用以下MidiSystem方法获取默认的Sequencer

static Sequencer getSequencer()

以下代码片段获取默认的Sequencer,获取其所需的任何系统资源,并使其可操作:

Sequencer sequencer;
// Get default sequencer.
sequencer = MidiSystem.getSequencer(); 
if (sequencer == null) {
    // Error -- sequencer device is not supported.
    // Inform user and return...
} else {
    // Acquire resources and make operational.
    sequencer.open();
}

调用open保留了Sequencer设备供程序使用。想象共享一个Sequencer并没有太多意义,因为它一次只能播放一个序列。当使用完Sequencer后,可以通过调用close使其可供其他程序使用。

可以按照访问 MIDI 系统资源中描述的方式获取非默认的Sequencer

加载一个序列

从系统获取并保留了一个Sequencer后,您需要加载Sequencer应该播放的数据。有三种典型的方法可以实现这一点:

  • 从 MIDI 文件中读取序列数据

  • 通过从另一个设备(如 MIDI 输入端口)接收 MIDI 消息实时录制

  • 通过向空序列添加轨道和向这些轨道添加MidiEvent对象来以编程方式构建它

现在我们将看一下获取序列数据的这种方式中的第一种。 (其他两种方式分别在录制和保存序列和编辑序列下描述。)这种方式实际上包括两种略有不同的方法。一种方法是将 MIDI 文件数据提供给InputStream,然后通过Sequencer.setSequence(InputStream)直接将其读取到sequencer中。使用此方法,您不需要显式创建Sequence对象。实际上,Sequencer实现甚至可能不会在幕后创建Sequence,因为一些sequencers具有处理直接从文件中处理数据的内置机制。

另一种方法是显式创建Sequence。如果要在播放之前编辑序列数据,则需要使用此方法。使用此方法,您调用MidiSystem的重载方法getSequence。该方法能够从InputStreamFileURL获取序列。该方法返回一个Sequence对象,然后可以将其加载到Sequencer中进行播放。在上面的代码摘录中,这是一个从File获取Sequence对象并将其加载到我们的sequencer的示例:

try {
    File myMidiFile = new File("seq1.mid");
    // Construct a Sequence object, and
    // load it into my sequencer.
    Sequence mySeq = MidiSystem.getSequence(myMidiFile);
    sequencer.setSequence(mySeq);
} catch (Exception e) {
   // Handle error and/or return
}

MidiSystemgetSequence方法一样,setSequence可能会抛出InvalidMidiDataException,在InputStream变体的情况下,还可能会抛出IOException,如果遇到任何问题。

播放一个序列

使用以下方法可以启动和停止Sequencer

    void start()

    void stop()

Sequencer.start方法开始播放序列。请注意,播放从序列中的当前位置开始。使用上面描述的setSequence方法加载现有序列会将sequencer的当前位置初始化为序列的开头。stop方法停止sequencer,但不会自动倒带当前Sequence。在不重置位置的情况下启动已停止的Sequence只是从当前位置恢复播放序列。在这种情况下,stop方法充当了暂停操作。但是,在开始播放之前,有各种Sequencer方法可将当前序列位置设置为任意值。(我们将在下面讨论这些方法。)

正如前面提到的,Sequencer通常具有一个或多个Transmitter对象,通过这些对象向Receiver发送MidiMessages。通过这些TransmittersSequencer播放Sequence,通过发出与当前Sequence中包含的MidiEvents相对应的适时MidiMessages。因此,播放Sequence的设置过程的一部分是在SequencerTransmitter对象上调用setReceiver方法,实际上将其输出连接到将使用播放数据的设备。有关TransmittersReceivers的更多详细信息,请参考传输和接收 MIDI 消息。

录制和保存序列

要将 MIDI 数据捕获到Sequence,然后保存到文件,需要执行一些除上述描述之外的额外步骤。以下概述显示了设置录制到Sequence中的Track所需的步骤:

  1. 使用MidiSystem.getSequencer获取一个新的用于录制的序列器,如上所述。

  2. 设置 MIDI 连接的“连线”。传输要录制的 MIDI 数据的对象应通过其setReceiver方法配置,以将数据发送到与录制Sequencer相关联的Receiver

  3. 创建一个新的Sequence对象,用于存储录制的数据。创建Sequence对象时,必须为序列指定全局时间信息。例如:

          Sequence mySeq;
          try{
              mySeq = new Sequence(Sequence.PPQ, 10);
          } catch (Exception ex) { 
              ex.printStackTrace(); 
          }
    
    

    Sequence的构造函数接受divisionType和时间分辨率作为参数。divisionType参数指定分辨率参数的单位。在这种情况下,我们指定正在创建的Sequence的时间分辨率为每四分音符 10 脉冲。Sequence构造函数的另一个可选参数是轨道数参数,这将导致初始序列以指定数量的(最初为空)Tracks开始。否则,Sequence将创建为没有初始Tracks;它们可以根据需要随后添加。

  4. Sequence中创建一个空的Track,使用Sequence.createTrack。如果Sequence是使用初始Tracks创建的,则此步骤是不必要的。

  5. 使用Sequencer.setSequence,选择我们的新Sequence来接收录制。setSequence方法将现有的SequenceSequencer绑定,这在某种程度上类似于将磁带加载到磁带录音机上。

  6. 对于每个要录制的Track,调用Sequencer.recordEnable。如果需要,在Sequence中通过调用Sequence.getTracks获取可用的Tracks的引用。

  7. Sequencer上调用startRecording

  8. 完成录制后,调用Sequencer.stopSequencer.stopRecording

  9. 使用MidiSystem.write将录制的Sequence保存到 MIDI 文件中。MidiSystemwrite方法将Sequence作为其参数之一,并将该Sequence写入流或文件。

编辑序列

许多应用程序允许通过从文件加载来创建序列,并且有相当多的应用程序也允许通过从实时 MIDI 输入(即录制)捕获来创建序列。然而,一些程序将需要从头开始创建 MIDI 序列,无论是以编程方式还是响应用户输入。功能齐全的序列程序允许用户手动构建新序列,以及编辑现有序列。

在 Java Sound API 中,这些数据编辑操作不是通过Sequencer方法实现的,而是通过数据对象本身的方法实现:SequenceTrackMidiEvent。你可以使用Sequence构造函数之一创建一个空序列,然后通过调用以下Sequence方法向其添加轨道:

    Track createTrack() 

如果你的程序允许用户编辑序列,你将需要这个Sequence方法来移除轨道:

    boolean deleteTrack(Track track) 

一旦序列包含轨道,你可以通过调用Track类的方法来修改轨道的内容。Track中包含的MidiEventsjava.util.Vector的形式存储在Track对象中,而Track提供了一组方法来访问、添加和移除列表中的事件。addremove方法相当直观,用于向Track中添加或移除指定的MidiEvent。提供了一个get方法,它接受一个索引,返回存储在那里的MidiEvent。此外,还有sizetick方法,分别返回轨道中的MidiEvents数量和轨道的持续时间,以总Ticks数表示。

在将事件添加到轨道之前创建新事件时,当然会使用MidiEvent构造函数。要指定或修改嵌入在事件中的 MIDI 消息,可以调用适当的MidiMessage子类(ShortMessageSysexMessageMetaMessage)的setMessage方法。要修改事件应发生的时间,调用MidiEvent.setTick

综合起来,这些低级方法为完整功能的音序器程序所需的编辑功能提供了基础。

使用高级音序器功能

译文:docs.oracle.com/javase/tutorial/sound/MIDI-seq-adv.html

到目前为止,我们专注于简单的 MIDI 数据播放和录制。本节将简要描述通过Sequencer接口和Sequence类的方法可用的一些更高级功能。

移动到序列中的任意位置

有两个Sequencer方法可以获取序列中音序器的当前位置。其中的第一个:

long getTickPosition()

返回从序列开始测量的 MIDI 滴答位置。第二种方法:

long getMicrosecondPosition()

返回当前位置的微秒数。此方法假定序列以存储在 MIDI 文件或Sequence中的默认速率播放。如果您按照下面描述的方式更改了播放速度,则它不会返回不同的值。

您也可以根据一个单位或另一个单位设置音序器的当前位置:

void setTickPosition(long tick)

void setMicrosecondPosition(long microsecond)

更改播放速度

如前所述,序列的速度由其速度确定,可以在序列的过程中变化。序列可以包含封装标准 MIDI 速度变化消息的事件。当音序器处理此类事件时,它会根据指定的速度更改播放速度。此外,您可以通过调用任何这些Sequencer方法来以编程方式更改速度:

    public void setTempoInBPM(float bpm)
    public void setTempoInMPQ(float mpq)
    public void setTempoFactor(float factor)

这些方法中的前两种分别设置每分钟的节拍数或每个四分音符的微秒数的速度。速度将保持在指定值,直到再次调用其中一种方法,或者在序列中遇到速度变化事件,此时当前速度将被新指定的速度覆盖。

第三种方法,setTempoFactor,性质不同。它会按比例缩放音序器设置的任何速度(无论是通过速度变化事件还是通过上述前两种方法之一)。默认比例为 1.0(无变化)。尽管此方法会导致播放或录制速度比标称速度快或慢(除非因子为 1.0),但不会改变标称速度。换句话说,getTempoInBPMgetTempoInMPQ返回的速度值不受速度因子影响,尽管速度因子会影响实际播放或录制速率。此外,如果速度通过速度变化事件或前两种方法之一更改,它仍会按上次设置的速度因子进行缩放。但是,如果加载新序列,则速度因子将重置为 1.0。

请注意,当序列的分割类型为 SMPTE 类型之一而不是 PPQ 时,所有这些速度变化指令都无效。

静音或独奏序列中的单独轨道

对于音序器的用户来说,通常可以方便地关闭某些轨道,以更清楚地听到音乐中发生的情况。一个功能齐全的音序器程序允许用户在播放过程中选择哪些轨道应该发声。(更准确地说,由于音序器实际上不会自己发声,用户选择哪些轨道将为音序器产生的 MIDI 消息流做出贡献。)通常,每个轨道上都有两种类型的图形控件:一个静音按钮和一个独奏按钮。如果激活了静音按钮,那个轨道在任何情况下都不会发声,直到静音按钮被停用。独奏是一个不太为人知的功能。它大致相当于静音的反义词。如果任何轨道上的独奏按钮被激活,只有那些独奏按钮被激活的轨道才会发声。这个功能让用户可以快速试听少量轨道,而无需将所有其他轨道静音。静音按钮通常优先于独奏按钮:如果两者都被激活,那个轨道不会发声。

使用Sequencer方法,静音或独奏轨道(以及查询轨道当前的静音或独奏状态)是很容易实现的。假设我们已经获得了默认的Sequencer并且已经将序列数据加载到其中。要静音序列中的第五个轨道,可以按照以下步骤进行:

    sequencer.setTrackMute(4, true);
    boolean muted = sequencer.getTrackMute(4);
    if (!muted) { 
        return;         // muting failed
    }

有几点需要注意上面的代码片段。首先,序列的轨道编号从 0 开始,以总轨道数减 1 结束。此外,setTrackMute的第二个参数是一个布尔值。如果为 true,则请求是将轨道静音;否则请求是取消静音指定的轨道。最后,为了测试静音是否生效,我们调用Sequencer getTrackMute方法,将要查询的轨道号传递给它。如果它返回true,正如我们在这种情况下所期望的那样,那么静音请求成功。如果返回false,则请求失败。

静音请求可能因各种原因而失败。例如,setTrackMute调用中指定的轨道号可能超过了总轨道数,或者音序器可能不支持静音。通过调用getTrackMute,我们可以确定我们的请求是成功还是失败。

顺便说一句,getTrackMute返回的布尔值确实可以告诉我们是否发生了失败,但它无法告诉我们失败的原因。我们可以测试看看失败是否是由于将无效的轨道号传递给setTrackMute方法引起的。为此,我们可以调用SequencegetTracks方法,该方法返回一个包含序列中所有轨道的数组。如果在setTrackMute调用中指定的轨道号超过了此数组的长度,则我们知道我们指定了一个无效的轨道号。

如果静音请求成功,那么在我们的示例中,当序列播放时第五个轨道将不会发声,当前静音的任何其他轨道也不会发声。

独奏轨道的方法和技巧与静音非常相似。要独奏一个轨道,调用SequencesetTrackSolo方法:

void setTrackSolo(int track, boolean bSolo)

setTrackMute相同,第一个参数指定基于零的轨道编号,第二个参数如果为true,则指定该轨道应处于独奏模式;否则该轨道不应处于独奏状态。

默认情况下,轨道既不静音也不独奏。

与其他 MIDI 设备同步

Sequencer有一个名为Sequencer.SyncMode的内部类。SyncMode对象代表 MIDI 序列器的时间概念如何与主设备或从设备同步的一种方式。如果序列器正在与主设备同步,序列器会根据来自主设备的某些 MIDI 消息调整其当前时间。如果序列器有一个从设备,序列器同样会发送 MIDI 消息来控制从设备的定时。

有三种预定义模式指定了序列器可能的主设备:INTERNAL_CLOCKMIDI_SYNCMIDI_TIME_CODE。后两种模式在序列器接收来自另一设备的 MIDI 消息时起作用。在这两种模式下,序列器的时间会根据系统实时定时时钟消息或 MIDI 时间码(MTC)消息进行重置。 (有关这些消息类型的更多信息,请参阅 MIDI 规范。)这两种模式也可以用作从模式,此时序列器会向其接收器发送相应类型的 MIDI 消息。第四种模式NO_SYNC用于指示序列器不应向其接收器发送定时信息。

通过调用带有支持的SyncMode对象作为参数的setMasterSyncMode方法,您可以指定序列器的定时如何受控。同样,setSlaveSyncMode方法确定序列器将向其接收器发送哪些定时信息。这些信息控制使用序列器作为主定时源的设备的定时。

指定特殊事件监听器

序列的每个轨道可以包含许多不同类型的MidiEvents。这些事件包括音符开和音符关消息、程序更改、控制更改和元事件。Java Sound API 为最后两种事件类型(控制更改事件和元事件)指定了“监听器”接口。您可以使用这些接口在播放序列时接收这些事件发生时的通知。

支持ControllerEventListener接口的对象可以在Sequencer处理特定控制变化消息时接收通知。控制变化消息是一种标准的 MIDI 消息类型,代表了 MIDI 控制器值的变化,比如音高弯曲轮或数据滑块。 (请参阅 MIDI 规范获取控制变化消息的完整列表。)当在序列播放过程中处理这样的消息时,消息指示任何设备(可能是合成器)从序列器接收数据以更新某些参数的值。该参数通常控制声音合成的某些方面,比如如果控制器是音高弯曲轮,则控制当前发声音符的音高。当录制序列时,控制变化消息意味着在创建消息的外部物理设备上移动了一个控制器,或者在软件中模拟了这样的移动。

这里是ControllerEventListener接口的使用方法。假设你已经开发了一个实现ControllerEventListener接口的类,意味着你的类包含以下方法:

    void controlChange(ShortMessage msg)

假设你已经创建了一个类的实例,并将其赋给一个名为myListener的变量。如果你在程序的某个地方包含以下语句:

    int[] controllersOfInterest = { 1, 2, 4 };
    sequencer.addControllerEventListener(myListener,
        controllersOfInterest);

那么你的类的controlChange方法将在Sequencer处理 MIDI 控制器编号为 1、2 或 4 的控制变化消息时被调用。换句话说,当Sequencer处理设置任何已注册控制器的值的请求时,Sequencer将调用你的类的controlChange方法。(请注意,将 MIDI 控制器编号分配给特定控制设备的详细信息在 MIDI 1.0 规范中有详细说明。)

controlChange方法接收一个包含受影响的控制器编号和控制器设置的新值的ShortMessage。你可以使用ShortMessage.getData1方法获取控制器编号,并使用ShortMessage.getData2方法获取控制器值的新设置。

另一种特殊事件监听器的类型由MetaEventListener接口定义。根据标准 MIDI 文件 1.0 规范,元消息是不在 MIDI 线协议中存在但可以嵌入到 MIDI 文件中的消息。它们对合成器没有意义,但可以被序列器解释。元消息包括指令(如变速命令)、歌词或其他文本以及其他指示(如音轨结束)。

MetaEventListener机制类似于ControllerEventListener。在任何需要在Sequencer处理MetaMessage时收到通知的类中实现MetaEventListener接口。这涉及向类中添加以下方法:

void meta(MetaMessage msg)

通过将此类的实例作为参数传递给Sequencer addMetaEventListener方法来注册该类的实例:

boolean b = sequencer.addMetaEventListener
        (myMetaListener);

这与ControllerEventListener接口所采取的方法略有不同,因为您必须注册以接收所有MetaMessages,而不仅仅是感兴趣的部分。如果顺序器在其序列中遇到MetaMessage,它将调用myMetaListener.meta,并将遇到的MetaMessage传递给它。meta方法可以调用其MetaMessage参数上的getType,以获取一个从 0 到 127 的整数,该整数表示消息类型,如标准 MIDI 文件 1.0 规范所定义。

合成声音

原文:docs.oracle.com/javase/tutorial/sound/MIDI-synth.html

大多数使用 Java Sound API 的 MIDI 包的程序都是用来合成声音。之前讨论过的整个 MIDI 文件、事件、序列和序列器的装置几乎总是最终将音乐数据发送到合成器以转换为音频。(可能的例外包括将 MIDI 转换为音乐符号的程序,可以被音乐家阅读,以及向外部 MIDI 控制设备发送消息的程序,如混音台。)

因此,Synthesizer接口对于 MIDI 包至关重要。本页展示了如何操作合成器播放声音。许多程序将简单地使用序列器将 MIDI 文件数据发送到合成器,并且不需要直接调用许多Synthesizer方法。然而,也可以直接控制合成器,而不使用序列器甚至MidiMessage对象,如本页末尾所述。

对于不熟悉 MIDI 的读者来说,合成架构可能看起来很复杂。其 API 包括三个接口:

  • 合成器

  • Midi 通道

  • 声音库

和四个类:

  • 乐器

  • 补丁

  • 声音库资源

  • VoiceStatus

作为对所有这些 API 的定位,下一节解释了一些 MIDI 合成的基础知识以及它们如何在 Java Sound API 中反映。随后的部分将更详细地查看 API。

理解 MIDI 合成

合成器是如何产生声音的?根据其实现方式,它可能使用一种或多种声音合成技术。例如,许多合成器使用波表合成。波表合成器从内存中读取存储的音频片段,以不同的采样率播放它们,并循环播放它们以创建不同音高和持续时间的音符。例如,要合成萨克斯风演奏 C#4 音符(MIDI 音符号 61)的声音,合成器可能会访问从萨克斯风演奏中音符中央 C(MIDI 音符号 60)的录音中提取的一个非常短的片段,然后以比录制时略快的采样率不断循环播放这个片段,从而创建一个略高音高的长音符。其他合成器使用诸如频率调制(FM)、加法合成或物理建模等技术,这些技术不使用存储的音频,而是使用不同的算法从头开始生成音频。

乐器

所有合成技术共同之处在于能够创造许多种声音。不同的算法,或者同一算法内不同参数的设置,会产生不同的声音结果。一个乐器是合成某种类型声音的规范。该声音可能模拟传统乐器,如钢琴或小提琴;也可能模拟其他类型的声源,例如电话或直升机;或者根本不模拟任何“现实世界”的声音。一个名为通用 MIDI 的规范定义了一个标准的 128 种乐器列表,但大多数合成器也允许使用其他乐器。许多合成器提供一系列内置乐器,始终可供使用;一些合成器还支持加载额外乐器的机制。

一个乐器可能是特定供应商的——换句话说,仅适用于一个合成器或同一供应商的几个型号。当两个不同的合成器使用不同的声音合成技术,或者即使基本技术相同,但使用不同的内部算法和参数时,就会出现不兼容性。由于合成技术的细节通常是专有的,因此不兼容性是常见的。Java Sound API 包括检测给定合成器是否支持给定乐器的方法。

一个乐器通常可以被视为一个预设;你不必了解产生其声音的合成技术的细节。然而,你仍然可以改变其声音的各个方面。每个 Note On 消息指定一个单独音符的音高和音量。你还可以通过其他 MIDI 命令如控制器消息或系统专用消息来改变声音。

通道

许多合成器是多音轨(有时称为多音色),意味着它们可以同时演奏不同乐器的音符。 (音色是使听众能够区分一种乐器与其他乐器的特征音质。) 多音轨合成器可以模拟整个真实乐器的合奏,而不仅仅是一次一个乐器。 MIDI 合成器通常通过利用 MIDI 规范允许数据传输的不同 MIDI 通道来实现此功能。在这种情况下,合成器实际上是一组发声单元,每个单元模拟不同的乐器,并独立响应在不同 MIDI 通道上接收到的消息。由于 MIDI 规范仅提供 16 个通道,典型的 MIDI 合成器可以同时演奏多达 16 种不同的乐器。合成器接收一系列 MIDI 命令,其中许多是通道命令。 (通道命令针对特定的 MIDI 通道;有关更多信息,请参阅 MIDI 规范。) 如果合成器是多音轨的,它会根据命令中指示的通道号将每个通道命令路由到正确的发声单元。

在 Java Sound API 中,这些发声单元是实现MidiChannel接口的类的实例。一个synthesizer对象至少有一个MidiChannel对象。如果合成器是多音轨的,通常有多个,通常是 16 个。每个MidiChannel代表一个独立的发声单元。

因为合成器的MidiChannel对象更多或更少是独立的,将乐器分配给通道不必是唯一的。例如,所有 16 个通道都可以演奏钢琴音色,就好像有一个由 16 台钢琴组成的合奏团。任何分组都是可能的—例如,通道 1、5 和 8 可以演奏吉他声音,而通道 2 和 3 演奏打击乐,通道 12 有低音音色。在给定的 MIDI 通道上演奏的乐器可以动态更改;这被称为程序更改

尽管大多数合成器一次只能激活 16 个或更少的乐器,但这些乐器通常可以从更大的选择中选择,并根据需要分配到特定的通道。

声音库和音色

在合成器中,乐器按照银行号和程序号进行层次化组织。银行和程序可以被视为乐器的二维表中的行和列。一个银行是一个程序的集合。 MIDI 规范允许一个银行中最多有 128 个程序,最多有 128 个银行。然而,特定的合成器可能仅支持一个银行,或几个银行,并且可能支持每个银行少于 128 个程序。

在 Java Sound API 中,层次结构中有一个更高级别的部分:声音库。声音库可以包含多达 128 个银行,每个银行包含多达 128 个乐器。一些合成器可以将整个声音库加载到内存中。

要从当前声音库中选择一个乐器,您需要指定一个银行号和一个程序号。MIDI 规范通过两个 MIDI 命令实现了这一点:银行选择和程序更改。在 Java Sound API 中,银行号和程序号的组合封装在一个Patch对象中。通过指定一个新的 patch,您可以更改 MIDI 通道的当前乐器。该 patch 可以被视为当前声音库中乐器的二维索引。

您可能想知道声音库是否也是按数字索引的。答案是否定的;MIDI 规范没有提供这一点。在 Java Sound API 中,可以通过读取声音库文件来获取Soundbank对象。如果合成器支持声音库,它的乐器可以根据需要单独加载到合成器中,或者一次性全部加载。许多合成器都有一个内置或默认的声音库;该声音库中包含的乐器始终对合成器可用。

声音

区分合成器可以同时播放的音色数量和音符数量是很重要的。前者在“通道”下面已经描述过。同时播放多个音符的能力被称为复音。即使一个合成器不是多音色的,通常也可以同时播放多个音符(所有音符具有相同的音色,但不同的音高)。例如,播放任何和弦,比如 G 大三和弦或 B 小七和弦,都需要复音。任何实时生成声音的合成器都有一个限制,即它可以同时合成的音符数量。在 Java Sound API 中,合成器通过getMaxPolyphony方法报告这个限制。

声音是一系列单音符,比如一个人可以唱的旋律。复音包括多个声音,比如合唱团唱的部分。例如,一个 32 声音的合成器可以同时播放 32 个音符。(然而,一些 MIDI 文献使用“声音”一词的含义不同,类似于“乐器”或“音色”的含义。)

将传入的 MIDI 音符分配给特定声音的过程称为声音分配。合成器维护一个声音列表,跟踪哪些声音是活动的(意味着它们当前有音符在响)。当一个音符停止响时,声音变为非活动状态,意味着它现在可以接受合成器接收到的下一个音符请求。一个传入的 MIDI 命令流很容易请求比合成器能够生成的更多同时音符。当所有合成器的声音都是活动的时,下一个 Note On 请求应该如何处理?合成器可以实现不同的策略:最近请求的音符可以被忽略;或者通过停止另一个音符,比如最近启动的音符,来播放它。

尽管 MIDI 规范并不要求这样做,合成器可以公开每个声音的内容。Java Sound API 包括一个VoiceStatus类来实现这一目的。

一个VoiceStatus报告了声音当前的活动或非活动状态,MIDI 通道,银行和程序号,MIDI 音符号,以及 MIDI 音量。

有了这个背景,让我们来看一下 Java Sound API 合成的具体细节。

管理乐器和音色库

在许多情况下,一个程序可以使用Synthesizer对象而几乎不需要显式调用任何合成 API。例如,假设你正在播放一个标准的 MIDI 文件。你将其加载到一个Sequence对象中,通过让一个序列器将数据发送到默认的合成器来播放。序列中的数据按照预期控制合成器,按时播放所有正确的音符。

然而,有些情况下这种简单的情景是不够的。序列包含正确的音乐,但乐器听起来全错了!这种不幸的情况可能是因为 MIDI 文件的创建者心目中的乐器与当前加载到合成器中的乐器不同。

MIDI 1.0 规范提供了银行选择和程序更改命令,这些命令影响每个 MIDI 通道上当前播放的乐器。然而,该规范并未定义每个补丁位置(银行和程序号)应该放置什么乐器。较新的 General MIDI 规范通过定义一个包含 128 个与特定乐器声音对应的程序的银行来解决这个问题。General MIDI 合成器使用 128 个与指定集合匹配的乐器。即使播放应该是相同乐器的不同 General MIDI 合成器听起来可能会有很大不同。然而,一个 MIDI 文件在大多数情况下应该听起来相似(即使不完全相同),无论哪个 General MIDI 合成器在播放它。

尽管如此,并非所有的 MIDI 文件创建者都希望受限于 General MIDI 定义的 128 种音色。本节展示如何更改合成器附带的默认乐器集合。(如果没有默认设置,意味着在访问合成器时没有加载任何乐器,那么无论如何你都必须使用这个 API 开始。)

了解加载的乐器

要了解当前加载到合成器中的乐器是否符合你的要求,可以调用这个Synthesizer方法:

Instrument[] getLoadedInstruments() 

并遍历返回的数组,查看当前加载的确切乐器。很可能,你会在用户界面中显示乐器的名称(使用InstrumentgetName方法),让用户决定是否使用这些乐器或加载其他乐器。Instrument API 包括一个报告乐器属于哪个声音库的方法。声音库的名称可能帮助你的程序或用户确定乐器的确切信息。

这个Synthesizer方法:

Soundbank getDefaultSoundbank() 

给出默认的声音库。Soundbank API 包括检索声音库名称、供应商和版本号的方法,通过这些信息,程序或用户可以验证库的身份。然而,当你第一次获得一个合成器时,不能假设默认声音库中的乐器已经被加载到合成器中。例如,一个合成器可能有大量内置乐器可供使用,但由于其有限的内存,它可能不会自动加载它们。

加载不同的乐器

用户可能决定加载与当前乐器不同的乐器(或者你可能以编程方式做出这个决定)。以下方法告诉你合成器附带哪些乐器(而不必从声音库文件加载):

Instrument[] getAvailableInstruments()

你可以通过调用以下方法加载任何这些乐器:

boolean loadInstrument(Instrument instrument) 

乐器被加载到合成器中,位置由乐器的Patch对象指定(可以使用InstrumentgetPatch方法检索)。

要从其他声音库加载乐器,首先调用SynthesizerisSupportedSoundbank方法,确保声音库与此合成器兼容(如果不兼容,可以遍历系统的合成器尝试找到支持声音库的合成器)。然后可以调用这些方法之一从声音库加载乐器:

boolean loadAllInstruments(Soundbank soundbank) 
boolean loadInstruments(Soundbank soundbank, 
  Patch[] patchList) 

正如名称所示,第一个加载给定声音库中的所有乐器,第二个加载声音库中选择的乐器。你也可以使用SoundbankgetInstruments方法访问所有乐器,然后遍历它们,并使用loadInstrument逐个加载选择的乐器。

不需要加载的所有乐器来自同一个声音库。您可以使用loadInstrumentloadInstruments从一个声音库加载某些乐器,从另一个声音库加载另一组乐器,依此类推。

每个乐器都有自己的Patch对象,指定了乐器应加载到合成器的位置。该位置由银行号和程序号定义。没有 API 可以通过更改补丁的银行或程序号来更改位置。

然而,可以使用Synthesizer的以下方法将乐器加载到除其补丁指定位置之外的位置:

boolean remapInstrument(Instrument from, Instrument to) 

此方法从合成器中卸载其第一个参数,并将其第二个参数放置在第一个参数占用的合成器补丁位置。

卸载乐器

将乐器加载到程序位置会自动卸载该位置已经加载的任何乐器,如果有的话。您还可以显式卸载乐器,而不一定要用新的替换它们。Synthesizer包括三个与三个加载方法对应的卸载方法。如果合成器接收到选择当前未加载任何乐器的程序位置的程序更改消息,则在发送程序更改消息的 MIDI 通道上不会有任何声音。

访问声音库资源

一些合成器在其声音库中存储除乐器之外的其他信息。例如,波表合成器存储一个或多个乐器可以访问的音频样本。因为样本可能被多个乐器共享,它们独立于任何乐器存储在声音库中。Soundbank接口和Instrument类都提供一个名为getSoundbankResources的方法调用,返回一个SoundbankResource对象列表。这些对象的细节特定于为其设计声音库的合成器。在波表合成的情况下,资源可能是一个封装自音频录音片段的一系列音频样本的对象。使用其他合成技术的合成器可能在合成器的SoundbankResources数组中存储其他类型的对象。

查询合成器的功能和当前状态

Synthesizer接口包括返回有关合成器功能的信息的方法:

    public long getLatency()
    public int getMaxPolyphony()

延迟度量了传递 MIDI 消息到合成器并合成器实际产生相应结果之间的最坏情况延迟。例如,合成器在接收到音符开启事件后可能需要几毫秒才开始生成音频。

getMaxPolyphony 方法指示合成器可以同时发出多少音符,如前面在 Voices 下讨论的那样。如同在同一讨论中提到的,合成器可以提供关于其音色的信息。这是通过以下方法实现的:

public VoiceStatus[] getVoiceStatus()

返回的数组中的每个 VoiceStatus 报告了音色的当前活动或非活动状态、MIDI 通道、银行和程序号、MIDI 音符号码和 MIDI 音量。数组的长度通常应该与 getMaxPolyphony 返回的相同数量一样。如果合成器没有播放,所有其 VoiceStatus 对象的 active 字段都设置为 false

您可以通过检索其 MidiChannel 对象并查询其状态来了解有关合成器当前状态的其他信息。这将在下一节中更详细地讨论。

使用通道

有时访问合成器的 MidiChannel 对象直接是有用或必要的。本节讨论了这种情况。

在不使用序列器的情况下控制合成器

当使用序列器时,比如从 MIDI 文件中读取的序列,您不需要自己向合成器发送 MIDI 命令。相反,您只需将序列加载到序列器中,将序列器连接到合成器,并让其运行。序列器负责安排事件的时间表,结果是可预测的音乐表现。当所需音乐事先已知时,这种情况是有效的,这在从文件中读取时是正确的。

然而,在某些情况下,音乐是在播放时即时生成的。例如,用户界面可能会显示一个音乐键盘或吉他指板,并允许用户通过鼠标点击随意弹奏音符。另一个例子,一个应用程序可能使用合成器不是为了演奏音乐本身,而是为了根据用户的操作生成音效。这种情况在游戏中很典型。最后一个例子,应用程序可能确实正在播放从文件中读取的音乐,但用户界面允许用户与音乐互动,动态地改变它。在所有这些情况下,应用程序直接向合成器发送命令,因为 MIDI 消息需要立即传递,而不是被安排在将来的某个确定时间点。

有至少两种方法可以将 MIDI 消息发送到合成器而不使用序列器。第一种方法是构造一个 MidiMessage 并通过 Receiver 的 send 方法将其传递给合成器。例如,要在 MIDI 通道 5(从 1 开始计数)上立即产生中央 C(MIDI 音符号码 60),可以执行以下操作:

    ShortMessage myMsg = new ShortMessage();
    // Play the note Middle C (60) moderately loud
    // (velocity = 93)on channel 4 (zero-based).
    myMsg.setMessage(ShortMessage.NOTE_ON, 4, 60, 93); 
    Synthesizer synth = MidiSystem.getSynthesizer();
    Receiver synthRcvr = synth.getReceiver();
    synthRcvr.send(myMsg, -1); // -1 means no time stamp

第二种方法是完全绕过消息传递层(即 MidiMessageReceiver API),直接与合成器的 MidiChannel 对象交互。您首先需要检索合成器的 MidiChannel 对象,使用以下 Synthesizer 方法:

public MidiChannel[] getChannels()

之后,您可以直接调用所需的MidiChannel方法。这比将相应的MidiMessages发送到合成器的Receiver并让合成器处理与其自己的MidiChannels的通信更直接。例如,前面示例对应的代码将是:

    Synthesizer synth = MidiSystem.getSynthesizer();
    MidiChannel chan[] = synth.getChannels(); 
    // Check for null; maybe not all 16 channels exist.
    if (chan[4] != null) {
         chan[4].noteOn(60, 93); 
    }

获取通道的当前状态

MidiChannel接口提供了与 MIDI 规范中定义的每个“通道音频”或“通道模式”消息一一对应的方法。我们在前面的示例中看到了使用 noteOn 方法的情况。但是,除了这些经典方法之外,Java Sound API 的MidiChannel接口还添加了一些“get”方法,用于检索最近由相应的音频或模式“set”方法设置的值:

    int       getChannelPressure()
    int       getController(int controller)
    boolean   getMono()
    boolean   getOmni() 
    int       getPitchBend() 
    int       getPolyPressure(int noteNumber)
    int       getProgram()

这些方法可能对向用户显示通道状态或决定随后发送给通道的值很有用。

静音和独奏通道

Java Sound API 添加了每个通道独奏和静音的概念,这不是 MIDI 规范所要求的。这类似于 MIDI 序列轨道上的独奏和静音。

如果静音打开,该通道将不会发声,但其他通道不受影响。如果独奏打开,该通道和任何其他独奏的通道将会发声(如果它没有被静音),但其他通道不会发声。同时被独奏和静音的通道将不会发声。MidiChannel API 包括四种方法:

    boolean      getMute() 
    boolean      getSolo()
    void         setMute(boolean muteState) 
    void         setSolo(boolean soloState)

允许播放合成声音

任何已安装的 MIDI 合成器产生的音频通常会通过采样音频系统路由。如果您的程序没有权限播放音频,则合成器的声音将听不到,并且会抛出安全异常。有关音频权限的更多信息,请参阅之前关于使用音频资源的权限使用音频资源的权限的讨论。

服务提供者接口简介

原文:docs.oracle.com/javase/tutorial/sound/SPI-intro.html

什么是服务?

服务是声音处理功能单元,当应用程序使用 Java Sound API 的实现时自动可用。它们由执行读取、写入、混合、处理和转换音频和 MIDI 数据工作的对象组成。Java Sound API 的实现通常提供一组基本服务,但 API 中也包含机制,支持第三方开发人员(或实现供应商自身)开发新声音服务。这些新服务可以“插入”到现有安装的实现中,扩展其功能而不需要发布新版本。在 Java Sound API 架构中,第三方服务被集成到系统中,以便应用程序的接口与“内置”服务的接口相同。在某些情况下,使用 javax.sound.sampledjavax.sound.midi 包的应用程序开发人员甚至可能不知道他们正在使用第三方服务。

潜在的第三方采样音频服务示例包括:

  • 声音文件读取器和写入器

  • 在不同音频数据格式之间转换的转换器

  • 新的音频混音器和输入/输出设备,无论是纯粹在软件中实现,还是在硬件中具有软件接口

第三方 MIDI 服务可能包括:

  • MIDI 文件读取器和写入器

  • 用于各种类型声音库文件的读取器(通常特定于特定合成器)

  • 受 MIDI 控制的声音合成器、音序器和 I/O 端口,无论是纯粹在软件中实现,还是在硬件中具有软件接口

服务如何工作

javax.sound.sampledjavax.sound.midi 包为希望在其应用程序中包含声音服务的应用程序开发人员提供功能。这些包是声音服务的消费者,提供接口以获取有关音频和 MIDI 服务的信息、控制和访问。此外,Java Sound API 还提供了两个定义抽象类的包,供声音服务的提供者使用:javax.sound.sampled.spijavax.sound.midi.spi 包。

新声音服务的开发人员实现 SPI 包中适当类的具体子类。这些子类以及支持新服务所需的任何其他类都放在一个包含所包含服务描述的 Java 存档(JAR)存档文件中。当此 JAR 文件安装在用户的 CLASSPATH 中时,运行时系统会自动使新服务可用,扩展 Java 平台运行时系统的功能。

一旦安装了新服务,它就可以像以前安装的任何服务一样访问。服务的消费者可以通过调用javax.sound.sampledjavax.sound.midi包中的AudioSystemMidiSystem类的方法来获取有关新服务的信息,或获取新服务类的实例,以返回有关新服务的信息,或返回新的或现有服务类的实例。应用程序无需直接引用 SPI 包(及其子类)中的类来使用已安装的服务。

例如,假设一个名为 Acme Software, Inc.的假想服务提供商有兴趣提供一个允许应用程序读取新格式声音文件的包(但其音频数据是标准数据格式的)。SPI 类AudioFileReader可以被子类化为一个名为AcmeAudioFileReader的类。在新的子类中,Acme 将提供AudioFileReader中定义的所有方法的实现;在这种情况下,只有两个方法(带参数变体),getAudioFileFormatgetAudioInputStream。然后,当应用程序尝试读取一个恰好是 Acme 文件格式的声音文件时,它会调用javax.sound.sampled中的AudioSystem类的方法来访问文件和有关文件的信息。方法AudioSystem.getAudioInputStreamAudioSystem.getAudioFileFormat提供了一个标准的 API 来读取音频流;安装了AcmeAudioFileReader类后,此接口会被扩展以透明地支持新文件类型。应用程序开发人员不需要直接访问新注册的 SPI 类:AudioSystem对象方法会将查询传递给已安装的AcmeAudioFileReader类。

为什么要有这些“工厂”类?为什么不允许应用程序开发人员直接访问新提供的服务?这是一种可能的方法,但通过门卫系统对象管理和实例化所有服务可以使应用程序开发人员免于了解已安装服务的身份。应用程序开发人员只需使用对他们有价值的服务,甚至可能都没有意识到。同时,这种架构允许服务提供者有效地管理其包中的可用资源。

通常,新声音服务的使用对应用程序是透明的。例如,想象一种情况,应用程序开发人员想要从文件中读取音频流。假设thePathName标识了一个音频输入文件,程序会这样做:

    File theInFile = new File(thePathName);
    AudioInputStream theInStream = AudioSystem.getAudioInputStream(theInFile); 

在幕后,AudioSystem确定了哪个已安装的服务可以读取文件,并要求其提供音频数据作为AudioInputStream对象。开发人员可能不知道或甚至不关心输入音频文件是某种新文件格式(例如 Acme 的格式),这些格式由已安装的第三方服务支持。程序与流的第一次接触是通过AudioSystem对象,其后所有对流及其属性的访问都是通过AudioInputStream的方法。这两者都是javax.sound.sampled API 中的标准对象;新文件格式可能需要的特殊处理完全被隐藏起来。

服务提供者如何准备新服务

服务提供者以特殊格式的 JAR 文件提供其新服务,这些文件将被安装在用户系统中 Java 运行时将找到的目录中。JAR 文件是存档文件,每个文件包含一组文件,这些文件可能在存档中的分层目录结构中组织。关于放入这些存档的类文件的准备细节将在接下来的几页中讨论,这些页面描述了音频和 MIDI SPI 包的具体内容;在这里,我们只是概述 JAR 文件创建的过程。

新服务或服务的 JAR 文件应包含 JAR 文件中支持的每个服务的类文件。遵循 Java 平台的约定,每个类文件都具有新定义类的名称,这是一个抽象服务提供者类的具体子类。JAR 文件还必须包含新服务实现所需的任何支持类。为了使运行时系统的服务提供者机制能够定位新服务,JAR 文件还必须包含特殊文件(下文描述),将 SPI 类名称映射到正在定义的新子类。

继续我们上面的例子,假设 Acme Software, Inc.正在分发一套新的采样音频服务包。假设这个包包含两个新服务:

  • AcmeAudioFileReader类,如上所述,是AudioFileReader的子类

  • 一个名为AcmeAudioFileWriterAudioFileWriter子类,将以 Acme 的新格式编写声音文件

从一个任意目录开始——我们称之为/devel——我们创建子目录并将新的类文件放入其中,以一种组织方式来给出新类将被引用的期望路径名:

    com/acme/AcmeAudioFileReader.class
    com/acme/AcmeAudioFileWriter.class

此外,对于每个新的 SPI 类的子类,我们在一个名为META-INF/services的特殊命名目录中创建一个映射文件。文件的名称是被子类化的 SPI 类的名称,文件包含该 SPI 抽象类的新子类的名称。

我们创建文件

  META-INF/services/javax.sound.sampled.spi.AudioFileReader

包括

    # Providers of sound file-reading services 
    # (a comment line begins with a pound sign)
    com.acme.AcmeAudioFileReader

以及文件

  META-INF/services/javax.sound.sampled.spi.AudioFileWriter

包括

    # Providers of sound file-writing services 
    com.acme.AcmeAudioFileWriter

现在我们在任何目录中运行jar命令行:

jar cvf acme.jar -C /devel .

-C选项会导致jar切换到/devel目录,而不是使用执行命令的目录。最后的句点参数指示jar归档该目录的所有内容(即/devel),但不包括目录本身。

这次运行将创建文件acme.jar,其中包含以下内容:

com/acme/AcmeAudioFileReader.class
com/acme/AcmeAudioFileWriter.class
META-INF/services/javax.sound.sampled.spi.AudioFileReader
META-INF/services/javax.sound.sampled.spi.AudioFileWriter
META-INF/Manifest.mf

文件Manifest.mf是由jar工具本身生成的,其中列出了存档中包含的所有文件。

用户如何安装新服务

对于希望通过他们的应用程序获得新服务访问权限的最终用户(或系统管理员),安装是简单的。他们将提供的 JAR 文件放在他们的CLASSPATH中的一个目录中。在执行时,Java 运行时会在需要时找到引用的类。

安装同一服务的多个提供者并不是错误。例如,两个不同的服务提供者可能提供支持读取相同类型的声音文件。在这种情况下,系统会任意选择一个提供者。在意识到哪个提供者被选择的用户应该只安装所需的那个。

提供采样音频服务

原文:docs.oracle.com/javase/tutorial/sound/SPI-providing-sampled.html

正如你所知,Java Sound API 包括两个包,javax.sound.sampled.spijavax.sound.midi.spi,它们定义了抽象类,供声音服务的开发者使用。通过实现并安装这些抽象类的子类,服务提供者可以注册新服务,扩展运行时系统的功能。本页面告诉你如何使用 javax.sound.sampled.spi 包来提供处理采样音频的新服务。

javax.sound.sampled.spi 包中有四个抽象类,代表着你可以为采样音频系统提供的四种不同类型的服务:

  • AudioFileWriter 提供音频文件写入服务。这些服务使应用程序能够将音频数据流写入特定类型的文件。

  • AudioFileReader 提供文件读取服务。这些服务使应用程序能够确定音频文件的特性,并获取一个流,从中可以读取文件的音频数据。

  • FormatConversionProvider 提供音频数据格式转换服务。这些服务允许应用程序将音频流从一种数据格式转换为另一种。

  • MixerProvider 提供特定类型混音器的管理。这种机制允许应用程序获取关于给定类型混音器的信息,并访问实例。

总结之前的讨论,服务提供者可以扩展运行时系统的功能。典型的 SPI 类有两种类型的方法:一种是响应关于特定提供者提供的服务类型的查询,另一种是直接执行新服务,或返回实际提供服务的对象实例。运行时环境的服务提供者机制提供了已安装服务与音频系统的注册,以及新服务提供者类的管理

本质上,服务实例与应用程序开发人员之间存在双重隔离。应用程序从不直接创建服务对象的实例,例如混音器或格式转换器,以用于其音频处理任务。程序甚至不会直接从管理它们的 SPI 类中请求这些对象。应用程序向javax.sound.sampled包中的AudioSystem对象发出请求,AudioSystem反过来使用 SPI 对象来处理这些查询和服务请求。

新音频服务的存在对用户和应用程序员可能是完全透明的。所有应用程序引用都通过javax.sound.sampled包的标准对象,主要是AudioSystem,新服务可能提供的特殊处理通常是完全隐藏的。

在本讨论中,我们将继续使用类似AcmeMixerAcmeMixerProvider的名称来指代新的 SPI 子类。

提供音频文件写入服务

让我们从AudioFileWriter开始,这是较简单的 SPI 类之一。

实现AudioFileWriter方法的子类必须提供一组方法的实现,以处理关于类支持的文件格式和文件类型的查询,以及提供实际将提供的音频数据流写入FileOutputStream的方法。

AudioFileWriter包括两个在基类中具有具体实现的方法:

boolean isFileTypeSupported(AudioFileFormat.Type fileType) 
boolean isFileTypeSupported(AudioFileFormat.Type fileType, AudioInputStream stream) 

这些方法中的第一个方法通知调用者,此文件写入器是否可以写入指定类型的声音文件。这个方法是一个一般性的查询,如果文件写入器可以写入那种类型的文件,它将返回true,假设文件写入器被提供适当的音频数据。然而,写入文件的能力可能取决于传递给文件写入器的特定音频数据的格式。文件写入器可能不支持每种音频数据格式,或者约束可能由文件格式本身施加。(并非所有类型的音频数据都可以写入所有类型的声音文件。)因此,第二个方法更具体,询问特定的AudioInputStream是否可以写入特定类型的文件。

通常情况下,您不需要覆盖这两个具体方法。每个方法只是调用两个其他查询方法之一并遍历返回的结果的包装器。这两个其他查询方法是抽象的,因此需要在子类中实现:

abstract AudioFileFormat.Type[] getAudioFileTypes() 
abstract AudioFileFormat.Type[] getAudioFileTypes(AudioInputStream stream) 

这些方法直接对应于前两个方法。每个方法返回所有支持的文件类型的数组-在第一个方法的情况下,通常是所有一般支持的,在第二个方法的情况下,是特定音频流支持的所有文件类型。第一个方法的典型实现可能简单地返回一个由文件写入器构造函数初始化的数组。第二个方法的实现可能测试流的AudioFormat对象,以查看请求的文件类型是否支持该数据格式。

AudioFileWriter的最后两个方法执行实际的文件写入工作:

abstract int write(AudioInputStream stream, 
     AudioFileFormat.Type fileType, java.io.File out) 
abstract int write(AudioInputStream stream, 
     AudioFileFormat.Type fileType, java.io.OutputStream out) 

这些方法将代表音频数据的字节流写入到第三个参数指定的流或文件中。如何完成这项工作的细节取决于指定类型文件的结构。write方法必须按照该格式声音文件的规定方式写入文件的头部和音频数据(无论是标准类型的声音文件还是新的、可能是专有的类型)。

提供音频文件读取服务

AudioFileReader类由六个抽象方法组成,您的子类需要实现这些方法-实际上,两个不同的重载方法,每个方法都可以接受FileURLInputStream参数。这两个重载方法中的第一个接受有关指定文件格式的查询:

abstract AudioFileFormat getAudioFileFormat(java.io.File file) 
abstract AudioFileFormat getAudioFileFormat(java.io.InputStream stream) 
abstract AudioFileFormat getAudioFileFormat(java.net.URL url) 

getAudioFileFormat方法的典型实现读取并解析声音文件的头部,以确定其文件格式。查看AudioFileFormat类的描述,了解需要从头部读取哪些字段,并参考特定文件类型的规范,以了解如何解析头部。

因为调用者将流作为参数提供给此方法,希望该方法不改变流,文件读取器通常应该从标记流开始。在读取到头部结束后,应该将流重置到其原始位置。

另一个重载的AudioFileReader方法提供文件读取服务,通过返回一个AudioInputStream,从中可以读取文件的音频数据:

abstract AudioInputStream getAudioInputStream(java.io.File file) 
abstract AudioInputStream getAudioInputStream(java.io.InputStream stream) 
abstract AudioInputStream getAudioInputStream(java.net.URL url) 

通常,getAudioInputStream的实现返回一个绕到文件数据块(在头部之后)开头的AudioInputStream,准备好进行读取。然而,可以想象,文件读取器返回的AudioInputStream可能表示一种从文件中包含的内容解码出来的数据流。重要的是,该方法返回一个格式化的流,从中可以读取文件中包含的音频数据。返回的AudioInputStream对象中封装的AudioFormat将告知调用者有关流的数据格式,通常情况下,但不一定是文件本身的数据格式。

通常,返回的流是AudioInputStream的一个实例;您不太可能需要对AudioInputStream进行子类化。

提供格式转换服务

FormatConversionProvider子类将具有一个音频数据格式的AudioInputStream转换为具有另一种格式的AudioInputStream。前者(输入)流被称为流,后者(输出)流被称为目标流。回想一下,AudioInputStream包含一个AudioFormat,而AudioFormat又包含一种特定类型的数据编码,由AudioFormat.Encoding对象表示。源流中的格式和编码称为源格式和源编码,目标流中的格式和编码同样被称为目标格式和目标编码。

转换工作是在FormatConversionProvider的重载抽象方法getAudioInputStream中执行的。该类还具有用于了解所有支持的目标和源格式和编码的抽象查询方法。有具体的包装方法用于查询特定的转换。

getAudioInputStream的两个变体是:

abstract AudioInputStream getAudioInputStream(AudioFormat.Encoding targetEncoding, 
     AudioInputStream sourceStream) 

abstract AudioInputStream getAudioInputStream(AudioFormat targetFormat, 
     AudioInputStream sourceStream) 

这些根据调用者是指定完整目标格式还是只是格式的编码而有所不同的第一个参数。

getAudioInputStream的典型实现通过返回一个围绕原始(源)AudioInputStream的新的AudioInputStream子类来工作,并在调用read方法时对其数据应用数据格式转换。例如,考虑一个名为AcmeCodec的新FormatConversionProvider子类的情况,它与一个名为AcmeCodecStream的新AudioInputStream子类一起工作。

AcmeCodec的第二个getAudioInputStream方法的实现可能是:

public AudioInputStream getAudioInputStream
      (AudioFormat outputFormat, AudioInputStream stream) {
        AudioInputStream cs = null;
        AudioFormat inputFormat = stream.getFormat();
        if (inputFormat.matches(outputFormat) ) {
            cs = stream;
        } else {
            cs = (AudioInputStream)
                (new AcmeCodecStream(stream, outputFormat));
            tempBuffer = new byte[tempBufferSize];
        }
        return cs;
    }

实际的格式转换发生在返回的AcmeCodecStream的新read方法中,它是AudioInputStream的子类。同样,访问这个返回的AcmeCodecStream的应用程序只需将其视为AudioInputStream进行操作,而不需要了解其实现的细节。

FormatConversionProvider的其他方法都允许查询对象支持的输入和输出编码和格式。以下四个方法是抽象的,需要被实现:

abstract AudioFormat.Encoding[] getSourceEncodings() 
abstract AudioFormat.Encoding[] getTargetEncodings() 
abstract AudioFormat.Encoding[] getTargetEncodings(
    AudioFormat sourceFormat) 
abstract  AudioFormat[] getTargetFormats(
    AudioFormat.Encoding targetEncoding, 
    AudioFormat sourceFormat) 

与上面讨论的AudioFileReader类的查询方法一样,这些查询通常通过检查对象的私有数据,并且对于后两种方法,将它们与参数进行比较来处理。

剩下的四个FormatConversionProvider方法是具体的,通常不需要被重写:

boolean isConversionSupported(
    AudioFormat.Encoding targetEncoding,
    AudioFormat sourceFormat) 
boolean isConversionSupported(AudioFormat targetFormat, 
    AudioFormat sourceFormat) 
boolean isSourceEncodingSupported(
    AudioFormat.Encoding sourceEncoding) 
boolean isTargetEncodingSupported(
    AudioFormat.Encoding targetEncoding) 

AudioFileWriter.isFileTypeSupported()类似,这些方法的默认实现本质上是调用其他查询方法之一并遍历返回的结果的包装器。

提供新类型的混音器

正如其名称所示,MixerProvider提供混音器的实例。每个具体的MixerProvider子类都充当应用程序使用的Mixer对象的工厂。当然,只有在定义一个或多个新的Mixer接口的实现时,定义新的MixerProvider才有意义。就像上面的FormatConversionProvider示例中,我们的getAudioInputStream方法返回了一个执行转换的AudioInputStream子类一样,我们的新类AcmeMixerProvider有一个getMixer方法,返回实现Mixer接口的另一个新类的实例。我们将后者称为AcmeMixer。特别是如果混音器是硬件实现的,提供者可能仅支持所请求设备的一个静态实例。如果是这样,它应该在每次调用getMixer时返回这个静态实例。

由于AcmeMixer支持Mixer接口,应用程序不需要任何额外的信息来访问其基本功能。然而,如果AcmeMixer支持Mixer接口中未定义的功能,并且供应商希望使这些扩展功能对应用程序可访问,那么混音器当然应该被定义为一个公共类,具有额外的、有文档记录的公共方法,以便希望利用这些扩展功能的程序可以导入AcmeMixer并将getMixer返回的对象转换为这种类型。

另外两种MixerProvider的方法是:

abstract Mixer.Info[] getMixerInfo() 

boolean isMixerSupported(Mixer.Info info) 

这些方法允许音频系统确定这个特定的提供者类是否可以提供应用程序需要的设备。换句话说,AudioSystem对象可以迭代所有已安装的MixerProviders,看看哪些,如果有的话,可以提供应用程序请求的AudioSystem的设备。getMixerInfo方法返回一个包含有关此提供程序对象提供的混音器类型信息的对象数组。系统可以将这些信息对象与其他提供程序的信息一起传递给应用程序。

一个MixerProvider可以提供多种类型的混音器。当系统调用MixerProvidergetMixerInfo方法时,它会得到一个信息对象列表,标识此提供程序支持的不同类型的混音器。然后系统可以调用MixerProvider.getMixer(Mixer.Info)来获取每个感兴趣的混音器。

你的子类需要实现getMixerInfo,因为它是抽象的。isMixerSupported方法是具体的,通常不需要被覆盖。默认实现只是将提供的Mixer.InfogetMixerInfo返回的数组中的每一个进行比较。

提供 MIDI 服务

原文:docs.oracle.com/javase/tutorial/sound/SPI-providing-MIDI.html

服务提供者接口简介 解释了javax.sound.sampled.spijavax.sound.midi.spi包定义了供声音服务开发人员使用的抽象类。通过实现这些抽象类的子类,服务提供者可以创建一个扩展运行时系统功能的新服务。前一节介绍了如何使用javax.sound.sampled.spi包。本节讨论如何使用javax.sound.midi.spi包为处理 MIDI 设备和文件提供新服务。

javax.sound.midi.spi 包中有四个抽象类,代表着你可以为 MIDI 系统提供的四种不同类型的服务:

  • MidiFileWriter 提供了 MIDI 文件写入服务。这些服务使应用程序能够将其生成或处理的 MIDI Sequence 保存到 MIDI 文件中。

  • MidiFileReader 提供了从 MIDI 文件中返回 MIDI Sequence 供应用程序使用的文件读取服务。

  • MidiDeviceProvider 提供了一个或多个特定类型的 MIDI 设备实例,可能包括硬件设备。

  • SoundbankReader 提供了声音库文件读取服务。SoundbankReader的具体子类解析给定的声音库文件,生成一个可以加载到Synthesizer中的Soundbank对象。

应用程序不会直接创建服务对象的实例,无论是提供者对象,比如MidiDeviceProvider,还是由提供者对象提供的对象,比如Synthesizer。程序也不会直接引用 SPI 类。相反,应用程序会向javax.sound.midi包中的MidiSystem对象发出请求,而MidiSystem又会使用javax.sound.midi.spi类的具体子类来处理这些请求。

提供 MIDI 文件写入服务

有三种标准的 MIDI 文件格式,Java Sound API 的实现都可以支持:Type 0、Type 1 和 Type 2。这些文件格式在文件中的 MIDI 序列数据的内部表示上有所不同,并且适用于不同类型的序列。如果一个实现本身不支持所有三种类型,服务提供者可以为未实现的类型提供支持。还有标准 MIDI 文件格式的变体,其中一些是专有的,同样可以由第三方供应商支持。

写入 MIDI 文件的能力由MidiFileWriter的具体子类提供。这个抽象类直接类比于javax.sampled.spi.AudioFileWriter。同样,方法被分组为查询方法,用于了解可以写入哪些类型的文件,以及用于实际写入文件的方法。与AudioFileWriter一样,其中两个查询方法是具体的:

boolean isFileTypeSupported(int fileType)
boolean isFileTypeSupported(int fileType, Sequence sequence) 

其中第一个提供关于文件写入器是否可以写入指定类型的 MIDI 文件类型的一般信息。第二个方法更具体:它询问特定 Sequence 是否可以写入指定类型的 MIDI 文件。通常情况下,您不需要覆盖这两个具体方法中的任何一个。在默认实现中,每个方法调用另外两个相应的查询方法之一,并遍历返回的结果。作为抽象方法,这另外两个查询方法需要在子类中实现:

abstract int[] getMidiFileTypes() 
abstract int[] getMidiFileTypes(Sequence sequence) 

其中第一个返回一个数组,其中包含一般支持的所有文件类型。一个典型的实现可能会在文件写入器的构造函数中初始化数组,并从这个方法返回数组。从文件类型集中,第二个方法找到文件写入器可以写入给定 Sequence 的子集。根据 MIDI 规范,不是所有类型的序列都可以写入所有类型的 MIDI 文件。

MidiFileWriter子类的write方法执行将给定Sequence中的数据编码为请求的 MIDI 文件类型的正确数据格式,将编码流写入文件或输出流的操作:

abstract int write(Sequence in, int fileType, 
                   java.io.File out) 
abstract int write(Sequence in, int fileType, 
                   java.io.OutputStream out) 

为了实现这一点,write方法必须通过迭代Sequence的轨道来解析Sequence,构建适当的文件头,并将头部和轨道写入输出。当然,MIDI 文件的头部格式由 MIDI 规范定义。它包括诸如标识这是 MIDI 文件的“魔数”,头部长度,轨道数以及序列的定时信息(分频类型和分辨率)等信息。MIDI 文件的其余部分由轨道数据组成,格式由 MIDI 规范定义。

让我们简要地看一下应用程序、MIDI 系统和服务提供者如何合作编写 MIDI 文件。在典型情况下,一个应用程序有一个特定的 MIDI Sequence要保存到文件中。程序查询MidiSystem对象,看看对于手头的特定Sequence支持哪些 MIDI 文件格式(如果有的话),然后尝试写入文件。MidiSystem.getMidiFileTypes(Sequence)方法返回系统可以写入特定序列的所有 MIDI 文件类型的数组。它通过调用每个已安装的MidiFileWriter服务的相应getMidiFileTypes方法来实现这一点,并将结果收集并以整数数组的形式返回,这可以被视为与给定Sequence兼容的所有文件类型的主列表。在写入Sequence到文件时,调用MidiSystem.write传递一个表示文件类型的整数,以及要写入的Sequence和输出文件;MidiSystem使用提供的类型来决定哪个已安装的MidiFileWriter应处理写入请求,并将相应的写入分派给适当的MidiFileWriter

提供 MIDI 文件读取服务

MidiFileReader抽象类直接类似于javax.sampled.spi.AudioFileReader类。两者都包括两个重载的方法,每个方法都可以接受FileURLInputStream参数。重载方法中的第一个返回指定文件的文件格式。对于MidiFileReader,API 为:

abstract MidiFileFormat getMidiFileFormat(java.io.File file) 
abstract MidiFileFormat getMidiFileFormat(
    java.io.InputStream stream) 
abstract MidiFileFormat getMidiFileFormat(java.net.URL url) 

具体的子类必须实现这些方法来返回一个填充的MidiFileFormat对象,描述指定 MIDI 文件(或流或 URL)的格式,假设该文件是文件读取器支持的类型,并且包含有效的头信息。否则,应抛出InvalidMidiDataException

另一个重载的方法从给定的文件、流或 URL 返回一个 MIDI Sequence

abstract Sequence getSequence(java.io.File file) 
abstract Sequence getSequence(java.io.InputStream stream) 
abstract Sequence getSequence(java.net.URL url) 

getSequence方法执行解析 MIDI 输入文件中的字节并构造相应Sequence对象的实际工作。这基本上是MidiFileWriter.write使用的过程的反向。由于 MIDI 文件的内容(由 MIDI 规范定义)与 Java Sound API 定义的Sequence对象之间存在一对一的对应关系,因此解析的细节是直接的。如果传递给getSequence的文件包含文件读取器无法解析的数据(例如,因为文件已损坏或不符合 MIDI 规范),则应抛出InvalidMidiDataException

提供特定的 MIDI 设备

一个MidiDeviceProvider可以被视为提供一种或多种特定类型的 MIDI 设备的工厂。该类包括一个返回 MIDI 设备实例的方法,以及查询方法来了解该提供者可以提供哪些类型的设备。

与其他 javax.sound.midi.spi 服务一样,应用程序开发人员通过调用 MidiSystem 方法间接访问 MidiDeviceProvider 服务,本例中是 MidiSystem.getMidiDeviceMidiSystem.getMidiDeviceInfo。子类化 MidiDeviceProvider 的目的是提供一种新类型的设备,因此服务开发人员还必须为返回的设备创建一个相应的类,就像我们在 javax.sound.sampled.spi 包中看到的 MixerProvider 一样。在那里,返回的设备类实现了 javax.sound.sampled.Mixer 接口;这里实现了 javax.sound.midi.MidiDevice 接口。它还可以实现 MidiDevice 的子接口,如 SynthesizerSequencer

因为 MidiDeviceProvider 的单个子类可以提供多种类型的 MidiDevice,所以该类的 getDeviceInfo 方法返回一个枚举不同可用 MidiDevicesMidiDevice.Info 对象数组:

abstract MidiDevice.Info[] getDeviceInfo() 

返回的数组当然可以只包含一个元素。提供者的典型实现可能会在其构造函数中初始化一个数组,并在此处返回它。这使得 MidiSystem 可以遍历所有已安装的 MidiDeviceProviders 来构建所有已安装设备的列表。然后,MidiSystem 可以将这个列表(MidiDevice.Info[] 数组)返回给应用程序。

MidiDeviceProvider 还包括一个具体的查询方法:

boolean isDeviceSupported(MidiDevice.Info info) 

这个方法允许系统查询提供者关于特定类型设备的信息。通常情况下,你不需要重写这个便利方法。默认实现会遍历由 getDeviceInfo 返回的数组,并将参数与每个元素进行比较。

第三个也是最后一个 MidiDeviceProvider 方法返回请求的设备:

abstract MidiDevice getDevice(MidiDevice.Info info) 

这个方法应该首先测试参数,确保它描述的是该提供者可以提供的设备。如果不是,它应该抛出一个 IllegalArgumentException。否则,它会返回该设备。

提供声音库文件读取服务

SoundBank 是一组可以加载到 Synthesizer 中的 InstrumentsInstrument 是一个实现声音合成算法的对象,产生特定类型的声音,并包含相关的名称和信息字符串。SoundBank 大致对应于 MIDI 规范中的一个银行,但它是一个更广泛和可寻址的集合;可以更好地将其视为 MIDI 银行的集合。

SoundbankReader 由一个重载的方法组成,系统调用该方法从声音库文件中读取一个 Soundbank 对象:

abstract Soundbank getSoundbank(java.io.File file) 
abstract Soundbank getSoundbank(java.io.InputStream stream) 
abstract Soundbank getSoundbank(java.net.URL url) 

SoundbankReader 的具体子类将与特定提供者定义的 SoundBankInstrumentSynthesizer 实现配合工作,以允许系统从文件中加载 SoundBank 到特定 Synthesizer 类的实例中。合成技术可能在一个 Synthesizer 到另一个 Synthesizer 之间有很大差异,因此,存储在 InstrumentSoundBank 中为 Synthesizer 合成过程提供控制或规范数据的数据可以采用各种形式。一种合成技术可能只需要少量字节的参数数据;另一种可能基于广泛的声音样本。SoundBank 中存在的资源将取决于它们加载到的 Synthesizer 的性质,因此 SoundbankReader 子类的 getSoundbank 方法的实现可以访问特定类型 SoundBank 的知识。此外,SoundbankReader 的特定子类了解用于存储 SoundBank 数据的特定文件格式。该文件格式可能是供应商特定和专有的。

SoundBank 只是一个接口,对 SoundBank 对象的内容只有弱约束。要实现这个接口,对象必须支持的方法(getResourcesgetInstrumentsgetVendorgetName 等)对对象包含的数据有宽松的要求。例如,getResourcesgetInstruments 可以返回空数组。子类化的 SoundBank 对象的实际内容,特别是它的乐器和非乐器资源,由服务提供者定义。因此,解析声音库文件的机制完全取决于该特定类型声音库文件的规范。

声音库文件是在 Java Sound API 之外创建的,通常由可以加载该类型声音库的合成器的供应商创建。一些供应商可能会提供用于创建此类文件的最终用户工具。

Trail: JavaBeans(TM)

原文:docs.oracle.com/javase/tutorial/javabeans/index.html

JavaBeans™使得重用软件组件变得容易。开发人员可以使用其他人编写的软件组件,而无需了解其内部工作原理。

要理解为什么软件组件很有用,想象一个工人在组装汽车。例如,她不需要从头开始建造收音机,而是简单地获取一个收音机并将其与汽车的其余部分连接起来。

本教程描述了 JavaBeans 的使用方法,包括以下课程:

通过展示如何使用 NetBeans 构建应用程序,快速介绍了 JavaBeans。

描述了用于 bean 属性、方法和事件的编码模式。它还概述了使用BeanInfo来定制开发体验在构建工具中的使用。

包括 bean 持久性、长期持久性和定制。

课程:快速入门

原文:docs.oracle.com/javase/tutorial/javabeans/quick/index.html

这节课描述了如何使用 NetBeans 构建一个简单的应用程序。有了像 NetBeans 这样的好工具,您可以将 JavaBeans 组件组装成一个应用程序,而无需编写任何代码。

这节课的前三页展示了如何使用 Java 平台中的图形 bean 创建一个简单的应用程序。最后一页演示了将第三方 bean 整合到您的应用程序中有多么容易。

  • 创建一个项目描述了在 NetBeans 中设置新项目的步骤。

  • 按钮是一个 Bean 展示了如何向应用程序的用户界面添加一个 bean,并描述了属性和事件。

  • 连接应用程序介绍了如何使用 NetBeans 响应应用程序中的 bean 事件。

  • 使用第三方 Bean 展示了向调色板添加新 bean 并在您的应用程序中使用它有多么容易。

一个可拼接的应用程序

创建一个项目

原文:docs.oracle.com/javase/tutorial/javabeans/quick/project.html

学习 JavaBeans 最简单的方法是开始使用它们。首先,下载并安装最新版本的 NetBeans。本教程描述了如何使用 NetBeans 7.0 版本。

NetBeans 是一个bean 构建工具,这意味着它识别 JavaBeans 组件(bean)并使您能够轻松地将组件拼接在一起形成应用程序。

按钮是一个 Bean

启动 NetBeans。从菜单中选择文件 > 新建项目...

选择 Java 和 Java 应用程序

点击查看完整图片

类别列表中选择Java,从项目列表中选择Java 应用程序。点击下一步 >

输入项目名称

点击查看完整图片

输入SnapApp作为应用程序名称。取消选中创建主类,然后点击完成。NetBeans 创建新项目,您可以在 NetBeans 的项目窗格中看到它:

项目窗格中的 SnapApp

SnapApp项目上按住控制键单击,从弹出菜单中选择新建 > JFrame 表单...

创建新框架

点击查看完整图片

在类名中填写SnapFrame,包名填写snapapp。点击完成。NetBeans 创建新类并显示其可视化设计工具:

NetBeans 的可视化设计工具

点击查看完整图片

在左侧的项目窗格中,您可以看到新创建的SnapFrame类。屏幕中央是 NetBeans 的可视化设计工具。右侧是Palette,其中包含您可以在可视化设计工具中添加到框架中的所有组件。

一个按钮是一个 Bean

原文:docs.oracle.com/javase/tutorial/javabeans/quick/button.html

仔细看看工具栏。列出的所有组件都是 bean。组件按功能分组。滚动查找Swing 控件组,然后点击按钮并将其拖到可视化设计器中。按钮就是一个 bean!

向 SnapFrame 添加按钮

在 NetBeans 右侧的工具栏下方是一个检查器窗格,你可以用它来检查和操作按钮。尝试关闭底部的输出窗口,以便给检查器窗格更多的空间。

按钮属性

属性

一个 bean 的属性是你可以改变的影响其外观或内部状态的事物。在这个例子中的按钮,属性包括前景色、字体和按钮上显示的文本。属性分为两组。属性列出了最常用的属性,而其他属性显示了不常用的属性。

继续编辑按钮的属性。对于一些属性,你可以直接在表格中输入值。对于其他属性,点击...按钮来编辑值。例如,点击前景属性右侧的...。一个颜色选择对话框弹出,你可以选择一个新的前景文本颜色。尝试一些其他属性看看会发生什么。注意你没有编写任何代码。

事件

Bean 也可以触发事件。点击 bean 属性窗格中的事件按钮。你会看到按钮能够触发的每一个事件的列表。

按钮事件

你可以使用 NetBeans 来通过它们的事件和属性连接 bean。要看看这是如何工作的,从工具栏中拖动一个标签SnapFrame的可视化设计器中。

向可视化设计器添加标签

编辑标签的属性,直到看起来完美为止。

连接应用程序

原文:docs.oracle.com/javase/tutorial/javabeans/quick/wiring.html

要将按钮和标签连接在一起,请点击可视化设计工具栏中的连接模式按钮。

可视化设计工具栏中的连接模式按钮

点击SnapFrame表单中的按钮。NetBeans 用红色轮廓显示按钮,以显示它是将生成事件的组件。

NetBeans 轮廓显示生成事件的组件

点击标签。NetBeans 的连接向导弹出。首先,您将选择要响应的事件。对于按钮来说,这是动作事件。点击动作旁边的+,然后选择actionPerformed。点击下一步

选择源事件

现在您可以选择按钮触发其动作事件时发生的情况。连接向导列出标签 bean 中的所有属性。在列表中选择text,然后点击下一步

选择目标属性

连接向导的最后一个屏幕中,填写您希望为text属性设置的值。点击,然后输入您按下了按钮!或类似的内容。点击完成

填写数值

NetBeans 将组件连接在一起,并在源代码编辑器中展示其成果。

连接的代码

点击源代码工具栏中的设计按钮返回 UI 设计器。点击运行主项目或按F6构建和运行您的项目。

运行主项目按钮

NetBeans 构建并运行项目。它会要求您标识主类,即SnapFrame。当应用程序窗口弹出时,点击按钮。您将在标签中看到您的不朽散文。

看吧,没有代码!

请注意,您没有编写任何代码。这就是 JavaBeans 的真正力量——借助像 NetBeans 这样的优秀构建工具,您可以快速将组件连接在一起,创建一个运行的应用程序。

使用第三方 Bean

原文:docs.oracle.com/javase/tutorial/javabeans/quick/addbean.html

几乎任何代码都可以打包为 bean。到目前为止,您看到的 bean 都是可视化 bean,但 bean 可以提供功能而不必具有可见组件。

JavaBeans 的强大之处在于您可以使用软件组件,而无需编写它们或了解其实现。

本页描述了如何将 JavaBean 添加到您的应用程序中,并利用其功能。

添加一个 Bean 到 NetBeans 调色板

下载一个示例 JavaBean 组件,BumperSticker``。Beans 以 JAR 文件的形式分发。将文件保存在计算机的某个位置。BumperSticker是一个图形组件,公开一个名为go()的方法,用于启动动画。

要将BumperSticker添加到 NetBeans 调色板中,请从 NetBeans 菜单中选择工具 > 调色板 > Swing/AWT 组件

NetBeans 调色板管理器

点击从 JAR 添加...按钮。NetBeans 会要求您定位包含您希望添加到调色板中的 bean 的 JAR 文件。找到您刚下载的文件,然后点击下一步

定位 JAR 文件

NetBeans 显示了 JAR 文件中的类列表。选择您希望添加到调色板中的类。在这种情况下,选择BumperSticker并点击下一步

选择要添加到调色板的 bean

最后,NetBeans 需要知道调色板的哪个部分将接收新的 bean。选择Beans,然后点击完成

选择调色板部分

点击关闭使调色板管理器窗口消失。现在看看调色板。BumperStickerBeans部分中。

使用您的新 JavaBean

BumperSticker从调色板拖到您的表单中。

我们都喜欢 Java 教程

您可以像处理其他 bean 一样处理BumperSticker实例。要看到这一点,将另一个按钮拖到表单中。这个按钮将启动BumperSticker的动画。

将另一个按钮添加到表单中

将按钮连接到BumperSticker bean,就像您已经将第一个按钮连接到文本字段一样。

  1. 首先,点击连接模式按钮。

  2. 点击第二个按钮。NetBeans 会给它一个红色轮廓。

  3. 点击BumperSticker组件。连接向导弹出。

  4. 点击+旁边的action,然后选择actionPerformed。点击下一步 >

  5. 选择方法调用,然后从列表中选择go()。点击完成

如果您对任何步骤感到不确定,请查看连接应用程序。这里的过程非常相似。

再次运行该应用程序。当你点击第二个按钮时,BumperSticker 组件会动画显示心形的颜色。

再次注意,你已经制作出一个功能完善的应用程序,而没有编写任何代码。

一个可拼接的应用程序

Lesson: 编写 JavaBeans 组件

原文:docs.oracle.com/javase/tutorial/javabeans/writing/index.html

编写 JavaBeans 组件非常容易。你不需要特殊的工具,也不必实现任何接口。编写 bean 只是遵循某些编码约定的问题。你所需要做的就是让你的类看起来像一个 bean —— 使用 bean 的工具将能够识别和使用你的 bean。

然而,NetBeans 提供了一些功能,使编写 bean 更容易。此外,Java SE API 包括一些支持类来帮助实现常见任务。

本课程中的代码示例基于一个简单的图形组件称为FaceBean

FaceBean 源代码仅包括:

FaceBean.java

包括 FaceBean 源代码的整个 NetBeans 项目:

FaceBean.zip

一个 bean 是一个遵循 JavaBeans 指南的方法名的 Java 类。一个 bean 构建工具使用内省来检查 bean 类。基于这种检查,bean 构建工具可以找出 bean 的属性、方法和事件。

以下各节描述了 JavaBeans 指南中关于属性、方法和事件的内容。最后,一个关于BeanInfo的部分展示了如何定制开发者与你的 bean 的体验。

属性

原文:docs.oracle.com/javase/tutorial/javabeans/writing/properties.html

要在 bean 类中定义属性,请提供公共的 getter 和 setter 方法。例如,以下方法定义了一个名为mouthWidthint属性:

public class FaceBean {
    private int mMouthWidth = 90;

    public int getMouthWidth() {
        return mMouthWidth;
    }

    public void setMouthWidth(int mw) {
        mMouthWidth = mw;
    }
}

像 NetBeans 这样的构建工具识别方法名,并在其属性列表中显示mouthWidth属性。它还识别类型int,并提供一个适当的编辑器,以便在设计时操作属性。

此示例显示了一个可读写的属性。还有其他组合也是可能的。例如,只读属性具有 getter 方法但没有 setter。只写属性只有 setter 方法。

boolean属性的特殊情况允许使用is而不是get来定义访问方法。例如,boolean属性running的访问器可以如下所示:

public boolean isRunning() {
    // ...
}

可用并在以下部分描述基本属性的各种特殊化。

索引属性

索引属性是一个数组而不是单个值。在这种情况下,bean 类提供了一个用于获取和设置整个数组的方法。以下是一个名为testGradesint[]属性的示例:

public int[] getTestGrades() {
    return mTestGrades;
}

public void setTestGrades(int[] tg) {
    mTestGrades = tg;
}

对于索引属性,bean 类还提供了用于获取和设置数组特定元素的方法。

public int getTestGrades(int index) {
    return mTestGrades[index];
}

public void setTestGrades(int index, int grade) {
    mTestGrades[index] = grade;
}

绑定属性

绑定属性在其值更改时通知监听器。这有两个含义:

  1. bean 类包括用于管理 bean 监听器的addPropertyChangeListener()removePropertyChangeListener()方法。

  2. 当绑定属性更改时,bean 向其注册的监听器发送PropertyChangeEvent

PropertyChangeEventPropertyChangeListener位于java.beans包中。

java.beans包还包括一个名为PropertyChangeSupport的类,它负责大部分绑定属性的工作。这个方便的类跟踪属性监听器,并包含一个方便的方法,向所有注册的监听器触发属性更改事件。

以下示例显示了如何使用PropertyChangeSupport使mouthWidth属性成为绑定属性。绑定属性的必要添加部分用粗体显示。

import java.beans.*;

public class FaceBean {
    private int mMouthWidth = 90;
    private PropertyChangeSupport mPcs =
        new PropertyChangeSupport(this);

    public int getMouthWidth() {
        return mMouthWidth;
    }

    public void setMouthWidth(int mw) {
        int oldMouthWidth = mMouthWidth;
        mMouthWidth = mw;
        mPcs.firePropertyChange("mouthWidth",
                                   oldMouthWidth, mw);
    }

    public void
    addPropertyChangeListener(PropertyChangeListener listener) {
        mPcs.addPropertyChangeListener(listener);
    }

    public void
    removePropertyChangeListener(PropertyChangeListener listener) {
        mPcs.removePropertyChangeListener(listener);
    }
}

绑定属性可以直接与其他 bean 属性绑定,使用像 NetBeans 这样的构建工具。例如,您可以将滑块组件的value属性绑定到示例中显示的mouthWidth属性。NetBeans 允许您在不编写任何代码的情况下执行此操作。

约束属性

约束属性是一种特殊类型的绑定属性。对于约束属性,bean 跟踪一组否决监听器。当约束属性即将更改时,监听器会就更改进行协商。任何一个监听器都有机会否决更改,此时属性保持不变。

否决监听器与属性更改监听器是分开的。幸运的是,java.beans包中包含一个VetoableChangeSupport类,大大简化了受限属性。

mouthWidth示例的更改显示为粗体:

import java.beans.*;

public class FaceBean {
    private int mMouthWidth = 90;
    private PropertyChangeSupport mPcs =
        new PropertyChangeSupport(this);
    private VetoableChangeSupport mVcs =
        new VetoableChangeSupport(this);

    public int getMouthWidth() {
        return mMouthWidth;
    }

    public void
    setMouthWidth(int mw) throws PropertyVetoException {
        int oldMouthWidth = mMouthWidth;
        mVcs.fireVetoableChange("mouthWidth",
                                    oldMouthWidth, mw);
        mMouthWidth = mw;
        mPcs.firePropertyChange("mouthWidth",
                                 oldMouthWidth, mw);
    }

    public void
    addPropertyChangeListener(PropertyChangeListener listener) {
        mPcs.addPropertyChangeListener(listener);
    }

    public void
    removePropertyChangeListener(PropertyChangeListener listener) {
        mPcs.removePropertyChangeListener(listener);
    }

    public void
    addVetoableChangeListener(VetoableChangeListener listener) {
        mVcs.addVetoableChangeListener(listener);
    }

    public void
    removeVetoableChangeListener(VetoableChangeListener listener) {
        mVcs.removeVetoableChangeListener(listener);
    }
}

NetBeans 中的开发支持

创建 bean 属性的编码模式很简单,但有时很难确定是否一切都正确。NetBeans 支持属性模式,因此您在编写代码时可以立即看到结果。

要利用此功能,请查看Navigator窗格,通常位于 NetBeans 窗口的左下角。通常,此窗格处于Members View模式,显示当前类中定义的所有方法和字段。

单击组合框切换到Bean Patterns视图。您将看到一个属性列表,其中列出了 NetBeans 可以从您的方法定义中推断出的属性。随着您的输入,NetBeans 会更新此列表,这是检查您工作的方便方式。

在下面的示例中,NetBeans 发现了可读写的mouthWidth属性和可读写的索引testGrades属性。此外,NetBeans 还意识到FaceBean允许注册PropertyChangeListenerVetoableChangeListener

NetBeans bean patterns Navigator

方法

原文:docs.oracle.com/javase/tutorial/javabeans/writing/methods.html

一个方法是豆子能做的事情。任何不是属性定义的公共方法都是一个豆子方法。当你在像 NetBeans 这样的构建工具的上下文中使用一个豆子时,你可以将豆子的方法作为应用程序的一部分。例如,你可以将按钮按下与调用你的豆子方法之一联系起来。

事件

原文:docs.oracle.com/javase/tutorial/javabeans/writing/events.html

一个 bean 类可以触发任何类型的事件,包括自定义事件。与属性一样,事件通过特定模式的方法名称来识别。

public void add*<Event>*Listener(*<Event>*Listener a)
public void remove*<Event>*Listener(*<Event>*Listener a)

监听器类型必须是java.util.EventListener的子类。

例如,Swing 的JButton是一个 bean,当用户点击它时会触发action事件。JButton包括以下方法(实际上是从AbstractButton继承而来),这些方法是事件的 bean 模式:

public void addActionListener(ActionListener l);
public void removeActionListener(ActionListener l);

Bean 事件被构建工具识别,并可用于将组件进行连线。例如,您可以将按钮的action事件连接起来,以触发某些操作,比如调用另一个 bean 的方法。

使用 BeanInfo

原文:docs.oracle.com/javase/tutorial/javabeans/writing/beaninfo.html

Bean,尤其是图形组件,可能有大量属性。如果你的类继承自 ComponentJComponent 或其他 Swing 类,它已经拥有一百多个属性。虽然像 NetBeans 这样的构建工具使编辑 bean 属性变得容易,但对于经验不足的程序员来说,很难找到要编辑的正确属性。

BeanInfo 概述

BeanInfo 是一个改变你的 bean 在构建工具中显示方式的类。构建工具可以查询 BeanInfo 以找出应该首先显示哪些属性以及哪些应该隐藏。

你的 bean 对应的 BeanInfo 类应该与 bean 类同名,只是在末尾加上 BeanInfo。例如,FaceBean 类有一个对应的描述它的 FaceBeanBeanInfo 类。

虽然可以手动实现 BeanInfo 类,但使用像 NetBeans 这样的工具来编辑 BeanInfo 会更容易。

在 NetBeans 中创建 BeanInfo

项目窗格中,按住 Control 键单击 bean 类的名称,然后从上下文菜单中选择BeanInfo Editor...

创建 BeanInfo,第 1 部分

NetBeans 注意到你没有 BeanInfo 并询问是否要创建一个。点击Yes

创建 BeanInfo,第 2 部分

NetBeans 会创建一个新的类并将你带到源代码编辑器。点击Designer切换到可视化编辑器。

BeanInfo 可视化编辑器

点击查看完整图片

从可视化编辑器左侧的列表中选择属性,然后在右侧编辑其属性。如果你不希望某个特定属性出现在使用构建工具的开发人员面前,点击Hidden。要表示某个属性应该在其他属性之前显示,点击Preferred。你还可以指示属性是否绑定或受限。

你可以为 bean 的事件源和方法提供类似的信息。

当构建工具加载你的 bean 类以将其添加到工具栏时,它会自动找到相应的 BeanInfo 并用它来决定如何向开发人员展示你的 bean。

课程:高级 JavaBeans 主题

原文:docs.oracle.com/javase/tutorial/javabeans/advanced/index.html

JavaBeans 开发入门很容易,但是 Bean 具有令人惊讶的深度。本课程涵盖了一些更高级的主题,包括 Bean 可以如何存储(持久化)以及如何为自定义数据类型提供自定义编辑器。

  • Bean Persistence 描述了保存和重建 Bean 的机制。

  • Long Term Persistence 涵盖了将 Bean 表示为 XML。

  • Bean Customization 概述了为自定义数据类型创建编辑器组件。

Bean 持久性

原文:docs.oracle.com/javase/tutorial/javabeans/advanced/persistence.html

当 bean 的属性、字段和状态信息被保存到存储中并从中检索时,bean 具有持久性的属性。组件模型提供了一种持久性机制,使组件的状态能够被存储在非易失性位置以供以后检索。

使持久性成为可能的机制称为 序列化。对象序列化意味着将对象转换为数据流并将其写入存储。任何使用该 bean 的小程序、应用程序或工具都可以通过反序列化来“重建”它。然后对象将恢复到其原始状态。

例如,Java 应用程序可以在 Microsoft Windows 机器上序列化一个 Frame 窗口,然后将序列化文件通过电子邮件发送到 Solaris 机器,然后 Java 应用程序可以将 Frame 窗口恢复到在 Microsoft Windows 机器上存在的确切状态。

任何使用该 bean 的小程序、应用程序或工具都可以通过 反序列化 来“重建”它。

所有的 bean 都必须持久化。为了持久化,您的 bean 必须通过实现 java.io.Serializable(在 API 参考文档中)接口或 java.io.Externalizable(在 API 参考文档中)接口来支持序列化。这些接口为您提供了自动序列化和定制序列化的选择。如果类的继承层次结构中的任何类实现了 SerializableExternalizable,那么该类就是可序列化的。

可序列化的类

只要该类或父类实现了 java.io.Serializable 接口,任何类都可以序列化。支持序列化的示例类包括 ComponentStringDateVectorHashtable。因此,Component 类的任何子类,包括 Applet,都可以被序列化。不支持序列化的显著类包括 ImageThreadSocketInputStream。尝试序列化这些类型的对象将导致 NotSerializableException

Java 对象序列化 API 自动将大多数 Serializable 对象的字段序列化到存储流中。这包括原始类型、数组和字符串。API 不会序列化或反序列化被标记为 transient 或 static 的字段。

控制序列化

您可以控制 bean 所经历的序列化级别。控制序列化的三种方式是:

  • 通过 Serializable 接口实现的自动序列化。Java 序列化软件将整个对象序列化,除了 transient 和 static 字段。

  • 定制序列化。通过使用 transient(或 static)修饰符标记要排除的字段,可以选择性地排除不想序列化的字段。

  • 自定义文件格式,由Externalizable接口及其两个方法实现。Beans 以特定文件格式编写。

默认序列化:Serializable 接口

Serializable接口通过使用 Java 对象序列化工具提供自动序列化。Serializable不声明任何方法;它充当标记,告诉对象序列化工具你的 bean 类是可序列化的。将你的类标记为Serializable意味着你告诉 Java 虚拟机(JVM)你已经确保你的类将与默认序列化一起使用。以下是与Serializable接口一起使用的一些重要点:

  • 实现Serializable的类必须能够访问超类的无参数构造函数。当对象从.ser文件中“重建”时,将调用此构造函数。

  • 如果在超类中已经实现了Serializable,那么你不需要在你的类中实现它。

  • 所有字段(除了静态和瞬态字段)都会被序列化。使用transient修饰符指定不想序列化的字段,并指定不可序列化的类。

使用transient关键字进行选择性序列化

要在Serializable对象中排除字段的序列化,请使用transient修饰符标记字段。

transient int status;

默认序列化不会序列化transientstatic字段。

选择性序列化:writeObject 和 readObject

如果你的可序列化类包含以下两种方法之一(签名必须完全相同),则默认序列化将不会发生。

private void writeObject(java.io.ObjectOutputStream out)
    throws IOException;
private void readObject(java.io.ObjectInputStream in)
    throws IOException, ClassNotFoundException;

通过编写自己的writeObjectreadObject方法的实现,可以控制更复杂对象的序列化。当需要对默认序列化无法处理的对象进行序列化,或者需要向序列化流添加不是对象数据成员的数据时,请实现writeObject。实现readObject以重建使用writeObject写入的数据流。

Externalizable 接口

当需要完全控制 bean 的序列化时(例如,写入和读取特定文件格式时),请使用Externalizable接口。要使用Externalizable接口,你需要实现两个方法:readExternalwriteExternal。实现Externalizable的类必须有一个无参数构造函数。

长期持久性

原文:docs.oracle.com/javase/tutorial/javabeans/advanced/longpersistence.html

长期持久性是一种模型,可以将 bean 保存为 XML 格式。

有关 XML 格式和如何为非 bean 实现长期持久性的信息,请参阅XML 模式和使用 XMLEncoder。

编码器和解码器

XMLEncoder类用于编写Serializable对象的文本表示的输出文件。以下代码片段是将 Java bean 及其属性以 XML 格式写入的示例:

XMLEncoder encoder = new XMLEncoder(
           new BufferedOutputStream(
           new FileOutputStream("Beanarchive.xml")));

encoder.writeObject(object);
encoder.close(); 

XMLDecoder类读取使用 XMLEncoder 创建的 XML 文档:

XMLDecoder decoder = new XMLDecoder(
    new BufferedInputStream(
    new FileInputStream("Beanarchive.xml")));

Object object = decoder.readObject();
decoder.close();

XML 中有什么?

XML bean 存档具有自己特定的语法,其中包括以下标签来表示每个 bean 元素:

  • 用于描述 XML 版本和编码类型的 XML 前言

  • 一个**<java>**标签,用于包含 bean 的所有对象元素

  • 一个**<object>**标签,用于表示从其序列化形式重建对象所需的一组方法调用

    <object class="javax.swing.JButton" method="new">
        <string>Ok</string>
    </object>
    
    

    或语句

    <object class="javax.swing.JButton">
        <void method="setText">
            <string>Cancel</string>
                </void>
        </object>
    
    
  • 用于定义适当的基本类型的标签:

    • **<boolean>**

    • **<byte>**

    • **<char>**

    • **<short>**

    • **<int>**

    • **<long>**

    • **<float>**

    • **<double>**

    <int>5555</int>
    
    
  • 一个<class>标签,用于表示 Class 的一个实例。

    <class>java.swing.JFrame</class>
    
    
  • 一个<array>标签用于定义数组

    <array class="java.lang.String" length="5">
    </array>
    
    

以下代码表示将为SimpleBean组件生成的 XML 存档:

<?xml version="1.0" encoding="UTF-8" ?>
<java>
  <object class="javax.swing.JFrame">
    <void method="add">
      <object class="java.awt.BorderLayout" field="CENTER"/>
      <object class="SimpleBean"/>
    </void>
    <void property="defaultCloseOperation">
      <object class="javax.swing.WindowConstants" field="DISPOSE_ON_CLOSE"/>
    </void>
    <void method="pack"/>
    <void property="visible">
      <boolean>true</boolean>
    </void>
  </object>
</java>

Bean 自定义

原文:docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html

自定义提供了一种修改 Bean 外观和行为的方式,使其在应用程序构建器中满足您的特定需求。对于 Bean 开发人员,有几个级别的自定义可用,以允许其他开发人员充分利用 Bean 的潜在功能。


以下链接对学习有关属性编辑器和自定义器很有用:

  • PropertyEditor 接口

  • PropertyEditorSupport

  • PropertyEditorManager

  • Customizer 接口

  • BeanInfo 接口


在符合 Beans 规范的构建工具中,可以在设计时自定义 Bean 的外观和行为。有两种方式可以自定义 Bean:

  • 通过使用属性编辑器。每个 Bean 属性都有自己的属性编辑器。NetBeans GUI Builder 通常在属性窗口中显示 Bean 的属性编辑器。与特定属性类型相关联的属性编辑器编辑该属性类型。

  • 通过使用自定义器。自定义器为您提供了完全的 GUI 控制权,用于对 Bean 进行自定义。当属性编辑器不实用或不适用时,将使用自定义器。与属性关联的属性编辑器不同,自定义器与 Bean 关联。

属性编辑器

属性编辑器是用于自定义特定属性类型的工具。属性编辑器在属性窗口中激活。此窗口确定属性的类型,搜索相关的属性编辑器,并以相关方式显示属性的当前值。

属性编辑器必须实现PropertyEditor接口,该接口提供了指定属性在属性表中如何显示的方法。以下图表示包含myBean1属性的属性窗口:

此图表示属性窗口中的 myBean1 属性

您可以通过单击属性条目开始编辑这些属性。单击大多数条目将弹出单独的面板。例如,要设置foregroundbackground,请使用带有颜色选择的选择框,或按下“…”按钮以使用标准 ColorEditor 窗口。单击toolTipText属性将打开 StringEditor 窗口。

支持类PropertyEditorSupport提供了PropertyEditor接口的默认实现。通过从PropertyEditorSupport子类化您的属性编辑器,您可以简单地重写您需要的方法。

要在属性窗口中显示当前属性值"sample",需要重写isPaintable以返回true。然后必须重写paintValue以在属性表中的矩形中绘制当前属性值。以下是ColorEditor如何实现paintValue

public void paintValue(java.awt.Graphics gfx, java.awt.Rectangle box) {
    Color oldColor = gfx.getColor();
    gfx.setColor(Color.black);
    gfx.drawRect(box.x, box.y, box.width-3, box.height-3);
    gfx.setColor(color);
    gfx.fillRect(box.x+1, box.y+1, box.width-4, box.height-4);
    gfx.setColor(oldColor);
}

要支持自定义属性编辑器,需要重写另外两个方法。重写supportsCustomEditor以返回 true,然后重写getCustomEditor以返回自定义编辑器实例。ColorEditor.getCustomEditor返回this

此外,PropertyEditorSupport类维护一个PropertyChangeListener列表,并在绑定属性更改时向这些侦听器发送属性更改事件通知。

如何将属性编辑器与属性关联

属性编辑器是通过以下方式发现并与给定属性关联的:

  • 通过BeanInfo对象进行显式关联。标题的编辑器通过以下代码行设置:

    pd.setPropertyEditorClass(TitleEditor.class);
    
    
  • 通过java.beans.PropertyEditorManager.registerEditor方法进行显式注册。此方法接受两个参数:bean 类类型和要与该类型关联的编辑器类。

  • 名称搜索。如果一个类没有明确关联的属性编辑器,那么PropertyEditorManager将通过以下方式搜索该类的属性编辑器:

    • 将"Editor"附加到完全限定的类名。例如,对于my.package.ComplexNumber类,属性编辑器管理器将搜索my.package.ComplexNumberEditor类。

    • 将"Editor"附加到类名并搜索类路径。

自定义器

您已经了解到构建工具为您创建自己的属性编辑器提供支持。对于复杂的、工业强度的 bean,视觉构建器应满足哪些其他需求?有时,将一个单一根选择关于 bean 类型渲染的一半属性变得无关紧要是不可取的。JavaBeans 规范提供了用户定义的自定义器,通过它们您可以为 bean 属性定义比属性编辑器提供的更高级别的定制。

当您使用一个 bean 自定义器时,您完全控制如何配置或编辑一个 bean。自定义器是专门针对 bean 定制的应用程序。有时属性不足以表示 bean 的可配置属性。自定义器用于需要复杂指令来更改 bean 的地方,以及属性编辑器过于原始无法实现 bean 定制的地方。

所有自定义器必须:

  • 扩展java.awt.Component或其子类之一。

  • 实现java.beans.Customizer接口,这意味着实现方法来注册PropertyChangeListener对象,并在目标 bean 发生更改时向这些侦听器触发属性更改事件。

  • 实现一个默认构造函数。

  • 通过BeanInfo.getBeanDescriptor将自定义器与其目标类关联。

教程:JDBC 数据库访问

原文:docs.oracle.com/javase/tutorial/jdbc/index.html

JDBC API 的设计初衷是让简单的事情保持简单。这意味着 JDBC 使得日常数据库任务变得容易。这个教程将通过示例引导您使用 JDBC 执行常见的 SQL 语句,并执行数据库应用程序常见的其他目标。

这个教程分为以下几个课程:

列出了 JDBC 的特性,描述了 JDBC 架构,并回顾了 SQL 命令和关系数据库概念。

涵盖了 JDBC API。

在第一课结束时,您将学会如何使用基本的 JDBC API 来创建表,向表中插入值,查询表,检索查询结果,并更新表。在这个过程中,您将学会如何使用简单语句和预编译语句,并看到一个存储过程的示例。您还将学会如何执行事务,以及如何捕获异常和警告。

教程:JDBC 简介

原文:docs.oracle.com/javase/tutorial/jdbc/overview/index.html

JDBC API 是一个可以访问任何类型表格数据的 Java API,特别是存储在关系数据库中的数据。

JDBC 帮助您编写管理这三种编程活动的 Java 应用程序:

  1. 连接到数据源,比如数据库

  2. 向数据库发送查询和更新语句

  3. 从数据库中检索并处理查询结果

以下简单的代码片段给出了这三个步骤的简单示例:

public void connectToAndQueryDatabase(String username, String password) {

    Connection con = DriverManager.getConnection(
                         "jdbc:myDriver:myDatabase",
                         username,
                         password);

    Statement stmt = con.createStatement();
    ResultSet rs = stmt.executeQuery("SELECT a, b, c FROM Table1");

    while (rs.next()) {
        int x = rs.getInt("a");
        String s = rs.getString("b");
        float f = rs.getFloat("c");
    }
}

这段简短的代码片段实例化了一个DriverManager对象,用于连接数据库驱动程序并登录数据库,实例化了一个Statement对象,将您的 SQL 语言查询传递给数据库;实例化了一个ResultSet对象,检索您的查询结果,并执行一个简单的while循环,用于检索和显示这些结果。就是这么简单。

JDBC 产品组件

JDBC 包括四个组件:

  1. JDBC API —  JDBC™ API 提供了从 Java™编程语言对关系数据进行编程访问的方式。使用 JDBC API,应用程序可以执行 SQL 语句,检索结果,并将更改传播回底层数据源。JDBC API 还可以在分布式、异构环境中与多个数据源交互。

    JDBC API 是 Java 平台的一部分,包括Java™标准版(Java™ SE)和Java™企业版(Java™ EE)。JDBC 4.0 API 分为两个包:java.sqljavax.sql. 这两个包都包含在 Java SE 和 Java EE 平台中。

  2. JDBC 驱动管理器 —  JDBC DriverManager类定义了可以将 Java 应用程序连接到 JDBC 驱动程序的对象。DriverManager一直是 JDBC 架构的支柱。它非常小而简单。

    标准扩展包javax.namingjavax.sql允许您使用注册到Java 命名和目录接口™(JNDI)命名服务的DataSource对象与数据源建立连接。您可以使用任一连接机制,但尽可能使用DataSource对象是推荐的。

  3. JDBC 测试套件 —  JDBC 驱动程序测试套件帮助您确定 JDBC 驱动程序是否能运行您的程序。这些测试并不全面或详尽,但它们确实涵盖了 JDBC API 中的许多重要特性。

  4. JDBC-ODBC 桥 —  Java 软件桥通过 ODBC 驱动程序提供 JDBC 访问。请注意,您需要将 ODBC 二进制代码加载到每台使用此驱动程序的客户端机器上。因此,ODBC 驱动程序最适合用于企业网络,其中客户端安装不是一个主要问题,或者用于在 Java 中编写的应用程序服务器代码的三层架构。

这个教程使用这四个 JDBC 组件中的前两个来连接数据库,然后构建一个使用 SQL 命令与测试关系数据库通信的 Java 程序。最后两个组件用于在专业环境中测试 Web 应用程序,或与支持 ODBC 的数据库管理系统通信。

JDBC 架构

原文:docs.oracle.com/javase/tutorial/jdbc/overview/architecture.html

JDBC API 支持数据库访问的两层和三层处理模型。

图 1:用于数据访问的两层架构。

DBMS 专有协议提供了客户端机器和数据库服务器之间的双向通信

在两层模型中,Java 小程序或应用程序直接与数据源通信。这需要一个能够与被访问的特定数据源通信的 JDBC 驱动程序。用户的命令被传递到数据库或其他数据源,并将这些语句的结果发送回用户。数据源可能位于用户通过网络连接的另一台机器上。这被称为客户端/服务器配置,用户的机器是客户端,而托管数据源的机器是服务器。网络可以是一个内部网络,例如连接公司内部员工的网络,也可以是互联网。

在三层模型中,命令被发送到一个“中间层”服务,然后再将命令发送到数据源。数据源处理命令并将结果发送回中间层,然后再发送给用户。MIS 主管发现三层模型非常有吸引力,因为中间层使得可以控制对企业数据的访问和更新类型。另一个优点是简化了应用程序的部署。最后,在许多情况下,三层架构可以提供性能优势。

图 2:用于数据访问的三层架构。

DBMS 专有协议提供了数据库服务器和服务器机器之间的双向通信。HTTP、RMI、CORBA 或其他调用提供了服务器机器和客户端机器之间的双向通信

直到最近,中间层通常是用诸如 C 或 C++之类的语言编写的,这些语言提供了快速的性能。然而,随着将 Java 字节码转换为高效的机器特定代码的优化编译器的引入,以及诸如 Enterprise JavaBeans™之类的技术,Java 平台正迅速成为中间层开发的标准平台。这是一个巨大的优势,使得可以利用 Java 的健壮性、多线程和安全功能。

随着企业越来越多地使用 Java 编程语言编写服务器代码,JDBC API 在三层架构的中间层中被越来越多地使用。使 JDBC 成为服务器技术的一些特点包括其支持连接池、分布式事务和断开的行集。JDBC API 也是允许从 Java 中间层访问数据源的途径。

关系数据库概述

原文:docs.oracle.com/javase/tutorial/jdbc/overview/database.html

数据库是一种以便于检索信息的方式存储信息的手段。简单来说,关系数据库是以行和列的形式呈现信息的数据库。表被称为关系,因为它是相同类型对象(行)的集合。表中的数据可以根据共同的键或概念相关联,从表中检索相关数据的能力是关系数据库这个术语的基础。数据库管理系统(DBMS)处理数据的存储、维护和检索方式。在关系数据库的情况下,关系数据库管理系统(RDBMS)执行这些任务。本书中使用的 DBMS 是一个包括 RDBMS 在内的通用术语。

完整性规则

关系表遵循某些完整性规则,以确保它们包含的数据保持准确并始终可访问。首先,关系表中的行应该都是不同的。如果有重复的行,解决哪一个是正确选择的问题可能会出现问题。对于大多数 DBMS,用户可以指定不允许重复行,如果这样做,DBMS 将阻止添加任何重复现有行的行。

传统关系模型的第二个完整性规则是列值不能是重复组或数组。数据完整性的第三个方面涉及空值的概念。数据库通过使用空值来指示缺失值的情况。它不等同于空白或零。空白被认为等于另一个空白,零等于另一个零,但两个空值不被视为相等。

当表中的每一行都不同时,可以使用一个或多个列来标识特定行。这个唯一的列或列组称为主键。任何作为主键的列都不能为 null;如果是,包含它的主键将不再是完整的标识符。这个规则称为实体完整性。

Employees表展示了一些关系数据库概念。它有五列和六行,每行代表一个不同的雇员。

Employees

员工编号 出生日期 车牌号
10001 阿克塞尔 华盛顿 43 年 8 月 28 日 5
10083 阿维德 沙玛 54 年 11 月 24 日 null
10120 乔纳斯 金斯伯格 69 年 1 月 1 日 null
10005 弗洛伦斯 沃约科夫斯基 71 年 7 月 4 日 12
10099 肖恩 华盛顿 66 年 9 月 21 日 null
10035 伊丽莎白 山口 59 年 12 月 24 日 null

该表的主键通常会是员工编号,因为每个员工编号都保证是不同的。(数字比字符串更有效率用于比较。)也可以使用 First_NameLast_Name,因为两者的组合在我们的示例数据库中也只标识一行。仅使用姓氏将不起作用,因为有两个姓氏为“Washington”的员工。在这种特殊情况下,名字都是不同的,因此可以想象使用该列作为主键,但最好避免使用可能出现重复的列。如果 Elizabeth Yamaguchi 在这家公司找到工作,而主键是 First_Name,则关系数据库管理系统将不允许添加她的名字(如果已经指定不允许重复)。因为表中已经有一个 Elizabeth,再添加一个将使主键无法作为标识一行的方法。请注意,虽然使用 First_NameLast_Name 是这个示例的唯一复合键,但在更大的数据库中可能不是唯一的。还要注意,Employees 表假设每个员工只能有一辆车。

SELECT 语句

SQL 是一种设计用于与关系数据库一起使用的语言。有一组被认为是标准的基本 SQL 命令,被所有关系数据库管理系统使用。例如,所有关系数据库管理系统都使用 SELECT 语句。

SELECT 语句,也称为查询,用于从表中获取信息。它指定一个或多个列标题,一个或多个要选择的表,以及一些选择条件。关系数据库管理系统返回满足所述要求的列条目的行。例如,以下 SELECT 语句将获取拥有公司车辆的员工的名字和姓氏:

SELECT First_Name, Last_Name
FROM Employees
WHERE Car_Number IS NOT NULL

符合要求(Car_Number 列中不为 null 的行集)的结果集如下。对于满足要求的每一行,都会打印出名字和姓氏,因为 SELECT 语句(第一行)指定了列 First_NameLast_NameFROM 子句(第二行)给出了将从中选择列的表。

FIRST_NAME LAST_NAME
Axel Washington
Florence Wojokowski

以下代码生成一个包含整个表的结果集,因为它要求表 Employees 中的所有列没有限制(没有 WHERE 子句)。请注意,SELECT * 意味着“SELECT 所有列”。

SELECT *
FROM Employees

WHERE 子句

SELECT 语句中的 WHERE 子句提供了选择值的条件。例如,在以下代码片段中,只有在列 Last_Name 以字符串 'Washington' 开头的行中才会选择值。

SELECT First_Name, Last_Name
FROM Employees
WHERE Last_Name LIKE 'Washington%'

关键字LIKE用于比较字符串,并提供了可以使用通配符的功能。例如,在上面的代码片段中,'Washington'末尾有一个百分号(%),表示任何包含字符串'Washington'加零个或多个额外字符的值都将满足这个选择条件。因此,'Washington'或'Washingtonian'都会匹配,但'Washing'不会。LIKE子句中使用的另一个通配符是下划线(_),代表任意一个字符。例如,

WHERE Last_Name LIKE 'Ba_man'

会匹配'Barman'、'Badman'、'Balman'、'Bagman'、'Bamman'等等。

下面的代码片段有一个使用等号(=)比较数字的WHERE子句。它选择了被分配车辆 12 的员工的名字和姓氏。

SELECT First_Name, Last_Name
FROM Employees
WHERE Car_Number = 12

下一个代码片段选择了员工编号大于 10005 的员工的名字和姓氏:

SELECT First_Name, Last_Name
FROM Employees
WHERE Employee_Number > 10005

WHERE子句可能会变得相当复杂,包含多个条件,在一些数据库管理系统中还可能有嵌套条件。本概述不会涵盖复杂的WHERE子句,但以下代码片段有一个带有两个条件的WHERE子句;这个查询选择了员工编号小于 10100 且没有公司车的员工的名字和姓氏。

SELECT First_Name, Last_Name
FROM Employees
WHERE Employee_Number < 10100 and Car_Number IS NULL

一种特殊类型的WHERE子句涉及连接,将在下一节中解释。

连接

关系数据库的一个显著特点是可以通过所谓的连接从多个表中获取数据。假设在检索拥有公司车辆的员工姓名后,想要找出谁拥有哪辆车,包括车辆的品牌、型号和年份。这些信息存储在另一个表Cars中:

Cars

Car_Number 品牌 型号 年份
5 本田 思域 DX 1996
12 丰田 卡罗拉 1999

为了将两个表关联起来,必须有一列同时出现在两个表中。这一列在一个表中必须是主键,在另一个表中被称为外键。在这种情况下,出现在两个表中的列是Car_Number,它是表Cars的主键,也是表Employees的外键。如果 1996 年本田思域被损坏并从Cars表中删除,那么Car_Number为 5 也必须从Employees表中删除,以保持所谓的参照完整性。否则,Employees表中的外键列(Car_Number)将包含一个不指向Cars表中任何内容的条目。外键必须为空或等于所引用表的现有主键值。这与主键不同,主键不可为空。在表EmployeesCar_Number列中有几个空值,因为员工可能没有公司车。

以下代码要求输入拥有公司车辆的员工的名字和姓氏,以及这些车辆的制造商、型号和年份。请注意,FROM子句列出了EmployeesCars表,因为请求的数据包含在这两个表中。在列名之前使用表名和点号(.)表示哪个表包含该列。

SELECT Employees.First_Name, Employees.Last_Name,
    Cars.Make, Cars.Model, Cars.Year
FROM Employees, Cars
WHERE Employees.Car_Number = Cars.Car_Number

这将返回一个类似以下的结果集:

FIRST_NAME LAST_NAME LICENSE_PLATE MILEAGE YEAR
John Washington ABC123 5000 1996
Florence Wojokowski DEF123 7500 1999

常见的 SQL 命令

SQL 命令分为不同的类别,主要包括数据操作语言(DML)命令和数据定义语言(DDL)命令。DML 命令处理数据,无论是检索数据还是修改数据以保持其最新状态。DDL 命令创建或更改表以及其他数据库对象,如视图和索引。

以下是更常见的 DML 命令列表:

  • SELECT —  用于从数据库中查询和显示数据。SELECT语句指定要包含在结果集中的列。应用程序中使用的 SQL 命令中绝大多数是SELECT语句。

  • INSERT —  向表中添加新行。INSERT用于填充新创建的表或向已存在的表中添加新行(或多行)。

  • DELETE —  从表中删除指定的行或一组行

  • UPDATE —  改变表中某一列或一组列中的现有值

更常见的 DDL 命令如下:

  • CREATE TABLE —  创建一个带有用户提供的列名的表。用户还需要为每个列中的数据指定一个类型。不同的关系型数据库管理系统具有不同的数据类型,因此用户可能需要使用元数据来确定特定数据库使用的数据类型。CREATE TABLE通常比数据操作命令使用频率低,因为表只创建一次,而添加或删除行或更改单个值通常更频繁发生。

  • DROP TABLE —  删除所有行并从数据库中删除表定义。根据 SQL92,过渡级别的规范,JDBC API 实现需要支持DROP TABLE命令。但是,对于DROP TABLECASCADERESTRICT选项的支持是可选的。此外,当存在引用正在被删除的表的视图或完整性约束时,DROP TABLE的行为是由实现定义的。

  • ALTER TABLE —  向表中添加或删除列。它还添加或删除表约束并更改列属性

结果集和游标

满足查询条件的行被称为结果集。结果集中返回的行数可以是零、一或多个。用户可以逐行访问结果集中的数据,游标提供了这样的功能。游标可以被看作是指向包含结果集行的文件的指针,并且该指针有能力跟踪当前正在访问的行。游标允许用户从顶部到底部处理结果集的每一行,因此可用于迭代处理。大多数数据库管理系统在生成结果集时会自动创建游标。

早期的 JDBC API 版本为结果集的游标增加了新的功能,允许它向前和向后移动,还允许它移动到指定的行或相对于另一行的位置。

更多信息请参见从结果集中检索和修改值。

事务

当一个用户正在访问数据库中的数据时,另一个用户可能同时访问相同的数据。例如,第一个用户正在同时更新表中的某些列,而第二个用户正在从同一表中选择列,这时第二个用户可能会得到部分旧数据和部分更新数据。因此,数据库管理系统使用事务来维护数据的一致状态(数据一致性),同时允许多个用户同时访问数据库(数据并发性)。

事务是由一个或多个 SQL 语句组成的逻辑工作单元。事务以提交或回滚结束,具体取决于数据一致性或数据并发性是否存在问题。提交语句将使事务中 SQL 语句产生的更改永久生效,而回滚语句将撤消事务中 SQL 语句产生的所有更改。

锁是一种机制,阻止两个事务同时操作相同的数据。例如,表锁会阻止在该表上存在未提交事务时删除该表。在某些数据库管理系统中,表锁还会锁定表中的所有行。行锁可以阻止两个事务修改同一行,或者阻止一个事务在另一个事务仍在修改该行时选择该行。

更多信息请参见使用事务。

存储过程

存储过程是一组可以通过名称调用的 SQL 语句。换句话说,它是可执行的代码,一个小型程序,执行特定任务,可以像调用函数或方法一样调用。传统上,存储过程是用特定于数据库管理系统的编程语言编写的。最新一代的数据库产品允许使用 Java 编程语言和 JDBC API 编写存储过程。用 Java 编程语言编写的存储过程在不同数据库管理系统之间是字节码可移植的。一旦编写了存储过程,它就可以被使用和重复使用,因为支持存储过程的数据库管理系统会将其存储在数据库中。查看使用存储过程获取有关编写存储过程的信息。

元数据

数据库存储用户数据,也存储关于数据库本身的信息。大多数数据库管理系统都有一组系统表,列出数据库中的表、每个表中的列名、主键、外键、存储过程等。每个数据库管理系统都有自己的函数来获取有关表布局和数据库功能的信息。JDBC 提供了 DatabaseMetaData 接口,驱动程序编写者必须实现该接口,以便其方法返回有关驱动程序和/或为其编写驱动程序的数据库管理系统的信息。例如,大量的方法返回驱动程序是否支持特定功能。这个接口为用户和工具提供了一种标准化的获取元数据的方式。一般来说,编写工具和驱动程序的开发人员最有可能关注元数据。

课程:JDBC 基础知识

原文:docs.oracle.com/javase/tutorial/jdbc/basics/index.html

在这节课中,您将学习 JDBC API 的基础知识。

  • 入门指南设置了一个基本的数据库开发环境,并展示了如何编译和运行 JDBC 教程示例。

  • 使用 JDBC 处理 SQL 语句概述了处理任何 SQL 语句所需的步骤。接下来的页面将更详细地描述这些步骤:

    • 建立连接连接到您的数据库。

    • 连接到数据源对象展示了如何使用DataSource对象连接到数据库,这是获取数据源连接的首选方式。

    • 处理 SQLException 展示了如何处理由数据库错误引起的异常。

    • 设置表格描述了 JDBC 教程示例中使用的所有数据库表格以及如何使用 JDBC API 和 SQL 脚本创建和填充表格。

    • 从结果集中检索和修改值开发了配置数据库、发送查询和从数据库检索数据的过程。

    • 使用预编译语句描述了创建数据库查询的更灵活方式。

    • 使用事务展示了如何控制数据库查询何时实际执行。

  • 使用 RowSet 对象介绍了RowSet对象;这些对象以一种比结果集更灵活且更易于使用的方式保存表格数据。接下来的页面将描述可用的不同类型的RowSet对象:

    • 使用 JdbcRowSet 对象

    • 使用 CachedRowSet 对象

    • 使用 JoinRowSet 对象

    • 使用 FilteredRowSet 对象

    • 使用 WebRowSet 对象

  • 使用高级数据类型介绍了其他数据类型;接下来的页面将更详细地描述这些数据类型:

    • 使用大对象

    • 使用 SQLXML 对象

    • 使用数组对象

    • 使用 DISTINCT 数据类型

    • 使用结构化对象

    • 使用自定义类型映射

    • 使用数据链接对象

    • 使用行 ID 对象

  • 使用存储过程展示了如何创建和使用存储过程,这是一组可以像调用 Java 方法一样具有可变输入和输出参数的 SQL 语句组。

  • 使用 GUI API 的 JDBC 演示了如何将 JDBC 与 Swing API 集成。

入门指南

原文:docs.oracle.com/javase/tutorial/jdbc/basics/gettingstarted.html

本教程附带的示例代码创建了一个数据库,供一家名为 The Coffee Break 的小咖啡馆的业主使用,该咖啡馆以磅为单位出售咖啡豆,以杯为单位出售冲泡咖啡。

以下步骤配置了一个 JDBC 开发环境,您可以在其中编译和运行教程示例:

  1. 在您的计算机上安装最新版本的 Java SE SDK

  2. 如果需要,安装您的数据库管理系统(DBMS)

  3. 从您的数据库供应商处安装一个 JDBC 驱动程序

  4. 安装 Apache Ant

  5. 安装 Apache Xalan

  6. 下载示例代码

  7. 修改build.xml文件

  8. 修改教程属性文件

  9. 编译和打包示例

  10. 创建数据库、表和填充表

  11. 运行示例

在您的计算机上安装最新版本的 Java SE SDK

在您的计算机上安装最新版本的 Java SE SDK。

确保 Java SE SDK bin目录的完整路径在您的PATH环境变量中,以便您可以从任何目录运行 Java 编译器和 Java 应用程序启动器。

如果需要,请安装您的数据库管理系统(DBMS)

本教程已在以下数据库管理系统上进行测试:

  • Java DB

    注意:Java DB 不再包含在最新版本的 JDK 中。Java DB 是 Apache Derby 的一个重新命名。如果您想使用 Java DB,请从Apache DB 项目下载最新版本。

  • MySQL

请注意,如果您使用其他 DBMS,可能需要修改教程示例的代码。

从您的数据库供应商处安装一个 JDBC 驱动程序

如果您使用 Java DB,则已经带有一个 JDBC 驱动程序。如果您使用 MySQL,请安装最新版本的 MySQL JDBC 驱动程序Connector/J。

联系您的数据库供应商,获取适用于您的 DBMS 的 JDBC 驱动程序。

JDBC 驱动程序有许多可能的实现。这些实现按以下方式分类:

  • 类型 1:将 JDBC API 实现为另一个数据访问 API 的映射的驱动程序,例如 ODBC(开放数据库连接)。这种类型的驱动程序通常依赖于本地库,这限制了它们的可移植性。JDBC-ODBC 桥就是类型 1 驱动程序的一个例子。

    注意:JDBC-ODBC 桥应被视为一个过渡解决方案。Oracle 不支持它。只有在您的数据库管理系统没有提供仅支持 Java 的 JDBC 驱动程序时才考虑使用它。

  • Type 2: 部分用 Java 编程语言编写,部分用本机代码编写的驱动程序。这些驱动程序使用特定于它们连接的数据源的本机客户端库。由于本机代码的存在,它们的可移植性受到限制。Oracle 的 OCI(Oracle Call Interface)客户端驱动程序是 Type 2 驱动程序的一个示例。

  • Type 3: 使用纯 Java 客户端并使用数据库独立协议与中间件服务器通信的驱动程序。然后中间件服务器将客户端的请求传达给数据源。

  • Type 4: 纯 Java 驱动程序,实现特定数据源的网络协议。客户端直接连接到数据源。

检查你的数据库管理系统中带有哪些驱动程序类型。Java DB 带有两种 Type 4 驱动程序,一个是嵌入式驱动程序,另一个是网络客户端驱动程序。MySQL Connector/J 是一种 Type 4 驱动程序。

安装 JDBC 驱动程序通常包括将驱动程序复制到计算机上,然后将其位置添加到类路径中。此外,除了 Type 4 驱动程序之外的许多 JDBC 驱动程序需要你安装客户端 API。通常不需要其他特殊配置。

安装 Apache Ant

这些步骤使用 Apache Ant,一个基于 Java 的工具,来构建、编译和运行 JDBC 教程示例。前往以下链接下载 Apache Ant:

[ant.apache.org/](https://ant.apache.org/)

确保 Apache Ant 可执行文件在你的PATH环境变量中,这样你就可以在任何目录中运行它。

安装 Apache Xalan

示例RSSFeedsTable.java,在使用 SQLXML 对象中有描述,如果你的数据库管理系统是 Java DB,则需要 Apache Xalan。该示例使用 Apache Xalan-Java。前往以下链接下载:

[xml.apache.org/xalan-j/](https://xml.apache.org/xalan-j/)

下载示例代码

示例代码JDBCTutorial.zip包括以下文件:

  • properties

    • javadb-build-properties.xml

    • javadb-sample-properties.xml

    • mysql-build-properties.xml

    • mysql-sample-properties.xml

  • sql

    • javadb

      • create-procedures.sql

      • create-tables.sql

      • drop-tables.sql

      • populate-tables.sql

    • mysql

      • create-procedures.sql

      • create-tables.sql

      • drop-tables.sql

      • populate-tables.sql

  • src/com/oracle/tutorial/jdbc

    • CachedRowSetSample.java

    • CityFilter.java

    • ClobSample.java

    • CoffeesFrame.java

    • CoffeesTable.java

    • CoffeesTableModel.java

    • DatalinkSample.java

    • ExampleRowSetListener.java

    • FilteredRowSetSample.java

    • JdbcRowSetSample.java

    • JDBCTutorialUtilities.java

    • JoinSample.java

    • ProductInformationTable.java

    • RSSFeedsTable.java

    • StateFilter.java

    • StoredProcedureJavaDBSample.java

    • StoredProcedureMySQLSample.java

    • SuppliersTable.java

    • WebRowSetSample.java

  • txt

    • colombian-description.txt
  • xml

    • rss-coffee-industry-news.xml

    • rss-the-coffee-break-blog.xml

  • build.xml

创建一个目录来包含示例的所有文件。这些步骤将此目录称为 *<JDBC 教程目录>*。将 JDBCTutorial.zip 的内容解压缩到 *<JDBC 教程目录>* 中。

修改 build.xml 文件

build.xml 文件是 Apache Ant 用于编译和执行 JDBC 示例的构建文件。文件 properties/javadb-build-properties.xmlproperties/mysql-build-properties.xml 包含 Java DB 和 MySQL 需要的额外 Apache Ant 属性。文件 properties/javadb-sample-properties.xmlproperties/mysql-sample-properties.xml 包含示例所需的属性。

修改这些 XML 文件如下:

修改 build.xml

build.xml 文件中,修改属性 ANTPROPERTIES,指向 properties/javadb-build-properties.xmlproperties/mysql-build-properties.xml,取决于您的 DBMS。例如,如果您正在使用 Java DB,则您的 build.xml 文件将包含以下内容:

<property
  name="ANTPROPERTIES"
  value="properties/javadb-build-properties.xml"/>

  <import file="${ANTPROPERTIES}"/>

同样,如果您正在使用 MySQL,您的 build.xml 文件将包含以下内容:

<property
  name="ANTPROPERTIES"
  value="properties/mysql-build-properties.xml"/>

  <import file="${ANTPROPERTIES}"/>

修改特定于数据库的属性文件

properties/javadb-build-properties.xmlproperties/mysql-build-properties.xml 文件(取决于您的 DBMS),根据以下表格中的描述修改以下属性:

属性 描述
JAVAC 您的 Java 编译器 javac 的完整路径名。
JAVA 您的 Java 运行时可执行文件 java 的完整路径名。
PROPERTIESFILE 属性文件的名称,可以是 properties/javadb-sample-properties.xmlproperties/mysql-sample-properties.xml
MYSQLDRIVER 你的 MySQL 驱动程序的完整路径名。对于 Connector/J,通常是 *<Connector/J 安装目录>*/mysql-connector-java-*版本号*.jar
JAVADBDRIVER 您的 Java DB 驱动程序的完整路径名。通常是 *<Java DB 安装目录>*/lib/derby.jar
XALANDIRECTORY 包含 Apache Xalan 的目录的完整路径名。
CLASSPATH JDBC 教程使用的类路径。您无需更改此值
XALAN 文件 xalan.jar 的完整路径名。
DB.VENDOR 一个值,可以是 derbymysql,取决于您是使用 Java DB 还是 MySQL。教程使用此值来构建连接到 DBMS 和识别特定于 DBMS 的代码和 SQL 语句所需的 URL。
DB.DRIVER JDBC 驱动程序的完全限定类名。对于 Java DB,这是 org.apache.derby.jdbc.EmbeddedDriver。对于 MySQL,这是 com.mysql.cj.jdbc.Driver
DB.HOST 托管您的 DBMS 的计算机的主机名。
DB.PORT 托管您的 DBMS 的计算机的端口号。
DB.SID 教程创建和使用的数据库名称。
DB.URL.NEWDATABASE 创建新数据库时用于连接到您的 DBMS 的连接 URL。您无需更改此值
DB.URL 用于连接到您的 DBMS 的连接 URL。您无需更改此值
DB.USER 具有在 DBMS 中创建数据库权限的用户的名称。
DB.PASSWORD 指定在DB.USER中的用户的密码。
DB.DELIMITER 用于分隔 SQL 语句的字符。不要更改此值。它应该是分号字符(;)。

修改教程属性文件

教程示例使用properties/javadb-sample-properties.xml文件或properties/mysql-sample-properties.xml文件中的值(取决于您的 DBMS)来连接到 DBMS 并初始化数据库和表,如下表所述:

属性 描述
dbms 取值为derbymysql,取决于您是使用 Java DB 还是 MySQL。本教程使用此值来构建连接到 DBMS 所需的 URL,并识别 DBMS 特定的代码和 SQL 语句。
jar_file 包含本教程所有类文件的 JAR 文件的完整路径名。
driver JDBC 驱动程序的完全限定类名。对于 Java DB,这是org.apache.derby.jdbc.EmbeddedDriver。对于 MySQL,这是com.mysql.cj.jdbc.Driver
database_name 教程创建和使用的数据库名称。
user_name 具有在 DBMS 中创建数据库权限的用户的名称。
password 指定在user_name中的用户的密码。
server_name 托管您的 DBMS 的计算机的主机名。
port_number 托管您的 DBMS 的计算机的端口号。

注意:为了简化演示 JDBC API,JDBC 教程示例代码不执行部署系统通常使用的密码管理技术。在生产环境中,您可以遵循 Oracle 数据库密码管理指南并禁用任何示例帐户。请参阅Oracle 数据库安全指南中的应用程序设计中的密码保护部分,了解密码管理指南和其他安全建议。

编译并打包示例

在命令提示符下,将当前目录更改为*<JDBC 教程目录>*。从该目录运行以下命令编译示例并将其打包到一个 jar 文件中:

ant jar

创建数据库、表格,并填充表格

如果您使用的是 MySQL,则运行以下命令来创建数据库:

ant create-mysql-database

注意build.xml文件中不存在用于为 Java DB 创建数据库的相应 Ant 目标。用于建立数据库连接的 Java DB 数据库 URL 包括创建数据库的选项(如果尚不存在)。有关更多信息,请参阅建立连接。

如果您正在使用 Java DB 或 MySQL,则可以从同一目录运行以下命令来删除现有的示例数据库表,重新创建表并填充它们。对于 Java DB,此命令还会在数据库不存在时创建数据库:

ant setup

注意:在运行示例中的 Java 类之前,您应该每次运行ant setup命令。这些示例中的许多示例都期望示例数据库表的内容中有特定的数据。

运行示例

build.xml文件中的每个目标对应于 JDBC 示例中的一个 Java 类或 SQL 脚本。以下表列出了build.xml文件中的目标,每个目标执行的类或脚本,以及每个目标需要的其他类或文件:

Ant 目标 类或 SQL 脚本 其他必需类或文件
javadb-create-procedure javadb/create-procedures.sql;查看build.xml文件以查看运行的其他 SQL 语句 无其他必需文件
mysql-create-procedure mysql/create-procedures.sql 无其他必需文件
run JDBCTutorialUtilities 无其他必需类
runct CoffeesTable JDBCTutorialUtilities
runst SuppliersTable JDBCTutorialUtilities
runjrs JdbcRowSetSample JDBCTutorialUtilities
runcrs CachedRowSetSampleExampleRowSetListener JDBCTutorialUtilities
runjoin JoinSample JDBCTutorialUtilities
runfrs FilteredRowSetSample JDBCTutorialUtilitiesCityFilterStateFilter
runwrs WebRowSetSample JDBCTutorialUtilities
runclob ClobSample JDBCTutorialUtilitiestxt/colombian-description.txt
runrss RSSFeedsTable JDBCTutorialUtilitiesxml目录中包含的 XML 文件
rundl DatalinkSample JDBCTutorialUtilities
runspjavadb StoredProcedureJavaDBSample JDBCTutorialUtilitiesSuppliersTableCoffeesTable
runspmysql StoredProcedureMySQLSample JDBCTutorialUtilitiesSuppliersTableCoffeesTable
runframe CoffeesFrame JDBCTutorialUtilitiesCoffeesTableModel

例如,要运行CoffeesTable类,请将当前目录更改为*<JDBC 教程目录>*,然后从该目录运行以下命令:

ant runct

使用 JDBC 处理 SQL 语句

原文:docs.oracle.com/javase/tutorial/jdbc/basics/processingsqlstatements.html

一般来说,要使用 JDBC 处理任何 SQL 语句,您需要按照以下步骤进行:

  1. 建立连接。

  2. 创建语句。

  3. 执行查询。

  4. 处理ResultSet对象。

  5. 关闭连接。

本页使用教程示例中的方法CoffeesTable.viewTable来演示这些步骤。此方法输出表COFFEES的内容。此方法将在本教程的后续部分中更详细地讨论:

  public static void viewTable(Connection con) throws SQLException {
    String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES";
    try (Statement stmt = con.createStatement()) {
      ResultSet rs = stmt.executeQuery(query);
      while (rs.next()) {
        String coffeeName = rs.getString("COF_NAME");
        int supplierID = rs.getInt("SUP_ID");
        float price = rs.getFloat("PRICE");
        int sales = rs.getInt("SALES");
        int total = rs.getInt("TOTAL");
        System.out.println(coffeeName + ", " + supplierID + ", " + price +
                           ", " + sales + ", " + total);
      }
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    }
  }

建立连接

首先,与要使用的数据源建立连接。数据源可以是 DBMS、传统文件系统或具有相应 JDBC 驱动程序的其他数据源。此连接由Connection对象表示。有关更多信息,请参见建立连接。

创建语句

Statement是表示 SQL 语句的接口。您执行Statement对象,它们生成ResultSet对象,这是表示数据库结果集的数据表。您需要一个Connection对象来创建Statement对象。

例如,CoffeesTable.viewTable使用以下代码创建了一个Statement对象:

stmt = con.createStatement();

有三种不同类型的语句:

  • Statement:用于执行没有参数的简单 SQL 语句。

  • PreparedStatement:(扩展Statement。)用于预编译可能包含输入参数的 SQL 语句。有关更多信息,请参见使用预编译语句。

  • CallableStatement:(扩展PreparedStatement。)用于执行可能包含输入和输出参数的存储过程。有关更多信息,请参见存储过程。

执行查询

要执行查询,请调用Statement中的execute方法,如下所示:

  • execute:如果查询返回的第一个对象是ResultSet对象,则返回true。如果查询可能返回一个或多个ResultSet对象,请使用此方法。通过反复调用Statement.getResultSet检索查询返回的ResultSet对象。

  • executeQuery:返回一个ResultSet对象。

  • executeUpdate:返回一个整数,表示受 SQL 语句影响的行数。如果您使用INSERTDELETEUPDATE SQL 语句,请使用此方法。

例如,CoffeesTable.viewTable使用以下代码执行了一个Statement对象:

ResultSet rs = stmt.executeQuery(query);

有关更多信息,请参见从结果集中检索和修改值。

处理 ResultSet 对象

通过游标访问ResultSet对象中的数据。请注意,此游标不是数据库游标。这个游标是指向ResultSet对象中一行数据的指针。最初,游标位于第一行之前。您调用ResultSet对象中定义的各种方法来移动游标。

例如,CoffeesTable.viewTable重复调用ResultSet.next方法来将游标向前移动一行。每次调用next时,该方法会输出游标当前位置的行中的数据:

      ResultSet rs = stmt.executeQuery(query);
      while (rs.next()) {
        String coffeeName = rs.getString("COF_NAME");
        int supplierID = rs.getInt("SUP_ID");
        float price = rs.getFloat("PRICE");
        int sales = rs.getInt("SALES");
        int total = rs.getInt("TOTAL");
        System.out.println(coffeeName + ", " + supplierID + ", " + price +
                           ", " + sales + ", " + total);
      }
      // ...

查看从结果集中检索和修改值获取更多信息。

关闭连接

当您使用完ConnectionStatementResultSet对象后,请调用其close方法立即释放它正在使用的资源。

或者,可以使用try-with-resources 语句自动关闭ConnectionStatementResultSet对象,无论是否抛出SQLException。(当 JDBC 在与数据源交互时遇到错误时,会抛出SQLException。查看处理 SQL 异常获取更多信息。)自动资源语句由一个try语句和一个或多个声明的资源组成。例如,CoffeesTable.viewTable方法会自动关闭其Statement对象,如下所示:

  public static void viewTable(Connection con) throws SQLException {
    String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES";
    try (Statement stmt = con.createStatement()) {
      ResultSet rs = stmt.executeQuery(query);
      while (rs.next()) {
        String coffeeName = rs.getString("COF_NAME");
        int supplierID = rs.getInt("SUP_ID");
        float price = rs.getFloat("PRICE");
        int sales = rs.getInt("SALES");
        int total = rs.getInt("TOTAL");
        System.out.println(coffeeName + ", " + supplierID + ", " + price +
                           ", " + sales + ", " + total);
      }
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    }
  }

以下语句是一个try-with-resources 语句,声明了一个资源stmt,当try块终止时将自动关闭该资源:

    try (Statement stmt = con.createStatement()) {
      // ...
    }

查看 try-with-resources 语句在 Essential Classes 教程中获取更多信息。

建立连接

原文:docs.oracle.com/javase/tutorial/jdbc/basics/connecting.html

首先,您需要与要使用的数据源建立连接。数据源可以是 DBMS、传统文件系统或其他具有相应 JDBC 驱动程序的数据源。通常,JDBC 应用程序使用以下两个类之一连接到目标数据源:

  • DriverManager:此完全实现的类将应用程序连接到由数据库 URL 指定的数据源。当此类首次尝试建立连接时,它会自动加载类路径中找到的任何 JDBC 4.0 驱动程序。请注意,您的应用程序必须在版本 4.0 之前手动加载任何 JDBC 驱动程序。

  • DataSource:此接口优先于DriverManager,因为它允许底层数据源的详细信息对您的应用程序透明。设置DataSource对象的属性,使其表示特定的数据源。有关更多信息,请参阅使用 DataSource 对象进行连接。有关使用DataSource类开发应用程序的更多信息,请参阅最新版本的Java EE 教程

注意:本教程中的示例使用DriverManager类而不是DataSource类,因为它更容易使用,而且示例不需要DataSource类的功能。

本页涵盖以下主题:

  • 使用 DriverManager 类

  • 指定数据库连接 URL

使用 DriverManager 类

使用DriverManager类连接到您的 DBMS 涉及调用方法DriverManager.getConnection。以下方法,JDBCTutorialUtilities.getConnection,建立数据库连接:

public Connection getConnection() throws SQLException {

    Connection conn = null;
    Properties connectionProps = new Properties();
    connectionProps.put("user", this.userName);
    connectionProps.put("password", this.password);

    if (this.dbms.equals("mysql")) {
        conn = DriverManager.getConnection(
                   "jdbc:" + this.dbms + "://" +
                   this.serverName +
                   ":" + this.portNumber + "/",
                   connectionProps);
    } else if (this.dbms.equals("derby")) {
        conn = DriverManager.getConnection(
                   "jdbc:" + this.dbms + ":" +
                   this.dbName +
                   ";create=true",
                   connectionProps);
    }
    System.out.println("Connected to database");
    return conn;
}

方法DriverManager.getConnection建立数据库连接。此方法需要数据库 URL,具体取决于您的 DBMS。以下是一些数据库 URL 的示例:

  1. MySQL:jdbc:mysql://localhost:3306/,其中localhost是托管数据库的服务器名称,3306是端口号

  2. Java DB:jdbc:derby:*testdb*;create=true,其中*testdb*是要连接的数据库的名称,create=true指示 DBMS 创建数据库。

    注意:此 URL 与 Java DB 嵌入式驱动程序建立数据库连接。Java DB 还包括使用不同 URL 的网络客户端驱动程序。

此方法使用Properties对象指定访问 DBMS 所需的用户名和密码。

注意

  • 通常,在数据库 URL 中,你还会指定要连接的现有数据库的名称。例如,URL jdbc:mysql://localhost:3306/mysql 代表了名为 mysql 的 MySQL 数据库的数据库 URL。本教程中的示例使用不指定特定数据库的 URL,因为示例会创建一个新数据库。

  • 在以前的 JDBC 版本中,要获取连接,你首先需要通过调用方法 Class.forName 来初始化你的 JDBC 驱动程序。这些方法需要一个类型为 java.sql.Driver 的对象。每个 JDBC 驱动程序都包含一个或多个实现接口 java.sql.Driver 的类。Java DB 的驱动程序是 org.apache.derby.jdbc.EmbeddedDriverorg.apache.derby.jdbc.ClientDriver,而 MySQL Connector/J 的驱动程序是 com.mysql.cj.jdbc.Driver。查看你的 DBMS 驱动程序的文档以获取实现接口 java.sql.Driver 的类名。

    任何在你的类路径中找到的 JDBC 4.0 驱动程序都会自动加载。(但是,在 JDBC 4.0 之前,你必须手动加载任何驱动程序,使用方法 Class.forName。)

该方法返回一个 Connection 对象,表示与 DBMS 或特定数据库的连接。通过这个对象查询数据库。

指定数据库连接 URL

数据库连接 URL 是你的 DBMS JDBC 驱动程序用来连接数据库的字符串。它可以包含诸如在哪里搜索数据库、要连接的数据库名称和配置属性等信息。数据库连接 URL 的确切语法由你的 DBMS 指定。

Java DB 数据库连接 URL

以下是 Java DB 的数据库连接 URL 语法:

jdbc:derby:[*subsubprotocol*:][*databaseName*][;*attribute*=*value*]*

  • *subsubprotocol* 指定 Java DB 应在何处搜索数据库,可以是目录、内存、类路径或 JAR 文件。通常会省略。

  • *databaseName* 是要连接的数据库的名称。

  • *attribute*=*value* 表示一个可选的、以分号分隔的属性列表。这些属性使你能够指示 Java DB 执行各种任务,包括以下内容:

    • 创建连接 URL 中指定的数据库。

    • 加密连接 URL 中指定的数据库。

    • 指定存储日志和跟踪信息的目录。

    • 指定用户名和密码以连接到数据库。

查看Java DB 技术文档中的Java DB 开发人员指南Java DB 参考手册以获取更多信息。

MySQL Connector/J 数据库 URL

以下是 MySQL Connector/J 的数据库连接 URL 语法:

jdbc:mysql://[*host*][,*failoverhost*...]
    [:*port*]/[*database*]
    [?*propertyName1*][=*propertyValue1*]
    [&*propertyName2*][=*propertyValue2*]...

  • *host*:*port* 是托管数据库的计算机的主机名和端口号。如果未指定,默认值为 *host**port* 分别为 127.0.0.1 和 3306。

  • *database* 是要连接的数据库的名称。如果未指定,将连接到没有默认数据库的连接。

  • *failover* 是一个备用数据库的名称(MySQL Connector/J 支持故障转移)。

  • *propertyName*=*propertyValue* 表示一个可选的、以&符号分隔的属性列表。这些属性使您能够指示 MySQL Connector/J 执行各种任务。

查看MySQL 参考手册 获取更多信息。

使用 DataSource 对象进行连接

原文:docs.oracle.com/javase/tutorial/jdbc/basics/sqldatasources.html

本节涵盖了DataSource对象,这是获取与数据源连接的首选方法。除了其他优点,稍后将解释的优点之外,DataSource对象还可以提供连接池和分布式事务。这种功能对企业数据库计算至关重要。特别是,它对企业 JavaBeans(EJB)技术至关重要。

本节向您展示如何使用DataSource接口获取连接以及如何使用分布式事务和连接池。这两者在您的 JDBC 应用程序中都涉及非常少的代码更改。

部署执行这些操作的类所需的工作,通常由系统管理员使用工具(如 Apache Tomcat 或 Oracle WebLogic Server)完成,取决于部署的DataSource对象的类型。因此,本节大部分内容都致力于展示系统管理员如何设置环境,以便程序员可以使用DataSource对象获取连接。

下面的主题包括:

  • 使用 DataSource 对象获取连接

  • 部署基本的 DataSource 对象

  • 部署其他 DataSource 实现

  • 获取和使用池化连接

  • 部署分布式事务

  • 使用连接进行分布式事务

使用 DataSource 对象获取连接

在建立连接中,您学习了如何使用DriverManager类获取连接。本节将向您展示如何使用DataSource对象获取与数据源的连接,这是首选方法。

由实现DataSource类的类实例化的对象代表特定的 DBMS 或其他数据源,比如文件。DataSource对象代表特定的 DBMS 或其他数据源,比如文件。如果一个公司使用多个数据源,它将为每个数据源部署一个单独的DataSource对象。DataSource接口由驱动程序供应商实现。它可以以三种不同的方式实现:

  • 基本的DataSource实现会生成标准的未池化或未用于分布式事务的Connection对象。

  • 支持连接池的DataSource实现会生成参与连接池的Connection对象,即可以被回收的连接。

  • 支持分布式事务的DataSource实现会生成可用于分布式事务的Connection对象,即访问两个或多个 DBMS 服务器的事务。

JDBC 驱动程序应至少包含基本的DataSource实现。例如,Java DB JDBC 驱动程序包括org.apache.derby.jdbc.ClientDataSource的实现,而对于 MySQL,则是com.mysql.jdbc.jdbc2.optional.MysqlDataSource。如果您的客户端在 Java 8 紧凑配置文件 2 上运行,则 Java DB JDBC 驱动程序是org.apache.derby.jdbc.BasicClientDataSource40。本教程的示例需要紧凑配置文件 3 或更高版本。

支持分布式事务的DataSource类通常也实现了对连接池的支持。例如,由 EJB 供应商提供的DataSource类几乎总是支持连接池和分布式事务。

假设前面示例中茁壮成长的 The Coffee Break 连锁店的所有者已决定通过互联网进一步扩展销售咖啡。随着预期的大量在线业务,所有者肯定需要连接池。打开和关闭连接涉及大量开销,所有者预计这个在线订购系统将需要大量的查询和更新。通过连接池,一组连接可以一遍又一遍地使用,避免为每次数据库访问创建新连接的开销。此外,所有者现在有第二个包含最近收购的咖啡烘焙公司数据的 DBMS。这意味着所有者希望能够编写使用旧 DBMS 服务器和新 DBMS 服务器的分布式事务。

链店所有者已重新配置计算机系统以服务新的、更大的客户群。所有者已购买最新的 JDBC 驱动程序和与之配套的 EJB 应用服务器,以便使用分布式事务并获得连接池带来的性能提升。许多与最近购买的 EJB 服务器兼容的 JDBC 驱动程序可用。现在,所有者拥有三层架构,中间层有一个新的 EJB 应用服务器和 JDBC 驱动程序,第三层是两个 DBMS 服务器。发出请求的客户端计算机是第一层。

部署基本的 DataSource 对象

系统管理员需要部署DataSource对象,以便 The Coffee Break 的编程团队可以开始使用它们。部署DataSource对象包括三个任务:

  1. 创建DataSource类的实例

  2. 设置其属性

  3. 使用使用 Java 命名和目录接口(JNDI)API 的命名服务进行注册

首先,考虑最基本的情况,即使用DataSource接口的基本实现,即不支持连接池或分布式事务的实现。在这种情况下,只需要部署一个DataSource对象。DataSource的基本实现产生与DriverManager类产生的相同类型的连接。

创建 DataSource 类的实例并设置其属性

假设一家只想要一个基本的DataSource实现的公司从 JDBC 供应商 DB Access, Inc 购买了一个驱动程序。该驱动程序包括实现DataSource接口的类com.dbaccess.BasicDataSource。以下代码摘录创建BasicDataSource类的实例并设置其属性。部署BasicDataSource实例后,程序员可以调用DataSource.getConnection方法获取连接到公司数据库CUSTOMER_ACCOUNTS。首先,系统管理员使用默认构造函数创建BasicDataSource对象*ds。然后,系统管理员设置三个属性。请注意,以下代码通常由部署工具执行:

com.dbaccess.BasicDataSource ds = new com.dbaccess.BasicDataSource();
ds.setServerName("grinder");
ds.setDatabaseName("CUSTOMER_ACCOUNTS");
ds.setDescription("Customer accounts database for billing");

变量*ds*现在代表安装在服务器上的数据库CUSTOMER_ACCOUNTS*ds*对象生成的任何连接都将是到数据库CUSTOMER_ACCOUNTS的连接。

使用使用 JNDI API 的命名服务注册 DataSource 对象

设置属性后,系统管理员可以将BasicDataSource对象注册到 JNDI(Java 命名和目录接口)命名服务中。通常使用的特定命名服务通常由系统属性确定,这里没有显示。以下代码摘录注册BasicDataSource对象并将其绑定到逻辑名称jdbc/billingDB

Context ctx = new InitialContext();
ctx.bind("jdbc/billingDB", ds);

此代码使用 JNDI API。第一行创建一个InitialContext对象,它类似于文件系统中的根目录的起始点。第二行将BasicDataSource对象*ds*与逻辑名称jdbc/billingDB关联或绑定。在下一个代码摘录中,您将向命名服务提供此逻辑名称,它将返回BasicDataSource对象。逻辑名称可以是任何字符串。在这种情况下,公司决定使用名称billingDB作为CUSTOMER_ACCOUNTS数据库的逻辑名称。

在前面的示例中,jdbc是初始上下文下的一个子上下文,就像根目录下的目录是子目录一样。名称jdbc/billingDB类似于路径名,其中路径中的最后一项类似于文件名。在这种情况下,billingDB是赋予BasicDataSource对象*ds*的逻辑名称。子上下文jdbc保留用于绑定到DataSource对象的逻辑名称,因此jdbc将始终是数据源逻辑名称的第一部分。

使用部署的 DataSource 对象

系统管理员部署了基本的DataSource实现后,程序员就可以开始使用了。这意味着程序员可以提供绑定到DataSource类实例的逻辑数据源名称,JNDI 命名服务将返回该DataSource类的实例。然后可以在该DataSource对象上调用getConnection方法,以获取连接到其表示的数据源的连接。例如,程序员可能会编写以下两行代码来获取一个产生与数据库CUSTOMER_ACCOUNTS连接的DataSource对象。

Context ctx = new InitialContext();
DataSource ds = (DataSource)ctx.lookup("jdbc/billingDB");

第一行代码获取一个初始上下文作为检索DataSource对象的起点。当您向lookup方法提供逻辑名称jdbc/billingDB时,该方法将返回系统管理员在部署时绑定到jdbc/billingDBDataSource对象。因为lookup方法的返回值是一个 Java Object,我们必须将其转换为更具体的DataSource类型,然后将其赋给变量*ds*

变量*ds*是实现DataSource接口的com.dbaccess.BasicDataSource类的实例。调用*ds*.getConnection方法将产生与CUSTOMER_ACCOUNTS数据库的连接。

Connection con = ds.getConnection("fernanda","brewed");

getConnection方法仅需要用户名和密码,因为变量*ds*在其属性中具有与CUSTOMER_ACCOUNTS数据库建立连接所需的其余信息,如数据库名称和位置。

DataSource 对象的优势

由于其属性,DataSource对象是获取连接的比DriverManager类更好的选择。程序员不再需要在其应用程序中硬编码驱动程序名称或 JDBC URL,这使它们更具可移植性。此外,DataSource属性使代码维护更加简单。如果有任何更改,系统管理员可以更新数据源属性,而不必担心更改每个连接到数据源的应用程序。例如,如果数据源被移动到不同的服务器,系统管理员只需将serverName属性设置为新服务器名称。

除了可移植性和易维护性之外,使用DataSource对象获取连接还可以提供其他优势。当DataSource接口被实现以与ConnectionPoolDataSource实现一起工作时,由该DataSource类的实例产生的所有连接将自动成为池化连接。同样,当DataSource实现被实现以与XADataSource类一起工作时,它产生的所有连接将自动成为可用于分布式事务的连接。下一节将展示如何部署这些类型的DataSource实现。

部署其他 DataSource 实现

系统管理员或其他从事该职能的人可以部署一个DataSource对象,以便它产生的连接是连接池连接。为此,他或她首先部署一个ConnectionPoolDataSource对象,然后部署一个实现与其配合工作的DataSource对象。设置ConnectionPoolDataSource对象的属性,以便它表示将生成连接的数据源。在ConnectionPoolDataSource对象已经注册到 JNDI 命名服务后,部署DataSource对象。通常只需设置DataSource对象的两个属性:descriptiondataSourceName。给定给dataSourceName属性的值是先前部署的标识ConnectionPoolDataSource对象的逻辑名称,该对象包含用于建立连接所需的属性。

使用ConnectionPoolDataSourceDataSource对象部署后,您可以在DataSource对象上调用DataSource.getConnection方法并获得一个连接池连接。此连接将连接到ConnectionPoolDataSource对象属性中指定的数据源。

以下示例描述了如何为 The Coffee Break 的系统管理员部署一个实现提供连接池连接的DataSource对象。系统管理员通常会使用部署工具,因此本节中显示的代码片段是部署工具将执行的代码。

为了获得更好的性能,The Coffee Break 公司从 DB Access, Inc.购买了一个 JDBC 驱动程序,其中包括实现ConnectionPoolDataSource接口的com.dbaccess.ConnectionPoolDS类。系统管理员创建此类的实例,设置其属性,并将其注册到 JNDI 命名服务中。The Coffee Break 从其 EJB 服务器供应商 Application Logic, Inc.购买了其DataSourcecom.applogic.PooledDataSourcecom.applogic.PooledDataSource类通过使用ConnectionPoolDataSourcecom.dbaccess.ConnectionPoolDS提供的底层支持来实现连接池。

必须首先部署ConnectionPoolDataSource对象。以下代码创建了com.dbaccess.ConnectionPoolDS的实例并设置其属性:

com.dbaccess.ConnectionPoolDS cpds = new com.dbaccess.ConnectionPoolDS();
cpds.setServerName("creamer");
cpds.setDatabaseName("COFFEEBREAK");
cpds.setPortNumber(9040);
cpds.setDescription("Connection pooling for " + "COFFEEBREAK DBMS");

在部署ConnectionPoolDataSource对象后,系统管理员部署DataSource对象。以下代码将com.dbaccess.ConnectionPoolDS对象*cpds*注册到 JNDI 命名服务中。请注意,与*cpds*变量关联的逻辑名称在jdbc的子上下文下添加了pool,类似于在分层文件系统中的另一个子目录下添加子目录。com.dbaccess.ConnectionPoolDS类的任何实例的逻辑名称始终以jdbc/pool开头。Oracle 建议将所有ConnectionPoolDataSource对象放在jdbc/pool子上下文下:

Context ctx = new InitialContext();
ctx.bind("jdbc/pool/fastCoffeeDB", cpds);

接下来,实现与*cpds*变量和com.dbaccess.ConnectionPoolDS类的其他实例交互的DataSource类被部署。下面的代码创建了这个类的一个实例并设置了其属性。请注意,为com.applogic.PooledDataSource的这个实例只设置了两个属性。设置description属性是因为它总是必需的。另一个设置的属性是dataSourceName,为*cpds*给出了逻辑 JNDI 名称,它是com.dbaccess.ConnectionPoolDS类的一个实例。换句话说,*cpds*代表了将为DataSource对象实现连接池的ConnectionPoolDataSource对象。

下面的代码,可能会被部署工具执行,创建了一个PooledDataSource对象,设置了其属性,并将其绑定到逻辑名称jdbc/fastCoffeeDB

com.applogic.PooledDataSource ds = new com.applogic.PooledDataSource();
ds.setDescription("produces pooled connections to COFFEEBREAK");
ds.setDataSourceName("jdbc/pool/fastCoffeeDB");
Context ctx = new InitialContext();
ctx.bind("jdbc/fastCoffeeDB", ds);

此时,部署了一个DataSource对象,应用程序可以从中获取到数据库COFFEEBREAK的连接池连接。

获取和使用连接池连接

连接池是数据库连接对象的缓存。这些对象代表了可以被应用程序用来连接数据库的物理数据库连接。在运行时,应用程序从连接池请求连接。如果连接池包含可以满足请求的连接,则将连接返回给应用程序。如果找不到连接,则创建一个新连接并返回给应用程序。应用程序使用连接对数据库执行一些工作,然后将对象返回到连接池。连接随后可用于下一个连接请求。

连接池促进了连接对象的重复使用,减少了连接对象被创建的次数。连接池显著提高了数据库密集型应用的性能,因为创建连接对象在时间和资源方面都是昂贵的。

现在这些DataSourceConnectionPoolDataSource对象已经部署,程序员可以使用DataSource对象获取连接池连接。获取连接池连接的代码与获取非连接池连接的代码类似,如下两行所示:

ctx = new InitialContext();
ds = (DataSource)ctx.lookup("jdbc/fastCoffeeDB");

变量*ds*代表一个DataSource对象,用于向数据库COFFEEBREAK生成连接池连接。您只需要检索一次这个DataSource对象,因为您可以使用它生成所需数量的连接池连接。在*ds*变量上调用getConnection方法会自动生成一个连接池连接,因为*ds*变量表示的DataSource对象被配置为生成连接池连接。

连接池对程序员通常是透明的。在使用连接池连接时,只需要做两件事情:

  1. 使用DataSource对象而不是DriverManager类来获取连接。在下面的代码行中,*ds*是一个已实现和部署的DataSource对象,它将创建池化连接,usernamepassword是代表具有访问数据库权限的用户的凭据的变量:

    Connection con = ds.getConnection(username, password);
    
    
  2. 使用finally语句来关闭池化连接。下面的finally块将出现在应用于使用池化连接的代码的try/catch块之后:

    try {
        Connection con = ds.getConnection(username, password);
        // ... code to use the pooled
        // connection con
    } catch (Exception ex {
        // ... code to handle exceptions
    } finally {
        if (con != null) con.close();
    }
    
    

否则,使用池化连接的应用程序与使用常规连接的应用程序相同。当进行连接池化时,应用程序员可能注意到的唯一其他事情是性能更好。

以下示例代码获取一个DataSource对象,该对象生成到数据库COFFEEBREAK的连接,并使用它来更新表COFFEES中的价格:

import java.sql.*;
import javax.sql.*;
import javax.ejb.*;
import javax.naming.*;

public class ConnectionPoolingBean implements SessionBean {

    // ...

    public void ejbCreate() throws CreateException {
        ctx = new InitialContext();
        ds = (DataSource)ctx.lookup("jdbc/fastCoffeeDB");
    }

    public void updatePrice(float price, String cofName,
                            String username, String password)
        throws SQLException{

        Connection con;
        PreparedStatement pstmt;
        try {
            con = ds.getConnection(username, password);
            con.setAutoCommit(false);
            pstmt = con.prepareStatement("UPDATE COFFEES " +
                        "SET PRICE = ? " +
                        "WHERE COF_NAME = ?");
            pstmt.setFloat(1, price);
            pstmt.setString(2, cofName);
            pstmt.executeUpdate();

            con.commit();
            pstmt.close();

        } finally {
            if (con != null) con.close();
        }
    }

    private DataSource ds = null;
    private Context ctx = null;
}

此代码示例中的连接参与连接池化,因为以下条件为真:

  • 已部署了一个实现ConnectionPoolDataSource的类的实例。

  • 已部署了一个实现DataSource的类的实例,并为其dataSourceName属性设置的值是之前部署的ConnectionPoolDataSource对象绑定的逻辑名称。

请注意,尽管这段代码与您之前看到的代码非常相似,但在以下方面有所不同:

  • 它导入了javax.sqljavax.ejbjavax.naming包,以及java.sql

    DataSourceConnectionPoolDataSource接口位于javax.sql包中,JNDI 构造函数InitialContext和方法Context.lookup属于javax.naming包。这个特定的示例代码是以一个使用javax.ejb包中的 API 的 EJB 组件的形式呈现的。这个示例的目的是展示您使用池化连接的方式与使用非池化连接的方式相同,因此您不必担心理解 EJB API。

  • 它使用DataSource对象来获取连接,而不是使用DriverManager工具。

  • 它使用一个finally块来确保连接被关闭。

获取和使用池化连接与获取和使用常规连接类似。当某人作为系统管理员部署了一个ConnectionPoolDataSource对象和一个正确的DataSource对象时,应用程序将使用该DataSource对象来获取池化连接。然而,应用程序应该使用一个finally块来关闭池化连接。为简单起见,前面的示例使用了一个finally块但没有catch块。如果try块中的方法抛出异常,它将默认抛出,并且finally子句将在任何情况下执行。

部署分布式事务

可以部署DataSource对象以获取可用于分布式事务的连接。与连接池一样,必须部署两个不同的类实例:一个XADataSource对象和一个实现与其一起工作的DataSource对象。

假设 The Coffee Break 企业家购买的 EJB 服务器包括com.applogic.TransactionalDS类,该类与com.dbaccess.XATransactionalDSXADataSource类一起工作。它可以与任何XADataSource类一起工作,使 EJB 服务器在 JDBC 驱动程序之间具有可移植性。当部署DataSourceXADataSource对象时,生成的连接将能够参与分布式事务。在这种情况下,com.applogic.TransactionalDS类被实现为生成的连接也是池化连接,这通常是作为 EJB 服务器实现的一部分提供的DataSource类的情况。

必须首先部署XADataSource对象。以下代码创建com.dbaccess.XATransactionalDS的一个实例并设置其属性:

com.dbaccess.XATransactionalDS xads = new com.dbaccess.XATransactionalDS();
xads.setServerName("creamer");
xads.setDatabaseName("COFFEEBREAK");
xads.setPortNumber(9040);
xads.setDescription("Distributed transactions for COFFEEBREAK DBMS");

以下代码将com.dbaccess.XATransactionalDS对象*xads*注册到 JNDI 命名服务。请注意,与*xads*关联的逻辑名称在jdbc下添加了子上下文xa。Oracle 建议com.dbaccess.XATransactionalDS类的任何实例的逻辑名称始终以jdbc/xa开头。

Context ctx = new InitialContext();
ctx.bind("jdbc/xa/distCoffeeDB", xads);

部署实现与*xads*和其他XADataSource对象交互的DataSource对象。请注意,DataSourcecom.applogic.TransactionalDS可以与任何 JDBC 驱动程序供应商的XADataSource类一起使用。部署DataSource对象涉及创建com.applogic.TransactionalDS类的实例并设置其属性。dataSourceName属性设置为jdbc/xa/distCoffeeDB,这是与com.dbaccess.XATransactionalDS关联的逻辑名称。这是实现DataSource类的分布式事务能力的XADataSource类。以下代码部署DataSource类的一个实例:

com.applogic.TransactionalDS ds = new com.applogic.TransactionalDS();
ds.setDescription("Produces distributed transaction " +
                  "connections to COFFEEBREAK");
ds.setDataSourceName("jdbc/xa/distCoffeeDB");
Context ctx = new InitialContext();
ctx.bind("jdbc/distCoffeeDB", ds);

现在已经部署了com.applogic.TransactionalDScom.dbaccess.XATransactionalDS类的实例,应用程序可以调用TransactionalDS类的实例上的getConnection方法,以获取连接到COFFEEBREAK数据库的连接,该连接可用于分布式事务。

使用连接进行分布式事务

要获取可用于分布式事务的连接,必须使用已经正确实现和部署的DataSource对象,如部署分布式事务部分所示。使用这样的DataSource对象,调用其上的getConnection方法。获得连接后,使用它就像使用任何其他连接一样。因为jdbc/distCoffeesDB已经与 JNDI 命名服务中的XADataSource对象关联,因此以下代码生成一个可用于分布式事务的Connection对象:

Context ctx = new InitialContext();
DataSource ds = (DataSource)ctx.lookup("jdbc/distCoffeesDB");
Connection con = ds.getConnection();

在连接作为分布式事务的一部分时,对其使用有一些次要但重要的限制。事务管理器控制分布式事务何时开始以及何时提交或回滚;因此,应用程序代码永远不应调用Connection.commitConnection.rollback方法。应用程序也不应调用Connection.setAutoCommit(true),这会启用自动提交模式,因为这也会干扰事务管理器对事务边界的控制。这就解释了为什么在分布式事务范围内创建的新连接默认情况下会禁用其自动提交模式。请注意,这些限制仅适用于连接参与分布式事务时;连接不参与分布式事务时没有限制。

对于以下示例,假设已经发货一份咖啡订单,这将触发对驻留在不同 DBMS 服务器上的两个表进行更新。第一个表是一个新的INVENTORY表,第二个是COFFEES表。因为这些表位于不同的 DBMS 服务器上,涉及它们两个的事务将是一个分布式事务。以下示例中的代码获取连接,更新COFFEES表,并关闭连接,是分布式事务的第二部分。

注意,代码并没有显式提交或回滚更新,因为分布式事务的范围由中间层服务器的基础系统基础设施控制。此外,假设用于分布式事务的连接是一个连接池连接,应用程序使用finally块来关闭连接。这样可以确保即使抛出异常,也会关闭有效连接,从而确保连接被返回到连接池以进行回收利用。

以下代码示例说明了一个企业 Bean,它是一个实现了可以被客户端计算机调用的方法的类。这个示例的目的是演示分布式事务的应用代码与其他代码没有任何不同,只是它不调用Connection方法commitrollbacksetAutoCommit(true)。因此,您不需要担心理解所使用的 EJB API。

import java.sql.*;
import javax.sql.*;
import javax.ejb.*;
import javax.naming.*;

public class DistributedTransactionBean implements SessionBean {

    // ...

    public void ejbCreate() throws CreateException {

        ctx = new InitialContext();
        ds = (DataSource)ctx.lookup("jdbc/distCoffeesDB");
    }

    public void updateTotal(int incr, String cofName, String username,
                            String password)
        throws SQLException {

        Connection con;
        PreparedStatement pstmt;

        try {
            con = ds.getConnection(username, password);
            pstmt = con.prepareStatement("UPDATE COFFEES " +
                        "SET TOTAL = TOTAL + ? " +
                        "WHERE COF_NAME = ?");
            pstmt.setInt(1, incr);
            pstmt.setString(2, cofName);
            pstmt.executeUpdate();
            stmt.close();
        } finally {
            if (con != null) con.close();
        }
    }

    private DataSource ds = null;
    private Context ctx = null;
}

处理 SQLException

原文:docs.oracle.com/javase/tutorial/jdbc/basics/sqlexception.html

本页涵盖以下主题:

  • SQLException 概述

  • 检索异常

  • 检索警告

  • 分类的 SQLException

  • SQLException 的其他子类

SQLException 概述

当 JDBC 在与数据源交互过程中遇到错误时,会抛出一个SQLException实例,而不是Exception。(在这种情况下,数据源代表Connection对象连接的数据库。)SQLException实例包含以下信息,可以帮助您确定错误的原因:

  • 错误描述。通过调用方法SQLException.getMessage检索包含此描述的String对象。

  • SQLState 代码。这些代码及其相应的含义已经由 ISO/ANSI 和 Open Group(X/Open)标准化,尽管一些代码已保留供数据库供应商自行定义。这个String对象由五个字母数字字符组成。通过调用方法SQLException.getSQLState检索此代码。

  • 错误代码。这是一个整数值,标识导致抛出SQLException实例的错误。其值和含义是特定于实现的,可能是底层数据源返回的实际错误代码。通过调用方法SQLException.getErrorCode检索错误。

  • 一个原因。SQLException实例可能具有因果关系,由导致抛出SQLException实例的一个或多个Throwable对象组成。要浏览这些原因链,递归调用方法SQLException.getCause,直到返回一个null值。

  • 任何链接异常的引用。如果发生多个错误,这些异常通过此链引用。通过在抛出异常上调用方法SQLException.getNextException检索这些异常。

检索异常

下面的方法,JDBCTutorialUtilities.printSQLException,输出了SQLException中包含的 SQLState、错误代码、错误描述以及原因(如果有的话),以及与之链接的任何其他异常:

public static void printSQLException(SQLException ex) {

    for (Throwable e : ex) {
        if (e instanceof SQLException) {
            if (ignoreSQLException(
                ((SQLException)e).
                getSQLState()) == false) {

                e.printStackTrace(System.err);
                System.err.println("SQLState: " +
                    ((SQLException)e).getSQLState());

                System.err.println("Error Code: " +
                    ((SQLException)e).getErrorCode());

                System.err.println("Message: " + e.getMessage());

                Throwable t = ex.getCause();
                while(t != null) {
                    System.out.println("Cause: " + t);
                    t = t.getCause();
                }
            }
        }
    }
}

例如,如果您使用 Java DB 调用方法CoffeesTable.dropTable,表COFFEES不存在,并且您删除对JDBCTutorialUtilities.ignoreSQLException的调用,输出将类似于以下内容:

SQLState: 42Y55
Error Code: 30000
Message: 'DROP TABLE' cannot be performed on
'TESTDB.COFFEES' because it does not exist.

您可以先检索 SQLState,然后相应地处理 SQLException,而不是打印 SQLException 信息。例如,如果 SQLState 等于代码 42Y55(并且您正在使用 Java DB 作为您的 DBMS),则方法 JDBCTutorialUtilities.ignoreSQLException 返回 true,导致 JDBCTutorialUtilities.printSQLException 忽略 SQLException

public static boolean ignoreSQLException(String sqlState) {

    if (sqlState == null) {
        System.out.println("The SQL state is not defined!");
        return false;
    }

    // X0Y32: Jar file already exists in schema
    if (sqlState.equalsIgnoreCase("X0Y32"))
        return true;

    // 42Y55: Table already exists in schema
    if (sqlState.equalsIgnoreCase("42Y55"))
        return true;

    return false;
}

检索警告

SQLWarning 对象是处理数据库访问警告的 SQLException 子类。警告不会像异常那样停止应用程序的执行;它们只是提醒用户某些事情未按计划发生。例如,警告可能会告诉您尝试撤销的权限未被撤销。或者警告可能会告诉您在请求的断开连接期间发生了错误。

可以在 Connection 对象、Statement 对象(包括 PreparedStatementCallableStatement 对象)或 ResultSet 对象上报告警告。这些类中的每一个都有一个 getWarnings 方法,您必须调用它以查看调用对象上报的第一个警告。如果 getWarnings 返回一个警告,您可以调用它的 getNextWarning 方法来获取任何额外的警告。执行语句会自动清除先前语句的警告,因此它们不会累积。然而,这意味着如果您想要检索在语句上报告的警告,您必须在执行另一个语句之前这样做。

JDBCTutorialUtilities.java 中的以下方法演示了如何获取关于 StatementResultSet 对象上报的任何警告的完整信息:

public static void getWarningsFromResultSet(ResultSet rs)
    throws SQLException {
    JDBCTutorialUtilities.printWarnings(rs.getWarnings());
}

public static void getWarningsFromStatement(Statement stmt)
    throws SQLException {
    JDBCTutorialUtilities.printWarnings(stmt.getWarnings());
}

public static void printWarnings(SQLWarning warning)
    throws SQLException {

    if (warning != null) {
        System.out.println("\n---Warning---\n");

    while (warning != null) {
        System.out.println("Message: " + warning.getMessage());
        System.out.println("SQLState: " + warning.getSQLState());
        System.out.print("Vendor error code: ");
        System.out.println(warning.getErrorCode());
        System.out.println("");
        warning = warning.getNextWarning();
    }
}

最常见的警告是 DataTruncation 警告,是 SQLWarning 的子类。所有 DataTruncation 对象的 SQLState 都是 01004,表示读取或写入数据时出现问题。DataTruncation 方法让您找出数据在哪一列或参数被截断,截断是在读取还是写入操作中发生的,应该传输多少字节,实际传输了多少字节。

分类的 SQLExceptions

您的 JDBC 驱动程序可能会抛出与常见 SQLState 或与特定 SQLState 类值不相关的常见错误状态对应的 SQLException 子类。这使您能够编写更具可移植性的错误处理代码。这些异常是以下类的子类之一:

  • SQLNonTransientException

  • SQLTransientException

  • SQLRecoverableException

查看 java.sql 包的最新 Javadoc 或您的 JDBC 驱动程序的文档,以获取有关这些子类的更多信息。

其他 SQLException 的子类

以下 SQLException 的子类也可能被抛出:

  • BatchUpdateException 在批量更新操作期间发生错误时抛出。除了SQLException提供的信息外,BatchUpdateException还提供了在错误发生之前执行的所有语句的更新计数。

  • SQLClientInfoException 在连接上无法设置一个或多个客户端信息属性时抛出。除了SQLException提供的信息外,SQLClientInfoException还提供了未设置的客户端信息属性列表。

设置表

原文:docs.oracle.com/javase/tutorial/jdbc/basics/tables.html

本页描述了 JDBC 教程中使用的所有表以及如何创建它们:

  • 咖啡表

  • 供应商表

  • 咖啡库存表

  • 商品库存表

  • 咖啡店表

  • 数据存储库表

  • 创建表

  • 填充表

咖啡表

COFFEES表存储了在 The Coffee Break 可供销售的咖啡信息:

COF_NAME SUP_ID PRICE SALES TOTAL
哥伦比亚 101 7.99 0 0
法式烘焙 49 8.99 0 0
浓缩咖啡 150 9.99 0 0
哥伦比亚无咖啡因 101 8.99 0 0
法式烘焙无咖啡因 49 9.99 0 0

以下描述了COFFEES表中的每列:

  • COF_NAME: 存储咖啡名称。具有 SQL 类型为 VARCHAR,最大长度为 32 个字符。由于每种咖啡的名称都不同,因此名称唯一标识特定的咖啡,并作为主键。

  • SUP_ID: 存储标识咖啡供应商的数字。具有 SQL 类型为 INTEGER。它被定义为引用SUPPLIERS表中SUP_ID列的外键。因此,DBMS 将强制执行此列中的每个值与SUPPLIERS表中相应列中的值之一匹配。

  • PRICE: 存储每磅咖啡的成本。具有 SQL 类型为 FLOAT,因为它需要存储带有小数点的值。(请注意,货币值通常存储在 SQL 类型 DECIMALNUMERIC 中,但由于不同的 DBMS 之间存在差异,并且为了避免与 JDBC 的早期版本不兼容,本教程使用更标准的类型 FLOAT。)

  • SALES: 存储本周销售的咖啡磅数。具有 SQL 类型为 INTEGER

  • TOTAL: 存储迄今为止销售的咖啡磅数。具有 SQL 类型为 INTEGER

供应商表

SUPPLIERS表存储了每个供应商的信息:

SUP_ID SUP_NAME STREET CITY STATE ZIP
101 Acme, Inc. 99 Market Street Groundsville CA 95199
49 优质咖啡 1 Party Place Mendocino CA 95460
150 高地咖啡 100 Coffee Lane Meadows CA 93966

以下描述了SUPPLIERS表中的每列:

  • SUP_ID: 存储标识咖啡供应商的数字。具有 SQL 类型为 INTEGER。这是此表中的主键。

  • SUP_NAME: 存储咖啡供应商的名称。

  • STREETCITYSTATEZIP:这些列存储咖啡供应商的地址。

咖啡库存表

COF_INVENTORY表存储了每个仓库中咖啡的数量信息:

WAREHOUSE_ID COF_NAME SUP_ID QUAN DATE_VAL
1234 House_Blend 49 0 2006_04_01
1234 House_Blend_Decaf 49 0 2006_04_01
1234 哥伦比亚 101 0 2006_04_01
1234 法式烘焙 49 0 2006_04_01
1234 浓缩咖啡 150 0 2006_04_01
1234 Colombian_Decaf 101 0 2006_04_01

以下描述了COF_INVENTORY表中每一列的含义:

  • WAREHOUSE_ID: 存储标识仓库的数字。

  • COF_NAME: 存储特定类型咖啡的名称。

  • SUP_ID: 存储标识供应商的数字。

  • QUAN: 存储表示商品数量的数字。

  • DATE: 存储时间戳数值,表示行最后更新时间。

MERCH_INVENTORY

MERCH_INVENTORY表存储有关库存中非咖啡商品数量的信息:

ITEM_ID ITEM_NAME SUP_ID QUAN DATE
00001234 大杯 00456 28 2006_04_01
00001235 小杯 00456 36 2006_04_01
00001236 碟子 00456 64 2006_04_01
00001287 咖啡壶 00456 12 2006_04_01
00006931 咖啡壶 00927 3 2006_04_01
00006935 隔热垫 00927 88 2006_04_01
00006977 餐巾纸 00927 108 2006_04_01
00006979 毛巾 00927 24 2006_04_01
00004488 咖啡机 08732 5 2006_04_01
00004490 咖啡研磨机 08732 9 2006_04_01
00004495 咖啡机 08732 4 2006_04_01
00006914 食谱书 00927 12 2006_04_01

以下描述了MERCH_INVENTORY表中每一列的含义:

  • ITEM_ID: 存储标识物品的数字。

  • ITEM_NAME: 存储物品的名称。

  • SUP_ID: 存储标识供应商的数字。

  • QUAN: 存储表示该物品可用数量的数字。

  • DATE: 存储时间戳数值,表示行最后更新时间。

COFFEE_HOUSES

COFFEE_HOUSES表存储咖啡店的位置信息:

STORE_ID CITY COFFEE MERCH TOTAL
10023 门多西诺 3450 2005 5455
33002 西雅图 4699 3109 7808
10040 旧金山 5386 2841 8227
32001 波特兰 3147 3579 6726
10042 旧金山 2863 1874 4710
10024 萨克拉门托 1987 2341 4328
10039 卡梅尔 2691 1121 3812
10041 洛杉矶 1533 1007 2540
33005 奥林匹亚 2733 1550 4283
33010 西雅图 3210 2177 5387
10035 旧金山 1922 1056 2978
10037 洛杉矶 2143 1876 4019
10034 圣何塞 1234 1032 2266
32004 Eugene 1356 1112 2468

以下描述了COFFEE_HOUSES表中每一列的含义:

  • STORE_ID: 存储标识咖啡店的数字。它表示咖啡店所在州,以 10 开头的值表示加利福尼亚州,以 32 开头的值表示俄勒冈州,以 33 开头的值表示华盛顿州。

  • CITY: 存储咖啡馆所在城市的名称。

  • COFFEE: 存储表示售出咖啡量的数字。

  • MERCH: 存储表示售出商品量的数字。

  • TOTAL: 存储表示售出咖啡和商品总量的数字。

DATA_REPOSITORY 表

表 DATA_REPOSITORY 存储引用文档和其他与 The Coffee Break 有关的数据的 URL。脚本 populate_tables.sql 不向此表添加任何数据。以下描述了此表中每列的内容:

  • DOCUMENT_NAME: 存储标识 URL 的字符串。

  • URL: 存储一个 URL。

创建表

您可以使用 Apache Ant 或 JDBC API 创建表。

使用 Apache Ant 创建表

要创建教程示例代码中使用的表,请在目录 *<JDBC tutorial directory>* 中运行以下命令:

ant setup

此命令运行多个 Ant 目标,包括以下内容,build-tables(来自 build.xml 文件):

<target name="build-tables"
  description="Create database tables">
  <sql
    driver="${DB.DRIVER}"
    url="${DB.URL}"
    userid="${DB.USER}"
    password="${DB.PASSWORD}"
    classpathref="CLASSPATH"
    delimiter="${DB.DELIMITER}"
    autocommit="false" onerror="abort">
    <transaction src=
  "./sql/${DB.VENDOR}/create-tables.sql"/>
  </sql>
</target>

示例为以下 sql Ant 任务参数指定值:

Parameter 描述
driver 您的 JDBC 驱动程序的完全限定类名。此示例中,Java DB 使用 org.apache.derby.jdbc.EmbeddedDriver,MySQL Connector/J 使用 com.mysql.cj.jdbc.Driver
url 数据库连接 URL,您的 DBMS JDBC 驱动程序用于连接到数据库的 URL。
userid 您的 DBMS 中有效用户的名称。
password userid 中指定用户的密码
classpathref 包含 driver 中指定的类的 JAR 文件的完整路径名
delimiter 分隔 SQL 语句的字符串或字符。此示例使用分号 (;)。
autocommit 布尔值;如果设置为 false,则所有 SQL 语句将作为一个事务执行。
onerror 当语句失败时执行的操作;可能的值为 continuestopabort。值 abort 指定如果发生错误,则事务将被中止。

示例将这些参数的值存储在单独的文件中。构建文件 build.xml 使用 import 任务检索这些值:

<import file="${ANTPROPERTIES}"/>

transaction 元素指定一个包含要执行的 SQL 语句的文件。文件 create-tables.sql 包含创建本页描述的所有表的 SQL 语句。例如,此文件的以下摘录创建表 SUPPLIERSCOFFEES

create table SUPPLIERS
    (SUP_ID integer NOT NULL,
    SUP_NAME varchar(40) NOT NULL,
    STREET varchar(40) NOT NULL,
    CITY varchar(20) NOT NULL,
    STATE char(2) NOT NULL,
    ZIP char(5),
    PRIMARY KEY (SUP_ID));

create table COFFEES
    (COF_NAME varchar(32) NOT NULL,
    SUP_ID int NOT NULL,
    PRICE numeric(10,2) NOT NULL,
    SALES integer NOT NULL,
    TOTAL integer NOT NULL,
    PRIMARY KEY (COF_NAME),
    FOREIGN KEY (SUP_ID)
        REFERENCES SUPPLIERS (SUP_ID));

注意:文件 build.xml 包含另一个名为 drop-tables 的目标,用于删除教程中使用的表。setup 目标在运行 build-tables 目标之前运行 drop-tables

使用 JDBC API 创建表

以下方法,SuppliersTable.createTable,创建 SUPPLIERS 表:

  public void createTable() throws SQLException {
    String createString =
      "create table SUPPLIERS " + "(SUP_ID integer NOT NULL, " +
      "SUP_NAME varchar(40) NOT NULL, " + "STREET varchar(40) NOT NULL, " +
      "CITY varchar(20) NOT NULL, " + "STATE char(2) NOT NULL, " +
      "ZIP char(5), " + "PRIMARY KEY (SUP_ID))";

    try (Statement stmt = con.createStatement()) {
      stmt.executeUpdate(createString);
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    }
  }

以下方法,CoffeesTable.createTable,创建 COFFEES 表:

  public void createTable() throws SQLException {
    String createString =
      "create table COFFEES " + "(COF_NAME varchar(32) NOT NULL, " +
      "SUP_ID int NOT NULL, " + "PRICE numeric(10,2) NOT NULL, " +
      "SALES integer NOT NULL, " + "TOTAL integer NOT NULL, " +
      "PRIMARY KEY (COF_NAME), " +
      "FOREIGN KEY (SUP_ID) REFERENCES SUPPLIERS (SUP_ID))";
    try (Statement stmt = con.createStatement()) {
      stmt.executeUpdate(createString);
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    }
  }

在这两种方法中,con是一个Connection对象,dbName是你正在创建表的数据库的名称。

要执行 SQL 查询,比如由String createString指定的查询,使用一个Statement对象。要创建一个Statement对象,从现有的Connection对象调用方法Connection.createStatement。要执行 SQL 查询,调用方法Statement.executeUpdate

所有的Statement对象在创建它们的连接关闭时都会被关闭。然而,明确关闭Statement对象是良好的编程实践,一旦你完成了它们的使用就立即关闭。这样可以立即释放语句正在使用的任何外部资源。通过调用方法Statement.close来关闭一个语句。将这个语句放在finally中以确保即使正常程序流被中断,比如抛出异常(比如SQLException),它也会关闭。

注意:在COFFEES之前必须先创建SUPPLIERS表,因为COFFEES包含一个外键SUP_ID,它引用SUPPLIERS

填充表

同样,你可以使用 Apache Ant 或 JDBC API 向表中插入数据。

使用 Apache Ant 填充表

除了创建本教程使用的表之外,命令ant setup还会填充这些表。这个命令运行 Ant 目标populate-tables,该目标运行 SQL 脚本populate-tables.sql

下面是从populate-tables.sql中填充SUPPLIERSCOFFEES表的摘录:

insert into SUPPLIERS values(
    49, 'Superior Coffee', '1 Party Place',
    'Mendocino', 'CA', '95460');
insert into SUPPLIERS values(
    101, 'Acme, Inc.', '99 Market Street',
    'Groundsville', 'CA', '95199');
insert into SUPPLIERS values(
    150, 'The High Ground',
    '100 Coffee Lane', 'Meadows', 'CA', '93966');
insert into COFFEES values(
    'Colombian', 00101, 7.99, 0, 0);
insert into COFFEES values(
    'French_Roast', 00049, 8.99, 0, 0);
insert into COFFEES values(
    'Espresso', 00150, 9.99, 0, 0);
insert into COFFEES values(
    'Colombian_Decaf', 00101, 8.99, 0, 0);
insert into COFFEES values(
    'French_Roast_Decaf', 00049, 9.99, 0, 0);

使用 JDBC API 填充表

下面的方法,SuppliersTable.populateTable,将数据插入表中:

  public void populateTable() throws SQLException {
    try (Statement stmt = con.createStatement()) {
      stmt.executeUpdate("insert into SUPPLIERS " +
                         "values(49, 'Superior Coffee', '1 Party Place', " +
                         "'Mendocino', 'CA', '95460')");
      stmt.executeUpdate("insert into SUPPLIERS " +
                         "values(101, 'Acme, Inc.', '99 Market Street', " +
                         "'Groundsville', 'CA', '95199')");
      stmt.executeUpdate("insert into SUPPLIERS " +
                         "values(150, 'The High Ground', '100 Coffee Lane', " +
                         "'Meadows', 'CA', '93966')");
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    }
  }

下面的方法,CoffeesTable.populateTable,将数据插入表中:

  public void populateTable() throws SQLException {
    try (Statement stmt = con.createStatement()) {
      stmt.executeUpdate("insert into COFFEES " +
                         "values('Colombian', 00101, 7.99, 0, 0)");
      stmt.executeUpdate("insert into COFFEES " +
                         "values('French_Roast', 00049, 8.99, 0, 0)");
      stmt.executeUpdate("insert into COFFEES " +
                         "values('Espresso', 00150, 9.99, 0, 0)");
      stmt.executeUpdate("insert into COFFEES " +
                         "values('Colombian_Decaf', 00101, 8.99, 0, 0)");
      stmt.executeUpdate("insert into COFFEES " +
                         "values('French_Roast_Decaf', 00049, 9.99, 0, 0)");
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    }
  }

从结果集中检索和修改值

原文:docs.oracle.com/javase/tutorial/jdbc/basics/retrieving.html

下面的方法,CoffeesTable.viewTable,输出了 COFFEES 表的内容,并演示了 ResultSet 对象和游标的使用:

  public static void viewTable(Connection con) throws SQLException {
    String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES";
    try (Statement stmt = con.createStatement()) {
      ResultSet rs = stmt.executeQuery(query);
      while (rs.next()) {
        String coffeeName = rs.getString("COF_NAME");
        int supplierID = rs.getInt("SUP_ID");
        float price = rs.getFloat("PRICE");
        int sales = rs.getInt("SALES");
        int total = rs.getInt("TOTAL");
        System.out.println(coffeeName + ", " + supplierID + ", " + price +
                           ", " + sales + ", " + total);
      }
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    }
  }

ResultSet 对象是表示数据库结果集的数据表,通常是通过执行查询数据库的语句生成的。例如,当 CoffeeTables.viewTable 方法通过 Statement 对象 stmt 执行查询时,会创建一个 ResultSet 对象 rs。请注意,可以通过实现 Statement 接口的任何对象创建 ResultSet 对象,包括 PreparedStatementCallableStatementRowSet

通过游标访问 ResultSet 对象中的数据。请注意,这个游标不是数据库游标。这个游标是指向 ResultSet 中一行数据的指针。最初,游标位于第一行之前。方法 ResultSet.next 将游标移动到下一行。如果游标位于最后一行之后,则此方法返回 false。此方法使用 while 循环重复调用 ResultSet.next 方法来迭代 ResultSet 中的所有数据。

本页涵盖以下主题:

  • ResultSet 接口

  • 从行中检索列值

  • 游标

  • 在 ResultSet 对象中更新行

  • 使用 Statement 对象进行批量更新

  • 在 ResultSet 对象中插入行

ResultSet 接口

ResultSet 接口提供了检索和操作执行查询结果的方法,ResultSet 对象可以具有不同的功能和特性。这些特性包括类型、并发性和游标保持性

ResultSet 类型

ResultSet 对象的类型确定了其功能级别在两个方面:游标如何被操作,以及对底层数据源进行的并发更改如何反映在 ResultSet 对象中。

ResultSet 对象的灵敏度由三种不同的 ResultSet 类型之一确定:

  • TYPE_FORWARD_ONLY:结果集无法滚动;其游标仅向前移动,从第一行之前到最后一行之后。结果集中包含的行取决于底层数据库如何生成结果。也就是说,它包含在查询执行时满足查询的行,或者在检索行时满足查询的行。

  • TYPE_SCROLL_INSENSITIVE:结果可以滚动;其游标可以相对于当前位置向前和向后移动,并且可以移动到绝对位置。结果集对在打开时对基础数据源进行的更改是不敏感的。它包含在查询执行时满足查询的行,或者在检索行时满足查询的行。

  • TYPE_SCROLL_SENSITIVE:结果可以滚动;其游标可以相对于当前位置向前和向后移动,并且可以移动到绝对位置。结果集反映了在结果集保持打开状态时对基础数据源所做的更改。

默认的ResultSet类型是TYPE_FORWARD_ONLY

注意:并非所有数据库和 JDBC 驱动程序都支持所有ResultSet类型。如果指定的ResultSet类型受支持,则方法DatabaseMetaData.supportsResultSetType返回true,否则返回false

ResultSet 并发性

ResultSet对象的并发性确定支持的更新功能级别。

有两个并发级别:

  • CONCUR_READ_ONLYResultSet对象不能使用ResultSet接口进行更新。

  • CONCUR_UPDATABLEResultSet对象可以使用ResultSet接口进行更新。

默认的ResultSet并发性是CONCUR_READ_ONLY

注意:并非所有的 JDBC 驱动程序和数据库都支持并发性。如果指定的并发级别由驱动程序支持,则方法DatabaseMetaData.supportsResultSetConcurrency返回true,否则返回false

方法CoffeesTable.modifyPrices演示了如何使用并发级别为CONCUR_UPDATABLEResultSet对象。

游标可保持性

调用方法Connection.commit可以关闭在当前事务期间创建的ResultSet对象。然而,在某些情况下,这可能不是期望的行为。ResultSet属性holdability使应用程序可以控制在调用commit时是否关闭ResultSet对象(游标)。

以下ResultSet常量可以提供给Connection方法createStatementprepareStatementprepareCall

  • HOLD_CURSORS_OVER_COMMITResultSet游标不会关闭;它们是可保持的:当调用commit方法时,它们保持打开状态。如果您的应用程序主要使用只读ResultSet对象,则可保持的游标可能是理想的。

  • CLOSE_CURSORS_AT_COMMIT:在调用commit方法时关闭ResultSet对象(游标)。在调用此方法时关闭游标可能会提高某些应用程序的性能。

默认的游标可保持性取决于您的 DBMS。

注意:并非所有的 JDBC 驱动程序和数据库都支持可保持和不可保持的游标。以下方法JDBCTutorialUtilities.cursorHoldabilitySupport输出ResultSet对象的默认游标保持性以及是否支持HOLD_CURSORS_OVER_COMMITCLOSE_CURSORS_AT_COMMIT

public static void cursorHoldabilitySupport(Connection conn)
    throws SQLException {

    DatabaseMetaData dbMetaData = conn.getMetaData();
    System.out.println("ResultSet.HOLD_CURSORS_OVER_COMMIT = " +
        ResultSet.HOLD_CURSORS_OVER_COMMIT);

    System.out.println("ResultSet.CLOSE_CURSORS_AT_COMMIT = " +
        ResultSet.CLOSE_CURSORS_AT_COMMIT);

    System.out.println("Default cursor holdability: " +
        dbMetaData.getResultSetHoldability());

    System.out.println("Supports HOLD_CURSORS_OVER_COMMIT? " +
        dbMetaData.supportsResultSetHoldability(
            ResultSet.HOLD_CURSORS_OVER_COMMIT));

    System.out.println("Supports CLOSE_CURSORS_AT_COMMIT? " +
        dbMetaData.supportsResultSetHoldability(
            ResultSet.CLOSE_CURSORS_AT_COMMIT));
}

从行中检索列值

ResultSet接口声明了获取器方法(例如,getBooleangetLong)用于从当前行检索列值。您可以使用列的索引号或别名或名称检索值。列索引通常更有效。列从 1 开始编号。为了最大的可移植性,应按照从左到右的顺序读取每行中的结果集列,并且每列只能读取一次。

例如,以下方法CoffeesTable.alternateViewTable,通过编号检索列值:

  public static void alternateViewTable(Connection con) throws SQLException {
    String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES";
    try (Statement stmt = con.createStatement()) {
      ResultSet rs = stmt.executeQuery(query);
      while (rs.next()) {
        String coffeeName = rs.getString(1);
        int supplierID = rs.getInt(2);
        float price = rs.getFloat(3);
        int sales = rs.getInt(4);
        int total = rs.getInt(5);
        System.out.println(coffeeName + ", " + supplierID + ", " + price +
                           ", " + sales + ", " + total);
      }
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    }
  }

作为获取器方法输入的字符串是不区分大小写的。当使用字符串调用获取器方法时,如果有多个列具有与字符串相同的别名或名称,则返回第一个匹配列的值。使用字符串而不是整数的选项设计用于在生成结果集的 SQL 查询中使用列别名和名称。对于在查询中明确命名的列(例如,select * from COFFEES),最好使用列号。如果使用列名,开发人员应确保它们通过使用列别名唯一地引用所需的列。列别名有效地重命名了结果集的列。要指定列别名,请在SELECT语句中使用 SQL AS子句。

适当类型的获取器方法检索每列中的值。例如,在方法CoffeeTables.viewTable中,ResultSet rs中每行的第一列是COF_NAME,存储了 SQL 类型VARCHAR的值。检索 SQL 类型VARCHAR值的方法是getString。每行中的第二列存储了 SQL 类型INTEGER的值,检索该类型值的方法是getInt

注意,虽然推荐使用方法getString来检索 SQL 类型CHARVARCHAR,但也可以使用它来检索任何基本的 SQL 类型。使用getString获取所有值可能非常有用,但也有其局限性。例如,如果用于检索数值类型,getString会将数值转换为 Java String对象,必须将该值转换回数值类型才能作为数字进行操作。在将值视为字符串处理的情况下,没有任何缺点。此外,如果要求应用程序检索除 SQL3 类型以外的任何标准 SQL 类型的值,请使用getString方法。

游标

如前所述,通过光标访问ResultSet对象中的数据,光标指向ResultSet对象中的一行。但是,当创建ResultSet对象时,光标位于第一行之前。方法CoffeeTables.viewTable通过调用ResultSet.next方法移动光标。还有其他可用于移动光标的方法:

  • next: 将光标向前移动一行。如果光标现在位于一行上,则返回true,如果光标位于最后一行之后,则返回false

  • previous: 将光标向后移动一行。如果光标现在位于一行上,则返回true,如果光标位于第一行之前,则返回false

  • first: 将光标移动到ResultSet对象中的第一行。如果光标现在位于第一行上,则返回true,如果ResultSet对象不包含任何行,则返回false

  • last:: 将光标移动到ResultSet对象中的最后一行。如果光标现在位于最后一行上,则返回true,如果ResultSet对象不包含任何行,则返回false

  • beforeFirst: 将光标定位在ResultSet对象的开头,即第一行之前。如果ResultSet对象不包含任何行,则此方法不起作用。

  • afterLast: 将光标定位在ResultSet对象的末尾,即最后一行之后。如果ResultSet对象不包含任何行,则此方法不起作用。

  • relative(int rows): 相对于当前位置移动光标。

  • absolute(int row): 将光标定位在参数row指定的行上。

请注意,ResultSet的默认灵敏度是TYPE_FORWARD_ONLY,这意味着它不能滚动;如果您的ResultSet不能滚动,则除了next之外,您不能调用任何移动光标的方法。下一节中描述的CoffeesTable.modifyPrices方法演示了如何移动ResultSet的光标。

更新ResultSet对象中的行

您不能更新默认的ResultSet对象,只能将其光标向前移动。但是,您可以创建可以滚动(光标可以向后移动或移动到绝对位置)和更新的ResultSet对象。

以下方法,CoffeesTable.modifyPrices,将每行的PRICE列乘以参数percentage

  public void modifyPrices(float percentage) throws SQLException {
    try (Statement stmt =
      con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE)) {
      ResultSet uprs = stmt.executeQuery("SELECT * FROM COFFEES");
      while (uprs.next()) {
        float f = uprs.getFloat("PRICE");
        uprs.updateFloat("PRICE", f * percentage);
        uprs.updateRow();
      }
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    }
  }

字段ResultSet.TYPE_SCROLL_SENSITIVE创建一个ResultSet对象,其光标可以相对于当前位置和绝对位置向前和向后移动。字段ResultSet.CONCUR_UPDATABLE创建一个可更新的ResultSet对象。查看ResultSet Javadoc 以了解可以指定的其他字段,以修改ResultSet对象的行为。

方法ResultSet.updateFloat更新了指定列(在本例中为PRICE)中光标所在行的指定float值。ResultSet包含各种更新方法,使您能够更新各种数据类型的列值。然而,这些更新方法都不会修改数据库;您必须调用方法ResultSet.updateRow来更新数据库。

使用语句对象进行批量更新

StatementPreparedStatementCallableStatement对象都有一个与之关联的命令列表。该列表可能包含用于更新、插入或删除行的语句;也可能包含 DDL 语句,如CREATE TABLEDROP TABLE。但是,它不能包含会产生ResultSet对象的语句,比如SELECT语句。换句话说,该列表只能包含产生更新计数的语句。

Statement对象在创建时关联的列表最初为空。您可以使用方法addBatch向此列表添加 SQL 命令,并使用方法clearBatch清空它。当您完成向列表添加语句时,调用方法executeBatch将它们全部发送到数据库以作为一个单元或批量执行。

例如,以下方法,CoffeesTable.batchUpdate,使用批量更新向COFFEES表添加了四行:

  public void batchUpdate() throws SQLException {
    con.setAutoCommit(false);
    try (Statement stmt = con.createStatement()) {

      stmt.addBatch("INSERT INTO COFFEES " +
                    "VALUES('Amaretto', 49, 9.99, 0, 0)");
      stmt.addBatch("INSERT INTO COFFEES " +
                    "VALUES('Hazelnut', 49, 9.99, 0, 0)");
      stmt.addBatch("INSERT INTO COFFEES " +
                    "VALUES('Amaretto_decaf', 49, 10.99, 0, 0)");
      stmt.addBatch("INSERT INTO COFFEES " +
                    "VALUES('Hazelnut_decaf', 49, 10.99, 0, 0)");

      int[] updateCounts = stmt.executeBatch();
      con.commit();
    } catch (BatchUpdateException b) {
      JDBCTutorialUtilities.printBatchUpdateException(b);
    } catch (SQLException ex) {
      JDBCTutorialUtilities.printSQLException(ex);
    } finally {
      con.setAutoCommit(true);
    }
  }

以下行禁用了Connection对象con的自动提交模式,这样当调用方法executeBatch时,事务将不会自动提交或回滚。

con.setAutoCommit(false);

为了正确处理错误,您应该在开始批量更新之前始终禁用自动提交模式。

方法Statement.addBatch将一个命令添加到与Statement对象stmt关联的命令列表中。在本例中,这些命令都是INSERT INTO语句,每个语句都添加了由五个列值组成的行。列COF_NAMEPRICE的值分别是咖啡的名称和价格。每行中的第二个值为 49,因为这是供应商 Superior Coffee 的标识号。最后两个值,列SALESTOTAL的条目,都从零开始,因为尚未进行销售。(SALES是本行咖啡在本周销售的磅数;TOTAL是该咖啡所有累计销售的总量。)

以下行将添加到其命令列表中的四个 SQL 命令发送到数据库以作为批量执行:

int[] updateCounts = stmt.executeBatch();

请注意,stmt使用方法executeBatch发送插入的批处理,而不是使用方法executeUpdate,后者只发送一个命令并返回单个更新计数。数据库管理系统按照添加到命令列表的顺序执行命令,因此它将首先添加 Amaretto 的值行,然后添加 Hazelnut 的行,然后是 Amaretto decaf,最后是 Hazelnut decaf。如果所有四个命令都成功执行,数据库管理系统将按照执行顺序为每个命令返回一个更新计数。指示每个命令影响了多少行的更新计数存储在数组updateCounts中。

如果批处理中的所有四个命令都成功执行,updateCounts将包含四个值,所有这些值都为 1,因为插入会影响一行。与stmt关联的命令列表现在将为空,因为之前添加的四个命令在stmt调用方法executeBatch时已发送到数据库。您随时可以使用方法clearBatch显式清空此命令列表。

Connection.commit方法使对COFFEES表的更新批量变为永久。之前为此连接禁用了自动提交模式,因此需要显式调用此方法。

以下行启用当前Connection对象的自动提交模式。

con.setAutoCommit(true);

现在,示例中的每个语句在执行后将自动提交,并且不再需要调用方法commit

执行参数化的批量更新

还可以进行参数化的批量更新,如下面的代码片段所示,其中con是一个Connection对象:

con.setAutoCommit(false);
PreparedStatement pstmt = con.prepareStatement(
                              "INSERT INTO COFFEES VALUES( " +
                              "?, ?, ?, ?, ?)");
pstmt.setString(1, "Amaretto");
pstmt.setInt(2, 49);
pstmt.setFloat(3, 9.99);
pstmt.setInt(4, 0);
pstmt.setInt(5, 0);
pstmt.addBatch();

pstmt.setString(1, "Hazelnut");
pstmt.setInt(2, 49);
pstmt.setFloat(3, 9.99);
pstmt.setInt(4, 0);
pstmt.setInt(5, 0);
pstmt.addBatch();

// ... and so on for each new
// type of coffee

int[] updateCounts = pstmt.executeBatch();
con.commit();
con.setAutoCommit(true);

处理批量更新异常

如果批处理中的一个 SQL 语句产生结果集(通常是查询),或者批处理中的一个 SQL 语句由于其他原因未成功执行,当调用方法executeBatch时,将会收到BatchUpdateException

你不应该将查询(一个SELECT语句)添加到一批 SQL 命令中,因为方法executeBatch期望从每个成功执行的 SQL 语句中返回一个更新计数。这意味着只有返回更新计数的命令(如INSERT INTOUPDATEDELETE)或返回 0 的命令(如CREATE TABLEDROP TABLEALTER TABLE)才能成功地作为一批与executeBatch方法一起执行。

BatchUpdateException包含一个与executeBatch方法返回的数组类似的更新计数数组。在这两种情况下,更新计数与产生它们的命令的顺序相同。这告诉你批处理中有多少个命令成功执行以及它们是哪些。例如,如果五个命令成功执行,数组将包含五个数字:第一个是第一个命令的更新计数,第二个是第二个命令的更新计数,依此类推。

BatchUpdateException是从SQLException派生的。这意味着你可以使用所有SQLException对象可用的方法。以下方法,JDBCTutorialUtilities.printBatchUpdateException,打印了所有SQLException信息以及BatchUpdateException对象中包含的更新计数。因为BatchUpdateException.getUpdateCounts返回一个int数组,代码使用for循环打印每个更新计数:

  public static void printBatchUpdateException(BatchUpdateException b) {
    System.err.println("----BatchUpdateException----");
    System.err.println("SQLState:  " + b.getSQLState());
    System.err.println("Message:  " + b.getMessage());
    System.err.println("Vendor:  " + b.getErrorCode());
    System.err.print("Update counts:  ");
    int[] updateCounts = b.getUpdateCounts();
    for (int i = 0; i < updateCounts.length; i++) {
      System.err.print(updateCounts[i] + "   ");
    }
  }

ResultSet对象中插入行

注意:并非所有的 JDBC 驱动程序都支持使用ResultSet接口插入新行。如果尝试插入新行而你的 JDBC 驱动程序数据库不支持此功能,将抛出SQLFeatureNotSupportedException异常。

以下方法,CoffeesTable.insertRow,通过ResultSet对象向COFFEES插入一行:

  public void insertRow(String coffeeName, int supplierID, float price,
                        int sales, int total) throws SQLException {

    try (Statement stmt =
          con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE))
    {      
      ResultSet uprs = stmt.executeQuery("SELECT * FROM COFFEES");
      uprs.moveToInsertRow();
      uprs.updateString("COF_NAME", coffeeName);
      uprs.updateInt("SUP_ID", supplierID);
      uprs.updateFloat("PRICE", price);
      uprs.updateInt("SALES", sales);
      uprs.updateInt("TOTAL", total);

      uprs.insertRow();
      uprs.beforeFirst();

    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    }
  }

此示例调用Connection.createStatement方法,带有两个参数,ResultSet.TYPE_SCROLL_SENSITIVEResultSet.CONCUR_UPDATABLE。第一个值使ResultSet对象的游标能够向前和向后移动。第二个值,ResultSet.CONCUR_UPDATABLE,如果要向ResultSet对象插入行,则需要它;它指定它可以被更新。

在 getter 方法中使用字符串的相同规定也适用于 updater 方法。

方法ResultSet.moveToInsertRow将游标移动到插入行。插入行是与可更新结果集关联的特殊行。它本质上是一个缓冲区,可以通过调用更新器方法构造新行,然后将该行插入结果集。例如,此方法调用ResultSet.updateString方法将插入行的COF_NAME列更新为Kona

方法ResultSet.insertRow将插入行的内容插入ResultSet对象和数据库中。

注意:使用ResultSet.insertRow插入一行后,应将游标移动到插入行之外的行。例如,此示例使用ResultSet.beforeFirst方法将其移动到结果集中的第一行之前。如果应用程序的另一部分使用相同的结果集且游标仍指向插入行,则可能会出现意外结果。

使用预编译语句

原文:docs.oracle.com/javase/tutorial/jdbc/basics/prepared.html

本页涵盖以下主题:

  • 预编译语句概述

  • 创建一个 PreparedStatement 对象

  • 为 PreparedStatement 参数提供值

预编译语句概述

有时,使用PreparedStatement对象向数据库发送 SQL 语句更加方便。这种特殊类型的语句源自你已经了解的更一般的Statement类。

如果你想多次执行一个Statement对象,通常使用PreparedStatement对象可以减少执行时间。

PreparedStatement对象的主要特点是,与Statement对象不同,它在创建时就被赋予了一个 SQL 语句。这样做的好处是,在大多数情况下,这个 SQL 语句会立即发送到数据库管理系统(DBMS)中进行编译。因此,PreparedStatement对象不仅包含一个 SQL 语句,而且包含一个已经预编译过的 SQL 语句。这意味着当执行PreparedStatement时,DBMS 可以直接运行PreparedStatement的 SQL 语句,而无需先进行编译。

尽管你可以使用PreparedStatement对象执行没有参数的 SQL 语句,但通常你最常使用它们来执行带有参数的 SQL 语句。使用带有参数的 SQL 语句的优点是,你可以多次执行相同的语句,并每次执行时提供不同的值。以下部分中有相关示例。

然而,预编译语句最重要的优点是可以帮助防止 SQL 注入攻击。SQL 注入是一种恶意利用应用程序中使用客户端提供的数据的技术,用于在 SQL 语句中执行意外命令。攻击者通过提供经过特殊设计的字符串输入来欺骗 SQL 引擎,从而未经授权地访问数据库以查看或操纵受限数据。所有 SQL 注入技术都利用应用程序中的一个漏洞:未正确验证或未验证的字符串文字被连接到动态构建的 SQL 语句中,并被 SQL 引擎解释为代码。预编译语句始终将客户端提供的数据视为参数的内容,而不是 SQL 语句的一部分。有关更多信息,请参阅 Oracle 数据库文档中的数据库 PL/SQL 语言参考部分中的SQL 注入部分。

以下方法,CoffeesTable.updateCoffeeSales,将当前周内销售的咖啡磅数存储在每种咖啡的SALES列中,并更新每种咖啡的TOTAL列中销售的咖啡总磅数:

  public void updateCoffeeSales(HashMap<String, Integer> salesForWeek) throws SQLException {
    String updateString =
      "update COFFEES set SALES = ? where COF_NAME = ?";
    String updateStatement =
      "update COFFEES set TOTAL = TOTAL + ? where COF_NAME = ?";

    try (PreparedStatement updateSales = con.prepareStatement(updateString);
         PreparedStatement updateTotal = con.prepareStatement(updateStatement))

    {
      con.setAutoCommit(false);
      for (Map.Entry<String, Integer> e : salesForWeek.entrySet()) {
        updateSales.setInt(1, e.getValue().intValue());
        updateSales.setString(2, e.getKey());
        updateSales.executeUpdate();

        updateTotal.setInt(1, e.getValue().intValue());
        updateTotal.setString(2, e.getKey());
        updateTotal.executeUpdate();
        con.commit();
      }
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
      if (con != null) {
        try {
          System.err.print("Transaction is being rolled back");
          con.rollback();
        } catch (SQLException excep) {
          JDBCTutorialUtilities.printSQLException(excep);
        }
      }
    }
  }

创建 PreparedStatement 对象

以下创建了一个接受两个输入参数的PreparedStatement对象:

    String updateString =
      "update COFFEES " + "set SALES = ? where COF_NAME = ?";
	// ...
    PreparedStatement updateSales = con.prepareStatement(updateString);

为 PreparedStatement 参数提供值

在执行PreparedStatement对象之前,必须在问号占位符的位置提供值(如果有的话)。通过调用PreparedStatement类中定义的 setter 方法来实现。以下语句为名为updateSalesPreparedStatement提供了两个问号占位符的值:

updateSales.setInt(1, e.getValue().intValue());
updateSales.setString(2, e.getKey());

这些 setter 方法的每个第一个参数指定了问号占位符。在这个例子中,setInt指定了第一个占位符,setString指定了第二个占位符。

在为参数设置了值之后,它会保留该值,直到被重置为另一个值,或者调用方法clearParameters。使用PreparedStatement对象updateSales,以下代码片段演示了在重置其参数值并保持另一个参数值不变后重用准备好的语句:

// changes SALES column of French Roast
//row to 100

updateSales.setInt(1, 100);
updateSales.setString(2, "French_Roast");
updateSales.executeUpdate();

// changes SALES column of Espresso row to 100
// (the first parameter stayed 100, and the second
// parameter was reset to "Espresso")

updateSales.setString(2, "Espresso");
updateSales.executeUpdate();

使用循环设置值

通常可以通过使用for循环或while循环为输入参数设置值,从而使编码更加简单。

CoffeesTable.updateCoffeeSales方法使用 for-each 循环重复设置updateSalesupdateTotal中的值:

for (Map.Entry<String, Integer> e : salesForWeek.entrySet()) {
  updateSales.setInt(1, e.getValue().intValue());
  updateSales.setString(2, e.getKey());
  // ...
}

方法CoffeesTable.updateCoffeeSales接受一个参数,HashMapHashMap参数中的每个元素都包含当前周内销售的一种咖啡的名称和该种咖啡的磅数。for-each 循环遍历HashMap参数的每个元素,并设置updateSalesupdateTotal中相应的问号占位符。

执行 PreparedStatement 对象

Statement对象一样,要执行PreparedStatement对象,调用一个执行语句:如果查询只返回一个ResultSet(如SELECT SQL 语句),则调用executeQuery,如果查询不返回ResultSet(如UPDATE SQL 语句),则调用executeUpdate,如果查询可能返回多个ResultSet对象,则调用executeCoffeesTable.updateCoffeeSales(HashMap<String, Integer>)中的两个PreparedStatement对象都包含UPDATE SQL 语句,因此都通过调用executeUpdate来执行:

updateSales.setInt(1, e.getValue().intValue());
updateSales.setString(2, e.getKey());
updateSales.executeUpdate();

updateTotal.setInt(1, e.getValue().intValue());
updateTotal.setString(2, e.getKey());
updateTotal.executeUpdate();
con.commit();

在执行updateSalesupdateTotals时,executeUpdate不需要提供参数;两个PreparedStatement对象已经包含要执行的 SQL 语句。

注意:在CoffeesTable.updateCoffeeSales的开头,自动提交模式被设置为 false:

con.setAutoCommit(false);

因此,在调用方法commit之前,不会提交任何 SQL 语句。有关自动提交模式的更多信息,请参阅事务。

executeUpdate 方法的返回值

executeQuery返回一个包含发送到 DBMS 的查询结果的ResultSet对象,executeUpdate的返回值是一个int值,表示更新了表的多少行。例如,下面的代码展示了executeUpdate的返回值被赋给变量n

updateSales.setInt(1, 50);
updateSales.setString(2, "Espresso");
int n = updateSales.executeUpdate();
// n = 1 because one row had a change in it

COFFEES已更新;值50替换了Espresso行中SALES列的值。这次更新影响了表中的一行,因此n等于 1。

当使用方法executeUpdate执行 DDL(数据定义语言)语句时,比如创建表时,它会返回值为int的 0。因此,在下面的代码片段中,用于执行创建表COFFEES的 DDL 语句时,n被赋值为 0:

// n = 0
int n = executeUpdate(createTableCoffees); 

注意,当executeUpdate的返回值为 0 时,可能意味着两种情况之一:

  • 执行的语句是一个影响零行的更新语句。

  • 执行的语句是一个 DDL 语句。

使用事务

原文:docs.oracle.com/javase/tutorial/jdbc/basics/transactions.html

有时候你不希望一个语句生效,除非另一个语句完成。例如,当“咖啡休息时间”的老板更新每周销售的咖啡量时,老板还希望更新迄今为止的总销售量。然而,每周销售量和总销售量应该同时更新;否则,数据将不一致。确保两个动作都发生或两个动作都不发生的方法是使用事务。事务是一组一个或多个语句,作为一个单元执行,因此要么所有语句都执行,要么所有语句都不执行。

本页涵盖以下主题

  • 禁用自动提交模式

  • 提交事务

  • 使用事务保持数据完整性

  • 设置和回滚保存点

  • 释放保存点

  • 何时调用 rollback 方法

禁用自动提交模式

当创建连接时,它处于自动提交模式。这意味着每个单独的 SQL 语句都被视为一个事务,并在执行后立即自动提交。(更准确地说,默认情况下,SQL 语句在完成时提交,而不是在执行时。当所有结果集和更新计数都被检索时,语句完成。然而,在几乎所有情况下,语句在执行后立即完成,因此提交。)

允许将两个或多个语句分组为一个事务的方法是禁用自动提交模式。在以下代码中演示了这一点,其中con是一个活动连接:

con.setAutoCommit(false);

提交事务

在禁用自动提交模式后,直到显式调用commit方法之前,不会提交任何 SQL 语句。在上一次调用commit方法之后执行的所有语句都包含在当前事务中,并作为一个单元一起提交。以下方法CoffeesTable.updateCoffeeSales,其中con是一个活动连接,演示了一个事务:

  public void updateCoffeeSales(HashMap<String, Integer> salesForWeek) throws SQLException {
    String updateString =
      "update COFFEES set SALES = ? where COF_NAME = ?";
    String updateStatement =
      "update COFFEES set TOTAL = TOTAL + ? where COF_NAME = ?";

    try (PreparedStatement updateSales = con.prepareStatement(updateString);
         PreparedStatement updateTotal = con.prepareStatement(updateStatement))

    {
      con.setAutoCommit(false);
      for (Map.Entry<String, Integer> e : salesForWeek.entrySet()) {
        updateSales.setInt(1, e.getValue().intValue());
        updateSales.setString(2, e.getKey());
        updateSales.executeUpdate();

        updateTotal.setInt(1, e.getValue().intValue());
        updateTotal.setString(2, e.getKey());
        updateTotal.executeUpdate();
        con.commit();
      }
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
      if (con != null) {
        try {
          System.err.print("Transaction is being rolled back");
          con.rollback();
        } catch (SQLException excep) {
          JDBCTutorialUtilities.printSQLException(excep);
        }
      }
    }
  }

在这种方法中,连接con的自动提交模式被禁用,这意味着当调用commit方法时,两个准备好的语句updateSalesupdateTotal将一起提交。每当调用commit方法(无论是在自动提交模式启用时自动调用还是在禁用时显式调用),事务中语句导致的所有更改都将变为永久性。在这种情况下,这意味着哥伦比亚咖啡的SALESTOTAL列已更改为50(如果TOTAL之前为0),并将保留此值,直到它们通过另一个更新语句进行更改。

语句con.setAutoCommit(true);启用自动提交模式,这意味着每个语句再次在完成时自动提交。然后,您回到默认状态,无需自己调用commit方法。建议仅在事务模式下禁用自动提交模式。这样,您可以避免为多个语句保持数据库锁,从而增加与其他用户发生冲突的可能性。

使用事务保持数据完整性

除了将语句组合在一起作为一个单元执行外,事务还可以帮助保持表中数据的完整性。例如,想象一下,一个员工应该在COFFEES表中输入新的咖啡价格,但推迟了几天才这样做。与此同时,价格上涨了,今天所有者正在尝试输入更高的价格。员工最终开始输入现在已过时的价格,与此同时所有者正在尝试更新表。在插入过时价格后,员工意识到这些价格不再有效,并调用Connection方法rollback来撤消它们的影响。(rollback方法中止事务并将值恢复为尝试更新之前的值。)与此同时,所有者正在执行SELECT语句并打印新价格。在这种情况下,可能会打印出一个已经回滚到先前值的价格,使打印的价格不正确。

这种情况可以通过使用事务来避免,提供一定程度的保护,防止两个用户同时访问数据时出现冲突。

为了在事务期间避免冲突,数据库管理系统使用锁,这是一种阻止其他人访问事务正在访问的数据的机制。 (请注意,在自动提交模式下,每个语句都是一个事务,锁仅保留一个语句。)设置锁之后,锁将持续有效,直到事务提交或回滚。 例如,数据库管理系统可以锁定表的一行,直到对其进行的更新被提交。 这种锁的效果是防止用户获取脏读取,即在数据永久化之前读取值。 (访问尚未提交的更新值被视为脏读,因为可能将该值回滚到其先前的值。 如果读取后来被回滚的值,您将读取一个无效的值。)

锁是如何设置的取决于所谓的事务隔离级别,它可以从根本不支持事务到支持实施非常严格访问规则。

事务隔离级别的一个示例是TRANSACTION_READ_COMMITTED,它不允许在提交之后访问值。 换句话说,如果事务隔离级别设置为TRANSACTION_READ_COMMITTED,数据库管理系统不允许发生脏读取。 接口Connection包括五个值,表示您可以在 JDBC 中使用的事务隔离级别:

隔离级别 事务 脏读 不可重复读 幻读
TRANSACTION_NONE 不支持 不适用 不适用 不适用
TRANSACTION_READ_COMMITTED 支持 阻止 允许 允许
TRANSACTION_READ_UNCOMMITTED 支持 允许 允许 允许
TRANSACTION_REPEATABLE_READ 支持 阻止 阻止 允许
TRANSACTION_SERIALIZABLE 支持 阻止 阻止 阻止

当事务 A 检索一行,事务 B 随后更新该行,然后事务 A 稍后再次检索相同的行时,会发生不可重复读。 事务 A 两次检索相同的行,但看到不同的数据。

当事务 A 检索满足给定条件的一组行时,事务 B 随后插入或更新一行,使得该行现在满足事务 A 中的条件,然后事务 A 稍后重复条件检索时,会发生幻读。 事务 A 现在看到了一个额外的行。 这行被称为幻像。

通常,您不需要对事务隔离级别做任何处理;您可以只使用您的 DBMS 的默认隔离级别。默认事务隔离级别取决于您的 DBMS。例如,对于 Java DB,它是TRANSACTION_READ_COMMITTED。JDBC 允许您查找您的 DBMS 设置的事务隔离级别(使用Connection方法getTransactionIsolation),并且还允许您将其设置为另一个级别(使用Connection方法setTransactionIsolation)。

注意:JDBC 驱动程序可能不支持所有事务隔离级别。如果驱动程序不支持在调用setTransactionIsolation时指定的隔离级别,则驱动程序可以替换为更高、更严格的事务隔离级别。如果驱动程序无法替换为更高的事务级别,则会抛出SQLException。使用方法DatabaseMetaData.supportsTransactionIsolationLevel来确定驱动程序是否支持给定级别。

设置和回滚到 Savepoints

方法Connection.setSavepoint在当前事务中设置一个Savepoint对象。方法Connection.rollback被重载以接受一个Savepoint参数。

以下方法,CoffeesTable.modifyPricesByPercentage,通过百分比priceModifier提高特定咖啡的价格。但是,如果新价格大于指定价格maximumPrice,则价格将恢复到原始价格:

  public void modifyPricesByPercentage(
    String coffeeName,
    float priceModifier,
    float maximumPrice) throws SQLException {
    con.setAutoCommit(false);
    ResultSet rs = null;
    String priceQuery = "SELECT COF_NAME, PRICE FROM COFFEES " +
                        "WHERE COF_NAME = ?";
    String updateQuery = "UPDATE COFFEES SET PRICE = ? " +
                         "WHERE COF_NAME = ?";
    try (PreparedStatement getPrice = con.prepareStatement(priceQuery, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
         PreparedStatement updatePrice = con.prepareStatement(updateQuery))
    {
      Savepoint save1 = con.setSavepoint();
      getPrice.setString(1, coffeeName);
      if (!getPrice.execute()) {
        System.out.println("Could not find entry for coffee named " + coffeeName);
      } else {
        rs = getPrice.getResultSet();
        rs.first();
        float oldPrice = rs.getFloat("PRICE");
        float newPrice = oldPrice + (oldPrice * priceModifier);
        System.out.printf("Old price of %s is $%.2f%n", coffeeName, oldPrice);
        System.out.printf("New price of %s is $%.2f%n", coffeeName, newPrice);
        System.out.println("Performing update...");
        updatePrice.setFloat(1, newPrice);
        updatePrice.setString(2, coffeeName);
        updatePrice.executeUpdate();
        System.out.println("\nCOFFEES table after update:");
        CoffeesTable.viewTable(con);
        if (newPrice > maximumPrice) {
          System.out.printf("The new price, $%.2f, is greater " +
                            "than the maximum price, $%.2f. " +
                            "Rolling back the transaction...%n",
                            newPrice, maximumPrice);
          con.rollback(save1);
          System.out.println("\nCOFFEES table after rollback:");
          CoffeesTable.viewTable(con);
        }
        con.commit();
      }
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    } finally {
      con.setAutoCommit(true);
    }
  }

以下语句指定了从getPrice查询生成的ResultSet对象的游标在调用commit方法时关闭。请注意,如果您的 DBMS 不支持ResultSet.CLOSE_CURSORS_AT_COMMIT,则此常量将被忽略:

getPrice = con.prepareStatement(query, ResultSet.CLOSE_CURSORS_AT_COMMIT);

该方法首先通过以下语句创建一个Savepoint

Savepoint save1 = con.setSavepoint();

该方法检查新价格是否大于maximumPrice值。如果是,则使用以下语句回滚事务:

con.rollback(save1);

因此,当方法通过调用Connection.commit方法提交事务时,它不会提交任何已回滚其关联Savepoint的行;它将提交所有其他更新的行。

释放 Savepoints

方法Connection.releaseSavepointSavepoint对象作为参数,并将其从当前事务中移除。

释放 Savepoints 后,尝试在回滚操作中引用它会导致抛出SQLException。在事务提交或整个事务回滚时,已创建的任何保存点都会自动释放并在事务提交时变为无效,或者在回滚整个事务时变为无效。将事务回滚到保存点会自动释放并使其他在该保存点之后创建的保存点无效。

何时调用rollback方法

如前所述,调用方法rollback会终止事务并将修改的任何值返回到它们之前的值。如果您尝试在事务中执行一个或多个语句并收到SQLException,请调用方法rollback来结束事务并重新开始事务。这是唯一的方法来知道什么已经提交,什么尚未提交。捕获SQLException告诉您出现了问题,但它并不告诉您什么已经提交或尚未提交。因为您不能确定没有任何提交,调用方法rollback是确保的唯一方法。

方法CoffeesTable.updateCoffeeSales演示了一个事务,并包括一个调用方法rollbackcatch块。如果应用程序继续并使用事务的结果,catch块中对rollback方法的调用将阻止使用可能不正确的数据。

使用 RowSet 对象

原文:docs.oracle.com/javase/tutorial/jdbc/basics/rowset.html

JDBC RowSet对象以一种使其比结果集更灵活且更易于使用的方式保存表格数据。

Oracle 为RowSet的一些更受欢迎的用途定义了五个接口,并为这些RowSet接口提供了标准参考。在本教程中,您将学习如何使用这些参考实现。

这些版本的RowSet接口及其实现是为程序员提供方便的。程序员可以自由编写自己的javax.sql.RowSet接口版本,扩展这五个RowSet接口的实现,或编写自己的实现。然而,许多程序员可能会发现标准参考实现已经符合他们的需求,并将直接使用它们。

本节介绍了RowSet接口以及扩展此接口的以下接口:

  • JdbcRowSet

  • CachedRowSet

  • WebRowSet

  • JoinRowSet

  • FilteredRowSet

下面的主题包括:

  • RowSet 对象能做什么?

  • RowSet 对象的种类

RowSet 对象能做什么?

所有RowSet对象都源自ResultSet接口,因此共享其功能。JDBC RowSet对象的特殊之处在于它们添加了以下新功能:

  • 作为 JavaBeans 组件

  • 添加滚动或可更新性

作为 JavaBeans 组件

所有RowSet对象都是 JavaBeans 组件。这意味着它们具有以下特性:

  • 属性

  • JavaBeans 通知机制

属性

所有RowSet对象都有属性。属性是具有对应的 getter 和 setter 方法的字段。属性暴露给构建工具(例如随 IDE 一起提供的 JDveloper 和 Eclipse),使您可以直观地操作 bean。有关更多信息,请参阅 JavaBeans 教程中的属性课程。

JavaBeans 通知机制

RowSet对象使用 JavaBeans 事件模型,其中注册的组件在发生某些事件时会收到通知。对于所有RowSet对象,三个事件会触发通知:

  • 光标移动

  • 行的更新、插入或删除

  • 整个RowSet内容的更改

事件的通知发送给所有监听器,即已实现RowSetListener接口并已将自己添加到RowSet对象的组件列表中以在发生任何三个事件时收到通知的组件。

一个监听器可以是一个 GUI 组件,比如一个条形图。如果条形图正在跟踪一个RowSet对象中的数据,那么每当数据发生变化时,监听器都希望知道新的数据值。因此,监听器将实现RowSetListener方法来定义在特定事件发生时将执行什么操作。然后,监听器还必须添加到RowSet对象的监听器列表中。下面的代码行将条形图组件bg注册到RowSet对象rs

rs.addListener(bg);

现在,每当光标移动、行发生变化或rs中的所有数据都更新时,bg都会收到通知。

添加滚动性或可更新性

一些数据库管理系统不支持可以滚动(可滚动)的结果集,有些不支持可以更新(可更新)的结果集。如果该数据库管理系统的驱动程序没有添加滚动或更新结果集的功能,您可以使用RowSet对象来实现。RowSet对象默认是可滚动和可更新的,因此通过将结果集的内容填充到RowSet对象中,您可以有效地使结果集可滚动和可更新。

RowSet 对象的种类

RowSet对象被认为是连接的或断开连接的。连接的RowSet对象使用 JDBC 驱动程序与关系数据库建立连接,并在其生命周期内保持该连接。断开连接的RowSet对象仅在从ResultSet对象中读取数据或将数据写回数据源时才与数据源建立连接。在从数据源读取数据或向数据源写入数据后,RowSet对象会断开与数据源的连接,从而变为“断开连接”。在其大部分生命周期中,断开连接的RowSet对象不与数据源建立连接,并且独立运行。接下来的两节将告诉您在连接或断开连接方面,RowSet对象可以做什么。

连接的 RowSet 对象

标准RowSet实现中只有一个连接的RowSet对象:JdbcRowSet。作为与数据库始终连接的对象,JdbcRowSet对象最类似于ResultSet对象,并且通常被用作包装器,使本来不可滚动和只读的ResultSet对象变得可滚动和可更新。

作为 JavaBeans 组件,例如,JdbcRowSet对象可以在 GUI 工具中用于选择 JDBC 驱动程序。JdbcRowSet对象可以这样使用,因为它实际上是驱动程序的包装器,该驱动程序获取了与数据库的连接。

断开连接的 RowSet 对象

另外四个实现是断开连接的RowSet实现。断开连接的RowSet对象具有连接的RowSet对象的所有功能,还具有仅适用于断开连接的RowSet对象的附加功能。例如,不必维护与数据源的连接使得断开连接的RowSet对象比JdbcRowSet对象或ResultSet对象更轻量级。断开连接的RowSet对象也是可序列化的,而且既可序列化又轻量级的组合使它们非常适合通过网络发送数据。它们甚至可以用于向 PDA 和手机等轻客户端发送数据。

CachedRowSet接口定义了所有断开连接的RowSet对象可用的基本功能。其他三个是CachedRowSet接口的扩展,提供更专业的功能。以下信息显示了它们之间的关系:

一个CachedRowSet对象具有JdbcRowSet对象的所有功能,还可以执行以下操作:

  • 获取到数据源的连接并执行查询

  • 从生成的ResultSet对象中读取数据并用该数据填充自身

  • 在断开连接时操作数据并对数据进行更改

  • 重新连接到数据源以将更改写回

  • 检查与数据源的冲突并解决这些冲突

一个WebRowSet对象具有CachedRowSet对象的所有功能,还可以执行以下操作:

  • 将自身写为 XML 文档

  • 读取描述WebRowSet对象的 XML 文档

一个JoinRowSet对象具有WebRowSet对象(因此也具有CachedRowSet对象)的所有功能,还可以执行以下操作:

  • 形成等效于SQL JOIN的操作而无需连接到数据源

一个FilteredRowSet对象同样具有WebRowSet对象(因此也具有CachedRowSet对象)的所有功能,还可以执行以下操作:

  • 应用过滤条件,以便只有选定的数据可见。这相当于在RowSet对象上执行查询,而无需使用查询语言或连接到数据源。

使用 JdbcRowSet 对象

原文:docs.oracle.com/javase/tutorial/jdbc/basics/jdbcrowset.html

JdbcRowSet 对象是一个增强的 ResultSet 对象。它与数据源保持连接,就像 ResultSet 对象一样。最大的区别在于它具有一组属性和监听器通知机制,使其成为一个 JavaBeans 组件。

JdbcRowSet 对象的主要用途之一是使一个 ResultSet 对象在没有这些功能的情况下可滚动和可更新。

本节涵盖以下主题:

  • 创建 JdbcRowSet 对象

  • 默认 JdbcRowSet 对象

  • 设置属性

  • 使用 JdbcRowSet 对象

  • 代码示例

创建 JdbcRowSet 对象

通过使用 RowSetProvider 类创建的 RowSetFactory 实例来创建 JdbcRowSet 对象。以下示例来自 JdbcRowSetSample.java

    RowSetFactory factory = RowSetProvider.newFactory();

    try (JdbcRowSet jdbcRs = factory.createJdbcRowSet()) {
      jdbcRs.setUrl(this.settings.urlString);
      jdbcRs.setUsername(this.settings.userName);
      jdbcRs.setPassword(this.settings.password);
      jdbcRs.setCommand("select * from COFFEES");
      jdbcRs.execute();
      // ...

RowSetFactory 接口包含创建不同类型 RowSet 实现的方法:

  • createCachedRowSet

  • createFilteredRowSet

  • createJdbcRowSet

  • createJoinRowSet

  • createWebRowSet

默认 JdbcRowSet 对象

使用 RowSetFactory 实例创建 JdbcRowSet 对象时,新的 JdbcRowSet 对象将具有以下属性:

  • type: ResultSet.TYPE_SCROLL_INSENSITIVE(具有可滚动游标)

  • concurrency: ResultSet.CONCUR_UPDATABLE(可更新)

  • escapeProcessing: true(驱动程序将执行转义处理;启用转义处理时,驱动程序将扫描任何转义语法并将其转换为特定数据库理解的代码)

  • maxRows: 0(行数没有限制)

  • maxFieldSize: 0(列值的字节数没有限制;仅适用于存储 BINARYVARBINARYLONGVARBINARYCHARVARCHARLONGVARCHAR 值的列)

  • queryTimeout: 0(执行查询的时间没有限制)

  • showDeleted: false(已删除的行不可见)

  • transactionIsolation: Connection.TRANSACTION_READ_COMMITTED(仅读取已提交的数据)

  • typeMap: null(与此 RowSet 对象一起使用的 Connection 对象关联的类型映射为 null

你必须记住的主要事项是,JdbcRowSet 和所有其他 RowSet 对象都是可滚动和可更新的,除非你为这些属性设置了不同的值。

设置属性

章节 默认 JdbcRowSet 对象 列出了创建新 JdbcRowSet 对象时默认设置的属性。如果使用默认构造函数,必须在填充新的 JdbcRowSet 对象数据之前设置一些额外的属性。

要获取其数据,JdbcRowSet对象首先需要连接到数据库。以下四个属性保存用于获取数据库连接的信息。

  • username:用户作为访问权限的一部分向数据库提供的名称

  • password:用户的数据库密码

  • url:用户想要连接的数据库的 JDBC URL

  • datasourceName:用于检索已在 JNDI 命名服务中注册的DataSource对象的名称

你设置哪些属性取决于你如何进行连接。首选的方式是使用DataSource对象,但你可能无法将DataSource对象注册到 JNDI 命名服务中,这通常由系统管理员完成。因此,代码示例都使用DriverManager机制来获取连接,你需要使用url属性而不是datasourceName属性。

另一个你必须设置的属性是command属性。该属性是确定JdbcRowSet对象将保存什么数据的查询。例如,以下代码行使用查询设置了command属性,该查询生成一个包含表COFFEES中所有数据的ResultSet对象:

jdbcRs.setCommand("select * from COFFEES");

在设置了command属性和连接所需的属性之后,通过调用execute方法,你就可以准备好通过数据填充jdbcRs对象。

jdbcRs.execute();

execute方法在后台为你执行许多操作:

  • 它使用你分配给urlusernamepassword属性的值连接到数据库。

  • 它执行你在command属性中设置的查询。

  • 它将结果的数据从ResultSet对象读取到jdbcRs对象中。

使用 JdbcRowSet 对象

你在JdbcRowSet对象中更新、插入和删除行的方式与在可更新的ResultSet对象中更新、插入和删除行的方式相同。同样,你在JdbcRowSet对象中导航的方式与在可滚动的ResultSet对象中导航的方式相同。

Coffee Break 咖啡连锁店收购了另一家咖啡连锁店,现在拥有一个不支持结果集滚动或更新的传统数据库。换句话说,由这个传统数据库产生的任何ResultSet对象都没有可滚动的游标,其中的数据也无法修改。然而,通过创建一个从ResultSet对象中获取数据的JdbcRowSet对象,实际上可以使ResultSet对象可滚动和可更新。

如前所述,JdbcRowSet对象默认是可滚动和可更新的。因为其内容与ResultSet对象中的内容相同,对JdbcRowSet对象的操作等同于对ResultSet对象本身的操作。而且因为JdbcRowSet对象与数据库有持续连接,它对自己数据所做的更改也会应用到数据库中的数据。

本节涵盖以下主题:

  • 导航 JdbcRowSet 对象

  • 更新列值

  • 插入行

  • 删除行

导航 JdbcRowSet 对象

一个不可滚动的ResultSet对象只能使用next方法将其光标向前移动,并且只能从第一行向最后一行向前移动。然而,默认的JdbcRowSet对象可以使用ResultSet接口中定义的所有光标移动方法。

一个JdbcRowSet对象可以调用方法next,也可以调用任何其他ResultSet光标移动方法。例如,以下代码行将光标移动到jdbcRs对象的第四行,然后再移回第三行:

jdbcRs.absolute(4);
jdbcRs.previous();

方法previous类似于方法next,可以在while循环中用于按顺序遍历所有行。不同之处在于你必须将光标移动到最后一行之后的位置,而previous将光标向前移动。

更新列值

你可以像更新ResultSet对象中的数据一样更新JdbcRowSet对象中的数据。

假设 Coffee Break 的老板想要提高一磅 Espresso 咖啡的价格。如果老板知道 Espresso 在jdbcRs对象的第三行,那么执行此操作的代码可能如下所示:

jdbcRs.absolute(3);
jdbcRs.updateFloat("PRICE", 10.99f);
jdbcRs.updateRow();

该代码将光标移动到第三行,并将PRICE列的值更改为 10.99,然后使用新价格更新数据库。

调用方法updateRow会更新数据库,因为jdbcRs已经保持了与数据库的连接。对于断开连接的RowSet对象,情况是不同的。

插入行

如果 Coffee Break 连锁店的老板想要添加一种或多种咖啡到他所提供的咖啡中,那么老板需要为每种新咖啡在COFFEES表中添加一行,就像在JdbcRowSetSample.java中的以下代码片段中所做的那样。请注意,由于jdbcRs对象始终连接到数据库,向JdbcRowSet对象插入一行与向ResultSet对象插入一行相同:你移动光标到插入行,使用适当的更新方法为每列设置一个值,然后调用方法insertRow

jdbcRs.moveToInsertRow();
jdbcRs.updateString("COF_NAME", "HouseBlend");
jdbcRs.updateInt("SUP_ID", 49);
jdbcRs.updateFloat("PRICE", 7.99f);
jdbcRs.updateInt("SALES", 0);
jdbcRs.updateInt("TOTAL", 0);
jdbcRs.insertRow();

jdbcRs.moveToInsertRow();
jdbcRs.updateString("COF_NAME", "HouseDecaf");
jdbcRs.updateInt("SUP_ID", 49);
jdbcRs.updateFloat("PRICE", 8.99f);
jdbcRs.updateInt("SALES", 0);
jdbcRs.updateInt("TOTAL", 0);
jdbcRs.insertRow();

当你调用方法insertRow时,新行将被插入jdbcRs对象并同时插入数据库。上述代码片段经历了这个过程两次,所以两行新行被插入jdbcRs对象和数据库。

删除行

与更新数据和插入新行一样,删除行对于JdbcRowSet对象和ResultSet对象来说是一样的。老板想要停止销售最后一行在jdbcRs对象中的 French Roast 无咖啡因咖啡。在以下代码行中,第一行将光标移动到最后一行,第二行删除了jdbcRs对象和数据库中的最后一行:

jdbcRs.last();
jdbcRs.deleteRow();

代码示例

示例JdbcRowSetSample.java执行以下操作:

  • 创建一个使用执行检索COFFEES表中所有行的查询产生的ResultSet对象初始化的新JdbcRowSet对象

  • 将光标移动到COFFEES表的第三行,并更新该行的PRICE

  • 插入两行新行,一个是HouseBlend,另一个是HouseDecaf

  • 将光标移动到最后一行并将其删除

使用 CachedRowSetObjects

原文:docs.oracle.com/javase/tutorial/jdbc/basics/cachedrowset.html

CachedRowSet对象很特殊,它可以在不连接到数据源的情况下运行,也就是说,它是一个断开连接RowSet对象。它的名字来源于它将数据存储(缓存)在内存中,这样它可以操作自己的数据而不是数据库中存储的数据。

CachedRowSet接口是所有断开连接的RowSet对象的超级接口,因此这里展示的所有内容也适用于WebRowSetJoinRowSetFilteredRowSet对象。

请注意,尽管CachedRowSet对象(以及从中派生的RowSet对象)的数据源几乎总是关系数据库,但CachedRowSet对象能够从以表格格式存储数据的任何数据源获取数据。例如,平面文件或电子表格可以是数据的来源。当为断开连接的RowSet对象实现RowSetReader对象以从这样的数据源读取数据时,这一点是正确的。CachedRowSet接口有一个从关系数据库读取数据的RowSetReader对象,因此在本教程中,数据源始终是数据库。

下面涵盖了以下主题:

  • 设置 CachedRowSet 对象

  • 填充 CachedRowSet 对象

  • Reader 的作用

  • 更新 CachedRowSet 对象

  • 更新数据源

  • Writer 的作用

  • 通知监听器

  • 发送大量数据

设置 CachedRowSet 对象

设置CachedRowSet对象涉及以下内容:

  • 创建 CachedRowSet 对象

  • 设置 CachedRowSet 属性

  • 设置关键列

创建 CachedRowSet 对象

通过使用RowSetProvider类创建的RowSetFactory实例来创建一个新的CachedRowSet对象。

以下示例来自CachedRowSetSample.java创建了一个CachedRowSet对象:

RowSetFactory factory = RowSetProvider.newFactory();
CachedRowSet crs = factory.createCachedRowSet();

对象crs的属性具有与创建时JdbcRowSet对象相同的默认值。此外,它已被分配默认SyncProvider实现RIOptimisticProvider的实例。

一个SyncProvider对象提供了一个RowSetReader对象(一个读取器)和一个RowSetWriter对象(一个写入器),一个断开连接的RowSet对象需要这些对象来从数据源读取数据或将数据写回数据源。读取器和写入器的功能将在后面的章节读取器的功能和写入器的功能中解释。需要记住的一点是,读取器和写入器完全在后台工作,因此它们如何工作的解释仅供参考。了解读取器和写入器的一些背景知识应该有助于你理解CachedRowSet接口中一些方法在后台做什么。

设置 CachedRowSet 属性

通常情况下,属性的默认值都是可以的,但是你可以通过调用适当的 setter 方法来更改属性的值。有一些没有默认值的属性,你必须自己设置。

为了获取数据,一个断开连接的RowSet对象必须能够连接到数据源,并且有一些选择要保存的数据的方法。以下属性保存了获取数据库连接所需的信息。

  • username: 用户在获取访问权限时向数据库提供的名称

  • password: 用户的数据库密码

  • url: 用户想要连接的数据库的 JDBC URL

  • datasourceName: 用于检索已经注册到 JNDI 命名服务的 DataSource 对象的名称

你必须设置哪些属性取决于你将如何建立连接。首选的方式是使用DataSource对象,但是你可能无法将DataSource对象注册到 JNDI 命名服务中,这通常由系统管理员完成。因此,代码示例都使用DriverManager机制来获取连接,你需要使用url属性而不是datasourceName属性。

以下代码行设置了usernamepasswordurl属性,以便使用DriverManager类获取连接。(你可以在你的 JDBC 驱动程序的文档中找到要设置为url属性值的 JDBC URL。)

public void setConnectionProperties(
    String username, String password) {
    crs.setUsername(username);
    crs.setPassword(password);
    crs.setUrl("jdbc:mySubprotocol:mySubname");
    // ...

另一个你必须设置的属性是command属性。数据从ResultSet对象读入RowSet对象。产生该ResultSet对象的查询是command属性的值。例如,以下代码行使用一个查询设置了command属性,该查询产生一个包含表MERCH_INVENTORY中所有数据的ResultSet对象:

crs.setCommand("select * from MERCH_INVENTORY");

设置关键列

如果要对 crs 对象进行任何更新并希望将这些更新保存到数据库中,必须设置另一个信息:关键列。关键列本质上与主键相同,因为它们指示唯一标识一行的一个或多个列。不同之处在于,主键设置在数据库中的表上,而关键列设置在特定的 RowSet 对象上。以下代码行将 crs 的关键列设置为第一列:

int[] keys = {1};
crs.setKeyColumns(keys);

MERCH_INVENTORY 中的第一列是 ITEM_ID。它可以作为关键列,因为每个项目标识符都不同,因此唯一标识表 MERCH_INVENTORY 中的一行且仅一行。此外,该列在 MERCH_INVENTORY 表的定义中被指定为主键。方法 setKeyColumns 接受一个数组,以允许可能需要两个或更多列来唯一标识一行。

有趣的一点是,方法 setKeyColumns 不设置属性的值。在这种情况下,它为字段 keyCols 设置值。关键列在内部使用,因此在设置它们之后,您不再对其进行任何操作。您将在 使用 SyncResolver 对象 部分中看到关键列是如何使用的。

填充 CachedRowSet 对象

填充断开连接的 RowSet 对象比填充连接的 RowSet 对象需要更多的工作。幸运的是,额外的工作是在后台完成的。在完成了设置 CachedRowSet 对象 crs 的初步工作后,以下代码行填充了 crs

crs.execute();

crs 中的数据是通过执行命令属性中的查询生成的 ResultSet 对象中的数据。

不同的是,CachedRowSet 实现的 execute 方法比 JdbcRowSet 实现做了更多的工作。更正确地说,CachedRowSet 对象的读取器,该方法委托其任务的对象,做了更多的工作。

每个断开连接的 RowSet 对象都分配了一个 SyncProvider 对象,并且这个 SyncProvider 对象提供了 RowSet 对象的 读取器(一个 RowSetReader 对象)。当创建 crs 对象时,它被用作默认的 CachedRowSetImpl 构造函数,除了为属性设置默认值外,还将 RIOptimisticProvider 实现的实例分配为默认的 SyncProvider 对象。

读取器的功能

当应用程序调用execute方法时,一个断开连接的RowSet对象的读取器在后台工作,将RowSet对象填充到数据中。新创建的CachedRowSet对象未连接到数据源,因此必须获取与该数据源的连接才能从中获取数据。默认的SyncProvider对象(RIOptimisticProvider)提供一个读取器,通过使用最近设置的用户名、密码和 JDBC URL 或数据源名称中的值来获取连接。然后读取器执行为命令设置的查询。它读取查询生成的ResultSet对象中的数据,将CachedRowSet对象填充到该数据中。最后,读取器关闭连接。

更新 CachedRowSet 对象

在 Coffee Break 场景中,所有者希望简化操作。所有者决定让仓库员工直接在 PDA(个人数字助理)中输入库存,从而避免让第二个人进行数据输入的容易出错的过程。在这种情况下,CachedRowSet对象是理想的,因为它轻量级、可序列化,并且可以在没有与数据源连接的情况下进行更新。

所有者将要求应用程序开发团队为仓库员工用于输入库存数据的 PDA 创建一个 GUI 工具。总部将创建一个填充有显示当前库存的表格的CachedRowSet对象,并通过互联网将其发送到 PDA。当仓库员工使用 GUI 工具输入数据时,该工具将每个条目添加到一个数组中,CachedRowSet对象将使用该数组在后台执行更新。完成库存后,PDA 将其新数据发送回总部,数据将上传到主服务器。

本节涵盖以下主题:

  • 更新列值

  • 插入和删除行

更新列值

更新CachedRowSet对象中的数据与更新JdbcRowSet对象中的数据完全相同。例如,来自CachedRowSetSample.java的以下代码片段将ITEM_ID列具有12345物品标识符的行中的QUAN列的值增加 1:

        while (crs.next()) {
          System.out.println("Found item " + crs.getInt("ITEM_ID") + ": " +
                             crs.getString("ITEM_NAME"));
          if (crs.getInt("ITEM_ID") == 1235) {
            int currentQuantity = crs.getInt("QUAN") + 1;
            System.out.println("Updating quantity to " + currentQuantity);
            crs.updateInt("QUAN", currentQuantity + 1);
            crs.updateRow();
            // Syncing the row back to the DB
            crs.acceptChanges(con);
          }
        } // End of inner while

插入和删除行

就像更新列值一样,在CachedRowSet对象中插入和删除行的代码与JdbcRowSet对象相同。

来自CachedRowSetSample.java的以下摘录将新行插入到CachedRowSet对象crs中:

crs.moveToInsertRow();
crs.updateInt("ITEM_ID", newItemId);
crs.updateString("ITEM_NAME", "TableCloth");
crs.updateInt("SUP_ID", 927);
crs.updateInt("QUAN", 14);
Calendar timeStamp;
timeStamp = new GregorianCalendar();
timeStamp.set(2006, 4, 1);
crs.updateTimestamp(
    "DATE_VAL",
    new Timestamp(timeStamp.getTimeInMillis()));
crs.insertRow();
crs.moveToCurrentRow();

如果总部决定停止储存某个特定物品,可能会直接删除咖啡本身的行。然而,在这种情况下,使用 PDA 的仓库员工也有能力删除它。以下代码片段找到ITEM_ID列中值为12345的行,并从CachedRowSet crs中删除它:

while (crs.next()) {
    if (crs.getInt("ITEM_ID") == 12345) {
        crs.deleteRow();
        break;
    }
}

更新数据源

JdbcRowSet对象进行更改与对CachedRowSet对象进行更改之间存在重大差异。因为JdbcRowSet对象连接到其数据源,updateRowinsertRowdeleteRow方法可以更新JdbcRowSet对象和数据源。然而,在断开连接的RowSet对象的情况下,这些方法会更新CachedRowSet对象内存中存储的数据,但无法影响数据源。断开连接的RowSet对象必须调用acceptChanges方法才能将其更改保存到数据源。在库存场景中,总部的应用程序将调用acceptChanges方法以更新数据库中QUAN列的新值。

crs.acceptChanges();

写入器的功能

execute方法类似,acceptChanges方法会在后台完成其工作。execute方法将其工作委托给RowSet对象的读取器,而acceptChanges方法将其任务委托给RowSet对象的写入器。在后台,写入器会打开与数据库的连接,使用RowSet对象所做的更改更新数据库,然后关闭连接。

使用默认实现

难点在于可能会出现冲突。冲突是指另一方已经更新了数据库中与RowSet对象中更新的值对应的值的情况。数据库中应该保留哪个值?当存在冲突时,写入器的处理方式取决于其如何实现,有许多可能性。在一个极端,写入器甚至不检查冲突,只是将所有更改写入数据库。这是RIXMLProvider实现的情况,它被WebRowSet对象使用。在另一端,写入器通过设置数据库锁来确保没有冲突,防止他人进行更改。

crs对象的写入器是默认SyncProvider实现提供的一个,名为RIOptimisticProviderRIOPtimisticProvider实现得名于其采用的乐观并发模型。该模型假设冲突会很少,甚至没有,因此不设置数据库锁。写入器会检查是否存在冲突,如果没有,则将对crs对象所做的更改写入数据库,这些更改变得持久。如果存在冲突,默认情况下不会将新的RowSet值写入数据库。

在这种情况下,默认行为非常有效。因为总部的人不太可能更改COF_INVENTORYQUAN列中的值,所以不会发生冲突。因此,在仓库中输入到crs对象中的值将被写入数据库,从而变得持久,这是期望的结果。

使用 SyncResolver 对象

然而,在其他情况下,可能存在冲突。为了适应这些情况,RIOPtimisticProvider实现提供了一个选项,让你查看冲突中的值,并决定哪些值应该持久化。这个选项就是使用SyncResolver对象。

当写入程序完成查找冲突并找到一个或多个冲突时,它会创建一个包含导致冲突的数据库值的SyncResolver对象。接下来,方法acceptChanges抛出一个SyncProviderException对象,应用程序可以捕获并用于检索SyncResolver对象。以下代码行检索SyncResolver对象resolver

try {
    crs.acceptChanges();
} catch (SyncProviderException spe) {
    SyncResolver resolver = spe.getSyncResolver();
}

对象resolver是一个RowSet对象,复制了crs对象,只包含导致冲突的数据库中的值。所有其他列值都为 null。

使用resolver对象,你可以迭代其行以定位不为空且因此引起冲突的值。然后你可以定位crs对象中相同位置的值并进行比较。以下代码片段检索resolver并使用SyncResolver方法nextConflict来迭代具有冲突值的行。对象resolver获取每个冲突值的状态,如果是UPDATE_ROW_CONFLICT,表示crs在冲突发生时正在尝试更新,则resolver对象获取该值的行号。然后代码将crs对象的游标移动到相同的行。接下来,代码找到resolver对象中包含冲突值的行中的列,该列将是一个不为空的值。从resolvercrs对象中检索该列中的值后,你可以比较两者并决定哪个值应该持久化。最后,代码使用setResolvedValue方法在crs对象和数据库中设置该值,如下所示来自CachedRowSetSample.java的代码:

    try {
        // ...
        // Syncing the new row back to the database.
        System.out.println("About to add a new row...");
        crs.acceptChanges(con);
        System.out.println("Added a row...");
        this.viewTable(con);
        // ...
    } catch (SyncProviderException spe) {

      SyncResolver resolver = spe.getSyncResolver();

      Object crsValue; // value in the RowSet object
      Object resolverValue; // value in the SyncResolver object
      Object resolvedValue; // value to be persisted

      while (resolver.nextConflict()) {

        if (resolver.getStatus() == SyncResolver.INSERT_ROW_CONFLICT) {
          int row = resolver.getRow();
          crs.absolute(row);

          int colCount = crs.getMetaData().getColumnCount();
          for (int j = 1; j <= colCount; j++) {
            if (resolver.getConflictValue(j) != null) {
              crsValue = crs.getObject(j);
              resolverValue = resolver.getConflictValue(j);

              // Compare crsValue and resolverValue to determine
              // which should be the resolved value (the value to persist)
              //
              // This example chooses the value in the RowSet object,
              // crsValue, to persist.,

              resolvedValue = crsValue;

              resolver.setResolvedValue(j, resolvedValue);
            }
          }
        }
      }
    }

通知监听器

作为 JavaBeans 组件意味着RowSet对象在发生某些事情时可以通知其他组件。例如,如果RowSet对象中的数据发生变化,RowSet对象可以通知感兴趣的方。这种通知机制的好处在于,作为应用程序员,你只需添加或移除将被通知的组件。

本节涵盖以下主题:

  • 设置监听器

  • 通知工作原理

设置监听器

一个RowSet对象的监听器是一个实现RowSetListener接口的组件,该接口包括以下方法:

  • cursorMoved:定义了当RowSet对象中的游标移动时,监听器将执行什么操作(如果有的话)。

  • rowChanged:定义了监听器在一行中一个或多个列值发生变化、插入了一行或删除了一行时将执行的操作(如果有的话)。

  • rowSetChanged:定义了监听器在RowSet对象被填充新数据时将执行的操作(如果有的话)。

一个可能想要成为监听器的组件示例是一个将RowSet对象中的数据制成图表的BarGraph对象。随着数据的变化,BarGraph对象可以更新自身以反映新数据。

作为一个应用程序员,利用通知机制的唯一要做的事情就是添加或移除监听器。下面这行代码意味着每当crs对象的光标移动,crs中的值发生变化,或者整个crs获取新数据时,BarGraph对象bar都会收到通知:

crs.addRowSetListener(bar);

你也可以通过移除监听器来停止通知,就像下面这行代码所做的那样:

crs.removeRowSetListener(bar);

使用咖啡休息场景,假设总部定期检查数据库以获取在线销售的咖啡最新价格列表。在这种情况下,监听器是咖啡休息网站上的PriceList对象priceList,它必须实现RowSetListener方法cursorMovedrowChangedrowSetChangedcursorMoved方法的实现可能是什么都不做,因为光标的位置不会影响priceList对象。另一方面,rowChangedrowSetChanged方法的实现必须确定所做的更改,并相应地更新priceList

通知如何工作

导致任何RowSet事件的方法会自动通知所有注册的监听器。例如,任何移动光标的方法也会调用每个监听器的cursorMoved方法。同样,execute方法会调用所有监听器的rowSetChanged方法,而acceptChanges会调用所有监听器的rowChanged方法。

发送大量数据

该方法CachedRowSetSample.java演示了如何将数据分成较小的部分发送。

使用 JoinRowSet 对象

原文:docs.oracle.com/javase/tutorial/jdbc/basics/joinrowset.html

JoinRowSet实现允许您在RowSet对象之间创建 SQL JOIN,当它们未连接到数据源时。这很重要,因为它节省了必须创建一个或多个连接的开销。

下面涵盖的主题有:

  • 创建 JoinRowSet 对象

  • 添加 RowSet 对象

  • 管理匹配列

JoinRowSet接口是CachedRowSet接口的子接口,因此继承了CachedRowSet对象的功能。这意味着JoinRowSet对象是一个断开连接的RowSet对象,可以在不始终连接到数据源的情况下运行。

创建 JoinRowSet 对象

JoinRowSet对象充当 SQL JOIN的持有者。来自JoinSample.java的以下示例显示了如何创建JoinRowSet对象:

    RowSetFactory factory = RowSetProvider.newFactory();  
    try (CachedRowSet coffees = factory.createCachedRowSet();
         CachedRowSet suppliers = factory.createCachedRowSet();
         JoinRowSet jrs = factory.createJoinRowSet()) {
      coffees.setCommand("SELECT * FROM COFFEES");
      // Set connection parameters for the CachedRowSet coffees
      coffees.execute();

      suppliers.setCommand("SELECT * FROM SUPPLIERS");
      // Set connection parameters for the CachedRowSet suppliers      suppliers.execute();      

      jrs.addRowSet(coffees, "SUP_ID");
      jrs.addRowSet(suppliers, "SUP_ID");

      // ...

变量jrs在添加RowSet对象之前不包含任何内容。

添加 RowSet 对象

任何RowSet对象都可以添加到JoinRowSet对象中,只要它可以成为 SQL JOIN的一部分。JdbcRowSet对象始终连接到其数据源,可以添加,但通常通过直接与数据源操作而不是通过添加到JoinRowSet对象来成为JOIN的一部分。提供JoinRowSet实现的目的是使断开连接的RowSet对象能够成为JOIN关系的一部分。

The owner of The Coffee Break chain of coffee houses wants to get a list of the coffees he buys from Acme, Inc. In order to do this, the owner will have to get information from two tables, COFFEES and SUPPLIERS. In the database world before RowSet technology, programmers would send the following query to the database:

String query =
    "SELECT COFFEES.COF_NAME " +
    "FROM COFFEES, SUPPLIERS " +
    "WHERE SUPPLIERS.SUP_NAME = Acme.Inc. " +
    "and " +
    "SUPPLIERS.SUP_ID = COFFEES.SUP_ID";

RowSet技术的世界中,您可以在不向数据源发送查询的情况下实现相同的结果。您可以将包含两个表中数据的RowSet对象添加到JoinRowSet对象中。然后,因为所有相关数据都在JoinRowSet对象中,您可以对其执行查询以获取所需数据。

来自JoinSample.testJoinRowSet的以下代码片段创建了两个CachedRowSet对象,coffees中包含来自表COFFEES的数据,suppliers中包含来自表SUPPLIERS的数据。coffeessuppliers对象必须连接到数据库以执行其命令并获取数据,但完成后,它们不必重新连接以形成JOIN

    try (CachedRowSet coffees = factory.createCachedRowSet();
         CachedRowSet suppliers = factory.createCachedRowSet();
         JoinRowSet jrs = factory.createJoinRowSet()) {
      coffees.setCommand("SELECT * FROM COFFEES");
      coffees.setUsername(settings.userName);
      coffees.setPassword(settings.password);
      coffees.setUrl(settings.urlString);
      coffees.execute();

      suppliers.setCommand("SELECT * FROM SUPPLIERS");
      suppliers.setUsername(settings.userName);
      suppliers.setPassword(settings.password);
      suppliers.setUrl(settings.urlString);
      suppliers.execute();  
	  // ...

管理匹配列

查看SUPPLIERS表,您会发现 Acme, Inc. 的标识号为 101。在COFFEES表中,供应商标识号为 101 的咖啡有哥伦比亚咖啡和哥伦比亚无咖啡因。这两个表的信息可以进行连接,因为两个表都有一个名为SUP_ID的列。在 JDBC 的RowSet技术中,JOIN所基于的列SUP_ID被称为匹配列

添加到JoinRowSet对象中的每个RowSet对象必须具有匹配列,即JOIN所基于的列。有两种方法可以为RowSet对象设置匹配列。第一种方法是将匹配列传递给JoinRowSet方法addRowSet,如下面的代码所示:

jrs.addRowSet(coffees, "SUP_ID");

此行代码将coffeesCachedRowSet添加到jrs对象中,并将coffeesSUP_ID列设置为匹配列。

此时,jrs中只有coffees。下一个添加到jrs中的RowSet对象必须能够与coffees进行JOIN,这对于suppliers是成立的,因为两个表都有SUP_ID列。下面的代码将suppliers添加到jrs中,并将SUP_ID列设置为匹配列。

jrs.addRowSet(suppliers, "SUP_ID");

现在jrs包含了coffeessuppliers之间的JOIN,所有者可以从中获取 Acme, Inc.供应的咖啡的名称。因为代码没有设置JOIN的类型,jrs保持内部 JOIN,这是默认值。换句话说,jrs中的一行组合了coffees中的一行和suppliers中的一行。它包含了coffees中的列以及suppliers中的列,对于COFFEES.SUP_ID列的值与SUPPLIERS.SUP_ID列的值匹配的行。以下代码打印出由 Acme, Inc.供应的咖啡的名称,其中String supplierName等于Acme, Inc.请注意,这是可能的,因为JoinRowSet对象jrs中现在包括了来自suppliersSUP_NAME列和来自coffeesCOF_NAME列。

      System.out.println("Coffees bought from " + supplierName + ": ");
      while (jrs.next()) {
        if (jrs.getString("SUP_NAME").equals(supplierName)) { 
          String coffeeName = jrs.getString(1);
          System.out.println("     " + coffeeName);
        }
      }

这将产生类似以下的输出:

Coffees bought from Acme, Inc.:
     Colombian
     Colombian_Decaf

JoinRowSet接口提供了用于设置将形成的JOIN类型的常量,但目前实现的唯一类型是JoinRowSet.INNER_JOIN

使用 FilteredRowSet 对象

原文:docs.oracle.com/javase/tutorial/jdbc/basics/filteredrowset.html

FilteredRowSet对象允许您减少在RowSet对象中可见的行数,以便您只处理与您正在进行的工作相关的数据。您决定要对数据设置什么限制(如何“过滤”数据),并将该过滤器应用到FilteredRowSet对象上。换句话说,FilteredRowSet对象只显示符合您设置限制的数据行。JdbcRowSet对象始终与其数据源连接,可以通过向数据源发送仅选择您想要查看的列和行的查询来进行此过滤。查询的WHERE子句定义了过滤条件。FilteredRowSet对象提供了一种让断开连接的RowSet对象进行此过滤的方法,而无需在数据源上执行查询,从而避免必须连接到数据源并向其发送查询。

例如,假设咖啡休息连锁店已经在美利坚合众国各地发展到数百家店铺,并且所有店铺都列在名为COFFEE_HOUSES的表中。业主希望通过一款不需要与数据库系统保持持久连接的咖啡店比较应用程序来衡量仅加利福尼亚州的店铺的成功。这种比较将关注销售商品与销售咖啡饮料的盈利能力以及其他各种成功指标,并将按照咖啡饮料销售额、商品销售额和总销售额对加利福尼亚州的店铺进行排名。由于COFFEE_HOUSES表有数百行数据,如果将搜索的数据量减少到仅包含STORE_ID列中指示加利福尼亚州的行,这些比较将更快更容易进行。

这正是FilteredRowSet对象通过提供以下功能来解决的问题:

  • 能够根据设定的条件限制可见的行

  • 能够选择哪些数据可见而无需连接到数据源

下面的主题包括:

  • 在谓词对象中定义过滤条件

  • 创建 FilteredRowSet 对象

  • 创建和设置谓词对象

  • 使用新的谓词对象设置 FilteredRowSet 对象以进一步过滤数据

  • 更新 FilteredRowSet 对象

  • 插入或更新行

  • 删除所有过滤器以使所有行可见

  • 删除行

在谓词对象中定义过滤条件

要设置FilteredRowSet对象中哪些行可见的条件,您需要定义一个实现Predicate接口的类。使用此类创建的对象将初始化为以下内容:

  • 值必须落在的范围的高端

  • 值必须落在的范围的低端

  • 列名或列号是必须落在高低边界设置的值范围内的值所在的列的列名或列号

请注意,值范围是包容的,这意味着边界处的值包括在范围内。例如,如果范围的高端为 100,低端为 50,则 50 的值被视为在范围内。49 不在范围内。同样,100 在范围内,但 101 不在范围内。

符合业主想要比较加利福尼亚店铺的情景,必须编写一个实现Predicate接口的过滤位于加利福尼亚的 Coffee Break 咖啡店的类。没有一种正确的方法来做这件事,这意味着在编写实现的方式上有很大的自由度。例如,您可以随意命名类及其成员,并以任何实现方式编写构造函数和三个评估方法,以实现所需的结果。

列出所有咖啡店的表名为COFFEE_HOUSES,有数百行。为了使事情更易管理,此示例使用了少得多的行数的表,足以演示如何进行过滤。

STORE_ID中的值是一个int值,表示咖啡店所在的州等信息。例如,以 10 开头的值表示该州是加利福尼亚。以 32 开头的STORE_ID值表示俄勒冈州,以 33 开头的表示华盛顿州。

以下类,StateFilter,实现了Predicate接口:

public class StateFilter implements Predicate {

    private int lo;
    private int hi;
    private String colName = null;
    private int colNumber = -1;

    public StateFilter(int lo, int hi, int colNumber) {
        this.lo = lo;
        this.hi = hi;
        this.colNumber = colNumber;
    }

    public StateFilter(int lo, int hi, String colName) {
        this.lo = lo;
        this.hi = hi;
        this.colName = colName;
    }

    public boolean evaluate(Object value, String columnName) {
        boolean evaluation = true;
        if (columnName.equalsIgnoreCase(this.colName)) {
            int columnValue = ((Integer)value).intValue();
            if ((columnValue >= this.lo)
                &&
                (columnValue <= this.hi)) {
                evaluation = true;
            } else {
                evaluation = false;
            }
        }
        return evaluation;
    }

    public boolean evaluate(Object value, int columnNumber) {

        boolean evaluation = true;

        if (this.colNumber == columnNumber) {
            int columnValue = ((Integer)value).intValue();
            if ((columnValue >= this.lo)
                &&
                (columnValue <= this.hi)) {
                evaluation = true;
            } else {
                evaluation = false;
            }
        }
        return evaluation;
    }

    public boolean evaluate(RowSet rs) {

        CachedRowSet frs = (CachedRowSet)rs;
        boolean evaluation = false;

        try {
            int columnValue = -1;

            if (this.colNumber > 0) {
                columnValue = frs.getInt(this.colNumber);
            } else if (this.colName != null) {
                columnValue = frs.getInt(this.colName);
            } else {
                return false;
            }

            if ((columnValue >= this.lo)
                &&
                (columnValue <= this.hi)) {
                evaluation = true;
            }
        } catch (SQLException e) {
            JDBCTutorialUtilities.printSQLException(e);
            return false;
        } catch (NullPointerException npe) {
            System.err.println("NullPointerException caught");
            return false;
        }
        return evaluation;
    }
}

这是一个非常简单的实现,检查由colNamecolNumber指定的列中的值是否在lohi的范围内,包括边界。以下代码行,来自FilteredRowSetSample.java,创建了一个过滤器,仅允许STORE_ID列值指示介于 10000 和 10999 之间的行,这表示加利福尼亚位置:

StateFilter myStateFilter = new StateFilter(10000, 10999, 1);

请注意,刚刚定义的StateFilter类仅适用于一列。可以通过将每个参数数组而不是单个值来使其适用于两个或更多列。例如,Filter对象的构造函数可能如下所示:

public Filter2(Object [] lo, Object [] hi, Object [] colNumber) {
    this.lo = lo;
    this.hi = hi;
    this.colNumber = colNumber;
}

colNumber对象中的第一个元素表示将根据其在lohi中的第一个元素进行检查的第一列。由colNumber指示的第二列中的值将与lohi中的第二个元素进行检查,依此类推。因此,这三个数组中的元素数量应该相同。下面的代码是evaluate(RowSet rs)方法的一个实现示例,用于Filter2对象,其中参数是数组:

public boolean evaluate(RowSet rs) {
    CachedRowSet crs = (CachedRowSet)rs;
    boolean bool1;
    boolean bool2;
    for (int i = 0; i < colNumber.length; i++) {

        if ((rs.getObject(colNumber[i] >= lo [i]) &&
            (rs.getObject(colNumber[i] <= hi[i]) {
            bool1 = true;
        } else {
            bool2 = true;
        }

        if (bool2) {
            return false;
        } else {
            return true;
        }
    }
}

使用Filter2实现的优势在于可以使用任何Object类型的参数,并且可以检查一个或多个列,而无需编写另一个实现。但是,您必须传递一个Object类型,这意味着您必须将原始类型转换为其Object类型。例如,如果您使用int值作为lohi,则必须在将其传递给构造函数之前将int值转换为Integer对象。String对象已经是Object类型,因此您无需转换它们。

创建 FilteredRowSet 对象

使用从RowSetProvider类创建的RowSetFactory实例来创建FilteredRowSet对象。以下是来自FilteredRowSetSample.java的示例:

    RowSetFactory factory = RowSetProvider.newFactory();
    try (FilteredRowSet frs = factory.createFilteredRowSet()) {
      // ...

与其他断开连接的RowSet对象一样,frs对象必须从表格数据源(在参考实现中是关系数据库)中填充自身数据。下面来自FilteredRowSetSample.java的代码片段设置了连接到数据库以执行其命令所需的属性。请注意,此代码使用DriverManager类建立连接,这是为了方便起见。通常情况下,最好使用已在实现了 Java 命名和目录接口(JNDI)的命名服务中注册的DataSource对象。

    RowSetFactory factory = RowSetProvider.newFactory();
    try (FilteredRowSet frs = factory.createFilteredRowSet()){
      frs.setCommand("SELECT * FROM COFFEE_HOUSES");
      frs.setUsername(settings.userName);
      frs.setPassword(settings.password);
      frs.setUrl(settings.urlString);
      frs.execute();
      // ...

下面的代码行将frs对象填充了存储在COFFEE_HOUSE表中的数据:

frs.execute();

execute方法通过调用frsRowSetReader对象在后台执行各种操作,该对象创建连接,执行frs的命令,将frs填充为从生成的ResultSet对象中获取的数据,并关闭连接。请注意,如果COFFEE_HOUSES表的行数超过了frs对象一次性内存中可以容纳的行数,将使用CachedRowSet的分页方法。

在这种情况下,Coffee Break 的所有者将在办公室完成前述任务,然后导入或下载存储在frs对象中的信息到咖啡馆比较应用程序中。从现在开始,frs对象将独立运行,不再依赖于与数据源的连接。

创建和设置谓词对象

现在FilteredRowSet对象frs包含了 Coffee Break 商店的列表,您可以为frs对象中可见的行数设置选择条件以缩小范围。

以下代码行使用先前定义的StateFilter类来创建对象myStateFilter,该对象检查STORE_ID列以确定哪些商店位于加利福尼亚(如果其 ID 号在 10000 到 10999 之间,则商店位于加利福尼亚):

StateFilter myStateFilter = new StateFilter(10000, 10999, 1);

以下行将myStateFilter设置为frs的过滤器。

frs.setFilter(myStateFilter);

要进行实际过滤,您调用next方法,该方法在参考实现中调用您先前实现的Predicate.evaluate方法的适当版本。

如果返回值为true,则该行将可见;如果返回值为false,则该行将不可见。

使用新的Predicate对象设置 FilteredRowSet 对象以进一步过滤数据

您可以串行设置多个过滤器。第一次调用setFilter方法并传递一个Predicate对象时,您已经应用了该过滤器中的过滤条件。在对每一行调用next方法后,只有满足过滤条件的行才会可见,然后您可以再次调用setFilter,传递不同的Predicate对象。尽管一次只设置一个过滤器,但效果是两个过滤器都会累积应用。

例如,所有者通过将stateFilter设置为frsPredicate对象来检索加利福尼亚的 Coffee Break 商店列表。现在所有者想要比较加利福尼亚的两个城市中的商店,旧金山(表COFFEE_HOUSES中的 SF)和洛杉矶(表中的 LA)。首先要做的是编写一个Predicate实现,用于过滤旧金山或洛杉矶的商店:

public class CityFilter implements Predicate {

    private String[] cities;
    private String colName = null;
    private int colNumber = -1;

    public CityFilter(String[] citiesArg, String colNameArg) {
        this.cities = citiesArg;
        this.colNumber = -1;
        this.colName = colNameArg;
    }

    public CityFilter(String[] citiesArg, int colNumberArg) {
        this.cities = citiesArg;
        this.colNumber = colNumberArg;
        this.colName = null;
    }

    public boolean evaluate Object valueArg, String colNameArg) {

        if (colNameArg.equalsIgnoreCase(this.colName)) {
            for (int i = 0; i < this.cities.length; i++) {
                if (this.cities[i].equalsIgnoreCase((String)valueArg)) {
                    return true;
                }
            }
        }
        return false;
    }

    public boolean evaluate(Object valueArg, int colNumberArg) {

        if (colNumberArg == this.colNumber) {
            for (int i = 0; i < this.cities.length; i++) {
                if (this.cities[i].equalsIgnoreCase((String)valueArg)) {
                    return true;
                }
            }
        }
        return false;
    }

    public boolean evaluate(RowSet rs) {

        if (rs == null) return false;

        try {
            for (int i = 0; i < this.cities.length; i++) {

                String cityName = null;

                if (this.colNumber > 0) {
                    cityName = (String)rs.getObject(this.colNumber);
                } else if (this.colName != null) {
                    cityName = (String)rs.getObject(this.colName);
                } else {
                    return false;
                }

                if (cityName.equalsIgnoreCase(cities[i])) {
                    return true;
                }
            }
        } catch (SQLException e) {
            return false;
        }
        return false;
    }
}

来自FilteredRowSetSample.java的以下代码片段设置了新的过滤器,并遍历frs中的行,打印出CITY列包含旧金山或洛杉矶的行。请注意,frs当前仅包含商店位于加利福尼亚的行,因此当将过滤器更改为另一个Predicate对象时,state对象的条件仍然有效。接下来的代码将过滤器设置为CityFilter对象cityCityFilter实现使用数组作为构造函数的参数,以说明可以如何完成:

  public void testFilteredRowSet() throws SQLException {

    StateFilter myStateFilter = new StateFilter(10000, 10999, 1);
    String[] cityArray = { "SF", "LA" };

    CityFilter myCityFilter = new CityFilter(cityArray, 2);

	RowSetFactory factory = RowSetProvider.newFactory();

    try (FilteredRowSet frs = factory.createFilteredRowSet()){
      frs.setCommand("SELECT * FROM COFFEE_HOUSES");
      frs.setUsername(settings.userName);
      frs.setPassword(settings.password);
      frs.setUrl(settings.urlString);
      frs.execute();

      System.out.println("\nBefore filter:");
      FilteredRowSetSample.viewTable(this.con);

      System.out.println("\nSetting state filter:");
      frs.beforeFirst();
      frs.setFilter(myStateFilter);
      this.viewFilteredRowSet(frs);

      System.out.println("\nSetting city filter:");
      frs.beforeFirst();
      frs.setFilter(myCityFilter);
      this.viewFilteredRowSet(frs);
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    }
  }

输出应包含每个位于加利福尼亚旧金山或洛杉矶的商店的行。如果有一行中CITY列包含 LA 且STORE_ID列包含 40003,则不会包含在列表中,因为在将过滤器设置为state时已经被过滤掉(40003 不在 10000 到 10999 的范围内)。

更新 FilteredRowSet 对象

您可以对FilteredRowSet对象进行更改,但前提是该更改不违反当前生效的任何过滤条件。例如,如果新值或值在过滤条件内,则可以插入新行或更改现有行中的一个或多个值。

插入或更新行

假设两家新的 Coffee Break 咖啡馆刚刚开业,所有者希望将它们添加到所有咖啡馆的列表中。如果要插入的行不符合当前累积的过滤条件,则将阻止其添加。

frs对象的当前状态是设置了StateFilter对象,然后设置了CityFilter对象。因此,frs目前仅显示符合两个过滤器条件的行。同样重要的是,除非符合两个过滤器的条件,否则无法向frs对象添加行。以下代码片段尝试向frs对象插入两行新行,其中一个行中的STORE_IDCITY列的值都符合条件,另一个行中的STORE_ID的值不符合过滤条件,但CITY列的值符合:

frs.moveToInsertRow();
frs.updateInt("STORE_ID", 10101);
frs.updateString("CITY", "SF");
frs.updateLong("COF_SALES", 0);
frs.updateLong("MERCH_SALES", 0);
frs.updateLong("TOTAL_SALES", 0);
frs.insertRow();

frs.updateInt("STORE_ID", 33101);
frs.updateString("CITY", "SF");
frs.updateLong("COF_SALES", 0);
frs.updateLong("MERCH_SALES", 0);
frs.updateLong("TOTAL_SALES", 0);
frs.insertRow();
frs.moveToCurrentRow();

如果使用方法next迭代frs对象,你会发现旧金山、加利福尼亚州的新咖啡馆的行,但不会看到华盛顿州旧金山的商店的行。

删除所有过滤器以使所有行可见

所有者可以通过取消过滤器来添加华盛顿州的商店。没有设置过滤器,frs对象中的所有行再次可见,任何位置的商店都可以添加到商店列表中。以下代码行取消当前过滤器,有效地使先前在frs对象上设置的两个Predicate实现无效。

frs.setFilter(null);

删除行

如果所有者决定关闭或出售其中一家 Coffee Break 咖啡馆,所有者将希望从COFFEE_HOUSES表中删除它。只要行可见,所有者就可以删除表现不佳的咖啡馆的行。

例如,假设刚刚使用参数 null 调用了方法setFilter,则frs对象上没有设置任何过滤器。这意味着所有行都是可见的,因此可以删除。然而,在设置了过滤掉除加利福尼亚州以外任何州的StateFilter对象myStateFilter之后,只有位于加利福尼亚州的商店才能被删除。当为frs对象设置了CityFilter对象myCityFilter时,只有旧金山、加利福尼亚州或洛杉矶、加利福尼亚州的咖啡馆可以被删除,因为它们是唯一可见的行。

使用 WebRowSet 对象

docs.oracle.com/javase/tutorial/jdbc/basics/webrowset.html

WebRowSet对象非常特殊,因为除了提供CachedRowSet对象的所有功能外,它还可以将自身写入为 XML 文档,并且还可以读取该 XML 文档以将自身转换回WebRowSet对象。由于 XML 是异构企业之间可以相互通信的语言,因此它已成为 Web 服务通信的标准。因此,WebRowSet对象通过使 Web 服务能够以 XML 文档的形式从数据库发送和接收数据来填补了一个真正的需求。

下面涵盖了以下主题:

  • 创建和填充 WebRowSet 对象

  • 将 WebRowSet 对象写入和读取为 XML

  • XML 文档中的内容是什么

  • 对 WebRowSet 对象进行更改

Coffee Break 公司已扩展到在线销售咖啡。用户可以从 Coffee Break 网站按磅订购咖啡。价格列表定期更新,通过从公司数据库获取最新信息。本节演示了如何通过WebRowSet对象和单个方法调用将价格数据发送为 XML 文档。

创建和填充 WebRowSet 对象

通过使用RowSetFactory的实例创建一个新的WebRowSet对象,该实例是从RowSetProvider类创建的,用于创建一个WebRowSet对象。以下示例来自WebRowSetSample.java

    RowSetFactory factory = RowSetProvider.newFactory();  
    try (WebRowSet priceList = factory.createWebRowSet();
         // ...
    ) {	  
      // ...

尽管priceList对象尚无数据,但它具有BaseRowSet对象的默认属性。其SyncProvider对象首先设置为RIOptimisticProvider实现,这是所有断开连接的RowSet对象的默认值。但是,WebRowSet实现会将SyncProvider对象重置为RIXMLProvider实现。

您可以使用RowSetFactory的实例创建一个WebRowSet对象,该实例是从RowSetProvider类创建的。有关更多信息,请参见使用 RowSetFactory 接口中的使用 JdbcRowSet 对象。

Coffee Break 总部定期向其网站发送价格列表更新。关于WebRowSet对象的这些信息将展示您可以通过 XML 文档发送最新价格列表的一种方式。

价格列表包括来自表COFFEES的列COF_NAMEPRICE中的数据。以下代码片段设置所需的属性,并使用价格列表数据填充priceList对象:

      int[] keyCols = {1};
      priceList.setUsername(settings.userName);
      priceList.setPassword(settings.password);
      priceList.setUrl(settings.urlString);
      priceList.setCommand("select COF_NAME, PRICE from COFFEES");
      priceList.setKeyColumns(keyCols);

      // Populate the WebRowSet
      priceList.execute();

此时,除了默认属性之外,priceList对象还包含来自COFFEES表中COF_NAMEPRICE列的数据,以及关于这两列的元数据。

将 WebRowSet 对象写入和读取为 XML

要将WebRowSet对象写入 XML 文档,请调用方法writeXml。要将该 XML 文档的内容读入WebRowSet对象,请调用方法readXml。这两种方法都在后台执行其工作,除了结果之外,其他都对您不可见。

使用writeXml方法

方法writeXml将调用它的WebRowSet对象作为表示其当前状态的 XML 文档写入。它将这个 XML 文档写入您传递给它的流。流可以是一个OutputStream对象,比如一个FileOutputStream对象,或者一个Writer对象,比如一个FileWriter对象。如果您向方法writeXml传递一个OutputStream对象,您将以字节形式写入,可以处理所有类型的数据;如果您向它传递一个Writer对象,您将以字符形式写入。以下代码演示将WebRowSet对象priceList作为 XML 文档写入FileOutputStream对象oStream

java.io.FileOutputStream oStream =
    new java.io.FileOutputStream("priceList.xml");
priceList.writeXml(oStream);

以下代码将代表priceList的 XML 文档写入FileWriter对象writer,而不是写入OutputStream对象。FileWriter类是一个方便的用于向文件写入字符的类。

java.io.FileWriter writer =
    new java.io.FileWriter("priceList.xml");
priceList.writeXml(writer);

方法writeXml的另外两个版本允许您在将其写入流之前,使用ResultSet对象的内容填充WebRowSet对象。在下面的代码行中,方法writeXmlResultSet对象rs的内容读入priceList对象,然后将priceList作为 XML 文档写入FileOutputStream对象oStream

priceList.writeXml(rs, oStream);

在下一行代码中,writeXml方法将priceList填充为rs的内容,但将 XML 文档写入FileWriter对象,而不是写入OutputStream对象:

priceList.writeXml(rs, writer);

使用readXml方法

方法readXml解析 XML 文档以构造 XML 文档描述的WebRowSet对象。与方法writeXml类似,您可以向readXml传递一个InputStream对象或Reader对象,从中读取 XML 文档。

java.io.FileInputStream iStream =
    new java.io.FileInputStream("priceList.xml");
priceList.readXml(iStream);

java.io.FileReader reader = new
    java.io.FileReader("priceList.xml");
priceList.readXml(reader);

请注意,您可以将 XML 描述读入一个新的WebRowSet对象中,或者读入调用writeXml方法的相同WebRowSet对象中。在从总部发送价格列表信息到网站的情况下,您将使用一个新的WebRowSet对象,如下面的代码所示:

WebRowSet recipient = new WebRowSetImpl();
java.io.FileReader reader =
    new java.io.FileReader("priceList.xml");
recipient.readXml(reader);

XML 文档中包含什么

RowSet对象不仅包含它们所包含的数据,还包含有关其列的属性和元数据。因此,表示WebRowSet对象的 XML 文档除了数据外还包括其他信息。此外,XML 文档中的数据包括当前值和原始值。 (回想一下,原始值是在对数据进行最近更改之前立即存在的值。这些值对于检查数据库中的相应值是否已更改是必要的,从而创建关于应该持久化哪个值的冲突:您放入RowSet对象的新值还是其他人放入数据库中的新值。)

WebRowSet XML 模式本身是一个 XML 文档,定义了表示WebRowSet对象的 XML 文档将包含什么以及必须以什么格式呈现。发送方和接收方都使用此模式,因为它告诉发送方如何编写 XML 文档(表示WebRowSet对象的文档)以及接收方如何解析 XML 文档。由于实际的写入和读取是由writeXmlreadXml方法的实现在内部完成的,因此作为用户,您不需要了解 WebRowSet XML 模式文档中的内容。

XML 文档以分层结构包含元素和子元素。以下是描述WebRowSet对象的 XML 文档中的三个主要元素:

  • 属性

  • 元数据

  • 数据

元素标签表示元素的开始和结束。例如,<properties>标签表示属性元素的开始,</properties>标签表示其结束。<map/>标签是一种简写方式,表示尚未为地图子元素(属性元素中的一个子元素)分配值。以下示例 XML 文档使用间距和缩进使其更易于阅读,但在实际的 XML 文档中不使用这些,其中间距不表示任何内容。

接下来的三个部分向您展示了在示例WebRowSetSample中创建的WebRowSet priceList对象的三个主要元素包含什么。

属性

priceList对象上调用writeXml方法将生成描述priceList的 XML 文档。此 XML 文档的属性部分将如下所示:

<properties>
  <command>
    select COF_NAME, PRICE from COFFEES
  </command>
  <concurrency>1008</concurrency>
  <datasource><null/></datasource>
  <escape-processing>true</escape-processing>
  <fetch-direction>1000</fetch-direction>
  <fetch-size>0</fetch-size>
  <isolation-level>2</isolation-level>
  <key-columns>
    <column>1</column>
  </key-columns>
  <map>
  </map>
  <max-field-size>0</max-field-size>
  <max-rows>0</max-rows>
  <query-timeout>0</query-timeout>
  <read-only>true</read-only>
  <rowset-type>
    ResultSet.TYPE_SCROLL_INSENSITIVE
  </rowset-type>
  <show-deleted>false</show-deleted>
  <table-name>COFFEES</table-name>
  <url>jdbc:mysql://localhost:3306/testdb</url>
  <sync-provider>
    <sync-provider-name>
      com.sun.rowset.providers.RIOptimisticProvider
    </sync-provider-name>
    <sync-provider-vendor>
      Sun Microsystems Inc.
    </sync-provider-vendor>
    <sync-provider-version>
      1.0
    </sync-provider-version>
    <sync-provider-grade>
      2
    </sync-provider-grade>
    <data-source-lock>1</data-source-lock>
  </sync-provider>
</properties>

请注意,某些属性没有值。例如,datasource属性用<datasource/>标签表示,这是一种简写方式,表示<datasource></datasource>。没有给出值,因为已设置url属性。建立的任何连接将使用此 JDBC URL 完成,因此不需要设置DataSource对象。此外,usernamepassword属性未列出,因为它们必须保持机密。

元数据

描述WebRowSet对象的 XML 文档的元数据部分包含有关该WebRowSet对象中列的信息。以下显示了描述priceList对象的WebRowSet对象的此部分的外观。因为priceList对象有两列,描述它的 XML 文档有两个<column-definition>元素。每个<column-definition>元素都有子元素提供有关所描述列的信息。

<metadata>
  <column-count>2</column-count>
  <column-definition>
    <column-index>1</column-index>
    <auto-increment>false</auto-increment>
    <case-sensitive>false</case-sensitive>
    <currency>false</currency>
    <nullable>0</nullable>
    <signed>false</signed>
    <searchable>true</searchable>
    <column-display-size>
      32
    </column-display-size>
    <column-label>COF_NAME</column-label>
    <column-name>COF_NAME</column-name>
    <schema-name></schema-name>
    <column-precision>32</column-precision>
    <column-scale>0</column-scale>
    <table-name>coffees</table-name>
    <catalog-name>testdb</catalog-name>
    <column-type>12</column-type>
    <column-type-name>
      VARCHAR
    </column-type-name>
  </column-definition>
  <column-definition>
    <column-index>2</column-index>
    <auto-increment>false</auto-increment>
    <case-sensitive>true</case-sensitive>
    <currency>false</currency>
    <nullable>0</nullable>
    <signed>true</signed>
    <searchable>true</searchable>
    <column-display-size>
      12
    </column-display-size>
    <column-label>PRICE</column-label>
    <column-name>PRICE</column-name>
    <schema-name></schema-name>
    <column-precision>10</column-precision>
    <column-scale>2</column-scale>
    <table-name>coffees</table-name>
    <catalog-name>testdb</catalog-name>
    <column-type>3</column-type>
    <column-type-name>
      DECIMAL
    </column-type-name>
  </column-definition>
</metadata>

从这个元数据部分,你可以看到每行中有两列。第一列是COF_NAME,它保存VARCHAR类型的值。第二列是PRICE,它保存REAL类型的值,等等。请注意,列类型是数据源中使用的数据类型,而不是 Java 编程语言中的类型。要获取或更新COF_NAME列中的值,你可以使用getStringupdateString方法,驱动程序会将其转换为VARCHAR类型,就像通常做的那样。

数据

数据部分提供了WebRowSet对象每行中每列的值。如果你已经填充了priceList对象并且没有对其进行任何更改,XML 文档的数据元素将如下所示。在下一节中,你将看到当你修改priceList对象中的数据时,XML 文档如何变化。

每一行都有一个<currentRow>元素,因为priceList有两列,所以每个<currentRow>元素包含两个<columnValue>元素。

<data>
  <currentRow>
    <columnValue>Colombian</columnValue>
    <columnValue>7.99</columnValue>
  </currentRow>
  <currentRow>
    <columnValue>
      Colombian_Decaf
    </columnValue>
    <columnValue>8.99</columnValue>
  </currentRow>
  <currentRow>
    <columnValue>Espresso</columnValue>
    <columnValue>9.99</columnValue>
  </currentRow>
  <currentRow>
    <columnValue>French_Roast</columnValue>
    <columnValue>8.99</columnValue>
  </currentRow>
  <currentRow>
    <columnValue>French_Roast_Decaf</columnValue>
    <columnValue>9.99</columnValue>
  </currentRow>
</data>

对 WebRowSet 对象进行更改

WebRowSet对象进行更改的方式与对CachedRowSet对象相同。然而,与CachedRowSet对象不同,WebRowSet对象会跟踪更新、插入和删除,以便writeXml方法可以写入当前值和原始值。接下来的三个部分演示了对数据进行更改,并展示了描述WebRowSet对象的 XML 文档在每次更改后的样子。关于 XML 文档,你无需做任何事情;任何对它的更改都是自动进行的,就像写入和读取 XML 文档一样。

插入行

如果 Coffee Break 连锁店的老板想要在价格表中添加一种新的咖啡,代码可能如下所示:

priceList.absolute(3);
priceList.moveToInsertRow();
priceList.updateString(COF_NAME, "Kona");
priceList.updateFloat(PRICE, 8.99f);
priceList.insertRow();
priceList.moveToCurrentRow();

在参考实现中,插入会立即在当前行之后进行。在前面的代码片段中,当前行是第三行,因此新行将在第三行之后添加,并成为新的第四行。为了反映这个插入,XML 文档将在<data>元素中第三个<currentRow>元素之后添加以下<insertRow>元素。

<insertRow>元素看起来类似于以下内容。

<insertRow>
  <columnValue>Kona</columnValue>
  <columnValue>8.99</columnValue>
</insertRow>

删除行

所有者认为浓缩咖啡销售不足,应从 The Coffee Break 商店出售的咖啡中移除。因此,所有者希望从价格表中删除浓缩咖啡。浓缩咖啡位于priceList对象的第三行,因此以下代码行将其删除:

priceList.absolute(3); priceList.deleteRow();

以下<deleteRow>元素将出现在 XML 文档数据部分的第二行之后,表示第三行已被删除。

<deleteRow>
  <columnValue>Espresso</columnValue>
  <columnValue>9.99</columnValue>
</deleteRow>

修改行

所有者进一步决定哥伦比亚咖啡的价格太贵,想将其降至每磅$6.99。以下代码设置了哥伦比亚咖啡的新价格,即第一行为每磅$6.99:

priceList.first();
priceList.updateFloat(PRICE, 6.99);

XML 文档将在<updateRow>元素中反映这一变化,给出新值。第一列的值未更改,因此仅有第二列的<updateValue>元素:

<currentRow>
  <columnValue>Colombian</columnValue>
  <columnValue>7.99</columnValue>
  <updateRow>6.99</updateRow>
</currentRow>

此时,通过插入一行、删除一行和修改一行,priceList对象的 XML 文档将如下所示:

<data>
  <insertRow>
    <columnValue>Kona</columnValue>
    <columnValue>8.99</columnValue>
  </insertRow>
  <currentRow>
    <columnValue>Colombian</columnValue>
    <columnValue>7.99</columnValue>
    <updateRow>6.99</updateRow>
  </currentRow>
  <currentRow>
    <columnValue>
      Colombian_Decaf
    </columnValue>
    <columnValue>8.99</columnValue>
  </currentRow>
  <deleteRow>
    <columnValue>Espresso</columnValue>
    <columnValue>9.99</columnValue>
  </deleteRow>
  <currentRow>
    <columnValue>French_Roast</columnValue>
    <columnValue>8.99</columnValue>
  </currentRow>
  <currentRow>
    <columnValue>
      French_Roast_Decaf
    </columnValue>
    <columnValue>9.99</columnValue>
  </currentRow>
</data>

WebRowSet 代码示例

示例WebRowSetSample.java展示了本页面描述的所有功能。

使用高级数据类型

原文:docs.oracle.com/javase/tutorial/jdbc/basics/sqltypes.html

本节介绍的高级数据类型使关系数据库在表列值方面更加灵活。例如,列可以用于存储 BLOB(二进制大对象)值,可以以原始字节形式存储非常大量的数据。列也可以是 CLOB(字符大对象)类型,能够以字符格式存储非常大量的数据。

ANSI/ISO SQL 标准的最新版本通常被称为 SQL:2003。该标准指定了以下数据类型:

  • SQL92 内置类型,包括熟悉的 SQL 列类型,如 CHARFLOATDATE

  • SQL99 内置类型,包括 SQL99 添加的类型:

    • BOOLEAN: 布尔(真或假)值

    • BLOB: 二进制大对象

    • CLOB: 字符大对象

  • SQL:2003 添加的新内置类型:

    • XML: XML 对象
  • 用户定义类型:

    • 结构化类型: 用户定义类型; 例如:

      CREATE TYPE PLANE_POINT
      AS (X FLOAT, Y FLOAT) NOT FINAL
      
      
    • DISTINCT 类型: 基于内置类型的用户定义类型; 例如:

      CREATE TYPE MONEY
      AS NUMERIC(10,2) FINAL
      
      
  • 构造类型: 基于给定基本类型的新类型:

    • REF(*structured-type*): 持久地指示驻留在数据库中的结构化类型实例的指针

    • *base-type* ARRAY[*n*]: n 个基本类型元素的数组

  • 定位器: 逻辑指针,指向驻留在数据库服务器上的数据。定位器 存在于客户端计算机上,是对服务器上数据的瞬时、逻辑指针。定位器通常指向无法在客户端上具体化的数据,如图像或音频。(具体化视图 是事先存储或“具体化”为模式对象的查询结果。) 在 SQL 级别定义了操作符,用于检索由定位器指示的数据的随机访问部分:

    • LOCATOR(*structured-type*): 服务器中结构化实例的定位器

    • LOCATOR(*array*): 服务器中数组的定位器

    • LOCATOR(*blob*): 服务器中二进制大对象的定位器

    • LOCATOR(*clob*): 服务器中字符大对象的定位器

  • Datalink: 用于管理数据源外部数据的类型。Datalink 值是 SQL MED(管理外部数据)的一部分,是 SQL ANSI/ISO 标准规范的一部分。

映射高级数据类型

JDBC API 为 SQL:2003 标准指定的高级数据类型提供了默认映射。以下列表列出了数据类型及其映射到的接口或类:

  • BLOB: Blob 接口

  • CLOB: Clob 接口

  • NCLOB: NClob 接口

  • ARRAY: Array 接口

  • XML: SQLXML 接口

  • 结构化类型: Struct 接口

  • REF(structured type): Ref 接口

  • ROWID: RowId 接口

  • DISTINCT: 基础类型映射的类型。例如,基于 SQL NUMERIC 类型的 DISTINCT 值映射到 java.math.BigDecimal 类型,因为在 Java 编程语言中,NUMERIC 映射到 BigDecimal

  • DATALINKjava.net.URL 对象

使用高级数据类型

检索、存储和更新高级数据类型的方式与处理其他数据类型的方式相同。您可以使用 ResultSet.get*DataType*CallableStatement.get*DataType* 方法来检索它们,PreparedStatement.set*DataType* 方法来存储它们,以及 ResultSet.update*DataType* 方法来更新它们。(变量 *DataType* 是映射到高级数据类型的 Java 接口或类的名称。)大概有 90% 的高级数据类型操作涉及使用 get*DataType*set*DataType*update*DataType* 方法。以下表格显示了要使用哪些方法:

高级数据类型 get*DataType* 方法 set*DataType* 方法 update*DataType* 方法
BLOB getBlob setBlob updateBlob
CLOB getClob setClob updateClob
NCLOB getNClob setNClob updateNClob
ARRAY getArray setArray updateArray
XML getSQLXML setSQLXML updateSQLXML
Structured type getObject setObject updateObject
REF(structured type) getRef setRef updateRef
ROWID getRowId setRowId updateRowId
DISTINCT getBigDecimal setBigDecimal updateBigDecimal
DATALINK getURL setURL updateURL

注意DISTINCT 数据类型与其他高级 SQL 数据类型的行为不同。作为一个基于已存在内置类型的用户定义类型,它在 Java 编程语言中没有接口。因此,您需要使用与 DISTINCT 数据类型基础的 Java 类型对应的方法。请参阅使用 DISTINCT 数据类型获取更多信息。

例如,以下代码片段检索了一个 SQL ARRAY 值。在此示例中,假设表 STUDENTS 中的列 SCORES 包含 ARRAY 类型的值。变量 *stmt* 是一个 Statement 对象。

ResultSet rs = stmt.executeQuery(
    "SELECT SCORES FROM STUDENTS " +
    "WHERE ID = 002238");
rs.next();
Array scores = rs.getArray("SCORES");

变量 *scores* 是指向存储在表 STUDENTS 中学生 002238 行中的 SQL ARRAY 对象的逻辑指针。

如果要将值存储在数据库中,可以使用相应的 set 方法。例如,以下代码片段中,*rs* 是一个 ResultSet 对象,存储了一个 Clob 对象:

Clob notes = rs.getClob("NOTES");
PreparedStatement pstmt =
    con.prepareStatement(
        "UPDATE MARKETS SET COMMENTS = ? " +
        "WHERE SALES < 1000000");
pstmt.setClob(1, notes);
pstmt.executeUpdate();

此代码将 *notes* 设置为发送到数据库的更新语句的第一个参数。由 *notes* 指定的 Clob 值将存储在表 MARKETSCOMMENTS 列中,在该列中的值小于一百万的每一行中。

使用大对象

原文:docs.oracle.com/javase/tutorial/jdbc/basics/blob.html

BlobClobNClob Java 对象的一个重要特性是,您可以在不将所有数据从数据库服务器传输到客户端计算机的情况下对它们进行操作。一些实现使用定位器(逻辑指针)来表示这些类型的实例,指向实例所代表的数据库中的对象。由于BLOBCLOBNCLOB SQL 对象可能非常大,使用定位器可以显著提高性能。但是,其他实现会在客户端计算机上完全实现大对象。

如果要将BLOBCLOBNCLOB SQL 值的数据传输到客户端计算机,请使用为此目的提供的BlobClobNClob Java 接口中的方法。这些大对象类型对象将它们所代表的对象的数据实现为流。

以下主题涵盖:

  • 向数据库添加大对象类型对象

  • 检索 CLOB 值

  • 添加和检索 BLOB 对象

  • 释放大对象占用的资源

向数据库添加大对象类型对象

以下摘录自ClobSample.addRowToCoffeeDescriptionsCOFFEE_DESCRIPTIONS表中添加CLOB SQL 值。Clob Java 对象myClob包含由fileName指定的文件的内容。

  public void addRowToCoffeeDescriptions(String coffeeName,
                                         String fileName) throws SQLException {
    String sql = "INSERT INTO COFFEE_DESCRIPTIONS VALUES(?,?)";
    Clob myClob = this.con.createClob();
    try (PreparedStatement pstmt = this.con.prepareStatement(sql);
      Writer clobWriter = myClob.setCharacterStream(1);){
      String str = this.readFile(fileName, clobWriter);
      System.out.println("Wrote the following: " + clobWriter.toString());
      if (this.settings.dbms.equals("mysql")) {
        System.out.println("MySQL, setting String in Clob object with setString method");
        myClob.setString(1, str);
      }
      System.out.println("Length of Clob: " + myClob.length());
      pstmt.setString(1, coffeeName);
      pstmt.setClob(2, myClob);
      pstmt.executeUpdate();
    } catch (SQLException sqlex) {
      JDBCTutorialUtilities.printSQLException(sqlex);
    } catch (Exception ex) {
      System.out.println("Unexpected exception: " + ex.toString());
    }
  }

下一行创建一个Clob Java 对象:

Clob myClob = this.con.createClob();

下一行检索一个流(在本例中为名为clobWriterWriter对象),用于将一系列字符写入Clob Java 对象myClobClobSample.readFile方法写入这些字符流;流来自由String fileName指定的文件。方法参数1表示Writer对象将从Clob值的开头开始写入字符流:

Writer clobWriter = myClob.setCharacterStream(1);

ClobSample.readFile方法逐行读取由文件fileName指定的文件,并将其写入由writerArg指定的Writer对象:

  private String readFile(String fileName, Writer writerArg) throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
      String nextLine = "";
      StringBuffer sb = new StringBuffer();
      while ((nextLine = br.readLine()) != null) {
        System.out.println("Writing: " + nextLine);
        writerArg.write(nextLine);
        sb.append(nextLine);
      }
      // Convert the content into to a string
      String clobData = sb.toString();
	  // Return the data.
      return clobData;
	}
  }

以下摘录创建一个PreparedStatement对象pstmt,将Clob Java 对象myClob插入COFFEE_DESCRIPTIONS中:

    String sql = "INSERT INTO COFFEE_DESCRIPTIONS VALUES(?,?)";
    Clob myClob = this.con.createClob();
    try (PreparedStatement pstmt = this.con.prepareStatement(sql);
      // ...
      ) {
      // ...
      pstmt.setString(1, coffeeName);
      pstmt.setClob(2, myClob);
      pstmt.executeUpdate();
      // ...

检索 CLOB 值

方法ClobSample.retrieveExcerptCOFFEE_DESCRIPTIONS表中COF_NAME列的值等于coffeeName参数指定的String值的行中检索存储在COF_DESC列中的CLOB SQL 值:

  public String retrieveExcerpt(String coffeeName,
                                int numChar) throws SQLException {

    String description = null;
    Clob myClob = null;
    String sql = "select COF_DESC from COFFEE_DESCRIPTIONS where COF_NAME = ?";

    try (PreparedStatement pstmt = this.con.prepareStatement(sql)) {
      pstmt.setString(1, coffeeName);
      ResultSet rs = pstmt.executeQuery();
      if (rs.next()) {
        myClob = rs.getClob(1);
        System.out.println("Length of retrieved Clob: " + myClob.length());
      }
      description = myClob.getSubString(1, numChar);
    } catch (SQLException sqlex) {
      JDBCTutorialUtilities.printSQLException(sqlex);
    } catch (Exception ex) {
      System.out.println("Unexpected exception: " + ex.toString());
    }
    return description;
  }

下一行从ResultSet对象rs中检索Clob Java 值:

myClob = rs.getClob(1);

以下行从myClob对象中检索子字符串。子字符串从myClob值的第一个字符开始,最多有numChar指定的连续字符数,其中numChar是一个整数。

description = myClob.getSubString(1, numChar);

添加和检索 BLOB 对象

添加和检索BLOB SQL 对象类似于添加和检索CLOB SQL 对象。使用Blob.setBinaryStream方法检索一个OutputStream对象,以写入调用该方法的Blob Java 对象(表示BLOB SQL 值)的BLOB SQL 值。

释放大对象所持有的资源

BlobClobNClob Java 对象在它们被创建的事务持续时间内至少保持有效。这可能导致应用程序在长时间运行的事务中耗尽资源。应用程序可以通过调用它们的free方法来释放BlobClobNClob资源。

在以下摘录中,调用方法Clob.free来释放先前创建的Clob对象所持有的资源:

Clob aClob = con.createClob();
int numWritten = aClob.setString(1, val);
aClob.free();

使用 SQLXML 对象

原文:docs.oracle.com/javase/tutorial/jdbc/basics/sqlxml.html

Connection 接口支持使用 createSQLXML 方法创建 SQLXML 对象。创建的对象不包含任何数据。可以通过在 SQLXML 接口上调用 setStringsetBinaryStreamsetCharacterStreamsetResult 方法向对象添加数据。

下面涵盖了以下主题:

  • 创建 SQLXML 对象

  • 在 ResultSet 中检索 SQLXML 值

  • 访问 SQLXML 对象数据

  • 存储 SQLXML 对象

  • 初始化 SQLXML 对象

  • 释放 SQLXML 资源

  • 示例代码

创建 SQLXML 对象

在以下摘录中,使用 Connection.createSQLXML 方法创建一个空的 SQLXML 对象。使用 SQLXML.setString 方法将数据写入创建的 SQLXML 对象。

Connection con = DriverManager.getConnection(url, props);
SQLXML xmlVal = con.createSQLXML();
xmlVal.setString(val);

在 ResultSet 中检索 SQLXML 值

SQLXML 数据类型类似于更基本的内置类型。可以通过在 ResultSetCallableStatement 接口中调用 getSQLXML 方法来检索 SQLXML 值。

例如,以下摘录从 ResultSet rs 的第一列检索一个 SQLXML 值:

SQLXML xmlVar = rs.getSQLXML(1);

SQLXML 对象在创建它们的事务持续时间内至少保持有效,除非调用它们的 free 方法。

访问 SQLXML 对象数据

SQLXML 接口提供了 getStringgetBinaryStreamgetCharacterStreamgetSource 方法来访问其内部内容。以下摘录使用 getString 方法检索 SQLXML 对象的内容:

SQLXML xmlVal= rs.getSQLXML(1);
String val = xmlVal.getString();

可以使用 getBinaryStreamgetCharacterStream 方法获取可直接传递给 XML 解析器的 InputStreamReader 对象。以下摘录从 SQLXML 对象获取一个 InputStream 对象,然后使用 DOM(文档对象模型)解析器处理流:

SQLXML sqlxml = rs.getSQLXML(column);
InputStream binaryStream = sqlxml.getBinaryStream();
DocumentBuilder parser = 
    DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document result = parser.parse(binaryStream);

getSource 方法返回一个 javax.xml.transform.Source 对象。源用作 XML 解析器和 XSLT 转换器的输入。

以下摘录使用通过调用 getSource 方法返回的 SAXSource 对象从 SQLXML 对象中检索和解析数据:

SQLXML xmlVal= rs.getSQLXML(1);
SAXSource saxSource = sqlxml.getSource(SAXSource.class);
XMLReader xmlReader = saxSource.getXMLReader();
xmlReader.setContentHandler(myHandler);
xmlReader.parse(saxSource.getInputSource());

存储 SQLXML 对象

SQLXML 对象可以像其他数据类型一样作为输入参数传递给 PreparedStatement 对象。setSQLXML 方法使用 SQLXML 对象设置指定的 PreparedStatement 参数。

在以下摘录中,authorData 是一个 java.sql.SQLXML 接口的实例,其数据先前已初始化。

PreparedStatement pstmt = conn.prepareStatement("INSERT INTO bio " +
                              "(xmlData, authId) VALUES (?, ?)");
pstmt.setSQLXML(1, authorData);
pstmt.setInt(2, authorId);

updateSQLXML 方法可用于更新可更新结果集中的列值。

如果在调用setSQLXMLupdateSQLXML之前未关闭SQLXML对象的java.xml.transform.ResultWriterOutputStream对象,将抛出SQLException

初始化 SQLXML 对象

SQLXML接口提供了setStringsetBinaryStreamsetCharacterStreamsetResult方法来初始化通过调用Connection.createSQLXML方法创建的SQLXML对象的内容。

以下摘录使用setResult方法返回一个SAXResult对象以填充一个新创建的SQLXML对象:

SQLXML sqlxml = con.createSQLXML();
SAXResult saxResult = sqlxml.setResult(SAXResult.class);
ContentHandler contentHandler = saxResult.getXMLReader().getContentHandler();
contentHandler.startDocument();

// set the XML elements and
// attributes into the result
contentHandler.endDocument();

以下摘录使用setCharacterStream方法获取一个java.io.Writer对象以初始化一个SQLXML对象:

SQLXML sqlxml = con.createSQLXML();
Writer out= sqlxml.setCharacterStream();
BufferedReader in = new BufferedReader(new FileReader("xml/foo.xml"));
String line = null;
while((line = in.readLine() != null) {
    out.write(line);
}

同样地,SQLXMLsetString方法可用于初始化一个SQLXML对象。

如果尝试在先前初始化过的SQLXML对象上调用setStringsetBinaryStreamsetCharacterStreamsetResult方法,将抛出SQLException。如果对同一SQLXML对象多次调用setBinaryStreamsetCharacterStreamsetResult方法,则会抛出SQLException,并且先前返回的javax.xml.transform.ResultWriterOutputStream对象不受影响。

释放 SQLXML 资源

SQLXML对象在创建它们的事务持续时间内至少保持有效。这可能导致应用程序在长时间运行的事务中耗尽资源。应用程序可以通过调用它们的free方法释放SQLXML资源。

在以下摘录中,调用method SQLXML.free来释放先前创建的SQLXML对象所持有的资源。

SQLXML xmlVar = con.createSQLXML();
xmlVar.setString(val);
xmlVar.free();

示例代码

MySQL 和 Java DB 及其各自的 JDBC 驱动程序并不完全支持本节中描述的SQLXML JDBC 数据类型。然而,示例RSSFeedsTable.java演示了如何处理 MySQL 和 Java DB 中的 XML 数据。

The Coffee Break 的所有者关注来自各种网站的几个 RSS 订阅源,涵盖餐厅和饮料行业新闻。RSS(真正简单的聚合或富站点摘要)订阅源是一个包含一系列文章和相关元数据的 XML 文档,如每篇文章的发布日期和作者。所有者希望将这些 RSS 订阅源存储到数据库表中,包括 The Coffee Break 博客的 RSS 订阅源。

文件rss-the-coffee-break-blog.xml是 The Coffee Break 博客的一个示例 RSS 订阅源。文件rss-coffee-industry-news.xml是(虚构的)Coffee Industry News 的一个示例 RSS 订阅源。

在 MySQL 中处理 XML 数据

示例RSSFeedsTable将 RSS 订阅源存储在名为RSS_FEEDS的表中,该表是使用以下命令创建的:

create table RSS_FEEDS
    (RSS_NAME varchar(32) NOT NULL,
    RSS_FEED_XML longtext NOT NULL,
    PRIMARY KEY (RSS_NAME));

MySQL 不支持 XML 数据类型。相反,此示例将 XML 数据存储在类型为 LONGTEXT 的列中,这是一种 CLOB SQL 数据类型。MySQL 有四种 CLOB 数据类型;LONGTEXT 数据类型在这四种类型中包含的字符数量最多。

方法 RSSFeedsTable.addRSSFeed 将一个 RSS 订阅添加到 RSS_FEEDS 表中。此方法的第一条语句将 RSS 订阅(在此示例中表示为 XML 文件)转换为类型为 org.w3c.dom.Document 的对象,该对象表示 DOM(文档对象模型)文档。这个类以及包含在 javax.xml 包中的类和接口包含了使您能够操作 XML 数据内容的方法。例如,以下语句使用 XPath 表达式从 Document 对象中检索 RSS 订阅的标题:

Node titleElement =
    (Node)xPath.evaluate("/rss/channel/title[1]",
        doc, XPathConstants.NODE);

XPath 表达式 /rss/channel/title[1] 检索第一个 <title> 元素的内容。对于文件 rss-the-coffee-break-blog.xml,这是字符串 The Coffee Break Blog

以下语句将 RSS 订阅添加到表 RSS_FEEDS 中:

// For databases that support the SQLXML
// data type, this creates a
// SQLXML object from
// org.w3c.dom.Document.

System.out.println("Adding XML file " + fileName);
String insertRowQuery =
    "insert into RSS_FEEDS " +
    "(RSS_NAME, RSS_FEED_XML) values " +
    "(?, ?)";
insertRow = con.prepareStatement(insertRowQuery);
insertRow.setString(1, titleString);

System.out.println("Creating SQLXML object with MySQL");
rssData = con.createSQLXML();
System.out.println("Creating DOMResult object");
DOMResult dom = (DOMResult)rssData.setResult(DOMResult.class);
dom.setNode(doc);

insertRow.setSQLXML(2, rssData);
System.out.println("Running executeUpdate()");
insertRow.executeUpdate();

方法 RSSFeedsTable.viewTable 检索 RSS_FEEDS 的内容。对于每一行,该方法创建一个名为 doc 的类型为 org.w3c.dom.Document 的对象,用于存储列 RSS_FEED_XML 中的 XML 内容。该方法检索 XML 内容并将其存储在名为 rssFeedXML 的类型为 SQLXML 的对象中。rssFeedXML 的内容被解析并存储在 doc 对象中。

在 Java DB 中处理 XML 数据

注意:有关在 Java DB 中处理 XML 数据的更多信息,请参阅 Java DB 开发人员指南 中的 "XML 数据类型和运算符" 部分。

示例 RSSFeedsTable 将 RSS 订阅存储在表 RSS_FEEDS 中,该表是使用以下命令创建的:

create table RSS_FEEDS
    (RSS_NAME varchar(32) NOT NULL,
    RSS_FEED_XML xml NOT NULL,
    PRIMARY KEY (RSS_NAME));

Java DB 支持 XML 数据类型,但不支持 SQLXML JDBC 数据类型。因此,您必须将任何 XML 数据转换为字符格式,然后使用 Java DB 运算符 XMLPARSE 将其转换为 XML 数据类型。

方法 RSSFeedsTable.addRSSFeed 将一个 RSS 订阅添加到 RSS_FEEDS 表中。此方法的第一条语句将 RSS 订阅(在此示例中表示为 XML 文件)转换为类型为 org.w3c.dom.Document 的对象。这在 在 MySQL 中处理 XML 数据 部分中有描述。

RSSFeedsTable.addRSSFeed 方法使用方法 JDBCTutorialUtilities.convertDocumentToString 将 RSS 订阅转换为 String 对象。

Java DB 有一个名为XMLPARSE的操作符,将字符字符串表示解析为 Java DB XML 值,以下摘录演示了这一点:

String insertRowQuery =
    "insert into RSS_FEEDS " +
    "(RSS_NAME, RSS_FEED_XML) values " +
    "(?, xmlparse(document cast " +
    "(? as clob) preserve whitespace))";

XMLPARSE操作符要求您将 XML 文档的字符表示转换为 Java DB 识别的字符串数据类型。在本例中,它将其转换为CLOB数据类型。有关 Apache Xalan 和 Java DB 要求的更多信息,请参阅入门指南和 Java DB 文档。

方法RSSFeedsTable.viewTable检索RSS_FEEDS的内容。因为 Java DB 不支持 JDBC 数据类型SQLXML,您必须将 XML 内容检索为字符串。Java DB 有一个名为XMLSERIALIZE的操作符,将 XML 类型转换为字符类型:

String query =
    "select RSS_NAME, " +
    "xmlserialize " +
    "(RSS_FEED_XML as clob) " +
    "from RSS_FEEDS";

XMLPARSE操作符一样,XMLSERIALIZE操作符要求在您的 Java 类路径中列出 Apache Xalan。

使用数组对象

原文:docs.oracle.com/javase/tutorial/jdbc/basics/array.html

注意:MySQL 和 Java DB 目前不支持ARRAY SQL 数据类型。因此,没有可用的 JDBC 教程示例来演示Array JDBC 数据类型。

下面的主题包括:

  • 创建数组对象

  • 检索和访问 ResultSet 中的数组值

  • 存储和更新数组对象

  • 释放数组资源

创建数组对象

使用方法Connection.createArrayOf创建Array对象。

例如,假设您的数据库包含一个名为REGIONS的表,该表已经通过以下 SQL 语句创建并填充;请注意,这些语句的语法将根据您的数据库而变化:

create table REGIONS
    (REGION_NAME varchar(32) NOT NULL,
    ZIPS varchar32 ARRAY[10] NOT NULL,
    PRIMARY KEY (REGION_NAME));

insert into REGIONS values(
    'Northwest',
    '{"93101", "97201", "99210"}');
insert into REGIONS values(
    'Southwest',
    '{"94105", "90049", "92027"}');

Connection con = DriverManager.getConnection(url, props);
String [] northEastRegion = { "10022", "02110", "07399" };
Array anArray = con.createArrayOf("VARCHAR", northEastRegion);

Oracle 数据库 JDBC 驱动程序使用oracle.sql.ARRAY类实现java.sql.Array接口。

在 ResultSet 中检索和访问数组值

与 JDBC 4.0 大对象接口(BlobClobNClob)一样,您可以操作Array对象,而无需将所有数据从数据库服务器传输到客户端计算机。Array对象将其表示的 SQL ARRAY作为结果集或 Java 数组实现。

以下摘录检索列ZIPS中的 SQL ARRAY值,并将其赋给java.sql.Array对象z对象。摘录检索z的内容并将其存储在zips中,zips是一个包含String类型对象的 Java 数组。摘录遍历zips数组并检查每个邮政(邮编)代码是否有效。此代码假定类ZipCode已经在先前定义,并且具有方法isValid,如果给定的邮政编码与有效邮政编码主列表中的一个匹配,则返回true

ResultSet rs = stmt.executeQuery(
    "SELECT region_name, zips FROM REGIONS");

while (rs.next()) {
    Array z = rs.getArray("ZIPS");
    String[] zips = (String[])z.getArray();
    for (int i = 0; i < zips.length; i++) {
        if (!ZipCode.isValid(zips[i])) {
            // ...
            // Code to display warning
        }
    }
}

在以下语句中,ResultSet方法getArray将当前行的列ZIPS中存储的值作为java.sql.Array对象z返回:

Array z = rs.getArray("ZIPS");

变量*z*包含一个定位器,这是指向服务器上 SQL ARRAY的逻辑指针;它不包含ARRAY本身的元素。作为逻辑指针,*z*可用于在服务器上操作数组。

在以下行中,getArrayArray.getArray方法,而不是前一行中使用的ResultSet.getArray方法。因为Array.getArray方法在 Java 编程语言中返回一个Object,并且每个邮政编码都是一个String对象,所以在分配给变量zips之前,结果被转换为String对象的数组。

String[] zips = (String[])z.getArray();

Array.getArray方法将 SQL ARRAY元素在客户端作为String对象数组实现。因为实际上变量*zips*包含数组的元素,所以可以在for循环中遍历zips,查找无效的邮政编码。

存储和更新数组对象

使用方法PreparedStatement.setArrayPreparedStatement.setObjectArray值作为输入参数传递给PreparedStatement对象。

以下示例将在先前创建的Array对象anArray设置为pstmt的第二个参数:

PreparedStatement pstmt = con.prepareStatement(
    "insert into REGIONS (region_name, zips) " + "VALUES (?, ?)");
pstmt.setString(1, "NorthEast");
pstmt.setArray(2, anArray);
pstmt.executeUpdate();

同样,使用方法PreparedStatement.updateArrayPreparedStatement.updateObject来使用Array值更新表中的列。

释放数组资源

Array对象在创建它们的事务持续时间内保持有效。这可能导致应用程序在长时间运行的事务中耗尽资源。应用程序可以通过调用它们的free方法来释放Array资源。

在以下摘录中,调用方法Array.free来释放先前创建的Array对象所持有的资源。

Array aArray = con.createArrayOf("VARCHAR", northEastRegionnewYork);
// ...
aArray.free();

使用 DISTINCT 数据类型

原文:docs.oracle.com/javase/tutorial/jdbc/basics/distinct.html

注意:MySQL 和 Java DB 目前不支持DISTINCT SQL 数据类型。因此,在本节描述的功能没有 JDBC 教程示例可用来演示。

DISTINCT 数据类型与其他高级 SQL 数据类型的行为不同。作为一个基于已有内置类型之一的用户定义类型,它没有接口作为其在 Java 编程语言中的映射。相反,DISTINCT 数据类型的标准映射是其底层 SQL 数据类型映射到的 Java 类型。

为了说明,创建一个DISTINCT数据类型,然后看看如何检索、设置或更新它。假设你总是使用两个字母的缩写表示一个州,并且想要创建一个用于这些缩写的DISTINCT数据类型。你可以使用以下 SQL 语句定义你的新DISTINCT数据类型:

CREATE TYPE STATE AS CHAR(2);

一些数据库使用另一种语法来创建DISTINCT数据类型,如下面的代码行所示:

CREATE DISTINCT TYPE STATE AS CHAR(2);

如果一个语法不起作用,你可以尝试另一个。或者,你可以查看你的驱动程序文档,以查看它期望的确切语法。

这些语句创建了一个新的数据类型STATE,它可以作为列值或作为 SQL 结构化类型的属性值使用。因为STATE类型的值实际上是两个CHAR类型的值,所以你使用与检索CHAR值相同的方法来检索它,即getString。例如,假设ResultSet *rs*的第四列存储了STATE类型的值,下面的代码行检索了它的值:

String state = rs.getString(4);

同样,你会使用setString方法将STATE值存储在数据库中,使用updateString方法修改其值。

使用结构化对象

原文:docs.oracle.com/javase/tutorial/jdbc/basics/sqlstructured.html

注意:MySQL 和 Java DB 目前不支持用户定义类型。因此,没有 JDBC 教程示例可用来演示本节描述的功能。

下列主题包括:

  • 结构化类型概述

  • 在结构化类型中使用 DISTINCT 类型

  • 使用结构化类型的引用

  • 创建 SQL REF 对象的示例代码

  • 将用户定义类型用作列值

  • 将用户定义类型插入表中

结构化类型概述

SQL 结构化类型和DISTINCT类型是用户可以在 SQL 中定义的两种数据类型。它们通常被称为 UDT(用户定义类型),您可以使用 SQL 的CREATE TYPE语句来创建它们。

回到 The Coffee Break 的例子,假设所有者的成功超出了所有预期,并且一直在扩张新分店。所有者决定向数据库添加一个STORES表,其中包含有关每个机构的信息。STORES将有四列:

  • STORE_NO代表每个店铺的识别号

  • LOCATION代表其地址

  • COF_TYPES代表其销售的咖啡

  • MGR代表店长的姓名

所有者将LOCATION列设为 SQL 结构化类型,COF_TYPES列设为 SQL ARRAYMGR列设为REF(MANAGER),其中MANAGER是 SQL 结构化类型。

所有者首先必须为地址和经理定义新的结构化类型。SQL 结构化类型类似于 Java 编程语言中的结构化类型,它具有称为属性的成员,可以是任何数据类型。所有者编写以下 SQL 语句来创建新数据类型ADDRESS

CREATE TYPE ADDRESS
(
    NUM INTEGER,
    STREET VARCHAR(40),
    CITY VARCHAR(40),
    STATE CHAR(2),
    ZIP CHAR(5)
);

在此语句中,新类型ADDRESS有五个属性,类似于 Java 类中的字段。属性NUM是一个INTEGER,属性STREET是一个VARCHAR(40),属性CITY是一个VARCHAR(40),属性STATE是一个CHAR(2),属性ZIP是一个CHAR(5)

下面的摘录中,con是一个有效的Connection对象,将ADDRESS的定义发送到数据库:

String createAddress =
    "CREATE TYPE ADDRESS " +
    "(NUM INTEGER, STREET VARCHAR(40), " +
    "CITY VARCHAR(40), STATE CHAR(2), ZIP CHAR(5))";
Statement stmt = con.createStatement();
stmt.executeUpdate(createAddress);

现在ADDRESS结构化类型已在数据库中注册为数据类型,所有者可以将其用作表列或结构化类型属性的数据类型。

在结构化类型中使用 DISTINCT 类型

The Coffee Break 的所有者计划在新的结构化类型MANAGER中包含的属性之一是经理的电话号码。因为所有者总是将电话号码列为一个 10 位数(以确保包括区号),并且永远不会将其作为数字进行操作,所以所有者决定定义一个名为PHONE_NO的新类型,该类型由 10 个字符组成。这种数据类型的 SQL 定义,可以被视为只有一个属性的结构化类型,如下所示:

CREATE TYPE PHONE_NO AS CHAR(10);

或者,如前面提到的,对于某些驱动程序,定义可能如下所示:

CREATE DISTINCT TYPE PHONE_NO AS CHAR(10);

DISTINCT类型始终基于另一个数据类型,该数据类型必须是预定义类型。换句话说,DISTINCT类型不能基于用户定义类型(UDT)。要检索或设置DISTINCT类型的值,请使用基础类型(其基础类型)的适当方法。例如,要检索基于CHAR类型的PHONE_NO实例,您将使用getString方法,因为这是检索CHAR的方法。

假设ResultSet对象*rs*的当前行的第四列中有一个类型为PHONE_NO的值,以下代码行检索它:

String phoneNumber = rs.getString(4);

类似地,以下代码行设置了一个输入参数,该参数具有类型PHONE_NO,用于发送到数据库的预备语句:

pstmt.setString(1, phoneNumber);

在前面的代码片段中添加,PHONE_NO的定义将通过以下代码行发送到数据库:

stmt.executeUpdate(
    "CREATE TYPE PHONE_NO AS CHAR(10)");

在向数据库注册类型PHONE_NO之后,所有者可以将其用作表中的列类型或作为结构化类型中属性的数据类型。以下 SQL 语句中MANAGER的定义使用PHONE_NO作为属性PHONE的数据类型:

CREATE TYPE MANAGER
(
    MGR_ID INTEGER,
    LAST_NAME VARCHAR(40),
    FIRST_NAME VARCHAR(40),
    PHONE PHONE_NO
);

重用之前定义的*stmt*,以下代码片段将结构化类型MANAGER的定义发送到数据库:

  String createManager =
    "CREATE TYPE MANAGER " +
    "(MGR_ID INTEGER, LAST_NAME " +
    "VARCHAR(40), " +
    "FIRST_NAME VARCHAR(40), " +
    "PHONE PHONE_NO)";
  stmt.executeUpdate(createManager);

使用结构化类型的引用

The Coffee Break 的所有者创建了三种新的数据类型,用作数据库中的列类型或属性类型:结构化类型LOCATIONMANAGER,以及DISTINCT类型PHONE_NO。企业家将PHONE_NO用作新类型MANAGER中属性PHONE的类型,并将ADDRESS用作表STORES中列LOCATION的数据类型。MANAGER类型可以用作列MGR的类型,但企业家更喜欢使用类型REF(MANAGER),因为企业家经常让一个人管理两到三家店铺。将REF(MANAGER)用作列类型可以避免在一个人管理多家店铺时重复所有MANAGER的数据。

已经创建了结构化类型 MANAGER,所有者现在可以创建一个包含可以被引用的 MANAGER 实例的表。对 MANAGER 实例的引用将具有类型 REF(MANAGER)。SQL REF 只不过是指向结构化类型的逻辑指针,因此 REF(MANAGER) 实例充当对 MANAGER 实例的逻辑指针。

因为 SQL REF 值需要永久与其引用的结构化类型实例关联在一起,所以它存储在一个特殊的表中,与其关联的实例一起。程序员不直接创建 REF 类型,而是创建将存储特定结构化类型实例的表,这些实例可以被引用。每个要被引用的结构化类型都将有自己的表。当你将结构化类型的实例插入表中时,数据库会自动创建一个 REF 实例。例如,为了包含可以被引用的 MANAGER 实例,所有者使用 SQL 创建了以下特殊表:

  CREATE TABLE MANAGERS OF MANAGER
  (OID REF(MANAGER)
  VALUES ARE SYSTEM GENERATED);

这个语句创建了一个带有特殊列 OID 的表,该列存储 REF(MANAGER) 类型的值。每次将 MANAGER 实例插入表中时,数据库都会生成一个 REF(MANAGER) 实例并将其存储在列 OID 中。隐式地,另外一列存储已插入表中的 MANAGER 的每个属性。例如,以下代码片段展示了企业家如何创建了三个 MANAGER 结构化类型的实例来代表三个经理:

  INSERT INTO MANAGERS (
    MGR_ID, LAST_NAME,
    FIRST_NAME, PHONE) VALUES
  (
    000001,
    'MONTOYA',
    'ALFREDO',
    '8317225600'
  );

  INSERT INTO MANAGERS (
    MGR_ID, LAST_NAME,
    FIRST_NAME, PHONE) VALUES
  (
    000002,
    'HASKINS',
    'MARGARET',
    '4084355600'
  );

  INSERT INTO MANAGERS (
    MGR_ID, LAST_NAME,
    FIRST_NAME, PHONE) VALUES
  (
    000003,
    'CHEN',
    'HELEN',
    '4153785600'
   );

MANAGERS 现在将有三行,每个已插入的经理一行。列 OID 将包含三个 REF(MANAGER) 类型的唯一对象标识符,每个 MANAGER 实例一个。这些对象标识符是由数据库自动生成的,并将永久存储在表 MANAGERS 中。隐式地,另外一列存储 MANAGER 的每个属性。例如,在表 MANAGERS 中,一行包含一个引用 Alfredo Montoya 的 REF(MANAGER),另一行包含一个引用 Margaret Haskins 的 REF(MANAGER),第三行包含一个引用 Helen Chen 的 REF(MANAGER)

要访问 REF(MANAGER) 实例,您可以从其表中选择。例如,所有者使用以下代码片段检索了对 ID 号为 000001 的 Alfredo Montoya 的引用:

  String selectMgr =
    "SELECT OID FROM MANAGERS " +
    "WHERE MGR_ID = 000001";
  ResultSet rs = stmt.executeQuery(selectMgr);
  rs.next();
  Ref manager = rs.getRef("OID");

现在变量 *manager* 可以被用作引用 Alfredo Montoya 的列值。

创建 SQL REF 对象的示例代码

以下代码示例创建了表MANAGERS,这是结构化类型MANAGER的实例表,可以引用,并将三个MANAGER实例插入表中。此表中的列OID将存储REF(MANAGER)的实例。执行此代码后,MANAGERS表将为插入的三个MANAGER对象的每个对象插入一行,并且OID列中的值将是标识存储在该行中的MANAGER实例的REF(MANAGER)类型。

package com.oracle.tutorial.jdbc;

import java.sql.*;

public class CreateRef {

    public static void main(String args[]) {

        JDBCTutorialUtilities myJDBCTutorialUtilities;
        Connection myConnection = null;

        if (args[0] == null) {
            System.err.println("Properties file not specified " +
                               "at command line");
            return;
        } else {
            try {
                myJDBCTutorialUtilities = new JDBCTutorialUtilities(args[0]);
            } catch (Exception e) {
                System.err.println("Problem reading properties " +
                                   "file " + args[0]);
                e.printStackTrace();
                return;
            }
        }

        Connection con = null;
        Statement stmt = null;

        try {
            String createManagers =
                "CREATE TABLE " +
                "MANAGERS OF MANAGER " +
                "(OID REF(MANAGER) " +
                "VALUES ARE SYSTEM " +
                "GENERATED)";

            String insertManager1 =
                "INSERT INTO MANAGERS " +
                "(MGR_ID, LAST_NAME, " +
                "FIRST_NAME, PHONE) " +
                "VALUES " +
                "(000001, 'MONTOYA', " +
                "'ALFREDO', " +
                "'8317225600')";

            String insertManager2 =
                "INSERT INTO MANAGERS " +
                "(MGR_ID, LAST_NAME, " +
                "FIRST_NAME, PHONE) " +
                "VALUES " +
                "(000002, 'HASKINS', " +
                "'MARGARET', " +
                "'4084355600')";

            String insertManager3 =
                "INSERT INTO MANAGERS " +
                "(MGR_ID, LAST_NAME, " +
                "FIRST_NAME, PHONE) " +
                "VALUES " +
                "(000003, 'CHEN', 'HELEN', " +
                "'4153785600')";

            con = myJDBCTutorialUtilities.getConnection();
            con.setAutoCommit(false);

            stmt = con.createStatement();
            stmt.executeUpdate(createManagers);

            stmt.addBatch(insertManager1);
            stmt.addBatch(insertManager2);
            stmt.addBatch(insertManager3);
            int [] updateCounts = stmt.executeBatch();

            con.commit();

            System.out.println("Update count for:  ");
            for (int i = 0; i < updateCounts.length; i++) {
                System.out.print("    command " + (i + 1) + " = ");
                System.out.println(updateCounts[i]);
            }
        } catch(BatchUpdateException b) {
            System.err.println("-----BatchUpdateException-----");
            System.err.println("Message:  " + b.getMessage());
            System.err.println("SQLState:  " + b.getSQLState());
            System.err.println("Vendor:  " + b.getErrorCode());
            System.err.print("Update counts for " + "successful commands:  ");
            int [] rowsUpdated = b.getUpdateCounts();
            for (int i = 0; i < rowsUpdated.length; i++) {
                System.err.print(rowsUpdated[i] + "   ");
            }
            System.err.println("");
        } catch(SQLException ex) {
            System.err.println("------SQLException------");
            System.err.println("Error message:  " + ex.getMessage());
            System.err.println("SQLState:  " + ex.getSQLState());
            System.err.println("Vendor:  " + ex.getErrorCode());
        } finally {
            if (stmt != null) { stmt.close(); }
              JDBCTutorialUtilities.closeConnection(con);
        }
    }
}

使用用户定义类型作为列值

我们的企业家现在拥有创建表STORES所需的 UDT。结构化类型ADDRESS是列LOCATION的类型,类型REF(MANAGER)是列MGR的类型。

UDT COF_TYPES基于 SQL 数据类型ARRAY,是列COF_TYPES的类型。以下代码行创建了类型COF_ARRAY作为具有 10 个元素的ARRAY值。COF_ARRAY的基本类型是VARCHAR(40)

  CREATE TYPE COF_ARRAY AS ARRAY(10) OF VARCHAR(40);

定义了新数据类型后,以下 SQL 语句创建了表STORES

  CREATE TABLE STORES
  (
    STORE_NO INTEGER,
    LOCATION ADDRESS,
    COF_TYPES COF_ARRAY,
    MGR REF(MANAGER)
  );

将用户定义类型插入表中

以下代码片段向STORES表中插入一行,按顺序提供了列STORE_NOLOCATIONCOF_TYPESMGR的值:

  INSERT INTO STORES VALUES
  (
    100001,
    ADDRESS(888, 'Main_Street',
      'Rancho_Alegre',
      'CA', '94049'),
    COF_ARRAY('Colombian', 'French_Roast',
      'Espresso', 'Colombian_Decaf',
      'French_Roast_Decaf'),
    SELECT OID FROM MANAGERS
      WHERE MGR_ID = 000001
  );

以下逐个列出每列及其插入的值。

  STORE_NO: 100001

此列类型为INTEGER,数字100001INTEGER类型,类似于之前在COFFEESSUPPLIERS表中插入的条目。

  LOCATION: ADDRESS(888, 'Main_Street',
    'Rancho_Alegre', 'CA', '94049')

此列的类型为结构化类型ADDRESS,此值是ADDRESS实例的构造函数。当我们将ADDRESS的定义发送到数据库时,其中一件事是为新类型创建构造函数。括号中的逗号分隔值是ADDRESS类型属性的初始化值,它们必须按照ADDRESS类型定义中属性列出的顺序出现。888是属性NUM的值,是INTEGER值。"Main_Street"STREET的值,"Rancho_Alegre"CITY的值,这两个属性都是VARCHAR(40)类型。属性STATE的值为"CA",是CHAR(2)类型,属性ZIP的值为"94049",是CHAR(5)类型。

  COF_TYPES: COF_ARRAY(
    'Colombian',
    'French_Roast',
    'Espresso',
    'Colombian_Decaf',
    'French_Roast_Decaf'),

COF_TYPES的类型为COF_ARRAY,基本类型为VARCHAR(40),括号中的逗号分隔值是数组元素为String对象。所有者定义了类型COF_ARRAY最多有 10 个元素。此数组有 5 个元素,因为企业家仅提供了 5 个String对象。

  MGR: SELECT OID FROM MANAGERS
    WHERE MGR_ID = 000001

MGR的类型是REF(MANAGER),这意味着该列中的值必须是指向结构化类型MANAGER的引用。所有MANAGER的实例都存储在表MANAGERS中。所有REF(MANAGER)的实例也存储在该表中,存储在列OID中。此表行中描述的商店的经理是 Alfredo Montoya,他的信息存储在具有属性MGR_ID100001MANAGER实例中。要获取与 Alfredo Montoya 的MANAGER对象关联的REF(MANAGER)实例,请选择表MANAGERSMGR_ID100001的行中的列OID。将存储在STORES表的MGR列中的值(REF(MANAGER)值)是 DBMS 生成的用于唯一标识此MANAGER结构化类型实例的值。

将上述 SQL 语句发送到数据库,使用以下代码片段:

  String insertMgr =
    "INSERT INTO STORES VALUES " +
    "(100001, " +
    "ADDRESS(888, 'Main_Street', " +
      "'Rancho_Alegre', 'CA', " +
      "'94049'), " +
    "COF_ARRAY('Colombian', " +
      "'French_Roast', 'Espresso', " +
      "'Colombian_Decaf', " +
      "'French_Roast_Decaf'}, " +
    "SELECT OID FROM MANAGERS " +
    "WHERE MGR_ID = 000001)";

  stmt.executeUpdate(insertMgr);

然而,因为你将发送多个INSERT INTO语句,将它们一起作为批量更新发送会更有效,就像以下代码示例中所示:

package com.oracle.tutorial.jdbc;

import java.sql.*;

public class InsertStores {
    public static void main(String args[]) {

        JDBCTutorialUtilities myJDBCTutorialUtilities;
        Connection myConnection = null;

        if (args[0] == null) {
            System.err.println(
                "Properties file " +
                "not specified " +
                "at command line");
            return;
        } else {
            try {
                myJDBCTutorialUtilities = new
                    JDBCTutorialUtilities(args[0]);
            } catch (Exception e) {
                System.err.println(
                    "Problem reading " +
                    "properties file " +
                    args[0]);
                e.printStackTrace();
                return;
            }
        }

        Connection con = null;
        Statement stmt = null;

        try {
            con = myJDBCTutorialUtilities.getConnection();
            con.setAutoCommit(false);

            stmt = con.createStatement();

            String insertStore1 =
                "INSERT INTO STORES VALUES (" +
                "100001, " +
                "ADDRESS(888, 'Main_Street', " +
                    "'Rancho_Alegre', 'CA', " +
                    "'94049'), " +
                "COF_ARRAY('Colombian', " +
                    "'French_Roast', " +
                    "'Espresso', " +
                    "'Colombian_Decaf', " +
                    "'French_Roast_Decaf'), " +
                "(SELECT OID FROM MANAGERS " +
                "WHERE MGR_ID = 000001))";

            stmt.addBatch(insertStore1);

            String insertStore2 =
                "INSERT INTO STORES VALUES (" +
                "100002, " +
                "ADDRESS(1560, 'Alder', " +
                    "'Ochos_Pinos', " +
                    "'CA', '94049'), " +
                "COF_ARRAY('Colombian', " +
                    "'French_Roast', " +
                    "'Espresso', " +
                    "'Colombian_Decaf', " +
                    "'French_Roast_Decaf', " +
                    "'Kona', 'Kona_Decaf'), " +
                "(SELECT OID FROM MANAGERS " +
                "WHERE MGR_ID = 000001))";

            stmt.addBatch(insertStore2);

            String insertStore3 =
                "INSERT INTO STORES VALUES (" +
                "100003, " +
                "ADDRESS(4344, " +
                    "'First_Street', " +
                    "'Verona', " +
                    "'CA', '94545'), " +
                "COF_ARRAY('Colombian', " +
                    "'French_Roast', " +
                    "'Espresso', " +
                    "'Colombian_Decaf', " +
                    "'French_Roast_Decaf', " +
                    "'Kona', 'Kona_Decaf'), " +
                "(SELECT OID FROM MANAGERS " +
                "WHERE MGR_ID = 000002))";

            stmt.addBatch(insertStore3);

            String insertStore4 =
                "INSERT INTO STORES VALUES (" +
                "100004, " +
                "ADDRESS(321, 'Sandy_Way', " +
                    "'La_Playa', " +
                    "'CA', '94544'), " +
                "COF_ARRAY('Colombian', " +
                    "'French_Roast', " +
                    "'Espresso', " +
                    "'Colombian_Decaf', " +
                    "'French_Roast_Decaf', " +
                    "'Kona', 'Kona_Decaf'), " +
                "(SELECT OID FROM MANAGERS " +
                "WHERE MGR_ID = 000002))";

            stmt.addBatch(insertStore4);

            String insertStore5 =
                "INSERT INTO STORES VALUES (" +
                "100005, " +
                "ADDRESS(1000, 'Clover_Road', " +
                    "'Happyville', " +
                    "'CA', '90566'), " +
                "COF_ARRAY('Colombian', " +
                    "'French_Roast', " +
                    "'Espresso', " + 
                    "'Colombian_Decaf', " +
                    "'French_Roast_Decaf'), " +
                "(SELECT OID FROM MANAGERS " +
                "WHERE MGR_ID = 000003))";

            stmt.addBatch(insertStore5);

            int [] updateCounts = stmt.executeBatch();

            ResultSet rs = stmt.executeQuery(
                "SELECT * FROM STORES");
            System.out.println("Table STORES after insertion:");
            System.out.println("STORE_NO   " + "LOCATION   " +
                "COF_TYPE   " + "MGR");

            while (rs.next()) {
                int storeNo = rs.getInt("STORE_NO");
                Struct location = (Struct)rs.getObject("LOCATION");
                Object[] locAttrs = location.getAttributes();
                Array coffeeTypes = rs.getArray("COF_TYPE");
                String[] cofTypes = (String[])coffeeTypes.getArray();

                Ref managerRef = rs.getRef("MGR");
                PreparedStatement pstmt = con.prepareStatement(
                    "SELECT MANAGER " +
                    "FROM MANAGERS " +
                    "WHERE OID = ?");

                pstmt.setRef(1, managerRef);
                ResultSet rs2 = pstmt.executeQuery();
                rs2.next();
                Struct manager = (Struct)rs2.getObject("MANAGER");
                Object[] manAttrs = manager.getAttributes();

                System.out.print(storeNo + "   ");
                System.out.print(
                    locAttrs[0] + " " +
                    locAttrs[1] + " " +
                    locAttrs[2] + ", " +
                    locAttrs[3] + " " +
                    locAttrs[4] + " ");

                for (int i = 0; i < cofTypes.length; i++)
                    System.out.print( cofTypes[i] + " ");

                System.out.println(
                    manAttrs[1] + ", " +
                    manAttrs[2]);

                rs2.close();
                pstmt.close();
            }

            rs.close();

        } catch(BatchUpdateException b) {
            System.err.println("-----BatchUpdateException-----");
            System.err.println("SQLState:  " + b.getSQLState());
            System.err.println("Message:  " + b.getMessage());
            System.err.println("Vendor:  " + b.getErrorCode());
            System.err.print("Update counts:  ");
            int [] updateCounts = b.getUpdateCounts();

            for (int i = 0; i < updateCounts.length; i++) {
                System.err.print(updateCounts[i] + "   ");
            }
            System.err.println("");

        } catch(SQLException ex) {
            System.err.println("SQLException: " + ex.getMessage());
            System.err.println("SQLState:  " + ex.getSQLState());
            System.err.println("Message:  " + ex.getMessage());
            System.err.println("Vendor:  " + ex.getErrorCode());
        } finally {
            if (stmt != null) { stmt.close(); }
                JDBCTutorialUtilities.closeConnection(con);
            }
        }
    }
}

使用自定义类型映射

原文:docs.oracle.com/javase/tutorial/jdbc/basics/sqlcustommapping.html

注意:MySQL 目前不支持用户定义类型。MySQL 和 Java DB 目前不支持结构化类型或DISTINCT SQL 数据类型。没有可用的 JDBC 教程示例来演示本节中描述的功能。

随着生意蒸蒸日上,The Coffee Break 的老板经常添加新店铺并对数据库进行更改。老板决定为结构化类型ADDRESS使用自定义映射。这使老板可以对映射ADDRESS类型的 Java 类进行更改。Java 类将为ADDRESS的每个属性都有一个字段。类的名称和字段的名称可以是任何有效的 Java 标识符。

下面涵盖了以下主题:

  • 实现 SQLData

  • 使用连接的类型映射

  • 使用自定义类型映射

实现 SQLData

实现自定义映射所需的第一步是创建一个实现接口SQLData的类。

结构化类型ADDRESS的 SQL 定义如下:


  CREATE TYPE ADDRESS
  (
    NUM INTEGER,
    STREET VARCHAR(40),
    CITY VARCHAR(40),
    STATE CHAR(2),
    ZIP CHAR(5)
  );

为了自定义映射ADDRESS类型而实现SQLData接口的类可能如下所示:

public class Address implements SQLData {
    public int num;
    public String street;
    public String city;
    public String state;
    public String zip;
    private String sql_type;

    public String getSQLTypeName() {
        return sql_type;
    }

    public void readSQL(SQLInput stream, String type)
        throws SQLException {
        sql_type = type;
        num = stream.readInt();
        street = stream.readString();
        city = stream.readString();
        state = stream.readString();
        zip = stream.readString();
    }

    public void writeSQL(SQLOutput stream)
        throws SQLException {
        stream.writeInt(num);
        stream.writeString(street);
        stream.writeString(city);
        stream.writeString(state);
        stream.writeString(zip);
    }
}

使用连接的类型映射

编写一个实现SQLData接口的类之后,设置自定义映射的唯一其他事项就是在类型映射中进行条目设置。例如,这意味着输入ADDRESS类型的完全限定 SQL 名称和Address类的Class对象。类型映射是java.util.Map接口的一个实例,与每个新创建的连接相关联,因此您可以使用它。假设con是活动连接,以下代码片段向与con关联的类型映射添加了一个 UDT ADDRESS的条目。

java.util.Map map = con.getTypeMap();
map.put("SchemaName.ADDRESS", Class.forName("Address"));
con.setTypeMap(map);

每当调用getObject方法检索ADDRESS类型的实例时,驱动程序将检查与连接关联的类型映射,并看到它有一个ADDRESS的条目。驱动程序将注意到Address类的Class对象,创建其实例,并在后台执行许多其他操作以将ADDRESS映射到Address。您只需生成映射的类,然后在类型映射中进行条目设置,让驱动程序知道有一个自定义映射。驱动程序将完成其余所有工作。

存储具有自定义映射的结构化类型时情况类似。当调用setObject方法时,驱动程序将检查要设置的值是否是实现了SQLData接口的类的实例。如果是(表示存在自定义映射),驱动程序将使用自定义映射将值转换为其 SQL 对应项,然后返回给数据库。再次强调,驱动程序在后台执行自定义映射;你只需向setObject方法提供具有自定义映射的参数即可。稍后在本节中将看到一个示例。

查看使用标准映射(Struct对象)和使用 Java 编程语言中的自定义映射(类)之间的区别。下面的代码片段展示了标准映射到Struct对象的映射,这是在连接的类型映射中没有条目时驱动程序使用的映射。

ResultSet rs = stmt.executeQuery(
    "SELECT LOCATION " +
    "WHERE STORE_NO = 100003");
rs.next();
Struct address = (Struct)rs.getObject("LOCATION");

变量address包含以下属性值:4344"First_Street""Verona""CA""94545"

下面的代码片段展示了当连接的类型映射中存在结构化类型ADDRESS的条目时会发生什么。请记住,列LOCATION存储类型为ADDRESS的值。

ResultSet rs = stmt.executeQuery(
    "SELECT LOCATION " +
    "WHERE STORE_NO = 100003");
rs.next();
Address store_3 = (Address)rs.getObject("LOCATION");

变量store_3现在是Address类的实例,每个属性值都是Address的一个字段的当前值。请注意,在将getObject方法检索的对象转换为Address对象并将其分配给store_3之前,必须记得进行转换。还要注意,store_3必须是一个Address对象。

将使用Struct对象与使用Address类的实例进行比较。假设商店搬到了邻近城镇的更好位置,因此您必须更新数据库。使用自定义映射,重置store_3的字段,如下面的代码片段所示:

ResultSet rs = stmt.executeQuery(
    "SELECT LOCATION " +
    "WHERE STORE_NO = 100003");
rs.next();
Address store_3 = (Address)rs.getObject("LOCATION");
store_3.num = 1800;
store_3.street = "Artsy_Alley";
store_3.city = "Arden";
store_3.state = "CA";
store_3.zip = "94546";
PreparedStatement pstmt = con.prepareStatement(
    "UPDATE STORES " +
    "SET LOCATION = ? " +
    "WHERE STORE_NO = 100003");
pstmt.setObject(1, store_3);
pstmt.executeUpdate();

LOCATION中的值是ADDRESS类型的实例。驱动程序检查连接的类型映射,看到将ADDRESS与类Address关联的条目,因此使用Address中指示的自定义映射。当代码使用变量*store_3*作为第二个参数调用setObject方法时,驱动程序检查并看到*store_3*表示Address类的实例,该类实现了结构化类型ADDRESSSQLData接口,并再次自动使用自定义映射。

没有ADDRESS的自定义映射,更新将更像这样:

PreparedStatement pstmt = con.prepareStatement(
    "UPDATE STORES " +
    "SET LOCATION.NUM = 1800, " +
    "LOCATION.STREET = 'Artsy_Alley', " + 
    "LOCATION.CITY = 'Arden', " +
    "LOCATION.STATE = 'CA', " +
    "LOCATION.ZIP = '94546' " +
    "WHERE STORE_NO = 100003");
pstmt.executeUpdate;

使用您自己的类型映射

到目前为止,您仅使用了与连接关联的类型映射进行自定义映射。通常情况下,大多数程序员只会使用这种类型映射。但是,也可以创建一个类型映射并将其传递给某些方法,以便驱动程序将使用该类型映射而不是与连接关联的类型映射。这允许为同一用户定义类型(UDT)使用两种不同的映射。实际上,可以为相同的 UDT 拥有多个自定义映射,只要每个映射都设置为实现SQLData接口的类和类型映射中的条目。如果您没有向可以接受类型映射的方法传递类型映射,则驱动程序将默认使用与连接关联的类型映射。

在使用与连接关联的类型映射之外的类型映射的情况非常少见。例如,如果几个程序员共同开发 JDBC 应用程序并且在使用相同连接,则可能需要为方法提供一个类型映射。如果两个或更多程序员为相同的 SQL UDT 创建了自定义映射,则每个人都需要提供自己的类型映射,从而覆盖连接的类型映射。

使用 Datalink 对象

原文:docs.oracle.com/javase/tutorial/jdbc/basics/sqldatalink.html

DATALINK值通过 URL 引用底层数据源之外的资源。URL,统一资源定位符,是指向万维网上资源的指针。资源可以是简单的文件或目录,也可以是对更复杂对象的引用,比如对数据库查询或搜索引擎的查询。

下列主题包括:

  • 存储外部数据引用

  • 检索外部数据引用

存储外部数据引用

使用方法PreparedStatement.setURL来为预编译语句指定一个java.net.URL对象。在 URL 类型不被 Java 平台支持的情况下,可以使用setString方法存储 URL。

例如,假设 The Coffee Break 的所有者想要在数据库表中存储一组重要的 URL。以下方法DatalinkSample.addURLRow向表DATA_REPOSITORY添加一行数据。该行包括标识 URL 的字符串,DOCUMENT_NAME和 URL 本身,URL

  public void addURLRow(String description, String url) throws SQLException {
    String query = "INSERT INTO data_repository(document_name,url) VALUES (?,?)";
    try (PreparedStatement pstmt = this.con.prepareStatement(query)) {
      pstmt.setString(1, description);
      pstmt.setURL(2,new URL(url));
      pstmt.execute();    
    } catch (SQLException sqlex) {
      JDBCTutorialUtilities.printSQLException(sqlex);
    } catch (Exception ex) {
      System.out.println("Unexpected exception");
      ex.printStackTrace();
    }
  }

检索外部数据引用

使用方法ResultSet.getURL检索外部数据引用作为java.net.URL对象。在getObjectgetURL方法返回的 URL 类型不被 Java 平台支持的情况下,通过调用getString方法将 URL 作为String对象检索。

下列方法DatalinkSample.viewTable显示了表DATA_REPOSITORY中所有 URL 的内容:

  public static void viewTable(Connection con, Proxy proxy)
    throws SQLException, IOException {
    String query = "SELECT document_name, url FROM data_repository";
    try (Statement stmt = con.createStatement()) {
      ResultSet rs = stmt.executeQuery(query);
      if ( rs.next() )  {
        String documentName = null;
        java.net.URL url = null;
        documentName = rs.getString(1);
        // Retrieve the value as a URL object.
        url = rs.getURL(2);    
        if (url != null) {
          // Retrieve the contents from the URL.
          URLConnection myURLConnection = url.openConnection(proxy);
          BufferedReader bReader =
            new BufferedReader(new InputStreamReader(myURLConnection.getInputStream()));
          System.out.println("Document name: " + documentName);
          String pageContent = null;
          while ((pageContent = bReader.readLine()) != null ) {
            // Print the URL contents
            System.out.println(pageContent);
          }
        } else { 
          System.out.println("URL is null");
        } 
      }
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    } catch(IOException ioEx) {
      System.out.println("IOException caught: " + ioEx.toString());
    } catch (Exception ex) {
      System.out.println("Unexpected exception");
      ex.printStackTrace();
    }
  }

示例DatalinkSample.java将 Oracle URL www.oracle.com 存储在表DATA_REPOSITORY中。然后,它显示了所有在DATA_REPOSITORY中存储的 URL 引用的文档内容,其中包括 Oracle 主页,www.oracle.com

以下语句从结果集中检索 URL 作为java.net.URL对象:

url = rs.getURL(2);

示例使用以下语句访问URL对象引用的数据:

          // Retrieve the contents from the URL.
          URLConnection myURLConnection = url.openConnection(proxy);
          BufferedReader bReader =
            new BufferedReader(new InputStreamReader(myURLConnection.getInputStream()));
          System.out.println("Document name: " + documentName);
          String pageContent = null;
          while ((pageContent = bReader.readLine()) != null ) {
            // Print the URL contents
            System.out.println(pageContent);
          }

方法URLConnection.openConnection可以不带参数,这意味着URLConnection表示直接连接到互联网。如果需要代理服务器连接到互联网,openConnection方法接受一个java.net.Proxy对象作为参数。以下语句演示如何创建一个 HTTP 代理,服务器名称为www-proxy.example.com,端口号为80

Proxy myProxy;
InetSocketAddress myProxyServer;
myProxyServer = new InetSocketAddress("www-proxy.example.com", 80);
myProxy = new Proxy(Proxy.Type.HTTP, myProxyServer);

使用 RowId 对象

原文:docs.oracle.com/javase/tutorial/jdbc/basics/sqlrowid.html

注意:MySQL 和 Java DB 目前不支持RowId JDBC 接口。因此,没有可用的 JDBC 教程示例来演示本节中描述的功能。

RowId对象表示数据库表中一行的地址。但请注意,ROWID类型不是标准 SQL 类型。ROWID值可能很有用,因为它们通常是访问单个行的最快方式,并且是表中行的唯一标识符。但是,您不应将ROWID值用作表的主键。例如,如果从表中删除特定行,则数据库可能会将其ROWID值重新分配给稍后插入的行。

下面涵盖了以下主题:

  • 检索 RowId 对象

  • 使用 RowId 对象

  • RowId 有效期

检索 RowId 对象

通过调用接口ResultSetCallableStatement中定义的 getter 方法检索java.sql.RowId对象。返回的RowId对象是一个不可变对象,您可以将其用作后续引用的唯一标识符。以下是调用ResultSet.getRowId方法的示例:

java.sql.RowId rowId_1 = rs.getRowId(1);

使用 RowId 对象

您可以将RowId对象设置为参数传递给参数化的PreparedStatement对象:

Connection conn = ds.getConnection(username, password);
PreparedStatement ps = conn.prepareStatement(
    "INSERT INTO BOOKLIST" +
    "(ID, AUTHOR, TITLE, ISBN) " +
    "VALUES (?, ?, ?, ?)");
ps.setRowId(1, rowId_1);

您还可以在可更新的ResultSet对象中使用特定的RowId对象更新列:

ResultSet rs = ...
rs.next();
rs.updateRowId(1, rowId_1);

RowId对象的值通常在数据源之间不可移植,并且在分别使用PreparedStatementResultSet对象的设置或更新方法时应被视为特定于数据源。因此,不建议从连接到一个数据源的ResultSet对象获取RowId对象,然后尝试在连接到不同数据源的不相关ResultSet对象中使用相同的RowId对象。

RowId 有效期

只要识别的行未被删除,RowId对象就有效,并且RowId对象的生命周期在数据源为RowId指定的生命周期范围内。

要确定数据库或数据源中RowId对象的生命周期,请调用方法DatabaseMetaData.getRowIdLifetime。它返回一个RowIdLifetime枚举数据类型的值。以下方法,JDBCTutorialUtilities.rowIdLifeTime,返回RowId对象的生命周期:

public static void rowIdLifetime(Connection conn)
    throws SQLException {

    DatabaseMetaData dbMetaData = conn.getMetaData();
    RowIdLifetime lifetime = dbMetaData.getRowIdLifetime();

    switch (lifetime) {
        case ROWID_UNSUPPORTED:
            System.out.println("ROWID type not supported");
            break;

        case ROWID_VALID_FOREVER:
            System.out.println("ROWID has unlimited lifetime");
            break;

        case ROWID_VALID_OTHER:
            System.out.println("ROWID has indeterminate lifetime");
            break;

        case ROWID_VALID_SESSION:
            System.out.println(
                "ROWID type has lifetime that " +
                "is valid for at least the " +
                "containing session");
            break;

        case ROWID_VALID_TRANSACTION:
            System.out.println(
                "ROWID type has lifetime that " +
                "is valid for at least the " +
                "containing transaction");
            break;
    }
}

使用存储过程

原文:docs.oracle.com/javase/tutorial/jdbc/basics/storedprocedures.html

存储过程是一组 SQL 语句,形成一个逻辑单元并执行特定任务,它们用于封装一组操作或查询以在数据库服务器上执行。例如,对员工数据库的操作(雇佣、解雇、晋升、查找)可以编码为应用程序代码执行的存储过程。存储过程可以编译并使用不同的参数和结果执行,并且可以具有任何组合的输入、输出和输入/输出参数。

请注意,大多数 DBMS 都支持存储过程,但它们的语法和功能有相当多的变化。因此,本教程包含两个示例,StoredProcedureJavaDBSample.javaStoredProcedureMySQLSample.java,分别演示如何在 Java DB 和 MySQL 中创建存储过程。

本页涵盖以下主题:

  • 存储过程示例概述

  • 参数模式

  • 在 Java DB 中创建存储过程

    • 使用 SQL 脚本或 JDBC API 在 Java DB 中创建存储过程

    • 使用 SQL 脚本或 JDBC API 在 Java DB 中创建存储过程

    • 在 Java DB 中调用存储过程

    • 将 Java 类打包到 JAR 文件中

  • 在 MySQL 中创建存储过程

    • 使用 SQL 脚本或 JDBC API 在 MySQL 中创建存储过程

    • 在 MySQL 中调用存储过程

存储过程示例概述

示例StoredProcedureJavaDBSample.javaStoredProcedureMySQLSample.java 创建并调用以下存储过程:

  • SHOW_SUPPLIERS: 打印一个包含咖啡供应商名称和他们向 The Coffee Break 供应的咖啡的结果集。此存储过程不需要任何参数。当示例调用此存储过程时,示例产生类似以下内容的输出:

    Acme, Inc.: Colombian_Decaf
    Acme, Inc.: Colombian
    Superior Coffee: French_Roast_Decaf
    Superior Coffee: French_Roast
    The High Ground: Espresso
    
    
  • GET_SUPPLIER_OF_COFFEE: 打印供应商supplierName为咖啡coffeeName的名称。它需要以下参数:

    • IN coffeeName varchar(32): 咖啡的名称

    • OUT supplierName varchar(40): 咖啡供应商的名称

    当示例以Colombian作为coffeeName的值调用此存储过程时,示例产生类似以下内容的输出:

    Supplier of the coffee Colombian: Acme, Inc.
    
    
  • RAISE_PRICE:将咖啡coffeeName的价格提高到价格newPrice。如果价格增加大于百分比maximumPercentage,则价格将按该百分比提高。如果价格newPrice低于咖啡的原始价格,则此过程不会更改价格。它需要以下参数:

    • IN coffeeName varchar(32):咖啡的名称

    • IN maximumPercentage float:提高咖啡价格的最大百分比

    • INOUT newPrice numeric(10,2):咖啡的新价格。调用RAISE_PRICE存储过程后,此参数将包含咖啡coffeeName的当前价格。

    当示例以Colombian作为coffeeName的值,0.10作为maximumPercentage的值,19.99作为newPrice的值调用此存储过程时,示例会产生类似以下输出:

    Contents of COFFEES table before calling RAISE_PRICE:
    Colombian, 101, 7.99, 0, 0
    Colombian_Decaf, 101, 8.99, 0, 0
    Espresso, 150, 9.99, 0, 0
    French_Roast, 49, 8.99, 0, 0
    French_Roast_Decaf, 49, 9.99, 0, 0
    
    Calling the procedure RAISE_PRICE
    
    Value of newPrice after calling RAISE_PRICE: 8.79
    
    Contents of COFFEES table after calling RAISE_PRICE:
    Colombian, 101, 8.79, 0, 0
    Colombian_Decaf, 101, 8.99, 0, 0
    Espresso, 150, 9.99, 0, 0
    French_Roast, 49, 8.99, 0, 0
    French_Roast_Decaf, 49, 9.99, 0, 0
    
    

参数模式

参数属性IN(默认值)、OUTINOUT是参数模式。它们定义形式参数的操作。以下表总结了有关参数模式的信息。

参数模式的特征 IN OUT INOUT
必须在存储过程定义中指定吗? 不需要;如果省略,则形式参数的参数模式为IN 必须指定。 必须指定。
参数是否向存储过程传递值或返回值? 向存储过程传递值。 返回值给调用者。 两者都是;向存储过程传递初始值;返回更新后的值给调用者。
形式参数在存储过程中是作为常量还是变量? 形式参数像常量一样起作用。 形式参数像未初始化的变量一样起作用。 形式参数像初始化的变量一样起作用。
形式参数是否可以在存储过程中分配值? 形式参数不能分配值。 形式参数不能在表达式中使用;必须分配值。 形式参数必须分配值。
可以传递给存储过程的实际参数(参数)有哪些? 实际参数可以是常量、初始化变量、文字常量或表达式。 实际参数必须是一个变量。 实际参数必须是一个变量。

在 Java DB 中创建存储过程

注意:有关在 Java DB 中创建存储过程的更多信息,请参阅Java DB 参考手册中的“CREATE PROCEDURE 语句”部分。

在 Java DB 中创建和使用存储过程涉及以下步骤:

  1. 在 Java 类中创建一个公共静态 Java 方法:此方法执行存储过程所需的任务。

  2. 创建存储过程:此存储过程调用您创建的 Java 方法。

  3. 调用存储过程

  4. 将包含您之前创建的公共静态 Java 方法的 Java 类打包到一个 JAR 文件中。

创建公共静态 Java 方法

下面的方法,StoredProcedureJavaDBSample.showSuppliers,包含存储过程SHOW_SUPPLIERS调用的 SQL 语句:

public static void showSuppliers(ResultSet[] rs)
    throws SQLException {

    Connection con = DriverManager.getConnection("jdbc:default:connection");
    Statement stmt = null;

    String query =
        "select SUPPLIERS.SUP_NAME, " +
        "COFFEES.COF_NAME " +
        "from SUPPLIERS, COFFEES " +
        "where SUPPLIERS.SUP_ID = " +
        "COFFEES.SUP_ID " +
        "order by SUP_NAME";

    stmt = con.createStatement();
    rs[0] = stmt.executeQuery(query);
}

SHOW_SUPPLIERS存储过程不接受任何参数。您可以通过在公共静态 Java 方法的方法签名中定义参数来指定存储过程中的参数。请注意,方法showSuppliers包含一个类型为ResultSet[]的参数。如果您的存储过程返回任意数量的ResultSet对象,请在您的 Java 方法中指定一个类型为ResultSet[]的参数。此外,请确保此 Java 方法是公共的和静态的。

从 URL jdbc:default:connection 中检索Connection对象。这是 Java DB 中的一种约定,表示存储过程将使用当前存在的Connection对象。

请注意,在此方法中未关闭Statement对象。不要在存储过程的 Java 方法中关闭任何Statement对象;如果这样做,当您调用存储过程时,ResultSet对象将不存在。

为了使存储过程返回一个生成的结果集,您必须将结果集分配给ResultSet[]参数的一个数组组件。在本例中,生成的结果集分配给了数组组件rs[0]

使用 SQL 脚本或 JDBC API 在 Java DB 中创建存储过程

Java DB 使用 Java 编程语言进行存储过程。因此,当您定义存储过程时,您需要指定要调用的 Java 类以及 Java DB 可以找到它的位置。

以下摘录自StoredProcedureJavaDBSample.createProcedures创建了一个名为SHOW_SUPPLIERS的存储过程:

public void createProcedures(Connection con)
    throws SQLException {

    Statement stmtCreateShowSuppliers = null;

    // ...

    String queryShowSuppliers =
        "CREATE PROCEDURE SHOW_SUPPLIERS() " +
        "PARAMETER STYLE JAVA " +
        "LANGUAGE JAVA " +
        "DYNAMIC RESULT SETS 1 " +
        "EXTERNAL NAME " +
        "'com.oracle.tutorial.jdbc." +
        "StoredProcedureJavaDBSample." +
        "showSuppliers'";

    // ...

    try {
        System.out.println("Calling CREATE PROCEDURE");
        stmtCreateShowSuppliers = con.createStatement();

        // ...

    } catch (SQLException e) {
        JDBCTutorialUtilities.printSQLException(e);
    } finally {
        if (stmtCreateShowSuppliers != null) {
            stmtCreateShowSuppliers.close();
        }
        // ...
    }
}

以下列表描述了您可以在CREATE PROCEDURE语句中指定的过程元素:

  • PARAMETER STYLE:标识用于将参数传递给存储过程的约定。以下选项有效:

    • JAVA:指定存储过程使用符合 Java 语言和 SQL 例程规范的参数传递约定。

    • DERBY:指定存储过程支持参数列表中的最后一个参数作为可变参数。

  • LANGUAGE JAVA:指定存储过程的编程语言(目前,JAVA是唯一的选项)。

  • DYNAMIC RESULT SETS 1:指定检索的最大结果集数量;在本例中为1

  • EXTERNAL NAME 'com.oracle.tutorial.jdbc.StoredProcedureJavaDBSample.showSuppliers' 指定了此存储过程调用的完全限定的 Java 方法。注意:Java DB 必须能够在类路径或直接添加到数据库的 JAR 文件中找到此处指定的方法。请参阅以下步骤,将 Java 类打包到 JAR 文件中。

以下语句(位于 StoredProcedureJavaDBSample.createProcedures 中)创建了一个名为 GET_SUPPLIERS_OF_COFFEE 的存储过程(为了清晰起见添加了换行符):

CREATE PROCEDURE GET_SUPPLIER_OF_COFFEE(
    IN coffeeName varchar(32),
    OUT supplierName
    varchar(40))
    PARAMETER STYLE JAVA
    LANGUAGE JAVA
    DYNAMIC RESULT SETS 0
    EXTERNAL NAME 'com.oracle.tutorial.jdbc.
        StoredProcedureJavaDBSample.
        getSupplierOfCoffee'

此存储过程有两个形式参数,coffeeNamesupplierName。参数说明符 INOUT 被称为参数模式。它们定义了形式参数的操作。有关更多信息,请参阅参数模式。此存储过程不检索结果集,因此过程元素 DYNAMIC RESULT SETS0

以下语句创建了一个名为 RAISE_PRICE 的存储过程(为了清晰起见添加了换行符):

CREATE PROCEDURE RAISE_PRICE(
    IN coffeeName varchar(32),
    IN maximumPercentage float,
    INOUT newPrice float)
    PARAMETER STYLE JAVA
    LANGUAGE JAVA
    DYNAMIC RESULT SETS 0
    EXTERNAL NAME 'com.oracle.tutorial.jdbc.
        StoredProcedureJavaDBSample.raisePrice'

您可以使用 SQL 脚本在 Java DB 中创建存储过程。查看脚本 javadb/create-procedures.sqlbuild.xml Ant 构建脚本中的 Ant 目标 javadb-create-procedure

在 Java DB 中调用存储过程

以下摘录自方法StoredProcedureJavaDBSample.runStoredProcedures 调用存储过程 SHOW_SUPPLIERS 并打印生成的结果集:

cs = this.con.prepareCall("{call SHOW_SUPPLIERS()}");
ResultSet rs = cs.executeQuery();

while (rs.next()) {
    String supplier = rs.getString("SUP_NAME");
    String coffee = rs.getString("COF_NAME");
    System.out.println(supplier + ": " + coffee);
}

注意:与 Statement 对象一样,要调用存储过程,可以根据过程返回多少个 ResultSet 对象来调用 executeexecuteQueryexecuteUpdate。但是,如果不确定过程返回多少个 ResultSet 对象,请调用 execute

以下摘录自方法 StoredProcedureJavaDBSample.runStoredProcedures 调用存储过程 GET_SUPPLIER_OF_COFFEE

cs = this.con.prepareCall("{call GET_SUPPLIER_OF_COFFEE(?, ?)}");
cs.setString(1, coffeeNameArg);
cs.registerOutParameter(2, Types.VARCHAR);
cs.executeQuery();

String supplierName = cs.getString(2);

接口 CallableStatement 扩展了 PreparedStatement。它用于调用存储过程。像使用 PreparedStatement 对象一样,通过调用适当的 setter 方法为 IN 参数(例如本例中的 coffeeName)指定值。但是,如果存储过程包含 OUT 参数,则必须使用 registerOutParameter 方法进行注册。

以下摘录自方法 StoredProcedureJavaDBSample.runStoredProcedures 调用存储过程 RAISE_PRICE

cs = this.con.prepareCall("{call RAISE_PRICE(?,?,?)}");
cs.setString(1, coffeeNameArg);
cs.setFloat(2, maximumPercentageArg);
cs.registerOutParameter(3, Types.NUMERIC);
cs.setFloat(3, newPriceArg);

cs.execute();

因为参数 newPrice(过程 RAISE_PRICE 中的第三个参数)具有参数模式 INOUT,您必须通过调用适当的 setter 方法指定其值,并使用 registerOutParameter 方法进行注册。

将 Java 类打包到 JAR 文件中

Ant 构建脚本 build.xml 包含编译和打包教程为 JAR 文件的目标。在命令提示符下,将当前目录更改为 *<JDBC tutorial directory>*。从该目录运行以下命令编译并打包教程为 JAR 文件:

ant jar

JAR 文件的名称是 *<JDBC tutorial directory>*/lib/JDBCTutorial.jar

Ant 构建脚本将文件 JDBCTutorial.jar 添加到类路径中。您还可以在 CLASSPATH 环境变量中指定 JAR 文件的位置。这样可以使 Java DB 找到存储过程调用的 Java 方法。

直接向数据库添加 JAR 文件

Java DB 首先在类路径中查找所需的类,然后在数据库中查找。本节展示了如何直接向数据库添加 JAR 文件。

使用以下系统存储过程将 JDBCTutorial.jar JAR 文件添加到数据库中(为了清晰起见已添加换行符):

CALL sqlj.install_jar(
  '*<JDBC tutorial directory>*/
  lib/JDBCTutorial.jar',
  'APP.JDBCTutorial', 0)
CALL sqlj.replace_jar(
  '*<JDBC tutorial directory>*/
  lib/JDBCTutorial.jar',
  'APP.JDBCTutorial')";
CALL syscs_util.syscs_set_database_property(
  'derby.database.classpath',
  'APP.JDBCTutorial')";

注意:方法 StoredProcedureJavaDBSample.registerJarFile 演示了如何调用这些系统存储过程。如果调用此方法,请确保已修改 javadb-sample-properties.xml,使属性 jar_file 的值设置为 JDBCTutorial.jar 的完整路径名。

SQL 模式中的 install_jar 过程向数据库添加 JAR 文件。此过程的第一个参数是在运行此过程的计算机上 JAR 文件的完整路径名。第二个参数是 Java DB 用于引用 JAR 文件的标识符。(标识符 APP 是 Java DB 默认模式。)replace_jar 过程替换数据库中已有的 JAR 文件。

系统存储过程 SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY 在当前连接上设置或删除数据库属性的值。此方法将属性 derby.database.classpath 设置为 install_jar 文件中指定的标识符。Java DB 首先在 Java 类路径中查找类,然后查找 derby.database.classpath

在 MySQL 中创建存储过程

在 Java DB 中创建和使用存储过程涉及以下步骤:

  1. 使用 SQL 脚本或 JDBC API 创建存储过程。

  2. 使用 CALL SQL 语句调用存储过程。参见 在 MySQL 中调用存储过程 部分。

使用 SQL 脚本或 JDBC API 在 MySQL 中创建存储过程

MySQL 使用基于 SQL 的语法来编写存储过程。以下摘录来自 SQL 脚本 mysql/create-procedures.sql 创建了名为 SHOW_SUPPLIERS 的存储过程:

SELECT 'Dropping procedure SHOW_SUPPLIERS' AS ' '|
drop procedure if exists SHOW_SUPPLIERS|

# ...

SELECT 'Creating procedure SHOW_SUPPLIERS' AS ' '|
create procedure SHOW_SUPPLIERS()
    begin
        select SUPPLIERS.SUP_NAME,
        COFFEES.COF_NAME
        from SUPPLIERS, COFFEES
        where SUPPLIERS.SUP_ID = COFFEES.SUP_ID
        order by SUP_NAME;
    end|

DROP PROCEDURE 语句会删除存储过程 SHOW_SUPPLIERS(如果存在的话)。在 MySQL 中,存储过程中的语句用分号分隔。然而,结束 create procedure 语句需要一个不同的分隔符。这个示例使用了竖线(|)字符;你可以使用其他字符(或多个字符)。分隔语句的字符在调用这个脚本的 Ant 目标中的 delimiter 属性中定义。这段摘录来自 Ant 构建文件 build.xml(为了清晰起见插入了换行符):

<target name="mysql-create-procedure">

  <sql driver="${DB.DRIVER}"
       url="${DB.URL}" userid="${DB.USER}"
       password="${DB.PASSWORD}"
       classpathref="CLASSPATH"
       print="true"
       delimiter="|"
       autocommit="false"
       onerror="abort">
       <transaction
         src="./sql/${DB.VENDOR}/
           create-procedures.sql">
       </transaction>
  </sql>

</target>

或者,你可以使用 DELIMITER SQL 语句来指定一个不同的分隔符字符。

CREATE PROCEDURE 语句由过程的名称、括号中以逗号分隔的参数列表以及 BEGINEND 关键字内的 SQL 语句组成。

你可以使用 JDBC API 来创建存储过程。下面的方法 StoredProcedureMySQLSample.createProcedureShowSuppliers 执行了与前面脚本相同的任务:

  public void createProcedureShowSuppliers() throws SQLException {

    String queryDrop = "DROP PROCEDURE IF EXISTS SHOW_SUPPLIERS";

    String createProcedure =
        "create procedure SHOW_SUPPLIERS() " +
          "begin " +
            "select SUPPLIERS.SUP_NAME, COFFEES.COF_NAME " +
              "from SUPPLIERS, COFFEES " +
              "where SUPPLIERS.SUP_ID = COFFEES.SUP_ID " +
              "order by SUP_NAME; " +
          "end";

    try (Statement stmtDrop = con.createStatement()) {
      System.out.println("Calling DROP PROCEDURE");
      stmtDrop.execute(queryDrop);
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    } 

    try (Statement stmt = con.createStatement()) {
      stmt.executeUpdate(createProcedure);
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    }
  }

请注意,在这个方法中分隔符没有被改变。

存储过程 SHOW_SUPPLIERS 生成一个结果集,尽管方法 createProcedureShowSuppliers 的返回类型是 void,并且该方法不包含任何参数。当使用方法 CallableStatement.executeQuery 调用存储过程 SHOW_SUPPLIERS 时,会返回一个结果集:

CallableStatement cs = null;
cs = this.con.prepareCall("{call SHOW_SUPPLIERS}");
ResultSet rs = cs.executeQuery();

下面从方法 StoredProcedureMySQLSample.createProcedureGetSupplierOfCoffee 中提取的内容包含了创建名为 GET_SUPPLIER_OF_COFFEE 的存储过程的 SQL 查询:

  public void createProcedureGetSupplierOfCoffee() throws SQLException {

    String queryDrop = "DROP PROCEDURE IF EXISTS GET_SUPPLIER_OF_COFFEE";

    String createProcedure =
        "create procedure GET_SUPPLIER_OF_COFFEE(IN coffeeName varchar(32), OUT supplierName varchar(40)) " +
          "begin " +
            "select SUPPLIERS.SUP_NAME into supplierName " +
              "from SUPPLIERS, COFFEES " +
              "where SUPPLIERS.SUP_ID = COFFEES.SUP_ID " +
              "and coffeeName = COFFEES.COF_NAME; " +
            "select supplierName; " +
          "end";

    try (Statement stmtDrop = con.createStatement()) {
      System.out.println("Calling DROP PROCEDURE");
      stmtDrop.execute(queryDrop);
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    }

    try (Statement stmt = con.createStatement()) {
      stmt.executeUpdate(createProcedure);
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    }
  }

这个存储过程有两个形式参数,coffeeNamesupplierName。参数说明符 INOUT 被称为参数模式。它们定义了形式参数的作用。更多信息请参见 参数模式。形式参数在 SQL 查询中定义,而不是在方法 createProcedureGetSupplierOfCoffee 中。为了给 OUT 参数 supplierName 赋值,这个存储过程使用了一个 SELECT 语句。

下面从方法 StoredProcedureMySQLSample.createProcedureRaisePrice 中提取的内容包含了创建名为 RAISE_PRICE 的存储过程的 SQL 查询:

  public void createProcedureRaisePrice() throws SQLException {

    String queryDrop = "DROP PROCEDURE IF EXISTS RAISE_PRICE";

    String createProcedure =
        "create procedure RAISE_PRICE(IN coffeeName varchar(32), IN maximumPercentage float, INOUT newPrice numeric(10,2)) " +
          "begin " +
            "main: BEGIN " +
              "declare maximumNewPrice numeric(10,2); " +
              "declare oldPrice numeric(10,2); " +
              "select COFFEES.PRICE into oldPrice " +
                "from COFFEES " +
                "where COFFEES.COF_NAME = coffeeName; " +
              "set maximumNewPrice = oldPrice * (1 + maximumPercentage); " +
              "if (newPrice > maximumNewPrice) " +
                "then set newPrice = maximumNewPrice; " +
              "end if; " +
              "if (newPrice <= oldPrice) " +
                "then set newPrice = oldPrice;" +
                "leave main; " +
              "end if; " +
              "update COFFEES " +
                "set COFFEES.PRICE = newPrice " +
                "where COFFEES.COF_NAME = coffeeName; " +
              "select newPrice; " +
            "END main; " +
          "end";

    try (Statement stmtDrop = con.createStatement()) {
      System.out.println("Calling DROP PROCEDURE");
      stmtDrop.execute(queryDrop);
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    }

    try (Statement stmt = con.createStatement()) {
      stmt.executeUpdate(createProcedure);
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    }
  }

存储过程使用 SETSELECT 语句给 INOUT 参数 newPrice 赋值。为了退出存储过程,存储过程首先将语句封装在一个标记为 mainBEGIN ... END 块中。为了退出过程,方法使用语句 leave main

在 MySQL 中调用存储过程

在 MySQL 中调用存储过程与在 Java DB 中调用它们相同。

下面是从方法StoredProcedureMySQLSample.runStoredProcedures中调用存储过程SHOW_SUPPLIERS并打印生成的结果集:

      cs = this.con.prepareCall("{call SHOW_SUPPLIERS}");
      ResultSet rs = cs.executeQuery();
      while (rs.next()) {
        String supplier = rs.getString("SUP_NAME");
        String coffee = rs.getString("COF_NAME");
        System.out.println(supplier + ": " + coffee);
      }

注意:与Statement对象一样,要调用存储过程,可以根据过程返回的ResultSet对象数量调用executeexecuteQueryexecuteUpdate。但是,如果不确定过程返回多少个ResultSet对象,请调用execute

下面是从方法StoredProcedureMySQLSample.runStoredProcedures中调用存储过程GET_SUPPLIER_OF_COFFEE的摘录:

      cs = this.con.prepareCall("{call GET_SUPPLIER_OF_COFFEE(?, ?)}");
      cs.setString(1, coffeeNameArg);
      cs.registerOutParameter(2, Types.VARCHAR);
      cs.executeQuery();
      String supplierName = cs.getString(2);

接口CallableStatement扩展了PreparedStatement。它用于调用存储过程。像使用PreparedStatement对象一样,通过调用适当的 setter 方法为IN参数(例如本例中的coffeeName)指定值。但是,如果存储过程包含OUT参数,必须使用registerOutParameter方法注册它。

下面是从方法StoredProcedureMySQLSample.runStoredProcedures中调用存储过程RAISE_PRICE的摘录:

      cs = this.con.prepareCall("{call RAISE_PRICE(?,?,?)}");
      cs.setString(1, coffeeNameArg);
      cs.setFloat(2, maximumPercentageArg);
      cs.registerOutParameter(3, Types.NUMERIC);
      cs.setFloat(3, newPriceArg);
      cs.execute();

因为参数newPrice(过程RAISE_PRICE中的第三个参数)具有参数模式INOUT,您必须通过调用适当的 setter 方法指定其值,并使用registerOutParameter方法注册它。

使用 GUI API 的 JDBC

原文:docs.oracle.com/javase/tutorial/jdbc/basics/jdbcswing.html

示例CoffeesFrame.java演示了如何将 JDBC 与 GUI API 集成,特别是 Swing API。它在表中显示了COFFEES数据库表的内容,并包含字段和按钮,使您可以向表中添加行。以下是此示例的屏幕截图:

示例 CoffeeFrames.java 的屏幕截图

该示例包含五个文本字段,对应于COFFEES表中的每一列。它还包含三个按钮:

  • 向表中添加行:根据文本字段中输入的数据向示例表中添加一行。

  • 更新数据库:根据示例表中的数据更新COFFEES表。

  • 放弃更改:检索COFFEES表的内容,替换示例表中的现有数据。

这个示例(需要CoffeesTableModel.java)演示了将 JDBC 与 Swing API 集成的一般步骤:

  1. 实现TableModel接口

  2. 实现RowSetListener接口

  3. 布局 Swing 组件

  4. 为示例中的按钮添加监听器

实现 javax.swing.event.TableModel

TableModel接口使得 Java Swing 应用程序能够管理JTable对象中的数据。示例CoffeesTableModel.java实现了这个接口。它指定了JTable对象应该如何从RowSet对象中检索数据并在表中显示。

注意:尽管此示例在 Swing 应用程序中显示了COFFEES表的内容,但CoffeesTableModel类应该适用于任何 SQL 表,只要它的数据可以用String对象表示。(但是,用于向COFFEES添加行的字段,这些字段在CoffeesFrame类中指定,必须针对其他 SQL 表进行修改。)

在实现TableModel接口的方法之前,CoffeeTableModel类的构造函数初始化了为这些实现方法所需的各种成员变量,如下所示:

public CoffeesTableModel(CachedRowSet rowSetArg)
    throws SQLException {

    this.coffeesRowSet = rowSetArg;
    this.metadata = this.coffeesRowSet.getMetaData();
    numcols = metadata.getColumnCount();

    // Retrieve the number of rows.
    this.coffeesRowSet.beforeFirst();
    this.numrows = 0;
    while (this.coffeesRowSet.next()) {
        this.numrows++;
    }
    this.coffeesRowSet.beforeFirst();
}

以下描述了在这个构造函数中初始化的成员变量:

  • CachedRowSet coffeesRowSet:存储COFFEES表的内容。

    本示例使用RowSet对象,特别是CachedRowSet对象,而不是ResultSet对象,有两个原因。CachedRowSet对象使应用程序的用户能够对其中包含的数据进行更改,而无需连接到数据库。此外,因为CachedRowSet对象是一个 JavaBeans 组件,它可以在发生某些事情时通知其他组件。在本示例中,当向CachedRowSet对象添加新行时,它会通知渲染表中数据的 Swing 组件刷新自身并显示新行。

  • ResultSetMetaData metadata: 检索表COFFEES中的列数以及每个列的名称。

  • int numcols, numrows: 分别存储表COFFEES中的列数和行数。

CoffeesTableModel.java示例实现了TableModel接口中的以下方法:

  • Class<?> getColumnClass(int columnIndex): 返回列中所有单元格值的最具体的超类。

  • int getColumnCount(): 返回模型中的列数。

  • String getColumnName(int columnIndex): 返回由参数columnIndex指定的列的名称。

  • int getRowCount(): 返回模型中的行数。

  • Object getValueAt(int rowIndex, int columnIndex): 返回交叉点处的单元格的值,该单元格位于列columnIndex和行rowIndex的交叉点处。

  • boolean isCellEditable(int rowIndex, int columnIndex): 如果列rowIndex和行columnIndex的交叉点处的单元格可以编辑,则返回 true。

以下方法未实现,因为此示例不允许用户直接编辑表的内容:

  • void addTableModelListener(TableModelListener l): 向列表中添加一个侦听器,每当数据模型发生更改时通知该侦听器。

  • void removeTableModelListener(TableModelListener l): 从列表中移除一个侦听器,每当数据模型发生更改时通知该侦听器。

  • void setValueAt(Object aValue, int rowIndex, int columnIndex): 将交叉点处的单元格中的值设置为对象aValue,该单元格位于列columnIndex和行rowIndex的交叉点处。

实现 getColumnCount 和 getRowCount

getColumnCountgetRowCount方法分别返回成员变量numcolsnumrows的值:

public int getColumnCount() {
    return numcols;
}

public int getRowCount() {
    return numrows;
}

实现 getColumnClass

getColumnClass方法返回指定列的数据类型。为了保持简单,此方法返回String类,从而将表中的所有数据转换为String对象。JTable类使用此方法确定如何在 GUI 应用程序中呈现数据。

public Class getColumnClass(int column) {
    return String.class;
}

实现 getColumnName

getColumnName方法返回指定列的名称。JTable类使用此方法为其每一列加上标签。

public String getColumnName(int column) {
    try {
        return this.metadata.getColumnLabel(column + 1);
    } catch (SQLException e) {
        return e.toString();
    }
}

实现 getColumnAt

getColumnAt方法检索行集coffeesRowSet中指定行和列的值。JTable类使用此方法来填充其表格。请注意,SQL 从 1 开始对其行和列进行编号,但TableModel接口从 0 开始;这就是为什么rowIndexcolumnIndex的值要增加 1 的原因。

public Object getValueAt(int rowIndex, int columnIndex) {

    try {
        this.coffeesRowSet.absolute(rowIndex + 1);
        Object o = this.coffeesRowSet.getObject(columnIndex + 1);
        if (o == null)
            return null;
        else
            return o.toString();
    } catch (SQLException e) {
        return e.toString();
    }
}

实现 isCellEditable

因为此示例不允许用户直接编辑表的内容(行是由另一个窗口控件添加的),所以无论rowIndexcolumnIndex的值如何,此方法都返回false

public boolean isCellEditable(int rowIndex, int columnIndex) {
    return false;
}

实现 javax.sql.RowSetListener

CoffeesFrame仅实现接口RowSetListener中的一个方法rowChanged。当用户向表中添加行时,将调用此方法。

public void rowChanged(RowSetEvent event) {

    CachedRowSet currentRowSet =
        this.myCoffeesTableModel.coffeesRowSet;

    try {
        currentRowSet.moveToCurrentRow();
        myCoffeesTableModel = new CoffeesTableModel(
            myCoffeesTableModel.getCoffeesRowSet());
        table.setModel(myCoffeesTableModel);

    } catch (SQLException ex) {

        JDBCTutorialUtilities.printSQLException(ex);

        // Display the error in a dialog box.

        JOptionPane.showMessageDialog(
            CoffeesFrame.this,
            new String[] {
                // Display a 2-line message
                ex.getClass().getName() + ": ",
                ex.getMessage()
            }
        );
    }
}

此方法更新 GUI 应用程序中的表格。

布置 Swing 组件

CoffeesFrame的构造函数初始化并布置 Swing 组件。以下语句检索COFFEES表的内容,将内容存储在CachedRowSet对象myCachedRowSet中,并初始化JTable Swing 组件:

CachedRowSet myCachedRowSet = getContentsOfCoffeesTable();
myCoffeesTableModel = new CoffeesTableModel(myCachedRowSet);
myCoffeesTableModel.addEventHandlersToRowSet(this);

// Displays the table   
table = new JTable(); 
table.setModel(myCoffeesTableModel);

如前所述,此示例使用RowSet对象(特别是CachedRowSet对象)而不是ResultSet对象来表示COFFEES表的内容。

方法CoffeesFrame.getContentsOfCoffeesTable检索表COFFEES的内容。

方法CoffeesTableModel.addEventHandlersToRowSet将在CoffeesFrame类中定义的事件处理程序添加到行集成员变量CoffeesTableModel.coffeesRowSet中。这使得CoffeesFrame类能够通知行集coffeesRowSet任何事件,特别是当用户点击按钮Add row to tableUpdate databaseDiscard changes时。当行集coffeesRowSet被通知到这些变化之一时,方法CoffeesFrame.rowChanged被调用。

语句table.setModel(myCoffeesTableModel)指定使用CoffeesTableModel对象myCoffeesTableModel来填充JTable Swing 组件table

以下语句指定CoffeesFrame类使用布局GridBagLayout来布置其 Swing 组件:

Container contentPane = getContentPane();
contentPane.setComponentOrientation(
    ComponentOrientation.LEFT_TO_RIGHT);
contentPane.setLayout(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints();

有关使用布局GridBagLayout的更多信息,请参见如何使用 GridBagLayout 中的 JFC/Swing 创建 GUI。

查看CoffeesFrame.java的源代码,了解如何将此示例的 Swing 组件添加到布局GridBagLayout中。

为按钮添加监听器

以下语句向按钮Add row to table添加了一个监听器:

button_ADD_ROW.addActionListener(
    new ActionListener() {

    public void actionPerformed(ActionEvent e) {

        JOptionPane.showMessageDialog(
            CoffeesFrame.this, new String[] {
                "Adding the following row:",
                "Coffee name: [" +
                textField_COF_NAME.getText() +
                "]",
                "Supplier ID: [" +
                textField_SUP_ID.getText() + "]",
                "Price: [" +
                textField_PRICE.getText() + "]",
                "Sales: [" +
                textField_SALES.getText() + "]",
                "Total: [" +
                textField_TOTAL.getText() + "]"
            }
        );

        try {
            myCoffeesTableModel.insertRow(
                textField_COF_NAME.getText(),
                Integer.parseInt(textField_SUP_ID.getText().trim()),
                Float.parseFloat(textField_PRICE.getText().trim()),
                Integer.parseInt(textField_SALES.getText().trim()),
                Integer.parseInt(textField_TOTAL.getText().trim())
            );
        } catch (SQLException sqle) {
            displaySQLExceptionDialog(sqle);
        }
    }
});

当用户点击此按钮时,它执行以下操作:

  • 创建一个消息对话框,显示要添加到表中的行。

  • 调用方法CoffeesTableModel.insertRow,将行添加到成员变量CoffeesTableModel.coffeesRowSet中。

如果抛出SQLException,则方法CoffeesFrame.displaySQLExceptionDialog将创建一个消息对话框,显示SQLException的内容。

以下语句向按钮更新数据库添加了一个监听器:

button_UPDATE_DATABASE.addActionListener(
    new ActionListener() {
        public void actionPerformed(ActionEvent e) {
            try {
                myCoffeesTableModel.coffeesRowSet.acceptChanges();
                msgline.setText("Updated database");
            } catch (SQLException sqle) {
                displaySQLExceptionDialog(sqle);
                // Now revert back changes
                try {
                    createNewTableModel();
                    msgline.setText("Discarded changes");
                } catch (SQLException sqle2) {
                    displaySQLExceptionDialog(sqle2);
                }
            }
        }
    }
);

当用户点击此按钮时,将使用行集myCoffeesTableModel.coffeesRowSet的内容更新表COFFEES

以下语句向按钮放弃更改添加了一个监听器:

button_DISCARD_CHANGES.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent e) {
        try {
            createNewTableModel();
        } catch (SQLException sqle) {
            displaySQLExceptionDialog(sqle);
        }
    }
});

当用户点击此按钮时,将调用方法CoffeesFrame.createNewTableModel,该方法重新填充JTable组件,其中包含COFFEES表的内容。

路线:Java 管理扩展(JMX)

原文:docs.oracle.com/javase/tutorial/jmx/index.html

Java 管理扩展(JMX) 路线介绍了包含在 Java 平台标准版(Java SE 平台)中的 JMX 技术,通过示例展示了如何使用 JMX 技术的最重要特性。

提供了 JMX 技术的简要描述,包括其目的和主要特点。

介绍了 JMX 技术的基本概念,管理 bean,又称为 MBeans。本课程还介绍了 MXBeans。

介绍了 JMX 技术的通知机制。

展示了如何实现 JMX API 的远程管理能力以及如何创建 JMX 客户端应用程序。

trail icon 接下来怎么做 提供了指向更高级文档的指针,描述了 JMX 技术。

教训:JMX 技术概述

原文:docs.oracle.com/javase/tutorial/jmx/overview/index.html

Java 管理扩展(JMX)技术是 Java 平台标准版(Java SE 平台)的标准部分。JMX 技术是在 Java 2 平台标准版(J2SE)5.0 发布中添加到平台中的。

JMX 技术提供了一种简单、标准的管理资源(如应用程序、设备和服务)的方式。由于 JMX 技术是动态的,您可以使用它来监视和管理资源的创建、安装和实施过程。您还可以使用 JMX 技术来监视和管理 Java 虚拟机(Java VM)。

JMX 规范为管理和监视应用程序和网络定义了 Java 编程语言中的架构、设计模式、API 和服务。

使用 JMX 技术,一个给定的资源由一个或多个称为托管 BeanMBeans的 Java 对象仪器化。这些 MBeans 注册在一个核心管理对象服务器中,称为MBean 服务器。MBean 服务器充当管理代理,并可以在大多数已启用 Java 编程语言的设备上运行。

规范定义了您用于管理已正确配置的任何资源的 JMX 代理。JMX 代理由一个 MBean 服务器(其中注册了 MBeans)和一组用于处理 MBeans 的服务组成。通过这种方式,JMX 代理直接控制资源并使其可供远程管理应用程序使用。

资源的仪器化方式与管理基础设施完全独立。因此,资源可以被管理,而不管它们的管理应用是如何实现的。

JMX 技术定义了标准连接器(称为 JMX 连接器),使您可以从远程管理应用程序访问 JMX 代理。使用不同协议的 JMX 连接器提供相同的管理接口。因此,管理应用程序可以透明地管理资源,而不管使用的通信协议是什么。只要这些系统或应用程序支持 JMX 代理,JMX 代理也可以被不符合 JMX 规范的系统或应用程序使用。

为什么使用 JMX 技术?

原文:docs.oracle.com/javase/tutorial/jmx/overview/why.html

JMX 技术为开发人员提供了一种灵活的手段来为基于 Java 技术的应用程序(Java 应用程序)提供仪器化,创建智能代理,实现分布式管理中间件和管理器,并将这些解决方案顺利集成到现有的管理和监控系统中。

  • JMX 技术使得 Java 应用程序可以在不需要大量投资的情况下进行管理

    基于 JMX 技术的代理(JMX 代理)可以在大多数支持 Java 技术的设备上运行。因此,Java 应用程序可以在设计上几乎没有影响地变得可管理。一个 Java 应用程序只需要嵌入一个托管对象服务器,并将部分功能作为一个或多个托管 bean(MBeans)注册到对象服务器中。这就是从管理基础设施中受益所需的全部。

  • JMX 技术提供了一种标准的方式来管理 Java 应用程序、系统和网络

    例如,Java 平台企业版(Java EE)5 应用服务器符合 JMX 架构,因此可以使用 JMX 技术进行管理。

  • JMX 技术可用于对 Java 虚拟机进行开箱即用的管理

    Java 虚拟机(Java VM)使用 JMX 技术进行高度仪器化。您可以启动一个 JMX 代理来访问内置的 Java VM 仪器,从而远程监视和管理 Java VM。

  • JMX 技术提供了一个可扩展的、动态的管理架构

    每个 JMX 代理服务都是一个独立的模块,可以根据需求插入到管理代理中。这种基于组件的方法意味着 JMX 解决方案可以从小型设备扩展到大型电信交换机等更大的设备。JMX 规范提供了一组核心代理服务。可以开发额外的服务,并在管理基础设施中动态加载、卸载或更新这些服务。

  • JMX 技术利用了现有的标准 Java 技术

    在需要时,JMX 规范引用现有的 Java 规范,例如 Java 命名和目录接口(J.N.D.I.)API。

  • 基于 JMX 技术的应用程序(JMX 应用程序)可以通过 NetBeans IDE 模块创建

    您可以从 NetBeans 更新中心获取一个模块(在 NetBeans 界面中选择工具 -> 更新中心),该模块使您可以使用 NetBeans IDE 创建 JMX 应用程序。这降低了 JMX 应用程序开发的成本。

  • JMX 技术与现有的管理解决方案和新兴技术集成

    JMX API 是任何管理系统供应商都可以实现的开放接口。JMX 解决方案可以使用查找和发现服务以及诸如 Jini 网络技术和服务位置协议(SLP)等协议。

JMX 技术的架构

原文:docs.oracle.com/javase/tutorial/jmx/overview/architecture.html

JMX 技术可以分为三个级别,如下:

  • 仪器化

  • JMX 代理

  • 远程管理

仪器化

要使用 JMX 技术管理资源,必须首先使用 Java 编程语言对资源进行仪器化。您使用称为 MBeans 的 Java 对象来实现对资源仪器化的访问。MBeans 必须遵循 JMX 规范中定义的设计模式和接口。这样做可以确保所有 MBeans 以标准化的方式提供受管理资源的仪器化。除了标准 MBeans 外,JMX 规范还定义了一种特殊类型的 MBean,称为 MXBean。MXBean 是仅引用预定义数据类型的 MBean。还有其他类型的 MBean,但本文将集中讨论标准 MBeans 和 MXBeans。

一旦资源被 MBeans 仪器化,就可以通过 JMX 代理进行管理。MBeans 不需要了解它们将操作的 JMX 代理。

MBeans 被设计为灵活、简单且易于实现。应用程序、系统和网络的开发人员可以使其产品以标准方式可管理,而无需了解或投资于复杂的管理系统。现有资源可以通过最小的努力变得可管理。

此外,JMX 规范的仪器化级别提供了通知机制。该机制使 MBeans 能够生成和传播通知事件给其他级别的组件。

JMX 代理

基于 JMX 技术的代理(JMX 代理)是一个标准的管理代理,直接控制资源并使其可供远程管理应用程序使用。JMX 代理通常位于控制资源的同一台机器上,但这并不是必需的。

JMX 代理的核心组件是MBean 服务器,一个注册 MBeans 的受管理对象服务器。JMX 代理还包括一组服务来管理 MBeans,并至少一个通信适配器或连接器,以允许管理应用程序访问。

当您实现一个 JMX 代理时,不需要了解它将管理的资源的语义或功能。事实上,JMX 代理甚至不需要知道它将服务的资源,因为任何按照 JMX 规范进行仪器化的资源都可以使用任何提供资源所需服务的 JMX 代理。同样,JMX 代理也不需要知道将访问它的管理应用程序的功能。

远程管理

JMX 技术仪器化可以通过许多不同的方式访问,可以通过现有的管理协议,如简单网络管理协议(SNMP)或专有协议来访问。 MBean 服务器依赖于协议适配器和连接器,使得 JMX 代理可以从代理的 Java 虚拟机(Java VM)外部的管理应用程序访问。

每个适配器通过特定协议提供了在 MBean 服务器中注册的所有 MBeans 的视图。例如,HTML 适配器可以在浏览器中显示一个 MBean。

连接器提供了一个管理端接口,处理管理者和 JMX 代理之间的通信。每个连接器通过不同的协议提供相同的远程管理接口。当远程管理应用程序使用此接口时,它可以通过网络透明地连接到 JMX 代理,而不受协议的限制。 JMX 技术提供了一种标准解决方案,用于基于 Java 远程方法调用(Java RMI)将 JMX 技术仪器化导出到远程应用程序。

监控和管理 Java 虚拟机

原文:docs.oracle.com/javase/tutorial/jmx/overview/javavm.html

JMX 技术也可以用于监控和管理 Java 虚拟机(Java VM)。

Java VM 具有内置的仪器,使您可以使用 JMX 技术监控和管理它。这些内置的管理实用程序通常被称为 Java VM 的开箱即用管理工具。为了监控和管理 Java VM 的不同方面,Java VM 包括一个平台 MBean 服务器和专门的 MXBeans,供符合 JMX 规范的管理应用程序使用。

平台 MXBeans 和平台 MBean 服务器

平台 MXBeans是 Java SE 平台提供的一组 MXBeans,用于监控和管理 Java VM 和 Java 运行时环境(JRE)的其他组件。每个平台 MXBean 封装了 Java VM 功能的一部分,例如类加载系统、即时(JIT)编译系统、垃圾收集器等。这些 MXBeans 可以通过符合 JMX 规范的监控和管理工具显示和交互,以便您监控和管理这些不同的 VM 功能。其中一种监控和管理工具是 Java SE 平台的 JConsole 图形用户界面(GUI)。

Java SE 平台提供了一个标准的平台 MBean 服务器,其中注册了这些平台 MXBeans。平台 MBean 服务器还可以注册您希望创建的任何其他 MBeans。

JConsole

Java SE 平台包括符合 JMX 规范的 JConsole 监控和管理工具。JConsole 使用 Java VM 的广泛仪器(平台 MXBeans)提供关于在 Java 平台上运行的应用程序的性能和资源消耗的信息。

开箱即用的管理实用程序在操作中

因为标准的实现 JMX 技术的监控和管理实用程序内置于 Java SE 平台中,您可以在不编写一行 JMX API 代码的情况下看到开箱即用的 JMX 技术在操作中。您可以通过启动 Java 应用程序,然后使用 JConsole 监控它来实现。

使用 JConsole 监控应用程序

本过程展示了如何监控记事本 Java 应用程序。在 Java SE 平台版本 6 之前的版本中,您需要使用以下选项启动要使用 JConsole 监控的应用程序。

-Dcom.sun.management.jmxremote

然而,Java SE 6 平台提供的 JConsole 版本可以连接到支持 Attach API 的任何本地应用程序。换句话说,任何在 Java SE 6 HotSpot VM 中启动的应用程序都会被 JConsole 自动检测到,不需要使用上述命令行选项启动。

  1. 启动记事本 Java 应用程序,使用以下命令在终端窗口中:

    java -jar 
        *jdk_home*/demo/jfc/Notepad/Notepad.jar
    
    

    其中jdk_home是安装 Java 开发工具包(JDK)的目录。如果您没有运行 Java SE 平台的 6 版本,您将需要使用以下命令:

    java -Dcom.sun.management.jmxremote -jar 
          *jdk_home*/demo/jfc/Notepad/Notepad.jar
    
    
  2. 一旦 Notepad 打开,在另一个终端窗口中,使用以下命令启动 JConsole:

    jconsole
    
    

    会显示一个新的连接对话框。

  3. 在新连接对话框中,从本地进程列表中选择Notepad.jar,然后点击连接按钮。

    JConsole 会打开并连接到Notepad.jar进程。当 JConsole 打开时,您将看到与 Notepad 相关的监控和管理信息概览。例如,您可以查看应用程序消耗的堆内存量,应用程序当前运行的线程数,以及应用程序消耗的中央处理单元(CPU)容量。

  4. 点击不同的 JConsole 选项卡。

    每个选项卡提供了关于 Notepad 运行的 Java 虚拟机不同功能区域的更详细信息。所有呈现的信息都是从本教程中提到的各种 JMX 技术 MXBeans 获取的。所有平台 MXBeans 都可以在 MBeans 选项卡中显示。MBeans 选项卡将在本教程的下一部分中进行讨论。

  5. 要关闭 JConsole,选择连接 -> 退出。

教程:介绍 MBeans

原文:docs.oracle.com/javase/tutorial/jmx/mbeans/index.html

本课程介绍了 JMX API 的基本概念,即受管理的 bean,或MBeans

一个 MBean 是一个受管理的 Java 对象,类似于 JavaBeans 组件,遵循 JMX 规范中设定的设计模式。一个 MBean 可以代表一个设备、一个应用程序或任何需要被管理的资源。MBeans 公开一个由以下内容组成的管理接口:

  • 一组可读或可写的属性,或两者兼有。

  • 一组可调用的操作。

  • 自我描述。

管理接口在 MBean 实例的整个生命周期中不会改变。当发生某些预定义事件时,MBeans 也可以发出通知。

JMX 规范定义了五种类型的 MBean:

  • 标准 MBeans

  • 动态 MBeans

  • 开放 MBeans

  • 模型 MBeans

  • MXBeans

本教程中的示例仅演示了最简单的 MBean 类型,即标准 MBeans 和 MXBeans。

标准 MBeans

原文:docs.oracle.com/javase/tutorial/jmx/mbeans/standard.html

本节介绍了一个简单的标准 MBean 示例。

通过编写一个名为 SomethingMBean 的 Java 接口和一个实现该接口的名为 Something 的 Java 类来定义标准 MBean。接口中的每个方法默认定义一个操作。属性和操作是遵循特定设计模式的方法。标准 MBean 由一个 MBean 接口和一个类组成。MBean 接口列出了所有公开属性和操作的方法。类实现此接口并提供受监视资源的功能。

以下部分将介绍一个标准 MBean 和一个简单的支持 JMX 技术的代理(JMX 代理)管理该 MBean。

MBean 接口

一个基本 MBean 接口的示例,HelloMBean,如下所示:


package com.example; 

public interface HelloMBean { 

    public void sayHello(); 
    public int add(int x, int y); 

    public String getName(); 

    public int getCacheSize(); 
    public void setCacheSize(int size); 
} 

按照惯例,一个 MBean 接口采用实现它的 Java 类的名称,后缀 MBean 添加在后面。在这种情况下,接口被称为 HelloMBean。实现此接口的 Hello 类将在下一节中描述。

根据 JMX 规范,一个 MBean 接口由具有名称和类型的属性组成,这些属性可读取并可能可写,另外还包括由 MBean 管理的应用程序可以调用的具有名称和类型的操作。HelloMBean 接口声明了两个操作:Java 方法 add()sayHello()

HelloMBean 声明了两个属性:Name 是一个只读字符串,CacheSize 是一个既可读又可写的整数。声明了 getter 和 setter 方法,允许受管应用程序访问并可能更改属性值。根据 JMX 规范的定义,getter 是任何不返回 void 并且名称以 get 开头的公共方法。getter 使管理者能够读取属性的值,其类型与返回对象的类型相同。setter 是任何接受单个参数并且名称以 set 开头的公共方法。setter 使管理者能够在属性中写入新值,其类型与参数的类型相同。

这些操作和属性的实现在下一节中展示。

MBean 实现

下面的 Hello Java 类实现了 HelloMBean MBean 接口:

package com.example; 

public class Hello ... 
    implements HelloMBean { 
    public void sayHello() { 
        System.out.println("hello, world"); 
    } 

    public int add(int x, int y) { 
        return x + y; 
    } 

    public String getName() { 
        return this.name; 
    }  

    public int getCacheSize() { 
        return this.cacheSize; 
    } 

    public synchronized void setCacheSize(int size) {
        ...

        this.cacheSize = size; 
        System.out.println("Cache size now " + this.cacheSize); 
    } 
    ...

    private final String name = "Reginald"; 
    private int cacheSize = DEFAULT_CACHE_SIZE; 
    private static final int 
        DEFAULT_CACHE_SIZE = 200; 
}

直接的 Hello 类提供了由 HelloMBean 声明的操作和属性的定义。sayHello()add() 操作非常简单,但实际操作可以根据需要简单或复杂。

还定义了获取Name属性和获取/设置CacheSize属性的方法。在此示例中,Name属性值永远不会改变。但是,在实际情况下,此属性可能随着受管资源的运行而发生变化。例如,该属性可能代表诸如正常运行时间或内存使用情况之类的统计信息。在这里,该属性仅仅是名称Reginald

调用setCacheSize方法可以修改CacheSize属性,将其从声明的默认值 200 改变。在实际情况下,更改CacheSize属性可能需要执行其他操作,例如丢弃条目或分配新条目。此示例仅仅打印一条消息以确认缓存大小已更改。但是,可以定义更复杂的操作,而不是简单调用println()

有了Hello MBean 及其接口的定义,它们现在可以用来管理它们所代表的资源,如下一节所示。

创建一个用于管理资源的 JMX 代理

一旦资源被 MBeans 进行了仪器化,该资源的管理就由 JMX 代理执行。

JMX 代理的核心组件是 MBean 服务器。MBean 服务器是一个托管对象服务器,其中注册了 MBeans。JMX 代理还包括一组服务来管理 MBeans。有关 MBean 服务器实现的详细信息,请参阅MBeanServer接口的 API 文档。

接下来的Main类代表了一个基本的 JMX 代理:

package com.example; 

import java.lang.management.*; 
import javax.management.*; 

public class Main { 

    public static void main(String[] args) 
        throws Exception { 

        MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); 
        ObjectName name = new ObjectName("com.example:type=Hello"); 
        Hello mbean = new Hello(); 
        mbs.registerMBean(mbean, name); 

        ...

        System.out.println("Waiting forever..."); 
        Thread.sleep(Long.MAX_VALUE); 
    } 
} 

JMX 代理Main首先通过调用java.lang.management.ManagementFactory类的getPlatformMBeanServer()方法来获取由平台创建和初始化的 MBean 服务器。如果平台尚未创建 MBean 服务器,则getPlatformMBeanServer()会通过调用 JMX 方法MBeanServerFactory.createMBeanServer()自动创建 MBean 服务器。Main获取的MBeanServer实例命名为mbs

接下来,Main为将要创建的 MBean 实例定义了一个对象名称。每个 JMX MBean 都必须有一个对象名称。对象名称是 JMX 类ObjectName的一个实例,并且必须符合 JMX 规范定义的语法。换句话说,对象名称必须包含一个域和一组键-属性。在Main定义的对象名称中,域是com.example(示例 MBean 所在的包)。此外,键-属性声明此对象是Hello类型。

创建了一个名为mbeanHello对象实例。然后,通过将对象和对象名称传递给 JMX 方法MBeanServer.registerMBean(),将名为mbeanHello对象注册为 MBean 在 MBean 服务器mbs中。

在 MBean 服务器中注册了Hello MBean 后,Main只是等待对Hello执行管理操作。在这个示例中,这些管理操作包括调用sayHello()add(),以及获取和设置属性值。

运行标准 MBean 示例

在检查了示例类之后,现在可以运行示例了。在这个示例中,使用 JConsole 与 MBean 交互。

要运行示例,请按照以下步骤操作:

  1. 将 JMX API 示例类的包jmx_examples.zip保存到你的工作目录work_dir

  2. 使用以下命令在终端窗口中解压示例类的包。

    unzip jmx_examples.zip
    
    
  3. work_dir目录中编译示例 Java 类。

    javac com/example/*.java
    
    
  4. 如果你正在运行 Java 开发工具包(JDK)版本 6,请使用以下命令启动Main应用程序。

    java com.example.Main
    
    

    如果你使用的 JDK 版本低于 6,你需要使用以下选项启动Main应用程序,以便监控和管理应用程序。

    java -Dcom.sun.management.jmxremote example.Main
    
    

    显示Main正在等待某些事件发生的确认。

  5. 在同一台机器的不同终端窗口中启动 JConsole。

    jconsole
    
    

    显示新连接对话框,列出了可以连接的正在运行的 JMX 代理的列表。

  6. 在新连接对话框中,从列表中选择com.example.Main,然后点击连接。

    显示您平台当前活动的摘要。

  7. 点击 MBeans 选项卡。

    此面板显示当前在 MBean 服务器中注册的所有 MBean。

  8. 在左侧框架中,展开 MBean 树中的com.example节点。

    你会看到由Main创建和注册的示例 MBean Hello。如果你点击Hello,你会在 MBean 树中看到其关联的属性和操作节点。

  9. 展开 MBean 树中Hello MBean 的属性节点。

    显示由Hello类定义的 MBean 属性。

  10. CacheSize属性的值更改为 150。

    在你启动Main的终端窗口中,会生成对属性更改的确认。

  11. 展开 MBean 树中Hello MBean 的操作节点。

    Hello MBean 声明的两个操作,sayHello()add(),是可见的。

  12. 通过点击sayHello按钮调用sayHello()操作。

    一个 JConsole 对话框通知您方法已成功调用。消息“hello, world”会在运行Main的终端窗口中生成。

  13. add()操作提供两个整数相加,然后点击add按钮。

    答案显示在一个 JConsole 对话框中。

  14. 要关闭 JConsole,请选择 Connection -> Exit。

MXBeans

原文:docs.oracle.com/javase/tutorial/jmx/mbeans/mxbeans.html

本节介绍了一种特殊类型的 MBean,称为 MXBeans

MXBean 是一种只引用预定义数据类型的 MBean 类型。通过这种方式,您可以确保您的 MBean 可以被任何客户端使用,包括远程客户端,而无需客户端访问表示您的 MBeans 类型的特定模型类。MXBeans 提供了一种方便的方式将相关值捆绑在一起,而无需客户端特别配置以处理这些捆绑。

与标准 MBeans 一样,MXBean 是通过编写一个名为 SomethingMXBean 的 Java 接口和实现该接口的 Java 类来定义的。但是,与标准 MBeans 不同,MXBeans 不要求 Java 类的名称为 Something。接口中的每个方法都定义了 MXBean 中的属性或操作。注解 @MXBean 也可以用于注解 Java 接口,而不需要接口的名称后跟 MXBean 后缀。

MXBeans 存在于 Java 2 Platform, Standard Edition (J2SE) 5.0 软件中,位于 java.lang.management 包中。然而,用户现在可以定义自己的 MXBeans,除了在 java.lang.management 中定义的标准集之外。

MXBeans 的主要思想是,例如在 MXBean 接口中引用的 java.lang.management.MemoryUsage 这样的类型,在本例中是 java.lang.management.MemoryMXBean,被映射到一组标准类型,即所谓的 Open Types,这些类型在 javax.management.openmbean 包中定义。确切的映射规则出现在 MXBean 规范中。然而,一般原则是简单类型如 int 或 String 保持不变,而复杂类型如 MemoryUsage 被映射为标准类型 CompositeDataSupport

MXBean 示例包括以下文件,这些文件位于 jmx_examples.zip 中:

  • QueueSamplerMXBean 接口

  • 实现 MXBean 接口的 QueueSampler

  • QueueSample 是由 MXBean 接口中的 getQueueSample() 方法返回的 Java 类型

  • Main,设置并运行示例的程序

MXBean 示例使用这些类执行以下操作:

  • 定义了一个管理 Queue<String> 类型资源的简单 MXBean

  • 在 MXBean 中声明一个 getter,getQueueSample,当调用时获取队列的快照并返回一个捆绑以下值的 Java 类 QueueSample

    • 获取快照的时间

    • 队列大小

    • 给定时间的队列头

  • 在 MBean 服务器中注册 MXBean

MXBean 接口

以下代码显示了示例 QueueSamplerMXBean MXBean 接口:

package com.example; 

public interface QueueSamplerMXBean { 
    public QueueSample getQueueSample(); 
    public void clearQueue(); 
} 

请注意,声明 MXBean 接口的方式与声明标准 MBean 接口的方式完全相同。QueueSamplerMXBean 接口声明了一个 getter,getQueueSample 和一个操作,clearQueue

定义 MXBean 操作

MXBean 操作在 QueueSampler 示例类中声明如下:

package com.example; 

import java.util.Date; 
import java.util.Queue; 

public class QueueSampler 
                implements QueueSamplerMXBean { 

    private Queue<String> queue; 

    public QueueSampler (Queue<String> queue) { 
        this.queue = queue; 
    } 

    public QueueSample getQueueSample() { 
        synchronized (queue) { 
            return new QueueSample(new Date(), 
                           queue.size(), queue.peek()); 
        } 
    } 

    public void clearQueue() { 
        synchronized (queue) { 
            queue.clear(); 
        } 
    } 
} 

QueueSampler 定义了由 MXBean 接口声明的 getQueueSample() getter 和 clearQueue() 操作。getQueueSample() 操作返回一个 QueueSample Java 类型的实例,该实例是使用 java.util.Queue 方法 peek()size() 返回的值以及 java.util.Date 的实例创建的。

定义 MXBean 接口返回的 Java 类型

QueueSampler 返回的 QueueSample 实例在 QueueSample 类中定义如下:

package com.example; 

import java.beans.ConstructorProperties; 
import java.util.Date; 

public class QueueSample { 

    private final Date date; 
    private final int size; 
    private final String head; 

    @ConstructorProperties({"date", "size", "head"}) 
    public QueueSample(Date date, int size, 
                        String head) { 
        this.date = date; 
        this.size = size; 
        this.head = head; 
    } 

    public Date getDate() { 
        return date; 
    } 

    public int getSize() { 
        return size; 
    } 

    public String getHead() { 
        return head; 
    } 
}   

QueueSample 类中,MXBean 框架调用 QueueSample 中的所有 getter 将给定实例转换为一个 CompositeData 实例,并使用 @ConstructorProperties 注解从 CompositeData 实例重建一个 QueueSample 实例。

创建并在 MBean 服务器中注册 MXBean

到目前为止,已经定义了以下内容:一个 MXBean 接口和实现它的类,以及返回的 Java 类型。接下来,MXBean 必须在 MBean 服务器中创建并注册。这些操作由相同的 Main 示例 JMX 代理执行,该代理在标准 MBean 示例中使用,但相关代码未在 标准 MBean 课程中显示。


package com.example; 

import java.lang.management.ManagementFactory; 
import java.util.Queue; 
import java.util.concurrent.ArrayBlockingQueue; 
import javax.management.MBeanServer; 
import javax.management.ObjectName; 

public class Main { 

    public static void main(String[] args) throws Exception { 
        MBeanServer mbs = 
            ManagementFactory.getPlatformMBeanServer(); 

        ...  
        ObjectName mxbeanName = new ObjectName("com.example:type=QueueSampler");

        Queue<String> queue = new ArrayBlockingQueue<String>(10);
        queue.add("Request-1");
        queue.add("Request-2");
        queue.add("Request-3");
        QueueSampler mxbean = new QueueSampler(queue);

        mbs.registerMBean(mxbean, mxbeanName);

        System.out.println("Waiting..."); 
        Thread.sleep(Long.MAX_VALUE); 
    } 
} 

Main 类执行以下操作:

  • 获取平台 MBean 服务器。

  • 为 MXBean QueueSampler 创建对象名称。

  • QueueSampler MXBean 创建一个 Queue 实例以进行处理。

  • Queue 实例提供给新创建的 QueueSampler MXBean。

  • 以与标准 MBean 完全相同的方式在 MBean 服务器中注册 MXBean。

运行 MXBean 示例

MXBean 示例使用了您在 标准 MBeans 部分中使用的 jmx_examples.zip 包中的类。此示例需要 Java SE 平台的第 6 版本。要运行 MXBeans 示例,请按照以下步骤操作:

  1. 如果尚未这样做,请将jmx_examples.zip保存到work_dir目录中。

  2. 在终端窗口中使用以下命令解压示例类的捆绑包。

    unzip jmx_examples.zip
    
    
  3. work_dir目录中编译示例 Java 类。

    javac com/example/*.java
    
    
  4. 启动Main应用程序。生成一个确认信息,表示Main正在等待某些事件发生。

    java com.example.Main
    
    
  5. 在同一台机器的不同终端窗口中启动 JConsole。显示新连接对话框,展示可以连接的运行中的 JMX 代理列表。

    jconsole
    
    
  6. 在新连接对话框中,从列表中选择com.example.Main并点击连接。

    显示平台当前活动的摘要。

  7. 点击 MBeans 选项卡。

    此面板显示当前在 MBean 服务器中注册的所有 MBeans。

  8. 在左侧框架中,展开 MBean 树中的com.example节点。

    你会看到示例 MBean QueueSampler是由Main创建和注册的。如果你点击QueueSampler,你会在 MBean 树中看到其关联的属性和操作节点。

  9. 展开属性节点。

    你会看到QueueSample属性出现在右侧窗格中,其值为javax.management.openmbean.CompositeDataSupport

  10. 双击CompositeDataSupport值。

    你会看到QueueSampledateheadsize,因为 MXBean 框架已将QueueSample实例转换为CompositeData。如果你将QueueSampler定义为标准 MBean 而不是 MXBean,JConsole 将找不到QueueSample类,因为它不在其类路径中。如果QueueSampler是标准 MBean,当检索QueueSample属性值时,你会收到ClassNotFoundException消息。JConsole 找到QueueSampler这一事实展示了在通过通用 JMX 客户端(如 JConsole)连接到 JMX 代理时使用 MXBeans 的实用性。

  11. 展开操作节点。

    显示一个按钮来调用clearQueue操作。

  12. 点击clearQueue按钮。

    显示成功调用方法的确认信息。

  13. 再次展开属性节点,并双击CompositeDataSupport值。

    headsize值已重置。

  14. 要关闭 JConsole,选择连接 -> 退出。

教训:通知

原文:docs.oracle.com/javase/tutorial/jmx/notifs/index.html

JMX API 定义了一种机制,使 MBeans 能够生成通知,例如,用于表示状态变化、检测到的事件或问题。

要生成通知,MBean 必须实现接口NotificationEmitter或扩展NotificationBroadcasterSupport。要发送通知,您需要构造类javax.management.Notification或其子类(例如AttributeChangedNotification)的实例,并将实例传递给NotificationBroadcasterSupport.sendNotification

每个通知都有一个来源。来源是生成通知的 MBean 的对象名称。

每个通知都有一个序列号。当顺序很重要且存在通知被错误处理的风险时,可以使用此编号来对来自同一来源的通知进行排序。序列号可以为零,但最好对于来自给定 MBean 的每个通知递增。

Hello MBean 在标准 MBeans 中实际上实现了通知机制。但是,出于简单起见,该课程中省略了此代码。Hello的完整代码如下:

package com.example;

import javax.management.*;

public class Hello
        extends NotificationBroadcasterSupport
        implements HelloMBean {

    public void sayHello() {
        System.out.println("hello, world");
    }

    public int add(int x, int y) {
        return x + y;
    }

    public String getName() {
        return this.name;
    }

    public int getCacheSize() {
        return this.cacheSize;
    }

    public synchronized void setCacheSize(int size) {
        int oldSize = this.cacheSize;
        this.cacheSize = size;

        System.out.println("Cache size now " + this.cacheSize);

        Notification n = new AttributeChangeNotification(this,
                                sequenceNumber++, System.currentTimeMillis(),
                                "CacheSize changed", "CacheSize", "int",
                                oldSize, this.cacheSize);

        sendNotification(n);
    }

    @Override
    public MBeanNotificationInfo[] getNotificationInfo() {
        String[] types = new String[]{
            AttributeChangeNotification.ATTRIBUTE_CHANGE
        };

        String name = AttributeChangeNotification.class.getName();
        String description = "An attribute of this MBean has changed";
        MBeanNotificationInfo info = 
                new MBeanNotificationInfo(types, name, description);
        return new MBeanNotificationInfo[]{info};
    }

    private final String name = "Reginald";
    private int cacheSize = DEFAULT_CACHE_SIZE;
    private static final int DEFAULT_CACHE_SIZE = 200;
    private long sequenceNumber = 1;
}

Hello MBean 实现扩展了NotificationBroadcasterSupport类。NotificationBroadcasterSupport实现了NotificationEmitter接口。

操作和属性的设置方式与标准 MBean 示例中的方式相同,唯一的例外是CacheSize属性的 setter 方法现在定义了一个值oldSize。此值记录了在设置操作之前CacheSize属性的值。

通知是从 JMX 类AttributeChangeNotification的实例n构造的,该类扩展了javax.management.Notification。通知是在setCacheSize()方法的定义中从以下信息构造的。这些信息作为参数传递给AttributeChangeNotification

  • 通知来源的对象名称,即Hello MBean,由this表示

  • 一个序列号,即sequenceNumber,设置为 1,并逐渐增加

  • 时间戳

  • 通知消息的内容

  • 已更改的属性名称,本例中为CacheSize

  • 已更改的属性类型

  • 旧属性值,在本例中为oldSize

  • 新属性值,在本例中为this.cacheSize

然后,通知n被传递给NotificationBroadcasterSupport.sendNotification()方法。

最后,MBeanNotificationInfo实例被定义,以描述 MBean 为给定类型的通知生成的不同通知实例的特征。在本例中,发送的通知类型是AttributeChangeNotification通知。

运行 MBean 通知示例

再次使用 JConsole 与Hello MBean 交互,这次是为了发送和接收通知。此示例需要 Java SE 平台的第 6 版。

  1. 如果尚未这样做,请将jmx_examples.zip保存到您的work_dir目录中。

  2. 在终端窗口中使用以下命令解压示例类的捆绑包。

    unzip jmx_examples.zip
    
    
  3. work_dir目录中编译示例 Java 类。

    javac com/example/*.java
    
    
  4. 启动Main应用程序。

    java com.example.Main
    
    

    生成一个确认,表示Main正在等待某些事件发生。

  5. 在同一台机器的不同终端窗口中启动 JConsole。

    jconsole
    
    

    显示新连接对话框,呈现可以连接的正在运行的 JMX 代理列表。

  6. 在新连接对话框中,从列表中选择com.example.Main并点击连接。

    显示您平台当前活动的摘要。

  7. 点击 MBeans 选项卡。

    此面板显示当前在 MBean 服务器中注册的所有 MBeans。

  8. 在左侧框架中,展开 MBean 树中的com.example节点。

    您会看到由Hello创建和注册的示例 MBeanHello。如果点击Hello,您会看到其在 MBean 树中的通知节点。

  9. 展开 MBean 树中Hello MBean 的通知节点。

    请注意,面板为空白。

  10. 点击订阅按钮。

    当前接收的通知数量(0)显示在通知节点标签中。

  11. 展开 MBean 树中Hello MBean 的属性节点,并将CacheSize属性的值更改为 150。

    在启动Main的终端窗口中,会显示对此属性更改的确认。请注意,显示在通知节点中的接收通知数量已更改为 1。

  12. 再次展开 MBean 树中Hello MBean 的通知节点。

    通知的详细信息将被显示。

  13. 要关闭 JConsole,请选择连接 -> 退出。

课程:远程管理

原文:docs.oracle.com/javase/tutorial/jmx/remote/index.html

JMX API 使您能够通过使用基于 JMX 技术的连接器(JMX 连接器)对资源进行远程管理。 JMX 连接器使 MBean 服务器对远程基于 Java 技术的客户端可访问。 连接器的客户端端口基本上导出与 MBean 服务器相同的接口。

JMX 连接器由连接器客户端和连接器服务器组成。 连接器服务器 附加到 MBean 服务器并监听来自客户端的连接请求。 连接器客户端 负责与连接器服务器建立连接。 连接器客户端通常位于与连接器服务器不同的 Java 虚拟机(Java VM)中,并且通常在不同的计算机上运行。 JMX API 定义了基于远程方法调用(RMI)的标准连接协议。 此协议使您能够从远程位置将 JMX 客户端连接到 MBean 服务器中的 MBean,并执行对 MBean 的操作,就像这些操作是在本地执行一样。

Java SE 平台提供了一种开箱即用的方式,通过使用 JMX API 的标准 RMI 连接器远程监视应用程序。 开箱即用的 RMI 连接器会自动将应用程序暴露给远程管理,而无需您自己创建专用的远程连接器服务器。 开箱即用的远程管理代理通过使用正确的属性启动 Java 应用程序来激活。 与 JMX 技术兼容的监视和管理应用程序然后可以连接到这些应用程序并远程监视它们。

通过 JConsole 将资源暴露给远程管理

原文:docs.oracle.com/javase/tutorial/jmx/remote/jconsole.html

使用 JMX API 将您的 Java 应用程序暴露给远程管理可以非常简单,如果您使用开箱即用的远程管理代理和现有的监控和管理工具,如 JConsole。

要将您的应用程序暴露给远程管理,您需要使用正确的属性启动它。此示例展示了如何将Main JMX 代理暴露给远程管理。


安全注意事项:

为简单起见,此示例中禁用了身份验证和加密安全机制。但是,在实际环境中实现远程管理时,您应该实现这些安全机制。下一步是什么? 提供了指向其他 JMX 技术文档的指针,显示如何激活安全性。


此示例需要 Java SE 平台的 6 版本。要远程监视Main JMX 代理,请按照以下步骤操作:

  1. 如果尚未这样做,请将jmx_examples.zip保存到您的work_dir目录中。

  2. 在终端窗口中使用以下命令解压示例类的捆绑包。

    unzip jmx_examples.zip
    
    
  3. work_dir目录中编译示例 Java 类。

    javac com/example/*.java
    
    
  4. 启动Main应用程序,指定暴露Main进行远程管理的属性。(对于 Windows,请使用插入符(^)而不是反斜杠(\)来将长命令分成多行):

    java -Dcom.sun.management.jmxremote.port=9999 \
         -Dcom.sun.management.jmxremote.authenticate=false \
         -Dcom.sun.management.jmxremote.ssl=false \
         com.example.Main
    
    

    生成一个等待某些事件发生的Main的确认。

  5. 不同的机器上的不同终端窗口中启动 JConsole:

    jconsole
    
    

    显示新连接对话框,显示可以在本地连接的正在运行的 JMX 代理列表。

  6. 选择远程进程,并在远程进程字段中输入以下内容:

    *hostname*:9999
    
    

    在此地址中,hostname是运行Main应用程序的远程机器的名称,9999 是将连接到开箱即用的 JMX 连接器的端口号。

  7. 点击连接。

    显示运行Main的 Java 虚拟机(Java VM)的当前活动摘要。

  8. 点击 MBeans 选项卡。

    此面板显示当前在远程 MBean 服务器中注册的所有 MBean。

  9. 在左侧框架中,在 MBean 树中展开com.example节点。

    您会看到由Main创建和注册的示例 MBean Hello。如果您点击Hello,即使它在不同的机器上运行,您也会看到其关联的属性和操作节点在 MBean 树中。

  10. 要关闭 JConsole,请选择连接 -> 退出。

创建自定义 JMX 客户端

原文:docs.oracle.com/javase/tutorial/jmx/remote/custom.html

本教程中的前几课已经向您展示了如何创建 JMX 技术的 MBeans 和 MXBeans,并将它们注册到 JMX 代理中。然而,所有之前的示例都使用了现有的 JMX 客户端 JConsole。本课将演示如何创建您自己的自定义 JMX 客户端。

一个自定义 JMX 客户端的示例,Client,包含在 jmx_examples.zip 中。这个 JMX 客户端与之前课程中看到的相同的 MBean、MXBean 和 JMX 代理进行交互。由于 Client 类的大小,将在以下部分中逐块进行检查。

导入 JMX 远程 API 类

要能够从 JMX 客户端远程连接到运行的 JMX 代理,您需要使用 javax.management.remote 中的类。

package com.example;
...

import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;

public class Client {
...

Client 类将创建 JMXConnector 实例,为此它将需要一个 JMXConnectorFactory 和一个 JMXServiceURL

创建一个通知监听器

JMX 客户端需要一个通知处理程序,以便监听并处理可能由注册在 JMX 代理的 MBean 服务器中的 MBeans 发送的任何通知。JMX 客户端的通知处理程序是 NotificationListener 接口的一个实例,如下所示。

... 

public static class ClientListener implements NotificationListener {

    public void handleNotification(Notification notification,
            Object handback) {
        echo("\nReceived notification:");
        echo("\tClassName: " + notification.getClass().getName());
        echo("\tSource: " + notification.getSource());
        echo("\tType: " + notification.getType());
        echo("\tMessage: " + notification.getMessage());
        if (notification instanceof AttributeChangeNotification) {
            AttributeChangeNotification acn =
                (AttributeChangeNotification) notification;
            echo("\tAttributeName: " + acn.getAttributeName());
            echo("\tAttributeType: " + acn.getAttributeType());
            echo("\tNewValue: " + acn.getNewValue());
            echo("\tOldValue: " + acn.getOldValue());
        }
    }
}    
...       

此通知监听器确定接收到的任何通知的来源,并检索通知中存储的信息。然后根据接收到的通知类型执行不同的操作。在这种情况下,当监听器接收到 AttributeChangeNotification 类型的通知时,它将通过调用 AttributeChangeNotification 方法 getAttributeNamegetAttributeTypegetNewValuegetOldValue 获取已更改的 MBean 属性的名称和类型,以及其旧值和新值。

代码稍后将创建一个新的 ClientListener 实例。


ClientListener listener = new ClientListener();

创建 RMI 连接器客户端

Client 类创建了一个 RMI 连接器客户端,配置为连接到在启动 JMX 代理 Main 时将要启动的 RMI 连接器服务器。这将允许 JMX 客户端与 JMX 代理进行交互,就好像它们在同一台机器上运行一样。

...

public static void main(String[] args) throws Exception {

echo("\nCreate an RMI connector client and " +
    "connect it to the RMI connector server");
JMXServiceURL url = 
    new JMXServiceURL("service:jmx:rmi:///jndi/rmi://:9999/jmxrmi");
JMXConnector jmxc = JMXConnectorFactory.connect(url, null);
...        

正如你所看到的,Client定义了一个名为urlJMXServiceURL,表示连接器客户端期望找到连接器服务器的位置。此 URL 允许连接器客户端从运行在本地主机端口 9999 上的 RMI 注册表中检索 RMI 连接器服务器存根jmxrmi,并连接到 RMI 连接器服务器。

识别了 RMI 注册表后,可以创建连接器客户端。连接器客户端jmxcJMXConnector接口的一个实例,通过JMXConnectorFactoryconnect()方法创建。在调用connect()方法时,传递了参数url和一个空的环境映射。

连接到远程 MBean 服务器

有了 RMI 连接,JMX 客户端必须连接到远程 MBean 服务器,以便可以通过远程 JMX 代理与其中注册的各种 MBeans 进行交互。

...

MBeanServerConnection mbsc = 
    jmxc.getMBeanServerConnection();

...     

通过调用JMXConnector实例jmxcgetMBeanServerConnection()方法,创建了一个名为MBeanServerConnection的实例,命名为 mbsc。

现在,连接器客户端已连接到由 JMX 代理创建的 MBean 服务器,并且可以注册 MBeans 并对其执行操作,连接对双方完全透明。

要开始,客户端定义了一些简单的操作,以发现代理的 MBean 服务器中找到的信息。

...

echo("\nDomains:");
String domains[] = mbsc.getDomains();
Arrays.sort(domains);
for (String domain : domains) {
    echo("\tDomain = " + domain);
}

...

echo("\nMBeanServer default domain = " + mbsc.getDefaultDomain());

echo("\nMBean count = " +  mbsc.getMBeanCount());
echo("\nQuery MBeanServer MBeans:");
Set<ObjectName> names = 
    new TreeSet<ObjectName>(mbsc.queryNames(null, null));
for (ObjectName name : names) {
    echo("\tObjectName = " + name);
}

...

客户端调用MBeanServerConnection的各种方法,以获取不同 MBeans 所在的域,MBean 服务器中注册的 MBeans 数量,以及它发现的每个 MBean 的对象名称。

通过代理执行远程 MBeans 上的操作

客户端通过创建一个 MBean 代理,通过 MBean 服务器连接访问 MBean 服务器中的Hello MBean。这个 MBean 代理是客户端本地的,并模拟了远程 MBean。


...

ObjectName mbeanName = new ObjectName("com.example:type=Hello");
HelloMBean mbeanProxy = JMX.newMBeanProxy(mbsc, mbeanName, 
                                          HelloMBean.class, true);

echo("\nAdd notification listener...");
mbsc.addNotificationListener(mbeanName, listener, null, null);

echo("\nCacheSize = " + mbeanProxy.getCacheSize());

mbeanProxy.setCacheSize(150);

echo("\nWaiting for notification...");
sleep(2000);
echo("\nCacheSize = " + mbeanProxy.getCacheSize());
echo("\nInvoke sayHello() in Hello MBean...");
mbeanProxy.sayHello();

echo("\nInvoke add(2, 3) in Hello MBean...");
echo("\nadd(2, 3) = " + mbeanProxy.add(2, 3));

waitForEnterPressed();

...

MBean 代理允许您通过 Java 接口访问 MBean,使您可以在代理上调用而不必编写冗长的代码来访问远程 MBean。在此处通过在javax.management.JMX类中调用newMBeanProxy()方法创建Hello的 MBean 代理,传递 MBean 的MBeanServerConnection、对象名称、MBean 接口的类名和true,表示代理必须表现为NotificationBroadcaster。JMX 客户端现在可以执行Hello定义的操作,就好像它们是本地注册的 MBean 的操作一样。JMX 客户端还添加了一个通知监听器,并更改了 MBean 的CacheSize属性,使其发送通知。

通过代理对远程 MXBeans 执行操作

您可以像创建 MBean 代理一样创建 MXBean 的代理。

...

ObjectName mxbeanName = new ObjectName ("com.example:type=QueueSampler");
QueueSamplerMXBean mxbeanProxy = JMX.newMXBeanProxy(mbsc, 
    mxbeanName,  QueueSamplerMXBean.class);
QueueSample queue1 = mxbeanProxy.getQueueSample();
echo("\nQueueSample.Date = " + queue1.getDate());
echo("QueueSample.Head = " + queue1.getHead());
echo("QueueSample.Size = " + queue1.getSize());
echo("\nInvoke clearQueue() in QueueSampler MXBean...");
mxbeanProxy.clearQueue();

QueueSample queue2 = mxbeanProxy.getQueueSample();
echo("\nQueueSample.Date = " +  queue2.getDate());
echo("QueueSample.Head = " + queue2.getHead());
echo("QueueSample.Size = " + queue2.getSize());

...

如上所示,要为 MXBean 创建代理,您只需调用JMX.newMXBeanProxy而不是newMBeanProxy。MXBean 代理mxbeanProxy允许客户端调用QueueSample MXBean 的操作,就好像它们是本地注册的 MXBean 的操作一样。

关闭连接

一旦 JMX 客户端获取了所需的所有信息,并在远程 JMX 代理的 MBean 服务器上执行了所有必要的操作,连接必须关闭。


jmxc.close();

通过调用JMXConnector.close方法关闭连接。

运行自定义 JMX 客户端示例

此示例需要 Java SE 平台的 6 版本。要使用自定义 JMX 客户端Client远程监视Main JMX 代理,请按照以下步骤操作:

  1. 如果尚未这样做,请将jmx_examples.zip保存到您的work_dir目录中。

  2. 在终端窗口中使用以下命令解压示例类的捆绑包。

    unzip jmx_examples.zip
    
    
  3. work_dir目录中编译示例 Java 类。

    javac com/example/*.java
    
    
  4. 启动Main应用程序,指定暴露Main以进行远程管理的属性:

    java -Dcom.sun.management.jmxremote.port=9999 \
         -Dcom.sun.management.jmxremote.authenticate=false \
         -Dcom.sun.management.jmxremote.ssl=false \ 
         com.example.Main
    
    

    生成一个确认,表明Main正在等待某些事件发生。

  5. 在另一个终端窗口中启动Client应用程序:

    java com.example.Client
    
    

    显示已获取MBeanServerConnection的确认。

  6. 按 Enter 键。

    显示了由Main启动的 MBean 服务器中注册的所有 MBeans 所在的域。

  7. 再次按 Enter 键。

    显示在 MBean 服务器中注册的 MBean 数量,以及所有这些 MBean 的对象名称。显示的 MBeans 包括在 Java VM 中运行的所有标准平台 MXBeans,以及由Main在 MBean 服务器中注册的Hello MBean 和QueueSampler MXBean。

  8. 再次按 Enter 键。

    Hello MBean 的操作由Client调用,结果如下:

    • Client添加通知监听器,以侦听来自Main的通知。

    • CacheSize属性的值从 200 更改为 150。

    • 在启动Main的终端窗口中,会显示CacheSize属性更改的确认信息。

    • 在启动Client的终端窗口中,显示来自Main的通知,通知ClientCacheSize属性更改。

    • 调用Hello MBean 的sayHello操作。

    • 在启动Main的终端窗口中,显示消息“Hello world”。

    • 调用Hello MBean 的add操作,参数为 2 和 3。结果由Client显示。

  9. 再次按 Enter 键。

    QueueSampler MXBean 的操作由Client调用,结果如下:

    • 显示QueueSampledateheadsize

    • 调用clearQueue操作。

  10. 再次按 Enter 键。

    Client关闭与 MBean 服务器的连接,并显示确认信息。

教程:Java API for XML Processing (JAXP)

原文:docs.oracle.com/javase/tutorial/jaxp/index.html

Java API for XML Processing (JAXP) 教程介绍了 Java API for XML Processing (JAXP) 1.4 技术,通过 JAXP 应用示例。

在阅读本教程之前

要充分利用 Java API for XML Processing (JAXP) 教程中的信息,您应该具备以下技术知识:

  • Java 编程语言及其开发环境。

  • 可扩展标记语言(XML)

  • 文档对象模型(DOM),由万维网联盟(W3C)DOM 工作组定义。

  • 简单 XML API(SAX),由 XML-DEV 邮件列表成员合作开发。

假定具有一些 DOM 和 SAX 的先验知识。本教程详细讨论了仅特定于 JAXP API 的代码。

简要描述了 JAXP 技术,包括其目的和主要特点。

介绍了 JAXP 技术中使用的概念,即简单 XML API(SAX):何时使用 SAX,如何解析 XML 文件,如何实现 SAX 验证,如何运行 SAX 解析器以及如何处理词法事件。提供了进一步信息的链接。

介绍了文档对象模型(DOM)使用的树结构,并展示了如何使用 DOM 函数创建节点、删除节点、更改节点内容以及遍历节点层次结构。

包括如何将文档对象模型写成 XML 文件的信息,以及如何从任意数据文件生成 DOM 以将其转换为 XML。

着重介绍了基于流的 Java 技术、事件驱动、拉取解析的 XML 读写 API。StAX 可以创建快速、相对易于编程且内存占用较小的双向 XML 解析器。

介绍了在 7u40 和 JDK8 中添加的属性。

讨论了 JAXP 实现限制,包括在 7u45 中添加的三个限制。

教程:JAXP 简介

原文:docs.oracle.com/javase/tutorial/jaxp/intro/index.html

Java API for XML Processing (JAXP) 是用于使用 Java 编程语言编写的应用程序处理 XML 数据的工具。JAXP 利用了解析器标准 Simple API for XML Parsing (SAX) 和 Document Object Model (DOM),因此您可以选择将数据解析为事件流或构建对象表示。JAXP 还支持可扩展样式表语言转换 (XSLT) 标准,让您控制数据的呈现方式,并使您能够将数据转换为其他 XML 文档或其他格式,如 HTML。JAXP 还提供命名空间支持,允许您处理可能存在命名冲突的 DTD。最后,从版本 1.4 开始,JAXP 实现了流式 XML (StAX) 标准。

为了灵活性,JAXP 允许您在应用程序中使用任何符合 XML 标准的解析器。它通过所谓的可插拔层实现这一点,让您可以插入 SAX 或 DOM API 的实现。可插拔层还允许您插入 XSL 处理器,让您控制 XML 数据的显示方式。

包概述

原文:docs.oracle.com/javase/tutorial/jaxp/intro/package.html

SAX 和 DOM API 分别由 XML-DEV 组和 W3C 定义。定义这些 API 的库如下:

  • javax.xml.parsers:JAXP API 提供了不同供应商的 SAX 和 DOM 解析器的通用接口。

  • org.w3c.dom:定义了Document类(DOM)以及 DOM 的所有组件的类。

  • org.xml.sax:定义了基本的 SAX API。

  • javax.xml.transform:定义了 XSLT API,让您可以将 XML 转换为其他形式。

  • javax.xml.stream:提供了特定于 StAX 的转换 API。

简单 XML API(SAX)是一种事件驱动、串行访问机制,逐个元素进行处理。该级别的 API 读取和写入 XML 到数据存储库或网络。对于服务器端和高性能应用程序,您需要充分了解这个级别。但对于许多应用程序,了解最基本的内容就足够了。

DOM API 通常是一个更容易使用的 API。它提供了熟悉的对象树结构。您可以使用 DOM API 来操作封装的应用程序对象的层次结构。DOM API 非常适合交互式应用程序,因为整个对象模型都存在于内存中,用户可以访问和操作它。

另一方面,构建 DOM 需要读取整个 XML 结构并将对象树保存在内存中,因此它需要更多的 CPU 和内存资源。因此,SAX API 往往更受服务器端应用程序和不需要数据的内存表示的数据过滤器的青睐。

javax.xml.transform中定义的 XSLT API 允许您将 XML 数据写入文件或将其转换为其他形式。正如本教程的 XSLT 部分所示,您甚至可以与 SAX API 一起使用它来将传统数据转换为 XML。

最后,在javax.xml.stream中定义的 StAX API 提供了基于 Java 技术的流式、事件驱动、拉取解析的 API,用于读取和写入 XML 文档。StAX 提供了比 SAX 更简单的编程模型,比 DOM 更高效的内存管理。

简单的 XML API

原文:docs.oracle.com/javase/tutorial/jaxp/intro/simple.html

SAX 解析 API 的基本概述如图 1-1 所示。要启动该过程,需要使用SAXParserFactory类的一个实例来生成解析器的一个实例。

图 1-1 SAX API

SAX API

解析器包装了一个SAXReader对象。当调用解析器的parse()方法时,阅读器会调用应用程序中实现的几个回调方法之一。这些方法由ContentHandlerErrorHandlerDTDHandlerEntityResolver接口定义。

下面是关键的 SAX API 摘要:

SAXParserFactory

SAXParserFactory对象根据系统属性javax.xml.parsers.SAXParserFactory创建解析器的实例。

SAXParser

SAXParser接口定义了几种parse()方法。通常情况下,你会将 XML 数据源和一个DefaultHandler对象传递给解析器,解析器会处理 XML 并调用处理程序对象中适当的方法。

SAXReader

SAXParser包装了一个SAXReader。通常情况下,你不需要关心这一点,但偶尔你需要使用SAXParsergetXMLReader()来获取它,以便进行配置。SAXReader与你定义的 SAX 事件处理程序进行交互。

DefaultHandler

在图中未显示的是,DefaultHandler实现了ContentHandlerErrorHandlerDTDHandlerEntityResolver接口(带有空方法),因此你只需要覆盖你感兴趣的方法。

ContentHandler

当识别到 XML 标签时,诸如startDocumentendDocumentstartElementendElement等方法会被调用。该接口还定义了characters()processingInstruction()方法,当解析器遇到 XML 元素中的文本或内联处理指令时会被调用。

ErrorHandler

方法error()fatalError()warning()会在响应各种解析错误时被调用。默认的错误处理程序对于致命错误会抛出异常,并忽略其他错误(包括验证错误)。这就是为什么即使使用 DOM,你也需要了解一些关于 SAX 解析器的知识的原因。有时,应用程序可能能够从验证错误中恢复。其他时候,可能需要生成异常。为确保正确处理,你需要向解析器提供自己的错误处理程序。

DTDHandler

定义了通常不会被调用的方法。在处理 DTD 时用于识别和处理未解析实体的声明。

EntityResolver

当解析器必须识别由 URI 标识的数据时,将调用resolveEntity方法。在大多数情况下,URI 只是一个 URL,指定了文档的位置,但在某些情况下,文档可能由 URN 标识-在网络空间中是唯一的公共标识符或名称。公共标识符可以在 URL 之外指定。EntityResolver然后可以使用公共标识符而不是 URL 来查找文档-例如,如果存在本地副本,则可以访问文档。

一个典型的应用程序至少实现了大部分ContentHandler方法。因为接口的默认实现除了致命错误外忽略所有输入,一个健壮的实现可能还想要实现ErrorHandler方法。

SAX 包

SAX 解析器在下表中列出的包中定义。

表 SAX 包

描述
org.xml.sax 定义了 SAX 接口。org.xml是由定义 SAX API 的组确定的包前缀。
org.xml.sax.ext 定义了用于执行更复杂的 SAX 处理的 SAX 扩展-例如,处理文档类型定义(DTD)或查看文件的详细语法。
org.xml.sax.helpers 包含一些辅助类,使得使用 SAX 更加容易-例如,通过定义一个默认处理程序,其中所有接口的方法都是空方法,这样你只需要重写你真正想要实现的方法。
javax.xml.parsers 定义了SAXParserFactory类,该类返回SAXParser。还定义了用于报告错误的异常类。

文档对象模型 API

原文:docs.oracle.com/javase/tutorial/jaxp/intro/dom.html

下图  展示了 DOM API 的运行情况。

图  DOM API

DOM API

您可以使用 javax.xml.parsers.DocumentBuilderFactory 类获取一个 DocumentBuilder 实例,并使用该实例生成符合 DOM 规范的 Document 对象。实际上,您获得的构建器是由系统属性 javax.xml.parsers.DocumentBuilderFactory 决定的,该属性选择用于生成构建器的工厂实现。(可以从命令行覆盖平台的默认值。)

您还可以使用 DocumentBuildernewDocument() 方法创建一个实现 org.w3c.dom.Document 接口的空 Document。或者,您可以使用构建器的解析方法之一从现有 XML 数据创建一个 Document。结果是一个类似上述 图  中显示的 DOM 树。


  • 注意 - 尽管它们被称为对象,但 DOM 树中的条目实际上是相当低级的数据结构。例如,考虑这个结构:<color>blue</color>。颜色标签有一个元素节点,在其下有一个包含数据 blue 的文本节点!这个问题将在本教程的 DOM 课程中详细探讨,但是那些期望得到对象的开发人员通常会惊讶地发现,在元素节点上调用 getNodeValue() 不会返回任何内容。要获得真正面向对象的树,请参阅 www.jdom.org 上的 JDOM API。

DOM 包

文档对象模型实现在以下 表  中列出的包中定义。

表  DOM 包

描述
org.w3c.dom 定义了 W3C 规范中 XML(以及可选的 HTML)文档的 DOM 编程接口。
javax.xml.parsers 定义了 DocumentBuilderFactory 类和 DocumentBuilder 类,后者返回一个实现 W3C Document 接口的对象。用于创建构建器的工厂由 javax.xml.parsers 系统属性确定,可以从命令行设置或在调用 new Instance 方法时覆盖。此包还定义了用于报告错误的 ParserConfigurationException 类。

可扩展样式表语言转换 APIs

原文:docs.oracle.com/javase/tutorial/jaxp/intro/extensible.html

下面的 图 展示了 XSLT APIs 的运行情况。

图 XSLT APIs

XSLT APIs

创建一个 TransformerFactory 对象,并用它来创建一个 Transformer。源对象是转换过程的输入。源对象可以从 SAX 读取器、DOM 或输入流创建。

类似地,结果对象是转换过程的结果。该对象可以是 SAX 事件处理程序、DOM 或输出流。

创建转换器时,可以根据一组转换指令创建它,这样指定的转换就会执行。如果没有任何特定的指令创建它,那么转换器对象只是将源复制到结果。

XSLT 包

XSLT APIs 定义在 表 中所示的包中。

表 XSLT 包

描述
javax.xml.transform 定义了 TransformerFactoryTransformer 类,用于获取能够执行转换的对象。创建转换器对象后,调用其 transform() 方法,提供输入(源)和输出(结果)。
javax.xml.transform.dom 用于从 DOM 创建输入(源)和输出(结果)对象的类。
javax.xml.transform.sax 用于从 SAX 解析器创建输入(源)对象和从 SAX 事件处理程序创建输出(结果)对象的类。
javax.xml.transform.stream 用于从 I/O 流创建输入(源)对象和输出(结果)对象的类。

XML 的流 API

原文:docs.oracle.com/javase/tutorial/jaxp/intro/streaming.html

StAX 是 JAXP 家族中最新的 API,为那些希望进行高性能流过滤、处理和修改的开发人员提供了一种替代方案,特别是对于内存较低和扩展性要求有限的情况。

总结一下,StAX 提供了标准的、双向的拉取解析器接口,用于流式 XML 处理,提供了比 SAX 更简单的编程模型,比 DOM 更高效的内存管理。StAX 使开发人员能够将 XML 流解析和修改为事件,并扩展 XML 信息模型以允许特定于应用程序的添加。有关 StAX 与几种替代 API 的更详细比较,请参见 XML 流 API,以及 将 StAX 与其他 JAXP API 进行比较。

StAX 包

StAX API 定义在 表 1-4 中所示的包中。

表 1-4 StAX 包

描述
javax.xml.stream 定义了 XMLStreamReader 接口,用于迭代 XML 文档的元素。XMLStreamWriter 接口指定了 XML 的写入方式。
javax.xml.transform.stax 提供了专门针对 StAX 的转换 API。

查找 JAXP 示例程序

原文:docs.oracle.com/javase/tutorial/jaxp/intro/sample.html

一组 JAXP 示例程序包含在可从Apache Xerces™ Project获取的Xerces2二进制下载包中。安装 Xerces2 后,示例程序位于目录*INSTALL_DIR*/xerces-*version*/samples/jaxp中。

这些示例程序旨在在Java 平台标准版(Java SE)6 或更高版本上运行。

您接下来该怎么办?

原文:docs.oracle.com/javase/tutorial/jaxp/intro/next.html

到目前为止,您已经有足够的信息可以开始浏览 JAXP 库。您的下一步取决于您想要实现什么。您可能想查看以下任何一课程:

  • 如果数据结构已经确定,并且您正在编写需要快速处理的服务器应用程序或 XML 过滤器,请参阅简单 XML API。

  • 如果您需要从 XML 数据构建对象树,以便在应用程序中对其进行操作,或者将内存中的对象树转换为 XML,请参阅文档对象模型。

  • 如果您需要将 XML 标记转换为其他形式,如果您想要生成 XML 输出,或者(与 SAX API 结合使用)如果您想要将传统数据结构转换为 XML,请参阅可扩展样式表语言转换。

  • 如果您想要基于流的 Java 技术、事件驱动、拉取解析 API 来读取和写入 XML 文档,或者想要创建快速、相对易于编程且具有轻量级内存占用的双向 XML 解析器,那么请参阅 XML 流 API。

课程:Simple API for XML

原文:docs.oracle.com/javase/tutorial/jaxp/sax/index.html

本课程重点介绍了用于访问 XML 文档的事件驱动、串行访问机制——Simple API for XML(SAX)。这种协议经常被需要传输和接收 XML 文档的 servlet 和面向网络的程序使用,因为它是目前处理 XML 文档的最快速、最占用内存最少的机制,除了 XML 流 API(StAX)之外。


注意 - 简而言之,SAX 面向状态独立处理,其中处理元素不依赖于之前的元素。另一方面,StAX 面向状态相关处理。有关更详细的比较,请参见何时使用 SAX。


设置程序以使用 SAX 需要比设置为使用文档对象模型(DOM)需要更多的工作。SAX 是一种事件驱动模型(您提供回调方法,解析器在读取 XML 数据时调用它们),这使得它更难以可视化。最后,您不能像回退串行数据流或重新排列已从该流中读取的字符一样“回退”到文档的早期部分或重新排列它。

出于这些原因,编写一个显示 XML 文档并可能修改它的面向用户的应用程序的开发人员将希望使用文档对象模型中描述的 DOM 机制。

然而,即使您计划专门构建 DOM 应用程序,熟悉 SAX 模型也有几个重要原因:

  • 相同的错误处理:SAX 和 DOM API 生成相同类型的异常,因此错误处理代码几乎相同。

  • 处理验证错误:默认情况下,规范要求忽略验证错误。如果您希望在发生验证错误时抛出异常(您可能会这样做),那么您需要了解 SAX 错误处理的工作原理。

  • 转换现有数据:正如您将在文档对象模型中看到的,有一种机制可以用来将现有数据集转换为 XML。然而,利用该机制需要理解 SAX 模型。

何时使用 SAX

原文:docs.oracle.com/javase/tutorial/jaxp/sax/when.html

当你想将现有数据转换为 XML 时,了解 SAX 事件模型是很有帮助的。转换过程的关键是修改现有应用程序,在读取数据时生成 SAX 事件。

SAX 是快速高效的,但其事件模型使其最适用于状态无关的过滤。例如,SAX 解析器在遇到元素标签时调用应用程序中的一个方法,并在找到文本时调用不同的方法。如果你正在进行的处理是状态无关的(意味着它不依赖于之前出现的元素),那么 SAX 就可以很好地工作。

另一方面,对于状态相关处理,当程序需要在元素 A 下对数据执行一项操作,但在元素 B 下需要执行不同的操作时,那么像 XML 流 API(StAX)这样的拉取解析器可能是更好的选择。使用拉取解析器,你可以在代码中的任何位置请求下一个节点,无论它是什么。因此,你可以轻松地改变处理文本的方式(例如),因为你可以在程序中的多个位置处理它(更多细节,请参见更多信息)。

SAX 需要比 DOM 更少的内存,因为 SAX 不构造 XML 数据的内部表示(树结构),而 DOM 则会。相反,SAX 只是在读取时将数据发送给应用程序;然后你的应用程序可以对看到的数据进行任何操作。

拉取解析器和 SAX API 都像串行 I/O 流一样工作。你可以看到数据随着流入,但不能回到较早的位置或跳到不同的位置。一般来说,这样的解析器在你只想读取数据并让应用程序对其进行操作时效果很好。

但是,当你需要修改 XML 结构 - 特别是当你需要交互式地修改它时 - 使用内存结构更有意义。DOM 就是这样的模型之一。然而,尽管 DOM 为大型文档(如书籍和文章)提供了许多强大的功能,但它也需要大量复杂的编码。该过程的详细信息在下一课程的何时使用 DOM 中进行了强调。

对于更简单的应用程序,这种复杂性可能是不必要的。对于更快速的开发和更简单的应用程序,其中一个面向对象的 XML 编程标准,如 JDOM(www.jdom.org)和 DOM4J(www.dom4j.org/),可能更合适。

使用 SAX 解析 XML 文件

原文:docs.oracle.com/javase/tutorial/jaxp/sax/parsing.html

在实际应用中,您将希望使用 SAX 解析器处理 XML 数据并对其进行有用的操作。本节将介绍一个名为SAXLocalNameCount的 JAXP 程序示例,该程序仅使用元素的localName组件计算元素的数量,而忽略了命名空间名称以简化操作。此示例还展示了如何使用 SAX ErrorHandler

创建骨架

SAXLocalNameCount程序保存在名为SAXLocalNameCount.java的文件中。

public class SAXLocalNameCount {
    static public void main(String[] args) {
        // ...
    }
}

因为您将独立运行它,所以需要一个main()方法。并且您需要命令行参数,以便告诉应用程序要处理哪个文件。在SAXLocalNameCount.java文件中找到示例的完整代码。

导入类

应用程序将使用的类的导入语句如下。

package sax;
import javax.xml.parsers.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;

import java.util.*;
import java.io.*;

public class SAXLocalNameCount {
    // ...
}

javax.xml.parsers包含SAXParserFactory类,用于创建所使用的解析器实例。如果无法生成与指定选项配置匹配的解析器,则会抛出ParserConfigurationException异常。(稍后,您将看到更多有关配置选项的内容)。javax.xml.parsers包还包含SAXParser类,这是工厂用于解析的返回值。org.xml.sax包定义了 SAX 解析器使用的所有接口。org.xml.sax.helpers包含DefaultHandler,它定义了将处理解析器生成的 SAX 事件的类。java.utiljava.io中的类用于提供哈希表和输出。

设置 I/O

首要任务是处理命令行参数,目前这些参数仅用于获取要处理的文件的名称。main方法中的以下代码告诉应用程序您希望SAXLocalNameCount处理哪个文件。

static public void main(String[] args) throws Exception {
    String filename = null;

    for (int i = 0; i < args.length; i++) {
        filename = args[i];
        if (i != args.length - 1) {
            usage();
        }
    }

    if (filename == null) {
        usage();
    } 
}

这段代码将main方法设置为在遇到问题时抛出Exception,并定义了命令行选项,这些选项是告诉应用程序要处理的 XML 文件的名称。在本课程的后面部分,当我们开始查看验证时,代码中的其他命令行参数将被检查。

当您运行应用程序时提供的filename字符串将通过内部方法convertToFileURL()转换为java.io.File URL。这是在SAXLocalNameCount中的以下代码完成的。

public class SAXLocalNameCount {
    private static String convertToFileURL(String filename) {
        String path = new File(filename).getAbsolutePath();
        if (File.separatorChar != '/') {
            path = path.replace(File.separatorChar, '/');
        }

        if (!path.startsWith("/")) {
            path = "/" + path;
        }
        return "file:" + path;
    }

    // ...
}

如果在运行程序时指定了不正确的命令行参数,则会调用SAXLocalNameCount应用程序的usage()方法,以在屏幕上打印出正确的选项。

private static void usage() {
    System.err.println("Usage: SAXLocalNameCount <file.xml>");
    System.err.println("       -usage or -help = this message");
    System.exit(1);
}

更多usage()选项将在本课程的后面部分进行讨论,当处理验证时。

实现ContentHandler接口

SAXLocalNameCount中最重要的接口是ContentHandler。该接口需要一些方法,SAX 解析器会在不同的解析事件发生时调用这些方法。主要的事件处理方法包括:startDocumentendDocumentstartElementendElement

实现此接口的最简单方法是扩展org.xml.sax.helpers包中定义的DefaultHandler类。该类为所有ContentHandler事件提供了空操作方法。示例程序扩展了该类。

public class SAXLocalNameCount extends DefaultHandler {
    // ...
} 


注意 - DefaultHandler还为DTDHandlerEntityResolverErrorHandler接口中定义的其他主要事件定义了空操作方法。您将在本课程的后续部分了解更多关于这些方法的信息。


接口要求这些方法中的每一个都抛出SAXException。在此抛出的异常会被发送回解析器,解析器会将其发送到调用解析器的代码。

处理内容事件

本节展示了处理ContentHandler事件的代码。

当遇到开始标签或结束标签时,标签的名称作为字符串传递给startElementendElement方法。当遇到开始标签时,它定义的任何属性也会作为Attributes列表传递。元素中找到的字符会作为字符数组传递,同时还会传递字符的数量(长度)和指向第一个字符的数组偏移量。

文档事件

以下代码处理了开始文档和结束文档事件:

public class SAXLocalNameCount extends DefaultHandler {

    private Hashtable tags;

    public void startDocument() throws SAXException {
        tags = new Hashtable();
    }

    public void endDocument() throws SAXException {
        Enumeration e = tags.keys();
        while (e.hasMoreElements()) {
            String tag = (String)e.nextElement();
            int count = ((Integer)tags.get(tag)).intValue();
            System.out.println("Local Name \"" + tag + "\" occurs " 
                               + count + " times");
        }    
    }

    private static String convertToFileURL(String filename) {
        // ...
    }

    // ...
}

此代码定义了解析器遇到正在解析的文档的开始和结束点时应用程序执行的操作。ContentHandler接口的startDocument()方法创建了一个java.util.Hashtable实例,在元素事件中将填充解析器在文档中找到的 XML 元素。当解析器到达文档末尾时,将调用endDocument()方法,以获取哈希表中包含的元素的名称和计数,并在屏幕上打印一条消息,告诉用户找到每个元素的次数。

这两个ContentHandler方法都会抛出SAXException。您将在设置错误处理中了解更多关于 SAX 异常的信息。

元素事件

如文档事件中所述,startDocument方法创建的哈希表需要填充解析器在文档中找到的各种元素。以下代码处理了开始元素事件:

public void startDocument() throws SAXException {
    tags = new Hashtable();
}

public void startElement(String namespaceURI,
                         String localName,
                         String qName, 
                         Attributes atts)
    throws SAXException {

    String key = localName;
    Object value = tags.get(key);

    if (value == null) {
        tags.put(key, new Integer(1));
    } 
    else {
        int count = ((Integer)value).intValue();
        count++;
        tags.put(key, new Integer(count));
    }
}

public void endDocument() throws SAXException {
    // ...
}

此代码处理元素标签,包括在开始标签中定义的任何属性,以获取该元素的命名空间统一资源标识符(URI)、本地名称和限定名称。然后,startElement()方法使用startDocument()创建的哈希映射填充每种类型元素的本地名称及其计数。请注意,当调用startElement()方法时,如果未启用命名空间处理,则元素和属性的本地名称可能为空字符串。代码通过在简单名称为空字符串时使用限定名称来处理该情况。

字符事件

JAXP SAX API 还允许您处理解析器传递给应用程序的字符,使用ContentHandler.characters()方法。


注意 - 字符事件在SAXLocalNameCount示例中没有展示,但为了完整起见,本节中包含了一个简要描述。


解析器不需要一次返回任何特定数量的字符。解析器可以一次返回一个字符,直到几千个字符,仍然是符合标准的实现。因此,如果您的应用程序需要处理它看到的字符,最好让characters()方法在java.lang.StringBuffer中累积字符,并且只在确定找到所有字符时对其进行操作。

当元素结束时,您完成了解析文本,因此通常在那时执行字符处理。但您可能还想在元素开始时处理文本。这对于包含与文本混合的 XML 元素的文档式数据是必要的。例如,考虑这个文档片段:

**<para>**这段文字包含**<bold>**重要**</bold>**的想法。**</para>**

初始文本This paragraph contains<bold>元素的开始处终止。文本important在结束标签</bold>处终止,最终文本ideas.在结束标签</para>处终止。

严格来说,字符处理程序应该扫描&<字符,并用适当的字符串&amp;&lt;替换它们。这将在下一节中解释。

处理特殊字符

在 XML 中,实体是具有名称的 XML 结构(或纯文本)。通过名称引用实体会导致它在文档中插入实体引用的位置。要创建实体引用,您用和分号括起实体名称:

&entityName;

当您处理包含许多特殊字符的大块 XML 或 HTML 时,可以使用 CDATA 部分。CDATA 部分类似于 HTML 中的<code>...</code>,只是更加强大:CDATA 部分中的所有空格都是有意义的,并且其中的字符不会被解释为 XML。CDATA 郅始于<![[CDATA[,结束于]]>

下面显示了一个 CDATA 部分的示例。

<p><termdef id="dt-cdsection" term="CDATA Section">CDATA 部分</term> 可以出现在任何字符数据可能出现的地方;它们用于转义包含否则会被识别为标记的文本块。CDATA 郅始于字符串 "<code>&lt;![CDATA[</code>",结束于字符串 "<code>]]&gt;</code>"

解析后,此文本将显示如下:

CDATA 部分可以出现在任何字符数据可能出现的地方;它们用于转义包含否则会被识别为标记的文本块。CDATA 郅始于字符串 "<![CDATA[",结束于字符串 "]]>"。

CDATA 的存在使得正确回显 XML 有点棘手。如果要输出的文本不在 CDATA 部分中,那么文本中的任何尖括号、和其他特殊字符应该被替换为适当的实体引用。(替换左尖括号和和符号是最重要的,其他字符将被正确解释而不会误导解析器。)但如果输出文本在 CDATA 部分中,那么不应进行替换,导致文本如前面的示例中所示。在像我们的 SAXLocalNameCount 应用程序这样的简单程序中,这并不特别严重。但许多 XML 过滤应用程序将希望跟踪文本是否出现在 CDATA 部分中,以便能够正确处理特殊字符。

设置解析器

以下代码设置解析器并启动它:

static public void main(String[] args) throws Exception {

    // Code to parse command-line arguments 
    //(shown above)
    // ...

    SAXParserFactory spf = SAXParserFactory.newInstance();
    spf.setNamespaceAware(true);
    SAXParser saxParser = spf.newSAXParser();
}

这些代码行创建了一个 SAXParserFactory 实例,由 javax.xml.parsers.SAXParserFactory 系统属性的设置确定。通过将 setNamespaceAware 设置为 true 来设置要创建的工厂以支持 XML 命名空间,然后通过调用其 newSAXParser() 方法从工厂获取一个 SAXParser 实例。


注意 - javax.xml.parsers.SAXParser 类是一个包装器,定义了一些便利方法。它包装了(有点不太友好的)org.xml.sax.Parser 对象。如果需要,可以使用 SAXParser 类的 getParser() 方法获取该解析器。


现在您需要实现所有解析器必须实现的 XMLReaderXMLReader 由应用程序用于告诉 SAX 解析器对所讨论的文档执行什么处理。XMLReadermain 方法中通过以下代码实现。

// ...
SAXParser saxParser = spf.newSAXParser();
XMLReader xmlReader = saxParser.getXMLReader();
xmlReader.setContentHandler(new SAXLocalNameCount());
xmlReader.parse(convertToFileURL(filename));

在这里,通过调用SAXParser实例的getXMLReader()方法获取用于解析器的XMLReader实例。然后,XMLReaderSAXLocalNameCount类注册为其内容处理程序,以便解析器执行的操作是处理内容事件中显示的startDocument()startElement()endDocument()方法。最后,XMLReader通过将 XML 文件的位置传递给解析器告诉解析器要解析哪个文档,传递的形式是由设置 I/O 中定义的convertToFileURL()方法生成的File URL。

设置错误处理

现在你可以开始使用你的解析器了,但最好实现一些错误处理。解析器可以生成三种错误:致命错误、错误和警告。当发生致命错误时,解析器无法继续。因此,如果应用程序没有生成异常,那么默认的错误事件处理程序会生成一个异常。但对于非致命错误和警告,默认错误处理程序永远不会生成异常,也不会显示任何消息。

如文档事件中所示,应用程序的事件处理方法会抛出SAXException。例如,ContentHandler接口中startDocument()方法的签名被定义为返回一个SAXException

public void startDocument() throws SAXException { /* ... */ }

可以使用消息、另一个异常或两者构造SAXException

因为默认解析器仅为致命错误生成异常,并且默认解析器提供的有关错误的信息有些有限,SAXLocalNameCount程序通过MyErrorHandler类定义了自己的错误处理。

xmlReader.setErrorHandler(new MyErrorHandler(System.err));

// ...

private static class MyErrorHandler implements ErrorHandler {
    private PrintStream out;

    MyErrorHandler(PrintStream out) {
        this.out = out;
    }

    private String getParseExceptionInfo(SAXParseException spe) {
        String systemId = spe.getSystemId();

        if (systemId == null) {
            systemId = "null";
        }

        String info = "URI=" + systemId + " Line=" 
            + spe.getLineNumber() + ": " + spe.getMessage();

        return info;
    }

    public void warning(SAXParseException spe) throws SAXException {
        out.println("Warning: " + getParseExceptionInfo(spe));
    }

    public void error(SAXParseException spe) throws SAXException {
        String message = "Error: " + getParseExceptionInfo(spe);
        throw new SAXException(message);
    }

    public void fatalError(SAXParseException spe) throws SAXException {
        String message = "Fatal Error: " + getParseExceptionInfo(spe);
        throw new SAXException(message);
    }
}

与设置解析器中展示XMLReader指向正确内容处理程序的方式相同,在这里通过调用其setErrorHandler()方法将XMLReader指向新的错误处理程序。

MyErrorHandler类实现了标准的org.xml.sax.ErrorHandler接口,并定义了一个方法来获取解析器生成的任何SAXParseException实例提供的异常信息。这个方法,getParseExceptionInfo(),简单地通过调用标准的SAXParseException方法getLineNumber()getSystemId()获取错误发生在 XML 文档中的行号和运行它的系统的标识符。然后,这个异常信息被馈送到基本 SAX 错误处理方法error()warning()fatalError()的实现中,这些方法被更新以发送关于文档中错误的性质和位置的适当消息。

处理非致命错误

当 XML 文档未通过有效性约束时会发生非致命错误。如果解析器发现文档无效,则会生成一个错误事件。这样的错误是由验证解析器生成的,给定文档类型定义(DTD)或模式,当文档具有无效标记时,当找到不允许的标记时,或者(在模式的情况下)当元素包含无效数据时。

关于非致命错误最重要的原则是,默认情况下它们会被忽略。但如果文档中发生验证错误,你可能不希望继续处理它。你可能希望将这类错误视为致命错误。

要接管错误处理,你需要重写处理致命错误、非致命错误和警告的DefaultHandler方法,作为ErrorHandler接口的一部分。正如在前一节的代码片段中所示,SAX 解析器将SAXParseException传递给这些方法中的每一个,因此在发生错误时生成异常就像简单地将其抛回一样。


注意 - 检查org.xml.sax.helpers.DefaultHandler中定义的错误处理方法可能会很有启发性。你会发现error()warning()方法什么都不做,而fatalError()会抛出异常。当然,你总是可以重写fatalError()方法以抛出不同的异常。但如果你的代码在发生致命错误时不抛出异常,那么 SAX 解析器会抛出异常。XML 规范要求如此。


处理警告

警告也会被默认忽略。警告是信息性的,只有在存在 DTD 或模式的情况下才能生成。例如,如果在 DTD 中两次定义了一个元素,则会生成警告。这并不是非法的,也不会引起问题,但你可能想知道,因为这可能不是有意的。将 XML 文档与 DTD 进行验证将在下一节中展示。

在没有验证的情况下运行 SAX 解析器示例

以下步骤解释了如何在没有验证的情况下运行 SAX 解析器示例。

在没有验证的情况下运行SAXLocalNameCount示例

  1. SAXLocalNameCount.java文件保存在名为sax的目录中。

  2. 编译文件如下:

    javac sax/SAXLocalNameCount.java
    
    
  3. 将示例 XML 文件rich_iii.xmltwo_gent.xml保存在data目录中。

  4. 在 XML 文件上运行SAXLocalNameCount程序。

    选择data目录中的一个 XML 文件,并在其上运行SAXLocalNameCount程序。在这里,我们选择在文件rich_iii.xml上运行该程序。

    java sax/SAXLocalNameCount data/rich_iii.xml
    
    

    XML 文件rich_iii.xml包含了威廉·莎士比亚的剧作理查三世的 XML 版本。当你在其上运行SAXLocalNameCount时,你应该会看到以下输出。

    Local Name "STAGEDIR" occurs 230 times
    Local Name "PERSONA" occurs 39 times
    Local Name "SPEECH" occurs 1089 times
    Local Name "SCENE" occurs 25 times
    Local Name "ACT" occurs 5 times
    Local Name "PGROUP" occurs 4 times
    Local Name "PLAY" occurs 1 times
    Local Name "PLAYSUBT" occurs 1 times
    Local Name "FM" occurs 1 times
    Local Name "SPEAKER" occurs 1091 times
    Local Name "TITLE" occurs 32 times
    Local Name "GRPDESCR" occurs 4 times
    Local Name "P" occurs 4 times
    Local Name "SCNDESCR" occurs 1 times
    Local Name "PERSONAE" occurs 1 times
    Local Name "LINE" occurs 3696 times
    
    

    SAXLocalNameCount程序解析 XML 文件,并提供每种类型的 XML 标记实例数量的计数。

  5. 在文本编辑器中打开文件data/rich_iii.xml

    为了检查错误处理是否有效,请从 XML 文件中删除一个条目的闭合标签,例如第 21 行显示的闭合标签</PERSONA>

    21 <PERSONA>爱德华,威尔士亲王,后来的国王爱德华五世。</PERSONA>

  6. 运行SAXLocalNameCount

    这次,你应该看到以下致命错误消息。

    Exception in thread "main" org.xml.sax.SAXException: Fatal Error: URI=file:data/rich_iii.xml Line=21: The element type "PERSONA" must be terminated by the matching end-tag "</PERSONA>".
    
    

    正如你所看到的,当遇到错误时,解析器生成了一个SAXParseException,这是SAXException的一个子类,用于标识错误发生的文件和位置。

实现 SAX 验证

原文:docs.oracle.com/javase/tutorial/jaxp/sax/validation.html

示例程序SAXLocalNameCount默认使用非验证解析器,但也可以激活验证。激活验证允许应用程序判断 XML 文档是否包含正确的标记,或这些标记是否按正确顺序出现。换句话说,它可以告诉您文档是否有效。然而,如果未激活验证,它只能告诉文档是否格式良好,就像在上一节中删除 XML 元素的闭合标记时所示。要进行验证,XML 文档需要关联到一个 DTD 或 XML 模式。SAXLocalNameCount程序可以选择这两个选项。

选择解析器实现

如果没有指定其他工厂类,则将使用默认的SAXParserFactory类。要使用来自不同制造商的解析器,可以更改指向其的环境变量的值。您可以从命令行执行:

java -Djavax.xml.parsers.SAXParserFactory=*yourFactoryHere* [...]

您指定的工厂名称必须是完全限定的类名(包括所有包前缀)。有关更多信息,请参阅SAXParserFactory类的newInstance()方法中的文档。

使用验证解析器

直到这一点,本课程一直集中在非验证解析器上。本节将检查验证解析器,以了解在使用它解析示例程序时会发生什么。

关于验证解析器必须理解的两件事:

  • 需要模式或 DTD。

  • 因为存在模式或 DTD,只要可能,ContentHandler.``ignorableWhitespace()方法就会被调用。

可忽略的空格

当存在 DTD 时,解析器将不再调用characters()方法处理它知道是无关紧要的空格。从只对 XML 数据感兴趣的应用程序的角度来看,这是一件好事,因为应用程序永远不会受到纯粹为了使 XML 文件可读而存在的空格的干扰。

另一方面,如果您正在编写一个过滤 XML 数据文件的应用程序,并且希望输出一个同样可读的文件版本,那么这些空格将不再是无关紧要的:它们将变得至关重要。要获取这些字符,您需要在应用程序中添加ignorableWhitespace方法。为了处理解析器看到的任何(通常是)可忽略的空格,您需要添加类似以下代码以实现ignorableWhitespace事件处理程序。

public void ignorableWhitespace (char buf[], int start, int length) throws SAXException { 
    emit("IGNORABLE");
}

这段代码只是生成一条消息,让您知道看到了可忽略的空格。然而,并非所有解析器都是平等的。SAX 规范不要求调用此方法。Java XML 实现在 DTD 可能时会这样做。

配置工厂

SAXParserFactory 需要设置为使用验证解析器而不是默认的非验证解析器。 下面是 SAXLocalNameCount 示例的 main() 方法中的代码,显示了如何配置工厂以实现验证解析器。

static public void main(String[] args) throws Exception {

    String filename = null;
    boolean dtdValidate = false;
    boolean xsdValidate = false;
    String schemaSource = null;

    for (int i = 0; i < args.length; i++) {

        if (args[i].equals("-dtd")) {
            dtdValidate = true;
        }
        else if (args[i].equals("-xsd")) {
            xsdValidate = true;
        } 
        else if (args[i].equals("-xsdss")) {
            if (i == args.length - 1) {
               usage();
            }
            xsdValidate = true;
            schemaSource = args[++i];
        } 
        else if (args[i].equals("-usage")) {
            usage();
        }
        else if (args[i].equals("-help")) {
            usage();
        }
        else {
            filename = args[i];
            if (i != args.length - 1) {
                usage();
            }
        }
    }

    if (filename == null) {
        usage();
    }

    SAXParserFactory spf = SAXParserFactory.newInstance();
    spf.setNamespaceAware(true);
    spf.setValidating(dtdValidate || xsdValidate);
    SAXParser saxParser = spf.newSAXParser();

    // ... 
}

在这里,SAXLocalNameCount 程序被配置为在启动时接受额外的参数,告诉它实现无验证、DTD 验证、XML Schema 定义(XSD)验证或针对特定模式源文件的 XSD 验证。 (这些选项的描述 -dtd-xsd-xsdss 也被添加到 usage() 方法中,但这里没有显示。)然后,工厂被配置为在调用 newSAXParser 时生成适当的验证解析器。 如 设置解析器 中所示,您还可以使用 setNamespaceAware(true) 来配置工厂返回一个支持命名空间的解析器。 Oracle 的实现支持任何配置选项的组合。 (如果某个实现不支持特定组合,则需要生成工厂配置错误)。

使用 XML Schema 进行验证

尽管本教程不涵盖 XML Schema 的完整内容,但本节向您展示了使用 XML Schema 语言编写的现有模式验证 XML 文档的步骤。 要了解有关 XML Schema 的更多信息,您可以查看在线教程 XML Schema Part 0: Primer,网址为 www.w3.org/TR/xmlschema-0/


注意 - 存在多种模式定义语言,包括 RELAX NG、Schematron 和 W3C 的 "XML Schema" 标准。 (即使 DTD 也算是一种 "模式",尽管它是唯一不使用 XML 语法描述模式约束的模式。)然而,"XML Schema" 给我们带来了一个术语上的挑战。 虽然短语 "XML Schema schema" 可以很精确,但我们将使用短语 "XML Schema definition" 来避免冗余的外观。


要在 XML 文档中收到验证错误的通知,解析器工厂必须配置为创建验证解析器,如前一节所示。 此外,以下条件必须成立:

  • 在 SAX 解析器上必须设置适当的属性。

  • 必须设置适当的错误处理程序。

  • 文档必须与模式关联。

设置 SAX 解析器属性

从定义将用于设置属性的常量开始是有帮助的。 SAXLocalNameCount 示例设置了以下常量。

public class SAXLocalNameCount extends DefaultHandler {

    static final String JAXP_SCHEMA_LANGUAGE =
        "http://java.sun.com/xml/jaxp/properties/schemaLanguage";

    static final String W3C_XML_SCHEMA =
        "http://www.w3.org/2001/XMLSchema";

    static final String JAXP_SCHEMA_SOURCE =
        "http://java.sun.com/xml/jaxp/properties/schemaSource";
}


注意 - 解析器工厂必须配置为生成一个既具有命名空间感知能力又进行验证的解析器。这在配置工厂中已经展示过。有关命名空间的更多信息在文档对象模型中提供,但现在,请理解模式验证是一个面向命名空间的过程。因为符合 JAXP 规范的解析器默认情况下不具有命名空间感知能力,所以必须设置用于模式验证的属性才能正常工作。


然后,您必须配置解析器以告诉它要使用哪种模式语言。在SAXLocalNameCount中,验证可以针对 DTD 或 XML Schema 执行。以下代码使用上面定义的常量来指定 W3C 的 XML Schema 语言作为程序启动时指定-xsd选项时要使用的语言。

// ...
if (xsdValidate) {
    saxParser.setProperty(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA);
    // ...
}

除了设置错误处理中描述的错误处理外,在配置解析器进行基于模式的验证时可能会发生一个错误。如果解析器不符合 JAXP 规范,因此不支持 XML Schema,它可能会抛出SAXNotRecognizedException。为了处理这种情况,setProperty()语句被包裹在 try/catch 块中,如下面的代码所示。

// ...
if (xsdValidate) {
    try {
        saxParser.setProperty(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA);
    }
    catch (SAXNotRecognizedException x){
        System.err.println("Error: JAXP SAXParser property not recognized: "
                           + JAXP_SCHEMA_LANGUAGE);

        System.err.println( "Check to see if parser conforms to the JAXP spec.");
        System.exit(1);
    }
}
// ...

将文档与模式关联

要使用 XML Schema 定义验证数据,必须确保 XML 文档与之关联。有两种方法可以做到这一点。

  • 通过在 XML 文档中包含模式声明。

  • 通过在应用程序中指定要使用的模式。


注意 - 当应用程序指定要使用的模式时,它会覆盖文档中的任何模式声明。


要在文档中指定模式定义,您可以创建如下的 XML:

<*documentRoot*

    xsi:noNamespaceSchemaLocation='*YourSchemaDefinition*.xsd'>

第一个属性定义了 XML 命名空间(xmlns)前缀xsi,代表 XML Schema 实例。第二行指定了要用于文档中没有命名空间前缀的元素的模式,即通常在任何简单、不复杂的 XML 文档中定义的元素。


注意 - 有关命名空间的更多信息包含在文档对象模型中的使用 XML Schema 进行验证中。现在,将这些属性视为您用来验证不使用它们的简单 XML 文件的"魔法咒语"。在学习更多关于命名空间的知识后,您将了解如何使用 XML Schema 来验证使用它们的复杂文档。这些想法在文档对象模型的使用多个命名空间进行验证中讨论。


您还可以在应用程序中指定模式文件,就像在SAXLocalNameCount中一样。

// ...
if (schemaSource != null) {
    saxParser.setProperty(JAXP_SCHEMA_SOURCE, new File(schemaSource));
}

// ...

在上面的代码中,变量schemaSource与一个模式源文件相关联,您可以通过使用-xsdss选项启动SAXLocalNameCount应用程序并提供要使用的模式源文件的名称来指向它。

验证解析器中的错误处理

重要的是要认识到,当文件未通过验证时抛出异常的唯一原因是由于设置错误处理中显示的错误处理代码。该代码在此作为提醒再次显示:

// ...

public void warning(SAXParseException spe) throws SAXException {
    out.println("Warning: " + getParseExceptionInfo(spe));
}

public void error(SAXParseException spe) throws SAXException {
    String message = "Error: " + getParseExceptionInfo(spe);
    throw new SAXException(message);
}

public void fatalError(SAXParseException spe) throws SAXException {
    String message = "Fatal Error: " + getParseExceptionInfo(spe);
    throw new SAXException(message);
}

// ...

如果这些异常没有被抛出,则验证错误将被简单地忽略。一般来说,SAX 解析错误是验证错误,尽管如果文件指定了解析器无法处理的 XML 版本,也会生成它。请记住,除非提供类似这里的错误处理程序,否则您的应用程序不会生成验证异常。

DTD 警告

如前所述,警告仅在 SAX 解析器处理 DTD 时生成。某些警告仅由验证解析器生成。非验证解析器的主要目标是尽可能快地运行,但它也会生成一些警告。

XML 规范建议由于以下原因生成警告:

  • 为实体、属性或符号提供额外的声明。(这些声明将被忽略。仅使用第一个。此外,请注意,当验证时,元素的重复定义总是会产生致命错误,就像您之前看到的那样。)

  • 引用未声明的元素类型。(仅当未声明的类型实际在 XML 文档中使用时才会发生有效性错误。当在 DTD 中引用未声明的元素时会产生警告。)

  • 为未声明的元素类型声明属性。

Java XML SAX 解析器还在其他情况下发出警告:

  • 在验证时不需要<!DOCTYPE ...>

  • 在不进行验证时引用未定义的参数实体。(在验证时会产生错误。尽管非验证解析器不需要读取参数实体,但 Java XML 解析器会这样做。因为这不是一个要求,所以 Java XML 解析器生成警告,而不是错误。)

  • 某些情况下,字符编码声明看起来不正确。

运行带验证的 SAX 解析器示例

在本节中,之前使用的SAXLocalNameCount示例程序将再次被使用,但这次将针对 XML Schema 或 DTD 进行验证。展示不同类型的验证的最佳方式是修改被解析的 XML 文件的代码,以及相关的模式和 DTD,以破坏处理并使应用程序生成异常。

尝试使用 DTD 验证错误进行实验

如上所述,这些示例重用了SAXLocalNameCount程序。您将在不进行验证运行 SAX 解析器示例中找到示例及其相关文件的位置。

  1. 如果尚未这样做,请将SAXLocalNameCount.java文件保存在名为sax的目录中。在文本编辑器中打开文件并进行上述更改。

  2. 如果尚未这样做,请按以下方式编译文件:

    javac sax/SAXLocalNameCount.java
    
    
  3. 如果尚未这样做,请将示例 XML 文件rich_iii.xmltwo_gent.xml保存在data目录中。

  4. 运行带有 DTD 验证的SAXLocalNameCount程序。

    要做到这一点,运行程序时必须指定-dtd选项。

    java sax/SAXLocalNameCount -dtd data/rich_iii.xml
    
    

    您看到的结果将类似于这样:

    Exception in thread "main" org.xml.sax.SAXException:
    Error: URI=file:data/rich_iii.xml 
    Line=4: Document is invalid: no grammar found.
    
    

    此消息表示没有语法可以针对文档rich_iii.xml进行验证,因此它自动无效。换句话说,该消息表示您正在尝试验证文档,但没有声明 DTD,因为没有DOCTYPE声明。因此,现在您知道 DTD 是有效文档的要求。这是有道理的。

  5. 将示例 DTD 文件play.dtd保存在data目录中。

  6. 在文本编辑器中打开文件data/rich_iii.xml。在data/rich_iii.xml的开头插入以下DOCTYPE声明。(该声明将验证解析器指向名为play.dtd的 DTD 文件。如果激活了 DTD 验证,则将检查正在解析的 XML 文件的结构是否符合play.dtd中提供的结构。)

    <!DOCTYPE PLAY SYSTEM "play.dtd">

    不要忘记保存修改,但保持文件打开,因为稍后会再次需要它。

  7. 返回data/rich_iii.xml并修改第 18 行中角色"KING EDWARD The Fourth"的标记。

    将开始和结束标记从<PERSONA></PERSONA>更改为<PERSON></PERSON>。现在第 18 行应该是这样的:

    18:<PERSON>KING EDWARD The Fourth</PERSON>

    再次,不要忘记保存修改,并保持文件打开。

  8. 运行带有 DTD 验证的SAXLocalNameCount程序。

    这次,运行程序时将看到不同的错误:

    java sax/SAXLocalNameCount -dtd data/rich_iii.xml
    Exception in thread "main" org.xml.sax.SAXException: 
    Error: URI=file:data/rich_iii.xml 
    Line=26: Element type "PERSON" must be declared.
    
    

    在这里,您可以看到解析器反对的一个未包含在 DTD data/play.dtd中的元素。

  9. data/rich_iii.xml中更正"KING EDWARD The Fourth"的标记。

    将开始和结束标记恢复为原始版本,<PERSONA></PERSONA>

  10. data/rich_iii.xml中,从第 16 行删除<TITLE>Dramatis Personae</TITLE>

    再次,不要忘记保存修改。

  11. 运行带有 DTD 验证的SAXLocalNameCount程序。

    与以前一样,您将看到另一个验证错误:

    java sax/SAXLocalNameCount -dtd data/rich_iii.xml
    Exception in thread "main" org.xml.sax.SAXException: 
    Error: URI=file:data/rich_iii.xml 
    Line=77: The content of element type "PERSONAE" must match "(TITLE,(PERSONA|PGROUP)+)".
    
    

    通过从第 16 行删除<TITLE>元素,<PERSONAE>元素变得无效,因为它不包含 DTD 期望的<PERSONAE>元素的子元素。请注意,错误消息指出错误在data/rich_iii.xml的第 77 行,即使您从第 16 行删除了<TITLE>元素。这是因为<PERSONAE>元素的结束标记位于第 77 行,解析器只有在解析到元素结尾时才会抛出异常。

  12. 在文本编辑器中打开 DTD 文件data/play.dtd

    在 DTD 文件中,你可以看到 <PERSONAE> 元素的声明,以及可以在符合剧本 DTD 的 XML 文档中使用的所有其他元素。 <PERSONAE> 的声明如下所示。

    <!ELEMENT PERSONAE (TITLE, (PERSONA | PGROUP)+)>
    
    

    正如你所看到的,<PERSONAE> 元素需要一个 <TITLE> 子元素。 管道 (|) 键表示 <PERSONA><PGROUP> 子元素可以包含在 <PERSONAE> 元素中,而在 (PERSONA | PGROUP) 分组后的加号 (+) 键表示必须包含至少一个或多个这些子元素中的任意一个。

  13. <PERSONAE> 的声明中,在 TITLE 后面添加一个问号 (?) 键。

    在 DTD 中给子元素的声明添加一个问号,使得该子元素的一个实例的存在是可选的。

    <!ELEMENT PERSONAE (TITLE?, (PERSONA | PGROUP)+)>
    
    

    如果在元素后面添加星号 (*),则可以包含零个或多个该子元素的实例。 但是,在这种情况下,在文档的某个部分中有多个标题是没有意义的。

    不要忘记保存对 data/play.dtd 所做的修改。

  14. 运行 SAXLocalNameCount 程序,并激活 DTD 验证。

    java sax/SAXLocalNameCount -dtd data/rich_iii.xml
    
    

    这次,您应该看到 SAXLocalNameCount 的正确输出,没有错误。

实验模式验证错误

前面的练习演示了使用 SAXLocalNameCount 验证 XML 文件与 DTD 的过程。 在这个练习中,您将使用 SAXLocalNameCount 验证不同的 XML 文件,同时还要针对标准 XML Schema 定义和自定义模式源文件进行验证。 同样,通过修改 XML 文件和模式,使解析器抛出错误来演示这种类型的验证。

如上所述,这些示例重用了 SAXLocalNameCount 程序。 可以在 不进行验证运行 SAX 解析器示例 中找到示例及其相关文件的位置。

  1. 如果还没有这样做,请将 SAXLocalNameCount.java 文件保存在名为 sax 的目录中。 在文本编辑器中打开文件并进行上述更改。

  2. 如果还没有这样做,请按照以下方式编译文件:

    javac sax/SAXLocalNameCount.java
    
    
  3. 将示例 XML 文件 personal-schema.xml 保存在 data 目录中,然后在文本编辑器中打开它。

    这是一个简单的 XML 文件,提供了一个小公司员工的姓名和联系方式。 在这个 XML 文件中,你会看到它已经与一个模式定义文件 personal.xsd 相关联。

    <personnel xsi:noNamespaceSchemaLocation='personal.xsd'>

  4. 将示例 XSD Schema 文件 personal.xsd 保存在 data 目录中,然后在文本编辑器中打开它。

    这个模式定义了关于每个员工所需的信息种类,以便将与模式关联的 XML 文档视为有效。例如,通过检查模式定义,你可以看到每个person元素需要一个name,每个人的名字必须包括一个family名和一个given名。员工还可以选择性地拥有电子邮件地址和 URL。

  5. data/personal.xsd中,将person元素所需的电子邮件地址最小数量从0更改为1

    email元素的声明现在如下。

    <xs:element ref="email" minOccurs='1' maxOccurs='unbounded'/>

  6. data/personal-schema.xml中,从person元素one.worker中删除email元素。

    Worker One 现在看起来像这样:

    <person id="one.worker">
      <name><family>Worker</family> <given>One</given></name>
      <link manager="Big.Boss"/>
    </person>
    
    
  7. personal-schema.xml运行SAXLocalNameCount,不进行模式验证。

    java sax/SAXLocalNameCount data/personal-schema.xml
    
    

    SAXLocalNameCount通知你每个元素在personal-schema.xml中出现的次数。

    Local Name "email" occurs 5 times
    Local Name "name" occurs 6 times
    Local Name "person" occurs 6 times
    Local Name "family" occurs 6 times
    Local Name "link" occurs 6 times
    Local Name "personnel" occurs 1 times
    Local Name "given" occurs 6 times
    
    

    你会发现email只出现了五次,而personal-schema.xml中有六个person元素。因此,因为我们将email元素的最小出现次数设置为每个person元素为 1,我们知道这个文档是无效的。然而,因为SAXLocalNameCount没有被告知要根据模式验证,所以没有报告错误。

  8. 再次运行SAXLocalNameCount,这次指定personal-schema.xml文档应该根据personal.xsd模式定义进行验证。

    正如你在上面看到的使用 XML 模式验证,SAXLocalNameCount有一个选项可以启用模式验证。使用以下命令运行SAXLocalNameCount

    java sax/SAXLocalNameCount -xsd data/personal-schema.xml
    
    

    这次,你将看到以下错误消息。

    Exception in thread "main" org.xml.sax.SAXException: Error: 
    URI=file:data/personal-schema.xml 
    Line=14: cvc-complex-type.2.4.a: Invalid content was found starting with 
    element 'link'. 
    One of '{email}' is expected.
    
    
  9. email元素恢复到person元素one.worker

  10. 第三次运行SAXLocalNameCount,再次指定personal-schema.xml文档应该根据personal.xsd模式定义进行验证。

    java sax/SAXLocalNameCount -xsd data/personal-schema.xml
    
    

    这次你将看到正确的输出,没有错误。

  11. 再次在文本编辑器中打开personal-schema.xml

  12. personnel元素中删除模式定义personal.xsd的声明。

    personnel元素中删除斜体代码。

    <personnel *xsi:noNamespaceSchemaLocation='personal.xsd'/*>

  13. 再次运行SAXLocalNameCount,再次指定模式验证。

    java sax/SAXLocalNameCount -xsd data/personal-schema.xml
    
    

    很明显,这不会起作用,因为尚未声明要验证 XML 文件的模式定义。你将看到以下错误。

    Exception in thread "main" org.xml.sax.SAXException: 
    Error: URI=file:data/personal-schema.xml 
    Line=2: cvc-elt.1: Cannot find the declaration of element 'personnel'.
    
    
  14. 再次运行SAXLocalNameCount,这次在命令行传递模式定义文件。

    java sax/SAXLocalNameCount -xsdss data/personal.xsd data/personal-schema.xml
    
    

    这次你使用了允许指定不在应用程序中硬编码的模式定义的SAXLocalNameCount选项。你应该看到正确的输出。

处理词法事件

原文:docs.oracle.com/javase/tutorial/jaxp/sax/events.html

到目前为止,您已经消化了许多 XML 概念,包括 DTD 和外部实体。您还学会了如何使用 SAX 解析器。本课程的其余部分涵盖了您只有在编写基于 SAX 的应用程序时才需要理解的高级主题。如果您的主要目标是编写基于 DOM 的应用程序,您可以直接跳转到文档对象模型。

您之前看到,如果您将文本写出为 XML,您需要知道是否处于 CDATA 部分中。如果是,则尖括号(<)和和号(&)应保持不变输出。但如果不在 CDATA 部分中,则应将它们替换为预定义的实体&lt;&amp;。但是您如何知道自己是否在处理 CDATA 部分?

另一方面,如果您以某种方式过滤 XML,您希望传递注释。通常解析器会忽略注释。您如何获取注释以便可以回显它们?

本节回答了这些问题。它向您展示了如何使用org.xml.sax.ext.LexicalHandler来识别注释、CDATA 部分和对解析实体的引用。

注释、CDATA 标记和对解析实体的引用构成词法信息-即,涉及 XML 文本本身而不是 XML 信息内容的信息。当然,大多数应用程序只关注 XML 文档的内容。这些应用程序将不使用LexicalEventListener API。但是输出 XML 文本的应用程序会发现它非常有价值。


注意 - 词法事件处理是一个可选的解析器功能。解析器实现不需要支持它。(参考实现是这样的。)本讨论假定您的解析器支持它。


LexicalHandler的工作原理

要在 SAX 解析器看到词法信息时得到通知,您需要使用LexicalHandler配置解析器底层的XmlReaderLexicalHandler接口定义了以下事件处理方法。

comment(String comment)

将注释传递给应用程序。

startCDATA(), endCDATA()

告诉您 CDATA 部分何时开始和结束,这告诉您的应用程序下次调用characters()时可以期望什么样的字符。

startEntity(String name), endEntity(String name)

给出解析实体的名称。

startDTD(String name, String publicId, String systemId), endDTD()

告诉您正在处理 DTD,并标识它。

要激活词法处理程序,您的应用程序必须扩展DefaultHandler并实现LexicalHandler接口。然后,您必须配置您的XMLReader实例,使解析器委托给它,并配置它将词法事件发送到您的词法处理程序,如下所示。

// ...

SAXParser saxParser = factory.newSAXParser();
XMLReader xmlReader = saxParser.getXMLReader();
xmlReader.setProperty("http://xml.org/sax/properties/lexical-handler",
                      handler); 
// ...

在这里,您可以使用XMLReader类中定义的setProperty()方法来配置XMLReader。作为 SAX 标准的一部分定义的属性名称是 URN,http://xml.org/sax/properties/lexical-handler

最后,添加类似以下代码来定义将实现接口的适当方法。

// ...

public void warning(SAXParseException err) {
    // ...
}

public void comment(char[] ch, int start, int length) throws SAXException {
    // ...   
}

public void startCDATA() throws SAXException {
    // ...
}

public void endCDATA() throws SAXException {
    // ...
}

public void startEntity(String name) throws SAXException {
    // ...
}

public void endEntity(String name) throws SAXException {
    // ...
}

public void startDTD(String name, String publicId, String systemId)
    throws SAXException {
    // ...
}

public void endDTD() throws SAXException {
    // ...
}

private void echoText() {
    // ...
}

// ...

这段代码将把您的解析应用程序转换为一个词法处理程序。剩下的就是为这些新方法中的每一个指定一个要执行的操作。

使用 DTDHandler 和 EntityResolver

原文:docs.oracle.com/javase/tutorial/jaxp/sax/using.html

本节介绍了另外两个 SAX 事件处理程序:DTDHandlerEntityResolver。当 DTD 遇到未解析的实体或符号声明时,将调用DTDHandler。当需要将 URN(公共 ID)解析为 URL(系统 ID)时,将使用EntityResolver

DTDHandler API

选择解析器实现展示了引用包含二进制数据(如图像文件)的文件的方法,使用 MIME 数据类型。这是最简单、最可扩展的机制。但是,为了与旧的 SGML 样式数据兼容,也可以定义未解析的实体。

NDATA关键字定义了一个未解析的实体:

<!ENTITY myEntity SYSTEM "..URL.." NDATA gif>

NDATA关键字表示此实体中的数据不是可解析的 XML 数据,而是使用其他符号的数据。在本例中,符号被命名为gif。然后 DTD 必须包含该符号的声明,类似于以下内容。

<!NOTATION gif SYSTEM "..URL..">

当解析器看到未解析的实体或符号声明时,除了将其传递给应用程序使用DTDHandler接口外,它不会对信息做任何处理。该接口定义了两个方法。

  • notationDecl(String name, String publicId, String systemId)

  • unparsedEntityDecl(String name, String publicId, String systemId, String notationName

notationDecl方法传递符号的名称和公共或系统标识符,或两者,取决于 DTD 中声明了哪个。unparsedEntityDecl方法传递实体的名称、适当的标识符和它使用的符号的名称。


注意 - DTDHandler接口由DefaultHandler类实现。


符号也可以用于属性声明。例如,以下声明需要 GIF 和 PNG 图像文件格式的符号。

<!ENTITY image EMPTY>
<!ATTLIST image ...  type  NOTATION  (gif | png) "gif">

在这里,类型声明为 gif 或 png。如果没有指定,则默认为 gif。

无论符号引用用于描述未解析的实体还是属性,都由应用程序进行适当处理。解析器对符号的语义一无所知。它只传递声明。

EntityResolver API

EntityResolver API 允许您将公共 ID(URN)转换为系统 ID(URL)。例如,您的应用程序可能需要将类似href="urn:/someName"的内容转换为"http://someURL"

EntityResolver接口定义了一个方法:

resolveEntity(String publicId, String systemId)

这种方法返回一个InputSource对象,可以用来访问实体的内容。将 URL 转换为InputSource很容易。但作为系统 ID 传递的 URL 很可能是原始文档的位置,而这个位置很可能在网络上的某个地方。要访问本地副本(如果有的话),必须在系统的某处维护一个目录,将名称(公共 ID)映射到本地 URL。

更多信息

原文:docs.oracle.com/javase/tutorial/jaxp/sax/info.html

以下链接提供了关于本课程中介绍的技术的进一步有用信息。

  • 有关 SAX 标准的更多信息,请参见SAX 标准页面

    www.saxproject.org.

  • 有关 StAX 拉解析器的更多信息,请参见:

    Java 社区流程页面:

    jcp.org/en/jsr/detail?id=173.

    Elliot Rusty Harold 的介绍:

    www.xml.com/pub/a/2003/09/17/stax.html.

  • 有关基于模式的验证机制的更多信息,请参见

    W3C 标准验证机制,XML Schema:

    www.w3.org/XML/Schema.

    RELAX NG 的基于正则表达式的验证机制:

    绿洲 Relax NG TC.

    Schematron 基于断言的验证机制:

    www.ascc.net/xml/resource/schematron/schematron.html.

课程:文档对象模型

原文:docs.oracle.com/javase/tutorial/jaxp/dom/index.html

这节课介绍了文档对象模型(DOM)。DOM 是一种标准的树结构,其中每个节点包含 XML 结构中的一个组件。最常见的节点类型是元素节点和文本节点。使用 DOM 函数可以创建节点,删除节点,更改它们的内容,并遍历节点层次结构。

这节课的示例演示了如何解析现有的 XML 文件以构建 DOM,显示和检查 DOM 层次结构,并探索命名空间的语法。它还展示了如何从头开始创建 DOM,并了解如何使用 Sun 的 JAXP 实现中的一些特定于实现的功能将现有数据集转换为 XML。

何时使用 DOM

原文:docs.oracle.com/javase/tutorial/jaxp/dom/when.html

文档对象模型标准首先是为文档(例如文章和书籍)设计的。此外,JAXP 1.4.2 实现支持 XML Schema,这对于任何特定应用程序都可能是一个重要考虑因素。

另一方面,如果您处理简单的数据结构,且 XML Schema 不是您计划的重要部分,那么您可能会发现更适合您目的的是 JDOM 或 dom4j 等更面向对象的标准之一。

从一开始,DOM 旨在是与语言无关的。由于它是为诸如 C 和 Perl 之类的语言设计的,DOM 并没有利用 Java 的面向对象特性。这一事实,加上文档和数据之间的区别,也有助于解释处理 DOM 与处理 JDOM 或 dom4j 结构之间的差异。

在本节中,我们将研究这些标准背后的模型之间的差异,以帮助您选择最适合您应用程序的标准。

文档与数据

DOM 中使用的文档模型与 JDOM 或 dom4j 中使用的数据模型之间的主要差异在于:

  • 存在于层次结构中的节点类型

  • 混合内容的能力

主要是数据层次结构中的“节点”构成的差异主要导致了使用这两种模型进行编程的差异。然而,与其他任何因素相比,混合内容的能力最能解释标准如何定义节点的差异。因此,我们首先来看一下 DOM 的混合内容模型。

混合内容模型

在 DOM 层次结构中,文本和元素可以自由混合。这种结构称为 DOM 模型中的混合内容。

文档中经常出现混合内容。例如,假设您想要表示这种结构:

<sentence>这是一个<bold>重要</bold>的想法。</sentence>

DOM 节点的层次结构可能如下所示,其中每行代表一个节点:

ELEMENT: sentence
   + TEXT: This is an
   + ELEMENT: bold
     + TEXT: important
   + TEXT: idea.

请注意,sentence 元素包含文本,然后是一个子元素,然后是额外的文本。文本和元素的混合定义了混合内容模型。

节点类型

为了提供混合内容的能力,DOM 节点本质上非常简单。在上述示例中,第一个元素的“内容”(其值)只是标识它是什么类型的节点。

第一次使用 DOM 的用户通常会被这个事实搞糊涂。在导航到<sentence>节点后,他们要求节点的“内容”,并期望得到一些有用的东西。相反,他们只能找到元素的名称,sentence


注意 - DOM 节点 API 定义了nodeValue()nodeType()nodeName()方法。对于第一个元素节点,nodeName()返回sentence,而nodeValue()返回 null。对于第一个文本节点,nodeName()返回#text,而nodeValue()返回“This is an”。重要的一点是,元素的与其内容不同。


在上面的例子中,询问“句子”的“文本”是什么意思?根据您的应用程序,以下任何一种都可能是合理的:

  • 这是一个

  • 这是一个想法。

  • 这是一个重要的想法。

  • 这是一个重要的想法。

一个更简单的模型

使用 DOM,您可以自由创建所需的语义。但是,您还需要进行必要的处理以实现这些语义。另一方面,像 JDOM 和 dom4j 这样的标准使得执行简单任务变得更容易,因为层次结构中的每个节点都是一个对象。

尽管 JDOM 和 dom4j 允许元素具有混合内容,但它们并非主要设计用于这种情况。相反,它们针对的是 XML 结构包含数据的应用程序。

数据结构中的元素通常只包含文本或其他元素,而不是两者兼有。例如,这里是代表简单地址簿的一些 XML:

<addressbook>
    <entry>
        <name>Fred</name>
        <email>fred@home</email>
    </entry>
      ...
</addressbook>


注意 - 对于像这样非常简单的 XML 数据结构,您还可以使用内置在 Java 平台 1.4 版本中的正则表达式包(java.util.regex)。


在 JDOM 和 dom4j 中,当您导航到包含文本的元素后,您可以调用诸如text()之类的方法来获取其内容。但是,在处理 DOM 时,您必须检查子元素列表以“组合”节点的文本,就像您之前看到的那样 - 即使该列表只包含一个项目(TEXT 节点)。

因此,对于简单的数据结构,比如地址簿,您可以通过使用 JDOM 或 dom4j 来节省一些工作量。即使数据在技术上是“混合的”,但在给定节点中始终只有一个(且仅有一个)文本段落时,使用其中一个模型可能是有意义的。

这是一个这种结构的示例,也可以很容易地在 JDOM 或 dom4j 中处理:

<addressbook>
    <entry>Fred
        <email>fred@home</email>
    </entry>
      ...
</addressbook>

在这里,每个条目都有一些标识性文本,后面跟着其他元素。有了这种结构,程序可以导航到一个条目,调用text()来找出它属于谁,并在正确的节点处处理<email>子元素。

增加复杂性

但是,为了全面了解在搜索或操作 DOM 时需要执行的处理类型,了解 DOM 可能包含的节点类型是很重要的。

这里有一个说明这一点的示例。这是这些数据的表示:

<sentence>
    The &projectName; <![CDATA[<i>project</i>]]> is
    <?editor: red><bold>important</bold><?editor: normal>.
</sentence>

这个句子包含一个实体引用 - 指向在其他地方定义的实体的指针。在这种情况下,实体包含项目的名称。示例还包含一个 CDATA 部分(未解释的数据,类似于 HTML 中的 <pre> 数据)以及处理指令<?...?>),在这种情况下告诉编辑器在呈现文本时使用的颜色。

这是该数据的 DOM 结构。它代表了一个健壮应用程序应该准备处理的结构类型:

+ ELEMENT: sentence
       + TEXT: The
       + ENTITY REF: projectName
        + COMMENT: 
        The latest name we are using
        + TEXT: Eagle
       + CDATA: <i>project</i>
       + TEXT: is
       + PI: editor: red
       + ELEMENT: bold
          + TEXT: important
       + PI: editor: normal

这个例子描述了 DOM 中可能出现的节点类型。尽管你的应用程序可能大部分时间都能忽略它们,但一个真正健壮的实现需要识别和处理每一个节点。

类似地,导航到一个节点的过程涉及处理子元素,忽略你不感兴趣的元素并检查你感兴趣的元素,直到找到你感兴趣的节点。

一个处理固定、内部生成数据的程序可以承担简化假设:处理指令、注释、CDATA 节点和实体引用在数据结构中不存在。但是真正健壮的应用程序,尤其是处理来自外部世界的各种数据的应用程序,必须准备处理所有可能的 XML 实体。

(一个“简单”的应用程序只能在输入数据包含它所期望的简化 XML 结构时工作。但是没有验证机制来确保更复杂的结构不存在。毕竟,XML 的设计目的就是允许它们存在。)

为了更加健壮,DOM 应用程序必须做到以下几点:

  1. 在搜索元素时:

    1. 忽略注释、属性和处理指令。

    2. 允许子元素不按预期顺序出现的可能性。

    3. 如果不进行验证,则跳过包含可忽略空格的 TEXT 节点。

  2. 在提取节点的文本时:

    1. 从 CDATA 节点以及文本节点提取文本。

    2. 在收集文本时忽略注释、属性和处理指令。

    3. 如果遇到实体引用节点或另一个元素节点,则递归(即对所有子节点应用文本提取过程)。

当然,许多应用程序不必担心这些事情,因为它们看到的数据类型将受到严格控制。但如果数据可能来自各种外部来源,那么应用程序可能需要考虑这些可能性。

执行这些功能所需的代码在本课程的末尾的 搜索节点 和 获取节点内容 中给出。现在,目标只是确定 DOM 是否适合你的应用程序。

选择你的模型

正如您所见,当您使用 DOM 时,即使是从节点获取文本这样的简单操作也需要一些编程。因此,如果您的程序处理简单的数据结构,那么 JDOM、dom4j,甚至 1.4 版本的正则表达式包(java.util.regex)可能更适合您的需求。

另一方面,对于完整的文档和复杂的应用程序,DOM 为您提供了很大的灵活性。如果需要使用 XML Schema,那么再次选择 DOM 是明智之举 - 至少目前是这样。

如果您在开发的应用程序中处理文档和数据,那么 DOM 可能仍然是您最佳选择。毕竟,一旦编写了用于检查和处理 DOM 结构的代码,就很容易为特定目的定制它。因此,选择在 DOM 中执行所有操作意味着您只需处理一组 API,而不是两组。

此外,DOM 标准是内存中文档模型的规范标准。它功能强大且稳健,并且有许多实现。这对许多大型安装来说是一个重要的决策因素,特别是对于需要尽量减少由 API 更改造成的成本的大型应用程序。

最后,即使通讯录中的文本今天可能不允许粗体、斜体、颜色和字体大小,但将来您可能会希望处理这些内容。因为 DOM 能处理几乎任何您提出的要求,选择 DOM 可以更轻松地使您的应用程序具备未来的可扩展性。

将 XML 数据读入 DOM

原文:docs.oracle.com/javase/tutorial/jaxp/dom/readingXML.html

在本节中,您将通过读取现有的 XML 文件构造一个文档对象模型。


注意 - 在可扩展样式表语言转换中,您将看到如何将 DOM 写出为 XML 文件。(您还将看到如何相对容易地将现有数据文件转换为 XML。)


创建程序

文档对象模型提供了让您创建、修改、删除和重新排列节点的 API。在尝试创建 DOM 之前,了解 DOM 的结构是很有帮助的。这一系列示例将通过一个名为DOMEcho的示例程序展示 DOM 的内部结构,您可以在安装了 JAXP API 后在目录*INSTALL_DIR*/jaxp-*version*/samples/dom中找到它。

创建骨架

首先,构建一个简单的程序,将 XML 文档读入 DOM,然后再将其写回。

从应用程序的正常基本逻辑开始,并检查确保命令行上已提供了参数:

public class DOMEcho {

    static final String outputEncoding = "UTF-8";

    private static void usage() {
        // ...
    }

    public static void main(String[] args) throws Exception {
        String filename = null;

        for (int i = 0; i < args.length; i++) {
            if (...) { 
                // ...
            } 
            else {
                filename = args[i];
                if (i != args.length - 1) {
                    usage();
                }
            }
        }

        if (filename == null) {
            usage();
        }
    }
}

此代码执行所有基本的设置操作。DOMEcho的所有输出都使用 UTF-8 编码。如果未指定参数,则调用usage()方法会简单地告诉您DOMEcho期望的参数,因此此处不显示代码。还声明了一个filename字符串,它将是要由DOMEcho解析为 DOM 的 XML 文件的名称。

导入所需的类

在本节中,所有类都以单独命名,以便您可以看到每个类来自何处,以便在需要引用 API 文档时参考。在示例文件中,导入语句使用较短的形式,如javax.xml.parsers.*

这些是DOMEcho使用的 JAXP API:

package dom;
import javax.xml.parsers.DocumentBuilder; 
import javax.xml.parsers.DocumentBuilderFactory;

这些类用于在解析 XML 文档时可能抛出的异常:

import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException; 
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.*

这些类读取示例 XML 文件并管理输出:

import java.io.File;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;

最后,导入 W3C 定义的 DOM、DOM 异常、实体和节点:

import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Entity;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;

处理错误

接下来,添加错误处理逻辑。最重要的一点是,当 JAXP 符合标准的文档构建器在解析 XML 文档时遇到问题时,需要报告 SAX 异常。DOM 解析器实际上不必在内部使用 SAX 解析器,但由于 SAX 标准已经存在,因此使用它来报告错误是有意义的。因此,DOM 应用程序的错误处理代码与 SAX 应用程序的错误处理代码非常相似:

private static class MyErrorHandler implements ErrorHandler {

    private PrintWriter out;

    MyErrorHandler(PrintWriter out) {
        this.out = out;
    }

    private String getParseExceptionInfo(SAXParseException spe) {
        String systemId = spe.getSystemId();
        if (systemId == null) {
            systemId = "null";
        }

        String info = "URI=" + systemId + " Line=" + spe.getLineNumber() +
                      ": " + spe.getMessage();
        return info;
    }

    public void warning(SAXParseException spe) throws SAXException {
        out.println("Warning: " + getParseExceptionInfo(spe));
    }

    public void error(SAXParseException spe) throws SAXException {
        String message = "Error: " + getParseExceptionInfo(spe);
        throw new SAXException(message);
    }

    public void fatalError(SAXParseException spe) throws SAXException {
        String message = "Fatal Error: " + getParseExceptionInfo(spe);
        throw new SAXException(message);
    }
}

正如您所看到的,DomEcho类的错误处理程序使用PrintWriter实例生成其输出。

实例化工厂

接下来,在main()方法中添加以下代码,以获取一个可以提供文档构建器的工厂实例。

public static void main(String[] args) throws Exception {
    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

    // ...
}

获取解析器并解析文件

现在,在main()中添加以下代码以获取一个构建器实例,并使用它来解析指定的文件。

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder(); 
Document doc = db.parse(new File(filename));

被解析的文件由在 main() 方法开头声明的 filename 变量提供,当程序运行时,它作为参数传递给 DOMEcho

配置工厂

默认情况下,工厂返回一个不进行验证的解析器,不了解命名空间。要获得一个验证解析器,或者一个了解命名空间的解析器(或两者兼有),您可以配置工厂来设置这两个选项中的一个或两个,使用以下代码。

public static void main(String[] args) throws Exception {

    String filename = null;
    boolean dtdValidate = false;
    boolean xsdValidate = false;
    String schemaSource = null;

    for (int i = 0; i < args.length; i++) {
        if (args[i].equals("-dtd"))  { 
            dtdValidate = true;
        } 
        else if (args[i].equals("-xsd")) {
            xsdValidate = true;
        } 
        else if (args[i].equals("-xsdss")) {
            if (i == args.length - 1) {
                usage();
            }
            xsdValidate = true;
            schemaSource = args[++i];
        }
        else {
            filename = args[i];
            if (i != args.length - 1) {
                usage();
            }
        }
    }

    if (filename == null) {
        usage();
    }

    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

    dbf.setNamespaceAware(true);
    dbf.setValidating(dtdValidate || xsdValidate);

    // ...

    DocumentBuilder db = dbf.newDocumentBuilder();
    Document doc = db.parse(new File(filename));
}

如您所见,命令行参数已设置好,以便您可以通知 DOMEcho 对 DTD 或 XML Schema 执行验证,并且工厂已配置为了解命名空间并执行用户指定的验证类型。


注意 - 符合 JAXP 标准的解析器并不需要支持所有这些选项的所有组合,即使参考解析器支持。如果您指定了无效的选项组合,在尝试获取解析器实例时,工厂会生成一个 ParserConfigurationException


有关如何使用命名空间和验证的更多信息,请参阅使用 XML Schema 进行验证,其中将描述上述摘录中缺失的代码。

处理验证错误

根据 SAX 标准规定,对验证错误的默认响应是不执行任何操作。JAXP 标准要求抛出 SAX 异常,因此您使用与 SAX 应用程序相同的错误处理机制。特别是,您使用 DocumentBuilder 类的 setErrorHandler 方法来提供一个实现 SAX ErrorHandler 接口的对象。


注意 - DocumentBuilder 还有一个 setEntityResolver 方法可供使用。


以下代码配置文档构建器使用在处理错误中定义的错误处理程序。

DocumentBuilder db = dbf.newDocumentBuilder();
OutputStreamWriter errorWriter = new OutputStreamWriter(System.err,
                                         outputEncoding);
db.setErrorHandler(new MyErrorHandler (new PrintWriter(errorWriter, true)));
Document doc = db.parse(new File(filename));

到目前为止,您看到的代码已经设置了文档构建器,并配置它在请求时执行验证。错误处理也已就位。然而,DOMEcho 还没有做任何事情。在下一节中,您将看到如何显示 DOM 结构并开始探索它。例如,您将看到在 DOM 中实体引用和 CDATA 部分的样子。也许最重要的是,您将看到文本节点(包含实际数据)如何存在于 DOM 中的元素节点下。

显示 DOM 节点

要创建或操作 DOM,有一个清晰的关于 DOM 中节点结构的概念是很有帮助的。本教程的这一部分揭示了 DOM 的内部结构,这样你就可以看到它包含的内容。DOMEcho 示例通过回显 DOM 节点,然后在屏幕上打印出来,适当缩进以使节点层次结构明显可见。这些节点类型的规范可以在DOM Level 2 Core Specification中找到,在Node规范下。下面的表 3-1 是从该规范中调整过来的。

表 3-1 节点类型

Node 节点名称 节点值 属性
Attr 属性名称 属性值 null
CDATASection #cdata-section CDATA 部分的内容 null
Comment #comment 注释的内容 null
Document #document null null
DocumentFragment #documentFragment null null
DocumentType 文档类型名称 null null
Element 标签名称 null null
Entity 实体名称 null null
EntityReference 引用的实体名称 null null
Notation 符号名称 null null
ProcessingInstruction 目标 不包括目标的整个内容 null
Text #text 文本节点的内容 null

此表中的信息非常有用;在处理 DOM 时,你将需要它,因为所有这些类型都混合在 DOM 树中。

获取节点类型信息

通过调用org.w3c.dom.Node类的各种方法来获取 DOM 节点元素类型信息。DOMEcho暴露的节点属性由以下代码回显。

private void printlnCommon(Node n) {
    out.print(" nodeName=\"" + n.getNodeName() + "\"");

    String val = n.getNamespaceURI();
    if (val != null) {
        out.print(" uri=\"" + val + "\"");
    }

    val = n.getPrefix();

    if (val != null) {
        out.print(" pre=\"" + val + "\"");
    }

    val = n.getLocalName();
    if (val != null) {
        out.print(" local=\"" + val + "\"");
    }

    val = n.getNodeValue();
    if (val != null) {
        out.print(" nodeValue=");
        if (val.trim().equals("")) {
            // Whitespace
            out.print("[WS]");
        }
        else {
            out.print("\"" + n.getNodeValue() + "\"");
        }
    }
    out.println();
}

每个 DOM 节点至少有一个类型、一个名称和一个值,这个值可能为空也可能不为空。在上面的示例中,Node接口的getNamespaceURI()getPrefix()getLocalName()getNodeValue()方法返回并打印回显节点的命名空间 URI、命名空间前缀、本地限定名称和值。请注意,对getNodeValue()返回的值调用trim()方法,以确定节点的值是否为空白字符,并相应地打印消息。

要查看Node方法的完整列表以及它们返回的不同信息,请参阅Node的 API 文档。

接下来,定义一个方法来设置节点打印时的缩进,以便节点层次结构能够清晰可见。

private void outputIndentation() {
    for (int i = 0; i < indent; i++) {
        out.print(basicIndent);
    }
}

DOMEcho显示节点树层次结构时,使用的基本缩进单位由DOMEcho构造函数类中添加以下突出显示的行来定义basicIndent常量。

public class DOMEcho {
    static final String outputEncoding = "UTF-8";

    private PrintWriter out;
    private int indent = 0;
    private final String basicIndent = " ";

    DOMEcho(PrintWriter out) {
        this.out = out;
    }
}

就像在处理错误中定义的错误处理程序一样,DOMEcho 程序将创建其输出作为 PrintWriter 实例。

词法控制

词法信息是您需要重建 XML 文档原始语法的信息。在编辑应用程序中保留词法信息非常重要,因为您希望保存的文档是对原始文档的准确反映-包括注释、实体引用以及一开始可能包含的任何 CDATA 部分。

然而,大多数应用程序只关注 XML 结构的内容。它们可以忽略注释,并且不在乎数据是在 CDATA 部分中编码还是作为纯文本,或者是否包含实体引用。对于这类应用程序,最好保留最少的词法信息,因为这简化了应用程序必须准备检查的 DOM 节点的数量和类型。

以下DocumentBuilderFactory方法让您控制在 DOM 中看到的词法信息。

setCoalescing()

CDATA节点转换为Text节点并附加到相邻的Text节点(如果有)。

setExpandEntityReferences()

为了扩展实体引用节点。

setIgnoringComments()

忽略注释。

setIgnoringElementContentWhitespace()

忽略不是元素内容的空白。

所有这些属性的默认值都是 false,这保留了重建传入文档所需的所有词法信息,以其原始形式。将它们设置为 true 可以构建最简单的 DOM,以便应用程序可以专注于数据的语义内容,而不必担心词法语法细节。表 3-2 总结了设置的效果。

表 3-2 词法控制设置

API 保留词法信息 关注内容
setCoalescing() False True
setExpandEntityReferences() False True
setIgnoringComments() False True
setIgnoringElementContent``Whitespace() False True

这些方法在DomEcho示例的主方法中的实现如下所示。

// ...

dbf.setIgnoringComments(ignoreComments);
dbf.setIgnoringElementContentWhitespace(ignoreWhitespace);
dbf.setCoalescing(putCDATAIntoText);
dbf.setExpandEntityReferences(!createEntityRefs);

// ...

布尔变量ignoreCommentsignoreWhitespaceputCDATAIntoTextcreateEntityRefs在主方法代码的开头声明,并且当运行DomEcho时通过命令行参数设置。

public static void main(String[] args) throws Exception {
    // ...

    boolean ignoreWhitespace = false;
    boolean ignoreComments = false;
    boolean putCDATAIntoText = false;
    boolean createEntityRefs = false;

    for (int i = 0; i < args.length; i++) {
        if (...) {  // Validation arguments here
           // ... 
        } 
        else if (args[i].equals("-ws")) {
            ignoreWhitespace = true;
        } 
        else if (args[i].startsWith("-co")) {
            ignoreComments = true;
        }
        else if (args[i].startsWith("-cd")) {
            putCDATAIntoText = true;
        } 
        else if (args[i].startsWith("-e")) {
            createEntityRefs = true;

            // ...
        } 
        else {
            filename = args[i];

            // Must be last arg
            if (i != args.length - 1) {
                usage();
            }
        }
    }

    // ...
}

打印 DOM 树节点

DomEcho应用程序允许您查看 DOM 的结构,并演示了 DOM 由哪些节点组成以及它们是如何排列的。一般来说,DOM 树中绝大多数节点将是ElementText节点。


注意 - 文本节点存在于 DOM 中的元素节点下方,数据始终存储在文本节点中。在 DOM 处理中最常见的错误可能是导航到元素节点并期望它包含存储在该元素中的数据。事实并非如此!即使是最简单的元素节点下面也有一个包含数据的文本节点。


打印 DOM 树节点的代码以适当的缩进显示如下。

private void echo(Node n) {
    outputIndentation();
    int type = n.getNodeType();

    switch (type) {
        case Node.ATTRIBUTE_NODE:
            out.print("ATTR:");
            printlnCommon(n);
            break;

        case Node.CDATA_SECTION_NODE:
            out.print("CDATA:");
            printlnCommon(n);
            break;

        case Node.COMMENT_NODE:
            out.print("COMM:");
            printlnCommon(n);
            break;

        case Node.DOCUMENT_FRAGMENT_NODE:
            out.print("DOC_FRAG:");
            printlnCommon(n);
            break;

        case Node.DOCUMENT_NODE:
            out.print("DOC:");
            printlnCommon(n);
            break;

        case Node.DOCUMENT_TYPE_NODE:
            out.print("DOC_TYPE:");
            printlnCommon(n);
            NamedNodeMap nodeMap = ((DocumentType)n).getEntities();
            indent += 2;
            for (int i = 0; i < nodeMap.getLength(); i++) {
                Entity entity = (Entity)nodeMap.item(i);
                echo(entity);
            }
            indent -= 2;
            break;

        case Node.ELEMENT_NODE:
            out.print("ELEM:");
            printlnCommon(n);

            NamedNodeMap atts = n.getAttributes();
            indent += 2;
            for (int i = 0; i < atts.getLength(); i++) {
                Node att = atts.item(i);
                echo(att);
            }
            indent -= 2;
            break;

        case Node.ENTITY_NODE:
            out.print("ENT:");
            printlnCommon(n);
            break;

        case Node.ENTITY_REFERENCE_NODE:
            out.print("ENT_REF:");
            printlnCommon(n);
            break;

        case Node.NOTATION_NODE:
            out.print("NOTATION:");
            printlnCommon(n);
            break;

        case Node.PROCESSING_INSTRUCTION_NODE:
            out.print("PROC_INST:");
            printlnCommon(n);
            break;

        case Node.TEXT_NODE:
            out.print("TEXT:");
            printlnCommon(n);
            break;

        default:
            out.print("UNSUPPORTED NODE: " + type);
            printlnCommon(n);
            break;
    }

    indent++;
    for (Node child = n.getFirstChild(); child != null;
         child = child.getNextSibling()) {
        echo(child);
    }
    indent--;
}

该代码首先使用 switch 语句打印出不同的节点类型和任何可能的子节点,并进行适当的缩进。

节点属性不包括在 DOM 层次结构的子节点中。而是通过Node接口的getAttributes方法获取。

DocType接口是w3c.org.dom.Node的扩展。它定义了getEntities方法,您可以使用该方法获取Entity节点 - 定义实体的节点。与Attribute节点一样,Entity节点不会出现为 DOM 节点的子节点。

节点操作

本节简要介绍了您可能想要应用于 DOM 的一些操作。

  • 创建节点

  • 遍历节点

  • 搜索节点

  • 获取节点内容

  • 创建属性

  • 删除和更改节点

  • 插入节点

创建节点

您可以使用Document接口的方法创建不同类型的节点。例如,createElementcreateCommentcreateCDATAsectioncreateTextNode等。有关创建不同节点的方法的完整列表,请参阅org.w3c.dom.Document的 API 文档。

遍历节点

org.w3c.dom.Node接口定义了一些方法,您可以使用这些方法遍历节点,包括getFirstChildgetLastChildgetNextSiblinggetPreviousSiblinggetParentNode。这些操作足以从树中的任何位置到达树中的任何其他位置。

搜索节点

当您搜索具有特定名称的节点时,需要考虑更多因素。虽然诱人的做法是获取第一个子节点并检查它是否正确,但搜索必须考虑到子列表中的第一个子节点可能是注释或处理指令。如果 XML 数据尚未经过验证,甚至可能是包含可忽略空格的文本节点。

本质上,您需要查看子节点列表,忽略那些不相关的节点,并检查您关心的节点。以下是在 DOM 层次结构中搜索节点时需要编写的一种例程。它在这里完整呈现(包括注释),以便您可以将其用作应用程序中的模板。

/**
 * Find the named subnode in a node's sublist.
 * <ul>
 * <li>Ignores comments and processing instructions.
 * <li>Ignores TEXT nodes (likely to exist and contain
 *         ignorable whitespace, if not validating.
 * <li>Ignores CDATA nodes and EntityRef nodes.
 * <li>Examines element nodes to find one with
 *        the specified name.
 * </ul>
 * @param name  the tag name for the element to find
 * @param node  the element node to start searching from
 * @return the Node found
 */
public Node findSubNode(String name, Node node) {
    if (node.getNodeType() != Node.ELEMENT_NODE) {
        System.err.println("Error: Search node not of element type");
        System.exit(22);
    }

    if (! node.hasChildNodes()) return null;

    NodeList list = node.getChildNodes();
    for (int i=0; i < list.getLength(); i++) {
        Node subnode = list.item(i);
        if (subnode.getNodeType() == Node.ELEMENT_NODE) {
           if (subnode.getNodeName().equals(name)) 
               return subnode;
        }
    }
    return null;
}

要深入了解此代码,请参阅增加复杂性中的何时使用 DOM。此外,您还可以使用词法控制中描述的 API 来修改解析器构造的 DOM 类型。不过,这段代码的好处是几乎适用于任何 DOM。

获取节点内容

当您想要获取节点包含的文本时,您需要再次查看子节点列表,忽略不相关的条目,并在TEXT节点、CDATA节点和EntityRef节点中找到的文本累积起来。以下是您可以用于该过程的一种例程。

/**
  * Return the text that a node contains. This routine:
  * <ul>
  * <li>Ignores comments and processing instructions.
  * <li>Concatenates TEXT nodes, CDATA nodes, and the results of
  *     recursively processing EntityRef nodes.
  * <li>Ignores any element nodes in the sublist.
  *     (Other possible options are to recurse into element 
  *      sublists or throw an exception.)
  * </ul>
  * @param    node  a  DOM node
  * @return   a String representing its contents
  */
public String getText(Node node) {
    StringBuffer result = new StringBuffer();
    if (! node.hasChildNodes()) return "";

    NodeList list = node.getChildNodes();
    for (int i=0; i < list.getLength(); i++) {
        Node subnode = list.item(i);
        if (subnode.getNodeType() == Node.TEXT_NODE) {
            result.append(subnode.getNodeValue());
        }
        else if (subnode.getNodeType() == Node.CDATA_SECTION_NODE) {
            result.append(subnode.getNodeValue());
        }
        else if (subnode.getNodeType() == Node.ENTITY_REFERENCE_NODE) {
            // Recurse into the subtree for text
            // (and ignore comments)
            result.append(getText(subnode));
        }
    }

    return result.toString();
}

关于这段代码的更深入解释,请参见增加复杂性中的何时使用 DOM。同样,你可以通过使用词法控制中描述的 API 来简化这段代码,以修改解析器构造的 DOM 类型。但这段代码的好处是几乎适用于任何 DOM。

创建属性

扩展了 Node 接口的org.w3c.dom.Element接口定义了一个setAttribute操作,用于向该节点添加属性。(从 Java 平台的角度来看,更好的名称应该是addAttribute。该属性不是类的属性,而是创建了一个新对象。)你还可以使用DocumentcreateAttribute操作来创建Attribute的实例,然后使用setAttributeNode方法来添加它。

删除和更改节点

要删除一个节点,你可以使用其父节点的removeChild方法。要更改它,你可以使用父节点的replaceChild操作或节点的setNodeValue操作。

插入节点

在创建新节点时要记住的重要事情是,当你创建一个元素节点时,你只需指定一个名称。实际上,该节点给你提供了一个挂载物件的钩子。你可以通过向其子节点列表添加内容来将物件挂在钩子上。例如,你可以添加一个文本节点、一个CDATA节点或一个属性节点。在构建过程中,请记住你在本教程中看到的结构。记住:层次结构中的每个节点都非常简单,只包含一个数据元素。

运行DOMEcho示例

要运行DOMEcho示例,请按照以下步骤操作。

  1. 导航至samples目录。% cd *install-dir*/jaxp-1_4_2-*release-date*/samples.

  2. 编译示例类。% javac dom/*

  3. 在 XML 文件上运行DOMEcho程序。

    选择data目录中的一个 XML 文件,并在其上运行DOMEcho程序。在这里,我们选择在文件personal-schema.xml上运行该程序。

    % java dom/DOMEcho data/personal-schema.xml

    XML 文件personal-schema.xml包含了一个小公司的人员档案。当你在其上运行DOMEcho程序时,你应该看到以下输出。

    DOC: nodeName="#document"
     ELEM: nodeName="personnel" 
           local="personnel"
     TEXT: nodeName="#text" 
           nodeValue=[WS]
     ELEM: nodeName="person" 
           local="person"
     ATTR: nodeName="id" 
           local="id" 
           nodeValue="Big.Boss"
     TEXT: nodeName="#text" 
           nodeValue=[WS]
     ELEM: nodeName="name" 
           local="name"
     ELEM: nodeName="family" 
           local="family"
     TEXT: nodeName="#text" 
           nodeValue="Boss"
     TEXT: nodeName="#text" 
           nodeValue=[WS]
     ELEM: nodeName="given" 
           local="given"
     TEXT: nodeName="#text" 
           nodeValue="Big"
     TEXT: nodeName="#text" 
           nodeValue=[WS]
     ELEM: nodeName="email" 
           local="email"
     TEXT: nodeName="#text" 
           nodeValue="chief@foo.example.com"
     TEXT: nodeName="#text" 
           nodeValue=[WS]
     ELEM: nodeName="link" 
           local="link"
     ATTR: nodeName="subordinates" 
           local="subordinates" 
           nodeValue="one.worker two.worker 
                      three.worker four.worker
                      five.worker"
     TEXT: nodeName="#text" 
           nodeValue=[WS]
     TEXT: nodeName="#text" 
           nodeValue=[WS]
     ELEM: nodeName="person" 
           local="person"
     ATTR: nodeName="id" 
           local="id" 
           nodeValue="one.worker"
     TEXT: nodeName="#text" 
           nodeValue=[WS]
     ELEM: nodeName="name" 
           local="name"
     ELEM: nodeName="family" 
           local="family"
     TEXT: nodeName="#text" 
           nodeValue="Worker"
     TEXT: nodeName="#text" 
           nodeValue=[WS]
     ELEM: nodeName="given" 
           local="given"
     TEXT: nodeName="#text" 
           nodeValue="One"
     TEXT: nodeName="#text" 
           nodeValue=[WS]
     ELEM: nodeName="email" 
           local="email"
     TEXT: nodeName="#text" 
           nodeValue="one@foo.example.com"
     TEXT: nodeName="#text" 
           nodeValue=[WS]
     ELEM: nodeName="link" 
           local="link"
     ATTR: nodeName="manager" 
           local="manager" 
           nodeValue="Big.Boss"
     TEXT: nodeName="#text"
           nodeValue=[WS]
    
    [...]
    
    

    正如你所看到的,DOMEcho打印出文档中不同元素的所有节点,并正确缩进以显示节点层次结构。

使用 XML Schema 进行验证

原文:docs.oracle.com/javase/tutorial/jaxp/dom/validating.html

本节介绍了 XML Schema 验证的过程。虽然完整讨论 XML Schema 超出了本教程的范围,但本节向您展示了使用 XML Schema 定义验证 XML 文档的步骤。(要了解更多关于 XML Schema 的信息,您可以查看在线教程,XML Schema Part 0: Primer。在本节结束时,您还将学习如何使用 XML Schema 定义来验证包含来自多个命名空间的元素的文档。

验证过程概述

要在 XML 文档中通知验证错误,必须满足以下条件:

  • 必须配置工厂,并设置适当的错误处理程序。

  • 文档必须与至少一个模式相关联,可能还有更多。

配置DocumentBuilder工厂

在配置工厂时,首先定义将在其中使用的常量是很有帮助的。这些常量与在使用 XML Schema 进行 SAX 解析时定义的常量相同,并且它们在DOMEcho示例程序的开头声明。

static final String JAXP_SCHEMA_LANGUAGE =
    "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
static final String W3C_XML_SCHEMA =
    "http://www.w3.org/2001/XMLSchema";

接下来,您配置DocumentBuilderFactory以生成一个支持命名空间的、使用 XML Schema 的验证解析器。这是通过在已创建的DocumentBuilderFactory实例dbf上调用setValidating方法来完成的,该实例是在实例化工厂中创建的。

// ...

dbf.setNamespaceAware(true);
dbf.setValidating(dtdValidate || xsdValidate);

if (xsdValidate) {
    try {
        dbf.setAttribute(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA);
    }
    catch (IllegalArgumentException x) {
        System.err.println("Error: JAXP DocumentBuilderFactory attribute " 
                           + "not recognized: " + JAXP_SCHEMA_LANGUAGE);
        System.err.println("Check to see if parser conforms to JAXP spec.");
        System.exit(1);
    }
}

// ...

因为符合 JAXP 的解析器默认情况下不支持命名空间,所以必须设置模式验证属性才能正常工作。您还设置了一个工厂属性来指定要使用的解析器语言。(另一方面,对于 SAX 解析,您在工厂生成的解析器上设置一个属性)。

将文档与模式关联

现在程序已准备好使用 XML Schema 定义进行验证,只需确保 XML 文档与(至少)一个相关联即可。有两种方法可以实现这一点:

  • 在 XML 文档中有模式声明时

  • 通过指定应用程序中要使用的模式


注意 - 当应用程序指定要使用的模式时,它会覆盖文档中的任何模式声明。


要在文档中指定模式定义,您可以创建如下 XML:

<*documentRoot* xsi:noNamespaceSchemaLocation='*YourSchemaDefinition.xsd*'> [...]

第一个属性定义了 XML 命名空间(xmlns)前缀xsi,代表“XML Schema 实例”。第二行指定了要用于文档中没有命名空间前缀的元素的模式-也就是,通常在任何简单、不复杂的 XML 文档中定义的元素。(您将看到如何处理多个命名空间在下一节中。)

您还可以在应用程序中指定模式文件,这是DOMEcho的情况。

static final String JAXP_SCHEMA_SOURCE =
    "http://java.sun.com/xml/jaxp/properties/schemaSource";

// ...

dbf.setValidating(dtdValidate || xsdValidate);
if (xsdValidate) {
    // ...    
}

if (schemaSource != null) {
    dbf.setAttribute(JAXP_SCHEMA_SOURCE, new File(schemaSource));
}

在这里,您也有机制可以指定多个模式。我们将在下面看一下。

使用多个命名空间进行验证

命名空间允许您在同一文档中组合服务不同目的的元素,而无需担心重叠的名称。


注意 - 本节讨论的材料也适用于使用 SAX 解析器进行验证。您在这里看到它,是因为您已经学到足够多关于命名空间的知识,才能理解讨论的内容。


举个例子,考虑一个 XML 数据集,用于跟踪人员数据。数据集可能包括来自税务申报表和雇员入职表的信息,两个元素在各自的模式中都命名为form

如果为税务命名空间定义了一个前缀,并为雇佣命名空间定义了另一个前缀,则人员数据可能包括以下部分。

<employee id="...">
  <name>....</name>
  <tax:form>
     ...w2 tax form data...
  </tax:form>
  <hiring:form>
     ...employment history, etc....
  </hiring:form>
</employee>

tax:form元素的内容显然与hiring:form元素的内容不同,并且必须进行不同的验证。

还要注意,在此示例中存在一个默认命名空间,未限定元素名称employeename属于该命名空间。为了使文档得到正确验证,必须声明该命名空间的模式,以及taxhiring命名空间的模式。


注意 - 默认命名空间实际上是一个特定的命名空间。它被定义为“没有名称的命名空间”。因此,您不能简单地将一个命名空间用作本周的默认命名空间,然后将另一个命名空间用作以后的默认命名空间。这个“无名命名空间”(或“空命名空间”)就像数字零一样。它没有任何值(没有名称),但它仍然被精确定义。因此,具有名称的命名空间永远不能用作默认命名空间。


解析时,只要已声明这些模式,数据集中的每个元素都将根据相应的模式进行验证。同样,这些模式可以作为 XML 数据集的一部分或在程序中声明。(也可以混合声明。总的来说,最好将所有声明放在一起。)

在 XML 数据集中声明模式

要声明用于上述示例中数据集的模式,XML 代码将类似于以下内容。

<documentRoot
  xmlns:xsi=
  "http://www.w3.org/2001/XMLSchema-instance"
  xsi:noNamespaceSchemaLocation=
    "employeeDatabase.xsd"
  xsi:schemaLocation=
  "http://www.irs.gov.example.com/ 
   fullpath/w2TaxForm.xsd
   http://www.ourcompany.example.com/ 
   relpath/hiringForm.xsd"
  xmlns:tax=
    "http://www.irs.gov.example.com/"
  xmlns:hiring=
    "http://www.ourcompany.example.com/"
>

noNamespaceSchemaLocation声明是您之前见过的内容,最后两个条目也是如此,它们定义了命名空间前缀taxhiring。新的是中间的条目,它定义了文档中引用的每个命名空间要使用的模式的位置。

xsi:schemaLocation声明由条目对组成,其中每对中的第一个条目是指定命名空间的完全限定 URI,第二个条目包含模式定义的完整路径或相对路径。一般来说,建议使用完全限定路径。这样,模式只会存在一份副本。

请注意,在定义模式位置时不能使用命名空间前缀。xsi:schemaLocation声明只能理解命名空间名称,而不能理解前缀。

在应用程序中声明模式

要在应用程序中声明等效的模式,代码看起来类似于以下内容。

static final String employeeSchema = "employeeDatabase.xsd";
static final String taxSchema = "w2TaxForm.xsd";
static final String hiringSchema = "hiringForm.xsd";

static final String[] schemas = {
    employeeSchema,
    taxSchema, 
    hiringSchema,
};

static final String JAXP_SCHEMA_SOURCE =
    "http://java.sun.com/xml/jaxp/properties/schemaSource";

// ...

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance()

// ...

factory.setAttribute(JAXP_SCHEMA_SOURCE, schemas);

在这里,指向模式定义(.xsd文件)的字符串数组作为参数传递给factory.setAttribute方法。请注意与声明要用作 XML 数据集一部分时的区别。

  • 默认(未命名)模式没有特殊声明。

  • 你不需要指定命名空间名称。相反,你只需要给出指向.xsd文件的指针。

为了进行命名空间分配,解析器读取.xsd文件,并在其中找到它们适用于的目标命名空间的名称。因为文件是用 URI 指定的,所以解析器可以使用EntityResolver(如果已定义)来找到模式的本地副本。

如果模式定义没有定义目标命名空间,则适用于默认(未命名或 null)命名空间。因此,在我们的示例中,你会期望在模式中看到这些目标命名空间声明:

  • 指向模式的 URI 的字符串

  • 具有模式内容的InputStream

  • 一个 SAX InputSource

  • 一个文件

  • 一组对象,每个对象都是这里定义的类型之一

只有当模式语言具有在运行时组装模式的能力时,才能使用对象数组。此外,当传递对象数组时,具有相同命名空间的两个模式是不允许的。

运行带有模式验证的DOMEcho示例

要运行带有模式验证的DOMEcho示例,请按照以下步骤进行。

  1. 导航到samples目录。% cd *install-dir*/jaxp-1_4_2-*release-date*/samples.

  2. 编译示例类,使用刚刚设置的类路径。% javac dom/*

  3. 在 XML 文件上运行DOMEcho程序,指定模式验证。

    data目录中选择一个 XML 文件,并使用指定的-xsd选项在其上运行DOMEcho程序。在这里,我们选择在文件personal-schema.xml上运行程序。

    % java dom/DOMEcho -xsd data/personal-schema.xml

    正如你在配置工厂中看到的,-xsd选项告诉DOMEchopersonal-schema.xml文件中定义的 XML 模式进行验证。在这种情况下,模式是文件personal.xsd,也位于sample/data目录中。

  4. 在文本编辑器中打开personal-schema.xml并删除模式声明。

    从开头的<personnel>标签中删除以下内容。

    xsi:noNamespaceSchemaLocation='personal.xsd'

    不要忘记保存文件。

  5. 再次运行DOMEcho,这次指定-xsd选项。 % java dom/DOMEcho -xsd data/personal-schema.xml

    这次,你将看到一连串的错误。

  6. 再次运行DOMEcho,这次指定-xsdss选项并指定模式定义文件。

    正如你在配置工厂中看到的,-xsdss选项告诉DOMEcho在程序运行时执行针对指定的 XML 模式定义的验证。再次使用文件personal.xsd

    % java dom/DOMEcho -xsdss data/personal.xsd data/personal-schema.xml

    你将看到与之前相同的输出,这意味着 XML 文件已成功根据模式进行验证。

更多信息

原文:docs.oracle.com/javase/tutorial/jaxp/dom/info.html

关于 W3C 文档对象模型(DOM)的更多信息,请参见DOM 标准页面。

关于基于模式的验证机制的更多信息,请参见以下内容。

  • W3C 标准验证机制,XML Schema

  • RELAX NG 基于正则表达式的验证机制

  • Schematron 基于断言的验证机制

教程:可扩展样式表语言转换

原文:docs.oracle.com/javase/tutorial/jaxp/xslt/index.html

可扩展样式表语言转换(XSLT)标准定义了用于处理 XML 数据(XPath)并指定对数据进行转换以将其转换为其他形式的机制。JAXP 包括 XSLT 的解释实现。

在这节课中,您将编写一个文档对象模型作为 XML 文件,并了解如何从任意数据文件生成一个 DOM,以便将其转换为 XML。最后,您将把 XML 数据转换为不同的形式,沿途学习 XPath 寻址机制。

介绍 XSL、XSLT 和 XPath

原文:docs.oracle.com/javase/tutorial/jaxp/xslt/intro.html

可扩展样式表语言(XSL)有三个主要子组件:

XSL-FO

格式化对象标准。迄今为止最大的子组件,此标准提供了描述字体大小、页面布局和对象呈现的机制。此子组件不包含在 JAXP 中,也不包含在本教程中。

XSLT

这是一种转换语言,允许您定义从 XML 到其他格式的转换。例如,您可以使用 XSLT 生成 HTML 或不同的 XML 结构。您甚至可以使用它生成纯文本或将信息放入其他文档格式中。(正如您将在从任意数据结构生成 XML 中看到的,一个聪明的应用程序可以利用它来操作非 XML 数据)。

XPath

归根结底,XSLT 是一种语言,让您指定在遇到特定元素时要执行的操作。但是,要为 XML 数据结构的不同部分编写程序,您需要在任何给定时间指定您正在讨论的结构部分。XPath 就是这种规范语言。它是一种寻址机制,允许您指定到元素的路径,以便例如,可以区分<article><title><person><title>。通过这种方式,您可以为不同的<title>元素描述不同类型的翻译。

本节的其余部分描述了组成 JAXP 转换 API 的包。

JAXP 转换包

这里是组成 JAXP 转换 API 的包的描述:

javax.xml.transform

该包定义了您用于获取Transformer对象的工厂类。然后,您使用来自其他三个包中的类创建源(source)和结果(result)对象,并调用其transform()方法使转换发生。源和结果对象是使用其他三个包中的类创建的。

javax.xml.transform.dom

定义了DOMSourceDOMResult类,让您可以将 DOM 用作转换的输入或输出。

javax.xml.transform.sax

定义了SAXSourceSAXResult类,让您可以将 SAX 事件生成器用作转换的输入,或将 SAX 事件作为输出传递给 SAX 事件处理器。

javax.xml.transform.stream

定义了StreamSourceStreamResult类,让您可以将 I/O 流用作转换的输入或输出。

XSLT 示例程序

与本教程中的其他课程不同,本课程中使用的示例程序未包含在 JAXP 1.4.2 参考实现提供的install-dir/jaxp-1_4_2-release-date/samples目录中。但是您可以在此处下载 XSLT 示例的 ZIP 文件

XPath 如何工作

原文:docs.oracle.com/javase/tutorial/jaxp/xslt/xpath.html

XPath 规范是各种规范的基础,包括 XSLT 和链接/寻址规范,如XPointer。因此,对 XPath 的理解对许多高级 XML 用法至关重要。本节介绍了 XPath 在 XSLT 上下文中的基本知识。

XPath 表达式

一般来说,XPath 表达式指定了选择一组 XML 节点的模式。然后 XSLT 模板在应用转换时使用这些模式。(另一方面,XPointer添加了定义范围的机制,以便可以使用 XPath 表达式进行寻址)。

XPath 表达式中的节点不仅指代元素,还指代文本和属性等其他内容。事实上,XPath 规范定义了一个抽象文档模型,定义了七种节点类型:

  • 元素

  • 文本

  • 属性

  • 注释

  • 处理指令

  • 命名空间

XML 数据的根元素由一个元素节点建模。XPath 根节点包含文档的根元素以及与文档相关的其他信息。

XSLT/XPath 数据模型

类似于文档对象模型(DOM),XSLT/XPath 数据模型由包含各种节点的树组成。在任何给定元素节点下,都有文本节点、属性节点、元素节点、注释节点和处理指令节点。

在这个抽象模型中,语法区别消失了,你只剩下了数据的规范化视图。例如,在文本节点中,无论文本是在 CDATA 部分中定义的还是包含实体引用,都没有区别。文本节点将包含规范化的数据,即在所有解析完成后存在的数据。因此,文本将包含一个<字符,无论是否使用实体引用(如<)或 CDATA 部分来包含它。(类似地,文本将包含一个&字符,无论是使用&传递的还是在 CDATA 部分中的)。

在这一部分,我们将主要处理元素节点和文本节点。有关其他寻址机制,请参阅 XPath 规范。

模板和上下文

XSLT 模板是一组应用于 XPath 表达式选择的节点的格式化指令。在样式表中,XSLT 模板看起来像这样:

<xsl:template match="//LIST">
    ...
</xsl:template>

表达式//LIST从输入流中选择LIST节点集。模板中的附加指令告诉系统如何处理它们。

由这样一个表达式选择的节点集定义了模板中其他表达式评估的上下文。该上下文可以被视为整个集合 - 例如,在确定它包含的节点数时。

上下文也可以被视为集合中的单个成员,因为每个成员都会逐个处理。例如,在LIST处理模板内部,表达式@type指的是当前LIST节点的类型属性。(类似地,表达式@*指的是当前 LIST 元素的所有属性)。

基本的 XPath 地址定位

XML 文档是一个树形结构(分层)的节点集合。与分层目录结构一样,指定指向层次结构中特定节点的路径是很有用的(因此规范的名称是 XPath)。事实上,许多目录路径的表示法完全保留不变:

  • 正斜杠(/)用作路径分隔符。

  • 从文档根开始的绝对路径以/开头。

  • 从给定位置开始的相对路径以其他任何字符开头。

  • 双点(..)表示当前节点的父节点。

  • 单点(.)表示当前节点。

例如,在一个可扩展的 HTML(XHTML)文档(一个看起来像 HTML 但符合 XML 规则的 XML 文档)中,路径/h1/h2/表示h1下的h2元素。(回想一下,在 XML 中,元素名称是区分大小写的,因此这种规范在 XHTML 中比在普通 HTML 中更有效,因为 HTML 是不区分大小写的)。

在诸如 XPath 的模式匹配规范中,规范/h1/h2选择所有位于h1元素下的h2元素。要选择特定的h2元素,您可以使用方括号[]进行索引(就像用于数组的那样)。因此,路径/h1[4]/h2[5]将选择第四个h1元素下的第五个h2元素。


注意 - 在 XHTML 中,所有元素名称都是小写的。这是 XML 文档的一个相当普遍的约定。但是,在像本教程这样的教程中,大写名称更容易阅读。因此,在剩下的 XSLT 课程中,所有 XML 元素名称将以大写形式呈现。(另一方面,属性名称将保持小写)。


XPath 表达式中指定的名称指的是一个元素。例如,在/h1/h2中,h1指的是h1元素。要引用属性,您需要在属性名称前加上@符号。例如,@type指的是元素的类型属性。假设您有一个带有 LIST 元素的 XML 文档,那么表达式LIST/@type将选择LIST元素的类型属性。


注意 - 因为表达式不以/开头,所以引用指定了相对于当前上下文的列表节点-无论文档中的位置是什么。


基本的 XPath 表达式

XPath 表达式的完整范围利用了 XPath 定义的通配符、运算符和函数。您很快将了解更多相关内容。在这里,我们简单介绍了一些最常见的 XPath 表达式。

表达式 @type="unordered" 指定了一个名为 type 的属性,其值为 unordered。诸如 LIST/@type 这样的表达式指定了 LIST 元素的 type 属性。

你可以将这两种表示法结合起来,得到一些有趣的东西。在 XPath 中,通常与索引相关联的方括号表示法([])被扩展为指定选择条件。因此,表达式 LIST[@type="unordered"] 选择所有类型值为 unordered 的 LIST 元素。

元素也存在类似的表达式。每个元素都有一个关联的字符串值,该值由连接在元素下的所有文本段组成。(有关该过程如何工作的更详细解释,请参见 元素的字符串值。)

假设您使用由 PROJECT 元素和具有项目名称文本字符串、多个列出参与者的 PERSON 元素以及可选记录项目状态的 STATUS 元素组成的 XML 结构来对组织中发生的事情进行建模。以下是使用扩展方括号表示法的其他示例:

  • /PROJECT[.="MyProject"]:选择名为 "MyProject" 的 PROJECT

  • /PROJECT[STATUS]:选择所有具有 STATUS 子元素的项目。

  • /PROJECT[STATUS="Critical"]:选择所有具有字符串值为 Critical 的 STATUS 子元素的项目。

结合索引地址

XPath 规范定义了相当多的寻址机制,它们可以以许多不同的方式组合。因此,XPath 为相对简单的规范提供了很多表达能力。本节展示了其他有趣的组合:

  • LIST[@type="ordered"][3]:选择所有类型为 ordered 的 LIST 元素,并返回第三个。

  • LIST[3][@type="ordered"]:选择第三个 LIST 元素,但仅当它是 ordered 类型时。


注意 - 在 XPath 规范 的第 2.5 节中列出了更多的地址操作符组合。这可能是规范中最有用的部分,用于定义 XSLT 转换。


通配符

根据定义,未经限定的 XPath 表达式选择与指定模式匹配的一组 XML 节点。例如,/HEAD 匹配所有顶级 HEAD 条目,而 /HEAD[1] 仅匹配第一个。表 4-1 列出了可用于 XPath 表达式中的通配符,以扩大模式匹配的范围。

表 4-1 XPath 通配符

通配符 含义
* 匹配任何元素节点(不包括属性或文本)。
node() 匹配任何类型的任何节点:元素节点、文本节点、属性节点、处理指令节点、命名空间节点或注释节点。
@* 匹配任何属性节点。

在项目数据库示例中,/*/PERSON[.="Fred"] 匹配任何命名为 Fred 的 PROJECTACTIVITY 元素。

扩展路径寻址

到目前为止,您看到的所有模式都指定了层次结构中的确切级别。例如,/HEAD指定了层次结构中第一级的任何HEAD元素,而/*/*指定了层次结构中第二级的任何元素。要指定层次结构中的不确定级别,请使用双斜杠(//)。例如,XPath 表达式//PARA选择文档中的所有段落元素,无论它们在哪里找到。

//模式也可以在路径中使用。因此,表达式/HEAD/LIST//PARA表示从/HEAD/LIST开始的子树中的所有段落元素。

XPath 数据类型和运算符

XPath 表达式产生一组节点、一个字符串、一个布尔值(真/假值)或一个数字。表 4-2 列出了可以在 Xpath 表达式中使用的运算符:

表 4-2 XPath 运算符

运算符 含义
&#124; 替代。例如,PARA&#124;LIST选择所有PARALIST元素。
or, and 返回两个布尔值的或/与。
=, != 等于或不等于,适用于布尔值、字符串和数字。
<, >, <=, >= 小于、大于、小于或等于、大于或等于,适用于数字。
+, -, *, div, mod 加、减、乘、浮点除法和模运算(例如,6 mod 4 = 2)。

表达式可以用括号分组,因此您不必担心运算符优先级。


注意 - 运算符优先级是一个术语,用来回答这个问题,“如果你指定 a + b * c,这是意味着 (a+b) * c 还是 a + (b*c)?”(运算符优先级与表中显示的大致相同)。


元素的字符串值

元素的字符串值是所有后代文本节点的连接,无论有多深。考虑这个混合内容的 XML 数据:

<PARA>This paragraph contains a <b>bold</b> word</PARA>

<PARA>元素的字符串值为这个段落包含一个粗体词。特别要注意的是,<B><PARA>的子元素,而文本bold<B>的子元素。

关键是节点的所有子节点中的所有文本都连接在一起以形成字符串值。

此外,值得理解的是,XPath 定义的抽象数据模型中的文本是完全规范化的。因此,无论 XML 结构中是否包含实体引用&lt;<CDATA部分中,元素的字符串值将包含<字符。因此,在使用 XSLT 样式表生成 HTML 或 XML 时,必须将<的出现转换为&lt;或将其置于CDATA部分中。类似地,&的出现必须转换为&amp;

XPath 函数

这一部分以 XPath 函数的概述结束。您可以使用 XPath 函数来选择一组节点,就像您使用元素规范一样。其他函数返回一个字符串、一个数字或一个布尔值。例如,表达式/PROJECT/text()获取PROJECT节点的字符串值。

许多函数依赖于当前上下文。在前面的示例中,每次调用text()函数的上下文是当前选择的PROJECT节点。

有许多 XPath 函数-太多了,无法在此详细描述。本节提供了一个简要列表,显示了可用的 XPath 函数以及它们的功能摘要。有关函数的更多信息,请参阅XPath 规范的第 4 节。

节点集函数

许多 XPath 表达式选择一组节点。实质上,它们返回一个节点集。一个函数也是如此。id(...)函数返回具有指定 ID 的节点。(元素仅在文档具有指定哪个属性具有 ID 类型的 DTD 时才具有 ID)。

位置函数

这些函数返回基于位置的数值。

  • last(): 返回最后一个元素的索引。例如,/HEAD[last()]选择最后一个HEAD元素。

  • position(): 返回索引位置。例如,/HEAD[position() <= 5]选择前五个HEAD元素。

  • count(...): 返回元素的计数。例如,/HEAD[count(HEAD)=0]选择所有没有子标题的HEAD元素。

字符串函数

这些函数操作或返回字符串。

  • concat(string, string, ...): 连接字符串值。

  • starts-with(string1, string2): 如果string1string2开头,则返回 true。

  • contains(string1, string2): 如果string1包含string2,则返回 true。

  • substring-before(string1, string2): 返回string1string2出现之前的部分。

  • substring-after(string1, string2): 返回string1string2之后的剩余部分。

  • substring(string, idx): 返回从索引位置到末尾的子字符串,其中第一个char的索引 = 1。

  • substring(string, idx, len): 返回从索引位置开始的指定长度的子字符串。

  • string-length(): 返回上下文节点的字符串值的大小;上下文节点是当前选择的节点-通过应用诸如string-length()的函数选择的 XPath 表达式中的节点。

  • string-length(string): 返回指定字符串的大小。

  • normalize-space(): 返回当前节点的规范化字符串值(没有前导或尾随空格,并且空格字符序列转换为单个空格)。

  • normalize-space(string): 返回指定字符串的规范化字符串值。

  • translate(string1, string2, string3): 将string1转换,用string2中的字符替换为string3中对应的字符。


注意 - XPath 定义了三种获取元素文本的方式:text()string(object),以及在表达式中隐含的元素名称所暗示的字符串值,例如:/PROJECT[PERSON="Fred"]


布尔函数

这些函数操作或返回布尔值。

  • not(...): 反转指定的布尔值。

  • true(): 返回 true。

  • false(): 返回 false。

  • lang(string): 如果上下文节点的语言(由xml:Lang属性指定)与指定的语言相同(或是指定语言的子语言),则返回 true;例如,对于<PARA_xml:Lang="en">...</PARA>Lang("en")为 true。

数值函数

这些函数操作或返回数值。

  • sum(...): 返回指定节点集中每个节点的数值之和。

  • floor(N): 返回不大于N的最大整数。

  • ceiling(N): 返回不小于N的最小整数。

  • round(N): 返回最接近N的整数。

转换函数

这些函数将一个数据类型转换为另一个数据类型。

  • string(...): 返回数字、布尔值或节点集的字符串值。

  • boolean(...): 为数字、字符串或节点集返回一个布尔值(非零数字、非空节点集和非空字符串均为 true)。

  • number(...): 返回布尔值、字符串或节点集的数值(true 为 1,false 为 0,包含数字的字符串变为该数字,节点集的字符串值转换为数字)。

命名空间函数

这些函数让你确定节点的命名空间特征。

  • local-name(): 返回当前节点的名称,不包括命名空间前缀。

  • local-name(...): 返回指定节点集中第一个节点的名称,不包括命名空间前缀。

  • namespace-uri(): 返回当前节点的命名空间 URI。

  • namespace-uri(...): 返回指定节点集中第一个节点的命名空间 URI。

  • name(): 返回当前节点的扩展名称(URI 加上本地名称)。

  • name(...): 返回指定节点集中第一个节点的扩展名称(URI 加上本地名称)。

总结

XPath 运算符、函数、通配符和节点寻址机制可以以各种方式组合。到目前为止,你所学到的内容应该让你能够很好地开始指定你需要的模式。