目录
🐳今日良言:理想的人生是靠拼来的,许愿到不了
🐳一、AOP
🐯1.基本概念
在介绍Spring AOP 之前,首先需要了解一下什么是AOP?
AOP(Aspect Oriented Programming): 面向切面编程,是一种思想,是一种软件开发的编程范式。
在传统的面向对象编程中,程序的功能逻辑被分散在各个对象中,而横切关注点(如日志记录、事务管理、安全控制等)则分散在多个对象之间,导致代码重复、可维护性差,并且难以修改和扩展。AOP 的目标就是解决这些问题。
以用户登录权限的校验,没学 AOP 之前,我们所有需要判断用户登录的页面中的方法,都要实现或调用用户验证的方法,然后有了 AOP 之后,我们只需要在某一处配置一下,所有需要判断用户登录页面(中的方法)就全部可以实现用户登录验证了,不再需要每个方法中都写相同的用户登录校验了。
以 Spring AOP 如下代码为例:
@RestController
@RequestMapping("/user")
public class UserController {
/**
* 某⽅法 1
*/
@RequestMapping("/m1")
public Object method(HttpServletRequest request) {
// 有 session 就获取,没有不会创建
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
// 说明已经登录,业务处理
return true;
} else {
// 未登录
return false;
}
}
/**
* 某⽅法 2
*/
@RequestMapping("/m2")
public Object method2(HttpServletRequest request) {
// 有 session 就获取,没有不会创建
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
// 说明已经登录
return true;
} else {
// 未登录
return false;
}
}
// 其他方法....
1. 每个⽅法中都要单独写⽤户登录验证的⽅法,即使封装成公共⽅法,也⼀样要传参调⽤和在⽅法中
进⾏判断。
2. 这些⽤户登录验证的⽅法和接下来要实现的业务⼏何没有任何关联,但每个⽅法中都要写⼀遍。
所以可以提供⼀个公共的 AOP ⽅法来进⾏统⼀的⽤户登录权限验证。
一言以蔽之:AOP 是面向切面编程:面向某一部分做集中处理的编程,如用户登录权限验证等,AOP 是对某一类事情的集中处理。
🐯2.AOP 组成
1). 切面(Aspect)
类
某一方面的具体内容就是一个切面,比如用户登录校验就是一个“切面”,而日志的统计记录又是一个“切面”。
切面是包含了:切点,通知的类,相当于 AOP 实现某个功能的集合。
2).切点(Pointcut)
方法
定义了一个拦截规则
切点相当于保存了众多连接点的一个集合(如果把切点看成一个表,而连接点就是表中的一条条数据)
3).通知(Advice)
方法具体实现代码
切面的工作(要做的事)被称为通知。
Spring 切⾯类中,可以在⽅法上使⽤以下注解,会设置⽅法为通知⽅法,在满⾜条件后会通知本方法进行调用,通知主要有五种:
前置通知使⽤ @Before:通知⽅法会在⽬标⽅法调⽤之前执⾏。
后置通知使⽤ @After:通知⽅法会在⽬标⽅法返回或者抛出异常后调⽤。返回通知使⽤ @AfterReturning:通知⽅法会在⽬标⽅法返回后调⽤。
异常通知使⽤ @AfterThrowing:通知⽅法会在⽬标⽅法抛出异常后调⽤。
环绕通知使⽤ @Around:通知包裹了被通知的⽅法,在被通知的⽅法通知之前和调⽤之后执⾏⾃定义的⾏为4).连接点(Join Point)
所有可能触发切点的点。
5).织入(Weaving)
代理的生成时机
主要有三个时机:
编译期:切⾯在⽬标类编译时被织⼊。这种⽅式需要特殊的编译器。AspectJ的织⼊编译器就是以这种⽅式织⼊切⾯的。
类加载期:切⾯在⽬标类加载到JVM时被织⼊。这种⽅式需要特殊的类加载器(ClassLoader),它可以在⽬标类被引⼊应⽤之前增强该⽬标类的字节码。AspectJ5的加载时织⼊(load-time weaving. LTW)就⽀持以这种⽅式织⼊切⾯。
运⾏期:切⾯在应⽤运⾏的某⼀时刻被织⼊。⼀般情况下,在织⼊切⾯时,AOP容器会为⽬标对象动态创建⼀个代理对象。SpringAOP就是以这种⽅式织⼊切⾯的。
接下来,学习一下 Spring AOP的使用:
1). 首先,添加Spring AOP框架:
在Maven中央仓库进行搜索:
选择Spring Boot AOP
2)、创建切面:
package com.example.demo.common;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* @author 26568
* @date 2023-06-27 13:45
*/
@Aspect // 切面
@Component
public class UserAop {
}
3)、创建切点,定义一个拦截规则
@Pointcut("execution(* com.example.demo.controller.Usercontroller.*(..))")
public void pointcut() {
}
4)、创建通知
创建前置通知:
@Before("pointcut()") // 切点名
public void doBefore() {
System.out.println("执行了前置通知:"+ LocalDateTime.now());
}
5)、创建连接点
创建controller包,然后在该包下创建UserController类:
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author 26568
* @date 2023-06-27 14:39
*/
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/test1")
public String sayHi() {
System.out.println("执行了 sayHi 方法");
return "hello spring aop";
}
@RequestMapping("/login")
public String login() {
System.out.println("执行了 login 方法");
return "do user login";
}
}
然后再controller 包下,创建一个ArticleController类:
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author 26568
* @date 2023-06-27 14:42
*/
@RestController
@RequestMapping("/art")
public class ArticleController {
@RequestMapping("/test1")
public String sayHi() {
System.out.println("执行了 ArticleController 的sayHi 方法");
return "article: hello spring boot aop";
}
}
启动项目,首先输入url:127.0.0.1:8080/user/test1
查看控制台打印信息:
会发现,每次都会执行前置通知,然后执行sayHi 方法。
然后输入URL:127.0.0.1:8080/user/login
也会先执行前置通知,然后执行login方法。
然后输入URL: 127.0.0.1:8080/art/test1
查看控制台打印结果:
多访问几次:
会发现,并没有打印前置通知,这是因为之前配置的拦截规则只拦截UserController类中所有方法,其他的都不会拦截。
异常通知和返回通知以及后置通知和前置通知使用类似,就不作介绍了,主要介绍一下比较复杂的环绕通知,代码如下:
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("开始执行环绕通知");
// 执行拦截方法
Object object = joinPoint.proceed();
System.out.println("结束环绕通知");
return object;
}
将前置通知的代码先注销掉,只观察环绕通知的效果:
启动项目,输入URL: 127.0.0.1:8080/user/test1
查看控制台打印信息:
放开前置通知的代码,完整代码如下:
此时,启动项目,输入URL:127.0.0.1:8080/user/test1
会发现先执行开始环绕通知,然后执行前置通知,再执行后置通知(如果有),最后结束环绕通知。
🐳二、Spring AOP 实现原理
AOP 是一种思想,而Spring AOP 是一个框架,提供了一种对 AOP 思想的实现,他们的关系和IoC 与 DI 类似。
AOP 的实现技术主要有两种:
1).静态代理:
静态代理是一种在编译时就已经确定代理关系的代理方式。在静态代理中,代理类和被代理类都要实现同一个接口或继承同一个父类,代理类中包含了被代理类的实例,并在调用被代理类的方法前后执行相应的操作。
静态代理的优点是实现简单,易于理解和掌握,但是它的缺点是需要为每个被代理类编写一个代理类,当被代理类的数量增多时,代码量会变得很大。
2)动态代理:
动态代理是一种在运行时动态生成代理类的代理方式。
动态代理的优点是可以为多个被代理类生成同一个代理类,从而减少了代码量,但是它的缺点是实现相对复杂,需要了解 Java 反射机制和动态生成字节码的技术。
Spring AOP 是建立在动态代理基础上的,因此 Spring 对 AOP 的支持局域于方法级别的拦截。Sptring AOP 支持JDK Proxy 和 GCLIB 方式实现动态代理(动态代理两种常用实现方法)。
🐼1.JDK 动态代理
JDK 动态代理是一种使用 Java 标准库中的 java.lang.reflect.Proxy 类来实现动态代理的技术。在 JDK 动态代理中,被代理类必须实现一个或多个接口,并通过 InvocationHandler 接口来实现代理类的具体逻辑。
JDK 动态代理通过反射实现动态代理。
JDK 动态代理的优点:实现简单,易于掌握和理解。
JDK 动态代理的缺点:只能代理实现了接口的类,无法代理没有实现接口的类。
代码实现:
import org.example.demo.service.AliPayService;
import org.example.demo.service.PayService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 动态代理:使⽤JDK提供的api(InvocationHandler、Proxy实现)
// 此种⽅式实现,要求被代理类必须实现接口
public class PayServiceJDKInvocationHandler implements InvocationHandler {
//⽬标对象即就是被代理对象
private Object target;
public PayServiceJDKInvocationHandler( Object target) {
this.target = target;
}
//proxy代理对象
@Override
public Object invoke(Object proxy, Method method, Object[] args) throw
s Throwable {
//1.安全检查
System.out.println("安全检查");
//2.记录⽇志
System.out.println("记录⽇志");
//3.时间统计开始
System.out.println("记录开始时间");
//通过反射调⽤被代理类的⽅法
Object retVal = method.invoke(target, args);
//4.时间统计结束
System.out.println("记录结束时间");
return retVal;
}
public static void main(String[] args) {
PayService target= new AliPayService();
//⽅法调⽤处理器
InvocationHandler handler =
new PayServiceJDKInvocationHandler(target);
//创建⼀个代理类:通过被代理类、被代理实现的接⼝、⽅法调⽤处理器来创建
PayService proxy = (PayService) Proxy.newProxyInstance(target.getClass().getClassLoader(),
new Class[]{PayService.class},
handler
);
proxy.pay();
}
}
🐼2.GCLIB
GCLIB 动态代理是一种使用GCLIB库来实现动态代理的技术。在GCLIB 动态代理中,通过实现代理类的子类来实现动态代理,而不是实现代理类的接口。
GCLIB 动态代理的优点:可以代理没有实现接口的类。
GCLIB 动态代理的缺点:实现相对复杂,需要了解GCLIB 库的使用方法。
代码实现:
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.example.demo.service.AliPayService;
import org.example.demo.service.PayService;
import java.lang.reflect.Method;
public class PayServiceCGLIBInterceptor implements MethodInterceptor {
//被代理对象
private Object target;
public PayServiceCGLIBInterceptor(Object target){
this.target = target;
}
@Override
public Object intercept(Object o, Method method, Object[] args, Method
Proxy methodProxy) throws Throwable {
//1.安全检查
System.out.println("安全检查");
//2.记录⽇志
System.out.println("记录⽇志");
//3.时间统计开始
System.out.println("记录开始时间");
//通过cglib的代理⽅法调⽤
Object retVal = methodProxy.invoke(target, args);
//4.时间统计结束
System.out.println("记录结束时间");
return retVal;
}
public static void main(String[] args) {
PayService target= new AliPayService();
PayService proxy= (PayService) Enhancer.create(target.getClass(),n
ew PayServiceCGLIBInterceptor(target));
proxy.pay();
}
}
🐼3.二者的区别
1).JDK 动态代理要求代理对象必须实现接口,CGLIB 不要求代理对象实现接口。
2).JDK 动态代理使用java.lang.reflect.Proxy 和 java.lang.reflect.InvocationHandler 来生成代理对象。
GCLIB 动态代理使用GCLIB 库来生成代理对象。
3).JDK 动态代理生成的代理对象是目标对象的接口实现,
GCLIB 动态代理生成的代理对象是目标对象的子类。
4).在JDK1.8之前,JDK 动态代理相对 GCLIB 动态代理 性能较低,生成代理对象速度较慢。
在JDK1.8以及之后,因为JDK 动态代理做了优化,所以它的性能比 GCLIB 要高。
注:在Spring 框架中,既使用了 JDK 动态代理又使用了 GCLIB 动态代理,默认情况下使用的是 JDK 动态代理,但是如果目标对象没有实现接口,就会使用 GCLIB 动态代理。