实战 - 项目中的异常处理方式

1. 异常链

对于真实的企业级应用而言,常常有严格的分层关系,层与层之间有非常清晰的划分,上层功能的实现严格依赖于下层的API,也不会跨层访问。

当业务逻辑层访问持久层出现SQLException异常时,程序不应该把底层的SQLException异常传到用户界面,有如下两个原因:

  • 对于正常用户而言,他们不想看到底层SQLException异常,SQLException异常对他们使用该系统没有任何帮助。
  • 对于恶意用户而言,将SQLException异常暴露出来不安全。

底层的原始异常直接传给用户是一种不负责任的表现。通常的做法是:程序先捕获原始异常,然后抛出一个新的业务异常,新的业务异常中包含了对用户的提示信息,这种处理方式被称为异常转译。

public class MyException extends Exception {
    public MyException(){

    }

    public MyException(String message){
        super(message);
    }
}
public class Test {
    public static void main(String[] args) {
        
    }

    public static void  calSal() throws MyException {
       try {
           //实现工资结算
       }catch (SQLException sqle){
           // 把原始异常信息记录下来,留给管理员
           // ...
           // 下面异常中的message就是对用户的提示
           throw new MyException("访问底层数据库出现异常");
       }catch (Exception e){
           // 把原始异常信息记录下来,留给管理员
           // ...
           // 下面异常中的message就是对用户的提示
           throw new MyException("系统未知异常");
       }
    }
}

这种把原始异常信息隐藏起来,仅向上提供必要的异常提示信息的处理方式,可以保证底层异常不会扩散到表现层,可以避免向上暴露太多的实现细节,这完全符合面向对象的封装原则。

2. catch+throw使用示例

1. 自定义异常

@Data
public class IncidentException extends Exception {
    private static final long serialVersionUID = -7888549951554226678L;

    private Integer code;

    private Object data;

    public IncidentException() {
        this("");
    }

    public IncidentException(String message) {
        this(0, message);
    }

    public IncidentException(Integer code, String message) {
        super(message);
        this.code = code;
    }

    public IncidentException(Integer code, String message, Object data) {
        super(message);
        this.code = code;
        this.data = data;
    }

    public IncidentException(Integer code, String message, Throwable throwable) {
        super(message, throwable);
        this.code = code;
    }
}

对于Checked异常的处理方式有如下两种:

  • 当前方法明确知道如何处理该异常,程序应该使用try…catch块来捕获该异常,然后在对应的catch块中修复该异常。
  • 当前方法不知道如何处理这种异常,应该在定义该方法时声明抛出该异常。

2. Service层抛出IncidentException

@Override
public BatchOperateResult toBeTransferred(ToBeTransferOperateQo toBeTransferOperateQo) throws IncidentException {
    try {
        // 该方法会抛出IOException
        List<String> cascadeIds = this.alertDao.getCascadeIds(notAddIds, null);
    
        // 该方法会抛出IOException, JsonSerializeException
        List<Alert> alerts = this.alertDao.getAlerts(notAddIds);

        // 该方法会抛出NotificationException
        riskEventService.saveBatch(alertList), addResult)
            
    } catch (IOException | JsonSerializeException | NotificationException e) {
        // 这里最好打印日志记录异常发生的详细情况,应用后台需要通过日志来记录异常发生的详细情况;
        // 程序先捕获原始异常,然后抛出一个新的业务异常,新的业务异常中包含了对用户的提示信息
        throw new IncidentException("标记为待流转失败", e);
    }
}

这种catch和throw结合使用的情况在大型企业级应用中非常常用,企业级应用对异常的处理通常分成两个部分:

① 应用后台需要通过日志来记录异常发生的详细情况;

② 应用还需要根据异常向应用使用者传达某种提示。在这种情形下,所有异常都需要两个方法共同完成,也就必须将catch和throw结合使用。

3. Controller层继续抛出IncidentException

@PostMapping(value = "/transfer")
public ApiResponse<BatchOperateResult> toBeTransferred(
    @RequestBody ToBeTransferOperateQo qo) throws IncidentException {
    BatchOperateResult batchOperateResult 
        = this.alertService.toBeTransferred(toBeTransferOperateWithLogQo.getData());
    return this.dealBatchResultOrThrow(batchOperateResult);
}

Controller层并没有对Service层抛出的异常对象进行捕获处理,而是继续抛出IncidentException

4. 统一异常处理

@RestControllerAdvice
@Slf4j
@Priority(1)
public class GlobalExceptionHandler {

    @ExceptionHandler(IncidentException.class)
    public ApiResponse<Object> handleIncidentException(IncidentException e) {
        // 打印日志
        log.error(e.getMessage(), e);
        // 给用户的提示信息
        return ApiResponse.newInstance(e.getCode(), e.getMessage(), e.getData());
    }
}

3. catch + throw使用示例

1. 自定义异常QueryAlertException

public class QueryAlertException extends IncidentRuntimeException {

    public QueryAlertException(String message) {
        super(message);
    }

    public QueryAlertException(String message, Throwable throwable) {
        super(message, throwable);
    }
}
@Data
public class IncidentRuntimeException extends RuntimeException {
    private Integer code;

    private Object data;

    public IncidentRuntimeException() {
        this("");
    }

    public IncidentRuntimeException(String message) {
        this(ApiResponse.CODE_ERR_COMMON, message);
    }

    public IncidentRuntimeException(Integer code, String message) {
        super(message);
        this.code = code;
    }

    public IncidentRuntimeException(Integer code, String message, Object data) {
        super(message);
        this.code = code;
        this.data = data;
    }

    public IncidentRuntimeException(String message, Throwable throwable) {
        this(ApiResponse.CODE_ERR_COMMON, message, throwable);
    }

    public IncidentRuntimeException(Integer code, String message, Throwable throwable) {
        super(message, throwable);
        this.code = code;
    }

    public static IncidentRuntimeException fromThrowable(final Throwable t) {
        if (t == null) {
            return null;
        } else {
            return fromThrowableNonNull(t);
        }
    }

    public static IncidentRuntimeException fromThrowableNonNull(final Throwable t) {
        if (t instanceof IncidentRuntimeException) {
            return (IncidentRuntimeException) t;
        } else {
            return new IncidentRuntimeException(t.getMessage(), t);
        }
    }
}

2. Service层抛出IOException

@Override
public CooperatorResultDto getAlertCooperators(String eventId) throws IOException {
	// 该方法会抛出IOException
    Alert alert = this.getAlertById(eventId);
    return cooperatorResultDto;
}

3. Controller层捕获IOException

@GetMapping("/cooperators/{id}")
public CooperatorResultDto getAlertCooperator(@PathVariable("id") String id) {
    try {
        return alertService.getAlertCooperators(id);
    } catch (IOException e) {
        throw new QueryAlertException("协作人信息查询失败");
    }
}

底层的原始异常直接传给用户是一种不负责任的表现。通常的做法是:程序先捕获原始异常,然后抛出一个新的业务异常,新的业务异常中包含了对用户的提示信息,这种处理方式被称为异常转译。

4. 统一异常处理

@RestControllerAdvice
@Slf4j
@Priority(1)
public class GlobalExceptionHandler {


    @ExceptionHandler(IncidentRuntimeException.class)
    public ApiResponse<Object> handleIncidentRuntimeException(IncidentRuntimeException e) {
        log.error(e.getMessage(), e);
        return ApiResponse.newInstance(e.getCode(), e.getMessage(), e.getData());
    }
}

4. catch+throw使用示例

1. 自定义异常

public class CommonException extends RuntimeException {
    private static final long serialVersionUID = 1L;

    private BizCodeEnum codeEnum;

    public CommonException() {
    }

    public BizCodeEnum getCodeEnum() {
        return codeEnum;
    }

    public CommonException(BizCodeEnum codeEnum) {
        super(codeEnum.getMessage());
        this.codeEnum = codeEnum;
    }

    public CommonException(BizCodeEnum codeEnum, Throwable cause) {
        super(codeEnum.getMessage(), cause);
        this.codeEnum = codeEnum;
    }

    public CommonException(String message) {
        super(message);
    }

    public CommonException(String message, Throwable cause) {
        super(message, cause);
    }
}

2. 异常状态码

@Slf4j
public enum BizCodeEnum implements BaseCode {
    /**
     * 异常状态码和信息
     */
    ATTACHMENT_UPLOAD_ERROR(10001, "attachment.upload.failed"),
    GRIDFS_ATTACHMENT_NOT_EXIST(10002, "mongodb.gridFs.attachment.not.exist"),
    ELASTICSEARCH_DOC_SELECT_ERROR(10007, "elasticsearch.document.select.failed"),

    private Integer code;
    private String message;

    BizCodeEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    BizCodeEnum(String message) {
        this(null, message);
    }

    @Override
    public Integer getCode() {
        if (code == null) {
            return 400;
        }
        return code;
    }

    @Override
    public String getMessage() {
        String i18nStr;
        try {
            i18nStr = I18nUtils.i18n(message);
        } catch (NoSuchMessageException e) {
            log.error("Not found Internationalized configuration:{}", message);
            i18nStr = message;
        }
        return i18nStr;
    }
}

3. Service层捕获IOException后抛出CommonException

@Override
public SearchResultRespVo searchDoc(DocSearchReqVo docSearchReqVo) {
    SearchRequest searchRequest = buildSearchRequest(docSearchReqVo);
    long total;
    List<DocListVo> docList = null;
    try {
        // 执行搜索
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        SearchHits hits = searchResponse.getHits();
        total = hits.getTotalHits().value;
        SearchHit[] searchHits = hits.getHits();
        docList = new ArrayList<>();
        for (SearchHit searchHit : searchHits) {
            docList.add(new DocListVo(JSON.parseObject(searchHit.getSourceAsString(), Doc.class)));
        }
    } catch (IOException e) {
        log.error("failed to query documents in elasticsearch:", e);
        throw new CommonException(BizCodeEnum.ELASTICSEARCH_DOC_SELECT_ERROR);
    }
    return new SearchResultRespVo(docList, total);
}

总结:

  • 捕获原始底层异常IOException
  • 打印日志
  • 抛出一个新的异常向用户返回提示信息

注意:

  • 因为CommonException是运行时异常,所以即使抛出了,也不需要显式的去处理。 CommonException extends RuntimeException
  • 如果是Checked Exception,那么就需要显式的处理,try…catch或者throws,比如:IncidentException extends Exception

4. 统一异常处理

@Slf4j
@RestControllerAdvice
public class DocExceptionHandler {

    @ExceptionHandler(CommonException.class)
    public ApiResponse<Object> serviceExceptionHandler(CommonException e) {
        log.error("service exception");
        if (Objects.nonNull(e.getCodeEnum())) {
            return ApiResponse.newInstance(e.getCodeEnum().getCode(), e.getMessage());
        } else {
            return ApiResponse.newInstance(400, e.getMessage());
        }
    }
}

5. throw使用示例

1. 自定义异常

public class DocAttachmentNotFoundException extends RuntimeException {
    public DocAttachmentNotFoundException() {
        super(I18nUtils.i18n("exception.docAttachment.instance.not.exists"));
    }
}

2. Service层抛出异常

@Override
public GridFsResource getById(String id) {
    DocAttachment docAttachment 
        = Optional.ofNullable(knowledgeMongoTemplate.findById(id, DocAttachment.class))
        .orElseThrow(DocAttachmentNotFoundException::new);
}

因为DocAttachmentNotFoundException是运行时异常,所以即使抛出了,也不需要显式的去处理。

3. 统一异常处理

@Slf4j
@RestControllerAdvice
public class DocExceptionHandler {

    @ExceptionHandler({ DocAttachmentNotFoundException.class })
    public ApiResponse<Object> exception(DocAttachmentNotFoundException e) {
        log.error(e.getMessage());
        return ApiResponse.newInstance(ApiResponse.CODE_PARAMETER_VALIDATE_FAILED, e.getMessage());
    }
}

总结:

对于Checked异常的处理方式有如下两种:

  • 当前方法明确知道如何处理该异常,程序应该使用try…catch块来捕获该异常,然后在对应的catch块中修复该异常。
  • 当前方法不知道如何处理这种异常,应该在定义该方法时声明抛出该异常。

使用Checked异常至少存在如下两大不便之处:

  • 对于程序中的Checked异常,Java要求必须显式捕获并处理该异常,或者显式声明抛出该异常。这样就增加了编程复杂度。
  • 如果在方法中显式声明抛出Checked异常,将会导致方法签名与异常耦合,如果该方法是重写父类的方法,则该方法抛出的异常还会受到被重写方法所抛出异常的限制。

在大部分情况下,推荐使用Runtime异常,而不使用Checked异常。尤其当程序需要自行抛出异常时,使用Runtime异常将更加简洁。

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在Spring Boot项目,可以通过异常处理来处理应用程序出现的异常。Spring Boot提供了多种处理异常的方式,下面介绍两种常用的方式:全局异常处理和自定义异常处理。 1. 全局异常处理: 在Spring Boot项目,可以通过定义一个全局异常处理器来统一处理应用程序的异常。可以使用@ControllerAdvice注解和@ExceptionHandler注解来实现全局异常处理。 首先,在项目创建一个类,使用@ControllerAdvice注解标记该类为全局异常处理器。然后,在该类定义一个或多个方法,使用@ExceptionHandler注解标记这些方法来处理具体的异常。例如: ```java @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public ResponseEntity<String> handleException(Exception ex) { // 处理异常逻辑... return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Internal Server Error"); } } ``` 上述代码,handleException方法用于处理所有的Exception类型的异常,当应用程序出现Exception类型的异常时,会执行该方法,并返回一个包含错误信息的ResponseEntity对象。 2. 自定义异常处理: 除了全局异常处理外,还可以自定义异常处理来处理特定的异常。可以通过创建自定义的异常类,并在需要抛出异常的地方抛出该异常,然后通过@ControllerAdvice注解和@ExceptionHandler注解来处理该异常。 首先,创建一个自定义的异常类,继承自Exception或其子类。例如: ```java public class MyCustomException extends Exception { public MyCustomException(String message) { super(message); } } ``` 然后,在需要抛出异常的地方抛出该异常。例如: ```java public void myMethod() throws MyCustomException { // 发生异常的逻辑... throw new MyCustomException("Something went wrong"); } ``` 最后,在全局异常处理使用@ExceptionHandler注解来处理该自定义异常。例如: ```java @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(MyCustomException.class) public ResponseEntity<String> handleMyCustomException(MyCustomException ex) { // 处理自定义异常逻辑... return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage()); } } ``` 上述代码,handleMyCustomException方法用于处理MyCustomException类型的异常,当应用程序出现MyCustomException类型的异常时,会执行该方法,并返回一个包含错误信息的ResponseEntity对象。 使用全局异常处理和自定义异常处理可以使得Spring Boot项目异常处理更加灵活和统一。根据具体需求,可以选择适合自己项目异常处理方式

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我一直在流浪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值