Hello Spring Framework——面向切面编程(AOP)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Hello Spring Framework——面向切面编程(AOP)相关的知识,希望对你有一定的参考价值。

本文主要参考了Spring官方文档第10章以及第11章和第40章的部分内容。如果要我总结Spring AOP的作用,不妨借鉴文档里的一段话:One of the key components of Spring is the AOP framework. While the Spring IoC container does not depend on AOP, meaning you do not need to use AOP if you don’t want to, AOP complements Spring IoC to provide a very capable middleware solution.(译:如果你不想使用AOP完全可以忽略,只单独使用IoC。但是作为重点,Spring AOP框架是对IoC的有力补充)。

***************************以下是正文部分***************************

官方文档对AOP的讲述有点复杂,我稍微修改了一下组织结构,试图从实用性的角度介绍Spring AOP。有些词汇的译法并没有采用公认的翻译。

一、几个重要的概念

切面(Aspect):能够嵌入多个类的模块。Spring AOP框架使用普通类实现切面。

连接点(Join point):正常业务流上的作用点,例如一个方法或一段异常处理。

建言(Advice):在切点上执行的方法。不同类型的建言包括:"around", "before" 和 "after"。

切点(Pointcut):代表了嵌入模块上的作用点。只有切点上才能运行建言。

AOP代理(AOP proxy):SpringAOP通常采用JDK Dynamic Proxy或CGLIB两种方式实现AOP代理。

织入(Weaving):织入的概念是所有AOP框架所共有的。它指将切面对象与建言对象连接的行为。织入行为能够始于编译期(compile time)、加载期(load time)或运行期(runtime)。Spring AOP的织入是在运行期。

二、对于@AspectJ的支持

Spring AOP框架支持@AspectJ风格的注解声明。

AspectJ也是一种AOP框架,它是对JVM的一种嵌入式开发。AspectJ的AOP织入始于编译期。Spring AOP框架仅借鉴了AspectJ提供的注解方案,因此织入依然是在运行期完成的。

(1)通过Maven添加依赖

技术分享
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>4.2.2.RELEASE</version>
</dependency>
maven依赖

注意:使用IDE查询相关依赖关系可以看到spring-aspects仅仅依赖了aspectjweaver包,在有些教材中建议大家引入整个AspectJ包其实是不准确的。对于依赖关系的管理,我的建议是尽量做到精确引入,以防未知异常。

(2)声明对@AspectJ注解的支持

方法一:

技术分享
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}
AppConfig

方法二:

技术分享
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
        
    <!-- 声明基于注解的支持 -->
    <aop:aspectj-autoproxy />
    <!-- 扫描包文件 -->
    <context:component-scan base-package="..." />
    
</beans>
XML文档声明

方法二是常用的方式,第一种了解即可。

(3)声明切面

技术分享
package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class AnyClazz {
    //...
}
增加@Aspect

在XML文档中配置Bean

技术分享
<bean id="myAspect" class="org.xyz.AnyClazz">
    <!-- configure properties of aspect here as normal -->
</bean>
配置bean

 @Aspect仅代表你希望将类声明为一个切面,如果希望使用Spring提供的包扫描自动初始化还需要增加@Component注解

技术分享
package org.xyz;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class AnyClazz {

}
支持包扫描的切面注解

(4)声明切点

切点包含三个方面的内容:切点的注解、连接点表达式、切点方法签名

技术分享
/*
 * 最常用的声明方式
 * @Pointcut代表此方法是一个切点
 * 切点的名字是anyMethod
 * 代表连接点对象的表达式execution(public packagename.*.*(..))
 */
@Pointcut("execution(public packagename.*.*(..))")
public void anyMethod() {
    //...
}
最常用的声明方式

连接点表达式的前缀有三种,第一种最常见,后面两种知道含义足够

技术分享
//execution意味着连接点的指定精确到类中的方法
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}

//within代表了连接点的指定只精确到包,为包中的所有类和方法都添加代理
@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {}

//只为符合方法名的连接点添加代理
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
连接点表达式前缀

常用的连接点表达式语句

技术分享
// 所有public方法
execution(public * *(..))

//所有以set开头的方法
execution(* set*(..))

//AccountService接口上的所有方法
execution(* com.xyz.service.AccountService.*(..))

//service包中的所有类上的所有方法(最常用)
execution(* com.xyz.service.*.*(..))

//service包及其子包里的所有类上的所有方法
within(com.xyz.service..*)
连接点表达式语句

(5)声明建言

初学者往往会在学习了建言之后混淆建言、切点和连接点三者的关系。这主要是因为Spring AOP建议将三者放在一条语句中声明。

技术分享
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {
    
    // 请注意()中的字符串并不是连接点表达语句而是指声明了@Aspect的方法,dataAccessOperation是切点的签名
    @Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }

}
BeforeExample

以上提供的是单独的建言声明,对建言正确的理解应该是:建言对应切点,切点对应连接点。如果将三者合成一条注解声明,可以让代码显得更加紧凑也更加常用。

技术分享
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() {
        // ...
    }

}
BeforeExample

另外几种常用建言举例:

i.正常返回的建言

技术分享
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() {
        // ...
    }

}
AfterReturning

ii.处理异常的建言

技术分享
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() {
        // ...
    }

}
AfterThrowing

iii.针对finally的建言

技术分享
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() {
        // ...
    }

}
After

vi.Around建言(重点)

技术分享
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 {
        // start stopwatch
        Object retVal = pjp.proceed();
        // stop stopwatch
        return retVal;
    }

}
Around

Around建言比较特殊,需要用ProceedingJoinPoint对象作为参数。熟悉JDK DynamicProxy的人应该对类似方法不陌生。

(6)为多层建言指定顺序

对某个连接点指定多层建言时就有必要为此提供一个执行顺序,让切面类实现Ordered接口就能指定这个顺序。

技术分享
package aop.order;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class AdvicerFirst implements Ordered {
    // 设定的值越小执行顺序越靠前
    private final int order = 1;

    public int getOrder() {
        return order;
    }

    @Before("execution(* aop.order.*.*(..))")
    public void display() {
        System.out.println("advicer 1st");
    }
}
AdvicerFirst
技术分享
package aop.order;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class AdvicerThen implements Ordered {
    //设定的值越大执行顺序越靠后
    private final int order = 10;
    
    public int getOrder() {
        return order;
    }

    @Around("execution(* aop.order.*.*(..))")
    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("advicer then start");
        Object retVal = pjp.proceed();
        System.out.println("advicer then end");
        return retVal;
    }
}
AdvicerThen

上面只是对Ordered接口的简单演示,实际开发中一般会增加setOrder(int order)方法,再在xml文档中通过IoC容器注入order值。

技术分享
<aop:aspectj-autoproxy/>

<bean id="beanId" class="className">
    <property name="order" value="10"/>
</bean>
注入order值

(7)使用CGLIB生成代理

有关JDK DynamicProxy和CGLIB生成代理的差异不在本文的讲解范围。配置CGLIB的支持标签

技术分享
<aop:aspectj-autoproxy proxy-target-class="true"/>
CGLIB支持标签

由于在Spring 3.2版本之后官方已经不再要求显式配置上述标签,针对JDK DynamicProxy无法实现代理的情况下Spring AOP框架会自动调用CGLIB。因此结论就是,你可以更放心的让Spring去自动生成代理了(感觉文档描述Spring AOP框架对CGLIB的支持就是广告)。

三、基于xml文档的AOP配置

上面详细介绍了如何通过注解来完成AOP代理,但通常情况下我们无法修改源代码或者我们能够获得的是已经经过编译的二进制文件。这种情形下如何使用Spring AOP框架来实现代理呢。

(1)为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:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 我将开发环境中常用的命名前缀配置在一起,方便查找 -->
    
    <!-- bean definitions here -->

</beans>
常用命名前缀

(2)声明切面

技术分享
<aop:config>
    <!-- 将普通Bean对象声明为一个切面 -->
    <aop:aspect id="myAspect" ref="aBean">
        ...
    </aop:aspect>
</aop:config>

<bean id="aBean" class="...">
    ...
</bean>
aop:aspect

(3)声明切点

技术分享
<aop:config>
    <!-- aop:pointcut也可以放在aop:aspect标签内部 -->
    <!-- id值代表切点签名,expression值代表连接点表达式 -->
    <aop:pointcut id="pointcutId" expression="execution(* packagename.*.*(..))"/>

</aop:config>
aop:pointcut

(4)声明建言

技术分享
<aop:aspect id="beforeExample" ref="aBean">
    <!-- aop:before标签同样可以声明在aop:aspect内部 -->
    <!-- pointcut-ref属性指代切点ID,method属性指代切面类中的方法 -->
    <aop:before pointcut-ref="pointcutId" method="aspectInMethodName"/>

    ...
</aop:aspect>
aop:before

除了上面的常规配置手段以外,Spring AOP框架还提供了一种高度集成化的配置方案,但是只适合于方便修改源码的情况下使用。

(5)实现切面接口

i.Around接口

技术分享
public class DebugInterceptor implements MethodInterceptor {

    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("Before: invocation=[" + invocation + "]");
        Object rval = invocation.proceed();
        System.out.println("Invocation returned");
        return rval;
    }
}
Interception around advice

ii.Before接口

技术分享
public class CountingBeforeAdvice implements MethodBeforeAdvice {

    private int count;

    public void before(Method m, Object[] args, Object target) throws Throwable {
        System.out.println("Before...");
    }

    public int getCount() {
        return count;
    }
}
Before advice

iii.AfterReturn接口

技术分享
public class CountingAfterReturningAdvice implements AfterReturningAdvice {

    private int count;

    public void afterReturning(Object returnValue, Method m, Object[] args, Object target)
            throws Throwable {
        System.out.println("After...");
    }

    public int getCount() {
        return count;
    }
}
After Returning advice

(6)声明顾问(advisor)

对于实现了切面接口的类,只需要在xml文档中使用aop:advisor标签就可以了。

技术分享
<aop:config>

    <aop:pointcut id="businessService"
        expression="execution(* com.xyz.myapp.service.*.*(..))"/>

    <aop:advisor
        pointcut-ref="businessService"
        advice-ref="tx-advice"/>

</aop:config>

<tx:advice id="tx-advice">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>
aop:advisor

注意:顾问标签在实际的开发中并不常用,在Spring集成Hibernate的时候会用来声明事务管理。原因正如前文所述,使用切面接口仅限于方便修改源码的情况,而在类似的情况下采用注解方式配置AOP框架才是更合理的选择。所有接口在org.springframework.aop包中,需要了解的可以自己查阅API文档。

四、总结

根据Spring官方文档最后对AOP框架的总结,说明了以下两个问题:

(1)选择Spring AOP还是完整的AspectJ?

基本上如果你打算使用Spring IoC框架那么你就应该采用Spring AOP框架。除非你仅仅是希望使用AOP,而切面对象又不是通过Spring容器产生,那么你只能使用AspectJ。

(2)使用注解还是xml文档配置?

通常情况下两者几乎等价,官方文档的建议是使用xml配置。大概是显得思路清晰,易于修改。采用xml配置的局限性在于生成的切面只能是单例对象,并且无法实现组合式切面配置。

技术分享
@Pointcut(propertyAccess() && operationReturningAnAccount())
public void accountPropertyAccess() {}
组合式切面配置

 

Spring IoC和AOP框架构成了它的底层实现,也是Spring学习的基础。了解这些知识并不能说明你已经精通了Spring,而是说明你可以继续更深入的学习了。

以上是关于Hello Spring Framework——面向切面编程(AOP)的主要内容,如果未能解决你的问题,请参考以下文章

从java层到framework到JNI到HAL到kernel的hello 例子

Spring Framework,Spring Security - 可以在没有 Spring Framework 的情况下使用 Spring Security?

Spring Framework 的理解

spring framework Annotation(注解)

撸了郭霖大神写的Framework源码笔记,含泪整理面经

Spring Framework Runtime