AOP面向切面编程(让业务更关注业务)

AOP面向切面编程技术思想是OOP面向对象编程思想的补充拓展
OOP通过继承、封装、多态等概念将一系列有共同载体的属性动作集合约束至一个对象中
AOP是不同业务模块相同行为功能统一管理实现的思想,这些功能通常是与业务无关的系统相关功能,例如日志记录、异常处理、事务管理、性能监控等.如果利用OOP实现则会造成高度耦合与代码重复性
AOP技术思想将这些与对象核心业务没有关系的功能层面称为切面,在运行中动态切入至指定切入点,而对业务方面没有感知,没有侵入,可以优雅的解决这个问题,这就是AOP思想

术语概念

它们之间的关系大概可总结为: 切面是切入点和通知处理的集合,
而切入点则定义了一系列连接点的位置让程序能够将通知织入连接点.

切面-Aspect

切面是AOP的核心概念.由切入点和通知组成,定义了不同的业务对象所需的通用边缘功能集合.

切入点-Pointcut

切入点定义了所要执行通知处理的连接点的位置&筛选条件,可由路径表达式/注解指定
路径表达式支持正则可指定切入(织入)那些路径下的类方法,注解则通过指定注解来切入(织入)被注解的方法.

通知-Advice

指定切入逻辑的执行时机
分为前置通知、后置通知、环绕通知、异常通知、正常退出通知

连接点-Joinpoint

程序正常执行的点,可以理解是被切入对象要执行的方法,在该方法执行前后会执行通知处理.

织入-Weaving

一个类被织入Advice产生的代理类就是原类逻辑加上AOP增强逻辑.织入就是将切面通知和对象连接并创建代理类的过程

使用场景举例

日志收集、权限认证、事务管理、链路追踪、性能统计、加分布式锁

SpringAOP注解方式使用

切面声明

通过在类上增加@Aspet声明类为切面类.其中可定义切入点和通知处理方法.切面类由Spring管理所以需要加上@Component注解.如果想要对不同切面执行顺序进行调整可使用@Order(value)注解,value越小执行顺序越靠前

切入点声明

切入点声明需要将该注解添加到方法上.方法体为空即可.

  1. 注解声明
    @Pointcut("@annotation(注解全路径名)")
  2. 路径表达式声明
    @Pointcut("execution(* com.reuben.springlearn.aop.jointPoint.service.impl.*.*(..))")
第一个 * 表示定义返回值类型为所有类型
中间路径为要切入的包名,包名后面两个点表示当前包和其下子包.一个点表示当前包
第二个 * 表示定义的类名为所有类
最后的 *(..) *表示所有方法,括号中两个.表示所有参数
通知声明
  1. 前置通知@Before("切入点声明标记方法()")
    前置通知标记方法将在连接点方法执行前执行,可进行路径记录/其他日志记录工作
  2. 环绕通知@Around("切入点声明标记方法()")
    环绕通知类似于MethodInterceptor,可手动进行方法继续执行/直接返回.还可进行参数增强.可记录方法执行效率
  3. 后置通知@After("切入点声明标记方法()")
    后置通知,可进行一些执行之后的处理记录工作,方法正常执行与否都会执行该注解标记的方法
  4. 返回通知@AfterReturning("切入点声明标记方法()", returning = "返回参数名称")
    方法后置返回通知,该通知可获取到方法返回信息.对返回信息进行记录或增强.只有方法正常执行后才会执行
  5. 异常通知@AfterThrowing(pointcut = "切入点声明标记方法()()", throwing = "异常名称")
    方法后置异常通知,切入方法发生异常时会执行该注解标记的方法

可参考该仓库Demo

白话讲解

  1. 以小区快递代收点举例,本来收快递的时候是需要每个人去执行收快递、拆快递、检查快递的动作,并且还需要执行快递箱的清理工作.但是这个功能对于每个人来说都是重复的,非常影响小区住户体验.所以小区就规定快递由小区代收点统一收取、检查与快递箱的清理.只需要收取一些代理费用(性能损耗)即可.
  2. 也可用洗菜/洗盘子/吃饭举例.不论什么蔬菜和盘子均需要进行清洗以及吃饭前后需要进行做饭和清理工作,而不管饭的种类是什么.

实现原理

实现原理简单描述为使用JDK动态代理和CGLIB代理技术,在JVM运行期间,程序初始化对象时动态生成目标对象的代理对象加载到内存中(织入).默认使用JDK动态代理,如目标对象(不能是final)未实现目标接口则使用CGLIB.

静态AOP和动态AOP异同

两种AOP均基于代理模式,生成被切入类的代理对象实现
静态代理是代理类字节码由程序员手动编写源码编译得到,运行时不会改动
动态代理的代理类则是由运行时通过反射动态生成字节码并加载出来的

静态AOP由AspectJ实现,实现代理逻辑于编译时期
动态AOP由JDK动态代理和CGLIB实现,于运行期动态生成代理

public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
    //其余方法均已隐藏.该方法是Spring动态代理方法.可以在最后一个return看出如果有实现接口则优先使用JDK动态代理
    public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
        if (NativeDetector.inNativeImage() || !config.isOptimize() && !config.isProxyTargetClass() && !this.hasNoUserSuppliedProxyInterfaces(config)) {
            return new JdkDynamicAopProxy(config);
        } else {
            Class<?> targetClass = config.getTargetClass();
            if (targetClass == null) {
                throw new AopConfigException("TargetSource cannot determine target class: Either an interface or a target is required for proxy creation.");
            } else {
                return (AopProxy)(!targetClass.isInterface() && !Proxy.isProxyClass(targetClass) ? new ObjenesisCglibAopProxy(config) : new JdkDynamicAopProxy(config));
            }
        }
    }
}

JDK动态代理和CGLIB实现机制

  1. JDK动态代理通过实现InvocationHandler接口的invoke方法,在初始化时加入源类对象,使用Proxy.newProxyInstance()创建代理对象,代理对象就可以通过拦截目标方法执行对应的代理逻辑并通过反射执行目标方法
  2. CGLIB通过FastClass机制,对被代理类方法建立索引,通过拦截方法通过方法索引直接调用对应方法实现代理功能.自定义MethodInterceptor重写intercept方法,并通过Enhancer.create()创建代理类.

参考

JavaGuide
AOP面试造火箭事件始末
GitHub仓库练习