Menu

Spring Boot Topic

Spring Core and MVC

1. Spring Core Framework

1.1 Inversion of Control (IoC) Container

What is IoC?

Inversion of Control is a principle where the control of object creation and dependency injection is transferred from the application code to the Spring container.

ApplicationContext

The central interface for providing configuration to an application.

// Different ways to create ApplicationContext
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
ApplicationContext context = new FileSystemXmlApplicationContext("/path/to/applicationContext.xml");

// Spring Boot way (auto-configured)
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Application.class, args);
    }
}

1.2 Dependency Injection (DI)

Types of Dependency Injection

Constructor Injection:

@Service
public class UserService {
    private final UserRepository userRepository;
    
    // Constructor injection
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

Setter Injection:

@Service
public class NotificationService {
    private EmailService emailService;
    
    // Setter injection
    @Autowired
    public void setEmailService(EmailService emailService) {
        this.emailService = emailService;
    }
}

Field Injection:

@Service
public class OrderService {
    @Autowired
    private PaymentService paymentService;
}

1.3 Bean Configuration

Java-based Configuration

@Configuration
@ComponentScan("com.example")
@PropertySource("classpath:application.properties")
public class AppConfig {
    
    @Bean
    public DataSource dataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
        dataSource.setUsername("root");
        dataSource.setPassword("password");
        return dataSource;
    }
    
    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
    
    @Bean
    @Profile("dev")
    public DevelopmentService devService() {
        return new DevelopmentService();
    }
    
    @Bean
    @Profile("prod")
    public ProductionService prodService() {
        return new ProductionService();
    }
}

XML-based Configuration

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.example"/>
    <context:property-placeholder location="classpath:application.properties"/>
    
    <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
        <property name="jdbcUrl" value="${database.url}"/>
        <property name="username" value="${database.username}"/>
        <property name="password" value="${database.password}"/>
    </bean>
    
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <constructor-arg ref="dataSource"/>
    </bean>
</beans>

1.4 Bean Scopes

@Component
@Scope("singleton") // Default - one instance per Spring container
public class SingletonService { }

@Component
@Scope("prototype") // New instance every time
public class PrototypeService { }

@Component
@Scope("request") // One instance per HTTP request
public class RequestScopedService { }

@Component
@Scope("session") // One instance per HTTP session
public class SessionScopedService { }

@Component
@Scope("application") // One instance per ServletContext
public class ApplicationScopedService { }

1.5 Lifecycle Callbacks

@Component
public class LifecycleExample implements InitializingBean, DisposableBean {
    
    @PostConstruct
    public void customInit() {
        System.out.println("Custom init method");
    }
    
    @PreDestroy
    public void customDestroy() {
        System.out.println("Custom destroy method");
    }
    
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("After properties set");
    }
    
    @Override
    public void destroy() throws Exception {
        System.out.println("Bean destruction");
    }
}

2. Spring MVC Framework

2.1 MVC Architecture

Model - Contains the data of the application
View - Represents the visualization of the data
Controller - Handles the user request and prepares the model

2.2 DispatcherServlet

The front controller that handles all incoming requests.

// Traditional web.xml configuration
<web-app>
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/spring-servlet.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

// Java Config (Spring Boot)
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
    // MVC configuration
}

2.3 Controller Types

Traditional @Controller

@Controller
@RequestMapping("/users")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @GetMapping
    public String listUsers(Model model) {
        model.addAttribute("users", userService.findAll());
        return "users/list"; // View name
    }
    
    @GetMapping("/{id}")
    public String getUser(@PathVariable Long id, Model model) {
        User user = userService.findById(id);
        model.addAttribute("user", user);
        return "users/detail";
    }
    
    @GetMapping("/create")
    public String showCreateForm(Model model) {
        model.addAttribute("user", new User());
        return "users/create";
    }
    
    @PostMapping
    public String createUser(@ModelAttribute User user, BindingResult result) {
        if (result.hasErrors()) {
            return "users/create";
        }
        userService.save(user);
        return "redirect:/users";
    }
}

RESTful @RestController

@RestController
@RequestMapping("/api/users")
public class UserApiController {
    
    @Autowired
    private UserService userService;
    
    @GetMapping
    public ResponseEntity<List<User>> getAllUsers() {
        return ResponseEntity.ok(userService.findAll());
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        User user = userService.findById(id);
        return ResponseEntity.ok(user);
    }
    
    @PostMapping
    public ResponseEntity<User> createUser(@RequestBody @Valid User user) {
        User savedUser = userService.save(user);
        return ResponseEntity.status(HttpStatus.CREATED).body(savedUser);
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User user) {
        user.setId(id);
        User updatedUser = userService.save(user);
        return ResponseEntity.ok(updatedUser);
    }
    
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.delete(id);
        return ResponseEntity.noContent().build();
    }
}

2.4 Request Mapping

@Controller
@RequestMapping("/products")
public class ProductController {
    
    // Basic mapping
    @GetMapping
    public String listProducts() { return "products/list"; }
    
    // Path variables
    @GetMapping("/{id}")
    public String getProduct(@PathVariable Long id) { }
    
    @GetMapping("/category/{catId}")
    public String getByCategory(@PathVariable("catId") Long categoryId) { }
    
    // Request parameters
    @GetMapping("/search")
    public String searchProducts(@RequestParam String keyword,
                                @RequestParam(defaultValue = "1") int page) { }
    
    // Multiple HTTP methods
    @RequestMapping(value = "/{id}", method = {RequestMethod.GET, RequestMethod.HEAD})
    public String getProductHead(@PathVariable Long id) { }
    
    // Content type mapping
    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE,
                produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Product> createProduct(@RequestBody Product product) { }
    
    // Headers mapping
    @GetMapping(headers = "X-API-Version=1")
    public String apiV1() { }
}

2.5 Handler Method Parameters

@Controller
public class ParameterExamplesController {
    
    // Common parameters
    @GetMapping("/example")
    public String example(
            // URL parts
            @PathVariable Long id,
            @RequestParam String name,
            
            // Request info
            HttpServletRequest request,
            HttpServletResponse response,
            HttpSession session,
            
            // Request body
            @RequestBody String body,
            @ModelAttribute User user,
            
            // Request headers
            @RequestHeader("User-Agent") String userAgent,
            @RequestHeader Map<String, String> headers,
            
            // Cookies
            @CookieValue("JSESSIONID") String sessionId,
            
            // Model and view
            Model model,
            ModelMap modelMap,
            
            // Errors and validation
            BindingResult bindingResult,
            Errors errors,
            
            // Session attributes
            @SessionAttribute("user") User sessionUser,
            
            // Request attributes
            @RequestAttribute("traceId") String traceId) {
        
        return "view";
    }
}

2.6 Response Handling

@Controller
public class ResponseExamplesController {
    
    // Return view name
    @GetMapping("/view")
    public String showView() {
        return "view-name";
    }
    
    // Return view with model
    @GetMapping("/model")
    public String showModel(Model model) {
        model.addAttribute("data", "value");
        return "view";
    }
    
    // Return ModelAndView
    @GetMapping("/model-and-view")
    public ModelAndView showModelAndView() {
        ModelAndView mav = new ModelAndView("view");
        mav.addObject("data", "value");
        return mav;
    }
    
    // Redirect
    @PostMapping("/create")
    public String create() {
        // Process creation
        return "redirect:/success";
    }
    
    // Forward
    @GetMapping("/forward")
    public String forward() {
        return "forward:/target";
    }
    
    // Return response body directly
    @GetMapping("/raw")
    @ResponseBody
    public String rawResponse() {
        return "Raw response";
    }
    
    // Return ResponseEntity for full control
    @GetMapping("/custom")
    public ResponseEntity<String> customResponse() {
        HttpHeaders headers = new HttpHeaders();
        headers.add("Custom-Header", "value");
        
        return ResponseEntity.status(HttpStatus.OK)
                .headers(headers)
                .body("Custom response");
    }
}

2.7 Exception Handling

Controller-based Exception Handling

@Controller
@ControllerAdvice
public class GlobalExceptionHandler {
    
    // Handle specific exception
    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException ex) {
        ErrorResponse error = new ErrorResponse("USER_NOT_FOUND", ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }
    
    // Handle multiple exceptions
    @ExceptionHandler({ValidationException.class, ConstraintViolationException.class})
    public ResponseEntity<ErrorResponse> handleValidationExceptions(Exception ex) {
        ErrorResponse error = new ErrorResponse("VALIDATION_ERROR", ex.getMessage());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
    }
    
    // Handle all exceptions
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleAllExceptions(Exception ex) {
        ErrorResponse error = new ErrorResponse("INTERNAL_ERROR", "An error occurred");
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
    }
}

Using @ResponseStatus

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "User not found")
public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(String message) {
        super(message);
    }
}

2.8 Validation

@Data
public class User {
    @NotNull(message = "ID cannot be null")
    private Long id;
    
    @NotBlank(message = "Name is required")
    @Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
    private String name;
    
    @Email(message = "Invalid email format")
    private String email;
    
    @Min(value = 18, message = "Age must be at least 18")
    @Max(value = 100, message = "Age must be less than 100")
    private Integer age;
    
    @Pattern(regexp = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).{8,}$", 
             message = "Password must contain at least 8 characters with uppercase, lowercase and numbers")
    private String password;
}

@RestController
public class UserController {
    
    @PostMapping("/users")
    public ResponseEntity<User> createUser(@RequestBody @Valid User user, 
                                         BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            // Handle validation errors
            throw new ValidationException("Validation failed");
        }
        User savedUser = userService.save(user);
        return ResponseEntity.ok(savedUser);
    }
}

2.9 Interceptors

@Component
public class LoggingInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) throws Exception {
        long startTime = System.currentTimeMillis();
        request.setAttribute("startTime", startTime);
        
        String requestInfo = String.format("Request: %s %s", 
            request.getMethod(), request.getRequestURI());
        System.out.println(requestInfo);
        
        return true; // Continue with execution
    }
    
    @Override
    public void postHandle(HttpServletRequest request, 
                         HttpServletResponse response, 
                         Object handler, 
                         ModelAndView modelAndView) throws Exception {
        long startTime = (Long) request.getAttribute("startTime");
        long executionTime = System.currentTimeMillis() - startTime;
        
        System.out.printf("Request processed in %d ms%n", executionTime);
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, 
                              HttpServletResponse response, 
                              Object handler, 
                              Exception ex) throws Exception {
        if (ex != null) {
            System.out.println("Request completed with exception: " + ex.getMessage());
        } else {
            System.out.println("Request completed successfully");
        }
    }
}

// Register interceptor
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    
    @Autowired
    private LoggingInterceptor loggingInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loggingInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/static/**", "/health");
    }
}

2.10 View Resolution

Configuration

@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
    
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.jsp("/WEB-INF/views/", ".jsp");
        // or for Thymeleaf
        // registry.thymeleaf();
    }
    
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/static/");
    }
}

Thymeleaf Configuration

@Configuration
public class ThymeleafConfig {
    
    @Bean
    public SpringTemplateEngine templateEngine() {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver());
        templateEngine.addDialect(new SpringSecurityDialect());
        return templateEngine;
    }
    
    @Bean
    public ITemplateResolver templateResolver() {
        SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
        resolver.setPrefix("classpath:/templates/");
        resolver.setSuffix(".html");
        resolver.setTemplateMode(TemplateMode.HTML);
        resolver.setCharacterEncoding("UTF-8");
        return resolver;
    }
    
    @Bean
    public ThymeleafViewResolver viewResolver() {
        ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
        viewResolver.setTemplateEngine(templateEngine());
        viewResolver.setCharacterEncoding("UTF-8");
        return viewResolver;
    }
}

2.11 File Upload

@Controller
public class FileUploadController {
    
    @GetMapping("/upload")
    public String showUploadForm() {
        return "upload-form";
    }
    
    @PostMapping("/upload")
    public String handleFileUpload(@RequestParam("file") MultipartFile file,
                                 Model model) {
        if (file.isEmpty()) {
            model.addAttribute("message", "Please select a file to upload");
            return "upload-form";
        }
        
        try {
            // Save file
            byte[] bytes = file.getBytes();
            Path path = Paths.get("uploads/" + file.getOriginalFilename());
            Files.write(path, bytes);
            
            model.addAttribute("message", 
                "File uploaded successfully: " + file.getOriginalFilename());
        } catch (IOException e) {
            model.addAttribute("message", "File upload failed: " + e.getMessage());
        }
        
        return "upload-result";
    }
}

// Configuration for file upload
@Configuration
public class FileUploadConfig {
    
    @Bean
    public MultipartResolver multipartResolver() {
        CommonsMultipartResolver resolver = new CommonsMultipartResolver();
        resolver.setMaxUploadSize(10485760); // 10MB
        resolver.setMaxUploadSizePerFile(2097152); // 2MB per file
        return resolver;
    }
}

3. Spring Core Advanced Features

3.1 AOP (Aspect-Oriented Programming)

@Aspect
@Component
public class LoggingAspect {
    
    private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
    
    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        logger.info("Before method: {}", joinPoint.getSignature().getName());
    }
    
    @AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", 
                   returning = "result")
    public void logAfterReturning(JoinPoint joinPoint, Object result) {
        logger.info("Method {} returned: {}", joinPoint.getSignature().getName(), result);
    }
    
    @AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", 
                  throwing = "error")
    public void logAfterThrowing(JoinPoint joinPoint, Throwable error) {
        logger.error("Method {} threw exception: {}", 
                    joinPoint.getSignature().getName(), error.getMessage());
    }
    
    @Around("execution(* com.example.service.*.*(..))")
    public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        
        Object result = joinPoint.proceed();
        
        long executionTime = System.currentTimeMillis() - start;
        logger.info("Method {} executed in {} ms", 
                   joinPoint.getSignature().getName(), executionTime);
        
        return result;
    }
}

3.2 Event Handling

// Custom event
public class UserCreatedEvent extends ApplicationEvent {
    private final User user;
    
    public UserCreatedEvent(Object source, User user) {
        super(source);
        this.user = user;
    }
    
    public User getUser() {
        return user;
    }
}

// Event publisher
@Service
public class UserService {
    
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    
    public User createUser(User user) {
        // Save user
        User savedUser = userRepository.save(user);
        
        // Publish event
        eventPublisher.publishEvent(new UserCreatedEvent(this, savedUser));
        
        return savedUser;
    }
}

// Event listener
@Component
public class UserEventListener {
    
    @EventListener
    public void handleUserCreated(UserCreatedEvent event) {
        User user = event.getUser();
        // Send welcome email, create audit log, etc.
        System.out.println("User created: " + user.getEmail());
    }
    
    @Async
    @EventListener
    public void handleUserCreatedAsync(UserCreatedEvent event) {
        // Async processing
    }
    
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleAfterCommit(UserCreatedEvent event) {
        // Process after transaction commit
    }
}

This comprehensive guide covers the essential aspects of Spring Core and Spring MVC, from basic dependency injection to advanced features like AOP and event handling. The framework's power lies in its flexibility and the extensive ecosystem built around these core concepts.

 

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?