spring

Spring


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

Pagination / Sorting

public interface JpaRepository<T, ID> {

  List<T> findAll();
  Page<T> findAll(Pageable pageable);

ebenso bei jeder anderen repo-Methode möglich

@GetMapping("/data")
Page<Dto> getThem(
       @PageableDefault( size = 10, page = 0, 
           sort = "name", direction = Sort.Direction.DESC ) 
       Pageable pageable
     ) {
  return repository
       .findAll(pageable)
       .map(Dto::new);
}
/data?page=4&size=20&sort=name,desc&sort=id,asc

Testing

@SpringBootTest
@AutoConfigureMockMvc
class ApiControllerIntegrationTest {

  @Autowired
  private MockMvc mockMvc;
  
  @Test
  void getting_data_works() throws Exception {
    mockMvc.perform(get("/api/data"))
           .andExpect(jsonPath("$.[*].id", hasItems( ... )));
  }

Controller $\leftrightarrow$ Service $\leftrightarrow$ Repository $\leftrightarrow$ DB


Mocking

@ExtendWith(SpringExtension.class)
@WebMvcTest(ApiController.class)
public class ApiControllerUnitTest {

  @Autowired
  private MockMvc mvc;

  @MockBean
  private DataRepository repository;

  @Test
  void getting_data_works() throws Exception {
    var data = List.of(42, 314);
    
    given(repository.findAll()).willReturn(data);
    
    mockMvc.perform(get("/api/data"))
           .andExpect(jsonPath("$.[*].id", hasItems(42, 314)));
  }
}

Controller $\leftrightarrow$ Service $\leftrightarrow$ 🥸Repository