| name | tomee-runtime |
| description | Invoke this skill to configure TomEE container for EJB applications. Detects @Stateless, @Singleton, @EJB annotations, javax.ejb/jakarta.ejb imports, or neo-javaee7-wp-api dependency. Use instead of Tomcat for EJB support. |
| disable-model-invocation | false |
| allowed-tools | Read, Edit, Write, Bash, Grep, Glob |
TomEE Runtime
Configure TomEE container for JavaEE/EJB applications in Cloud Foundry.
Purpose
Migrate Neo applications using TomEE runtime (with EJB support) to Cloud Foundry's TomEE 10 container instead of Tomcat.
Detection
This skill applies if any of these patterns are found:
In pom.xml
<dependency>
<groupId>com.sap.cloud</groupId>
<artifactId>neo-javaee7-wp-api</artifactId>
</dependency>
In Java source files
import javax.ejb.Stateless;
import javax.ejb.Singleton;
import javax.ejb.EJB;
import javax.ejb.Schedule;
@Stateless
public class MyService { }
@Singleton
public class MySingleton { }
In web.xml or deployment descriptors
<ejb-local-ref>
<ejb-ref-name>ejb/MyService</ejb-ref-name>
<local>com.example.MyService</local>
</ejb-local-ref>
Prerequisites
Working directory: This skill must run inside the -cf-migration copy of your app, created by jakarta-java25-migration or neo-to-cf-migration-orchestrator. If your current directory does not end in -cf-migration, switch to it before proceeding.
Before invoking this skill, ensure you have invoked:
- jakarta-java25-migration -
Use the jakarta-java25-migration skill
- Migrates to Jakarta EE 10
- REQUIRED before this skill
Note: TomEE is an alternative to the Tomcat migration path. Choose TomEE if your application uses EJB features.
Transformation Steps
Step 1: Replace Neo Dependencies
Before (Neo):
<dependency>
<groupId>com.sap.cloud</groupId>
<artifactId>neo-javaee7-wp-api</artifactId>
<version>${neo.version}</version>
<scope>provided</scope>
</dependency>
After (Cloud Foundry TomEE):
See assets/pom-cf-tomee.xml for a complete template.
Key changes:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.sap.cloud.sjb.cf</groupId>
<artifactId>cf-tomee-bom</artifactId>
<version>${cf-tomee-bom-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.sap.cloud.sdk</groupId>
<artifactId>sdk-modules-bom</artifactId>
<version>${sdk-modules-bom-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<properties>
<cf-tomee-bom-version>2.25.0</cf-tomee-bom-version>
<sdk-modules-bom-version>5.14.0</sdk-modules-bom-version>
<jakarta.persistence-api-version>3.2.0</jakarta.persistence-api-version>
</properties>
<dependencies>
<dependency>
<groupId>com.sap.cloud.sdk.cloudplatform</groupId>
<artifactId>scp-cf</artifactId>
</dependency>
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-api</artifactId>
<version>10.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
Step 2: Migrate EJB Annotations
Before (Java EE 7):
import javax.ejb.Stateless;
import javax.ejb.EJB;
import javax.ejb.Schedule;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.annotation.PostConstruct;
@Stateless
public class MyBusinessService {
@EJB
private OtherService otherService;
public String doWork() {
return otherService.process();
}
}
@Singleton
@Startup
public class ApplicationInitializer {
@PostConstruct
public void init() {
System.out.println("Application started");
}
@Schedule(hour = "*", minute = "*/5", persistent = false)
public void scheduledTask() {
System.out.println("Running scheduled task");
}
}
After (Jakarta EE 10):
import jakarta.ejb.Stateless;
import jakarta.ejb.EJB;
import jakarta.ejb.Schedule;
import jakarta.ejb.Singleton;
import jakarta.ejb.Startup;
import jakarta.annotation.PostConstruct;
@Stateless
public class MyBusinessService {
@EJB
private OtherService otherService;
public String doWork() {
return otherService.process();
}
}
@Singleton
@Startup
public class ApplicationInitializer {
@PostConstruct
public void init() {
System.out.println("Application started");
}
@Schedule(hour = "*", minute = "*/5", persistent = false)
public void scheduledTask() {
System.out.println("Running scheduled task");
}
}
Step 3: Create resources.xml for Data Sources
For TomEE, use resources.xml instead of Tomcat's context.xml.
Create src/main/webapp/WEB-INF/resources.xml:
<?xml version='1.0' encoding='utf-8'?>
<resources>
<Resource id="jdbc/DefaultDB"
provider="xs.openejb:XS Default JDBC Database"
type="javax.sql.DataSource">
service=${service_name_for_DefaultDB}
</Resource>
</resources>
Note: TomEE uses xs.openejb:XS Default JDBC Database provider instead of Tomcat's TomcatDataSourceFactory. The service placeholder is specified as element content, not as an XML attribute.
Step 4: Configure persistence.xml for TomEE JTA
If the application uses JPA, the persistence.xml must be updated for TomEE's JTA environment. There are four critical changes:
- File location — Move
persistence.xml from src/main/resources/META-INF/ to src/main/webapp/META-INF/. TomEE processes DDL generation correctly only when the persistence.xml is at the WAR root META-INF/ location, not at WEB-INF/classes/META-INF/.
- Explicit EclipseLink provider — TomEE defaults to OpenJPA. Without an explicit
<provider>, EclipseLink may not be properly initialized as the JPA provider.
- Short JNDI datasource name — TomEE resolves datasources differently than Neo. Use
jdbc/DefaultDB without the java:comp/env/ prefix.
- HANA Cloud NCLOB for large text columns — HANA Cloud limits
NVARCHAR to 5000 characters. Any @Lob field with length > 5000 must use columnDefinition = "NCLOB" instead of length, otherwise DDL generation silently fails for those tables.
Before (Neo):
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="2.0">
<persistence-unit name="MyApp" transaction-type="JTA">
<jta-data-source>java:comp/env/jdbc/DefaultDB</jta-data-source>
<class>com.example.MyEntity</class>
<properties>
<property name="eclipselink.ddl-generation" value="create-tables" />
<property name="eclipselink.ddl-generation.output-mode" value="database" />
</properties>
</persistence-unit>
</persistence>
After (Cloud Foundry TomEE):
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence
https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
version="3.0">
<persistence-unit name="MyApp" transaction-type="JTA">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<jta-data-source>jdbc/DefaultDB</jta-data-source>
<class>com.example.MyEntity</class>
<properties>
<property name="eclipselink.ddl-generation" value="create-or-extend-tables" />
<property name="eclipselink.target-database" value="HANA"/>
</properties>
</persistence-unit>
</persistence>
Key changes:
- File moved from
src/main/resources/META-INF/ to src/main/webapp/META-INF/ — TomEE's DDL generation only runs correctly when persistence.xml is at the WAR root META-INF/ location. Placing it under resources/META-INF/ (which maps to WEB-INF/classes/META-INF/ in the WAR) causes DDL generation to silently fail.
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> — explicit EclipseLink provider (TomEE defaults to OpenJPA)
<jta-data-source>jdbc/DefaultDB</jta-data-source> — short JNDI name without java:comp/env/ prefix
eclipselink.ddl-generation=create-or-extend-tables — use this value instead of create-tables for robustness during schema evolution
Warning: If persistence.xml is placed under src/main/resources/META-INF/ instead of src/main/webapp/META-INF/, EclipseLink's DDL generation will silently fail in TomEE — no tables will be created and no error will be logged at startup. The failure only becomes visible at runtime when queries hit missing tables. Always verify by adding <property name="eclipselink.logging.level" value="FINE"/> temporarily and checking for CREATE TABLE statements in the logs.
HANA Cloud @Lob Column Migration
HANA Cloud enforces a maximum of 5000 characters for NVARCHAR columns. EclipseLink maps @Lob @Column(length = N) to NVARCHAR(N) — if N > 5000, the CREATE TABLE statement fails silently.
Before (works on Neo HANA on-premise, fails on HANA Cloud):
@Lob
@Column(nullable = false, length = 16 * 1024)
private String longText = "";
@Lob
@Column(length = 512 * 1024)
private String introduction = "";
After (works on HANA Cloud):
@Lob
@Column(nullable = false, columnDefinition = "NCLOB")
private String longText = "";
@Lob
@Column(columnDefinition = "NCLOB")
private String introduction = "";
Replace length = N with columnDefinition = "NCLOB" on every @Lob field where the original length exceeds 5000. Keep other @Column attributes (nullable, updatable) as-is.
Note: NCLOB has no practical size limit on HANA Cloud and is the correct type for large text fields. Fields with length <= 5000 (e.g., @Column(length = 1024)) can remain as NVARCHAR and do not need this change.
Step 5: Create resource_configuration.yml
Create src/main/webapp/META-INF/sap_java_buildpack/config/resource_configuration.yml:
---
tomee/webapps/ROOT/WEB-INF/resources.xml:
service_name_for_DefaultDB: ${app-name}-hana
Note: The path is tomee/webapps/ROOT/WEB-INF instead of tomcat/webapps/ROOT.
Step 6: Update MTA Descriptor
_schema-version: "3.2"
ID: ${app-name}
version: 0.0.1
modules:
- name: ${app-name}
type: java.tomcat
path: target/${app-name}.war
parameters:
buildpack: sap_java_buildpack_jakarta
disk-quota: 512MB
memory: 512MB
properties:
TARGET_RUNTIME: tomee
ENABLE_SECURITY_JAVA_API_V2: true
SET_LOGGING_LEVEL: 'ROOT: INFO'
JBP_CONFIG_RESOURCE_CONFIGURATION:
- tomee/webapps/ROOT/WEB-INF/resources.xml:
service_name_for_DefaultDB: ${app-name}-hana
requires:
- name: ${app-name}-hana
resources:
- name: ${app-name}-hana
type: com.sap.xs.hana-schema
Key differences:
TARGET_RUNTIME: tomee property enables TomEE instead of Tomcat
JBP_CONFIG_RESOURCE_CONFIGURATION uses YAML list format (not inline string)
- The path uses
tomee/webapps/ROOT/WEB-INF instead of tomcat/webapps/ROOT
Step 7: Remove CXFNonSpringJaxrsServlet from web.xml (Critical for CDI/EJB Injection)
If the Neo application declared CXFNonSpringJaxrsServlet in web.xml to configure JAX-RS, you must remove it when using TomEE. CXFNonSpringJaxrsServlet instantiates JAX-RS endpoint classes directly (via new), completely bypassing TomEE's CDI container. This means @Inject, @EJB, and @Resource fields in endpoint classes will remain null, causing NullPointerException at runtime.
TomEE has built-in CXF/JAX-RS integration that is CDI-aware. If your JAX-RS Application subclass has @ApplicationPath("/rest"), TomEE will auto-discover it and wire endpoints through the CDI container.
Detect
grep -n "CXFNonSpringJaxrsServlet" src/main/webapp/WEB-INF/web.xml
If found, apply the fix below.
Fix
Remove the entire <servlet> and <servlet-mapping> block for CXF from web.xml:
<servlet>
<servlet-name>CXFServlet</servlet-name>
<servlet-class>
org.apache.cxf.jaxrs.servlet.CXFNonSpringJaxrsServlet
</servlet-class>
<init-param>
<param-name>jakarta.ws.rs.Application</param-name>
<param-value>com.example.RestActivator</param-value>
</init-param>
<init-param>
<param-name>jaxrs.providers</param-name>
<param-value>com.fasterxml.jackson.jakarta.rs.json.JacksonJsonProvider</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>CXFServlet</servlet-name>
<url-pattern>/rest/*</url-pattern>
</servlet-mapping>
Move provider registration to the JAX-RS Application class:
If providers (e.g., Jackson) were registered via jaxrs.providers init-param, register them in the Application.getClasses() method instead:
```java
@ApplicationPath("/rest")
public class RestActivator extends Application {
private final Set<Class<?>> classes = new HashSet<>();
public RestActivator() {
classes.add(MyEndpoint.class);
classes.add(com.fasterxml.jackson.jakarta.rs.json.JacksonJsonProvider.class);
}
@Override
public Set<Class<?>> getClasses() {
return classes;
}
}
> **Note:** This only applies to TomEE. On plain Tomcat (without built-in JAX-RS), `CXFNonSpringJaxrsServlet` is required because there is no container-managed CXF integration. If migrating to Tomcat instead of TomEE, keep the servlet but be aware that CDI injection will not work in endpoint classes.
### Step 8: Update beans.xml to Jakarta EE Namespace
If the project has a `beans.xml` with the old `javax` namespace, update it to Jakarta CDI 4.0. The old namespace may cause CDI bean discovery issues on TomEE 10.
**Before:**
```xml
<beans xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
After:
<beans 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/beans_4_0.xsd"
bean-discovery-mode="all"
version="4.0">
Note: bean-discovery-mode="all" ensures all classes are discoverable as CDI beans, including those without explicit CDI annotations. This matches the implicit behavior of CDI 1.0 (beans_1_0.xsd).
Step 9: Update EJB JNDI Lookups
Before (Neo-specific JNDI):
Context ctx = new InitialContext();
MyService service = (MyService) ctx.lookup("java:comp/env/ejb/MyService");
After (Standard Jakarta EE JNDI):
Context ctx = new InitialContext();
MyService service = (MyService) ctx.lookup("java:module/MyService");
MyService service = (MyService) ctx.lookup("java:global/${app-name}/MyService");
Step 10: Handle Timer Service
TomEE Timer Service works similarly to Neo, but configuration may differ:
import jakarta.ejb.Schedule;
import jakarta.ejb.Singleton;
import jakarta.ejb.Timer;
import jakarta.ejb.Timeout;
import jakarta.annotation.Resource;
import jakarta.ejb.TimerService;
@Singleton
public class ScheduledTasks {
@Resource
private TimerService timerService;
@Schedule(hour = "*", minute = "*/10", persistent = false)
public void periodicTask() {
}
public void scheduleTask(long delay) {
timerService.createSingleActionTimer(delay, null);
}
@Timeout
public void handleTimeout(Timer timer) {
}
}
Note: Set persistent = false for timers in Cloud Foundry as persistent timers require database storage.
Directory Structure
src/main/
├── java/
│ └── com/example/
│ ├── ejb/
│ │ └── MyBusinessService.java
│ └── servlet/
│ └── MyServlet.java
└── webapp/
├── META-INF/
│ ├── persistence.xml # JPA config (MUST be here, not resources/META-INF)
│ └── sap_java_buildpack/
│ └── config/
│ └── resource_configuration.yml
└── WEB-INF/
├── resources.xml # TomEE data source config
├── web.xml
└── openejb-jar.xml (optional)
Configuration Files
| File | Location | Purpose |
|---|
resources.xml | src/main/webapp/WEB-INF/ | TomEE data source configuration |
resource_configuration.yml | src/main/webapp/META-INF/sap_java_buildpack/config/ | Service name mapping |
openejb-jar.xml | src/main/webapp/WEB-INF/ | EJB deployment configuration |
CF Services
Same services as Tomcat-based deployment, but with TomEE runtime.
Verification
1. Compile Check
mvn clean compile
2. Deploy
mvn clean package
cf deploy . -f
3. Verify TomEE Runtime
cf logs ${app-name} --recent | grep -i tomee
4. Test EJB Access
curl "https://${app-url}/api/service"
Common Issues
Issue: EJB not found
Cause: JNDI name mismatch.
Solution: Use standard Jakarta EE JNDI names:
java:module/BeanName
java:global/app/module/BeanName
Issue: Timer not firing
Cause: Persistent timer without database.
Solution: Set persistent = false on @Schedule.
Issue: "Ambiguous EJB reference"
Cause: Multiple beans implement same interface.
Solution: Use @EJB(beanName = "SpecificBean").
Issue: ClassCastException with EJB proxies
Cause: Incorrect interface casting.
Solution: Ensure you're casting to the interface, not the implementation.
Issue: TomEE startup fails with "serviceName is null" — TomEEDataSourceFactory NullPointerException
Cause: The resources.xml data source placeholder uses XML attribute syntax (service="${service_name_for_DefaultDB}") instead of element body text. The SAP Java Buildpack's placeholder substitution does not work with XML attributes — only with element body text.
Diagnosis: Stack trace shows NullPointerException: Cannot invoke "String.equals(Object)" because "serviceName" is null in VcapServices.getService().
Solution: Use element body text for the service placeholder in resources.xml:
<Resource id="jdbc/DefaultDB"
provider="xs.openejb:XS Default JDBC Database"
type="javax.sql.DataSource"
service="${service_name_for_DefaultDB}" />
<Resource id="jdbc/DefaultDB"
provider="xs.openejb:XS Default JDBC Database"
type="javax.sql.DataSource">
service=${service_name_for_DefaultDB}
</Resource>
Also verify that the path in resource_configuration.yml and JBP_CONFIG_RESOURCE_CONFIGURATION matches the actual resources.xml location (tomee/webapps/ROOT/WEB-INF/resources.xml, not META-INF).
Issue: Tables not created — DDL generation silently fails in TomEE JTA environment
Cause: The persistence.xml is placed at src/main/resources/META-INF/ (packaged to WEB-INF/classes/META-INF/ in the WAR). TomEE's DDL generation only works correctly when persistence.xml is at the WAR root META-INF/ location (src/main/webapp/META-INF/). No error is logged at startup — the failure only surfaces at runtime as Could not find table/view errors.
Diagnosis: Application starts normally but all JPA queries fail with DatabaseException: invalid table name: Could not find table/view TABLENAME in schema. To confirm DDL is being attempted, add <property name="eclipselink.logging.level" value="FINE"/> and check logs for CREATE TABLE statements.
Solution: See Step 4 above. Move persistence.xml from src/main/resources/META-INF/ to src/main/webapp/META-INF/ and ensure all three configuration fixes are applied:
- File location:
src/main/webapp/META-INF/persistence.xml
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<jta-data-source>jdbc/DefaultDB</jta-data-source> (without java:comp/env/ prefix)
Issue: CREATE TABLE fails silently — HANA Cloud NVARCHAR 5000 limit
Cause: HANA Cloud enforces a maximum of 5000 characters for NVARCHAR columns. Entity fields annotated with @Lob @Column(length = 16384) or similar cause EclipseLink to generate NVARCHAR(16384), which HANA rejects with error code 267: specified length too long for its datatype. EclipseLink logs this as a warning but continues — no tables are created for any entity that hits this limit, and the error is easy to miss unless logging is set to FINE.
Diagnosis: cf logs --recent shows [EL Warning] with Error Code: 267 and specified length too long for its datatype during startup. Or, if logging is not at FINE level, all JPA queries fail with Could not find table/view.
Solution: Replace length = N (where N > 5000) with columnDefinition = "NCLOB" on all @Lob fields. See Step 4 — HANA Cloud @Lob Column Migration above.
Issue: NullPointerException — @Inject/@EJB fields are null in JAX-RS endpoints
Cause: CXFNonSpringJaxrsServlet is declared in web.xml. This servlet instantiates JAX-RS endpoint classes directly via new, bypassing TomEE's CDI container entirely. All @Inject, @EJB, and @Resource fields remain null. The error manifests as NullPointerException when any injected service method is called (e.g., "Cannot invoke ... because this.myService is null").
Solution: See Step 7 above. Remove the CXFNonSpringJaxrsServlet <servlet> and <servlet-mapping> from web.xml. TomEE's built-in CXF integration auto-discovers @ApplicationPath and manages endpoint lifecycle through CDI. Move any jaxrs.providers init-param registrations to the Application.getClasses() method.
Issue: AuthorizationException or isUserInRole() returns false — getUserPrincipal() returns null
Cause: The most common cause is that the REST API URL patterns (e.g., /rest/*, /api/*) are not covered by a <security-constraint> in web.xml. The XSSecurityAuthenticator valve only validates JWT tokens for URLs that match a security constraint. Without a matching constraint, the valve skips JWT validation entirely — getUserPrincipal() returns null and all isUserInRole() calls return false, regardless of injection method (@Context, @Inject, or SessionContext). This is easy to miss when Neo-era constraints covered only static pages while REST APIs were reached via auth-method-specific URL prefixes (/s/api/*, /b/api/*) that were removed during migration.
Diagnosis: Check if getUserPrincipal() returns null. If it does, this is a missing security constraint issue, not an injection issue.
Solution: Add a <security-constraint> covering /rest/* (or /*) with <role-name>Everyone</role-name>. See the authentication-xsuaa skill, Step 5.
Issue: AuthorizationException or isUserInRole() returns false in @Stateless EJBs — principal IS set but roles not recognized
Cause: If getUserPrincipal() returns a valid principal but isUserInRole() still returns false: @Context HttpServletRequest is a JAX-RS annotation. It only works reliably in JAX-RS resource classes (endpoints). In @Stateless EJBs that are not JAX-RS resources — such as service providers, CDI producers, or utility beans — the @Context request is a CXF-internal wrapper that doesn't delegate isUserInRole() to the Catalina/XSUAA security realm. This causes isUserInRole("admin") to return false even though the user has the role assigned, resulting in authorization failures.
Important: SessionContext.isCallerInRole() also does not work in this scenario. TomEE's OpenEJBSecurityListener$RequestCapturer does not fully propagate XSUAA roles from the Catalina security realm to the OpenEJB security context, so sessionContext.isCallerInRole() returns false as well.
Solution: Replace @Context (JAX-RS injection) with @Inject (CDI injection) for HttpServletRequest. In Jakarta EE 10 (CDI 4.0), HttpServletRequest is a built-in CDI bean. CDI injects a request-scoped proxy that correctly delegates to the actual Catalina request — which has the XSUAA security context set by XSSecurityAuthenticator. Both isUserInRole() and getUserPrincipal() then work correctly.
Before (broken — @Context in non-JAX-RS EJB):
@Stateless
public class MyUserContext {
@Context
HttpServletRequest httpRequest;
public boolean isAdmin() {
return httpRequest.isUserInRole("admin");
}
}
Also broken — SessionContext doesn't get XSUAA roles either:
@Stateless
public class MyUserContext {
@Resource
SessionContext sessionContext;
public boolean isAdmin() {
return sessionContext.isCallerInRole("admin");
}
}
After (works — @Inject CDI injection):
@Stateless
public class MyUserContext {
@Inject
HttpServletRequest httpRequest;
public boolean isAdmin() {
return httpRequest.isUserInRole("admin");
}
public String getEmail() {
Principal p = httpRequest.getUserPrincipal();
if (p instanceof Token) {
return ((Token) p).getClaimAsString(TokenClaims.EMAIL);
}
return null;
}
}
Note: @Context continues to work correctly in JAX-RS resource classes (@Path-annotated endpoints). This issue only affects @Stateless/@Singleton EJBs that are injected via CDI into JAX-RS endpoints.
Issue: CDI beans not discovered on TomEE 10
Cause: beans.xml uses the old javax namespace (http://java.sun.com/xml/ns/javaee/beans_1_0.xsd). TomEE 10 may not process bean discovery correctly with the legacy namespace.
Solution: See Step 8 above. Update beans.xml to the Jakarta CDI 4.0 namespace (https://jakarta.ee/xml/ns/jakartaee/beans_4_0.xsd) with bean-discovery-mode="all" and version="4.0".
TomEE vs Tomcat Comparison
| Feature | Tomcat | TomEE |
|---|
| EJB Support | No | Yes |
| JPA/JTA | Manual setup | Built-in |
| CDI | via extension | Built-in |
| Timer Service | No | Yes |
| Data Source Config | context.xml | resources.xml |
| Property | - | TARGET_RUNTIME: tomee |
| BOM | cf-tomcat-bom | cf-tomee-bom |
Next Steps
After completing TomEE migration:
- Apply other applicable skills (Authentication, Destinations, etc.)
- The workflow is the same as Tomcat-based apps