MFormations
Modern Java Engineering

Chapitre 14

14 - Jakarta EE

14 - Jakarta EE

Cours 14 : Jakarta EE

1. Servlets

1.1 Servlet Lifecycle

Loading → Initialization (init) → Service (doGet/doPost) → Destruction (destroy)

1.2 Servlet simple

@WebServlet("/api/orders/*")
public class OrderServlet extends HttpServlet {
    
    @Inject
    private OrderService orderService;
    
    @Override
    public void init() throws ServletException {
        // Initialisation du servlet
    }
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        resp.setContentType("application/json");
        String orderId = req.getPathInfo().substring(1);
        Order order = orderService.findById(orderId);
        resp.getWriter().write(convertToJson(order));
    }
    
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        Order order = parseBody(req, Order.class);
        Order created = orderService.create(order);
        resp.setStatus(HttpServletResponse.SC_CREATED);
        resp.getWriter().write(convertToJson(created));
    }
}

1.3 Filters

@WebFilter("/api/*")
public class LoggingFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
            FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpReq = (HttpServletRequest) request;
        long start = System.currentTimeMillis();
        
        chain.doFilter(request, response);
        
        long duration = System.currentTimeMillis() - start;
        log.info("{} {} - {}ms", httpReq.getMethod(), 
            httpReq.getRequestURI(), duration);
    }
}

@WebFilter("/api/admin/*")
public class SecurityFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
            FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpReq = (HttpServletRequest) request;
        String token = httpReq.getHeader("Authorization");
        
        if (token == null || !validateToken(token)) {
            ((HttpServletResponse) response).setStatus(401);
            return;
        }
        chain.doFilter(request, response);
    }
}

1.4 Listeners

@WebListener
public class AppContextListener implements ServletContextListener {
    
    @Override
    public void contextInitialized(ServletContextEvent event) {
        // Initialisation au démarrage de l'application
        log.info("Application démarrée");
    }
    
    @Override
    public void contextDestroyed(ServletContextEvent event) {
        // Nettoyage à l'arrêt
        log.info("Application arrêtée");
    }
}

@WebListener
public class SessionListener implements HttpSessionListener {
    
    @Override
    public void sessionCreated(HttpSessionEvent event) {
        log.info("Session créée: {}", event.getSession().getId());
    }
    
    @Override
    public void sessionDestroyed(HttpSessionEvent event) {
        log.info("Session détruite: {}", event.getSession().getId());
    }
}

2. JAX-RS (REST)

2.1 Endpoint REST

@Path("/api/orders")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class OrderResource {
    
    @Inject
    private OrderService orderService;
    
    @GET
    public Response getOrders(@QueryParam("page") @DefaultValue("1") int page,
                               @QueryParam("size") @DefaultValue("20") int size) {
        Page<Order> orders = orderService.findAll(page, size);
        return Response.ok(orders).build();
    }
    
    @GET
    @Path("/{id}")
    public Response getOrder(@PathParam("id") String id) {
        return orderService.findById(id)
            .map(order -> Response.ok(order).build())
            .orElse(Response.status(404).build());
    }
    
    @POST
    public Response createOrder(Order order, @Context UriInfo uriInfo) {
        Order created = orderService.create(order);
        URI uri = uriInfo.getAbsolutePathBuilder()
            .path(created.getId().toString()).build();
        return Response.created(uri).entity(created).build();
    }
    
    @PUT
    @Path("/{id}")
    public Response updateOrder(@PathParam("id") String id, Order order) {
        orderService.update(id, order);
        return Response.ok().build();
    }
    
    @DELETE
    @Path("/{id}")
    public Response deleteOrder(@PathParam("id") String id) {
        orderService.delete(id);
        return Response.noContent().build();
    }
}

2.2 Exception Mapper

@Provider
public class OrderNotFoundExceptionMapper 
        implements ExceptionMapper<OrderNotFoundException> {
    
    @Override
    public Response toResponse(OrderNotFoundException exception) {
        return Response.status(404)
            .entity(new ErrorResponse("ORDER_NOT_FOUND", exception.getMessage()))
            .type(MediaType.APPLICATION_JSON)
            .build();
    }
}

@Provider
public class ValidationExceptionMapper 
        implements ExceptionMapper<ValidationException> {
    
    @Override
    public Response toResponse(ValidationException exception) {
        return Response.status(422)
            .entity(new ErrorResponse("VALIDATION_ERROR", 
                exception.getMessage()))
            .type(MediaType.APPLICATION_JSON)
            .build();
    }
}

3. CDI (Contexts and Dependency Injection)

3.1 Beans

@ApplicationScoped
public class OrderService {
    
    @Inject
    private OrderRepository orderRepository;
    
    @Inject
    private Event<OrderCreatedEvent> orderCreatedEvent;
    
    public Order createOrder(Order order) {
        orderRepository.save(order);
        orderCreatedEvent.fire(new OrderCreatedEvent(order.getId()));
        return order;
    }
}

@RequestScoped
public class OrderContext {
    private String currentOrderId;
    // ...
}

3.2 Producers

@ApplicationScoped
public class Resources {
    
    @Produces
    @PersistenceContext(unitName = "orderPU")
    private EntityManager em;
    
    @Produces
    @OrderDatabase
    public DataSource createDataSource() {
        // Configuration programmatique
    }
    
    @Produces
    @ApplicationScoped
    public ObjectMapper objectMapper() {
        return new ObjectMapper()
            .registerModule(new JavaTimeModule())
            .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    }
}

3.3 Interceptors

@Interceptor
@Logged
public class LoggingInterceptor {
    
    @AroundInvoke
    public Object logMethodEntry(InvocationContext context) throws Exception {
        String method = context.getMethod().getName();
        String params = Arrays.toString(context.getParameters());
        log.info("Méthode {} appelée avec paramètres: {}", method, params);
        
        long start = System.currentTimeMillis();
        try {
            return context.proceed();
        } finally {
            log.info("Méthode {} terminée en {}ms", method, 
                System.currentTimeMillis() - start);
        }
    }
}

@Interceptor
@Transactional
public class TransactionInterceptor {
    
    @Inject
    private UserTransaction transaction;
    
    @AroundInvoke
    public Object manageTransaction(InvocationContext context) throws Exception {
        transaction.begin();
        try {
            Object result = context.proceed();
            transaction.commit();
            return result;
        } catch (Exception e) {
            transaction.rollback();
            throw e;
        }
    }
}

4. JPA (Java Persistence API)

4.1 Entities

@Entity
@Table(name = "orders")
@NamedQueries({
    @NamedQuery(name = "Order.findByCustomer", 
        query = "SELECT o FROM Order o WHERE o.customerId = :customerId"),
    @NamedQuery(name = "Order.findByStatus", 
        query = "SELECT o FROM Order o WHERE o.status = :status")
})
public class OrderEntity {
    
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;
    
    @Column(name = "customer_id", nullable = false)
    private String customerId;
    
    @Enumerated(EnumType.STRING)
    private OrderStatus status;
    
    @Column(name = "total_amount", precision = 10, scale = 2)
    private BigDecimal totalAmount;
    
    @Column(name = "currency", length = 3)
    private String currency;
    
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, 
               orphanRemoval = true, fetch = FetchType.LAZY)
    private List<OrderLineEntity> lines = new ArrayList<>();
    
    @Version
    private int version; // Optimistic locking
    
    @Column(name = "created_at")
    private LocalDateTime createdAt;
    
    @PrePersist
    public void prePersist() {
        createdAt = LocalDateTime.now();
    }
}

4.2 Relationships

@Entity
@Table(name = "order_lines")
public class OrderLineEntity {
    
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id", nullable = false)
    private OrderEntity order;
    
    @Column(name = "product_id", nullable = false)
    private String productId;
    
    @Column(name = "quantity", nullable = false)
    private int quantity;
    
    @Column(name = "unit_price", precision = 10, scale = 2)
    private BigDecimal unitPrice;
    
    @Column(name = "line_total", precision = 10, scale = 2)
    private BigDecimal lineTotal;
}

@Entity
@Table(name = "products")
public class ProductEntity {
    
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;
    
    @Column(unique = true, nullable = false)
    private String sku;
    
    @Column(nullable = false)
    private String name;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_id")
    private CategoryEntity category;
    
    @ElementCollection
    @CollectionTable(name = "product_tags", 
        joinColumns = @JoinColumn(name = "product_id"))
    @Column(name = "tag")
    private Set<String> tags = new HashSet<>();
}

4.3 Repository with JPA

@Stateless
public class OrderRepository {
    
    @PersistenceContext(unitName = "orderPU")
    private EntityManager em;
    
    public OrderEntity findById(UUID id) {
        return em.find(OrderEntity.class, id);
    }
    
    public List<OrderEntity> findByCustomer(String customerId) {
        return em.createNamedQuery("Order.findByCustomer", OrderEntity.class)
            .setParameter("customerId", customerId)
            .getResultList();
    }
    
    public OrderEntity save(OrderEntity order) {
        if (order.getId() == null) {
            em.persist(order);
            return order;
        } else {
            return em.merge(order);
        }
    }
    
    public void delete(UUID id) {
        OrderEntity order = em.getReference(OrderEntity.class, id);
        em.remove(order);
    }
}

5. EJB (Enterprise JavaBeans)

5.1 Stateless Session Bean

@Stateless
public class OrderService {
    
    @PersistenceContext
    private EntityManager em;
    
    @Resource
    private SessionContext context;
    
    public Order createOrder(Order order) {
        em.persist(order);
        return order;
    }
    
    public List<Order> findOrdersByCustomer(String customerId) {
        return em.createQuery("SELECT o FROM Order o WHERE o.customerId = :cid", Order.class)
            .setParameter("cid", customerId)
            .getResultList();
    }
}

5.2 Singleton Session Bean

@Singleton
@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER)
public class OrderCounter {
    
    private AtomicLong counter = new AtomicLong();
    
    @Lock(LockType.WRITE)
    public long increment() {
        return counter.incrementAndGet();
    }
    
    @Lock(LockType.READ)
    public long getCount() {
        return counter.get();
    }
    
    @PostConstruct
    public void init() {
        counter.set(0);
    }
}

5.3 Message-Driven Bean

@MessageDriven(
    activationConfig = {
        @ActivationConfigProperty(
            propertyName = "destinationType",
            propertyValue = "jakarta.jms.Queue"
        ),
        @ActivationConfigProperty(
            propertyName = "destinationLookup",
            propertyValue = "jms/orderQueue"
        )
    }
)
public class OrderProcessingMDB implements MessageListener {
    
    @Inject
    private OrderService orderService;
    
    @Override
    public void onMessage(Message message) {
        try {
            if (message instanceof ObjectMessage) {
                Order order = (Order) ((ObjectMessage) message).getObject();
                orderService.processOrder(order);
            }
        } catch (JMSException e) {
            log.error("Erreur traitement message", e);
        }
    }
}

6. Bean Validation

6.1 Validation constraints

public class OrderRequest {
    
    @NotNull(message = "Customer ID is required")
    private String customerId;
    
    @NotEmpty(message = "Order must have at least one item")
    @Valid
    private List<OrderItemRequest> items;
}

public class OrderItemRequest {
    
    @NotNull
    private String productId;
    
    @Min(1)
    @Max(999)
    private int quantity;
    
    @DecimalMin("0.01")
    @DecimalMax("999999.99")
    @NotNull
    private BigDecimal unitPrice;
}

// Custom validator
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ValidOrderValidator.class)
public @interface ValidOrder {
    String message() default "Invalid order";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

public class ValidOrderValidator 
        implements ConstraintValidator<ValidOrder, OrderRequest> {
    
    @Override
    public boolean isValid(OrderRequest order, ConstraintValidatorContext context) {
        if (order.getItems() == null || order.getItems().isEmpty()) {
            return false;
        }
        return order.getItems().stream()
            .allMatch(item -> item.getQuantity() > 0);
    }
}

7. Batch (JSR 352)

@Named
public class OrderBatchlet extends AbstractBatchlet {
    
    @Inject
    private OrderService orderService;
    
    @Override
    public String process() throws Exception {
        List<Order> pendingOrders = orderService.findPendingOrders();
        for (Order order : pendingOrders) {
            orderService.processOrder(order);
        }
        return "COMPLETED";
    }
}

// job.xml
<job id="orderProcessingJob" xmlns="https://jakarta.ee/xml/ns/jakartaee">
    <step id="processOrders">
        <batchlet ref="orderBatchlet"/>
    </step>
</job>

8. JMS (Java Message Service)

@Stateless
public class OrderMessageProducer {
    
    @Resource(lookup = "jms/orderQueue")
    private Queue orderQueue;
    
    @Inject
    private JMSContext jmsContext;
    
    public void sendOrder(Order order) {
        ObjectMessage message = jmsContext.createObjectMessage(order);
        jmsContext.createProducer()
            .setDeliveryMode(DeliveryMode.PERSISTENT)
            .setPriority(5)
            .setTimeToLive(86400000) // 24 hours
            .send(orderQueue, message);
    }
}

Points clés

  • Servlets : Lifecycle init → service → destroy
  • JAX-RS : @Path, @GET, @POST, exception mappers
  • CDI : @Inject, @Produces, @Interceptor
  • JPA : Entities, relationships (OneToMany, ManyToOne)
  • EJB : @Stateless, @Singleton, MDB
  • Bean Validation : @NotNull, @Min, custom validators
  • Batch : JSR 352 pour le traitement par lots
  • JMS : Producteurs et consommateurs asynchrones