Przez ostatnie miesiące brałem udział w wielu rozmowach rekrutacyjnych na stanowisko
Java Software Developera. Wiele z pytań, które się pojawiły lub pojawiały najczęściej, udało mi się zapamiętać i zapisać. Dzisiaj chciałbym podzielić się z Tobą tymi pytaniami.
Jeżeli właśnie przygotowujesz się do rozmów na podobne stanowisko mam nadzięję, że
poniższy zestaw pytań będzie dla Ciebie wartościowy a odpowiedzi, które przygotowałem w jakimś stopniu uzupełnią Twoją wiedzę.

1. W jakich projektach uczestniczyłeś? Jakich technologii używałeś?

Standardowe pytanie. Tutaj pozostawiam pole do popisu Tobie :)

2. Jak działa JIT? Czy ma jakieś wady?

Program napisany z wykorzystaniem języka Java jest kompilowany do kodu bajtowego, który jest zrozumiały dla maszyny wirtualnej Javy. JVM wykonując program interpretuje kolejne instrukcje kodu bajtowego konwertując je na natywne instrukcje maszyny, na której jest uruchomiona. Ponieważ interpretacja jest wolna w porównaniu do wykonywania kodu natywnego twórcy wprowadzili do środowiska uruchomieniowego JITa.

JIT (Just-In-Time compiler) jest komponentem maszyny wirtualnej Javy, który poprawia wydajność aplikacji stosując różnego rodzaju optymalizacje np. null check elimination, branch prediction, zagnieżdżanie metod (method inlining), eliminacja martwego kodu (dead code elimination), kompilacja do kodu natywnego i wiele innych.

Dla każdej z metod JVM zlicza ilość jej wywołań. Kiedy ilość wywołań danej metody osiągnie pewien próg (domyślnie 2000) metoda a w zasadzie jej bytecode jest przekazywany do kompilatora JIT, który jest odpowiedzialny za wykonanie na niej zestawu faz/optymalizacji, o których wspomniałem wcześniej w tym na końcu kompilacji do kodu natywnego. Skompilowany kod natywny jest zapisywany w przestrzeni procesu JVM w tzw. code cache.

JIT po starcie aplikacji zaczyna ją profilować analizując nasz kod i zbierając różnego rodzaju statystyki. Ma to wpływ na wydajność naszej aplikacji gdyż JVM przeznacza sporo zasobów na ten proces szczególnie na początku gdy aplikacja działa w trybie w pełni interpretowanym bez żadnych optymalizacji co może być jedną z wad JIT'a. Proces po starcie nazywamy "rozgrzewaniem" (warm up). Warto wziąć ten proces pod uwagę gdy badamy naszą aplikację pod kątem wydajnościowym i pamiętać, że wyniki mogą się różnić od tych, które zaobserwujemy na środowisku produkcyjnym.

3. Do czego służy adnotacja @PostConstruct?

Adnotacja @PostConstruct pochodzi z specyfikacji JSR 250 (Java Specification Request). Spring implementuje tą specyfikację tym samym zapewniając działanie tej adnotacji.

Adnotację @PostConstruct umieszczamy nad metodą klasy, która jest beanem tj. komponentem zarządzalnym przez Springa. Spring wywoła tą metodę jako callback po poprawnym zainicjalizowaniu naszego beana tj. po poprawnym wstrzyknięciu zależności dla tego komponentu. Wspomniany callback może być dobrym miejscem na różnego rodzaju post inicjalizację.

Kontrakt/Specyfikacja zapewnia, że ta metoda zostanie wywołana tylko raz w trakcie cyklu życia tego beana. Jeżeli chodzi o syganturę metody powinna być typu void, niestatyczna, bezparametrowa o dowolnym poziomie widoczności (public, protected, package private or private).

Warto zaznaczyć, że używanie @PostConstruct ma sens tylko wtedy gdy używamy wstrzykiwania typu field injection lub setter injection. Jeżeli używamy wstrzykiwania przez konstruktor czyli rekomendowanego przeze mnie podejścia inicjalizację możemy wykonać w konstruktorze. Dzięki temu łatwiej jest przetestować taki komponent, który możemy stworzyć poprawnie tylko i wyłącznie z wykorzystaniem konstruktora bez względu na to czy robimy to w testach czy w trakcie stawiania całego kontekstu springowego bez bawienia się w mechanizmy refleksji.

W praktyce o adnotacji @PostConstruct powinniśmy zapomnieć na rzecz wstrzykiwania przez konstruktor ewentualnie zostawiając ją sobie jako alternatywe dla kodu legacy.

4. Wymień sposoby wstrzykiwania zależności. Jakiego sposobu używasz i dlaczego?

Wstrzykiwanie zależności (Dependency Injection) jest wzorcem projektowym, który polega na przekazywaniu do obiektu gotowych instacji obiektów (zależności), z których ten obiekt się składa tak aby zapewnić swoją funkcjonalność w przeciwieństwie do tworzenia ich samodzielnie. Podejście to ma wiele zalet m.in. obiekt, który dostaje zależności z zewnątrz (np. z kontenera springowego) nie jest uzależniony od konkretnej implementacji a więc łatwo można przekonfigurować aplikację lub podmienić implementację w testach.

Wyróżniamy następujące sposoby wstrzykiwania zależności:

  • field injection
  • setter injection
  • constructor injection

Preferuję ostatnie podejście czyli wstrzykiwanie przez konstruktor. Podejście to pozwala w łatwy sposób na skonfigurowanie obiektu pod testy bez wykorzystywania mechanizmów refleksji. Mamy również pewność, że obiekt stworzony przez new zawsze zwróci nam poprawny i spójny obiekt gotowy do działania w przeciwieństwie do field injection / setter injection gdzie łatwo jest się „nadziać” na NullPointerException. Konstruktor pełni także funkcję dokumentacyjną klasy gdyż wiemy dokładnie co jest potrzebne aby poprawnie skonfigurować obiekt czego nie ma np. w przypadku setter injection gdzie musielibyśmy się zastanawiać jakich setterow wystarczy użyć aby obiekt był w spójnym stanie. Wadą używania setterów jest również to, że łamiemy zasadę hermetyzacji przez co łatwo w niekontrolowany sposób popsuć stan obiektu.

Kolejnym argumentem na rzecz constructor injection jest to, że mamy kontrolę nad ilością zależności. Przykładowo dodanie 5 zależności do konstruktora już może zniechęcać - szybko widzimy jak ich liczba się rozrasta więc możemy dzięki temu szybciej podjąć decyzję w stronę refactoringu i stworzeniu osobnej klasy odpowiedzialnej za daną podfunkcjonalność. W przypadku field injection dodanie kolejnej zależności jest banalnie proste i przyjemne, liczba zależności szybko rośnie przez co klasa może z czasem być trudna w utrzymaniu.

5. Co to jest hermetyzacja?

Hermetyzacja (lub enkapsulacja) polega na ukrywaniu implementacji czyli składowych lub metod obiektu, z których się składa. Hermetyzacja nadaje obiektowi charakter czarnej skrzynki co jest kluczowe dla koncepcji wielokrotnego użycia kodu jak i jego niezawodności. Oznacza to, że sposób przechowywania danych w klasie może się diametralnie zmienić, ale dopóki udostępnia ona te same metody do manipulacji danymi żaden obiekt nie zostanie tym dotknięty. Jeżeli stan obiektu zmieni się mimo, że nie wywołano na jego rzecz żadnej metody oznacza to, że została złamana zasada hermetyzacji.

6. Jakie rzeczy weszły od Java 11?

Pozwolę sobie wymienić tylko niektóre nowe funkcjonalności. Po komplet odsyłam do tego artykułu.

  • nowe metody w klasie String: isBlank, lines, strip, stripLeading, stripTrailing, repeat
  • nowe statyczne metody w klasie Files: readString, writeString
  • nowy wydajniejszy HTTP client z wsparciem HTTP/1.1, HTTP/1.2, Web Socket

7. Jak napisałbyś metodę kontrolera do usunięcia wielu użytkowników?

Jednym ze sposobów na rozwiązanie tego zadania może być przekazanie listy identyfikatorów do usunięcia w następujący sposób:

@DeleteMapping("/users/{userIds}")
public void deleteUsers(@PathVariable List<Long> userIds) {...}

8. Mikroserwisy vs Monolity

Mikroserwisy są sposobem na projektowanie aplikacji jako niezależnie wdrażanych usług w przeciwieństwie do monolitów, których wszystkie funkcjonalności są wdrażane i uruchamiane w jednym procesie aplikacyjnym.

Pojedyńczy mikroserwis jest zazwyczaj odpowiedzialny za jakąś pojedyńczą funkcjonalność/obszar, który trzeba dostarczyć dla biznesu.

W monolicie realizacja konkretnego przypadku biznesowego jest wykonywana przez zbiór obiektów/serwisów komunikujących się pomiędzy sobą, żyjących w obrębie tego samego procesu. Natomiast w architekturze mikroserwisowej na jeden proces biznesowy może być zaangażowanych wiele mikroserwisów komunikujących się pomiędzy sobą używając takich technologii jak np. HTTP REST, JMS, gRCP.

Architektura w oparciu o mikroserwisy ma wiele zalet szczególnie w przypadku dużych złożonych systemów m.in.:

  • skalowalność

Poszczególne komponenty mogą być skalowalne niezależnie od pozostałych. Jest to niesamowita zaleta. Możemy mieć komponent, który intensywnie wykorzystuje CPU np. jest odpowiedzialny za przetwarzanie obrazów w związku z tym wdrażamy taki mikroserwis na instancje zoptymalizowane pod kątem CPU np. Amazon EC2 Compute Optimized Instances. Inny moduł może być bazą danych in-memory więc wdrożymy go na instancje zoptymalizowane pod pamięć np. Amazon EC2 Memory Optimized instances. W przypadku monolitu nie mamy takich możliwości, musimy pójść na kompromis jeżeli chodzi o hardware. W miarę jak monolit rośnie musimy skalować wertykalnie czyli dokładać zasobów sprzętowych tj. CPU, RAM a jeżeli chcemy aby aplikacja obsłużyła większy ruch skalując horyzontalnie skalujemy cały duży monolit mimo, że np. część usług jest wykorzystywana rzadko i tego nie wymaga co jest jednocześnie trudne i kosztowne.

  • niezawodność

Kolejny problem w przypadku monolitu jest taki, że wszystkie moduły naszej aplikacji są częścią jednego procesu aplikacji tzn, że problem powstały w jednym module np. wyciek pamięci może wpłynąć na działanie całego procesu co w efekcie może skutkować położeniem całej aplikacji i niedostępnością całego systemu. Mikroserwisy nie mają tego problemu ze względu na to, że są to niezależnie wdrażane komponenty uruchamiane jako niezależne procesy w systemie operacyjnym.

  • dowolność technologii (polyglot)

Każdy element może być tworzony w technologii najlepiej dostosowanej do funkcjonalności jaką ma realizować zgodnie z "use the right tool for the right job" np. z wykorzystaniem technologii Java i Spring wystawimy usługi restowe, Pythona użyjemy do Machine Learning a C/C++ do napisania aplikacji typu video-streaming.

  • modularność

Małe komponenty pozwalają na łatwe wdrożenie i rozbudowę bez konieczności ingerencji w całą aplikację. W monolicie jedna mała zmiana mogła powodować redeploy całego systemu, który jak możemy się spodziewać był długim i żmudnym procesem budowania, wykonywania się testów jednostkowych i integracyjnych oraz innych kroków co mogło skutkować także niedostępnością całego systemu przez jakiś czas. W mikroserwisach małe zmiany są prostsze do zrealizowania i wdrożenia. Dodatkowo łatwo podzielić takie zmiany pomiędzy zespołami developerskimi w przeciwieństwie do monolitu gdzie praca wielu osób nad jednym olbrzymim komponentem jest ciężka i trudna w utrzymaniu.

Jak ze wszystkim oczywiście architektura w oparciu o mikroserwisy ma również wady i należy przemyśleć czy będzie miała zastosowanie w naszym systemie m.in:

  • złożoność

Ze względu na to, że aplikacja składa się z wielu serwisów komunikujących się między sobą musimy zapewnić aby komuniacja pomiędzy nimi zachodziła w bezpieczny sposób dodatkowo dochodzi nam również obsługa różnych błędów głównie sieciowych związanych z naszą infrastrukturą. W przypadku monolitu nie mieliśmy tego problemu ponieważ wszystkie funkcjonalności były w środku więc na wywołanie lokalne metody nie musieliśmy się specjalnie zabezpieczać.

Testowanie w środowisku rozproszonym, zarządzanie infrastrukturą również jest wyzwaniem i narzutem na złożoność.

  • zapewnienie spójności danych

W przypadku monolitu zapewnienie spójności było relatywnie proste gdyż mieliśmy lokalną transakcję ACID, która zapewniała nam spójność i atomowość na poziomie bazy danych. W przypadku architektury mikroserwisowej gdzie mamy do czynienia z systemem rozproszonym nie jest tak łatwo i w zależności od potrzeb musimy posiłkować się wzorcami typu Saga Pattern i Eventual Consistency.

9. Co wiesz o SOLID, IoC, Dependency Injection?

SOLID opisuje pięc podstawowych zasad projektowania w programowaniu obiektowym.

S - Single Responsibility Principle
Każda klasa powinna być skoncetrowana tylko na jednym zadaniu, mieć tylko jeden powód do zmiany.

O - Open/Closed Principle
Klasy powinny być otwarte na rozszerzenia i zamknięte na modyfikacje. Przy nowych wymaganiach kod nie powinien być modyfikowany ale dodawany nowy, który rozszerza funkcjonalność.

L - Liskov Substitution Principle
Klasa pochodna powinna być możliwa do użycia w miejsce klasy nadrzędnej zachowując się w taki sam sposób bez modyfikacji. Klasa pochodna nie ma wpływu na zachowanie się klasy nadrzędnej.

I - Interface Segregation Principle
Wiele dedykowanych interfejsów jest lepsze niż jeden ogólny.

D - Dependency Inversion Principle
Wysokopoziomowe moduły nie powinny zależeć od modułów niskopoziomowych - zależności między nimi powinny wynikać z abstrakcji. Kod powinien zależeć od rzeczy „stabilnych” tj. abstrakcji, które się nie zmieniają.

IoC (Inversion of Control) jest wzrorcem architektury polegającym na odwróceniu sterowania programem i zrzuceniu tego na framework np. Spring. Implementacją tego wzorca może być np. Dependency Injection lub Aspect Oriented Programming.

10. Dlaczego BigDecimal jest lepszy od double do przechowywania wartości pieniężnych? Dlaczego double nie jest precyzyjny?

Typ double nie jest precyzyjny w kontekście obliczeń finansowych ponieważ obliczenia wykonywane są w systemie binarnym zgodnie z standardem IEEE 754 w przeciwieństwie do BigDecimal, którego obliczenia działają na systemie dziesiętnym. W związku z tym nie jesteśmy w stanie wyrazić dokładnie pewnych wartości w systemie binarnym a jedynie pewną aproksymację tej wartości np. 0.09999999999999998 zamiast 0.1. Jeżeli chodzi o wartości pieniężne chcemy podczas obliczeń dokładnie odwzorować rzeczywistość tj. nie chcemy sytuacji, w której np. na koncie bankowym klienta jest kwota mniejsza niż to by wynikało z dokonanych transakcji. Z pomocą przychodzi nam typ BigDecimal w języku Java, który pozwala nam kontrolować precyzję i zapewnić dokładność obliczeń.

11. Do czego służy BindingResult w Springu?

BindingResult jest interfejsem, który definiuje w jaki sposób zbieramy wyniki walidacji obiektu, który walidujemy. Domyślnie spring rzuci wyjątkiem jeżeli dane, które walidujemy nie są poprawne. Dzięki BindingResult możemy mieć dostęp do wyniku walidacji przeprowadzonej przez framework i ręcznie zdecydować co dalej np:

@PostMapping("/register")
public ResponseEntity<?> register(@Valid @RequestBody RegisterRequest request, BindingResult, result) {
if (result.hasErrors()) {
return ResponseEntity.badRequest().body(result.toString());
}
registerService.register(request);
return ResponseEntity.ok().build();
}

12. Wymień poziomy izolacji transakcji i wyjaśnij jak działa jeden z nich?

Wyróżniamy następujące poziomy izolacji transakcji:

  • READ_UNCOMMITTED
  • READ_COMMITTED
  • REPEATABLE_READ
  • SERIALIZABLE

READ_COMMITTED - transakcja z ustawionym takim trybem izolacji będzie „widziała” tylko zacommitowane zmiany co oznacza, że ponowne odczyty mogą zwracać różne wyniki z uwagi na inne transakcje, które wykonują się w tym samym czasie. Aby zapewnić indempotentność na poziomie odczytu (pojedyńczego rekordu) transakcji tj. aby każdy kolejny odczyt zwracał ten sam wynik możemy zwiększyć poziom izolacji na REPEATABLE_READ. Warto zaznaczyć, że mimo ustawienia REPETABLE_READ może wystąpić tzw. phantom read, w którym pomiędzy odczytami w jednej transakcji inne transakcje dodają/usuwają rekordy w tym samym czasie co prowadzi do różnych rezultatów na poziomie odczytu. Jeżeli chcemy uodpornić się na phantom read możemy ustawić jeszcze bardziej restrykcyjny poziom izolacji tj. SERIALIZABLE jednak z pewnością wpłynie to negatywnie na wydajność.

13. Jakie właściwości ma adnotacja @Transactional? Co można ustawić?

Adnotacja @Transactional posiada m.in. następujące właściwości:

  • isolation
    poziom izolacji transakcji; domyślnie przyjmuję wartość Isolation.DEFAULT czyli poziom izolacji będzie domyślny, zdeterminowany przez bazę danych, która jest „pod maską”
  • rollbackFor
    określa występowanie jakich wyjątków powinno powodować wycofanie transakcji; domyślnie transakcja zostanie zrollbackowana gdy wystąpi jeden z wyjątków typu Error lub RuntimeException, checked exceptions domyślnie nie powodują wycofania transakcji
  • noRollbackFor
    określa występowanie jakich wyjątków nie powinno powodować wycofania transakcji
  • propagation
    typ propagacji domyślnie Propagation.REQUIRED
  • timeout
    timeout dla transakcji w sekundach, domyślnie timeout po stronie aplikacji jest wyłączony
  • readOnly
    flaga określająca czy transakcja jest tylko do odczytu
  • transactionManager
    nazwa beana odpowiedzialnego za zarządzanie transakcjami (możemy mieć wiele transaction managerow w jednej aplikacji)

14. Jak walidować przychodzące requesty REST w Springu? Jaki błąd wystąpi gdy podamy stringa zamiast inta?

W aplikacji spring boot'owej gdy używamy zależności spring-boot-starter-web domyślnym formatem serializacji/deserializacji modelu transportowego jest json a biblioteka do tego to jackson. Gdy podamy łańcuch znaków zamiast typu integer powstanie błąd formatu czyli tak naprawdę błąd deserializacji. Jackson zwróci błąd parsowania, objawi się to wyjątkiem typu InvalidFormatException.

Walidację modelu transportowego możemy wykonać na różne sposoby.
Warto zaznaczyć, że walidację możemy podzielić na 3 poziomy:

  • walidacja formatu
    Sprawdzamy czy komunikat poprawnie się parsuje. Zwykle nie musimy ingerować w tą można powiedzieć niskopoziomową walidację. Biblioteka, której używamy powinna być za to odpowiedzialna tj. w przykładzie wyżej.
  • walidacja struktury
    Sprawdzamy czy już przeparsowany komunikat jest zgodny z naszymi oczekiwaniami np. czy adres email jest poprawny, hasło jest w odpowiednim formacie itd. Nie bieżemy pod uwagę aktualnego stanu systemu.
  • walidacja spójności
    Sprawdzamy czy komunikat/komenda może być poprawnie wykonana biorąc pod uwagę aktualny stan systemu. Przykładowo nie możemy zarejestrować 2x użytkownika na ten sam email.

Jeżeli chodzi o walidację struktury możemy posiłkować się biblioteką hibernate-validator (implementacja standardu JSR 380). W Spring boot aby dodać wsparcie dla tego typu walidacji możemy wykorzystać spring-boot-starter-validation. Spring wspiera adnotację @NotNull, @NotBlank, @Min, @Max itd. w związku z tym obok request body w kontrolerach restowych możemy użyć adnotacji @Valid aby dany model był automatycznie zwalidowany.

Możemy również ręcznie walidować komunikaty w odpowiednio przygotowanych do tego klasach jeżeli np. mamy jakieś specyficzne wymagania lub po prostu chcemy mieć pełną kontrolę i preferujemy taki sposób na walidację.

15. Jak byś zabezpieczył api restowe?

Api restowe możemy zabezpieczyć na wiele sposobów. Jednym z nich może być autoryzacja z wykorzystaniem tokena JWT (JSON Web Token). Natomiast cała komunikacja powinna być zabezpieczona z wykorzystaniem protokołu TLS.

16. Jak przechowywać hasła w bazie danych? Jak działa Bcrypt?

Rekomendowanym podejściem do bezpiecznego przechowywania haseł w bazie danych jest użycie jednokierunkowej funkcji hashującej z solą. Jednym z algorytmów implementujących to podejście jest bcrypt. Bcrypt jest funkcją hashującą dedykowaną do przechowywania haseł w bezpieczny sposób opartą o szyfr Blowish. Bcrypt na podstawie hasła lub innego dowolnego ciągu zwróci nam 60 znakowy hash hasła. Budowa wyjściowego skrótu wygląda następująco:

$2[wersja]$[koszt]$[sól][hash]

gdzie:

wersja - identyfikator algorytmu, domyślnie 'a' czyli bcrypt
koszt - liczba z przedziału 04-99 określająca tzw. work factor algorytmu, domyślnie 12 czyli 2^12 = 4096 rund, każde zwiększenie współczynnika o jeden zwiększa dwukrotnie czas obliczeń
sól - losowy ciąg o wielkości 22 znaków
hash - hash o wielkości 31 znaków

Przykład hasha dla hasła 'java':
$2a$12$1gUpfSyUB.OaegFGSe/3rOrLiuZCGauzK2nAqVWaBCA7vRWfLAC2a

Bcrypt jest uznawany za skuteczny i bezpieczny algorytm. Sól pełni rolę mitygacji przed atakami z wykorzystaniem tęczowych tablic a odpowiednio ustawiony „work factor” algorytmu powoduje, że czas łamania hasła znacząco się wydłuża z uwagi na złożoność obliczeniową, która rośnie wykładniczo w związku z tym łamanie metodą siłową przestaje się opłacać.

Więcej na temat bezpieczeństwa haseł możemy przeczytać tutaj.

17. Jakie znasz zalety / wady stream api w Java?

Zalety:
- zwięzłość i czytelność szczególnie w połączeniu z referencjami do metod
- elastyczność, możemy bardzo łatwo zmieniać kolejność kroków w algorytmie
- dzięki parallelStream() możemy w prosty sposób zrównoleglić operacje na danych

Wady:
- utrudnia debugowanie, chociaż IDE coraz lepiej sobie z tym radzą
- wyrażenia (lambda) w map/filter itd. nie mogą rzucać tzw. checked exceptions co może być problematyczne np. jak przetwarzamy I/O

18. Jak działa protokół HTTP? Jakie są metody?

HTTP (Hypertext Transfer Protocol) jest protokołem warstwy aplikacji, który definiuje komunikację pomiędzy przeglądarką a serwerem HTTP. W tym modelu serwer HTTP jest odpowiedzialny za obsługę żądań pochodzących od klienta - przeważnie przeglądarki internetowej. Klient jest inicjatorem połączenia a więc wszelkie zasoby, skrypty, obrazki są przesyłane przez serwer HTTP do klienta w ramach odpowiedzi na żądanie.

Wspomniałem o tym, że klientem jest przeważnie przeglądarka internetowa natomiast w praktyce mogą to być również inne aplikacje działające w imieniu użytkownika np. urządzenie mobilne albo inny system, który się z nami integruje. Rodzaj „narzędzia”, które komunikuje się z serwerem jest zdefinowany przez nagłówek User-Agent.

Warto wspomnieć, że protokół HTTP jest protokołem bezstanowym co oznacza, że każde żądanie do serwera jest interpretowane oddzielnie i nie ma pomiędzy nimi logicznego powiązania. Połączenie TCP, na bazie którego opiera się ten protokół zwykle jest zamykane i nie jest utrzymywane chyba, że ze względów wydajnościowych chcemy utrzymać połączenie możemy „powiedzieć” o tym serwerowi używając nagłówka Connection: keep-alive (HTTP Persistent Connection).

Komunikaty (żądania i odpowiedzi) w protokole HTTP zbudowane są z nagłówków czyli metadanych oraz payload'u (lub body), który zawiera odpowiedź, o którą klient żądał, może to być np. strona www. Każde żądanie zawiera również tzw. metodę HTTP, która definiuje akcję jaką chcemy wykonać w ramach danego zasobu.

Wyróżniamy następujące podstawowe metody HTTP:

GET - pobranie lub odczytanie zasobu, żądanie tego typu nie powinno zmieniać stanu na serwerze w kontekście danego zasobu.

HEAD - żądanie podobne do GET z takim wyjątkiem, że nie zwraca body. Użyteczne gdy chcemy sprawdzić tylko metadane/nagłówki zasobu bez narzutu na transfer np. możemy sprawdzić czy dany plik istnieje i jaki ma rozmiar bez jego pobierania.

POST - wysłanie danych na serwer, zwykle gdy wypełniamy jakiś formularz korzystamy z tej metody aby przesłać wypełnione dane na serwer po czym serwer tworzy nowy zasób.

PUT - wysyłanie danych na serwer w celu aktualizacji istniejącego zasobu.

DELETE - usunięcie istniejącego zasobu.

PATCH - zmiana jakiejś części istniejącego zasobu np. zmiana hasła użytkownika.

Więcej o protokole HTTP można przeczytać tutaj.

19. Jak wygląda model pamięci w Java? Jakie znasz Garbage Collectory? Jakie mają zastosowanie?

Maszyna wirtualna Javy jest zwykłym procesem w systemie operacyjnym, który tj. każdy inny proces potrzebuje pamięci do funkcjonowania. Aby programy napisane w Java funkcjonowały w poprawny sposób wirtualna maszyna dzieli pamięć na różne obszary, które mają odpowiednie zastosowanie:

  • heap (sterta)
    Pamięć wykorzystywana do przechowywania obiektów, które tworzymy w naszej aplikacji. Utworzone obiekty żyją dopóki ich używamy tj. mamy wskazanie do nich przez referencje. Kiedy na dany obiekt nie wskazuje już żadna referencja obiekt jest niszczony przez algorytm Garbage Collectora, który jest odpowiedzialny za zwalnianie pamięci przez obiekty, które już nie są używane.

    Heap również możemy podzielić na 2 obszary tzw. Young Generation (który dzieli się również na Eden, Survivor0 i Survivor1) i Old Generation. W Java istnieje tzw. hipoteza generacyjna, która mówi, że tzw. „młode obiekty” żyją krócej niż stare tj. istnieje większe prawdopodobieństwo, że obiekt, który przeżył kilka lub kilkanaście cykli GC będzie żył dalej niż obiekt, który dopiero co został stworzony. Dlatego też obiekty, które „swoje przeżyły” przerzucane są do Old Generation. Podział ten pozwala na dostosowanie odpowiednich algorytmów Garbage Collectora do danego obszaru pamięci np. dla przestrzeni Young Generation będziemy chcieli częściej uruchomić skanowanie GC niż dla Old Generation.
  • non-heap
    Natywna pamięć maszyny wirtualnej. W tej przestrzeni zapisywane są obiekty potrzebne maszynie wirtualnej do poprawnego funkcjonowania. Wyróżniamy m.in. takie obszary jak:

    - metaspace
    Zawiera m.in. kod naszej aplikacji, constant poole, dane pól i metod

    - code cache
    Przestrzeń m.in. do zapamiętywania kodu natywnego przez JITa, o którym wspomniałem wcześniej

    - stos dla wątków

Garbage collectory w JVM używane są do odśmiecania pamięci czyli zwalniania nieużywanych obiektów. Dzięki temu nie musimy ręcznie zarządzać pamięcią tj. w językach C/C++, możemy zrzucić tę odpowiedzialność na maszynę wirtualną w tym przypadku na konkretny algorytm Garbage Collectora. Wyróżniamy m.in. następujące algorytmy GC:

  • Serial
  • Parallel
  • Concurrent Mark Sweep
  • G1

Domyślnie od Javy 9 używany jest algorytm G1. Mamy możliwość zmiany algorytmu przekazując odpowiednie parametry do JVM podczas uruchamiania aplikacji np:

java -XX:+UseParallelGC -jar Application.java

20. Jakie znasz typy ataków webowych? Wymień i objaśnij jeden z nich.

Możemy wyróżnić m.in. takie ataki jak XSS (Cross-site scripting), SQLI (SQL Injection), Path Traversal, DDoS (Distributed Denial of Service), SSRF (Server-Side Request Forgery).

Atak SQLI polega na zmanipulowaniu wejścia (np. nazwy użytkownika lub frazy wyszukiwania) w taki sposób aby silnik bazodanowy podczas parsowania tego zapytania zinterpretował je inaczej niż twórca aplikacji założył tj. zmieniając to zapytanie. Atakujący mając kontrolę nad zapytaniem zyskuje dostęp do danych w bazie danych, do których nie powinien mieć dostępu.

Aby nasza aplikacja nie była podatna na ten typ ataku powinniśmy zastosować tzw. prepared statement. Baza danych wtedy parsuje samo zapytanie oddzielnie kolejno wstrzykując podane wartości w odpowiednie miejsca w zapytaniu co nie tylko poprawia bezpieczeństwo ale też wydajność gdyż zapytanie nie musi być wielokrotnie parsowane przez silnik bazodanowy.

Gratulacje dotrwałeś do końca! Jeżeli ten artykuł był dla Ciebie wartościowy podziel się nim z innymi