Home > Java > javaTutorial > Summary of exception handling in Java EE projects (a must-read article)

Summary of exception handling in Java EE projects (a must-read article)

高洛峰
Release: 2017-01-17 11:17:20
Original
1528 people have browsed it

Why should we talk about exception handling in J2EE projects? Many Java beginners may want to say: "Isn't exception handling just try....catch...finally? Everyone knows this!". The author thought so when I first learned Java. How to define the corresponding exception class in a multi-layer j2ee project? How to handle exceptions at each layer in the project? When is an exception thrown? When are exceptions logged? How should exceptions be recorded? When do you need to convert checked Exception into unchecked Exception, and when do you need to convert unChecked Exception into checked Exception? Should exceptions be presented to the front-end page? How to design an exception framework? This article will explore these issues.

1. JAVA Exception Handling

In procedural programming languages, we can determine whether a method is executed normally by returning a value. For example, in a program written in C language, if the method is executed correctly, 1 will be returned. If there is an error, 0 will be returned. In applications developed in VB or Delphi, when an error occurs, we pop up a message box to the user.

We cannot obtain the detailed information of the error through the return value of the method. Maybe because the methods are written by different programmers, when the same type of error occurs in different methods, the returned results and error messages are not consistent.

So the Java language adopts a unified exception handling mechanism.

What is an exception? Errors that occur at runtime and can be caught and handled.

In the Java language, Exception is the parent class of all exceptions. Any exception extends the Exception class. Exception is equivalent to an error type. If you want to define a new error type, extend a new Exception subclass. The advantage of using exceptions is that you can accurately locate the source code location that caused the program error and obtain detailed error information.

Java exception handling is implemented through five keywords, try, catch, throw, throws, finally. The specific exception handling structure is implemented by the try….catch….finally block. The try block stores Java statements that may cause exceptions, and catch is used to capture exceptions and handle them. The Finally block is used to clear unreleased resources in the program. Regardless of how the code in the try block returns, the finally block is always executed.

A typical exception handling code

public String getPassword(String userId)throws DataAccessException{
String sql = “select password from userinfo where userid='”+userId +”'”;
String password = null;
Connection con = null;
Statement s = null;
ResultSet rs = null;
try{
con = getConnection();//获得数据连接
s = con.createStatement();
rs = s.executeQuery(sql);
while(rs.next()){
password = rs.getString(1);
}
rs.close();
s.close();
}
Catch(SqlException ex){
throw new DataAccessException(ex);
}
finally{
try{
if(con != null){
con.close();
}
}
Catch(SQLException sqlEx){
throw new DataAccessException(“关闭连接失败!”,sqlEx);
}
}
return password;
}
Copy after login


You can see the advantages of Java's exception handling mechanism:

Unified classification of errors is implemented by extending the Exception class or its subclasses. This avoids the possibility that the same error may have different error messages in different methods. When the same error occurs in different methods, just throw the same exception object.

Get more detailed error information. Through exception classes, exceptions can be given more detailed error information that is more useful to users. To facilitate users to trace and debug programs.

Separate the correct return result from the error message. Reduces program complexity. The caller does not need to know more about the returned results.

Force the caller to handle exceptions to improve the quality of the program. When a method declaration needs to throw an exception, the caller must use a try...catch block to handle the exception. Of course, the caller can also let the exception continue to be thrown to the upper level.

2. Checked exception or unChecked exception?

Java exceptions are divided into two categories: checked exceptions and unChecked exceptions. All exceptions that inherit java.lang.Exception are checked exceptions. All exceptions that inherit java.lang.RuntimeException are unChecked exceptions.

When a method calls a method that may throw a checked exception, the exception must be caught and processed or re-thrown through a try...catch block.
Let’s take a look at the declaration of the createStatement() method of the Connection interface.

public Statement createStatement() throws SQLException;
SQLException是checked异常。当调用createStatement方法时,java强制调用者必须对SQLException进行捕获处理。
public String getPassword(String userId){
try{
……
Statement s = con.createStatement();
……
Catch(SQLException sqlEx){
……
}
……
}
Copy after login

or

public String getPassword(String userId)throws SQLException{
Statement s = con.createStatement();
}
Copy after login

(Of course, resources like Connection and Satement need to be closed in time. This is just to illustrate that checked exceptions must force the caller to catch or continue to throw)

unChecked exception is also called a runtime exception. Usually RuntimeException indicates an exception that the user cannot recover from, such as being unable to obtain a database connection, unable to open a file, etc. Although users can also catch unChecked exceptions just like checked exceptions. But if the caller does not catch the unChecked exception, the compiler will not force you to do so.

For example, a code that converts characters into integer values ​​is as follows:

String str = “123”;
int value = Integer.parseInt(str);
Copy after login

The method signature of parseInt is:

public staticint parseInt(String s)throws NumberFormatException
Copy after login

When the incoming parameters cannot be converted to If the corresponding integer is found, a NumberFormatException will be thrown. Because NumberFormatException extends from RuntimeException, it is an unChecked exception. Therefore, there is no need to try…catch

when calling the parseInt method.

因为java不强制调用者对unChecked异常进行捕获或往上抛出。所以程序员总是喜欢抛出unChecked异常。或者当需要一个新的异常类时,总是习惯的从RuntimeException扩展。当你去调用它些方法时,如果没有相应的catch块,编译器也总是让你通过,同时你也根本无需要去了解这个方法倒底会抛出什么异常。看起来这似乎倒是一个很好的办法,但是这样做却是远离了java异常处理的真实意图。并且对调用你这个类的程序员带来误导,因为调用者根本不知道需要在什么情况下处理异常。而checked异常可以明确的告诉调用者,调用这个类需要处理什么异常。如果调用者不去处理,编译器都会提示并且是无法编译通过的。当然怎么处理是由调用者自己去决定的。

所以Java推荐人们在应用代码中应该使用checked异常。就像我们在上节提到运用异常的好外在于可以强制调用者必须对将会产生的异常进行处理。包括在《java Tutorial》等java官方文档中都把checked异常作为标准用法。

使用checked异常,应意味着有许多的try…catch在你的代码中。当在编写和处理越来越多的try…catch块之后,许多人终于开始怀疑checked异常倒底是否应该作为标准用法了。

甚至连大名鼎鼎的《thinking in java》的作者Bruce Eckel也改变了他曾经的想法。Bruce Eckel甚至主张把unChecked异常作为标准用法。并发表文章,以试验checked异常是否应该从java中去掉。Bruce Eckel语:“当少量代码时,checked异常无疑是十分优雅的构思,并有助于避免了许多潜在的错误。但是经验表明,对大量代码来说结果正好相反”

关于checked异常和unChecked异常的详细讨论可以参考

《java Tutorial》 http://java.sun.com/docs/books/tutorial/essential/exceptions/runtime.html

使用checked异常会带来许多的问题。

checked异常导致了太多的try…catch 代码

可能有很多checked异常对开发人员来说是无法合理地进行处理的,比如SQLException。而开发人员却不得不去进行try…catch。当开发人员对一个checked异常无法正确的处理时,通常是简单的把异常打印出来或者是干脆什么也不干。特别是对于新手来说,过多的checked异常让他感到无所适从。

try{
……
Statement s = con.createStatement();
……
Catch(SQLException sqlEx){
sqlEx.PrintStackTrace();
}
Copy after login

或者

try{
……
Statement s = con.createStatement();
……
Catch(SQLException sqlEx){
//什么也不干
}
Copy after login

checked异常导致了许多难以理解的代码产生

当开发人员必须去捕获一个自己无法正确处理的checked异常,通常的是重新封装成一个新的异常后再抛出。这样做并没有为程序带来任何好处。反而使代码晚难以理解。

就像我们使用JDBC代码那样,需要处理非常多的try…catch.,真正有用的代码被包含在try…catch之内。使得理解这个方法变理困难起来

checked异常导致异常被不断的封装成另一个类异常后再抛出

public void methodA()throws ExceptionA{
…..
throw new ExceptionA();
}
public void methodB()throws ExceptionB{
try{
methodA();
……
}catch(ExceptionA ex){
throw new ExceptionB(ex);
}
}
Public void methodC()throws ExceptinC{
try{
methodB();
…
}
catch(ExceptionB ex){
throw new ExceptionC(ex);
}
}
Copy after login

我们看到异常就这样一层层无休止的被封装和重新抛出。

checked异常导致破坏接口方法

一个接口上的一个方法已被多个类使用,当为这个方法额外添加一个checked异常时,那么所有调用此方法的代码都需要修改。
可见上面这些问题都是因为调用者无法正确的处理checked异常时而被迫去捕获和处理,被迫封装后再重新抛出。这样十分不方便,并不能带来任何好处。在这种情况下通常使用unChecked异常。

chekced异常并不是无一是处,checked异常比传统编程的错误返回值要好用得多。通过编译器来确保正确的处理异常比通过返回值判断要好得多。

如果一个异常是致命的,不可恢复的。或者调用者去捕获它没有任何益处,使用unChecked异常。

如果一个异常是可以恢复的,可以被调用者正确处理的,使用checked异常。

在使用unChecked异常时,必须在在方法声明中详细的说明该方法可能会抛出的unChekced异常。由调用者自己去决定是否捕获unChecked异常

倒底什么时候使用checked异常,什么时候使用unChecked异常?并没有一个绝对的标准。但是笔者可以给出一些建议

当所有调用者必须处理这个异常,可以让调用者进行重试操作;或者该异常相当于该方法的第二个返回值。使用checked异常。
这个异常仅是少数比较高级的调用者才能处理,一般的调用者不能正确的处理。使用unchecked异常。有能力处理的调用者可以进行高级处理,一般调用者干脆就不处理。

这个异常是一个非常严重的错误,如数据库连接错误,文件无法打开等。或者这些异常是与外部环境相关的。不是重试可以解决的。使用unchecked异常。因为这种异常一旦出现,调用者根本无法处理。

如果不能确定时,使用unchecked异常。并详细描述可能会抛出的异常,以让调用者决定是否进行处理。

3. 设计一个新的异常类

在设计一个新的异常类时,首先看看是否真正的需要这个异常类。一般情况下尽量不要去设计新的异常类,而是尽量使用java中已经存在的异常类。

IllegalArgumentException, UnsupportedOperationException
Copy after login


不管是新的异常是chekced异常还是unChecked异常。我们都必须考虑异常的嵌套问题。

public void methodA()throws ExceptionA{
…..
throw new ExceptionA();
}
Copy after login

方法methodA声明会抛出ExceptionA.

public void methodB()throws ExceptionB

methodB声明会抛出ExceptionB,当在methodB方法中调用methodA时,ExceptionA是无法处理的,所以ExceptionA应该继续往上抛出。一个办法是把methodB声明会抛出ExceptionA.但这样已经改变了MethodB的方法签名。一旦改变,则所有调用methodB的方法都要进行改变。

另一个办法是把ExceptionA封装成ExceptionB,然后再抛出。如果我们不把ExceptionA封装在ExceptionB中,就丢失了根异常信息,使得无法跟踪异常的原始出处。

public void methodB()throws ExceptionB{
try{
methodA();
……
}catch(ExceptionA ex){
throw new ExceptionB(ex);
}
}
Copy after login


如上面的代码中,ExceptionB嵌套一个ExceptionA.我们暂且把ExceptionA称为“起因异常”,因为ExceptionA导致了ExceptionB的产生。这样才不使异常信息丢失。

所以我们在定义一个新的异常类时,必须提供这样一个可以包含嵌套异常的构造函数。并有一个私有成员来保存这个“起因异常”。

public Class ExceptionB extends Exception{
private Throwable cause;
public ExceptionB(String msg, Throwable ex){
super(msg);
this.cause = ex;
}
public ExceptionB(String msg){
super(msg);
}
public ExceptionB(Throwable ex){
this.cause = ex;
}
}
Copy after login


当然,我们在调用printStackTrace方法时,需要把所有的“起因异常”的信息也同时打印出来。所以我们需要覆写printStackTrace方法来显示全部的异常栈跟踪。包括嵌套异常的栈跟踪。

public void printStackTrace(PrintStrean ps){
if(cause == null){
super.printStackTrace(ps);
}else{
ps.println(this);
cause.printStackTrace(ps);
}
}
Copy after login

一个完整的支持嵌套的checked异常类源码如下。我们在这里暂且把它叫做NestedException

public NestedException extends Exception{
private Throwable cause;
public NestedException (String msg){
super(msg);
}
public NestedException(String msg, Throwable ex){
super(msg);
This.cause = ex;
}
public Throwable getCause(){
return (this.cause ==null ?this :this.cause);
}
public getMessage(){
String message = super.getMessage();
Throwable cause = getCause();
if(cause != null){
message = message + “;nested Exception is ” + cause;
}
return message;
}
public void printStackTrace(PrintStream ps){
if(getCause == null){
super.printStackTrace(ps);
}else{
ps.println(this);
getCause().printStackTrace(ps);
}
}
public void printStackTrace(PrintWrite pw){
if(getCause() == null){
super.printStackTrace(pw);
}
else{
pw.println(this);
getCause().printStackTrace(pw);
}
}
public void printStackTrace(){
printStackTrace(System.error);
}
}
Copy after login



同样要设计一个unChecked异常类也与上面一样。只是需要继承RuntimeException。

4. 如何记录异常

作为一个大型的应用系统都需要用日志文件来记录系统的运行,以便于跟踪和记录系统的运行情况。系统发生的异常理所当然的需要记录在日志系统中。

public String getPassword(String userId)throws NoSuchUserException{
UserInfo user = userDao.queryUserById(userId);
If(user == null){
Logger.info(“找不到该用户信息,userId=”+userId);
throw new NoSuchUserException(“找不到该用户信息,userId=”+userId);
}
else{
return user.getPassword();
}
}
public void sendUserPassword(String userId)throws Exception {
UserInfo user = null;
try{
user = getPassword(userId);
//……..
sendMail();
//
}catch(NoSuchUserException ex)(
logger.error(“找不到该用户信息:”+userId+ex);
throw new Exception(ex);
}
Copy after login



我们注意到,一个错误被记录了两次.在错误的起源位置我们仅是以info级别进行记录。而在sendUserPassword方法中,我们还把整个异常信息都记录了。

笔者曾看到很多项目是这样记录异常的,不管三七二一,只有遇到异常就把整个异常全部记录下。如果一个异常被不断的封装抛出多次,那么就被记录了多次。那么异常倒底该在什么地方被记录?

异常应该在最初产生的位置记录!

如果必须捕获一个无法正确处理的异常,仅仅是把它封装成另外一种异常往上抛出。不必再次把已经被记录过的异常再次记录。

如果捕获到一个异常,但是这个异常是可以处理的。则无需要记录异常

public Date getDate(String str){
Date applyDate = null;
SimpleDateFormat format = new SimpleDateFormat(“MM/dd/yyyy”);
try{
applyDate = format.parse(applyDateStr);
}
catch(ParseException ex){
//乎略,当格式错误时,返回null
}
return applyDate;
}
Copy after login


捕获到一个未记录过的异常或外部系统异常时,应该记录异常的详细信息

try{
……
String sql=”select * from userinfo”;
Statement s = con.createStatement();
……
Catch(SQLException sqlEx){
Logger.error(“sql执行错误”+sql+sqlEx);
}
Copy after login


究竟在哪里记录异常信息,及怎么记录异常信息,可能是见仁见智的问题了。甚至有些系统让异常类一记录异常。当产生一个新异常对象时,异常信息就被自动记录。

public class BusinessException extends Exception {
private void logTrace() {
StringBuffer buffer=new StringBuffer();
buffer.append("Business Error in Class: ");
buffer.append(getClassName());
buffer.append(",method: ");
buffer.append(getMethodName());
buffer.append(",messsage: ");
buffer.append(this.getMessage());
logger.error(buffer.toString());
}
public BusinessException(String s) {
super(s);
race();
}
Copy after login



这似乎看起来是十分美妙的,其实必然导致了异常被重复记录。同时违反了“类的职责分配原则”,是一种不好的设计。记录异常不属于异常类的行为,记录异常应该由专门的日志系统去做。并且异常的记录信息是不断变化的。我们在记录异常同应该给更丰富些的信息。以利于我们能够根据异常信息找到问题的根源,以解决问题。

虽然我们对记录异常讨论了很多,过多的强调这些反而使开发人员更为疑惑,一种好的方式是为系统提供一个异常处理框架。由框架来决定是否记录异常和怎么记录异常。而不是由普通程序员去决定。但是了解些还是有益的。

5. J2EE项目中的异常处理

目前,J2ee项目一般都会从逻辑上分为多层。比较经典的分为三层:表示层,业务层,集成层(包括数据库访问和外部系统的访问)。

J2ee项目有着其复杂性,J2ee项目的异常处理需要特别注意几个问题。

在分布式应用时,我们会遇到许多checked异常。所有RMI调用(包括EJB远程接口调用)都会抛出java.rmi.RemoteException;同时RemoteException是checked异常,当我们在业务系统中进行远程调用时,我们都需要编写大量的代码来处理这些checked异常。而一旦发生RemoteException这些checked异常对系统是非常严重的,几乎没有任何进行重试的可能。也就是说,当出现RemoteException这些可怕的checked异常,我们没有任何重试的必要性,却必须要编写大量的try…catch代码去处理它。一般我们都是在最底层进行RMI调用,只要有一个RMI调用,所有上层的接口都会要求抛出RemoteException异常。因为我们处理RemoteException的方式就是把它继续往上抛。这样一来就破坏了我们业务接口。RemoteException这些J2EE系统级的异常严重的影响了我们的业务接口。我们对系统进行分层的目的就是减少系统之间的依赖,每一层的技术改变不至于影响到其它层。

//
public class UserSoaImplimplements UserSoa{
public UserInfo getUserInfo(String userId)throws RemoteException{
//……
远程方法调用.
//……
}
}
public interface UserManager{
public UserInfo getUserInfo(Stirng userId)throws RemoteException;
}
Copy after login


同样JDBC访问都会抛出SQLException的checked异常。

为了避免系统级的checked异常对业务系统的深度侵入,我们可以为业务方法定义一个业务系统自己的异常。针对像SQLException,RemoteException这些非常严重的异常,我们可以新定义一个unChecked的异常,然后把SQLException,RemoteException封装成unChecked异常后抛出。

如果这个系统级的异常是要交由上一级调用者处理的,可以新定义一个checked的业务异常,然后把系统级的异常封存装成业务级的异常后再抛出。

一般地,我们需要定义一个unChecked异常,让集成层接口的所有方法都声明抛出这unChecked异常。

public DataAccessExceptionextends RuntimeException{
……
}
public interface UserDao{
public String getPassword(String userId)throws DataAccessException;
}
public class UserDaoImplimplements UserDAO{
public String getPassword(String userId)throws DataAccessException{
String sql = “select password from userInfo where userId= ‘”+userId+”'”;
try{
…
//JDBC调用
s.executeQuery(sql);
…
}catch(SQLException ex){
throw new DataAccessException(“数据库查询失败”+sql,ex);
}
}
}
Copy after login


定义一个checked的业务异常,让业务层的接口的所有方法都声明抛出Checked异常.

public class BusinessExceptionextends Exception{
…..
}
public interface UserManager{
public Userinfo copyUserInfo(Userinfo user)throws BusinessException{
Userinfo newUser = null;
try{
newUser = (Userinfo)user.clone();
}catch(CloneNotSupportedException ex){
throw new BusinessException(“不支持clone方法:”+Userinfo.class.getName(),ex);
}
}
}
Copy after login


J2ee表示层应该是一个很薄的层,主要的功能为:获得页面请求,把页面的参数组装成POJO对象,调用相应的业务方法,然后进行页面转发,把相应的业务数据呈现给页面。表示层需要注意一个问题,表示层需要对数据的合法性进行校验,比如某些录入域不能为空,字符长度校验等。

J2ee从页面所有传给后台的参数都是字符型的,如果要求输入数值或日期类型的参数时,必须把字符值转换为相应的数值或日期值。

如果表示层代码校验参数不合法时,应该返回到原始页面,让用户重新录入数据,并提示相关的错误信息。
通常把一个从页面传来的参数转换为数值,我们可以看到这样的代码

ModeAndView handleRequest(HttpServletRequest request,HttpServletResponse response)throws Exception{
String ageStr = request.getParameter(“age”);
int age = Integer.parse(ageStr);
…………
String birthDayStr = request.getParameter(“birthDay”);
SimpleDateFormat format = new SimpleDateFormat(“MM/dd/yyyy”);
Date birthDay = format.parse(birthDayStr);
}
Copy after login


上面的代码应该经常见到,但是当用户从页面录入一个不能转换为整型的字符或一个错误的日期值。

Integer.parse()方法被抛出一个NumberFormatException的unChecked异常。但是这个异常绝对不是一个致命的异常,一般当用户在页面的录入域录入的值不合法时,我们应该提示用户进行重新录入。但是一旦抛出unchecked异常,就没有重试的机会了。像这样的代码造成大量的异常信息显示到页面。使我们的系统看起来非常的脆弱。

同样,SimpleDateFormat.parse()方法也会抛出ParseException的unChecked异常。

这种情况我们都应该捕获这些unChecked异常,并给提示用户重新录入。

ModeAndView handleRequest(HttpServletRequest request,HttpServletResponse response)throws Exception{
String ageStr = request.getParameter(“age”);
String birthDayStr = request.getParameter(“birthDay”);
int age = 0;
Date birthDay = null;
try{
age=Integer.parse(ageStr);
}catch(NumberFormatException ex){
error.reject(“age”,”不是合法的整数值”);
}
…………
try{
SimpleDateFormat format = new SimpleDateFormat(“MM/dd/yyyy”);
birthDay = format.parse(birthDayStr);
}catch(ParseException ex){
error.reject(“birthDay”,”不是合法的日期,请录入'MM/dd/yyy'格式的日期”);
}
}
Copy after login

在表示层一定要弄清楚调用方法的是否会抛出unChecked异常,什么情况下会抛出这些异常,并作出正确的处理。

在表示层调用系统的业务方法,一般情况下是无需要捕获异常的。如果调用的业务方法抛出的异常相当于第二个返回值时,在这种情况下是需要捕获。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持PHP中文网。

更多Java EE项目中的异常处理总结(一篇不得不看的文章)相关文章请关注PHP中文网!

Related labels:
source:php.cn
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template