Migrates Java 8/11 apps to Java 25 and javax.* to jakarta.* namespaces using OpenRewrite recipes. Detects Java <25 in pom.xml or javax imports in source files.
npx claudepluginhub sap-samples/btp-neo-java-app-migration --plugin sap-btp-neo-migrationThis skill is limited to using the following tools:
Migrate your Neo Java application from Java 8/11 and Java EE to Java 25 and Jakarta EE 10.
Guides Next.js Cache Components and Partial Prerendering (PPR): 'use cache' directives, cacheLife(), cacheTag(), revalidateTag() for caching, invalidation, static/dynamic optimization. Auto-activates on cacheComponents: true.
Processes PDFs: extracts text/tables/images, merges/splits/rotates pages, adds watermarks, creates/fills forms, encrypts/decrypts, OCRs scans. Activates on PDF mentions or output requests.
Share bugs, ideas, or general feedback.
Migrate your Neo Java application from Java 8/11 and Java EE to Java 25 and Jakarta EE 10.
Upgrade the Java runtime and migrate from javax.* namespaces to jakarta.* namespaces for forward compatibility with modern application servers and Cloud Foundry buildpacks.
This skill applies if any of these patterns are found:
<!-- Java 8 or 11 source/target -->
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<!-- OR -->
<properties>
<java.version>11</java.version>
</properties>
// javax.* imports indicate pre-Jakarta EE
import javax.servlet.*;
import javax.persistence.*;
import javax.annotation.*;
import javax.mail.*;
import javax.ejb.*;
Before making any changes, create a copy of the application directory as a sibling folder. All migration work is done on this copy — the original stays untouched.
APP_DIR=$(pwd)
APP_NAME=$(basename "$APP_DIR")
COPY_DIR="$(dirname "$APP_DIR")/${APP_NAME}-cf-migration"
if [ -d "$COPY_DIR" ]; then
echo "Migration copy already exists at $COPY_DIR — using it."
else
cp -r "$APP_DIR" "$COPY_DIR"
echo "Created migration copy at $COPY_DIR"
fi
cd "$COPY_DIR"
Now that we are inside the copy, create the .migration/ directory and save the config there:
mkdir -p .migration
Save the paths to .migration/cf-migration-config.json (create or update the file):
{
"sourceAppDir": "<original APP_DIR>",
"migrationAppDir": "<COPY_DIR>"
}
All subsequent steps in this skill and all downstream skills (
sdk-replacement,authentication-xsuaa,mta-descriptor, etc.) must operate inside$COPY_DIR. The.migration/directory is inside the copy, not the original.
Before:
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
After:
<properties>
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
</properties>
Execute the OpenRewrite recipes to automatically migrate code:
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-migrate-java:RELEASE \
-Drewrite.activeRecipes=org.openrewrite.java.migrate.UpgradeToJava25 \
-Drewrite.activeRecipes=org.openrewrite.java.migrate.jakarta.JakartaEE10 \
-Drewrite.exportDatatables=true
Note:
- Make sure you have Java SE 25 and the latest Maven version installed
- The recipe artifact coordinates ensure the latest migration recipes are downloaded
This command will:
javax.* imports with jakarta.*Important: The
org.apache.chemistry.opencmislibraries have NOT been migrated to Jakarta EE. If your application uses OpenCMIS (Document Management), you must exclude it from the Jakarta migration.
Check for OpenCMIS usage:
grep -r "opencmis" pom.xml
grep -r "org.apache.chemistry" src/main/java/
If OpenCMIS is used, add these specific exclusions:
<dependency>
<groupId>org.apache.chemistry.opencmis</groupId>
<artifactId>chemistry-opencmis-client-impl</artifactId>
<version>${opencmis.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-frontend-jaxws</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-transports-http</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-ws-policy</artifactId>
</exclusion>
</exclusions>
</dependency>
Note: Add the excluded libraries as separate dependencies using their latest versions, if applicable.
Before:
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
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">
After:
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
version="6.0">
| Before (javax.*) | After (jakarta.*) |
|---|---|
javax.servlet.* | jakarta.servlet.* |
javax.servlet.http.* | jakarta.servlet.http.* |
javax.persistence.* | jakarta.persistence.* |
javax.annotation.* | jakarta.annotation.* |
javax.ejb.* | jakarta.ejb.* |
javax.mail.* | jakarta.mail.* |
javax.ws.rs.* | jakarta.ws.rs.* |
javax.json.* | jakarta.json.* |
javax.validation.* | jakarta.validation.* |
com.fasterxml.jackson.jaxrs.* | com.fasterxml.jackson.jakarta.rs.* |
Important — Jackson JAX-RS providers: The Jackson JAX-RS provider artifact changed from
jackson-jaxrs-json-provider(javax) tojackson-jakarta-rs-json-provider(jakarta). In addition to the package rename, the classJacksonJaxbJsonProviderwas renamed toJacksonXmlBindJsonProvider. The simplerJacksonJsonProvider(which supports JAXB annotations too) is the recommended replacement. OpenRewrite will update Javaimportstatements, but it does not update class references inweb.xml. If yourweb.xmlconfigures JAX-RS providers via<init-param>, you must update those manually.
Detect:
grep -r "com.fasterxml.jackson.jaxrs" --include="*.xml" src/main/webapp/
If this returns results, update the class name:
Before:
<init-param>
<param-name>jaxrs.providers</param-name>
<param-value>
com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider
</param-value>
</init-param>
After:
<init-param>
<param-name>jaxrs.providers</param-name>
<param-value>
com.fasterxml.jackson.jakarta.rs.json.JacksonJsonProvider
</param-value>
</init-param>
Note: The available classes in the Jakarta artifact are
JacksonJsonProvider(recommended) andJacksonXmlBindJsonProvider. The old nameJacksonJaxbJsonProviderdoes not exist in the Jakarta artifact — using it will causeClassNotFoundExceptionat runtime even though the JAR is in the WAR.
Also update the Maven dependency:
Before:
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-json-provider</artifactId>
</dependency>
After:
<dependency>
<groupId>com.fasterxml.jackson.jakarta.rs</groupId>
<artifactId>jackson-jakarta-rs-json-provider</artifactId>
</dependency>
When migrating to Jakarta EE 10, CXF must be upgraded from 3.x to 4.x (CXF 3.x uses javax.ws.rs.*, which is incompatible). In CXF 4.x, the Swagger 2 module (cxf-rt-rs-service-description-swagger) was removed and replaced by the OpenAPI v3 module.
This step is conditional. Only apply if the detection check below returns results.
# Check for Swagger 2 CXF module in pom.xml
find . -name "pom.xml" -not -path "*/target/*" -exec grep -l "cxf-rt-rs-service-description-swagger" {} \;
# Check for Swagger 2 annotations in Java code
grep -r "io.swagger.annotations" --include="*.java" .
# Check for Swagger2Feature in Java code or web.xml
grep -r "Swagger2Feature" --include="*.java" --include="*.xml" .
If any of the above commands return results, apply the transformations below.
Before:
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-rs-service-description-swagger</artifactId>
<version>${cxf.version}</version>
</dependency>
After:
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-rs-service-description-openapi-v3</artifactId>
<version>${cxf.version}</version>
</dependency>
Note: Ensure
${cxf.version}is 4.x or later (e.g.,4.0.5). CXF 3.x does not have the openapi-v3 module.
| Before (Swagger 2) | After (OpenAPI v3) |
|---|---|
import io.swagger.annotations.Api | import io.swagger.v3.oas.annotations.tags.Tag |
import io.swagger.annotations.ApiOperation | import io.swagger.v3.oas.annotations.Operation |
import io.swagger.annotations.ApiParam | import io.swagger.v3.oas.annotations.Parameter |
import io.swagger.annotations.ApiResponse | import io.swagger.v3.oas.annotations.responses.ApiResponse |
@Api(value = "...") | @Tag(name = "...") |
@ApiOperation(value = "...") | @Operation(summary = "...") |
@ApiParam(value = "...") | @Parameter(description = "...") |
If CXF features are registered in code (e.g., ServiceRegistry or Application subclass):
Before:
import org.apache.cxf.jaxrs.swagger.Swagger2Feature;
Swagger2Feature feature = new Swagger2Feature();
feature.setBasePath("/api");
After:
import org.apache.cxf.jaxrs.openapi.OpenApiFeature;
OpenApiFeature feature = new OpenApiFeature();
CXF's OpenApiFeature transitively depends on jakarta.xml.ws.WebServiceFeature. In many projects, jakarta.xml.ws-api is declared with <scope>provided</scope>, assuming the application server or buildpack supplies it. However, sap_java_buildpack_jakarta does not provide jakarta.xml.ws-api. This causes a runtime crash:
NoClassDefFoundError: jakarta/xml/ws/WebServiceFeature
Detect:
# Check if jakarta.xml.ws-api is at provided scope
find . -name "pom.xml" -not -path "*/target/*" -exec grep -B2 -A2 "jakarta.xml.ws-api" {} \;
If the dependency exists with <scope>provided</scope>, remove the scope (or change to compile) so it gets packaged into the WAR:
Before:
<dependency>
<groupId>jakarta.xml.ws</groupId>
<artifactId>jakarta.xml.ws-api</artifactId>
<version>4.0.2</version>
<scope>provided</scope>
</dependency>
After:
<dependency>
<groupId>jakarta.xml.ws</groupId>
<artifactId>jakarta.xml.ws-api</artifactId>
<version>4.0.2</version>
</dependency>
Note: This applies specifically when deploying to
sap_java_buildpack_jakarta. Other application servers (e.g., full Jakarta EE servers like TomEE) may provide this API, but the SAP Java Buildpack with Tomcat does not.
If the application configures OpenAPI/Swagger via web.xml servlet:
Before:
<servlet>
<servlet-name>Swagger</servlet-name>
<servlet-class>io.swagger.jaxrs.listing.ApiListingResource</servlet-class>
</servlet>
After:
<servlet>
<servlet-name>OpenApi</servlet-name>
<servlet-class>io.swagger.v3.jaxrs2.integration.OpenApiServlet</servlet-class>
<init-param>
<param-name>openApi.configuration.resourcePackages</param-name>
<param-value>your.api.package</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>OpenApi</servlet-name>
<url-pattern>/openapi/*</url-pattern>
</servlet-mapping>
The original commons-fileupload library depends on javax.servlet and cannot compile against Jakarta Servlet 6.0. The replacement is commons-fileupload2-jakarta-servlet6, which has breaking API changes.
This step is conditional. Only apply if the detection check below returns results.
# Check for old commons-fileupload dependency in pom.xml
find . -name "pom.xml" -not -path "*/target/*" -exec grep -l "<artifactId>commons-fileupload</artifactId>" {} \;
# Check for old fileupload imports in Java code
grep -r "org.apache.commons.fileupload\." --include="*.java" . | grep -v "fileupload2"
If any of the above commands return results, apply the transformations below.
Before:
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
After:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-fileupload2-jakarta-servlet6</artifactId>
<version>2.0.0-M2</version>
</dependency>
Note: Check Maven Central for the latest version.
| Before | After |
|---|---|
org.apache.commons.fileupload.FileItem | org.apache.commons.fileupload2.core.FileItem |
org.apache.commons.fileupload.FileItemFactory | org.apache.commons.fileupload2.core.DiskFileItemFactory |
org.apache.commons.fileupload.FileUploadException | org.apache.commons.fileupload2.core.FileUploadException |
org.apache.commons.fileupload.disk.DiskFileItemFactory | org.apache.commons.fileupload2.core.DiskFileItemFactory |
org.apache.commons.fileupload.servlet.ServletFileUpload | org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletFileUpload |
The DiskFileItemFactory constructor changed to a builder pattern:
Before:
FileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
After:
DiskFileItemFactory factory = DiskFileItemFactory.builder().get();
JakartaServletFileUpload upload = new JakartaServletFileUpload(factory);
FileItem.get() and FileItem.delete() now throw IOException in fileupload2. Any code calling these methods must add exception handling:
Before:
byte[] data = fileItem.get();
fileItem.delete();
After:
try {
byte[] data = fileItem.get();
} catch (IOException e) {
throw new RuntimeException("Failed to read uploaded file", e);
}
try {
fileItem.delete();
} catch (IOException e) {
// log and ignore, or handle as needed
}
Alternatively, if the calling method can propagate IOException, add it to the throws clause.
Ensure dependencies use Jakarta EE 10 compatible versions:
<dependencies>
<!-- Servlet API -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
<!-- JPA (if used) — do NOT use provided scope, Tomcat does not provide JPA -->
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>3.1.0</version>
</dependency>
<!-- Annotations -->
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
After OpenRewrite runs, test code with hand-written servlet mocks or stubs will fail to compile for two reasons:
@Override on methods that no longer exist in the interfaceNote: OpenRewrite does not automatically fix either of these issues in hand-written mock classes.
# Find test classes implementing servlet interfaces directly
# Search from project root to handle multi-module projects
grep -rl "implements.*HttpServletRequest\|implements.*HttpServletResponse\|implements.*ServletContext\|implements.*ServletRequest\|implements.*HttpSession" --include="*.java" .
If this returns results, those files need fixing.
Jakarta Servlet 6.0 removed several deprecated methods. If your mock overrides these, the @Override annotation will cause a compile error because the method no longer exists in the interface. Remove the @Override annotation (you can keep the method body if your tests call it directly, or delete the method entirely).
Methods removed from HttpServletRequest (inherited from ServletRequest):
// REMOVE @Override — method removed in Servlet 6.0
// @Override <-- delete this line
public String getRealPath(String path) {
return null;
}
Methods removed from HttpServletRequest:
// REMOVE @Override — method removed in Servlet 6.0
// @Override <-- delete this line
public boolean isRequestedSessionIdFromUrl() {
return false;
}
Methods removed from ServletContext:
// REMOVE @Override — method removed in Servlet 6.0
// @Override <-- delete this line
public String getRealPath(String path) {
return null;
}
Tip: Compile first (
mvn test-compile) and look for errors likemethod does not override or implement a method from a supertype. Each such error means@Overridemust be removed.
For each affected class, add the missing method implementations that throw UnsupportedOperationException (or return a sensible default). These are the new abstract methods added in Jakarta Servlet 6.0:
HttpServletRequest (new in Servlet 6.0 — also satisfies ServletRequest):
@Override
public String getRequestId() {
throw new UnsupportedOperationException();
}
@Override
public String getProtocolRequestId() {
throw new UnsupportedOperationException();
}
@Override
public jakarta.servlet.ServletConnection getServletConnection() {
throw new UnsupportedOperationException();
}
ServletContext (new in Servlet 6.0):
@Override
public String getRequestCharacterEncoding() {
return null;
}
@Override
public void setRequestCharacterEncoding(String encoding) {
// no-op
}
@Override
public String getResponseCharacterEncoding() {
return null;
}
@Override
public void setResponseCharacterEncoding(String encoding) {
// no-op
}
@Override
public int getSessionTimeout() {
return 0;
}
@Override
public void setSessionTimeout(int sessionTimeout) {
// no-op
}
@Override
public jakarta.servlet.ServletRegistration.Dynamic addJspFile(String servletName, String jspFile) {
return null;
}
HttpServletResponse (new in Servlet 6.0):
@Override
public void sendRedirect(String location, int sc, boolean clearBuffer) throws IOException {
throw new UnsupportedOperationException();
}
HttpSession (new in Servlet 6.0):
@Override
public Accessor getAccessor() {
throw new UnsupportedOperationException();
}
Note: The exact set of missing methods depends on which interfaces your test code implements and which methods were already overridden. The compiler error messages will tell you exactly which methods are missing.
If the project uses Mockito (or can add it), replace hand-written mock classes entirely. This is future-proof against further interface evolution.
CRITICAL: If the project already uses Mockito 1.x (
mockito-all), you must upgrade to Mockito 5.x. Mockito 1.x uses cglib for bytecode generation, which is blocked by Java 25's module system (InaccessibleObjectException/IllegalAccessErrorat test runtime). Mockito 5.x uses ByteBuddy, which supports Java 25+.
# Check for old mockito-all (Mockito 1.x bundled cglib)
find . -name "pom.xml" -not -path "*/target/*" -exec grep -l "mockito-all" {} \;
# Check for outdated mockito-core (pre-5.x)
find . -name "pom.xml" -not -path "*/target/*" -exec grep -A1 "mockito-core" {} \; | grep -E "<version>[1-4]"
If either returns results, upgrade:
Before:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.10.19</version>
<scope>test</scope>
</dependency>
After:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.14.2</version>
<scope>test</scope>
</dependency>
Note:
mockito-allwas a fat JAR that bundled cglib and other dependencies. It was discontinued.mockito-coreis the correct artifact for Mockito 5.x. The core API (spy,doReturn,when,mock) is unchanged, but several supporting APIs have breaking changes — see below.
Replace hand-written mocks (optional but recommended):
Before (hand-written mock):
public class MockHttpServletRequest implements HttpServletRequest {
private Map<String, String> parameters = new HashMap<>();
// ... dozens of manually implemented methods ...
}
After (Mockito):
import static org.mockito.Mockito.*;
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getParameter("name")).thenReturn("value");
when(request.getMethod()).thenReturn("GET");
Mockito 5.x removed and relocated several deprecated APIs. These cause compile errors after upgrading from Mockito 1.x/2.x/3.x:
org.mockito.Matchers → org.mockito.ArgumentMatchersThe Matchers class was removed in Mockito 5.x. All static imports must be updated:
Detect:
grep -r "org.mockito.Matchers" --include="*.java" src/test/
Fix (bulk replacement):
grep -rl "org.mockito.Matchers" --include="*.java" src/test/ | xargs sed -i 's/org\.mockito\.Matchers/org.mockito.ArgumentMatchers/g'
org.mockito.runners.MockitoJUnitRunner → org.mockito.junit.MockitoJUnitRunnerThe runners sub-package was removed:
Detect:
grep -r "org.mockito.runners.MockitoJUnitRunner" --include="*.java" src/test/
Fix (bulk replacement):
grep -rl "org.mockito.runners.MockitoJUnitRunner" --include="*.java" src/test/ | xargs sed -i 's/org\.mockito\.runners\.MockitoJUnitRunner/org.mockito.junit.MockitoJUnitRunner/g'
UnnecessaryStubbingExceptionMockito 5.x defaults to strict stubbing when using @RunWith(MockitoJUnitRunner.class). Tests that set up when(...) stubs that are never called will fail with UnnecessaryStubbingException at runtime.
Detect: Run mvn test and look for errors containing unnecessary Mockito stubbings.
Fix: Switch affected test classes to lenient mode:
Before:
@RunWith(MockitoJUnitRunner.class)
public class MyServiceTest {
After:
@RunWith(MockitoJUnitRunner.Silent.class)
public class MyServiceTest {
Note: The ideal fix is to remove the unnecessary stubs, but
Silent.classis a safe quick fix that preserves test behavior. Only use it for tests that actually have unused stubs — do not apply it to all test classes preemptively.
If the project uses nl.jqno.equalsverifier version 3.x+, several API changes may break tests:
Detect:
# Check for equalsverifier in pom.xml
grep -A1 "equalsverifier" pom.xml | grep "<version>"
# Check for removed API usage
grep -r "allFieldsShouldBeUsed" --include="*.java" src/test/
allFieldsShouldBeUsed() removedIn EqualsVerifier 3.x, allFieldsShouldBeUsed() was removed because it is now the default behavior. Remove the call:
Before:
EqualsVerifier.forClass(MyClass.class)
.allFieldsShouldBeUsed()
.usingGetClass()
.suppress(Warning.NONFINAL_FIELDS)
.verify();
After:
EqualsVerifier.forClass(MyClass.class)
.usingGetClass()
.suppress(Warning.NONFINAL_FIELDS)
.verify();
EqualsVerifier 3.x may flag fields not used in equals() or classes that inherit equals() directly from Object. Suppress with the appropriate Warning:
// If equals() doesn't use all fields:
.suppress(Warning.ALL_FIELDS_SHOULD_BE_USED)
// If the class doesn't override equals() at all:
.suppress(Warning.INHERITED_DIRECTLY_FROM_OBJECT)
mvn test-compile
This must succeed before proceeding. If it fails, read the compiler errors — they will list the exact missing methods for each class.
Java 25 removed the javax.xml.bind module (JAXB) from the JDK. Libraries that depend on JAXB — such as Apache POI (OOXML/PPTX/XLSX processing), JAXB-based XML marshalling, or older SOAP clients — will fail at runtime with ClassNotFoundException: javax.xml.bind.JAXBException.
This step is conditional. Only apply if the detection check below returns results.
# Check for libraries known to need javax.xml.bind
find . -name "pom.xml" -not -path "*/target/*" -exec grep -l -E "apache.*poi|jaxb|jaxws|javax.xml.bind" {} \;
# Check for direct javax.xml.bind usage in Java code
grep -r "javax.xml.bind" --include="*.java" .
# Check for POI OOXML (commonly needs JAXB)
find . -name "pom.xml" -not -path "*/target/*" -exec grep -l "poi-ooxml" {} \;
If any commands return results, the application likely needs explicit JAXB dependencies.
Add both the API and implementation — the API alone is not sufficient for runtime operation:
<!-- javax.xml.bind API — removed from JDK 17 -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<!-- JAXB runtime implementation -->
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.9</version>
</dependency>
Scope guidance:
- If JAXB is used in main code (XML marshalling, SOAP clients): use default scope (compile)
- If JAXB is only needed by test dependencies (e.g., POI in test code): use
<scope>test</scope>- If JAXB is needed by a runtime dependency that doesn't expose JAXB types in its API: use
<scope>runtime</scope>
The jaxb-api artifact provides the javax.xml.bind.* interfaces and annotations. The jaxb-impl artifact provides the actual marshalling/unmarshalling engine. Without both, libraries like Apache POI will either throw ClassNotFoundException (missing API) or silently fail to parse XML content (missing implementation, e.g., PPTX slides returning 0 pages).
Note: If the project has already migrated to Jakarta XML Binding (
jakarta.xml.bind.*), this step is not needed. This is specifically for third-party libraries that still use the oldjavax.xml.bindpackage (like Apache POI 3.x/4.x).
No new configuration files required for this skill.
No CF services required for this skill.
mvn clean compile
Should complete without errors.
mvn test-compile
Should complete without errors. If it fails, see Step 9 above for fixing Jakarta Servlet 6.0 interface evolution issues in test mocks.
# Should return no results (except for allowed exceptions like javax.sql)
grep -rh "^import javax\." src/main/java/ | grep -v "javax.sql" | grep -v "javax.naming"
mvn test
Solution: Ensure you have internet access and Maven can download plugins.
Cause: Some APIs have changed between Java EE and Jakarta EE. Solution: Check specific API documentation for breaking changes.
Cause: Jakarta Servlet 6.0 added new abstract methods to interfaces like HttpServletRequest, ServletContext, and HttpServletResponse. It also removed deprecated methods like getRealPath() and isRequestedSessionIdFromUrl(). Hand-written mock/stub classes in test code that implement these interfaces will have both missing new methods and invalid @Override annotations on removed methods.
Solution: See Step 9 above. Remove @Override on removed methods, add stubs for new methods, or replace hand-written mocks with Mockito.
Cause: CXF 4.x (required for jakarta.ws.rs.*) removed cxf-rt-rs-service-description-swagger. The Swagger 2 module no longer exists in CXF 4.x.
Solution: See Step 6 above. Replace with cxf-rt-rs-service-description-openapi-v3 and migrate annotations from io.swagger.annotations to io.swagger.v3.oas.annotations.
Cause: The original commons-fileupload depends on javax.servlet and cannot compile against Jakarta Servlet 6.0. The replacement library commons-fileupload2-jakarta-servlet6 has breaking API changes: different package names, builder-pattern factory construction, and FileItem.get()/delete() now throw IOException.
Solution: See Step 7 above. Replace the dependency and update all import statements, factory construction, and exception handling.
Note: javax.sql.* is part of the JDK, not Java EE. These imports should NOT be changed.
Cause: The Jackson JAX-RS provider package changed from com.fasterxml.jackson.jaxrs (javax) to com.fasterxml.jackson.jakarta.rs (jakarta). OpenRewrite updates Java import statements but does not update class references in web.xml <init-param> values. The CXFServlet fails to initialize because it cannot find the old class name.
Solution: See Step 5 above. Update the jaxrs.providers init-param in web.xml and the Maven dependency from jackson-jaxrs-json-provider to jackson-jakarta-rs-json-provider. Important: The class was also renamed — JacksonJaxbJsonProvider does not exist in the Jakarta artifact. Use com.fasterxml.jackson.jakarta.rs.json.JacksonJsonProvider instead. If you first update only the package (to com.fasterxml.jackson.jakarta.rs.json.JacksonJaxbJsonProvider), you will get a second ClassNotFoundException.
jakarta.persistence.RollbackException not foundCause: jakarta.persistence-api is set to <scope>provided</scope> in pom.xml. Tomcat is a servlet container, not a full Jakarta EE server — it does not provide JPA classes. With provided scope, the JPA API JAR is excluded from the WAR, so Weld cannot load any class that references jakarta.persistence.* types (e.g., RollbackException, NoResultException). All DAO classes are silently skipped, causing cascading WELD-001408: Unsatisfied dependencies errors for every injected DAO.
Solution: Remove <scope>provided</scope> from the jakarta.persistence-api dependency. The servlet API (jakarta.servlet-api) should remain provided (Tomcat provides it), but JPA, CDI, and other Jakarta EE APIs that Tomcat does not provide must be packaged in the WAR.
Cause: Mockito 1.x (mockito-all) uses cglib for bytecode generation. Java 25's module system blocks cglib's reflective access, causing all mocked tests to fail at runtime.
Solution: See Step 9 Option C above. Replace mockito-all with mockito-core:5.14.2 which uses ByteBuddy (Java 25 compatible). The test API (spy, when, doReturn, mock) is unchanged.
Cause: Java 25 removed the javax.xml.bind module from the JDK. Libraries like Apache POI that use JAXB internally will fail at runtime.
Solution: See Step 10 above. Add jaxb-api:2.3.1 and jaxb-impl:2.3.9 as dependencies. Both are required — API alone causes silent parsing failures.
Cause: CXF's OpenApiFeature transitively depends on jakarta.xml.ws.WebServiceFeature. If jakarta.xml.ws-api is at provided scope, it won't be packaged into the WAR. The sap_java_buildpack_jakarta (Tomcat) does not provide this API, unlike full Jakarta EE servers.
Solution: See Step 6d above. Remove <scope>provided</scope> from jakarta.xml.ws-api so it ships in the WAR.
Cause: Mockito 5.x removed the org.mockito.Matchers class. Static imports like import static org.mockito.Matchers.any no longer resolve.
Solution: See Step 9 — Mockito 5.x API Breaking Changes above. Replace org.mockito.Matchers with org.mockito.ArgumentMatchers in all test files.
Cause: Mockito 5.x moved MockitoJUnitRunner from org.mockito.runners to org.mockito.junit.
Solution: See Step 9 — Mockito 5.x API Breaking Changes above. Replace org.mockito.runners.MockitoJUnitRunner with org.mockito.junit.MockitoJUnitRunner.
Cause: Mockito 5.x defaults to strict stubbing. Tests with when(...) stubs that are never invoked during the test are rejected.
Solution: See Step 9 — Strict Stubbing above. Switch the @RunWith to MockitoJUnitRunner.Silent.class for affected test classes, or remove the unused stubs.
Cause: EqualsVerifier 3.x removed the allFieldsShouldBeUsed() method because it is now the default.
Solution: See Step 9 — EqualsVerifier 3.x Breaking Changes above. Remove the allFieldsShouldBeUsed() call.
Cause: EqualsVerifier 3.x is stricter about field usage and equals() inheritance.
Solution: Suppress with Warning.ALL_FIELDS_SHOULD_BE_USED or Warning.INHERITED_DIRECTLY_FROM_OBJECT as appropriate.
After completing this skill, proceed to: