Getting Started with Spring MVC, Spring Boot, and Deep Dive into Spring Session

Spring MVC

Spring Controller

Before diving into Spring controllers, let's look at a high-level view of what a Java web service does:

Java web service overview

A Spring controller has three key responsibilities:

  • Bean configuration: applying controller annotations and managing beans
  • Loading web resources: essentially serving web pages
  • URL routing configuration: using the RequestMapping annotation

1. Using Controller Annotations

As discussed in the Spring dependency injection article, adding a @Controller annotation lets Spring manage those beans:

import org.springframework.stereotype.Controller

@Controller
class PageController {
}

2. Serving Web Pages

Serving a page typically means returning the view name from a method:

import org.springframework.stereotype.Controller

@Controller
class PageController {
    fun home(): String {
        return "homepage.html"
    }
}

Since Spring Boot automatically configures static resource loading, the full path might be src/main/resources/static/homepage.html, but we only need to specify the path relative to the static folder. If the file is inside a subfolder named views under static, the code would be:

import org.springframework.stereotype.Controller

@Controller
class PageController {
    fun home(): String {
        return "views/homepage.html"
    }
}

3. Using @RequestMapping

Routing is the process of parsing a URL and returning a web resource. Spring MVC fully supports routing via the @RequestMapping annotation:

import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.RequestMapping

@Controller
class PageController {
    @RequestMapping("/home")
    fun home(): String {
        return "views/homepage.html"
    }
}

Note: @RequestMapping is flexible but potentially less safe. @GetMapping and @PostMapping are more explicit and secure.

Handling GET Requests

Here is the standard URL format:

URL format

When you access a local server with 127.0.0.1:8080/home?id=xxx, the browser and server prepend the protocol, making the full URL http://localhost:8080/home?id=xxx. For a URL like https://www.example.com/home?id=xxx, port 443 is implied by the HTTPS protocol.

1. Defining Parameters

To receive request parameters in Spring:

import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.*

@Controller
class ItemController {
    @RequestMapping("/items")
    fun list(@RequestParam("id") id: String): String {
        return "html/items.html"
    }
}

For multiple parameters:

import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.*

@Controller
class ItemController {
    @RequestMapping("/items")
    fun list(
        @RequestParam("id") id: String,
        @RequestParam("pageNum") pageNum: Int
    ): String {
        return "html/items.html"
    }
}

The corresponding URL would be http://xxxx/items?id=xxx&pageNum=1.

Optional Parameters
@GetMapping("/items")
fun list(
    @RequestParam(name = "pageNum", required = false) pageNum: Int,
    @RequestParam("id") id: String
): String {
    return "html/items.html"
}
Returning JSON

Add the @ResponseBody annotation:

import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.*

@Controller
class ItemController {
    @GetMapping("/api/entries")
    @ResponseBody
    fun getEntries(@RequestParam("id") id: String): String {
        return "ID: $id"
    }
}

Spring MVC automatically converts the Java object to JSON. Methods returning JSON in this way are commonly called APIs.

Using @RestController is an alternative that eliminates the need to @ResponseBody on every method:

import org.springframework.web.bind.annotation.*

@RestController
class ItemController {
    @GetMapping("/api/entries")
    fun getEntries(@RequestParam("id") id: String): String {
        return "ID: $id"
    }
}

Spring Boot

If you recall from the dependency injection article, the IOC container scanned only beans annotated under the fm.douban package. Services or interfaces from other packages would not be scanned and instantiated automatically. The solution is @ComponentScan.

1. Component Scanning

  1. The main class annotated with @SpringBootApplication can specify base packages via scanBasePackages:
@SpringBootApplication(scanBasePackages = ["fm.douban.app", "fm.douban.service"])
class Application {
    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            SpringApplication.run(Application::class.java, *args)
        }
    }
}
  1. You can also create a configuration class with @ComponentScan:
@ComponentScan("fm.service", "fm.app")
class ProjectConfiguration {
    // ...
}

2. Using the Logger

The @PostConstruct annotation marks a method to run automatically after the system starts:

@PostConstruct
fun setup() {
    println("ItemController started")
    if (dataService != null) {
        println("dataService injected successfully.")
    } else {
        println("dataService injection failed.")
    }
}

However, System.out.println is unreliable in large Spring applications and can overwhelm the system. Logging is the proper approach.

1. Configuration

Add a log level configuration in application.properties:

logging.level.root=info

Or configure per package:

logging.level.fm.douban.app=info

The main log levels are:

Log levels

Setting info means debug messages are not printed.

2. Coding

Instantiate the logger and use it:

import org.slf4j.LoggerFactory
import org.springframework.web.bind.annotation.RestController
import javax.annotation.PostConstruct

@RestController
class ItemController {
    private val logger = LoggerFactory.getLogger(ItemController::class.java)

    @PostConstruct
    fun init() {
        logger.info("ItemController started")
    }
}

Besides info(), there are warn(), error(), and debug() methods.

3. Configuration Properties

Place custom properties in application.properties. Group related items and separate different prefixes with a blank line for readability.

The main benefit of configuration files is decoupling: changing a value doesn't require code modification.

Define a custom property:

track.title=My Favorite Track

Inject it using @Value:

import org.springframework.beans.factory.ennotation.Value

class ItemController {
    @Value("\${track.title}")
    private lateinit var trackTitle: String
}

Missing properties will cause startup errors.

Spring Session

1. Cookies

A cookie is a text snippet stored in the browser in key=value format, separated by ;. Browser limits on cookie size and quantity mean cookies are mainly used for login data, and optionally for small flags like isLoggedIn=true, userName=John.

Reading Cookies

Add an HttpServletRequest parameter to a controller method and call getCookies():

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Map;
import java.util.HashMap;

@Controller
public class ItemController {
    @GetMapping("/featured")
    @ResponseBody
    public Map<String, Object> jsonResponse(HttpServletRequest request) {
        Map<String, Object> data = new HashMap<>();
        data.put("status", "ok");
        Cookie[] cookies = request.getCookies();
        data.put("cookies", cookies);
        return data;
    }
}

You can also use @CookieValue, but it requires knowing the exact cookie name:

import org.springframework.web.bind.annotation.CookieValue;

@RequestMapping("/items")
fun list(@CookieValue("JSESSIONID") sessionId: String): Map<String, Any> {
    val result = mutableMapOf<String, Any>("result" to "item list")
    result["JSESSIONID"] = sessionId
    return result
}

Writing Cookies

Use HttpServletResponse.addCookie():

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Map;
import java.util.HashMap;

@Controller
public class ItemController {
    @GetMapping("/featured")
    @ResponseBody
    public Map<String, Object> jsonResponse(
            HttpServletRequest request,
            HttpServletResponse response) {
        Map<String, Object> map = new HashMap<>();
        Cookie sessionCookie = new Cookie("JSESSIONID", "session");
        sessionCookie.setDomain("127.0.0.1");
        sessionCookie.setPath("/");
        sessionCookie.setMaxAge(-1);   // browser session lifetime
        sessionCookie.setHttpOnly(false);
        response.addCookie(sessionCookie);

        Cookie[] cookies = request.getCookies();
        map.put("cookies", cookies);
        return map;
    }
}

Cookie in browser

Spring Session API

Storing login information directly in cookies is insecure. Sessions solve this: the cookie carries only the session ID. The session data stays on the server.

import com.example.exercise.model.User;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Map;
import java.util.HashMap;

@Controller
public class ItemController {
    @GetMapping("/featured")
    @ResponseBody
    public Map<String, Object> jsonResponse(
            HttpServletRequest request,
            HttpServletResponse response) {
        Map<String, Object> map = new HashMap<>();
        User currentUser = new User("User", "123456");

        HttpSession session = request.getSession();
        session.setAttribute("UserMessage", currentUser);

        User storedUser = (User) session.getAttribute("UserMessage");
        map.put("user", storedUser);
        return map;
    }
}

Even though we didn't explicitly create a cookie, the container sets the JSESSIONID cookie to track the session. The session ID is typically encrypted, so the client cannot read the actual session content.

Cookies are stored on the client and usually limited to 4KB. Sessions are stored on the server; while there is no hard size limit, storing too much impacts performance.

Spring Session Configuration

The system automatically places JSESSIONID in a default cookie, but you can customize cookie properties.

Add the dependency:

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-core</artifactId>
</dependency>

Then configure session management:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
import java.util.concurrent.ConcurrentHashMap;

@Configuration
@EnableSpringHttpSession
public class SessionConfiguration {

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("JSESSIONID");
        serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
        serializer.setCookiePath("/");
        serializer.setUseHttpOnlyCookie(false);
        serializer.setCookieMaxAge(24 * 60 * 60);
        return serializer;
    }

    @Bean
    public MapSessionRepository sessionRepository() {
        return new MapSessionRepository(new ConcurrentHashMap<>());
    }
}

Spring Request Interceptor

Interceptors can redirect unauthenticated users to a login page. Spring provides the HandlerInterceptor interface.

Creating an Interceptor

Create a class implementing HandlerInterceptor:

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

public class AuthInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse resposne,
                             Object handler) throws Exception {
        // pre-processing logic, e.g., checking session login
        return true; // must return true to proceed
    }

    @Override
    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler,
                           ModelAndView modelAndView) throws Exception {
        // post-processing, e.g., logging
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception ex) throws Exception {
        // after view rendering
    }
}

Configuring the Interceptor

Implement WebMvcConfigurer and register the interceptor:

import com.example.exercise.intercepter.AuthInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthInterceptor())
                .addPathPatterns("/**");
    }
}

Tags: Spring MVC Spring Boot Spring Session java Cookies

Posted on Sun, 28 Jun 2026 17:25:29 +0000 by johnnyblaze1980