Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ recipeDependencies {
parserClasspath("org.springframework:spring-core:4.+")
parserClasspath("org.springframework:spring-core:5.+")
parserClasspath("org.springframework:spring-core:6.+")
parserClasspath("org.springframework:spring-core:7.+")

parserClasspath("org.springframework.retry:spring-retry:2.+")

parserClasspath("org.springframework:spring-test:6.+")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2026 the original author or authors.
* <p>
* Licensed under the Moderne Source Available License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://docs.moderne.io/licensing/moderne-source-available-license
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.java.spring.framework;

import lombok.Getter;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Preconditions;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.search.UsesType;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.TypeUtils;

import java.util.Collections;

public class CleanUpRetryableTypeArguments extends Recipe {

@Getter
final String displayName = "Clean up type arguments for `Retryable`";

@Getter
final String description = "Cleans up type arguments of `Retryable` to keep only the first generic parameter after migrating from `RetryCallback`.";

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(new UsesType<>("org.springframework.core.retry.Retryable", true), new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.ParameterizedType visitParameterizedType(J.ParameterizedType parameterizedType, ExecutionContext ctx) {
J.ParameterizedType p = super.visitParameterizedType(parameterizedType, ctx);
if (TypeUtils.isOfClassType(p.getType(), "org.springframework.core.retry.Retryable")) {
if (p.getTypeParameters() != null && p.getTypeParameters().size() > 1) {
p = p.withTypeParameters(Collections.singletonList(p.getTypeParameters().get(0)));
}
}
return p;
}
});
}
}
Binary file modified src/main/resources/META-INF/rewrite/classpath.tsv.gz
Binary file not shown.
42 changes: 22 additions & 20 deletions src/main/resources/META-INF/rewrite/recipes.csv

Large diffs are not rendered by default.

48 changes: 48 additions & 0 deletions src/main/resources/META-INF/rewrite/spring-framework-70.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,51 @@ recipeList:
- org.openrewrite.java.jackson.UpgradeJackson_2_3
- org.openrewrite.java.spring.kafka.UpgradeSpringKafka_4_0
- org.openrewrite.java.jspecify.MigrateFromSpringFrameworkAnnotations
- org.openrewrite.java.spring.framework.MigrateSpringRetryToResilience

---
type: specs.openrewrite.org/v1beta/recipe
name: org.openrewrite.java.spring.framework.MigrateSpringRetryToResilience
displayName: Migrate Spring Retry to Spring Resilience
description: >-
Migrate from the external `spring-retry` library (`org.springframework.retry`) to the built-in
Spring Resilience support in Spring Framework 7 (`org.springframework.core.retry`).
This recipe changes types, updates imports, and removes the `spring-retry` dependency.
tags:
- spring
- retry
- resilience
recipeList:
# Remove the external spring-retry dependency (no longer needed in Spring Framework 7)
- org.openrewrite.java.dependencies.RemoveDependency:
groupId: org.springframework.retry
artifactId: spring-retry
# Migrate RetryTemplate from spring-retry's support package to spring-core's retry package
- org.openrewrite.java.ChangeType:
oldFullyQualifiedTypeName: org.springframework.retry.support.RetryTemplate
newFullyQualifiedTypeName: org.springframework.core.retry.RetryTemplate
# Migrate RetryCallback to Retryable (the new functional interface)
- org.openrewrite.java.ChangeType:
oldFullyQualifiedTypeName: org.springframework.retry.RetryCallback
newFullyQualifiedTypeName: org.springframework.core.retry.Retryable
# Migrate RetryListener
- org.openrewrite.java.ChangeType:
oldFullyQualifiedTypeName: org.springframework.retry.RetryListener
newFullyQualifiedTypeName: org.springframework.core.retry.RetryListener
# Migrate RetryOperations interface
- org.openrewrite.java.ChangeType:
oldFullyQualifiedTypeName: org.springframework.retry.RetryOperations
newFullyQualifiedTypeName: org.springframework.core.retry.RetryOperations
# Migrate @EnableRetry to @EnableResilientMethods
- org.openrewrite.java.ChangeType:
oldFullyQualifiedTypeName: org.springframework.retry.annotation.EnableRetry
newFullyQualifiedTypeName: org.springframework.resilience.annotation.EnableResilientMethods
# Migrate @Retryable annotation
- org.openrewrite.java.ChangeType:
oldFullyQualifiedTypeName: org.springframework.retry.annotation.Retryable
newFullyQualifiedTypeName: org.springframework.resilience.annotation.Retryable
# Migrate @Recover annotation (removed in new API, but change type to keep compilation error visible)
# Users will need to manually refactor recovery logic
# Clean up type arguments of Retryable to match the new 1-parameter signature
- org.openrewrite.java.spring.framework.CleanUpRetryableTypeArguments

Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
/*
* Copyright 2026 the original author or authors.
* <p>
* Licensed under the Moderne Source Available License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://docs.moderne.io/licensing/moderne-source-available-license
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.java.spring.framework;

import org.junit.jupiter.api.Test;
import org.openrewrite.DocumentExample;
import org.openrewrite.InMemoryExecutionContext;
import org.openrewrite.java.JavaParser;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;

import static org.openrewrite.java.Assertions.java;
import static org.openrewrite.maven.Assertions.pomXml;

class MigrateSpringRetryToResilienceTest implements RewriteTest {

@Override
public void defaults(RecipeSpec spec) {
spec.recipeFromResources("org.openrewrite.java.spring.framework.MigrateSpringRetryToResilience")
.parser(JavaParser.fromJavaVersion()
.classpathFromResources(new InMemoryExecutionContext(), "spring-retry-2")
.dependsOn(
//language=java
"""
package org.springframework.core.retry;
@FunctionalInterface
public interface Retryable<R> {
R execute() throws Throwable;
}
""",
//language=java
"""
package org.springframework.core.retry;
public class RetryTemplate {
public RetryTemplate() {}
public <R> R execute(Retryable<R> retryable) throws Exception { return null; }
public <R> R invoke(Retryable<R> retryable) { return null; }
}
""",
//language=java
"""
package org.springframework.core.retry;
public interface RetryListener {
}
""",
//language=java
"""
package org.springframework.core.retry;
public interface RetryOperations {
}
""",
//language=java
"""
package org.springframework.resilience.annotation;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EnableResilientMethods {
}
""",
//language=java
"""
package org.springframework.resilience.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retryable {
}
"""
));
}

@DocumentExample
@Test
void migrateRetryTemplateImport() {
rewriteRun(
//language=java
java(
"""
import org.springframework.retry.support.RetryTemplate;

class MyService {
void doSomething() {
RetryTemplate template = new RetryTemplate();
}
}
""",
"""
import org.springframework.core.retry.RetryTemplate;

class MyService {
void doSomething() {
RetryTemplate template = new RetryTemplate();
}
}
"""
)
);
}

@Test
void migrateRetryCallbackToRetryable() {
rewriteRun(
//language=java
java(
"""
import org.springframework.retry.RetryCallback;

class MyService {
RetryCallback<String, Exception> callback;
}
""",
"""
import org.springframework.core.retry.Retryable;

class MyService {
Retryable<String> callback;
}
"""
)
);
}

@Test
void migrateRetryListener() {
rewriteRun(
//language=java
java(
"""
import org.springframework.retry.RetryListener;

class MyListener implements RetryListener {
}
""",
"""
import org.springframework.core.retry.RetryListener;

class MyListener implements RetryListener {
}
"""
)
);
}

@Test
void migrateEnableRetryAnnotation() {
rewriteRun(
//language=java
java(
"""
import org.springframework.retry.annotation.EnableRetry;

@EnableRetry
class AppConfig {
}
""",
"""
import org.springframework.resilience.annotation.EnableResilientMethods;

@EnableResilientMethods
class AppConfig {
}
"""
)
);
}

@Test
void migrateRetryableAnnotation() {
rewriteRun(
//language=java
java(
"""
import org.springframework.retry.annotation.Retryable;

class MyService {
@Retryable
String callService() {
return "result";
}
}
""",
"""
import org.springframework.resilience.annotation.Retryable;

class MyService {
@Retryable
String callService() {
return "result";
}
}
"""
)
);
}

@Test
void removeSpringRetryDependencyFromMaven() {
rewriteRun(
//language=xml
pomXml(
"""
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>2.0.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>7.0.0</version>
</dependency>
</dependencies>
</project>
""",
"""
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>7.0.0</version>
</dependency>
</dependencies>
</project>
"""
)
);
}

@Test
void noChangeWhenSpringRetryNotUsed() {
rewriteRun(
//language=java
java(
"""
class MyService {
void doSomething() {
System.out.println("no retry here");
}
}
"""
)
);
}
}
Binary file modified src/test/resources/META-INF/rewrite/classpath.tsv.gz
Binary file not shown.