拦截器(Interceptor)与过滤器(Filter)类似,是面向切面编程的一种具体实现。你可以使用拦截器执行某些任务,比如在控制器处理请求前记录日志、更新配置等。在 Spring 中,当请求发送到控制器时,在被控制器处理之前,它必须经过拦截器。
拦截器与过滤器的区别
拦截器(Interceptor)和过滤器(Filter)的主要区别在于作用范围和实现方式。
作用范围
- 过滤器作用于整个 Web 应用程序,可以过滤所有请求和响应。它是 Servlet 规范的一部分,由 Servlet 容器管理。
- 拦截器通常作用于特定框架。比如,在 Spring Boot 中,它主要拦截特定框架的请求处理流程,并在特定框架内处理请求。
实现方式
- 过滤器实现 javax.servlet.Filter 接口,并在 web.xml 中或通过注解进行配置。需要实现 init()、doFilter() 和 destroy() 等方法。
- 拦截器在不同框架中有不同的实现方式。比如,在 Spring MVC 中,可以实现 HandlerInterceptor 接口或继承 HandlerInterceptorAdapter 类。需要实现 preHandle()、postHandle() 和 afterCompletion() 等方法。
拦截器的作用
- 登录验证和访问控制:拦截器可用于检查用户的登录状态和权限,并根据需要执行相关处理。比如,可以使用拦截器验证用户的登录状态。如果未登录,则重定向到登录页面或返回相应的错误信息。
- 异常处理和统一错误处理:拦截器可以捕获并处理请求处理过程中发生的异常。可以根据异常类型执行适当的处理,比如返回自定义错误页面或错误信息,或执行特定的错误处理逻辑。当然,它还有许多其他应用场景,这里不再一一列举。
自定义拦截器
如果你需要自定义拦截器,必须实现 org.springframework.web.servlet.HandlerInterceptor 接口或继承 org.springframework.web.servlet.handler.HandlerInterceptorAdapter 类,并且需要重写以下三个方法:
- **preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)**:此方法在请求处理之前调用。该方法在拦截器类中首先执行,用于一些预初始化操作或对当前请求进行预处理。你还可以进行一些判断,以确定请求是否应该继续。此方法的返回值为布尔类型。当它返回 false 时,表示请求结束,后续的拦截器和控制器将不再执行。当它返回 true 时,将调用下一个拦截器的 preHandle 方法。如果已经是最后一个拦截器,则将调用当前请求的控制器方法。
- **postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)**:此方法在当前请求处理完成后执行,即在控制器方法被调用之后。但是,它将在 DispatcherServlet 渲染视图之前被调用。因此,我们可以在此方法中在控制器处理后对 ModelAndView 对象进行操作。
- **afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex)**:只有当相应拦截器类的 postHandle 方法的返回值为 true 时,此方法才会执行。顾名思义,此方法将在整个请求结束后执行,即在 DispatcherServlet 渲染相应视图之后。此方法主要用于资源清理。
接下来,让我们通过实际代码学习。以用户登录权限验证为例:
用户登录权限验证
1.自定义拦截器
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@Component
publicclass LoginInterceptor implements HandlerInterceptor {
// 在调用目标方法之前执行的方法。
// 返回 true 表示拦截器验证成功,执行目标方法。
// 返回 false 表示拦截器验证失败,不执行后续业务逻辑。
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 用户登录判断业务。
HttpSession session = request.getSession(false);
if (session!= null && session.getAttribute("session_userinfo")!= null) {
// 用户已登录。
returntrue;
}
response.setStatus(401);
returnfalse;
}
}
代码中的 preHandle 方法是拦截器的主要方法,在目标方法被调用之前执行。它接收三个参数:HttpServletRequest 对象表示当前 HTTP 请求,HttpServletResponse 对象表示当前 HTTP 响应,Object handler 表示被拦截的处理器(通常是控制器中的一个方法)。
在 preHandle 方法中,首先通过 request.getSession(false)(如果存在)获取当前请求的 HttpSession 对象,然后判断这个 HttpSession 对象是否为 null 以及是否存在名为“session_userinfo”的属性。
如果这个条件为真,则表示用户已登录,可以继续执行后续业务,所以返回 true。否则,验证失败,将 HTTP 响应的状态码设置为 401,表示未授权,然后返回 false,不再继续执行后续业务。
2.将自定义拦截器添加到系统配置中
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
publicclass MyConfig implements WebMvcConfigurer {
// 注入。
@Autowired
private LoginInterceptor loginInterceptor;
// 添加拦截器。
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**") // 拦截所有 URL。
.excludePathPatterns("/user/login") // 排除 URL:/user/login(登录)。
.excludePathPatterns("/user/reg") // 排除 URL:/user/reg(注册)。
.excludePathPatterns("/image/**") // 排除“image”文件夹下的所有文件。
.excludePathPatterns("/**/*.js") // 排除任意深度目录下的所有“.js”文件。
.excludePathPatterns("/**/*.css");
}
}
在配置类中,重写 addInterceptors 方法。此方法用于注册拦截器。在这里,通过调用 InterceptorRegistry 的 addInterceptor 方法添加拦截器,并设置拦截路径和排除路径。
具体来说,通过调用 addInterceptor(loginInterceptor) 添加 LoginInterceptor 拦截器。然后使用 addPathPatterns 方法指定需要拦截的 URL 路径模式。这里,“/**”用于表示拦截所有 URL。使用 excludePathPatterns 方法排除一些不会被拦截的特定 URL 路径。
3.用户控制器
@RestController
@RequestMapping("/user")
publicclass UserController {
@RequestMapping("/login")
public String login() {
return"login";
}
@RequestMapping("/index")
public String index() {
return"index";
}
@RequestMapping("/reg")
public String reg() {
return"reg";
}
}
使用浏览器访问路径/user/login 的结果输出如下:
然后,使用浏览器访问路径/user/index 的结果输出如下:
可以看到,返回了 401 错误,这是预期的结果。
然后,使用浏览器访问路径/user/reg 的结果输出如下:
添加统一访问前缀
在 WebMvcConfigurer 接口中,configurePathMatch 方法用于配置路径匹配规则。这里我们给所有请求地址添加前缀“pre”。
@Configuration
public class MyConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("pre", new Predicate<Class<?>>() {
@Override
public boolean test(Class<?> aClass) {
return true;
}
});
}
}
在这个例子中,传递给 addPathPrefix 方法的前缀是“pre”,Predicate 对象是一个实现了 Predicate<Class<?>>接口的匿名内部类。Predicate 接口是 Java 8 中引入的一个函数式接口,其 test 方法用于判断传入的类是否满足条件。
在这个匿名内部类中,重写 test 方法使其始终返回 true,这意味着所有类都满足条件,并且将添加统一的访问前缀。
因此,通过这段代码的配置,所有请求路径都将在前面添加“pre”前缀。比如,如果原始路径是“example”,添加前缀后的路径变为“/pre/example”。这样可以实现对请求路径的统一处理。
注意:如果添加了前缀,拦截器的排除路径也应该相应更改。
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.excludePathPatterns("/pre/user/login") // 排除 URL:/user/login(登录)。
.excludePathPatterns("/pre/user/reg") // 排除 URL:/user/reg(注册)。
.addPathPatterns("/**") // 拦截所有 URL。
.excludePathPatterns("/pre/image/**") // 排除“image”文件夹下的所有文件。
.excludePathPatterns("/pre/**/*.js") // 排除任意深度目录下的所有“.js”文件。
.excludePathPatterns("/pre/**/*.css");
}
配置本地资源映射路径
实现 WebMvcConfigurer 接口并重写 addResourceHandlers(ResourceHandlerRegistry registry) 方法,其中:
- addResourceHandler() 添加访问路径;
- addResourceLocations() 添加映射的真实路径。映射的真实路径末尾必须跟“/”,否则无法映射。“/”在 Windows 和 Linux 中都适用。
示例代码:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
publicclass MyWebMVCConfig implements WebMvcConfigurer {
@Value("${file.location}") // D:/test/
private String fileLocation;
@Value("${file.path}") // /file/**
private String filePath;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 当资源处理器匹配时,将 URL 映射到位置,即本地文件夹。
registry.addResourceHandler(filePath).addResourceLocations("file:///" + fileLocation); // 这里的最后一个“/”不能省略。
}
}
这段代码将配置一个拦截器。如果访问路径是 addResourceHandler 中的路径,那么它将被映射到 addResourceLocations 参数中的路径。这样,其他人就可以访问服务器上的本地文件,如本地图片或本地音乐视频。
统一异常处理
猜猜访问以下代码后会返回什么?
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public String login() {
Object object = null;
object.hashCode();
return "login";
}
}
答案是:
有没有一种方法可以在发生异常时返回有用信息,而不是这样混乱的错误消息?这就是统一异常处理。
@ControllerAdvice
@ResponseBody
public class MyExceptionAdvice {
@ExceptionHandler(NullPointerException.class)
public HashMap<String, Object> handleNPE(NullPointerException e) {
HashMap<String, Object> result = new HashMap<>();
result.put("code", 1001);
result.put("msg", "npe:" + e.getMessage());
result.put("data", null);
return result;
}
}
- @ControllerAdvice 注解表示这个类是一个全局异常处理器。它将捕获应用程序中抛出的异常并执行相应的处理逻辑。
- @ExceptionHandler(NullPointerException.class) 注解指定处理 NullPointerException 类型异常的方法 handleNPE()。
- handleNPE() 方法的参数是一个 NullPointerException 类型的异常对象,表示具体捕获的异常实例。
- handleNPE() 方法返回一个 HashMap<String, Object>对象,用于封装异常处理结果。
这段代码的作用是,当捕获到 NullPointerException 异常时,执行 handleNPE() 方法并返回一个包含异常处理结果的 HashMap 对象。这个结果将以 JSON 格式返回给客户端。
访问 localhost:8080/user/login 的结果如下:
当有多个异常处理器时,处理顺序如下: 异常处理器按照它们在代码中定义的顺序执行。如果一个异常匹配多个异常处理器,将首先执行与异常类型更具体匹配的那个。如果没有具体匹配,则可能考虑更通用的异常处理器。
比如,如果有一个针对异常的特定子类的异常处理器和另一个针对更通用的超类异常的异常处理器,将优先执行针对特定子类的处理器。
@ControllerAdvice
@ResponseBody
publicclass MyExceptionAdvice {
@ExceptionHandler(NullPointerException.class)
public HashMap<String, Object> handleNPE(NullPointerException e) {
HashMap<String, Object> result = new HashMap<>();
result.put("code", -1);
result.put("msg", "npe:" + e.getMessage());
result.put("data", null);
return result;
}
@ExceptionHandler(Exception.class)
public HashMap<String, Object> handleAllException(Exception e) {
HashMap<String, Object> result = new HashMap<>();
result.put("code", -1);
result.put("msg", "Exception:" + e.getMessage());
result.put("data", null);
return result;
}
}
再次尝试访问/user/login。
结论是:如果有匹配,子类优先。如果没有匹配,查找父类。