Spring
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, 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