Suche
Kolumne: EnterpriseTales

Alles valide? Bean Validation in POJOs mittels aspektorientierter Programmierung

Lars Röwekamp, Arne Limburg
enterprise_tales

Wir berichteten im Rahmen dieser Kolumne bereits über die Neuerungen von Bean Validation 1.1. Besonders erwähnenswert war und ist dabei das neue Feature der Method Validation. Leider ist die Integration dieses Features noch nicht sehr weit vorangeschritten. Bisher ist es nur bei CDI-Beans möglich, sich darauf zu verlassen, dass z. B. ein Parameter, der mit @NotNull annotiert ist, auch wirklich niemals null ist. In allen anderen Situationen hilft es normalerweise nur, die Method Validation aus dem Code heraus „von Hand“ aufzurufen. In dieser Kolumne wollen wir eine alternative Möglichkeit vorstellen. Damit ist es möglich, in beliebigen POJOs auf Method Validation zu setzen.

Bean Validation ist der Java-EE-Standard für schichtenübergreifende Validierung. Er kann verwendet werden, wenn es darum geht, Property Constraints per Annotations zu definieren und deren Einhaltung automatisch sicherzustellen. Das Sicherstellen der Einhaltung übernehmen dabei integrierende Standards wie JSF und JPA, die auf Bean Validation aufsetzen und zu geeigneten Zeitpunkten annotierte Java-Objekte validieren und gegebenenfalls auf Constraint-Verletzungen reagieren. Bei JSF geschieht das in der Process-Validation-Phase, bei JPA in der Regel beim Transaktions-Commit. Das führt bei korrekter Konfiguration dazu, dass fehlerhafte Daten via JSF gar nicht erst eingegeben werden können, und wenn doch, wird durch den Check von JPA dennoch sichergestellt, dass diese auf keinen Fall in der Datenbank landen.

Für die Dinge, die zwischen Dateneingabe und Datenspeicherung passieren, also für die Businesslogik, gab es in Bean Validation 1.0 allerdings keine Möglichkeit, diese Checks automatisiert durchzuführen. Das hat sich mit Bean Validation 1.1 geändert. Mit dieser Version des Standards wurde die Method Validation eingeführt, die es ermöglicht, Methoden und Parameter mit Constraint Annotations zu versehen. Das ermöglicht es auch anderen Standards wie CDI und JAX-RS, leicht auf Bean Validation zu setzen. Seit Bean Validation 1.1 und JAX-RS 2.0 geschieht das auch: Annotiert man z. B. einen Parameter einer CDI-Bean oder einer JAX-RS-Ressource mit @NotNull, gibt es automatisch eine entsprechende Fehlermeldung.

Bean Validation in POJOs

Was passiert aber, wenn ich einen Methodenparameter einer JPA-Entität oder eines POJOs, das überhaupt nicht vom Server gemanagt wird, mit einer Constraint-Annotation wie z. B. @NotNull versehe? Die Antwort ist: gar nichts. Es gibt ja auch aktuell kein Framework, das die Validierung anstoßen könnte. Möchte man auf Bean Validation in POJOs setzen, muss man diese aktuell von Hand ansteuern. Wenn man diesen Versuch unternimmt, stößt man auf ein Problem im Method-Validation-API: Anders als das Property-Validation-API, das die zu validierende Property als String-Parameter erwartet, benötigt das Method-Validation-API ein Objekt vom Typ java.lang.reflect.Method. Das ist im Businesscode etwas unhandlich. Diese Hürde lässt sich aber mit einer kleinen Utility-Klasse nehmen, die aus dem Objekt, auf dem die Methode aufgerufen wird, dem Methodennamen und den Parametern die benötigte Methode extrahiert (Listing 1). Dazu müssen wir zunächst aus den tatsächlichen Argumenten die jeweiligen Typen (Klassen) extrahieren. Nun müssen wir bei allen Methoden mit dem gesuchten Namen überprüfen, ob die Argumenttypen übereinstimmen. Dabei ist zu beachten, dass keine 1:1-Übereinstimmung gesucht ist, weil die Klassen der tatsächlichen Argumente durchaus Unterklassen der Klassen der Methodenparameter sein können.

Listing 1

Method getMethod(Object object, String methodName, Object... arguments) {
    Class<?>[] argumentTypes = Arrays.asList(arguments).stream()
      .map(p -> p != null? p.getClass(): p).toArray(size -> new Class[size]);
    return getMethod(object.getClass(), methodName, argumentTypes);
  }

  Method getMethod(Class<?> type, String methodName, Class<?>[] argumentTypes) {
    if (type == null) {
      throw new IllegalArgumentException("Method " + methodName + " not found");
    }
    Optional<Method> method = Arrays.asList(type.getDeclaredMethods()).stream()
      .filter(m -> m.getName().equals(methodName))
      .filter(m -> areAssignable(m.getParameterTypes(), argumentTypes))
      .findFirst();
    if (method.isPresent()) {
      return method.get();
    } else {
      return getMethod(type.getSuperclass(), methodName, argumentTypes);
    }
  }
  
  boolean areAssignable(Class<?>[] parameterTypes, Class<?>[] argumentTypes) {
    if (parameterTypes.length != argumentTypes.length) {
      return false;
    }
    for (int i = 0; i < parameterTypes.length; i++) {
      if (argumentTypes[i] != null
          && !parameterTypes[i].isAssignableFrom(argumentTypes[i])) {
        return false;
      }
    }
    return true;
  }

Mit dem so entstandenen Code lässt sich aus Objekt, Methodennamen und Argumenten die java.lang.reflect.Method extrahieren, mit der sich dann die Method Validation aufrufen lässt (Listing 2).

Möchte man in seinen Constraint-Validatoren zusätzlich CDI verwenden, sollte man den Bean Validator nicht wie im Listing selbst erzeugen, sondern aus dem CDI-Kontext holen. Dies geht in Java EE 7 über CDI.current().select(Validator.class).get().

Leider muss bei der Verwendung unserer Validierungsmethode der Methodenname als String übergeben werden. Das ist zwar besser, als die java.lang.reflect.Method direkt übergeben zu müssen, sehr schön ist es aber auch nicht, und vor allem nicht besonders Refactoring-sicher. Benennt man die Methode um, ist die IDE zwar in der Lage, alle Aufrufstellen mit umzubenennen, besagter String muss aber von Hand angepasst werden. Wird dies vergessen, gibt es auch erst zur Laufzeit den entsprechenden Fehler.

Listing 2

private static final ValidatorFactory FACTORY
  = Validation.buildDefaultValidatorFactory();

void validateParameters(Object object, String methodName, Object... arguments) {
      Method method = getMethod(object, methodName, arguments);
      validateParameters(object, method, arguments);
    }

    void validateParameters(Object object, Method method, Object... arguments) {
      ExecutableValidator validator = FACTORY.getValidator().forExecutables();
      Set<ConstraintViolation<Object>> violations
      = validator.validateParameters(object, method, arguments);
    check(violations);
  }

  void validateReturnValue(Object object, Method method, Object result) {
    ExecutableValidator validator = FACTORY.getValidator().forExecutables();
    Set<ConstraintViolation<Object>> violations
      = validator.validateReturnValue(object, method, result);
    check(violations);
  }

  void check(Set<ConstraintViolation<Object>> violations) {
    if (!violations.isEmpty()) {
      throw new ConstraintViolationException(violations);
    }
  }

Aspektorientierung to the Rescue

Was ist eigentlich Aspektorientierung? Diese vor zehn Jahren groß aufgekommene Technologie stellt Querschnittsaspekte der Softwareentwicklung in den Mittelpunkt. Ziel ist es, immer wieder verwendete Querschnittsfunktionalität (Aspects) an zentraler Stelle zu implementieren und automatisch an den entsprechenden Stellen im Code auszuführen. Die Stellen im Code, an denen ein Aspect potenziell ausgeführt werden könnte, werden dabei JoinPoints genannt. Um zu definieren, dass ein bestimmter Aspect an einem konkreten JoinPoint ausgeführt wird, werden so genannte PointCuts verwendet. Bei ihnen handelt es sich um Expressions, die z. B. auch Wildcards enthalten können, mit denen eine bestimmte Codestelle beschrieben wird. Der Code, der am JoinPoint ausgeführt werden soll, wird Advice genannt. Hier unterscheidet man zwischen Before Advices, die vor der entsprechenden Codestelle ausgeführt werden, After Advices, die nach der Codestelle ausgeführt werden, und Around Advices, die, man ahnt es schon, um die entsprechende Codestelle herum ausgeführt werden. Bei den After Advices kann zusätzlich noch dazwischen entschieden werden, ob der Code auch oder sogar nur dann ausgeführt werden soll, wenn die Stelle durch den Wurf einer Exception verlassen wurde. Anwendungsfälle für aspektorientierte Programmierung sind typische Beispiel für Querschnittsfunktionalität, etwa Transaktionsbehandlung, Logging oder eben Validierung.

In ihrer Anwendung ähnelt aspektorientierte Programmierung damit den aus JSR-318 bekannten Interceptoren. Aspektorientierte Programmierung ist aber viel flexibler und kann dadurch in deutlich mehr Situationen angewendet werden. Nicht nur Methodenaufrufe, sondern z. B. auch der Zugriff auf ein Feld eines POJOs kann als Pointcut definiert werden.

Technische Umsetzung von aspektorientierter Programmierung

Die Verwendung aspektorientierter Programmierung in Java-Projekten erfordert einen zusätzlichen Build-Schritt. Dabei wird der Bytecode der Java-Klassen insofern verändert, als an den jeweiligen Pointcuts die Advices eingefügt werden. Für das verbreitete Aspekt-Framework AspectJ übernimmt das der Compiler ajc. Dieser kann entweder anstelle des normalen Java-Compilers verwendet werden. Diese Variante wird in Eclipse angewendet, indem man das AspectJ-Plug-in installiert und für das eigene Projekt aktiviert. Alternativ kann der ajc-Compiler auch als zweiter Schritt nach dem eigentlichen Kompilieren verwendet werden. Dann ändert er den entstandenen Bytecode nachträglich und webt die Aspekte ein. Diese Variante wird im aspectj-maven-plugin verwendet.

Kombination von Bean Validation und Aspektorientierung

Dem aufmerksamen Leser wird nicht entgangen sein, dass Validierung einer der Anwendungsfälle für aspektorientierte Programmierung ist. Und da AspectJ in der Lage ist, beliebige POJOs mit Aspekten zu versehen, liegt es nahe, unser oben beschriebenes Problem, nämlich, dass Method-Validation nicht in POJOs zur Verfügung steht, über Aspektorientierung zu lösen. Alles was wir brauchen, ist ein Aspekt, der an jede Methode gehängt wird, an der eine Bean-Validation-Annotation existiert. Es gibt verschiedene Möglichkeiten, einen Aspekt in AspectJ zu schreiben. Wir wählen hier die Variante über Annotations. Dann müssen wir nur eine beliebige Klasse mit @Aspect annotieren. Um dann den tatsächlichen Code zu definieren, der an einem JoinPoint ausgeführt werden soll, müssen wir eine Methode mit der Annotation @Before, @AfterReturning oder @Around versehen, an der wir den Pointcut angeben, also die Expression, mit der AspectJ feststellt, an welchen Stellen der Advice ausgeführt werden soll. In unserem Fall suchen wir alle Methoden, die mit einer Bean-Validation-Constraint-Annotation versehen sind. Eine solche Annotation bezieht sich immer entweder auf mehrere Parameter (Cross-Parameter-Validation) oder auf den Rückgabewert. Im zugehörigen Aspekt müssten wir also zunächst die Parameter validieren, dann die Methode ausführen und dann den Rückgabewert validieren. Wir wählen daher einen @Around Advice. Um diesen an alle Methoden zu binden, die mit @NotNull versehen sind, müssten wir folgende Expression verwenden: execution(@javax.validation.constraints.NotNull  * *.*(..)). Das Schlüsselwort execution legt dabei fest, dass wir unseren Aspekt an eine Methodenausführung binden wollen. Das erste Sternchen bedeutet, dass es sich dabei um eine Methode mit beliebiger Sichtbarkeit handelt, die weiteren Sternchen geben ein beliebiges Package und einen beliebigen Methodennamen an. Die zwei Punkte in der Klammer signalisieren beliebige Parameter.

Wie bereits geschrieben, würde unser Aspekt so um alle Methodenaufrufe herum ausgeführt, deren Methoden mit @NotNull annotiert sind. Wir wollen unseren Advice aber an Methoden mit beliebiger Constraint-Annotation hängen. Um nicht für jede Annotation einen separaten Pointcut definieren zu wollen, müssen wir die Expression generalisieren: Eine Constraint-Annotation in Bean Validation lässt sich daran erkennen, dass sie ihrerseits wieder mit einer (Meta-)Annotation versehen ist, nämlich mit @Constraint. Glücklicherweise lassen sich Expressions in AspectJ beliebig schachteln, sodass man eine entsprechende Expression tatsächlich formulieren kann: execution(@(@javax.validation.Constraint)  * *.*(..)) (Listing 3).

Noch nicht abgedeckt sind mit dieser Expression Methoden, bei denen ein Parameter mit einer Constraint-Annotation versehen ist. Bei solchen Methoden müssten wir nur die Parametervalidierung durchführen. Wir definieren diese daher in einem @Before Advice (Listing 3).

Listing 3

@Aspect
public class BeanValidationAspect {

  @Before("execution(* *.*(.., @(@javax.validation.Constraint *) (*), ..))")
  public void validateParameters(JoinPoint point) {
    ValidationUtil.validateParameters(
      point.getTarget(), point.getSignature().getName(), point.getArgs());
  }

  @Around("execution(@(@javax.validation.Constraint *) * *.*(..))")
  public Object validate(ProceedingJoinPoint point) throws Throwable {
    Object target = point.getTarget();
    Object[] arguments = point.getArgs();
    String methodName = point.getSignature().getName();
    Method method = ValidationUtil.getMethod(target, methodName, arguments);
    ValidationUtil.validateParameters(target, method, arguments);
    Object returnValue = point.proceed();
    ValidationUtil.validateReturnValue(target, method, returnValue);
    return returnValue;
  }
}

Fazit

Mit Bean Validation 1.1 wurde Method Validation eingeführt. Diese bringt allerdings das Problem mit sich, dass zur Bedienung des API eine java.lang.reflect.Method benötigt wird, die im Applikationscode normalerweise nicht direkt zur Verfügung steht. Wir haben in dieser Kolumne gezeigt, wie man über eine kleine Hilfsmethode dazu kommen kann, dass man nur den Methodennamen als String verwenden kann.

Der viel größere Nachteil der Method Validation ist allerdings, dass es bisher kaum Frameworks gibt, die sie unterstützen. In Java EE ist das nur CDI. Folglich kann Method Validation auch nur in CDI Beans verwendet werden und z. B. nicht in JPA Entitys oder anderen POJOs.

Um dieses Problem zu umgehen, haben wir in dieser Kolumne die Kombination von Bean Validation mit AspectJ vorgestellt. Dadurch wird es möglich, Method Validation in beliebigen Klassen anzuwenden. Das erfordert allerdings einen zusätzlichen Build-Schritt. Vielleicht werden ja in der Zukunft mehr Frameworks Method Validation out of the box unterstützen, sodass dieser zusätzliche Schritt nicht mehr nötig ist. In diesem Sinne: Stay tuned.

Geschrieben von
Lars Röwekamp
Lars Röwekamp
Lars Röwekamp ist Gründer des IT-Beratungs- und Entwicklungsunternehmens open knowledge GmbH, beschäftigt sich im Rahmen seiner Tätigkeit als „CIO New Technologies“ mit der eingehenden Analyse und Bewertung neuer Software- und Technologietrends. Ein besonderer Schwerpunkt seiner Arbeit liegt derzeit in den Bereichen Enterprise und Mobile Computing, wobei neben Design- und Architekturfragen insbesondere die Real-Life-Aspekte im Fokus seiner Betrachtung stehen. Lars Röwekamp, Autor mehrerer Fachartikel und -bücher, beschäftigt sich seit der Geburtsstunde von Java mit dieser Programmiersprache, wobei er einen Großteil seiner praktischen Erfahrungen im Rahmen großer internationaler Projekte sammeln konnte.
Arne Limburg
Arne Limburg
Arne Limburg ist Softwarearchitekt bei der open knowledge GmbH in Oldenburg. Er verfügt über langjährige Erfahrung als Entwickler, Architekt und Consultant im Java-Umfeld und ist auch seit der ersten Stunde im Android-Umfeld aktiv.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.