From 78803d0aaaaba60b5db9f653e93464d956b8ef47 Mon Sep 17 00:00:00 2001 From: nurdy-kim Date: Thu, 21 Aug 2025 10:08:38 +0900 Subject: [PATCH 1/6] WIP: reactor propagation --- .../boot3/UseReactorContextPropagation.java | 59 +++++++++++++++ .../UseReactorContextPropagationTest.java | 74 +++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 src/main/java/org/openrewrite/java/spring/boot3/UseReactorContextPropagation.java create mode 100644 src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/UseReactorContextPropagationTest.java diff --git a/src/main/java/org/openrewrite/java/spring/boot3/UseReactorContextPropagation.java b/src/main/java/org/openrewrite/java/spring/boot3/UseReactorContextPropagation.java new file mode 100644 index 000000000..a3325bf82 --- /dev/null +++ b/src/main/java/org/openrewrite/java/spring/boot3/UseReactorContextPropagation.java @@ -0,0 +1,59 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * 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 + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * 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.boot3; + +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.MethodMatcher; +import org.openrewrite.java.search.UsesType; +import org.openrewrite.java.spring.AddSpringProperty; +import org.openrewrite.java.tree.J; + +public class UseReactorContextPropagation extends Recipe { + @Override + public String getDisplayName() { + return "Use `spring.reactor.context-propagation` property"; + } + + @Override + public String getDescription() { + return "Replace `Hooks.enableAutomaticContextPropagation()` with `spring.reactor.context-propagation=true`."; + } + + private static final String SPRING_BOOT_APPLICATION = "org.springframework.boot.autoconfigure.SpringBootApplication"; + private static final MethodMatcher MATCHER = new MethodMatcher("reactor.hooks.Hooks enableAutomaticContextPropagation()"); + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check( + new UsesType<>(SPRING_BOOT_APPLICATION, true), + new JavaIsoVisitor(){ + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { + J.MethodInvocation mi = (J.MethodInvocation) super.visitMethodInvocation(method, ctx); + if (MATCHER.matches(mi)) { + doAfterVisit(new AddSpringProperty("spring.reactor.context-propagation", "true", null, null).getVisitor()); + return null; + } + return mi; + } + } + ); + } +} diff --git a/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/UseReactorContextPropagationTest.java b/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/UseReactorContextPropagationTest.java new file mode 100644 index 000000000..1f15c1fab --- /dev/null +++ b/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/UseReactorContextPropagationTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * 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 + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * 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.boot3; + +import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.properties.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +public class UseReactorContextPropagationTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new UseReactorContextPropagation()) + .parser(JavaParser.fromJavaVersion().classpath("spring-boot")); + } + + @DocumentExample + @Test + void replaceMethodCallWithProperty() { + rewriteRun( + //language=java + spec -> spec.recipeFromResources("org.openrewrite.java.spring.boot3.UseReactorContextPropagation"), + java( + """ + import reactor.core.publisher.Hooks; + import org.springframework.boot.SpringApplication; + import org.springframework.boot.autoconfigure.SpringBootApplication; + + @SpringBootApplication + public class MyApp{ + public static void main(String[] args) { + Hooks.enableAutomaticContextPropagation(); + SpringApplication.run(MyApplication.class, args); + } + } + """, + """ + import reactor.core.publisher.Hooks; + import org.springframework.boot.SpringApplication; + import org.springframework.boot.autoconfigure.SpringBootApplication; + + @SpringBootApplication + public class MyApp { + public static void main(String[] args) { + SpringApplication.run(MyApplication.class, args); + } + } + """ + ), + properties( + "", + "spring.reactor.context-propagation=true" + ) + ); + } +} From 8837cb85aa49e316678d71d71924484af5617124 Mon Sep 17 00:00:00 2001 From: nurdy-kim Date: Sat, 23 Aug 2025 00:47:27 +0900 Subject: [PATCH 2/6] UseReactContextPropagation --- .../boot3/UseReactorContextPropagation.java | 14 ++- .../UseReactorContextPropagationTest.java | 110 +++++++++++------- 2 files changed, 77 insertions(+), 47 deletions(-) diff --git a/src/main/java/org/openrewrite/java/spring/boot3/UseReactorContextPropagation.java b/src/main/java/org/openrewrite/java/spring/boot3/UseReactorContextPropagation.java index a3325bf82..705f83819 100644 --- a/src/main/java/org/openrewrite/java/spring/boot3/UseReactorContextPropagation.java +++ b/src/main/java/org/openrewrite/java/spring/boot3/UseReactorContextPropagation.java @@ -18,12 +18,15 @@ import org.openrewrite.ExecutionContext; import org.openrewrite.Preconditions; import org.openrewrite.Recipe; +import org.openrewrite.Tree; import org.openrewrite.TreeVisitor; import org.openrewrite.java.JavaIsoVisitor; import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.search.UsesMethod; import org.openrewrite.java.search.UsesType; import org.openrewrite.java.spring.AddSpringProperty; import org.openrewrite.java.tree.J; +import org.openrewrite.properties.tree.Properties; public class UseReactorContextPropagation extends Recipe { @Override @@ -37,18 +40,25 @@ public String getDescription() { } private static final String SPRING_BOOT_APPLICATION = "org.springframework.boot.autoconfigure.SpringBootApplication"; - private static final MethodMatcher MATCHER = new MethodMatcher("reactor.hooks.Hooks enableAutomaticContextPropagation()"); + private static final MethodMatcher MATCHER = new MethodMatcher("reactor.core.publisher.Hooks enableAutomaticContextPropagation()"); @Override public TreeVisitor getVisitor() { - return Preconditions.check( + return + Preconditions.check( new UsesType<>(SPRING_BOOT_APPLICATION, true), new JavaIsoVisitor(){ @Override public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { J.MethodInvocation mi = (J.MethodInvocation) super.visitMethodInvocation(method, ctx); if (MATCHER.matches(mi)) { + // remove unused import + maybeRemoveImport("reactor.core.publisher.Hooks"); + + // add 'context-propagation' into spring property doAfterVisit(new AddSpringProperty("spring.reactor.context-propagation", "true", null, null).getVisitor()); + + // remove Method call return null; } return mi; diff --git a/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/UseReactorContextPropagationTest.java b/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/UseReactorContextPropagationTest.java index 1f15c1fab..c72657c4b 100644 --- a/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/UseReactorContextPropagationTest.java +++ b/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/UseReactorContextPropagationTest.java @@ -15,60 +15,80 @@ */ package org.openrewrite.java.spring.boot3; -import static org.openrewrite.java.Assertions.java; -import static org.openrewrite.properties.Assertions.*; +import static org.openrewrite.java.Assertions.*; import org.junit.jupiter.api.Test; import org.openrewrite.DocumentExample; -import org.openrewrite.java.JavaParser; import org.openrewrite.test.RecipeSpec; import org.openrewrite.test.RewriteTest; +import static org.openrewrite.properties.Assertions.properties; + public class UseReactorContextPropagationTest implements RewriteTest { - @Override - public void defaults(RecipeSpec spec) { - spec.recipe(new UseReactorContextPropagation()) - .parser(JavaParser.fromJavaVersion().classpath("spring-boot")); - } + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new UseReactorContextPropagation()); + } - @DocumentExample - @Test - void replaceMethodCallWithProperty() { - rewriteRun( - //language=java - spec -> spec.recipeFromResources("org.openrewrite.java.spring.boot3.UseReactorContextPropagation"), - java( - """ - import reactor.core.publisher.Hooks; - import org.springframework.boot.SpringApplication; - import org.springframework.boot.autoconfigure.SpringBootApplication; + @DocumentExample + @Test + void replaceMethodCallWithProperty() { + rewriteRun( + spec -> spec.recipe(new UseReactorContextPropagation()), + java( + """ + package org.springframework.boot.autoconfigure; + public @interface SpringBootApplication {} + """ + ), + java( + """ + package org.springframework.boot; + public class SpringApplication { + public static void run(Class cls, String[] args) {} + } + """ + ), + java( + """ + package reactor.core.publisher; + public class Hooks { + public static void enableAutomaticContextPropagation() {} + } + """ + ), + java( + """ + import reactor.core.publisher.Hooks; + import org.springframework.boot.SpringApplication; + import org.springframework.boot.autoconfigure.SpringBootApplication; - @SpringBootApplication - public class MyApp{ - public static void main(String[] args) { - Hooks.enableAutomaticContextPropagation(); - SpringApplication.run(MyApplication.class, args); - } - } - """, - """ - import reactor.core.publisher.Hooks; - import org.springframework.boot.SpringApplication; - import org.springframework.boot.autoconfigure.SpringBootApplication; + @SpringBootApplication + public class MyApplication { + public static void main(String[] args) { + Hooks.enableAutomaticContextPropagation(); + SpringApplication.run(MyApplication.class, args); + } + } + """, + """ + import org.springframework.boot.SpringApplication; + import org.springframework.boot.autoconfigure.SpringBootApplication; - @SpringBootApplication - public class MyApp { - public static void main(String[] args) { - SpringApplication.run(MyApplication.class, args); - } - } - """ - ), - properties( - "", - "spring.reactor.context-propagation=true" - ) - ); - } + @SpringBootApplication + public class MyApplication { + public static void main(String[] args) { + SpringApplication.run(MyApplication.class, args); + } + } + """ + ), + properties( + "", + "spring.reactor.context-propagation=true", + spec -> spec.path("application.properties") + ) + ); + } } From 7a1df6b81d31c5e9b5cb1366771f46155b131863 Mon Sep 17 00:00:00 2001 From: nurdy-kim Date: Tue, 26 Aug 2025 20:51:06 +0900 Subject: [PATCH 3/6] feat: add MigrateHooksToReactorContextProperty --- .../MigrateHooksToReactorContextProperty.java | 128 ++++++++++++++++++ .../boot3/UseReactorContextPropagation.java | 69 ---------- ...ateHooksToReactorContextPropertyTest.java} | 6 +- 3 files changed, 131 insertions(+), 72 deletions(-) create mode 100644 src/main/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextProperty.java delete mode 100644 src/main/java/org/openrewrite/java/spring/boot3/UseReactorContextPropagation.java rename src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/{UseReactorContextPropagationTest.java => MigrateHooksToReactorContextPropertyTest.java} (93%) diff --git a/src/main/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextProperty.java b/src/main/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextProperty.java new file mode 100644 index 000000000..bea5bd1e6 --- /dev/null +++ b/src/main/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextProperty.java @@ -0,0 +1,128 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * 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 + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * 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.boot3; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.jspecify.annotations.Nullable; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.ScanningRecipe; +import org.openrewrite.Tree; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.search.UsesType; +import org.openrewrite.java.spring.AddSpringProperty; +import org.openrewrite.java.tree.J; +import org.openrewrite.properties.tree.Properties; +import org.openrewrite.yaml.tree.Yaml; + +import static org.openrewrite.Preconditions.and; + +public class MigrateHooksToReactorContextProperty extends ScanningRecipe { + @Override + public String getDisplayName() { + return "Use `spring.reactor.context-propagation` property"; + } + + @Override + public String getDescription() { + return "Replace `Hooks.enableAutomaticContextPropagation()` with `spring.reactor.context-propagation=true`."; + } + + @Override + public AtomicBoolean getInitialValue(ExecutionContext ctx) { + return new AtomicBoolean(false); + } + + private static final String SPRING_BOOT_APPLICATION_FQN = "org.springframework.boot.autoconfigure.SpringBootApplication"; + private static final String HOOKS_TYPE = "reactor.core.publisher.Hooks"; + private static final MethodMatcher HOOKS_MATCHER = new MethodMatcher("reactor.core.publisher.Hooks enableAutomaticContextPropagation()"); + + @Override + public TreeVisitor getScanner(AtomicBoolean foundHooksInSpringApp) { + return Preconditions.check( + and( + new UsesType<>(SPRING_BOOT_APPLICATION_FQN, true), + new UsesType<>(HOOKS_TYPE, true) + ), + new JavaIsoVisitor() { + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { + J.MethodInvocation mi = super.visitMethodInvocation(method, ctx); + + if (HOOKS_MATCHER.matches(mi)) { + foundHooksInSpringApp.set(true); + } + + return mi; + } + } + ); + } + + @Override + public TreeVisitor getVisitor(AtomicBoolean foundHooksInSpringApp) { + if (!foundHooksInSpringApp.get()) { + return TreeVisitor.noop(); + } + + return new TreeVisitor() { + @Override + public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { + if (tree instanceof J.CompilationUnit) { + return new HooksRemovalVisitor().visitNonNull(tree, ctx); + } + + if (isApplicationProperties(tree)) { + return addSpringProperty(ctx, tree, "spring.reactor.context-propagation", "true"); + } + + return tree; + } + }; + } + + private static boolean isApplicationProperties(@Nullable Tree tree) { + return (tree instanceof Properties.File && + "application.properties".equals(((Properties.File)tree).getSourcePath().getFileName().toString())) || + (tree instanceof Yaml.Documents && + ((Yaml.Documents)tree).getSourcePath().getFileName().toString().matches("application\\.ya*ml")); + } + + private static Tree addSpringProperty(ExecutionContext ctx, Tree properties, String property, String value) { + return new AddSpringProperty(property, value, null, null) + .getVisitor() + .visitNonNull(properties, ctx); + } + + private static class HooksRemovalVisitor extends JavaIsoVisitor { + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { + J.MethodInvocation mi = super.visitMethodInvocation(method, ctx); + + // Only remove if we're in a @SpringBootApplication class + if (HOOKS_MATCHER.matches(mi)) { + maybeRemoveImport(HOOKS_TYPE); + + return null; + } + + return mi; + } + } +} diff --git a/src/main/java/org/openrewrite/java/spring/boot3/UseReactorContextPropagation.java b/src/main/java/org/openrewrite/java/spring/boot3/UseReactorContextPropagation.java deleted file mode 100644 index 705f83819..000000000 --- a/src/main/java/org/openrewrite/java/spring/boot3/UseReactorContextPropagation.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2024 the original author or authors. - *

- * 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 - *

- * https://docs.moderne.io/licensing/moderne-source-available-license - *

- * 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.boot3; - -import org.openrewrite.ExecutionContext; -import org.openrewrite.Preconditions; -import org.openrewrite.Recipe; -import org.openrewrite.Tree; -import org.openrewrite.TreeVisitor; -import org.openrewrite.java.JavaIsoVisitor; -import org.openrewrite.java.MethodMatcher; -import org.openrewrite.java.search.UsesMethod; -import org.openrewrite.java.search.UsesType; -import org.openrewrite.java.spring.AddSpringProperty; -import org.openrewrite.java.tree.J; -import org.openrewrite.properties.tree.Properties; - -public class UseReactorContextPropagation extends Recipe { - @Override - public String getDisplayName() { - return "Use `spring.reactor.context-propagation` property"; - } - - @Override - public String getDescription() { - return "Replace `Hooks.enableAutomaticContextPropagation()` with `spring.reactor.context-propagation=true`."; - } - - private static final String SPRING_BOOT_APPLICATION = "org.springframework.boot.autoconfigure.SpringBootApplication"; - private static final MethodMatcher MATCHER = new MethodMatcher("reactor.core.publisher.Hooks enableAutomaticContextPropagation()"); - - @Override - public TreeVisitor getVisitor() { - return - Preconditions.check( - new UsesType<>(SPRING_BOOT_APPLICATION, true), - new JavaIsoVisitor(){ - @Override - public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { - J.MethodInvocation mi = (J.MethodInvocation) super.visitMethodInvocation(method, ctx); - if (MATCHER.matches(mi)) { - // remove unused import - maybeRemoveImport("reactor.core.publisher.Hooks"); - - // add 'context-propagation' into spring property - doAfterVisit(new AddSpringProperty("spring.reactor.context-propagation", "true", null, null).getVisitor()); - - // remove Method call - return null; - } - return mi; - } - } - ); - } -} diff --git a/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/UseReactorContextPropagationTest.java b/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextPropertyTest.java similarity index 93% rename from src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/UseReactorContextPropagationTest.java rename to src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextPropertyTest.java index c72657c4b..b7e8bf1b1 100644 --- a/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/UseReactorContextPropagationTest.java +++ b/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextPropertyTest.java @@ -24,18 +24,18 @@ import static org.openrewrite.properties.Assertions.properties; -public class UseReactorContextPropagationTest implements RewriteTest { +public class MigrateHooksToReactorContextPropertyTest implements RewriteTest { @Override public void defaults(RecipeSpec spec) { - spec.recipe(new UseReactorContextPropagation()); + spec.recipe(new MigrateHooksToReactorContextProperty()); } @DocumentExample @Test void replaceMethodCallWithProperty() { rewriteRun( - spec -> spec.recipe(new UseReactorContextPropagation()), + spec -> spec.recipe(new MigrateHooksToReactorContextProperty()), java( """ package org.springframework.boot.autoconfigure; From 2d02f113fa54115bab8fbab823e7973fd428f235 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 26 Aug 2025 14:22:12 +0200 Subject: [PATCH 4/6] Apply formatter --- .../MigrateHooksToReactorContextProperty.java | 192 +++++++++--------- ...rateHooksToReactorContextPropertyTest.java | 3 +- 2 files changed, 95 insertions(+), 100 deletions(-) diff --git a/src/main/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextProperty.java b/src/main/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextProperty.java index bea5bd1e6..1c7c726cb 100644 --- a/src/main/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextProperty.java +++ b/src/main/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextProperty.java @@ -15,14 +15,8 @@ */ package org.openrewrite.java.spring.boot3; -import java.util.concurrent.atomic.AtomicBoolean; - import org.jspecify.annotations.Nullable; -import org.openrewrite.ExecutionContext; -import org.openrewrite.Preconditions; -import org.openrewrite.ScanningRecipe; -import org.openrewrite.Tree; -import org.openrewrite.TreeVisitor; +import org.openrewrite.*; import org.openrewrite.java.JavaIsoVisitor; import org.openrewrite.java.MethodMatcher; import org.openrewrite.java.search.UsesType; @@ -31,98 +25,100 @@ import org.openrewrite.properties.tree.Properties; import org.openrewrite.yaml.tree.Yaml; +import java.util.concurrent.atomic.AtomicBoolean; + import static org.openrewrite.Preconditions.and; public class MigrateHooksToReactorContextProperty extends ScanningRecipe { - @Override - public String getDisplayName() { - return "Use `spring.reactor.context-propagation` property"; - } - - @Override - public String getDescription() { - return "Replace `Hooks.enableAutomaticContextPropagation()` with `spring.reactor.context-propagation=true`."; - } - - @Override - public AtomicBoolean getInitialValue(ExecutionContext ctx) { - return new AtomicBoolean(false); - } - - private static final String SPRING_BOOT_APPLICATION_FQN = "org.springframework.boot.autoconfigure.SpringBootApplication"; - private static final String HOOKS_TYPE = "reactor.core.publisher.Hooks"; - private static final MethodMatcher HOOKS_MATCHER = new MethodMatcher("reactor.core.publisher.Hooks enableAutomaticContextPropagation()"); - - @Override - public TreeVisitor getScanner(AtomicBoolean foundHooksInSpringApp) { - return Preconditions.check( - and( - new UsesType<>(SPRING_BOOT_APPLICATION_FQN, true), - new UsesType<>(HOOKS_TYPE, true) - ), - new JavaIsoVisitor() { - @Override - public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { - J.MethodInvocation mi = super.visitMethodInvocation(method, ctx); - - if (HOOKS_MATCHER.matches(mi)) { - foundHooksInSpringApp.set(true); - } - - return mi; - } - } - ); - } - - @Override - public TreeVisitor getVisitor(AtomicBoolean foundHooksInSpringApp) { - if (!foundHooksInSpringApp.get()) { - return TreeVisitor.noop(); - } - - return new TreeVisitor() { - @Override - public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { - if (tree instanceof J.CompilationUnit) { - return new HooksRemovalVisitor().visitNonNull(tree, ctx); - } - - if (isApplicationProperties(tree)) { - return addSpringProperty(ctx, tree, "spring.reactor.context-propagation", "true"); - } - - return tree; - } - }; - } - - private static boolean isApplicationProperties(@Nullable Tree tree) { - return (tree instanceof Properties.File && - "application.properties".equals(((Properties.File)tree).getSourcePath().getFileName().toString())) || - (tree instanceof Yaml.Documents && - ((Yaml.Documents)tree).getSourcePath().getFileName().toString().matches("application\\.ya*ml")); - } - - private static Tree addSpringProperty(ExecutionContext ctx, Tree properties, String property, String value) { - return new AddSpringProperty(property, value, null, null) - .getVisitor() - .visitNonNull(properties, ctx); - } - - private static class HooksRemovalVisitor extends JavaIsoVisitor { - @Override - public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { - J.MethodInvocation mi = super.visitMethodInvocation(method, ctx); - - // Only remove if we're in a @SpringBootApplication class - if (HOOKS_MATCHER.matches(mi)) { - maybeRemoveImport(HOOKS_TYPE); - - return null; - } - - return mi; - } - } + @Override + public String getDisplayName() { + return "Use `spring.reactor.context-propagation` property"; + } + + @Override + public String getDescription() { + return "Replace `Hooks.enableAutomaticContextPropagation()` with `spring.reactor.context-propagation=true`."; + } + + @Override + public AtomicBoolean getInitialValue(ExecutionContext ctx) { + return new AtomicBoolean(false); + } + + private static final String SPRING_BOOT_APPLICATION_FQN = "org.springframework.boot.autoconfigure.SpringBootApplication"; + private static final String HOOKS_TYPE = "reactor.core.publisher.Hooks"; + private static final MethodMatcher HOOKS_MATCHER = new MethodMatcher("reactor.core.publisher.Hooks enableAutomaticContextPropagation()"); + + @Override + public TreeVisitor getScanner(AtomicBoolean foundHooksInSpringApp) { + return Preconditions.check( + and( + new UsesType<>(SPRING_BOOT_APPLICATION_FQN, true), + new UsesType<>(HOOKS_TYPE, true) + ), + new JavaIsoVisitor() { + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { + J.MethodInvocation mi = super.visitMethodInvocation(method, ctx); + + if (HOOKS_MATCHER.matches(mi)) { + foundHooksInSpringApp.set(true); + } + + return mi; + } + } + ); + } + + @Override + public TreeVisitor getVisitor(AtomicBoolean foundHooksInSpringApp) { + if (!foundHooksInSpringApp.get()) { + return TreeVisitor.noop(); + } + + return new TreeVisitor() { + @Override + public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { + if (tree instanceof J.CompilationUnit) { + return new HooksRemovalVisitor().visitNonNull(tree, ctx); + } + + if (isApplicationProperties(tree)) { + return addSpringProperty(ctx, tree, "spring.reactor.context-propagation", "true"); + } + + return tree; + } + }; + } + + private static boolean isApplicationProperties(@Nullable Tree tree) { + return (tree instanceof Properties.File && + "application.properties".equals(((Properties.File) tree).getSourcePath().getFileName().toString())) || + (tree instanceof Yaml.Documents && + ((Yaml.Documents) tree).getSourcePath().getFileName().toString().matches("application\\.ya*ml")); + } + + private static Tree addSpringProperty(ExecutionContext ctx, Tree properties, String property, String value) { + return new AddSpringProperty(property, value, null, null) + .getVisitor() + .visitNonNull(properties, ctx); + } + + private static class HooksRemovalVisitor extends JavaIsoVisitor { + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { + J.MethodInvocation mi = super.visitMethodInvocation(method, ctx); + + // Only remove if we're in a @SpringBootApplication class + if (HOOKS_MATCHER.matches(mi)) { + maybeRemoveImport(HOOKS_TYPE); + + return null; + } + + return mi; + } + } } diff --git a/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextPropertyTest.java b/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextPropertyTest.java index b7e8bf1b1..782273a56 100644 --- a/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextPropertyTest.java +++ b/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextPropertyTest.java @@ -15,13 +15,12 @@ */ package org.openrewrite.java.spring.boot3; -import static org.openrewrite.java.Assertions.*; - import org.junit.jupiter.api.Test; import org.openrewrite.DocumentExample; import org.openrewrite.test.RecipeSpec; import org.openrewrite.test.RewriteTest; +import static org.openrewrite.java.Assertions.java; import static org.openrewrite.properties.Assertions.properties; public class MigrateHooksToReactorContextPropertyTest implements RewriteTest { From def6acbd6f4517e3f36ba1af4366b3adbdc7c8fb Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 26 Aug 2025 14:23:00 +0200 Subject: [PATCH 5/6] Apply best practices --- .../spring/boot3/MigrateHooksToReactorContextProperty.java | 3 +-- .../spring/boot3/MigrateHooksToReactorContextPropertyTest.java | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextProperty.java b/src/main/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextProperty.java index 1c7c726cb..5ba018dc8 100644 --- a/src/main/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextProperty.java +++ b/src/main/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextProperty.java @@ -108,13 +108,12 @@ private static Tree addSpringProperty(ExecutionContext ctx, Tree properties, Str private static class HooksRemovalVisitor extends JavaIsoVisitor { @Override - public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { + public J.@Nullable MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { J.MethodInvocation mi = super.visitMethodInvocation(method, ctx); // Only remove if we're in a @SpringBootApplication class if (HOOKS_MATCHER.matches(mi)) { maybeRemoveImport(HOOKS_TYPE); - return null; } diff --git a/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextPropertyTest.java b/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextPropertyTest.java index 782273a56..9b1916af5 100644 --- a/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextPropertyTest.java +++ b/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextPropertyTest.java @@ -23,7 +23,7 @@ import static org.openrewrite.java.Assertions.java; import static org.openrewrite.properties.Assertions.properties; -public class MigrateHooksToReactorContextPropertyTest implements RewriteTest { +class MigrateHooksToReactorContextPropertyTest implements RewriteTest { @Override public void defaults(RecipeSpec spec) { From c85c43d8049512e55e7b465f3c1cf0c605d00055 Mon Sep 17 00:00:00 2001 From: nurdy-kim Date: Wed, 10 Sep 2025 21:15:51 +0900 Subject: [PATCH 6/6] Refactoring AtomicBoolean to Accumlator, add recipe to SpringBoot3.2 Migration --- .../MigrateHooksToReactorContextProperty.java | 273 ++++++++++++------ .../META-INF/rewrite/spring-boot-32.yml | 1 + ...rateHooksToReactorContextPropertyTest.java | 44 ++- 3 files changed, 225 insertions(+), 93 deletions(-) diff --git a/src/main/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextProperty.java b/src/main/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextProperty.java index 5ba018dc8..a5238f086 100644 --- a/src/main/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextProperty.java +++ b/src/main/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextProperty.java @@ -19,105 +19,194 @@ import org.openrewrite.*; import org.openrewrite.java.JavaIsoVisitor; import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.marker.JavaProject; import org.openrewrite.java.search.UsesType; import org.openrewrite.java.spring.AddSpringProperty; import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaSourceFile; +import org.openrewrite.marker.Marker; import org.openrewrite.properties.tree.Properties; import org.openrewrite.yaml.tree.Yaml; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; import static org.openrewrite.Preconditions.and; -public class MigrateHooksToReactorContextProperty extends ScanningRecipe { - @Override - public String getDisplayName() { - return "Use `spring.reactor.context-propagation` property"; - } - - @Override - public String getDescription() { - return "Replace `Hooks.enableAutomaticContextPropagation()` with `spring.reactor.context-propagation=true`."; - } - - @Override - public AtomicBoolean getInitialValue(ExecutionContext ctx) { - return new AtomicBoolean(false); - } - - private static final String SPRING_BOOT_APPLICATION_FQN = "org.springframework.boot.autoconfigure.SpringBootApplication"; - private static final String HOOKS_TYPE = "reactor.core.publisher.Hooks"; - private static final MethodMatcher HOOKS_MATCHER = new MethodMatcher("reactor.core.publisher.Hooks enableAutomaticContextPropagation()"); - - @Override - public TreeVisitor getScanner(AtomicBoolean foundHooksInSpringApp) { - return Preconditions.check( - and( - new UsesType<>(SPRING_BOOT_APPLICATION_FQN, true), - new UsesType<>(HOOKS_TYPE, true) - ), - new JavaIsoVisitor() { - @Override - public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { - J.MethodInvocation mi = super.visitMethodInvocation(method, ctx); - - if (HOOKS_MATCHER.matches(mi)) { - foundHooksInSpringApp.set(true); - } - - return mi; - } - } - ); - } - - @Override - public TreeVisitor getVisitor(AtomicBoolean foundHooksInSpringApp) { - if (!foundHooksInSpringApp.get()) { - return TreeVisitor.noop(); - } - - return new TreeVisitor() { - @Override - public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { - if (tree instanceof J.CompilationUnit) { - return new HooksRemovalVisitor().visitNonNull(tree, ctx); - } - - if (isApplicationProperties(tree)) { - return addSpringProperty(ctx, tree, "spring.reactor.context-propagation", "true"); - } - - return tree; - } - }; - } - - private static boolean isApplicationProperties(@Nullable Tree tree) { - return (tree instanceof Properties.File && - "application.properties".equals(((Properties.File) tree).getSourcePath().getFileName().toString())) || - (tree instanceof Yaml.Documents && - ((Yaml.Documents) tree).getSourcePath().getFileName().toString().matches("application\\.ya*ml")); - } - - private static Tree addSpringProperty(ExecutionContext ctx, Tree properties, String property, String value) { - return new AddSpringProperty(property, value, null, null) - .getVisitor() - .visitNonNull(properties, ctx); - } - - private static class HooksRemovalVisitor extends JavaIsoVisitor { - @Override - public J.@Nullable MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { - J.MethodInvocation mi = super.visitMethodInvocation(method, ctx); - - // Only remove if we're in a @SpringBootApplication class - if (HOOKS_MATCHER.matches(mi)) { - maybeRemoveImport(HOOKS_TYPE); - return null; - } - - return mi; - } - } +import lombok.Data; +import lombok.Value; +import lombok.With; + +public class MigrateHooksToReactorContextProperty extends ScanningRecipe { + @Override + public String getDisplayName() { + return "Use `spring.reactor.context-propagation` property"; + } + + @Override + public String getDescription() { + return "Replace `Hooks.enableAutomaticContextPropagation()` with `spring.reactor.context-propagation=true`."; + } + + @Override + public ProjectsWithHooks getInitialValue(ExecutionContext ctx) { + return new ProjectsWithHooks(); + } + + private static final String SPRING_BOOT_APPLICATION_FQN = "org.springframework.boot.autoconfigure.SpringBootApplication"; + private static final String HOOKS_TYPE = "reactor.core.publisher.Hooks"; + private static final MethodMatcher HOOKS_MATCHER = new MethodMatcher("reactor.core.publisher.Hooks enableAutomaticContextPropagation()"); + + @Override + public TreeVisitor getScanner(ProjectsWithHooks acc) { + return Preconditions.check( + and( + new UsesType<>(SPRING_BOOT_APPLICATION_FQN, true), + new UsesType<>(HOOKS_TYPE, true) + ), + new JavaIsoVisitor() { + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { + J.MethodInvocation mi = super.visitMethodInvocation(method, ctx); + + if (HOOKS_MATCHER.matches(mi)){ + JavaSourceFile sourceFile = getCursor().firstEnclosing(JavaSourceFile.class); + if (sourceFile != null){ + Optional javaProject = sourceFile.getMarkers().findFirst(JavaProject.class); + if (javaProject.isPresent()){ + acc.getProjectsWithHooks().add(javaProject.get()); + } else { + acc.setHasHooksInSingleProject(true); + } + + getCursor().putMessageOnFirstEnclosing(JavaSourceFile.class, "has-hooks", true); + } + } + return mi; + } + } + ); + } + + @Override + public TreeVisitor getVisitor(ProjectsWithHooks acc) { + if(acc.getProjectsWithHooks().isEmpty() && !acc.isHasHooksInSingleProject()){ + return TreeVisitor.noop(); + } + + return new TreeVisitor() { + @Override + public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx){ + + if (!(tree instanceof SourceFile)) { + return tree; + } + + SourceFile sourceFile = (SourceFile) tree; + + // check already processed. + if (sourceFile.getMarkers().findFirst(PropertyAdded.class).isPresent()){ + return tree; + } + + Optional currentProject = sourceFile.getMarkers().findFirst(JavaProject.class); + + if (tree instanceof J.CompilationUnit ) { + + boolean shouldProcess = false; + + if (currentProject.isPresent()){ + shouldProcess = acc.getProcessedProjects().contains(currentProject.get()); + } else if (acc.isHasHooksInSingleProject()){ + shouldProcess = true; + } + + if (shouldProcess) { + return new HooksRemovalVisitor().visitNonNull(tree, ctx); + } + } + + if (isApplicationProperties(tree)){ + boolean shouldAddProperty = false; + + if (currentProject.isPresent()){ + JavaProject project = currentProject.get(); + if (acc.getProjectsWithHooks().contains(project) && + !acc.getProcessedProjects().contains(project)) { + acc.getProcessedProjects().add(project); + shouldAddProperty = true; + } + } else if (acc.isHasHooksInSingleProject() && !acc.isPropertiesProcessedForSingleProject()) { + // single project or testing environment + acc.setPropertiesProcessedForSingleProject(true); + shouldAddProperty = true; + } + + if (shouldAddProperty) { + Tree result = addSpringProperty(ctx, tree); + + if (result instanceof SourceFile) { + result = ((SourceFile) result).withMarkers( + ((SourceFile) result).getMarkers().addIfAbsent(new PropertyAdded(Tree.randomId())) + ); + } + return result; + } + } + + return tree; + } + }; + } + + private static boolean isApplicationProperties(@Nullable Tree tree) { + if (tree instanceof Properties.File){ + String fileName = ((Properties.File) tree).getSourcePath().getFileName().toString(); + return fileName.equals("application.properties") || fileName.matches("application-.*\\.properties"); + } + + if (tree instanceof Yaml.Documents){ + String fileName = ((Yaml.Documents) tree).getSourcePath().getFileName().toString(); + return fileName.matches("application(-.*)?\\.(yml|yaml)"); + } + + return false; + } + + private static Tree addSpringProperty(ExecutionContext ctx, Tree properties) { + return new AddSpringProperty("spring.reactor.context-propagation", "true", null, null) + .getVisitor().visitNonNull(properties, ctx); + } + + private static class HooksRemovalVisitor extends JavaIsoVisitor { + @Override + public J.@Nullable MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { + J.MethodInvocation mi = super.visitMethodInvocation(method, ctx); + + if (HOOKS_MATCHER.matches(mi)) { + maybeRemoveImport(HOOKS_TYPE); + return null; + } + + return mi; + } + } + + @Data + public static class ProjectsWithHooks { + Set projectsWithHooks = new HashSet<>(); + Set processedProjects = new HashSet<>(); + + boolean hasHooksInSingleProject = false; // single project or has no markers + boolean propertiesProcessedForSingleProject = false; + } + + @Value + @With + static class PropertyAdded implements Marker { + UUID id; + } + } diff --git a/src/main/resources/META-INF/rewrite/spring-boot-32.yml b/src/main/resources/META-INF/rewrite/spring-boot-32.yml index a0906703f..65efd5f69 100644 --- a/src/main/resources/META-INF/rewrite/spring-boot-32.yml +++ b/src/main/resources/META-INF/rewrite/spring-boot-32.yml @@ -86,6 +86,7 @@ recipeList: - org.openrewrite.java.spring.boot3.RelocateLauncherClasses - org.openrewrite.java.spring.boot3.UpgradeMyBatisToSpringBoot_3_2 - org.openrewrite.java.springdoc.UpgradeSpringDoc_2_5 + - org.openrewrite.java.spring.boot3.MigrateHooksToReactorContextProperty --- type: specs.openrewrite.org/v1beta/recipe diff --git a/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextPropertyTest.java b/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextPropertyTest.java index 9b1916af5..27d276534 100644 --- a/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextPropertyTest.java +++ b/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextPropertyTest.java @@ -20,8 +20,9 @@ import org.openrewrite.test.RecipeSpec; import org.openrewrite.test.RewriteTest; -import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.java.Assertions.*; import static org.openrewrite.properties.Assertions.properties; +import static org.openrewrite.yaml.Assertions.yaml; class MigrateHooksToReactorContextPropertyTest implements RewriteTest { @@ -90,4 +91,45 @@ public static void main(String[] args) { ) ); } + + @Test + void shouldNotAddPropertyWhenNoHooksPresent() { + rewriteRun( + spec -> spec.recipe(new MigrateHooksToReactorContextProperty()), + java( + """ + package org.springframework.boot.autoconfigure; + public @interface SpringBootApplication {} + """ + ), + java( + """ + package org.springframework.boot; + public class SpringApplication { + public static void run(Class cls, String[] args) {} + } + """ + ), + java( + """ + import org.springframework.boot.SpringApplication; + import org.springframework.boot.autoconfigure.SpringBootApplication; + + @SpringBootApplication + public class MyApplication { + public static void main(String[] args) { + // No Hooks.enableAutomaticContextPropagation() here + SpringApplication.run(MyApplication.class, args); + } + } + """ + ), + properties( + """ + server.port=8080 + """, + spec -> spec.path("application.properties") + ) + ); + } }