spring

Spring


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/PATCH

        PUT http://www.appdomain.com/users/123
        Content-Type: application/json
{resource}
Resource im RequestBody unter Request-URI ersetzen/updaten

Status Codes

200 - OK
400 - Bad Request
Request fehlerhaft, später sicher Fehler
Bestellt 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, 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"
  }
}

Best practice: Dto

@Entity
public class School {

  @Id
  @GeneratedValue
  private Integer id;
    
  @NotBlank
  private String name;

  @OneToMany(mappedBy = "school")
  private Collection<Student> students;
public record SchoolDto(int id, String name) { }

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