Tìm hiểu Spring Retry
spring
66
spring retry
1
spring boot
66
White

Nguyễn Tuấn Anh viết ngày 21/10/2019

1. Spring Retry là gì?

Spring Retry là một project nhỏ được tách ra từ project Spring Batch. Nếu trong quá trình thực hiện (attempt) một job mà có exception được ném ra khiến job đó bị thất bại, Spring Retry sẽ cho phép chúng ta retry (thử lại) việc thực hiện job đó tối đa N - 1 lần cho tới khi thành công. Nếu sau N - 1 lần retry mà job đó vẫn thất bại, Spring Retry sẽ tiến hành recover (khôi phục).

Khi cấu hình Spring Retry, chúng ta thường quan tâm tới 2 policy sau:

  1. Retry policy: quyết định việc retry job
  2. Class của các exception khiến job phải retry ❶.
  3. Số lần tối đa thực hiện job (maxAttempts), tức là con số N ở bên trên ❷.
  4. Backoff policy: xác định khoảng thời gian (backoff) giữa các lần attempt ❸.

Để áp dụng Spring Retry, chúng ta có thể sử dụng 1 trong 2 cách sau:

  1. Sử dụng annotation @Retryable
  2. Sử dụng RetryTemplate

2. Dependency

Do Spring Retry có sử dụng đến AOP, nên ngoài dependency spring-retry, chúng ta cần thêm dependency spring-boot-starter-aop:

<dependency>
  <groupId>org.springframework.retry</groupId>
  <artifactId>spring-retry</artifactId>
  <version>1.2.4.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

3. Enable Spring Retry

Để enable Spring Retry, chúng ta khai báo annotation @EnableRetry bên dưới @Configuration:

import org.springframework.retry.annotation.EnableRetry;

@Configuration
@EnableRetry
public class AppConfig {
    ...
}

4. Spring Retry với @Retryable

Đây là cách đơn giản nhất để chúng ta áp dụng Spring Retry.

Với 2 tác vụ retry và recover, chúng ta sẽ sử dụng 2 method-level annotation tương ứng là @Retryable@Recover:

import lombok.extern.slf4j.Slf4j;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Slf4j
@Component
public class MyJob {

    private int retryCount = 0;

    @Retryable(
            value = {IOException.class},
            maxAttempts = 4,
            backoff = @Backoff(delay = 2000L)
    )
    public void doStuff(int foo, int bar) throws IOException {
        log.info("Attempting at {} time(s)", ++retryCount);
        if (foo % bar != 0) {
            throw new IOException();
        }
    }

    @Recover
    public void recover() {
        log.info("Recovering");
    }
}

Với @Retryable:

  • value: tương ứng với cấu hình ❶. Mặc định là rỗng.
  • maxAttempts: tương ứng với cấu hình ❷. Mặc định bằng 3.
  • delay: tương ứng với cấu hình ❸. Đơn vị là millisecond. Mặc định bằng 1000L.

Phương thức được đánh dấu bởi @Recover sẽ được gọi sau khi phương thức được đánh dấu bởi @Retryable đã thực hiện maxAttempts lần.

Test:

@SpringBootTest
class ApplicationTests {

    @Autowired
    private MyJob myJob;

    @Test
    public void test() {
        try {
            myJob.doStuff(5, 3);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

Kết quả:

2019-10-20 10:33:09.433  : Attempting at 1 time(s)
2019-10-20 10:33:11.439  : Attempting at 2 time(s)
2019-10-20 10:33:13.440  : Attempting at 3 time(s)
2019-10-20 10:33:15.441  : Attempting at 4 time(s)
2019-10-20 10:33:15.442  : Recovering

5. Spring Retry với RetryTemplate

RetryTemplate là implementation của interface RetryOperations:

public interface RetryOperations {

    <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback) throws E;

    <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback) throws E;

    <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RetryState retryState) throws E, ExhaustedRetryException;

    <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback, RetryState retryState)
            throws E;
}

Ở đây, RetryCallback là một callback interface - nhiệm vụ của chúng ta là phải implement logic của retry hoặc recover. RetryTemplate sẽ dùng policy hiện tại để tạo một RetryContext và truyền RetryContext này vào bên trong RetryCallback tại mỗi lần attempt. Với RetryContext, chúng ta có thể lấy được trực tiếp số lần attempt hiện tại thông qua phương thức getRetryCount(), thay vì phải tạo một biến retryCount như trong phần trước.

Với RestTemplate:

  • ❶: mặc định là Exception.
  • ❷: mặc định bằng 3.
  • ❸: mặc định không có backoff giữa các lần attempt.

Với retry policy, chúng ta có thể custom thông qua các implementation của RetryPolicy như SimpleRetryPolicy, TimeoutRetryPolicy, ...

Với backoff policy, chúng ta có thể custom thông qua các implementation của BackOffPolicy như FixedBackOffPolicy, ...

...

Ở đây, mình sẽ tạo một bean RetryTemplate như sau:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.RetryPolicy;
import org.springframework.retry.backoff.BackOffPolicy;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;

import java.io.IOException;
import java.util.Collections;
import java.util.Map;

@Configuration
public class RetryConfiguration {

    @Bean
    public RetryTemplate retryTemplate() {
        RetryTemplate template = new RetryTemplate();
        template.setRetryPolicy(retryPolicy());
        template.setBackOffPolicy(backOffPolicy());
        return template;
    }

    private RetryPolicy retryPolicy() {
        Map<Class<? extends Throwable>, Boolean> retryableExceptions = Collections.singletonMap(IOException.class, true);
        return new SimpleRetryPolicy(4, retryableExceptions);
    }

    private BackOffPolicy backOffPolicy() {
        FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
        backOffPolicy.setBackOffPeriod(2000L);
        return backOffPolicy;
    }

}

RetryTemplate cung cấp phương thức execute() để thực hiện retry và recover:

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Slf4j
@Component
public class MyJob {

    @Autowired
    private RetryTemplate retryTemplate;

    public void doStuff(int foo, int bar) throws IOException {
        retryTemplate.execute(
            retryCallback -> {
                log.info("Attempting at {} time(s)", retryCallback.getRetryCount() + 1);
                if (foo % bar != 0) {
                    throw new IOException();
                }
                return null;
            },
            recoveryCallback -> {
                log.info("Recovering");
                return null;
            }
        );
    }
}

Test:

@SpringBootTest
class ApplicationTests {

    @Autowired
    private MyJob myJob;

    @Test
    public void test() {
        try {
            myJob.doStuff(5, 3);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

Kết quả:

2019-10-20 10:41:50.236  : Attempting at 1 time(s)
2019-10-20 10:41:52.240  : Attempting at 2 time(s)
2019-10-20 10:41:54.240  : Attempting at 3 time(s)
2019-10-20 10:41:56.240  : Attempting at 4 time(s)
2019-10-20 10:41:56.240  : Recovering

RetryListener

RetryListener là một interface được dùng để bổ sung thêm các callback:

public interface RetryListener {

    // Được gọi trước lần attempt đầu tiên
    <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback);

    // Được gọi sau khi recover
    <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable);

    // Được gọi sau mỗi lần attempt thất bại
    <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable);
}

Chúng ta sẽ cấu hình RetryListener thông qua implementation của nó là RetryListenerSupport:

CustomRetryListener.java

import lombok.extern.slf4j.Slf4j;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.listener.RetryListenerSupport;

@Slf4j
public class CustomRetryListener extends RetryListenerSupport {

    @Override
    public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
        log.info("Open callback");
        return true;
    }

    @Override
    public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
        log.info("Close callback");
    }

    @Override
    public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
        log.info("Error callback");
    }
}

RetryConfiguration.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.RetryListener;
import org.springframework.retry.RetryPolicy;
import org.springframework.retry.backoff.BackOffPolicy;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;

import java.io.IOException;
import java.util.Collections;
import java.util.Map;

@Configuration
public class RetryConfiguration {

    @Bean
    public RetryTemplate retryTemplate() {
        RetryTemplate template = new RetryTemplate();
        template.setRetryPolicy(retryPolicy());
        template.setBackOffPolicy(backOffPolicy());
        template.setListeners(new RetryListener[]{new CustomRetryListener()});
        return template;
    }

    private RetryPolicy retryPolicy() {
        Map<Class<? extends Throwable>, Boolean> retryableExceptions = Collections.singletonMap(IOException.class, true);
        return new SimpleRetryPolicy(4, retryableExceptions);
    }

    private BackOffPolicy backOffPolicy() {
        FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
        backOffPolicy.setBackOffPeriod(2000L);
        return backOffPolicy;
    }

}

Kết quả lúc này:

2019-10-20 16:25:06.435  : Open callback
2019-10-20 16:25:06.439  : Attempting at 1 time(s)
2019-10-20 16:25:06.439  : Error callback
2019-10-20 16:25:08.440  : Attempting at 2 time(s)
2019-10-20 16:25:08.441  : Error callback
2019-10-20 16:25:10.441  : Attempting at 3 time(s)
2019-10-20 16:25:10.441  : Error callback
2019-10-20 16:25:12.441  : Attempting at 4 time(s)
2019-10-20 16:25:12.441  : Error callback
2019-10-20 16:25:12.442  : Recovering
2019-10-20 16:25:12.442  : Close callback

Tài liệu tham khảo

https://github.com/spring-projects/spring-retry

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Nguyễn Tuấn Anh

27 bài viết.
206 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
49 41
MyContact là một ứng dụng mà mình thường viết mỗi khi học một ngôn ngữ hay công nghệ mới. MyContact chỉ là một ứng dụng CRUD đơn giản, cho phép ngư...
Nguyễn Tuấn Anh viết gần 3 năm trước
49 41
White
27 14
Hướng dẫn lập trình Spring Security Trong bài viết lần này, mình sẽ giúp các bạn bước đầu tìm hiểu (Link) thông qua xây dựng các chức năng: Đăng ...
Nguyễn Tuấn Anh viết gần 3 năm trước
27 14
White
16 1
Giới thiệu Spring Framework Trong bài viết này, mình sẽ giới thiệu cho các bạn về một trong những Java EE framework rất nổi bật và phổ biến hiện n...
Nguyễn Tuấn Anh viết gần 3 năm trước
16 1
Bài viết liên quan
White
2 0
I used Spring boot, Hibernate few times back then at University, I'v started using it again recently. In this (Link), I want to check how Spring J...
Rey viết 10 tháng trước
2 0
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
27 bài viết.
206 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!