Középhaladó Olvasási idő: ~8 perc

Spring MVC

DispatcherServlet, @RestController, request mapping, validation, exception handling

Spring MVC

A Spring MVC a Servlet API-ra épülő, szinkron, request-response alapú webes keretrendszer, amely a DispatcherServlet köré szerveződik.


1. Definíció

A Spring MVC (Model-View-Controller) a Spring Framework webes modulja, amely a jakarta.servlet.http.HttpServlet API-ra épül. A központi elem a DispatcherServlet, amely egy Front Controller mintát valósít meg: minden HTTP kérés egyetlen belépési ponton érkezik, majd a keretrendszer az URL, HTTP metódus és egyéb feltételek alapján a megfelelő handler metódushoz irányítja.

A Spring MVC alapértelmezetten szinkron és blokkoló: minden kérés egy servlet thread-et foglal el a feldolgozás teljes idejére. A Spring Boot automatikusan konfigurálja a beágyazott Tomcat/Jetty/Undertow servert és a DispatcherServlet-et.

HTTP Request → DispatcherServlet → HandlerMapping → HandlerAdapter
    → Controller → Service → Repository → Response

2. Alapfogalmak

DispatcherServlet architektúra

A DispatcherServlet a következő komponenseket koordinálja:

Komponens Szerep
HandlerMapping URL → Controller metódus leképezés
HandlerAdapter Controller metódus meghívása
HttpMessageConverter Request/response body konverzió (JSON, XML)
ViewResolver View név → template feloldás (Thymeleaf, JSP)
HandlerExceptionResolver Kivételek kezelése
LocaleResolver Nyelv feloldás
MultipartResolver Fájlfeltöltés kezelés

Controller annotációk

@Controller          // View-t visszaadó controller
@RestController      // = @Controller + @ResponseBody (JSON/XML)
@RequestMapping      // Osztály vagy metódus szintű URL mapping
@GetMapping          // HTTP GET shortcut
@PostMapping         // HTTP POST shortcut
@PutMapping          // HTTP PUT shortcut
@DeleteMapping       // HTTP DELETE shortcut
@PatchMapping        // HTTP PATCH shortcut

Paraméter binding

@PathVariable        // URL path változó: /users/{id}
@RequestParam        // Query paraméter: ?name=John
@RequestBody         // HTTP body → Java objektum (deserialization)
@RequestHeader       // HTTP header értékek
@CookieValue         // Cookie értékek
@ModelAttribute      // Form data → Java objektum
@RequestPart         // Multipart fájl

Request feldolgozás folyamata

  1. HTTP kérés érkezik a DispatcherServlet-hez
  2. HandlerMapping megkeresi a megfelelő controller metódust
  3. HandlerInterceptor.preHandle() lefut
  4. HandlerAdapter meghívja a controller metódust
  5. HttpMessageConverter deserializálja a request body-t
  6. Controller végrehajtja az üzleti logikát
  7. HttpMessageConverter serializálja a response body-t
  8. HandlerInterceptor.postHandle() lefut
  9. Response visszaküldése a kliensnek

3. Gyakorlati használat

REST API controller

@RestController
@RequestMapping("/api/v1/users")
@Validated
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping
    public List<UserDto> findAll(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        return userService.findAll(PageRequest.of(page, size));
    }

    @GetMapping("/{id}")
    public UserDto findById(@PathVariable Long id) {
        return userService.findById(id);
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public UserDto create(@Valid @RequestBody CreateUserRequest request) {
        return userService.create(request);
    }

    @PutMapping("/{id}")
    public UserDto update(
            @PathVariable Long id,
            @Valid @RequestBody UpdateUserRequest request) {
        return userService.update(id, request);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable Long id) {
        userService.delete(id);
    }
}

Bean Validation integráció

public record CreateUserRequest(
        @NotBlank(message = "Name is required")
        @Size(min = 2, max = 100)
        String name,

        @Email(message = "Invalid email format")
        @NotBlank
        String email,

        @Min(18) @Max(150)
        int age
) {}

A @Valid a controller paraméteren aktiválja a validációt. Ha a validáció sikertelen, MethodArgumentNotValidException keletkezik.

Globális exception kezelés

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(EntityNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ErrorResponse handleNotFound(EntityNotFoundException ex) {
        return new ErrorResponse("NOT_FOUND", ex.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
        List<String> errors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(e -> e.getField() + ": " + e.getDefaultMessage())
                .toList();
        return new ErrorResponse("VALIDATION_FAILED", errors.toString());
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResponse handleGeneral(Exception ex) {
        return new ErrorResponse("INTERNAL_ERROR", "Something went wrong");
    }
}

public record ErrorResponse(String code, String message) {}

4. Kód példák

Content negotiation

@RestController
@RequestMapping("/api/products")
public class ProductController {

    @GetMapping(produces = {MediaType.APPLICATION_JSON_VALUE,
                            MediaType.APPLICATION_XML_VALUE})
    public List<Product> findAll() {
        return productService.findAll();
    }
}

Az Accept header alapján a Spring kiválasztja a megfelelő HttpMessageConverter-t.

Egyedi HttpMessageConverter

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureMessageConverters(
            List<HttpMessageConverter<?>> converters) {
        converters.add(new MappingJackson2HttpMessageConverter(
                new ObjectMapper()
                    .registerModule(new JavaTimeModule())
                    .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
        ));
    }
}

Async controller (DeferredResult)

@GetMapping("/async/{id}")
public DeferredResult<UserDto> findByIdAsync(@PathVariable Long id) {
    DeferredResult<UserDto> result = new DeferredResult<>(5000L);
    executorService.submit(() -> {
        try {
            UserDto user = userService.findById(id);
            result.setResult(user);
        } catch (Exception e) {
            result.setErrorResult(e);
        }
    });
    return result;
}

A DeferredResult felszabadítja a servlet thread-et, míg a háttérben fut a feldolgozás.

ResponseEntity finomhangolás

@GetMapping("/{id}")
public ResponseEntity<UserDto> findById(@PathVariable Long id) {
    return userService.findOptional(id)
            .map(user -> ResponseEntity.ok()
                    .header("X-Custom", "value")
                    .cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS))
                    .body(user))
            .orElse(ResponseEntity.notFound().build());
}

5. Trade-offok

Spring MVC vs WebFlux

Szempont Spring MVC WebFlux
I/O modell Blokkoló (thread-per-request) Non-blocking (event loop)
Thread-ek ~200 Tomcat thread (alapért.) Kevés event loop thread
Skálázás Vertikális (több thread) Horizontális (kevesebb erőforrás)
Debugolás Egyszerű stack trace Reaktív lánc nehezebb
Ökoszisztéma Teljes (JPA, JDBC, stb.) Korlátozott (R2DBC, WebClient)
Tanulási görbe Alacsony Magas

Mikor Spring MVC

  • CRUD alkalmazások hagyományos adatbázissal (JPA/JDBC)
  • Egyszerű REST API-k kevés párhuzamos kéréssel
  • Csapat nem ismeri a reaktív programozást
  • Szinkron third-party könyvtárak használata

Mikor NEM Spring MVC

  • 10 000+ egyidejű kapcsolat (WebSocket, SSE)
  • Microservice gateway nagy áteresztőképességgel
  • Streaming feldolgozás (Kafka, event sourcing)

6. Gyakori hibák

❌ Üzleti logika a controllerben

// ROSSZ: controller tartalmazza az üzleti logikát
@PostMapping
public UserDto create(@RequestBody CreateUserRequest req) {
    if (userRepo.existsByEmail(req.email())) {
        throw new DuplicateException("Email exists");
    }
    User user = new User(req.name(), req.email());
    user = userRepo.save(user);
    emailService.sendWelcome(user);
    return UserDto.from(user);
}

// JÓ: service rétegbe delegálás
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserDto create(@Valid @RequestBody CreateUserRequest req) {
    return userService.create(req);
}

❌ Hiányzó @Valid annotáció

// ROSSZ: validáció nem fut le
@PostMapping
public UserDto create(@RequestBody CreateUserRequest req) { ... }

// JÓ: @Valid aktiválja a Bean Validation-t
@PostMapping
public UserDto create(@Valid @RequestBody CreateUserRequest req) { ... }

❌ Exception handler nélküli API

Validációs és üzleti hibák stack trace-ként szivárognak ki a klienshez. Mindig használj @RestControllerAdvice-t!

❌ @ResponseStatus elfelejtése

A @PostMapping alapértelmezetten 200 OK-t ad vissza. Létrehozásnál @ResponseStatus(HttpStatus.CREATED) a helyes.

❌ PathVariable típus-mismatch kezeletlen

// /api/users/abc → NumberFormatException → 500
// Kezeld @ExceptionHandler-rel vagy használj regex-et:
@GetMapping("/{id:\\d+}")
public UserDto findById(@PathVariable Long id) { ... }

7. Mélyebb összefüggések

HandlerInterceptor vs Servlet Filter

Szempont HandlerInterceptor Servlet Filter
Regisztráció WebMvcConfigurer @Component / FilterRegistrationBean
Spring context Elérhető (Spring bean) Csak ha Spring-managed
Végrehajtási pont Handler (controller) szinten Servlet szinten (előbb fut)
preHandle Igen doFilter() előtt
postHandle Igen (response előtt) Nincs külön
afterCompletion Igen (response után) Nincs külön

HttpMessageConverter belső működés

A Spring az Accept és Content-Type header alapján választja ki a converter-t:

  1. Request: Content-Type: application/jsonMappingJackson2HttpMessageConverter.read()
  2. Response: Accept: application/jsonMappingJackson2HttpMessageConverter.write()
  3. Ha Jackson a classpath-on van, automatikus regisztráció
  4. XML-hez jackson-dataformat-xml dependency kell

Controller tanácsok (Advice)

@ControllerAdvice(basePackages = "com.example.api")
public class ApiAdvice {
    // Csak az api package controllerjeire vonatkozik

    @ModelAttribute
    public void addCommonAttributes(Model model) {
        model.addAttribute("appVersion", "2.0");
    }

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.registerCustomEditor(LocalDate.class,
                new PropertyEditorSupport() {
                    @Override
                    public void setAsText(String text) {
                        setValue(LocalDate.parse(text));
                    }
                });
    }
}

Argument Resolver egyedi típusokhoz

public class CurrentUserArgumentResolver
        implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter param) {
        return param.hasParameterAnnotation(CurrentUser.class);
    }

    @Override
    public Object resolveArgument(MethodParameter param,
            ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest,
            WebDataBinderFactory binderFactory) {
        return SecurityContextHolder.getContext()
                .getAuthentication().getPrincipal();
    }
}

8. Interjúkérdések

  1. Mi a DispatcherServlet szerepe? Front Controller minta: egyetlen belépési pont, HandlerMapping-gel keresi meg a controller metódust, HandlerAdapter-rel hívja meg.

  2. Mi a különbség a @Controller és @RestController között? @RestController = @Controller + @ResponseBody. A @Controller View nevet ad vissza (Thymeleaf), a @RestController JSON/XML body-t.

  3. Hogyan működik a validáció Spring MVC-ben? @Valid / @Validated aktiválja a Bean Validation-t (Hibernate Validator). Hiba esetén MethodArgumentNotValidException. A @RestControllerAdvice kezeli globálisan.

  4. Mi a @RestControllerAdvice? Globális exception handler + model attribute + init binder. A @ControllerAdvice + @ResponseBody kombinációja.

  5. Hogyan testreszabod a HTTP status kódot? @ResponseStatus annotáció, ResponseEntity<T> visszatérési típus, vagy HttpServletResponse.setStatus().

  6. Mi a különbség a HandlerInterceptor és a Servlet Filter között? Filter: servlet szinten, Spring context nélkül is fut. Interceptor: Spring-managed, preHandle/postHandle/afterCompletion lifecycle.

  7. Hogyan kezeled a fájlfeltöltést? @RequestPart MultipartFile file. MultipartResolver bean szükséges (Spring Boot automatikusan konfigurálja). spring.servlet.multipart.max-file-size limitálja.


9. Szószedet

Fogalom Jelentés
DispatcherServlet Front Controller, minden HTTP kérés belépési pontja
HandlerMapping URL → controller metódus leképezés
HandlerAdapter Controller metódus meghívásáért felelős
HttpMessageConverter Java objektum ↔ HTTP body konverzió
@RestController @Controller + @ResponseBody, JSON/XML API-khoz
@RequestMapping URL és HTTP metódus leképezés annotáció
@Valid Bean Validation aktiválás controller paraméteren
@RestControllerAdvice Globális exception handler + model attribute
ResponseEntity HTTP status + headers + body finomhangolás
HandlerInterceptor Spring szintű pre/post processing
Content negotiation Accept/Content-Type alapú formátum választás
DeferredResult Aszinkron response (servlet thread felszabadítás)

10. Gyorsreferencia

REQUEST LIFECYCLE:
  Client → Filter → DispatcherServlet → Interceptor.preHandle()
    → HandlerAdapter → Controller → Service → Repository
    → Interceptor.postHandle() → Response

ANNOTÁCIÓK:
  @RestController          JSON/XML API controller
  @RequestMapping("/api")  Alap URL prefix
  @GetMapping("/{id}")     GET + path variable
  @PostMapping             POST endpoint
  @Valid @RequestBody      Body validáció
  @PathVariable            URL szegmens
  @RequestParam            Query paraméter
  @ResponseStatus(201)     HTTP status felülírás

EXCEPTION HANDLING:
  @RestControllerAdvice    Globális handler
  @ExceptionHandler(X)     Adott kivétel kezelése
  ResponseEntity<Error>    Egyedi response

KONFIGURÁCIÓ:
  WebMvcConfigurer         Interceptor, converter, CORS
  server.port=8080         Szerver port
  spring.jackson.*         JSON serialization beállítások

TESZTELÉS:
  @WebMvcTest              Controller réteg teszt (slice)
  MockMvc                  HTTP kérés szimuláció
  @MockBean                Service mock

🎮 Játékok

10 kérdés