Spring-AOP

158 阅读9分钟

什么是AOP

纵向抽取方式

在以往我们编写的代码中,如果一段代码是重复的,则可以将重复代码抽取成一个方法,等需要使用的时候再继承那个类即可。比如一些监视性能的方法代码。但这种抽取成类方法的纵向抽取方式还是会出现重复代码,如下所示:

其中注释1,2为业务代码,斜体代码为方法性能监视代码,黑色粗体代码为事务开始和提交代码。业务代码夹杂在重复非业务性代码之中。这些重复非业务代码依附在业务类方法*removeTopiccreateForum*中,无法将其转移到其他地方。整个方法的大致结构如下:

面向切面编程AOP

针对上述问题的一种解决方案是AOP:通过横向抽取机制将分散在业务类方法里的重复非业务代码抽取到各个独立的模块:

从而降低业务逻辑各个部分之间的耦合度,提高程序的可重用性,同时提高了开发的效率。

AOP的另一种直观图(图源自狂神说Spring07:AOP就这么简单):

AOP的原理

本节需要了解代理模式

Spring AOP使用两种代理机制:基于JDK的动态代理和基于CGLib的动态代理。此处我们以上面提到的逻辑方法作为例子。

JDK的动态代理

(1)被代理类ForumServiceImpl,它负责实现业务代码。

(2)被移除的重复非业务的性能监视代码放在代理类PerformanceHandler中,该类负责性能监视的相关方法并通过method.invoke()语句中的Java反射机制间接调用了业务方法。

(3)测试方法,它通过newProxyInstance ()方法来动态创建ForumService的代理类:

CGLib动态代理

使用JDK创建代理有一个限制:它只能为接口创建代理实例,比如上例是为接口ForumService创建的。对于一些没有通过接口定制业务方法的类,CGLib可以帮助我们。它采用底层的字节码技术,为一个类创建子类,在子类中采用方法拦截的技术拦截所有父类方法的调用并插入横切逻辑。

如下所示,CglibProxt作为代理类 实现了org.springframework.cglib.proxy.MethodInterceptor接口,该接口是一个方法拦截器,代理类可以通过接口方法*intercept*来拦截被代理类方法的调用,并指定拦截方法的顺序中添加其他操作。

*intercept*方法的参数obj表示被代理类实例,method为被代理类方法的反射对象,args为方法的动态参数,proxy为被代理类实例。

代理类的属性Enhancer类对象主要用于为 被代理类生成一个动态子类,可以在被代理类方法执行前后中执行一些其他操作。

测试方法如下:

其他示例

我们再以电影上映为例,尝试一下CGLib动态代理

(1)被代理类:

public class ActionMovie{
    public void play() {
        System.out.println("正在播放电影《终结者》");
    }
}

(2)代理类

public class MovieProxy implements MethodInterceptor {
    private Enhancer enhancer = new Enhancer();
    public Object getProxy(Class clazz) {
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        return enhancer.create();
    }

    public void playAdvertisement(boolean isStart){
        if ( isStart ) {
            System.out.println("即将开始播放,开始售卖零食");
        } else {
            System.out.println("即将结束播放,准备关门");
        }
    }

    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        playAdvertisement(true);
        Object res = methodProxy.invokeSuper(o, objects);
        playAdvertisement(false);
        return  res;
    }
}

(3)测试方法:

@Test
public void test(String[] args) {
        //生成被代理类ActionMovie的代理实例movieProxy
        MovieProxy movieProxy = new MovieProxy();
        ActionMovie actionMovie = (ActionMovie)movieProxy.getProxy(ActionMovie.class);
        actionMovie.play();
}

需要注意的是:由于CGHLib采用动态创建子类的方式生成代理对象,因此不能对被代理类中的final或private方法进行代理。

两种方式的使用场景

Spring AOP默认是使用JDK动态代理,如果被代理类没有接口则会使用CGLib代理。如果是单例类最好使用CGLib代理,否则最好使用JDK代理。这是因为JDK在创建代理对象时的性能要高于CGLib代理,而生成代理对象的运行性能却比CGLib的低。

AOP的术语

连接点

连接点是程序执行的某个特定位置,如类开始初始化前,类初始化后,类的某个方法调用前/后,方法抛出异常后。一个类或一段程序代码拥有一些边界性质的特定点,这些特定点称为连接点。Spring仅支持方法的连接点,即仅能在方法调用前/后,方法抛出异常时的这些程序执行点插入其他操作。

切点

每一个程序类都拥有多个连接点,而AOP是通过“切点”定位到特定的连接点。以数据库查询为例,连接点相当于数据库中的记录,切点则相当于查询条件。一个切点可以匹配多个连接点。

增强(Advice)

增强描述了插入目标类连接点上的一段程序代码,此外它还拥有和连接点相关的信息,即执行点方位。Spring提供的增强接口都是带方位名的,如表示方法调用前的位置BeforeAdvice,表示方法返回后的位置AfterReturningAdvice,ThrowsAdvice等,

织入

织入是指将增强添加到目标类的具体连接点的过程,Spring采用的是动态代理织入方式,即在运行期为目标类添加增加生成子类。

代理(Proxy)

一个类被AOP织入增强后,就产生一个结果类,它一个结合原类和增强逻辑的代理类。根据不同的代理方式,代理类既可能是和原类具有相同接口的类,也可能是原类的子类。

切面(Aspect)

切面由切点和增强组成,它包括横切逻辑的定义和连接点的定义。Spring AOP就是负责实施切面的框架,它将切面定义的横切逻辑(比如前面提到的性能监视代码)织入切面所定义的连接点中。

Spring的AOP支持

  • 基于代理的经典SpringAOP;
  • 基于XML的AOP;
  • 基于注解的AOP;

此处只介绍后两种。

基于XML的AOP

该方式通过配置文件来定义切面、切入点及声明通知,而所有的切面和通知都必须定义在 < aop:config> 元素中。

在使用AOP织入前,需要导入依赖:

 <!--aspectj支持-->
 <dependency>
   <groupId>org.aspectj</groupId>
   <artifactId>aspectjweaver</artifactId>
   <version>1.8.3</version>
 </dependency>

示例

(1)service接口和实现类:

public interface UserService {
     void search();
}
@Service("UserService")
public class UserServiceImpl implements UserService {

    public void search() {
        // 可以通过 int a = 2 / 0; 语句来模拟异常,此时后置通知方法不会执行。
        System.out.println("业务操作:查询用户");
    }
}

(2)增强类:一个模拟记录的日志类

@Component
public class MyLog  {
    //  前置通知
    public void beforePrintLog() {
        System.out.println("前置通知");
    }
    //  后置通知,在切入点方法正常执行后执行,它和异常通知只能执行一个
    public void afterPrintLog() {
        System.out.println("后置通知");
    }
    //   异常通知
    public void ThrowingPrintLog() {
        System.out.println("异常通知");
    }
    //    最终通知,无论切入点方法是否正常执行它都会在其后面执行
    public void finallyPrintLog() {
        System.out.println("最终通知");
    }
    //环绕通知里需要我们手动执行切入点方法search
    public Object aroundPrintLog(ProceedingJoinPoint pjp) {
        Object res = null;
        try {
            Object[] args = pjp.getArgs();  //得到切入点方法的参数
            
            System.out.println("环绕通知里的前置通知");
            res = pjp.proceed(args);    //调用切入点方法search
            
            System.out.println("环绕通知里的后置通知");
            return res;
        }catch (Throwable t) {
            System.out.println("环绕通知里的异常通知");
            throw new RuntimeException(t);
        }finally {
            System.out.println("环绕通知里的最终通知");
        }
    }
}

(3)配置文件aop.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.2.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-4.2.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">

    <context:component-scan base-package="com.jnju"/>

    <!--
切入点表达式:
    使用到的关键字为: execution
    标志表达式写法为:访问修饰符 返回值 包名.类名.方法名(参数列表)。
    例如 "execution(public void com.on1.service.impl.AccountServiceImpl.saveAccount())
   全通配写法:
        (1)访问修饰符可以省略;
        (2)返回值可以使用通配符*代替,即任意返回值都可以。此例是* com.on1.service.impl.AccountServiceImpl.saveAccount();
        (3)包名也可使用通配符*,有几级包就要写几个*。此例是* *.*.*.*.AccountServiceImpl.saveAccount()
        此外包名还可以使用..表示当前包及其子包。此例是 * *..AccountServiceImpl.saveAccount()
        (4)类名和方法名都可以使用*来实现通配。此例是 * *..*.*(),此处表示的是无参方法
        (5)对于参数列表,若是基本类型则直接写名称,比如int,如果是引用类型则写包名.类名,比如java.lang.String。
        以另一个方法updateAccount(int accountID)为例,则为:* *..*.*(int)
        此外参数列表也可通过*表示有参数,其类型是任意的。
   实际开发中切入点表达式的通常写法:切换到某一类的所有方法,比如 * com.on1.service.impl.*.*(..)
-->
    <aop:config>
        <!-- 配置切入点表达式,此标签写在aop:aspect标签内部则只能是当前切面使用,写在外部则是所有切面可用-->
        <aop:pointcut id="pt1" expression="execution(* com.jnju.service.impl.*.*(..))"/>
        <!-- 配置切面-->
        <aop:aspect id="logAdvice" ref="myLog">
            <aop:before method="beforePrintLog" pointcut-ref="pt1"/>
            <aop:after-returning method="afterPrintLog" pointcut-ref="pt1"/>
            <aop:after-throwing method="ThrowingPrintLog" pointcut-ref="pt1"/>
            <aop:after method="finallyPrintLog" pointcut-ref="pt1"/>
           <aop:around method="aroundPrintLog" pointcut-ref="pt1"/>
        </aop:aspect>
    </aop:config>
</beans>

(4)测试方法

    @Test
    public void testAOP() {
        ApplicationContext context = new ClassPathXmlApplicationContext("aop.xml");
        UserService userService = (UserService) context.getBean("UserService");

        userService.search();
    }

输出:

前置通知
环绕通知里的前置通知
业务操作:查询用户
环绕通知里的后置通知
环绕通知里的最终通知
最终通知
后置通知

最后再给出xml文件里的相关标签:

基于注解的AOP

在开始之前需要开启注解扫描:

<aop:aspectj-autoproxy/>

例子同样不变,只需修改增强类MyLog

@Component
@Aspect
public class MyLog  {

    @Pointcut("execution(* com.jnju.service.impl.*.*(..))")
    private void pt1(){}        //切入点表达式

    //  前置通知
    @Before("pt1()")
    public void beforePrintLog() {
        System.out.println("前置通知");
    }
    //  后置通知,在切入点方法正常执行后执行,它和异常通知只能执行一个
    @AfterReturning("pt1()")
    public void afterPrintLog() {
        System.out.println("后置通知");
    }
    //   异常通知
    @AfterThrowing("pt1()")
    public void ThrowingPrintLog() {
        System.out.println("异常通知");
    }
    //    最终通知,无论切入点方法是否正常执行它都会在其后面执行
    @After("pt1()")
    public void finallyPrintLog() {
        System.out.println("最终通知");
    }
    @Around("pt1()")
    public Object aroundPrintLog(ProceedingJoinPoint pjp) {
        Object res = null;
        try {
            Object[] args = pjp.getArgs();  //得到切入点方法的参数
            // 如果环绕通知操作(此处的输出语句)写在调用方法前,则表示是前置通知
            System.out.println("环绕通知里的前置通知");
            res = pjp.proceed(args);    //调用切入点方法
            // 如果环绕通知操作(此处的输出语句)写在调用方法之后,则表示是后置通知
            System.out.println("环绕通知里的后置通知");
            return res;
        }catch (Throwable t) {
            // 如果环绕通知操作(此处的输出语句)写在捕捉异常,则表示是异常通知
            System.out.println("环绕通知里的异常通知");
            throw new RuntimeException(t);
        }finally {
            // 如果环绕通知操作(此处的输出语句)写在finally语句,则表示是最终通知
            System.out.println("环绕通知里的最终通知");
        }
    }
}

输出不变。

参考资料

《精通Spring4.x 企业应用开发实战》