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异常将更加简洁。