为何要在J2EE项目中谈异常处理呢?可能许多java初学者都想说:“异常处理不就是try….catch…finally吗?这谁都会啊!”。笔者在初学java时也是这样认为的。如何在一个多层的j2ee项目中定义相应的异常类?在项目中的每一层如何进行异常处理?异常什么时候被抛出?异常什么时候被记录?异常该怎么记录?什么时候须要把checked Exception转化成unchecked Exception ,什么时候须要把unChecked Exception转化成checked Exception?异常是否应该呈现到前端页面?如何设计一个异常框架?本文将就这些问题进行探讨。
1. JAVA异常处理
在面向过程式的编程语言中,咱们能够经过返回值来肯定方法是否正常执行。好比在一个c语言编写的程序中,若是方法正确的执行则返回1.错误则返回0。在vb或delphi开发的应用程序中,出现错误时,咱们就弹出一个消息框给用户。
经过方法的返回值咱们并不能得到错误的详细信息。可能由于方法由不一样的程序员编写,当同一类错误在不一样的方法出现时,返回的结果和错误信息并不一致。
因此java语言采起了一个统一的异常处理机制。
什么是异常?运行时发生的可被捕获和处理的错误。
在java语言中,Exception是全部异常的父类。任何异常都扩展于Exception类。Exception就至关于一个错误类型。若是要定义一个新的错误类型就扩展一个新的Exception子类。采用异常的好处还在于能够精确的定位到致使程序出错的源代码位置,并得到详细的错误信息。
Java异常处理经过五个关键字来实现,try,catch,throw ,throws, finally。具体的异常处理结构由try….catch….finally块来实现。try块存放可能出现异常的java语句,catch用来捕获发生的异常,并对异常进行处理。Finally块用来清除程序中未释放的资源。无论理try块的代码如何返回,finally块都老是被执行。
一个典型的异常处理代码
java 代码
-
- 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;
- }
-
能够看出Java的异常处理机制具备的优点:
给错误进行了统一的分类,经过扩展Exception类或其子类来实现。从而避免了相同的错误可能在不一样的方法中具备不一样的错误信息。在不一样的方法中出现相同的错误时,只须要throw 相同的异常对象便可。
得到更为详细的错误信息。经过异常类,能够给异常更为详细,对用户更为有用的错误信息。以便于用户进行跟踪和调试程序。
把正确的返回结果与错误信息分离。下降了程序的复杂度。调用者无须要对返回结果进行更多的了解。
强制调用者进行异常处理,提升程序的质量。当一个方法声明须要抛出一个异常时,那么调用者必须使用try….catch块对异常进行处理。固然调用者也可让异常继续往上一层抛出。
2. Checked 异常 仍是 unChecked 异常?
Java异常分为两大类:checked 异常和unChecked 异常。全部继承java.lang.Exception 的异常都属于checked异常。全部继承java.lang.RuntimeException的异常都属于unChecked异常。
当一个方法去调用一个可能抛出checked异常的方法,必须经过try…catch块对异常进行捕获进行处理或者从新抛出。
咱们看看Connection接口的createStatement()方法的声明。
public Statement createStatement() throws SQLException;
SQLException是checked异常。当调用createStatement方法时,java强制调用者必须对SQLException进行捕获处理。
java 代码
- public String getPassword(String userId){
- try{
- ……
- Statement s = con.createStatement();
- ……
- Catch(SQLException sqlEx){
- ……
- }
- ……
- }
或者
java 代码
- public String getPassword(String userId)throws SQLException{
- Statement s = con.createStatement();
- }
(固然,像Connection,Satement这些资源是须要及时关闭的,这里仅是为了说明checked 异常必须强制调用者进行捕获或继续抛出)
unChecked异常也称为运行时异常,一般RuntimeException都表示用户没法恢复的异常,如没法得到数据库链接,不能打开文件等。虽然用户也能够像处理checked异常同样捕获unChecked异常。可是若是调用者并无去捕获unChecked异常时,编译器并不会强制你那么作。
好比一个把字符转换为整型数值的代码以下:
java 代码
- String str = “123”;
- int value = Integer.parseInt(str);
parseInt的方法签名为:
java 代码
- public staticint parseInt(String s)throws NumberFormatException
当传入的参数不能转换成相应的整数时,将会抛出NumberFormatException。由于NumberFormatException扩展于RuntimeException,是unChecked异常。因此调用parseInt方法时无须要try…catch
由于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异常的详细讨论能够参考
使用checked异常会带来许多的问题。
checked异常致使了太多的try…catch 代码
可能有不少checked异常对开发人员来讲是没法合理地进行处理的,好比SQLException。而开发人员却不得不去进行try…catch。当开发人员对一个checked异常没法正确的处理时,一般是简单的把异常打印出来或者是干脆什么也不干。特别是对于新手来讲,过多的checked异常让他感到无所适从。
java 代码
- try{
- ……
- Statement s = con.createStatement();
- ……
- Catch(SQLException sqlEx){
- sqlEx.PrintStackTrace();
- }
- 或者
- try{
- ……
- Statement s = con.createStatement();
- ……
- Catch(SQLException sqlEx){
- //什么也不干
- }
checked异常致使了许多难以理解的代码产生
当开发人员必须去捕获一个本身没法正确处理的checked异常,一般的是从新封装成一个新的异常后再抛出。这样作并无为程序带来任何好处。反而使代码晚难以理解。
就像咱们使用JDBC代码那样,须要处理很是多的try…catch.,真正有用的代码被包含在try…catch以内。使得理解这个方法变理困难起来
checked异常致使异常被不断的封装成另外一个类异常后再抛出
java 代码
- 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);
- }
- }
咱们看到异常就这样一层层无休止的被封装和从新抛出。
checked异常致使破坏接口方法
一个接口上的一个方法已被多个类使用,当为这个方法额外添加一个checked异常时,那么全部调用此方法的代码都须要修改。
可见上面这些问题都是由于调用者没法正确的处理checked异常时而被迫去捕获和处理,被迫封装后再从新抛出。这样十分不方便,并不能带来任何好处。在这种状况下一般使用unChecked异常。
chekced异常并非无一是处,checked异常比传统编程的错误返回值要好用得多。经过编译器来确保正确的处理异常比经过返回值判断要好得多。
若是一个异常是致命的,不可恢复的。或者调用者去捕获它没有任何益处,使用unChecked异常。
若是一个异常是能够恢复的,能够被调用者正确处理的,使用checked异常。
在使用unChecked异常时,必须在在方法声明中详细的说明该方法可能会抛出的unChekced异常。由调用者本身去决定是否捕获unChecked异常
倒底何时使用checked异常,何时使用unChecked异常?并无一个绝对的标准。可是笔者能够给出一些建议
当全部调用者必须处理这个异常,可让调用者进行重试操做;或者该异常至关于该方法的第二个返回值。使用checked异常。
这个异常仅是少数比较高级的调用者才能处理,通常的调用者不能正确的处理。使用unchecked异常。有能力处理的调用者能够进行高级处理,通常调用者干脆就不处理。
这个异常是一个很是严重的错误,如数据库链接错误,文件没法打开等。或者这些异常是与外部环境相关的。不是重试能够解决的。使用unchecked异常。由于这种异常一旦出现,调用者根本没法处理。
若是不能肯定时,使用unchecked异常。并详细描述可能会抛出的异常,以让调用者决定是否进行处理。
3.
设计一个新的异常类
在设计一个新的异常类时,首先看看是否真正的须要这个异常类。通常状况下尽可能不要去设计新的异常类,而是尽可能使用java中已经存在的异常类。
如
java 代码
- IllegalArgumentException, UnsupportedOperationException
无论是新的异常是chekced异常仍是unChecked异常。咱们都必须考虑异常的嵌套问题。
java 代码
- public void methodA()throws ExceptionA{
- …..
- throw new ExceptionA();
- }
方法methodA声明会抛出ExceptionA.
public void methodB()throws ExceptionB
methodB声明会抛出ExceptionB,当在methodB方法中调用methodA时,ExceptionA是没法处理的,因此ExceptionA应该继续往上抛出。一个办法是把methodB声明会抛出ExceptionA.但这样已经改变了MethodB的方法签名。一旦改变,则全部调用methodB的方法都要进行改变。
另外一个办法是把ExceptionA封装成ExceptionB,而后再抛出。若是咱们不把ExceptionA封装在ExceptionB中,就丢失了根异常信息,使得没法跟踪异常的原始出处。
java 代码
- public void methodB()throws ExceptionB{
- try{
- methodA();
- ……
- }catch(ExceptionA ex){
- throw new ExceptionB(ex);
- }
- }
如上面的代码中,ExceptionB嵌套一个ExceptionA.咱们暂且把ExceptionA称为“原由异常”,由于ExceptionA致使了ExceptionB的产生。这样才不使异常信息丢失。
因此咱们在定义一个新的异常类时,必须提供这样一个能够包含嵌套异常的构造函数。并有一个私有成员来保存这个“原由异常”。
java 代码
- 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;
- }
- }
固然,咱们在调用printStackTrace方法时,须要把全部的“原由异常”的信息也同时打印出来。因此咱们须要覆写printStackTrace方法来显示所有的异常栈跟踪。包括嵌套异常的栈跟踪。
java 代码
- public void printStackTrace(PrintStrean ps){
- if(cause == null){
- super.printStackTrace(ps);
- }else{
- ps.println(this);
- cause.printStackTrace(ps);
- }
- }
一个完整的支持嵌套的checked异常类源码以下。咱们在这里暂且把它叫作NestedException
java 代码
- 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);
- }
- }
-
一样要设计一个unChecked异常类也与上面同样。只是须要继承RuntimeException。
4.
如何记录异常
做为一个大型的应用系统都须要用日志文件来记录系统的运行,以便于跟踪和记录系统的运行状况。系统发生的异常理所固然的须要记录在日志系统中。
java 代码
- 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);
- }
咱们注意到,一个错误被记录了两次.在错误的起源位置咱们仅是以info级别进行记录。而在sendUserPassword方法中,咱们还把整个异常信息都记录了。
笔者曾看到不少项目是这样记录异常的,无论三七二一,只有遇到异常就把整个异常所有记录下。若是一个异常被不断的封装抛出屡次,那么就被记录了屡次。那么异常倒底该在什么地方被记录?
异常应该在最初产生的位置记录!
若是必须捕获一个没法正确处理的异常,仅仅是把它封装成另一种异常往上抛出。没必要再次把已经被记录过的异常再次记录。
若是捕获到一个异常,可是这个异常是能够处理的。则无须要记录异常
java 代码
- 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;
- }
捕获到一个未记录过的异常或外部系统异常时,应该记录异常的详细信息
java 代码
- try{
- ……
- String sql=”select * from userinfo”;
- Statement s = con.createStatement();
- ……
- Catch(SQLException sqlEx){
- Logger.error(“sql执行错误”+sql+sqlEx);
- }
究竟在哪里记录异常信息,及怎么记录异常信息,多是见仁见智的问题了。甚至有些系统让异常类一记录异常。当产生一个新异常对象时,异常信息就被自动记录。
java 代码
- 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();
- }
这彷佛看起来是十分美妙的,其实必然致使了异常被重复记录。同时违反了“类的职责分配原则”,是一种很差的设计。记录异常不属于异常类的行为,记录异常应该由专门的日志系统去作。而且异常的记录信息是不断变化的。咱们在记录异常同应该给更丰富些的信息。以利于咱们可以根据异常信息找到问题的根源,以解决问题。
虽然咱们对记录异常讨论了不少,过多的强调这些反而使开发人员更为疑惑,一种好的方式是为系统提供一个异常处理框架。由框架来决定是否记录异常和怎么记录异常。而不是由普通程序员去决定。可是了解些仍是有益的。
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系统级的异常严重的影响了咱们的业务接口。咱们对系统进行分层的目的就是减小系统之间的依赖,每一层的技术改变不至于影响到其它层。
java 代码
- //
- public class UserSoaImplimplements UserSoa{
- public UserInfo getUserInfo(String userId)throws RemoteException{
- //……
- 远程方法调用.
- //……
- }
- }
- public interface UserManager{
- public UserInfo getUserInfo(Stirng userId)throws RemoteException;
- }
一样JDBC访问都会抛出SQLException的checked异常。
为了不系统级的checked异常对业务系统的深度侵入,咱们能够为业务方法定义一个业务系统本身的异常。针对像SQLException,RemoteException这些很是严重的异常,咱们能够新定义一个unChecked的异常,而后把SQLException,RemoteException封装成unChecked异常后抛出。
若是这个系统级的异常是要交由上一级调用者处理的,能够新定义一个checked的业务异常,而后把系统级的异常封存装成业务级的异常后再抛出。
通常地,咱们须要定义一个unChecked异常,让集成层接口的全部方法都声明抛出这unChecked异常。
java 代码
- 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);
- }
- }
- }
定义一个checked的业务异常,让业务层的接口的全部方法都声明抛出Checked异常.
java 代码
- 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);
- }
- }
- }
J2ee表示层应该是一个很薄的层,主要的功能为:得到页面请求,把页面的参数组装成POJO对象,调用相应的业务方法,而后进行页面转发,把相应的业务数据呈现给页面。表示层须要注意一个问题,表示层须要对数据的合法性进行校验,好比某些录入域不能为空,字符长度校验等。
J2ee从页面全部传给后台的参数都是字符型的,若是要求输入数值或日期类型的参数时,必须把字符值转换为相应的数值或日期值。
若是表示层代码校验参数不合法时,应该返回到原始页面,让用户从新录入数据,并提示相关的错误信息。
一般把一个从页面传来的参数转换为数值,咱们能够看到这样的代码
java 代码
- 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);
-
- }
上面的代码应该常常见到,可是当用户从页面录入一个不能转换为整型的字符或一个错误的日期值。
Integer.parse()方法被抛出一个NumberFormatException的unChecked异常。可是这个异常绝对不是一个致命的异常,通常当用户在页面的录入域录入的值不合法时,咱们应该提示用户进行从新录入。可是一旦抛出unchecked异常,就没有重试的机会了。像这样的代码形成大量的异常信息显示到页面。使咱们的系统看起来很是的脆弱。
一样,SimpleDateFormat.parse()方法也会抛出ParseException的unChecked异常。
这种状况咱们都应该捕获这些unChecked异常,并给提示用户从新录入。
java 代码
- 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’格式的日期”);
- }
-
- }
在表示层必定要弄清楚调用方法的是否会抛出unChecked异常,什么状况下会抛出这些异常,并做出正确的处理。
在表示层调用系统的业务方法,通常状况下是无须要捕获异常的。若是调用的业务方法抛出的异常至关于第二个返回值时,在这种状况下是须要捕获