问题
最近遇到一个问题,使用Java写某个DSL标记语言X的parser(解析器)Maven插件的时候,对外暴露一个名为Callback
的接口和一个待实现的方法getHTML()——基于调用处传入的文件名srcX
构造出HTML文件的输出路径(其实此处的Callback
就是一个闭包,文件名是一个自由变量)。大致代码如下:
parser.parse(srcX, new Callback() {
@Override
public FileWriter getHTML() {
return new FileWriter(outputPath(suffix(srcX, "html")));
}
});
private String suffix(String filename, String suffix) {
return Joiner.on(".").join(filename, suffix);
}
//这里假设输入和输出根路径地址已知
private File outputPath(String file) {
return new File(
file.replace(srcDir.getAbsolutePath(), //srcDir: File
outputDir.getAbsolutePath())); //outputDir: File
}
目前为止还没有任何问题。但若是运行时,这段程序很可能抛出异常java.io.FileNotFoundException: your-file-name (No such file or directory)。原因在于file的路径当中可能存在多级父级目录,例如:outputDir/p1/p2/srcX.html,那么当FileWriter尝试创建srcX.html就会失败。此时最简单的方法就是提前创建好所有的父级目录,于是outputPath()方法会变成下面这样:
private File outputPath(String file) {
File outputFile = new File(
file.replace(srcDir.getAbsolutePath(),
outputDir.getAbsolutePath()));
outputFile.getParentFile().mkdirs(); //创建可能不存在的父级目录
return outputFile;
}
似乎这段程序可以正常工作了,但是创建文件夹这样的操作是可能失败的。所以我们需要关注是否创建成功,若失败,则写入Log文件当中。修改程序如下:
private File outputPath(String file) {
File outputFile = new File(
file.replace(srcDir.getAbsolutePath(),
outputDir.getAbsolutePath()));
final File parentDirs = outputFile.getParentFile();
if (!parentDirs.exists()) {
if (!parentDirs.mkdirs()) {//创建可能不存在的父级目录
getLog().error("Cannot create parent dirs for {}", outputFile);
}
}
return outputFile;
}
注意
这里我们需要先判断父级目录是否存在,即parentDirs.exists()?可是parentDirs.mkdirs()不是直接返回boolean值来表示是否创建成功吗?是这样么?这儿有mkdirs()方法的说明:
public boolean mkdirs()
Creates the directory named by this abstract pathname, including any necessary but nonexistent parent directories. Note that if this operation fails it may have succeeded in creating some of the necessary parent directories.
Returns:
true if and only if the directory was created, along with all necessary parent directories; false otherwise
也就是说只有当这个目录及其所有的父级目录都被创建时,才返回true,反之返回false。照这个推论,如果所有目录事先已经存在了,这个方法应该也会返回true,毕竟都被创建过了嘛。但是只要稍微看一眼源码,你就会发现事实并非如此:
//mkdirs源码
if (exists()) {
return false;
}
所以这里需要特别强调was created是一种操作,如果没有进行这个操作,那就不能算这个方法成功。
前面已经提到过,我需要写一个maven的插件,所以最好在这种导致程序崩溃的地方抛出一个maven中通用的异常MojoExecutionException。这样,更改代码如下:
private File outputPath(String file) {
File outputFile = new File(
file.replace(srcDir.getAbsolutePath(),
outputDir.getAbsolutePath()));
final File parentDirs = outputFile.getParentFile();
if (!parentDirs.exists()) {
if (!parentDirs.mkdirs()) {//创建可能不存在的父级目录
getLog().error("Cannot create parent dirs for {}", outputFile);
throw new MojoExecutionException("Cannot create parent dirs");
}
}
return outputFile;
}
此时,问题才显出端倪——异常MojoExecutionException是一个受检的异常(checked Exception),它间接继承自java.lang.Exception。可是我们的getHTML()方法并没有在签名中抛出任何异常,编译无法通过。那唯一的办法就是try...catch了,但是我不应该捕获自己刚刚抛出来的异常,否则抛出受检异常的意义何在?
这时,自然而然会想到,将方法签名改成getHTML() throws MojoExecutionException。确实可行,但是并不合适,因为MojoExecutionException只是Maven插件规定的异常,而getHTML()则是一个对外暴露的API,不应该依赖于某个具体的异常。所以我将异常扩大化:getHTML() throws Exception,这样做的好处很明显,坏处也很显眼。
好处
- 牢记《Unix编程艺术》中的“宽收严发”原则。即子类实现父类、接口的方法,入参可以扩大,出参可以缩小。举个例子:父类、接口有个方法
public Object something(HashMap map) throws Exception
那么子类实现这个方法可以这样写
public String something(Map map)
throws ExecutionException, NoSuchMethodException
这里,入参是HashMap,出参是Object和Exception。入参扩大,所以子类出现了Map;出参缩小,所以子类出现了String和ExecutionException和NoSuchMethodException。同理,此处getHTML() throws Exception由子类实现的形式可以是getHTML() throws MojoExecutionException。
坏处
- 不管getHTML()是否需要抛出异常,你都得在实现代码中抛出异常;
- 由于对外表现的是抛出较宽泛的Exception,所以丧失了对于具体受检 (checked exception)异常进行检查的好处。
这里有个JDK中比较类似的例子,就是关于Runnable
和Callable
接口的设计问题:
public interface Runnable {
public void run();
}
public interface Callable<V> {
V call() throws Exception;
}
它们就是两个极端,Runnable
必须将受检的异常转换成非受检(unchecked exception)或者发明一种方式来将异常暴露给调用者;Callable
就是无论如何都得抛出异常,而且迫使用户去捕获一个较宽泛的异常。
解决方式
这个时候,泛型就派上用场了。
interface Callback<E extends Exception> {
FileWriter getHTML() throws E;
}
//interface parser
public <E extends Exception> void parse(String srcX, Callback<E> cb) throws E;
通过这种方式,我们可以捕获具体的异常:
try {
parser.parse(srcX, new Callback<MojoExecutionException>() {
@Override
public FileWriter getHTML() throws MojoExecutionException {
return new FileWriter(outputPath(suffix(srcX, "html")));
}
});
} catch (MojoExecutionException e) {
getLog().error("Failed to execute. {}", e);
}
使用lambda表达式可以简化成下面的模样:
try {
parser.parse(srcX, (Callback<MojoExecutionException>) () -> new FileWriter(outputPath(suffix(srcX, "html"))));
} catch (MojoExecutionException e) {
getLog().error("Failed to execute. {}", e);
}
我们解决了迫使用户去捕获一个较宽泛的异常的问题,但是无论如何都得抛出异常这个问题还是没有得到解决。或许我们需要一个像是throws Nothing一样的语法,表示什么也没有抛出来。我们知道RuntimeException是非受检的异常(unchecked exception),所以throws RuntimeException就表明这个异常跟没有抛出异常一样,不需要捕获。如下:
parser.parse(srcX, new Callback<Nothing>() {
@Override
public FileWriter getHTML() throws Nothing {
return new FileWriter(outputPath(suffix(srcX, "html")));
}
});
public abstract class Nothing extends RuntimeException {}
走到这一步,我们算是较为完全地解决了匿名内部类的异常处理问题。
异常透明化
With the throws type parameter on the Block interface, we can now accurately generify over the set of exceptions thrown by the Block; with the generic forEach method, we can mirror the exception behavior of the block in forEach(). This is called exception transparency because now the exception behavior of forEach can match the exception behavior of its block argument. Exception transparency simplifies the construction of library classes that implement idioms like internal iteration of data structures, because it is common that methods that accept function-valued arguments will invoke those functions, meaning that the library method will throw a superset of the exceptions thrown by its function-valued arguments.
interface Block<T, throws E> {
public void invoke(T element) throws E;
}
interface NewCollection<T> {
public<throws E> forEach(Block<T, throws E> block) throws E;
}
异常透明化,简单来讲,就是调用者的签名中的异常完全由它的函数值(function-valued)的参数决定,所有这些调用者最终的异常都会是该函数值所注异常的超集。
异常透明化就是用来解决我们常用的通过内部类模拟闭包调用时异常处理的手法了。
闭包的定义
一个包含了自由变量的开发表达式,和该自由变量的约束环境组合之后,产生了一种封闭的状态。
参考链接
[1] Exception Transparency
[2] Throwing Checked Exceptions from Anonymous Inner Classes