检查Spring Retry的include,exclude,ExhaustedRetryException的行为


概述

  • 通过Spring Boot Spring Retry使用示例程序检查行为
  • 指定包含在@Retryable批注中的重试异常

  • 使用@Retryable注释(带有排除项)指定不是重试目标的异常

  • 确认存在@Recover无法捕获的异常时,发生ExhaustedRetryException

示例程序

的源代码列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
├── 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
  • 春季重试1.2.4

Maven构建文件pom.xml

这次,使用Maven运行构建。
pom.xml基于Spring Initializr生成的那个。
将spring-retry添加到依赖项以使用Spring Retry。
另外,添加spring-boot-starter-aop,因为在运行时需要AOP类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<?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文件。

1
$ mvn package

通过使用

java命令指定JAR文件来启动服务器。

1
2
3
4
5
6
7
8
9
$ java -jar target/sample.retry.exhausted-0.0.1-SNAPSHOT.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::             (v2.2.0.M4)

源代码

HogeApplication.java

Spring Boot启动入口类。
指定@EnableRetry批注以使用Spring Retry。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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请求并返回响应的控制器类。
发生异常时,请使用指定@ExceptionHandler批注的方法对其进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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

仅从

异常继承的异常类。

1
2
3
4
package info.maigo.lab.sample.retry.exhausted;

public class HogeException extends Exception {
}

HogeHogeException.java

从HogeException继承的异常类。

1
2
3
4
package info.maigo.lab.sample.retry.exhausted;

public class HogeHogeException extends HogeException {
}

HogeService.java

服务类别。
在invoke方法中,将基于指定的字符串创建并抛出异常对象。
@Retryable批注在invoke方法中指定。包含指定要重试的异常类型,而排除指定不重试的异常类型。使用maxAttempts指定重试次数。指定等待直到重试退避的等待时间。
即使尝试指定的次数后发生异常,也会调用带有@Recover批注的方法(即使该异常不是重试的目标,但如果与类型匹配,也会调用此方法)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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。
恢复方法返回HogeException的字符串表示形式。

1
2
$ curl http://localhost:8080/?name=info.maigo.lab.sample.retry.exhausted.HogeException
{"result":"HogeService#recover: info.maigo.lab.sample.retry.exhausted.HogeException"}

在服务器端查看标准输出日志。
您可以看到执行重试之后将调用restore方法,并且调用方法被调用了3次。

1
2
3
4
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并重试

指定发生HogeHogeException并使用curl访问。
恢复方法返回HogeHogeException的字符串表示形式。

1
2
$ curl http://localhost:8080/?name=info.maigo.lab.sample.retry.exhausted.HogeHogeException
{"result":"HogeService#recover: info.maigo.lab.sample.retry.exhausted.HogeHogeException"}

在服务器端查看标准输出日志。
您可以看到在invoke方法仅被调用一次之后,recover方法被调用了。

1
2
HogeService#invoke: info.maigo.lab.sample.retry.exhausted.HogeHogeException
HogeService#recover: info.maigo.lab.sample.retry.exhausted.HogeHogeException

HogeHogeException在@Retryable注释的排除中指定,因此不执行重试处理。
我一次调用了invoke方法,并获得了HogeHogeException,然后调用了restore方法,并返回了结果字符串。
另外,HogeHogeException是@Retryable注释的include中指定的HogeException的子类,因此,如果未在exclude中指定HogeHogeException,则将重试。

@Recover引发无法捕获的异常时,将发生ExhaustedRetryException

通过指定

java.lang.Exception来进行curl访问。
抛出org.springframework.retry.ExhaustedRetryException异常,而没有被restore方法捕获。

1
2
$ curl http://localhost:8080/?name=java.lang.Exception
{"handleException":"org.springframework.retry.ExhaustedRetryException / Cannot locate recovery method; nested exception is java.lang.Exception"}

在服务器端查看标准输出日志。
您可以看到invoke方法仅被调用一次。恢复方法尚未被调用。

1
HogeService#invoke: java.lang.Exception

java.lang.Exception未在@Retryable注释的include中指定,因此不执行重试处理。
另外,抛出ExhaustedRetryException,因为@Recover注释不是指定方法可以捕获的异常类型。
在这种情况下,可以使用ExhaustedRetryException类的getCause方法获取导致它的异常java.lang.Exception的对象。

参考

  • GitHub --spring-projects / spring-retry
  • org.springframework.retry:spring-retry:1.2.4.RELEASE API Doc :: Javadoc.IO
  • 可重试(Spring Retry 1.2.4.RELEASE API)
  • ExhaustedRetryException(Spring Retry 1.2.4.RELEASE API)
  • @Retryable exclude选项无法按预期运行·问题#47·spring-projects / spring-retry·GitHub
  • 使用Spring Retry --Qiita实现声明性重试处理