0x07.Spring AOP基本使用

[TOC]

概述

在软件开发中,散布于应用中多处的功能被称为横切关注点(cross-cutting concern),一般而言这些横切关注点与具体的业务逻辑是相互分离的,如记录日志、控制事务等,往往被嵌入到业务代码中。AOP就是为了将这些横切关注点和业务逻辑分离。

在IOC容器中,DI实现了对象之间的依赖解耦,aop可以实现横切关注点和与他们所影响的对象之间的解耦。日志是应用切面的常见范例,除此之外切面还适用于多个场景,如声明式事务、安全和缓存。

AOP术语

切面能帮助我们模块化横切关注点,并使之能够复用。在面向对象的设计里,复用功能的实现方式有继承和委托。但是如果在整个应用中使用相同的基类,继承往往会导致一个脆弱的对象体系,而委托可能需要对委托对象进行复杂的调用。

面向切面编程(AOP)的切面,就是分散在应用中的横切关注点,AOP的目的是将横切逻辑从业务逻辑中抽离出来,再通过声明的方式定义这个功能要在何处以何种方式使用,而无需修改受影响的类。

这样做有两个好处,一方面是横切逻辑代码能够集中于一处,便于维护,另外业务代码可以专注业务逻辑。

描述切面的常用术语:

  • 通知(advice)
  • 切点(pointcut)
  • 连接点(joinpoint)

实际上在AOP中,本质上只是要定义一个通用的行为以及此行为要切入的位置和时机。通知描述了一个切面的动作和触发时机,切点和连接点则描述切入的位置。(Spring AOP只支持方法级别的切入,所以位置只有方法级别的)

通知(advice)

通知,advice,就是动作。通知定义了切面是什么,以及何时使用,除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。Spring切面可以使用五种类型的通知:

  • 前置通知(Before):在目标方法被调用之前调用advice
  • 后置通知(After): 在目标方法调用之后调用advice,不会关心方法的输出是什么
  • 返回通知(After-returning):在目标方法成功执行之后调用advice
  • 异常通知(After-throwing):在方法抛出异常之后调用通知
  • 环绕通知(Around):advice包裹了被通知的方法,在被通知的方法调用之前和之后执行自定义的行为,这是权限最高的advice

连接点(joinpoint)

连接点是应用执行过程中能够插入一个切面的一个点,这个点可以是调用方法时、抛出异常时、甚至是修改一个字段时,切面代码可以利用这些点插入到应用的正常流程之中,来添加新的行为。

连接点似乎和切面切入的时机相关,和位置似乎无关,其实不是的,程序中代码的执行时机和位置是相互关联的,只有执行中的应用才有切入的意义,所以连接点定义一个时机,实际上也就是代码的位置,只是在运行时位置和时机就等价了。

切点(pointcut)

切点的定义会匹配所要织入的一个或多个连接点。我们通常使用明确的类或者方法名称,或是利用正则表达式定义所要匹配的类和方法名称来指定这些切点。

切面(aspect)

通知和切点的结合就是一个切面。切点和通知定义了位置、执行时机和行为。

引入(introduction)

引入允许我们向现有的类添加新方法或属性。

织入(weaving)

织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中,在目标对象的生命周期里有多少个点可以进行织入:

  • 编译期

切点在目标类编译时被织入,此方式需要特殊的编译器,AspectJ的织入编译器就是以这种方式织入切面的。

  • 类加载期

切面在目标诶加载到JVM时被织入,这种方式需要特殊的类加载器,它可以在目标了被引入应用之前增强该目标类的字节码。AspectJ5的加载时织入就支持这种方式织入切面。

  • 运行期

切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,aop容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。

AOP代理

由AOP框架创建的用来实现切面逻辑(切面切入目标执行)的对象,在Spring AOP中,这个对象是jdk动态代理或CGLIB生成的代理对象。

Spring对AOP的支持

Spring提供了4种类型的AOP支持:

  • 基于代理的经典SpringAOP
  • 纯pojo切面
  • @AspectJ注解驱动的切面
  • 注入式AspectJ切面(使用AspectJ扩展来实现AOP)

Spring AOP的局限:

  • Spring AOP构建在动态代理的基础之上,因此Spring对AOP的支持局限于方法拦截。
  • Spring通知是java编写的
  • Spring在运行时通知对象,直到应用需要被代理的bean时,Spring才创建代理对象。

Spring AOP in action

定义切面

切面由advice和pointcut组成。

声明Aspect

使用@Aspect声明一个POJO为aspect类,但是使用时还需加上@Component来交给Spring管理,或者使用xml方式,总之要注册成bean,否则不能被Spring识别。

  • 基于注解风格的Aspect
@Component
@Aspect
public class UserServiceAspect {
    // ...
}

声明advice

对应五种advice,有五种注解:

  • @Before
  • @AfterReturning
  • @AfterThrowing
  • @After
  • @Around

Advice的注解是标注在方法上的,这个方法的逻辑是切入到切点的横切逻辑。注解的含义是发生的时机,如before、after等,注解内需要注明此advice对于的PointCut,即切点的定义,主要一个完整的横切逻辑的定义才完整(动作、时机、位置)。

    // 引用类中定义的pointcut方法
    @Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }
 
    @Around("execution(* com.springinaction.aop.DemoService.print(String))")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        // start stopwatch
        Object retVal = pjp.proceed();
        // stop stopwatch
        return retVal;
    }

Around的优先级高于其他的advice,如果在Around的advice中没有调用proceed方法,目标方法不会被调用。故Around的advice方法第一个参数需要设置为ProceedingJoinPoint。

定义切点

Spring借助AspectJ的切点表达式语言来定义Spring切点:

AspectJ指示器 描述
args() 限制匹配的方法的参数,更常用的是绑定形式,将方法参数传递给advice
@args() 限制匹配方法的参数由指定注解标注
execution() 匹配方法
this() 匹配目标对象的类型
target() 匹配指定类型
@target() 匹配目标对象且类上有指定类型的注解
within() 匹配对象的类
@within() 匹配注解标注的类型
@annotation() 匹配指定注解

除以上之外的AspectJ指示器都不可用,会抛出IllegalArgumentException。
另外Spring引入了一个bean()指示器,运行切点通过beanid来匹配bean。

例:

  • execution(* com.test.Test.method(..) ):匹配任意参数的任意返回值的 com.test.Test类的method()方法
  • execution(* com.test.Test.method(..) && within(com.*)):匹配com.test.Test.method方法和com包下任意方法。

使用多个指示器时,可以使用and,or,not或者对应的&&,||,!进行连接(not是条件取反)。

advice方法和Joinpoint交互

通过给advice方法声明JoinPoint 参数来获取当前的joinpoint,另外Around类型的advice使用ProceedingJoinPoint(它是JoinPoint类的子类,是@Around方法的必选参数)。

传递方法参数到advice

关于前述的多个指示器,官方reference有这些说明:

  • 'this' is more commonly used in a binding form
  • 'target' is more commonly used in a binding form
  • 'args' is more commonly used in a binding form
  • '@target' can also be used in a binding form
  • '@within' can also be used in a binding form
  • '@annotation' can also be used in a binding form
  • '@args' can also be used in a binding form

也就是说除了within()和execution(),其余的都可以用来绑定参数。

@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}

@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
    // ...
}

or

@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
    // ...
}

or @annotation

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
    AuditCode value();
}

@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
    AuditCode code = auditable.value();
    // ...
}

or generic type

@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
    // Advice implementation
}
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
    // Advice implementation
}

启用AOP

  • JavaConfig方式
@Configuration
@ComponentScan // 配置自动扫描,默认是本类所在的包及子包
@EnableAspectJAutoProxy // 启用Aspectj自动代理 ,不开启AOP无效
public class RootConfig {

}

Sample

// configuration 
@Configuration
@ComponentScan // 配置自动扫描,默认是本类所在的包及子包
@EnableAspectJAutoProxy // 启用Aspectj自动代理 ,不开启AOP无效
@PropertySource("classpath:/conf.properties")
public class SpringAopConfig {
    @Autowired // 可以将Property注入到env中来
    private Environment env;

    @Bean
    public PrintStream stream() {
        return System.out;
    }

    /**
     * Java中配置占位符的方式,需要此Bean和上面的@PropertySource("classpath:/conf.properties")注解支持,
     * 而后可以在Component中使用{@value使用占位符注入属性值}
     * 
     * @return
     */
    @Bean
    public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }

    @Bean
    public Environment env() {
        return env;
    }
}

// Service
@Service
public class DemoService {

    @Autowired
    private PrintStream stream;

    public void print(String msg) {
        stream.println(msg);
    }
}

// Aspect
@Component
@Aspect
public class DemoAspect {

    @Before("execution(* com.springinaction.aop.DemoService.print(String))")
    public void beforePrint() {
        System.out.println("--beforePrint--");
    }

    @Before("execution(* com.springinaction.aop.DemoService.print(String)) && args(msg)")
    public void beforePrintwithArgs(String msg) {
        System.out.println("--beforePrintwithArgs--");
        System.out.println("in advice get msg:" + msg);
    }

    @Pointcut("execution(* com.springinaction.aop.DemoService.print(String)) && args(msg)")
    public void print(String msg) {

    }

    @Around("print(msg)")
    public void holdPrint(/* ProceedingJoinPoint jp, */String msg) throws Throwable {
        System.out.println("holdPrint");
        // jp.proceed();  // 注掉就不会进入目标方法
        // jp.proceed(new Object[] { "what" });
        System.out.println("holdPrint end");
    }
}

//  Test
    @Test
    public void testPrint() {
        ApplicationContext context = new AnnotationConfigApplicationContext(SpringAopConfig.class);
        DemoService service = context.getBean(DemoService.class);
        System.out.println(service);
        service.print("hello aop");
    }

Around方法的执行内会环绕在before、after等方法的执行之外。

给目标对象引入新功能(introduction)

使用@DeclareParents注解为bean引入接口方法

// 增强接口
public interface Enhance {
    public void enhance();
}
// 增强接口的默认实现
public class DefaultEnhance implements Enhance {

    @Override
    public void enhance() {
        System.out.println("this is an enhance");

    }

}
// 增强Aspect
@Component
@Aspect
public class Introducer {
    @DeclareParents(value = "com.springinaction.aop.DemoService", defaultImpl = DefaultEnhance.class)
    public Enhance defaultEnhance;
}
// Test
    @Test
    public void testIntroducer() {
        ApplicationContext context = new AnnotationConfigApplicationContext(SpringAopConfig.class);
        Enhance service = (Enhance) context.getBean(DemoService.class);
        service.enhance();
        ((DemoService) service).print("tets");
    }

小结

Spring AOP的原理是动态代理;
Spring AOP只能拦截方法;
Spring AOP的使用方法包括启动aop支持、定义横切逻辑两大部分。定义横切逻辑是定义切面和advice,用于拦截目标方法。在定义横切逻辑方面,需求大致在于方法调用前、执行正常返回、执行抛异常、执行返回(finally)来添加与方法内实质逻辑无关的逻辑,另外还有一个强大的Around advice能够包裹目标方法,可以干涉方法的执行过程。

Advice在拦截目标方法时,可以获取到方法的参数,Around方法甚至可以对参数进行操作来修改参数。

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

推荐阅读更多精彩内容