Spring AOP使Spring Boot统一功能处理模块成为可能,以下是三个经典的应用场景
- 统一用户登录权限验证
- 统一数据格式返回
- 统一异常处理
一:用户登录权限验证
(1)传统用户登录验证
在没有使用Spring AOP的情况下,传统的或者最初级的登录验证通常是硬编码在业务逻辑中的。这意味着你需要在每个需要验证的方法或代码段中直接写入验证逻辑。这种做法通常会导致代码重复,并且使得业务逻辑和安全验证逻辑紧密耦合,这在维护和扩展上不是最佳实践
例子
// 用户登录
import javax.servlet.http.HttpSession;
@RestController
public class LoginController {
@Autowired
private AuthenticationService authenticationService; // 负责验证用户身份的服务
@PostMapping("/login")
public String login(@RequestParam String username, @RequestParam String password, HttpSession session) {
if (authenticationService.authenticate(username, password)) {
session.setAttribute("user", username); // 在Session中存储用户信息
return "Login successful";
} else {
return "Login failed";
}
}
}
// 权限校验
public class SomeService {
public void someAdminOperation(HttpSession session) {
String currentUser = (String) session.getAttribute("user");
if (currentUser == null) {
throw new UnauthorizedException("User is not logged in.");
}
if (!authenticationService.hasAdminPermission(currentUser)) {
throw new UnauthorizedException("User does not have admin permission.");
}
// 业务逻辑
}
public void someUserOperation(HttpSession session) {
String currentUser = (String) session.getAttribute("user");
if (currentUser == null) {
throw new UnauthorizedException("User is not logged in.");
}
if (!authenticationService.hasUserPermission(currentUser)) {
throw new UnauthorizedException("User does not have user permission.");
}
// 业务逻辑
}
// 更多方法...
}
可以看出,这种方式缺点很大
- 每个方法中都要单独写用户登录验证的方法,即便封装成公共方法,也同样要传参调用和在方法中进行判断
- 添加控制器越多,调用用户登录验证的方法也越多,这样就增加了后期的修改和维护成本
- 这些用户登录验证方法和下面的业务逻辑几乎没有任何关系
(2)使用原生Spring AOP进行用户登录验证
使用Spring AOP的切面也可以实现用户登录校验,但是存在下面的问题
- 在切面类中拿到HttpSession有一定难度
- 定义拦截规则比较难,这是因为我需要只对一部分方法进行拦截
例子:使用Spring AOP的前置通知或环绕通知完成
(3)Spring 拦截器
Spring拦截器:允许你在Spring MVC的请求处理过程中的特定点进行干预。它们在Spring的Web应用程序中广泛使用
Spring拦截器需要实现HandlerInterceptor
接口,该接口包含三个主要方法
preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
:该方法在请求处理之前调用。返回值决定是否继续执行拦截器链(true)或是直接返回(false)postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
:该方法在处理器执行完毕后调用,但在视图渲染之前执行afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
:该方法在整个请求处理完毕后调用,即在视图渲染完毕时
A:自定义拦截器
如下,利用拦截器实现用户登录权限校验。具体步骤如下
- 实现
HandlerInterceptor
接口 - 重写
preHeadler
方法,然后编写自己的业务代码 - 将拦截器添加到配置文件中,并且设置拦截规则(重写
addInterceptors
)方法addPathPatterns
:表示需要拦截的URL,**表示所有excludePathPatterns
:表示需要排除的URL
工程项目展示
UserController.java
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/getuser")
public String getUser() {
System.out.println("如果打印了,说明应该拦截但没有拦截");
return "应该拦截但没有拦截";
}
@RequestMapping("/login")
public String login() {
System.out.println("没有拦截");
return "没有拦截";
}
}
LoginInterceptor.java
package com.example.demo.config;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* 返回boolean类型,如果为true表示验证成功,如果为false表示验证失败
*/
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 用户登录业务判断
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
// 说明用户已经登录
return true;
}
// 未登录(这里也可以利用response跳转至登录页面)
response.setStatus(401); // 未认证
return false;
}
}
AppConfig.java
package com.example.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // 先全部拦截
.excludePathPatterns("/user/login") // 排除的url地址
.excludePathPatterns("/user/reg")
.excludePathPatterns("/login.html");
}
}
测试
- 第一张图:由于
/user/login
排除在外,因此正确访问 - 第二张图:由于
/user/getuser
在拦截规则内,所以无法访问
B:拦截器实现原理
①:概述
如下图
- 左图是正常的调用顺序
- 右图是加入拦截器后,会在调用Controller之前进行相应的业务处理
②:源码分析
所有的Controller执行都会通过一个调度器DispatcherServlet
实现
详解如下
- DispatcherServlet:
- Spring MVC的核心控制器是
DispatcherServlet
。当一个请求到达时,它首先被DispatcherServlet
接收。 DispatcherServlet
的任务是将请求路由到相应的处理器(Controller)并返回响应。在这个过程中,它负责协调各种组件,如处理器映射(Handler Mapping)、处理器适配器(Handler Adapter)和视图解析器(View Resolver)。
- Spring MVC的核心控制器是
- 处理器映射(Handler Mapping):
HandlerMapping
组件负责根据请求查找相应的处理器(Controller)及其拦截器链。- 在Spring MVC中,
HandlerMapping
通常会返回一个HandlerExecutionChain
对象,该对象包含了处理器(Controller)及其应用的拦截器链。
- 拦截器链(Interceptor Chain):
- 拦截器链是由一系列实现了
HandlerInterceptor
接口的拦截器组成的。 - 拦截器的执行顺序是在
HandlerMapping
中定义的,而且只会作用于通过这个HandlerMapping
找到的处理器。
- 拦截器链是由一系列实现了
- 拦截器方法的调用:
preHandle
:在处理器(Controller)方法执行之前调用。如果返回true
,处理流程继续;如果返回false
,流程终止,后续的postHandle
和afterCompletion
不会被调用。postHandle
:在处理器方法执行之后、视图渲染之前调用。可以操作请求和响应,但无法中断响应流。afterCompletion
:在整个请求完成后,也就是视图渲染完成后调用。用于清理资源。
- HandlerAdapter:
HandlerAdapter
负责执行处理器(Controller)中的方法。- 它在
preHandle
方法返回true
之后执行相应的处理器方法,然后调用postHandle
。
- 视图渲染:
- 完成处理器方法的执行和
postHandle
方法后,DispatcherServlet
将处理结果传递给相应的视图进行渲染。
- 完成处理器方法的执行和
- afterCompletion:
- 最后,无论请求处理过程中是否抛出异常,
afterCompletion
方法都会被调用,用于执行清理工作
- 最后,无论请求处理过程中是否抛出异常,
(4)补充:统一访问前缀的添加
在Web开发中,使用统一访问前缀(Uniform Access Prefix)主要有以下几个作用
- 组织和管理URL路径:通过设置统一的前缀,可以更好地组织和管理URL路径。这在大型应用中尤为重要,因为它有助于保持URL的结构清晰和一致,使得路径的层次和模块划分更加明显
- 模块化设计:在多模块应用中,不同模块可能会有自己的一套URL路径。使用统一访问前缀可以很好地区分这些模块,使得每个模块都在自己的命名空间内运行,从而减少路径冲突的可能性
- 安全性:统一的前缀有助于安全配置,例如,在使用拦截器或过滤器进行权限控制时,可以只针对特定的URL前缀应用这些控制,这样就能更有效地管理安全策略
- 易于维护和扩展:当需要对应用进行扩展或重构时,有了统一的前缀,就可以更容易地重构URL结构,而不会影响到整个应用的其他部分。这对于长期维护和升级应用非常有利
- 路由管理:在一些现代Web框架中,比如Spring MVC,统一前缀可以与路由机制结合使用,简化控制器的路由配置。例如,可以为同一个控制器内的所有方法指定一个共同的前缀。
- API版本控制:在设计API时,统一前缀常用于版本控制。例如,可以使用
/api/v1/
作为第一版API的前缀,当API升级到第二版时,可以使用/api/v2/
,这样就能在不中断旧版本服务的情况下进行升级 - 清晰的API界限:对于提供RESTful服务的应用来说,统一前缀有助于明确API和非API部分的界限,这对于那些同时提供前端视图和API服务的应用尤其重要
添加方法如下
AppConfig.java
package com.example.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // 先全部拦截
.excludePathPatterns("/user/login") // 排除的url地址
.excludePathPatterns("/user/reg")
.excludePathPatterns("/login.html");
}
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("api", c->true);
}
}
二:统一异常处理
统一异常处理使用@ControllerAdvice+@ExceptionHandler
实现
@ControllerAdvice
:用于声明一个类作为全局异常处理器。它可以捕获整个Spring应用中控制器抛出的异常,并允许开发者在一个地方处理这些异常。一般来说我们会建立一个类,然后加上这个注解ExceptionHandler
:是一个方法级别的注解,用于指定某个方法处理特定的异常。当指定类型的异常被抛出时,这些方法会被自动调用,允许开发者定义异常的处理逻辑。如果没有捕获到特定类型异常,则会走全局异常。有以下特定异常类型- 检查型异常(Checked Exceptions)
IOException
:输入输出异常,如文件未找到、网络错误等SQLException
:与数据库操作相关的异常ClassNotFoundException
:指定类不存在的异常NoSuchMethodException
:尝试访问不存在的方法时抛出的异常 - 非检查型异常(Unchecked Exceptions)
NullPointerException
:尝试使用null
对象时抛出的异常ArrayIndexOutOfBoundsException
:数组索引越界异常ArithmeticException
:算术异常,如除以零IllegalArgumentException
:表示向方法传递了一个不合法或不适当的参数
- Spring框架特定的异常
DataAccessException
:Spring的数据访问异常基类HttpClientErrorException
和HttpServerErrorException
:Spring RestTemplate使用时的客户端和服务器错误异常MethodArgumentNotValidException
:当方法参数验证失败时抛出的异常NoSuchRequestHandlingMethodException
:Spring MVC在找不到匹配的请求处理方法时抛出的异常
- 检查型异常(Checked Exceptions)
例子:下面UserController分别在getuser方法和login方法中设置了空指针异常和数值异常
UserController.java
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
// 空指针异常
@RequestMapping("/getuser")
public String getUser() {
Object o = null;
o.hashCode();
return "应该拦截但没有拦截";
}
// 算数异常
@RequestMapping("/login")
public String login() {
int sum = 10 / 0;
return "login";
}
}
MyExHandler.java
package com.example.demo.config;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
@ControllerAdvice
@ResponseBody // 注意
public class MyExHandler {
/**
* 拦截所有空指针异常,并统一返回
* @param e
* @return
*/
@ExceptionHandler(NullPointerException.class)
public HashMap<String,Object> nullException(NullPointerException e) {
HashMap<String,Object> result = new HashMap<>();
result.put("code", "-1"); // 错误码
result.put("msg", "空指针异常:" + e.getMessage()); // 错误码描述信息
result.put("data", null);
return result;
}
@ExceptionHandler(Exception.class)
public HashMap<String,Object> exception(Exception e) {
HashMap<String,Object> result = new HashMap<>();
result.put("code", "-1"); // 错误码
result.put("msg", "异常:" + e.getMessage()); // 错误码描述信息
result.put("data", null);
return result;
}
}
效果
三:统一数据返回格式
统一数据返回格式@ControllerAdvice+@ResponseBodyAdvice
实现
@ControllerAdvice
:用于声明一个类作为全局异常处理器@ResponseBodyAdvice
:是一个接口,用于在响应体写入到HTTP响应之前对其进行处理。需要重写以下两个方法supports
:用于控制是否需要统一返回。如果设置为true
则beforeBodyWrite
继续执行,否则直接结束beforeBodyWrite
:你的统一数据返回格式的逻辑
例子:如果没有统一数据返回格式,那么所返回的格式则是五花八门
UserController.java
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Random;
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/getnum")
public Integer getNum() {
return new Random().nextInt(10);
}
@RequestMapping("/getuser")
public String getUser() {
return "getuser";
}
@RequestMapping("/login")
public String login() {
return "login";
}
}
使用统一数据格式后,均为json,便于和前端沟通
ResponseBodyAdvice
package com.example.demo.config;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
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;
import java.util.HashMap;
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
// 引入JackSon对String特殊处理
@Autowired
private ObjectMapper objectMapper;
/**
* 如果返回true,那么beforeBodyWrite执行
* @param returnType
* @param converterType
* @return
*/
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return false;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 统一返回
HashMap<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", "");
result.put("data", body);
// 注意需要对String类型特殊处理,因为String在转换时会报错
if (body instanceof String) {
try {
return objectMapper.writeValueAsString(result);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
// 否则Spring会自动转换
return result;
}
}
评论区