CDI Interceptoren, gesprächige Service-Schicht


Es gibt Momente da kann man nicht genug Informationen über den Ablauf seiner Anwendung haben, so zum Beispiel um Fehler-Situationen nach stellen zu können. Also muss ein ausführliches Logging her, am besten in der Form das wir möglichst wenig damit zu tun haben.

loggggs

Für diesen Fall bieten sich CDI Interceptoren an. Ein CDI Interceptor ist im Grunde eine einfache Methode die mittels Annotationen registriert werden kann und so Aufrufe von anderen Methoden umschließ. Mit diesem Vorgehen lässt sich sehr einfach ein entsprechendes Logging implementieren. Die Aktivierung eines solchen Loggings kann dann z.B. über eine entsprechende Logger-Kategorie geschehen (Vorteil: zur Laufzeit lässt sich diese Anpassen) oder (ohnehin Verpflichtend) über den Eintrag in der beans.xml realisiert werden. Auch wäre es denkbar solche Log-Informationen nur im Fehlerfall auszugeben. Was im Detail geloggt werden soll ist sicherlich immer Ansichtssache und unterscheidet sich je nach Art der Anwendung. Hier ein kurzes Beispiel wie so etwas aussehen könnte und die Ausgabe von Klasseninformationen( Klasse, Methode, Parameter), technischem HTTP-Context (IP Adresse, HTTP Headers) und zusätzlicher Debug Informationen über Annotations-Parameter

@Inherited
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Debug {

   @Nonbinding String message() default "";
}

Interceptor Binding

@Interceptor
@Debug
public class DebugInterceptor implements Serializable {

    @AroundInvoke
    public Object debug(InvocationContext ctx) throws Exception {
        String debugMessage = ctx.getMethod().getAnnotation(Debug.class).message();
        this.logRuntimeInformation(ctx, Level.FINE, debugMessage);
        return ctx.proceed();
    }

    private void logRuntimeInformation(InvocationContext ctx, Level level, String message) {
        String targetClass = ctx.getTarget().getClass().getSuperclass().getName();
        String targetMethod = ctx.getMethod().getName();
        String parameters = Stream.of(ctx.getParameters()).map(Object::toString).collect(Collectors.joining(", "));

        String remoteAddr;
        String headers;
        if (httpRequest != null) {
            remoteAddr = httpRequest.getRemoteAddr();
            headers = Collections.list(httpRequest.getHeaderNames()).stream().map((key) -> key + ":" + httpRequest.getHeader(key)).collect(Collectors.joining(", "));
        } else {
            remoteAddr = "unknown";
            headers = "undefined";
        }

        this.logger.log(level, String.format("Call of: %s#%s (%s) with parameters: [%s] from client %s (Headers: [%s])", targetClass, targetMethod, message, parameters, remoteAddr, headers));
    }

}

Implementierung

@ApplicationScoped
public class HelloWorldService implements Serializable{

    @Debug(message="Abruf formatiertes Datum")
    public String getFormattedDate(String format) {
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(format));
    }

    @Debug(message="Produziere einen Fehler")
    public String getSomeError(String msg) {
        return String.format("%s %d", msg, "World");
    }
}

Verwendung

 

Live? In Farbe? github !

Advertisements

Schlanke JEE-Services mit Apache Meecrowave

Das Programmmodell von JEE (aka Java EE / Jakarta EE) ermöglicht schon seit vielen Jahren – namentlich seit der Version 5 – eine effiziente Entwicklung von schlanken Geschäftsanwendungen. Als Ablaufumgebungen stehen ausgereifte und leistungsfähige JEE-Server wie WildFly oder Open Liberty zur Verfügung, die als eine Art Allrounder den Anwendungen recht umfassende Infrastruktur anbieten und sogar mehrere Anwendungen gleichzeitig betreiben können.

Gerade der letzte Punkt, der Parallelbetrieb mehrere Anwendungen, wird allerdings nur selten genutzt, um Störeffekte zwischen Anwendungen zu minimieren. Es gibt also typischerweise genau eine Anwendung pro Server, der ggf. sogar selbst in einem dedizierten Rechner – meist einer virtuelle Maschine – läuft.

Wenn man nun noch einen nur eingeschränkten Teil der angebotenen Infrastruktur benötigt, bspw. JPA, CDI und JAX-RS, also den klassischen Microservice-Stack, kann man sich die Frage stellen, ob tatsächlich ein kompletter JEE-Server gebraucht wird, oder ob man nicht einfach die benötigten Dienste in die Anwendung einbetten könnte.

An dieser Stelle setzt Apache Meecrowave an. Dieses Apache-Projekt verknüpft den JPA-Provider EclipseLink, den CDI-Container OpenWebBeans und die REST-Implementierung CXF zu einem ganz schlanken Server – oder eigentlich zu einer Server-Bibliothek, mit deren Hilfe man den Server einfach als Main-Programm startet.

Zur Einbindung von Meecrowave reicht es aus, die folgende Dependency in den Classpath zu holen:

  
    org.apache.meecrowave
    meecrowave-core
    1.2.3
  

Der in der Anwendung eingebettete Server wird dann z. B. so gestartet:

  public static void main(String[] args) {
    try (Meecrowave meecrowave = new Meecrowave().bake()) {
      // Do work here ...
    }

Beim Eintritt in den try-Block wird der Server gestartet, beim Verlassen wieder gestoppt. Sämtliche aus CDI bekannte Mechanismen für den Bootstrap einer Anwendung können zur Aktivierung von Services etc. genutzt werden, z. B. Observer für den Container Lifecycle:

  void startSomething(@Observes @Initialized(ApplicationScoped.class) Object obj) {
    // Gets called on server start ...
  }

Ein einfacher REST-Service ist dann nur noch ein Ding von wenigen Zeilen:

@Path("hello")
@ApplicationScoped
public class HelloResource {

  @GET
  @Produces(MediaType.TEXT_PLAIN)
  public String getHello() {
    return "Hello!";
  }
}

So ist also dieser – zugegebenermaßen winzige – Microservice mit nur ganz wenig Code schon fertig. Und beeindruckt mit Startzeiten im Sekundenbruchteil-Bereich sowie einem Memory Footprint von nur ca. 12 MB.

Ein einfaches Demoprojekt kann von GitHub heruntergeladen werden: https://github.com/GEDOPLAN/meecrowave-demo.

Bis bald – vielleicht in einem unserer Trainings in Berlin, Bielefeld, Köln oder bei Ihnen!
http://gedoplan-it-training.de/

Jackson, mehr Annotationen

In früheren Artikeln haben wir bereits einige Features von Jackson gesehen die uns das Erzeugen und Verarbeiten von JSON-Strukturen einfacher macht. So lassen sich unsere Businessobjekte mittels @JsonView gefilterte in eine JSON-Ansicht bringen oder Dank @JsonIdentityInfo und entsprechenden Resolver selbst JPA-Relationen sauber verarbeiten. Zwei weitere Möglichkeiten schauen wir uns heute an

updateValue

Wenn JPA-Relationen oder auch einzelne Felder nicht ins JSON Format übertragen werden sollen lässt sich das sehr einfach mittels @JsonIgnore oder @JsonView erledigen. Bei der Verwendung solcher Strukturen als Input für das Updaten unserer Entitäten führt das jedoch zu einem Problem: nicht vorhandene Attribute werden mit „null“ vorbelegt, was in aller Regel zu nicht gewollten Änderungen unserer Businessobjekte führt. Hier kommt, ähnlich wie bei der Verwendung von DTOs, in aller Regel ein Mapping-Framework ins Spiel welches die neuen Daten aus unserem JSON dazu verwendet das bestehende Objekt zu aktualisieren. Seit der Version 2.9 können wir dies auch getrost von Jackson erledigen lassen:

   @PUT
   @Path("{id}")
   public Response updateAuthor(@PathParam("id") Integer id, JsonNode authorJson){
        Author dbAuthor = this.authorRepository.getAuthorById(id);
        dbAuthor = objectMapper.updateValue(dbAuthor, authorJson);
        dbAuthor= this.authorRepository.merge(dbAuthor);
        return Response.ok().build();
   }

@JsonAppend

DTOs sind ein praktisches Vorgehen um zusätzliche Attribute oder gänzlich andere Strukturen als unsere Businessobjekte per REST zu liefern. Der Aufwand zum Schreiben und der Pflege dieser DTO-Klassen sollte allerdings nicht unterschätzt werden. Wollen wir lediglich einige einzelne zusätzliche Attribute zu den bestehenden Businessobjekt-Attribute an unser JSON anhängen hält Jackson auch hierfür eine Lösung parat:

//- Entity - ------------------------------------------
@Entity
@JsonAppend(
    attrs = {
        @JsonAppend.Attr(value = "bookcount")
    }
)
public class Author {...}

//- Resource - ------------------------------------------

    @GET
    @Path("{id}")
    public Response getAuthor(@PathParam("id") Integer id) {
        ...
            String response=objectMapper
                    .writerFor(Author.class)
                    .withView(GlobalViews.Overview.class)
                    .withAttribute("bookcount", authorById.getBooks().size())
                    .writeValueAsString(authorById);
            
            return Response.ok(response).build();
        }
    }

(im Default sind diese Append-Attribute optional, werden also nur gerendert wenn auch ein Wert zugewiesen wird. Dieses Verhalten kann durch das Setzen des Annotation-Attributes ‚include‘ geändert werden

Jackson und Java EE 8


Jackson ist eine Bibliothek die sich unter anderem darum kümmert das unsere Businessobjekte in JSON umgewandelt werden. Als default Provider für diese Aufgabe war Jackson bisher im Wildfly Application Server vorhanden. Seit Java EE 8 (und Wildfly 13) gibt es nun einen neuen Standard der sich um diese Aufgabe kümmert: JSON-B. Dieser ist leider in Sachen Funktionsumfang bei weitem noch nicht auf den Stand den Jackson erreicht hat. Um Jackson weiterhin zu nutzen sind (leider) einige Schritte von nöten

Laut Spezifikation von JAX-RS reicht es aus eine @Provider-Klasse an zu bieten welche für einen bestimmten Media Type die Konvertierung unserer Businessobjekte übernimmt. Diesen liefert Jackson mit einer entsprechenden Maven Dependency praktischerweise gleich mit (com.fasterxml.jackson.jaxrs, jackson-jaxrs-json-provider ). Schade nur das es bei dieser Theorie bleibt. Zumindest der Wildfly 13 (im EE8 Profil) und der Glassfish 5 weigern sich dieses einfache Vorgehen mit Erfolg zu krönen.

Wildfly 13

Ein Bug in RestEasy (RESTEASY-1911 ) verhindert hier das Jackson korrekt eingebunden wird. Ab der Version 3.6 soll dieser Umstand behoben sein, bis dahin hilft der Ausschluss des Resteasy Json Providers:

<jboss-deployment-structure xmlns="urn:jboss:deployment-structure:1.0">
    <jdeployment>
        <jexclusions>
            <jmodule name="org.jboss.resteasy.resteasy-json-binding-provider"><j/module>
        <j/exclusions>
    <j/deployment>
<j/jboss-deployment-structure>

src/main/webapp/WEB-INF/jboss-deployment-structure.xml

Glassfish 5

Die Referenzimplementierung für Java EE 8 ist hartnäckig was das Ersetzen von JSON-B als JSON Provider angeht. Hier ist ein zusätzlicher Konfigurationsparamter notwenig:

@ApplicationPath("resources")
public class ApplicationConfig extends Application {

    @Override
    public Map getProperties() {
        Map proprties = new HashMap();
        proprties.put("jersey.config.server.disableMoxyJson", true); //Glassfish = 5

        return proprties;
    }

}

Optional: globaler ObjectMapper

Jacksons ObjectMapper bietet eine ganze Reihe von globalen Einstellungen um das Verhalten beim Parsen von JSON-Strukturen zu beeinflussen (Format, Umgang mit null-Werten und unbekannter Attribute…). Um in der gesamten Anwendung eine einheitliche Konfiguration zu verwenden bietet sich ein entsprechner CDI Producer an:

@ApplicationScoped
public class GlobalCDIProducer {

    private ObjectMapper mapper;
    
    @Produces
    public ObjectMapper getMapper(){
        if(this.mapper==null){
            this.mapper=new ObjectMapper();
            DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm");
            this.mapper.setDateFormat(df);
        }
        return this.mapper;
    }
}

Diesen können wir natürlich selber in unseren Resouce-Klassen injizieren und Verwenden, aber auch JAX-RS zur Verfügung stellen:

@Provider
public class JacksonProvider implements ContextResolver<ObjectMapper> {

    @Inject
    private ObjectMapper mapper;
    
    @Override
    public ObjectMapper getContext(Class<?> type) {
        return getMapper();
    }
}

Java EE 8, Security (JSR 375)


Für die Deklaration von Security brauchen wird xml-Dateien. Sei es die web.xml oder serverspezifische Konfigurationsdateien. So ist es lange Zeit gewesen. Bis jetzt. Java EE 8 weicht diese Feststellung nun auf und führt Interfaces ein, wie den HttpAuthenticationMechanism um die Deklaration dieser Regeln in den Code zu verlagern.

ee8security

Java EE 8 liefert neben dem angesprochenen Interface (und weiterer) auch gleich drei passende Implementierungen mit:

  • BasicAuthenticationMechanismDefinition
  • FormAuthenticationMechanismDefinition
  • CustomFormAuthenrticationMechanismDefinition

In diesem Beispiel widmen wir uns einem davon: FormAuthenticationMechanismDefinition. Die Idee ist einfach. Anstatt die Konfiguration in xml-Dateien vor zu nehmen, verwenden wir Annotationen um die Security-Regeln (an einer ApplicationScoped-Bean) zu deklarieren. Eine solche Deklaration könnte so aussehen:

@FormAuthenticationMechanismDefinition(
        loginToContinue = @LoginToContinue(
                loginPage = "/login.html",
                errorPage = "/error.html"))

Damit legen wir erst einmal nur fest wie der User seine Login-Informationen übergeben soll (nämlich über die login.html Datei). Ebenso spannend ist die Frage „Wie autorisiert unsere Anwendung einen User?“. Wer hätte es erwartet, auch hier liefert der JSR375 ein passendes Interface (IdentityStore) und entsprechende Implementierungen (DataBaseIdentityStoreDefinition, LdapIdentityStoreDefinition).

@DatabaseIdentityStoreDefinition(
        dataSourceLookup = "java:jboss/datasources/DemoDS",
        callerQuery = "select PASSWORD from USER where USERNAME=?",
        groupsQuery = "select GROUPNAME from USERGROUPS where USERNAME=?",
        hashAlgorithm = de.gedoplan.PlainTextPasswordHash.class
)
@ApplicationScoped
public class ApplicationSecurityConfig {}

Hier geben wir an welche Datasource und Queries verwendet werden sollen um einen Benutzer zu validieren und dessen Benutzergruppen aus zu lesen. Zusätzlich muss hier eine PasswordHash-Implementierung angegeben werden (javax.security.enterprise.identitystore.PasswordHash). Mit Pbkdf2PasswordHash wird eine Implementierung mitgeliefert, das Beispiel oben verwendet eine eigene Implementierung welche die Passwörter in Klartext in der Datenbank ablegt (don’t do this at home).

Das war es schon ( fast ) jetzt können wir unsere Resourcen mit entsprechenden Berechtigungen versehen. Hier in Kürze 3 Varianten:

Servlets:

@WebServlet(urlPatterns = "/servlet")
@ServletSecurity(@HttpConstraint(rolesAllowed = "ADMIN"))
public class DemoServlet extends HttpServlet{...}

JAX-RS, Rest Webservices

@Path("demo")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Stateless
public class DemoResource {

    @GET
    @PermitAll
    public DemoModel getDemo() {...}
    
    @GET
    @Path("admin")
    @RolesAllowed("ADMIN")
    public DemoModel getAdminDemo() {...}
}

Webseiten über web.xml (dann doch noch mal xml)

    <security-constraint>
        <web-resource-collection>
            <web-resource-name>Protected user resource/url</web-resource-name>
            <url-pattern>/secured/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>ADMIN</role-name>
            <role-name>USER</role-name>
        </auth-constraint>
    </security-constraint>

Das wars. Alles. Live. In Farbe:

https://github.com/GEDOPLAN/jee8-security

Anmerkung zur Verwendung von Wildfly:
Das gezeigte Feature muss durch die Aktivierung der Security-Domain „jaspitest“ erst aktiviert werden (ab Wildfly 10)
(in der jboss-web.xml<jboss-web> <security-domain>jaspitest </jboss-web>)
Hier auch eine Anleitung für andere Versionen: click

Generics, Type Erasure ausgetrickst

Generics sind eine feine Sache die in nahezu jeder Application anwendung finden. Die meisten Entwickler werden jedoch festgestellt haben das die generische Typinformation zur Laufzeit nicht mehr zur Verfügung stehen. Aber wie dann zur Laufzeit an den Typ gelangen?

„Type Erasure“ heißt das „Phänomen“ das aus abwärtskompatiblen Gründen fester Bestandteil des Java Compilers ist. Generische Informationen sind somit nur zur Compile-Zeit vorhanden und werden bei der Übersetzung in den ByteCode entfernt. Damit fällt eine Factory wir die folgende erst einmal weg:

public class Factory<T> {

public T create() {
       return new T(); // Type-Erasure in Action
}

Wir können natürlich nun die konkrete Klasse als zusätzlichen Parameter in eine solche Factory übergeben um auf Basis dieser neue Instanzen zu erzeugen. Das funktioniert, fühlt sich aber nach „doppeltem Aufwand“ an, da die konkrete Klasse zwei mal angegeben wird.

Ein kleine Schlupfloch gibt es aber: anonyme Klassen. Anders als „normale“ Klassen sind bei anonymen Klassen die generischen Informationen auch zur Laufzeit vorhanden.

In unserer Factory können wir dann per Reflection den generischen Typ ermitteln und damit z.B. eine entsprechende Instanz erzeugen.

public class Factory<T> {

    public T create() throws InstantiationException, IllegalAccessException {
        Type superClassType = this.getClass().getGenericSuperclass();
        Type tType = ((ParameterizedType) superClassType).getActualTypeArguments()[0];
        Class clazz = (Class) tType;
        
        return clazz.newInstance();
    }
}
//Type Erasure > Exception
Customer c = new Factory<Customer>().create();

//anonyme Klasse
Customer c = new Factory<Customer>(){}.create();

Angular, asyncrone Initialisierung

Es gibt Dinge die sollte man einfach nicht aufschieben, dazu zählen auch immer wieder diverse Schritte die vor dem Start der eigentlichen Anwendung nötig sind. Sei es eine externe Konfiguration zu laden oder den alten Login Status eines Benutzers zu prüfen. Angular bietet hier die Möglichkeit diese (oftmals asynchronen) Aufgaben per APP_INITIALIZER Multi-Provider zu realisieren

angular_init_demo

Für diesen Zweck bietet Angular einen Multi Provider (analog zu z.B. NG_VALIDATORS) an: APP_INITIALIZER der in unserem Modul mittels Provider-Deklaration registriert wird.

@NgModule({
  providers: [
    {
      provide: APP_INITIALIZER,
      multi: true,
      deps: [HttpClient],
      useFactory: initApplication
    },

Neben der provide-Token (APP_INITIALIZER) und der Angabe das es sich um einen Multi-Provider3 handelt (multi: true), müssen wir eventuell benötige Depedencies deklarieren die in unserem Beispiel unserer Factory-Funktion übergeben werden.

Bei der Factory handelt es sich um eine im Modul deklarierte Methode die eine neue Methode zurück liefern muss die wärend der Initialisierung aufgerufen wird.
Diese erzeugte Methode kann dann entweder synchron ablaufen oder aber ein Promise liefern auf dessen Abarbeitung gewartet wird (Achtung, auf die Rückkehr von Observables wird hier nicht gewartet).

export function initApplication(http: HttpClient) {
  return () => {
    return new Promise((resolve, reject) => {
      //make some init steps, load data for example
      http
        .get('https://jsonplaceholder.typicode.com/users/1')
        .delay(5000) //wait a little bit, just to enjoy the loading gif
        .subscribe(
          r => {
            //handle response
            console.log('Init 1');
            resolve(r);
          },
          e => {
            // dont forget to handle errors or user will only see a blank screen
            document.body.innerHTML = 'Schwerer Ausnahmefehler';
          }
        );
    });
  };
}

Die Anwendung lädt, führt dann erst die Initialisierung durch und startet dann wie gewohnt. Dadurch sind wir in der Lage im Vorfeld sicher zu stellen das benötigte Services bereits initialisiert sind oder die Anwendung korrekt konfiguriert wurde

Demo? Auf GitHub.