确认Spring Retry中include、exclude和ExhaustedRetryException的行为
简述
-
- Spring Boot + Spring Retry によるサンプルプログラムで挙動を確認する
@Retryable アノテーションにてリトライ対象の例外を include で指定する
@Retryable アノテーションにてリトライ対象ではない例外を exclude で指定する
@Recover で捕捉できない例外がある場合に ExhaustedRetryException が発生するのを確認する
样本程序的源代码列表
├── pom.xml
└── src
└── main
└── java
└── info
└── maigo
└── lab
└── sample
└── retry
└── exhausted
├── HogeApplication.java
├── HogeController.java
├── HogeException.java
├── HogeHogeException.java
└── HogeService.java
确认操作环境
-
- macOS Mojave
-
- OpenJDK 11.0.2
-
- Spring Boot 2.2.0 M4
- Spring Retry 1.2.4
Maven 的构建文件 pom.xml
这次使用 Maven 来执行构建。
pom.xml 是基于 Spring Initializr 生成的。
为了使用 Spring Retry,在 dependencies 中添加 spring-retry。
另外,在运行时需要 AOP 类,所以添加了 spring-boot-starter-aop。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.M4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>info.maigo.lab</groupId>
<artifactId>sample.retry.exhausted</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>sample.retry.exhausted</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.retry/spring-retry -->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.2.4.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</pluginRepository>
</pluginRepositories>
</project>
构建并启动服务器
使用mvn package命令生成JAR文件。
$ mvn package
使用Java命令指定JAR文件启动服务器。
$ java -jar target/sample.retry.exhausted-0.0.1-SNAPSHOT.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.2.0.M4)
源代码
HogeApplication.java 的描述
使用Spring Boot来创建启动类。为了使用Spring Retry,指定了@EnableRetry注解。
package info.maigo.lab.sample.retry.exhausted;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.retry.annotation.EnableRetry;
@SpringBootApplication
@EnableRetry
public class HogeApplication {
public static void main(String[] args) {
SpringApplication.run(HogeApplication.class, args);
}
}
HogeController.java的翻译如下:
接收HTTP请求并返回响应的Controller类。
如果发生异常,则在指定了@ExceptionHandler注解的方法中进行处理。
package info.maigo.lab.sample.retry.exhausted;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HogeController {
@Autowired
private HogeService hogeService;
@RequestMapping("/")
public Map<String, Object> index(@RequestParam(name = "name", defaultValue = "java.lang.RuntimeException") String name) throws Exception {
return new HashMap<String, Object>() {
{
put("result", hogeService.invoke(name));
}
};
}
@ExceptionHandler(Exception.class)
public Map<String, Object> handleException(HttpServletRequest req, Exception e) {
return new HashMap<String, Object>() {
{
put("handleException", e.getClass().getName() + " / " + e.getMessage());
}
};
}
}
HogeException.java 经行了重写。
只继承了Exception类的异常类。
package info.maigo.lab.sample.retry.exhausted;
public class HogeException extends Exception {
}
HogeHoge异常.java
继承了HogeException的异常类。
package info.maigo.lab.sample.retry.exhausted;
public class HogeHogeException extends HogeException {
}
HogeService.java文件
服务类。
invoke方法根据指定的字符串生成异常对象并抛出。
invoke方法使用@Retryable注解。通过include指定重试的异常类型,通过exclude指定不重试的异常类型。通过maxAttempts指定重试次数,通过backoff指定重试的等待时间。
使用@Recover注解指定的方法将在尝试指定次数后,即使发生异常也会被调用(如果异常类型匹配,则调用此方法,即使它不是可重试的异常)。
package info.maigo.lab.sample.retry.exhausted;
import java.lang.reflect.Constructor;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.retry.annotation.Recover;
@Service
public class HogeService {
@Retryable(
include = {HogeException.class},
exclude = {HogeHogeException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000))
public String invoke(String name) throws Exception {
System.out.println("HogeService#invoke: " + name);
Class cls = Class.forName(name);
Constructor cnst = cls.getDeclaredConstructor();
Exception e = (Exception) cnst.newInstance();
throw e;
}
@Recover
public String recover(HogeException e, String name) {
System.out.println("HogeService#recover: " + name);
return "HogeService#recover: " + e.getClass().getName();
}
}
执行示例
引发 HogeException 并进行重试
当访问启动的服务器时,使用curl命令将输出JSON。指定引发HogeException异常。通过recover方法返回HogeException的字符表示。
$ curl http://localhost:8080/?name=info.maigo.lab.sample.retry.exhausted.HogeException
{"result":"HogeService#recover: info.maigo.lab.sample.retry.exhausted.HogeException"}
查看服务器端的标准输出日志。
可以看到在执行重试并调用 invoke 方法3次后,recover 方法被调用了。
HogeService#invoke: info.maigo.lab.sample.retry.exhausted.HogeException
HogeService#invoke: info.maigo.lab.sample.retry.exhausted.HogeException
HogeService#invoke: info.maigo.lab.sample.retry.exhausted.HogeException
HogeService#recover: info.maigo.lab.sample.retry.exhausted.HogeException
由于HogeException在@Retryable注解的include中被指定,因此会执行重试处理。如果在最后一次重试中发生异常,则会调用被指定为@Recover注解的方法。
不发生HogeHogeException并不重试
使用 curl 指定出现 HogeHogeException 并访问。
在 recover 方法中,返回了 HogeHogeException 的字符串表示。
$ curl http://localhost:8080/?name=info.maigo.lab.sample.retry.exhausted.HogeHogeException
{"result":"HogeService#recover: info.maigo.lab.sample.retry.exhausted.HogeHogeException"}
查看服务器端的标准输出日志。
在调用一次invoke方法后,可以看到调用了recover方法。
HogeService#invoke: info.maigo.lab.sample.retry.exhausted.HogeHogeException
HogeService#recover: info.maigo.lab.sample.retry.exhausted.HogeHogeException
由于HogeHogeException在@Retryable注释的exclude中被指定,所以不会进行重试处理。
调用invoke方法一次会发生HogeHogeException,recover方法会被调用并返回结果字符串。
此外,由于HogeHogeException是在@Retryable注释的include中指定的HogeException的子类,所以如果未在exclude中指定HogeHogeException,则会成为重试目标。
当检测到@Recover无法捕获的异常时,将触发ExhaustedRetryException。
使用curl指定发生java.lang.Exception,并无法通过recover方法捕捉到,导致抛出org.springframework.retry.ExhaustedRetryException异常。
$ curl http://localhost:8080/?name=java.lang.Exception
{"handleException":"org.springframework.retry.ExhaustedRetryException / Cannot locate recovery method; nested exception is java.lang.Exception"}
查看服务器端的标准输出日志。
可以看到 invoke 方法只被调用了一次,而 recover 方法没有被调用。
HogeService#invoke: java.lang.Exception
由于 java.lang.Exception 没有在 @Retryable 注解的 include 中指定,因此不会进行重试处理。另外,指定的 @Recover 注解方法中无法捕获的异常类型将引发 ExhaustedRetryException。在这种情况下,可以通过 ExhaustedRetryException 类的 getCause 方法获取导致异常的 java.lang.Exception 对象。
参考文献
-
- GitHub – spring-projects/spring-retry
-
- org.springframework.retry:spring-retry:1.2.4.RELEASE API Doc :: Javadoc.IO
-
- Retryable (Spring Retry 1.2.4.RELEASE API)
-
- ExhaustedRetryException (Spring Retry 1.2.4.RELEASE API)
-
- @Retryable exclude option does not work as expected · Issue #47 · spring-projects/spring-retry · GitHub
- Spring Retry を利用して宣言型のリトライ処理を実装する – Qiita