AOP
什么是AOP?
AOP:Aspect Oriented Programming,面向切面编程。
切面指的是某一类特定问题,因此面向切面编程也可以理解为面向特定方法编程。例如,在任何一个系统中,总有一些页面不是用户可以随便访问的,这就要对客户端发过来的请求进行检验,检验用户是否登录,这个检验用户登录的方法就是一个特定方法。此时,就可以用户AOP的思想来解决问题。
AOP是一种思想,表示对某一类事物的集中处理。
实现一个AOP
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
实现AOP
下述代码实现的AOP解决的问题是:对每一个接口的耗时时间使用日志来打印。这个方法的作用是对每个接口进行测试,判断哪个接口需要进行优化,因此相对来说在后台还是比较重要。
@Aspect // 表示这是一个切面类
@Slf4j // 打印日志
@Component // 将此Bean对象装配到IoC容器中
public class TimeAspect {
/**
* 打印每个接口耗时的日志
* @param joinPoint 表示作用的目标方法
* @return
*/
@Around("execution(* com.example.demo.controller.*.*(..))")
// 表示作用方式和作用域,即AOP在哪个环节起作用并且对哪些方式起作用
public Object timeCost(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed(); // 执行目标方法
long end = System.currentTimeMillis();
log.info(joinPoint.getSignature() + "消耗时间" + (end - start) + "ms");
return result;
}
}
AOP核心概念
切点
PointCut,切点,也称之为切入点。
切点的任务就是提供一组规则,告诉程序哪些方法需要被用来进行功能的增强。
例如在上述程序中的下述代码,就是一个切点:
定义切点
在上述AOP的小demo中,切点是和通知在一起进行书写的。如果在一个程序中,有很多的AOP,这些AOP作用的方法都相同,我们就可以把切点单独定义出来,这样可以减少代码的冗余。
@Pointcut("execution(* controller.*.*())")
public void pointCut() {
}
对于定义好的切点来说,如果通知也是在本类中的话,那么可以直接写成 @Before("切点()"),例如上述切点,通知可以写成@Before("pointCut()")。如果定义的切点并不是本类的话,就需要写成@Before("类的全限定名称 + 切点()")。
如果一个方法有多个被切面时,默认是按照类名来进行排序,不过有@Order注解可以用来进行排序。@Order中的数字越小,前置通知的时间越早,后置通知的时间越迟。
切点表达式
1. execution表达式:根据方法的签名来匹配
execution(<访问修饰符> <返回类型> <包名.类名.方法.方法参数> <异常>)
可以看到。访问修饰符和异常是可以不写的。
2. @annotation表达式:根据自定义注解的方式进行匹配。
execution表达式的方式适合匹配一些有规则的方法,如果我们需要进行匹配规则的点在某几个包下的某几个类的某几个方法,那么这种规则就不再使用。相对来说,这种使用自定义注解的方式更为适用,也就是对于每个需要匹配的方法,我们在其上面加一个注解即可。
实现步骤:①自定义注解②使用@annotation表达式描述切点③在连接点的方法上添加自定义注解。
自定义注解的实现:
/**
* 自定义注解
*/
@Target(ElementType.METHOD) // 元注解,表示注解作用的方法
@Retention(RetentionPolicy.RUNTIME) // 元注解,表示注解的声明周期
public @interface MyAnnotation {
}
使用@annotation表达式描述切点,同时为了测试@Order注解的作用,定义了顺序。
@Component // 表示要注入IoC容器中
@Slf4j // 表示打印日志
@Aspect // 表示这是一个切面类
@Order(1) // 定义顺序
public class MyAspect {
/**
* 定义切点
*/
@Pointcut("@annotation(com.example.demo.aop.MyAnnotation)")
public void pointcut() {
}
/**
* 定义前置通知
*/
@Before("pointcut()")
public void before() {
log.info("执行前置通知");
}
/**
* 定义后置通知
*/
@After("pointcut()")
public void after() {
log.info("执行后置通知");
}
/**
* 定义环绕通知
* @param joinPoint 表示目标方法
* @return
* @throws Throwable
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("执行环绕前通知");
Object result = joinPoint.proceed();
log.info("执行环绕后通知");
return result;
}
}
使用@annotation表达式描述切点,同时为了测试@Order注解的作用,定义了顺序。
@Component // 表示要注入IoC容器中
@Slf4j // 表示打印日志
@Aspect // 表示这是一个切面类
@Order(2) // 定义顺序
public class HisAspect {
/**
* 定义前置通知
* 由于使用的是其他类定义的切点,因此要用全限定名称 + 类型
*/
@Before("com.example.demo.aop.MyAspect.pointcut()")
public void before() {
log.info("执行前置通知");
}
/**
* 定义后置通知
* 由于使用的是其他类定义的切点,因此要用全限定名称 + 类型
*/
@After("com.example.demo.aop.MyAspect.pointcut()")
public void after() {
log.info("执行后置通知");
}
/**
* 定义环绕通知
* 由于使用的是其他类定义的切点,因此要用全限定名称 + 类型
* @param joinPoint 表示目标方法
* @return
* @throws Throwable
*/
@Around("com.example.demo.aop.MyAspect.pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("执行环绕前通知");
Object result = joinPoint.proceed();
log.info("执行环绕后通知");
return result;
}
}
启动类,用来测试结果
@Controller
@Slf4j
public class TestController {
@RequestMapping("/hi")
@MyAnnotation
public void hi() {
log.info("目标方法");
}
}
连接点
满足切点规则的点,就是连接点。
例如在上述切面中,三层架构中controller层的所有方法都会被切面切入。
通知
通知就是在AOP中具体执行的业务逻辑,然后程序猿将其抽象成一个方法。
在上述的AOP中,这个方法就是一个通知。
public Object timeCost(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed(); // 执行目标方法
long end = System.currentTimeMillis();
log.info(joinPoint.getSignature() + "消耗时间" + (end - start) + "ms");
return result;
}
前置通知(@Before):表示通知在目标方法执行前被执行。
后置通知(@After):表示通知在目标方法执行后被执行。
环绕通知(@Around):表示通知再目标方法执行前后被执行。
返回后通知(@AfterReturning):表示通知在目标方法执行后被执行,如果有异常就不会执行。
异常后通知(@AfterThrowing): 表示通知在目标方法异常后被执行。
如果一个方法有多个通知,那么通知的执行顺序就是:
正常情况下:环绕前通知 → 前置通知 → 返回后通知 → 后置通知 → 环绕后通知;
异常情况下:环绕前通知 → 前置通知 → 异常后通知 → 后置通知。
切面
切面 = 切点 + 通知。
通过切面我们可以知道,AOP在哪些方法中执行什么样的业务逻辑。
代理模式
SpringAOP是基于动态代理实现的。
代理的目的,其实就是对目标方法进行功能增强。
定义
代理,就是为目标对象提供一种代理以控制对这个对象的访问。它的作用就是通过一个代理类,让我们在调用目标方法的时候,不是直接对目标方法进行调用,而是通过代理类间接调用。
在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
使用代理前:
使用代理后:
主要角色
业务接口类(Subject):表示目标对象要做什么事,可以是抽象类或接口。
业务实现类(RealSubject):表示目标对象具体做的业务逻辑,也就是被代理对象要做的业务。
代理类(Proxy):当来调用目标对象时,通过访问代理类来实现。
静态代理
由程序员创建代理类或使用特定工具自动生成源代码再对其进行编译,程序运行前代理类的.class文件就以及存在,并且已经确定好要代理的对象。
以房东和中介为例。房东属于目标对象,中介属于代理对象。房东有租房子和卖房子两项业务,但是对于一个中介来说,他在开启业务之前,只跟房东确定好了租房子的业务,而卖房子的并没有确定好。因此,当有客户时,客户想买,但是中介代理的只有租,不能卖。这就类似于静态代理,当代理对象在程序运行前,就确定好要代理什么内容,程序开始后,代理对象只能代理程序运行前确定好的,对于没有确定的,它就不能代理。
HouseSubject接口
// 业务接口类
public interface HouseSubject {
/**
* 出租房屋
*/
void rentHouse();
/**
* 出售房子
*/
void saleHouse();
}
RealHouseSubject类
// 目标对象、被代理对象
public class RealHouseSubject implements HouseSubject{
/**
* 出租房屋
*/
@Override
public void rentHouse() {
System.out.println("我是房东,我要出租房子");
}
/**
* 出售房子
*/
@Override
public void saleHouse() {
System.out.println("我是房东,我要出售房子");
}
}
HouseProxy类
// 代理类
public class HouseProxy implements HouseSubject{
// 目标对象
private HouseSubject houseSubject;
public HouseProxy(HouseSubject houseSubject) {
this.houseSubject = houseSubject;
}
@Override
public void rentHouse() {
System.out.println("开始进行代理");
houseSubject.rentHouse();
System.out.println("结束代理");
}
}
客户端来访,使用代理对象
public class Main {
public static void main(String[] args) {
HouseSubject subject = new RealHouseSubject();
HouseProxy houseProxy = new HouseProxy(subject);
houseProxy.rentHouse();
}
}
在上述代码中不难看出, 代理类在程序运行前就确定好代理什么目标对象,代理目标对象的什么业务。当程序开始后,客户端来进行交易,代理类就只能代理确定好的目标对象的业务,其他一概不能操作。
动态代理
相比于静态代理来说,动态代理较为灵活。
在动态代理中,不需要为每一个目标对象都单独创建一个代理对象,而是把这个创建代理对象的任务放到程序运行时有JVM来实现,也就是说动态代理在程序运行时,根据需要动态创建。
仍然以房东和中介举例。房东属于目标对象,中介属于代理对象。房东只需要把要干啥给出,而此时中介并不需要是哪个房东,要干哪个事。当客户来了之后,诉说自己的想要什么,中介再根据具体要干的活去找哪个房东有这个东西。假如说,客户要买房,那中介就找哪个房东要卖房;客户要长租,中介就找哪个房子会长租。这就类似于动态代理,代理对象并不知道会来什么业务,也不知道目标对象有什么业务,只要客户来了,根据需要去创建一个具体的代理,然后执行业务。
动态代理分为:JDK动态代理和CGLIB动态代理。
JDK动态代理
1. 定义一个接口以及实现类,也就是静态代理中的HouseSubject、RealHouseSubject。
2. 定义一个代理类,实现InvocationHandler接口,并重写invoke方法,在invoke方法中我们会调用目标方法并实现一些业务逻辑。
3. 通过Proxy.newProxyInstance(ClassLoader loader, Class<?>[ ] interfaces, InvocationHandler h)方法创建代理对象。
HouseSubject接口
// 业务接口类
public interface HouseSubject {
/**
* 出租房屋
*/
void rentHouse();
/**
* 出售房子
*/
void saleHouse();
}
RealHouseSubject类
// 目标对象、被代理对象
public class RealHouseSubject implements HouseSubject{
/**
* 出租房屋
*/
@Override
public void rentHouse() {
System.out.println("我是房东,我要出租房子");
}
/**
* 出售房子
*/
@Override
public void saleHouse() {
System.out.println("我是房东,我要出售房子");
}
}
JDKInvocation类,即Proxy类
/**
* JDK动态代理的实现
*/
public class JDKInvocation implements InvocationHandler {
// 目标对象
private Object target;
public JDKInvocation(Object target) {
this.target = target;
}
/**
*
* @param proxy 代理对象
*
* @param method 代理对象需要实现的方法,即其中需要重写的方法
*
* @param args method所对应方法的参数
*
* @return
*
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("开始代理");
Object result = method.invoke(target, args); // 通过反射来实现的
System.out.println("结束代理");
return result;
}
}
客户端来访,创建代理对象
public class Main {
public static void main(String[] args) {
HouseSubject subject = new RealHouseSubject();
HouseSubject proxy = (HouseSubject) Proxy.newProxyInstance(subject.getClass().getClassLoader(),
// 类加载器,用于加载代理对象
new Class[]{HouseSubject.class},
// 被代理类实现的一些接口,同时也决定了JDK只能实现接口
new JDKInvocation(subject));
// 实现了InvocationHandler接口的对象
proxy.saleHouse();
}
}
CGLIB动态代理
1. 定义一个接口以及实现类,也就是静态代理中的HouseSubject、RealHouseSubject。
2. 自定义MethodInterceptor并重写intercept方法,interceptor方法就相当于中介干活,即代理对象,用来增强目标方法,和JDK动态代理中的invoke方法类似。
3. 当有客户端来访时,通过Enhancer类的create()创建真正代理对象。
引入依赖
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
HouseSubject接口
// 业务接口类
public interface HouseSubject {
/**
* 出租房屋
*/
void rentHouse();
/**
* 出售房子
*/
void saleHouse();
}
RealHouseSubject类
// 目标对象、被代理对象
public class RealHouseSubject implements HouseSubject{
/**
* 出租房屋
*/
@Override
public void rentHouse() {
System.out.println("我是房东,我要出租房子");
}
/**
* 出售房子
*/
@Override
public void saleHouse() {
System.out.println("我是房东,我要出售房子");
}
}
CGLibInterceptor类,即Proxy类
/**
* CGLib动态代理的实现
* 类似于JDK的invoke方法
*/
public class CGLibInterceptor implements MethodInterceptor {
// 目标对象
private Object target;
public CGLibInterceptor(Object target) {
this.target = target;
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("开始代理");
// 调用目标对象
Object result = method.invoke(target, objects);
System.out.println("结束代理");
return result;
}
}
客户端来访,创建代理对象
public class Main {
public static void main(String[] args) {
HouseSubject subject = new RealHouseSubject();
HouseSubject proxy = (HouseSubject) Enhancer.create(subject.getClass(),
// 被代理的类或接口的类型
new CGLibInterceptor(subject));
// 自定义方法拦截器MethodInterceptor
proxy.saleHouse();
}
}
对于JDK动态代理和CGLIB动态代理两者,JDK动态代理只能代理接口,而CGLIB动态代理可以代理类和接口。
不管是JDK动态代理还是CGLIB动态代理,都可以明显的发现,对于代理对象来说,都不知道要代理的内容是什么,当运行时,来了什么内容,要对哪个目标对象做业务,那么代理对象就代理哪个目标对象。
静态代理和动态代理进行对比之后,就可以发现动态代理的高效性,不用针对每一个目标对象都去构建一个代理对象。
源码分析
SpringAOP是基于动态代理实现的,而动态代理又有JDK和CGLIB两种方法进行实现。SpringAOP则是两种方法结合使用。
如果是代理接口,JDK和CGLIB都可以使用;如果是代理类,那么只有JDK可以使用。在SpringBoot2.x之后,默认使用CGLIB代理。当然,如果在配置文件中设置spring.aop.proxy-target-class=false,那就变成了JDK代理,但是如果目标对象是类的话,那么还是CGLIB动态代理。
对于AOP的介绍就到这里了,AOP是Spring两大主要内容之一了(另一个是IoC),因此非常重要,不论是日常工作中,还是八股文中,都是一个比较重要的内容。在文章中,简单介绍了一下AOP的内容,及背后的设计模式,还有SpringAOP的源码,接下来会对Spring实现的一些功能进行简单介绍,例如统一格式返回,拦截器,统一异常以及权限管理中后端利用AOP思想来实现拦截URL的过程。