springboot 事务 抛出异常_springmvc/springboot 全局异常处理和自定义异常

本文介绍了如何在SpringBoot中实现全局异常处理,包括使用ControllerAdvice和AOP进行异常捕获,以及如何利用Spring的事务管理进行异常时的事务回滚。同时,文章展示了如何自定义异常和构建统一的返回格式,以及处理远程调用异常的方法。
摘要由CSDN通过智能技术生成

前言

异常处理其实一直都是项目开发中的大头,但关注异常处理的人一直都特别少。经常是简单的 try/catch 所有异常,然后简单的 printStackTrace ,最多使用 logger 来打印下日志或者重新抛出异常,还有的已经有自定义异常了,但是还是在 controller 捕获异常,需要 catch(异常1 )catch(异常2) 特别繁琐,而且容易漏。

其实 springmvc 在 ControllerAdvice 已经提供了一种全局处理异常的方式,并且我们还可以使用 aop 来统一处理异常,这样在任何地方我们都只需要关注自己的业务,而不用关注异常处理,而且抛出异常还可以利用 spring 的事务,它只有在检测到异常才会事务回滚。

重要说明

下面的相关代码用到了 lombok ,不知道的可以百度下 lombok 的用途

使用建造者模式

统一异常处理

这里使用 springmvc 的 ControllerAdvice 来做统一异常处理

import com.sanri.test.testmvc.dto.ResultEntity;

import com.sanri.test.testmvc.exception.BusinessException;

import com.sanri.test.testmvc.exception.RemoteException;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.ArrayUtils;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.web.bind.annotation.ExceptionHandler;

import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.ArrayList;

import java.util.List;

@RestControllerAdvice

@Slf4j

public class GlobalExceptionHandler {

@Value("${project.package.prefix:com.sanri.test}")

protected String packagePrefix;

/**

* 处理业务异常

* @param e

* @return

*/

@ExceptionHandler(BusinessException.class)

public ResultEntity businessException(BusinessException e){

printLocalStackTrack(e);

return e.getResultEntity();

}

@ExceptionHandler(RemoteException.class)

public ResultEntity remoteException(RemoteException e){

ResultEntity parentResult = e.getParent().getResultEntity();

ResultEntity resultEntity = e.getResultEntity();

//返回给前端的是业务错误,但是需要在控制台把远程调用异常给打印出来

log.error(parentResult.getReturnCode()+":"+parentResult.getMessage()

+" \n -| "+resultEntity.getReturnCode()+":"+resultEntity.getMessage());

printLocalStackTrack(e);

//合并两个结果集返回

ResultEntity merge = ResultEntity.err(parentResult.getReturnCode())

.message(parentResult.getMessage()+" \n |- "+resultEntity.getReturnCode()+":"+resultEntity.getMessage());

return merge;

}

/**

* 打印只涉及到项目类调用的异常堆栈

* @param e

*/

private void printLocalStackTrack(BusinessException e) {

StackTraceElement[] stackTrace = e.getStackTrace();

List localStackTrack = new ArrayList<>();

StringBuffer showMessage = new StringBuffer();

if (ArrayUtils.isNotEmpty(stackTrace)) {

for (StackTraceElement stackTraceElement : stackTrace) {

String className = stackTraceElement.getClassName();

int lineNumber = stackTraceElement.getLineNumber();

if (className.startsWith(packagePrefix)) {

localStackTrack.add(stackTraceElement);

showMessage.append(className + "(" + lineNumber + ")\n");

}

}

log.error("业务异常:" + e.getMessage() + "\n" + showMessage);

} else {

log.error("业务异常,没有调用栈 " + e.getMessage());

}

}

/**

* 异常处理,可以绑定多个

* @return

*/

@ExceptionHandler(Exception.class)

public ResultEntity result(Exception e){

e.printStackTrace();

return ResultEntity.err(e.getMessage());

}

}

统一返回值

一般我们都会定义统一的返回,这样前端好做返回值的解析,像这样

package com.sanri.test.testmvc.dto;

import lombok.Data;

import lombok.ToString;

import java.io.Serializable;

/**

* 普通消息返回

* @param

*/

@Data

@ToString

public class ResultEntity implements Serializable {

private String returnCode = "0";

private String message;

private T data;

public ResultEntity() {

this.message = "ok";

}

public ResultEntity(T data) {

this();

this.data = data;

}

public static ResultEntity ok() {

return new ResultEntity();

}

public static ResultEntity err(String returnCode) {

ResultEntity resultEntity = new ResultEntity();

resultEntity.returnCode = returnCode;

resultEntity.message = "fail";

return resultEntity;

}

public static ResultEntity err() {

return err("-1");

}

public ResultEntity message(String msg) {

this.message = msg;

return this;

}

public ResultEntity data(T data) {

this.data = data;

return this;

}

}

自定义异常

自定义异常,就我目前的工作经历来看的话,异常一般就三种 。

第一种是业务异常,即给定的输入不能满足业务条件,如时间过期、姓名重复、身份证不对啊等等

第二种是调用第三方系统时的异常,其实也属于业务异常

第三种是系统的致命错误,一般是出错了,但这个要在测试和开发阶段就需要处理好,线上出错只能是给用户说系统出错了,然后开发查日志来看错误。

业务异常

对于业务异常来说,我们有时候需要对错误进行编号,因为前端需要拿到编号来做一些页面跳转的工作,而且客户在投诉错误的时候也可以告诉运营编号,然后可以做应对的措施;但绝大部分的时候是不需要错误编号的,这时可以随机生成一个编号。我们可以定一个号段来定义错误编号,比如 0 定义为正常,1~100 为通用错误, 101 ~1000 是 A 系统,1000 ~ 2000 是 B 系统,然后 10000 以上是随机代码等。

import com.sanri.test.testmvc.dto.ResultEntity;

import org.apache.commons.lang3.ArrayUtils;

import org.apache.commons.lang3.ObjectUtils;

import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;

import java.util.List;

/**

* 系统业务异常(根异常),异常号段为 :

* 0 : 成功

* 1 ~ 9999 内定系统异常段

* 10000 ~ 99999 自定义异常码段

* 100000 ~ Integer.MAX_VALUE 动态异常码段

*/

public class BusinessException extends RuntimeException {

protected ResultEntity resultEntity;

protected static final int MIN_AUTO_CODE = 100000;

public static BusinessException create(String message) {

int value= (int) (MIN_AUTO_CODE + Math.round((Integer.MAX_VALUE - MIN_AUTO_CODE) * Math.random()));

return create(value + "",message);

}

public static BusinessException create(String returnCode,String message){

if(StringUtils.isBlank(returnCode)){

return create(message);

}

BusinessException businessException = new BusinessException();

businessException.resultEntity = ResultEntity.err(returnCode).message(message);

return businessException;

}

public static BusinessException create(ExceptionCause exceptionCause ,Object...args){

ResultEntity resultEntity = exceptionCause.result();

String message = resultEntity.getMessage();

if(ArrayUtils.isNotEmpty(args)){

String [] argsStringArray = new String [args.length];

for (int i=0;i

Object arg = args[i];

argsStringArray[i] = ObjectUtils.toString(arg);

}

String formatMessage = String.format(message, argsStringArray);

resultEntity.setMessage(formatMessage);

}

BusinessException businessException = new BusinessException();

businessException.resultEntity = resultEntity;

return businessException;

}

@Override

public String getMessage() {

return resultEntity.getMessage();

}

public ResultEntity getResultEntity() {

return resultEntity;

}

}

远程调用异常

远程调用异常一般别人也会返回错误码和错误消息给我们,这时我们可以定义一个远程异常,把业务异常做为父级异常,这时候呈现的错误结构会是这样,举个例子

投保业务出错

-| E007 生效日期必须大于当前日期

代码如下,用到了建造者设计模式,如不知道这个设计模式,可以自行百度

import com.sanri.test.testmvc.dto.ResultEntity;

import com.sun.deploy.net.proxy.RemoveCommentReader;

public class RemoteException extends BusinessException{

private BusinessException parent;

private RemoteException(BusinessException parent) {

this.parent = parent;

}

/**

* 创建远程异常

* @param parent

* @param remoteCode

* @param remoteMessage

* @return

*/

public static RemoteException create(BusinessException parent,String remoteCode,String remoteMessage){

RemoteException remoteException = new RemoteException(parent);

remoteException.resultEntity = ResultEntity.err(remoteCode).message(remoteMessage);

return remoteException;

}

/**

* 简易创建远程信息

* @param parent

* @param remoteMessage

* @return

*/

public static RemoteException create(BusinessException parent,String remoteMessage){

return create(parent,"remoteError",remoteMessage);

}

public static RemoteException create(String localMessage,String remoteCode,String remoteMessage){

return new Builder().localMessage(localMessage).remoteCode(remoteCode).remoteMessage(remoteMessage).build();

}

public static RemoteException create(String localMessage,String remoteMessage){

return new Builder().localMessage(localMessage).remoteMessage(remoteMessage).build();

}

public static class Builder{

private String localCode;

private String localMessage;

private String remoteCode;

private String remoteMessage;

public Builder localCode(String localCode){

this.localCode = localCode;

return this;

}

public Builder localMessage(String localMessage){

this.localMessage = localMessage;

return this;

}

public Builder remoteCode(String remoteCode){

this.remoteCode = remoteCode;

return this;

}

public Builder remoteMessage(String remoteMessage){

this.remoteMessage = remoteMessage;

return this;

}

public RemoteException build(){

BusinessException businessException = BusinessException.create(localCode, localMessage);

RemoteException remoteException = RemoteException.create(businessException,remoteCode,remoteMessage);

return remoteException;

}

}

public BusinessException getParent() {

return parent;

}

}

优雅的抛出异常

见过很多项目抛出新异常时使用了这样的方式 throw new BusinessException(...) 感觉特别不雅观。

我们不需要暴露异常的构造函数,可以这样子

BusinessException.create("姓名重复,请重新输入");

或者我们可以使用枚举,在枚举类中添加一个方法来创建异常,这针对需要错误编号的异常。

使用方法:

throw SystemMessage.NOT_LOGIN.exception();

代码定义:

import com.sanri.test.testmvc.dto.ResultEntity;

public interface ExceptionCause {

T exception(Object... args);

ResultEntity result();

}

import com.sanri.test.testmvc.dto.ResultEntity;

public enum SystemMessage implements ExceptionCause {

NOT_LOGIN(4001,"未登录或 session 失效"),

PERMISSION_DENIED(4002,"没有权限"),

DATA_PERMISSION_DENIED(4007,"无数据权限"),

SIGN_ERROR(4003,"签名错误,你的签名串为 [%s]")

;

ResultEntity resultEntity = new ResultEntity();

private SystemMessage(int returnCode,String message){

resultEntity.setReturnCode(returnCode+"");

resultEntity.setMessage(message);

}

@Override

public BusinessException exception(Object...args) {

return BusinessException.create(this,args);

}

@Override

public ResultEntity result() {

return resultEntity;

}

/**

* 自定义消息的结果返回

* @param args

* @return

*/

public ResultEntity result(Object ... args){

String message = resultEntity.getMessage();

resultEntity.setMessage(String.format(message,args));

return resultEntity;

}

public String getReturnCode(){

return resultEntity.getReturnCode();

}

}

我们可以进一步封装,将其转换成断言,这个就看个人喜好了,将可以这样使用,只是写个例子,一般登录都在过滤器就拦截了。

assertLogin();

/**

* 断言用户是否为登录状态

*/

public void assertLogin(){

// 获取当前用户,从 session 或 redis 或 auth2 或 shiro 或 SSO 中获取

User user = xxx.get();

if(user == null){

throw SystemMessage.NOT_LOGIN.exception();

}

}

演示使用方法

@RestController

public class ExceptionController {

/**

* 静态异常展示,固定错误码

*/

@GetMapping("/staticException")

public void staticException(){

throw SystemMessage.ACCESS_DENIED.exception("无权限");

}

/**

* 动态异常,前端不关注错误码

*/

@GetMapping("/dynamicException")

public void dynamicException(){

throw BusinessException.create("名称重复,请使用别的名字");

}

/**

* 第三方调用异常,需显示层级异常

*/

@GetMapping("/remoteException")

public void remoteException(){

//模拟远端错误

String remoteCode = "E007";

String remoteMessage = "生效日期必须大于当前日期";

throw RemoteException.create("某某业务调用错误",remoteCode,remoteMessage);

}

}

github 项目代码

sanri-tools 工具

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值