目录
Spring Boot统一功能处理详解
1. 拦截器详解
1.1 什么是拦截器
1.2 拦截器快速入门
1.2.1 定义拦截器
1.2.2 注册配置拦截器
1.2.3 拦截器执行流程
1.3 拦截器详解
1.3.1 拦截路径配置
1.3.2 登录校验拦截器实现
1.4 DispatcherServlet源码分析
1.4.1 什么是DispatcherServlet
1.4.2 初始化过程
1.4.3 处理请求流程
1.4.4 适配器模式在Spring MVC中的应用
2. 统一数据返回格式
2.1 为什么需要统一数据返回格式
2.2 快速入门
2.3 存在的问题及解决方案
2.4 统一结果类Result
3. 统一异常处理
3.1 为什么需要统一异常处理
3.2 基本实现
3.3 精细化异常处理
3.4 自定义业务异常
3.5 异常处理最佳实践
4. 案例代码详解
4.1 登录页面
4.2 图书列表
4.3 其他功能
5. 总结
用适配器和不用适配器这两者有啥本质的区别?
用适配器 vs 不用适配器:本质区别解析
1. 场景设定
2. 只有“不用适配器”的世界 (If-Else 地狱)
这种方式的本质缺陷:
3. 使用“适配器模式”的世界 (多态的胜利)
这种方式的本质优势:
4. 总结对比表
Spring Boot统一功能处理详解
1. 拦截器详解
1.1 什么是拦截器
拦截器(Interceptor)是Spring框架提供的一种机制,用于在请求处理的不同阶段(请求前、请求后、视图渲染后)插入自定义逻辑。它类似于Web开发中的过滤器(Filter),但拦截器是基于Java反射机制实现的,工作在DispatcherServlet之后,属于Spring MVC框架的一部分。
拦截器的主要应用场景包括:
- 用户身份认证和授权
- 日志记录
- 性能监控
- 防止表单重复提交
- 处理国际化
- 统一异常处理
1.2 拦截器快速入门
1.2.1 定义拦截器
首先我们来看如何定义一个基本的拦截器。以下是一个登录拦截器的完整代码:
// 导入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中。以下是注册配置拦截器的代码:
// 导入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 拦截路径配置
在实际应用中,我们通常需要精确控制哪些路径需要拦截,哪些路径不需要拦截。例如,登录页面本身就不需要进行登录验证。以下是更完善的拦截器配置:
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中是否存在用户信息:
// 导入项目常量 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会执行初始化方法。以下是简化版的初始化流程:
@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()方法处理请求:
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()方法的实现:
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)适配到统一的请求处理流程中。
适配器模式示例: 假设我们有不同类型的控制器:
// 传统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来适配这些不同的控制器:
// 适配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来实现全局统一数据返回格式。
// 导入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类型和对象类型的流程不同。
问题重现:
@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类型特殊处理
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
一个标准的统一结果类通常如下:
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注解来实现全局异常处理。
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 精细化异常处理
我们可以针对不同类型的异常提供不同的处理策略:
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 自定义业务异常
在实际项目中,通常会定义自定义异常类来表示业务异常:
/** * 业务异常基类 */ 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 登录页面
前端代码需要适配统一返回格式:
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 图书列表
图书列表需要处理登录状态和统一返回格式:
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 其他功能
其他功能包括删除图书、批量删除、添加图书和修改图书,都需要适配统一返回格式和异常处理。以下是删除图书的示例:
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. 总结对比表
| 特性 | 不用适配器 | 用适配器 |
代码结构 | 充斥着 | 统一的方法调用 ( |
维护成本 | 增加新类型需修改核心逻辑 (风险大) | 增加新类型只需扩展新类 (风险小) |
关注点 | 调用者必须关注所有具体实现的细节 | 调用者只关注统一的标准接口 |
设计模式原则 | 违反开闭原则 | 符合开闭原则、单一职责原则 |
现实映射 | 你去国外旅行,必须学会所有国家的语言 | 你带了一个万能翻译官,你只管跟翻译官说中文 |
一句话总结:
适配器模式的本质是将**“调用的复杂性”从调用者**(DispatcherServlet)转移到了适配器(HandlerAdapter)中,从而让调用者的逻辑保持纯粹、稳定。
这是一个关于设计模式核心思想的问题,特别是在你上传的 Spring MVC 图片背景下,这个对比非常经典。
核心区别在于:代码的“扩展性”和“调用者的复杂度”。
简单来说:
不用适配器:调用者(DispatcherServlet)必须亲自去了解每一个被调用者(Controller)的怪癖和细节。增加新类型时,必须修改调用者代码(违反开闭原则)。
用适配器:调用者只需要跟一个标准的“中介”(Adapter)对接。增加新类型时,只需要增加一个新的中介,调用者代码完全不用动。
简单总结一下:
不用适配器:你的主程序(
DispatcherServlet)里会塞满if-else,每多一种 Controller 类型,你就得改一次主程序。这叫“牵一发而动全身”。用适配器:你的主程序只管调用标准接口(
handle())。具体的脏活累活(比如怎么反射调用、怎么强转类型)都扔给具体的适配器类去干。这叫“各司其职”。
这就好比电源插座(DispatcherServlet)只提供两孔或三孔的标准,而具体的电器(Controller)可能有英标、美标、欧标插头。不用适配器,你就得把墙上的插座改造成能插所有国家插头的怪物;用了适配器,插座永远不用变,只需要买对应的转换头(HandlerAdapter)就行了。