rest

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

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

rest_evolution

Hypermedia as the Engine of Application State


Spring


Inversion of Control

library vs framework


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

IoC-Container

  • Implementierung von ApplicationContext
  • managed konfigurierte Objekte(beans)

@Configuration
public class AppConfig {

    @Bean
    public MyBean myBean() {
        // instantiate, configure and return bean ...
    }
}

IntelliJ

new project


dependencies


@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 @Bean suchen
@ComponentScan
In Subpackages (rest.*) @Component suchen

@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 überschreibt equals/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
  • StudentRepository wird injected
  • CommandLineRunner wird 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 hal_model


{
  "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);
}