|
|
||
|---|---|---|
| .gitea/workflows | ||
| db | ||
| src/main | ||
| wildfly | ||
| .gitignore | ||
| Containerfile | ||
| pom.xml | ||
| README.md | ||
ledger-ejb
A deliberately old-school Jakarta EE sample application for a VM-to-OpenShift migration test fleet. It is the RED verdict app: hard to containerize naively and intentionally rich in the patterns the Migration Toolkit for Applications (MTA / Konveyor) flags as cloud-readiness issues.
Domain: a double-entry ledger. Accounts, journal entries, and journal lines, where every entry must balance (sum of debits == sum of credits), with that invariant enforced in the EJB tier.
Why Jakarta EE 10 (
jakarta.*) and notjavax.*? The architecture is legacy on purpose (EJBs, container-managed transactions, JNDI datasource lookups, JMS/MDB). But WildFly 30's default profile is Jakarta EE 10; the oldjavax.*EE8 mode is gone in 30. So the app usesjakarta.*APIs to actually deploy on stock WildFly 30, while keeping the legacy architecture that MTA still flags for cloud readiness. That is the point.
Module layout
ledger-ejb/
├── pom.xml # single WAR, finalName=ledger -> ledger.war
├── README.md
├── db/
│ └── init.sql # idempotent schema + seed (8 accounts, 7 entries)
└── src/main/
├── java/com/example/ledger/
│ ├── domain/ # JPA entities
│ │ ├── Account.java
│ │ ├── JournalEntry.java
│ │ └── JournalLine.java
│ ├── ejb/ # EJB tier (CMT, JNDI, @Singleton/@Stateful/@Stateless)
│ │ ├── AccountServiceBean.java # @Stateless
│ │ ├── LedgerServiceBean.java # @Stateless, posts + sends JMS
│ │ ├── JournalEntryBuilderBean.java # @Stateful conversational builder
│ │ ├── StartupSeedCheckBean.java # @Singleton @Startup seed check
│ │ └── UnbalancedEntryException.java # @ApplicationException(rollback=true)
│ ├── jms/
│ │ └── LedgerEventsMDB.java # @MessageDriven on LedgerEvents
│ └── web/ # plain servlets + JSP (no Spring)
│ ├── AccountsServlet.java
│ ├── LedgerServlet.java
│ ├── PostEntryServlet.java # drives the @Stateful builder
│ └── HealthServlet.java # 200 JSON with DB ping
├── resources/META-INF/
│ └── persistence.xml # ledgerPU -> java:jboss/datasources/LedgerDS
└── webapp/
├── index.jsp # redirect to /accounts
└── WEB-INF/
├── beans.xml
├── jboss-ejb3.xml # vendor MDB binding
└── jsp/*.jsp
Build
Java 17 toolchain (the project targets maven.compiler.release=17):
mvn -B -DskipTests package
# -> target/ledger.war
If no local Maven is present, build inside a container. Because of an SELinux bind-mount quirk on this host, copy the source in, build, and copy the artifact back out rather than bind-mounting the workspace:
podman run --rm --name ledger-build registry.access.redhat.com/ubi9/openjdk-17 \
sleep infinity &
podman cp . ledger-build:/home/jboss/src
podman exec -w /home/jboss/src ledger-build mvn -B -DskipTests package
podman cp ledger-build:/home/jboss/src/target/ledger.war ./target/ledger.war
podman stop ledger-build
Deploy to WildFly 30
- Apply the standalone.xml datasource and JMS queue snippets below.
- Create the database and run the schema/seed:
psql -h <host> -U ledger -d ledger -f db/init.sql - Drop the WAR into the deployments folder:
cp target/ledger.war $WILDFLY_HOME/standalone/deployments/ - Browse to
http://localhost:8080/ledger/(redirects to the accounts list)./ledger/accounts— chart of accounts/ledger/ledger— posted journal entries/ledger/post— multi-step posting form (uses the @Stateful builder)/ledger/health—200JSON with a live DB ping via the EJB
standalone.xml snippets
1. Datasource — java:jboss/datasources/LedgerDS
Note the plaintext password (ledger-db-pw) embedded in the config. This is
the discovery secrets case the fleet is meant to surface — do not "fix" it.
Add inside <subsystem xmlns="urn:jboss:domain:datasources:7.0"> <datasources>:
<datasource jndi-name="java:jboss/datasources/LedgerDS"
pool-name="LedgerDS" enabled="true" use-java-context="true">
<connection-url>jdbc:postgresql://ledger-db:5432/ledger</connection-url>
<driver>postgresql</driver>
<pool>
<min-pool-size>2</min-pool-size>
<max-pool-size>20</max-pool-size>
</pool>
<security>
<user-name>ledger</user-name>
<!-- plaintext credential — the discovery secrets case -->
<password>ledger-db-pw</password>
</security>
</datasource>
And register the PostgreSQL JDBC driver in the same subsystem's <drivers>:
<driver name="postgresql" module="org.postgresql">
<driver-class>org.postgresql.Driver</driver-class>
</driver>
(Install the driver as a JBoss module, e.g.
$WILDFLY_HOME/modules/org/postgresql/main/ with postgresql-42.7.4.jar and a
module.xml, or deploy the driver JAR directly.)
2. JMS queue — java:/jms/queue/LedgerEvents
Uses WildFly's embedded Artemis. Add inside
<subsystem xmlns="urn:jboss:domain:messaging-activemq:..."> <server name="default">:
<jms-queue name="LedgerEvents"
entries="java:/jms/queue/LedgerEvents java:jboss/exported/jms/queue/LedgerEvents"/>
The LedgerServiceBean sends a TextMessage to this queue on every successful
posting (transactionally, inside the container-managed transaction), and the
LedgerEventsMDB consumes and logs each event.
Seed summary
db/init.sql is idempotent and creates:
- 8 accounts —
1000 Cash,1100 Accounts Receivable,1500 Equipment,2000 Accounts Payable,2100 Loans Payable,3000 Owner Equity,4000 Sales Revenue,5000 Office Expense(ASSET / LIABILITY / EQUITY / REVENUE / EXPENSE). - 7 balanced journal entries — capital injection, equipment purchase, credit
sale, customer payment, supplies on account, bank loan, and a multi-line
supplier payment + bank fee. Every entry satisfies
sum(debit) == sum(credit).
MTA-relevant anti-patterns deliberately present
- EJB session beans —
@Stateless,@Stateful,@Singleton @Startup. - Container-managed transactions (CMT) —
@TransactionAttributethroughout,@ApplicationException(rollback=true). - JNDI datasource lookup —
java:jboss/datasources/LedgerDSviapersistence.xml; not a config/env-driven connection. - Plaintext datasource password in
standalone.xml(ledger-db-pw) — the discovery secrets case. - JMS + MDB —
@MessageDrivenconsumer plus transactional JMS send tied to the embedded Artemis broker (broker coupling, server-managed delivery). - @Stateful conversational state parked in the HttpSession — sticky, instance-pinned state that breaks horizontal scaling.
- @Startup @Singleton with a boot-time DB dependency — deploy-time coupling to the database / ordered startup.
- Vendor-specific deployment descriptor —
jboss-ejb3.xmlbinding the MDB to the proprietary Artemis resource adapter. - Server-supplied (
provided) EE runtime — the app expects a full Jakarta EE application server (WildFly), not an embeddable/self-contained runtime.