找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3011

积分

0

好友

403

主题
发表于 昨天 12:50 | 查看: 7| 回复: 0

在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通知)来统一处理。但这会遇到两个棘手问题:

  1. 难以获取请求对象:在普通的AOP切面中,不方便直接获取HttpServletRequestHttpSession对象。
  2. 排除规则复杂:像登录/login、注册/reg这样的接口本身就不应被拦截,使用AOP定义复杂的排除逻辑比较麻烦。

1.3 Spring拦截器:理想的解决方案

Spring MVC提供了HandlerInterceptor接口,完美解决了上述问题。实现分为两步:

  1. 创建自定义拦截器:实现HandlerInterceptor接口的preHandle方法。
  2. 注册拦截器:通过WebMvcConfigureraddInterceptors方法将其加入系统。

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则拦截请求并结束流程。

拦截器preHandle方法示意图

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方法中,会调用HandlerExecutionChainapplyPreHandle方法,该方法会遍历并执行所有注册的HandlerInterceptorpreHandle方法。这也就是我们自定义逻辑被调用的地方。

DispatcherServlet处理请求日志

自定义拦截器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错误信息,而非凌乱的错误栈。

统一异常处理返回的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类型的消息转换器与其他类型不同,直接包装可能导致类型转换异常。

处理String返回类型的代码

3.2 软性约束:自定义统一返回对象

更常见的做法是定义一个通用的返回类(如AjaxResultRResult等),要求开发者在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结合使用效果更佳:ResponseBodyAdvicebeforeBodyWrite方法会判断,如果返回值已经是AjaxResult类型(HashMap),则直接返回,否则再进行封装。这样就同时保证了灵活性和规范性。

3.3 @ControllerAdvice 源码机制简介

@ControllerAdvice本身是一个派生自@Component的注解,这意味着它会被Spring容器扫描并管理。Spring MVC在初始化RequestMappingHandlerAdapter时,会通过afterPropertiesSet()方法调用initControllerAdviceCache(),该方法会查找容器中所有带有@ControllerAdvice注解的Bean,并将其缓存起来。

当发生异常或返回数据时,Spring MVC会从缓存中找到相应的Advice Bean(即我们的ErrorAdviceResponseAdvice),并调用其中匹配的@ExceptionHandlerResponseBodyAdvice方法,从而完成全局处理逻辑。

总结

通过拦截器实现统一登录校验,通过@ControllerAdvice@ExceptionHandler实现统一异常处理,通过ResponseBodyAdvice或自定义返回类实现统一数据返回格式,这三者构成了Spring Boot后端服务统一功能处理的核心框架。掌握这些技术,能够显著提升代码的整洁度、可维护性和团队协作效率,是每一位后端开发者必须练好的基本功。如果你在实践过程中遇到问题,欢迎到云栈社区的Java板块与大家交流探讨。




上一篇:SpringBoot项目jar与war打包部署差异详解与实战指南
下一篇:如何解决RAG检索关键词被切断?六大文本分块策略详解
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-7 16:54 , Processed in 0.591988 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表