This project demonstrates how we could manage end-to-end the migration process of a Java application from by example Spring Boot to Quarkus using the following concepts:
- A rule definition encapsulating the information about what to search within the code source and the instructions to be executed by a provider: User, AI and Openrewrite to migrate what the rule targets to do
- The report generated by the rules processed by a rule engine and detailing what the engine found, order of execution, metadata, etc
- A provider (tool, user, AI) able to transform the code using the instructions of the report - aka migration plan
Today
flowchart TD
A[Rule Definition] --> B[Scan Code]
B --> C[Analysis report / Migration plan]
C --> D[Transform]
style A fill:#e1f5fe
style C fill:#fff3e0
Improved
flowchart TD
A[Controlled flow] --> B[Scan Code]
A1[Rule Definition] --> B[Scan Code]
B --> C[Analysis report / Migration plan]
C --> D{Results?}
D -->|Empty| E[No Action]
D -->|Matches| F[Select the provider]
F --> G{Provider?}
G -->|User| H[Execute manual Tasks]
G -->|AI| I[Prompt AI augmented messages]
G -->|OpenRewrite| J[Apply Recipe]
H --> K[Migrated Code]
I --> K
J --> K
style A fill:#e1f5fe
style C fill:#fff3e0
style E fill:#f3e5f5
style K fill:#e8f5e8
style F fill:#fff8e1
The rule represents per se the contract definition between what we would like to discover within the code source scanned: java, properties, xml, json, maven or gradle files and what a provider should do to properly transform the code. Ideally we should provide a list of instructions as presented hereafter and tight to the provider able to execute them manually or using a well established technology as openrewrite or AI, etc ...
- category: mandatory
description: Replace the Spring Boot Application Annotation with QuarkusMain
labels:
- konveyor.io/source=springboot
- konveyor.io/target=quarkus
message: "Replace the Spring Boot Application Annotation with QuarkusMain"
ruleID: springboot-annotations-to-quarkus-00000
when:
java.referenced:
location: ANNOTATION
pattern: org.springframework.boot.autoconfigure.SpringBootApplication
# Order to apply the instructions against the flow
order: 2
# New section added to help to transform properly the code !
instructions:
ai:
- promptMessage: "Remove the org.springframework.boot.autoconfigure.SpringBootApplication annotation from the main Spring Boot Application class"
manual:
- todo: "Remove the org.springframework.boot.autoconfigure.SpringBootApplication annotation from the main Spring Boot Application class"
openrewrite:
- name: Migrate Spring Boot to Quarkus
preconditions:
- name: org.openrewrite.java.dependencies.search.ModuleHasDependency
groupIdPattern: org.springframework.boot
artifactIdPattern: spring-boot
version: '[3.5,)'
recipeList:
- dev.snowdrop.openrewrite.recipe.spring.ReplaceSpringBootApplicationAnnotationWithQuarkusMain
- dev.snowdrop.openrewrite.recipe.spring.AddQuarkusRun
gav:
- dev.snowdrop:openrewrite-recipes:1.0.0-SNAPSHOT
The project uses the Spring TODO example as the project to be analyzed using augmented rules.
The poc has been designed using the following technology:
- Quarkus and Picocli to manage the CLI part
- konveyor jdt language server to scan the java files to find using a rule definition: an annotation, import, method, etc
- Openrewrite recipe to execute using the
maven rewrite
goal the transformation as defined part of the rule's instructions - The launch of the jdt-ls server like the maven openrewrite's goal command are executed as OS processes using Java
ProcessBuilder
.
Remark: The rule engine of this PoC is pretty basic and only translate the YAML java.referenced
value to the corresponding json request
needed to execute the JSON-RPC call with the command io.konveyor.tackle.RuleEntry.
- Java 21 installed and Maven 3.9
First, compile the project
./mvnw clean install
Download the konveyor language server using the following image:
set VERSION latest
set ID $(podman create --name kantra-download quay.io/konveyor/kantra:$VERSION)
podman cp $ID:/jdtls ./jdt/konveyor-jdtls
optional: Copy the konveyor-jdtls/java-analyzer-bundle/java-analyzer-bundle.core/target/java-analyzer-bundle.core-1.0.0-SNAPSHOT.jarjava-analyzer-bundle.core-1.0.0-SNAPSHOT.jar
to the ./lib/
folder of this project to use it as dependency (to access the code) as it is not published on a maven repository server !
To execute the command using the Quarkus Picocli CLI able to scan, analyze and generate the migration plan report (optional), execute this command
Usage: java-analyzer analyze [-v] [--jdt-ls-path=<jdtLsPath>]
[--jdt-workspace=<jdtWorkspace>] [-r=<rulesPath>]
<appPath>
Analyze a project for migration
<appPath> Path to the Java project to analyze
--jdt-ls-path=<jdtLsPath>
Path to JDT-LS installation (default: from config)
--jdt-workspace=<jdtWorkspace>
Path to JDT workspace directory (default: from
config)
-r, --rules=<rulesPath> Path to rules directory (default: from config)
-v, --verbose Enable verbose output
...
mvn -pl migration-tool quarkus:dev -Dquarkus.args="analyze --jdt-ls-path /PATH/TO/java-analyzer-quarkus/jdt/konveyor-jdtls --jdt-workspace /PATH/TO/java-analyzer-quarkus/jdt -r /PATH/TO/java-analyzer-quarkus/rules ./applications/spring-boot-todo-app"
To avoid to pass the parameters to the command, you can use the "defaults" application.properties and just pass the path of the application to be analyzed
mvn -pl migration-tool quarkus:dev -Dquarkus.args="analyze ../applications/spring-boot-todo-app"
You can check the log of the language server from this folder: jdt/.jdt_workspace/.metadata/.log
!
During the execution of the command, you will be able to see within the terminal the log reporting the JSON responses like also a summary table
2025-09-29 12:52:43,605 INFO [dev.sno.ana.ser.LsSearchService] (ForkJoinPool.commonPool-worker-5) ==== CLIENT: --- Search Results found for rule: springboot-import-to-quarkus-00000.
2025-09-29 12:52:43,606 INFO [dev.sno.ana.ser.LsSearchService] (ForkJoinPool.commonPool-worker-5) ==== CLIENT: --- JSON response: [
{
"name": "org.springframework.boot.autoconfigure.SpringBootApplication",
"kind": 2.0,
"location": {
"uri": "file:///Users/cmoullia/code/application-modernisation/migration-tool-parent/applications/spring-boot-todo-app/src/main/java/com/todo/app/AppApplication.java",
"range": {
"start": {
"line": 3.0,
"character": 7.0
},
"end": {
"line": 3.0,
"character": 67.0
}
}
},
"containerName": ""
}
]
...
=== Code Analysis Results ===
┌─────────────────────────────────────────────┬─────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Rule ID │Found│ Information Details │
├─────────────────────────────────────────────┼─────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│springboot-annotations-notfound-00000 │ No │No symbols found │
├─────────────────────────────────────────────┼─────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│springboot-annotations-to-quarkus-00000 │ Yes │Found SpringBootApplication at line 6, char: 1 - 22 │
│ │ │file:///Users/cmoullia/code/application-modernisation/migration-tool-parent/applications/spring-boot-todo-app/src/main/java/com/to│
│ │ │do/app/AppApplication.java │
├─────────────────────────────────────────────┼─────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│springboot-import-to-quarkus-00000 │ Yes │Found org.springframework.boot.autoconfigure.SpringBootApplication at line 4, char: 7 - 67 │
│ │ │file:///Users/cmoullia/code/application-modernisation/migration-tool-parent/applications/spring-boot-todo-app/src/main/java/com/to│
│ │ │do/app/AppApplication.java │
└─────────────────────────────────────────────┴─────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
If you want to populate an analysis report (kind of migration plan) then pass the parameter -o json
top the command. A json file having as
name: analysing-report_yyyy-mm-dd_hh:mm.json
will be generated within the project scanned
mvn -pl migration-tool quarkus:dev -Dquarkus.args="analyze ../applications/spring-boot-todo-app -o json"
Now that we have a migration plan (aka list of instructions to be executed by a provider like openrewrite), we can now execute the command transform
with or without the dryRun
mode.
mvn -pl migration-tool quarkus:dev -Dquarkus.args="transform ../applications/spring-boot-todo-app --dry-run"
Log of the command executed
2025-09-29 13:03:20,735 INFO [dev.sno.com.TransformCommand] (Quarkus Main Thread) ✅ Starting transformation for project at: /Users/cmoullia/code/application-modernisation/migration-tool-parent/applications/spring-boot-todo-app
2025-09-29 13:03:20,739 INFO [dev.sno.com.TransformCommand] (Quarkus Main Thread) 📄 Loading migration tasks from: analysing-report_2025-09-26_14:05.json
2025-09-29 13:03:20,879 INFO [dev.sno.com.TransformCommand] (Quarkus Main Thread) 📋 Found 3 migration tasks to process
2025-09-29 13:03:20,880 INFO [dev.sno.com.TransformCommand] (Quarkus Main Thread) 🔄 Processing migration task: springboot-annotations-notfound-00000
2025-09-29 13:03:20,881 WARN [dev.sno.com.TransformCommand] (Quarkus Main Thread) ⚠️ No OpenRewrite instructions found for task, skipping
2025-09-29 13:03:20,882 INFO [dev.sno.com.TransformCommand] (Quarkus Main Thread) 🔄 Processing migration task: springboot-annotations-to-quarkus-00000
2025-09-29 13:03:28,091 INFO [dev.sno.com.TransformCommand] (Quarkus Main Thread) ✅ OpenRewrite execution completed successfully
2025-09-29 13:03:28,092 INFO [dev.sno.com.TransformCommand] (Quarkus Main Thread) 🔄 Processing migration task: springboot-import-to-quarkus-00000
2025-09-29 13:03:28,093 WARN [dev.sno.com.TransformCommand] (Quarkus Main Thread) ⚠️ No OpenRewrite instructions found for task, skipping
2025-09-29 13:03:28,094 INFO [dev.sno.com.TransformCommand] (Quarkus Main Thread) ----------------------------------------
2025-09-29 13:03:28,095 INFO [dev.sno.com.TransformCommand] (Quarkus Main Thread) --- Elapsed time: 7359 ms ---
2025-09-29 13:03:28,096 INFO [dev.sno.com.TransformCommand] (Quarkus Main Thread) ----------------------------------------
Remark: If you use the --dry-run
parameter, then openrewrite will generate a rewrite.patch
file under the scanned project: target/rewrite
instead of changing the code directly !
Remark: The commands can also be executed using the jar file. Create in this case, an .env
file, to configure properly the jdt, rules and workspace properties
# .env file content
ANALYZER_JDT_LS_PATH=jdt/konveyor-jdtls
ANALYZER_JDT_WORKSPACE_PATH=jdt
ANALYZER_RULES_PATH=cookbook/rules
Next source it and execute the following java commands:
java -jar migration-tool/target/quarkus-app/quarkus-run.jar analyze $(pwd)/applications/spring-boot-todo-app
java -jar migration-tool/target/quarkus-app/quarkus-run.jar transform $(pwd)/applications/spring-boot-todo-app
Here are the openrewrite maven plugin command to be used to apply recipe(s) top of a spring boot project. Take care that your project is under git control as code will be transformed !
cd applications/spring-boot-todo-app
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=dev.snowdrop:java-analyzer-quarkus:1.0.0-SNAPSHOT \
-Dorg.openrewrite.quarkus.spring.ReplaceSpringBootApplicationAnnotationWithQuarkusMain
Instead of changing the code, you can use the dryrun mode to get a patch
cd applications/spring-boot-todo-app
mvn -U org.openrewrite.maven:rewrite-maven-plugin:dryRun \
-Drewrite.recipeArtifactCoordinates=dev.snowdrop:openrewrite-recipes:1.0.0-SNAPSHOT \
-Drewrite.activeRecipes=dev.snowdrop.openrewrite.recipe.spring.ReplaceSpringBootApplicationWithQuarkusMainAnnotation
When done, open the diff patch generated: /PATH/TO/spring-boot-todo-app/target/rewrite/rewrite.patch
To execute several recipes aggregated in a yaml file placed at the root of the project, execute this command:
mvn -U org.openrewrite.maven:rewrite-maven-plugin:dryRun \
-Drewrite.activeRecipes=dev.snowdrop.text.SearchText,dev.snowdrop.java.StandardJavaConventions,dev.snowdrop.java.spring.SearchSpringBootAnnotation \
-Drewrite.recipeArtifactCoordinates=org.openrewrite:rewrite-java:8.62.4,org.openrewrite.recipe:rewrite-java-dependencies:1.42.0,dev.snowdrop:openrewrite-recipes:1.0.0-SNAPSHOT \
-Drewrite.exportDatatables=true \
-Drewrite.configLocation=my-rewrite-1.yml
...
[WARNING] These recipes would make changes to applications/spring-boot-todo-app/my-rewrite.yml:
[WARNING] dev.snowdrop.text.SearchText
[WARNING] org.openrewrite.text.Find: {find=public class TaskController}
[WARNING] Patch file available:
[WARNING] /Users/cmoullia/code/application-modernisation/migration-tool-parent/applications/spring-boot-todo-app/target/rewrite/rewrite.patch
[WARNING] Estimate time saved: 40m
[WARNING] Run 'mvn rewrite:run' to apply the recipes.
Command using another YAML example
mvn -U org.openrewrite.maven:rewrite-maven-plugin:dryRun \
-Drewrite.activeRecipes=dev.snowdrop.java.spring.SearchSpringBootAnnotation \
-Drewrite.recipeArtifactCoordinates=org.openrewrite:rewrite-java:8.62.4,org.openrewrite.recipe:rewrite-java-dependencies:1.42.0,dev.snowdrop:openrewrite-recipes:1.0.0-SNAPSHOT \
-Drewrite.exportDatatables=true \
-Drewrite.configLocation=my-rewrite-2.yml
Example where we set the parameters of the recipe using the options
. Until now, it is only possible to pass the options of a recipe and not a list !!
mvn -U org.openrewrite.maven:rewrite-maven-plugin:dryRun \
-Drewrite.activeRecipes=org.openrewrite.java.ReplaceAnnotation \
-Drewrite.option=ReplaceAnnotation.annotationPatternToReplace="@org.springframework.boot.autoconfigure.SpringBootApplication",annotationTemplateToInsert="@io.quarkus.runtime.annotations.QuarkusMain" \
-Drewrite.recipeArtifactCoordinates=org.openrewrite:rewrite-java:8.62.4,org.openrewrite.recipe:rewrite-java-dependencies:1.42.0
Task | Status | Description | Comment |
---|---|---|---|
MT-001 | Check what spring-migrator-tool did to reuse some ideas to configure the instructions using Actions able to configure the recipe. The action's definition is used as input to apply the corresponding openrewrite's recipe. |
||
MT-002 | Investigate if the language server could be replaced using Openrewrite concepts such as: searchResult's marker, DataTable or Scanning recipe | ||
MT-003 | Discuss and define the level of granularity between what the rule targets to do and the steps/actions that the provider will support. Such a granularity can start with a 1 to 1 relation to a 1 to many but where the many is executed as a composite component or pipeline: https://docs.openrewrite.org/concepts-and-explanations/recipes#recipe-execution-pipeline, https://docs.openrewrite.org/concepts-and-explanations/recipes#scanning-recipes |
||
MT-004 | What about creating a migration plan like this one: https://docs.openrewrite.org/recipes/java/migrate/search/planjavamigration ? | ||
MT-005 | Do we have to integrate a workflow engine part of the solution to externalize the sequential approach of the openrewrite framework within a separate engine ? |
Before to run the server and client, configure the following system properties or override the Quarkus propertiesapplication.properties:
JDT_WKS
: Path of the folder containing the jdt-ls workspace, .metadata and log. Default:./jdt/
JDT_LS_PATH
: Path of the jdt language server folder. Default:./jdt/konveyor-jdtls
LS_CMD
: Language server command to be executed. Default:io.konveyor.tackle.ruleEntry
, etcAPP_PATH
: Path of the java project to analyze. Default:./applications/spring-boot-todo-app
RULES_PATH
: Path of the rules. Default:./rules
mvn exec:java
Here is the trick to do to add a bundle to the OSGI jdt-ls server. This step is optional as we will pass the bundle path as initialization parameter to the language server !
Edit the config.ini
file corresponding to your architecture: mac, linux, mac_arm under the folder konveyor-jdtls/config_
Modify within the config.ini file the osgi.bundles
property and include after the org.apache.commons.lang3...
jar the BundleSymbolicName of: java-analyzer-bundle.core-1.0.0-SNAPSHOT.jar
osgi.bundles=...org.apache.commons.lang3_3.14.0.jar@4,reference\:file\:java-analyzer-bundle.core-1.0.0-SNAPSHOT.jar@2,...
Copy the java-analyzer-bundle.core-1.0.0-SNAPSHOT.jar file from the path konveyor-jdtls/java-analyzer-bundle/java-analyzer-bundle.core/target/
to the plugins
folder
Alternatively, you can also download the Eclipse JDT Language Server:
wget https://www.eclipse.org/downloads/download.php?file=/jdtls/milestones/1.50.0/jdt-language-server-1.50.0-202509041425.tar.gz > jdt-language-server-1.50.0.tar.gz
mkdir jdt-ls
tar -vxf jdt-language-server-1.50.0.tar.gz -C jdt-ls