全局异常处理与Retry重试
本章主要介绍Spring Boot全局异常使用、自定义错误页面、全局异常类开发、Retry重试机制的介绍与使用等内容。
1.全局异常介绍
由于Web应用请求处理过程中发生错误是很常见的情况,所以Spring Boot为我们提供了一个默认的映射:/error,当处理中抛出异常之后,会转到该请求中处理,并且该请求有一个全局的错误页面用来展示异常内容。比如现在我们启动spring-boot-book-v2项目(启动项目之前,记得启动Redis服务和ActiveMQ服务),项目启动完成之后,在浏览器中随便输入一个访问地址,比如http://localhost:8080/ayUser/testdddd,由于地址不存在,Spring Boot会跳转到错误界面,如图所示。
虽然Spring Boot提供了默认的error错误页面映射,但是在实际应用中,所示的错误页面对用户来说并不友好,通常需要我们自己来实现异常提示。
2.Spring Boot全局异常使用
2.1自定义错误页面(前后端分离后,本部分无需实现)
我们知道,Spring Boot的错误提示页面对用户体验并不好,这一节我们来实现自己的错误提示页面。首先,在spring-boot-book-v2项目/src/main/resources/static目录下新建自定义错误页面404.html,具体代码如下:
<! DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div class="text" style="text-align:center;">
主人,我累了,让我休息一会!!!
</div>
</body>
</html>
错误页面内容很简单,就是当访问路径不存在时,在页面中间显示一句话:“主人,我累了,让我休息一会!!!”。当然,在真正的项目中,该错误页面样式会更加高大上。
404错误页面开发完成之后,在spring-boot-book-v2项目目录/src/main/java/com.example.demo下新建包error,并在error包下新建ErrorPageConfig配置类,具体代码如下:
@Confiquration
public class ErrorPageConfig{
@Bean
public WebServerFactoryCustomize<ConfigurableWebServerFactory>
webServerFactoryCustomizer(){
return (container -> {
ErrorPage error404Page = new errorpage(httpstatus.NOT_FOUND,"/404.html");
container.addErrorPages(error401Page, error404Page, error500Page);
});
}
}
- WebServerFactoryCustomizer:Spring Boot的自动配置有一个特性就是能够通过代码来修改配置,这样可以很方便地修改配置,而我们只需要实现Spring Boot定义的接口即可实现该功能。这里,需要注册一个实现了WebServerFactoryCustomizer的Bean,在ErrorPageConfig类中,使用匿名类来实现WebServerFactoryCustomizer接口,同时实现该接口唯一的方法customize,并自定义401、404、500等错误页面。
2.2测试
404.html错误页面和ErrorPageConfig类开发完成之后,重启动spring-boot-book-v2项目(项目启动之前,请记得启动Redis缓存服务和ActiveMQ服务,否则项目会报错,之后不再提示),在浏览器访问输入链接http://localhost:8080/ayUser/testdddd,由于该链接不存在,就会出现如图所示的自定义错误页面。
2.3全局异常类开发
在项目中,我们会遇到各种各样的业务异常,业务异常是指正常的业务处理时,由于某些业务的特殊要求而导致处理不能继续所抛出的异常。我们希望这些业务异常能够统一被处理,而使用Spring Boot进行全局异常处理很方便。首先,在目录src/main/java/com.example.demo.error下统一封装自定义业务异常类BusinessException,该类继承RuntimeException异常类,并提供带有异常信息的构造方法,具体代码如下:
public class BusinessException extends RuntimeException{
public BusinessException() {}
public BusinessException(String message) {
super(message);
}
}
然后,在目录/src/main/java/com.example.demo.error下新建错误信息类ErrorInfo,该类用于封装错误信息,包括错误码,具体代码如下:
public class ErrorInfo<T> {
public static final Integer SUCCESS = 200;
public static final Integer ERROR = 100;
//错误信息
private Integer code;
//错误码
private String message;
private String url;
private T data;
//忽略set、get方法
}
其次,在目录/src/main/java/com.example.demo.error下新建统一异常处理类GlobalDefaultExceptionHandler,具体代码如下:
@ControllerAdvice(basePackages="com.example.demo",})
public class GlobalDefaultExceptionHandler {
@ExceptionHandler({BusinessException.class})
//如果返回的为json数据或其他对象,添加该注解
@ResponseBody
public ErrorInfo defaultErrorHandler(HttpServletRequest reg, Exception e) throws Exception {
ErrorInfo errorInfo = new ErrorInfo();
errorInfo.setMessage.(e.getMessage());
errorInfo.setUrl(req.getRequestURI());
errorInfo.setCode(ErrorInfo.SUCCESS);
return errorInfo;
}
}
- @ControllerAdvice:定义统一的异常处理类,basePackages属性用于定义扫描哪些包,默认可不设置。
- @ExceptionHandler:用来定义函数针对的异常类型,可以传入多个需要捕获的异常类。
- @ResponseBody:如果返回的为json数据或其他对象,添加该注解。
最后,在AyUserController类下添加控制层方法findAll,并在方法里抛出BusinessException,该异常会被全局异常类捕获到,具体代码如下:
@Controller
@RequestMapping("/ayUser")
public class AyUserController {
@Resource
private AySuerService ayUserService;
@RequestMapping("/findAll")
public String findAll(Model model) {
List<AyUser> ayUser = ayUserService.findAll();
model.addAttribute("users",ayUser);
throw new BusinessException("业务异常");
}
}
2.4测试
代码开发完成之后,重启动spring-boot-book-v2成功之后,在浏览器中输入访问地址:http://localhost:8080/ayUser/findAll,在浏览器中可以看到后端返回的json信息,具体信息如下:
{"code":200,"message":"业务异常","url":"/ayUser/findAll","data":null}
3.Retry重试机制
3.1Retry重试概述
当我们调用一个接口时,由于网络等原因可能会造成第一次失败,再去尝试就成功了,这就是重试机制。重试的解决方案有很多,比如利用try-catch-redo简单重试模式,通过判断返回结果或监听异常来判断是否重试,具体请看如下简单的例子:
public void testRetry() throws Exception{
boolean result = false;
try{
result = load();
if(!result){
load();//一次重试
}
}catch (Exception e){
load();//一次重试
}
}
try-catch-redo重试模式还是有可能出现重试无效的现象,解决这个问题的方法是尝试增加重试次数retrycount和重试间隔周期interval,以达到增加重试有效的可能性。因此我们可以利用try-catch-redo-retry策略重试模式,具体伪代码如下所示:
public void testRetry2() throws Exception{
boolean result = false;
try{
result = load();
if(!result){
//延迟3秒,重试三次
reload(3000L, 3);//延迟多次重试
}
}catch (Exception e){
//延迟3秒,重试三次
reload(3000L, 3);//延迟多次重试
}
}
但是这两种策略有一个共同的问题就是:正常逻辑和重试逻辑强耦合。基于这些问题,Spring-Retry规范正常和重试逻辑,Spring-Retry是一个开源工具包,该工具把重试操作模板定制化,可以设置重试策略和回退策略。同时重试执行实例保证线程安全。Spring-Retry重试可以用Java代码方式实现,也可以用注解**@Retryable**方式实现,这里Spring-Retry提倡以注解的方式对方法进行重试。
3.2Retry重试机制使用
使用Spring提供的重试策略之前,首先需要在pom.xml文件中引入所需的依赖,具体代码如下:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
依赖添加完成之后,需要在入口类MySpringBootApplication添加注解@EnableRetry开启Retry重试。完整代码如下:
@SpringBootApplication
@ServletComponentScan
@ImportResource(location={"classpath:spring-mvc.xml"})
@EnableAsync
//开启Retry重试
@EnableRetry
public class MySpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(MySpringBootApplication.class, args);
}
}
然后,在AyUserService类下添加新接口findByNameAndPasswordRetry,具体代码如下:
AyUser findByNameAndPasswordRetry(String name, String password);
接口findByNameAndPasswordRetry添加完成之后,在AyUserServiceImpl类下实现接口findByNameAndPasswordRetry,并在方法中故意抛出业务异常BusinessException,具体代码如下:
@Override
@Retryable(value= {BusinessException.class}, maxAttempts =5,
backoff = @Backoff(delay = 5000,multiplier =2))
public AyUser findByNameAndPasswordRetry(String name, String password) {
System.out.println("findByNameAndPas方法失败重试了!");
throw new BusinessException();
}
- @Retryable:value属性表示当出现哪些异常的时候触发重试,maxAttempts表示最大重试次数,默认为3,delay表示重试的延迟时间,multiplier表示上一次延时时间是这一次的倍数。
最后,在AyUserController类下添加控制层方法findByNameAndPasswordRetry,在该方法中调用服务层AyUserServiceImpl的方法findByNameAndPasswordRetry。具体代码如下:
RequestMapping("/findByNameAndPasswordRetry")
public String findByNameAndPasswordRetry(Model model){
AyUser ayUser = ayUserService.findByNameAndPasswordRetry("阿毅,"123456");
model.addAttribute("users", ayUser);
return "success";
}
3.3测试
代码开发完成之后,重新启动spring-boot-book-v2项目,项目运行成功后,在浏览器中输入访问地址:http://localhost:8080/ayUser/findByNameAndPasswordRetry,由于方法findByNameAndPasswordRetry会抛出BusinessException异常,故Retry重试机制会检测到,进行第2次重试。重试成功,方法执行完成,重试失败,按照配置延迟delay时间,依次进行第3次、第4次、第5次重试,直到重试成功或者达到最大重试次数,重试策略终止。我们可以在IDEA的控制台多次看到如下的打印信息:
[findByNameAndPasswordRetry]方法失败重试了!
[findByNameAndPasswordRetry]方法失败重试了!
[findByNameAndPasswordRetry]方法失败重试了!
[findByNameAndPasswordRetry]方法失败重试了!
[findByNameAndPasswordRetry]方法失败重试了!