确认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
广告
将在 10 秒后关闭
bannerAds