Spring AOP

Spring AOP

在软件中,有些行为对于大多数应用都是通用的。日志、安全和事务管理的确很重要,但它们是否是应用对象主动参与的行为呢?如果让应用对象只关注于自己所针对的业务领域问题,而其他方面的问题有其他应用对象来处理,这样会更好

在软件开发中,分布于应用中多处的功能被称为横切关注点(cross-cutting coners)。通常,这些横切关注点从概念上是与应用的业务逻辑相分离的(但是往往直接嵌入到应用的业务逻辑之中)。将这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的

依赖注入(DI)有助于应用对象之间的解耦,而AOP可以实现横切关注点与它们所影响的对象之间的解耦

AOP基础

如下的ArithmeticCalculator接口,定义加减乘除方法:

public interface ArithmeticCalculator {

    //加
    int add(int i, int j);
    //减
    int sub(int i, int j);
    //乘
    int mul(int i, int j);
    //除
    int div(int i, int j);

}

假设程序现在有如下的需求:

  1. 日志-在程序执行期间追踪正在发生的活动
  2. 验证-希望计算器只能处理正确的运算

根据需求,实现ArithmeticCalculator接口如下:

public class ArithmeticCalculatorImpl implements ArithmeticCalculator {

    @Override
    public int add(int i, int j) {
        System.out.println("The method add begins with [" + i + "," + j + "]");
        int result =  i + j;
        System.out.println("The method add ends with " + result);
        return result;
    }
    ......
}

这样做会发现:

  1. 加减乘除方法中都要添加类似的输出语句
  2. 方法中的逻辑会显的很混乱

总结起来,这样做有如下的问题:

  • 代码混乱:越来越多的非业务需求(日志和验证等)加入后,原有的业务方法急剧膨胀。每个方法在处理核心逻辑的同时还必须兼顾其他多个关注点
  • 代码分散:以日志需求为例,只是为了满足这个单一需求,就不得不在多个模块(方法)里多次重复相同的日志代码。如果日志需求发生变化,必须修改所有模块

如何解决上述问题呢?
可以使用动态代理,代理设计模式的原理是:使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。

动态代理

如下,创建一个计算器的日志代理类ArithmeticCalculatorLogginProxy,详细说明见注释:

package com.wz.spring.aop.helloworld;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

public class ArithmeticCalculatorLogginProxy {

    //要代理的对象
    private ArithmeticCalculator target;

    public ArithmeticCalculatorLogginProxy(ArithmeticCalculator target) {
        this.target = target;
    }

    public ArithmeticCalculator getLoggingProxy(){
        ArithmeticCalculator proxy = null;
        //代理对象由哪一个类加载器负责加载
        ClassLoader loader = target.getClass().getClassLoader();
        //代理对象的类型,即其中有哪些方法
        Class[] interfaces = new Class[] {ArithmeticCalculator.class};
        //当调用代理对象其中的方法时,执行代码
        InvocationHandler h = new InvocationHandler() {
            /**
             * proxy - 正在返回的那个代理对象,一般情况下,在invoke方法中都不使用该对象
             * method - 正在被调用的方法
             * args - 调用方法时,传入的参数
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                String methodName = method.getName();
                //日志
                System.out.println("The method " + methodName + " begins with " + Arrays.asList(args));
                //执行方法
                Object result = method.invoke(target, args);
                //日志
                System.out.println("The method " + methodName + " ends with " + result);
                return result;
            }
        };
        proxy = (ArithmeticCalculator) Proxy.newProxyInstance(loader, interfaces, h);

        return proxy;
    }

}

进行如下的测试,代码如下:

        ArithmeticCalculator target = new ArithmeticCalculatorImpl();
        ArithmeticCalculator proxy = new ArithmeticCalculatorLogginProxy(target).getLoggingProxy();

        int result = proxy.add(1, 2);
        System.out.println("-->" + result);

测试控制台输出如下:

The method add begins with [1, 2]
The method add ends with 3
-->3

可以发现,用动态代理实现的日志方式,比原来的那种方式,更简洁,更方便

但如果每个需求都要写个动态代理,其实还是有很多的工作量,能不能用一个更简单的方式来实现呢?答案就是使用AOP

AOP简介

AOP(Aspect-Oriented Programming)面向切面编程。AOP的主要编程对象是切面(aspect),而切面模块化横切关注点

在应用AOP编程时,仍然需要定义公共功能,但可以明确的定义这个功能在哪里,以什么方式应用,并且在不必修改受影响的类。这样一来横切关注点就被模块化到特殊的对象(切面)里

AOP的好处:

  • 每个事物逻辑位于一个位置,代码不分散,便于维护和升级
  • 业务模块更简洁,只包含核心业务代码

AOP

AOP术语

切面(Aspect):横切关注点(跨越应用程序多个模块的功能)被模块化的特殊对象。(如上图中的验证,日志等)。
通知(Advice):切面必须要完成的工作(可以把切面里的每一个方法称为通知)
目标(Target):被通知的对象
代理(Proxy):向目标对象应用通知之后创建的对象

连接点(Joinpoint):程序执行的某个特定位置。如类某个方法调用前、调用后、方法抛出异常后等。连接点由两个信息确定:方法(表示程序的执行点),相对点(表示方位)。例如,ArithmeticCalculator#add()方法执行前的连接点,其中执行点为ArithmeticCalculator#add(),方位为该方法执行前的位置

切点(pointcut):每个都有多个连接点,例如ArithmeticCalculator的所有方法都是连接点,即连接点是程序类中客观存在的事务。AOP通过切点定位到特定的连接点。类比:连接点相当于数据库中的记录,切点相当于查询条件。切点和连接点不是一对一的关系,一个切点匹配多个连接点,切点通过org.springframework.aop.Pointcut接口进行描述,它使用类和方法作为连接点的查询条件

在Spring2.0以上版本中,可以使用基于AspectJ注解或基于XML配置的AOP。AspectJ是Java社区里最完整最流行的AOP框架。

简单的理解:

  • 切面是通知和切点的集合
  • 通知定义了切面是什么以及何时使用
  • 如果通知定义了切面的”什么”和”何时”,那么切点就定义了”何处”。切点的定义会匹配通知所要织入的一个或多个连接点

在Spring中启用AspectJ注解支持

要在Spring应用中使用AspectJ注解,必须在classpath下包含AspectJ类库:aopalliance.jaraspectj.weaver.jarspring-aspects.jar。全部的包如下:
aop的包

创建Spring的配置文件applicationContext.xml,加入beansaopcontext命令空间

现在要做的是还是一样在调用ArithmeticCalculator接口中的方法时加入日志功能

基本步骤如下:

1.给ArithmeticCalculatorImpl添加@Component注解

@Component
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {

    @Override
    public int add(int i, int j) {
        int result =  i + j;
        return result;
    }
    ......
}

2.从业务逻辑中抽取横切关注点,有关联的、相似的横切关注点可组成切面。这里,创建日志切面LoggingAspect

LoggingAspect声明为一个切面需要如下的步骤:

  • 需要把该类放入IoC容器中,使用@Component注解
  • 声明为一个切面,使用@Aspect注解

LoggingAspect结构如下:

//把该类声明为一个切面
//1.需要把该类放入ioc容器中
//2.再声明为一个切面
@Component
@Aspect
public class LoggingAspect {
    ......
}

3.applicationContext.xml中的配置,这里是基于注解的方式

    <!-- 配置自动扫描的包 -->
    <context:component-scan base-package="com.wz.spring.aop.impl"></context:component-scan>
    <!-- 使AspectJ注解起作用:自动为匹配的类生成代理对象 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

通知

Spring切面可以应用5中类型的通知

  • Before-在方法被调用之前调用通知
  • After-在方法完成之后调用通知,无论方法执行是否成功
  • After-returning-在方法成功执行之后调用通知
  • After-throwing-在方法抛出异常后调用通知
  • Around-通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为
前置通知

LoggingAspect中创建一个beforeMethod方法,这里主要是用来在调用ArithmeticCalculator接口的方法前输出日志

@Before声明方法是一个前置通知,在目标方法开始之前执行

    //声明该方法是一个前置通知,在目标方法开始之前执行
    @Before("execution(public int com.wz.spring.aop.impl.ArithmeticCalculator.add(int, int))")
    public void beforeMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();//方法名称
        List<Object> args = Arrays.asList(joinPoint.getArgs());//参数
        System.out.println("The method " + methodName + " begins" + args);
    }

一些说明:

1.@Before("execution(public int com.wz.spring.aop.impl.ArithmeticCalculator.add(int, int))")表示在AthimeticCalculatoradd方法调用前执行

如果想作用于AthimeticCalculator下的所有方法,可以使用*

@Before("execution(public int com.wz.spring.aop.impl.AthimeticCalculator.*(int, int))")

execution用于匹配连接点的执行方法

  • execution * com.wz.spring.aop.impl.AthimeticCalculator.*(..)匹配AthimeticCalculator中声明的所有方法,第一个*代表任意修饰符及任意返回值,第二个*代表任意方法,..匹配任意数量的参数。若目标类与接口与该切面在同一个包中,可以省略包名
  • execution public * com.wz.spring.aop.impl.AthimeticCalculator.*(..)匹配AthimeticCalculator接口的所有公共方法
  • execution public double com.wz.spring.aop.impl.AthimeticCalculator.*(..)匹配AthimeticCalculator接口中返回double类型数值的方法
  • execution public double com.wz.spring.aop.impl.AthimeticCalculator.*(double,..)匹配第一个参数为double类型的方法,..匹配任意数量的参数
  • execution public double com.wz.spring.aop.impl.AthimeticCalculator.*(double,double)匹配参数类型为doubledouble类型的方法

这里需要注意的是,一定要在applicationContext.xml中,添加上:

    <!-- 使AspectJ注解起作用:自动为匹配的类生成代理对象 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

其作用是:当我们调用一个目标方法,而这个目标方法跟注解声明的这个方法相匹配时,AOP框架会自动为方法所在的类生成一个代理。

2.方法的参数JoinPoint,主要用来获取方法的名称和参数

3.测试如下

    public static void main(String[] args) {
        //1.创建spring ioc容器
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        //2.从ioc容器中获取bean的实例
        ArithmeticCalculator arithmeticCalculator = ctx.getBean(ArithmeticCalculator.class);
        //3.使用bean
        int result =  arithmeticCalculator.add(1, 2);
        System.out.println("result  = " + result);

    }

控制台输出结果如下:

The method add begins[1, 2]
result  = 3
后置通知

后置通知在目标方法执行后(无论是否发生异常),执行通知。
在后置方法中还不能访问目标方法执行的结果。

    //后置通知在目标方法执行后(无论是否发生异常),执行通知
    @After("execution(public int com.wz.spring.aop.impl.ArithmeticCalculator.*(int, int))")
    public void afterMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("The method " + methodName + " ends");
    }

如下一个数除以0,如果抛出异常,任然会执行通知:

result =  arithmeticCalculator.div(1, 0);
System.out.println("result  = " + result);

控制台输出结果为:

The method div begins[1, 0]
The method div ends
返回通知

在方法正常结束后执行的代码,返回通知是可以访问到方法的返回值的。

  • 在返回通知中,只要将returning属性添加到注解中,就可以访问连接点的返回值。该属性的值即为用来传入返回值的参数名称
  • 必须在通知方法的签名中添加一个同名参数。在运行时,Spring AOP会通过这个参数传递返回值
    @AfterReturning(value="execution(public int com.wz.spring.aop.impl.ArithmeticCalculator.*(int, int))", returning="result")
    public void afterReturning(JoinPoint joinPoint, Object result ) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("The method "+methodName +" return with "+result);
    }
异常通知

在方法出现异常时会执行的代码,可以访问到异常的对象,且可以指定在出现特定异常时在执行通知代码

  • throwing属性添加到@AfterThrowing注解中
  • 如果只对某种特殊类型的异常类型感兴趣,可以将参数声明为其他异常的参数类型,然后通知就只在抛出这个类型及其子类的异常时才被执行
    @AfterThrowing(value="execution(public int com.wz.spring.aop.impl.ArithmeticCalculator.*(int, int))", throwing="ex")
    public void afterThrowing(JoinPoint joinPoint, Exception ex) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("The method "+methodName +" throw with "+ex);
    }

一个数除以0,此时测试,控制输出如下的接口:

The method div begins[1, 0]
The method div ends
The method div throw with java.lang.ArithmeticException: / by zero
环绕通知

环绕通知需要携带ProceedingJoinPoint类型的参数。环绕通知类似于动态代理的全过程,ProceedingJoinPoint类型的参数可以决定是否执行目标方法。且环绕通知必须有返回值,返回值即为目标方法的返回值。

    @Around("execution(public int com.wz.spring.aop.impl.ArithmeticCalculator.*(int, int))")
    public Object around(ProceedingJoinPoint pjd) {
        System.out.println("around");
        Object result = null;
        String methodName = pjd.getSignature().getName();
        //执行目标方法
        try {
            //前置通知
            System.out.println("The method " + methodName + " begins with " + Arrays.asList(pjd.getArgs()));
            result = pjd.proceed();
            //后置通知
            System.out.println("The method "+methodName +" ends with "+Arrays.asList(pjd.getArgs()));
        } catch (Throwable e) {
            //异常通知
            e.printStackTrace();
        }
        //后置通知
        return result;
    }

切面的优先级

现在再添加一个验证切面ValidationAspect,如下:

@Aspect
@Component
public class ValidationAspect {

    @Before("execution(public int com.wz.spring.aop.impl.ArithmeticCalculator.*(int, int))")
    public void validateArgs(JoinPoint joinPoint){
        System.out.println("validate: " + Arrays.asList(joinPoint.getArgs()));
    }

}

那现在就有一个问题是,日志通知和验证通知,哪一个在前,哪一个在后呢?

可以指定切面的优先级,指定哪个切面在前,哪个切面在后。

使用@Order注解指定切面的优先级,值越小优先级越高

如下:

@Order(1)
@Aspect
@Component
public class ValidationAspect {

    @Before("execution(public int com.wz.spring.aop.impl.ArithmeticCalculator.*(int, int))")
    public void validateArgs(JoinPoint joinPoint){
        System.out.println("validate: " + Arrays.asList(joinPoint.getArgs()));
    }

}

此时,控制台就会先输出验证的通知:

validate: [1, 2]
The method add begins[1, 2]

重用切点表达式
@Pointcut来声明切入点表达式,后面的其他通知直接使用方法名来引用当前的切入点表达式。

        /**
         * 定义一个方法,用于声明切入点表达式
         * 一般的,该方法中不需要添入其他的代码
         */
        @Pointcut("execution(public int aop.impl.AthimeticCalculator.*(int, int))")
        public void declareJointPoinExpression() {}

        @Before("declareJointPoinExpression()")
        public void beforeMethod(JoinPoint joinpoint) {
            String methodName = joinpoint.getSignature().getName();
            List<Object> args = Arrays.asList(joinpoint.getArgs());
            System.out.println("The method "+methodName +" begins with "+args);
    }

Spring基于配置文件的方式来配置AOP

Spring也支持在Bean配置文件中声明切面。这种声明是通过aop schema中的XML元素完成的。
正常情况下,基于注解的声明要优于基于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"
        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-3.2.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd">


        <!-- 配置Bean -->
        <bean id="athimeticCalculator" class="aop.impl.xml.AthimeticCalculatorImpl"></bean>

        <!-- 配置切面的bean -->
        <bean id="loggingAspect" class="aop.impl.xml.LoggingAspect"></bean>
        <bean id="validationAspect" class="aop.impl.xml.ValidationAspect"></bean>

        <!-- 配置AOP -->
        <aop:config>
            <!-- 配置切点表达式 这里可以服用,避免重复 -->
            <aop:pointcut expression="execution(* aop.impl.xml.*.*(..))" id="pointcut"/>
            <!-- 配置切面及通知 -->
            <aop:aspect ref="loggingAspect" order="2">
                <aop:before method="beforeMethod" pointcut-ref="pointcut"/>
                <aop:after method="afterMethod" pointcut-ref="pointcut"/>
            </aop:aspect>
            <aop:aspect ref="validationAspect" order="1">
                <aop:before method="validateArgs" pointcut-ref="pointcut"/>
            </aop:aspect>
        </aop:config>

    </beans>

需要注意的是:

1.大多数的AOP配置元素必须在<aop:config>元素的上下文内使用
2.在<aop:config>元素内,可以声明一个或多个通知器、切面或者切点。aop:aspectref元素引用了一个POJO Bean
3.aop:before表示前置通知,pointcut属性定义了通知所应用的切点。
4.aop:pointcut定义了一个命名切点,可以避免重复定义切点

Spring的AOP配置元素简化了基于POJO切面的声明

AOP配置元素描述
<aop:advisor>定义AOP通知器
<aop:after>定义AOP后置通知(不管被通知的方法是否执行成功)
<aop:after-returning>定义AOP after-returning通知
<aop:after-throwing>定义after-throwing通知
<aop:around>定义AOP环绕通知
<aop:aspect>定义切面
<aop:aspectj-autoproxy>启用@AspectJ注解驱动的切面
<aop:before>定义AOP前置通知
<aop:config>顶层AOP配置元素。大多数的<aop:*>元素必须包含在<aop:config>元素内
<aop:declare-parents>为被通知的对象引入额外的接口,并透明的实现
<aop:pointcut>定义切点
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值