Ćwiczenia demonstracyjne - Spring Framework

Writing /home/grochu/public_html/wiki/data/meta/zajecia/java/12_spring_exercises.meta failed
Unable to save metadata file. Hint: disk full; file permissions; safe_mode setting.
Writing /home/grochu/public_html/wiki/data/meta/zajecia/java/sidebar.meta failed

REVEAL theme=simple&disableLayout=0&transition=none&controls=1&show_progress_bar=1&build_all_lists=0&show_image_borders=0&horizontal_slide_level=2&enlarge_vertical_slide_headers=0&show_slide_details=1&open_in_new_window=1&size=1024×768

Ćwiczenia praktyczne (hands-on) z rozwiązaniami i wskazówkami.

Ćwiczenie 1: Hello World w Spring Boot

Opis: Utwórz prostą aplikację Spring Boot, która wyświetla „Hello Spring Boot!” w logach i obsługuje żądanie HTTP.

Kroki wykonania:

1. Stwórz nowy projekt Spring Boot (File > New > Project > Spring Boot) 2. Nazwa projektu: HelloSpring 3. Dodaj dependencję: Spring Web 4. Utwórz główną klasę aplikacji 5. Utwórz prosty kontroler REST 6. Uruchom aplikację

Wskazówki: - Użyj Spring Initializr (https://start.spring.io) do wygenerowania projektu - Główna klasa musi być w głównym pakiecie projektu - Kontroler powinien być w podpakiecie controller - Aplikacja domyślnie uruchamia się na porcie 8080

Na co zwrócić uwagę: - Adnotacja @SpringBootApplication jest wymagana w głównej klasie - SpringApplication.run() uruchamia aplikację - Metoda w kontrolerze powinna być dostępna spoza pakietu

Rozwiązanie:

Główna klasa aplikacji (src/main/java/com/example/HelloSpring/Application.java):

package com.example.HelloSpring;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
        System.out.println("Hello Spring Boot!");
    }
}

Kontroler (src/main/java/com/example/HelloSpring/controller/GreetController.java):

package com.example.HelloSpring.controller;
 
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class GreetController {
 
    @GetMapping("/hello")
    public String hello() {
        return "Hello Spring Boot!";
    }
 
    @GetMapping("/greet")
    public String greet() {
        return "Witaj w Spring Boot!";
    }
}

Testowanie: - Uruchom aplikację (Shift + F10 w IntelliJ IDEA) - Otwórz przeglądarkę i wejdź na: http://localhost:8080/hello - Powinna wyświetlić się odpowiedź: Hello Spring Boot!

Ćwiczenie 2: Wstrzykiwanie zależności (Dependency Injection)

Opis: Utwórz aplikację demonstrującą wstrzykiwanie zależności - serwis biznesowy będzie wstrzykiwany do kontrolera.

Kroki wykonania:

1. Stwórz interfejs UserService i jego implementację 2. Oznacz implementację adnotacją @Service 3. Wstrzyknij serwis do kontrolera przez konstruktor 4. Utwórz endpoint, który korzysta z serwisu

Wskazówki: - Wstrzykiwanie przez konstruktor jest najbezpieczniejsze - Interfejsy pomagają w testowaniu i rozszerzaniu kodu - Spring automatycznie tworzy instancję i wstrzykuje zależność

Na co zwrócić uwagę: - Klasa implementująca interfejs musi być zaadnotowana @Service - Konstruktor musi być publiczny - Spring szuka beanów oznaczonych @Service, @Component, @Repository

Rozwiązanie:

Interfejs (service/UserService.java):

package com.example.HelloSpring.service;
 
public interface UserService {
    String getUserInfo(String username);
    String getGreeting(String name);
}

Implementacja (service/UserServiceImpl.java):

package com.example.HelloSpring.service;
 
import org.springframework.stereotype.Service;
 
@Service
public class UserServiceImpl implements UserService {
 
    @Override
    public String getUserInfo(String username) {
        return "Informacje o użytkowniku: " + username;
    }
 
    @Override
    public String getGreeting(String name) {
        return "Cześć " + name + ", witaj w aplikacji!";
    }
}

Kontroler (controller/UserController.java):

package com.example.HelloSpring.controller;
 
import com.example.HelloSpring.service.UserService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class UserController {
 
    private final UserService userService;
 
    // Wstrzykiwanie zależności przez konstruktor
    public UserController(UserService userService) {
        this.userService = userService;
    }
 
    @GetMapping("/user/{username}")
    public String getUserInfo(@PathVariable String username) {
        return userService.getUserInfo(username);
    }
 
    @GetMapping("/greet/{name}")
    public String greet(@PathVariable String name) {
        return userService.getGreeting(name);
    }
}

Testowanie: - http://localhost:8080/user/john → Informacje o użytkowniku: john - http://localhost:8080/greet/Anna → Cześć Anna, witaj w aplikacji!

Ćwiczenie 3: REST API - CRUD dla zasobu User

Opis: Utwórz REST API do zarządzania użytkownikami (Create, Read, Update, Delete). Dane przechowuj w pamięci (HashMap).

Kroki wykonania:

1. Utwórz klasę User (model danych) 2. Utwórz serwis UserRepository (zarządzanie użytkownikami) 3. Utwórz kontroler REST z metodami CRUD 4. Mapuj żądania HTTP na operacje CRUD

Wskazówki: - Użyj HashMap<Long, User> do przechowywania użytkowników - Każdy użytkownik powinien mieć unikalny ID - Zwracaj odpowiednie kody HTTP (200, 201, 404, 204)

Na co zwrócić uwagę: - @PostMapping tworzy zasób - @GetMapping pobiera zasób - @PutMapping aktualizuje zasób - @DeleteMapping usuwa zasób - @PathVariable pobiera parametr ze ścieżki

Rozwiązanie:

Model (model/User.java):

package com.example.HelloSpring.model;
 
public class User {
    private Long id;
    private String name;
    private String email;
 
    public User() {}
 
    public User(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
 
    // Gettery i settery
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
}

Repozytorium (service/UserRepository.java):

package com.example.HelloSpring.service;
 
import com.example.HelloSpring.model.User;
import org.springframework.stereotype.Repository;
import java.util.*;
 
@Repository
public class UserRepository {
    private final Map<Long, User> users = new HashMap<>();
    private Long nextId = 1L;
 
    public User save(User user) {
        if (user.getId() == null) {
            user.setId(nextId++);
        }
        users.put(user.getId(), user);
        return user;
    }
 
    public Optional<User> findById(Long id) {
        return Optional.ofNullable(users.get(id));
    }
 
    public List<User> findAll() {
        return new ArrayList<>(users.values());
    }
 
    public void delete(Long id) {
        users.remove(id);
    }
}

Kontroler REST (controller/UserRestController.java):

package com.example.HelloSpring.controller;
 
import com.example.HelloSpring.model.User;
import com.example.HelloSpring.service.UserRepository;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
 
@RestController
@RequestMapping("/api/users")
public class UserRestController {
 
    private final UserRepository userRepository;
 
    public UserRestController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
 
    @GetMapping
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }
 
    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        return userRepository.findById(id)
            .map(user -> new ResponseEntity<>(user, HttpStatus.OK))
            .orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
    }
 
    @PostMapping
    public ResponseEntity<User> createUser(@RequestBody User user) {
        User saved = userRepository.save(user);
        return new ResponseEntity<>(saved, HttpStatus.CREATED);
    }
 
    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(
            @PathVariable Long id,
            @RequestBody User userDetails) {
        return userRepository.findById(id)
            .map(user -> {
                user.setName(userDetails.getName());
                user.setEmail(userDetails.getEmail());
                userRepository.save(user);
                return new ResponseEntity<>(user, HttpStatus.OK);
            })
            .orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
    }
 
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userRepository.delete(id);
        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }
}

Testowanie (przy użyciu Postman lub curl):

Tworzenie nowego użytkownika:

POST /api/users
Content-Type: application/json
 
{
  "name": "Jan Kowalski",
  "email": "jan@example.com"
}

Pobranie wszystkich użytkowników:

GET /api/users

Pobranie konkretnego użytkownika:

GET /api/users/1

Aktualizacja użytkownika:

PUT /api/users/1
Content-Type: application/json
 
{
  "name": "Jan Nowak",
  "email": "jan.nowak@example.com"
}

Usunięcie użytkownika:

DELETE /api/users/1

Ćwiczenie 4: Obsługa błędów - Global Exception Handler

Opis: Utwórz globalny handler wyjątków, aby przechwytywać błędy i zwracać jednolite odpowiedzi błędów.

Kroki wykonania:

1. Utwórz wyjątek ResourceNotFoundException 2. Utwórz klasę @ControllerAdvice do globalnej obsługi wyjątków 3. Definiuj metody @ExceptionHandler dla różnych typów wyjątków 4. Zwracaj odpowiednie kody HTTP i wiadomości błędów

Wskazówki: - @ControllerAdvice obsługuje wyjątki z całej aplikacji - @ExceptionHandler mapuje konkretny typ wyjątku na metodę obsługi - Zwracaj ResponseEntity z odpowiednim kodem HTTP

Na co zwrócić uwagę: - Ustal porządek handlersów (od bardziej szczegółowych do ogólnych) - Zwracaj informacyjne wiadomości błędów - Obsługuj wyjątki walidacji i wyjątki biznesowe oddzielnie

Rozwiązanie:

Wyjątek (exception/ResourceNotFoundException.java):

package com.example.HelloSpring.exception;
 
public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

Global Exception Handler (exception/GlobalExceptionHandler.java):

package com.example.HelloSpring.exception;
 
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.Map;
 
@ControllerAdvice
public class GlobalExceptionHandler {
 
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<Object> handleResourceNotFound(
            ResourceNotFoundException ex) {
 
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("status", HttpStatus.NOT_FOUND.value());
        body.put("error", "Zasób nie znaleziony");
        body.put("message", ex.getMessage());
 
        return new ResponseEntity<>(body, HttpStatus.NOT_FOUND);
    }
 
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<Object> handleIllegalArgument(
            IllegalArgumentException ex) {
 
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("status", HttpStatus.BAD_REQUEST.value());
        body.put("error", "Nieprawidłowy argument");
        body.put("message", ex.getMessage());
 
        return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
    }
 
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object> handleGlobalException(Exception ex) {
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
        body.put("error", "Błąd serwera");
        body.put("message", "Wewnętrzny błąd serwera");
 
        return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

Modyfikacja repozytorium do rzucania wyjątków:

@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
    return userRepository.findById(id)
        .map(user -> new ResponseEntity<>(user, HttpStatus.OK))
        .orElseThrow(() -> 
            new ResourceNotFoundException("Użytkownik o ID " + id + " nie znaleziony")
        );
}

Testowanie: - Pobierz użytkownika z nieistniejącym ID: GET /api/users/999 - Powinien zwrócić kod 404 i szczegółową wiadomość błędu

Ćwiczenie 5: Spring Data JPA - Praca z bazą danych

Opis: Zintegruj aplikację z bazą danych H2 (w pamięci) i użyj Spring Data JPA do operacji CRUD.

Kroki wykonania:

1. Dodaj dependencje: Spring Data JPA i H2 Database 2. Utwórz encję User 3. Utwórz repozytorium rozszerzające JpaRepository 4. Skonfiguruj połączenie do bazy danych 5. Użyj repozytorium w serwisie

Wskazówki: - H2 to baza danych w pamięci, idealna do testów - @Entity oznacza klasę jako encję - @Id i @GeneratedValue obsługują klucz główny - Spring Data JPA generuje SQL automatycznie

Na co zwrócić uwagę: - Encja musi mieć konstruktor bezargumentowy - Klucz główny musi być oznaczony @Id - Nazwy pól powinny odpowiadać kolumnom w bazie

Rozwiązanie:

Plik pom.xml - dodaj dependencje:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

Encja (entity/User.java):

package com.example.HelloSpring.entity;
 
import jakarta.persistence.*;
 
@Entity
@Table(name = "users")
public class User {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
 
    @Column(nullable = false)
    private String name;
 
    @Column(nullable = false, unique = true)
    private String email;
 
    public User() {}
 
    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }
 
    // Gettery i settery
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
}

Repozytorium (repository/UserRepository.java):

package com.example.HelloSpring.repository;
 
import com.example.HelloSpring.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
 
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
}

Plik application.properties:

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true

Serwis (service/UserService.java):

package com.example.HelloSpring.service;
 
import com.example.HelloSpring.entity.User;
import com.example.HelloSpring.repository.UserRepository;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
 
@Service
public class UserService {
    private final UserRepository userRepository;
 
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
 
    public User save(User user) {
        return userRepository.save(user);
    }
 
    public Optional<User> findById(Long id) {
        return userRepository.findById(id);
    }
 
    public List<User> findAll() {
        return userRepository.findAll();
    }
 
    public void delete(Long id) {
        userRepository.deleteById(id);
    }
}

Testowanie: - Uruchom aplikację - Wejdź na http://localhost:8080/h2-console (konsola H2) - Utwórz nowych użytkowników: POST /api/users - Sprawdź w konsoli H2, czy dane zostały zapisane w bazie

Ćwiczenie 6: Spring Security - Autentykacja i autoryzacja

Opis: Dodaj zabezpieczenia do aplikacji - autentykacja użytkowników i kontrola dostępu do endpointów.

Kroki wykonania:

1. Dodaj dependencję Spring Security 2. Utwórz klasę konfiguracji zabezpieczeń 3. Zdefiniuj domyślnego użytkownika 4. Zabezpiecz endpointy na podstawie ról 5. Przetestuj dostęp z autentykacją

Wskazówki: - Spring Security domyślnie chroni wszystkie endpointy - Definiuj reguły w metodzie configure() lub filterChain() - Użyj @PreAuthorize do weryfikacji uprawnień na metodzie - Domyślny użytkownik to user

Na co zwrócić uwagę: - Hasło generowane jest losowo przy uruchomieniu (sprawdź w logach) - Można definiować własnych użytkowników w kodzie - CSRF jest domyślnie włączony dla POST/PUT/DELETE

Rozwiązanie:

Plik pom.xml - dodaj dependencję:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Konfiguracja (config/SecurityConfig.java):

package com.example.HelloSpring.config;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
 
@Configuration
@EnableWebSecurity
public class SecurityConfig {
 
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
 
    @Bean
    public UserDetailsService userDetailsService(PasswordEncoder encoder) {
        UserDetails admin = User.builder()
            .username("admin")
            .password(encoder.encode("admin123"))
            .roles("ADMIN")
            .build();
 
        UserDetails user = User.builder()
            .username("user")
            .password(encoder.encode("user123"))
            .roles("USER")
            .build();
 
        return new InMemoryUserDetailsManager(admin, user);
    }
 
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) 
            throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/admin/**")
                    .hasRole("ADMIN")
                .requestMatchers("/api/users/**")
                    .hasAnyRole("USER", "ADMIN")
                .requestMatchers("/", "/hello")
                    .permitAll()
                .anyRequest()
                    .authenticated())
            .httpBasic()
            .and()
            .csrf().disable();
 
        return http.build();
    }
}

Modyfikacja kontrolera z @PreAuthorize:

@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
    userRepository.deleteById(id);
    return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}

Testowanie: - Bez autentykacji: dostęp do GET /api/users zwróci 401 Unauthorized - Z autentykacją Basic (user:user123): dostęp będzie pozwolony - DELETE /api/users/{id} dostęp tylko dla admin:admin123

Podsumowanie ćwiczeń

Ćwiczenia obejmują:

  • Podstawową konfigurację aplikacji Spring Boot
  • Wstrzykiwanie zależności i zarządzanie beanami
  • REST API i operacje CRUD
  • Obsługę błędów i wyjątków
  • Integrację z bazą danych (JPA)
  • Zabezpieczenia (Spring Security)

Po każdym ćwiczeniu:

  • Eksperymentuj z kodem
  • Dodaj logowanie do debugowania
  • Przetestuj różne scenariusze
  • Obserwuj komunikaty błędów