在该系列的上一篇文章中我演示了NIO.2的三个方法:文件拷贝、文件和目录的删除和文件移动。在这篇文章中,我将向大家展示路径相关的方法(如获取路径、检索路径信息)、文件和目录测试方法(如文件或目录的存在性测试)以及面向属性的方法。
问:怎样获得一个 java.nio.file.Path 对象?
答:你可以调用 java.nio.file.Paths 类的下列任何一静态个方法,通过文件或目录在文件系统中的名称来获得一个Path对象:
第一个 get() 方法可以通过任意数量的字符组成一个路径,例如,Path path = Paths.get(“a”, “b”); 在将字符转换为字符串的时候,该方法将使用适合特定文件系统的文件分隔符来分隔元素。例如,在Windows平台上执行 System.out.println(path); 方法,结果输出为 ab。
你也可以在元素之间硬编码一个分隔符,如Path path = Paths.get(“a”, “”, “b”);。但是,这不是一个好的想法。在Windows平台上,最终会获得ab.但是,在一个以冒号为文件分隔符的平台上,你将很有可能会观察到一个 InvalidPathException 异常,指出斜杠是一个非法的字符。
你可能想使用 File.separator 来替代硬编码一个文件分隔符。但是,这也不是一个好的想法。在NIO.2 中支持多种文件系统,但是 File.separator 指的是默认的文件系统。尝试通过一个包含 File.separator 路径来获取Path对象,如果该文件系统环境下,不能识别该常量分割符将抛出 InvalidPathException 异常。
我们应该使用 FileSystem 的 String getSeparator() 的方法来代替硬编码一个分割符或者指定File.separator。getSeparator() 方法返回一个FileSystem 实例调用关联的文件系统的文件分隔符。例如,FileSystem.getDefault().getSeparator() 返回默认文件系统的分隔符。
在该系统的第一部分我简单的演示了 get() 方法的使用。在列表1中展示了另外一个小应用的源代码,在这个应用中,我强制使用了上述的文件分隔符。
import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.Paths; public class ObtainPath { public static void main(String[] args) { Path path = Paths.get("a", "b"); System.out.println(path); path = Paths.get(FileSystems.getDefault().getSeparator() + "a", "b", "c"); System.out.println(path); path = Paths.get("a", ":", "b"); System.out.println(path); } }
在上述代码中的第二个例子中,该文件分隔符被作为根目录使用,但是存在一种更好的方式来获取该目录,我将在此文的后面演示。
编译列表1 (javac ObtainPath.java) 并运行该应用(java ObtainPath)。在Windows平台上,我看到的结果如下:
ab abc Exception in thread "main" java.nio.file.InvalidPathException: Illegal char <:> at index 2: a:b at sun.nio.fs.WindowsPathParser.normalize(WindowsPathParser.java:182) at sun.nio.fs.WindowsPathParser.parse(WindowsPathParser.java:153) at sun.nio.fs.WindowsPathParser.parse(WindowsPathParser.java:77) at sun.nio.fs.WindowsPath.parse(WindowsPath.java:94) at sun.nio.fs.WindowsFileSystem.getPath(WindowsFileSystem.java:255) at java.nio.file.Paths.get(Paths.java:84) at ObtainPath.main(ObtainPath.java:15)
顺便提一下,如果将null做为路径元素传 get() 方法,如path = Paths.get(“a”, null, “b”),将会出现什么现象?
第二个 get() 方法是从一个 URI 转换为path,列表2的一个小应用程序演示了使用该方法时的两个问题。
import java.net.URI; import java.net.URISyntaxException; import java.nio.file.FileSystemNotFoundException; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; public class ObtainPath { public static void main(String[] args) throws URISyntaxException { try { Path path = Paths.get(new URI("")); System.out.println(path); } catch (IllegalArgumentException iae) { System.err.println("You cannot pass a URI object initialized to the " + "empty string to get()."); iae.printStackTrace(); } try { Path path = Paths.get(new URI("nntp://x")); System.out.println(path); } catch (FileSystemNotFoundException fsnfe) { System.err.println("No file system associated with the specified scheme."); fsnfe.printStackTrace(); } } }
编译列表2并运行该应用程序。你就能看到如下的异常输出:
You cannot pass a URI object initialized to the empty string to get(). java.lang.IllegalArgumentException: Missing scheme at java.nio.file.Paths.get(Paths.java:134) at ObtainPath.main(ObtainPath.java:15) No file system associated with the specified scheme. java.nio.file.FileSystemNotFoundException: Provider "nntp" not installed at java.nio.file.Paths.get(Paths.java:147) at ObtainPath.main(ObtainPath.java:27)
第一个例子演示了因缺失scheme的URI而出现的 IllegalArgumentException 异常。第二个例子演示了因为没有nntp scheme的提供者而出现的 FileSystemNotFoundException 的异常。
问:我能从Path对象中检索到什么信息呢?
答:Path接口声明了几个方法。通过Path对象,可以检索到单个的名称元素和其他种类的信息,下面列出了这些方法的描述:
列表3展示了一个小应用程序的源码,该应用程序演示了这些路径信息检索的方法。该应用通过合适的方式增加了根目录。
import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.Paths; public class RetrievePathInfo { public static void main(String[] args) { dumpPathInfo(Paths.get("a", "b", "c")); FileSystem fs = FileSystems.getDefault(); Iterable<Path> roots = fs.getRootDirectories(); for(Path root: roots) { dumpPathInfo(Paths.get(root.toString(), "a", "b", "c")); break; } } static void dumpPathInfo(Path path) { System.out.printf("Path: %s%n", path); System.out.printf("Filename: %s%n", path.getFileName()); System.out.println("Components"); for (int i = 0; i < path.getNameCount(); i++) System.out.printf(" %s%n", path.getName(i)); System.out.printf("Parent: %s%n", path.getParent()); System.out.printf("Root: %s%n", path.getRoot()); System.out.printf("Absolute: %b%n", path.isAbsolute()); System.out.printf("Subpath [0, 2): %s%n%n", path.subpath(0, 2)); } }
列表3创建了两个路径,一个是相对路径、一个绝对路径。然后,调用之前列出来的方法显示不同种类的信息。在第二个路径中,调用 FileSystem 的 Iterable<Path> getRootDirectories() 方法来检索文件系统默认的根目录。第一个根目录被预先添加到路径中用来显示路径信息。
编译列表3(javac RetrievePathInfo.java)然后运行该应用(java RetrievePathInfo)。在Windows平台下,我看的输出结果如下:
Path: abc Filename: c Components a b c Parent: ab Root: null Absolute: false Subpath [0, 2): ab Path: C:abc Filename: c Components a b c Parent: C:ab Root: C: Absolute: true Subpath [0, 2): ab
注意:如果预置了根目录,isAbsolute() 将返回true。如果只添加一个文件分隔符,如 Paths.get(FileSystems.getDefault().getSeparator() + "a", "b", "c")。在Windows平台下,isAbsolute() 将返回false。在Windows平台下一个绝对路径必须包含驱动说明符,如C:.
问:我还能对Path对象做哪些操作?
答:Path接口提供移除冗余的路径,从另个路径中创建相对路径,连接两个路径等等操作。前三种操作可以通过下列方法实现:
Path normalize() Path relativize(Path other) Path resolve(Path other) (and the Path resolve(String other) variant)
列表4演示这些方法(为了方便,我在两个地方硬编码,这应该通过获取根目录来代替的)
import java.nio.file.Path; import java.nio.file.Paths; public class MorePathOp { public static void main(String[] args) { Path path1 = Paths.get("reports", ".", "2015", "jan"); System.out.println(path1); System.out.println(path1.normalize()); path1 = Paths.get("reports", "2015", "..", "jan"); System.out.println(path1.normalize()); System.out.println(); path1 = Paths.get("reports", "2015", "jan"); System.out.println(path1); System.out.println(path1.relativize(Paths.get("reports", "2016", "mar"))); try { System.out.println(path1.relativize(Paths.get("/reports", "2016", "mar"))); } catch (IllegalArgumentException iae) { iae.printStackTrace(); } System.out.println(); path1 = Paths.get("reports", "2015"); System.out.println(path1); System.out.println(path1.resolve("apr")); System.out.println(path1.resolve("/apr")); } }
列表4的 main() 方法首先演示了 normalize() 处理当前目录,然后演示了父目录(..)。结果是 reports/jan。
接下来,main() 演示了之前 relativize() 的例子。然后演示了不能通过绝对路径来获取相对路径,如果尝试这么做的,将抛出IllegalArgumentException的异常。
最后,main() 方法演示了先前 resolve() 的例子,然后展示通过使用一个绝对路径来对比产生的结果。
你可以使用Path对象的其他方法,例如,通过 Path toAbsolutePath() 方法来将一个路径转换为绝对路径,通过 boolean equals(Object other) 方法来比较两个路径是否相同。
问:怎么知道一个文件是可执行的、可读的或可写的呢?
答:File类提供下列的静态方法来获取文件的可执行性、可读性和可写性:
通常在执行、读或写一个文件时,都需要调用其中的一个方法来判断。例如,在尝试写一个文件时,你可能需要先检测一个文件是否是可写的,如:
if (!Files.isWritable(Paths.get("file"))) { System.out.println("file is not writable"); return; } byte[] data = { /* some comma-separated list of byte data items */ }; Files.write(Paths.get("file"), data);
但是,这段代码有一个问题。检测完这个文件是可写的以后,在写之前,其他程序访问了该文件,并把它设置为可读的。这样,写文件的代码将会执行失败。
检测时间和使用时间之间的竞态条件即为广为人知的 Time of check to time of use (TOCTTOU — 发音为TOCK-too) 问题,这也是为什么 javaodc中说,这些方法的检测结果会瞬间过时。
TOCTTOU对外界干扰的概率很低的很多应用来说不是什么大问题。但是,对安全敏感的应用来说,则是一个重大问题,黑客可能利用这点盗取用户的认证信息。对于这样的应用,
你可能需要避免使用 isWritable() 及它的同类方法,而使用 try/catch 包裹文件的 I/O 代码来代替。
问:我怎么知道一个文件或目录是否存在?
答:Files类提供下列静态方法来判断一个文件或目录是否存在:
在进行某种操作之前,你可能需要调用 exists() 方法来判断,以防在文件不存在时抛出异常。例如,你可能在删除文件之前测试文件的存在性:
Path path = Paths.get("file"); if (Files.exists(path)) Files.delete(path);
注意:!exists(path) 不等于 notExists(path)(因为 !exists() 不一定是原子的,而 notExists() 是原子的)。同时,如果 exists() 和 notExists() 都返回false,则说明文件的存在性不清楚。最后,类似于访问性判断方法,这些方法的结果也是会瞬间过时的,因此,在安全性敏感的应用中应该避免至少应改谨慎使用。
问:我怎么知道还能在Path对象上进行哪种操作?
答:Files类提供以下静态方法俩判断一个路径是不是一个目录,是不是隐藏的,是不是一个正规文件,两个路径是不是指的同一个文件,以及路径是不是一个符号链接:
boolean isDirectory(Path path, LinkOption... options):如果不想让该方法跟踪符号链接,则指定LinkOption.NOFOLLOW_LINKS参数。 boolean isHidden(Path path) boolean isRegularFile(Path path, LinkOption... options) ---如果不想让该方法跟踪符号链接,则指定LinkOption.NOFOLLOW_LINKS参数。 boolean isSameFile(Path path, Path path2) boolean isSymbolicLink(Path path)
Consider isHidden(Path) 如果路径指向的文件是隐藏的,则返回true,列表5中应用痛过使用这个方法过滤目录中的隐藏文件,其输出结果如下:
import java.io.IOException; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; public class ListHiddenFiles { public static void main(String[] args) throws IOException { if (args.length != 1) { System.err.println("usage: java ListHiddenFiles directory"); return; } Path dir = Paths.get(args[0]); DirectoryStream<Path> stream = Files.newDirectoryStream(dir); for (Path file: stream) if (Files.isHidden(file)) System.out.println(file); } }
main() 首先验证命令行参数,该唯一参数即为查询影藏文件的目录。该字符串被转换为了一个 Path对象。
这个Path对象然后被传递到Files类的 DirectoryStream<Path> newDirectoryStream(Path dir) 方法中,该方法返回一个 java.nio.file.DirectoryStream 对象,然后遍历该path对象指定的目录下的所有实体。
DirectoryStream 实现了 Iterable 接口,所以你可以使用增强的 for循环来遍历目录下实体,每次遍历出来的实体,都调用 isHidden() 方法,只有当该方法返回true的时候,才输出。
编译列表5(javac ListHiddenFiles.java)然后运行该应用,java ListHiddenFiles C:. 。在我的Windows平台下,我看的输出结果如下:
C:hiberfil.sys C:pagefile.sys
问:怎么获取或者设置Path对象的属性?
答:Files提供下列几个静态方法来获取或设置Path对象的属性:
FileTime getLastModifiedTime(Path path, LinkOption... options) UserPrincipal getOwner(Path path, LinkOption... options) Set<PosixFilePermission> getPosixFilePermissions(Path path, LinkOption... options) boolean isDirectory(Path path, LinkOption... options) boolean isHidden(Path path) boolean isRegularFile(Path path, LinkOption... options) boolean isSymbolicLink(Path path) Path setAttribute(Path path, String attribute, Object value, LinkOption... options) Path setLastModifiedTime(Path path, FileTime time) Path setOwner(Path path, UserPrincipal owner) Path setPosixFilePermissions(Path path, Set<PosixFilePermission> perms) long size(Path path)
在此之前,我演示的 isDirectory()、 isHidden()、isRegularFile() 和 isSymbolicLink() 都对属于Path对象。这些方法同时也返回一个Path对象的directory、hidden、regularFile 或 symbolicLink 属性值。
列表6这个应用程序展示了通过 getLastModifiedTime(Path)、getOwner(Path) 和 size(Path) 方法获取一个Path对象的最后修改事件、拥有者以及大小及这些值的输出。
import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; public class PathAttrib { public static void main(String[] args) throws IOException { if (args.length != 1) { System.err.println("usage: java PathAttrib path-object"); return; } Path path = Paths.get(args[0]); System.out.printf("Last modified time: %s%n", Files.getLastModifiedTime(path)); System.out.printf("Owner: %s%n", Files.getOwner(path)); System.out.printf("Size: %d%n", Files.size(path)); } }
编译列表6(javac PathAttrib.java)),并运行该应用,如 java PathAttrib PathAttrib.java。我看到的输出结果如下:
Last modified time: 2015-03-18T17:56:15.802635Z Owner: Owner-PCOwner (User) Size: 604
问:属性视图是什么?我应该怎么处理?
答:Javadoc中各种属性方法(如:getAttribute()、getPosixFilePermissions()、setAttribute() 和 setPosixFilePermissions())涉及到 java.nio.file.attribute 包中的接口类型。这些接口被不同的属性视图识别,这些属性视图组成了文件的属性。一个属性视图映射到一个文件系统的实现(例如POSIX)或一个通用的功能(如文件所有者关系)。
NIO.2 提供 AttributeView 作为属性视图继承层次中的跟接口。该接口申明一个 getName() 方法,用于返回属性视图的名称。FileAttributeView 继承自 AttributeView 接口,这是一个对文件系统的文件透明的值,是一个只读的或者可更新的视图。FileAttributeView 不提供方法,但是作为很多特定面向文件的的属性视图的超类接口存在。
注意:一个文件系统的实现可能支持basic文件属性视图,或者支持几种后续提到的属性视图(甚至支持其他的不在上述列表中的属性视图)
Files类提供 <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption… options) 泛型方法来返回特定类型的文件属性视图。例如,AclFileAttributeView view = Files.getFileAttributeView(path, AclFileAttributeView.class);。如果属性视图是无效,则该方法返回空。
通常情况下,你需要直接处理 FileAttributeView 或它的子接口,而是通过 getAttribute() 和 setAttribute() 来代替,每个方法的参数要求一个如下语法的字符串:
[view-name:]attribute-name
view-name 标示一个FileAttributeView,FileAttributeView可以识别一个文件属性的集合。如果指定了,它必须是basic、dos、posix、owner 或者acl 之中一个。如果没有指定,默认是basic。basic是一个能识别大多数文件系统通用属性的视图。(这里没有用户自定义的文件属性视图)。attribute-name 是属性的名称,各种属性视图能识别指定名称的属性。
如果指定的属性视图不被支持,则每个方法都会抛出 java.lang.UnsupportedOperationException。为了找出什么视图是支持的,需要调用FileSystem的Set<String> supportedFileAttributeViews() 方法。
列表7演示了输出指定路径下,默认的文件系统支持的属性视图,并输出了所有的(总是支持)basic和(如果支持)acl及dos视图的属性值。
import java.io.IOException; import java.nio.file.Files; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.Paths; public class PathAttrib { public static void main(String[] args) throws IOException { boolean isACL = false; boolean isDOS = false; FileSystem defFS = FileSystems.getDefault(); for (String fileAttrView : defFS.supportedFileAttributeViews()) { System.out.printf("Default file system supports: %s%n", fileAttrView); if (fileAttrView.equals("acl")) isACL = true; if (fileAttrView.equals("dos")) isDOS = true; } System.out.println(); if (args.length != 1) { System.err.println("usage: java PathAttrib path-object"); return; } Path path = Paths.get(args[0]); // Output basic attributes, which are always supported. System.out.println("Basic attributes:"); String[] attrNames = { "lastModifiedTime", "lastAccessTime", "creationTime", "size", "isRegularFile", "isDirectory", "isSymbolicLink", "isOther", // something other that a regular file, directory, or // symbolic link "fileKey" // an object that uniquely identifies the given file, or // null when a file key is not available. }; for (String attrName: attrNames) System.out.printf("%s: %s%n", attrName, Files.getAttribute(path, "basic:" + attrName)); System.out.println(); // Output ACL owner attribute when this view is supported. if (isACL) { System.out.println("ACL attributes:"); System.out.printf("Owner: %s%n%n", Files.getAttribute(path, "acl:owner")); } // Output DOS attributes when this view is supported. if (isDOS) { System.out.println("DOS attributes:"); attrNames = new String[] { "readonly", "hidden", "system", "archive" }; for (String attrName: attrNames) System.out.printf("%s: %s%n", attrName, Files.getAttribute(path, "dos:" + attrName)); System.out.println(); } } }
编译列表7并运行,如 java PathAttrib PathAttrib.java。我看到的输出结果如下:
Default file system supports: acl Default file system supports: basic Default file system supports: owner Default file system supports: user Default file system supports: dos Basic attributes: lastModifiedTime: 2015-03-18T19:36:47.435624Z lastAccessTime: 2015-03-18T19:21:16.681388Z creationTime: 2015-03-18T19:21:16.681388Z size: 2417 isRegularFile: true isDirectory: false isSymbolicLink: false isOther: false fileKey: null ACL attributes: Owner: Owner-PCOwner (User) DOS attributes: readonly: false hidden: false system: false archive: true
问:听说有更高效的方法来批量读取文件属性,而不是单个的处理。我该怎么样批量读取文件属性?
答:Files申明了两个静态方法来批量读取文件属性。
<A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options) Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options)
第一个方法返回 java.nio.file.attribute.BasicFileAttributes 或它的子接口对象,该对象包含了所有的指定类型属性值。通过返回的Call类型的方法可以获得对象这些值。
第二个方法返回一个map,map包含了所有由属性参数识别的的名值对。这些参数的形式是 [view-name:]attribute-list; 例如, “posix:permissions,owner,size”、
列表8演示这些方法。该应用一个Path对象为参数,每个方法都去读取basic文件属性,然后输出相应的值:
import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; import java.util.Map; import java.util.TreeMap; public class PathAttrib { public static void main(String[] args) throws IOException { if (args.length != 1) { System.err.println("usage: java PathAttrib path-object"); return; } Path path = Paths.get(args[0]); BasicFileAttributes attrs; attrs = Files.readAttributes(path, BasicFileAttributes.class); System.out.printf("Basic Attributes for %s...%n%n", path); System.out.printf("creationTime: %s%n", attrs.creationTime()); System.out.printf("fileKey: %s%n", attrs.fileKey()); System.out.printf("isDirectory: %b%n", attrs.isDirectory()); System.out.printf("isOther: %b%n", attrs.isOther()); System.out.printf("isRegularFile: %b%n", attrs.isRegularFile()); System.out.printf("isSymbolicLink: %b%n", attrs.isSymbolicLink()); System.out.printf("lastAccessTime: %s%n", attrs.lastAccessTime()); System.out.printf("lastModifiedTime: %s%n", attrs.lastModifiedTime()); System.out.printf("size: %d%n", attrs.size()); System.out.println(); Map<String, Object> attrMap = new TreeMap<>(Files.readAttributes(path, "*")); for (Map.Entry<String, Object> entry: attrMap.entrySet()) System.out.printf("%s: %s%n", entry.getKey(), entry.getValue()); } }
在列表8中,我传入 “*” 作为第二个参数传递给第二个方法 readAttributes(),而不是指定一个具体basic中的视图名称,这就意味着basic属性视图的所有属性都需要返回。同时,我将 readAttributes() 的返回结果传递到 java.util.TreeMap 构造器中来获取到一个排序好的map,所以,输出实体的顺序是经过排序的。编译列表8,并运行该应用。如:java PathAttrib PathAttrib.java,我观察到的输出的结果如下:
Basic Attributes for PathAttrib.java... creationTime: 2015-03-18T20:20:43.635406Z fileKey: null isDirectory: false isOther: false isRegularFile: true isSymbolicLink: false lastAccessTime: 2015-03-18T20:20:43.635406Z lastModifiedTime: 2015-03-18T20:35:20.446953Z size: 1618 creationTime: 2015-03-18T20:20:43.635406Z fileKey: null isDirectory: false isOther: false isRegularFile: true isSymbolicLink: false lastAccessTime: 2015-03-18T20:20:43.635406Z lastModifiedTime: 2015-03-18T20:35:20.446953Z size: 1618
问:我还能在一个Path对象上进行哪些属性操作?
答:你可以读取一个Path对象关联的文件的存储属性值(存储池、设备、分区、卷、具体的文件系统、其它文件存储的特定实现的内容)。可通过下列的方法完成该任务:
列表9是一个小应用程序的源码,这个应用程序演示了怎样使用 getAttribute() 获取NTFS文件存储实现的属性并输出属性值:
import java.io.IOException; import java.nio.file.Files; import java.nio.file.FileStore; import java.nio.file.Path; import java.nio.file.Paths; public class PathAttrib { public static void main(String[] args) throws IOException { if (args.length != 1) { System.err.println("usage: java PathAttrib path-object"); return; } Path path = Paths.get(args[0]); FileStore fs = Files.getFileStore(path); if (fs.type().equals("NTFS")) { String[] attrNames = { "totalSpace", "usableSpace", "unallocatedSpace", "volume:vsn", "volume:isRemovable", "volume:isCdrom" }; for (String attrName: attrNames) System.out.println(fs.getAttribute(attrName)); } } }
当FileStore的 String type() 方法返回的文件存储类型为NTFS时,该文件存储的实现可以识别列表中的属性(不应该通过 getAttribute() 方法获取总空间、使用空间、未分配空间,你应该使用更方便的方法,long getTotalSpace()、long getUsableSpace() 和 long getUnallocatedSpace() 方法)。
编译列表9,并运行。如 java PathAttrib PathAttrib.java。我观察到的输出结果如下:如果文件存储类型不是NTFS,则没有任何输出:
499808989184 229907410944 229907410944 1079402743 false false
FileStore也声明一个 <V extends FileStoreAttributeView> V getFileStoreAttributeView(Class<V> type) 方法来获取指定类型的FileStoreAttributeView(一个只读或可更新的文件存储属性视图)对象,但因为官方没有提供子接口,这些方法可能只适合于用户自定义的文件系统和API包。
在第三部分,我将演示更多高级的关于目录层级的拷贝、文件的查找及目录改变的监控的方法来结束这个系列。你将学习到一些文件的访问、循环、监控的内容。
原文链接: JavaWorld 翻译:ImportNew.com -paddx
译文链接:[]