Blog

Versenden komplexer Formulare mit RESTEasy - Teil 1

Maarten Winkels

Aktualisiert Oktober 22, 2025
7 Minuten

RESTEasy ist ein Framework für die Erstellung von RESTful-Anwendungen in Java. In diesem Blog zeige ich Ihnen, wie Sie ganz einfach RESTful Webservices erstellen können, die Daten aus einem HTML-Formular entgegennehmen. Wir werden auch die Möglichkeiten erkunden, RESTEasy zu erweitern, um komplexere Fälle zu behandeln.

Hinweis: Der gesamte Quellcode in diesem Beitrag ist als gezipptes Maven-Projekt beigefügt.

Einfaches Beispiel

Lassen Sie uns also ein ganz einfaches Beispiel betrachten:

public class Person {
  private String firstName;
  private String lastName;
}
@Pfad("/person")
public class PersonResource {
  private PersonRepository repository;
  @POST
  @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
  @Products(MediaType.APPLICATION_JSON)
  public Person savePerson(Person person) {
  return repository.save(person);
  }
}

Die Absicht dieser beiden Klassen sollte ziemlich klar sein: Wir wollen die savePerson-Methode als WebService bereitstellen, Daten aus einem HTML-Formular akzeptieren und JSON erzeugen. Die für die PersonResource verwendeten Anmerkungen sind JAX-RS-Annotationen. Beachten Sie, dass wir in der Service-Methode ein Person-Objekt akzeptieren und zurückgeben. Wir erwarten, dass das RESTEasy-Framework das Unmarshalling aus den Formularfeldern und das Marshalling in JSON übernimmt. Glücklicherweise verfügt JAX-RS auch über einige praktische Annotationen für diese Aufgabe: Mit der Annotation@FormParam können Sie festlegen, welches Formularfeld welchem Parameter oder Feld zugeordnet werden soll. Leider wird in der JAX-RS-Spezifikation nicht auf die Zuordnung von Formularen zu Rich Objects eingegangen, so dass die Signatur für unsere Servicemethode folgendermaßen aussehen würde

  public Person savePerson (
  @FormParam("firstName") String firstName,
  @FormParam("lastName") String lastName,
  ...
  ) {

was ziemlich schnell ziemlich groß und unhandlich werden würde, wenn das Formular alle üblichen Namens- und Adressfelder enthalten muss. Hier kommt RESTEasy mit der @Form-Annotation zur Hilfe. Diese Annotation weist das RESTEasy-Framework an, das gesamte Formular auf ein Parameterobjekt abzubilden, was uns von der Aufgabe entbindet, alle Formularfelder in der Parameterliste der Servicemethode anzugeben. Der Code wird somit:

public class Person {
  @FormParam("firstName") private String firstName;
  @FormParam("lastName") private String lastName;
}
@Pfad("/person")
public class PersonResource {
  private PersonRepository repository;
  @POST
  @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
  @Products(MediaType.APPLICATION_JSON)
  public Person savePerson(@Form Person person) {
  return repository.save(person);
  }
}

Wie kann ich das testen?

Mit diesen beiden einfachen Klassen sind wir mit der Implementierung unserer einfachen Anforderungen fertig. Aber wie testen wir das? RESTEasy wird mit einer JUnit-Erweiterung geliefert, die für das Testen von Ressourcen sehr nützlich ist.

@RunWith(MockitoJUnitRunner.class)
public class PersonResourceTest extends BaseResourceTest{
  private PersonResource resource;
  @Mock private PersonRepository repository;
  @Vor
  public void setupResource() {
  MockitoAnnotations.initMocks(getClass());
  resource = new PersonResource();
  resource.setRepository(repository);
(1) dispatcher.getRegistry().addSingletonResource(resource);
  }
  @After
  public void removeResource() {
  dispatcher.getRegistry().removeRegistrations(PersonResource.class);
  }
  @Test
  public void shouldSavePerson() throws Exception {
(2) MockHttpRequest request = post("/person");
  request.addFormHeader("vorname", "Maarten");
  request.addFormHeader("Nachname", "Winkels");
(3) ArgumentCaptor<Person>  personCaptor = ArgumentCaptor.forClass(Person.class);
  when(repository.save(personCaptor.capture())).thenAnswer(new ArgumentAnswer<Person>(0));
  MockHttpResponse response = new MockHttpResponse();
(4) dispatcher.invoke(Anfrage, Antwort);
  verify(repository).save(any(Person.class));
(5) Person person = capturePerson.getLastValue();
  assertEquals("Maarten", person.getVorname());
  assertEquals("Winkels", person.getLastName());
(6) assertEquals(200, response.getStatus());
  assertEquals("{"Vorname": "Maarten", "Nachname": "Winkels"}", response.getContentAsString());
  }
}

Der obige Test ist ein echter Integrationstest in Bezug auf RESTEasy, in dem Sinne, dass ein HttpRequest vom Framework verarbeitet wird und unsere PersonResource dazu veranlasst, eine JSON-Antwort zu erzeugen. Mockito wird verwendet, um das PersonRepository (das noch nicht implementiert ist) zu mocken und um das Verhalten des Codes im Test zu überprüfen. Schritt für Schritt durch den Code:

  1. Die PersonResource mit dem Mock für das Repository ist beim Framework im Test registriert. Wir müssen sie hier als Singleton registrieren, da wir die Ressource mit einer Attrappe vorbereiten müssen. Beachten Sie, dass die Klasse in der @After-Methode entfernt wird, um sicherzustellen, dass die Tests keine Nebeneffekte haben.
  2. Ein MockHttpRequest ist für die Verarbeitung mit bestimmten Werten vorbereitet.
  3. Das Mock-Repository ist so eingerichtet, dass es das in der Anfrage enthaltene Argument erfasst und das gleiche Argument als Antwort zurückgibt. ArgumentAnswer ist eine kleine Erweiterung des Mockito/Hamcrest-Frameworks, die es dem Mock ermöglicht, den in den Aufruf des Mocks übergebenen Parameter als Rückgabewert zurückzugeben.
  4. Die Anfrage wird auf dem Dispatcher ausgeführt, wobei die Antwort in einer MockHttpResponse erfasst wird.
  5. Die von uns erfasste Person wird überprüft, um zu sehen, dass die Werte aus der Anfrage kopiert werden.
  6. Die Antwort wird auf Rückgabewert und Inhalt geprüft.

Das war einfach! Wir haben unsere gesamte RESTful-Anwendung integriert getestet, ohne einen Container starten zu müssen!

Ein wenig komplexer: verschachtelte Objektstrukturen und Formulare

Versuchen wir es mal mit etwas Komplexerem: Gehen wir davon aus, dass die Person eine Adresse hat. Wir codieren zuerst den Test:

  @Test
  public void shouldSavePersonWithAddress() throws Exception {
  MockHttpRequest request = post("/person");
  request.addFormHeader("vorname", "Maarten");
  request.addFormHeader("Nachname", "Winkels");
  request.addFormHeader("address.street", "Dorpsstraat");
  request.addFormHeader("address.houseNumber", "19a");
  ArgumentCaptor<Person>  personCaptor = ArgumentCaptor.forClass(Person.class);
  when(repository.save(personCaptor.capture())).thenAnswer(new ArgumentAnswer<Person>(0));
  MockHttpResponse response = new MockHttpResponse();
  dispatcher.invoke(Anfrage, Antwort);
  verify(repository).save(any(Person.class));
  Person Person = capturePerson.getLastValue();
  assertEquals("Maarten", person.getVorname());
  assertEquals("Winkels", person.getLastName());
  assertEquals("Dorpsstraat", person.getAddress().getStreet());
  assertEquals("19a", person.getAddress().getHouseNumber());
  assertEquals(200, response.getStatus());
  assertEquals("{"Vorname": "Maarten", "Nachname": "Winkels", "Adresse":{"Straße": "Dorpsstraat", "Hausnummer": "19a"}}", response.getContentAsString());
  }

Wie Sie aus dem Test ersehen können, hat eine Person nun eine Adresse und wir erwarten, dass die Formularfelder, die mit "Adresse." beginnen, den Feldern der Klasse Adresse zugeordnet werden. In gewissem Sinne wird das Formular jetzt auf die verschachtelte Objektstruktur von Person und Adresse abgebildet. Leider bieten weder JAX-RS noch RESTEasy Unterstützung für diese Funktion. (RESTEasy unterstützt zwar eine @Form-Annotation in einer @Form gemappten Parameterklasse, aber die Angabe eines Präfixes wird nicht unterstützt, so dass nur ein einziges verschachteltes Objekt desselben Typs möglich ist.) Also führen wir eine neue Annotation ein, um anzuzeigen, dass ein Feld einem Teil der Formularfelder zugeordnet ist:

public class Person {
...
  @NestedFormParams("Adresse")
  private Adresse;
...
}

Für das RESTEasy-Framework bedeutet diese neue Annotation eine neue Art der Injektion von Werten in Objekte, die der Injektion von Werten in @Form annotierte Parameter sehr ähnlich ist. *Injektorenwerden von einer InjectorFactory erstellt. Die nachstehende ExtendedInjectorFactory erweitert die RESTEasy InjectorFactoryImpl, damit RESTEasy die neue Annotation kennt.

public class ExtendedInjectorFactory extends InjectorFactoryImpl{
  private final ResteasyProviderFactory factory;
  public ExtendedInjectorFactory(ResteasyProviderFactory factory) {
  super(factory);
  this.factory = factory;
  }
  @Override
  public ValueInjector createParameterExtractor(Class injectTargetClass, AccessibleObject injectTarget, Class type, Type genericType, Annotation[] annotations, boolean useDefault) {
  NestedFormParams param = FindAnnotation.findAnnotation(annotations, NestedFormParams.class);
  if (param != null) {
  return new NestedFormInjector(type, factory, param.value());
  }
  return super.createParameterExtractor(injectTargetClass, injectTarget, type, genericType, annotations, useDefault);
  }
}

In diesem Test wird die neue InjectorFactory bei der ProviderFactory registriert: [java] @Before public void setupResource() { getProviderFactory().setInjectorFactory(new ExtendedInjectorFactory(getProviderFactory())); ... [/java] } Immer wenn eine NestedFormParams Annotation gefunden wird, wird ein NestedFormInjector erstellt, um die Injektion zu verarbeiten. Da die Funktionalität dem FormInjector so ähnlich ist, ist er einfach eine Erweiterung dieser Klasse.

public class NestedFormInjector extends FormInjector {
  private final String prefix;
  public NestedFormInjector(Klasse<?>  type, ResteasyProviderFactory factory, String prefix) {
  super(Typ, Fabrik);
  this.prefix = prefix;
  }
  @Override
  public Object inject(HttpRequest request, HttpResponse response) {
  if (!containsPrefixedName(request.getDecodedFormParameters().keySet())) {
  null zurückgeben;
  }
  return super.inject(new PrefixedFormFieldsHttpRequest(prefix, request), response);
  }
  private boolean containsPrefixedName(Set<Zeichenfolge>  Schlüssel) {
  for (String Schlüssel : Schlüssel) {
  if (key.startsWith(prefix)) {
  true zurückgeben;
  }
  }
  return false;
  }
}

Um zu verstehen, wie der NestedFormInjector funktioniert, muss man wissen, wie der FormInjector funktioniert. Der FormInjector wird für einen bestimmten Typ erstellt. Er verwendet einen ConstructorInjector, um eine Instanz dieses Typs zu erstellen, und einen PropertyInjector, um weitere Werte in diese Instanz zu injizieren. Die Injektion dieser Werte wird von anderen Injektoren ausgeführt. Wenn ein Feld in der Klasse mit einer @FormParam-Annotation versehen ist, wird der FormInjector verwendet, um den Wert aus dem Formularparameter in das Feld zu injizieren. Der NestedFormInjector funktioniert, indem er den Zugriff aller FormInjectors, die für seine Eigenschaften verwendet werden, auf Formularfelder beschränkt, die mit dem angegebenen Präfix (und einem Punkt) beginnen. Dies wird erreicht, indem die ursprüngliche HttpRequest in eine PrefixedFormFieldHttpRequest verpackt wird. Objekte dieser Klasse verpacken die ursprünglichen Formularfelder in eine Map, die bei jeder Suche das Präfix hinzufügt. Den Code finden Sie in der angehängten ZIP-Datei.

Fazit

Durch die Erweiterung einiger Kernklassen des RESTEasy-Frameworks haben wir die Funktionalität hinzugefügt, ein HTTP-Formular auf eine komplexe Assoziation von zwei Klassen abzubilden. Dies kann sehr nützlich sein, wenn komplexe Formulare gepostet werden. Ein nächster Schritt wäre die Zuordnung von Formularfeldern zu Sammlungen, Listen oder Maps. Darüber werde ich in einem der nächsten Beiträge schreiben.

Verfasst von

Maarten Winkels

Contact

Let’s discuss how we can support your journey.