Spring AOP 入门与使用

1.概述

  • AOP(Aspect-Oriented Programming, 面向切面编程): 是一种新的方法论, 是对传统 OOP(Object-Oriented Programming, 面向对象编程) 的补充.
  • AOP 的主要编程对象是切面(aspect), 而切面模块化横切关注点.
  • 在应用 AOP 编程时, 仍然需要定义公共功能, 但可以明确的定义这个功能在哪里, 以什么方式应用, 并且不必修改受影响的类. 这样横切关注点就被模块化到特殊的对象(切面)里.
  • AOP 的好处:
    • 每个事物逻辑位于一个位置, 代码不分散, 便于维护和升级
    • 业务模块更简洁, 只包含核心业务代码.

2. AOP术语

  • 切面(Aspect): 横切关注点(跨越应用程序多个模块的功能)被模块化的特殊对象
  • 通知(Advice): 切面必须要完成的工作
  • 目标(Target): 被通知的对象
  • 代理(Proxy): 向目标对象应用通知之后创建的对象
  • 连接点(Joinpoint):程序执行的某个特定位置:如类某个方法调用前、调用后、方法抛出异常后等。连接点由两个信息确定:方法表示的程序执行点;相对点表示的方位。
  • 切点(pointcut):每个类都拥有多个连接点,连接点即程序类中客观存在的事务。AOP通过切点定位到特定的连接点。类比:连接点相当于数据库中的记录,切点相当于查询条件。切点和连接点不是一对一的关系,一个切点匹配多个连接点,切点通过org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。

3. AspectJ

  • 要在 Spring 应用中使用 AspectJ 注解, 必须在 classpath 下包含 AspectJ 类库: aopalliance.jar、aspectj.weaver.jar 和
    spring-aspects.jar
  • 将 aop Schema 添加到 根元素中.
  • 要在 Spring IOC 容器中启用 AspectJ 注解支持, 只要在 Bean 配置文件中定义一个空的 XML 元素 <aop:aspectj-autoproxy>
  • 当 Spring IOC 容器侦测到 Bean 配置文件中的 <aop:aspectj-autoproxy> 元素时, 会自动为与 AspectJ 切面匹配的 Bean 创建代理.

4.用AspectJ注解声明切面

  • 要在 Spring 中声明 AspectJ 切面, 只需要在 IOC 容器中将切面声明为 Bean 实例. 当在 Spring IOC 容器中初始化 AspectJ 切面之后, Spring IOC 容器就会为那些与 AspectJ 切面相匹配的 Bean 创建代理.
  • 在 AspectJ 注解中, 切面只是一个带有 @Aspect 注解的 Java 类.
  • 通知是标注有某种注解的简单的 Java 方法.

  • AspectJ 支持 5 种类型的通知注解:

    类型 作用时间
    @Before 在方法执行之后执行
    @After 在方法执行之后执行(无论是否发生异常)
    @AfterRunning 在方法返回结果之后执行
    @AfterThrowing 异常通知, 在方法抛出异常之后
    @Around 围绕着方法执行
  • 举个栗子:

Object result = null;
try {
    //前置通知
    result = method.invoke(target, args);//方法的调用
    //返回通知,可以得到返回值
}catch (Exception e){
    e.printStackTrace();
    //异常通知,可以得到异常
}
//后置通知,因为方法可能出现异常,所以访问不到方法的返回值

5.前置通知

  • ArithmeticCalculator接口
public interface ArithmeticCalculator {
    public int add(int i, int j);
    public int sub(int i, int j);
    public int mul(int i, int j);
    public int div(int i, int j);
}
  • ArithmeticCalculator实现类
//将其放入IoC容器
@Component
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
    @Override
    public int add(int i, int j) {
        int result = i+j;
        return result;
    }

    @Override
    public int sub(int i, int j) {
        int result = i-j;
        return result;
    }

    @Override
    public int mul(int i, int j) {
        int result = i*j;
        return result;
    }

    @Override
    public int div(int i, int j) {
        int result = i/j;
        return result;
    }
}
  • 声明一个切面,并声明前置通知方法
@Aspect
@Component
public class LoggingAspect {

    //声明该方法是前置通知
    @Before("execution(public int com.chen.demo.ArithmeticCalculator.*(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 with " + args);
    }
}
  • spring配置文件
<!--使AspectJ注解起作用:自动为匹配的类生成代理对象-->
<aop:aspectj-autoproxy/>

6. 后置通知

  • 在后置通知中还不能访问目标方法执行的结果
  • 声明后置通知方法
    //声明该方法是后置通知(无论是否发生异常)
    @After("execution(public int com.chen.demo.ArithmeticCalculator.*(..))")
    public void afterMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        System.out.println("The method " + methodName + " ends with " + args);
    }

7. 返回通知

  • 返回通知可以得到方法返回值
  • 声明返回通知方法
    //在方法正常结束后执行的代码
    @AfterReturning(value = "execution(public int com.chen.demo.ArithmeticCalculator.*(int,int))",returning = "result")
    public void afterReturning(JoinPoint joinPoint,Object result){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("The method "+methodName+" ends with "+result);
    }

8. 异常通知

  • 可以访问到异常对象,且可以指定在出现特定异常时再执行通知代码
  • 声明异常通知方法
    //在目标方法出现异常时会执行的代码
    @AfterThrowing(value ="execution(public int com.chen.demo.ArithmeticCalculator.*(..))",throwing = "ex")
    public void afterThrowing(JoinPoint joinPoint,Exception ex){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("The method "+methodName+" occurs exception  "+ex);
    }

9.环绕通知

  • 环绕通知是所有通知类型中功能最为强大的, 能够全面地控制连接点. 甚至可以控制是否执行连接点
  • 对于环绕通知来说, 连接点的参数类型必须是 ProceedingJoinPoint 。 它是 JoinPoint 的子接口, 允许控制何时执行, 是否执行连接点。
  • 在环绕通知中需要明确调用 ProceedingJoinPoint 的 proceed() 方法来执行被代理的方法。 如果忘记这样做就会导致通知被执行了, 但目标方法没有被执行
  • 环绕通知的方法需要返回目标方法执行之后的结果, 即调用 joinPoint.proceed() 的返回值, 否则会出现空指针异常。

  • 声明环绕通知方法

    @Around(value ="execution(public int com.chen.demo.ArithmeticCalculator.*(int,int))")
    public Object aroundMethod(ProceedingJoinPoint pjd){
        //执行目标方法
        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 "+result);
        } catch (Throwable throwable) {
            //异常通知
            throwable.printStackTrace();
            System.out.println("The method "+methodName+" occurs exception  "+throwable);
            throw new RuntimeException(throwable);
        }
        //后置通知
        System.out.println("The method " + methodName + " ends");
        return  result;
    }

10. 优先级

  • 在同一个连接点上应用不止一个切面时,除非明确指定,否则它们的优先级是不确定的。
  • 切面的优先级可以通过实现 Ordered 接口或利用 @Order 注解指定。
  • 实现 Ordered 接口, getOrder() 方法的返回值越小, 优先级越高。
  • 若使用 @Order 注解, value值在注解中,值越小优先级越高

  • 若在上面的栗子中再加入验证的切面

@Order(1)
@Aspect
@Component
public class ValidationAspect {
    @Before(value = "orderAndPointcut_3.LoggingAspect.declareJoinPointExpression()")
    public void validateArgs(JoinPoint joinPoint){
        System.out.println("validate:"+ Arrays.asList(joinPoint.getArgs()));
    }
}
  • 原来的LoggingAspect
@Order(2)
@Aspect
@Component
public class LoggingAspect {
    ...
}

11. 重用切点表达式

  • 在写 AspectJ 切面时, 可以直接在通知注解中写切入点表达式。 但同一个切点表达式可能会在多个通知中重复出现
  • 在 AspectJ 切面中, 可以通过 @Pointcut 注解将一个切入点声明成简单的方法。 切入点的方法体通常是空的, 因为将切入点定义与应用程序逻辑混在一起是不合理的。
  • 切入点方法的访问控制符同时也控制着这个切入点的可见性。如果切入点要在多个切面中共用, 最好将它们集中在一个公共的类中。 在这种情况下, 它们必须被声明为 public。 在引入这个切入点时, 必须将类名也包括在内。 如果类没有与这个切面放在同一个包中, 还必须包含包名。
  • 其他通知可以通过方法名称引入该切入点。

  • 定义一个用于声明切入点表达式方法

    @Pointcut("execution(public int com.chen.demo.ArithmeticCalculator.*(int,int))")
    public void declareJoinPointExpression(){}
  • 此时的前置通知方法
    @Before("declareJoinPointExpression()")
    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);
    }
  • 在其他类中公用表达式
    @Aspect
    @Component
    public class ValidationAspect {
        @Before(value = "com.chen.demo.LoggingAspect.declareJoinPointExpression()")
        public void validateArgs(JoinPoint joinPoint){
            System.out.println("validate:"+ Arrays.asList(joinPoint.getArgs()));
        }
    }

12. 基于配置文件实现AOP

  • 除了使用 AspectJ 注解声明切面, Spring 也支持在 Bean 配置文件中声明切面. 这种声明是通过 aop schema 中的 XML 元素完成的.
  • 正常情况下, 基于注解的声明要优先于基于 XML 的声明. 通过 AspectJ 注解, 切面可以与 AspectJ 兼容, 而基于 XML 的配置则是 Spring 专有的. 由于 AspectJ 得到越来越多的 AOP 框架支持, 所以以注解风格编写的切面将会有更多重用的机会.

  • Spring配置文件

    <!--配置bean-->
    <bean id="arithmeticCalculator" class="com.chen.xml.ArithmeticCalculatorImpl"/>
    <bean id="loggingAspect" class="com.chen.xml.LoggingAspect"/>
    <bean id="validationAspect" class="com.chen.xml.ValidationAspect"/>
    <!--配置AOP-->
    <aop:config>
        <!--配置切点表达式-->
        <aop:pointcut id="pointcut" expression="execution(* com.chen.xml.ArithmeticCalculatorImpl.*(int,int))"/>
        <!--配置切面及通知-->
        <aop:aspect ref="loggingAspect" order="2">
            <aop:before method="beforeMethod" pointcut-ref="pointcut"/>
            <aop:after method="afterMethod" pointcut-ref="pointcut"/>
            <aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="ex"/>
            <aop:after-returning method="afterReturning" pointcut-ref="pointcut" returning="result"/>
            <!--<aop:around method="aroundMethod" pointcut-ref="pointcut"/>-->
        </aop:aspect>
        <aop:aspect ref="validationAspect" order="1">
            <aop:before method="validateArgs" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>

猜你喜欢

转载自blog.csdn.net/qq_37138933/article/details/79194924
今日推荐