java 异常处理

一:什么是异常

 异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。

 比如说,你的代码少了一个分号,那么运行出来结果是提示是错误 java.lang.Error;如果你用System.out.println(11/0),那么你是因为你用0做了除数,会抛出 java.lang.ArithmeticException 的异常。

 异常发生的原因有很多,通常包含以下几大类:

  • 用户输入了非法数据。
  • 要打开的文件不存在。
  • 网络通信时连接中断,或者JVM内存溢出。

 这些异常有的是因为用户错误引起,有的是程序错误引起的,还有其它一些是因为物理错误引起的。-

 要理解Java异常处理是如何工作的,你需要掌握以下三种类型的异常:

  • 检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
  • 运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
  • 错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。

 二:异常的好处

  异常带来诸多好处。首先,它将错误处理代码从正常代码(normal code)中分离出来。你可以将那些执行概率为99.9%的代码封装在一个try块内,然后将异常处理代码----这些代码是不经常执行的----置于catch子句中。这种方式的好处是,正常代码因此而更简洁。
  如果你不知道如何处理某个方法中的一个特定错误,那么你可以在方法中抛出异常,将处理权交给其他人。如果你抛出一个检查异常(checked exception),那么Java编译器将强制客户程序员(cilent programmer)处理这个潜在异常,或者捕捉之,或者在方法的throws子句中声明之。Java编译器确保检查异常被处理,这使得Java程序更为健壮。


 三:Exception 类的层次

异常类有两个主要的子类:IOException 类和 RuntimeException 类


 四:Java 内置异常类

  Java 语言定义了一些异常类在 java.lang 标准包中。

  标准运行时异常类的子类是最常见的异常类。由于 java.lang 包是默认加载到所有的 Java 程序的,所以大部分从运行时异常类继承而来的异常都可以直接使用。

  Java 根据各个类库也定义了一些其他的异常,下面的表中列出了 Java 的非检查性异常

异常描述
ArithmeticException当出现异常的运算条件时,抛出此异常。例如,一个整数"除以零"时,抛出此类的一个实例。
ArrayIndexOutOfBoundsException用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引。
ArrayStoreException试图将错误类型的对象存储到一个对象数组时抛出的异常。
ClassCastException当试图将对象强制转换为不是实例的子类时,抛出该异常。
IllegalArgumentException抛出的异常表明向方法传递了一个不合法或不正确的参数。
IllegalMonitorStateException抛出的异常表明某一线程已经试图等待对象的监视器,或者试图通知其他正在等待对象的监视器而本身没有指定监视器的线程。
IllegalStateException在非法或不适当的时间调用方法时产生的信号。换句话说,即 Java 环境或 Java 应用程序没有处于请求操作所要求的适当状态下。
IllegalThreadStateException线程没有处于请求操作所要求的适当状态时抛出的异常。
IndexOutOfBoundsException指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。
NegativeArraySizeException如果应用程序试图创建大小为负的数组,则抛出该异常。
NullPointerException当应用程序试图在需要对象的地方使用 null 时,抛出该异常
NumberFormatException当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。
SecurityException由安全管理器抛出的异常,指示存在安全侵犯。
StringIndexOutOfBoundsException此异常由 String 方法抛出,指示索引或者为负,或者超出字符串的大小。
UnsupportedOperationException当不支持请求的操作时,抛出该异常。

  下面的表中列出了 Java 定义在 java.lang 包中的检查性异常类

异常描述
ClassNotFoundException应用程序试图加载类时,找不到相应的类,抛出该异常。
CloneNotSupportedException当调用 Object 类中的 clone 方法克隆对象,但该对象的类无法实现 Cloneable 接口时,抛出该异常。
IllegalAccessException拒绝访问一个类的时候,抛出该异常。
InstantiationException当试图使用 Class 类中的 newInstance 方法创建一个类的实例,而指定的类对象因为是一个接口或是一个抽象类而无法实例化时,抛出该异常。
InterruptedException一个线程被另一个线程中断,抛出该异常。
NoSuchFieldException请求的变量不存在
NoSuchMethodException请求的方法不存在

五:异常方法

  下面的列表是 Throwable 类的主要方法:

序号方法及说明
1public String getMessage()
返回关于发生的异常的详细信息。这个消息在Throwable 类的构造函数中初始化了。
2public Throwable getCause()
返回一个Throwable 对象代表异常原因。
3public String toString()
使用getMessage()的结果返回类的串级名字。
4public void printStackTrace()
打印toString()结果和栈层次到System.err,即错误输出流。
5public StackTraceElement [] getStackTrace()
返回一个包含堆栈层次的数组。下标为0的元素代表栈顶,最后一个元素代表方法调用堆栈的栈底。
6public Throwable fillInStackTrace()
用当前的调用栈层次填充Throwable 对象栈层次,添加到栈层次任何先前信息中。

六:如何正确处理异常

 5.1:如何选择异常类型

  5.1.1:异常的类别

  正如我们所知道的,java中的异常的超类是java.lang.Throwable(后文省略为Throwable),它有两个比较重要的子类,java.lang.Exception(后文省略为Exception)和java.lang.Error(后文省略为Error),其中Error由JVM虚拟机进行管理,如我们所熟知的OutOfMemoryError异常等,所以我们本文不关注Error异常,那么我们细说一下Exception异常。
  Exception异常有个比较重要的子类,叫做RuntimeException。我们将RuntimeException或其他继承自RuntimeException的子类称为非受检异常(unchecked Exception),其他继承自Exception异常的子类称为受检异常(checked Exception)。本文重点来关注一下受检异常非受检异常这两种异常。

   5.1.2:如何选择异常

  如果在一个应用中,需要开发一个方法(如某个功能的service方法),这个方法如果中间可能出现异常,那么你需要考虑这个异常出现之后是否调用者可以处理,并且你是否希望调用者进行处理,如果调用者可以处理,并且你也希望调用者进行处理,那么就要抛出受检异常,提醒调用者在使用你的方法时,考虑到如果抛出异常时如果进行处理,相似的,如果在写某个方法时,你认为这是个偶然异常,理论上说,你觉得运行时可能会碰到什么问题,而这些问题也许不是必然发生的,也不需要调用者显示的通过异常来判断业务流程操作的,那么这时就可以使用一个RuntimeException这样的非受检异常.

  5.1.3:检查型异常和非检查型异常

  现在,主要问题就是抛出检查型异常还是非检查型异常了。检查型异常是Exception的子类(或者Exception类本身),但不包括RuntimeException和它的子类。非检查型异常是RuntimeException和它的任何子类。Error类及其子类也是检查型的,但是你应该仅着眼于异常,你所做的应该是决定抛出RuntimeException的子类(非检查异常)还是Exception的子类(检查异常)。
  如果抛出了检查型异常(而没有捕获它),那么你需要在方法的throws子句中声明该异常。客户程序员使用这个方法,他要么在其方法内捕获并处理这个异常,要么还在throws子句中抛出。检查型异常强制客户程序员对可能抛出的异常采取措施。
  如果你抛出的是非检查型异常,那么客户程序员可以决定捕获与否。然而,编译器并不强制客户程序员对非检查型异常采取措施。事实上,他们甚至不知道可能这些异常。显然,在非检查型异常上客户程序员会少费些脑筋。
  有一个简单的原则是:
  如果希望客户程序员有意识地采取措施,那么抛出检查型异常。
  一般而言,表示类的误用的异常应该是非检查型异常。String类的chartAt()方法抛出的StringIndexOutOfBoundsException就是一个非检查型异常。String类的设计者并不打算强制客户程序员每次调用charAt(int index)时都检查index参数的合法性。
  另一方面,java.io.FileInputStream类的read()方法抛出的是IOException,这是一个检查异常。这个异常表明尝试读取文件时出错了。这并不意味着客户程序员错误地使用了FileInputStream类,而是说这个方法无法履行它地职责,即从文件中读出下一个字节。FileInputStream类地设计者认为这个意外情况很普遍,也很重要,因而强制客户程序员处理之。
  这就是窍门所在。如果意外情况是方法无法履行职责,而你又认为它很普遍或很重要,客户程序员必须采取措施,那么抛出检查型异常。否则,抛出非检查型异常。

  5.1.4:什么时候才需要抛异常

  异常应于何时抛出?答案归于一条原则:如果方法遇到一个不知道如何处理的意外情况(abnormal condition),那么它应该抛出异常

  几个例子:

 (1)第一个示例使用java.io包的FileInputStream类和DataInputStream类。这是使用FileInputStream类将文件内容发送到标准输出(standard output)的代码:

public static void main(String[] args)
        throws IOException {

        if (args.length == 0) {
            System.out.println("Must give filename as first arg.");
            return;
        }

        FileInputStream in;
        try {
            in = new FileInputStream(args[0]);
        }
        catch (FileNotFoundException e) {
            System.out.println("Can't find file: " + args[0]);
            return;
        }

        int ch;
        while ((ch = in.read()) != -1) {
            System.out.print((char) ch);
        }
        System.out.println();

        in.close();
    }

  在本例中,FileInputStream类的read方法报告了“已到达文件末尾”的情况,但是,它并没有采用抛出异常的方式,而是返回了一个特殊值:-1。在这个方法中,到达文件末尾被视为方法的“正常”部分,这不是意外情况。读取字节流的通常方式是,继续往下读直到达字节流末尾。
  与此不同的是,DataInputStream类采取了另一种方式来报告文件末尾:

public static void main(String[] args)
        throws IOException {

        if (args.length == 0) {
            System.out.println("Must give filename as first arg.");
            return;
        }

        FileInputStream fin;
        try {
            fin = new FileInputStream(args[0]);
        }
        catch (FileNotFoundException e) {
            System.out.println("Can't find file: " + args[0]);
            return;
        }

        DataInputStream din = new DataInputStream(fin);
        try {
            int i;
            for (;;) {
                i = din.readInt();
                System.out.println(i);
            }
        }
        catch (EOFException e) {
        }

        fin.close();
    }

  DataInputStream类的readInt()方法每次读取四个字节,然后将其解释为一个int型数据。当读到文件末尾时,readInt()方法将抛出EOFException。
  这个方法抛出异常的原因有二。首先,readInt()无法返回一个特殊值来指示已经到达文件末尾,因为所有可能的返回值都是合法的整型数据。(例如,它不能采用-1这个特殊值来指示文件末尾,因为-1可能就是流中的正常数据。)其次,如果readInt()在文件末尾处只读到一个、两个、或者三个字节,那么,这就可以视为“意外情况”了。本来这个方法是要读四个字节的,但只有一到三个字节可读。由于该异常是使用这个类时的不可分割的部分,它被设计为检查型异常(Exception类的子类)。客户程序员被强制要求处理该异常。
  指示“已到达末尾”情况的第三种方式在StringTokenizer类和Stack类中得到演示:

public static void main(String[] args)
        throws IOException {

        if (args.length == 0) {
            System.out.println("Must give filename as first arg.");
            return;
        }

        FileInputStream in = null;
        try {
            in = new FileInputStream(args[0]);
        }
        catch (FileNotFoundException e) {
            System.out.println("Can't find file: " + args[0]);
            return;
        }

        // Read file into a StringBuffer
        StringBuffer buf = new StringBuffer();
        try {
            int ch;
            while ((ch = in.read()) != -1) {
                buf.append((char) ch);
            }
        }
        finally {
            in.close();
        }

        // Separate StringBuffer into tokens and
        // push each token into a Stack
        StringTokenizer tok = new StringTokenizer(buf.toString());
        Stack stack = new Stack();
        while (tok.hasMoreTokens()) {
            stack.push(tok.nextToken());
        }

        // Print out tokens in reverse order.
        while (!stack.empty()) {
            System.out.println((String) stack.pop());
        }
    }

  上面的程序逐字节读取文件,将字节数据转换为字符数据,然后将字符数据放到StringBuffer中。它使用StringTokenizer类提取以空白字符为分隔符的token(这里是一个字符串),每次提取一个并压入Stack中。最后,所有token都被从Stack中弹出并打印,每行打印一个。因为Stack类实现的是后进先出(LIFO)栈,所以,打印出来的数据顺序和文件中的数据顺序刚好相反。
  StringTokenizer类和Stack类都必须能够指示“已到达末尾”情况。StringTokenizer的构造方法接纳源字符串。每一次调用nextToken()方法都将返回一个字符串,它是源字符串的下一个token。源字符串的所有token都必然会被消耗掉,StringTokenizer类必须通过某种方式指示已经没有更多的token供返回了。这种情况下,本来是可以用一个特殊的值null来指示没有更多token的。但是,此类的设计者采用了另一个办法。他提供了一个额外的方法hasMoreTokens(),该方法返回一个布尔值来指示是否已到达末尾。每次调用nextToken()方法之前,你必须先调用hasMoreTokens()。
  这种方法表明设计者并不认为到达token流的末尾是意外情况。相反,它是使用这个类的常规情况。然而,如果你在调用nextToken()之前不检查hasMoreTokens(),那么你最后会得到一个异常NoSuchElementException。虽然该异常在到达token流末尾时抛出,但它却是一个非检查异常(RuntimeException的子类)。该异常的抛出不是为了指示“已到达末尾”,而是指示一个软件缺陷----你并没有正确地使用该类。
  与此类似,Stack类有一个类似的方法empty(),这个方法返回一个布尔值指示栈已经为空。每次调用pop()之前,你都必须先调用empty()方法。如果你忘了调用empty()方法,而直接在一个空栈上调用pop()方法,那么,你将得到一个异常EmptyStackException。虽然该异常是栈已经为空的情况下抛出的,但它也是一个非检查异常。它的作用不是检测空栈,而是指示客户代码中的一个软件缺陷(Stack类的不恰当使用)。

   5.1.5:应该抛出怎样的异常

  了解完了什么时候才需要抛出异常后,我们再思考一个问题,真的当我们抛出异常时,我们应该选用怎样的异常呢?究竟是受检异常还是非受检异常呢(RuntimeException)呢?我来举例说明一下这个问题,先从受检异常说起,比如说有这样一个业务逻辑,需要从某文件中读取某个数据,这个读取操作可能是由于文件被删除等其他问题导致无法获取从而出现读取错误,那么就要从redis或mysql数据库中再去获取此数据,参考如下代码,getKey(Integer)为入口程序.

public String getKey(Integer key){
    String  value;
    try {
        InputStream inputStream = getFiles("/file/nofile");
        //接下来从流中读取key的value指
        value = ...;
    } catch (Exception e) {
        //如果抛出异常将从mysql或者redis进行取之
        value = ...;
    }
}
 
public InputStream getFiles(String path) throws Exception {
    File file = new File(path);
    InputStream inputStream = null;
    try {
        inputStream = new BufferedInputStream(new FileInputStream(file));
    } catch (FileNotFoundException e) {
        throw new Exception("I/O读取错误",e.getCause());
    }
    return inputStream;
}

  ok,看了以上代码以后,你也许心中有一些想法,原来受检异常可以控制义务逻辑,对,没错,通过受检异常真的可以控制业务逻辑,但是切记不要这样使用,我们应该合理的抛出异常,因为程序本身才是流程,异常的作用仅仅是当你进行不下去的时候找到的一个借口而已,它并不能当成控制程序流程的入口或出口,如果这样使用的话,是在将异常的作用扩大化,这样将会导致代码复杂程度的增加,耦合性会提高,代码可读性降低等问题。那么就一定不要使用这样的异常吗?其实也不是,在真的有这样的需求的时候,我们可以这样使用,只是切记,不要把它真的当成控制流程的工具或手段。那么究竟什么时候才要抛出这样的异常呢?要考虑,如果调用者调用出错后,一定要让调用者对此错误进行处理才可以,满足这样的要求时,我们才会考虑使用受检异常。

  接下来,我们来看一下非受检异常呢(RuntimeException),对于RuntimeException这种异常,我们其实很多见,比如java.lang.NullPointerException/java.lang.IllegalArgumentException等,那么这种异常我们时候抛出呢?当我们在写某个方法的时候,可能会偶然遇到某个错误,我们认为这个问题时运行时可能为发生的,并且理论上讲,没有这个问题的话,程序将会正常执行的时候,它不强制要求调用者一定要捕获这个异常,此时抛出RuntimeException异常,举个例子,当传来一个路径的时候,需要返回一个路径对应的File对象:

public void test() {
    myTest.getFiles("");
}
 
public File getFiles(String path) {
    if(null == path || "".equals(path)){
        throw  new NullPointerException("路径不能为空!");
    }
    File file = new File(path);
 
    return file;
}

  上述例子表明,如果调用者调用getFiles(String)的时候如果path是空,那么就抛出空指针异常(它是RuntimeException的子类),调用者不用显示的进行try…catch…操作进行强制处理.这就要求调用者在调用这样的方法时先进行验证,避免发生RuntimeException。

  5.1.5:应该选用哪种异常

  通过以上的描述和举例,可以总结出一个结论,RuntimeException异常和受检异常之间的区别就是:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检异常,否则就选择非受检异常(RuntimeException)。一般来讲,如果没有特殊的要求,我们建议使用RuntimeException异常。


七:如何优雅的设计java异常

  根据项目场景来看,需要两个domain模型,一个是用户实体,一个是地址实体.
Address domain如下:

@Entity
@Data
public class Address {
    @Id
    @GeneratedValue
    private Integer id;
    private String province;//
    private String city;//
    private String county;//
    private Boolean isDefault;//是否是默认地址
 
    @ManyToOne(cascade={CascadeType.ALL})
    @JoinColumn(name="uid")
    private User user;
}

User domain如下:

@Entity
@Data
public class User {
    @Id
   @GeneratedValue
   private Integer id;
   private String name;//姓名
 
    @OneToMany(cascade= CascadeType.ALL,mappedBy="user",fetch = FetchType.LAZY)
        private Set<Address> addresses;
}

  ok,上边是一个模型关系,用户-收货地址的关系是1-n的关系。上边的@Data是使用了一个叫做lombok的工具,它自动生成了Setter和Getter等方法,用起来非常方便,感兴趣的读者可以自行了解一下。

dao介绍

  数据连接层,我们使用了spring-data-jpa这个框架,它要求我们只需要继承框架提供的接口,并且按照约定对方法进行取名,就可以完成我们想要的数据库操作。
用户数据库操作如下:

@Repository
public interface IUserDao extends JpaRepository<User,Integer> {
 
}

收货地址操作如下:

@Repository
public interface IAddressDao extends JpaRepository<Address,Integer> {
 
}

  正如读者所看到的,我们的DAO只需要继承JpaRepository,它就已经帮我们完成了基本的CURD等操作,如果想了解更多关于spring-data的这个项目,请参考一下spring的官方文档,它比不方案我们对异常的研究。

Service异常设计

 ok,终于到了我们的重点了,我们要完成service一些的部分操作:添加收货地址,删除收货地址,获取收货地址列表.
 首先看我的service接口定义:

public interface IAddressService {
 
/**
 * 创建收货地址
 * @param uid
 * @param address
 * @return
 */
Address createAddress(Integer uid,Address address);
 
/**
 * 删除收货地址
 * @param uid
 * @param aid
 */
void deleteAddress(Integer uid,Integer aid);
 
/**
 * 查询用户的所有收货地址
 * @param uid
 * @return
 */
List<Address> listAddresses(Integer uid);
}

我们来关注一下实现:

添加收货地址

 首先再来看一下之前整理的约束条件:

 入参:

  • 用户id
  • 收货地址实体信息

 约束:

  • 用户id不能为空,且此用户确实是存在的
  • 收货地址的必要字段不能为空
  • 如果用户还没有收货地址,当此收货地址创建时设置成默认收货地址

先看以下代码实现:

@Override
public Address createAddress(Integer uid, Address address) {
    //============ 以下为约束条件   ==============
    //1.用户id不能为空,且此用户确实是存在的
    Preconditions.checkNotNull(uid);
    User user = userDao.findOne(uid);
    if(null == user){
        throw new RuntimeException("找不到当前用户!");
    }
    //2.收货地址的必要字段不能为空
    BeanValidators.validateWithException(validator, address);
    //3.如果用户还没有收货地址,当此收货地址创建时设置成默认收货地址
    if(ObjectUtils.isEmpty(user.getAddresses())){
        address.setIsDefault(true);
    }
 
    //============ 以下为正常执行的业务逻辑   ==============
    address.setUser(user);
    Address result = addressDao.save(address);
    return result;
}

  其中,已经完成了上述所描述的三点约束条件,当三点约束条件都满足时,才可以进行正常的业务逻辑,否则将抛出异常(一般在此处建议抛出运行时异常-RuntimeException)。

  介绍以下以上我所用到的技术:

  • Preconfitions.checkNotNull(T t)这个是使用Guava中的com.google.common.base.Preconditions进行判断的,因为service中用到的验证较多,所以建议将Preconfitions改成静态导入的方式:
1
import static com.google.common.base.Preconditions.checkNotNull;

  当然Guava的github中的说明也建议我们这样使用。

  • BeanValidators.validateWithException(validator, address);

  这个使用了hibernate实现的jsr 303规范来做的,需要传入一个validator和一个需要验证的实体,那么validator是如何获取的呢,如下:

@Configuration
public class BeanConfigs {
 
@Bean
public javax.validation.Validator getValidator(){
    return new LocalValidatorFactoryBean();
}
}

  他将获取一个Validator对象,然后我们在service中进行注入便可以使用了:

1
2
@Autowired    
private Validator validator ;

  那么BeanValidators这个类是如何实现的?其实实现方式很简单,只要去判断jsr 303的标注注解就ok了。
  那么jsr 303的注解写在哪里了呢?当然是写在address实体类中了:

@Entity
@Setter
@Getter
public class Address {
@Id
    @GeneratedValue
    private Integer id;
    @NotNull
private String province;//
@NotNull
private String city;//
@NotNull
private String county;//
private Boolean isDefault = false;//是否是默认地址
 
@ManyToOne(cascade={CascadeType.ALL})
@JoinColumn(name="uid")
private User user;
}

  写好你需要的约束条件来进行判断,如果合理的话,才可以进行业务操作,从而对数据库进行操作。
  这块的验证是必须的,一个最主要的原因是:这样的验证可以避免脏数据的插入。如果读者有正式上线的经验的话,就可以理解这样的一个事情,任何的代码错误都可以容忍和修改,但是如果出现了脏数据问题,那么它有可能是一个毁灭性的灾难。程序的问题可以修改,但是脏数据的出现有可能无法恢复。所以这就是为什么在service中一定要判断好约束条件,再进行业务逻辑操作的原因了。

  此处的判断为业务逻辑判断,是从业务角度来进行筛选判断的,除此之外,有可能在很多场景中都会有不同的业务条件约束,只需要按照要求来做就好。

对于约束条件的总结如下:

  • 基本判断约束(null值等基本判断)
  • 实体属性约束(满足jsr 303等基础判断)
  • 业务条件约束(需求提出的不同的业务约束)

当这个三点都满足时,才可以进行下一步操作

 ok,基本介绍了如何做一个基础的判断,那么再回到异常的设计问题上,上述代码已经很清楚的描述如何在适当的位置合理的判断一个异常了,那么如何合理的抛出异常呢?
 只抛出RuntimeException就算是优雅的抛出异常吗?当然不是,对于service中的抛出异常,笔者认为大致有两种抛出的方法:

  1. 抛出带状态码RumtimeException异常
  2. 抛出指定类型的RuntimeException异常

 相对这两种异常的方式进行结束,第一种异常指的是我所有的异常都抛RuntimeException异常,但是需要带一个状态码,调用者可以根据状态码再去查询究竟service抛出了一个什么样的异常。
 第二种异常是指在service中抛出什么样的异常就自定义一个指定的异常错误,然后在进行抛出异常。
  一般来讲,如果系统没有别的特殊需求的时候,在开发设计中,建议使用第二种方式。但是比如说像基础判断的异常,就可以完全使用guava给我们提供的类库进行操作。jsr 303异常也可以使用自己封装好的异常判断类进行操作,因为这两种异常都是属于基础判断,不需要为它们指定特殊的异常。但是对于第三点义务条件约束判断抛出的异常,就需要抛出指定类型的异常了。
对于

1
throw new RuntimeException( "找不到当前用户!" );

定义一个特定的异常类来进行这个义务异常的判断:

public class NotFindUserException extends RuntimeException {
public NotFindUserException() {
    super("找不到此用户");
}
 
public NotFindUserException(String message) {
    super(message);
}
}

  然后将此处改为:

1
throw new NotFindUserException( "找不到当前用户!" );

or

1
throw new NotFindUserException();

  ok,通过以上对service层的修改,代码更改如下:

@Override
public Address createAddress(Integer uid, Address address) {
    //============ 以下为约束条件   ==============
    //1.用户id不能为空,且此用户确实是存在的
    checkNotNull(uid);
    User user = userDao.findOne(uid);
    if(null == user){
        throw new NotFindUserException("找不到当前用户!");
    }
    //2.收货地址的必要字段不能为空
    BeanValidators.validateWithException(validator, address);
    //3.如果用户还没有收货地址,当此收货地址创建时设置成默认收货地址
    if(ObjectUtils.isEmpty(user.getAddresses())){
        address.setIsDefault(true);
    }
 
    //============ 以下为正常执行的业务逻辑   ==============
    address.setUser(user);
    Address result = addressDao.save(address);
    return result;
}

  这样的service就看起来稳定性和理解性就比较强了。

删除收货地址:

 入参:

  • 用户id
  • 收货地址id

 约束:

  • 用户id不能为空,且此用户确实是存在的
  • 收货地址不能为空,且此收货地址确实是存在的
  • 判断此收货地址是否是用户的收货地址
  • 判断此收货地址是否为默认收货地址,如果是默认收货地址,那么不能进行删除

 它与上述添加收货地址类似,故不再赘述,delete的service设计如下:

@Override
public void deleteAddress(Integer uid, Integer aid) {
    //============ 以下为约束条件   ==============
    //1.用户id不能为空,且此用户确实是存在的
    checkNotNull(uid);
    User user = userDao.findOne(uid);
    if(null == user){
        throw new NotFindUserException();
    }
    //2.收货地址不能为空,且此收货地址确实是存在的
    checkNotNull(aid);
    Address address = addressDao.findOne(aid);
    if(null == address){
        throw new NotFindAddressException();
    }
    //3.判断此收货地址是否是用户的收货地址
    if(!address.getUser().equals(user)){
        throw new NotMatchUserAddressException();
    }
    //4.判断此收货地址是否为默认收货地址,如果是默认收货地址,那么不能进行删除
    if(address.getIsDefault()){
       throw  new DefaultAddressNotDeleteException();
    }
 
    //============ 以下为正常执行的业务逻辑   ==============
    addressDao.delete(address);
}

  设计了相关的四个异常类:NotFindUserException,NotFindAddressException,NotMatchUserAddressException,DefaultAddressNotDeleteException.根据不同的业务需求抛出不同的异常。

获取收货地址列表:

 入参:

  • 用户id

 约束:

  • 用户id不能为空,且此用户确实是存在的

代码如下:

@Override
public List<Address> listAddresses(Integer uid) {
    //============ 以下为约束条件   ==============
    //1.用户id不能为空,且此用户确实是存在的
    checkNotNull(uid);
    User user = userDao.findOne(uid);
    if(null == user){
        throw new NotFindUserException();
    }
 
    //============ 以下为正常执行的业务逻辑   ==============
    User result = userDao.findOne(uid);
    return result.getAddresses();
}

api异常设计

 大致有两种抛出的方法:

  1. 抛出带状态码RumtimeException异常
  2. 抛出指定类型的RuntimeException异常

  这个是在设计service层异常时提到的,通过对service层的介绍,我们在service层抛出异常时选择了第二种抛出的方式,不同的是,在api层抛出异常我们需要使用这两种方式进行抛出:要指定api异常的类型,并且要指定相关的状态码,然后才将异常抛出,这种异常设计的核心是让调用api的使用者更能清楚的了解发生异常的详细信息,除了抛出异常外,我们还需要将状态码对应的异常详细信息以及异常有可能发生的问题制作成一个对应的表展示给用户,方便用户的查询。(如github提供的api文档,微信提供的api文档等),还有一个好处:如果用户需要自定义提示消息,可以根据返回的状态码进行提示的修改。

api验证约束

  首先对于api的设计来说,需要存在一个dto对象,这个对象负责和调用者进行数据的沟通和传递,然后dto->domain在传给service进行操作,这一点一定要注意,第二点,除了说道的service需要进行基础判断(null判断)和jsr 303验证以外,同样的,api层也需要进行相关的验证,如果验证不通过的话,直接返回给调用者,告知调用失败,不应该带着不合法的数据再进行对service的访问,那么读者可能会有些迷惑,不是service已经进行验证了,为什么api层还需要进行验证么?这里便设计到了一个概念:编程中的墨菲定律,如果api层的数据验证疏忽了,那么有可能不合法数据就带到了service层,进而讲脏数据保存到了数据库。

  所以缜密编程的核心是:永远不要相信收到的数据是合法的。

api异常设计

  设计api层异常时,正如我们上边所说的,需要提供错误码和错误信息,那么可以这样设计,提供一个通用的api超类异常,其他不同的api异常都继承自这个超类:

public class ApiException extends RuntimeException {
protected Long errorCode ;
protected Object data ;
 
public ApiException(Long errorCode,String message,Object data,Throwable e){
    super(message,e);
    this.errorCode = errorCode ;
    this.data = data ;
}
 
public ApiException(Long errorCode,String message,Object data){
    this(errorCode,message,data,null);
}
 
public ApiException(Long errorCode,String message){
    this(errorCode,message,null,null);
}
 
public ApiException(String message,Throwable e){
    this(null,message,null,e);
}
 
public ApiException(){
 
}
 
public ApiException(Throwable e){
    super(e);
}
 
public Long getErrorCode() {
    return errorCode;
}
 
public void setErrorCode(Long errorCode) {
    this.errorCode = errorCode;
}
 
public Object getData() {
    return data;
}
 
public void setData(Object data) {
    this.data = data;
}
}

  然后分别定义api层异常:ApiDefaultAddressNotDeleteException,ApiNotFindAddressException,ApiNotFindUserException,ApiNotMatchUserAddressException。
以默认地址不能删除为例:

public class ApiDefaultAddressNotDeleteException extends ApiException {
 
public ApiDefaultAddressNotDeleteException(String message) {
    super(AddressErrorCode.DefaultAddressNotDeleteErrorCode, message, null);
}
}

  AddressErrorCode.DefaultAddressNotDeleteErrorCode就是需要提供给调用者的错误码。错误码类如下:

public abstract class AddressErrorCode {
    public static final Long DefaultAddressNotDeleteErrorCode = 10001L;//默认地址不能删除
    public static final Long NotFindAddressErrorCode = 10002L;//找不到此收货地址
    public static final Long NotFindUserErrorCode = 10003L;//找不到此用户
    public static final Long NotMatchUserAddressErrorCode = 10004L;//用户与收货地址不匹配
}

  ok,那么api层的异常就已经设计完了,在此多说一句,AddressErrorCode错误码类存放了可能出现的错误码,更合理的做法是把他放到配置文件中进行管理。

api处理异常

  api层会调用service层,然后来处理service中出现的所有异常,首先,需要保证一点,一定要让api层非常轻,基本上做成一个转发的功能就好(接口参数,传递给service参数,返回给调用者数据,这三个基本功能),然后就要在传递给service参数的那个方法调用上进行异常处理。

此处仅以添加地址为例:

@Autowired
private IAddressService addressService;
 
 
/**
 * 添加收货地址
 * @param addressDTO
 * @return
 */
@RequestMapping(method = RequestMethod.POST)
public AddressDTO add(@Valid @RequestBody AddressDTO addressDTO){
    Address address = new Address();
    BeanUtils.copyProperties(addressDTO,address);
    Address result;
    try {
        result = addressService.createAddress(addressDTO.getUid(), address);
    }catch (NotFindUserException e){
        throw new ApiNotFindUserException("找不到该用户");
    }catch (Exception e){//未知错误
        throw new ApiException(e);
    }
    AddressDTO resultDTO = new AddressDTO();
    BeanUtils.copyProperties(result,resultDTO);
    resultDTO.setUid(result.getUser().getId());
 
    return resultDTO;
}

  这里的处理方案是调用service时,判断异常的类型,然后将任何service异常都转化成api异常,然后抛出api异常,这是常用的一种异常转化方式。相似删除收货地址和获取收货地址也类似这样处理,在此,不在赘述。

api异常转化

  已经讲解了如何抛出异常和何如将service异常转化为api异常,那么转化成api异常直接抛出是否就完成了异常处理呢? 答案是否定的,当抛出api异常后,我们需要把api异常返回的数据(json or xml)让用户看懂,那么需要把api异常转化成dto对象(ErrorDTO),看如下代码:

@ControllerAdvice(annotations = RestController.class)
class ApiExceptionHandlerAdvice {
 
/**
 * Handle exceptions thrown by handlers.
 */
@ExceptionHandler(value = Exception.class)
@ResponseBody
public ResponseEntity<ErrorDTO> exception(Exception exception,HttpServletResponse response) {
    ErrorDTO errorDTO = new ErrorDTO();
    if(exception instanceof ApiException){//api异常
        ApiException apiException = (ApiException)exception;
        errorDTO.setErrorCode(apiException.getErrorCode());
    }else{//未知异常
        errorDTO.setErrorCode(0L);
    }
    errorDTO.setTip(exception.getMessage());
    ResponseEntity<ErrorDTO> responseEntity = new ResponseEntity<>(errorDTO,HttpStatus.valueOf(response.getStatus()));
    return responseEntity;
}
 
@Setter
@Getter
class ErrorDTO{
    private Long errorCode;
    private String tip;
}
}

  ok,这样就完成了api异常转化成用户可以读懂的DTO对象了,代码中用到了@ControllerAdvice,这是spring MVC提供的一个特殊的切面处理。

  当调用api接口发生异常时,用户也可以收到正常的数据格式了,比如当没有用户(uid为2)时,却为这个用户添加收货地址,postman(Google plugin 用于模拟http请求)之后的数据:

1
2
3
4
{
   "errorCode" : 10003 ,
   "tip" : "找不到该用户"
}

  本文只从如何设计异常作为重点来讲解,涉及到的api传输和service的处理,还有待优化,比如api接口访问需要使用https进行加密,api接口需要OAuth2.0授权或api接口需要签名认证等问题,文中都未曾提到,本文的重心在于异常如何处理


 

Throwable类源码

  StackTraceElement。一个final类,代表栈轨迹中的元素,一个异常可能有多个元素。

Throwable。java里所有错误和异常的基类。

public class Throwable implements Serializable {  
   //异常详细信息  
    private String detailMessage;  
    //初始化异常原因case为本身  
    private Throwable cause = this;  
    //栈轨迹中的元素  
    private StackTraceElement[] stackTrace;  
    //一般也就四个构造函数  
    public Throwable() {  
        fillInStackTrace();  
    }  
  
    public Throwable(String message) {  
        fillInStackTrace();  
        detailMessage = message;  
    }  
  
    public Throwable(String message, Throwable cause) {  
        fillInStackTrace();  
        detailMessage = message;  
        this.cause = cause;  
    }  
  
    public Throwable(Throwable cause) {  
        fillInStackTrace();  
        detailMessage = (cause==null ? null : cause.toString());  
        this.cause = cause;  
    }  
  
    //自定义异常时  可以重写此方法  
    public String getMessage() {  
        return detailMessage;  
    }  
  
    //也可以重写此方法,比喻绑定国际化资源  
    public String getLocalizedMessage() {  
        return getMessage();  
    }  
  
    //if cause == null return null  
    public Throwable getCause() {  
        return (cause==this ? null : cause);  
    }  
    //构造异常链  
   public synchronized Throwable initCause(Throwable cause) {  
        if (this.cause != this)//一旦通过初始化或者initCause设置了cause,就不能再次设置了  
            throw new IllegalStateException("Can't overwrite cause");  
        if (cause == this)//如果 cause 是此 throwable。(throwable 不能是它自己的 cause。)  
            throw new IllegalArgumentException("Self-causation not permitted");  
        this.cause = cause;  
        return this;  
    }  
  
      
    public void printStackTrace() {  
        printStackTrace(System.err);  
    }  
  
    //先打印thia,再打印方法调用的栈轨迹,最后打印cause  
    public void printStackTrace(PrintStream s) {  
        synchronized (s) {  
            s.println(this);  
            StackTraceElement[] trace = getOurStackTrace();  
            for (int i=0; i < trace.length; i++)  
                s.println("\tat " + trace[i]);  
  
            Throwable ourCause = getCause();  
            if (ourCause != null)  
                ourCause.printStackTraceAsCause(s, trace);  
        }  
    }  
  
    //迭代打印cause  
    private void printStackTraceAsCause(PrintStream s,  
                                        StackTraceElement[] causedTrace)  
    {  
        // assert Thread.holdsLock(s);  
  
        // Compute number of frames in common between this and caused  
        StackTraceElement[] trace = getOurStackTrace();  
        int m = trace.length-1, n = causedTrace.length-1;  
        while (m >= 0 && n >=0 && trace[m].equals(causedTrace[n])) {  
            m--; n--;  
        }  
        int framesInCommon = trace.length - 1 - m;  
  
        s.println("Caused by: " + this);  
        for (int i=0; i <= m; i++)  
            s.println("\tat " + trace[i]);  
        if (framesInCommon != 0)  
            s.println("\t... " + framesInCommon + " more");  
  
        // Recurse if we have a cause  
        Throwable ourCause = getCause();  
        if (ourCause != null)  
            ourCause.printStackTraceAsCause(s, trace);  
    }  
    public void printStackTrace(PrintWriter s) {  
            //...同上  
    }  
    private void printStackTraceAsCause(PrintWriter s,  
                                        StackTraceElement[] causedTrace)  
    {  
        //...同上}  
  
    public StackTraceElement[] getStackTrace() {  
        return (StackTraceElement[]) getOurStackTrace().clone();  
    }  
  
     //native方法获取方法调用的栈轨迹  一个Throwable的stackTrace是固定的  
    private synchronized StackTraceElement[] getOurStackTrace() {  
        // Initialize stack trace if this is the first call to this method  
        if (stackTrace == null) {  
            int depth = getStackTraceDepth();  
            stackTrace = new StackTraceElement[depth];  
            for (int i=0; i < depth; i++)  
                stackTrace[i] = getStackTraceElement(i);  
        }  
        return stackTrace;  
    }  
  
    native int getStackTraceDepth();  
  
    native StackTraceElement getStackTraceElement(int index);  
  
  
    }  
}  

异常打印堆栈信息:简单非检查异常实例分析

public class ExceptionDemo {
    public static void main(String[] args) {
         System.out.println("计算开始!");
         calculate();
    }
    public static void calculate(){
        int n = 5;
        int m = 0;
        int result = devide(n,m);
        System.out.println("result:"+result);
    }
    public static int devide(int n, int m){
        return n/m;
    } 
}

  当devide函数除数为0时发生异常,devide()函数将抛出ArithmeticException异常,调用他的calculate也无法正常完成,因此也发送异常,而caculate的调用者main同样会发生异常,这样一直想调用栈的栈底回溯。这种行为叫做异常的冒泡,异常的冒泡是为了在当前发生异常的函数或者这个函数的调用者中找到最近的异常处理程序。由于这个例子中没有使用任何异常处理机制,因此异常最终由main函数抛给JRE,导致程序终止。

  JVM用方法调用栈来跟踪每个线程中一系列的方法调用过程,该栈保存了每个调用方法的本地信息.对于独立的JAVA程序,可以一直到该程序的main方法.当一个新方法被调用的时候,JVM把描述该方法的栈结构置入栈顶,位于栈顶的方法为正确执行的方法.当一个JAVA方法正常执行完毕,JVM回从调用栈中弹处该方法的栈结构,然后继续处理前一个方法.如果java方法在执行代码的过程中抛出异常,JVM必须找到能捕获异常的catch块代码.它首先查看当前方法是否贼这样的catch代码块,如果存在就执行该catch代码块,否则JVM回调用栈中弹处该方法的栈结构,继续到前一个方法中查找合适的catch代码块.最后如果JVM向上追到了main()方法,也就是一直把异常抛给了main()方法,仍然没有找到该异常处理的代码块,该线程就会异常终止,如果该线程是主线程,应用程序也随之终止,此时JVM将把异常直接抛给用户,在用户终端上会看到原始的异常信息.

  使用try...catch,解决一下异常

public class ExceptionDemo {
    public static void main(String[] args) {
         System.out.println("计算开始!");
         try{
         calculate();
         }catch(ArithmeticException e){
             System.out.println("除数为零");
             e.printStackTrace();
         }
         sum();
    }
    public static void sum(){
        int n = 5;
        int m = 4;
        int result = n+m;
        System.out.println("SumResult:"+result);
    }
    public static void calculate(){
        int n = 5;
        int m = 0;
        int result = devide(n,m);
        System.out.println("result:"+result);
    }
    public static int devide(int n, int m){
        return n/m;
    } 
}

  其中e.printStackTrace();用于打印异常回溯。

 

简单检查异常实例分析:

  编写代码后检查异常直接回显示报错的地方,如下图所示,三处变红的地方,之后通过throws来抛出异常。

 

出处资料:

  http://www.cnblogs.com/JavaVillage/articles/384483.html#top

  http://www.importnew.com/28000.html

  https://www.runoob.com/java/java-exceptions.html

  https://zy19982004.iteye.com/blog/1974796

转载于:https://www.cnblogs.com/myseries/p/10907968.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值