Spring之AOP

AOP前奏

这里写图片描述
需求1-日志:在程序执行期间追踪正在发生的活动
需求2-验证:希望计算器只能处理正数的运算
(1)ArithmeticCalculator接口:

package com.sjk.spring.aop;

public interface ArithmeticCalculator {

    public double add(double i,double j);
    public double sub(double i,double j);
    public double mul(double i,double j);
    public double div(double i,double j);

}

(2)ArithmeticCalculator接口的实现类ArithmeticCalculatorImpl

package com.sjk.spring.aop;

public class ArithmeticCalculatorImpl implements ArithmeticCalculator {

    public double add(double i,double j) {
        double result=i+j;
        return result;
    }

    public double sub(double i,double j) {
        double result=i-j;
        return result;
    }

    public double mul(double i,double j) {
        double result=i*j;
        return result;
    }

    public double div(double i,double j) {
        double result=i/j;
        return result;
    }

}

(3)在实现类里直接添加需求日志
这里写图片描述

package com.sjk.spring.aop;

public class ArithmeticCalculatorImpl implements ArithmeticCalculator {

    public double add(double i,double j) {
        System.out.println("日志:The method add begins with ["+i+","+j+"]");
        double result=i+j;
        System.out.println("The method add ends with "+result);
        return result;
    }

    public double sub(double i,double j) {
        System.out.println("日志:The method sub begins with ["+i+","+j+"]");
        double result=i-j;
        System.out.println("The method sub ends with "+result);
        return result;
    }

    public double mul(double i,double j) {
        System.out.println("日志:The method mul begins with ["+i+","+j+"]");
        double result=i*j;
        System.out.println("The method mul ends with "+result);
        return result;
    }

    public double div(double i,double j) {
        System.out.println("日志:The method div begins with ["+i+","+j+"]");
        double result=i/j;
        System.out.println("The method div ends with "+result);
        return result;
    }

}

由以上代码可知,在实现业务需求的代码中直接添加日志会出现如下一些问题:

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

可以使用动态代理方法解决上述问题:

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

这里写图片描述
动态代理类:

package com.sjk.spring.aop;

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

public class ArithmeticCalculatorProxy {

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

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

    public ArithmeticCalculator getProxy() {
        ArithmeticCalculator proxy;
        //代理对象由哪一个类加载器负责加载
        ClassLoader loader=target.getClass().getClassLoader();
        //代理对象类型,即其中有哪些方法
        Class [] interfaces=new Class[] {ArithmeticCalculator.class};
        //当调用代理对象其中的方式时,该执行的代码
        /**
         * poxy:正在返回的那个代理对象,一般在invoke方法中都不使用该对象
         * method:正在被调用的方法
         * args:调用方法时,传入的参数
         */
        InvocationHandler h=new InvocationHandler() {

            @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;
    }

}

测试类:

package com.sjk.spring.aop;

public class Main {

    public static void main(String[] args) {
        ArithmeticCalculator target=new ArithmeticCalculatorImpl();
        //得到target的代理类
        ArithmeticCalculator proxy=new  ArithmeticCalculatorProxy(target).getProxy();
        double result=proxy.add(1, 2);
        System.out.println("result:"+result);
        result=proxy.div(4, 2);
        System.out.println("result:"+result);
    }

}

使用动态代理能解决问题,但一般在开发过程中不使用动态代理来完成上述功能,因为写动态代理代码比较繁琐,而且对程序员的要求也比较高。在实际开发过程中可以使用一种更简单的方式达到同样的效果,这就是AOP。

1、AOP简介

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

2、AOP术语

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

3、AspectJ注解配置AOP

在Spring中配置AOP有两种方式:AspectJ注解和基于XML文件配置AOP。

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

用 AspectJ 注解声明切面:
(1)要在Spring中声明AspectJ切面, 只需要在IOC容器中将切面声明为Bean实例。 当在Spring IOC容器中初始化 AspectJ切面之后, Spring IOC容器就会为那些与 AspectJ切面相匹配的Bean创建代理。
(2)在 AspectJ 注解中, 切面只是一个带有 @Aspect 注解的 Java 类。
(3)通知是标注有某种注解的简单的 Java 方法。
(4)AspectJ 支持 5 种类型的通知注解:

@Before: 前置通知, 在方法执行之前执行
@After: 后置通知, 在方法执行之后执行 
@AfterRunning: 返回通知, 在方法返回结果之后执行
@AfterThrowing: 异常通知, 在方法抛出异常之后
@Around: 环绕通知, 围绕着方法执行

3.1前置通知

前置通知:在方法执行之前执行的通知。
前置通知使用 @Before 注解, 并将切入点表达式的值作为注解值。

配置过程
1.加入jar包
这里写图片描述
2.在xml文件(上图中applicationContext-aop.xml)中加入aop,context命名空间
这里写图片描述
3.基于注解的方式
(1)在xml文件中加入如下配置
<!– 配置自动扫描的包 –>
<context:component-scan base-package=”com.sjk.spring.aop”>
</context:component-scan>
<!– 使AspectJ注解起作用:自动为匹配的类生成代理对象 –>
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
这里写图片描述
(2)把横切关注点的代码抽象到切面的类中
①切面首先是一个IOC中的bean,即加入@component注解
②切面还需要加入@Aspect注解
(3)在类中声明各种通知(在这里只声明前置通知)
①声明一个方法
②在方法面前加@Before注解
这里写图片描述

配置的细节:

  • 利用方法签名编写 AspectJ 切入点表达式:
    ①execution * com.atguigu.spring.ArithmeticCalculator.*(..):
    匹配ArithmeticCalculator中声明的所有方法,第一个 * 代表任意修饰符及任意返回值。第二个 * 代表任意方法。 .. 匹配任意数量的参数。若目标类与接口与该切面在同一个包中, 可以省略包名。
    ②execution public * ArithmeticCalculator.*(..): 匹配ArithmeticCalculator接口的所有公有方法。
    ③execution public double ArithmeticCalculator.*(..): 匹配ArithmeticCalculator中返回double类型数值的方法。
    ④execution public double ArithmeticCalculator.*(double, ..): 匹配第一个参数为 double 类型的方法, .. 匹配任意数量任意类型的参数。
    ⑤execution public double ArithmeticCalculator.*(double, double): 匹配参数类型为 double, double 类型的方法。

  • 在 AspectJ 中, 切入点表达式可以通过操作符 &&, ||, ! 结合起来:

  • 可以在通知方法中声明一个类型为JoinPoint的参数。然后就能访问链接细节。如方法名称和参数值。

3.2后置通知

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

这里写图片描述

3.3返回通知

   
   返回通知:在目标方法正常结束执行之后,执行的通知。
   返回通知可以访问目标方法执行的结果。
 

这里写图片描述

3.4异常通知

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

这里写图片描述

3.5环绕通知

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

这里写图片描述

3.6指定切面的优先级和重用切入点

1.指定切面的优先级

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

这里写图片描述
这里写图片描述

2.重用切入点

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

这里写图片描述
这里写图片描述

4、基于XML文件配置AOP

    除了使用 AspectJ 注解声明切面, Spring也支持在 Bean 配置文件中声明切面。这种声明是通过 aop schema中的XML元素完成的。
    正常情况下, 基于注解的声明要优先于基于XML的声明。通过 AspectJ 注解, 切面可以与 AspectJ 兼容, 而基于XML的配置则是Spring专有的。由于AspectJ得到越来越多的AOP框架支持, 所以以注解风格编写的切面将会有更多重用的机会。
    当使用 XML 声明切面时, 需要在  根元素中导入aop Schema
    在Bean配置文件中, 所有的Spring AOP配置都必须定义在<aop:config> 元素内部。对于每个切面而言, 都要创建一个<aop:aspect> 元素来为具体的切面实现引用后端Bean实例。
    切面Bean必须有一个标示符, 供<aop:aspect>元素引用。

关于基于XML文件配置AOP的方法就不过多介绍。

本博客的内容来自尚硅谷—佟刚老师的课程《Spring4》
本博客中涉及的源码源码

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值