目录
[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 拦截器执行流程
当我们启动服务并访问任意请求时,可以通过日志观察到拦截器的执行顺序:
- 首先执行
preHandle()方法 - 然后执行Controller中的目标方法
- 接着执行
postHandle()方法 - 最后执行
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()方法中,拦截器的执行主要分为三个阶段:
applyPreHandle():在Controller方法执行前调用所有拦截器的preHandle()方法applyPostHandle():在Controller方法执行后、视图渲染前调用所有拦截器的postHandle()方法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开发中,前后端分离架构已成为主流。统一的数据返回格式有以下优点:
- 方便前端统一处理响应数据
- 降低前后端沟通成本
- 便于维护和扩展
- 统一错误处理机制
- 便于API文档生成和测试
通常,一个标准的响应格式包含以下字段:
- code/status:状态码,表示请求结果
- message:描述信息,提供更详细的说明
- data:实际业务数据
- timestamp:时间戳,表示响应时间
2.2 快速入门
Spring Boot提供了@ControllerAdvice和ResponseBodyAdvice来实现全局统一数据返回格式。
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响应。主要的转换器按优先级排序为:
ByteArrayHttpMessageConverter- 处理字节数组StringHttpMessageConverter- 处理字符串SourceHttpMessageConverter- 处理XML源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应用中,异常是不可避免的。统一异常处理的好处包括:
- 避免将内部错误细节暴露给客户端
- 提供一致的错误响应格式
- 减少重复的try-catch代码
- 便于监控和日志记录
- 提高系统的健壮性和用户体验
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 异常处理最佳实践
-
分层处理异常:
- DAO层:抛出数据访问异常
- Service层:处理业务逻辑异常,抛出业务异常
- Controller层:捕获异常,返回统一格式
-
异常日志记录:
- 记录详细的异常堆栈
- 包含请求参数、用户信息等上下文
-
安全考虑:
- 不要将敏感信息(如数据库结构、系统路径)暴露给客户端
- 生产环境应隐藏内部错误细节
-
异常分类:
- 系统异常:如数据库连接失败,应记录详细日志
- 业务异常:如参数校验失败,应提供用户友好的提示
- 客户端异常:如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中三种重要的统一处理机制:
-
拦截器:
- 用于在请求处理的不同阶段插入自定义逻辑
- 实现方式:实现
HandlerInterceptor接口并注册到WebMvcConfigurer - 应用场景:登录校验、权限控制、日志记录等
-
统一数据返回格式:
- 通过
@ControllerAdvice+ResponseBodyAdvice实现 - 统一响应结构,提高前后端协作效率
- 特殊处理String类型返回值
- 通过
-
统一异常处理:
- 通过
@ControllerAdvice+@ExceptionHandler实现 - 针对不同异常类型提供差异化处理
- 提高系统健壮性和用户体验
- 通过
这些机制共同构成了现代Web应用的基础架构,使代码更加清晰、可维护,同时提高了开发效率。在实际项目中,应根据业务需求灵活运用这些技术,构建高质量的应用系统。
用适配器和不用适配器这两者有啥本质的区别?
用适配器 vs 不用适配器:本质区别解析
结合你上传的 Spring MVC 图片案例,我们来对比一下这两种方式在代码层面的本质区别。
1. 场景设定
假设除了图片中提到的三种 Controller:
-
传统接口 :
Controller(方法是handleRequest) -
注解方式 :
@Controller(方法名任意,比如hello()) -
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 的做法(如你上传的图片所示)。
-
定义一个统一接口
HandlerAdapter(所有适配器都长这样)。 -
为每种 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)对接。增加新类型时,只需要增加一个新的中介,调用者代码完全不用动。
简单总结一下:
-
不用适配器 :你的主程序(
DispatcherServlet)里会塞满if-else,每多一种 Controller 类型,你就得改一次主程序。这叫"牵一发而动全身"。 -
用适配器 :你的主程序只管调用标准接口(
handle())。具体的脏活累活(比如怎么反射调用、怎么强转类型)都扔给具体的适配器类去干。这叫"各司其职"。
这就好比电源插座(DispatcherServlet)只提供两孔或三孔的标准,而具体的电器(Controller)可能有英标、美标、欧标插头。不用适配器,你就得把墙上的插座改造成能插所有国家插头的怪物;用了适配器,插座永远不用变,只需要买对应的转换头(HandlerAdapter)就行了。