Spring w najkrótszej definicji to wielka mapa obiektów. Poprzez wstrzykiwanie zależności framework zajmuje się tworzeniem grafu obiektów za nas. Całość zarządzana jest przez ApplicationContext
, który jest sercem aplikacji.
Dzisiaj artykuł będący odpowiedzią na pytanie zadane mi przez jednego z czytelników.
Jak uruchamiana jest aplikacja w Springu? Czym tak naprawdę są i w jakiej kolejności są inicjalizowane
ApplicationContext
iWebApplicationContext
? Czym się różni@ContextConfiguration
od@SpringBootAplication
? Jak się ma do tego@WebMvcTest
,@EnableWebMvc
i@SpringBootTest
?
Zacznijmy więc po kolei.
Application Context #
W momencie startu aplikacji Springa tworzony jest jej kontekst - ApplicationContext
. To w nim rejestrowane są beany, które później używane są przez framework to tworzenia grafu obiektów. Każda aplikacja Springa posiada własny kontekst, minimum (i zazwyczaj) jeden.
Spójrzmy na poniższy kod aplikacji w Spring Boocie.
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(MyApplication.class, args);
UsersRepository repository = context.getBean(UsersRepository.class);
}
}
W metodzie main
możemy dostać się do kontekstu aplikacji i wykonywać na nim różne operacje - na przykład pobierać zarejestrowane beany.
Zazwyczaj nie musimy bezpośrednio operować z klasą ApplicationContext
, ale warto wiedzieć, że ona tam pod spodem jest i zarządza obiektami w naszej aplikacji.
WebApplicationContext #
WebApplicationContext
oznacza z kolei kontekst aplikacji Springa w środowisku webowym. Mamy z nim do czynienia w momencie, gdy korzystamy z modułów webowych oraz servletów.
Wówczas domyślny kontekst aplikacyjny zostaje zastąpiony webowym, który jest po prostu jego rozszerzeniem. Dodaje on metodę getServletContext()
, która zwraca… ServletContext
;)
package org.springframework.web.context;
public interface WebApplicationContext extends ApplicationContext {
ServletContext getServletContext();
}
ServletContext natomiast zajmuje się całą obsługą żądań HTTP, które trafiają do aplikacji i sprawia, że aplikacja Springowa może z łatwością pisać, czytać z sieci i obsługiwać wszystkie związane z tym zagadnienia.
@SpringBootApplication #
Co robi adnotacja @SpringBootApplication
? Niech przemówi dokumentacja:
This is a convenience annotation that is equivalent to declaring
@Configuration
,@EnableAutoConfiguration
@ComponentScan
.
Jest niczym innym jak aliasem nad kilka innych adnotacji.
W skrócie - oznaczenie głównej klasy naszej aplikacji adnotacją @SpringBootApplication
skraca kod i korzystając z podejścia convention over configuration przyśpiesza proces pisania aplikacji.
Jeśli powyższe adnotacje nie są dla nas odpowiednie, możemy ręcznie wybrać pożądane ustawienia.
@EnableWebMvc #
Do czego służy adnotacja @EnableWebMvc
? Importuje ona konfigurację WebMvcConfigurationSupport
. Można jej użyć, gdy chcemy dodać jakieś specjalne zachowanie do obsługi żądań HTTP w aplikacji.
@EnableWebMvc
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
Uwaga, w aplikacjach pisanych w Spring Boot adnotacja ta nie jest wymagana jeśli już korzystamy ze spring-boot-web-starter. Dodanie wsparcia dla konfiguracji WebMvc dzieje się automatycznie.
Dzięki tej konfiguracji możemy zdefiniować między innymi konwertery dla niestandardowych klas obsługując żądania webowe. Na przykład konwerter stringów w obiekty klasy Currency
.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToCurrencyConverter());
}
}
public class StringToCurrencyConverter implements Converter<String, Currency> {
@Override
public Currency convert(String from) {
// parse string to currency
}
}
@SpringBootTest #
Adnotacji @SpringBootTest
używany w testach gdy chcemy uruchomić pełny kontener aplikacji. Oznacza to, że Spring użyje Twojej głównej klasy konfiguracyjnej (np. MyApplication
) i załaduje wszystkie beany jakie masz zdefiniowane.
@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
class RegisterUseCaseIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private UserRepository userRepository;
@Test
void registrationWorksThroughAllLayers() throws Exception {
UserResource user = new UserResource("Zaphod", "zaphod@galaxy.net");
mockMvc.perform(post("/forums/{forumId}/register", 42L)
.contentType("application/json")
.param("sendWelcomeMail", "true")
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isOk());
UserEntity userEntity = userRepository.findByName("Zaphod");
assertThat(userEntity.getEmail()).isEqualTo("zaphod@galaxy.net");
}
}
Daje nam to możliwość stworzenia testów automatycznych, które upewnią nas, że w poprawny sposób wszystko ze sobą połączyliśmy. Testy te będą jednak kosztowne, gdyż będą trwać więcej czasu. Warto jednak mieć kilka takich testów w swojej aplikacji.
Uwaga - domyślnie SpringBootTest
stawia wszystkie komponenty Twojej aplikacji ale nie uruchamia pełnego serwera HTTP. Jeśli chcesz przetestować pełny zakres aplikacji musisz podać parametr do adnotacji.
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MyApplicationTests {
@Test
public void contextLoads() {
}
}
Więcej na ten temat w dokumentacji.
@WebMvcTest #
Jeśli nie potrzebujemy testować wszystkich modułów aplikacji a tylko interfejs webowy, możemy w tym celu skorzystać z adnotacji @WebMvcTest
.
Sprawia ona, że Spring Boot startuje jedynie warstwę webową - bez całego kontekstu aplikacji. Spring przeskanuje klasy oznaczone adnotacją @RestController
, @Controller
, i tym podobne, ale nie włączy automatycznie do kontekstu np. klas oznaczonych tylko adnotacją @Component
. Dzięki temu może przetestować zachowanie kontrolerów aplikacji, sprawdzić czy obiekty poprawnie się parsują oraz zachowują zdefiniowany kontrakt. Takie testy upewnią nas, że interfejs działa poprawnie, a jednocześnie będą trwać mniej czasu niż pełne testy, które uruchamiamy z adnotacją @SpringBootTest
.
@RunWith(SpringRunner.class)
@WebMvcTest
public class WebLayerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void shouldReturnDefaultMessage() throws Exception {
this.mockMvc.perform(get("/"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string(containsString("Hello World")));
}
}
W testach z @WebMvcTest
możemy nawet określić który dokładnie kontroler chcemy aby wystartował. Dzięki temu testy powinny być jeszcze szybsze.
Jeśli korzystamy z obiektów zależnych musimy je dodatkowo zamockować adnotacją @MockBean
. W testach z adnotacją @SpringBootTest
te obiekty byłyby domyślnie stworzone w kontekśćie Springa.
@RunWith(SpringRunner.class)
@WebMvcTest(GreetingController.class)
public class WebMockTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private GreetingService service;
@Test
public void greetingShouldReturnMessageFromService() throws Exception {
when(service.greet()).thenReturn("Hello Mock");
this.mockMvc.perform(get("/"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string(containsString("Hello World")));
}
}
@ContextConfiguration #
Został nam jeszcze do omówienia @ConfigurationContext
.
Jest to adnotacja pochodząca z domyślnej wersji Springa (bez Boota). Służy ona do uruchomienia testów integracyjnych i wskazanie jak kontekst Springa powinien zostać uruchomiony. Tak naprawdę robi więc to samo co @SpringBootTest
i jeśli pracujemy w świecie boota, to nie musimy z niej korzystać.
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class UserTest {
@Resource
UsersRepository usersRepository;
@Test
public void shouldSaveUserToDb() {
User user = new User(1L, "Mariusz", LocalDateTime.now());
Group group = new Group(1L, "Spring Polska", LocalDateTime.now());
Set<Group> groups = new HashSet<>();
groups.add(group);
user.setGroups(groups);
usersRepository.save(user);
User fetched = usersRepository.getOne(1L);
assertEquals("Mariusz", fetched.getUsername());
assertEquals(1, fetched.getGroups().size());
}
}
Adnotacji do testów integracyjnych w Spring Boocie jest dość sporo i jeśli potrzebujesz dokładniej dowiedzieć się jak z nich korzystać w dokumentacji znajdziesz ich dość dobry opis.
Podsumowanie #
Po przeczytaniu tego wpisu powinieneś wiedzieć już dużo więcej na temat wnętrzności Springa. Jeśli masz dodatkowe pytania, zostaw je proszę w komentarzu.
PS. Jeśli wpis Ci pomógł, podziel się proszę z jedną osobą, dla której może być wartościowy. Dzięki temu będę mógł docierać do kolejnych czytelników.