# API
Um die gewünschte Schnittstelle zu definieren, stehen mehrere Annotationen zur Verfügung.
# @RestController
In einer Klasse, die mit @RestController annotiert ist, werden HTTP-Endpunkte programmiert, die aufrufbar sein sollen. @RestController ist eine Spezialisierung von @Controller und wird daher automatisch von Spring gefunden (die Applikationsklasse muss also nicht angefasst werden).
# @*Mapping
Innerhalb dieser Klasse werden Methoden mit @GetMapping, @PostMapping usw. markiert, was sie zu aufrufbaren HTTP-Endpoints macht.
Die Route – also der Pfad, der aufzurufen ist – wird dabei der Annotation übergeben.
TIP
@GetMapping & Co sind Spezialisierungen von @RequestMapping und setzen dabei schon den Wert für method auf GET usw.
Alle anderen Optionen von @RequestMapping, wie z. B. consumes und produces stehen natürlich auch bei @*Mapping zur Verfügung.
Der Rückgabewert der Methoden wird als HTTP-Response ausgeliefert. Dabei kann dies einfach nur ein String sein oder auch eine Objektinstanz.
Das berühmte Beispiel:
@RestController
class HelloController {
@GetMapping("/hello")
fun hello() = "<h1>Hello, world!</h1>"
}
Dies führt beim Aufruf im Browser zu folgendem Bild:
Standardmäßig wird bei einem String als Rückgabewert text/html ausgeliefert, falls ein Browser anfragt. Bei anderen Clients wie curl wird text/plain ausgeliefert. Das wird anhand des Accept Headers entschieden, den der Client mitschickt.
Der Content-Type der Response kann mittels produces explizit geändert werden:
@GetMapping("/hello-plain", produces = [MediaType.TEXT_PLAIN_VALUE])
fun hello2() = "Hello, world!"
# @PathVariable
Im Pfad können benannte Segment-Platzhalter verwendet werden (auch mehrere). Diese werden durch geschweifte Klammern gekennzeichnet. Die dort tatsächlich übergebenen Werte landen dann in den mit @PathVariable markierten Methodenparametern.
@GetMapping("/user/{name}")
fun user(@PathVariable("name") name: String) = "Details for $name"
Die explizite Angabe des Platzhalternamens ist dabei optional. Fehlt diese, nimmt Spring den Name des Methodenparameters. Dieser Code ist also äquivalent, kürzer und somit empfehlenswerter:
@GetMapping("/user/{name}")
fun user(@PathVariable name: String) = "Details for $name"
Ein Aufruf von /user/bob sieht dann so aus:
In der Dokumentation (opens new window) sind Platzhalter detailliert erklärt.
# @RequestParam
Request-Parameter können einfach als Methodenparameter mit @RequestParam markiert werden. Auch hier kann der explizite Parametername entfallen:
@GetMapping("/echo")
fun echo(@RequestParam what: String) = "echoing: $what"
Handelt es sich beim Methodenargument um einen simplen Typ (String, Int, ... siehe genaue Definition hier (opens new window)), egal ob nullable oder nicht, kann die @RequestParam Annotation auch weggelassen werden, denn Spring mappt solche Argumente standardmäßig als @RequestParam:
@GetMapping("/echo")
fun echo(what: String) = "echoing: $what"
Der Übersicht halber und um Fehler zu vermeiden, sollte man überlegen, die Annotation aber trotzdem anzugeben.
Nullability, also ob der Parametertyp z. B. als String oder String? deklariert wurde, macht einen wichtigen Unterschied: Ist der Parameter nicht als nullable (also String) deklariert und fehlt dann im Request, wird von Spring automatisch ein HTTP-Fehler 400 (Bad Request) ausgelöst und der Code in der Methode nicht ausgeführt.
Entscheidend ist auch der angegebene Typ. So nimmt ein Parameter number: Int weder "test" noch "2.5" als gültigen Wert entgegen und quittiert dies ebenfalls mit Bad Request.
Hier zeigt sich wieder, dass man nicht mühselig alles selbst überprüfen muss. Man definiert stattdessen einfach, was man erwartet, und ist dem nicht so, liefert Spring selbstständig eine Fehlermeldung aus.
TIP
Um Parameter einzufangen, die mehrfach angegeben werden können (beispielsweise?item=first&item=second), wird als Typ List<String> verwendet (statt String sind natürlich auch andere Typen wie Int möglich). Der Methodenparameter muss dann mit @RequestParam markiert werden.
@RequestParam item: List<String>
# Weitere Methoden-Argumente
In der Dokumentation (opens new window) finden noch weitere Zuweisungsmöglichkeiten für Methodenparameter.
Es können beispielsweise auch Cookie-Werte gelesen werden (@CookieValue). Ebenso ist ein Low-Level Zugriff auf den Request und die Response möglich.
# @CrossOrigin
Um CORS Requests zu erlauben, kann an die entsprechenden Methoden oder auch die ganze Klasse die Annotation @CrossOrigin gepackt werden. Die Dokumentation dazu (opens new window) erklärt, was alles konfiguriert werden kann.
@CrossOrigin(
originPatterns = ["localhost:*"],
allowedHeaders = ["X-Custom-Header"],
allowCredentials = "true"
)
# Logging
Standardmäßig werden bei einem Fehler wegen fehlender oder falscher Parameter keine Logzeilen ausgegeben. Man kann das Loglevel aber einfach in application.properties verändern:
logging.level.org.springframework.web=DEBUG
So werden diverse Infos geloggt:
2021-01-27 18:29:06.915 DEBUG 13108 --- [ctor-http-nio-2] o.s.w.s.adapter.HttpWebHandlerAdapter : [063f90c7-2] HTTP GET "/num?x=drei"
2021-01-27 18:29:06.916 DEBUG 13108 --- [ctor-http-nio-2] s.w.r.r.m.a.RequestMappingHandlerMapping : [063f90c7-2] Mapped to com.example.demo.HelloController#num(int)
2021-01-27 18:29:06.917 DEBUG 13108 --- [ctor-http-nio-2] o.s.web.method.HandlerMethod : [063f90c7-2] Could not resolve parameter [0] in public java.lang.String com.example.demo.HelloController.num(int): 400 BAD_REQUEST "Type mismatch."; nested exception is org.springframework.beans.TypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'int'; nested exception is java.lang.NumberFormatException: For input string: "drei"
2021-01-27 18:29:06.919 DEBUG 13108 --- [ctor-http-nio-2] org.springframework.web.HttpLogging : [063f90c7-2] Resolved [ServerWebInputException: 400 BAD_REQUEST "Type mismatch."; nested exception is org.springframework.beans.TypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'int'; nested exception is java.lang.NumberFormatException: For input string: "drei"] for HTTP GET /num
2021-01-27 18:29:06.919 DEBUG 13108 --- [ctor-http-nio-2] org.springframework.web.HttpLogging : [063f90c7-2] Writing "<html><body><h1>Whitelabel Error Page</h1><p>This application has no configured error view, so you (truncated)...
2021-01-27 18:29:06.920 DEBUG 13108 --- [ctor-http-nio-2] o.s.w.s.adapter.HttpWebHandlerAdapter : [063f90c7-2] Completed 400 BAD_REQUEST
Um nur Details von fehlerhaften Request-JSONs zu sehen, genügt diese Einstellung:
logging.level.org.springframework.web.server.handler.ResponseStatusExceptionHandler=DEBUG
Damit werden Nachrichten der folgenden Art geloggt:
2021-02-16 13:17:55.588 DEBUG 19660 --- [ctor-http-nio-2] o.s.w.s.h.ResponseStatusExceptionHandler : [558c919c-1] Resolved [ServerWebInputException: 400 BAD_REQUEST "Failed to read HTTP message"; nested exception is org.springframework.core.codec.DecodingException: JSON decoding error: Instantiation of [simple type, class demo.api.CreateDeviceDto$Feature] value failed for JSON property type due to missing (therefore NULL) value for creator parameter type which is a non-nullable type; nested exception is com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException: Instantiation of [simple type, class demo.api.CreateDeviceDto$Feature] value failed for JSON property type due to missing (therefore NULL) value for creator parameter type which is a non-nullable type
at [Source: (io.netty.buffer.ByteBufInputStream); line: 6, column: 3] (through reference chain: demo.api.CreateDeviceDto["features"]->java.util.ArrayList[0]->demo.api.CreateDeviceDto$Feature["type"])] for HTTP POST /api/device
# Kotlin
Hier (opens new window) und hier (opens new window) werden noch weitere Möglichkeiten aufgezeigt, wie man Endpunkte in einer Kotlin-spezifischeren Art definieren kann.