[JAVA] Implement declarative retry processing using Spring Retry

Overview

--Write a sample program and check the behavior of Spring Boot + Spring Retry --Spring Retry eliminates the need to write code to determine and control processing retries --Describe the number of trials and interval time in @Retryable annotation

Source code list of sample program

├── build.gradle
├── settings.gradle
└── src
    └── main
        └── java
            └── info
                └── maigo
                    └── lab
                        └── sample
                            └── springretry
                                ├── Application.java
                                ├── HogeController.java
                                ├── HogeService.java
                                ├── KotsuException.java
                                ├── PonException.java
                                ├── PonKotsuRepository.java
                                └── PonKotsuRetryListener.java

Sample program overview

--Return a message in JSON for HTTP request. --PonKotsuRepository is a pompous repository. It returns a message with a certain probability, but most fail. --HogeService will call PonKotsuRepository again and retry if PonKotsuRepository fails. --Write the number of trials and interval time in the @Retryable annotation. --Give up after failing a certain number of times. --PonKotsuRepository returns PonException or KotsuException on failure. --If the last failure is PonException, handle it with the specified method with @Recover annotation of Spring Retry. --If the last failure is KotsuException, handle it with the specified method with @ExceptionHandler annotation of Spring Retry. --A class that implements the RetryListener interface, and outputs the status of retry processing to the standard output.

Operation check environment

Gradle build file

build.gradle

Add implementation'org.springframework.retry: spring-retry: 1.2.4.RELEASE' to dependencies to use Spring Retry. Also, add runtime'org.springframework.boot: spring-boot-starter-aop' because you need the AOP class at runtime. How to specify dependent libraries can be found in GitHub -spring \ -projects / spring \ -retry.


plugins {

  // The Java Plugin
  // https://docs.gradle.org/current/userguide/java_plugin.html
  id 'java'

  // Gradle - Plugin: org.springframework.boot
  // https://plugins.gradle.org/plugin/org.springframework.boot
  id 'org.springframework.boot' version '2.2.0.M4'

  // Gradle - Plugin: io.spring.dependency-management
  /// https://plugins.gradle.org/plugin/io.spring.dependency-management
  id 'io.spring.dependency-management' version '1.0.8.RELEASE'  
}

group = 'info.maigo.lab'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 11

repositories {
  mavenCentral()
  maven { url 'https://repo.spring.io/snapshot' }
  maven { url 'https://repo.spring.io/milestone' }
}

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-web:2.2.0.M4'
  implementation 'org.springframework.retry:spring-retry:1.2.4.RELEASE'
  runtime 'org.springframework.boot:spring-boot-starter-aop'
}

settings.gradle


pluginManagement {
  repositories {
    maven { url 'https://repo.spring.io/snapshot' }
    maven { url 'https://repo.spring.io/milestone' }
    gradlePluginPortal()
  }
  resolutionStrategy {
    eachPlugin {
      if (requested.id.id == 'org.springframework.boot') {
        useModule("org.springframework.boot:spring-boot-gradle-plugin:${requested.version}")
      }
    }
  }
}

rootProject.name = 'sample.springretry'

Build and start server

Generate a JAR file with the build task in Gradle's Java plugin.

$ gradle build

BUILD SUCCESSFUL in 3s
3 actionable tasks: 3 executed

Execute the generated jar file with the java command. This will start the web server with Spring Boot.

$ java -jar build/libs/sample.springretry-0.0.1-SNAPSHOT.jar

Source code

Application.java

Boot entry class by Spring Boot. The @EnableRetry annotation is specified to use Spring Retry.

package info.maigo.lab.sample.springretry;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.retry.annotation.EnableRetry;

@SpringBootApplication
@EnableRetry
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

HogeController.java

A Controller class that receives an HTTP request and returns a response. When an exception occurs, the result of processing by the method with @ExceptionHandler annotation is returned.

package info.maigo.lab.sample.springretry;

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.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HogeController {

    @Autowired
    private HogeService hogeService;

    @RequestMapping("/{message}")
    public Map<String, Object> index(@PathVariable("message") String message) {
        return hogeService.send(message);
    }

    @ExceptionHandler(Exception.class)
    public Map<String, Object> handleException(HttpServletRequest req, Exception e) {
        return new HashMap<String, Object>() {
            {
                put("handleException", e.getMessage());
            }
        };
    }
}

PonKotsuRepository.java

A sneaky Repository class. Raise PonException or KotsuException with a certain probability.

package info.maigo.lab.sample.springretry;

import java.util.Random;
import org.springframework.stereotype.Repository;

/**
 *A pompous repository.
 */
@Repository
public class PonKotsuRepository {

    private static final Random r = new Random(System.currentTimeMillis());

    /**
     *Hopefully it returns a message, but almost fails.
     * @param message
     * @return message
     * @throws PonException PonException
     * @throws KotsuException Tips Exception
     */
    public String send(String message) {
        int i = r.nextInt(5);
        if (i < 2) {
            System.out.println("PonKotsuRepository: Throws PonException.");
            throw new PonException();
        } else if (i < 4) {
            System.out.println("PonKotsuRepository: Throws KotsuException.");
            throw new KotsuException();
        } else {
            System.out.println("PonKotsuRepository: Returns a message.");
            return "1/Message that 3 does not convey: " + message;
        }
    }
}

HogeService.java

A Service class that uses the sneaky Repository class. The @Retryable annotation is used to specify the number of retries and the waiting time until retry. The method with @Recover annotation is called when an exception occurs even after trying the specified number of times. This method takes the exception that occurred in the last attempt as the first argument, and takes the argument of the method with @Retryable annotation after the second argument. This time, we have prepared two methods with @Recover annotation. One is used when PonException occurs and returns the result as a string. The other is used when a KotsuException occurs and throws a RuntimeException. Annotation information used in Spring Retry is Spring \ -Retry -Simple and essential code-free retry processing framework and [org \ .springframework \ .retry \ .annotation \ (Spring Retry 1 \ .2 \ .4 \ .RELEASE API )](https://static.javadoc.io/org.springframework.retry/spring-retry/1.2. 4.RELEASE/index.html?org/springframework/retry/annotation/package-summary.html) will be helpful.

package info.maigo.lab.sample.springretry;

import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
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 {

    @Autowired
    private PonKotsuRepository repository;

    @Retryable(value = {PonException.class, KotsuException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000))
    public Map<String, Object> send(String message) {
        String result = repository.send(message);
        return new HashMap<String, Object>() {
            {
                put("result", result);
            }
        };
    }

    @Recover
    public Map<String, Object> recoverSend(PonException e, String message) {
        System.out.println("recoverSend: PonException");
        return new HashMap<String, Object>() {
            {
                put("error", "The last thing that happened was PonException");
            }
        };
    }

    @Recover
    public Map<String, Object> recoverSend(KotsuException e, String message) {
        System.out.println("recoverSend: KotsuException");
        throw new RuntimeException("The last thing that happened was KotsuException");
    }
}

PonException.java

Pong exception. It just inherits RuntimeException.

package info.maigo.lab.sample.springretry;

public class PonException extends RuntimeException {
}

KotsuException.java

Tips exception. It just inherits RuntimeException.

package info.maigo.lab.sample.springretry;

public class KotsuException extends RuntimeException {
}

PonKotsuRetryListener.java

A class that implements the RetryListener interface. It consists of callbacks that are called during the retry process. In this implementation, the status of retry processing is output to the standard output.

package info.maigo.lab.sample.springretry;

import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.listener.RetryListenerSupport;
import org.springframework.stereotype.Component;

@Component
public class PonKotsuRetryListener extends RetryListenerSupport {

    @Override
    public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
        System.out.println("PonKotsuRetryListener#close: " + getThrowableString(throwable));
        super.close(context, callback, throwable);
    }

    @Override
    public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
        System.out.println("PonKotsuRetryListener#onError: " + getThrowableString(throwable));
        super.onError(context, callback, throwable);
    }

    @Override
    public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
        System.out.println("PonKotsuRetryListener#open");
        return super.open(context, callback);
    }

    private static String getThrowableString(Throwable throwable) {
        return throwable == null ? "null" : throwable.getClass().getSimpleName();
    }
}

Execution example

Execution example 1

JSON is output when accessing the server started by curl.

$ curl http://localhost:8080/hello
{"result":"1/Message that 3 does not convey: hello"}

View the standard output on the server side. The message can be returned normally by retrying after the Pon exception occurs.

PonKotsuRetryListener#open
PonKotsuRepository: Throws PonException.
PonKotsuRetryListener#onError: PonException
PonKotsuRepository: Returns a message.
PonKotsuRetryListener#close: null

Execution example 2

$ curl http://localhost:8080/foo
{"error":"The last thing that happened was PonException"}

View the standard output on the server side. It occurs in the order of Pon exception → Kotsu exception → Pon exception. For the last Pon exception, the result is returned by PonKotsuRepository # recoverSend.

PonKotsuRetryListener#open
PonKotsuRepository: Throws PonException.
PonKotsuRetryListener#onError: PonException
PonKotsuRepository: Throws KotsuException.
PonKotsuRetryListener#onError: KotsuException
PonKotsuRepository: Throws PonException.
PonKotsuRetryListener#onError: PonException
recoverSend: PonException
PonKotsuRetryListener#close: PonException

Execution example 3

$ curl http://localhost:8080/bar
{"handleException":"The last thing that happened was KotsuException"}

View the standard output on the server side. It occurs in the order of Pon exception → Kotsu exception → Kotsu exception. For the last Kotsu exception, HogeController # handleException returns the result.

PonKotsuRetryListener#open
PonKotsuRepository: Throws PonException.
PonKotsuRetryListener#onError: PonException
PonKotsuRepository: Throws KotsuException.
PonKotsuRetryListener#onError: KotsuException
PonKotsuRepository: Throws KotsuException.
PonKotsuRetryListener#onError: KotsuException
recoverSend: KotsuException
PonKotsuRetryListener#close: KotsuException

Reference material

Recommended Posts

Implement declarative retry processing using Spring Retry
Asynchronous processing with Spring Boot using @Async
Spring Retry Troubleshooting
Implement common processing using method swizzling on iOS
Implement ProgressBar using library
[Swift] Implement swipe processing
[Ruby] Implement error handling / iteration processing using while method
Try using Spring JDBC
Execution of initial processing using Spring Boot Command Line Runner
Implement GraphQL with Spring Boot
Spring with Kotorin --6 Asynchronous processing
Data processing using Apache Flink
Implement category function using ancestory
Using Mapper with Java (Spring)
Try using Spring Boot Security
[Swift] Asynchronous processing using PromiseKit
[Processing] Try using GT Force.
Csv output processing using super-csv
[Swift] Implement UITableView using xib