一.JAVA中的异常处理机制

在Java中异常定义为Throwable类,其有两个子类分别为Error类与Expection类。其中Error由JVM抛出,用于描述系统内部错误或资源耗尽错误,这种错误一旦发生无法由程序进行处理,仅能通知用户并终止程序。Exception则指的是程序能够进行处理的异常,其中RuntimeException类异常指由于程序错误导致的异常,如数组越界等。
在Java中异常处理机制分为抛出异常与捕捉异常,抛出异常将异常对象层层抛出直至JVM,而捕捉异常则使用try,catch,finally,throw,throws关键字进行异常捕捉。其中catch异常捕获存在通用原则:越是底层的异常越应定义在后部;finally常用于关闭外界资源,如实体管理器工厂、实体管理器、数据库连接等,且若在try,catch语句块内遇到return语句则finally语句块将在return之前执行,但finally语句块在下列情况下将不会被执行:1.在finally语句块中发生了异常2.提前使用exit()函数退出程序3.程序所在线程死亡。
且若现存异常不满足需求则在Java中可通过继承Exception自定义异常类。
二.SpringBoot中的异常处理
SpringBoot中的异常处理分为全局异常捕获处理与局部异常捕获处理。在SpringBoot项目开发过程中可以发现用户输入错误数据导致程序出现异常程序不会终止,而是显示Whitelabel Error Page页面并显示错误提示,这是由于SpringBoot提供了一套默认的异常处理机制,一旦程序出现错误就会自动请求/error,并将其交由BasicExceptionController处理,由其返回默认显示页面并显示异常信息。
而若需要对异常处理方式进行自定义,针对局部异常处理可使用@ExceptoinHandler注解,如@ExceptionHandler(value={ArithmeticException.class}),
则当在此controller内出现ArithmeticException异常时将调用此方法进行异常处理。针对全局异常处理则可将@ControllerAdvice与@ExceptionHandler配合使用,将@ControllerAdvice加之类前从而将此类定义为全局异常处理类,再在该类的方法前使用@ExceptionHandler注解则能够对特定异常执行特定处理方法。
三.异常的合理使用
把异常当做正常处理逻辑的一部分的那种程序,就会遭受与所有典型的意大利面条式代码同样的可读性和可维护性问题。
Andy Hunt 和 Dave Thomas
上述的句子引自《代码大全第二版》,个人理解:异常处理应只能完成其本职工作,即异常处理本身,而不应该将程序正常运行的逻辑放入异常处理之中,否则随着异常处理代码逻辑越来越繁杂,其本身将可能像正常代码逻辑一样产生异常。且异常处理相比于正常逻辑处理将需要更多开销,在框架中由其如此,如在SpringBoot项目中,假设需要接收用户注册所输入的用户名信息,由于用户很可能输入不规范字符类型,因此需要对输入进行检测,在这种情况下若利用异常处理机制能够很方便地进行处理且不影响正常输入代码逻辑,但SpringBoot框架在进行异常处理中需要调用框架所集成的异常处理机制,若开启日志则花销将更加巨大,因此对于类似业务逻辑可以使用原始的输入检测方法代替异常处理。
同时在《代码大全第二版》中给出如下的异常处理建议:
- 用异常通知程序的其他部分发生了不可忽略的错误
- 只在真正例外的情况下才抛出异常
- 不能用异常来推卸责任
- 避免在构造函数和析构函数中抛出异常,除非你在同一地方把它们捕获
- 在恰当的抽象层次抛出异常
- 在异常消息中加入关于导致异常发生的全部信息
- 避免使用空的catch语句
- 了解所用函数库可能抛出的异常
- 考虑创建一个集中的异常报告机制
- 把项目中对异常的使用标准化
- 考虑异常的替代方案
建议一是基于异常本身设计的用意,即提供无法被忽略的错误通知机制,从而将错误控制在一定的范围内避免扩散。
建议二是为了防止异常的滥用,使用异常将使得程序复杂性增加,且异常向上抛出的过程中调用函数需要了解被调用函数可能抛出的异常,这将弱化封装性。
建议三指明异常捕获的位置不可一再拖延,若当前代码块能够处理异常就应当在当前代码块内处理异常。
建议四是基于构造函数与析构函数自身的复杂性,当将异常引入构造函数与析构函数时情况将变得极其复杂,因此个人选择在构造函数与析构函数中不使用异常处理。
建议五是为了防止对代码封装性的破坏。对象与对象之间存在层间关系,上下层之间的抽象程度不同,从原则上讲上层代码不应了解下层代码的实现细节,而异常的抛出将会暴露低层的实现方式,因此不同层间抛出的异常抽象程度也应不同。如底层函数为文件处理,因此可能返回EOFException异常,若将此异常返回至调用方将暴露自身的实现细节,因此应将低层异常封装为自定义异常对象,当产生异常时返回此自定义异常,从而避免封装性的破坏。
建议六是为了使异常抛出的价值最大化。当异常抛出时说明程序某处发生了异常错误,此时应当将错误产生的原因详细输出,使得用户可以迅速定位问题发生处,因此许多框架使用日志对异常进行记录。
建议七中假设使用了空的catch语句,则表示当异常发生时不进行任何处理,则用户无法判断异常是否发生,而这种情况下产生的潜在危害甚至大于不进行异常捕获,不进行异常捕获至少当异常发生时用户能够注意到异常从而进行相应的处理。同时在书中提到若确实存在某种情况必须采用空的catch语句则需要使用注释或向日志文件中记录信息从而将此情况“文档化”。
建议八主要针对不要求子程序定义可能抛出的异常的编程语言。若对函数库可能抛出的异常不了解则当函数库抛出异常时将导致程序崩溃。
建议九中所提及的集中异常报告机制类似于SpringBoot中的全局异常处理类。通过将异常处理集中于一个类中从而确保异常处理的一致性。
建议十即要求一个项目在构建前需要对异常的使用进行标准化限制,如规定何种场合允许使用throw-catch语句,何种场合允许将异常抛出,是否允许在构造函数与析构函数中使用异常等。由于异常在通常情况下可能散布于程序的各个模块内,若不对异常进行标准化限制将导致项目变得更加杂乱。
建议十一指出不要为了使用异常处理机制而使用异常,在许多情况下可以采用更好的方式来替代异常处理,在使用异常处理前应像考虑选择哪种算法一样考虑异常处理机制在当前场景下是否优于其他错误处理机制。
在项目构建过程中需要将以上十一条建议融会贯通,正确的异常处理使用方式能够将异常处理机制的优点充分发挥,但不恰当的使用方式带来的危害甚至大于程序崩溃。
四.防御式编程
防御式编程的思想为子程序应该不因传入错误数据而被破坏,哪怕是由其他子程序产生的错误数据。即在设计自己的模块时对于所有外部传入的参数都要假定其不满足事先约定的数据格式,并对不同的假设进行不同的处理。
在思想上防御式编程与异常处理机制的使用存在相似性,异常处理机制要求对程序运行过程中可能产生的异常进行猜测并编写相应的处理方法,而防御式编程则需要猜测从外部传入的数据都可能存在哪些错误可能性,且同样需要对不同的可能性进行处理。从运行流程来看,异常处理机制作用于模块运行过程中,而防御式编程则作用于模块开始处,因此对于需要高健壮性的程序可将二者结合使用,虽然会增加代码的复杂性,但能够大大增加程序的容错性。
同时在《代码大全第二版》中提到了隔栏容损策略与进攻式编程。隔栏策略在输入数据之后建立隔栏层,专门用于对输入的数据进行清理,则当数据从隔栏中出来后即为干净可信的数据。采用这种方式将使得由防御式编程产生的代码更加易于管理,也可以将所有防御式代码集中于一个类中,即类似于异常处理机制中的建议九:考虑创建一个集中的异常报告机制。进攻式编程应用于开发阶段,其思想在于将所有可能错误在开发阶段充分展现。如完全填充分配到的所有内存,确保每一个case语句中的default分支或else分支都能产生严重错误等。
五.总结
异常处理、防御式编程、进攻式编程都是为了增加代码的健壮性。毫无疑问他们的使用将增加代码的复杂性,但相比于他们能够带来的代码稳定性而言代价可忽略不计。但尚未纳入代码规范的编程方式均存在利弊,只有合理使用才能最大限度发挥其优点。
Comment