● Code Review

Java Coding Standards — NexaFlow Workflow Service

Revisión y refactorización aplicando estándares idiomáticos Java 17+ ◆ Quarkus 3.x ◆ Java 17+ Panache ORM RESTEasy Reactive
7
Antipatrones detectados
3
Críticos (runtime risk)
12
Mejoras aplicadas
5
Reglas Quarkus-específicas
◆ Framework detectado: QUARKUSpom.xml contiene io.quarkus:quarkus-maven-plugin. Aplicando convenciones [QUARKUS]: JAX-RS resources, CDI scopes, Panache active-record, RESTEasy Reactive, JBoss Logging.
Antipatrones detectados
Crítico
@Singleton en lugar de @ApplicationScoped [QUARKUS]
WorkflowController usa @Singleton, que no es proxyable — rompe la interceptación CDI (transacciones, seguridad, observabilidad). Usar @ApplicationScoped siempre que se necesite interception.
Crítico
Llamada .get() sin check sobre Optional
workflowService.find(s).get() lanza NoSuchElementException en silencio si no hay resultado. Reemplazar por .orElseThrow() con excepción de dominio.
Crítico
Catch genérico Exception con e.printStackTrace() silencioso
Oculta errores reales (conexión BD, timeout), devuelve lista vacía sin contexto. Usar logging estructurado y relanzar como excepción de dominio o dejar propagar.
Aviso
Inyección con campo private @Inject — problemático en Quarkus [QUARKUS]
Los campos private con @Inject pueden causar problemas con proxies CDI. Usar campo package-private o inyección por constructor.
Aviso
Raw types (List sin parámetro genérico)
public List getAll() y public List findAll() usan raw types. Declarar siempre el tipo genérico: List<WorkflowDto>.
Aviso
Lista larga de parámetros en create()
create(String n, String d, String oid, int p) — 4 parámetros primitivos. Usar un DTO/record de comando: CreateWorkflowRequest(name, description, ownerId, priority).
Mejora
Nombre de clase WorkflowControllerWorkflowResource [QUARKUS]
En Quarkus los endpoints JAX-RS se nombran con sufijo *Resource, no *Controller (ese es el patrón Spring).
Código refactorizado
resource/WorkflowResource.java
Renombrado + refactorizado
╳ Antes — WorkflowController.java
// WorkflowController.java @Singleton // ← BUG: no proxyable public class WorkflowController { @Inject private WorkflowService workflowService; // ← private: proxy issues public List getAll(int page) { // raw type ^ return workflowService.findAll(page); } public Object getBySlug(String s) { WorkflowEntity w = workflowService.find(s).get(); // NPE! return w; } public void create( String n, String d, String oid, int p) { workflowService.create(n, d, oid, p); } }
✓ Después — WorkflowResource.java
// [QUARKUS] JAX-RS resource @ApplicationScoped @Path("/workflows") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class WorkflowResource { @Inject // package-private: OK Quarkus WorkflowService workflowService; @GET public List<WorkflowDto> getAll( @QueryParam("page") @DefaultValue("0") int page) { return workflowService.findAll(page); } @GET @Path("/{slug}") public WorkflowDto getBySlug( @PathParam("slug") String slug) { return workflowService.findBySlug(slug); // orElseThrow en service ↑ } @POST @ResponseStatus(201) public WorkflowDto create( @Valid CreateWorkflowRequest req) { return workflowService.create(req); } }
service/WorkflowService.java
Logging + Optional correcto + tipos
╳ Antes
// sin scope CDI explícito public class WorkflowService { public List findAll(int page) { try { return WorkflowEntity .findAll() .page(Page.of(page, 50)) .list(); } catch (Exception e) { e.printStackTrace(); // ← mal return new ArrayList(); } } public Optional find(String slug){ return WorkflowEntity .find("slug", slug) .firstResultOptional(); } }
✓ Después
@ApplicationScoped public class WorkflowService { private static final Logger log = Logger.getLogger(WorkflowService.class); public List<WorkflowDto> findAll(int page) { return WorkflowEntity .findAll() .page(Page.of(page, PAGE_SIZE)) .list() .stream() .map(WorkflowDto::from) .toList(); // Java 16+ } public WorkflowDto findBySlug(String slug){ return WorkflowEntity .<WorkflowEntity>find("slug", slug) .firstResultOptional() .map(WorkflowDto::from) .orElseThrow(() -> { log.warnf("workflow_not_found slug=%s", slug); return new WorkflowNotFoundException(slug); }); } }
dto/ y domain/ — Nuevos tipos Java 17+
Records + Excepción de dominio
╳ No existía (parámetros sueltos)
// create() recibía 4 parámetros sueltos: String n, String d, String oid, int p // Sin DTO de respuesta tipado public Object getBySlug(...) // Sin excepción de dominio // Lanzaba NoSuchElementException genérica
✓ Tipos nuevos (Java 17 records)
// dto/CreateWorkflowRequest.java public record CreateWorkflowRequest( @NotBlank String name, String description, @NotBlank String ownerId, @Min(1) @Max(5) int priority ) {} // dto/WorkflowDto.java public record WorkflowDto( Long id, String slug, String name, WorkflowStatus status, LocalDateTime createdAt ) { static WorkflowDto from(WorkflowEntity e) { return new WorkflowDto( e.id, e.slug, e.name, e.status, e.createdAt); } } // domain/WorkflowNotFoundException.java public class WorkflowNotFoundException extends RuntimeException { public WorkflowNotFoundException(String slug){ super("Workflow not found: " + slug); } }
Reglas aplicadas
Regla Categoría Antes Después
@ApplicationScoped para beans CDI normales [QUARKUS] @Singleton @ApplicationScoped
Inyección package-private con @Inject [QUARKUS] private @Inject package-private
Sufijo *Resource en JAX-RS [QUARKUS] WorkflowController WorkflowResource
Optional.orElseThrow() con excepción de dominio Shared .get() directo .orElseThrow(() -> new WorkflowNotFoundException(...))
Tipos genéricos explícitos (no raw types) Shared List, Optional List<WorkflowDto>
Records Java 17 para DTOs inmutables Java 17+ 4 parámetros sueltos record CreateWorkflowRequest(...)
JBoss Logging con Logger.getLogger() [QUARKUS] e.printStackTrace() log.warnf("workflow_not_found slug=%s", slug)
Excepciones de dominio específicas Shared catch genérico + lista vacía WorkflowNotFoundException + handler @ServerExceptionMapper
Stream .toList() (Java 16+) Java 17+ new ArrayList() .stream().map(Dto::from).toList()
Bean Validation en request body [QUARKUS] Sin validación @Valid + @NotBlank, @Min, @Max
Constante nombrada para page size Shared Magic number 50 private static final int PAGE_SIZE = 50
Nombres de parámetros descriptivos Shared s, n, d, oid, p slug, req, page
Estructura de proyecto corregida [QUARKUS]
src/main/java/io/nexaflow/workflow/ config/ WorkflowConfig.java @ConfigMapping(prefix="workflow") resource/ ← JAX-RS (no "controller") WorkflowResource.java GlobalExceptionMapper.java @ServerExceptionMapper service/ WorkflowService.java @ApplicationScoped domain/ WorkflowEntity.java extends PanacheEntity WorkflowNotFoundException.java WorkflowStatus.java enum dto/ WorkflowDto.java record CreateWorkflowRequest.java record + Bean Validation util/ src/main/resources/ application.properties Quarkus convention (no .yml) import.sql src/test/java/io/nexaflow/workflow/ resource/ WorkflowResourceTest.java @QuarkusTest + @InjectMock service/ WorkflowServiceTest.java @ExtendWith(MockitoExtension) — no CDI
Tests — patrón Quarkus correcto
test/ — WorkflowResourceTest.java y WorkflowServiceTest.java
Integración CDI — @QuarkusTest
// WorkflowResourceTest.java @QuarkusTest class WorkflowResourceTest { @InjectMock WorkflowService workflowService; @Test void should_return_404_when_not_found() { when(workflowService .findBySlug("unknown")) .thenThrow(new WorkflowNotFoundException("unknown")); given().when() .get("/workflows/unknown") .then().statusCode(404); } }
Unitario puro — sin @QuarkusTest
// WorkflowServiceTest.java @ExtendWith(MockitoExtension.class) class WorkflowServiceTest { @Mock WorkflowRepository repo; @InjectMocks WorkflowService service; @Test void findBySlug_throws_when_missing() { when(repo.findBySlug("x")) .thenReturn(Optional.empty()); assertThatThrownBy(() -> service.findBySlug("x") ).isInstanceOf( WorkflowNotFoundException.class); } }