Benutzerdefinierte Validierung von Spring MVC

1. Übersicht

Wenn wir Benutzereingaben validieren müssen, bietet Spring MVC im Allgemeinen vordefinierte Standardvalidatoren an.

Wenn wir jedoch eine bestimmte Art von Eingabe validieren müssen, haben wir die Möglichkeit, eine eigene, benutzerdefinierte Validierungslogik zu erstellen .

In diesem Artikel machen wir genau das - wir erstellen einen benutzerdefinierten Validator, um ein Formular mit einem Telefonnummernfeld zu validieren, und zeigen dann einen benutzerdefinierten Validator für mehrere Felder an.

Dieser Artikel konzentriert sich auf Spring MVC. In unserem Artikel Validierung in Spring Boot wird beschrieben, wie Sie benutzerdefinierte Validierungen in Spring Boot durchführen.

2. Setup

Fügen Sie die Abhängigkeit zu Ihrer pom.xml- Datei hinzu, um von der API zu profitieren :

 org.hibernate hibernate-validator 6.0.10.Final  

Die neueste Version der Abhängigkeit kann hier überprüft werden.

Wenn wir Spring Boot verwenden, können wir nur das Spring-Boot-Starter-Web hinzufügen , wodurch auch die Abhängigkeit vom Hibernate-Validator entsteht .

3. Benutzerdefinierte Validierung

Wenn Sie einen benutzerdefinierten Validator erstellen, müssen Sie unsere eigenen Anmerkungen einführen und in unserem Modell verwenden, um die Validierungsregeln durchzusetzen.

Erstellen wir also unseren benutzerdefinierten Validator, der die Telefonnummern überprüft . Die Telefonnummer muss eine Nummer mit mehr als acht Ziffern, jedoch nicht mehr als 11 Ziffern sein.

4. Die neue Anmerkung

Erstellen wir eine neue @ Schnittstelle , um unsere Anmerkung zu definieren:

@Documented @Constraint(validatedBy = ContactNumberValidator.class) @Target( { ElementType.METHOD, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) public @interface ContactNumberConstraint { String message() default "Invalid phone number"; Class[] groups() default {}; Class[] payload() default {}; }

Mit der Annotation @Constraint haben wir die Klasse definiert, die unser Feld validieren soll. Die Nachricht () ist die Fehlermeldung, die in der Benutzeroberfläche angezeigt wird, und der zusätzliche Code ist der Code mit den meisten Boilerplates, der den Spring-Standards entspricht.

5. Erstellen eines Validators

Erstellen wir nun eine Validator-Klasse, die die Regeln unserer Validierung erzwingt:

public class ContactNumberValidator implements ConstraintValidator { @Override public void initialize(ContactNumberConstraint contactNumber) { } @Override public boolean isValid(String contactField, ConstraintValidatorContext cxt) { return contactField != null && contactField.matches("[0-9]+") && (contactField.length() > 8) && (contactField.length() < 14); } }

Die Validierungsklasse implementiert die ConstraintValidator- Schnittstelle und muss die isValid- Methode implementieren . In dieser Methode haben wir unsere Validierungsregeln definiert.

Natürlich verwenden wir hier eine einfache Validierungsregel, um zu zeigen, wie der Validator funktioniert.

ConstraintValidator definiert die Logik zum Überprüfen einer bestimmten Einschränkung für ein bestimmtes Objekt. Implementierungen müssen der folgenden Einschränkung entsprechen:

  • Das Objekt muss in einen nicht parametrisierten Typ aufgelöst werden
  • Generische Parameter des Objekts müssen unbegrenzte Platzhaltertypen sein

6. Anwenden der Validierungsanmerkung

In unserem Fall haben wir eine einfache Klasse mit einem Feld erstellt, um die Validierungsregeln anzuwenden. Hier richten wir unser kommentiertes Feld zur Validierung ein:

@ContactNumberConstraint private String phone;

Wir haben ein Zeichenfolgenfeld definiert und es mit unserer benutzerdefinierten Anmerkung @ContactNumberConstraint kommentiert. In unserem Controller haben wir unsere Zuordnungen erstellt und den Fehler ggf. behandelt:

@Controller public class ValidatedPhoneController { @GetMapping("/validatePhone") public String loadFormPage(Model m) { m.addAttribute("validatedPhone", new ValidatedPhone()); return "phoneHome"; } @PostMapping("/addValidatePhone") public String submitForm(@Valid ValidatedPhone validatedPhone, BindingResult result, Model m) { if(result.hasErrors()) { return "phoneHome"; } m.addAttribute("message", "Successfully saved phone: " + validatedPhone.toString()); return "phoneHome"; } }

Wir haben diesen einfachen Controller mit einer einzelnen JSP- Seite definiert und verwenden die Methode submitForm , um die Validierung unserer Telefonnummer zu erzwingen.

7. Die Ansicht

Unsere Ansicht ist eine einfache JSP-Seite mit einem Formular, das ein einzelnes Feld enthält. Wenn der Benutzer das Formular sendet, wird das Feld von unserem benutzerdefinierten Validator validiert und mit der Meldung "Erfolgreiche oder fehlgeschlagene Validierung" auf dieselbe Seite weitergeleitet:

 Phone:      

8. Tests

Lassen Sie uns nun unseren Controller testen und prüfen, ob er uns die richtige Antwort und Ansicht gibt:

@Test public void givenPhonePageUri_whenMockMvc_thenReturnsPhonePage(){ this.mockMvc. perform(get("/validatePhone")).andExpect(view().name("phoneHome")); }

Testen wir außerdem, ob unser Feld anhand von Benutzereingaben validiert wurde:

@Test public void givenPhoneURIWithPostAndFormData_whenMockMVC_thenVerifyErrorResponse() { this.mockMvc.perform(MockMvcRequestBuilders.post("/addValidatePhone"). accept(MediaType.TEXT_HTML). param("phoneInput", "123")). andExpect(model().attributeHasFieldErrorCode( "validatedPhone","phone","ContactNumberConstraint")). andExpect(view().name("phoneHome")). andExpect(status().isOk()). andDo(print()); }

Im Test stellen wir einem Benutzer die Eingabe von "123" zur Verfügung, und - wie erwartet - funktioniert alles und wir sehen den Fehler auf der Clientseite .

9. Benutzerdefinierte Validierung auf Klassenebene

A custom validation annotation can also be defined at the class level to validate more than one attribute of the class.

A common use case for this scenario is verifying if two fields of a class have matching values.

9.1. Creating the Annotation

Let's add a new annotation called FieldsValueMatch that can be later applied to a class. The annotation will have two parameters field and fieldMatch that represent the names of the fields to compare:

@Constraint(validatedBy = FieldsValueMatchValidator.class) @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface FieldsValueMatch { String message() default "Fields values don't match!"; String field(); String fieldMatch(); @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @interface List { FieldsValueMatch[] value(); } }

We can see our custom annotation also contains a List sub-interface for defining multiple FieldsValueMatch annotations on a class.

9.2. Creating the Validator

Next, we need to add the FieldsValueMatchValidator class that will contain the actual validation logic:

public class FieldsValueMatchValidator implements ConstraintValidator { private String field; private String fieldMatch; public void initialize(FieldsValueMatch constraintAnnotation) { this.field = constraintAnnotation.field(); this.fieldMatch = constraintAnnotation.fieldMatch(); } public boolean isValid(Object value, ConstraintValidatorContext context) { Object fieldValue = new BeanWrapperImpl(value) .getPropertyValue(field); Object fieldMatchValue = new BeanWrapperImpl(value) .getPropertyValue(fieldMatch); if (fieldValue != null) { return fieldValue.equals(fieldMatchValue); } else { return fieldMatchValue == null; } } }

The isValid() method retrieves the values of the two fields and checks if they are equal.

9.3. Applying the Annotation

Let's create a NewUserForm model class intended for data required for user registration, that has two email and password attributes, along with two verifyEmail and verifyPassword attributes to re-enter the two values.

Since we have two fields to check against their corresponding matching fields, let's add two @FieldsValueMatch annotations on the NewUserForm class, one for email values, and one for password values:

@FieldsValueMatch.List({ @FieldsValueMatch( field = "password", fieldMatch = "verifyPassword", message = "Passwords do not match!" ), @FieldsValueMatch( field = "email", fieldMatch = "verifyEmail", message = "Email addresses do not match!" ) }) public class NewUserForm { private String email; private String verifyEmail; private String password; private String verifyPassword; // standard constructor, getters, setters }

To validate the model in Spring MVC, let's create a controller with a /user POST mapping that receives a NewUserForm object annotated with @Valid and verifies whether there are any validation errors:

@Controller public class NewUserController { @GetMapping("/user") public String loadFormPage(Model model) { model.addAttribute("newUserForm", new NewUserForm()); return "userHome"; } @PostMapping("/user") public String submitForm(@Valid NewUserForm newUserForm, BindingResult result, Model model) { if (result.hasErrors()) { return "userHome"; } model.addAttribute("message", "Valid form"); return "userHome"; } }

9.4. Testing the Annotation

To verify our custom class-level annotation, let's write a JUnit test that sends matching information to the /user endpoint, then verifies that the response contains no errors:

public class ClassValidationMvcTest { private MockMvc mockMvc; @Before public void setup(){ this.mockMvc = MockMvcBuilders .standaloneSetup(new NewUserController()).build(); } @Test public void givenMatchingEmailPassword_whenPostNewUserForm_thenOk() throws Exception { this.mockMvc.perform(MockMvcRequestBuilders .post("/user") .accept(MediaType.TEXT_HTML). .param("email", "[email protected]") .param("verifyEmail", "[email protected]") .param("password", "pass") .param("verifyPassword", "pass")) .andExpect(model().errorCount(0)) .andExpect(status().isOk()); } }

Als Nächstes fügen wir außerdem einen JUnit- Test hinzu, der nicht übereinstimmende Informationen an den Endpunkt / user sendet und bestätigt, dass das Ergebnis zwei Fehler enthält:

@Test public void givenNotMatchingEmailPassword_whenPostNewUserForm_thenOk() throws Exception { this.mockMvc.perform(MockMvcRequestBuilders .post("/user") .accept(MediaType.TEXT_HTML) .param("email", "[email protected]") .param("verifyEmail", "[email protected]") .param("password", "pass") .param("verifyPassword", "passsss")) .andExpect(model().errorCount(2)) .andExpect(status().isOk()); }

10. Zusammenfassung

In diesem kurzen Artikel haben wir gezeigt, wie Sie benutzerdefinierte Validatoren erstellen, um ein Feld oder eine Klasse zu überprüfen und sie mit Spring MVC zu verbinden.

Wie immer finden Sie den Code aus dem Artikel auf Github.