目录
前言
拦截器
什么是拦截器?
拦截器的使用
自定义拦截器
注册并配置拦截器
拦截器详解
拦截路径
拦截器执行流程
统一数据返回格式
优点
统一异常处理
前言
在前面中,我们已经学习了spring中的一些常用操作,那么本篇,我们就继续往下深入学习。
我们在做一些小项目的时候,假如我们想要判断用户是否已经登录,按照我们前面的学习,我们就需要用到seesion来进行判断。设想一下,我们有几个界面,而这几个界面都需要在用户登录后才能进行查看的,就想淘宝,如果我们未登录,那么他就跳转到登录界面。
对于这样的操作,我们的界面不止一个,那么我们对应的在每个页面调用后端的API,其中的方法每次都需要判断用户是否登录,这样会让代码冗余,所以,在Spring中,给我们提供了一种功能,能够让我们将这些重复的代码进行抽取——拦截器。
拦截器
什么是拦截器?
拦截器(Interceptor)是一种在请求处理流程中,对请求和响应进行拦截和预处理的机制。它允许开发者在请求到达目标处理器(如控制器方法)之前或之后执行统一的逻辑,从而实现诸如权限校验、日志记录、性能监控、请求过滤等功能。
拦截器的使用
拦截器的使用步骤分为两步:
- 定义拦截器
- 注册并配置拦截器
自定义拦截器
在Spring MVC框架中,拦截器通过实现 HandlerInterceptor 接口来定义拦截逻辑。
java">package com.example.demo.interceptor;
import com.example.demo.Result.Results;
import com.example.demo.constant.Constants;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
/**
* 登录拦截器,用于在请求处理前验证用户是否已登录
*/
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private ObjectMapper objectMapper;
/**
* 在请求处理之前进行拦截
*
* @param request HttpServletRequest对象,用于获取请求信息
* @param response HttpServletResponse对象,用于设置响应信息
* @param handler 请求处理器,可以是HandlerMethod或RequestMappingHandler等
* @return boolean 表示是否继续执行其他拦截器和目标方法。返回true表示继续执行,返回false表示中断执行。
* @throws Exception 抛出异常表示拦截器处理出现错误
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("方法执行前");
//进行登录校验
HttpSession session= request.getSession(false);
if(session!=null&&"true".equals(session.getAttribute(Constants.USER_SESSION_KEY))){
// 用户已登录,继续执行请求
return true;
}
// 用户未登录,返回未授权错误信息
Results results=Results.unLogin();
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getOutputStream().write(objectMapper.writeValueAsString(results).getBytes());
response.setContentType("application/json;charset=UTF-8");
response.getOutputStream().close();
return false;
}
/**
* 在请求处理之后,视图渲染之前进行拦截
*
* @param request HttpServletRequest对象,用于获取请求信息
* @param response HttpServletResponse对象,用于设置响应信息
* @param handler 请求处理器,可以是HandlerMethod或RequestMappingHandler等
* @param modelAndView ModelAndView对象,用于添加模型数据或修改视图
* @throws Exception 抛出异常表示拦截器处理出现错误
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("方法执行后");
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
/**
* 在请求完成之后执行的方法
*
* @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 {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
注册并配置拦截器
注册配置拦截器我们需要实现 WebMvcConfiguer 接口,并实现其中的 addInterceptors 方法。
java">package com.example.demo.config;
import com.example.demo.interceptor.LoginInterceptor;
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;
/**
* 配置类用于配置Web相关的设置
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 登录拦截器,用于拦截请求以进行登录验证
*/
@Autowired
private LoginInterceptor loginInterceptor;
/**
* 添加拦截器以配置请求的预处理和后处理
*
* @param registry 拦截器注册对象,用于注册自定义拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册拦截器
registry.addInterceptor(loginInterceptor)
//拦截以"/book/"开头的所有请求
.addPathPatterns("/book/**")
//排除"/user/login"请求,使其不被拦截
.excludePathPatterns("/user/login");
}
}
假如此时我们未登录,调用一下查询功能:
可以看到,会对请求进行拦截。
知道了拦截器的是如何使用的,那么就来进一步了解拦截器。
拦截器详解
拦截器的使用细节我们讲以下两个部分:
- 拦截路径配置
- 拦截器实现原理
拦截路径
拦截路径指的是我们定义的拦截器对哪些请求生效,我们在注册配置拦截器的时候,通过 addPathPatterns() 方法就可以来指定要拦截哪些请求,也可以通过 excludePathPatterns() 方法来指定哪些请求不需要拦截。
关于拦截路径设置的规则,有以下几种:
拦截路径 | 含义 | 举例 |
/* | 一级路径 | 能匹配/user,/book,但不能匹配/book/getList等 |
/** | 任意级路径 | 能匹配/user,/user/login,即任意路径都可以匹配 |
/book/* | /book下的一级路径 | 能匹配/book/addBook,不能匹配/book/addBook/get,/book |
/book/** | /book下的任意级路径 | 能匹配/book,/book/addBook,/book/addBook/2,不能匹配/user/login |
上面的这些拦截规则可以拦截项目中的URL,包括静态文件(如图片文件、JS和CSS等文件).
如果我们使用下面这种拦截规则,就会将前端界面的请求也给拦截住。
我们可以通过设置前端界面不拦截:
java">package com.example.demo.config;
import com.example.demo.interceptor.LoginInterceptor;
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.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 配置类用于配置Web相关的设置
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 登录拦截器,用于拦截请求以进行登录验证
*/
@Autowired
private LoginInterceptor loginInterceptor;
public List<String> excludePath= Arrays.asList("/user/*",
"/css/**",
"/js/**",
"/img/**",
"/**/*.html");//放行路径
/**
* 添加拦截器以配置请求的预处理和后处理
*
* @param registry 拦截器注册对象,用于注册自定义拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册拦截器
registry.addInterceptor(loginInterceptor)
//拦截以"/book/"开头的所有请求
.addPathPatterns("/**")
//排除"/user/login"请求,使其不被拦截
.excludePathPatterns(excludePath);
}
}
这样,就可以获取到前端界面。
拦截器执行流程
在没有添加拦截器之前,我们调用顺序是:
在添加拦截器后:
- 在添加完拦截器后,执行Controller中的方法之前,请求会先被拦截器拦截住,执行preHandler() 方法,这个方法会返回一个布尔类型的值。如果返回true,说明要放行,继续访问controller中的方法;如果返回false,则不会放行(controller中的方法不会执行).
- controller中的方法执行完后,会继续执行 postHandler() 方法以及 afterCompletion() 方法,执行完毕后,最终给浏览器响应数据。
我们通过观察日志,可以看到当Tomcat在启动之后,会有核心的类 DispatcherServlet 来控制程序的执行顺序。
所有的请求都会先进入到DispatcherServlet 中,执行 doDispatch() 调度方法,如果有拦截器,就会先执行拦截器中 preHandle() 方法中的代码,如果 preHandle() 返回true,那么就会继续访问controller中的方法,当controller中的方法执行完毕,就会再回过来执行 postHandle() 和 afterCpmpletion() 方法,返回给DispatcherServlet,最终给浏览器响应数据。
我们观察 DispatcherServlet 中的源码,就可以看到这三个方法的执行流程:
在 DispatcherServlet 源码中,我还可以看到使用了适配器模式:
适配器模式
适配器模式也叫包装器模式。将一个类的接口,转换成客户期望的另有一个接口,适配器让原本接口不兼容的类可以合作无间。
简单来说:就是目标类不能直接使用,通过一个新类进行包装,适配调用方使用,把两个不兼容的接口通过一定的方式使之兼容。
使用适配器来使两个接口兼容:
其实在我们前面学习Spring日志的时候,其中的 slf4j 就使用到了适配器模式,我们想要打印日志的时候,只需要调用slf4j的API,而它会自动去调用底层的 log4j 或者 logback 来打印日志。
示例:
java">package com.example.demo.config;
/**
* Slf4j接口定义了打印日志的方法
*/
public interface Slf4j {
/**
* 打印日志信息
* @param message 需要打印的日志信息
*/
void print(String message);
}
/**
* Log4j类提供了具体的日志打印实现
*/
class Log4j{
/**
* 打印日志信息
* @param message 需要打印的日志信息
*/
void log4jPrint(String message){
System.out.println("Log4j: "+message);
}
}
/**
* Slf4jAdapter类是Slf4j接口与Log4j类之间的适配器
* 它使得Log4j可以通过Slf4j接口来打印日志
*/
class Slf4jAdapter implements Slf4j{
/**
* log4j实例用于实际的日志打印
*/
private Log4j log4j;
/**
* 构造函数,接收一个Log4j实例
* @param log4j Log4j实例,用于实际的日志打印
*/
public Slf4jAdapter(Log4j log4j) {
this.log4j = log4j;
}
/**
* 实现Slf4j接口的print方法,通过Log4j实例来打印日志
* @param message 需要打印的日志信息
*/
@Override
public void print(String message) {
log4j.log4jPrint(message);
}
}
/**
* Demo类包含主程序,用于演示Slf4jAdapter的使用
*/
class Demo{
/**
* 主函数,创建Log4j实例并通过Slf4jAdapter适配,然后打印日志信息
* @param args 命令行参数
*/
public static void main(String[] args) {
Log4j log4j = new Log4j();
Slf4j slf4j = new Slf4jAdapter(log4j);
slf4j.print("Hello World");
}
}
可以看到,我们不需要修改log4j的api,只需要使用适配器,就可以更换日志框架,维护系统。
那么为什么不直接调用Log4j呢?
适配器模式其实可以看做一种“补偿模式”,用来补救设计上的缺陷,使用这种模式是无奈之举。如果在设计初期,能够规避接口不兼容的问题,那么就不需要使用适配器模式了。
统一数据返回格式
我们在做项目的时候,如果前端调用后端返回的数据格式都不同,后序如果修改起来,就显得有点杂乱,所以我们可以对返回的数据格式进行统一——统一数据返回格式。
在SpringBoot,如果我们想要在返回数据响应之前对数据进行一些逻辑操作,那么我们就需要使用到注解 @ControllerAdvice 和 ResponseBodyAdvice 接口的实现。
定义下存常量的类:
java">package com.example.demo.constant;
public class Constants {
public static final String USER_SESSION_KEY = "user";
public static final Integer SUCCESS_CODE = 200;//成功
public static final Integer FAIL_CODE = -2;//失败
public static final Integer UNLOGIN_CODE = -1;//未登录
}
统一返回数据格式:
java">package com.example.demo.Result;
import com.example.demo.constant.Constants;
import lombok.Data;
@Data
public class Results<T> {
private Integer code;//200成功,-1未登录,-2程序异常
private String msg;
private T data;
public static <T> Results success(T data){
Results results=new Results();
results.setCode(Constants.SUCCESS_CODE);
results.setMsg("");
results.setData(data);
return results;
}
public static Results unLogin(){
Results results=new Results();
results.setCode(Constants.UNLOGIN_CODE);
results.setMsg("用户未登录");
return results;
}
public static <T> Results fail(String msg){
Results results=new Results();
results.setCode(Constants.FAIL_CODE);
results.setMsg(msg);
return results;
}
public static <T> Results fail(){
Results results=new Results();
results.setCode(Constants.FAIL_CODE);
results.setMsg("程序出现异常");
return results;
}
}
响应处理:
java">package com.example.demo.config;
import com.example.demo.Result.Results;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
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;
/**
* 全局响应处理,统一处理所有响应
* 该类实现了ResponseBodyAdvice接口,用于自定义响应体
*/
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
/**
* 判断是否支持当前的返回类型和转换器类型
* 该方法始终返回true,表示支持所有类型的响应处理
*/
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
/**
* 在写入响应体前处理数据
* 该方法根据返回的数据类型,进行相应的处理和封装
* 如果返回类型是String,则将其作为成功消息封装进Results对象
* 如果返回类型已经是Results,则直接返回
* 否则,将返回值作为成功数据封装进Results对象
*/
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if(body instanceof String){
return objectMapper.writeValueAsString(Results.success(body));
}
if(body instanceof Results){
return body;
}
return Results.success(body);
}
}
调用一下看看:
可以看到,这样的话,如果前端想要获取到我们后端的数据,以及后续修改操作等,就更加清晰容易操作了。
我们再来看一处地方:
这里为什么要这样写呢?
SpringMVC默认会注册一些自导的HttpMessageConverter(从先后顺序排序分别为ByteArrayHttpMessageConverter、StringHttpMessageConverter、SourceHttpMessageConverte、SourceHttpMessageConverterr、AllEncompassingFormHttpMessageConverter)
而其中的 AllEncompassingFormHttpMessageConverter 会根据项目依赖情况添加对应的HttpMessageConverter。
在依赖中引⼊jackson包后,容器会把 MappingJackson2HttpMessageConverter ⾃动注册到
messageConverters 链的末尾。Spring会根据返回的数据类型,从 messageConverters 链选择合适的 HttpMessageConverter。当返回的数据是⾮字符串时,使⽤的MappingJackson2HttpMessageConverter 写⼊返回对象。当返回的数据是字符串时, StringHttpMessageConverter 会先被遍历到,这时会认为
StringHttpMessageConverter 可以使用。
可以看到子类StringHttpMessageConverter中的addDefaultHeaders()方法接收的参数为String,但我们需要返回的类型为Results类型,所以这里我们就需要利用SpringBoot内置提供的Jackson来实现信息的序列化。即:
@SneakyThrows
是 Lombok 提供的一个注解,用于简化 Java 中的异常处理。它允许开发者在方法中抛出受检异常(checked exceptions),而无需在方法签名中显式声明throws
,也不需要使用try-catch
块。
优点
统一异常处理
当我们的程序出现异常时,我们也可以对这些异常进行统一处理。
统一异常处理使用的是 @ControllerAdvice 和 @ExceptionHandler 来实现的。
- @ControllerAdvice 表⽰控制器通知类
- @ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执行某个通知,也就是执⾏某个⽅法事件
java">package com.example.demo.config;
import cn.hutool.core.io.resource.NoResourceException;
import com.example.demo.Result.Results;
import lombok.extern.slf4j.Slf4j;
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;
import org.springframework.web.servlet.resource.NoResourceFoundException;
/**
* 全局异常处理类
* 用于统一处理项目中的异常,提高代码的健壮性和用户体验
*/
@ControllerAdvice
@Slf4j
@ResponseBody
public class ErrorAdviceHandler {
/**
* 处理通用异常
* @param e 异常对象
* @return 返回处理结果
*/
@ExceptionHandler
public Object handler(Exception e){
log.error("异常信息:{}",e.getMessage());
return Results.fail(e.getMessage());
}
/**
* 处理数组越界异常
* @param e 异常对象
* @return 返回处理结果
*/
@ExceptionHandler
public Object handler(ArrayIndexOutOfBoundsException e){
log.error("异常信息:{}",e.getMessage());
return Results.fail(e.getMessage());
}
/**
* 处理空指针异常
* @param e 异常对象
* @return 返回处理结果
*/
@ResponseStatus
@ExceptionHandler
public Object handler(NullPointerException e){
log.error("异常信息:{}",e.getMessage());
return Results.fail(e.getMessage());
}
/**
* 处理算数异常
* @param e 异常对象
* @return 返回处理结果
*/
@ExceptionHandler
public Object handler(ArithmeticException e){
log.error("异常信息:{}",e.getMessage());
return Results.fail(e.getMessage());
}
/**
* 处理资源未找到异常
* @param e 异常对象
* @return 返回处理结果
*/
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler
public Object handler(NoResourceFoundException e){
log.error("异常信息:{} path:{}",e.getDetailMessageCode(),e.getResourcePath());
return Results.fail(e.getMessage());
}
}
测试案例:
java">package com.example.demo.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Demo控制器类,用于处理演示项目的HTTP请求
*/
@RestController
@RequestMapping("/demo")
public class DemoController {
/**
* 处理/t1请求的方法
* 该方法演示了未处理的除零异常,用于教学或测试目的
*
* @return 成功信息字符串,但在执行中将引发除零异常
*/
@RequestMapping("/t1")
public String t1(){
// 该行代码将产生除零异常
int a=1/0;
return "success";
}
/**
* 处理/t2请求的方法
* 该方法演示了未处理的空指针异常,用于教学或测试目的
*
* @return 成功信息字符串,但在执行中将引发空指针异常
*/
@RequestMapping("/t2")
public String t2(){
// 该行代码将产生空指针异常
String str=null;
str.length();
return "success";
}
/**
* 处理/t3请求的方法
* 该方法演示了未处理的数组越界异常,用于教学或测试目的
*
* @return 成功信息字符串,但在执行中将引发数组越界异常
*/
@RequestMapping("/t3")
public String t3(){
// 初始化一个包含三个元素的数组
int[] arr={1,2,3};
// 该行代码将产生数组越界异常
arr[3]=4;
return "success";
}
}
此外,如果发生空指针异常或者其他异常,异常处理中有Exception和NullPointerException的话,优先执行具体的异常,即执行NullPointerException异常。
具体异常优先于通用异常
以上就是本篇所有内容~
若有不足,欢迎指正~