OpenRewrite es un software que plantea un enfoque para el mantenimiento y “modernización” (actualización) de proyectos.

Es decir:

¿Cómo lo hace?

Se basa en el concepto receta (recipe), como una receta de cocina, que contiene una serie de pasos a seguir.
Por ejemplo, si trabajamos con Spring security 5.4 y queremos pasar a la versión 5.5, iríamos a la página oficial donde encontraríamos un changelog y unas guías de migración.

En este caso, pongamos que queremos hacer lo siguiente:

  1. Actualizar la dependencia.
  2. Actualizar los objetos Java que estemos usando: pueden ser objetos que han sido sustituidos, invocaciones que ahora tienen más parámetros, lo que sea.
  3. Actualizar propiedades en nuestros ficheros de configuración (properties/yaml).

El concepto receta integraría toda la información necesaria para realizar esos pasos, de manera que solo tenemos que preocuparnos de pasar esa receta a nuestro proyecto. La receta se encargaría de hacer todos esos pasos por nosotros.

¿Quién lo hace?

Se llama OpenRewrite porque es un proyecto open source donde los desarrolladores/as de frameworks, libs… se dedican a escribir las recetas que necesitarán las migraciones de las herramientas que han desarrollado.

El propio core de OpenRewrite escribe recetas para migraciones comunes que no pertenecen a ningún framework.

Las podemos encontrar aquí:

Recetas populares

En la propia página de OpenRewrite hay una sección que nos ofrece las recetas más comunes y que suelen ser las mas buscadas.

Recetas propias

No solo podemos nutrirnos de las recetas que escriben otros, sino que también podemos escribir nuestras propias recetas. Si necesitamos unos pasos específicos para actualizar nuestros proyectos, podemos escribir nuestras recetas y ejecutarlas (por ejemplo, cambiamos objetos en libs, invocaciones...).

¿Cómo se ejecuta?

El core de OpenRewrite se basa en la ejecución de un maven-plugin. Por lo tanto, podemos hacer uso de él de la misma forma en que ejecutamos un maven-plugin.

La gente de Moderne.io dispone de un SaaS que ofrece lo mismo que el core, pero de una manera masiva. Esto es justo lo que nos interesa: pasar recetas a tantos repositorios como tengamos para que estén siempre actualizados.

El producto SaaS es de pago y su configuración es básica. Le damos credenciales para acceder a nuestros repositorios GIT y él se encargará de tener actualizado y pasar las recetas que indiquemos de manera autónoma. Ofrece estadísticas y un sinfín de cosas más.

¿Cómo funciona?

Tradicionalmente, se utilizaba Java AST (Abstract Syntax Tree) para la manipulación y creación de nodos que conformaban un archivo Java.
Con él éramos capaces de hacer transformaciones aisladas, es decir, no teníamos contexto de si nuestro archivo Java tenía sentido o relación con otros elementos dentro de nuestro proyecto.

OpenRewrite se basa en LostLees Semantic Trees. Este enfoque no solo es capaz de transformar nuestro código Java, sino que es capaz de interpretar el contexto y las relaciones entre los distintos elementos de nuestro proyecto. Entiende la semántica del código que analiza.

LST monta el árbol de relaciones de bloques de código montando una estructura de relaciones. Así, vemos no solo que lee el código y sabe qué es una variable y dónde está escrita, sino que también sabe dónde se está usando y referenciando. Lo mismo con los métodos, constructores y demás elementos del código:

Esquema de cómo se organiza openrewrite

Ejemplos sencillos

A través de varios ejemplos sencillos que puedes ir siguiendo en tu ordenador, vamos a ir probando algunas recetas públicas y vamos a escribir algunas propias.

Preparando el conejillo de indias

Todo el código de esta demo lo puedes encontrar en este repositorio.

git clone https://github.com/paradigmadigital/openrewrite-tutorial
git checkout demo1

En la branch demo1 encontraremos dos carpetas principales:

carpetas creadas: recipes / singleprojectstarter

La carpeta recipes contendrá nuestras recetas. La carpeta singleprojectstarter es un proyecto hecho son SpringInitalizer en su versión Spring boot 2.7.9 y Java 11.

Rewrite-maven

Como he comentado antes, hay muchos “proveedores” de recetas. En los siguientes ejemplos vamos a usar recetas del rewrite-maven. Estas recetas están enfocadas únicamente en modificaciones de ficheros pom.

Usando la primera receta

Si nos fijamos, el parent que trae es el de Spring Boot (2.7.9 y Java 11). Ahora, vamos a buscar una receta que sea capaz de cambiarnos el parent en las recetas disponibles en el repo oficial de OpenRewrite.

Entre las recetas precocinadas de Maven vemos que esta cambia el parent.

Esta receta es parametrizada, así que editamos el archivo recipes.yml con este contenido en la carpeta “recipes” del repo.

Así que escribimos la primera receta:

type: specs.openrewrite.org/v1beta/recipe
name: com.openrewrite.demo.Recipe1
displayName: Some recipe examples
recipeList:
  - org.openrewrite.maven.ChangeParentPom:
      oldGroupId: org.springframework.boot
      newGroupId: org.springframework.boot
      oldArtifactId: spring-boot-starter-parent
      newArtifactId: spring-boot-starter-parent
      newVersion: 2.7.13

Hay parámetros generales que debemos especificar para el recetario que estamos creando:

type: specs.openrewrite.org/v1beta/recipe
name: com.openrewrite.demo.Recipe1
displayName: Some recipe examples
recipeList:

RecipeList

En este caso, nuestro recetario tiene una única receta de tipo org.openrewrite.maven.ChangeParentPom, que es una receta parametrizada:

type: specs.openrewrite.org/v1beta/recipe
name: com.openrewrite.demo.Recipe1
displayName: Some recipe examples
recipeList:
  - org.openrewrite.maven.ChangeParentPom:
      oldGroupId: org.springframework.boot
      newGroupId: org.springframework.boot
      oldArtifactId: spring-boot-starter-parent
      newArtifactId: spring-boot-starter-parent
      newVersion: 2.7.13

Ejecutando la receta sobre el proyecto

Como indicamos al principio, vamos a usar el rewrite-maven-plugin y, por lo tanto, necesitamos estar en el directorio del proyecto donde vamos a ejecutar el plugin.

cd singleprojectstarter

mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=org.openrewrite:rewrite-maven:8.1.2 \
-Drewrite.activeRecipes=com.openrewrite.demo.Recipe1 \
-Drewrite.configLocation=../recipes/recipes.yaml

Argumentos:

🚨Warning “proyectos multimódulo”:

Cuando nuestro proyecto es multimódulo, es necesario que la ruta del configLocation sea absoluta, ya que se ejecutará la receta para el módulo Maven padre y todos sus módulos hijos. Si la ruta fuera relativa, cuando entrara la ejecución del plugin en el módulo hijo, no cuadraría. Poniendo la ruta absoluta nos quitamos de problemas.

El final de la ejecución nos muestra un resumen de las recetas ejecutadas y los cambios:

...
[INFO] Validating active recipes...
[INFO] Project [singleprojectstarter] Resolving Poms...
[INFO] Project [singleprojectstarter] Parsing source files
[INFO] Running recipe(s)...
[WARNING] Changes have been made to singleprojectstarter/pom.xml by:
[WARNING]     com.openrewrite.demo.Recipe1
[WARNING]         org.openrewrite.maven.ChangeParentPom: {oldGroupId=org.springframework.boot, newGroupId=org.springframework.boot, oldArtifactId=spring-boot-starter-parent, newArtifactId=spring-boot-starter-parent, newVersion=2.7.13}
[WARNING] Please review and commit the results.
[WARNING] Estimate time saved: 5m
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  5.911 s
[INFO] Finished at: 2024-05-30T12:39:43+02:00
...

Podemos ver el diff y los cambios que ha realizado:

--- a/singleprojectstarter/pom.xml
+++ b/singleprojectstarter/pom.xml
@@ -5,7 +5,7 @@
        <parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
-               <version>2.7.9</version>
+               <version>2.7.13</version>
                <relativePath/> <!-- lookup parent from repository -->
        </parent>

Es poco, pero ya hemos ejecutado una receta parametrizada.

Recapitulando

Hasta ahora, hemos visto que:

Actualizar Parent de un proyecto

Ahora, le voy a decir que me suba a la última versión del Spring Boot a la versión 3.

En el ejemplo anterior también hemos subido de versión, pero la receta era ChangeParentPom. Al no cambiar el groupId ni el artifactId no hemos cambiado el parent, sino que lo hemos actualizado.

Para ello, tenemos [recetas precompiladas del rewrite-maven-plugin]. Esta receta es más correcta para este propósito y, si nos fijamos en su parametrización, no deja cambiar el groupId ni el artifactId. Solo tiene 3 parámetros que indican que, si el parent del proyecto tiene ese groupId y ese artifactId, debe migrarlo a la nueva versión 3.1.0.

      groupId: org.springframework.boot
      artifactId: spring-boot-starter-parent
      newVersion: 3.1.0 

Así que añadimos otro paso a nuestra receta com.openrewrite.demo.Recipe1, para ejecutar org.openrewrite.maven.UpgradeParentVersion.

Indicamos la versión que queremos subir y nos quedará así:

type: specs.openrewrite.org/v1beta/recipe
name: com.openrewrite.demo.Recipe1
displayName: Change Maven Parent Pom
recipeList:
  - org.openrewrite.maven.ChangeParentPom:
      oldGroupId: org.springframework.boot
      newGroupId: org.springframework.boot
      oldArtifactId: spring-boot-starter-parent
      newArtifactId: spring-boot-starter-parent
      newVersion: 2.7.13
  - org.openrewrite.maven.UpgradeParentVersion:
      groupId: org.springframework.boot
      artifactId: spring-boot-starter-parent
      newVersion: 3.1.0

Reiniciamos los cambios (git stash) y vamos a ver que se ejecutan las recetas en orden:

git stash

Como no ha cambiado el nombre de la receta, volvemos a ejecutar lo mismo:

mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=org.openrewrite:rewrite-maven:8.1.2 \
-Drewrite.activeRecipes=com.openrewrite.demo.Recipe1 \
-Drewrite.configLocation=../recipes/recipes.yaml

...

[INFO] Validating active recipes...
[INFO] Project [singleprojectstarter] Resolving Poms...
[INFO] Project [singleprojectstarter] Parsing source files
[INFO] Running recipe(s)...
[WARNING] Changes have been made to singleprojectstarter/pom.xml by:
[WARNING]     com.openrewrite.demo.Recipe1
[WARNING]         org.openrewrite.maven.ChangeParentPom: {oldGroupId=org.springframework.boot, newGroupId=org.springframework.boot, oldArtifactId=spring-boot-starter-parent, newArtifactId=spring-boot-starter-parent, newVersion=2.7.13}
[WARNING]         org.openrewrite.maven.UpgradeParentVersion: {groupId=org.springframework.boot, artifactId=spring-boot-starter-parent, newVersion=3.1.0}
[WARNING] Please review and commit the results.
[WARNING] Estimate time saved: 5m

Vemos que ha ejecutado la recipeList en orden. Primero subió a la 2.7.9 y, en el siguiente paso, a la 3.1.0.

Si vemos el diff, ha subido la versión (ahora tenemos la 3.1.0), que era la foto final que queríamos:

--- a/singleprojectstarter/pom.xml
+++ b/singleprojectstarter/pom.xml
@@ -5,7 +5,7 @@
        <parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
-               <version>2.7.9</version>
+               <version>3.1.0</version>
                <relativePath/> <!-- lookup parent from repository -->
        </parent>

Hacemos git stash para volver a la foto inicial.

Actualizando properties

Encontramos otra receta de actualización o añadido de property. Vamos a aprovecharla para subir a Java 17.

En este caso, no queremos que esté asociada a la receta del com.openrewrite.demo.ChangeParentPom. Queremos tenerla en el recetario, pero separada para poder ejecutarla aisladamente.

Así que, en el mismo fichero, añadimos “—” para indicar que son parámetros de otra receta e insertamos nuestros datos.

El fichero final quedará así:

type: specs.openrewrite.org/v1beta/recipe
name: com.openrewrite.demo.Recipe1
displayName: Change Maven Parent Pom
recipeList:
  - org.openrewrite.maven.ChangeParentPom:
      oldGroupId: org.springframework.boot
      newGroupId: org.springframework.boot
      oldArtifactId: spring-boot-starter-parent
      newArtifactId: spring-boot-starter-parent
      newVersion: 2.7.13
  - org.openrewrite.maven.UpgradeParentVersion:
      groupId: org.springframework.boot
      artifactId: spring-boot-starter-parent
      newVersion: 3.1.0 

type: specs.openrewrite.org/v1beta/recipe
name: com.openrewrite.demo.Recipe2
displayName: Update java property to 17
recipeList:
  - org.openrewrite.maven.AddProperty:
      key: java.version
      value: 17
      preserveExistingValue: false
      trustParent: false  

Ahora vamos a ejecutar la receta com.openrewrite.demo.UpdateJava17, que vemos que hace uso de la receta interna org.openrewrite.maven.AddProperty:

mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
 -Drewrite.recipeArtifactCoordinates=org.openrewrite:rewrite-maven:8.1.2 \
 -Drewrite.activeRecipes=com.openrewrite.demo.Recipe2 \
 -Drewrite.configLocation=../recipes/recipes.yaml

[INFO] Validating active recipes...
[INFO] Project [singleprojectstarter] Resolving Poms...
[INFO] Project [singleprojectstarter] Parsing source files
[INFO] Running recipe(s)...
[WARNING] Changes have been made to singleprojectstarter/pom.xml by:
[WARNING]     com.openrewrite.demo.Recipe2
[WARNING]         org.openrewrite.maven.AddProperty: {key=java.version, value=17, preserveExistingValue=false, trustParent=false}
[WARNING] Please review and commit the results.
[WARNING] Estimate time saved: 5m
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS

Vemos en el diff que se ha actualizado.

@@ -14,7 +14,7 @@
        <name>singleprojectstarter</name>
        <description>Demo project for Spring Boot</description>
        <properties>
-               <java.version>11</java.version>
+               <java.version>17</java.version>
        </properties>
        <dependencies>

Añadiendo dependencias

Vamos a darle una naturaleza web añadiendo el spring-boot-starter-web.

Encontramos la receta y, como esta receta tiene entidad propia, creamos una definición con el nombre com.openrewrite.demo.AddWebNature en nuestro fichero de configuración.

Añadimos:

type: specs.openrewrite.org/v1beta/recipe
name: com.openrewrite.demo.AddWebNature
displayName: Add Web nature
recipeList:
  - org.openrewrite.maven.AddDependency:
      groupId: org.springframework.boot
      artifactId: spring-boot-starter-web
      version: 3.1.0
      onlyIfUsing: org.springframework.boot.*
      scope: compile
      acceptTransitive: true 

💡“El árbol LST es muy listo”

A veces queremos tener la dependencia pero no la usamos en código. En este caso, le estoy “engañando” indicando que sí que tengo un import de org.springframework.boot en la clase Application.java. Al montar el árbol, OpenRewrite verá que sí se está usando ese import y, entonces, sí que ejecutará la receta añadiendo la dependencia.

Ejecutamos la receta com.openrewrite.demo.AddWebNature:

mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
 -Drewrite.recipeArtifactCoordinates=org.openrewrite:rewrite-maven:8.1.2 \
 -Drewrite.activeRecipes=com.openrewrite.demo.AddWebNature \
 -Drewrite.configLocation=../recipes/recipes.yaml

[INFO] Running recipe(s)...
[WARNING] Changes have been made to singleprojectstarter/pom.xml by:
[WARNING]     com.openrewrite.demo.AddWebNature
[WARNING]         org.openrewrite.maven.AddDependency: {groupId=org.springframework.boot, artifactId=spring-boot-starter-web, version=3.1.0, scope=compile, onlyIfUsing=org.springframework.boot.*, acceptTransitive=true}
[WARNING] Please review and commit the results.
[WARNING] Estimate time saved: 5m

Vemos el diff y vemos que ha metido la dependencia.

        </properties>
        <dependencies>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter</artifactId>
                </dependency>
+               <dependency>
+                       <groupId>org.springframework.boot</groupId>
+                       <artifactId>spring-boot-starter-web</artifactId>
+               </dependency>
                <dependency>

Quitando dependencias

Imaginemos que tenemos que retroceder una versión de una lib determinada porque tiene un bug. Vamos a llamarla WebErrorWorkAround.

En nuestra intervención, tenemos que hacer estos pasos:

  1. Tenemos que borrar la versión.
  2. Añadir otra versión de la lib
  3. Añadir un comentario en el pom.xml indicando el porqué de ese cambio.

Usamos el step de borrar y el de añadir, en ese orden. La receta nos quedará así (la añadimos al recetario):

type: specs.openrewrite.org/v1beta/recipe
name: com.openrewrite.demo.WebErrorWorkAround
displayName: Workaround for Web error on latest version
recipeList:
  - org.openrewrite.maven.RemoveDependency:
      groupId: org.springframework.boot
      artifactId: spring-boot-starter-web
      scope: compile
  - org.openrewrite.maven.AddDependency:
      groupId: org.springframework.boot
      artifactId: spring-boot-starter-web
      version: 3.1.1
      scope: runtime
      onlyIfUsing: org.springframework.boot.test.context.*
      type: jar
      classifier: ''
      optional: null
      acceptTransitive: false 
  - org.openrewrite.maven.AddCommentToMavenDependency:
      xPath: /project/dependencies/dependency
      groupId: org.springframework.boot
      artifactId: spring-boot-starter-web
      commentText: This is excluded due to CVE <X> and will be removed when we upgrade the next version is available.

Como podemos ver, quitamos el starter web, añadimos el starter web en otra versión y añadimos un comentario al pom.xml indicando por qué se ha realizado este cambio usando la receta pública org.openrewrite.maven.AddCommentToMavenDependency.

Ejecutamos la receta com.openrewrite.demo.WebErrorWorkAround:

mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
 -Drewrite.recipeArtifactCoordinates=org.openrewrite:rewrite-maven:8.1.2 \
 -Drewrite.activeRecipes=com.openrewrite.demo.WebErrorWorkAround \
  -Drewrite.configLocation=../recipes/recipes.yaml

[INFO] Running recipe(s)...
[WARNING] Changes have been made to singleprojectstarter/pom.xml by:
[WARNING]     com.openrewrite.demo.WebErrorWorkAround
[WARNING]         org.openrewrite.maven.RemoveDependency: {groupId=org.springframework.boot, artifactId=spring-boot-starter-web, scope=compile}
[WARNING]         org.openrewrite.maven.AddDependency: {groupId=org.springframework.boot, artifactId=spring-boot-starter-web, version=3.1.1, scope=runtime, onlyIfUsing=org.springframework.boot.test.context.*, type=jar, classifier=, acceptTransitive=false}
[WARNING]         org.openrewrite.maven.AddCommentToMavenDependency: {xPath=/project/dependencies/dependency, groupId=org.springframework.boot, artifactId=spring-boot-starter-web, commentText=This is excluded due to CVE <X> and will be removed when we upgrade the next version is available.}
[WARNING] Please review and commit the results.
[WARNING] Estimate time saved: 5m
Vemos el diff y encontramos la nueva versión del springboot-starter-web y el comentario añadido.
        <dependencies>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter</artifactId>
                </dependency>
+               <dependency>
+                       <!--This is excluded due to CVE <X> and will be removed when we upgrade the next version is available.-->
+                       <groupId>org.springframework.boot</groupId>
+                       <artifactId>spring-boot-starter-web</artifactId>
+                       <version>3.1.1</version>
+                       <classifier></classifier>
+                       <scope>runtime</scope>
+               </dependency>

En resumen

El contenido total de nuestro recipes.yaml es este:

type: specs.openrewrite.org/v1beta/recipe
name: com.openrewrite.demo.Recipe1
displayName: Change Maven Parent Pom
recipeList:
- org.openrewrite.maven.ChangeParentPom:
    oldGroupId: org.springframework.boot
    newGroupId: org.springframework.boot
    oldArtifactId: spring-boot-starter-parent
    newArtifactId: spring-boot-starter-parent
    newVersion: 2.7.13
- org.openrewrite.maven.UpgradeParentVersion:
    groupId: org.springframework.boot
    artifactId: spring-boot-starter-parent
    newVersion: 3.1.0  

type: specs.openrewrite.org/v1beta/recipe
name: com.openrewrite.demo.Recipe2
displayName: Update java property to 17
recipeList:
- org.openrewrite.maven.AddProperty:
    key: java.version
    value: 17
    preserveExistingValue: false
    trustParent: false 

type: specs.openrewrite.org/v1beta/recipe
name: com.openrewrite.demo.AddWebNature
displayName: Add Web nature
recipeList:
- org.openrewrite.maven.AddDependency:
    groupId: org.springframework.boot
    artifactId: spring-boot-starter-web
    version: 3.1.0
    onlyIfUsing: org.springframework.boot.*
    scope: compile
    acceptTransitive: true 

type: specs.openrewrite.org/v1beta/recipe
name: com.openrewrite.demo.WebErrorWorkAround
displayName: Workaround for Web error on latest version
recipeList:
- org.openrewrite.maven.RemoveDependency:
    groupId: org.springframework.boot
    artifactId: spring-boot-starter-web
    scope: compile
- org.openrewrite.maven.AddDependency:
    groupId: org.springframework.boot
    artifactId: spring-boot-starter-web
    version: 3.1.1
    scope: runtime
    onlyIfUsing: org.springframework.boot.test.context.*
    type: jar
    classifier: ''
    optional: null
    acceptTransitive: false 
- org.openrewrite.maven.AddCommentToMavenDependency:
    xPath: /project/dependencies/dependency
    groupId: org.springframework.boot
    artifactId: spring-boot-starter-web
    commentText: This is excluded due to CVE <X> and will be removed when we upgrade the next version is available.

Hasta aquí hemos visto cómo podemos ir ejecutando recetas en un proyecto Maven y también algunas de las recetas comunes, cómo configurarlas y cómo ejecutarlas. En el siguiente post vamos a ver recetas específicas de Spring Framework.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete