SpringAOP

AOP全程Aspect Oriented Programming面向切面编程,是一种编程范式,用于指导开发者如何组织程序结构。相关的概念比如OOP全称为Object Oriented Programming面向对象编程。

AOP的作用是在不惊动原始设计的基础上为其功能进行增强,这也是Spring倡导的一种概念:无侵入式/无入侵式编程。

例如:需要获得SQL执行的时间

Long startTime = System.currentTimeMillis();
//业务逻辑...
Long endTime = System.currentTimeMillis();
System.out.println(endTime - startTime);

具体实现是抽取统计逻辑通用代码(通知),在需要添加统计执行时长的位置(连接点),在需要添加的具体方法的特定位置(切入点),将通知与切入点绑定的操作称为切面。

AOP
核心概念 说明
代理(Proxy) SpringAOP核心本质是采用代理模式实现的
连接点(JoinPoint) 程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等。SpringAOP中可理解为方法的执行。
切入点(PointCut) 匹配连接点的式子
通知(Advice) 在切入点处执行的操作,即共性功能。SpringAOP中功能最终会以方法的形式来呈现。
通知类 定义通知的类
切面(Aspect) 描述通知与切入点的对应关系

SpringAOP中一个切入点可以指描述一个具体方法,也可以匹配多个方法。

  • 描述一个具体方法:比如com.jc.dao包下的UserDao接口中的无形参无返回值的save()方法
  • 匹配多个方法:所有的save()方法、所有以get开头的方法,所有以Dao结尾的接口中的任意方法、所有带有一个参数的方法

入门

实现:在接口执行前输出当前系统时间

实现方式有两种可通过XML也可以使用注解的方式,推荐注解方式开发。

操作思路

  1. 导入坐标
  2. 制作连接点方法
  3. 提取共性功能,对应通知类的通知。
  4. 定义切入点
  5. 绑定切入点与通知之间的关系(切面)

导入坐标

默认spring-context包中已经包含了SpringAOP包

SpringAOP

导入aspectj包

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.7</version>
</dependency>

在Spring核心配置中添加@EnableAspectJAutoProxy来开启Spring对AOP注解驱动支持

$ vim config/SpringConfig.java
@Configuration
@ComponentScan("com.jc")
@EnableAspectJAutoProxy
public class SpringConfig {
}

定义通知类并添加@Component受到Spring容器管理,同时添加@Aspect定义当前类为切面类。

$ vim aop/BaseAdvice.java
//定义通知类
@Component
@Aspect
public class BaseAdvice {
}

定义切入点,切入点定义依托一个不具有实际意义的方法,即无参数、无返回值、方法体无实际逻辑,格式为private void pt(){}

//定义通知类
@Component
@Aspect
public class BaseAdvice {
    //定义切入点
    @Pointcut("execution(void com.jc.dao.UserDao.update())")
    private void pt(){}
}

绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置。例如:@Before("pt()")

//定义切入点
@Pointcut("execution(void com.jc.dao.UserDao.update())")
private void pt(){}
//定义共享功能
@Before("pt()")
public void method(){
    System.out.println(System.currentTimeMillis());
}

流程

  • SpringAOP本质是代理模式

SpringAOP工作流程

  1. SpringIoC容器启动
  2. 通过@EnableAspectJAutoProxy@Aspect读取所有切面配置的切入点
  3. 初始化Bean,判断Bean对应类中的方法是否匹配到任意切入点。若匹配失败则创建Bean对象,若匹配成功则创建原始对象(目标对象)的代理对象。
  4. 获取Bean执行方法,获取Bean后调用方法并执行以完成操作,当获取的Bean是代理对象时则根据代理对象的运行模式执行原始方法与增强的内容来完成操作。

SpringAOP核心概念

  • 目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的。
  • 代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现。

错误:添加@EnableAspectJAutoProxy注解后出现无法访问的错误

Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.jc.service.impl.AccountServiceImpl' available

因为SpringAOP是基于动态代理创建的对象,而默认是使用JDK的动态。对于JDK的动态代理对象是实现类的兄弟类,所以通过实现类的对象去匹配是找不到目标对象的,可通过接口类型或实现类的ID去匹配。若要非在实现类的对象去拿的话,可以修改Spring动态代理方式为cglib,cglib是代理出一个实现类的子类,可使用实现类的对象去匹配。

@EnableAspectJAutoProxy(proxyTargetClass=true)

切入点表达式

切入点是要进行增强的方法,切入点表达式则是要进行增强方法的描述方式。

描述方式1:描述接口

例如:执行com.jc.service包下AccountService接口中的无参save()方法

@Pointcut("execution(void com.jc.service.AccountService.save())")

描述方式2:描述实现类

@Pointcut("execution(void com.jc.service.impl.AccountServiceImpl.save())")

切入点表达式标准格式

动词关键词(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)

例如:

@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private SysUserMapper userMapper;

    @Override
    public SysUser getById(long id){
        return userMapper.getById(id);
    }
}
@Pointcut("execution(public com.jc.domain.SysUser com.jc.service.impl.AccountServiceImpl.getById(long))")
元素 实例 说明
动作关键词 execution 描述切入点的行为动作,比如execution表示执行到指定切入点。
访问修饰符 public 可省略
返回值 com.jc.domain.SysUser -
包名 com.jc.service.impl -
类/接口名 AccountServiceImpl -
方法名 getById -
参数 long -
异常名 - 方法定义中抛出指定异常,可省略。

通配符

使用通配符描述切入点

通配符 说明 示例
* 单个独立的任意符号,可独立出现,可作为前缀或后缀的匹配符出现。 public * com.jc.*.UserService.find*(*)
.. 多个连续的任意符号,可独立出现,用于简化包名与参数的书写。 public User com..UserService.findById(..)
+ 专用于匹配子类型 * *..*Service+.*(..)

常用书写

* com.jc.*.*Service.*(..)

所有代码按标准规范开发,否则以下技巧会全部失效。

书写技巧 说明
切入点 通常描述接口,而不描述实现类。
访问控制修饰符 针对接口开发均采用public描述,可省略。
返回值类型 对增/删/改使用精准类型加速匹配,对于查询使用*通配快速描述。
包名 书写尽量不使用..匹配,因为效率过低,常用*做单个包描述匹配或精准匹配。
接口名/类名 与模块相关的采用*匹配。比如UserService书写成*Service来绑定业务层接口名。
方法名 动词采用精准匹配,名词采用*匹配。比如getById书写成getBy*
参数 规则较为复杂,根据业务方法灵活调整。
异常 通常不使用异常作为匹配规则

通知类型

AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时,要将其加入到合理的位置。

AOP通知共分为5种类型:

通知类型 标注
前置通知 @Before
后置通知 @After
环绕通知 @Around
返回后通知 @AfterReturning
抛出异常后通知 @AfterThrowing

环绕通知

  • 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知。
  • 通知中如果没有使用ProceedingJoinPoint对原始方法进行调用,将跳过原始方法的执行。
  • 对原始方法的调用可以不接收返回值,通知方法设置为void即可。若接收返回值则必须设定为Object类型。
  • 原始方法的返回值如果是void类型,通知方法的返回值可以设置成void,也可以设置成Object
  • 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须抛出Throwable对象。
//定义共享功能
@Around("pt()")
public Object method(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("around before advice...");
    Object obj = pjp.proceed();//对原始操作的调用
    System.out.println("around after advice...");
    return obj;
}

例如:测量业务层接口万次执行效率

业务层接口执行前后分别记录时间,求差值得到执行时长。通知类型选择前后均可以增强的类型-环绕通知。

//定义通知类
@Component
@Aspect
public class BaseAdvice {
    //定义切入点:匹配业务层所有方法
    @Pointcut("execution(* com.jc.service.*Service.*(..))")
    private void servicePt(){}
    //定义共享功能
    @Around("servicePt()")
    public Object runSpeed(ProceedingJoinPoint pjp) throws Throwable {
        Long startTime = System.currentTimeMillis();
        Object obj = pjp.proceed();//对原始操作的调用
        Long endTime = System.currentTimeMillis();
        Long spendTime = endTime - startTime;
        //获取执行签名信息
        Signature s = pjp.getSignature();
        //通过签名获取执行类型
        String className = s.getDeclaringTypeName();
        //通过签名获取执行操作名称
        String methodName = s.getName();
        System.out.println(className + " " + methodName + " spend " + spendTime + "ms");
        return obj;
    }
}
com.jc.service.AccountService getById spend 813ms

通知获取数据

AOP通知获取数据分为三种:参数、返回值、异常

获取切入点方法的参数

  • JoinPoint 适用于前置、后置、返回后、抛出异常后通知
@Before("servicePt()")
public void before(JoinPoint jp){
    Object[] args = jp.getArgs();
    System.out.println("Before:" + Arrays.toString(args));
}

@After("servicePt()")
public void after(JoinPoint jp){
    Object[] args = jp.getArgs();
    System.out.println("After:" + Arrays.toString(args));
}
  • ProceedJointPoint 适用于环绕通知
@Around("servicePt()")
public Object runSpeed(ProceedingJoinPoint pjp) throws Throwable {
        Object[] args = pjp.getArgs();
        Object obj = pjp.proceed(args);//对原始操作的调用
        return obj;
}

获取切入点方法返回值

  • 返回后通知
@AfterReturning(value = "servicePt()", returning = "ret")
public void afterReturning(Object ret){
    System.out.println("After Returning:" + ret);
}

若同时需要JoinPoint和Object两个返回值,则JoinPoint必须在前。

@AfterReturning(value = "servicePt()", returning = "ret")
public void afterReturning(JoinPoint jp, Object ret){
    System.out.println("After Returning:" + jp + " " + ret);
}
  • 环绕通知

获取切入点方法运行异常信息

  • 抛出异常后通知
  • 环绕通知

例如:在业务方法执行前对所有输入参数去空格处理,使用处理后的参数调用原始方法,环绕通知中存在对原始方法的调用。

@Around("servicePt()")
public Object trimSpace(ProceedingJoinPoint pjp) throws Throwable {
    Object[] args = pjp.getArgs();
    //去除字符串参数中的空格
    for (int i = 0; i < args.length; i++) {
        if(args[i].getClass().equals(String.class)){
            args[i] = args[i].toString().trim();
        }
    }
    //对原始操作的调用
    Object obj = pjp.proceed(args);
    return obj;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容