JBoss-to-Spring-Boot Migration
Objective
Migrate Java EE/Jakarta EE applications on JBoss EAP/WildFly to Spring Boot 3.2.x, eliminating application server dependencies and modernizing to cloud-native containerized deployment.
Scope
Converts JBoss/WildFly-based enterprise Java apps into standalone Spring Boot JARs with embedded servers. Phase 0 scans the project to determine which migration phases are needed; only required phases execute.
Non-Goals (require separate handling):
- EJB Remote Interfaces over IIOP/CORBA — redesign to REST, gRPC, or messaging
- JCA Resource Adapters — replace with Spring Integration or vendor client libraries
- EJB 2.x Entity Beans (CMP/BMP) — manually convert to JPA entities first
- Proprietary JBoss Clustering (JGroups/Infinispan) — use Spring Session with Redis/Hazelcast
- Vaadin UI — version-specific upgrade (Vaadin 8→24 Flow with vaadin-spring-boot-starter)
- Complex Arquillian @Deployment data init — migrate to @Sql/@TestConfiguration/@BeforeEach
- JAXB Marshalling Tests — migrate to jakarta.xml.bind equivalents, never delete
- Uncommon Hibernate validators (@SafeHtml, @ScriptAssert, etc.) — custom ConstraintValidator
- XA/JTA Distributed Transactions — per-datasource @Transactional or JtaTransactionManager
Constraints
API & Functional Parity
- Preserve all public class names, method signatures, REST endpoint paths, HTTP methods, request/response formats, and status codes.
- Business logic must remain unchanged — only framework/infrastructure code is modified.
- Transaction boundaries and isolation levels must be preserved.
- Preserve Javadoc comments and code documentation throughout the migration.
Test Integrity
- Do NOT mark tests @Disabled or delete tests as a migration shortcut.
- Migrate Arquillian tests to @SpringBootTest equivalents preserving all assertions.
- JAXB tests → migrate to jakarta.xml.bind or Jackson equivalents.
Security
- CRITICAL: Only add spring-boot-starter-security if SECURITY_NEEDED=true.
- Never hardcode credentials in source code.
Code Quality
- Constructor injection throughout. Exceptions:
@PersistenceContext EntityManager — correct Spring idiom for JPA EntityManager; exempt from constructor injection.
@Value on scalar-property fields in @Component configuration POJOs — simple config holders, not collaborator dependencies.
- Fields with public setters mandated by an implemented interface (e.g., framework callback interfaces requiring setter injection).
- Abstract base class refactoring: enumerate ALL concrete subclasses first, update ALL in the same pass with
super(…) calls. Incomplete passes leave half the hierarchy on field injection.
- @SessionScope Serializable beans: non-serializable dependencies (HttpServletRequest, JmsTemplate) — use constructor injection +
private transient field. Transient prevents serialization errors but does NOT exempt from constructor injection rule.
- Follow Spring Boot conventions. Remove unused imports and dead code.
Incremental Migration
- Verify build passes after each phase before proceeding. Commit after each successful phase.
Worked Examples
Example 1: @Stateless EJB → Spring @Service
Before (JBoss):
import javax.ejb.Stateless;
import javax.ejb.EJB;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
@Stateless
public class OrderService {
@EJB
private InventoryService inventoryService;
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public Order placeOrder(OrderRequest request) {
inventoryService.reserve(request.getItemId(), request.getQuantity());
return createOrder(request);
}
}
After (Spring Boot):
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.annotation.Propagation;
@Service
@Transactional
public class OrderService {
private final InventoryService inventoryService;
public OrderService(InventoryService inventoryService) {
this.inventoryService = inventoryService;
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Order placeOrder(OrderRequest request) {
inventoryService.reserve(request.getItemId(), request.getQuantity());
return createOrder(request);
}
}
Example 2: JAX-RS Resource → Spring MVC Controller
See references/jaxrs-spring-mvc-migration.md for a complete before/after JAX-RS→Spring MVC example with @ApplicationPath folding, @QueryParam → @RequestParam(required=false), and Response → ResponseEntity.
Additional worked examples (MDB→@JmsListener, JBoss Security→Spring Security) in references/worked-examples-conditional.md.
Workflow
Pipeline Architecture
Phase 0: Scanner → Phase 1: Build & Config → Phase 2: Business Logic
│ (ALWAYS) (ALWAYS)
├→ Phase 3: Security & Messaging (if SECURITY_NEEDED or JMS_NEEDED)
├→ Phase 4: Testing & UI (ALWAYS, scope varies)
└→ Phase 5: Deployment & Verification (ALWAYS)
Phase 0: Project Analysis
Scan the project and set feature flags to determine which phases execute.
Core feature flags (determine phase execution):
| Flag |
Detection Rule |
| SECURITY_NEEDED |
<security-constraint> in web.xml, @RolesAllowed, @DeclareRoles, or <security-domain> in jboss-web.xml |
| JMS_NEEDED |
@MessageDriven, @JMSDestinationDefinition, javax.jms.*/jakarta.jms.* imports |
| JSF_NEEDED |
.xhtml in src/main/webapp/, or javax.faces.*/jakarta.faces.* imports |
| BATCH_NEEDED |
jakarta.batch.*/javax.batch.* imports, or META-INF/batch-jobs/ directory |
| WEBSOCKET_NEEDED |
@ServerEndpoint or jakarta.websocket.* imports |
| JPA_NEEDED |
pom.xml contains spring-boot-starter-data-jpa (post-Step 2) OR any Java file imports javax.persistence.*/jakarta.persistence.* |
| STATIC_UI_EXISTS |
.html/.css/.js/.jsp/image files in src/main/webapp/ |
| ARQUILLIAN_TESTS |
@RunWith(Arquillian, @Deployment, or arquillian.xml |
| IS_MULTI_MODULE |
pom.xml <modules> or packaged as EAR |
Library flags — full detection rules in references/phase0-detection-flags.md:
| Flag |
Detection Rule |
| HAS_JODA_TIME |
pom.xml contains joda-time dependency |
| HAS_JACKSON_V1 |
pom.xml contains org.codehaus.jackson, OR imports org.codehaus.jackson |
| HAS_C3P0 |
pom.xml contains c3p0, OR persistence.xml has hibernate.c3p0 properties |
| HAS_GUAVA |
pom.xml contains com.google.guava AND any Java file imports com.google.common.base.Optional (both required) |
| HAS_LOMBOK |
pom.xml contains org.projectlombok dependency |
| HAS_SPRING_XML |
Any XML in src/main/resources/ or src/main/webapp/WEB-INF/ contains <beans xmlns="http://www.springframework.org/schema/beans" |
| HAS_LOG4J |
src/main/resources/ contains log4j.xml or log4j.properties |
| JAX_WS_NEEDED |
@WebService, @WebMethod, or javax.xml.ws.*/jakarta.xml.ws.* imports |
| HAS_JAXB |
Imports javax.xml.bind.*/jakarta.xml.bind.*, OR pom.xml contains jaxb-api |
| HAS_JSONB |
Imports jakarta.json.bind.*/javax.json.bind.*, OR pom.xml contains jakarta.json.bind-api |
| HAS_EJB_TIMERS |
@Schedule, @Timeout, or TimerService from javax.ejb/jakarta.ejb |
| HAS_JAVAMAIL |
Imports javax.mail.*/jakarta.mail.*, OR pom.xml contains javax.mail/jakarta.mail |
| HAS_SERVLET_FILTERS |
@WebFilter or @WebListener annotations |
| HAS_MULTI_DATASOURCE |
persistence.xml contains multiple <persistence-unit> elements, OR standalone.xml defines multiple datasource subsystems. Fallback: grep -c '<jta-data-source>' src/main/resources/META-INF/persistence.xml with count >1. |
| SUREFIRE_DISABLED |
pom.xml <skip>true</skip> or <skipTests>true</skipTests> under maven-surefire-plugin. Fix: remove <skip>true</skip> or set to false; verify with `mvn help:effective-pom |
| HAS_CDI_INTERCEPTORS |
Any Java file contains @Interceptor or @InterceptorBinding → add spring-boot-starter-aop in Step 2. |
| HAS_JSONP |
Any Java file imports javax.json.* (JSON-P, not JSON-B) → migrate to Jackson ObjectMapper. |
| LOC_ESTIMATE |
find src/main/java -name '*.java' | xargs wc -l |
Complexity: GREEN (<5K LOC, simple) → YELLOW (5–20K, moderate) → ORANGE (20–50K, multi-module/JSF: break into sub-phases per module, validate independently, allocate 1.5× estimated time) → RED (>50K, EAR+JSF+JMS+Batch). RED projects: budget 2× estimated time and review Phase 0 scan results before starting.
Mixed servlet API detection: if BOTH javax.servlet and jakarta.servlet exist in imports, flag as a pre-migration blocker — resolve to a single namespace before proceeding.
Phase execution: Phases 1, 2, 5 ALWAYS run. Phase 3 runs if SECURITY_NEEDED or JMS_NEEDED. Phase 4 always runs but scope varies by flags. Log scan results and phase plan for traceability.
Phase 1: Build Foundation, Configuration & Persistence
Exit: mvn clean compile -Dmaven.test.skip=true passes.
Step 1 (if IS_MULTI_MODULE): Consolidate multi-module (EJB+WAR+EAR) into single Spring Boot module. After consolidation: (a) verify only ONE src/main/java tree exists — dual source trees cause edits to the wrong tree to have zero effect; (b) remove stale legacy module directories; (c) delete any build-helper-maven-plugin executions that referenced old module paths — Maven silently ignores dead source paths.
Step 2: Create Spring Boot project foundation.
- Replace parent POM with spring-boot-starter-parent (3.2.x+), change WAR→JAR, add spring-boot-maven-plugin.
- Add starters: web, data-jpa, actuator, validation, test.
- Conditional starters: security (if SECURITY_NEEDED), artemis (if JMS_NEEDED), batch (if BATCH_NEEDED), websocket (if WEBSOCKET_NEEDED), mail (if HAS_JAVAMAIL), thymeleaf (if JSF_NEEDED), aop (if HAS_CDI_INTERCEPTORS).
- If JMS_NEEDED with embedded broker: add
org.apache.activemq:artemis-jakarta-server (BOM-managed). Do NOT use artemis-jms-server — it is NOT in the Spring Boot 3.2.x BOM.
- If JAX_WS_NEEDED and preserving SOAP: add
spring-boot-starter-web-services AND wsdl4j:wsdl4j explicitly (Spring-WS 4.x drops wsdl4j as transitive).
- If HAS_JAXB: add jakarta.xml.bind-api + glassfish jaxb-runtime. If both HAS_JAXB and jackson-dataformat-xml are present, add
@JacksonXmlRootElement(localName=<original @XmlRootElement name>) to every XML-serialized DTO — jackson-dataformat-xml silently ignores @XmlRootElement.
hibernate-jpamodelgen: if present, change groupId from org.hibernate to org.hibernate.orm (Hibernate 6 / Spring Boot 3.x rename). The Spring Boot BOM manages org.hibernate.orm:hibernate-jpamodelgen.
- Maven-plugin dual-listing: before removing plugins from
<build><plugins>, grep <dependencies> for <type>maven-plugin</type> entries (e.g., maven-war-plugin, wildfly-maven-plugin). Remove both locations in same pass.
- Remove JBoss BOMs, repositories, plugins.
Step 3: Migrate dependencies — remove individual Spring/Hibernate deps (managed by starters). Remove legacy libs (joda-time, codehaus jackson, c3p0, javax.inject). Keep lombok with provided scope. Replace javax.xml.bind.* imports with jakarta.xml.bind.* (do NOT delete JAXB tests).
Step 4: Create @SpringBootApplication class and configuration.
- Create application.yml: server.servlet.context-path, spring.datasource., spring.jpa., management.endpoints.*.
- Session timeout: web.xml
<session-timeout> is in MINUTES; Spring Boot server.servlet.session.timeout without suffix defaults to SECONDS. Always use explicit m suffix: server.servlet.session.timeout: 30m (not 30).
- Before deleting persistence.xml: extract datasource JNDI names, any multiple persistence-unit entries, and custom Hibernate properties — these must be replicated in application.yml. If datasource is JNDI (
<jta-data-source>java:jboss/…</jta-data-source>), locate *-ds.xml or standalone.xml datasource subsystem for JDBC URL/driver/credentials. If absent, check pom.xml for available JDBC drivers and choose replacement. Add a MIGRATION comment naming the original JNDI reference.
- If using
ddl-auto=create or create-drop with data.sql: add spring.jpa.defer-datasource-initialization=true (without this, data.sql runs before DDL and INSERTs fail with "table not found").
- H2 scope decision: If JPA_NEEDED=true and no production database driver is in pom.xml, add
com.h2database:h2 with <scope>runtime</scope>. WARNING: If main application.yml contains jdbc:h2: URL, H2 MUST be <scope>runtime</scope> — using test scope causes ClassNotFoundException on @SpringBootTest while @DataJpaTest slices pass silently. Only use test scope when a separate production DB driver exists AND H2 URL is exclusively in src/test/resources/application.yml.
- If @SpringBootApplication is NOT in the root package of all entities/components: add
@EntityScan("com.example.root") and scanBasePackages on @SpringBootApplication. For sibling root packages (e.g., org.eclipse.pathfinder vs org.eclipse.cargotracker), include BOTH in scanBasePackages AND @EntityScan.
- YAML merge rule: application.yml sections are NOT merged across profiles — a profile-specific file REPLACES matching top-level keys entirely. If a profile defines any key within a section, it must repeat ALL keys in that section.
- Delete: jboss-web.xml, jboss-ejb3.xml, jboss-deployment-structure.xml, persistence.xml, beans.xml, web.xml.
- If HAS_SPRING_XML: convert to @Configuration + @Bean. If HAS_LOG4J: convert to logback-spring.xml.
Step 5: Migrate JPA entities and persistence.
- Replace all
javax.persistence → jakarta.persistence imports.
- Project-wide inline FQCN sweep:
grep -rn "javax\." src/ | grep -v "^.*import " at the start of Step 5 to record ALL non-import javax.* occurrences (catch blocks, DTOs, instanceof, return types) as explicit migration targets.
- Remove unitName from @PersistenceContext.
- Validation annotation mapping:
@Length → @Size
@NotEmpty → @NotBlank for String/CharSequence fields ONLY; for Collection/array/Map fields use jakarta.validation.constraints.@NotEmpty (change only the import). Applying @NotBlank to a collection causes ConstraintDeclarationException at startup.
@URL → @Pattern(regexp="^(https?|ftp)://.*")
@Range(min, max) → @Min(min) + @Max(max)
@Email (hibernate) → @Email (jakarta.validation)
@SafeHtml → remove (no equivalent). @ScriptAssert → custom ConstraintValidator.
- If HAS_JACKSON_V1: codehaus→fasterxml. If HAS_JODA_TIME: joda→java.time. If HAS_JSONB: JSON-B→Jackson annotations.
Phase 2: Business Logic & Presentation
Exit: mvn clean compile passes.
⚠ MIGRATION COMMENT RULE — Scope: applies to ALL comment text (Javadoc, inline, block, {@code}) in MIGRATION-labeled lines, in ALL file types (Java, test, Thymeleaf, HTML). Regular Javadoc describing annotations (e.g., /** Uses @Transactional for... */) is NOT subject to this rule — only MIGRATION-labeled comments.
Precedence: This rule TAKES PRECEDENCE over the MINIMIZE CHANGES principle. Always reword MIGRATION comments to avoid annotation tokens, even when quoting the original annotation would be shorter.
Rule: MIGRATION comments must NOT contain @AnnotationName tokens. Write plain English instead.
Per-task self-check: Before marking ANY task complete, delete .bak files then verify: find . -name "*.bak" -not -path "*/target/*" -delete && grep -rn "MIGRATION.*@[A-Z]" <files modified by this task> — must return empty. Fix immediately if not.
| ❌ Wrong |
✅ Correct |
// MIGRATION: Removed @ApplicationException |
// MIGRATION: Replaced EJB app exception with Spring ResponseStatus |
// MIGRATION: @Autowired removed |
// MIGRATION: Replaced field injection with constructor injection |
// MIGRATION: @Stateless converted |
// MIGRATION: Converted stateless session bean to Spring service |
// MIGRATION: @Scheduled replaces timer |
// MIGRATION: Converted EJB timer to Spring scheduled task |
// MIGRATION: @Ignore removed |
// MIGRATION: Re-enabled test after Arquillian removal |
// MIGRATION: @InjectMocks added |
// MIGRATION: Added Mockito injection for unit test |
// MIGRATION: @EnableBatchProcessing removed |
// MIGRATION: Removed batch annotation that disables auto-config |
// MIGRATION: @Async added |
// MIGRATION: Made event listener asynchronous |
See references/verify-legacy-imports.md for the full verification procedure.
Step 6: Migrate EJBs and CDI to Spring beans.
@Stateless → @Service + @Transactional ONLY when the service performs persistence operations (EntityManager, repository calls). Prerequisite: spring-boot-starter-data-jpa (includes spring-tx). For pure-computation services with no persistence, omit @Transactional. If a class has both @Stateless and @Path, produce a single @RestController (not @Service + @RestController) — see Step 9.
@Singleton → @Service/@Component (@Startup → CommandLineRunner or @EventListener(ApplicationReadyEvent.class)). Remove implements Serializable and serialVersionUID from singleton beans. Do NOT add @Scope("singleton") — it is the Spring default.
@Stateful → @Service + @SessionScope. WARNING: (a) @SessionScope requires the bean to be Serializable for HTTP session serialization. (b) If @PostConstruct accesses the scoped proxy before an HTTP session exists (e.g., during app startup), use @Scope("prototype") instead. (c) CDI @Produces @SessionScoped factory methods → @Bean @SessionScope in a @Configuration class. See references/test-configuration-patterns.md for test setup.
- Remove
@LocalBean, @Local, @Remote. Replace @EJB/@Inject → constructor injection.
- CDI no-arg constructor removal: CDI requires public no-arg constructors for proxy generation. Spring single-constructor injection does not — remove no-arg constructors that exist only for CDI compliance. Keep them only if the class is serialized or has framework-mandated no-arg requirements.
@PersistenceContext EntityManager: replace @Inject EntityManager with @PersistenceContext private EntityManager em. This is exempt from the constructor injection rule.
- CDI
@Produces Logger → private static final Logger logger = LoggerFactory.getLogger(ClassName.class). Delete the LoggerProducer class.
- CDI scopes:
@ApplicationScoped → @Component, @RequestScoped → @Component + @RequestScope, @Model → @Controller + @RequestScope.
- CDI events:
Event<T>.fire() → ApplicationEventPublisher.publishEvent(), Event<T>.fireAsync() → ApplicationEventPublisher.publishEvent() + @Async on the @EventListener. CRITICAL structural difference: @Observes is a PARAMETER annotation; @EventListener is a METHOD annotation — move annotation to method, parameter becomes plain type. Cross-file: @EnableAsync must be present on any @Configuration or @SpringBootApplication when @Async @EventListener exists — absence causes silent synchronous execution. CompletionStage cleanup: remove entire CompletionStage variable and .exceptionally() lambda from fireAsync() callers — publishEvent() returns void. Payload type verification: grep publishEvent() call sites to confirm actual payload type matches listener parameter type. CDI qualifier-differentiated events: define static nested wrapper classes per qualifier, use publishEvent(new QualifierEvent(payload)).
- CDI interceptors/decorators →
@Aspect + @Around. @Around advice methods MUST declare throws Throwable — without it, checked exceptions from the intercepted method are wrapped in UndeclaredThrowableException.
- CDI
@Alternative → @ConditionalOnProperty. COMPLEMENTARY CONDITION RULE: the default bean gets matchIfMissing=true and the alternative gets matchIfMissing=false on the same property. DEPENDENCY CHAIN RULE: apply the same condition to every bean in the activation chain that has side effects in its constructor.
- For custom CDI qualifier
@interface files: import org.springframework.beans.factory.annotation.Qualifier (compile classpath) — NOT jakarta.inject.Qualifier (runtime only via hibernate-core, causes compile error).
- Scan all
package-info.java for @Vetoed — remove the annotation and its CDI import.
- EJB lifecycle annotations:
@PostActivate / @PrePassivate — DELETE the annotation AND the method body (no Spring equivalent for stateful passivation). @PostConstruct / @PreDestroy (JSR-250) — RETAIN as-is (Spring supports them natively).
- ⚠ STOP: Before deleting JaxRsActivator.java / JAX-RS Application subclass, record its
@ApplicationPath value. This prefix must be prepended to EVERY @RequestMapping in Step 9. Losing it causes 404 on ALL REST endpoints. Delete Resources.java (CDI @Produces EntityManager).
- EJB exception hierarchy:
EJBTransactionRolledbackException → catch as DataIntegrityViolationException. Maintain most-specific-first catch ordering.
- @Schedules (plural): split into separate
@Scheduled methods, one per cron expression, each delegating to a shared private method. Drop persistent=false (no Spring equivalent).
- Replace
java.util.logging.Logger → org.slf4j.Logger. See references/common-pitfalls-extended.md § JUL→SLF4J Quick Reference for the complete level mapping, placeholder conversion, and import replacement. Key rules: severe→error, warning→warn, fine→debug, config→info; {0},{1} → {},{}; unwrap new Object[]{a,b} to varargs; drop .getName() from LoggerFactory.getLogger().
- javax→jakarta completeness: when migrating a file, ALL
javax.* namespaces in that file must be updated in the same pass. Mixed-namespace files cause Step 17 failures.
- If HAS_EJB_TIMERS:
@Schedule → @Scheduled(cron=…), @Timeout → @Scheduled(fixedDelay=…), add @EnableScheduling. @EnableScheduling/@EnableAsync can be on any @Component subtype including @Service — does not require the main class. Note: fixedDelay/fixedRate values must be compile-time constants — replace TimeUnit.SECONDS.toMillis(3) with 3000L or use fixedDelayString="${app.timer.delay}". For dynamic timer lifecycle (timerService.createIntervalTimer()/timer.cancel()), use an active-flag pattern: volatile boolean active; @Scheduled method checks if (!active) return; start/stop toggle the flag.
- If HAS_SERVLET_FILTERS:
@WebFilter → OncePerRequestFilter + @Component. @WebListener → @Component.
- If HAS_JAVAMAIL: manual Session/Transport →
JavaMailSender, configure spring.mail.*.
- If HAS_GUAVA:
Optional.fromNullable → Optional.ofNullable.
- If HAS_JSONP: replace
javax.json.Json, JsonObject, JsonArray with Jackson ObjectMapper, ObjectNode, ArrayNode.
Step 7: Replace JNDI lookups — @Resource(lookup=…) → @Value/@ConfigurationProperties, InitialContext.lookup() → Spring DI.
Step 8: Replace container-managed transactions — REQUIRED→@Transactional, REQUIRES_NEW→Propagation.REQUIRES_NEW, NOT_SUPPORTED→Propagation.NOT_SUPPORTED, BMT→TransactionTemplate. When using TransactionTemplate, declare it as a @Bean — it is NOT auto-configured by Spring Boot. @RolesAllowed → @PreAuthorize (if SECURITY_NEEDED, else remove).
Step 8b (if multi-datasource): Define separate DataSource/EntityManagerFactory/TransactionManager beans per datasource, or use JTA with me.snowdrop:narayana-spring-boot-starter (NOT spring-boot-starter-jta-narayana, which does not exist for Boot 3.x). Narayana requires explicit version — it is NOT managed by the Spring Boot BOM. Check Maven Central for the latest 3.x-compatible release. Note: spring-boot-starter-jta-atomikos was also removed from Boot 3.x. Hibernate Envers @Audited works as-is after javax→jakarta import migration.
Step 9: Migrate JAX-RS to Spring MVC. See references/jaxrs-spring-mvc-migration.md for detailed patterns.
- @ApplicationPath folding (CRITICAL): Locate the JAX-RS Application subclass (deleted in Step 6) and note its
@ApplicationPath value. Prepend this to EVERY @RequestMapping path. Spring Boot has no @ApplicationPath equivalent. @ApplicationPath("/rest") + @Path("/members") → @RequestMapping("/rest/members"). Empty @ApplicationPath("") or @ApplicationPath("/") → no prefix needed.
@Path → @RestController + @RequestMapping. @GET/@POST/@PUT/@DELETE → @GetMapping/@PostMapping/@PutMapping/@DeleteMapping.
@PathParam → @PathVariable. @QueryParam("name") Type param → @RequestParam(value="name", required=false) Type param — JAX-RS query params are always optional; omitting required=false returns HTTP 400 for missing params.
@HeaderParam → @RequestHeader. @FormParam → @RequestParam (with @PostMapping).
- Unannotated parameter in
@POST/@PUT method with @Consumes → add @RequestBody. Spring MVC requires this explicitly; omitting it silently results in null.
Response → ResponseEntity. JAX-RS ExceptionMapper → @ControllerAdvice + @ExceptionHandler (preserve HTTP status codes and error body format). Note: @ResponseStatus drops the body — use ResponseEntity.badRequest().body(errors) when error bodies are needed.
@Context UriInfo → ServletUriComponentsBuilder. See references/jaxrs-spring-mvc-migration.md § UriInfo Translation Table for the full mapping. Replace field injection with constructor injection.
- JAX-RS
ContainerRequestFilter → HandlerInterceptor or OncePerRequestFilter. ContainerResponseFilter → HandlerInterceptor.postHandle() or OncePerRequestFilter. CRITICAL: set ALL response headers BEFORE chain.doFilter() — headers set after doFilter() are silently dropped on a committed response.
Step 9b (if JAX_WS_NEEDED): See references/jaxws-websocket-migration.md for JAX-WS SOAP migration.
Step 9c (if WEBSOCKET_NEEDED): See references/jaxws-websocket-migration.md for WebSocket migration. For JAX-RS SSE (SseEventSink/SseBroadcaster): see references/jaxrs-spring-mvc-migration.md § JAX-RS SSE → SseEmitter.
Phase 3: Security & Messaging (CONDITIONAL)
SKIP if SECURITY_NEEDED=false AND JMS_NEEDED=false. Exit: mvn clean compile.
Step 10 (if SECURITY_NEEDED): Create SecurityConfig with @EnableWebSecurity + SecurityFilterChain bean. Map web.xml constraints to authorizeHttpRequests(). Actuator ordering: .requestMatchers("/actuator/**").permitAll() must be the FIRST rule in authorizeHttpRequests chain — later rules can shadow it. @RolesAllowed → @PreAuthorize + @EnableMethodSecurity. Configure formLogin/httpBasic. SessionContext.getCallerPrincipal() → SecurityContextHolder.getContext().getAuthentication().
Step 11 (if JMS_NEEDED): @MessageDriven → @Component + @JmsListener. MessageProducer → JmsTemplate. Embedded Artemis: spring.artemis.mode=embedded, add artemis-jakarta-server dependency. External broker: spring.artemis.mode=native + broker-url.
Step 11b (if BATCH_NEEDED): Convert META-INF/batch-jobs/*.xml → @Configuration with Job/Step beans. See references/spring-batch5-migration.md for Spring Batch 5 specifics.
ItemWriter.write(List<? extends T>) → write(Chunk<? extends T>). Use chunk.getItems() for the list.
- Do NOT add
@EnableBatchProcessing — it disables BatchAutoConfiguration on Spring Boot 3.
@BatchProperty → @Value("#{jobParameters['paramName']}"). Annotate ANY component using @Value("#{jobParameters[…]}") with @StepScope — not just ItemReader.
- Scheduled jobs: include
run.id=System.currentTimeMillis() in JobParameters to avoid JobInstanceAlreadyCompleteException.
Phase 4: Testing & UI
Exit: mvn clean test passes.
Step 12: Migrate tests. See references/test-configuration-patterns.md for core patterns and references/test-mockito-advanced.md for Mockito-specific patterns.
- Ensure
src/test/resources/ directory exists: mkdir -p src/test/resources/.
- Add @SpringBootTest context-loads test + @WebMvcTest/MockMvc controller tests.
- Create
src/test/resources/application.yml with H2 override: jdbc:h2:mem:testdb, ddl-auto=create-drop. This file REPLACES (not merges with) main application.yml — copy ALL non-datasource config verbatim, including: server.servlet.context-path, spring.jpa.hibernate.naming.physical-strategy, spring.jpa.defer-datasource-initialization, management.* actuator settings. Do NOT set explicit spring.jpa.properties.hibernate.dialect — Hibernate 6 auto-detects from JDBC URL; explicit dialect triggers HHH90000025 deprecation. Only create this H2 override if JPA_NEEDED=true.
- If @SpringBootApplication is in a different package subtree from tests: use
@SpringBootTest(classes = MainApp.class).
- If ARQUILLIAN_TESTS: remove @RunWith(Arquillian), @Deployment, ShrinkWrap → @SpringBootTest + @AutoConfigureMockMvc. Preserve all test methods/assertions. Migrate data setup to @TestConfiguration, @Sql, or @BeforeEach. Delete arquillian.xml, test-ds.xml.
- If SECURITY_NEEDED: add spring-security-test; use
@WithMockUser in controller tests. @MockBean the HandlerInterceptor if it requires authentication context — see references/test-mockito-advanced.md.
- JUnit 4→5:
@RunWith → @ExtendWith, Assert → Assertions. CRITICAL — count args before reordering: 2-arg assertEquals("Widget", actual) — the String IS the expected value, do NOT reorder. Only 3-arg assertEquals("msg", expected, actual) needs the message moved to last. Message-last applies to ALL assertion methods: assertTrue("msg", cond) → assertTrue(cond, "msg"); assertFalse("msg", cond) → assertFalse(cond, "msg"); assertNotNull("msg", obj) → assertNotNull(obj, "msg"); assertNull("msg", obj) → assertNull(obj, "msg"). See references/test-configuration-patterns.md for full rule.
@Test(expected=X.class) → assertThrows(X.class, () -> { ... }). Place ONLY the throwing call inside the lambda — never Mockito stubs or setup.
- Do NOT mechanically convert
@Ignore to @Disabled. Investigate WHY the test was ignored — Arquillian-era reasons often don't apply in Spring Boot + H2.
- Do NOT @Disable tests. Ordered tests sharing committed DB state: omit class-level @Transactional — each method must commit so subsequent methods observe prior data.
- javax→jakarta in test files: test files need the same
javax.* → jakarta.* migration as production code.
- @WebMvcTest rules: (a) never combine with
@AutoConfigureMockMvc (redundant); (b) mutually exclusive with @ExtendWith(MockitoExtension.class) — use @MockBean instead; (c) omit classes= when test is in @SpringBootApplication package subtree (auto-discovered); (d) custom @EnableWebSecurity configs are NOT auto-loaded — add @Import(SecurityConfig.class) to avoid CSRF 403 errors; (e) auto-includes any @Component implementing Converter/Formatter/GenericConverter — add @MockBean for non-trivial dependency chains; (f) @PersistenceContext on controller → add @MockBean EntityManagerFactory (PersistenceAnnotationBeanPostProcessor is active in MVC slice); (g) thenReturn() stubs must match exact service return type — Mockito generic inference fails on type mismatch.
- @InjectMocks does NOT populate
@PersistenceContext or @Value fields — use ReflectionTestUtils.setField() in @BeforeEach. See references/test-mockito-advanced.md.
- UnnecessaryStubbingException under MockitoExtension: wrap subset-only
@BeforeEach stubs with lenient().when(). Do NOT apply class-level LENIENT strictness. See references/test-mockito-advanced.md.
- XML produces endpoints: chain
.accept(MediaType.APPLICATION_XML) in MockMvc for endpoints with produces=APPLICATION_XML_VALUE.
*IT.java integration tests: run via maven-failsafe-plugin, not surefire. Add failsafe include if needed — see references/test-configuration-patterns.md.
@Lazy @Value("${local.server.port}"): use @Lazy or inject via @LocalServerPort in @BeforeEach. See references/test-configuration-patterns.md.
- If BATCH_NEEDED: align test call sites with Spring Batch 5 API (Chunk, SkipListener rename, checkpoint/restart test removal).
- Smoke test package: place in the root test package matching
@SpringBootApplication package.
- ClassPathXmlApplicationContext in tests: if backing XML was deleted in Step 4, migrate to
@SpringBootTest + @TestConfiguration.
Step 13: Migrate UI and static resources.
- If JSF_NEEDED: convert .xhtml → Thymeleaf templates (src/main/resources/templates/). See
references/jsf-backing-bean-migration.md for backing bean → @Controller patterns.
spring-boot-starter-thymeleaf does NOT include Layout Dialect — do NOT use layout:decorate. Use built-in th:fragment parameterized fragments.
- Thymeleaf URL preprocessing for dynamic base URLs:
@{__${url}__(page=${n})}.
- Pre-encoded URL values: bypass
@{} to avoid double-encoding — use ${#request.contextPath + '/path?p=' + val}.
- If the application uses BRMS/Drools/KIE: preserve the KIE/Drools runtime and wire via a
@Configuration class producing KieContainer @Bean instead of CDI @Produces. See references/phase0-detection-flags.md for HAS_DROOLS flag.
- If STATIC_UI_EXISTS: move resources from webapp/ → resources/static/. Create WebMvcConfigurer for directory index.
- Delete src/main/webapp/ after migration. Delete faces-config.xml.
Phase 5: Deployment & Verification
Exit: mvn clean verify passes + all 20 exit criteria met.
Step 14: Configure Actuator — expose health/info/metrics, enable liveness/readiness probes. Custom health checks → HealthIndicator. Mandatory YAML for standalone mode (probes NOT auto-enabled):
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
probes:
enabled: true
info:
env:
enabled: true
Step 15: Containerize with multi-stage Dockerfile. Use this template (choose Java 17 or 21 based on pom.xml java.version):
# -- Build stage --
FROM maven:3.9-amazoncorretto-17 AS build
# For Java 21: maven:3.9-amazoncorretto-21
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests -B
# -- Runtime stage --
FROM amazoncorretto:17-alpine
# For Java 21: amazoncorretto:21-alpine
# Fallback if amazoncorretto unavailable: eclipse-temurin:17-jre-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
RUN chown appuser:appgroup app.jar
USER appuser
EXPOSE 8080
ENTRYPOINT ["java", \
"-XX:+UseContainerSupport", \
"-XX:MaxRAMPercentage=75.0", \
"-jar", "app.jar"]
Variants: (a) Multi-module: COPY <module>/target/<finalName>.jar app.jar; COPY all referenced pom.xml files before dependency:go-offline. (b) Local/proprietary artifacts (KJar, custom libs): COPY libs/ . + RUN mvn install:install-file … before dependency:go-offline. (c) Non-Alpine images (amazoncorretto non-alpine, eclipse-temurin non-alpine): use groupadd -r / useradd -r -g, NOT addgroup -S / adduser -S — Alpine BusyBox syntax crashes Debian-based images.
Spring Boot 3.x JarLauncher path is org.springframework.boot.loader.launch.JarLauncher (NOT .loader.JarLauncher). Create .dockerignore.
Step 16: Cleanup — update README.md, .gitignore. Delete remaining JBoss configs and legacy directories. Clean up editor backup files — run TWICE (second pass catches backups created during first pass):
find . -name "*.bak" -not -path "*/target/*" -delete
find . -name "*.bak" -not -path "*/target/*" -delete
Add *.bak to .gitignore. Post-delete verification: find . -name "*.bak" -not -path "*/target/*" — must return empty.
Step 17: Final verification scan. See references/verify-legacy-imports.md for full procedure with copy-paste-ready grep commands. Run each grep INDEPENDENTLY — never chain with && (grep returns exit code 1 on zero matches, silently skipping later checks).
Reference Dispatch
Load reference files on demand when the worker encounters these signals:
| Signal in Source Code |
Reference File |
| Phase 0 library flag detection, pom.xml dep scanning |
references/phase0-detection-flags.md |
@Path, @ApplicationPath, @QueryParam, JAX-RS Response, SSE SseEventSink |
references/jaxrs-spring-mvc-migration.md |
@WebService, @ServerEndpoint, @OnMessage |
references/jaxws-websocket-migration.md |
jakarta.batch, META-INF/batch-jobs/, ItemWriter |
references/spring-batch5-migration.md |
@Embeddable, @Lob on array fields, Hibernate 6 nulls, CriteriaQuery |
references/hibernate6-behavior-changes.md |
Arquillian, test context, H2, JUnit 4→5, *IT.java |
references/test-configuration-patterns.md |
@InjectMocks, Mockito, STRICT_STUBS, @MockBean |
references/test-mockito-advanced.md |
.xhtml, @ViewScoped, @FlowScoped, JSF backing beans |
references/jsf-backing-bean-migration.md |
| Build errors, runtime exceptions, configuration problems, JUL→SLF4J |
references/common-pitfalls-extended.md |
| MDB→@JmsListener, Security worked examples |
references/worked-examples-conditional.md |
Reference Procedures
The procedures below describe mechanical transforms and verification sweeps the worker performs on demand using standard shell commands. There are no executable scripts — the worker reads each procedure and issues the commands directly.
Pre-migration procedures run BEFORE any file-by-file migration work.
Post-batch procedures run AFTER each batch of related changes.
Post-migration procedures run AFTER all code changes are complete.
references/jaxrs-spring-mvc-migration.md — Detailed JAX-RS → Spring MVC mapping rules including @ApplicationPath folding, @QueryParam required=false, @FormParam→@RequestParam, implicit @RequestBody, UriBuilder, Response patterns, MultivaluedMap hierarchy, ExceptionMapper→@ControllerAdvice, SSE→SseEmitter, UriInfo translatio
…(truncated)
1---2name: jboss-to-spring-boot3description: Migrates Java EE/Jakarta EE enterprise applications from JBoss EAP/WildFly to Spring Boot 3.2.x standalone JARs. Covers EJB→Spring beans, JAX-RS→Spring MVC, CDI→Spring DI, JPA javax→jakarta, JMS→Spring JMS, JSF→Thymeleaf, security, batch, testing, containerization. Uses conditional pipeline: Phase 0 detects features, subsequent phases run only when needed. Trigger: JBoss, WildFly, Java EE, Jakarta EE, Spring Boot migration, EJB, JAX-RS, CDI.4---56# JBoss-to-Spring-Boot Migration78## Objective910Migrate Java EE/Jakarta EE applications on JBoss EAP/WildFly to Spring Boot 3.2.x, eliminating application server dependencies and modernizing to cloud-native containerized deployment.1112## Scope1314Converts JBoss/WildFly-based enterprise Java apps into standalone Spring Boot JARs with embedded servers. Phase 0 scans the project to determine which migration phases are needed; only required phases execute.1516**Non-Goals** (require separate handling):171. EJB Remote Interfaces over IIOP/CORBA — redesign to REST, gRPC, or messaging182. JCA Resource Adapters — replace with Spring Integration or vendor client libraries193. EJB 2.x Entity Beans (CMP/BMP) — manually convert to JPA entities first204. Proprietary JBoss Clustering (JGroups/Infinispan) — use Spring Session with Redis/Hazelcast215. Vaadin UI — version-specific upgrade (Vaadin 8→24 Flow with vaadin-spring-boot-starter)226. Complex Arquillian @Deployment data init — migrate to @Sql/@TestConfiguration/@BeforeEach237. JAXB Marshalling Tests — migrate to jakarta.xml.bind equivalents, never delete248. Uncommon Hibernate validators (@SafeHtml, @ScriptAssert, etc.) — custom ConstraintValidator259. XA/JTA Distributed Transactions — per-datasource @Transactional or JtaTransactionManager2627## Constraints2829### API & Functional Parity30- Preserve all public class names, method signatures, REST endpoint paths, HTTP methods, request/response formats, and status codes.31- Business logic must remain unchanged — only framework/infrastructure code is modified.32- Transaction boundaries and isolation levels must be preserved.33- Preserve Javadoc comments and code documentation throughout the migration.3435### Test Integrity36- Do NOT mark tests @Disabled or delete tests as a migration shortcut.37- Migrate Arquillian tests to @SpringBootTest equivalents preserving all assertions.38- JAXB tests → migrate to jakarta.xml.bind or Jackson equivalents.3940### Security41- CRITICAL: Only add spring-boot-starter-security if SECURITY_NEEDED=true.42- Never hardcode credentials in source code.4344### Code Quality45- Constructor injection throughout. Exceptions:46 1. `@PersistenceContext EntityManager` — correct Spring idiom for JPA EntityManager; exempt from constructor injection.47 2. `@Value` on scalar-property fields in `@Component` configuration POJOs — simple config holders, not collaborator dependencies.48 3. Fields with public setters mandated by an implemented interface (e.g., framework callback interfaces requiring setter injection).49- **Abstract base class refactoring**: enumerate ALL concrete subclasses first, update ALL in the same pass with `super(…)` calls. Incomplete passes leave half the hierarchy on field injection.50- **@SessionScope Serializable beans**: non-serializable dependencies (HttpServletRequest, JmsTemplate) — use constructor injection + `private transient` field. Transient prevents serialization errors but does NOT exempt from constructor injection rule.51- Follow Spring Boot conventions. Remove unused imports and dead code.5253### Incremental Migration54- Verify build passes after each phase before proceeding. Commit after each successful phase.5556## Worked Examples5758### Example 1: @Stateless EJB → Spring @Service5960**Before (JBoss):**61```java62import javax.ejb.Stateless;63import javax.ejb.EJB;64import javax.ejb.TransactionAttribute;65import javax.ejb.TransactionAttributeType;6667@Stateless68public class OrderService {69 @EJB70 private InventoryService inventoryService;7172 @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)73 public Order placeOrder(OrderRequest request) {74 inventoryService.reserve(request.getItemId(), request.getQuantity());75 return createOrder(request);76 }77}78```7980**After (Spring Boot):**81```java82import org.springframework.stereotype.Service;83import org.springframework.transaction.annotation.Transactional;84import org.springframework.transaction.annotation.Propagation;8586@Service87@Transactional88public class OrderService {89 private final InventoryService inventoryService;9091 public OrderService(InventoryService inventoryService) {92 this.inventoryService = inventoryService;93 }9495 @Transactional(propagation = Propagation.REQUIRES_NEW)96 public Order placeOrder(OrderRequest request) {97 inventoryService.reserve(request.getItemId(), request.getQuantity());98 return createOrder(request);99 }100}101```102103### Example 2: JAX-RS Resource → Spring MVC Controller104105See `references/jaxrs-spring-mvc-migration.md` for a complete before/after JAX-RS→Spring MVC example with `@ApplicationPath` folding, `@QueryParam` → `@RequestParam(required=false)`, and `Response` → `ResponseEntity`.106107> Additional worked examples (MDB→@JmsListener, JBoss Security→Spring Security) in `references/worked-examples-conditional.md`.108109## Workflow110111### Pipeline Architecture112113```114Phase 0: Scanner → Phase 1: Build & Config → Phase 2: Business Logic115 │ (ALWAYS) (ALWAYS)116 ├→ Phase 3: Security & Messaging (if SECURITY_NEEDED or JMS_NEEDED)117 ├→ Phase 4: Testing & UI (ALWAYS, scope varies)118 └→ Phase 5: Deployment & Verification (ALWAYS)119```120121### Phase 0: Project Analysis122123Scan the project and set feature flags to determine which phases execute.124125**Core feature flags** (determine phase execution):126127| Flag | Detection Rule |128|------|---------------|129| SECURITY_NEEDED | `<security-constraint>` in web.xml, `@RolesAllowed`, `@DeclareRoles`, or `<security-domain>` in jboss-web.xml |130| JMS_NEEDED | `@MessageDriven`, `@JMSDestinationDefinition`, `javax.jms.*`/`jakarta.jms.*` imports |131| JSF_NEEDED | `.xhtml` in src/main/webapp/, or `javax.faces.*`/`jakarta.faces.*` imports |132| BATCH_NEEDED | `jakarta.batch.*`/`javax.batch.*` imports, or META-INF/batch-jobs/ directory |133| WEBSOCKET_NEEDED | `@ServerEndpoint` or `jakarta.websocket.*` imports |134| JPA_NEEDED | pom.xml contains `spring-boot-starter-data-jpa` (post-Step 2) OR any Java file imports `javax.persistence.*`/`jakarta.persistence.*` |135| STATIC_UI_EXISTS | .html/.css/.js/.jsp/image files in src/main/webapp/ |136| ARQUILLIAN_TESTS | `@RunWith(Arquillian`, `@Deployment`, or arquillian.xml |137| IS_MULTI_MODULE | pom.xml `<modules>` or packaged as EAR |138139**Library flags** — full detection rules in `references/phase0-detection-flags.md`:140141| Flag | Detection Rule |142|------|---------------|143| HAS_JODA_TIME | pom.xml contains `joda-time` dependency |144| HAS_JACKSON_V1 | pom.xml contains `org.codehaus.jackson`, OR imports `org.codehaus.jackson` |145| HAS_C3P0 | pom.xml contains `c3p0`, OR persistence.xml has `hibernate.c3p0` properties |146| HAS_GUAVA | pom.xml contains `com.google.guava` **AND** any Java file imports `com.google.common.base.Optional` (both required) |147| HAS_LOMBOK | pom.xml contains `org.projectlombok` dependency |148| HAS_SPRING_XML | Any XML in src/main/resources/ or src/main/webapp/WEB-INF/ contains `<beans xmlns="http://www.springframework.org/schema/beans"` |149| HAS_LOG4J | src/main/resources/ contains `log4j.xml` or `log4j.properties` |150| JAX_WS_NEEDED | `@WebService`, `@WebMethod`, or `javax.xml.ws.*`/`jakarta.xml.ws.*` imports |151| HAS_JAXB | Imports `javax.xml.bind.*`/`jakarta.xml.bind.*`, OR pom.xml contains `jaxb-api` |152| HAS_JSONB | Imports `jakarta.json.bind.*`/`javax.json.bind.*`, OR pom.xml contains `jakarta.json.bind-api` |153| HAS_EJB_TIMERS | `@Schedule`, `@Timeout`, or `TimerService` from `javax.ejb`/`jakarta.ejb` |154| HAS_JAVAMAIL | Imports `javax.mail.*`/`jakarta.mail.*`, OR pom.xml contains javax.mail/jakarta.mail |155| HAS_SERVLET_FILTERS | `@WebFilter` or `@WebListener` annotations |156| HAS_MULTI_DATASOURCE | persistence.xml contains multiple `<persistence-unit>` elements, OR standalone.xml defines multiple datasource subsystems. Fallback: `grep -c '<jta-data-source>' src/main/resources/META-INF/persistence.xml` with count >1. |157| SUREFIRE_DISABLED | pom.xml `<skip>true</skip>` or `<skipTests>true</skipTests>` under maven-surefire-plugin. **Fix**: remove `<skip>true</skip>` or set to `false`; verify with `mvn help:effective-pom | grep -A5 surefire`. |158| HAS_CDI_INTERCEPTORS | Any Java file contains `@Interceptor` or `@InterceptorBinding` → add `spring-boot-starter-aop` in Step 2. |159| HAS_JSONP | Any Java file imports `javax.json.*` (JSON-P, not JSON-B) → migrate to Jackson `ObjectMapper`. |160| LOC_ESTIMATE | `find src/main/java -name '*.java' \| xargs wc -l` |161162**Complexity**: GREEN (<5K LOC, simple) → YELLOW (5–20K, moderate) → ORANGE (20–50K, multi-module/JSF: break into sub-phases per module, validate independently, allocate 1.5× estimated time) → RED (>50K, EAR+JSF+JMS+Batch). RED projects: budget 2× estimated time and review Phase 0 scan results before starting.163164**Mixed servlet API detection**: if BOTH `javax.servlet` and `jakarta.servlet` exist in imports, flag as a pre-migration blocker — resolve to a single namespace before proceeding.165166**Phase execution**: Phases 1, 2, 5 ALWAYS run. Phase 3 runs if SECURITY_NEEDED or JMS_NEEDED. Phase 4 always runs but scope varies by flags. Log scan results and phase plan for traceability.167168### Phase 1: Build Foundation, Configuration & Persistence169170Exit: `mvn clean compile -Dmaven.test.skip=true` passes.171172**Step 1** (if IS_MULTI_MODULE): Consolidate multi-module (EJB+WAR+EAR) into single Spring Boot module. After consolidation: (a) verify only ONE src/main/java tree exists — dual source trees cause edits to the wrong tree to have zero effect; (b) remove stale legacy module directories; (c) delete any `build-helper-maven-plugin` executions that referenced old module paths — Maven silently ignores dead source paths.173174**Step 2**: Create Spring Boot project foundation.175- Replace parent POM with spring-boot-starter-parent (3.2.x+), change WAR→JAR, add spring-boot-maven-plugin.176- Add starters: web, data-jpa, actuator, validation, test.177- Conditional starters: security (if SECURITY_NEEDED), artemis (if JMS_NEEDED), batch (if BATCH_NEEDED), websocket (if WEBSOCKET_NEEDED), mail (if HAS_JAVAMAIL), thymeleaf (if JSF_NEEDED), aop (if HAS_CDI_INTERCEPTORS).178- If JMS_NEEDED with embedded broker: add `org.apache.activemq:artemis-jakarta-server` (BOM-managed). Do NOT use `artemis-jms-server` — it is NOT in the Spring Boot 3.2.x BOM.179- If JAX_WS_NEEDED and preserving SOAP: add `spring-boot-starter-web-services` AND `wsdl4j:wsdl4j` explicitly (Spring-WS 4.x drops wsdl4j as transitive).180- If HAS_JAXB: add jakarta.xml.bind-api + glassfish jaxb-runtime. If both HAS_JAXB and jackson-dataformat-xml are present, add `@JacksonXmlRootElement(localName=<original @XmlRootElement name>)` to every XML-serialized DTO — jackson-dataformat-xml silently ignores `@XmlRootElement`.181- `hibernate-jpamodelgen`: if present, change groupId from `org.hibernate` to `org.hibernate.orm` (Hibernate 6 / Spring Boot 3.x rename). The Spring Boot BOM manages `org.hibernate.orm:hibernate-jpamodelgen`.182- **Maven-plugin dual-listing**: before removing plugins from `<build><plugins>`, grep `<dependencies>` for `<type>maven-plugin</type>` entries (e.g., maven-war-plugin, wildfly-maven-plugin). Remove both locations in same pass.183- Remove JBoss BOMs, repositories, plugins.184185**Step 3**: Migrate dependencies — remove individual Spring/Hibernate deps (managed by starters). Remove legacy libs (joda-time, codehaus jackson, c3p0, javax.inject). Keep lombok with provided scope. Replace javax.xml.bind.* imports with jakarta.xml.bind.* (do NOT delete JAXB tests).186187**Step 4**: Create @SpringBootApplication class and configuration.188- Create application.yml: server.servlet.context-path, spring.datasource.*, spring.jpa.*, management.endpoints.*.189- **Session timeout**: web.xml `<session-timeout>` is in MINUTES; Spring Boot `server.servlet.session.timeout` without suffix defaults to SECONDS. Always use explicit `m` suffix: `server.servlet.session.timeout: 30m` (not `30`).190- **Before deleting persistence.xml**: extract datasource JNDI names, any multiple persistence-unit entries, and custom Hibernate properties — these must be replicated in application.yml. If datasource is JNDI (`<jta-data-source>java:jboss/…</jta-data-source>`), locate `*-ds.xml` or `standalone.xml` datasource subsystem for JDBC URL/driver/credentials. If absent, check pom.xml for available JDBC drivers and choose replacement. Add a MIGRATION comment naming the original JNDI reference.191- If using `ddl-auto=create` or `create-drop` with data.sql: add `spring.jpa.defer-datasource-initialization=true` (without this, data.sql runs before DDL and INSERTs fail with "table not found").192- **H2 scope decision**: If JPA_NEEDED=true and no production database driver is in pom.xml, add `com.h2database:h2` with `<scope>runtime</scope>`. WARNING: If main application.yml contains `jdbc:h2:` URL, H2 MUST be `<scope>runtime</scope>` — using test scope causes `ClassNotFoundException` on `@SpringBootTest` while `@DataJpaTest` slices pass silently. Only use test scope when a separate production DB driver exists AND H2 URL is exclusively in `src/test/resources/application.yml`.193- If @SpringBootApplication is NOT in the root package of all entities/components: add `@EntityScan("com.example.root")` and `scanBasePackages` on @SpringBootApplication. For sibling root packages (e.g., `org.eclipse.pathfinder` vs `org.eclipse.cargotracker`), include BOTH in scanBasePackages AND @EntityScan.194- **YAML merge rule**: application.yml sections are NOT merged across profiles — a profile-specific file REPLACES matching top-level keys entirely. If a profile defines any key within a section, it must repeat ALL keys in that section.195- Delete: jboss-web.xml, jboss-ejb3.xml, jboss-deployment-structure.xml, persistence.xml, beans.xml, web.xml.196- If HAS_SPRING_XML: convert to @Configuration + @Bean. If HAS_LOG4J: convert to logback-spring.xml.197198**Step 5**: Migrate JPA entities and persistence.199- Replace all `javax.persistence` → `jakarta.persistence` imports.200- **Project-wide inline FQCN sweep**: `grep -rn "javax\." src/ | grep -v "^.*import "` at the start of Step 5 to record ALL non-import javax.* occurrences (catch blocks, DTOs, instanceof, return types) as explicit migration targets.201- Remove unitName from @PersistenceContext.202- Validation annotation mapping:203 - `@Length` → `@Size`204 - `@NotEmpty` → `@NotBlank` **for String/CharSequence fields ONLY**; for Collection/array/Map fields use `jakarta.validation.constraints.@NotEmpty` (change only the import). Applying `@NotBlank` to a collection causes ConstraintDeclarationException at startup.205 - `@URL` → `@Pattern(regexp="^(https?|ftp)://.*")`206 - `@Range(min, max)` → `@Min(min)` + `@Max(max)`207 - `@Email` (hibernate) → `@Email` (jakarta.validation)208 - `@SafeHtml` → remove (no equivalent). `@ScriptAssert` → custom ConstraintValidator.209- If HAS_JACKSON_V1: codehaus→fasterxml. If HAS_JODA_TIME: joda→java.time. If HAS_JSONB: JSON-B→Jackson annotations.210211### Phase 2: Business Logic & Presentation212213Exit: `mvn clean compile` passes.214215> ⚠ **MIGRATION COMMENT RULE** — Scope: applies to ALL comment text (Javadoc, inline, block, `{@code}`) in MIGRATION-labeled lines, in ALL file types (Java, test, Thymeleaf, HTML). Regular Javadoc describing annotations (e.g., `/** Uses @Transactional for... */`) is NOT subject to this rule — only MIGRATION-labeled comments.216>217> **Precedence**: This rule TAKES PRECEDENCE over the MINIMIZE CHANGES principle. Always reword MIGRATION comments to avoid annotation tokens, even when quoting the original annotation would be shorter.218>219> **Rule**: MIGRATION comments must NOT contain `@AnnotationName` tokens. Write plain English instead.220>221> **Per-task self-check**: Before marking ANY task complete, delete .bak files then verify: `find . -name "*.bak" -not -path "*/target/*" -delete && grep -rn "MIGRATION.*@[A-Z]" <files modified by this task>` — must return empty. Fix immediately if not.222>223> | ❌ Wrong | ✅ Correct |224> |---|---|225> | `// MIGRATION: Removed @ApplicationException` | `// MIGRATION: Replaced EJB app exception with Spring ResponseStatus` |226> | `// MIGRATION: @Autowired removed` | `// MIGRATION: Replaced field injection with constructor injection` |227> | `// MIGRATION: @Stateless converted` | `// MIGRATION: Converted stateless session bean to Spring service` |228> | `// MIGRATION: @Scheduled replaces timer` | `// MIGRATION: Converted EJB timer to Spring scheduled task` |229> | `// MIGRATION: @Ignore removed` | `// MIGRATION: Re-enabled test after Arquillian removal` |230> | `// MIGRATION: @InjectMocks added` | `// MIGRATION: Added Mockito injection for unit test` |231> | `// MIGRATION: @EnableBatchProcessing removed` | `// MIGRATION: Removed batch annotation that disables auto-config` |232> | `// MIGRATION: @Async added` | `// MIGRATION: Made event listener asynchronous` |233>234> See `references/verify-legacy-imports.md` for the full verification procedure.235236**Step 6**: Migrate EJBs and CDI to Spring beans.237- `@Stateless` → `@Service` + `@Transactional` **ONLY when the service performs persistence operations** (EntityManager, repository calls). Prerequisite: `spring-boot-starter-data-jpa` (includes spring-tx). For pure-computation services with no persistence, omit `@Transactional`. If a class has both `@Stateless` and `@Path`, produce a single `@RestController` (not `@Service` + `@RestController`) — see Step 9.238- `@Singleton` → `@Service`/`@Component` (`@Startup` → CommandLineRunner or `@EventListener(ApplicationReadyEvent.class)`). Remove `implements Serializable` and `serialVersionUID` from singleton beans. Do NOT add `@Scope("singleton")` — it is the Spring default.239- `@Stateful` → `@Service` + `@SessionScope`. **WARNING**: (a) `@SessionScope` requires the bean to be `Serializable` for HTTP session serialization. (b) If `@PostConstruct` accesses the scoped proxy before an HTTP session exists (e.g., during app startup), use `@Scope("prototype")` instead. (c) CDI `@Produces @SessionScoped` factory methods → `@Bean @SessionScope` in a `@Configuration` class. See `references/test-configuration-patterns.md` for test setup.240- Remove `@LocalBean`, `@Local`, `@Remote`. Replace `@EJB`/`@Inject` → constructor injection.241- **CDI no-arg constructor removal**: CDI requires public no-arg constructors for proxy generation. Spring single-constructor injection does not — remove no-arg constructors that exist only for CDI compliance. Keep them only if the class is serialized or has framework-mandated no-arg requirements.242- `@PersistenceContext EntityManager`: replace `@Inject EntityManager` with `@PersistenceContext private EntityManager em`. This is exempt from the constructor injection rule.243- CDI `@Produces Logger` → `private static final Logger logger = LoggerFactory.getLogger(ClassName.class)`. Delete the LoggerProducer class.244- CDI scopes: `@ApplicationScoped` → `@Component`, `@RequestScoped` → `@Component` + `@RequestScope`, `@Model` → `@Controller` + `@RequestScope`.245- CDI events: `Event<T>.fire()` → `ApplicationEventPublisher.publishEvent()`, `Event<T>.fireAsync()` → `ApplicationEventPublisher.publishEvent()` + `@Async` on the `@EventListener`. **CRITICAL structural difference**: `@Observes` is a PARAMETER annotation; `@EventListener` is a METHOD annotation — move annotation to method, parameter becomes plain type. **Cross-file**: `@EnableAsync` must be present on any `@Configuration` or `@SpringBootApplication` when `@Async @EventListener` exists — absence causes silent synchronous execution. **CompletionStage cleanup**: remove entire `CompletionStage` variable and `.exceptionally()` lambda from `fireAsync()` callers — `publishEvent()` returns void. **Payload type verification**: `grep publishEvent()` call sites to confirm actual payload type matches listener parameter type. **CDI qualifier-differentiated events**: define static nested wrapper classes per qualifier, use `publishEvent(new QualifierEvent(payload))`.246- CDI interceptors/decorators → `@Aspect` + `@Around`. **@Around advice methods MUST declare `throws Throwable`** — without it, checked exceptions from the intercepted method are wrapped in UndeclaredThrowableException.247- CDI `@Alternative` → `@ConditionalOnProperty`. **COMPLEMENTARY CONDITION RULE**: the default bean gets `matchIfMissing=true` and the alternative gets `matchIfMissing=false` on the same property. **DEPENDENCY CHAIN RULE**: apply the same condition to every bean in the activation chain that has side effects in its constructor.248- For custom CDI qualifier `@interface` files: import `org.springframework.beans.factory.annotation.Qualifier` (compile classpath) — NOT `jakarta.inject.Qualifier` (runtime only via hibernate-core, causes compile error).249- Scan all `package-info.java` for `@Vetoed` — remove the annotation and its CDI import.250- **EJB lifecycle annotations**: `@PostActivate` / `@PrePassivate` — DELETE the annotation AND the method body (no Spring equivalent for stateful passivation). `@PostConstruct` / `@PreDestroy` (JSR-250) — RETAIN as-is (Spring supports them natively).251- **⚠ STOP**: Before deleting JaxRsActivator.java / JAX-RS Application subclass, **record its `@ApplicationPath` value**. This prefix must be prepended to EVERY `@RequestMapping` in Step 9. Losing it causes 404 on ALL REST endpoints. Delete Resources.java (CDI @Produces EntityManager).252- **EJB exception hierarchy**: `EJBTransactionRolledbackException` → catch as `DataIntegrityViolationException`. Maintain most-specific-first catch ordering.253- **@Schedules (plural)**: split into separate `@Scheduled` methods, one per cron expression, each delegating to a shared private method. Drop `persistent=false` (no Spring equivalent).254- Replace `java.util.logging.Logger` → `org.slf4j.Logger`. See `references/common-pitfalls-extended.md` § JUL→SLF4J Quick Reference for the complete level mapping, placeholder conversion, and import replacement. Key rules: severe→error, warning→warn, fine→debug, config→info; `{0}`,`{1}` → `{}`,`{}`; unwrap `new Object[]{a,b}` to varargs; drop `.getName()` from `LoggerFactory.getLogger()`.255- **javax→jakarta completeness**: when migrating a file, ALL `javax.*` namespaces in that file must be updated in the same pass. Mixed-namespace files cause Step 17 failures.256- If HAS_EJB_TIMERS: `@Schedule` → `@Scheduled(cron=…)`, `@Timeout` → `@Scheduled(fixedDelay=…)`, add `@EnableScheduling`. `@EnableScheduling`/`@EnableAsync` can be on any `@Component` subtype including `@Service` — does not require the main class. **Note**: `fixedDelay`/`fixedRate` values must be compile-time constants — replace `TimeUnit.SECONDS.toMillis(3)` with `3000L` or use `fixedDelayString="${app.timer.delay}"`. For dynamic timer lifecycle (`timerService.createIntervalTimer()`/`timer.cancel()`), use an active-flag pattern: `volatile boolean active`; `@Scheduled` method checks `if (!active) return;` start/stop toggle the flag.257- If HAS_SERVLET_FILTERS: `@WebFilter` → `OncePerRequestFilter` + `@Component`. `@WebListener` → `@Component`.258- If HAS_JAVAMAIL: manual Session/Transport → `JavaMailSender`, configure `spring.mail.*`.259- If HAS_GUAVA: `Optional.fromNullable` → `Optional.ofNullable`.260- If HAS_JSONP: replace `javax.json.Json`, `JsonObject`, `JsonArray` with Jackson `ObjectMapper`, `ObjectNode`, `ArrayNode`.261262**Step 7**: Replace JNDI lookups — `@Resource(lookup=…)` → `@Value`/`@ConfigurationProperties`, `InitialContext.lookup()` → Spring DI.263264**Step 8**: Replace container-managed transactions — REQUIRED→`@Transactional`, REQUIRES_NEW→`Propagation.REQUIRES_NEW`, NOT_SUPPORTED→`Propagation.NOT_SUPPORTED`, BMT→`TransactionTemplate`. When using `TransactionTemplate`, declare it as a `@Bean` — it is NOT auto-configured by Spring Boot. `@RolesAllowed` → `@PreAuthorize` (if SECURITY_NEEDED, else remove).265266**Step 8b** (if multi-datasource): Define separate DataSource/EntityManagerFactory/TransactionManager beans per datasource, or use JTA with `me.snowdrop:narayana-spring-boot-starter` (NOT `spring-boot-starter-jta-narayana`, which does not exist for Boot 3.x). Narayana requires explicit version — it is NOT managed by the Spring Boot BOM. Check [Maven Central](https://central.sonatype.com/artifact/me.snowdrop/narayana-spring-boot-starter) for the latest 3.x-compatible release. Note: `spring-boot-starter-jta-atomikos` was also removed from Boot 3.x. Hibernate Envers @Audited works as-is after javax→jakarta import migration.267268**Step 9**: Migrate JAX-RS to Spring MVC. See `references/jaxrs-spring-mvc-migration.md` for detailed patterns.269- **@ApplicationPath folding** (CRITICAL): Locate the JAX-RS Application subclass (deleted in Step 6) and note its `@ApplicationPath` value. Prepend this to EVERY `@RequestMapping` path. Spring Boot has no `@ApplicationPath` equivalent. `@ApplicationPath("/rest")` + `@Path("/members")` → `@RequestMapping("/rest/members")`. Empty `@ApplicationPath("")` or `@ApplicationPath("/")` → no prefix needed.270- `@Path` → `@RestController` + `@RequestMapping`. `@GET/@POST/@PUT/@DELETE` → `@GetMapping/@PostMapping/@PutMapping/@DeleteMapping`.271- `@PathParam` → `@PathVariable`. `@QueryParam("name") Type param` → `@RequestParam(value="name", required=false) Type param` — JAX-RS query params are always optional; omitting `required=false` returns HTTP 400 for missing params.272- `@HeaderParam` → `@RequestHeader`. `@FormParam` → `@RequestParam` (with `@PostMapping`).273- Unannotated parameter in `@POST`/`@PUT` method with `@Consumes` → add `@RequestBody`. Spring MVC requires this explicitly; omitting it silently results in null.274- `Response` → `ResponseEntity`. JAX-RS ExceptionMapper → `@ControllerAdvice` + `@ExceptionHandler` (preserve HTTP status codes and error body format). **Note**: `@ResponseStatus` drops the body — use `ResponseEntity.badRequest().body(errors)` when error bodies are needed.275- `@Context UriInfo` → `ServletUriComponentsBuilder`. See `references/jaxrs-spring-mvc-migration.md` § UriInfo Translation Table for the full mapping. Replace field injection with constructor injection.276- JAX-RS `ContainerRequestFilter` → `HandlerInterceptor` or `OncePerRequestFilter`. `ContainerResponseFilter` → `HandlerInterceptor.postHandle()` or `OncePerRequestFilter`. **CRITICAL**: set ALL response headers BEFORE `chain.doFilter()` — headers set after `doFilter()` are silently dropped on a committed response.277278**Step 9b** (if JAX_WS_NEEDED): See `references/jaxws-websocket-migration.md` for JAX-WS SOAP migration.279280**Step 9c** (if WEBSOCKET_NEEDED): See `references/jaxws-websocket-migration.md` for WebSocket migration. **For JAX-RS SSE** (`SseEventSink`/`SseBroadcaster`): see `references/jaxrs-spring-mvc-migration.md` § JAX-RS SSE → SseEmitter.281282### Phase 3: Security & Messaging (CONDITIONAL)283284SKIP if SECURITY_NEEDED=false AND JMS_NEEDED=false. Exit: `mvn clean compile`.285286**Step 10** (if SECURITY_NEEDED): Create SecurityConfig with `@EnableWebSecurity` + `SecurityFilterChain` bean. Map web.xml constraints to `authorizeHttpRequests()`. **Actuator ordering**: `.requestMatchers("/actuator/**").permitAll()` must be the FIRST rule in `authorizeHttpRequests` chain — later rules can shadow it. `@RolesAllowed` → `@PreAuthorize` + `@EnableMethodSecurity`. Configure formLogin/httpBasic. `SessionContext.getCallerPrincipal()` → `SecurityContextHolder.getContext().getAuthentication()`.287288**Step 11** (if JMS_NEEDED): `@MessageDriven` → `@Component` + `@JmsListener`. `MessageProducer` → `JmsTemplate`. Embedded Artemis: `spring.artemis.mode=embedded`, add `artemis-jakarta-server` dependency. External broker: `spring.artemis.mode=native` + `broker-url`.289290**Step 11b** (if BATCH_NEEDED): Convert META-INF/batch-jobs/*.xml → @Configuration with Job/Step beans. See `references/spring-batch5-migration.md` for Spring Batch 5 specifics.291- `ItemWriter.write(List<? extends T>)` → `write(Chunk<? extends T>)`. Use `chunk.getItems()` for the list.292- Do NOT add `@EnableBatchProcessing` — it disables BatchAutoConfiguration on Spring Boot 3.293- `@BatchProperty` → `@Value("#{jobParameters['paramName']}")`. Annotate ANY component using `@Value("#{jobParameters[…]}")` with `@StepScope` — not just ItemReader.294- Scheduled jobs: include `run.id=System.currentTimeMillis()` in JobParameters to avoid JobInstanceAlreadyCompleteException.295296### Phase 4: Testing & UI297298Exit: `mvn clean test` passes.299300**Step 12**: Migrate tests. See `references/test-configuration-patterns.md` for core patterns and `references/test-mockito-advanced.md` for Mockito-specific patterns.301- Ensure `src/test/resources/` directory exists: `mkdir -p src/test/resources/`.302- Add @SpringBootTest context-loads test + @WebMvcTest/MockMvc controller tests.303- Create `src/test/resources/application.yml` with H2 override: `jdbc:h2:mem:testdb`, `ddl-auto=create-drop`. This file REPLACES (not merges with) main application.yml — copy ALL non-datasource config verbatim, including: `server.servlet.context-path`, `spring.jpa.hibernate.naming.physical-strategy`, `spring.jpa.defer-datasource-initialization`, `management.*` actuator settings. Do NOT set explicit `spring.jpa.properties.hibernate.dialect` — Hibernate 6 auto-detects from JDBC URL; explicit dialect triggers HHH90000025 deprecation. Only create this H2 override if JPA_NEEDED=true.304- If @SpringBootApplication is in a different package subtree from tests: use `@SpringBootTest(classes = MainApp.class)`.305- If ARQUILLIAN_TESTS: remove @RunWith(Arquillian), @Deployment, ShrinkWrap → @SpringBootTest + @AutoConfigureMockMvc. Preserve all test methods/assertions. Migrate data setup to @TestConfiguration, @Sql, or @BeforeEach. Delete arquillian.xml, test-ds.xml.306- If SECURITY_NEEDED: add spring-security-test; use `@WithMockUser` in controller tests. `@MockBean` the `HandlerInterceptor` if it requires authentication context — see `references/test-mockito-advanced.md`.307- JUnit 4→5: `@RunWith` → `@ExtendWith`, `Assert` → `Assertions`. **CRITICAL — count args before reordering**: 2-arg `assertEquals("Widget", actual)` — the String IS the expected value, do NOT reorder. Only 3-arg `assertEquals("msg", expected, actual)` needs the message moved to last. **Message-last applies to ALL assertion methods**: `assertTrue("msg", cond)` → `assertTrue(cond, "msg")`; `assertFalse("msg", cond)` → `assertFalse(cond, "msg")`; `assertNotNull("msg", obj)` → `assertNotNull(obj, "msg")`; `assertNull("msg", obj)` → `assertNull(obj, "msg")`. See `references/test-configuration-patterns.md` for full rule.308- `@Test(expected=X.class)` → `assertThrows(X.class, () -> { ... })`. Place ONLY the throwing call inside the lambda — never Mockito stubs or setup.309- Do NOT mechanically convert `@Ignore` to `@Disabled`. Investigate WHY the test was ignored — Arquillian-era reasons often don't apply in Spring Boot + H2.310- Do NOT @Disable tests. Ordered tests sharing committed DB state: omit class-level @Transactional — each method must commit so subsequent methods observe prior data.311- **javax→jakarta in test files**: test files need the same `javax.*` → `jakarta.*` migration as production code.312- **@WebMvcTest rules**: (a) never combine with `@AutoConfigureMockMvc` (redundant); (b) mutually exclusive with `@ExtendWith(MockitoExtension.class)` — use `@MockBean` instead; (c) omit `classes=` when test is in `@SpringBootApplication` package subtree (auto-discovered); (d) custom `@EnableWebSecurity` configs are NOT auto-loaded — add `@Import(SecurityConfig.class)` to avoid CSRF 403 errors; (e) auto-includes any `@Component` implementing `Converter`/`Formatter`/`GenericConverter` — add `@MockBean` for non-trivial dependency chains; (f) `@PersistenceContext` on controller → add `@MockBean EntityManagerFactory` (PersistenceAnnotationBeanPostProcessor is active in MVC slice); (g) `thenReturn()` stubs must match exact service return type — Mockito generic inference fails on type mismatch.313- **@InjectMocks does NOT populate `@PersistenceContext` or `@Value` fields** — use `ReflectionTestUtils.setField()` in `@BeforeEach`. See `references/test-mockito-advanced.md`.314- **UnnecessaryStubbingException under MockitoExtension**: wrap subset-only `@BeforeEach` stubs with `lenient().when()`. Do NOT apply class-level LENIENT strictness. See `references/test-mockito-advanced.md`.315- **XML produces endpoints**: chain `.accept(MediaType.APPLICATION_XML)` in MockMvc for endpoints with `produces=APPLICATION_XML_VALUE`.316- **`*IT.java` integration tests**: run via `maven-failsafe-plugin`, not surefire. Add failsafe include if needed — see `references/test-configuration-patterns.md`.317- **`@Lazy @Value("${local.server.port}")`**: use `@Lazy` or inject via `@LocalServerPort` in `@BeforeEach`. See `references/test-configuration-patterns.md`.318- If BATCH_NEEDED: align test call sites with Spring Batch 5 API (Chunk, SkipListener rename, checkpoint/restart test removal).319- **Smoke test package**: place in the root test package matching `@SpringBootApplication` package.320- **ClassPathXmlApplicationContext in tests**: if backing XML was deleted in Step 4, migrate to `@SpringBootTest` + `@TestConfiguration`.321322**Step 13**: Migrate UI and static resources.323- If JSF_NEEDED: convert .xhtml → Thymeleaf templates (src/main/resources/templates/). See `references/jsf-backing-bean-migration.md` for backing bean → @Controller patterns.324 - `spring-boot-starter-thymeleaf` does NOT include Layout Dialect — do NOT use `layout:decorate`. Use built-in `th:fragment` parameterized fragments.325 - Thymeleaf URL preprocessing for dynamic base URLs: `@{__${url}__(page=${n})}`.326 - Pre-encoded URL values: bypass `@{}` to avoid double-encoding — use `${#request.contextPath + '/path?p=' + val}`.327- If the application uses BRMS/Drools/KIE: preserve the KIE/Drools runtime and wire via a `@Configuration` class producing `KieContainer @Bean` instead of CDI `@Produces`. See `references/phase0-detection-flags.md` for HAS_DROOLS flag.328- If STATIC_UI_EXISTS: move resources from webapp/ → resources/static/. Create WebMvcConfigurer for directory index.329- Delete src/main/webapp/ after migration. Delete faces-config.xml.330331### Phase 5: Deployment & Verification332333Exit: `mvn clean verify` passes + all 20 exit criteria met.334335**Step 14**: Configure Actuator — expose health/info/metrics, enable liveness/readiness probes. Custom health checks → HealthIndicator. **Mandatory YAML for standalone mode** (probes NOT auto-enabled):336337```yaml338management:339 endpoints:340 web:341 exposure:342 include: health,info,metrics343 endpoint:344 health:345 probes:346 enabled: true347 info:348 env:349 enabled: true350```351352**Step 15**: Containerize with multi-stage Dockerfile. Use this template (choose Java 17 or 21 based on pom.xml `java.version`):353354```dockerfile355# -- Build stage --356FROM maven:3.9-amazoncorretto-17 AS build357# For Java 21: maven:3.9-amazoncorretto-21358WORKDIR /app359COPY pom.xml .360RUN mvn dependency:go-offline -B361COPY src ./src362RUN mvn clean package -DskipTests -B363364# -- Runtime stage --365FROM amazoncorretto:17-alpine366# For Java 21: amazoncorretto:21-alpine367# Fallback if amazoncorretto unavailable: eclipse-temurin:17-jre-alpine368RUN addgroup -S appgroup && adduser -S appuser -G appgroup369WORKDIR /app370COPY --from=build /app/target/*.jar app.jar371RUN chown appuser:appgroup app.jar372USER appuser373EXPOSE 8080374ENTRYPOINT ["java", \375 "-XX:+UseContainerSupport", \376 "-XX:MaxRAMPercentage=75.0", \377 "-jar", "app.jar"]378```379380**Variants**: (a) **Multi-module**: `COPY <module>/target/<finalName>.jar app.jar`; COPY all referenced pom.xml files before `dependency:go-offline`. (b) **Local/proprietary artifacts** (KJar, custom libs): `COPY libs/ .` + `RUN mvn install:install-file …` before `dependency:go-offline`. (c) **Non-Alpine images** (amazoncorretto non-alpine, eclipse-temurin non-alpine): use `groupadd -r` / `useradd -r -g`, NOT `addgroup -S` / `adduser -S` — Alpine BusyBox syntax crashes Debian-based images.381382Spring Boot 3.x JarLauncher path is `org.springframework.boot.loader.launch.JarLauncher` (NOT `.loader.JarLauncher`). Create `.dockerignore`.383384**Step 16**: Cleanup — update README.md, .gitignore. Delete remaining JBoss configs and legacy directories. Clean up editor backup files — run TWICE (second pass catches backups created during first pass):385```bash386find . -name "*.bak" -not -path "*/target/*" -delete387find . -name "*.bak" -not -path "*/target/*" -delete388```389Add `*.bak` to .gitignore. **Post-delete verification**: `find . -name "*.bak" -not -path "*/target/*"` — must return empty.390391**Step 17**: Final verification scan. See `references/verify-legacy-imports.md` for full procedure with copy-paste-ready grep commands. Run each grep INDEPENDENTLY — never chain with `&&` (grep returns exit code 1 on zero matches, silently skipping later checks).392- **PRE-GREP PROTOCOL**: ALWAYS run `find . -name "*.bak" -not -path "*/target/*" -delete` before ANY grep verification scan. .bak files contain stale pre-migration content that causes false positives.393- Legacy imports: grep for javax.ws.rs, javax.ejb, javax.persistence, etc. — must be ZERO. Filter comments and Javadoc.394- Inline FQCNs: `grep -r "javax\." src/main/java/ | grep -v "import " | ...` — filter comments and Javadoc lines.395- Legacy annotations: grep for @Stateless, @Stateful, @MessageDriven, @EJB, @Path (JAX-RS), @TransactionAttribute — must be ZERO.396- Constructor injection: grep for `@Autowired` on field declarations in src/main/java/ — must be ZERO. `@PersistenceContext` on EntityManager fields is acceptable.397- Dependency tree: `mvn dependency:tree | grep 'jboss\|wildfly\|jersey\|javax.ws.rs'` — only jboss-logging acceptable.398- Test integrity: verify no @Disabled added during migration.399- **MIGRATION comment rule**: `grep -rn "MIGRATION.*@[A-Z]" src/` — must return empty.400- **Unconditional .bak sweep** — run as the LAST action before `mvn clean verify`, regardless of whether a Debugger phase ran. Debugger tools and multi-worker tasks create .bak files at any point:401 ```bash402 find . -name "*.bak" -not -path "*/target/*" -delete403 find . -name "*.bak" -not -path "*/target/*" # must return empty404 ```405- Run `mvn clean verify`.406407## Reference Dispatch408409Load reference files on demand when the worker encounters these signals:410411| Signal in Source Code | Reference File |412|---|---|413| Phase 0 library flag detection, pom.xml dep scanning | `references/phase0-detection-flags.md` |414| `@Path`, `@ApplicationPath`, `@QueryParam`, JAX-RS `Response`, SSE `SseEventSink` | `references/jaxrs-spring-mvc-migration.md` |415| `@WebService`, `@ServerEndpoint`, `@OnMessage` | `references/jaxws-websocket-migration.md` |416| `jakarta.batch`, `META-INF/batch-jobs/`, `ItemWriter` | `references/spring-batch5-migration.md` |417| `@Embeddable`, `@Lob` on array fields, Hibernate 6 nulls, CriteriaQuery | `references/hibernate6-behavior-changes.md` |418| Arquillian, test context, H2, JUnit 4→5, `*IT.java` | `references/test-configuration-patterns.md` |419| `@InjectMocks`, Mockito, `STRICT_STUBS`, `@MockBean` | `references/test-mockito-advanced.md` |420| `.xhtml`, `@ViewScoped`, `@FlowScoped`, JSF backing beans | `references/jsf-backing-bean-migration.md` |421| Build errors, runtime exceptions, configuration problems, JUL→SLF4J | `references/common-pitfalls-extended.md` |422| MDB→@JmsListener, Security worked examples | `references/worked-examples-conditional.md` |423424## Reference Procedures425426The procedures below describe mechanical transforms and verification sweeps the worker performs on demand using standard shell commands. There are no executable scripts — the worker reads each procedure and issues the commands directly.427428- **Pre-migration procedures** run BEFORE any file-by-file migration work.429- **Post-batch procedures** run AFTER each batch of related changes.430- **Post-migration procedures** run AFTER all code changes are complete.431432- **`references/jaxrs-spring-mvc-migration.md`** — Detailed JAX-RS → Spring MVC mapping rules including @ApplicationPath folding, @QueryParam required=false, @FormParam→@RequestParam, implicit @RequestBody, UriBuilder, Response patterns, MultivaluedMap hierarchy, ExceptionMapper→@ControllerAdvice, SSE→SseEmitter, UriInfo translatio433434…(truncated)