每当提到Exeption就会有人跳出来说“Exception not use for flow control”,那到底是什么意思呢?什么情况下Exception就算控制流程了,什么时候就该抛出Exception了呢?
首先什么是Exception?
Definition: An exception is an event, which occurs during the execution of a program, that disrupts the normal flow of the program's instructions.
再看什么是“流程”?如果流程是指程序的每一步执行,那异常就是控制流程的,它就是用来区分程序的正常流程和非正常流程的,从上面异常的定义就可以看出。因此为了明确我们应该说”不要用异常控制程序的正常流程“。如何定义正常流程和非正常流程很难,这是一个主观的决定,没有一个统一的标准,只能根据实际情况。网上找个例子:
bool isDouble(string someString) {
try {
double d = Convert.ParseInt32(someString);
} catch(FormatException e) {
return false;
}
return true;
}
这个程序其实不是想convert数字,而是想知道一个字符串是否包含一个数字,通过判断是不是有异常的方式来决定返回true还是false,这是个Smell,这种应该算”异常控制了正常流程“。我们可以通过正则表达式或其他方式来判断。
另外Clean Code上一个例子:
try {
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();
} catch(MealExpensesNotFound e) {
m_total += getMealPerDiem();
}
MealExpensesNotFound异常影响了正常的计算m_total的业务逻辑。对于这种情况可以通过一下方式改进:
public class PerDiemMealExpenses implements MealExpenses {
public int getTotal() {
// return the per diem default
}
}
以上两个例子是比较明显的异常控制正常流程,Smell很明显,不会有很大争议,但是实际情况中可能有很多例子没有这么明显,因为都是主观判定的。比如一下代码,算不算异常控制正常流程?
public int doSomething()
{
doA();
try {
doB();
} catch (MyException e) {
return ERROR;
}
doC();
return SUCCESS;
}
看到这样一段程序,如果没有上下文,我们无法判断。但是如果doSomething是想让我们回答yes or no,success or error,我们不应该通过有无异常来判断yes or no,success or error,应该有个单独的方法来判断,这个方法就只做这一件事情。如果doSometing是执行一个操作,那么在这个过程中我们假定是不会出现问题的,否则抛出异常是比较合理的。
1. Java Class Loading Mechanism
首先当编译一个Java文件时,编译器就会在生成的字节码中内置一个public,static,final的class字段,该字段属于java.lang.Class类型,该class字段使用点来访问,所以可以有:
java.lang.Class clazz = MyClass.class
当class被JVM加载,就不再加载相同的class。class在JVM中通过(ClassLoader,Package,ClassName)来唯一决定。ClassLoader指定了一个class的scope,这意味着如果两个相同的包下面的class被不同的ClassLoader加载,它们是不一样的,并且不是type-compatible的。
JVM中所有的ClassLoader(bootstrap ClassLoader除外)都是直接或间接继承于java.lang.ClassLoader抽象类,并且人为逻辑上指定了parent-child关系,实现上child不一定继承于parent,我们也可以通过继承它来实现自己的ClassLoader。
JVM ClassLoder架构,从上到下依次为parent-child关系:
- Bootstrap ClassLoader - 启动类加载器,主要负责加载核心Java类如java.lang.Object和其他运行时所需class,位于JRE/lib目录下或-Xbootclasspath指定的目录。我们不知道过多的关于Bootstrap ClassLoader的细节,因为它是一个native的实现,不是Java实现,所以不同JVMs的Bootstrap ClassLoader的行为也不尽相同。调用java.lang.String.getClassLoder() 返回null。
- sun.misc.ExtClassLoader - 扩展类加载器,负责加载JRE/lib/ext目录及-Djava.ext.dirs指定目录。
- sun.misc.AppClassLoader - 应用类加载器,负责加载java.class.path目录
另外,还有一些其他的ClassLoader如:
java.net.URLClassLoader,java.security.SecureClassLoader,java.rmi.server.RMIClassLoader,sun.applet.AppletClassLoader
- 用户还可以自己继承java.lang.ClassLoader来实现自己的ClassLoader,用来动态加载class文件。
ClassLoader特性:
- 每个ClassLoader维护一份自己的命名空间,同一个ClassLoader命名空间不能加载两个同名的类。
- 为实现Java安全沙箱模型,默认采用parent-child加载链结构,除Bootstrap ClassLoader没有parent外,每个ClassLoader都有一个逻辑上的parent,就是加载这个ClassLoader的ClassLoader,因为ClassLoader本身也是一个类,直接或间接的继承java.lang.ClassLoader抽象类。
java.lang.Thread中包含一个public的方法public ClassLoader getContextClassLoader(),它返回某一线程相关的ClassLoader,该ClassLoader是线程的创建者提供的用来加载线程中运行的classes和资源的。如果没有显式的设置其ClassLoader,默认是parent线程的Context ClassLoader。Java默认的线程上下文加载器是AppClassLoader。
ClassLoader工作原理:
了解ClassLoader工作原理,先来看一个ClassLoader类简化版的loadClass()方法源码
1 protected Class<?> loadClass(String name, boolean resolve)
2 throws ClassNotFoundException
3 {
4 synchronized (getClassLoadingLock(name)) {
5 // First, check if the class has already been loaded
6 Class c = findLoadedClass(name);
7 if (c == null) {
8 long t0 = System.nanoTime();
9 try {
10 if (parent != null) {
11 c = parent.loadClass(name, false);
12 } else {
13 c = findBootstrapClassOrNull(name);
14 }
15 } catch (ClassNotFoundException e) {
16 // ClassNotFoundException thrown if class not found
17 // from the non-null parent class loader
18 }
19
20 if (c == null) {
21 // If still not found, then invoke findClass in order
22 // to find the class.
24 c = findClass(name);
25 }
26 }
27 if (resolve) {
28 resolveClass(c);
29 }
30 return c;
31 }
32 }
首先查看该class是否已被加载,如果已被加载则直接返回,否则调用parent的loadClass来加载,如果parent是null代表是Bootstrap ClassLoader,则有Bootstrap ClassLoader来加载,如果都未加载成功,最后由该ClassLoader自己加载。这种parent-child委派模型,保证了恶意的替换Java核心类不会发生,因为如果定义了一个恶意java.lang.String,它首先会被JVM的Bootstrap ClassLoader加载自己JRE/lib下的,而不会加载恶意的。另外,Java允许同一package下的类可以访问受保护成员的访问权限,如定义一个java.lang.Bad,但是因为java.lang.String由Bootstrap ClassLoader加载而java.lang.Bad由AppClassLoader加载,不是同一ClassLoader加载,仍不能访问。
2. Hotswap - 热部署
即不重启JVM,直接替换class。因为ClassLoader特性,同一个ClassLoader命名空间不能加载两个同名的类,所以在不重启JVM的情况下,只能通过新的ClassLoader来重新load新的class。
1 public static void main(String[] args) throws InterruptedException, MalformedURLException {
2 IExample oldExample = new Example();
3 oldExample.plus();
4 System.out.println(oldExample.getCount());
5
6 Hotswap hotswap = new Hotswap();
7 while (true) {
8 IExample newExample = hotswap.swap(oldExample);
9 String message = newExample.message();
10 int count = newExample.plus();
11 System.out.println(message.concat(" : " + count));
12 oldExample = newExample;
13 Thread.sleep(5000);
14 }
15 }
16
利用hotswap替换就的Example,每5秒钟轮询一次,swap方法实现如下:
1 private IExample swap(IExample old) {
2 try {
3 String sourceFile = srcPath().concat("Example.java");
4 if (isChanged(sourceFile)) {
5 comiple(sourceFile, classPath());
6 MyClassLoader classLoader = new MyClassLoader(new URL[]{new URL("file:"+classPath())});
7 Class<?> clazz = classLoader.loadClass("Example");
8 System.out.println(IExample.class.getClassLoader());
9 IExample exampleInstance = ((IExample) clazz.newInstance()).copy(old);
10 System.out.println(exampleInstance.getClass().getClassLoader());
11 return exampleInstance;
12 }
13 } catch ...
24 return old;
25 }
这里必须将exampleInstance转型为IExample接口而不是Exmaple,否则会抛出ClassCastExecption,这是因为swap方法所在类Hotswap是有AppClassLoader加载的,而且加载Hotswap的同时会加载该类引用的Exmaple的symbol link,而Example是MyClassLoader加载的,不同的ClassLoader加载的类之间直接用会抛出ClassCastException, 在本例中ClassLoader实现如下:
1 public class MyClassLoader extends URLClassLoader {
2
3 public MyClassLoader(URL[] urls) {
4 super(urls);
5 }
6
7 @Override
8 public Class<?> loadClass(String name) throws ClassNotFoundException {
9 if ("Example".equals(name)) {
10 return findClass(name);
11 }
12 return super.loadClass(name);
13 }
14 }
而对IExample我们还是调用super的loadClass方法,该方法实现仍是JVM的parent-child委派方式,因此最终由AppClassLoader加载,加载Hotswap时加载的symbol link也是由AppClassLoader加载的,因此能够成功。
此外再热部署时,被替换的类的所有引用及状态都要迁移到新的类上,本例中只是很简单的调用copy函数迁移了count的状态。
Tomcat的jsp热部署机制就是基于ClassLoader实现的,对于其类的热部署机制是通过修改内存中的class字节码实现的。
Resource:
1. java.lang.IllegalThreadStateException: process hasn't exited1 public static void main(String[] args) {
2 try {
3 Process process = Runtime.getRuntime().exec("javac");
4 System.out.println(process.exitValue());
5 } catch (IOException e) {
6 e.printStackTrace();
7 }
8 }
exec方法创建了一个native的进程,并返回该process的对象,如果进程还没有返回,调用exitValue方法就会出现此异常,因为该方法没有阻塞,其实现如下:
1 public synchronized int exitValue() {
2 if (!hasExited) {
3 throw new IllegalThreadStateException("process hasn't exited");
4 }
5 return exitcode;
6 }
2. waitFor方法
1 public static void main(String[] args) {
2 try {
3 Process process = Runtime.getRuntime().exec("javac");
4 int result = process.waitFor();
5 System.out.println(result);
6 } catch (IOException e) {
7 e.printStackTrace();
8 } catch (InterruptedException e) {
9 e.printStackTrace();
10 }
11 }
waitFor方法会一直阻塞直到native进程完成,并返回native进程的执行结果。如果native进程无法执行完成,waitFor方法将一直阻塞下去,其实现如下:
1 public synchronized int waitFor() throws InterruptedException {
2 while (!hasExited) {
3 wait();
4 }
5 return exitcode;
6 }
该程序在jdk1.7 windows下测试工作正常,返回2; 但是jdk1.4 windows下测试出现hang。JDK documention的解释是
The methods that create processes may not work well for special processes on certain native platforms,
such as native windowing processes, daemon processes, Win16/DOS processes on Microsoft Windows,or shell scripts.
The created subprocess does not have its own terminal or console. All its standard io (i.e. stdin, stdout, stderr)
operations will be redirected to the parent process through three streams (getOutputStream(), getInputStream(),
getErrorStream()). The parent process uses these streams to feed input to and get output from the subprocess. Because some native platforms only provide limited buffer size for standard input and output streams,
failure to promptly write the input stream or read the output stream of the subprocess may cause
the subprocess to block, and even deadlock.
所以,出现hang时,及时的flush标准输入输出或者错误流能够消除hang,如上面的javac,我们知道redirect到stderr中,所以解决hang后的代码
1 public static void main(String[] args) {
2 try {
3 Process process = Runtime.getRuntime().exec("echo 'abc'>b.txt");
4 BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
5 String line;
6 while((line=reader.readLine())!=null){
7 System.out.println(line);
8 }
9 int result = process.waitFor();
10 System.out.println(result);
11 } catch (IOException e) {
12 e.printStackTrace();
13 } catch (InterruptedException e) {
14 e.printStackTrace();
15 }
16 }
3. exec() is not a command line 并不是所有的command line命令都可以用exec 1 public static void main(String[] args) {
2 try {
3 Process process = Runtime.getRuntime().exec("echo 'abc'>a.txt");
4 BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
5 String line;
6 while((line=reader.readLine())!=null){
7 System.out.println(line);
8 }
9 int result = process.waitFor();
10 System.out.println(result);
11 } catch (IOException e) {
12 e.printStackTrace();
13 } catch (InterruptedException e) {
14 e.printStackTrace();
15 }
16 }
结果为:
1 'abc'>a.txt
2 0
并没有将创建a.txt,而从命令行执行
"echo 'abc'>a.txt"却正确创建了a.txt