Spring-retry

Spring-retry

Spring-retry è uno dei tanti side-project di Spring: famoso framework utilizzato per la dependency injection. Questa libreria ci permette di rilanciare metodi in maniera automatica, rendendo inoltre questa operazione trasparente al resto dell’applicazione. In questo post cercherò, tramite 3 semplici esempi, di illustrarvi le caratteristiche principali di questa libreria.

Setup

Per aggiungere spring-retry ad un progetto basta aggiungere la dipendenza nel pom.xml

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>1.1.2.RELEASE</version>
</dependency>

Se nel vostro progetto non utilizzate Maven o un altro gestore delle dipendenze potete clonare il repository git presente su Github.

Caso d’uso

Il nostro scopo sarà quello di ottenere da un servizio REST il cambio attuale Euro/Dollaro. Qualora il server rispondesse con il codice HTTP 503, rilanceremo in automatico il metodo fino a che il server non ritornerà lo status code 200. Prima di vedere un esempio reale passeremo per due esempi che produrranno un valore del cambio mock, ma che ci saranno utili per comprendere meglio quali sono le possibilità offerte dalle API. Iniziamo con il definire il nostro ApplicationContext.

@Configuration
@EnableRetry
@EnableAspectJAutoProxy(proxyTargetClass=true)
public class RetryApplicationContext {
	
	@Bean(name="dummyRateCalculator")
	public DummyExchangeRateCalculator getDummyExchangeRateCalculator() {
	      return new DummyExchangeRateCalculator();
	}
	
	@Bean(name="advancedDummyRateCalculator")
	public AdvancedDummyExchangeRateCalculator getAdvancedDummyExchangeRateCalculator() {
	      return new AdvancedDummyExchangeRateCalculator();
	}
	
	@Bean(name="realRateCalculator")
	public RealExchangeRateCalculator getRealExchangeRateCalculator() {
	      return new RealExchangeRateCalculator();
	}
	
}

L’unica cosa particolare da notare in questa classe è l’utilizzo dell’annotation @EnableRetry per abilitare spring-retry. Abbiamo poi definito 3 bean che implementano la stessa interfaccia: ExchangeRateCalculator:

public interface ExchangeRateCalculator {
    public abstract Double getCurrentRate();
}

Lanceremo i nostri esempi tramite il seguente main:

public static void main( String[] args )
    {
    	ApplicationContext context = new AnnotationConfigApplicationContext(RetryApplicationContext.class);
    	
    	ExchangeRateCalculator dummyRateCalculator = (ExchangeRateCalculator) context.getBean("dummyRateCalculator");
    	ExchangeRateCalculator advancedDummyRateCalculator = (ExchangeRateCalculator) context.getBean("advancedDummyRateCalculator");
    	ExchangeRateCalculator realRateCalculator = (ExchangeRateCalculator) context.getBean("realRateCalculator");
    	
    	System.out.println("--- dummyRateCalculator ---");
    	System.out.println("Return value " + dummyRateCalculator.getCurrentRate());
    	
    	System.out.println("--- advancedDummyRateCalculator ---");
    	System.out.println("Return value " + advancedDummyRateCalculator.getCurrentRate());
    	
    	System.out.println("--- realRateCalculator ---");
    	System.out.println("Return value " + realRateCalculator.getCurrentRate());
    }

@Retryable

Il “cuore” di spring-retry è l’annotation @Retryable, analizziamo il primo bean per capirne insieme l’utilizzo.

public class DummyExchangeRateCalculator implements ExchangeRateCalculator {
	
	private int attempts = 0;
	private static final double BASE_EXCHANGE_RATE = 1.09;
	
	@Retryable(value=RuntimeException.class)
	public Double getCurrentRate(){
		System.out.println("Calculating - Attempt " + attempts);
		attempts++;
		//Do something...
		throw new RuntimeException("Error");
	}
	
	@Recover
	public Double recover(RuntimeException e){
		System.out.println("Recovering - returning safe value");
		return BASE_EXCHANGE_RATE;
	}
}

L’ouput del metodo getCurrentRate è il seguente:

Calculating - Attempt 0
Calculating - Attempt 1
Calculating - Attempt 2
Recovering - returning safe value

Questo bean non esegue nessun tipo di operazione se non lanciare un’eccezione, ma ci può aiutare a cogliere il meccanismo alla base di spring-retry. Come possiamo vedere il funzionamento è abbastanza immediato. Ogni metodo annotato con @Retryable allo scatenarsi dell’eccezione definita nell’annotation stessa (in questo caso RuntimeException) viene rilanciato 3 volte, se anche alla terza volta il metodo fallisce viene invocato il metodo annotato con @Recover. Qualora questo metodo non venisse fornito l’eccezione verrebbe rilanciata.

Vediamo con il prossimo esempio come configurare in maniera granulare il comportamento del nostro metodo:

public class AdvancedDummyExchangeRateCalculator implements ExchangeRateCalculator {
	
	private static final double BASE_EXCHANGE_RATE = 1.09;
	private int attempts = 0;
	private SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");

	@Retryable(value=RuntimeException.class,maxAttempts=10,backoff = @Backoff(delay = 5000,multiplier=1.5))
	public Double getCurrentRate(){
		System.out.println("Calculating - Attempt " + attempts + " at " + sdf.format(new Date()));
		attempts++;
		//Do something...
		throw new RuntimeException("Error");
	}
	
	@Recover
	public Double recover(RuntimeException e){
		System.out.println("Recovering - returning safe value");
		return BASE_EXCHANGE_RATE;
	}
}

Il codice è in tutto e per tutto simile al precedente, l’unica cosa che è stata modificata è la configurazione di @Retryable. Come prima cosa abbiamo aumentato, tramite l’attributo maxAttempts, il numero di volte che il metodo verrà rilanciato. Inoltre tramite l’annotation @Backoff abbiamo impostato un delay iniziale di 5 secondi, che aumenterà del 50% ad ogni iterazione. In questo caso l’output sulla console è il seguente:

Calculating - Attempt 0 at 14:54:00
Calculating - Attempt 1 at 14:54:05
Calculating - Attempt 2 at 14:54:12
Calculating - Attempt 3 at 14:54:23
Calculating - Attempt 4 at 14:54:40
Calculating - Attempt 5 at 14:55:06
Calculating - Attempt 6 at 14:55:36
Calculating - Attempt 7 at 14:56:06
Calculating - Attempt 8 at 14:56:36
Calculating - Attempt 9 at 14:57:06
Recovering - returning safe value

Notate come il tempo che intercorre tra un tentativo e l’altro aumenti effettivamente del 50%.

unirest

Passiamo ora al terzo bean: questa volta leggeremo il dato sul cambio Euro/Dollaro da un servizio REST tramite la libreria unirest. Questa libreria, creata dal team di Mashape, rende l’interrogazione di un servizio REST un’operazione veramente banale.

public class RealExchangeRateCalculator implements ExchangeRateCalculator {
	
	private static final double BASE_EXCHANGE_RATE = 1.09;
	private int attempts = 0;
	private SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
	
	@Retryable(maxAttempts=10,value=RuntimeException.class,backoff = @Backoff(delay = 10000,multiplier=2))
	public Double getCurrentRate() {
		
		System.out.println("Calculating - Attempt " + attempts + " at " + sdf.format(new Date()));
		attempts++;
		
		try {
			HttpResponse<JsonNode> response = Unirest.get("http://rate-exchange.herokuapp.com/fetchRate")
				.queryString("from", "EUR")
				.queryString("to","USD")
				.asJson();
			
			switch (response.getStatus()) {
			case 200:
				return response.getBody().getObject().getDouble("Rate");
			case 503:
				throw new RuntimeException("Server Response: " + response.getStatus());
			default:
				throw new IllegalStateException("Server not ready");
			}
		} catch (UnirestException e) {
			throw new RuntimeException(e);
		}
	}
	
	@Recover
	public Double recover(RuntimeException e){
		System.out.println("Recovering - returning safe value");
		return BASE_EXCHANGE_RATE;
	}

}

In questo caso abbiamo anche applicato un minimo di business logic tramite eccezioni. Solo nel caso in cui il server sia temporaneamente non disponibile (cioè con codice di ritorno 503) ha senso rilanciare il metodo. In tutti gli altri casi di errore, come un 404, la situazione non cambierà nel breve periodo e quindi non ha senso continuare con l’esecuzione. Lanciando un’eccezione diversa (IllegalStateException), il meccanismo di retry non verrà eseguito.

XML

Avrete notato che tutte le configurazioni di spring-retry sono fatte tramite annotaion. Ma proprio come per il fratello maggiore Spring Framework, anche spring-retry ha la possibilità di utilizzare la configurazione XML. In questo caso si sfrutta la programmazione ad aspetti, aggiungendo ai nostri bean un Interceptor in questo modo:

<aop:config>
    <aop:pointcut id="retryable" expression="execution(* it..*RateCalculator.getCurrentRate(..))" />
    <aop:advisor pointcut-ref="retryable" advice-ref="retryAdvice" order="-1"/>
</aop:config>

<bean id="retryAdvice" class="org.springframework.batch.retry.interceptor.RetryOperationsInterceptor"/>

Cambiando le proprietà del bean “retryAdvice” possiamo configurare la strategia del retry in maniera del tutto simile a quanto fatto in precedenza con l’annotion @Retryable.

Conclusioni

Credo che ogni qualvolta abbiate a che fare con sistemi esterni di qualche tipo, spring-retry sia un must. La feature più importante, dal mio punto di vista, non è tanto la semplicità d’uso ma il fatto che per tutto il resto dell’applicazione questa operazione rimanga trasparente. I più curiosi possono dare un’occhiata al progetto su github. Alla prossima!

Francesco Strazzullo

Faccio il Front-end developer per e-xtrategy dove mi occupo di applicazioni AngularJS e mobile. In passato ho lavorato principalmente con applicazioni con stack Spring+Hibernate+JSF 2.X+Primefaces. Sono tra i collaboratori del progetto Primefaces Extensions: suite di componenti aggiuntivi ufficialmente riconosciuta da Primefaces. Sono anche uno dei fondatori del progetto MaterialPrime: una libreria JSF che segue le direttive del Material Design di Google. 

  • Marco

    Ciao bell’articolo, non lo conoscevo spring-retry, è molto interessante, appena mi capita lo butto in mezzo.