Czy w moim przypadku walidacja danych wejściowych powinna odbywać się na wielu poziomach ?

0

Cześć, mój kod ma za zadanie pobrać współrzędne dla lokalizacji podanej przez użytkownika i zapisanie ich w bazie danych. Nie będę wrzucał całego kodu oto najważniejsze części:

1.ApiClient

@Component
@Slf4j
public class LocationApiClient {
    private final String API_KEY;
    private final Duration timeout = Duration.ofSeconds(5);
    private final WebClient webClient;
    @Autowired
    public LocationApiClient(@Qualifier("location") WebClient webClient,
                             @Value("${WEATHER_API_KEY}") String API_KEY) {
        this.webClient = webClient;
        this.API_KEY = API_KEY;
    }

    public LocationRequest fetchGeocodingData(String locationName) {
        try {
            return webClient.get()
                    .uri(uriBuilder -> uriBuilder
                            .queryParam("q", locationName)
                            .queryParam("appid", API_KEY)
                            .build())
                    .exchangeToFlux(clientResponse -> {
                        HttpStatusCode status = clientResponse.statusCode();
                        if (status.equals(HttpStatus.TOO_MANY_REQUESTS)) {
                            log.error("Exceeded number of allowed calls to Geocoding API. Please try again later: {}",
                                    clientResponse.statusCode());
                            return Flux.error(new TooManyRequestsException(
                                    "Exceeded number of allowed calls to Geocoding API. Please try again later."));
                        } else if (status.equals(HttpStatus.UNAUTHORIZED)) {
                            log.error("Invalid authorization token");
                            return Flux.error(new AuthorizationException("Invalid authorization token"));
                        } else if (status.equals(HttpStatus.SERVICE_UNAVAILABLE)) {
                            log.error("Server is unavailable");
                            return Flux.error(new ServerIsUnavailable(
                                    "Failed to fetch geocoding data. Please try again later"));
                        }
                        return clientResponse.bodyToFlux(LocationRequest.class);
                    })
                    .next()
                    .timeout(timeout)
                    .block();
        } catch (Exception e) {
            log.error("An error occurred while fetching geocoding data: {}", e.getMessage());
            throw new LocationClientException("An error occurred while fetching geocoding data", e);
        }
    }
}

2.Service który zapisuje współrzędne w bazie danych

@Service
@Slf4j
public class LocationServiceImpl implements LocationService {

    private final LocationDAO locationDAO;
    private final LocationApiClient locationApiClient;

    @Autowired
    public LocationServiceImpl(LocationDAO locationDAO, LocationApiClient locationApiClient) {
        this.locationDAO = locationDAO;
        this.locationApiClient = locationApiClient;
    }

    @Override
     public LocationRequest fetchAndSaveCoordinates(String locationName) {
        try {
            LocationRequest locationRequest = locationApiClient.fetchGeocodingData(locationName);
            validateLocationRequest(locationRequest);
            saveLocationIfNotExist(locationName, locationRequest);
            return locationRequest;
        } catch (LocationNotFound exception) {
            throw exception;
        } catch (Exception ex) {
            log.error("An unexpected error occurred", ex);
            throw new LocationServiceException("An unexpected error occurred", ex);
        }
    }
    private void validateLocationRequest(LocationRequest locationRequest) {
        if (locationRequest == null) {
            throw new LocationNotFound("The specified location could not be found in our data source.");
        }
    }

    private void saveLocationIfNotExist(String locationName, LocationRequest locationRequest) {
        Location existingLocation = locationDAO.findLocationByLocationName(locationName);
        if (existingLocation == null) {
            saveLocation(locationRequest);
        }
    }
    private void saveLocation(LocationRequest locationRequest) {
        Location location = mapToLocationEntity(locationRequest);
        locationDAO.save(location);
        log.info("Successfully saved the location {}", location.getLocationName());
    }
    private Location mapToLocationEntity(LocationRequest locationRequest) {
        Location location = new Location();
        location.setLocationName(locationRequest.locationName());
        location.setLatitude(locationRequest.latitude());
        location.setLongitude(locationRequest.longitude());
        location.setState(locationRequest.state());
        location.setCountry(locationRequest.country());
        return location;
    }
}

3.Controller

@RestController
@RequestMapping("/api/location/coordinates/")
public class LocationController {
    private LocationService locationService;

    @Autowired
    public LocationController(LocationService locationService) {
        this.locationService = locationService;
    }

    @GetMapping
    public void handleErrorWhenLocationIsNotGiven() {
        throw new LocationNotGiven("Location name cannot be null or empty. First you need to specify your location.");
    }
    @GetMapping("{locationName}")
    public LocationRequest fetchLocationCoordinates(
            @PathVariable String locationName) throws IOException {
        return locationService.fetchAndSaveCoordinates(locationName);
    }
}


Chciałem rzucić wyjątek w chwili, gdy użytkownik nie wprowadzi żadnych danych, a będzie chciał wyszukać współrzędne.
W tym celu stworzyłem w controllerze mapowanie

@GetMapping
    public void handleErrorWhenLocationIsNotGiven() {
        throw new LocationNotGiven("Location name cannot be null or empty. First you need to specify your location.");
    }

Czy w pozostałych warstwach(service i apiClient), również powinienem sprawdzać i testować kod na wypadek, gdyby wejście użytkownika było puste ?
Mi się wydaje, że nie, bo w sytuacji, kiedy użytkownik będzie próbował wyszukać współrzędne nie wprowadzając nic, controller przekieruje go na mapowanie GetMapping i rzuci wyjątek. Ale dopiero się uczę i wolę zapytać kogoś bardziej doświadczonego.

Z góry dzięki za pomoc.

1

Ja się nie znam, ale na moje to front powinien sprawdzać czy dane są podane. No, ale jak już musisz, to sprawdzaj najbliżej responsu, jeśli tam masz puste, to wszędzie indziej nagle się nie zmaterializują te dane

1

Podstawowa walidacja czyli czy pole jest puste, czy string ma np określoną długość, czy wartość jest oczekiwanego typu (bool, int, string, itd) powinno odbywać się jak najbliżej klienta. Większość frameworków webowych ma gotowe komponenty do tego + istnieją zewnętrzne biblioteki.

Poniżej warstwy API walidujemy reguły biznesowe. Żeby zobrazować czym jest reguła biznesowa to przytoczę szkolny przykład związany z transferem pieniędzy i sprawdzenie czy na koncie źródłowym jest wystarczająca ilość środków, albo czy na stanie magazynowym jest wystarczająca ilość produktów żeby zrealizować zamówienie.

2
Ornstein napisał(a):

3.Controller
```java
@RestController
@RequestMapping("/api/location/coordinates/")
public class LocationController {
    private LocationService locationService;

    @Autowired
    public LocationController(LocationService locationService) {
        this.locationService = locationService;
    }

    @GetMapping
    public void handleErrorWhenLocationIsNotGiven() {
        throw new LocationNotGiven("Location name cannot be null or empty. First you need to specify your location.");
    }
    @GetMapping("{locationName}")
    public LocationRequest fetchLocationCoordinates(
            @PathVariable String locationName) throws IOException {
        return locationService.fetchAndSaveCoordinates(locationName);
    }
}


Chciałem rzucić wyjątek w chwili, gdy użytkownik nie wprowadzi żadnych danych, a będzie chciał wyszukać współrzędne.
W tym celu stworzyłem w controllerze mapowanie

@GetMapping
    public void handleErrorWhenLocationIsNotGiven() {
        throw new LocationNotGiven("Location name cannot be null or empty. First you need to specify your location.");
    }

Po pierwsze jeżeli używasz @PathVariable, to ono jest obowiązkowe out-of-box. Tam ma właściwość required, która domyślnie jest true. Nie ma więc potrzeby tworzyć dodatkowego mapowania. Należy jedynie stworzyć odpowiednią implementację ResponseEntityExceptionHandler.handleMissingPathVariable, która będzie ogarniać brak parametru.
Inne podejście to ustawienie @PathVarieble(required=false) lub zmiana typu na Optional<String> i następnie ręczna obsługa, przez wyrzucenie odpowiedniego wyjątku „byznesowego” i obsługę tego w @ExceptionHandler.

0

Poczytaj o faul fast. Dzisiaj serwis wołasz tylko z kontrolera, jutro będziesz wołać z innego serwisu i już będzie problem.

Ta historia ma 3 części ale myślę że dwie następne znajdziesz samemu:
https://www.bitc.com.pl/post/fail-fast-or-die-part-i?utm_source=pocket_mylist

0

@Dregorio: dzięki za radę.

@markone_dev: fajnie wyjaśnione, dzięki.

@Koziołek: Skorzystam z tego drugiego podejścia, które opisałeś. To pierwsze wydaje się bardziej skomplikowane.

@RequiredNickname: Nigdy o tym nie słyszałem. Przeczytałem tę historię i wygląda na to, że stosowanie tej techniki daje ogrom benefitów. Powiedziałbym, że jest to taki "must have". Muszę zacząć ją wdrażać w moje projekty. Dzięki za wskazówkę.

0

API które wywołujesz w żaden sposób nie definiuje kontraktu API z jakąś specyfikacją OpenAPI lub chociaż technicznego kontraktu danych z JSON schema? Jeżeli, nie to sam sobie stwórz JSON schema z odpowiedzi API, wygeneruj klasy z jsonschema2pojo i dostaniesz walidację za darmo. Z tego co pamiętam to możesz zdefiniować pola jako 'final' także zminimalizujesz ryzyko, że ktoś będzie majstrował przy tym obiekcie po deserializacji.

EDIT: Teraz doczytałem, że chodzi Ci o walidację danych wejściowych z Controllera. Tutaj sytuacja jest taka sama, tyle, że to Ty musisz zdefiniować specyfikację OpenAPI lub chociaż kontrakt danych z JSON schema i dostaniesz tę walidacje za darmo.

EDIT2: Nie ma to znaczenia czy front robi walidację czy nie. Ty ją musisz też zrobić. W pewiem sposob to podchodzi pod 'defence in depth'. Jak juz masz te dane wewnatrz backendu to nie musisz robic walidacji we wszystkich warstwach. Jak już upierasz się na walidacje ręczna to znajdziesz takich którzy Ci powiedzą, że powinna odbyć się w 'service' poniewaz HTTP Controller może się zmienić na coś innego i znowu będziesz musiał robić walidacje.

0

Ten "serwis" nie ma praktycznie żądnej logiki.

1

@Riddle: Głównie zależało mi, aby oddzielić logikę łączenia się z API i pobierania danych, a zapisywania ich w bazie danych.

1
Ornstein napisał(a):

@Riddle: Głównie zależało mi, aby oddzielić logikę łączenia się z API i pobierania danych, a zapisywania ich w bazie danych.

To czy w takim razie czy validateLocationRequest() nie powinno być w kliencie, a saveLocationIfNotExist w dao?

1

Masz rację, ma to zdecydowanie większy sens. Nie wpadłbym na to w najbliższym czasie. Dzięki za wskazówkę.

1 użytkowników online, w tym zalogowanych: 0, gości: 1