Spring参考手册 4 面向切面编程

翻译自Spring官方文档 4.1.2版本

相关文章:

一、简介

面向切面编程英文全称:Aspect-Oriented Programming (AOP)是面向对象编程(OOP)的补充。具体概念就不赘述。

1.1 AOP概念

我们首先学习一些AOP的核心概念和专业术语。这些并不是Spring特有的,不幸的是AOP的术语不是特别直观,如果Spring用它自己的术语会更让人难以理解。

  • Aspect 切面。事务管理是一个很好的例子。在Spring里切面实现特殊类或者使用@Aspect注解的类。
  • Join point 在Spring AOP中join point代表一个方法的执行。
  • Advice 特定Join point产生的通知。例如:"around","before","after",就像拦截器。
  • Pointcut 一种匹配Join point的描述。Advice将会作用于所有符合Pointcut描述的Join point
  • Introduction 声明一些方法或者成员变量在某方面为一个类型。

Advice的类型:

  • Before advice:在一个Join point(以后都说方法)前执行,但是它没有能力阻止不执行这个方法,除非它发生异常。
  • After returning advice:当一个方法正常完成后执行。
  • After throwing advice:当一个方法抛出异常后执行。
  • After (finally) advice:当一个方法完成后执行,不管正常还是异常。
  • Around advice:在一个方法执行前和执行后调用。

二、@AspectJ支持

Spring的注解和AspectJ 5的一样,但是没有任何依赖AspectJ编译器。

2.1 启用@AspectJ支持

通过Java配置

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}

通过XML配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd
       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd
       http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsd">

    <aop:aspectj-autoproxy />

2.2 声明一个切面

首先创建一个类:

package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class NotVeryUsefulAspect {

}

将它声明为一个bean:

<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
    <!-- configure properties of aspect here as normal -->
</bean>

如果配置了自动扫描发现bean的方式也可以这样:

package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect
@Component
public class NotVeryUsefulAspect {

}

切面可以有其他的方法和成员变量,和其他的类一样,只是多了@Aspect注解。

2.3 声明一个切点

Spring AOP只支持在Spring beans的方法执行声明一个切点。有一个切点的声明包含两部分:一个方法签名和一个切点表达式,准确的匹配哪些方法是我们感兴趣的。在@AspectJ注解风格的切面编程里,@Pointcut注解表示切点(这个注解相关的方法必须使用void返回类型)。

下面是一个例子:

@Pointcut("execution(* transfer(..))")// 切点表达式
private void anyOldTransfer() {}// 切点方法签名

这个切点将会匹配所有名字为transfer的方法(当然是在Spring beans里)。完整的切点语法规则参见AspectJ Programming Guide

支持的切点标识符

切点标识符就是上面例子中:execution部分,下面是支持的列表:

  • execution 用来匹配方法执行,这个将会是你在Spring AOP里主要用到的标识符
  • within 只匹配指定类型(不太好理解,后面有例子,就是根据包名匹配)
  • this 只匹配引用是给定类型的一个实例的方法(还是看后面的例子吧)
  • target 只匹配目标对象是给定类型的一个实例的方法
  • args 只匹配参数是给定类型实例的方法

@target@args@within@annotation这些都是根据注解来匹配的。

注意,Spring AOP是一个基于代理的框架,被保护的方法不会被拦截,所以上面声明的切点只会对public方法起作用。

联合切点表达式

可以使用&&||!这些符号来联合切点表达式。还可以通过切点的名字来引用切点。请看下面的例子:

@Pointcut("execution(public * (..))")
private void anyPublicOperation() {}

@Pointcut("within(com.xyz.someapp.trading..)")
private void inTrading() {}

@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}

anyPublicOperation切点匹配所有public的方法。

inTrading切点匹配所有com.xyz.someapp.trading包下的方法。

tradingOperation切点是上面两个切点的交集。

最佳实践是用小的切点表达式来构成复杂的切点表达式,就像上面的例子。

一些通用的切点定义

在企业环境中,你会经常引用应用程序的各个模块。我们推荐定义一个"系统架构"切面,捕获通用的切点。一个典型的切面看起来像下面这个类:

package com.xyz.someapp;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class SystemArchitecture {

    /
     * 一个web层的切点,匹配任何在com.xyz.someapp.web包下的方法
     */
    @Pointcut("within(com.xyz.someapp.web..)")
    public void inWebLayer() {}

    /
     * 一个service层的切点,匹配任何在com.xyz.someapp.service包下的方法
     */
    @Pointcut("within(com.xyz.someapp.service..)")
    public void inServiceLayer() {}

    /
     * 一个数据访问层的切点,匹配任何在com.xyz.someapp.dao包下的方法
     */
    @Pointcut("within(com.xyz.someapp.dao..)")
    public void inDataAccessLayer() {}

    /
     * 匹配在service包下的方法,例如com.xyz.someapp.user.service.addUser()这个方法
     */
    @Pointcut("execution( com.xyz.someapp..service..(..))")
    public void businessService() {}

    /*
     * 匹配在dao包下的方法
     */
    @Pointcut("execution( com.xyz.someapp.dao..(..))")
    public void dataAccessOperation() {}

}

2.4 一些列子

Spring AOP的用户可能和切点表达式交往最频繁。一个方法执行表达式的格式为:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? 
    name-pattern(param-pattern) throws-pattern?)

?表示是可选的。

  • modifiers-pattern? 修饰符部分
  • ret-type-pattern 返回类型部分
  • declaring-type-pattern? 声明部分
  • name-pattern 名称部分
  • param-pattern 参数部分
  • throws-pattern? 异常抛出部分

*表示匹配某任意部分。(..)表示任意数量的参数。(*,String)表示匹配参数数量是2,第一个参数是任意类型,第二个参数必须是String的方法。

下面是一些常见的切点表达式:

  • 任意公开方法的执行
execution(public * *(..))
  • 以set开头的任意方法
execution(* set*(..))
  • 定义在service包下的任意方法
execution(* com.xyz.service..(..))
  • 定义在service包以及子包下的任意方法
execution(* com.xyz.service...(..))
  • service包里的任意切点 (method execution only in Spring AOP)
within(com.xyz.service.*)
  • 一个参数是Serializable的任意切点 (method execution only in Spring AOP)
args(java.io.Serializable)
  • 切点匹配的方法上有@Transactional的注解
@annotation(org.springframework.transaction.annotation.Transactional)

译者注:很多我也没实践过,以后实践了再加些更通俗的解释吧。

2.5 声明通知

通知(Advice)是和切点表达式关联的。在切点表达式匹配的方法之前、之后或者前后执行。

前置通知

前置通知在一个切面里使用@Before注解:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    @Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }

}

com.xyz.myapp.SystemArchitecture是上面的一个例子:

@Pointcut("execution( com.xyz.someapp.dao..(..))")
public void dataAccessOperation() {}

或者直接用切点的表达式:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    @Before("execution(* com.xyz.myapp.dao..(..))")
    public void doAccessCheck() {
        // ...
    }

}

方法返回后通知

如其字面意思,当一个匹配的方法执行并正常返回后执行。它使用@AfterReturning注解声明:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }

}

注意:通知和切点完全可以写在一个切面里。

有时你需要访问方法返回的值。你可以使用下面这种形式绑定返回值:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning(
        pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // ...
    }

}

returning属性的名字必须与通知方法的参数名字保持一致。returning项目还会约束方法返回值的类型(Object在这里将会匹配任何返回值,如果是String,将只会匹配String类型的返回值)

方法抛出异常后通知

当切点匹配的方法抛出异常后执行的通知。使用@AfterThrowing注解:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doRecoveryActions() {
        // ...
    }

}

如果想只获得指定类型的异常,可以使用throwing属性,和上面的returning类似:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing(
        pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {
        // ...
    }

}

后置通知

只要是一个切点匹配的方法退出了就会执行方法后通知,不管是否成功返回。它使用@After注解。你必须同时考虑到正常和异常的情况。一个典型的应用就是释放资源等:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;

@Aspect
public class AfterFinallyExample {

    @After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doReleaseLock() {
        // ...
    }

}

环绕通知

环绕通知经常被用在需要在一个线程安全的方法执行前后共享状态。总是使用能满足你需求的能力最弱的切面(例如,如果前置通知就可以完成的不要使用环绕通知)。

环绕通知使用@Around注解来声明。通知方法的第一个参数必须是ProceedingJoinPoint类型。在通知方法体内调用ProceedingJoinPointproceed()方法会执行切点所匹配的方法。proceed()方法也可以接收一个Object[]类型的参数给切点所匹配的方法。

译者注:proceed()方法之前相当于前置通知处理的范围,proceed()方法之后相当于后置通知处理的范围。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;

@Aspect
public class AroundExample {

    @Around("com.xyz.myapp.SystemArchitecture.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        // retVal是方法的返回值
        Object retVal = pjp.proceed();

        return retVal;
    }

}

访问当前的切点

任何通知的方法都可以声明一个org.aspectj.lang.JoinPoint类型的参数作为它的第一个参数(环绕通知中的ProceedingJoinPointJoinPoint的子类,所以可以作为环绕通知的第一个参数)。JoinPoint接口提供了很多实用的方法例如:

  • getArgs() 返回切点匹配方法的参数
  • getThis() 返回代理对象
  • getTarget() 返回目标对象
  • getSignature() 返回通知关联的切点方法的签名

未完,待续...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,478评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,825评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,482评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,726评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,633评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,018评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,168评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,320评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,264评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,288评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,995评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,587评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,909评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,284评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,862评论 2 339

推荐阅读更多精彩内容

  • 本章内容: 面向切面编程的基本原理 通过POJO创建切面 使用@AspectJ注解 为AspectJ切面注入依赖 ...
    谢随安阅读 3,115评论 0 9
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,561评论 18 139
  • 在生活中,监控用电量是一个很重要的功能,但并不是大多数家庭重点关注的问题。软件系统的一些功能就像家里的电表一样,这...
    yjaal阅读 557评论 0 3
  • 你会那样吗?刻意让自己不要变得太深沉,因为你知道越深奥的地方越是人少,想到害怕孤独,不想让自己读太多书,去太多地方...
    柯柯酱koo阅读 248评论 0 0
  • 《源泉》25周年再版序言 《源泉》一书25年来连续再版,很多人询问我对此有何感受。除了藏在心底的满足感之外,还能有...
    溪水旁阅读 1,149评论 0 1