Klassenweite und parameterübergreifende Constraints mit der Spring Expression Language

How to SpEL Validation

How to SpEL Validation

Klassenweite und parameterübergreifende Constraints mit der Spring Expression Language

How to SpEL Validation


Die Validierung ist ein wichtiger Aspekt in der Anwendungsentwicklung, der sich durch alle Ebenen einer Schichtenarchitektur zieht. Mit JSR 349 Bean Validation [1] steht dem Entwickler eine ganze Reihe von standardisierten Annotationen zur Verfügung. Die Referenz­implementierung Hibernate Validator [2] erlaubt es, Constraints auf einer Klasse und deren Attributen oder mehreren Parametern einer Methode zu definieren. Die Implementierung ist aber recht aufwendig. Die Spring Expression Language kann hier Abhilfe schaffen.

Video: Modernes Java-Komponentendesign mit Spring 4.3

Das Validieren von Objekten oder Beans ist ein weitreichender Querschnittsaspekt in der Softwareentwicklung. Die Validierung sollte so früh wie möglich durchgeführt werden, um dem Benutzer direktes Feedback zu geben und das unnötige Ausführen von Anwendungslogik bei invaliden Daten zu verhindern. Trotzdem ist eine Validierung nicht nur im UI-Layer, sondern auch zu einem späteren Zeitpunkt sinnvoll, z. B. in der Serviceschicht oder in der Persistenzschicht. Die Gründe hierfür sind, dass sich nach Anwendung der Businesslogik der Zustand der Beans verändert haben könnte, bzw. die jeweilige Schicht aus unterschiedlichen Kontexten heraus konsumiert werden könnte, z. B. ein Aufruf über einen Web Service statt über die Benutzeroberfläche. Deswegen sollten Beans zu jedem Zeitpunkt validiert werden können. Dies sollte möglichst deklarativ durch die Beschreibung von Constraints auf den Eigenschaften eines Objekts erfolgen.

Um dieses Ziel zu erreichen, gibt es eine ganze Reihe von sowohl standardisierten als auch Hibernate-eigenen Java-Annotationen, die auf Attributen, Methoden oder Konstruktoren deklariert werden können. Tabelle 1 zeigt eine Übersicht der gängigsten Annotationen.

Boolean

Number

Date

String

Collection

Any Type

@AssertFalse

@DecimalMax

@Future

@Pattern

@Size

@Null

@AssertTrue

@DecimalMin

@Past

@Email

@NotEmpty

@NotNull

@Digits

@NotBlank

@Max

@CreditcardNumber

@Min

@Length

Tabelle 1: Auszug aus den vorhandenen Annotationen (fett: JSR 349, kursiv: Hibernate Validator)

Zur Veranschaulichung zeigt Listing 1 den Einsatz der Annotationen für die Klasse User mit den Standardannotationen @NotNull und der Hibernate-eigenen Annotation @Email, die festlegen, dass das jeweilige Attribut nicht null sein darf bzw. eine valide E-Mail-Adresse enthalten muss. In Listing 2 ist passend hierzu einen JUnit-Testfall dargestellt, um zu demonstrieren, wie die Validierung der Attribute eines Userobjekts letztendlich durchgeführt werden kann.

Der Testfall basiert auf dem Spring Framework [3]. Dies ist unter anderem an den Annotationen @ Run­With(Spring­JUnit4­ClassRunner.class) und @Con­text­Con­figu­ration zu erkennen. Letztere bindet eine Spring-Konfiguration ein (Listing 3). Hier wird mit der LocalValidatorFactoryBean eine Factory Bean instanziiert, die Validator Beans erzeugt. Eine Instanz dieser Validator Beans wurde in Listing 2 über @Autowired in den Testfall injiziert. Des Weiteren wird eine MethodValidationPostProcessor-Bean registriert, damit Spring auch automatisch auf Methodenebene validiert. Die Validierung selbst wird dann über den injizierten Validator und dessen Methode validate ausgelöst, die die zu validierende Bean entgegennimmt und eine Menge von potenziellen Constraint-Verletzungen zurückgibt.

Listing 1: Einsatz der Annotationen

public class User {
  @NotNull
  private String login;
  @NotNull
  @Email
  private String email;
  
  private Set<Car> cars;
  // Getters und setters
}

Listing 2: JUnit-Testfall

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { SpringConfig.class })
 
public class ValidateTest {
 
  @Autowired
    private Validator validator;
 
  @Test
  public void testValidateEmail() {
    User userValid = new User();
    userValid.setEmail("myemail@example.com");
    userValid.setLogin("Testuser");
    Set<ConstraintViolation<User>> violations = validator.validate(userValid);
    assertEquals(0, violations.size());
    User userInvalid = new User();
    userInvalid.setEmail("@example.com");
    violations = validator.validate(userInvalid);
    assertEquals(1, violations.size());
    assertEquals("email", violations.iterator().next()
      .getPropertyPath().iterator().next().getName());
  }
}

Listing 3: Spring-Konfiguration

@Configuration
@ComponentScan({"yourpackage"})
public class SpringConfig {
  @Bean
  public Validator validator() {
    return new LocalValidatorFactoryBean();
  }
 
  @Bean
  public MethodValidationPostProcessor methodValidationPostProcessor(Validator validator) {
    MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
    methodValidationPostProcessor.setValidator(validator);
    return methodValidationPostProcessor;
  }
}

Klassenweite Constraints definieren

Das Beispiel demonstriert, dass es recht einfach ist, einzelne Attribute mithilfe der vorhandenen Annotationen zu validieren. Es gibt jedoch auch Constraints, die sich auf mehrere Attribute beziehen. Diese sind jedoch in der Regel so individuell, dass es nicht sinnvoll ist, hierfür Standardannotationen anzubieten. Daher müssen für diese Anwendungsfälle eigene Annotationen definiert und die Validierungslogik muss selbst implementiert werden.

Im Fall der User-Klasse könnte beispielsweise eine Constraint definiert werden, die den Benutzernamen so einschränkt, dass entweder das Attribut login oder email gesetzt sein muss. Im Umkehrschluss folgt daraus, dass nur eines von beiden null sein darf. Um dies auszudrücken, wird eine eigene Annotation @ValidUsername definiert (Listing 4). Die Annotation ist wiederum mit Metaannotationen versehen, die folgende Semantik haben:

  • @Target definiert, auf ...