Mit seiner Dissertation hat Roy Thomas Fielding im Jahr 2000 das Programmierparadigma Representational State Transfer (kurz: Rest) für verteilte Systeme, vor allem für Web-Services, beschrieben.
In der Folge entstanden sehr viele RESTful Web APIs, die leider nicht alle Vorteile bzw. Ideen hinter dem beschriebenen Architekturstil umsetzten und somit nicht das volle Potential ausnutzten.
Leonard Richardson entwickelte daraufhin ein Reifegradmodell mit dem sich Rest-Services auf ihre “Rest-kompatibilität” prüfen lassen. Bekannt wurde das Modell unter dem Namen “Richardson Maturity Model / RMM”.
Dieses Modell teilt Services in 4 verschiedene Stufen ein:
Daran soll sich auch die in diesem Artikel entstehende Implementierung orientieren. Es wird ein Go basierter Rest-Service implementiert, der zumindest Level 2 des RMM umsetzt. Auf die Verwendung von Hypermedia wird verzichtet. In diesem Beispiel soll keine “externe” Bibliothek oder Framework eingesetzt werden.
Über einen Customer-Service können Kunden angelegt oder gelistet werden. Jeder Kunde stellt eine Ressource dar und erhält dementsprechend eine eindeutige URI über die er abgerufen werden kann.
Beispiel einer eindeutigen Kunden URI:
http://localhost:8080/customer/{id}
Ein Kunde wird folgende Properties besitzen:
Im Beispiel wird auch auf die Umsetzung einer Content Negotiation verzichtet. Alle Daten sollen fest mittels JSON repräsentiert werden. Unabhängig welche HTTP-Header mitgeschickt werden.
Beispielkunde:
{
"id": 1,
"firstname": "Kristian",
"lastname": "Köhler"
}
Eine Kommunikation mit dem Service soll so aussehen:
Kunde mit Id 1 anfragen:
GET http://localhost:8080/customer/1
Es wurde kein Kunde mit Id 1 gefunden (das Repository ist am Anfang noch leer):
HTTP/1.1 404 Not Found
Content-Length: 9
Content-Type: text/plain; charset=utf-8
Connection: close
Not found
Neuer Kunde anlegen:
PUT http://localhost:8080/customer/1
{
"id": 1,
"firstname": "Kristian",
"lastname": "Köhler"
}
Kunde wurde erfolgreich angelegt:
HTTP/1.1 204 No Content
Date: <val>
Connection: close
Nochmalige Anfrage
GET http://localhost:8080/customer/1
Daten werden erfolgreich ermittelt:
HTTP/1.1 200 OK
Content-Type: application/json
Date: <val>
Content-Length: 53
Connection: close
{
"id": 1,
"firstname": "Kristian",
"lastname": "Köhler"
}
Die Implementierung des Service wird in einen Controller und ein Repository aufgeteilt. Der Controller soll die Http-Anfragen abarbeiten, das Repository ist für die Datenablage verantworlich. Der eigentliche Server wird über das go http Package implementiert.
Das komplette Beispiel kann auch in GitHub gefunden werden.
Im ersten Schritt wird ein struct
für die Kunden-Repräsentation benötigt. Der Kunde enthält die drei oben genannten Attribute Id
, Firstname
und Lastname
. Diese werden über Field Tags mit Metainformationen für das JSON Marshalling versehen, so dass die JSON-Felder nur Kleinbuchstaben enthalten. Zusätzlich wird über das omitempty
definiert, dass das Feld, wenn es leer ist, nicht übertragen werden soll. In der Doku zum JSON Marshaller findet man Dokumentation zu weiteren möglichen JSON Tags, die das Verhalten bei einer JSON Serialisierung steuern.
package model
type Customer struct {
Id int `json:"id,omitempty"`
Firstname string `json:"firstname,omitempty"`
Lastname string `json:"lastname,omitempty"`
}
Die Ablage der Kundendaten übernimmt im Beispiel ein CustomerRepository, das zwei Methoden beinhaltet. Mit der Methode Get
können Daten abgefragt werden, mit Add
können neue Daten aufgenommen werden. Beide Methoden werden im dafür vorgesehenen Interface CustomerRepository
definiert.
type CustomerRepository interface {
Get(id int) *model.Customer
Add(customer *model.Customer)
}
Für die Beispiel-Implementierung wird das ArrayCustomerRepository
umgesetzt, das intern ein Array für die Ablage der Kunden nutzt und das Interface CustomerRepository
implementiert.
type ArrayCustomerRepository struct {
customers []model.Customer
}
Wie im obigen Diagramm gezeigt wird ein Controller implementiert, der das Handler
Interface aus dem net/http Package umsetzt. Somit kann die Controller Implementierung direkt in der http.Handle()
Methode aus dem go http Package verwendet werden. Dazu aber später mehr.
Der Controller muss, damit das Handler
Interface korrekt umgesetzt wird, die ServeHTTP
Methode implementieren. In dieser Methode erfolgt die Auswertung der HTTP Verben (siehe hierzu auch RMM Level 2) und der Aufruf der entsprechenden Business-Logik. Anschließend wird noch der Kunde ins JSON Format gewandelt und ausgegeben.
type CustomerController struct {
repo repo.CustomerRepository
}
func (controller *CustomerController) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
...
if request.Method == "GET" {
...
customer := controller.repo.Get(customerId)
...
json, err := json.Marshal(customer)
...
header := writer.Header()
header.Add("content-type", "application/json")
writer.Write(json)
...
}
}
Im Beispiel wird auf “Go Bordmittel” gesetzt. Die Implementierung des Handler Interfaces geht mit Bibliotheken oder Frameworks wie z. B. http://www.gorillatoolkit.org/ einfacher von der Hand!
Der komplette Controller aus dem Beispiel:
type CustomerController struct {
repo repo.CustomerRepository
}
func NewController(repo repo.CustomerRepository) CustomerController {
return CustomerController{repo: repo}
}
func (controller *CustomerController) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
path := request.URL.Path
idx := strings.LastIndex(path, "/") + 1
customerId, err := strconv.Atoi(path[idx:])
if err != nil {
writer.WriteHeader(400)
writer.Write([]byte("Not found"))
return
}
if request.Method == "GET" {
customer := controller.repo.Get(customerId)
if customer == nil {
writer.WriteHeader(404)
writer.Write([]byte("Not found"))
return
}
json, _ := json.Marshal(customer)
header := writer.Header()
header.Add("content-type", "application/json")
writer.Write(json)
}
if request.Method == "PUT" {
customer := &model.Customer{}
decoder := json.NewDecoder(request.Body)
decoder.Decode(customer)
controller.repo.Add(customer)
writer.WriteHeader(204)
writer.Write([]byte("Not found"))
}
}
Um den Controller über HTTP zur Verfügung zu stellen kann dieser direkt als Parameter bei Aufruf der http.Handle()
Methode verwendet werden. Durch den Aufruf der Methode wird der Handler im sogenannten DefaultServerMux angemeldet und erhält zukünftig die entsprechenden Aufrufe. Beim ersten Parameter der Methode handelt es sich um das Pattern unter dem der Handler angemeldet wird.
ServeMux is an HTTP request multiplexer. It matches the URL of each incoming request against a list of registered patterns and calls the handler for the pattern that most closely matches the URL.
Mit einem http.ListenAndServe(port, nil)
wird dann der Server gestartet.
port := ":8080"
...
log.Printf("Listen on port %s", port)
http.Handle("/customer/", &customerController)
log.Fatal(http.ListenAndServe(port, nil))
Der komplette Code:
package main
import (
"log"
"net/http"
"github.com/kkoehler/golang/jsonrest/repo"
"github.com/kkoehler/golang/jsonrest/controller"
)
func main() {
port := ":8080"
log.Printf("Listen on port %s", port)
repository := repo.ArrayCustomerRepository{}
repository.Init()
customerController := controller.NewController(&repository)
http.Handle("/customer/", &customerController)
log.Fatal(http.ListenAndServe(port, nil))
}
Das komplette Beispiel kann auch in GitHub gefunden werden.
Nach der Umsetzung der eigentlichen Business-Logik soll der Service noch abgesichert werden. Dieser logische Aspekt bzw. Cross-Cutting Concern wird bei Go im http Package über eine sogenannte Middleware umgesetzt. Hierbei handelt es sich, vereinfacht ausgedrückt, um einen Wrapper-Handler, der einfach bei der Anmeldung des Handlers “zwischengeschoben” wird. Man erreicht so etwas wie eine Aspektorientierte Programmierung.
Innerhalb einer Middleware lassen sich die verschiedensten Funktionalitäten umsetzen. Denkbar und sinnvoll sind z. B.:
Die Implementierung einer Middleware kann wie folgt aussehen:
package middleware
import (
"log"
"net/http"
)
func SecurityMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
...
next.ServeHTTP(w, r)
...
})
}
Beim Start des Servers kann nun, anstelle des Ziel Handlers, die SecurityMiddleware
verwendet werden. Diese sollte natürlich den eigentlichen Ziel-Handler als Referenz übergeben bekommen.
customerController := controller.NewController(&repository)
http.Handle("/customer/", middleware.SecurityMiddleware(&customerController))
Wie man sieht lässt sich mit Go auch mit “Bordmitteln” recht einfach ein performanter Rest-basierter WebService implementieren. Der eingebaute HTTP Server gilt auch schon als “production-ready” und unterstützt direkt HTTP/2. Punkte wie SSL oder Connection Konfigurationen solle man sich allerdings sicher noch vor einer Produktivnahme genauer anschauen.
Das komplette Beispiel kann auch in GitHub gefunden werden.
22.02.2019
Der praktische Soforteinstieg für Developer und Softwarearchitekten, die direkt mit Go produktiv werden wollen.
zur Buchseite beim Rheinwerk Verlag Rheinwerk Computing, ISBN 978-3-8362-7559-0 (als PDF, EPUB, MOBI und Papier)
Source Fellows GmbH
Lerchenstraße 31
72762 Reutlingen
Telefon: (0049) 07121 6969 802
E-Mail: info@source-fellows.com