JavaEE进阶——SpringBoot拦截器详解:从入门到实战

目录

[Spring Boot统一功能处理详解](#Spring Boot统一功能处理详解)

[1. 拦截器详解](#1. 拦截器详解)

[1.1 什么是拦截器](#1.1 什么是拦截器)

[1.2 拦截器快速入门](#1.2 拦截器快速入门)

[1.2.1 定义拦截器](#1.2.1 定义拦截器)

[1.2.2 注册配置拦截器](#1.2.2 注册配置拦截器)

[1.2.3 拦截器执行流程](#1.2.3 拦截器执行流程)

[1.3 拦截器详解](#1.3 拦截器详解)

[1.3.1 拦截路径配置](#1.3.1 拦截路径配置)

[1.3.2 登录校验拦截器实现](#1.3.2 登录校验拦截器实现)

[1.4 DispatcherServlet源码分析](#1.4 DispatcherServlet源码分析)

[1.4.1 什么是DispatcherServlet](#1.4.1 什么是DispatcherServlet)

[1.4.2 初始化过程](#1.4.2 初始化过程)

[1.4.3 处理请求流程](#1.4.3 处理请求流程)

[1.4.4 适配器模式在Spring MVC中的应用](#1.4.4 适配器模式在Spring MVC中的应用)

[2. 统一数据返回格式](#2. 统一数据返回格式)

[2.1 为什么需要统一数据返回格式](#2.1 为什么需要统一数据返回格式)

[2.2 快速入门](#2.2 快速入门)

[2.3 存在的问题及解决方案](#2.3 存在的问题及解决方案)

[2.4 统一结果类Result](#2.4 统一结果类Result)

[3. 统一异常处理](#3. 统一异常处理)

[3.1 为什么需要统一异常处理](#3.1 为什么需要统一异常处理)

[3.2 基本实现](#3.2 基本实现)

[3.3 精细化异常处理](#3.3 精细化异常处理)

[3.4 自定义业务异常](#3.4 自定义业务异常)

[3.5 异常处理最佳实践](#3.5 异常处理最佳实践)

[4. 案例代码详解](#4. 案例代码详解)

[4.1 登录页面](#4.1 登录页面)

[4.2 图书列表](#4.2 图书列表)

[4.3 其他功能](#4.3 其他功能)

[5. 总结](#5. 总结)

用适配器和不用适配器这两者有啥本质的区别?

[用适配器 vs 不用适配器:本质区别解析](#用适配器 vs 不用适配器:本质区别解析)

[1. 场景设定](#1. 场景设定)

[2. 只有"不用适配器"的世界 (If-Else 地狱)](#2. 只有“不用适配器”的世界 (If-Else 地狱))

这种方式的本质缺陷:

[3. 使用"适配器模式"的世界 (多态的胜利)](#3. 使用“适配器模式”的世界 (多态的胜利))

这种方式的本质优势:

[4. 总结对比表](#4. 总结对比表)


Spring Boot统一功能处理详解

1. 拦截器详解

1.1 什么是拦截器

拦截器(Interceptor)是Spring框架提供的一种机制,用于在请求处理的不同阶段(请求前、请求后、视图渲染后)插入自定义逻辑。它类似于Web开发中的过滤器(Filter),但拦截器是基于Java反射机制实现的,工作在DispatcherServlet之后,属于Spring MVC框架的一部分。

拦截器的主要应用场景包括:

  • 用户身份认证和授权
  • 日志记录
  • 性能监控
  • 防止表单重复提交
  • 处理国际化
  • 统一异常处理

1.2 拦截器快速入门

1.2.1 定义拦截器

首先我们来看如何定义一个基本的拦截器。以下是一个登录拦截器的完整代码:

java 复制代码
// 导入slf4j日志框架的注解,用于生成日志记录器
import lombok.extern.slf4j.Slf4j;

// 将此类标记为Spring组件,使其被Spring容器管理
import org.springframework.stereotype.Component;

// 导入Spring MVC的拦截器接口
import org.springframework.web.servlet.HandlerInterceptor;

// 导入Servlet API中的请求、响应和会话对象
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

/**
 * 登录拦截器
 * 实现HandlerInterceptor接口,重写其方法
 * @Slf4j注解会自动生成一个名为log的Logger对象,用于记录日志
 */
@Slf4j
@Component // 将此类注册为Spring Bean,使其可以被自动注入
public class LoginInterceptor implements HandlerInterceptor {

    /**
     * preHandle方法:在Controller方法执行前调用
     * 返回true表示放行,继续执行后续操作
     * 返回false表示拦截,中断请求处理
     * 
     * @param request  HTTP请求对象
     * @param response HTTP响应对象
     * @param handler  当前请求的处理器(Controller方法)
     * @return boolean 是否继续处理请求
     * @throws Exception 可能抛出的异常
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 记录日志,表示拦截器在目标方法执行前被触发
        log.info("LoginInterceptor目标方法执行前执行..");
        
        // 返回true,表示放行请求,继续执行Controller中的方法
        return true;
    }

    /**
     * postHandle方法:在Controller方法执行后,视图渲染前调用
     * 
     * @param request  HTTP请求对象
     * @param response HTTP响应对象
     * @param handler  当前请求的处理器
     * @param modelAndView 视图和模型数据对象,可用于修改视图或添加属性
     * @throws Exception 可能抛出的异常
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, 
                          ModelAndView modelAndView) throws Exception {
        // 记录日志,表示拦截器在目标方法执行后被触发
        log.info("LoginInterceptor目标方法执行后执行");
    }

    /**
     * afterCompletion方法:在整个请求完成,视图渲染完毕后调用
     * 这是拦截器的最后一个方法,通常用于资源清理
     * 
     * @param request  HTTP请求对象
     * @param response HTTP响应对象
     * @param handler  当前请求的处理器
     * @param ex       在处理过程中发生的异常,如果没有异常则为null
     * @throws Exception 可能抛出的异常
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, 
                               Exception ex) throws Exception {
        // 记录日志,表示拦截器在视图渲染完毕后执行
        log.info("LoginInterceptor视图渲染完毕后执行,最后执行");
    }
}
1.2.2 注册配置拦截器

定义了拦截器后,还需要将其注册到Spring MVC中。以下是注册配置拦截器的代码:

java 复制代码
// 导入Spring的依赖注入注解
import org.springframework.beans.factory.annotation.Autowired;

// 标识此类为配置类,替代xml配置
import org.springframework.context.annotation.Configuration;

// 导入Web MVC配置相关的接口和类
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Web配置类
 * 实现WebMvcConfigurer接口,用于自定义Spring MVC配置
 */
@Configuration // 标记为配置类,Spring启动时会加载此类
public class WebConfig implements WebMvcConfigurer {

    // 自动注入之前定义的LoginInterceptor拦截器
    @Autowired
    private LoginInterceptor loginInterceptor;

    /**
     * 添加拦截器到注册表
     * 该方法会被Spring MVC自动调用
     * 
     * @param registry 拦截器注册表,用于注册和配置拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册自定义拦截器
        // addPathPatterns设置拦截路径,"/**"表示拦截所有请求
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**");
    }
}
1.2.3 拦截器执行流程

当我们启动服务并访问任意请求时,可以通过日志观察到拦截器的执行顺序:

  1. 首先执行preHandle()方法
  2. 然后执行Controller中的目标方法
  3. 接着执行postHandle()方法
  4. 最后执行afterCompletion()方法

如果preHandle()方法返回false,则后续的Controller方法和拦截器的其他方法都不会被执行,请求被拦截。

1.3 拦截器详解

1.3.1 拦截路径配置

在实际应用中,我们通常需要精确控制哪些路径需要拦截,哪些路径不需要拦截。例如,登录页面本身就不需要进行登录验证。以下是更完善的拦截器配置:

java 复制代码
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;
import java.util.Arrays;
import java.util.List;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Autowired
    private LoginInterceptor loginInterceptor;
    
    // 定义不需要拦截的路径集合
    private List<String> excludePaths = Arrays.asList(
        "/user/login",       // 登录接口
        "/**/*.js",          // 所有JS静态资源
        "/**/*.css",         // 所有CSS静态资源
        "/**/*.png",         // 所有PNG图片
        "/**/*.html"         // 所有HTML页面
    );
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")          // 拦截所有请求
                .excludePathPatterns(excludePaths); // 排除指定的路径
    }
}

常见的拦截路径配置模式:

拦截路径 含义 举例
/* 一级路径 能匹配/user/book/login,不能匹配/user/login
/** 任意级路径 能匹配/user/user/login/user/reg
/book/* /book下的一级路径 能匹配/book/addBook,不能匹配/book/addBook/1/book
/book/** /book下的任意级路径 能匹配/book/book/addBook/book/addBook/2,不能匹配/user/login

注意:这些拦截规则同样适用于静态文件(如图片、JS、CSS等)。

1.3.2 登录校验拦截器实现

下面是一个实际的登录校验拦截器实现,它会检查Session中是否存在用户信息:

java 复制代码
// 导入项目常量
import com.example.demo.constant.Constants;

// 日志注解
import lombok.extern.slf4j.Slf4j;

// Spring组件注解
import org.springframework.stereotype.Component;

// 拦截器接口
import org.springframework.web.servlet.HandlerInterceptor;

// Servlet API
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

/**
 * 登录校验拦截器
 */
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {

    /**
     * 在Controller方法执行前进行登录校验
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取Session对象,参数false表示如果Session不存在不创建新的Session
        HttpSession session = request.getSession(false);
        
        // 检查Session是否存在且包含用户信息
        if (session != null && session.getAttribute(Constants.SESSION_USER_KEY) != null) {
            // Session中有用户信息,放行请求
            return true;
        }
        
        // 未登录,设置HTTP状态码为401(未授权)
        response.setStatus(401);
        // 拦截请求
        return false;
    }
}

HTTP状态码401详解: 401状态码表示"Unauthorized",即未经过认证。它指示身份验证是必需的,且没有提供身份验证凭证或身份验证失败。如果请求已经包含授权凭据,那么401状态码表示服务器不接受这些凭据。

在实际应用中,前端可以根据这个状态码跳转到登录页面,提示用户进行登录。

1.4 DispatcherServlet源码分析

1.4.1 什么是DispatcherServlet

DispatcherServlet是Spring MVC的核心,它是一个前端控制器(Front Controller),负责接收所有HTTP请求,并将请求分派给适当的处理器(Controller)。它还负责请求处理的整个生命周期,包括:

  • 初始化Web应用上下文
  • 解析请求
  • 处理多部分请求(文件上传)
  • 查找处理器
  • 应用拦截器
  • 处理异常
  • 渲染视图
1.4.2 初始化过程

当Tomcat启动后,DispatcherServlet会执行初始化方法。以下是简化版的初始化流程:

java 复制代码
@Override
public final void init() throws ServletException {
    try {
        // 1. 加载配置参数
        PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
        
        // 2. 构造DispatcherServlet
        BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
        ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
        bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
        initBeanWrapper(bw);
        bw.setPropertyValues(pvs, true);
    } catch (BeansException ex) {
        // 异常处理
    }
    
    // 3. 调用子类实现的初始化方法
    initServletBean();
}

initServletBean()方法在FrameworkServlet类中实现,主要负责创建Web应用上下文(ApplicationContext)。

1.4.3 处理请求流程

当请求到达DispatcherServlet时,会调用doDispatch()方法处理请求:

java 复制代码
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
        ModelAndView mv = null;
        Exception dispatchException = null;

        try {
            // 1. 处理文件上传
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);

            // 2. 获取处理器执行链(包括处理器和拦截器)
            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null) {
                noHandlerFound(processedRequest, response);
                return;
            }

            // 3. 获取处理器适配器
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

            // 4. 执行拦截器的preHandle方法
            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }

            // 5. 执行目标方法(Controller方法)
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            // 6. 应用默认视图名称(如果需要)
            applyDefaultViewName(processedRequest, mv);
            
            // 7. 执行拦截器的postHandle方法
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        } catch (Exception ex) {
            dispatchException = ex;
        } catch (Throwable err) {
            dispatchException = new NestedServletException("Handler dispatch failed", err);
        }
        
        // 8. 处理分发结果(包括渲染视图)
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    } catch (Exception ex) {
        // 9. 触发完成处理(包括执行拦截器的afterCompletion方法)
        triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
    } finally {
        // 10. 清理资源
        if (asyncManager.isConcurrentHandlingStarted()) {
            if (mappedHandler != null) {
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }
        } else {
            if (multipartRequestParsed) {
                cleanupMultipart(processedRequest);
            }
        }
    }
}

拦截器执行流程详解 : 在doDispatch()方法中,拦截器的执行主要分为三个阶段:

  1. applyPreHandle():在Controller方法执行前调用所有拦截器的preHandle()方法
  2. applyPostHandle():在Controller方法执行后、视图渲染前调用所有拦截器的postHandle()方法
  3. triggerAfterCompletion():在视图渲染完成后调用所有拦截器的afterCompletion()方法

以下是applyPreHandle()方法的实现:

java 复制代码
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // 遍历所有拦截器
    for (int i = 0; i < this.interceptorList.size(); i++) {
        HandlerInterceptor interceptor = this.interceptorList.get(i);
        // 调用拦截器的preHandle方法
        if (!interceptor.preHandle(request, response, this.handler)) {
            // 如果返回false,触发已完成处理(执行之前已经执行过的拦截器的afterCompletion方法)
            triggerAfterCompletion(request, response, null);
            return false;
        }
        this.interceptorIndex = i; // 记录已经执行的拦截器索引
    }
    return true; // 所有拦截器都放行
}
1.4.4 适配器模式在Spring MVC中的应用

HandlerAdapter是Spring MVC中适配器模式的典型应用。适配器模式将一个类的接口转换成客户端期望的另一个接口,使得原本不兼容的类可以一起工作。

适配器模式的角色

  • Target(目标接口):客户端期望的接口
  • Adaptee(适配者):需要被适配的类
  • Adapter(适配器):将Adaptee适配到Target的类
  • Client(客户端):使用目标接口的对象

在Spring MVC中,HandlerAdapter就是适配器,它将各种不同类型的处理器(Controller)适配到统一的请求处理流程中。

适配器模式示例: 假设我们有不同类型的控制器:

java 复制代码
// 传统Controller接口
public interface Controller {
    ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}

// 基于注解的控制器
@Controller
public class MyController {
    @RequestMapping("/hello")
    public String hello() {
        return "hello";
    }
}

// HttpRequestHandler类型
public class MyHttpRequestHandler implements HttpRequestHandler {
    @Override
    public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.getWriter().write("Hello from HttpRequestHandler");
    }
}

Spring MVC使用不同的HandlerAdapter来适配这些不同的控制器:

java 复制代码
// 适配Controller接口
public class SimpleControllerHandlerAdapter implements HandlerAdapter {
    @Override
    public boolean supports(Object handler) {
        return handler instanceof Controller;
    }
    
    @Override
    public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return ((Controller) handler).handleRequest(request, response);
    }
}

// 适配注解控制器
public class RequestMappingHandlerAdapter implements HandlerAdapter {
    // 复杂的实现,处理@RequestMapping等注解
}

// 适配HttpRequestHandler
public class HttpRequestHandlerAdapter implements HandlerAdapter {
    @Override
    public boolean supports(Object handler) {
        return handler instanceof HttpRequestHandler;
    }
    
    @Override
    public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        ((HttpRequestHandler) handler).handleRequest(request, response);
        return null;
    }
}

通过适配器模式,DispatcherServlet可以统一对待所有类型的控制器,而不必关心它们的具体实现。

2. 统一数据返回格式

2.1 为什么需要统一数据返回格式

在Web开发中,前后端分离架构已成为主流。统一的数据返回格式有以下优点:

  1. 方便前端统一处理响应数据
  2. 降低前后端沟通成本
  3. 便于维护和扩展
  4. 统一错误处理机制
  5. 便于API文档生成和测试

通常,一个标准的响应格式包含以下字段:

  • code/status:状态码,表示请求结果
  • message:描述信息,提供更详细的说明
  • data:实际业务数据
  • timestamp:时间戳,表示响应时间

2.2 快速入门

Spring Boot提供了@ControllerAdviceResponseBodyAdvice来实现全局统一数据返回格式。

java 复制代码
// 导入Spring Web相关类
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;

// 导入自定义的Result类
import com.example.demo.model.Result;

/**
 * 全局响应处理
 * @ControllerAdvice注解表示这是一个控制器通知类,可以处理所有Controller的异常和返回值
 */
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {

    /**
     * 判断是否要执行beforeBodyWrite方法
     * @param returnType 返回类型
     * @param converterType 消息转换器类型
     * @return true表示需要处理,false表示不需要处理
     */
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true; // 对所有返回类型都进行处理
    }

    /**
     * 在响应体写入前进行处理
     * @param body 响应体内容
     * @param returnType 返回类型
     * @param selectedContentType 选择的内容类型
     * @param selectedConverterType 选择的消息转换器类型
     * @param request 请求对象
     * @param response 响应对象
     * @return 处理后的响应体
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                 Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                 ServerHttpRequest request, ServerHttpResponse response) {
        // 将原本的返回值封装到Result对象中
        return Result.success(body);
    }
}

2.3 存在的问题及解决方案

在使用统一返回格式时,会遇到一个问题:当Controller返回String类型时,会出现类型转换异常。原因是Spring MVC处理String类型和对象类型的流程不同。

问题重现

java 复制代码
@RestController
@RequestMapping("/test")
public class TestController {
    @RequestMapping("/t1")
    public String t1() {
        return "t1"; // 会抛出类型转换异常
    }
    
    @RequestMapping("/t2")
    public boolean t2() {
        return true; // 正常工作
    }
    
    @RequestMapping("/t3")
    public Integer t3() {
        return 200; // 正常工作
    }
}

解决方案:针对String类型特殊处理

java 复制代码
import com.example.demo.model.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
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;

@Slf4j
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
    
    // 创建ObjectMapper对象,用于JSON序列化
    private static ObjectMapper mapper = new ObjectMapper();
    
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }
    
    @SneakyThrows // Lombok注解,自动处理异常
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                 Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                 ServerHttpRequest request, ServerHttpResponse response) {
        
        // 1. 如果已经是Result类型,直接返回
        if (body instanceof Result) {
            return body;
        }
        
        // 2. 如果是String类型,需要特殊处理
        if (body instanceof String) {
            // 使用Jackson将Result对象序列化为JSON字符串
            return mapper.writeValueAsString(Result.success(body));
        }
        
        // 3. 其他类型,直接包装成Result
        return Result.success(body);
    }
}

原因分析 : Spring MVC内置了一系列HttpMessageConverter,用于将对象转换为HTTP响应。主要的转换器按优先级排序为:

  1. ByteArrayHttpMessageConverter - 处理字节数组
  2. StringHttpMessageConverter - 处理字符串
  3. SourceHttpMessageConverter - 处理XML源
  4. AllEncompassingFormHttpMessageConverter - 处理表单数据,它会根据依赖自动添加其他转换器

当我们引入Jackson依赖后,MappingJackson2HttpMessageConverter会被添加到转换器列表末尾。当返回对象类型时,Spring会使用Jackson转换器;但当返回String类型时,会优先使用StringHttpMessageConverter,而这个转换器期望接收String类型,但我们的拦截器返回了Result对象,导致类型不匹配异常。

解决方案中,我们针对String类型特殊处理,先将Result对象序列化为JSON字符串,再返回给StringHttpMessageConverter

2.4 统一结果类Result

一个标准的统一结果类通常如下:

java 复制代码
import lombok.Data;

/**
 * 统一返回结果
 * @param <T> 泛型,表示data字段的数据类型
 */
@Data // Lombok注解,自动生成getter/setter/toString等方法
public class Result<T> {
    // 状态码,通常使用枚举或常量
    private int status;
    // 错误信息,成功时可以为空
    private String errorMessage;
    // 业务数据
    private T data;
    // 时间戳
    private long timestamp;
    
    // 私有构造函数,防止外部直接实例化
    private Result() {
        this.timestamp = System.currentTimeMillis();
    }
    
    // 成功响应的工厂方法
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setStatus(ResultStatus.SUCCESS); // 假设ResultStatus.SUCCESS=200
        result.setData(data);
        return result;
    }
    
    // 失败响应的工厂方法
    public static <T> Result<T> fail(String errorMessage) {
        Result<T> result = new Result<>();
        result.setStatus(ResultStatus.FAIL); // 假设ResultStatus.FAIL=500
        result.setErrorMessage(errorMessage);
        return result;
    }
    
    // 未登录响应
    public static <T> Result<T> unlogin() {
        Result<T> result = new Result<>();
        result.setStatus(ResultStatus.UNAUTHORIZED); // 401
        result.setErrorMessage("用户未登录");
        return result;
    }
    
    // 自定义状态码的响应
    public static <T> Result<T> custom(int status, String errorMessage, T data) {
        Result<T> result = new Result<>();
        result.setStatus(status);
        result.setErrorMessage(errorMessage);
        result.setData(data);
        return result;
    }
}

// 状态码常量
public class ResultStatus {
    public static final int SUCCESS = 200;        // 成功
    public static final int FAIL = 500;            // 服务器内部错误
    public static final int UNAUTHORIZED = 401;   // 未授权
    public static final int NOT_FOUND = 404;      // 资源未找到
    public static final int VALIDATION_ERROR = 400; // 参数校验失败
}

3. 统一异常处理

3.1 为什么需要统一异常处理

在Web应用中,异常是不可避免的。统一异常处理的好处包括:

  1. 避免将内部错误细节暴露给客户端
  2. 提供一致的错误响应格式
  3. 减少重复的try-catch代码
  4. 便于监控和日志记录
  5. 提高系统的健壮性和用户体验

3.2 基本实现

Spring Boot提供了@ControllerAdvice@ExceptionHandler注解来实现全局异常处理。

java 复制代码
import com.example.demo.model.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * 全局异常处理器
 */
@ResponseBody // 表示返回JSON数据,而不是视图
@ControllerAdvice // 全局控制器通知
public class ErrorAdvice {
    
    /**
     * 处理所有Exception及其子类异常
     * @param e 异常对象
     * @return 统一错误响应
     */
    @ExceptionHandler
    public Object handler(Exception e) {
        // 记录异常日志(实际项目中应更详细)
        System.err.println("发生异常: " + e.getMessage());
        e.printStackTrace();
        
        // 返回错误结果
        return Result.fail("系统繁忙,请稍后再试");
    }
}

3.3 精细化异常处理

我们可以针对不同类型的异常提供不同的处理策略:

java 复制代码
import com.example.demo.model.Result;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseBody
@ControllerAdvice
public class ErrorAdvice {
    
    /**
     * 通用异常处理器
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 设置HTTP状态码为500
    public Result<?> handleGeneralException(Exception e) {
        // 记录详细错误日志
        log.error("系统发生未处理异常: {}", e.getMessage(), e);
        return Result.fail("系统异常: " + e.getMessage());
    }
    
    /**
     * 处理空指针异常
     */
    @ExceptionHandler(NullPointerException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<?> handleNullPointerException(NullPointerException e) {
        log.error("发生空指针异常: {}", e.getMessage(), e);
        return Result.fail("系统错误: 未初始化的对象被引用");
    }
    
    /**
     * 处理算术异常
     */
    @ExceptionHandler(ArithmeticException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST) // 400 - 客户端请求错误
    public Result<?> handleArithmeticException(ArithmeticException e) {
        log.error("发生算术异常: {}", e.getMessage(), e);
        return Result.fail("计算错误: " + e.getMessage());
    }
    
    /**
     * 处理参数校验异常
     */
    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<?> handleIllegalArgumentException(IllegalArgumentException e) {
        log.warn("参数校验失败: {}", e.getMessage());
        return Result.fail("参数错误: " + e.getMessage());
    }
    
    /**
     * 处理自定义业务异常
     */
    @ExceptionHandler(BusinessException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<?> handleBusinessException(BusinessException e) {
        // 业务异常通常包含错误码
        log.warn("业务异常[{}]: {}", e.getErrorCode(), e.getMessage());
        return Result.custom(e.getErrorCode(), e.getMessage(), null);
    }
}

3.4 自定义业务异常

在实际项目中,通常会定义自定义异常类来表示业务异常:

java 复制代码
/**
 * 业务异常基类
 */
public class BusinessException extends RuntimeException {
    private int errorCode; // 业务错误码
    
    public BusinessException(String message) {
        super(message);
        this.errorCode = ResultStatus.FAIL; // 默认错误码
    }
    
    public BusinessException(int errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }
    
    public int getErrorCode() {
        return errorCode;
    }
}

/**
 * 用户相关异常
 */
public class UserException extends BusinessException {
    public UserException(String message) {
        super(ResultStatus.USER_ERROR, message); // 假设USER_ERROR=10001
    }
}

/**
 * 资源不存在异常
 */
public class ResourceNotFoundException extends BusinessException {
    public ResourceNotFoundException(String resourceName, Object id) {
        super(ResultStatus.NOT_FOUND, 
              String.format("%s[id=%s]不存在", resourceName, id));
    }
}

3.5 异常处理最佳实践

  1. 分层处理异常

    • DAO层:抛出数据访问异常
    • Service层:处理业务逻辑异常,抛出业务异常
    • Controller层:捕获异常,返回统一格式
  2. 异常日志记录

    • 记录详细的异常堆栈
    • 包含请求参数、用户信息等上下文
  3. 安全考虑

    • 不要将敏感信息(如数据库结构、系统路径)暴露给客户端
    • 生产环境应隐藏内部错误细节
  4. 异常分类

    • 系统异常:如数据库连接失败,应记录详细日志
    • 业务异常:如参数校验失败,应提供用户友好的提示
    • 客户端异常:如404,应返回适当的HTTP状态码

4. 案例代码详解

4.1 登录页面

前端代码需要适配统一返回格式:

java 复制代码
function login() {
    $.ajax({
        type: "post",
        url: "/user/login",
        data: {
            name: $("#userName").val(),
            password: $("#password").val()
        },
        success: function(result) {
            console.log(result);
            // 检查返回结果的状态
            if (result.status == "SUCCESS" && result.data == true) {
                // 登录成功,跳转到图书列表页
                location.href = "book_list.html";
            } else {
                // 登录失败,显示错误提示
                alert("账号或密码不正确!");
            }
        },
        error: function(xhr, status, error) {
            // 处理HTTP错误
            if (xhr.status == 401) {
                alert("会话已过期,请重新登录");
            } else {
                alert("登录请求失败: " + error);
            }
        }
    });
}

4.2 图书列表

图书列表需要处理登录状态和统一返回格式:

java 复制代码
function getBookList() {
    $.ajax({
        type: "get",
        url: "/book/getListByPage" + location.search,
        success: function(result) {
            // 检查返回结果
            if (result == null || result.data == null) {
                alert("获取数据失败");
                return;
            }
            
            var finalHtml = "";
            var data = result.data; // PageResult对象
            
            // 遍历图书列表
            for (var book of data.records) {
                finalHtml += `
                <tr>
                    <td><input type="checkbox" class="book-item" value="${book.id}"></td>
                    <td>${book.id}</td>
                    <td>${book.bookName}</td>
                    <td>${book.author}</td>
                    <td>${book.count}</td>
                    <td>${book.publish}</td>
                    <td>${formatDate(book.createTime)}</td>
                    <td>${formatDate(book.updateTime)}</td>
                    <td>
                        <button class="btn-edit" onclick="editBook(${book.id})">修改</button>
                        <button class="btn-delete" onclick="deleteBook(${book.id})">删除</button>
                    </td>
                </tr>`;
            }
            
            // 更新表格内容
            $("#bookList tbody").html(finalHtml);
            
            // 更新分页信息
            updatePagination(data);
        },
        error: function(error) {
            if (error != null && error.status == 401) {
                // 未登录,跳转到登录页
                location.href = "login.html";
            } else {
                alert("获取图书列表失败: " + (error.responseJSON ? error.responseJSON.errorMessage : error.statusText));
            }
        }
    });
}

// 日期格式化函数
function formatDate(dateStr) {
    if (!dateStr) return "";
    var date = new Date(dateStr);
    return date.getFullYear() + "-" + 
           padZero(date.getMonth() + 1) + "-" + 
           padZero(date.getDate()) + " " + 
           padZero(date.getHours()) + ":" + 
           padZero(date.getMinutes()) + ":" + 
           padZero(date.getSeconds());
}

function padZero(num) {
    return num < 10 ? "0" + num : num;
}

// 更新分页UI
function updatePagination(pageResult) {
    $("#currentPage").text(pageResult.currentPage);
    $("#totalPages").text(pageResult.totalPage);
    $("#totalCount").text(pageResult.totalCount);
    
    // 禁用/启用分页按钮
    $("#prevPage").prop("disabled", pageResult.currentPage <= 1);
    $("#nextPage").prop("disabled", pageResult.currentPage >= pageResult.totalPage);
}

4.3 其他功能

其他功能包括删除图书、批量删除、添加图书和修改图书,都需要适配统一返回格式和异常处理。以下是删除图书的示例:

java 复制代码
function deleteBook(id) {
    if (!confirm("确定要删除这本书吗?")) {
        return;
    }
    
    $.ajax({
        type: "post",
        url: "/book/deleteBook",
        data: { bookId: id },
        success: function(result) {
            if (result.status == "SUCCESS" || result.data == "") {
                alert("删除成功");
                // 重新加载图书列表
                getBookList();
            } else {
                alert("删除失败: " + (result.errorMessage || "未知错误"));
            }
        },
        error: function(error) {
            if (error != null && error.status == 401) {
                // 未登录,跳转到登录页
                location.href = "login.html";
            } else {
                alert("删除请求失败: " + (error.responseJSON ? error.responseJSON.errorMessage : error.statusText));
            }
        }
    });
}

5. 总结

通过本章节的学习,我们掌握了Spring Boot中三种重要的统一处理机制:

  1. 拦截器

    • 用于在请求处理的不同阶段插入自定义逻辑
    • 实现方式:实现HandlerInterceptor接口并注册到WebMvcConfigurer
    • 应用场景:登录校验、权限控制、日志记录等
  2. 统一数据返回格式

    • 通过@ControllerAdvice + ResponseBodyAdvice实现
    • 统一响应结构,提高前后端协作效率
    • 特殊处理String类型返回值
  3. 统一异常处理

    • 通过@ControllerAdvice + @ExceptionHandler实现
    • 针对不同异常类型提供差异化处理
    • 提高系统健壮性和用户体验

这些机制共同构成了现代Web应用的基础架构,使代码更加清晰、可维护,同时提高了开发效率。在实际项目中,应根据业务需求灵活运用这些技术,构建高质量的应用系统。


用适配器和不用适配器这两者有啥本质的区别?

用适配器 vs 不用适配器:本质区别解析

结合你上传的 Spring MVC 图片案例,我们来对比一下这两种方式在代码层面的本质区别。

1. 场景设定

假设除了图片中提到的三种 Controller:

  1. 传统接口Controller (方法是 handleRequest)

  2. 注解方式@Controller (方法名任意,比如 hello())

  3. Servlet处理HttpRequestHandler (方法是 handleRequest 但参数略有不同)

现在 DispatcherServlet (核心调度器) 需要调用它们。

2. 只有"不用适配器"的世界 (If-Else 地狱)

如果没有适配器模式,DispatcherServlet 就必须亲自处理所有类型的差异。代码大概会写成这样:

复制代码
// DispatcherServlet.java (伪代码)

public void doDispatch(HttpServletRequest request, HttpServletResponse response) {
    // 1. 获取处理器
    Object handler = getHandler(request);

    // 2. 极其丑陋的类型判断 (硬编码)
    if (handler instanceof Controller) {
        // 处理方式 A:强转为 Controller 接口调用
        ((Controller) handler).handleRequest(request, response);
    } 
    else if (handler instanceof HttpRequestHandler) {
        // 处理方式 B:强转为 HttpRequestHandler 接口调用
        ((HttpRequestHandler) handler).handleRequest(request, response);
    } 
    else if (handler.getClass().isAnnotationPresent(Controller.class)) {
        // 处理方式 C:通过反射去寻找 @RequestMapping 方法并调用
        // ... 一大堆复杂的反射逻辑 ...
        method.invoke(handler, ...);
    } 
    else if (handler instanceof Servlet) {
        // 假如未来加了 Servlet 类型,你必须回来改这行代码!
        ((Servlet) handler).service(request, response);
    }
    // ...以此类推
}

这种方式的本质缺陷:

  • 违反开闭原则 (Open-Closed Principle) :每当你想要支持一种新的 Controller 写法(比如未来出了个 FunctionController),你都必须修改 DispatcherServlet 的核心代码。

  • 高耦合:核心调度器与具体的 Controller 实现细节死死绑定在一起。

  • 逻辑膨胀doDispatch 方法会随着支持类型的增加变得无限长,难以维护。

3. 使用"适配器模式"的世界 (多态的胜利)

这正是 Spring MVC 的做法(如你上传的图片所示)。

  1. 定义一个统一接口 HandlerAdapter(所有适配器都长这样)。

  2. 为每种 Controller 写一个专门的适配器实现类。

DispatcherServlet 的代码变成了这样:

复制代码
// DispatcherServlet.java (伪代码 - 现在的样子)

public void doDispatch(HttpServletRequest request, HttpServletResponse response) {
    // 1. 获取处理器
    Object handler = getHandler(request);

    // 2. 关键步骤:获取对应的适配器
    // 循环遍历所有注册的适配器,问它们:"你能处理这个 handler 吗?"
    HandlerAdapter adapter = getHandlerAdapter(handler);

    // 3. 统一调用
    // DispatcherServlet 根本不需要知道 handler 是旧接口还是新注解
    // 它只知道 adapter.handle() 一定能搞定
    adapter.handle(request, response, handler);
}

适配器内部干脏活 (参考 image_66ca99.png):

复制代码
// 专门处理 @Controller 的适配器
public class RequestMappingHandlerAdapter implements HandlerAdapter {
    public boolean supports(Object handler) {
        return (handler instanceof HandlerMethod); // 检查是否是注解类型
    }
    public ModelAndView handle(...) {
        // 这里面写复杂的反射逻辑,调用 @RequestMapping 方法
        return invokeHandlerMethod(...);
    }
}

// 专门处理 Controller 接口的适配器
public class SimpleControllerHandlerAdapter implements HandlerAdapter {
    public boolean supports(Object handler) {
        return (handler instanceof Controller);
    }
    public ModelAndView handle(...) {
        // 直接强转调用
        return ((Controller) handler).handleRequest(...);
    }
}

这种方式的本质优势:

  • 符合开闭原则 :如果要加一个新的 Controller 类型,不需要修改 DispatcherServlet 。只需要写一个新的 HandlerAdapter 实现类并注册进容器即可。

  • 统一接口DispatcherServlet 面对的是整齐划一的 HandlerAdapter 接口,而不是千奇百怪的具体实现。

4. 总结对比表

|------------|-------------------------------|--------------------------------|
| 特性 | 不用适配器 | 用适配器 |
| 代码结构 | 充斥着 if (instanceof) 和强制类型转换 | 统一的方法调用 (interface.method()) |
| 维护成本 | 增加新类型需修改核心逻辑 (风险大) | 增加新类型只需扩展新类 (风险小) |
| 关注点 | 调用者必须关注所有具体实现的细节 | 调用者只关注统一的标准接口 |
| 设计模式原则 | 违反开闭原则 | 符合开闭原则、单一职责原则 |
| 现实映射 | 你去国外旅行,必须学会所有国家的语言 | 你带了一个万能翻译官,你只管跟翻译官说中文 |

一句话总结:

适配器模式的本质是将**"调用的复杂性"从调用者**(DispatcherServlet)转移到了适配器(HandlerAdapter)中,从而让调用者的逻辑保持纯粹、稳定。

这是一个关于设计模式核心思想的问题,特别是在你上传的 Spring MVC 图片背景下,这个对比非常经典。

核心区别在于:代码的"扩展性"和"调用者的复杂度"

简单来说:

  • 不用适配器 :调用者(DispatcherServlet)必须亲自去了解每一个被调用者(Controller)的怪癖和细节。增加新类型时,必须修改调用者代码(违反开闭原则)。

  • 用适配器:调用者只需要跟一个标准的"中介"(Adapter)对接。增加新类型时,只需要增加一个新的中介,调用者代码完全不用动。


简单总结一下:

  1. 不用适配器 :你的主程序(DispatcherServlet)里会塞满 if-else,每多一种 Controller 类型,你就得改一次主程序。这叫"牵一发而动全身"。

  2. 用适配器 :你的主程序只管调用标准接口(handle())。具体的脏活累活(比如怎么反射调用、怎么强转类型)都扔给具体的适配器类去干。这叫"各司其职"。

这就好比电源插座(DispatcherServlet)只提供两孔或三孔的标准,而具体的电器(Controller)可能有英标、美标、欧标插头。不用适配器,你就得把墙上的插座改造成能插所有国家插头的怪物;用了适配器,插座永远不用变,只需要买对应的转换头(HandlerAdapter)就行了。

相关推荐
凛_Lin~~4 小时前
安卓 面试八股文整理(基础组件篇)
java·安卓
ooolmf5 小时前
matlab2024读取温度01
java·前端·javascript
曹牧5 小时前
Java:Foreach语法糖
java·开发语言·python
编程火箭车5 小时前
【Java SE 基础学习打卡】24 循环结构 - while
java·编程基础·循环结构·while循环·java se·do-while循环·避免死循环
Haooog5 小时前
微服务保护学习
java·学习·微服务·sentinel
程序员云帆哥5 小时前
告别Swagger!Spring Boot集成Smart-Doc自动生成API文档
java·接口文档·api文档
222you5 小时前
SpringIOC的注解开发
java·开发语言
hgz07105 小时前
Spring Boot、Spring MVC、Spring 三者核心区别
java
god005 小时前
Selenium等待判断元素页面加载完成
java·开发语言