Spring Enterprise: A Guide to Application Advisor (Upgrade Plan)

· 38 min read

This post introduces Application Advisor, which comes with Spring Enterprise, as a supplement to the official manual. Following on from patch apply in the previous post, this entry focuses on the upgrade-plan apply feature.

Introduction

As I wrote last time, Application Advisor provides two major patching features.

This time it's the latter. The manual is here (v1.6).

img.png

Where patch apply only looks at patch versions such as "3.1.4 → 3.1.5", upgrade-plan apply takes care of upgrades that cross generations, such as "Spring Boot 2.7 → 3.5".

Note that the patches and minor upgrades I refer to here assume Semantic Versioning for each Java dependency. In other words, think of it in these terms:

Patching is built on the assumption that user code is unaffected. That is why patch apply updates dependencies without touching user code. Major and minor upgrades, on the other hand, can affect user code, so they need a different design from patch apply.

What are recipes and mapping coordinates?

The upgrade-plan apply command brings in concepts that patch apply does not have: Recipes and Mapping Coordinates.

They cover the following:

The Application Advisor CLI ships with recipes and mapping coordinates. The manual describes them in detail here.

For a simple upgrade, it looks like this.

img_1.png

Application Advisor uses the mapping coordinates to calculate units of execution called Steps. For each of these units, upgrade-plan runs the recipes and completes successfully. In the diagram above, that is two Steps. (Note: upgrading from 2.7 for real takes considerably more steps than this.) The manual walks through this using Spring Petclinic as an example.

Application Advisor lets you customize these recipes and mapping coordinates. Doing so makes upgrades like the following possible.

img_2.png

The example in this diagram assumes a custom framework that bundles Spring. Based on the customized mapping coordinates, Advisor learns which Spring Boot version goes with each version of that custom framework. With that, the steps required to reach the upgrade are recalculated. In this diagram, that adds one Step. You can also run your own recipes at each of those steps.

Recipes themselves are written on top of OpenRewrite. A whole range of approaches is supported, from minor fixes that use the recipes included by default, through to writing a recipe of your own in Java.

This makes it possible to upgrade not only plain Spring Boot but custom packages as well. Also, I have written this on the assumption of Spring Boot so far, but it can also be used with Spring Framework and other Java frameworks.

Right then — from here on I will walk through custom recipes and mapping coordinates. Be aware, though, that this is inevitably difficult at first. If you are going to do it for real, I recommend going in with some form of support from Broadcom behind you.

Attempting an automated TERASOLUNA upgrade

To put the approach above into practice, let's see whether we can upgrade TERASOLUNA, a Japanese-made framework.

How TERASOLUNA versions map to Spring Framework

TERASOLUNA and Spring Framework line up as follows (based on my own research).

TERASOLUNA spring-framework spring-data-commons spring-security
5.7.x 5.3.39 2.7.18 5.7.13
5.8.x 6.0.3 3.0.0 6.0.1
5.9.x 6.1.3 3.2.2 6.2.1
5.10.x 6.2.15 3.5.7 6.5.7
5.11.x 7.0.3 4.0.2 7.0.2

As you can see, 5.7 → 5.8 comes with a major change, from the Spring Framework 5 line to the 6 line. Let's see whether Application Advisor can carry out that upgrade.

Sample code

I put it here.

https://github.com/mhoshi-vm/simple-terasoluna

Running upgrade-plan without customization

First, let's run upgrade-plan without any customization.

1. Downloading the CLI

Same as last time. Follow the steps in the manual, or if you have the Spring Enterprise Repository configured you can also fetch it with the following. (This downloads 1.6.7 for Mac ARM64.)

mvn -U dependency:get -Dartifact=com.vmware.tanzu.spring:application-advisor-cli-macos-arm64:1.6.7:tar -Dtransitive=false

2. Checking the plan

Move to the root directory of the source code and run upgrade-plan.

advisor upgrade-plan get

With 1.6.7, the latest at the time of writing, the output was as follows.

advisor upgrade-plan get
     
🚀 Existing build-configuration is already up-to-date


🏃 Fetching and processing upgrade plan details [00m 01s] ok

Projects discovered:
        - junit: 6.0.x (no upgrades available)
        - junit-platform: 6.0.x (no upgrades available)

The projects ["spring-framework", "spring-data-commons"] could not be included in the Upgrade Plan because they are used as transitive dependencies for other projects, and no upgrades are configured for them.
Please request your administrator to configure the projects of the following dependencies:

        - org.terasoluna.gfw:terasoluna-gfw-common
                uses:
                        - spring-framework
                        - spring-data-commons
                blocking upgrades for:
                        - spring-data-commons
        - org.terasoluna.gfw:terasoluna-gfw-web
                uses:
                        - spring-framework
                        - spring-data-commons
                blocking upgrades for:
                        - spring-data-commons
        - org.terasoluna.gfw:terasoluna-gfw-jodatime
                uses:
                        - spring-framework
                        - spring-data-commons
                blocking upgrades for:
                        - spring-data-commons

In order to learn more about publishing upgrade mappings, visit https://techdocs.broadcom.com/us/en/vmware-tanzu/spring/tanzu-spring/commercial/spring-tanzu/app-advisor-custom-upgrades.html

No upgrade plans available - your project seems to be up to date.

The could not be included in the Upgrade Plan because they are used as transitive dependencies for other projects, and no upgrades are configured for them. line in the output, mapped onto this context, means the following.

This output suggests that TERASOLUNA's related libraries cannot be updated.

        - org.terasoluna.gfw:terasoluna-gfw-common
                uses:
                        - spring-framework
                        - spring-data-commons
                blocking upgrades for:
                        - spring-data-commons
        - org.terasoluna.gfw:terasoluna-gfw-web
                uses:
                        - spring-framework
                        - spring-data-commons
                blocking upgrades for:
                        - spring-data-commons
        - org.terasoluna.gfw:terasoluna-gfw-jodatime
                uses:
                        - spring-framework
                        - spring-data-commons
                blocking upgrades for:
                        - spring-data-commons

In other words, it errored out and nothing could be upgraded. Since no mapping has been supplied, this is as expected so far.

Running upgrade-plan with a custom mapping

Create the following custom mapping in a directory of your choice. The file name is up to you as well; I will use terasoluna.json.

{
  "slug": "terasoluna-gfw",
  "coordinates": [
    "org.terasoluna.gfw:terasoluna-gfw-common",
    "org.terasoluna.gfw:terasoluna-gfw-web",
    "org.terasoluna.gfw:terasoluna-gfw-jodatime"
  ],
  "repositoryUrl": "https://github.com/terasolunaorg/terasoluna-gfw",
  "rewrite": {
    "5.7.x": {
      "recipes": [],
      "nextRewrite": {
        "version": "5.8.x"
      },
      "requirements": {
        "supportedGenerations": {
          "spring-data-commons": "2.7.x",
          "spring-framework": "5.3.x",
          "spring-security": "5.7.x"
        },
        "excludedArtifacts": []
      }
    },
    "5.8.x": {
      "recipes": [],
      "nextRewrite": {
      },
      "requirements": {
        "supportedGenerations": {
          "spring-data-commons": "3.0.x",
          "spring-framework": "6.0.x",
          "spring-security": "6.0.x"
        },
        "excludedArtifacts": []
      }
    }
  }
}

The key points of its contents are as follows.

Strictly speaking, TERASOLUNA 5.9.x and later should be described here as well, but I have left them out this time.

Then export the following environment variable.

export SPRING_ADVISOR_MAPPING_CUSTOM_0_FILEPATH=<directory>/terasoluna.json

Running Advisor CLI v1.6.7 in this state gives the following.

advisor upgrade-plan get
     
🚀 Existing build-configuration is already up-to-date


🏃 [ 1 / 2 ] Validating syntax of upgrade mappings [00m 01s] ok
🏃 [ 2 / 2 ] Fetching and processing upgrade plan details [00m 01s] ok

Projects discovered:
        - junit: 6.0.x (no upgrades available)
        - junit-platform: 6.0.x (no upgrades available)
        - terasoluna-gfw: 5.7.x → 5.8.x
        - spring-data-commons: 2.7.x → 4.1.x
        - spring-framework: 5.3.x → 7.0.x

Upgrade Plan for your Dependencies:
        - Step 1:
                * Upgrade terasoluna-gfw from 5.7.x to 5.8.x
                * Upgrade spring-data-commons from 2.7.x to 3.0.x
                * Upgrade spring-framework from 5.3.x to 6.0.x
        - Step 2:
                * Upgrade spring-framework from 6.0.x to 6.1.x
                * Upgrade spring-data-commons from 3.0.x to 3.2.x
        - Step 3:
                * Upgrade spring-framework from 6.1.x to 6.2.x
                * Upgrade spring-data-commons from 3.2.x to 3.5.x

Some upgrades were not included in the upgrade plan.
Please, upgrade and release if needed the following projects:
        * terasoluna-gfw:5.8.x
                Last version of spring-framework is 6.0.x
        * terasoluna-gfw:5.8.x
                Last version of spring-data-commons is 3.0.x

Here is the key part.

Upgrade Plan for your Dependencies:
        - Step 1:
+               * Upgrade terasoluna-gfw from 5.7.x to 5.8.x
                * Upgrade spring-data-commons from 2.7.x to 3.0.x
                * Upgrade spring-framework from 5.3.x to 6.0.x
        - Step 2:
                * Upgrade spring-framework from 6.0.x to 6.1.x
                * Upgrade spring-data-commons from 3.0.x to 3.2.x
        - Step 3:
                * Upgrade spring-framework from 6.1.x to 6.2.x
                * Upgrade spring-data-commons from 3.2.x to 3.5.x

As this output shows, the mapping coordinates file has made it possible to define the Steps. In particular, the line Upgrade terasoluna-gfw from 5.7.x to 5.8.x shows that the update of the custom library is defined by the mapping coordinates file too. The output below appears because the mapping coordinates file does not describe TERASOLUNA 5.9.x and later.

Some upgrades were not included in the upgrade plan.
Please, upgrade and release if needed the following projects:
        * terasoluna-gfw:5.8.x
                Last version of spring-framework is 6.0.x
        * terasoluna-gfw:5.8.x
                Last version of spring-data-commons is 3.0.x

From here, run apply to make the actual changes.

advisor upgrade-plan apply

Running it with Advisor CLI 1.6.7 produced the output below.

🚀 Existing build-configuration is already up-to-date


🏃 [ 1 / 4 ] Validating syntax of upgrade mappings [00m 01s] ok
🏃 [ 2 / 4 ] Validating the license of rewrite artifacts [00m 13s] ok
🏃 [ 3 / 4 ] Fetching and processing upgrade plan details [00m 01s] ok

Projects to upgrade:
        * terasoluna-gfw from 5.7.x to 5.8.x
        * spring-data-commons from 2.7.x to 3.0.x
        * spring-framework from 5.3.x to 6.0.x

🔨 [ 4 / 4 ] Upgrading sources... [00m 18s] ok

👍 Successfully applied upgrade.


⚠️  Warnings:

* Application Advisor might produce a partial upgrade and will incrementally cover all the required changes to upgrade all the Spring projects. If you have questions or are experimenting issues upgrading your applications, please request our help or support in https://support.broadcom.com

Looking at the actual changes with git diff, here is what we see.

First, the TERASOLUNA version has been raised. Along with that, because the Spring Framework 6 line dropped Java 8 support, the Javax > Jakarta namespace change and the version refresh are carried out as well.

diff --git a/pom.xml b/pom.xml
index c170406..1d7b1e3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -14,7 +14,7 @@
   <properties>
     <maven.compiler.release>17</maven.compiler.release>
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-    <terasoluna.version>5.7.4.RELEASE</terasoluna.version>
+    <terasoluna.version>5.8.1.RELEASE</terasoluna.version>
   </properties>
 
   <dependencyManagement>
@@ -60,8 +60,8 @@
     </dependency>
 
     <dependency>
-      <groupId>javax.servlet</groupId>
-      <artifactId>javax.servlet-api</artifactId>
+      <groupId>jakarta.servlet</groupId>
+      <artifactId>jakarta.servlet-api</artifactId>
       <scope>provided</scope>
     </dependency>

Furthermore, since Spring Framework 6 no longer matches a trailing "/" in URLs by default, it helps out with that too. (This one comes from a recipe bundled with the Advisor CLI.)

diff --git a/src/main/java/com/example/demo/GreetingController.java b/src/main/java/com/example/demo/GreetingController.java
index b7d903f..09f6305 100644
--- a/src/main/java/com/example/demo/GreetingController.java
+++ b/src/main/java/com/example/demo/GreetingController.java
@@ -13,7 +13,7 @@ public class GreetingController {
         this.service = service;
     }
 
-    @GetMapping("/greet")
+    @GetMapping({"/greet", "/greet/"})
     public String greet(@RequestParam(defaultValue = "world") String who) {
         return service.greet(who);
     }

There was a time in the past when Spring Framework bean definitions were mainly written in XML rather than with JavaConfig, and that is handled as well.

diff --git a/src/main/resources/applicationContext.xml b/src/main/resources/applicationContext.xml
index a527888..f799a1e 100644
--- a/src/main/resources/applicationContext.xml
+++ b/src/main/resources/applicationContext.xml
@@ -5,11 +5,11 @@
        xmlns:mvc="http://www.springframework.org/schema/mvc"
        xsi:schemaLocation="
          http://www.springframework.org/schema/beans
-         http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
+         https://www.springframework.org/schema/beans/spring-beans.xsd
          http://www.springframework.org/schema/context
-         http://www.springframework.org/schema/context/spring-context-4.3.xsd
+         https://www.springframework.org/schema/context/spring-context.xsd
          http://www.springframework.org/schema/mvc
-         http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd">
+         https://www.springframework.org/schema/mvc/spring-mvc.xsd">
 
   <!-- The whole application context. TERASOLUNA blank projects are wired in
        XML, not with @Configuration classes. -->
diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml
index 21d02bd..82263d6 100644
--- a/src/main/webapp/WEB-INF/web.xml
+++ b/src/main/webapp/WEB-INF/web.xml
@@ -1,9 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
+<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
-                             http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
-         version="4.0">
+         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
+         version="6.0">
 
   <servlet>
     <servlet-name>dispatcher</servlet-name>

It also creates files called sql-error-codes.xml and src/main/resources/META-INF/spring.factories. These are generated to preserve Spring Framework 5 behavior. That is not the point of this article, so do try it yourself.

Running upgrade-plan with a custom recipe

Now let's go further and run it with a custom recipe added. Here I will implement 2.15. [Step 15] Migrating from Joda Time to JSR-310 from the TERASOLUNA guide.

First, reset the earlier changes for now.

git reset --hard HEAD

Registering a recipe in the mapping file

Register the recipes in the mapping file as follows.

{
  "slug": "terasoluna-gfw",
  "coordinates": [
    "org.terasoluna.gfw:terasoluna-gfw-common",
    "org.terasoluna.gfw:terasoluna-gfw-web"
  ],
  "repositoryUrl": "https://github.com/terasolunaorg/terasoluna-gfw",
  "rewrite": {
    "5.7.x": {
      "recipes": [
+       {
+         "name": "org.openrewrite.java.dependencies.UpgradeDependencyVersion",
+         "params": {
+           "groupId": "org.terasoluna.gfw",
+           "artifactId": "*",
+           "newVersion": "5.8.x"
+         }
+       },
+       {
+         "name": "org.openrewrite.java.ChangeType",
+         "params": {
+           "oldFullyQualifiedTypeName": "org.terasoluna.gfw.common.date.jodatime.JodaTimeDateFactory",
+           "newFullyQualifiedTypeName": "org.terasoluna.gfw.common.date.ClassicDateFactory"
+         }
+       },
+       {
+         "name": "org.openrewrite.xml.ChangeTagAttribute",
+         "params": {
+            "elementName": "bean",
+            "attributeName": "class",
+            "oldValue": "org.terasoluna.gfw.common.date.jodatime.DefaultJodaTimeDateFactory",
+            "newValue": "org.terasoluna.gfw.common.date.DefaultClassicDateFactory"
+          }
+        },
+        {
+          "name": "org.openrewrite.java.dependencies.RemoveDependency",
+          "params": {
+            "groupId": "org.terasoluna.gfw",
+            "artifactId": "terasoluna-gfw-jodatime"
+          }
+        }
      ],
      "nextRewrite": {
        "version": "5.8.x"
      },
      "requirements": {
        "supportedGenerations": {
          "spring-data-commons": "2.7.x",
          "spring-framework": "5.3.x",
          "spring-security": "5.7.x"
        },
        "excludedArtifacts": []
      }
    },
    "5.8.x": {
      "recipes": [],
      "nextRewrite": {
      },
      "requirements": {
        "supportedGenerations": {
          "spring-data-commons": "3.0.x",
          "spring-framework": "6.0.x",
          "spring-security": "6.0.x"
        },
        "excludedArtifacts": []
      }
    }
  }
}

Building on the previous mapping coordinates file, I have added recipes to 5.7.x. Here, as described in the TERASOLUNA migration guide, the dependency swap is expressed through recipes.

Run it again.

export SPRING_ADVISOR_MAPPING_CUSTOM_0_FILEPATH=.advisor/mappings/terasoluna.json
advisor upgrade-plan apply  

I will skip the console output and look at git diff instead. On top of the earlier result, the following has been applied, which shows that the recipe ran.

-    <!-- Joda-Time support. TERASOLUNA drops this at 5.8: the migration guide's
-         [Step 15] replaces it with the JSR-310 ClockFactory. -->
     <dependency>
-      <groupId>org.terasoluna.gfw</groupId>
-      <artifactId>terasoluna-gfw-jodatime</artifactId>
-      <version>${terasoluna.version}</version>
-    </dependency>

The code has been rewritten as well.

diff --git a/src/main/java/com/example/demo/GreetingService.java b/src/main/java/com/example/demo/GreetingService.java
index b9c704e..0cc8b2d 100644
--- a/src/main/java/com/example/demo/GreetingService.java
+++ b/src/main/java/com/example/demo/GreetingService.java
@@ -2,7 +2,7 @@ package com.example.demo;
 
 import java.util.Date;
 import org.springframework.stereotype.Service;
-import org.terasoluna.gfw.common.date.jodatime.JodaTimeDateFactory;
+import org.terasoluna.gfw.common.date.ClassicDateFactory;
 
 /**
  * TERASOLUNA's guideline marks service classes with Spring's @Service
@@ -14,9 +14,9 @@ import org.terasoluna.gfw.common.date.jodatime.JodaTimeDateFactory;
 @Service
 public class GreetingService {
 
 @Service
 public class GreetingService {
 
-    private final JodaTimeDateFactory dateFactory;
+    private final ClassicDateFactory dateFactory;
 
-    public GreetingService(JodaTimeDateFactory dateFactory) {
+    public GreetingService(ClassicDateFactory dateFactory) {
         this.dateFactory = dateFactory;
     }

With that, the upgrade we needed has been automated. There are some simple tests in the project as well, and running ./mvnw test completes without problems. (Note: the Joda Time migration done here is a fairly simple one. In practice you would follow the guide and draw up a more thorough plan.) And so we managed to upgrade TERASOLUNA, which the Advisor CLI knows nothing about.

All of the changes made up to this point can be seen here.

https://github.com/mhoshi-vm/simple-terasoluna/pull/1/changes

What does it take to use upgrade-plan in production?

Here are some other tips for taking this into production.

Managing recipes and mapping coordinates is a hassle...

Broadcom strives, as much as possible, to make recipes and mapping coordinates work by default so that users do not have to write them. So my first recommendation is to raise a support ticket with Broadcom. As long as the request is reasonable, bundling the recipes and mappings into a later release is possible.

The custom recipes and mappings shown here are really only for private dependencies that the world does not know about, or as a temporary workaround.

Writing mapping coordinates is a hassle...

You can generate the mapping coordinates file automatically with the following command.

advisor mapping create -c='org.terasoluna.gfw:terasoluna-gfw-common' < /dev/null

What you pass to -c is a Maven coordinate (groupId:artifactId).

Note that this command resolves the Maven repository from where you run it and generates the coordinates file. Here are a few things worth knowing before you run it.

When you want to find out up front which projects are not covered, advisor mapping search --prefix <prefix> is quicker.

Writing recipes is a hassle...

These days I would recommend having AI/LLMs write the recipes. There is plenty of OpenRewrite knowledge on the internet, so you can get reasonable accuracy.

Does it work in an air-gapped environment?

The Advisor CLI supports air-gapped environments.

How often is the Advisor CLI updated?

upgrade-plan is always shipped as a set with the latest recipes. Put another way, if you do not upgrade, the recipes for the newest patches are not included (for example, 1.6.5 does not include the recipe for moving to Spring Boot 4.1), which means you cannot upgrade from certain versions. So when operating upgrade-plan, updating frequently is recommended. Bugs in the Advisor CLI itself may also have been fixed, so it is best to try the latest version whenever you can.

Want to learn more ?

The following link introduces application advisor from a different perspective. Registration required but free.

Tanzu Academy : Spring Application Advisor Introduction

Wrap-up

I wrote about Application Advisor's upgrade-plan.