Mastering Unified Login, Interceptors, and Exception Handling in Spring Boot

This tutorial walks through implementing unified user login verification, request interception, global exception handling, and a consistent response format in Spring Boot using AOP, HandlerInterceptor, @ControllerAdvice, and ResponseBodyAdvice, complete with code examples and architectural explanations.

Architect's Guide
Architect's Guide
Architect's Guide
Mastering Unified Login, Interceptors, and Exception Handling in Spring Boot

Introduction

The following module introduces the unified functionality handling in Spring Boot, focusing on three goals: unified user login permission verification, unified data format return, and unified exception handling.

1. User Login Permission Verification

Initially, each controller method performed its own login check, leading to duplicated code, higher maintenance cost, and no clear separation from business logic. A common AOP solution was considered, but it cannot access HttpSession and cannot easily exclude specific endpoints such as login and registration.

1.1 Original Login Verification

@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/m1")
    public Object method(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute("userinfo") != null) {
            return true; // logged in
        } else {
            return false; // not logged in
        }
    }
    @RequestMapping("/m2")
    public Object method2(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute("userinfo") != null) {
            return true;
        } else {
            return false;
        }
    }
    // other methods ...
}

Problems of this approach:

Every method must repeat the same login check, even if extracted to a utility method.

Adding more controllers increases the number of calls to the login check, raising maintenance cost.

The login logic is unrelated to the actual business logic of each method.

Therefore, a common AOP method is needed.

1.2 Spring AOP Login Verification

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class UserAspect {
    @Pointcut("execution(* com.example.demo.controller..*.*(..))")
    public void pointcut() {}

    @Before("pointcut()")
    public void doBefore() {}

    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) {
        Object obj = null;
        System.out.println("Around method start");
        try {
            obj = joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        System.out.println("Around method end");
        return obj;
    }
}

The AOP approach faces two issues:

HttpSession cannot be obtained inside the aspect.

Excluding specific methods (e.g., login, registration) from interception is difficult.

1.3 Spring Interceptor Solution

Spring provides HandlerInterceptor to solve the above problems.

Create a custom interceptor that implements the preHandle method for login checks.

Register the interceptor in WebMvcConfigurer via addInterceptors.

package com.example.demo.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/** Login interceptor */
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute("userinfo") != null) {
            return true; // logged in
        }
        log.error("Current user has no access permission");
        response.setStatus(401);
        return false; // block request
    }
}
package com.example.demo.config;

import org.springframework.beans.factory.annotation.Autowired;
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 MyConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**") // intercept all requests
                .excludePathPatterns("/user/login", "/user/reg"); // exclude login & registration
    }
}

Key points: addPathPatterns defines URLs to intercept ("/**" means all). excludePathPatterns defines URLs to skip.

1.4 Interceptor Implementation Principle

All controller calls go through DispatcherServlet. During doDispatch, applyPreHandle invokes each registered HandlerInterceptor 's preHandle method before the controller logic runs.

After the pre‑handle step, the controller method executes, then post‑handle and after‑completion steps follow.

1.5 Extension: Adding a Global API Prefix

@Configuration
public class AppConfig implements WebMvcConfigurer {
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.addPathPrefix("api", c -> true); // enable prefix for all controllers
    }
}

2. Unified Exception Handling

Use @ControllerAdvice together with @ExceptionHandler to catch exceptions globally.

package com.example.demo.config;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;

/** Global exception handler */
@ControllerAdvice
public class ErrorAdvice {
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public HashMap<String, Object> exceptionAdvice(Exception e) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("code", "-1");
        result.put("msg", e.getMessage());
        return result;
    }

    @ExceptionHandler(ArithmeticException.class)
    @ResponseBody
    public HashMap<String, Object> arithmeticAdvice(ArithmeticException e) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("code", "-2");
        result.put("msg", e.getMessage());
        return result;
    }
}

When an exception occurs, the corresponding method returns a HashMap with a custom error code and message. Multiple handlers are matched from the most specific to the most general.

3. Unified Data Return Format

Two approaches are shown:

3.1 Using @ControllerAdvice + ResponseBodyAdvice

package com.example.demo.config;

import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.HashMap;

/** Global response wrapper */
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true; // apply to all responses
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("state", 1);
        result.put("msg", "");
        result.put("data", body);
        return result;
    }
}

This wrapper forces every response into a JSON object with state, msg, and data. When the controller returns a String, the wrapper would cause a conversion error; the article explains the cause and a workaround.

3.2 Preferred Soft Constraint: Custom Result Class

Define a utility class AjaxResult with static methods success and fail that build the standard map. Controllers return AjaxResult.success(data) or AjaxResult.fail(code, msg) explicitly.

package com.example.demo.common;

import java.util.HashMap;

/** Unified response object */
public class AjaxResult {
    public static HashMap<String, Object> success(Object data) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("msg", "");
        result.put("data", data);
        return result;
    }
    public static HashMap<String, Object> success(String msg, Object data) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("msg", msg);
        result.put("data", data);
        return result;
    }
    public static HashMap<String, Object> fail(int code, String msg) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("code", code);
        result.put("msg", msg);
        result.put("data", "");
        return result;
    }
    public static HashMap<String, Object> fail(int code, String msg, Object data) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("code", code);
        result.put("msg", msg);
        result.put("data", data);
        return result;
    }
}

Controllers can then use AjaxResult.success(...) or AjaxResult.fail(...) to produce a consistent JSON structure without forcing a global wrapper.

3.3 Source Code Insight of @ControllerAdvice

The article briefly explores the internal Spring MVC flow: @ControllerAdvice is a @Component that gets registered during RequestMappingHandlerAdapter.afterPropertiesSet(), which builds a cache of advice beans. When an exception occurs or a response is about to be written, the corresponding advice methods are invoked.

Overall, the article demonstrates how to achieve a clean, maintainable backend architecture by centralizing authentication, error handling, and response formatting in Spring Boot.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

aopSpring Bootglobal exception handlinghandlerinterceptorUnified response
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.