In der Aufzählung oben fehlen SpringBoot, WebComponents
und irgendwas mit Containern oder Cloud.
Also hier ist das Beispielprojekt auf Github.
Die Anwendung mit
git clone git@github.com:gluehloch/springboot-demo.git
und
mvn clean package
bauen. Im Anschluss das gepackte Jar mit
java -jar ./target/restdemo-1.0.jar
starten. Unter der Adresse http://localhost:8080/demo/ping
findet sich
eine Response mit verschiedenen Datumsformaten wieder.
Die Anwendung verwendet Spring-Boot bzw. das eingebaute Jackson für die
Serialisierung/Deserialisierung von Objekten. Über die application.properties
kann das allgemeine Verhalten von Jackson eingestellt werden. Die folgenden Eigenschaften sind dafür zuständig:
spring.jackson.serialization.write_dates_as_timestamps = false
spring.jackson.date-format = yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone = Europe/Berlin
Ich denke, die Schalter oben sind weitestgehend selbsterklärend.
Auf die Timezone/Länderkennzeichen würde ich nicht verzichten.
Auch wenn mir bekannt ist, wo der Server steht und die Konsumenten
vermutlich nur aus Deutschland kommen. Allein dieser Denkansatz
ist diskutabel.
In der Beispielanwendung habe ich mich für obigen Einstellungen entschieden.
Für java.util.Date
erhält man damit die folgende Ausgabe
Z.B. 2019-10-17 18:49:10
. Wer es genauer mag, packt die
Millisekunden mit hinten dran. Genauere Informationen finden sich
in der Klasse PingDateTime
. Dort habe ich verschiedene Datumsformaten
hinterlegt.
Die nächsten Abschnitte gliedern sich in einen Server und einen Client Teil.
Der Server ist mit Java und SpringBoot implementiert. Das Frontend besteht
aus einer HTML und einer Javascript Datei.
Server
RestController:
Hier ein Auszug aus dem Spring-Boot RestController:
@RestController
@RequestMapping("/demo")
public class DateTimeController {
@GetMapping("/ping")
public PingDateTime ping() {
PingDateTime pdt = new PingDateTime();
pdt.setDateTime(new Date());
return pdt;
}
}
Interessant ist die Klasse PingDateTime.
Für die Datum/String Transformation ist
Jackson verantwortlich.
Mit der Annotation @JsonFormat
in der Klasse DateTimeJson
wird der Serialisierungsprozess gesteuert.
Out-of-the-box funktioniert die Annotation nur mit den Java Datentypen java.util.Date
und java.time.LocalDate
bzw. java.time.LocalDateTime
. Für den Joda org.joda.time.DateTime
Datentyp gibt es auf GitHub eine
Extension für Jackson.
Fehlt die Annotation, versucht der Serialisierer Jackson
eine Objekt-Dekonstruktion per Reflection. Im Falle von java.util.Date
werden
die Millisekunden (Unix-Timestamp) zurückgeliefert. In der Spring-Boot Anwendung
wird dateTimeMillies
formatiert ausgegeben trotz fehlender @JsonFormat
Angabe.
Das Joda DateTime Objekt
erfährt bei der Serialisierung eine detailreiche Dekonstruktion, wenn nicht
die oben genannten Extension verwendet wird.
Eine Bemerkung zu dem Kürzel "UTC".
"UTC" ist die sogenannte Weltzeit (Coordinated Universal Time). Diese definiert
keine Zeitzonen und gilt einheitlich auf der ganzen Welt. 'UTC' ist im strengen
Sinne keine Zeitzone. Die Parameterbezeichnung 'timezone' für den Typ 'UTC' ist
also nicht ganz korrekt. Die Verwendung von "UTC" hat dann einen Vorteil,
wenn die Konsumenten in verschiedenen Zeitzonen sitzen.
Das Datumsangaben ohne Zeitzonen keine gute Idee sind, ist auch der Grund,
warum java.util.Date
durch java.time.LocalDateTime
ersetzt werden sollte.
JSON Objekt
Das REST/JSON Modell mit den verschiedenen Formatangaben:
public class PingDateTime {
// -- java.util.Date
@JsonFormat(
shape = JsonFormat.Shape.STRING,
pattern = "dd.MM.yyyy HH:mm",
locale = "de_DE",
timezone = "Europe/Berlin")
private Date dateTimeBerlin;
@JsonFormat(shape =
JsonFormat.Shape.STRING,
pattern = "dd.MM.yyyy HH:mm:ss.SSSZ",
locale = "de_DE",
timezone = "Europe/Berlin")
private Date dateTimeBerlinWithMilli;
@JsonFormat(
shape = JsonFormat.Shape.STRING,
pattern = "dd.MM.yyyy HH:mm",
locale = "de_DE",
timezone = "UTC")
private Date dateTimeUTC;
private Date dateTimeWithoutFormatDefinition;
// -- Joda DateTime
@JsonFormat(
shape = JsonFormat.Shape.STRING,
pattern = "dd.MM.yyyy HH:mm",
locale = "de_DE",
timezone = "Europe/Berlin")
private DateTime jodaDateTimeBerlin;
// -- java.time.LocalDateTime
@JsonFormat(
shape = JsonFormat.Shape.STRING,
pattern = "dd.MM.yyyy HH:mm",
locale = "de_DE",
timezone = "UTC")
private LocalDateTime localDateTimeUTC;
@JsonFormat(
shape = JsonFormat.Shape.STRING,
pattern = "dd.MM.yyyy HH:mm",
locale = "de_DE",
timezone = "Europe/Berlin")
private LocalDateTime localDateTimeBerlin;
// getter and setter ...
/**
* Initialisierung.
*/
public void setDateTime(Date dateTime) {
this.dateTimeUTC = dateTime;
this.dateTimeBerlin = dateTime;
this.dateTimeBerlinWithMilli = dateTime;
this.dateTimeWithoutFormatDefinition = dateTime;
this.localDateTimeUTC =
dateTime.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime();
this.localDateTimeBerlin =
dateTime.toInstant().atZone(ZoneId.of("Europe/Berlin")).toLocalDateTime();
this.jodaDateTimeBerlin = DateTime.now();
}
}
In Projektverzeichnis findet sich die Datei rest.http
. Wer VisualCode mit dem
Rest-Plugin
verwendet, kann sich mit Hilfe dieser Datei die Response bequem im Editor anschauen.
Ebenfalls empfehlenswert ist das Tool httpie. Mit dem
Kommandozeilenaufruf
http http://localhost:8080/demo/ping
erhält man eine nett formatierte Ausgabe
der Response:
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Fri, 15 Nov 2019 18:26:13 GMT
Connection: close
{
"dateTimeBerlin": "15.11.2019 19:26",
"dateTimeUTC": "15.11.2019 18:26",
"dateTimeWithoutFormatDefinition": 1573842373004,
"jodaDateTimeBerlin": "15.11.2019 19:26",
"localDateTimeUTC": "15.11.2019 18:26",
"localDateTimeBerlin": "15.11.2019 19:26"
}
Mit curl würde das natürlich auch funktionieren.
Client
Als Konsumenten für den REST Service habe ich mir eine kleine Javascript
'Anwendung' gebaut, die diesen Servie anzapft und die aktuelle Uhrzeit
darstellt.
Der Client besteht aus zwei Dateien: index.html
und DateTimeController.js
.
In der HTML Seite ist das Element <date-time></date-time>
spannend.
Das ist der Aufhänger für die WebComponent, die in der JS Datei definiert wird.
Kurze Anmerkung: Im Internet Explorer oder im Browser Edge funktioniert das
Beispiel nicht. Siehe canisuse.
WebComponent
In der Datei DateTimeController.js
finden sich zwei Klassen. Einen Controller
für das anzapfen des REST Services auf Server Seite. Interessant ist die
Funktion date()
. Diese definiert ein sogenanntes Promise
. Aufgabe:
Abfrage der REST Schnittstelle und im Erfolgsfall die Response nach JSON
konvertieren oder eine Fehlermeldung in der Konsole ausgeben.
export default class DateTimeController {
constructor() {
this.$dateTime = null; // Hier speichern wir die aktuelle Uhrzeit.
}
date() {
return new Promise((resolve, reject) => {
fetch('./demo/ping').then(response => {
return response.json();
}).then(data => {
resolve(data);
}).catch(err => {
console.error(err);
reject(err);
});
});
}
getCurrentDateTime(callback) {
this.date().then(dateTime => {
this.storeDate(dateTime);
callback(dateTime);
})
}
storeDate(dateTime) {
this.$dateTime = dateTime;
console.log(dateTime);
}
}
Im nächsten Schritt wird ein DOM Element angelegt. Dieses enthält den Button
zur Abfrage der Uhrzeit und ein Ausgabeelement zur Anzeige der selbigen.
const template = document.createElement('template');
template.innerHTML = `<button>Get Time</button><br/><h3>Uhrzeit:</h3><div id="dateTime"></div>`;
Die WebComponent selbst findet sich im nächsten Code-Schnipsel:
class DateTimeElement extends HTMLElement {
constructor() {
super();
this.dateTimeController = new DateTimeController();
this._shadowRoot = this.attachShadow({ 'mode': 'open' });
this._shadowRoot.appendChild(template.content.cloneNode(true));
this.$dateTime = this._shadowRoot.querySelector('#dateTime');
this.$getTimeButton = this._shadowRoot.querySelector('button');
this.$getTimeButton.addEventListener('click', (e) => {
this.getDateTime();
});
}
getDateTime() {
this.dateTimeController.getCurrentDateTime((dateTime) => {
this.$dateTime.innerHTML = dateTime.dateTimeBerlinWithMilli;
});
}
render(dateTime) {
this.$dateTime.innerHTML = dateTime.dateTimeBerlin;
}
connectedCallback() {
console.log('connected!');
}
disconnectedCallback() {
console.log('disconnected!');
}
attributeChangedCallback(name, oldVal, newVal) {
console.log(`Attribute: ${name} changed!`);
}
adoptedCallback() {
console.log('adopted!');
}
}
window.customElements.define('date-time', DateTimeElement);
Fertig.
AngularIO
Zum Abschluss und als Ergänzung: In AngularIO würde ich ein Javascript Date immer mit
dem folgenden Ausdruck 'pipen':
{{model.dateTime | date: 'dd.MM.yyyy HH:mm': '+0000'}}
Referenzen: