在Spring Boot应用开发中,实现用户登录校验、统一的异常处理和数据返回格式是构建健壮、可维护后端服务的基石。手动在每个Controller方法中重复这些逻辑不仅低效,而且难以维护。本文将深入探讨如何使用Spring MVC提供的机制,高效地实现这些统一功能处理。
一、用户登录权限效验
用户登录权限的校验方式经历了从分散到统一的演进过程,这背后是代码可维护性和开发效率的不断提升。
1.1 最初分散的验证方式及其弊端
最初,我们可能会在每个需要验证的方法内部手动检查Session:
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/m1")
public Object method(HttpServletRequest request){
// 有 session 就获取,没有不会创建
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
// 说明已经登录,业务处理
return true;
} else {
// 未登录
return false;
}
}
// 其他方法需要重复同样的逻辑...
}
这种方式的缺点显而易见:
- 代码冗余:相同的验证逻辑在每个方法中重复。
- 维护成本高:随着控制器增多,需要修改的验证点也呈线性增长。
- 关注点混淆:核心业务逻辑与权限验证代码耦合在一起。
1.2 为何Spring AOP不是最佳选择?
你可能会想到使用Spring AOP(如@Before通知)来统一处理。但这会遇到两个棘手问题:
- 难以获取请求对象:在普通的AOP切面中,不方便直接获取
HttpServletRequest和HttpSession对象。
- 排除规则复杂:像登录
/login、注册/reg这样的接口本身就不应被拦截,使用AOP定义复杂的排除逻辑比较麻烦。
1.3 Spring拦截器:理想的解决方案
Spring MVC提供了HandlerInterceptor接口,完美解决了上述问题。实现分为两步:
- 创建自定义拦截器:实现
HandlerInterceptor接口的preHandle方法。
- 注册拦截器:通过
WebMvcConfigurer的addInterceptors方法将其加入系统。
1.3.1 准备工作:基础Controller
首先,我们创建包含登录等基础功能的Controller。
package com.example.demo.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@RequestMapping("/login")
public boolean login(HttpServletRequest request, String username, String password){
if (StringUtils.hasLength(username) && StringUtils.hasLength(password)) {
if ("admin".equals(username) && "admin".equals(password)) {
// 登录成功
HttpSession session = request.getSession();
session.setAttribute("userinfo", "admin");
return true;
} else {
return false;
}
}
return false;
}
@RequestMapping("/getinfo")
public String getInfo(){
log.debug("执行了 getinfo 方法");
return "执行了 getinfo 方法";
}
@RequestMapping("/reg")
public String reg(){
log.debug("执行了 reg 方法");
return "执行了 reg 方法";
}
}
1.3.2 核心:自定义登录拦截器
接下来,创建核心的登录拦截器。
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;
/**
* 登录拦截器
*/
@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; // 放行
}
log.error("当前用户没有访问权限");
response.setStatus(401);
return false; // 拦截
}
}
preHandle方法返回true表示放行请求,返回false则拦截请求并结束流程。

1.3.3 配置:将拦截器加入系统
最后,通过配置类注册拦截器,并定义拦截与排除的路径。
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("/**") // 拦截所有请求
.excludePathPatterns("/user/login") // 排除登录接口
.excludePathPatterns("/user/reg"); // 排除注册接口
}
}
addPathPatterns:定义需要拦截的URL模式,/**表示拦截所有请求。
excludePathPatterns:定义需要排除的URL,通常包括登录、注册、静态资源等。
补充说明:拦截器 vs 过滤器
过滤器(Filter)是Servlet容器层面的组件,触发时机比拦截器更早(在Spring MVC接管请求之前),因此通常不用于处理需要Spring上下文支持的业务逻辑,如基于注解的权限验证。
1.4 拦截器实现原理浅析
在没有拦截器时,请求的调用链是直接的:用户 -> DispatcherServlet -> Controller。

加入拦截器后,流程变为:用户 -> DispatcherServlet -> 拦截器(preHandle) -> Controller。这实际上是AOP环绕通知思想的一种实现。

源码层面,Spring MVC的核心调度器DispatcherServlet在其doDispatch方法中,会调用HandlerExecutionChain的applyPreHandle方法,该方法会遍历并执行所有注册的HandlerInterceptor的preHandle方法。这也就是我们自定义逻辑被调用的地方。


从设计模式角度看,DispatcherServlet扮演了代理对象的角色,在调用真实目标对象(Controller)前后,插入了拦截器的处理逻辑。

1.5 扩展:统一添加访问前缀
有时我们需要为所有API接口统一添加前缀(例如/api),这也可以通过实现WebMvcConfigurer轻松完成。
@Configuration
public class AppConfig implements WebMvcConfigurer {
// 所有的接口添加 api 前缀
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("api", c -> true);
}
}
二、统一异常处理
程序运行中难免会出现异常,如果不加处理,Spring Boot默认会返回一堆包含错误栈信息的HTML页面或JSON,这对前端和用户体验都不友好。使用@ControllerAdvice和@ExceptionHandler可以实现优雅的全局异常处理。
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;
/**
* 统一处理异常
*/
@ControllerAdvice
public class ErrorAdvice {
@ExceptionHandler(Exception.class) // 处理所有Exception
@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;
}
}
@ControllerAdvice:声明一个控制器通知类,用于全局处理。
@ExceptionHandler:标注在方法上,指定要处理的异常类型。当出现对应异常时,该方法会被调用。
异常匹配规则:当有多个@ExceptionHandler时,会优先匹配最精确(子类)的异常处理器。例如,一个NullPointerException会优先匹配NullPointerException.class的处理方法,而不是Exception.class的方法。
示例:在Controller中制造一个空指针异常。
@RestController
@RequestMapping("/u")
public class UserController {
@RequestMapping("/index")
public String index(){
Object obj = null;
int i = obj.hashCode(); // 这里会抛出NullPointerException
return "Hello,User Index.";
}
}
启用统一异常处理后,前端收到的将是规整的JSON错误信息,而非凌乱的错误栈。

三、统一数据返回格式
统一的数据返回格式便于前后端协作,降低沟通成本,是团队开发中的重要规范。实现方式主要有两种:硬性封装和软性约束。
3.1 硬性封装:使用 @ControllerAdvice + ResponseBodyAdvice
这种方式通过实现ResponseBodyAdvice接口,在数据写出响应体之前,对所有控制器的返回进行强制包装。
package com.example.demo.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
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;
/**
* 统一数据返回封装
*/
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true; // 对所有返回类型生效
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 如果已经是HashMap(可能是异常处理返回的),直接返回
if (body instanceof HashMap) {
return body;
}
// 如果返回类型是String,需要特殊处理(String类型转换器问题)
if (body instanceof String) {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.writeValueAsString(AjaxResult.success(body));
}
// 普通对象,进行统一封装
return AjaxResult.success(body);
}
}
注意:这里需要处理返回类型为String的特殊情况,因为Spring对String类型的消息转换器与其他类型不同,直接包装可能导致类型转换异常。

3.2 软性约束:自定义统一返回对象
更常见的做法是定义一个通用的返回类(如AjaxResult、R、Result等),要求开发者在Controller中显式返回这个类的对象。这种方式更灵活,也便于在返回体中添加额外的业务状态信息。
package com.example.demo.common;
import java.util.HashMap;
/**
* 自定义的统一返回对象
*/
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;
}
}
在Controller中使用:
@RequestMapping("/getinfo")
public HashMap<String, Object> getInfo(){
// ... 业务逻辑
return AjaxResult.success("查询成功", userData);
}
将软性约束与硬性封装的ResponseBodyAdvice结合使用效果更佳:ResponseBodyAdvice的beforeBodyWrite方法会判断,如果返回值已经是AjaxResult类型(HashMap),则直接返回,否则再进行封装。这样就同时保证了灵活性和规范性。
3.3 @ControllerAdvice 源码机制简介
@ControllerAdvice本身是一个派生自@Component的注解,这意味着它会被Spring容器扫描并管理。Spring MVC在初始化RequestMappingHandlerAdapter时,会通过afterPropertiesSet()方法调用initControllerAdviceCache(),该方法会查找容器中所有带有@ControllerAdvice注解的Bean,并将其缓存起来。
当发生异常或返回数据时,Spring MVC会从缓存中找到相应的Advice Bean(即我们的ErrorAdvice或ResponseAdvice),并调用其中匹配的@ExceptionHandler或ResponseBodyAdvice方法,从而完成全局处理逻辑。
总结
通过拦截器实现统一登录校验,通过@ControllerAdvice和@ExceptionHandler实现统一异常处理,通过ResponseBodyAdvice或自定义返回类实现统一数据返回格式,这三者构成了Spring Boot后端服务统一功能处理的核心框架。掌握这些技术,能够显著提升代码的整洁度、可维护性和团队协作效率,是每一位后端开发者必须练好的基本功。如果你在实践过程中遇到问题,欢迎到云栈社区的Java板块与大家交流探讨。