0
# Web Framework (Spring MVC)
1
2
Spring Web MVC is the traditional servlet-based web framework built on the Model-View-Controller architecture. It provides flexible request handling, comprehensive data binding, validation, and view resolution for building web applications.
3
4
## Maven Dependencies
5
6
```xml
7
<!-- Spring Web MVC -->
8
<dependency>
9
<groupId>org.springframework</groupId>
10
<artifactId>spring-webmvc</artifactId>
11
<version>5.3.39</version>
12
</dependency>
13
14
<!-- Spring Web (base web utilities) -->
15
<dependency>
16
<groupId>org.springframework</groupId>
17
<artifactId>spring-web</artifactId>
18
<version>5.3.39</version>
19
</dependency>
20
21
<!-- Servlet API -->
22
<dependency>
23
<groupId>javax.servlet</groupId>
24
<artifactId>javax.servlet-api</artifactId>
25
<version>4.0.1</version>
26
<scope>provided</scope>
27
</dependency>
28
29
<!-- Jackson for JSON processing -->
30
<dependency>
31
<groupId>com.fasterxml.jackson.core</groupId>
32
<artifactId>jackson-databind</artifactId>
33
<version>2.12.7</version>
34
</dependency>
35
```
36
37
## Core Imports
38
39
```java { .api }
40
// Controller and Request Mapping
41
import org.springframework.web.bind.annotation.RestController;
42
import org.springframework.web.bind.annotation.Controller;
43
import org.springframework.web.bind.annotation.RequestMapping;
44
import org.springframework.web.bind.annotation.GetMapping;
45
import org.springframework.web.bind.annotation.PostMapping;
46
import org.springframework.web.bind.annotation.PutMapping;
47
import org.springframework.web.bind.annotation.DeleteMapping;
48
import org.springframework.web.bind.annotation.PatchMapping;
49
50
// Request/Response Binding
51
import org.springframework.web.bind.annotation.RequestParam;
52
import org.springframework.web.bind.annotation.PathVariable;
53
import org.springframework.web.bind.annotation.RequestBody;
54
import org.springframework.web.bind.annotation.ResponseBody;
55
import org.springframework.web.bind.annotation.RequestHeader;
56
import org.springframework.web.bind.annotation.CookieValue;
57
58
// Model and View
59
import org.springframework.ui.Model;
60
import org.springframework.web.servlet.ModelAndView;
61
import org.springframework.web.servlet.View;
62
63
// HTTP Response
64
import org.springframework.http.ResponseEntity;
65
import org.springframework.http.HttpStatus;
66
import org.springframework.http.HttpHeaders;
67
68
// Configuration
69
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
70
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
71
import org.springframework.context.annotation.Configuration;
72
73
// Exception Handling
74
import org.springframework.web.bind.annotation.ExceptionHandler;
75
import org.springframework.web.bind.annotation.ControllerAdvice;
76
77
// Validation
78
import org.springframework.validation.BindingResult;
79
import org.springframework.validation.annotation.Validated;
80
import javax.validation.Valid;
81
```
82
83
## Core MVC Components
84
85
### DispatcherServlet
86
87
```java { .api }
88
// Central dispatcher for HTTP request handlers/controllers
89
public class DispatcherServlet extends FrameworkServlet {
90
91
public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver";
92
public static final String LOCALE_RESOLVER_BEAN_NAME = "localeResolver";
93
public static final String THEME_RESOLVER_BEAN_NAME = "themeResolver";
94
public static final String HANDLER_MAPPING_BEAN_NAME = "handlerMapping";
95
public static final String HANDLER_ADAPTER_BEAN_NAME = "handlerAdapter";
96
public static final String HANDLER_EXCEPTION_RESOLVER_BEAN_NAME = "handlerExceptionResolver";
97
public static final String REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME = "viewNameTranslator";
98
public static final String VIEW_RESOLVER_BEAN_NAME = "viewResolver";
99
public static final String FLASH_MAP_MANAGER_BEAN_NAME = "flashMapManager";
100
101
// Set the WebApplicationContext for this servlet
102
public void setApplicationContext(ApplicationContext applicationContext);
103
104
// Initialize strategy objects used by this servlet
105
protected void initStrategies(ApplicationContext context);
106
107
// Process the actual dispatching to the handler
108
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception;
109
}
110
111
// Interface to provide configuration for web application
112
public interface WebApplicationContext extends ApplicationContext {
113
String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";
114
String SCOPE_REQUEST = "request";
115
String SCOPE_SESSION = "session";
116
String SCOPE_APPLICATION = "application";
117
String SERVLET_CONTEXT_BEAN_NAME = "servletContext";
118
String CONTEXT_PARAMETERS_BEAN_NAME = "contextParameters";
119
String CONTEXT_ATTRIBUTES_BEAN_NAME = "contextAttributes";
120
121
ServletContext getServletContext();
122
}
123
```
124
125
### Handler Mapping
126
127
```java { .api }
128
// Interface for objects that define mapping between requests and handler objects
129
public interface HandlerMapping {
130
String BEST_MATCHING_HANDLER_ATTRIBUTE = HandlerMapping.class.getName() + ".bestMatchingHandler";
131
String LOOKUP_PATH = HandlerMapping.class.getName() + ".lookupPath";
132
String PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE = HandlerMapping.class.getName() + ".pathWithinHandlerMapping";
133
String BEST_MATCHING_PATTERN_ATTRIBUTE = HandlerMapping.class.getName() + ".bestMatchingPattern";
134
String INTROSPECT_TYPE_LEVEL_MAPPING = HandlerMapping.class.getName() + ".introspectTypeLevelMapping";
135
String URI_TEMPLATE_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".uriTemplateVariables";
136
String MATRIX_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".matrixVariables";
137
String PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE = HandlerMapping.class.getName() + ".producibleMediaTypes";
138
139
HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
140
}
141
142
// Chain including handler and interceptors
143
public class HandlerExecutionChain {
144
145
public HandlerExecutionChain(Object handler);
146
public HandlerExecutionChain(Object handler, HandlerInterceptor... interceptors);
147
148
public Object getHandler();
149
public HandlerInterceptor[] getInterceptors();
150
public void addInterceptor(HandlerInterceptor interceptor);
151
public void addInterceptor(int index, HandlerInterceptor interceptor);
152
public void addInterceptors(HandlerInterceptor... interceptors);
153
}
154
155
// Interface for handler interceptors
156
public interface HandlerInterceptor {
157
158
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
159
return true;
160
}
161
162
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
163
ModelAndView modelAndView) throws Exception {
164
}
165
166
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
167
Exception ex) throws Exception {
168
}
169
}
170
```
171
172
### Handler Adapter
173
174
```java { .api }
175
// MVC framework extension point for using any handler interface
176
public interface HandlerAdapter {
177
178
boolean supports(Object handler);
179
180
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
181
182
long getLastModified(HttpServletRequest request, Object handler);
183
}
184
185
// Adapter for @RequestMapping annotated handler methods
186
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
187
implements BeanFactoryAware, InitializingBean {
188
189
public void setCustomArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers);
190
public void setCustomReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers);
191
public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters);
192
public void setWebBindingInitializer(WebBindingInitializer webBindingInitializer);
193
}
194
```
195
196
### Model and View
197
198
```java { .api }
199
// Holder for both Model and View in web MVC framework
200
public class ModelAndView {
201
202
public ModelAndView();
203
public ModelAndView(String viewName);
204
public ModelAndView(View view);
205
public ModelAndView(String viewName, Map<String, ?> model);
206
public ModelAndView(View view, Map<String, ?> model);
207
public ModelAndView(String viewName, String modelName, Object modelObject);
208
209
public void setViewName(String viewName);
210
public String getViewName();
211
public void setView(View view);
212
public View getView();
213
public boolean hasView();
214
public boolean isReference();
215
public Map<String, Object> getModel();
216
public ModelAndView addObject(String attributeName, Object attributeValue);
217
public ModelAndView addObject(Object attributeValue);
218
public ModelAndView addAllObjects(Map<String, ?> modelMap);
219
public void clear();
220
public boolean isEmpty();
221
}
222
223
// Interface for MVC View for web interaction
224
public interface View {
225
String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus";
226
String PATH_VARIABLES = View.class.getName() + ".pathVariables";
227
String SELECTED_CONTENT_TYPE = View.class.getName() + ".selectedContentType";
228
229
default String getContentType() {
230
return null;
231
}
232
233
void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
234
}
235
236
// Interface for objects that can resolve views by name
237
public interface ViewResolver {
238
View resolveViewName(String viewName, Locale locale) throws Exception;
239
}
240
```
241
242
## Annotations and Request Mapping
243
244
### Controller Annotations
245
246
```java { .api }
247
// Indicates that an annotated class is a "Controller"
248
@Target(ElementType.TYPE)
249
@Retention(RetentionPolicy.RUNTIME)
250
@Documented
251
@Component
252
public @interface Controller {
253
@AliasFor(annotation = Component.class)
254
String value() default "";
255
}
256
257
// Combination of @Controller and @ResponseBody
258
@Target(ElementType.TYPE)
259
@Retention(RetentionPolicy.RUNTIME)
260
@Documented
261
@Controller
262
@ResponseBody
263
public @interface RestController {
264
@AliasFor(annotation = Controller.class)
265
String value() default "";
266
}
267
268
// Maps HTTP requests to handler methods of MVC controllers
269
@Target({ElementType.TYPE, ElementType.METHOD})
270
@Retention(RetentionPolicy.RUNTIME)
271
@Documented
272
@Mapping
273
public @interface RequestMapping {
274
String name() default "";
275
276
@AliasFor("path")
277
String[] value() default {};
278
279
@AliasFor("value")
280
String[] path() default {};
281
282
RequestMethod[] method() default {};
283
284
String[] params() default {};
285
286
String[] headers() default {};
287
288
String[] consumes() default {};
289
290
String[] produces() default {};
291
}
292
```
293
294
### HTTP Method Mappings
295
296
```java { .api }
297
// Shortcut annotations for specific HTTP methods
298
299
@Target(ElementType.METHOD)
300
@Retention(RetentionPolicy.RUNTIME)
301
@Documented
302
@RequestMapping(method = RequestMethod.GET)
303
public @interface GetMapping {
304
@AliasFor(annotation = RequestMapping.class)
305
String name() default "";
306
307
@AliasFor(annotation = RequestMapping.class)
308
String[] value() default {};
309
310
@AliasFor(annotation = RequestMapping.class)
311
String[] path() default {};
312
313
@AliasFor(annotation = RequestMapping.class)
314
String[] params() default {};
315
316
@AliasFor(annotation = RequestMapping.class)
317
String[] headers() default {};
318
319
@AliasFor(annotation = RequestMapping.class)
320
String[] consumes() default {};
321
322
@AliasFor(annotation = RequestMapping.class)
323
String[] produces() default {};
324
}
325
326
@Target(ElementType.METHOD)
327
@Retention(RetentionPolicy.RUNTIME)
328
@Documented
329
@RequestMapping(method = RequestMethod.POST)
330
public @interface PostMapping {
331
// Same attributes as GetMapping
332
}
333
334
@Target(ElementType.METHOD)
335
@Retention(RetentionPolicy.RUNTIME)
336
@Documented
337
@RequestMapping(method = RequestMethod.PUT)
338
public @interface PutMapping {
339
// Same attributes as GetMapping
340
}
341
342
@Target(ElementType.METHOD)
343
@Retention(RetentionPolicy.RUNTIME)
344
@Documented
345
@RequestMapping(method = RequestMethod.DELETE)
346
public @interface DeleteMapping {
347
// Same attributes as GetMapping
348
}
349
350
@Target(ElementType.METHOD)
351
@Retention(RetentionPolicy.RUNTIME)
352
@Documented
353
@RequestMapping(method = RequestMethod.PATCH)
354
public @interface PatchMapping {
355
// Same attributes as GetMapping
356
}
357
```
358
359
### Request Parameter Binding
360
361
```java { .api }
362
// Binds request parameters to method parameters
363
@Target(ElementType.PARAMETER)
364
@Retention(RetentionPolicy.RUNTIME)
365
@Documented
366
public @interface RequestParam {
367
@AliasFor("name")
368
String value() default "";
369
370
@AliasFor("value")
371
String name() default "";
372
373
boolean required() default true;
374
375
String defaultValue() default ValueConstants.DEFAULT_NONE;
376
}
377
378
// Binds URI template variables to method parameters
379
@Target(ElementType.PARAMETER)
380
@Retention(RetentionPolicy.RUNTIME)
381
@Documented
382
public @interface PathVariable {
383
@AliasFor("name")
384
String value() default "";
385
386
@AliasFor("value")
387
String name() default "";
388
389
boolean required() default true;
390
}
391
392
// Binds HTTP request body to method parameter
393
@Target(ElementType.PARAMETER)
394
@Retention(RetentionPolicy.RUNTIME)
395
@Documented
396
public @interface RequestBody {
397
boolean required() default true;
398
}
399
400
// Binds method return value to web response body
401
@Target({ElementType.TYPE, ElementType.METHOD})
402
@Retention(RetentionPolicy.RUNTIME)
403
@Documented
404
public @interface ResponseBody {
405
}
406
407
// Binds request header to method parameter
408
@Target(ElementType.PARAMETER)
409
@Retention(RetentionPolicy.RUNTIME)
410
@Documented
411
public @interface RequestHeader {
412
@AliasFor("name")
413
String value() default "";
414
415
@AliasFor("value")
416
String name() default "";
417
418
boolean required() default true;
419
420
String defaultValue() default ValueConstants.DEFAULT_NONE;
421
}
422
423
// Binds cookie value to method parameter
424
@Target(ElementType.PARAMETER)
425
@Retention(RetentionPolicy.RUNTIME)
426
@Documented
427
public @interface CookieValue {
428
@AliasFor("name")
429
String value() default "";
430
431
@AliasFor("value")
432
String name() default "";
433
434
boolean required() default true;
435
436
String defaultValue() default ValueConstants.DEFAULT_NONE;
437
}
438
```
439
440
## Configuration
441
442
### Web MVC Configuration
443
444
```java { .api }
445
// Enables Spring MVC configuration
446
@Retention(RetentionPolicy.RUNTIME)
447
@Target(ElementType.TYPE)
448
@Documented
449
@Import(DelegatingWebMvcConfiguration.class)
450
public @interface EnableWebMvc {
451
}
452
453
// Interface for customizing Spring MVC configuration
454
public interface WebMvcConfigurer {
455
456
default void configurePathMatch(PathMatchConfigurer configurer) {
457
}
458
459
default void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
460
}
461
462
default void configureAsyncSupport(AsyncSupportConfigurer configurer) {
463
}
464
465
default void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
466
}
467
468
default void addFormatters(FormatterRegistry registry) {
469
}
470
471
default void addInterceptors(InterceptorRegistry registry) {
472
}
473
474
default void addResourceHandlers(ResourceHandlerRegistry registry) {
475
}
476
477
default void addCorsMappings(CorsRegistry registry) {
478
}
479
480
default void addViewControllers(ViewControllerRegistry registry) {
481
}
482
483
default void configureViewResolvers(ViewResolverRegistry registry) {
484
}
485
486
default void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
487
}
488
489
default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {
490
}
491
492
default void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
493
}
494
495
default void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
496
}
497
498
default void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
499
}
500
501
default void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
502
}
503
504
default Validator getValidator() {
505
return null;
506
}
507
508
default MessageCodesResolver getMessageCodesResolver() {
509
return null;
510
}
511
}
512
```
513
514
## HTTP Message Conversion
515
516
### Message Converters
517
518
```java { .api }
519
// Strategy interface for converting from and to HTTP requests and responses
520
public interface HttpMessageConverter<T> {
521
522
boolean canRead(Class<?> clazz, MediaType mediaType);
523
524
boolean canWrite(Class<?> clazz, MediaType mediaType);
525
526
List<MediaType> getSupportedMediaTypes();
527
528
T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;
529
530
void write(T t, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;
531
}
532
533
// Abstract base class for HttpMessageConverter implementations
534
public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConverter<T> {
535
536
protected AbstractHttpMessageConverter();
537
protected AbstractHttpMessageConverter(MediaType supportedMediaType);
538
protected AbstractHttpMessageConverter(MediaType... supportedMediaTypes);
539
540
public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes);
541
public List<MediaType> getSupportedMediaTypes();
542
543
protected abstract boolean supports(Class<?> clazz);
544
protected abstract T readInternal(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;
545
protected abstract void writeInternal(T t, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;
546
}
547
548
// JSON message converter using Jackson
549
public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
550
551
public MappingJackson2HttpMessageConverter();
552
public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper);
553
554
public void setObjectMapper(ObjectMapper objectMapper);
555
public ObjectMapper getObjectMapper();
556
public void setPrettyPrint(boolean prettyPrint);
557
}
558
```
559
560
## Response Handling
561
562
### ResponseEntity
563
564
```java { .api }
565
// Extension of HttpEntity that adds HttpStatus
566
public class ResponseEntity<T> extends HttpEntity<T> {
567
568
public ResponseEntity(HttpStatus status);
569
public ResponseEntity(T body, HttpStatus status);
570
public ResponseEntity(MultiValueMap<String, String> headers, HttpStatus status);
571
public ResponseEntity(T body, MultiValueMap<String, String> headers, HttpStatus status);
572
573
public HttpStatus getStatusCode();
574
public int getStatusCodeValue();
575
576
// Builder pattern
577
public static BodyBuilder status(HttpStatus status);
578
public static BodyBuilder status(int status);
579
public static BodyBuilder ok();
580
public static <T> ResponseEntity<T> ok(T body);
581
public static BodyBuilder created(URI location);
582
public static BodyBuilder accepted();
583
public static HeadersBuilder<?> noContent();
584
public static BodyBuilder badRequest();
585
public static HeadersBuilder<?> notFound();
586
public static BodyBuilder unprocessableEntity();
587
588
// Builder interfaces
589
public interface BodyBuilder extends HeadersBuilder<BodyBuilder> {
590
<T> ResponseEntity<T> body(T body);
591
}
592
593
public interface HeadersBuilder<B extends HeadersBuilder<B>> {
594
B header(String headerName, String... headerValues);
595
B headers(HttpHeaders headers);
596
B allow(HttpMethod... allowedMethods);
597
B eTag(String etag);
598
B lastModified(ZonedDateTime lastModified);
599
B location(URI location);
600
B cacheControl(CacheControl cacheControl);
601
B varyBy(String... requestHeaders);
602
<T> ResponseEntity<T> build();
603
}
604
}
605
```
606
607
## Exception Handling
608
609
### Exception Handler Annotations
610
611
```java { .api }
612
// Annotation for handling exceptions in specific handler classes/methods
613
@Target(ElementType.METHOD)
614
@Retention(RetentionPolicy.RUNTIME)
615
@Documented
616
public @interface ExceptionHandler {
617
Class<? extends Throwable>[] value() default {};
618
}
619
620
// Specialization of @Component for classes that declare @ExceptionHandler methods
621
@Target(ElementType.TYPE)
622
@Retention(RetentionPolicy.RUNTIME)
623
@Documented
624
@Component
625
public @interface ControllerAdvice {
626
@AliasFor("basePackages")
627
String[] value() default {};
628
629
String[] basePackages() default {};
630
631
Class<?>[] basePackageClasses() default {};
632
633
Class<?>[] assignableTypes() default {};
634
635
Class<? extends Annotation>[] annotations() default {};
636
}
637
638
// Combination of @ControllerAdvice and @ResponseBody
639
@Target(ElementType.TYPE)
640
@Retention(RetentionPolicy.RUNTIME)
641
@Documented
642
@ControllerAdvice
643
@ResponseBody
644
public @interface RestControllerAdvice {
645
@AliasFor(annotation = ControllerAdvice.class)
646
String[] value() default {};
647
648
@AliasFor(annotation = ControllerAdvice.class)
649
String[] basePackages() default {};
650
651
@AliasFor(annotation = ControllerAdvice.class)
652
Class<?>[] basePackageClasses() default {};
653
654
@AliasFor(annotation = ControllerAdvice.class)
655
Class<?>[] assignableTypes() default {};
656
657
@AliasFor(annotation = ControllerAdvice.class)
658
Class<? extends Annotation>[] annotations() default {};
659
}
660
661
// Annotation for mapping specific exception and/or HTTP status
662
@Target({ElementType.TYPE, ElementType.METHOD})
663
@Retention(RetentionPolicy.RUNTIME)
664
@Documented
665
public @interface ResponseStatus {
666
@AliasFor("code")
667
HttpStatus value() default HttpStatus.INTERNAL_SERVER_ERROR;
668
669
@AliasFor("value")
670
HttpStatus code() default HttpStatus.INTERNAL_SERVER_ERROR;
671
672
String reason() default "";
673
}
674
```
675
676
## Practical Usage Examples
677
678
### Basic REST Controller
679
680
```java { .api }
681
@RestController
682
@RequestMapping("/api/users")
683
public class UserController {
684
685
private final UserService userService;
686
687
public UserController(UserService userService) {
688
this.userService = userService;
689
}
690
691
@GetMapping
692
public ResponseEntity<List<User>> getAllUsers(
693
@RequestParam(defaultValue = "0") int page,
694
@RequestParam(defaultValue = "10") int size,
695
@RequestParam(required = false) String sort) {
696
697
Pageable pageable = PageRequest.of(page, size);
698
if (sort != null) {
699
pageable = PageRequest.of(page, size, Sort.by(sort));
700
}
701
702
Page<User> users = userService.findAll(pageable);
703
704
HttpHeaders headers = new HttpHeaders();
705
headers.add("X-Total-Count", String.valueOf(users.getTotalElements()));
706
707
return ResponseEntity.ok()
708
.headers(headers)
709
.body(users.getContent());
710
}
711
712
@GetMapping("/{id}")
713
public ResponseEntity<User> getUserById(@PathVariable Long id) {
714
User user = userService.findById(id);
715
return ResponseEntity.ok(user);
716
}
717
718
@PostMapping
719
public ResponseEntity<User> createUser(@Valid @RequestBody CreateUserRequest request) {
720
User user = userService.createUser(request);
721
722
URI location = ServletUriComponentsBuilder
723
.fromCurrentRequest()
724
.path("/{id}")
725
.buildAndExpand(user.getId())
726
.toUri();
727
728
return ResponseEntity.created(location).body(user);
729
}
730
731
@PutMapping("/{id}")
732
public ResponseEntity<User> updateUser(
733
@PathVariable Long id,
734
@Valid @RequestBody UpdateUserRequest request) {
735
736
User user = userService.updateUser(id, request);
737
return ResponseEntity.ok(user);
738
}
739
740
@DeleteMapping("/{id}")
741
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
742
userService.deleteUser(id);
743
return ResponseEntity.noContent().build();
744
}
745
746
@GetMapping("/search")
747
public ResponseEntity<List<User>> searchUsers(
748
@RequestParam String query,
749
@RequestParam(required = false) String email,
750
@RequestParam(required = false) LocalDate createdAfter) {
751
752
UserSearchCriteria criteria = UserSearchCriteria.builder()
753
.query(query)
754
.email(email)
755
.createdAfter(createdAfter)
756
.build();
757
758
List<User> users = userService.searchUsers(criteria);
759
return ResponseEntity.ok(users);
760
}
761
}
762
```
763
764
### Request/Response DTOs and Validation
765
766
```java { .api }
767
// Request DTOs with validation
768
public class CreateUserRequest {
769
770
@NotBlank(message = "Username is required")
771
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
772
private String username;
773
774
@NotBlank(message = "Email is required")
775
@Email(message = "Email should be valid")
776
private String email;
777
778
@NotBlank(message = "First name is required")
779
private String firstName;
780
781
@NotBlank(message = "Last name is required")
782
private String lastName;
783
784
@Pattern(regexp = "^\\+?[1-9]\\d{1,14}$", message = "Phone number should be valid")
785
private String phoneNumber;
786
787
// Constructors, getters, setters
788
}
789
790
public class UpdateUserRequest {
791
792
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
793
private String username;
794
795
@Email(message = "Email should be valid")
796
private String email;
797
798
private String firstName;
799
private String lastName;
800
private String phoneNumber;
801
802
// Constructors, getters, setters
803
}
804
805
// Response DTOs
806
public class UserResponse {
807
private Long id;
808
private String username;
809
private String email;
810
private String firstName;
811
private String lastName;
812
private String phoneNumber;
813
private LocalDateTime createdAt;
814
private LocalDateTime updatedAt;
815
816
// Constructors, getters, setters
817
}
818
819
// Error response
820
public class ErrorResponse {
821
private String message;
822
private String code;
823
private LocalDateTime timestamp;
824
private String path;
825
private Map<String, String> validationErrors;
826
827
// Constructors, getters, setters
828
}
829
```
830
831
### Exception Handling
832
833
```java { .api }
834
@RestControllerAdvice
835
public class GlobalExceptionHandler {
836
837
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
838
839
@ExceptionHandler(UserNotFoundException.class)
840
public ResponseEntity<ErrorResponse> handleUserNotFound(
841
UserNotFoundException ex,
842
HttpServletRequest request) {
843
844
ErrorResponse error = ErrorResponse.builder()
845
.message(ex.getMessage())
846
.code("USER_NOT_FOUND")
847
.timestamp(LocalDateTime.now())
848
.path(request.getRequestURI())
849
.build();
850
851
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
852
}
853
854
@ExceptionHandler(MethodArgumentNotValidException.class)
855
public ResponseEntity<ErrorResponse> handleValidationErrors(
856
MethodArgumentNotValidException ex,
857
HttpServletRequest request) {
858
859
Map<String, String> validationErrors = new HashMap<>();
860
ex.getBindingResult().getFieldErrors().forEach(error ->
861
validationErrors.put(error.getField(), error.getDefaultMessage()));
862
863
ErrorResponse error = ErrorResponse.builder()
864
.message("Validation failed")
865
.code("VALIDATION_ERROR")
866
.timestamp(LocalDateTime.now())
867
.path(request.getRequestURI())
868
.validationErrors(validationErrors)
869
.build();
870
871
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
872
}
873
874
@ExceptionHandler(DataIntegrityViolationException.class)
875
public ResponseEntity<ErrorResponse> handleDataIntegrityViolation(
876
DataIntegrityViolationException ex,
877
HttpServletRequest request) {
878
879
String message = "Data integrity violation";
880
String code = "DATA_INTEGRITY_ERROR";
881
882
// Check for specific constraint violations
883
if (ex.getMessage().contains("users_username_key")) {
884
message = "Username already exists";
885
code = "DUPLICATE_USERNAME";
886
} else if (ex.getMessage().contains("users_email_key")) {
887
message = "Email already exists";
888
code = "DUPLICATE_EMAIL";
889
}
890
891
ErrorResponse error = ErrorResponse.builder()
892
.message(message)
893
.code(code)
894
.timestamp(LocalDateTime.now())
895
.path(request.getRequestURI())
896
.build();
897
898
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
899
}
900
901
@ExceptionHandler(Exception.class)
902
public ResponseEntity<ErrorResponse> handleGenericException(
903
Exception ex,
904
HttpServletRequest request) {
905
906
logger.error("Unexpected error occurred", ex);
907
908
ErrorResponse error = ErrorResponse.builder()
909
.message("An unexpected error occurred")
910
.code("INTERNAL_ERROR")
911
.timestamp(LocalDateTime.now())
912
.path(request.getRequestURI())
913
.build();
914
915
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
916
}
917
918
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
919
public ResponseEntity<ErrorResponse> handleMethodNotSupported(
920
HttpRequestMethodNotSupportedException ex,
921
HttpServletRequest request) {
922
923
ErrorResponse error = ErrorResponse.builder()
924
.message("HTTP method not supported: " + ex.getMethod())
925
.code("METHOD_NOT_SUPPORTED")
926
.timestamp(LocalDateTime.now())
927
.path(request.getRequestURI())
928
.build();
929
930
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).body(error);
931
}
932
}
933
```
934
935
### MVC Configuration
936
937
```java { .api }
938
@Configuration
939
@EnableWebMvc
940
public class WebMvcConfig implements WebMvcConfigurer {
941
942
@Override
943
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
944
// JSON converter
945
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
946
ObjectMapper objectMapper = new ObjectMapper();
947
objectMapper.registerModule(new JavaTimeModule());
948
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
949
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
950
jsonConverter.setObjectMapper(objectMapper);
951
converters.add(jsonConverter);
952
953
// String converter
954
StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(StandardCharsets.UTF_8);
955
converters.add(stringConverter);
956
}
957
958
@Override
959
public void addInterceptors(InterceptorRegistry registry) {
960
// Request logging interceptor
961
registry.addInterceptor(new RequestLoggingInterceptor())
962
.addPathPatterns("/api/**");
963
964
// Authentication interceptor
965
registry.addInterceptor(new AuthenticationInterceptor())
966
.addPathPatterns("/api/**")
967
.excludePathPatterns("/api/auth/**", "/api/public/**");
968
969
// Rate limiting interceptor
970
registry.addInterceptor(new RateLimitingInterceptor())
971
.addPathPatterns("/api/**");
972
}
973
974
@Override
975
public void addCorsMappings(CorsRegistry registry) {
976
registry.addMapping("/api/**")
977
.allowedOrigins("http://localhost:3000", "https://myapp.com")
978
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
979
.allowedHeaders("*")
980
.allowCredentials(true)
981
.maxAge(3600);
982
}
983
984
@Override
985
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
986
configurer
987
.favorParameter(false)
988
.favorPathExtension(false)
989
.ignoreAcceptHeader(false)
990
.defaultContentType(MediaType.APPLICATION_JSON)
991
.mediaType("json", MediaType.APPLICATION_JSON)
992
.mediaType("xml", MediaType.APPLICATION_XML);
993
}
994
995
@Override
996
public void configurePathMatch(PathMatchConfigurer configurer) {
997
configurer.setUseTrailingSlashMatch(false);
998
configurer.setUseSuffixPatternMatch(false);
999
}
1000
1001
@Override
1002
public void addResourceHandlers(ResourceHandlerRegistry registry) {
1003
// Static resources
1004
registry.addResourceHandler("/static/**")
1005
.addResourceLocations("classpath:/static/")
1006
.setCacheControl(CacheControl.maxAge(Duration.ofDays(365)));
1007
1008
// API documentation
1009
registry.addResourceHandler("/docs/**")
1010
.addResourceLocations("classpath:/docs/")
1011
.setCacheControl(CacheControl.maxAge(Duration.ofHours(1)));
1012
}
1013
1014
@Override
1015
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
1016
configurer.setDefaultTimeout(30000);
1017
configurer.setTaskExecutor(asyncTaskExecutor());
1018
configurer.registerCallableInterceptors(new CallableProcessingInterceptor() {
1019
@Override
1020
public <T> void postProcess(NativeWebRequest request, Callable<T> task, Object concurrentResult) {
1021
// Custom async processing logic
1022
}
1023
});
1024
}
1025
1026
@Bean
1027
public TaskExecutor asyncTaskExecutor() {
1028
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
1029
executor.setCorePoolSize(5);
1030
executor.setMaxPoolSize(20);
1031
executor.setQueueCapacity(100);
1032
executor.setThreadNamePrefix("async-mvc-");
1033
executor.initialize();
1034
return executor;
1035
}
1036
}
1037
```
1038
1039
### File Upload and Multipart Support
1040
1041
```java { .api }
1042
@RestController
1043
@RequestMapping("/api/files")
1044
public class FileController {
1045
1046
private final FileStorageService fileStorageService;
1047
1048
public FileController(FileStorageService fileStorageService) {
1049
this.fileStorageService = fileStorageService;
1050
}
1051
1052
@PostMapping("/upload")
1053
public ResponseEntity<FileUploadResponse> uploadFile(
1054
@RequestParam("file") MultipartFile file,
1055
@RequestParam(required = false) String description) {
1056
1057
// Validate file
1058
if (file.isEmpty()) {
1059
throw new InvalidFileException("File is empty");
1060
}
1061
1062
if (file.getSize() > 10 * 1024 * 1024) { // 10MB limit
1063
throw new InvalidFileException("File size exceeds limit");
1064
}
1065
1066
String contentType = file.getContentType();
1067
if (!isValidContentType(contentType)) {
1068
throw new InvalidFileException("Invalid file type: " + contentType);
1069
}
1070
1071
// Store file
1072
StoredFile storedFile = fileStorageService.store(file, description);
1073
1074
FileUploadResponse response = FileUploadResponse.builder()
1075
.id(storedFile.getId())
1076
.filename(storedFile.getFilename())
1077
.originalFilename(file.getOriginalFilename())
1078
.contentType(contentType)
1079
.size(file.getSize())
1080
.uploadedAt(LocalDateTime.now())
1081
.build();
1082
1083
return ResponseEntity.ok(response);
1084
}
1085
1086
@PostMapping("/upload-multiple")
1087
public ResponseEntity<List<FileUploadResponse>> uploadMultipleFiles(
1088
@RequestParam("files") MultipartFile[] files) {
1089
1090
List<FileUploadResponse> responses = Arrays.stream(files)
1091
.map(file -> uploadFile(file, null).getBody())
1092
.collect(Collectors.toList());
1093
1094
return ResponseEntity.ok(responses);
1095
}
1096
1097
@GetMapping("/{fileId}")
1098
public ResponseEntity<Resource> downloadFile(@PathVariable String fileId) {
1099
StoredFile storedFile = fileStorageService.findById(fileId);
1100
Resource resource = fileStorageService.loadAsResource(storedFile.getPath());
1101
1102
return ResponseEntity.ok()
1103
.contentType(MediaType.parseMediaType(storedFile.getContentType()))
1104
.header(HttpHeaders.CONTENT_DISPOSITION,
1105
"attachment; filename=\"" + storedFile.getFilename() + "\"")
1106
.body(resource);
1107
}
1108
1109
@DeleteMapping("/{fileId}")
1110
public ResponseEntity<Void> deleteFile(@PathVariable String fileId) {
1111
fileStorageService.delete(fileId);
1112
return ResponseEntity.noContent().build();
1113
}
1114
1115
private boolean isValidContentType(String contentType) {
1116
return contentType != null && (
1117
contentType.startsWith("image/") ||
1118
contentType.equals("application/pdf") ||
1119
contentType.equals("text/plain") ||
1120
contentType.equals("application/msword") ||
1121
contentType.equals("application/vnd.openxmlformats-officedocument.wordprocessingml.document")
1122
);
1123
}
1124
}
1125
1126
// Multipart resolver configuration
1127
@Configuration
1128
public class MultipartConfig {
1129
1130
@Bean
1131
public MultipartResolver multipartResolver() {
1132
CommonsMultipartResolver resolver = new CommonsMultipartResolver();
1133
resolver.setMaxUploadSize(50 * 1024 * 1024); // 50MB
1134
resolver.setMaxUploadSizePerFile(10 * 1024 * 1024); // 10MB per file
1135
resolver.setMaxInMemorySize(1024 * 1024); // 1MB
1136
resolver.setDefaultEncoding("UTF-8");
1137
return resolver;
1138
}
1139
}
1140
```
1141
1142
### Async Processing
1143
1144
```java { .api }
1145
@RestController
1146
@RequestMapping("/api/async")
1147
public class AsyncController {
1148
1149
private final EmailService emailService;
1150
private final ReportService reportService;
1151
1152
public AsyncController(EmailService emailService, ReportService reportService) {
1153
this.emailService = emailService;
1154
this.reportService = reportService;
1155
}
1156
1157
@PostMapping("/send-email")
1158
public Callable<ResponseEntity<String>> sendEmailAsync(@RequestBody EmailRequest request) {
1159
return () -> {
1160
// This runs in a separate thread
1161
emailService.sendEmail(request.getTo(), request.getSubject(), request.getBody());
1162
return ResponseEntity.ok("Email sent successfully");
1163
};
1164
}
1165
1166
@GetMapping("/report/{id}")
1167
public DeferredResult<ResponseEntity<ReportResponse>> generateReport(@PathVariable Long id) {
1168
DeferredResult<ResponseEntity<ReportResponse>> deferredResult = new DeferredResult<>(30000L);
1169
1170
// Set timeout handler
1171
deferredResult.onTimeout(() -> {
1172
deferredResult.setResult(ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT)
1173
.body(new ReportResponse("Report generation timed out")));
1174
});
1175
1176
// Set error handler
1177
deferredResult.onError(throwable -> {
1178
deferredResult.setResult(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
1179
.body(new ReportResponse("Error generating report: " + throwable.getMessage())));
1180
});
1181
1182
// Generate report asynchronously
1183
CompletableFuture.supplyAsync(() -> reportService.generateReport(id))
1184
.whenComplete((report, throwable) -> {
1185
if (throwable != null) {
1186
deferredResult.setErrorResult(throwable);
1187
} else {
1188
deferredResult.setResult(ResponseEntity.ok(report));
1189
}
1190
});
1191
1192
return deferredResult;
1193
}
1194
1195
@PostMapping("/process-batch")
1196
public ResponseEntity<String> processBatchAsync(@RequestBody List<ProcessRequest> requests) {
1197
// Start async processing and return immediately
1198
CompletableFuture.runAsync(() -> {
1199
requests.forEach(request -> {
1200
try {
1201
processRequest(request);
1202
} catch (Exception e) {
1203
logger.error("Error processing request: " + request.getId(), e);
1204
}
1205
});
1206
});
1207
1208
return ResponseEntity.accepted().body("Batch processing started");
1209
}
1210
1211
@GetMapping("/stream-data")
1212
public StreamingResponseBody streamData() {
1213
return outputStream -> {
1214
try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream))) {
1215
for (int i = 1; i <= 1000; i++) {
1216
writer.println("Data chunk " + i);
1217
writer.flush();
1218
1219
// Simulate processing time
1220
Thread.sleep(10);
1221
}
1222
} catch (InterruptedException e) {
1223
Thread.currentThread().interrupt();
1224
}
1225
};
1226
}
1227
}
1228
```
1229
1230
### Custom Interceptors
1231
1232
```java { .api }
1233
@Component
1234
public class RequestLoggingInterceptor implements HandlerInterceptor {
1235
1236
private static final Logger logger = LoggerFactory.getLogger(RequestLoggingInterceptor.class);
1237
1238
@Override
1239
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
1240
long startTime = System.currentTimeMillis();
1241
request.setAttribute("startTime", startTime);
1242
1243
logger.info("Incoming request: {} {} from {}",
1244
request.getMethod(),
1245
request.getRequestURI(),
1246
getClientIpAddress(request));
1247
1248
return true;
1249
}
1250
1251
@Override
1252
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
1253
ModelAndView modelAndView) {
1254
// Log after handler execution
1255
}
1256
1257
@Override
1258
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
1259
Exception ex) {
1260
long startTime = (Long) request.getAttribute("startTime");
1261
long endTime = System.currentTimeMillis();
1262
long duration = endTime - startTime;
1263
1264
logger.info("Completed request: {} {} - Status: {} - Duration: {} ms",
1265
request.getMethod(),
1266
request.getRequestURI(),
1267
response.getStatus(),
1268
duration);
1269
1270
if (ex != null) {
1271
logger.error("Request completed with exception", ex);
1272
}
1273
}
1274
1275
private String getClientIpAddress(HttpServletRequest request) {
1276
String xForwardedFor = request.getHeader("X-Forwarded-For");
1277
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
1278
return xForwardedFor.split(",")[0].trim();
1279
}
1280
1281
String xRealIp = request.getHeader("X-Real-IP");
1282
if (xRealIp != null && !xRealIp.isEmpty()) {
1283
return xRealIp;
1284
}
1285
1286
return request.getRemoteAddr();
1287
}
1288
}
1289
1290
@Component
1291
public class AuthenticationInterceptor implements HandlerInterceptor {
1292
1293
private final AuthenticationService authenticationService;
1294
1295
public AuthenticationInterceptor(AuthenticationService authenticationService) {
1296
this.authenticationService = authenticationService;
1297
}
1298
1299
@Override
1300
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
1301
throws Exception {
1302
1303
String token = extractToken(request);
1304
1305
if (token == null) {
1306
response.setStatus(HttpStatus.UNAUTHORIZED.value());
1307
response.getWriter().write("{\"error\": \"Authentication token required\"}");
1308
return false;
1309
}
1310
1311
try {
1312
UserDetails user = authenticationService.validateToken(token);
1313
request.setAttribute("currentUser", user);
1314
return true;
1315
} catch (InvalidTokenException e) {
1316
response.setStatus(HttpStatus.UNAUTHORIZED.value());
1317
response.getWriter().write("{\"error\": \"Invalid authentication token\"}");
1318
return false;
1319
}
1320
}
1321
1322
private String extractToken(HttpServletRequest request) {
1323
String authHeader = request.getHeader("Authorization");
1324
if (authHeader != null && authHeader.startsWith("Bearer ")) {
1325
return authHeader.substring(7);
1326
}
1327
return null;
1328
}
1329
}
1330
```
1331
1332
Spring Web MVC provides a comprehensive framework for building web applications with clean separation of concerns, flexible request handling, and robust integration with the Spring ecosystem.