在介绍Spring AOP之前,首先要了解一下什么是AOP?
AOP (Aspect Oriented Programming)︰面向切面编程,它是一种思想,它是对某一类事情的集中处理。比如用户登录权限的效验,没学AOP之前,我们所有需要判断用户登录的页面(中的方法),都要各自实现或调用用户验证的方法,然而有了AOP之后,我们只需要在某一处配置一下,所有需要判断用户登录页面(中的方法)就全部可以实现用户登录验证了,不再需要每个方法中都写相同的用户登录验证了。
而AOP是一种思想,而Spring AOP是一个框架,提供了一种对AOP思想的实现,它们的关系和loC与DI类似。
我们之前的处理方式是每个Controller都要写一遍用户登录验证,然而当你的功能越来越多,那么你要写的登录验证也越来越多,而这些方法又是相同的,这么多的方法就会代码修改和维护的成本。那有没有简单的处理方案呢?答案是有的,对于这种功能统一,且使用的地方较多的功能,就可以考虑AOP来统一处理了。
除了统一的用户登录判断之外,AOP还可以实现:
也就是说使用AOP可以扩充多个对象的某个能力,所以AOP可以说是OOP (Object OrientedProgramming,面向对象编程)的补充和完善。
Spring AOP学习主要分为以下3个部分:
1.学习AOP是如何组成的?也就是学习AOP组成的相关概念。
2.学习Spring AOP使用。
3.学习Spring AOP实现原理。下面我们分别来看。
切面(Aspect)由切点(Pointcut)和通知(Advice)组成,它既包含了横切逻辑的定义,也包括了连接点的定义。
切面是包含了:通知、切点和切面的类,相当于AOP实现的某个功能的集合。
应用执行过程中能够插入切面的一个点,这个点可以是方法调用时,抛出异常时,甚至修改字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
连接点相当于需要被增强的某个AOP功能的所有方法。
Pointcut是匹配Join Point的谓词。
Pointcut 的作用就是提供一组规则(使用AspectJ pointcut expression language来描述)来匹配Join Point,给满足规则的Join Point添加Advice。
切点相当于保存了众多连接点的一个集合(如果把切点看成一个表,而连接点就是表中一条一条的数据)。
切面也是有目标的——它必须完成的工作。在AOP术语中,切面的工作被称之为通知。
通知︰定义了切面是什么,何时使用,其描述了切面要完成的工作,还解决何时执行这个工作的问题。
Spring切面类中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本方法进行调用:
切点相当于要增强的方法。
AOP整个组成部分的概念如下图所示,以多个页面都要访问用户登录权限为例:
使用Spring AOP来实现一下AOP的功能,完成的目标是拦截所有UserController里面的方法,每次调用UserController中任意一个方法时,都执行相应的通知事件。
Spring AOP 的实现步骤是:
通知定义的是被拦截的方法具体要执行的业务,比如用户登录权限验证方法就是具体要执行的业务Spring AOP中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本方法进行调用:
具体实现如下:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class UserAspect {
// 定义切点⽅法
@Pointcut("execution(* com.example.demo.controller.UserController.*
(..))")
public void pointcut(){ }
// 前置通知
@Before("pointcut()")
public void doBefore(){
System.out.println("执⾏ Before ⽅法");
}
// 后置通知
@After("pointcut()")
public void doAfter(){
System.out.println("执⾏ After ⽅法");
}
// return 之前通知
@AfterReturning("pointcut()")
public void doAfterReturning(){
System.out.println("执⾏ AfterReturning ⽅法");
}
// 抛出异常之前通知
@AfterThrowing("pointcut()")
public void doAfterThrowing(){
System.out.println("执⾏ doAfterThrowing ⽅法");
}
// 添加环绕通知
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint){
Object obj = null;
System.out.println("Around ⽅法开始执⾏");
try {
// 执⾏拦截⽅法
obj = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("Around ⽅法结束执⾏");
return obj;
}
}
Spring AOP是构建在动态代理基础上,因此Spring对AOP的支持局限于方法级别的拦截。
Spring AOP支持JDK Proxy和CGLIB方式实现动态代理。默认情况下,实现了接口的类,使用AOP会基于JDK生成代理类,没有实现接口的类,会基于CGLIB生成代理类。
这两种方式的代理目标都是被代理类中的方法,在运行期,动态的织入字节码生成代理类。
InvocationHandler
及 Proxy
,在运行时动态的在内存中生成了代理类对象,该代理对象是通过实现同样的接口实现(类似静态代理接口实现的方式),只是该代理类是在运行期时,动态的织入统一的业务逻辑字节码来完成。织入是把切面应用到目标对象并创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中。
在目标对象的生命周期里有多个点可以进行织入∶
此种实现在设计模式上称为动态代理模式,在实现的技术手段上,都是在class代码运行期,动态的织入字节码生成代理类。
AOP是对某方面能力的统一实现,它是一种实现思想,Spring AOP是对AOP的具体实现,SpringAOP可通过AspectJ(注解) 的方式来实现AOP的功能,Spring AOP 的实现步骤是:
Spring AOP是通过动态代理的方式,在运行期将AOP代码织入到程序中的,它的实现方式有两种JDK Proxy和CGLIB。
接下来是Spring Boot统一功能处理模块了,也是AOP的实战环节,要实现的课程目标有以下3个:
接下我们一个一个来看。
用户登录权限的发展从之前每个方法中自己验证用户登录权限,到现在统一的用户登录验证处理,它是—个逐渐完善和逐渐优化的过程。
Spring 中提供了具体的实现拦截器:HandlerInterceptor,
统一用户登录权限的效验使用WebMvcConfigurer + HandlerInterceptor来实现。
拦截器的实现分为以下两个步骤∶
接下来使用代码来实现一个用户登录的权限效验,自定义拦截器是一个普通类,具体实现代码如下
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
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.setStatus(401);
return false;
}
}
将上一步中的自定义拦截器加入到系统配置信息中,具体实现代码如下:
@Configuration
public class AppConfig implements WebMvcConfigurer {
// 添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // 拦截所有接⼝
.excludePathPatterns("/art/param11"); // 排除接⼝
}
}
其中:
说明:以上拦截规则可以拦截此项目中的使用URL,包括静态文件(图片文件、JS和CSS等文件
排除所有的静态资源
// 拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // 拦截所有接⼝
.excludePathPatterns("/**/*.js")
.excludePathPatterns("/**/*.css")
.excludePathPatterns("/**/*.jpg")
.excludePathPatterns("/login.html")
.excludePathPatterns("/**/login"); // 排除接⼝
}
然而有了拦截器之后,会在调用Controller 之前进行相应的业务处理,执行的流程如下图所示:
通过上面的源码分析,我们可以看出,Spring 中的拦截器也是通过动态代理和环绕通知的思想实现的大体的调用流程如下:
所有请求地址添加 api 前缀:
@Configuration
public class AppConfig implements WebMvcConfigurer {
// 所有的接⼝添加 api 前缀
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("api", c -> true);
}
}
其中第二个参数是⼀个表达式,设置为 true 表示启动前缀。
统一异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的,
@ControllerAdvice表示控制器通知类,@ExceptionHandler是异常处理器,两个结合表示当出现异常的时候执行某个通知就是执行某个方法事件,具体实现代码如下:
import java.util.HashMap;
@ControllerAdvice
public class ErrorAdive {
@ExceptionHandler(Exception.class)
@ResponseBody
public Object handler(Exception e) {
HashMap<String, Object> map = new HashMap<>();
map.put("success", 0);
map.put("status", 1);
map.put("msg", e.getMessage());
return map;
}
}
PS:方法名和返回值可以自定义,其中最重要的是@ExceptionHandler(Exception.class)注解.
以上方法表示,如果出现了异常就返回给前端一个HashMap的对象,其中包含的字段如代码中定义的那样。
我们可以针对不同的异常,返回不同的结果,比以下代码所示:
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 ExceptionAdvice {
@ExceptionHandler(Exception.class)
public Object exceptionAdvice(Exception e) {
HashMap<String, Object> result = new HashMap<>();
result.put("success", -1);
result.put("message", "总的异常信息:" + e.getMessage());
result.put("data", null);
return result;
}
@ExceptionHandler(NullPointerException.class)
public Object nullPointerexceptionAdvice(NullPointerException e) {
HashMap<String, Object> result = new HashMap<>();
result.put("success", -1);
result.put("message", "空指针异常:" + e.getMessage());
result.put("data", null);
return result;
}
}
当有多个异常通知时,匹配顺序为当前类及其子类向上依次匹配,案例演示。在UserController中设置一个空指针异常,实现代码如下:
@RestController
@RequestMapping("/u")
public class UserController {
@RequestMapping("/index")
public String index() {
Object obj = null;
int i = obj.hashCode();
return "Hello,User Index.";
}
}
统一数据返回格式的优点有很多,比如以下几个:
统一的数据返回格式可以使用
@ControllerAdvice + ResponseBodyAdvice 的方式实现,具体实现代码如下:
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 {
/**
* 内容是否需要重写(通过此⽅法可以选择性部分控制器和⽅法进⾏重写)
* 返回 true 表示重写
*/
@Override
public boolean supports(MethodParameter returnType, Class
converterType) {
return true;
}
/**
* ⽅法返回之前调⽤此⽅法
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter
returnType, MediaType selectedContentType, Class selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
// 构造统⼀返回对象
HashMap<String, Object> result = new HashMap<>();
result.put("success", 1);
result.put("message", "");
result.put("data", body);
return result;
}
}