程序代码在没有问题的情况下是可以按照预定的业务逻辑正常的运行,并输出预定的结果;程序因为某些入参或程序未覆盖的逻辑以及一些资源限制等等导致程序不能按照预定逻辑正常执行,出现程序中断的情况就是异常。而针对异常往往需要修改代码避免出现异常或出现异常时进行特殊处理保证程序的正常运行,不对程序整体产生影响以及对于出现异常的业务后续也能跟踪处理。
一、异常
异常的顶级父类是 java.lang.Throwable ,异常整体分为两种:
1、java.lang.Error
对于 Error 错误来说,代表系统的严重错误,比如硬件层面、JVM错误或者内存不足等等,对于这类错误通常不需要特别关注处理,因为往往不是因为程序代码问题导致的,也就没办法通过修改代码加以修复。比如:java.lang.OutOfMemoryError: Java heap space 错误,要么尝试修改内存大小,要么调整出现OOM异常的代码能否占用更小的内存实现功能。
2、java.lang.Exception
对于 Exception 异常来说,是遇到的最多异常,这类异常往往是项目中需要着重处理关注的点,也有两种分类:
受检异常:即编码过程中一些编码软件就能出现提示的异常或程序运行前进行编译的过程中编译无法通过的异常,导致程序无法运行起来,这部分异常在测试前就能发现并修改并且必须处理才能运行,所有不是 RuntimeException 的异常就是受检异常。
非受检异常:即编译能够通过,编码软件不提示异常;程序可以正常运行,但出现某些特殊情况就会导致程序处理异常;这种异常只能在运行中才会出现,如果测试不全面可能会带到生产环境;这种异常就是 java.lang.RuntimeException 运行时异常,不需要强制处理。
异常产生:
当jvm执行程序时,执行到异常程序处会检测到这个异常的信息,然后创建一个对应的异常对象(这个异常对象中包含:异常的内容、原因、代码位置等信息);此时异常处后续代码的执行中断,然后检测产生异常的代码是否有异常处理机制(try/catch),如果有则按照异常处理机制的内容进行异常处理,如果没有异常处理机制则直接抛出这个异常对象给方法的调用者由方法调用者来处理这个异常,如果调用者也没有异常处理机制则继续向上抛出给调用者,最终检测到某个调用者有异常处理机制则按照异常处理机制执行,如果所有调用者都没有异常处理机制则最终交给jvm处理这个异常,jvm首先将异常对象打印在控制台(如果有配置日志记录,也会将控制台的信息记录在日志记录中)并中断当前产生异常的程序执行。
抛出异常:
throw :
作用:可以使用throw关键字在指定的方法中抛出指定的导常
使用格式:throw new xxxException(“异常产生的原因”)
注意:
1、throw关键字必须写在方法的内部。
2、throw关键字后边new的对象必须是Exception或者Exception的子类对象。
3、throw关键字抛出指定的异常对象,我们就必须处理这个异常对象,如果throw关键字后边创建的是RuntimeException或RuntimeException的子类对象,可以不处理,默认会交给JVM处理(即打印异常对象并中断程序);如果throw关键字后边创建的是受检异常(编码软件提示或编译无法通过),就必须处理这个异常,否则程序无法运行,处理方式可以通过 throws 在方法上抛出异常 或 try/catch 处理异常。
二、异常处理
1、throws异常处理:
throws 关键字异常处理方式是交给调用者处理产生的异常。
当方法内部抛出异常对象的时候就必须处理这个异常对象,可以使用throws关键字处理异常对象把异常对象声明抛出给方法的调用者处理(自己不处理,给别人处理),如果调用者也不处理最终交给JVM处理。
使用格式:在方法声明时使用 throws xxxExcepiton,yyyExcepiton {throw new xxxException("产生原因”);throw new yyyException("产生原因”)}。
主意:
1、throws关键字必须写在方法声明处
2、throws关键字后边声明的异常必须是Exception或者是Exception的子类
3、方法内部如果抛出了多个异常对象,那么throws后边必须也声明多个异常如果抛出的多个异常对象有父子类关系,那么直接声明父类异常即可
4、调用了一个声明抛出异常的方法,则必须处理声明的异常,可以继续使用throws声明抛出,交给方法的调用者处理,最终交给JVM处理,或者使用try/catch自己处理异常
如果父类的方法使用 throws 抛出了异常那么子类在重写父类的该方法时可以有以下几种选择:
1、可以保持和父类一样 throws 抛出相同的异常
2、可以 throws 抛出父类该方法异常的子类异常
3、可以不抛出异常
如果父类方法没有使用 throws 抛出异常,那么子类重写时也不能抛出异常,如果有异常只能在内部try/catch处理
2、try/catch异常处理:
try/catch方式是自己处理异常在方法内部直接处理异常而不将异常抛出给调用者。
使用格式:try{可能产生异常的代码}catch(定义一个异常的变量,用来接收try中抛出的异常对象,需要和try中抛出的异常类型匹配或其父类否则不会被捕获){异常的处理逻辑};一般来说异常处理逻辑中会将异常进行记录,并按照业务进行处理。
注意:
1、try中可能会抛出多个异常对象,那么就可以使用多个catch来处理这些异常对象,每个catch处理不同类型的异常
2、如果try中产生了异常就会执行对应catch中的异常处理逻辑,执行完毕catch中的处理逻辑,会继续执行try/catch之后的代码;如果try中没有产生异常就不会执行catch中异常的处理逻辑,执行完try中的代码,继续执行try/catch之后的代码
3、如果定义的catch中没有try中抛出异常的类型或其父类,则没有catch的处理逻辑会被执行;且最终还是交由JVM处理(打印异常,中断程序),try/catch后续逻辑也不会执行
4、try中代码块出现异常的代码之后的程序也不会执行
try/catch针对多个异常代码处理异常一般有以下几种方式:
1、每个会出现异常的代码都分别进行try/catch,这种写法没问题,但是代码比较臃肿,不建议使用。
2、所有会出现异常的代码都写在一个try中,然后通过多个catch分别处理这些异常,注意点在于范围小的异常类型catch写在范围大的异常类型catch前面即子类异常catch写在父类异常的catch前面,否则抛出的异常会直接被前面的父类catch捕获,后面的子类catch没有作用也会编译不通过
3、同样将会出现异常的代码写在一个try中,但是只写一个catch,这个catch的类型是所有可能抛出异常的类型的父类,这样就可以实现所有抛出的异常都被这一个catch捕获
推荐使用第二种,使用第二种既不显得代码臃肿相比于第三种也可以更精细的处理异常,可以根据每个异常的类型做不同的处理逻辑,同时可以最后写一个比较顶级的异常类型做兜底,防止所有catch都匹配不上。
finally关键字:
finally不能单独使用,需要配合try使用不论是否有catch都行;作用在于不论try中是否出现异常,不论出现的异常是否被catch捕获(或没有catch),finally中的逻辑都会执行,所以一般用来做收尾工作,比如资源的释放,连接的关闭等等;try/catch/finally执行顺序按照先后顺序执行,即先执行try,如果出现异常并且通过catch进行捕获,就执行catch中的逻辑,最后执行finally中的逻辑,如果没有catch或catch没有匹配异常类型则执行try之后就执行finally逻辑。
注意:finally中避免使用return语句,因为finally中的逻辑必执行且在try执行之后,所以如果try中没有异常,try中返回的值会最终被finally中的return语句覆盖,那么调用方法返回的值将永远是finally中return的值,所以要避免在finally中使用return语句。
3、异常对象操作方法:
e.getMessage():获取异常提示信息
e.toString():获取异常类型及异常提示信息
e.printStackTrace():打印异常全部信息(如果有集成日志,采用log的记录日志方法可将整个异常对象记录在日志中,记录的信息可以和e.printStackTrace()输出的信息一致)
e.getClass():获取异常类型
三、自定义异常
如果Java自带的异常类型不满足需求时,这时就可以自定义异常,使用自定义的异常去处理一些场景。
注意:自定义异常类需要继承 Exception 或 RuntimeException,如果继承Exception表示当前异常是受检异常抛出该类型异常必须处理(throws或try/catch处理),如果继承RuntimeException表示非受检异常,可以不处理
方式一:该种自定义异常最简洁,只能输入异常提示
package com.my.test.member.biz.exception;/*** 注意:* 1、需要继承 Exception 或 RuntimeException,如果继承Exception表示当前异常是受检异常抛出该类型异常必须处理(throws或try/catch处理),如果继承RuntimeException表示非受检异常,可以不处理* 2、添加一个无参构造器以及一个带异常信息的构造器,无参构造器默认类就有一个,但是添加了其他构造器之后这个默认的无参构造器就没了,所以这两个构造器都要显示的写出来*/
public class CustomException extends RuntimeException{public CustomException(){super();}public CustomException(String msg){//这个msg异常消息会在日志中打印出来,如果不传msg,那么抛出异常时不会打印异常消息super(msg);}}
方式二:自定义异常并自定义设置状态码和消息(一般在项目中用于设置不同的错误提示用不同状态码和消息表示)
package com.my.test.member.biz.exception;public class CustomException extends RuntimeException{public int getCode() {return code;}public void setCode(int code) {this.code = code;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}private int code;private String msg;public CustomException(int code,String msg){super();this.code = code;this.msg = msg;}
}
测试代码:
package com.my.test.member.biz.exception;public class TestExceptiion {public static void main(String[] args) {test();}public static void test(){try {//抛出异常throw new CustomException("自定义异常触发!");}catch (CustomException e){//catch捕获异常,CustomException继承自RuntimeException,是RuntimeException子类所以写在RuntimeException前面,可以通过log将异常信息记录在日志中System.out.println(e);}catch (RuntimeException e){//RuntimeException写在后面作为兜底异常处理,可以通过log将异常信息记录在日志中System.out.println(e);}}}
四、spring boot异常处理
针对spring boot的异常处理,首先程序中应当定义一个统一的返回类型,这样不论是否成功、是否异常,客户端都能按照特定的格式解析数据和消息,否则客户端解析太乱。
自定义返回类型 R :
package com.my.test.member.api;import com.my.test.member.api.constant.CodeConstant;import java.io.Serializable;public class R<T> implements Serializable {private static final long serialVersionUID = 1L;/*** 返回编码 0-成功1-失败*/private int code;/*** 返回消息*/private String message;/*** 返回数据*/private T data;public int getCode() {return code;}public void setCode(int code) {this.code = code;}public String getMessage() {return message;}public void setMessage(String message) {this.message = message;}public T getData() {return data;}public void setData(T data) {this.data = data;}/*** 返回成功,无数据,默认消息* @return*/public static R ok() {return setResult(CodeConstant.SUCCESS,CodeConstant.OK_MSG,null);}/*** 返回成功,自定义消息和数据* @param msg* @param data* @param <T>* @return*/public static <T> R ok(String msg,T data) {return setResult(CodeConstant.SUCCESS,msg,data);}/*** 返回成功,自定义数据* @param data* @param <T>* @return*/public static <T> R ok(T data) {return setResult(CodeConstant.SUCCESS,CodeConstant.OK_MSG,data);}/*** 返回成功,自定义状态码、消息、数据,针对一些特殊情况的处理,可以自定义状态码,否则一般都是采用成功的默认状态码* @param code* @param message* @param data* @param <T>* @return*/public static <T> R ok(int code,String message,T data) {return setResult(code,message,data);}/*** 返回失败,无数据,默认消息* @return*/public static R fail() {return setResult(CodeConstant.FAIL,CodeConstant.FAIL_MSG,null);}/*** 返回失败,自定义消息* @param message* @return*/public static R fail(String message) {return setResult(CodeConstant.FAIL,message,null);}/*** 返回失败,自定义消息和数据* @param msg* @param data* @param <T>* @return*/public static <T> R fail(String msg,T data) {return setResult(CodeConstant.FAIL,msg,data);}/*** 返回失败,自定义状态码、消息和数据,针对一些特殊情况的处理,可以自定义状态码,否则一般都是采用失败的默认状态码* @param code* @param message* @param data* @param <T>* @return*/public static <T> R fail(int code,String message,T data) {return setResult(code,message,data);}/*** 负责创建应答对象* @param code* @param message* @param data* @param <T>* @return*/private static <T> R<T> setResult(int code,String message,T data){R<T> result = new R();result.setCode(code);result.setMessage(message);result.setData(data);return result;}
}
常量类 CodeConstant :
package com.my.test.member.api.constant;public class CodeConstant {/*** 成功code标识*/public static final int SUCCESS = 0;/*** 失败code标识*/public static final int FAIL = 1;/*** 消息提示成功*/public static final String OK_MSG = "OK";/*** 消息提示失败*/public static final String FAIL_MSG = "FAIL";}
自定义异常(根据项目需要选择是否自定义异常):
package com.my.test.member.biz.exception;/*** 自定义异常*/
public class CustomException extends RuntimeException{public int getCode() {return code;}public void setCode(int code) {this.code = code;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}private int code;private String msg;/*** 抛出没有异常消息的异常*/public CustomException(){super();}/*** 自定义状态码和消息的异常* @param code* @param msg*/public CustomException(int code,String msg){super(msg);this.code = code;this.msg = msg;}/*** 根据统一异常枚举设置状态码和消息的异常* @param exceptionEnum*/public CustomException(ExceptionEnum exceptionEnum){super(exceptionEnum.getMsg());this.code = exceptionEnum.getCode();this.msg = exceptionEnum.getMsg();}}
自定义异常枚举类(ExceptionEnum统一维护异常信息):
package com.my.test.member.biz.exception;/*** 异常提示枚举类,将系统中的异常情况全部定义在这里,统一维护规范异常信息,可以将code设定为具有标识意义的值,比如code前两位标识哪个模块,后几位标识状态码信息*/
public enum ExceptionEnum {NAME_NULL_EXCEPTION(1001,"名称为空!"),USER_NOT_EXIST_EXCEPTION(1002,"用户数据不存在!"),PASSWORD_ERROR_EXCEPTION(1003,"密码错误!");public Integer getCode() {return code;}public void setCode(Integer code) {this.code = code;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}/*** 状态码*/private Integer code;/*** 消息*/private String msg;ExceptionEnum(Integer code, String msg) {this.code = code;this.msg = msg;}/*** 根据code获取对应枚举对象* @param code* @return*/public static ExceptionEnum getException(Integer code){for (ExceptionEnum exceptionEnum : ExceptionEnum.values()) {if(code.equals(exceptionEnum.getCode())){return exceptionEnum;}}return null;}
}
全局统一异常处理类(核心类):
package com.my.test.member.biz.exception;import com.my.test.member.api.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;import java.util.List;/*** 全局异常统一处理*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {/*** 全局异常处理 @ExceptionHandler 该注解用于表示当前方法处理哪种类型的异常,@ExceptionHandler标注的方法可以写多个,每个方法处理不同类型的异常* 一般将子类型的异常处理方法写在父类型的前面* @param e* @return*/@ExceptionHandler(Exception.class)public R handleGlobalException(Exception e) {log.error("全局异常信息 e:{}", e);if (e instanceof CustomException){CustomException ce = (CustomException) e;//如果是自定义异常,那么返回自定义的code和消息return R.fail(ce.getCode(),ce.getMsg(),null);}else if (e instanceof MethodArgumentNotValidException){//MethodArgumentNotValidException异常是使用 JSR303(javax.validation.constraints包下面的注解) 比如javax.validation.constraints.NotNull 注解验证参数时,框架抛出的异常类型//参数不满足条件时会把参数的验证错误信息,放入到FieldErrors中MethodArgumentNotValidException mnve = (MethodArgumentNotValidException) e;//getFieldErrors获取所有不满足验证条件的字段的错误提示信息List<FieldError> fieldErrors = mnve.getBindingResult().getFieldErrors();//取多个异常信息中的第一个返回即可,javax.validation.constraints下的注解是对参数的验证,所以这里状态码定为400,fieldErrors里面记录的第一个错误信息不一定是类中字段对应的顺序return R.fail(400,fieldErrors.get(0).getDefaultMessage(),null);}//非前面判断的异常直接返回默认失败状态码和消息return R.fail(e.getMessage());}
}
接口测试:
参数类 dto:
package com.my.test.member.api.dto;import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;/*** 用户dto*/
public class UserDTO {public String getName() {return name;}public void setName(String name) {this.name = name;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}/*** 用户id* 使用JSR303相关注解对字段进行验证,groups用于指定当前验证规则(如:@NotNull、@Null、@NotBlank等等)的分组* controller接口参数加入 @Validated 注解验证规则才能生效,并且验证规则是否生效和 @Validated 注解指定的分组对应* 验证规则指定分组为 UpdateException 则 @Validated 也要指定分组为 UpdateException 那么当前验证规则才能生效* 如果 @Validated 注解不指定分组,那么也只对验证规则不指定分组的验证规则生效,如下面 password 的验证规则未指定分组* 分组接口如:UpdateException 是一个空接口即可*/@NotNull(message = "用户id不能为空!",groups = {UpdateException.class,SelectException.class})@Null(message = "用户id必须为空!",groups = {AddException.class})private Integer id;/*** 用户名称*/@NotBlank(message = "用户名称不能为空!",groups = {AddException.class,UpdateException.class})@NotBlank(message = "用户名称不能为空!")private String name;/*** 用户密码*/@NotBlank(message = "用户密码不能为空!")private String password;}
接口类 controller:
package com.my.test.member.biz.controller;import com.my.test.member.api.R;
import com.my.test.member.api.dto.UserDTO;
import com.my.test.member.biz.exception.CustomException;
import com.my.test.member.biz.exception.ExceptionEnum;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.validation.annotation.Validated;/*** 异常测试controller*/
@RestController
@RequestMapping("exception")
public class TestExceptionController {/*** @Validated({UpdateException.class,SelectException.class}) 表示启用JSR303的验证功能,其中UpdateException.class,SelectException.class表示当前的参数验证* 只对分组为UpdateException和SelectException的字段进行对应规则的验证,如果不写分组,表示只对验证规则没有指定分组的字段进行验证* @param userDTO* @return*/@PostMapping(value = "/login")public R info(@RequestBody @Validated({UpdateException.class,SelectException.class}) UserDTO userDTO){if (!userDTO.getName().equals("张三")){throw new CustomException(ExceptionEnum.USER_NOT_EXIST_EXCEPTION);}if (!userDTO.getPassword().equals("123456")){throw new CustomException(1003,"密码错误!");}return R.ok("查询成功,用户:"+userDTO.getName());}}
注意上面的示例中使用了JSR303的验证方式,按照上面的使用方式需要做如下操作:
1、导入JSR303的jar包
<!-- spring boot项目导入 spring-boot-starter-validation 包即可(已包含hibernate-validator包) --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><!-- 非spring boot项目导入 hibernate-validator 包 --><dependency><groupId>org.hibernate</groupId><artifactId>hibernate-validator</artifactId><version>6.2.3.Final</version></dependency>
2、添加验证规则分组接口:
AddException:
package com.my.test.member.api.group.exception;public interface AddException {
}
DeleteException:
package com.my.test.member.api.group.exception;public interface DeleteException {
}
SelectException:
package com.my.test.member.api.group.exception;public interface SelectException {
}
UpdateException:
package com.my.test.member.api.group.exception;public interface UpdateException {
}
Java日志框架JUL、Log4j、logback、log4j2使用