Jakarta EE 10: Das erste große Release seit 5 Jahren zielt auf Cloud-Anwendungen

Seite 2: Nebenläufigkeit für alle

Inhaltsverzeichnis

Die Möglichkeit, einzelne Funktionen gezielt asynchron und somit non-blocking ausführen zu können, ist ein wichtiger Baustein zeitgemäßer Softwarearchitekturen. In Jakarta EE-Anwendungen ließ sich das bisher deklarativ nur innerhalb von EJBs (Enterprise JavaBeans) realisieren. Außerhalb davon musste man dagegen auf den ManagedExecutorService zurückgreifen und den asynchronen Task manuell anstoßen.

Die aktuelle Version 3.0 der Concurrency API ermöglicht nun auch CDI-Beans, einzelne Methoden deklarativ als asynchron zu markieren und damit gezielt nebenläufig ablaufen zu lassen. Dazu dient die neue Annotation @Asynchronous aus dem Package jakarta.enterprise.concurrent. Achtung: Das Package jakarta.enterprise.ejb enthält eine Annotation mit demselben Namen. Als optionaler Parameter dient der zu nutzende Thread-Pool.

Anders als die beiden bisherigen Ansätze, die lediglich Methoden unterstützen, die das 2004 mit Java 5 eingeführte und somit in die Jahre gekommene Future zurückgeben, erlaubt die neue @Asynchronous-Annotation Methoden mit den Rückgabewerten CompletionStage und CallableFuture. Asynchrone Tasks können auf die Weise aneinandergereiht ausgeführt und ihre Rückgabewerte automatisch an die Folgetasks übergeben werden. Zusätzlich wird eine sinnvolle Fehlerbehandlung in dem nicht immer ganz trivialen asynchronen Kontext ermöglicht. Folgender Codeausschnitt zeigt ein Beispiel für die Anwendung von @Asynchronous:

@ApplicationScoped
public class AsyncWorkingService {

  @Asynchronous(executor = 
    "java:app/concurrent/someExecutorService")
  public CompletableFuture<WorkResult> 
    doSomeWork(final Workload workload) {
      try {
        var workResult = process(workload);
        return Asynchronous.Result.complete(workResult); 
      } catch (WorkException ex) {
        throw new CompletionException(ex);
    }
  }


  private WorkResult process(Workload workload) 
    throws WorkException 
  {
    ...
    return new WorkResult();
  }
  ...
}

Die neue Annotation ist ausschließlich für den Einsatz im Kontext von CDI Managed Beans gedacht. Die Spezifikation erwähnt explizit, dass sie nicht innerhalb einer EJB zum Einsatz kommen darf und dass mit ihr annotierte Methoden nicht zusätzlich mit der gleichnamigen Annotation aus der MicroProfile-Fault-Tolerance-API versehen sein dürfen.

Dass die Concurrency-API bisher dem Jakarta EE Full Profile zugeordnet war, hat immer wieder zu Diskussionen geführt. Es war nicht nachvollziehbar, warum Anwendungen für das Web Profile die gezielte asynchrone Ausführung von Taks nicht nutzen sollten. Mit der aktuellen Version 3.0 wechselt die API in das Web Profile.

Auch Jakarta JPA 3.1 bringt einige interessante Neuerungen mit sich. Künftig lassen sich UUIDs als Basistypen verwenden und können somit unter anderem als Primärschlüssel inklusive passender ID-Generatorstrategie verwenden:

@Entity
public class SomeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;
    
    private String someAttribute;
    private String anotherAttribute;
    ... 

}

Zwar erlaubten sowohl Hibernate als auch EclipseLink das Vorgehen in der Vergangenheit über proprietäre Bordmittel, aber es war nicht via JPA-Spezifikation standardisiert.

Die JPA Query Language und Criteria-API bringen ebenfalls Ergänzungen mit. Mit EXTRACT lassen sich numerische Teile eines Datums YEAR, QUARTER, MONTH, WEEK, DAY, HOUR, MINUTE und SECOND extrahieren. Die neuen Datumsfunktionen LOCAL DATE, LOCAL TIME, LOCAL DATE TIME erlauben das Handling von Local-Date- und -Time-Klassen aus dem java.time Package. Die numerischen Funktionen bringen als Erweiterungen die selbsterklärenden Funktionen CEILING, EXP, FLOOR, LN, POWER, ROUND und SIGN mit.

Während es früher normal war, sich bei einer Webanwendung via Nutzername und Passwort anzumelden, erlauben moderne Anwendungen meist eine Authentisierung gegen einen Drittanbieter wie Google, Facebook oder Twitter. Dazu bietet die Jakarta-Security-API 3.0 einen neuen Authentifizierungs-Mechanismus. Neben Basic Authentication, Form Authentication und Custom Form Authentication kennt sie neuerdings eine Authentifizierung auf Basis von OpenID Connect. Grundlage dafür ist der OpenID Connect Authorization Code Flow, der wiederum Teil der OpenID-Connect-Core-Spezifikation ist.

Die Authentifizierung über den OpenID Connect Authorization Code Flow ist nun mit der Jakarta-Security-API möglich (Abb. 4).

Folgender Codeausschnitt zeigt eine annotierte CDI Bean, um die OpenID-Connect-Authentifizierung zu aktivieren. Die für den Provider benötigten Metadaten finden sich in den Parametern der Annotation. Das Beispiel setzt dafür auf die Unified Expression Language.

@OpenIdAuthenticationMechanismDefinition(
  providerURI = "https://some-openid-connect-provider.com",
  clientId = "${my-client-id}",
  clientSecret = "${my-secret}",
  redirectURI = "${baseURL}/callback",
  extraParameters = {
    "provider_specific_key_1 = my_value_1",
    "provider_specific_key_2 = my_value_2"
  })
@ApplicationScoped
public class OpenIdConnectAuthentication { ... }

Ist ein Log-in über den OpenID Connect Provider erfolgreich, wird automatisch eine Credential-Instanz erzeugt, die das vom Provider zurückgelieferte Token erhält. Dass Letzteres valide ist, stellt ein für OpenID Connect spezifischer IdentityStore sicher.

Da der OpenID Connect Provider keine Kenntnis über die Rollen und Gruppen auf Seiten des Clients hat, lässt sich ein anwendungsspezifisches Mapping angeben. Die resultierenden Gruppen finden sich später in einer Instanz der Klasse CredentialValidationResult wieder.

Der neue Mechanismus ist neben der reinen Authentifizierung dafür zuständig, das Ablaufen des Tokens zu überwachen und gegebenenfalls über RefreshToken automatisch einen neuen Token anzufordern.

JAX-RS spezifiziert seit der 2008 veröffentlichten Version 1.0 Java SE als eine von mehreren Ablaufumgebungen für eine JAX-RS-basierte Anwendung. Leider hat die Spezifikation sich bisher darüber ausgeschwiegen, wie genau das zugehörige Bootstrapping auszusehen hat. Daher hat jeder Hersteller seine eigene proprietäre Lösung implementiert, um vom Applikationsserver unabhängige (Micro)Services aka Runnables zu realisieren.

Mit der Version 3.1 standardisiert JAX-RS nun das Bootstrapping einer JAX-RS-Server-Anwendung innerhalb von Java SE. Zwar sind die proprietären Lösungen weiterhin als Alternative erlaubt, aber der standardisierte Weg ist explizit der empfohlene, um für die Zukunft einen potenziellen Vendor-Lock-in zu umgehen.

Für einen einfachen Microservice-Endpunkt, der an Port 8080 lauschen soll, könnte ein Bootstrapping folgendermaßen aussehen:

import java.net.URI;
import jakarta.ws.rs.SeBootstrap;
import jakarta.ws.rs.SeBootstrap.Configuration;
import jakarta.ws.rs.core.Application;

public class HelloWorldAppBootstrap {

  public static void main(final String[] args) 
    throws InterruptedException 
  {
    final Application application = new HelloWorldApp();

    Configuration config = 
      Configuration.builder().port(8080).build();

    final Configuration appConfig = 
       Configuration.builder().from(config).build();

    // start embedded HTTP server for given
    // application and configuration
    SeBootstrap.start(application, appConfig)
      .thenAccept(instance -> 
    {
      // handle instance.stopOnShutdown callback
      ...
    });
    Thread.currentThread().join();
  }
}

Neben dem standardisierten Bootstrapping innerhalb von Java SE bringt die Spezifikation Jakarta RESTful Web Services 3.1 noch eine weitere erwähnenswerte Neuerung mit sich: das Handling von Multipart/Form-data Media-Type Requests. Obwohl RESTful Web Services eine der populärsten APIs aus Jakarta EE ist, fehlte das Format merkwürdigerweise bisher. Entwicklerinnen und Entwickler waren gezwungen, auf ein mit @MultiPartConfig annotiertes Servlet zurückzugreifen, das immerhin seit Servlet-API 3.0 und somit Java EE 6 (2009) verfügbar ist. Alternativ mussten sie eine proprietäre Library nutzen.

Die neue Umsetzung sieht vor, dass Anwendungen in der @Consumes-Annotation signalisieren, dass sie ein Multipart-Request erwarten. Für den Zugriff auf die einzelnen Teile der Anfrage bietet die API zwei Optionen: Entweder lassen sich Anwendungen eine Liste von EntityPart-Objekten übergeben oder sie übergeben die Teile des Multipart-Request als einzelne Parameter und adressieren sie über deren Namen. Folgender Code zeigt die Methodendeklaration für beide Varianten:

@Path("multipart-demo")
public class MultipartRequestHandler {
    
  @POST
  @PATH("list")	
  @Consumes(MediaType.MULTIPART_FORM_DATA)
  public Response 
    handleEntityPartList(List<EntityPart> parts)

  {
    for (EntityPart part : parts) {
      String name = part.getName();
      Optional<String> fileName = part.getFileName();
      InputStream inputStream = part.getContent();
      MultivaluedMap<String, String> 
        partHeaders = part.getHeaders(); 
      MediaType mediaType = part.getMediaType();

      // do something with part and headers 
      ...
    }
    return Response.ok().build(); 
  }

  @POST
  @PATH("parts")	
  @Consumes(MediaType.MULTIPART_FORM_DATA)
  public Response 
    handleEntityParts(@FormParam("name") String imageName,
                      @FormParam("image") InputStream image,
                      @FormParam("tags") EntityPart tags) 
  {

    // do something with selected parts 
    ...
  }
}