欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java异常、SpringBoot和SpringMVC的全局异常处理

程序员文章站 2024-02-19 10:57:28
...

最近几个项目做完后自己也要开始总结一些东西,之前经常是学到啥就写到自己的小笔记中去,现在要开始把东西慢慢整理出来,第一篇博客,路在脚下,希望自己不要懈怠!

了解“异常”

1.异常分类

Java异常、SpringBoot和SpringMVC的全局异常处理

上图是Java程序语言设计中异常层次结构的简单示意图,所有异常都是由Throwable继承来的,下一层分为Error和Exception

Error:描述了Java运行时系统内部的一些错误和资源耗尽错误,在应用程序中我们不应该抛出这种类型的对象。如果出现了这种情况,能做的只有通知用户系统出现了这个错误,并尽可能的安全终止。直接已知的子类还有:

AnnotationFormatError(当注释解析器尝试从类文件中读取注释并确定注释格式错误时抛出。用于反射读取注释API可以抛出此错误)

AssertionError(抛出表示断言失败)

CoderMalfunctionError(当CharsetDecoder的decodeLoop方法或者CharsetEncoder的encodeLoop方法出现不可预知错误时抛出)

FactoryConfigurationError(当存在Parser Factories配置问题时抛出。当无法找到或实例化系统属性中指定的解析器工厂的类时,通常会抛出此错误)

IOError(发生严重I / O错误时抛出)

 LinkageError(子类LinkageError表示一个类对另一个类有一些依赖性; 然而,后一类在前一类的编译后发生了互相矛盾的变化)

 ThreadDeath(调用Thread.stop()方法时,在线程中抛出一个实例)

TransformerFactoryConfigurationError(当存在Transformer Factories配置问题时抛出。当无法找到或实例化系统属性中指定的转换工厂的类时,通常会抛出此错误)

AWTError(发生严重的Abstract Window Toolkit错误时抛出)

VirtualMachineError(抛出此异常表示Java虚拟机已损坏或已耗尽其继续运行所需的资源)

*Error:抛出这个错误是因为递归太深.其实真正的原因是因为Java线程操作是基于栈的,当调用方法内部方法也就是进行一次递归的时候就会把当前方法压入栈直到方法内部的方法执行完全之后,就会返回上一个方法,也就是出栈操作执行上一个方法。出现这种情况的例子:

public class *Test {  

    public static void main(String[] args){  
        test();  
    }  
    private static void test() {  
        test();  
    }  
}

OutOfMemoryError:因为内存溢出,JVM不能分配给对象的创建空间.并且GC也不能够回收足够的空间.当你创建对象的速度快于JVM回收空间的时候就会发生空间不足这个问题                                                                                                                                            出现这种情况的例子:

public class OutOfMemoryTest {  

    public static void main(String[] args){  
        List<Object> testlist = new ArrayList<Object>();  
        while(true){  
            int[] index = new int[20_0000_0000];  
            testlist.add(index);  
        }  
    }  
}  

Exception:类被分为两个分支,按照规则划分为

checked Exception就是在写代码的时候,IDE(比如Eclipse)会要求你写try catch的那种Exception,比如IOException。这种Exception是Java的设计者要求你的程序去处理的。这种异常一般不会影响程序的主体,容易手动诊断修复,所以Java要求你在catch下面写出处理的代码,以保证程序遇到此类exception之后还可以正常运行。

  • ClassNotFoundException 找不到类
  • CloneNotSupportedException 试图克隆一个不能实现Cloneable接口的对象
  • illegalAccessException 对一个类访问被拒绝
  • InstantiationException 试图创建一个抽象类或者抽象接口的实例化对象
  • InterruptedException 一个线程被另一个线程中断
  • NosuchFieldException 请求的字段不存在
  • NosucnMethodException 请求的方法不存在

unchecked这一类就是你在代码处理了checked exception之后,你在运行时候依然会遇到的exception,所以又叫做RunTimeException,比如NullPointerException, IndexOutOfBoundsException。此类exception相较于前面那种更容易影响程序运行,从设计者角度不提倡从程序中catch出来并处理,当然你也可以这么做。

  • ClassCastException 错误的类型转换
  • ArrayIndexOutOfBoundsException 数组访问越界
  • NullPointerException 访问空指针
  • ArithmeticException 除数为0
  • NumberFormatException 字符串转化成数字
  • IllegalArgumentException 传递了不合法的参数
  • UnsupportedOperationException 该操作不支持,如使用Arrays.asList()后调用add、remove出现的异常,这种方法生成的List是Array的内部类ArrayList,与java.util.ArrayList不一样,他们都是继承自AbstractList(方法默认throw 此类异常),但是util.ArrayList重写了这些方法
  • StringIndexOutOfBoundsException 字符串越界,如str.subString(100),index超出了范围
  • Spring框架下,所有SQL异常都被org.springframework重写为RuntimeException,事务会发生回滚!(Sprng中的一些异常)
  •  java.lang.Object
                                   |____java.lang.Throwable
                                        |____ java.lang.Exception
                                             |____ java.lang.RuntimeException
                                                  |____ org.springframework.core.NestedRuntimeException
                                                      |____org.springframework.dao.DataAccessException
                                                           |____  org.springframework.dao.NonTransientDataAccessException
                                                               |____org.springframework.dao.DataIntegrityViolationException
                                                                   |____org.springframework.dao.DuplicateKeyException
               反编译可以看出:org.springframework.dao中的异常都是RuntimeException的子类

2.异常捕获与处理

Java的异常处理一般使用 try catch finally  throw throws这些控制语句,下面详细介绍他们的用法

1.怎么用

OutputStream os = null
try {
    is = new FileOutputStream(new File("C:\\iphonefile\\BuickDealerPlatform.ipa"))
    可能出现异常的代码块 
} catch(IOException e) {
    e.printStackTrace();    // 打印异常堆栈
    throw new UserDefinedException("",e);    //手动抛出自定义的异常对象
    // 或者去做一些其他的事
} catch(ExceptionName e) {
    ...
    // 当产生ExceptionName这种异常时的处理措施
} finally {
    if (os != null) {
        os.close()
    }
    ...    
    // 无论有没有捕获到异常都必须处理的代码,一般用于流的关闭、资源的关闭、删除临时文件
}


// Java SE7后出现了一种try-with-resource的写法,资源的关闭直接由try来搞定,不用写finally
static String readFirstLineFromFile(String path) throws IOException {
    try(BufferedReader br = new BufferedReader(new FileReader(path))) {
        return br.readLine();
    } 
}

// 也有人说这个很鸡肋,文件流读取完后其他代码报错,文件流无法正常释放。本人在项目中基本上是用的这种写法,暂时还没有出现BUG

上面代码基本上都用到了这些关键字,try包裹我们要处理的代码块、catch去捕获我们想要捕获到的异常并做出相应的处理、finally做最后也是必须做的收尾工作,以及SE7新特性try-with-resource的使用

2.throw和throws有什么不同

throw:语句抛出一个异常,写了这一句就是真正要抛出这个异常了。出现在方法体内

if(i > 3) {
    throw new MyselfException("数字太大了")
}

throws:声明该方法可能会抛出这些异常,这样它的调用者就知道要去处理这些异常了。出现在方法头上

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
		//allow cross-origin access
		HttpServletResponse res = (HttpServletResponse)response;
		res.setHeader("Access-Control-Allow-Origin", "*");
		res.setHeader("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE");
		res.setHeader("Access-Control-Max-Age", "3600");
		res.setHeader("Access-Control-Allow-Headers", "content-type,x-requested-with");
		res.setHeader("Access-Control-Allow-Credentials","true");

		chain.doFilter(request, response);
	}

都是消极的处理方式,将异常往上抛,交给别人来处理

3.try-catch-finally-return执行顺序

  • 不管代码块是否有异常,如果有finally块,则finally中的代码一定会执行
  • try catch中有return语句,finally中没有return时:那么除非在finally中修改包装类型和静态变量、全局变量外都不会对try catch中的return值造成影响,先走try、try中没有异常且有return时,先执行return上面的代码再执行return后面的运算,然后执行finally中的代码,再回到try中把该值return出去。走catch中return时同理;
  • try catch中有return语句,finally中有return时:一般不建议在finally中return只拿来做一些关闭的动作,如果这里有return时,try、catch块中的return在返回前都会经过finally,这时如果finally中有return就会直接return出去。

 

SpringBoot项目中使用到的异常处理

首先我们要明确项目中异常处理整体的架子是怎么样的,目前自己做的SpringBoot项目中采用的异常处理是:Controller、Service、Dao、UtilComponent组件工具类各自在自己的层中抛出自定义的该层异常对象,比如我们先自定义了一个BaseException,然后针对分层定义了四个自定义异常类:ControllerException、ServiceException、DaoException、ComponentException,它们都继承了BaseException。各层throw new ExceptionName("异常信息",e),统一由我们写的一个ExceptionMapper去处理,它会重新封装异常信息,返回一个Response给前台。具体代码如下:

Java异常、SpringBoot和SpringMVC的全局异常处理

public abstract class BaseException extends RuntimeException {

    protected int errCode;

    protected String errMsg;

    public BaseException(Throwable e) {
        super(e);
    }
    public BaseException(String msg) {
        super(msg);
    }
    public BaseException(String msg, Throwable throwable) {
        super(msg, throwable);
    }

    public int getErrCode() {
        return errCode;
    }

    public void setErrCode(int errCode) {
        this.errCode = errCode;
    }

    public String getErrMsg() {
        return errMsg;
    }

    public void setErrMsg(String errMsg) {
        this.errMsg = errMsg;
    }
}
public class ControllerException extends BaseException {

    public ControllerException(Throwable throwable) {
        super(throwable);
    }

    public ControllerException(String errMsg) {
        super(errMsg);
        this.errMsg = errMsg;
    }

    public ControllerException(String errMsg, Throwable throwable) {
        super(errMsg, throwable);
        this.errMsg = errMsg;
    }

    public ControllerException(int errCode, String errMsg, Throwable throwable) {
        super(errMsg, throwable);
        this.errCode = errCode;
        this.errMsg = errMsg;
    }
}
public class ServiceException extends BaseException {

    public ServiceException(Throwable throwable) {
        super(throwable);
    }

    public ServiceException(String errMsg) {
        super(errMsg);
        this.errMsg = errMsg;
    }

    public ServiceException(String errMsg, Throwable throwable) {
        super(errMsg, throwable);
        this.errMsg = errMsg;
    }

    public ServiceException(int errCode, String errMsg, Throwable throwable) {
        super(errMsg, throwable);
        this.errCode = errCode;
        this.errMsg = errMsg;
    }
}
public class ComponentException extends BaseException {

    public ComponentException(Throwable throwable) {
        super(throwable);
    }

    public ComponentException(String errMsg) {
        super(errMsg);
        this.errMsg = errMsg;
    }

    public ComponentException(String errMsg, Throwable throwable) {
        super(errMsg, throwable);
        this.errMsg = errMsg;
    }
    public ComponentException(int errCode, String errMsg, Throwable throwable) {
        super(errMsg, throwable);
        this.errCode = errCode;
        this.errMsg = errMsg;
    }
}
public class APPMSExceptionMapper implements ExceptionMapper<Exception> {

	private static org.slf4j.Logger logger = LoggerFactory.getLogger(APPMSExceptionMapper.class);

	@Override
	public Response toResponse(Exception e) {
		//所有异常在这里统一输出
		logger.error(e.getMessage(),e);

		ExceptionResponse result = this.createResponse(e);
		return Response.status(Status.BAD_REQUEST.getStatusCode()).entity(result).type(MediaType.APPLICATION_JSON).build();
	}

	private ExceptionResponse createResponse(Exception ex) {
		int errorCode = AppMSConstants.EX_DEFAULT_ERROR_CODE;
		String msg = ex.getMessage();

		//只获取异常栈中和应用相关的调用信息
		StackTraceElement[] traceElements = ex.getStackTrace();
		StringBuilder stackTraceBuilder = new StringBuilder(ex.toString().concat(AppMSConstants.COLON));
		String trackElement;
		for (StackTraceElement element : traceElements) {
			trackElement = element.toString();
			if (trackElement.contains(AppMSConstants.EX_STACK_FILTER_TOKEN)) {
				stackTraceBuilder.append(trackElement.concat(AppMSConstants.SEMICOLON));
			}
		}

		if (ex instanceof BaseException) {
			errorCode = ((BaseException) ex).getErrCode();
		}
		return new ExceptionResponse(errorCode, msg, stackTraceBuilder.toString());
	}

}
try{
    uploadMapper.appUpload(appInfo, appVersion, supportDeviceIdList);
} catch (Exception e) {
    throw new DaoException("保存应用信息、版本信息与支持设备信息出错", e);
}

上面这种情况是捕获所有层的异常,还有一种是将异常一层层地往上抛,抛到Controller进行处理

单个Controller处理异常:

@RestController
@RequestMapping("/testadvice")
public class TestController {

    @RequestMapping(value = "/test", method = RequestMethod.GET)
    public String test() {
        throw new IOException("抛出IO异常");
    }

    @ExceptionHandler(IOException.class)
    public ControllerExceptionResponse exceptionHandler() {
        ControllerExceptionResponse ge= new ControllerExceptionResponse ();
        ge.setCode(999);
        ge.setMsg("IO异常捕获");
        return ge;
    }

}

全部Controller范围内的异常处理(全局)

//@ControllerAdvice(annotations=RestController.class)
//@ControllerAdvice(basePackages={"com.sgm.xxx","com.sgm.ooo"})
@ControllerAdvice
public class GlobalExceptionHandler {

	private static org.slf4j.Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

	@ExceptionHandler(ServiceException.class)
	protected ResponseEntity<ResultDto> processServiceBizException(ServiceException ex) {
		ResultDto result = new ResultDto();
		logger.error("业务逻辑异常", ex);
		logger.error(getStackMessage(ex));
		result.setStatus(String.valueOf(ex.getErrCode()));
		result.setMessage(ex.getMessage());
		return new ResponseEntity<ResultDto>(result, HttpStatus.FORBIDDEN);
	}

	@ExceptionHandler(DaoException.class)
	protected ResponseEntity<ResultDto> processDaoException(DaoException ex) {
		ResultDto result = new ResultDto();
		logger.error("数据库异常", ex);
		logger.error(getStackMessage(ex));
		result.setStatus(String.valueOf(ex.getErrCode()));
		result.setMessage(ex.getMessage());
		return new ResponseEntity<ResultDto>(result, HttpStatus.INTERNAL_SERVER_ERROR);
	}

	@ExceptionHandler(MethodArgumentNotValidException.class)
	protected ResponseEntity<ResultDto> processMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
		ResultDto result = new ResultDto();
		StringBuffer errorMsgBuffer = new StringBuffer();

		List<ObjectError> errors = ex.getBindingResult().getAllErrors();

		for (ObjectError error : errors) {
			if (errorMsgBuffer.length() > 0)
				errorMsgBuffer.append(";");
			errorMsgBuffer.append(error.getDefaultMessage());
		}
		logger.error("Spring Bean 参数校验异常", ex);
		return new ResponseEntity<ResultDto>(result, HttpStatus.FORBIDDEN);
	}

	private String getStackMessage(Throwable throwable) {
		String msg = throwable == null ? "" : throwable.getMessage();
		return msg;
	}

}

@ControllerAdvice是controller的一个辅助类,最常用的就是作为全局异常处理的切面类

@ControllerAdvice可以指定扫描范围

@ControllerAdvice约定了几种可行的返回值,如果是直接返回model类的话,需要使用@ResponseBody进行json转换

 

SpringMVC项目中使用到的异常处理

SpringMvc的异常处理其实跟上面的一个套路,要么各层抛自定义异常出来,要么都往上抛到Controller再处理,但是SpringMVC中要配置一些东西

application.xml中要将GlobalExceptionHandler加载到spring中管理

<bean id="globalExceptionHandler" class="com.sgm.midjs.exception.GlobalExceptionHandler"></bean>

之前做的一个项目是用的@ControllerAdvice配合@ExceptionHandler(ServiceException.class)注解来做的全局异常处理器,具体代码和上面的相似,只不过SpringBoot省去了需要做的一些配置工作,约定大于配置嘛。

以上,希望有用。