REST
REpresentational State Transfer
- Client-Server Trennung
- Stateless
- Kurze Request-Response Kommunikation
- Einheitliches Interface
- URI - Uniform Resource Identifier
- Resourcen getrennt von ihrer Repräsentation
- Cachebar
- Zugriff üblicherweise über HTTP
HTTP

URL - Uniform Resource Locator
protocol://hostname:port/path?field=value
Request
GET http://www.htlstp.ac.at/logo
Accept: image/webp, image/apng, image/*, */*;q=0.8
POST http://www.htlstp.ac.at/api/teachers/
Content-Type: application/json
{
"name": "SCRE",
"dept": "IF"
}
Response
HTTP/1.1 200 OK
Server: nginx
Date: Mon, 18 May 2020 18:08:47 GMT
Content-Type: image/png
Content-Length: 4663
Connection: keep-alive
Expires: Mon, 25 May 2020 04:35:30 +0200
PNG
IHDR
N£%É;%ÚCl³Úí:º0ºRî`Ýze++¡·1Úqõ.¥ôBëm7¹·îd%á
...Request Methoden
- GET
GET http://www.appdomain.com/users GET http://www.appdomain.com/users/123 GET http://www.appdomain.com/users?size=20&page=5- lädt die angegebene Resource
- sollte bei jedem Aufruf dasselbe Resultat liefern
- DELETE
DELETE http://www.appdomain.com/users/123- löscht die angegebene Resource
- POST
POST http://www.appdomain.com/users Content-Type: application/json
{resource}- Resource im RequestBody speichern
- Response sollte URI der angelegten Resource sein
- PUT
PUT http://www.appdomain.com/users/123 Content-Type: application/json
{resource}- Resource im RequestBody unter Request-URI updaten/speichern
Status Codes
- 200 - OK
- 400 - Bad Request
- Request fehlerhaft, später sicher Fehler
- Besellt Kebab bei McDonalds
- 500 - Internal Server Error
- Request ok, Serverfehler, später vielleicht Erfolg
- Bestellt Pommes, die sind noch nicht fertig
- 201 - Created
- 204 - No Content
- nach DELETE
- 404 - Not Found
Evolution

Hypermedia as the Engine of Application State
Spring
Inversion of Control

- Spring Boot
- Erledigt Setup
- Spring Core
- Inversion of Control
- Dependency Lookup/Injection
- Spring Web
- Spring Data JPA
Inversion of Control / Dependency Injection
public class TextEditor {
private SpellChecker checker;
public TextEditor() {
this.checker = new EnglishSpellChecker();
}
}
"new is glue"
public class TextEditor {
private SpellChecker checker;
public TextEditor(SpellChecker checker) {
this.checker = checker;
}
}
DI - Container

- Implementierung von
ApplicationContext - managed konfigurierte Objekte(beans)
@Configuration
public class AppConfig {
@Bean
public MyBean myBean() {
// instantiate, configure and return bean ...
}
}IntelliJ


@SpringBootApplication
package rest;
@SpringBootApplication
public class App {
public static void main(String[] args) {
var appContext = SpringApplication.run(App.class, args);
}@EnableAutoConfiguration- Baut den IoC-Container auf etc.
@Configuration- In dieser Klasse
@Beansuchen @ComponentScan- In Subpackages (
rest.*)@Componentsuchen
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
@Bean
void printHelloWorld() {
System.out.println("Hello World");
}
}@Component
class MyComponent {
public MyComponent() {
System.out.println("Constructing Component");
}
}
Constructing Component
Hello World
Beans
@RequestScope
@Component
public class Logger {
private final Path log;
public Logger(Path logDirectory) throws IOException {
log = Files.createTempFile(logDirectory, "request", "log");
}
@PreDestroy
private void deleteFile() throws IOException {
Files.delete(log);
}
}
@PreDestroy
private void preDestroy() {
this.close();
this.shutdown();
}
Scope
@Scope("singleton")- default
- eine Instanz pro
ApplicationContext @Scope("prototype")- eine Instanz pro Injection
@RequestScope- eine Instanz pro HTTP Request
@SessionScope- eine Instanz pro HTTP Session
Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@Entity
public class Student extends AbstractPersistable<Long>
implements Serializable {
@NotBlank
private String name;
@Past
@NotNull
private LocalDate dateOfBirth;
}
AbstractPersistableüberschreibtequals/hashCode- id in Entity definieren für mehr Kontrolle
Repository
@Entity
public class Student {
@Id
@GeneratedValue
private Integer id;
protected Student() {
}
}
public interface StudentRepository extends
JpaRepository<Student, Integer> {
@Repository // Alias für @Component
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID>
implements JpaRepositoryImplementation<T, ID>
Zur Laufzeit wird eine Klasse von SimpleJpaRepository abgeleitet und injected
public interface StudentRepository
extends JpaRepository<Student, Integer> {
Student findStudentByName(String name);
Stream<Student> findAllByNameContaining(String substring);
@Query("select s from Student s where s.id = 1")
Student findFirstStudent();
}CommandLineRunner
@Configuration
public class DatabaseSetup {
@Bean
CommandLineRunner saveStudents(StudentRepository repository) {
return args -> {
repository.save(new Student("Alfred", 1));
repository.save(new Student("Bernd", 2));
};
}
}- Bean wird bei der Konfiguration erzeugt
StudentRepositorywird injectedCommandLineRunnerwird ausgeführt
Controller
@Controller
@RequestMapping("path")
public class MyController {
@GetMapping("/entity")
public HttpEntity<Student> responseEntity() {
return new ResponseEntity<>(new Student("", 0), HttpStatus.OK);
}
}
@Controller- Alias für
@Component - Kontrolliert Http Requests
@RequestMapping- alle HTTP Methoden an www.server.com/path
@GetMapping("/entity")
public HttpEntity<Student> one() {
return new ResponseEntity<>(STUDENT, HttpStatus.OK);
}
@GetMapping- GET-Requests an /path/entity
HttpEntity<Body>- kapselt HTTP Response und Status-Code
@ResponseBody
@GetMapping("/body")
public Student one() {
return STUDENT;
}
@ResponseBody- returnter Wert ist Response-Body
@Controller
@RequestMapping("path")
@ResponseBody // alle Methoden
public class RestController {
@RestController // @Controller + @ResponseBody
@RequestMapping("api")
public class StudentRestController {
private final StudentRepository repository;
public StudentRestController(StudentRepository repository) {
this.repository = repository;
}
@GetMapping("/students")
List<Student> all() {
return repository.findAll();
}
}
GET Collection
@GetMapping("/students")
List<Student> all() {
return repository.findAll();
}
Response Code: 200 OK
[
{
"id": 1,
"name": "Alfred",
"number": 1
},
{
"id": 2,
"name": "Bernd",
"number": 2
}
]
@GetMapping("/students")
List<Student> findByName(
@RequestParam(
name="name",
required=true) // default, bei false -> null
// Alternativ: Optional
String name) {
return repository.findByName();
}
GET /students?name=Alfred
[
{
"id": 1,
"name": "Alfred",
"number": 1
}
]
GET one
@GetMapping("/students/{id}")
Student one(@PathVariable Integer id) {
return repository.findById(id)
.orElseThrow(() -> new StudentNotFoundException(id));
}
Response Code: 200 OK
{
"id": 1,
"name": "Alfred",
"number": 1
}
Response Code: 500 Internal Server Error
{
"timestamp": "2020-05-20T19:27:41.773+0000",
"status": 500,
"error": "Internal Server Error",
"message": "Could not find Student 404",
"path": "/api/students/404"
}
Exceptions
@GetMapping("/students/{id}")
Student one(@PathVariable Integer id) {
return repository.findById(id)
.orElseThrow(() -> new StudentNotFoundException(id));
}
@ResponseBody
@ExceptionHandler(StudentNotFoundException.class)
ProblemDetail handleStudentNotFound(StudentNotFoundException ex) {
return ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
}
Response code: 404 Not Found
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"instance": "/api/students/404"
}
besser gesammelt außerhalb des Controllers
Advice
@ResponseBody
@ControllerAdvice
public class StudentRestAdvice {
@ExceptionHandler(StudentNotFoundException.class)
ProblemDetail handleStudentNotFound(StudentNotFoundException ex) {
return ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
}
}
@Controller + @ResponseBody = @RestController
@RestControllerAdvice
public class StudentRestAdvice
POST
@PostMapping("/students")
ResponseEntity<Student> newStudent(@RequestBody Student student) {
Student saved = repository.save(student);
return new ResponseEntity<>(saved, HttpStatus.CREATED);
}
POST /students
{
"name": "Cäsar",
"number": 3
}
Response code: 201 Created
{
"id": 3,
"name": "Cäsar",
"number": 3
}
Constraint Violation
@PostMapping("/students")
ResponseEntity<Student> newStudent(@RequestBody Student student) {
Student saved = repository.save(student);
return new ResponseEntity<>(saved, HttpStatus.CREATED);
}
@Entity
public class Student {
@NotNull
private String name;
POST /students
{
"number": 400
}
Response code: 500 Internal Server Error
An internal Server Error occurred.
@Valid
@PostMapping("/students")
ResponseEntity<Student> newStudent(@Valid @RequestBody Student student) {
Student saved = repository.save(student);
return new ResponseEntity<>(saved, HttpStatus.CREATED);
}
- Validierung beim Unmarshalling
- wirft
MethodArgumentNotValidException
@ExceptionHandler({MethodArgumentNotValidException.class,
StudentValidationException.class})
ProblemDetail handleValidationErrors(Exception e) {
return ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST, e.getMessage());
}
Location
@PostMapping("/students")
ResponseEntity<Student> newStudent(@Valid @RequestBody Student student) {
Student saved = repository.save(student);
URI uri = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.build(saved.getId());
return ResponseEntity
.created(uri)
.body(saved);
}
Response code: 201 Created
Location: /students/3
{
"id": 3,
"name": "Cäsar",
"number": 3
}
PUT
@PutMapping("/students/{id}")
ResponseEntity<Student> replaceStudent(
@Valid @RequestBody Student newStudent, @PathVariable Integer id) {
Student toSave = repository.findById(id)
.map(student -> {
student.setName(newStudent.getName());
student.setNumber(newStudent.getNumber());
return student;
}).orElseGet(() -> {
newStudent.setId(id);
return newStudent;
});
Student saved = trySave(toSave);
boolean newStudentCreated = toSave == newStudent;
if (newStudentCreated) {
URI uri = getCreatedUri(saved);
return ResponseEntity.created(uri).body(saved);
} else
return ResponseEntity.ok(saved);
}
Update
PUT /students/2
Content-Type: application/json
{
"name": "Brigitte",
"number": 42
}
Response code: 200 OK
{
"id": 2,
"name": "Brigitte", // vorher Bernd
"number": 42
}
INSERT
PUT http://localhost:8080/api/students/201
Content-Type: application/json
{
"name": "Newly created",
"number": 201
}
Response code: 201 Created
Location: /students/3 // != 201
{
"id": 3,
"name": "Newly created",
"number": 201
}
DELETE
@ResponseStatus(HttpStatus.NO_CONTENT)
@DeleteMapping("/students/{id}")
void deleteStudent(@PathVariable Integer id) {
try {
repository.deleteById(id);
} catch (DataAccessException e) {
throw new StudentNotFoundException(id, e);
}
}
DELETE /students/2
Response code: 204 No Content
<Response body is empty>
Beziehungen
@Entity
public class Student {
@ManyToOne
private School school;
GET /students/4
Response code: 200 OK
{
"id": 4,
"name": "HTL Student",
"number": 100,
"school": {
"id": 3,
"name": "HTL",
"students": [
{
"id": 4,
"name": "HTL Student",
"number": 100,
"school": {
...
@Entity
public class Student {
@ManyToOne
private School school;
@Entity
public class School {
@OneToMany(mappedBy = "school")
@JsonIgnore
private Collection<Student> students;
GET /students/4
Response code: 200 OK
{
"id": 4,
"name": "HTL Student",
"number": 100,
"school": {
"id": 3,
"name": "HTL"
}
}
RPC
- Client muss für jede Ressource URI wissen
- keine Contextabhängigkeiten
- statisch
HAL
Hypertext Application Language

{
"id": 4,
"name": "HTL Student",
"number": 100,
"school": {
"id": 3,
"name": "HTL"
},
"_links": {
"self": {
"href": "http://localhost:8080/api/students/4"
},
"students": {
"href": "http://localhost:8080/api/students"
},
"school": {
"href": "http://localhost:8080/api/schools/3"
}
}
}
HATEOAS
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>GET
@GetMapping("/students/{id}")
Student one(@PathVariable Integer id) {
return repository.findById(id)
.orElseThrow(() -> new StudentNotFoundException(id));
}
@GetMapping("/students/{id}")
EntityModel<Student> one(@PathVariable Integer id) {
var student = repository.findById(id)
.orElseThrow(() -> new StudentNotFoundException(id));
return EntityModel.of(student,
linkTo(methodOn(StudentHateosRestController.class).one(id))
.withSelfRel(),
linkTo(methodOn(StudentHateosRestController.class).all())
.withRel("students"));
}
@GetMapping("/students/{id}")
public EntityModel<Student> one(@PathVariable Integer id) {
var student = repository.findById(id)
.orElseThrow(() -> new StudentNotFoundException(id));
return assembler.toModel(student);
}
@Component
public class StudentModelAssembler implements
RepresentationModelAssembler<Student, EntityModel<Student>> {
@Override
public EntityModel<Student> toModel(Student student) {
return EntityModel.of(student,
linkTo(methodOn(StudentHateoasRestController.class)
.one(student.getId())).withSelfRel(),
linkTo(methodOn(StudentHateoasRestController.class)
.all()).withRel("students"));
}
}
POST
@PostMapping("/students")
ResponseEntity<EntityModel<Student>> newStudent(
@Valid @RequestBody Student student) {
Student saved;
saved = trySave(student);
var studentModel = assembler.toModel(saved);
return ResponseEntity
.created(studentModel
.getRequiredLink(IanaLinkRelations.SELF).toUri())
.body(studentModel);
}