spring框架入门之切面编程AOP

2021-05-15 10:21:00 Author: juejin.cn
觉得文章还不错?,点我收藏



这篇文章主要讲解的spring中AOP代理相关的内容,涉及AOP的基本概念,以及spring中AOP相关的术语,通过xml配置和注解配置同时实现AOP配置。

关于代理的理解:拥有被代理对象的全部方法,同时对被代理的对象进行方法的扩展(增强),代理对象可以实现更多的功能。

AOP 全称是Aspect Oriented Programming (面向切面编程)

主要功能:对业务逻辑的各个部分进行隔离,从而使得业务逻辑各个部分得耦合度降低,提高程序的可重用性。简单的讲,就是将程序中的重复代码进行抽取,在需要执行的地方使用动态代理技术,在不修改源代码的基础上,对现有方法进行增强。

主要优点:减少重复代码 提高开发效率 维护方便

1. 动态代理

特点:字节码随用随创建,随用随加载

作用:在不修改源码的基础上对方法进行增强

分类:

 基于接口的动态代理基于子类的动态代理
涉及的类ProxyEnhancer
提供者JDK官方第三方库cglib
如何创建代理对象使用Proxy中的 newProxyInstance 方法使用Enhancer类中的create方法
创建代理对象的要求被代理类至少实现一个接口,没有则不能使用被代理对象不能是最终类

1.1 基于接口的动态代理

newProxyInstance方法的参数:

  • ClassLoader : 类加载器。用于加载代理对象字节码,和被代理类使用相同的类加载器。

    写法: 代理对象.getClass().getClassLoader()

  • Class[] : 字节码数组。用于让代理对象和被代理对象有相同方法。

    写法: 代理对象.getClass().getInterfaces()

  • InvocationHandler:用于增强的代码。书写对被代理方法增强的代码,一般书写此接口的实现类,通常情况下是匿名内部类,但不是必须的,此接口的实现类一般谁用到谁写。

  • InvocationHandler参数中的invoke方法,执行被代理对象的任何接口方法都会经过该方法。方法参数及其含义:

    • proxy :代理对象的引用
    • method :当前执行的方法
    • args:当前执行方法所需的参数
    • 返回值:与被代理类有相同的返回值

代码示例:

public class Client {
    public static void main(String[] args) {
        final ProducerImpl producer = new ProducerImpl();

        producer.saleProduct(1000f);// 销售产品,拿到钱1000.0
        System.out.println("对方法进行增强后。。。。。");
        Producer proxyProduct = (Producer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
                producer.getClass().getInterfaces(),
                new InvocationHandler() {

                    /**
                     * 执行被代理对象的任何接口方法都会经过该方法
                     * 方法的参数含义
                     * @param proxy  代理对象的引用
                     * @param method 当前执行方法
                     * @param args   当前执行方法所需的参数
                     * @return       和被代理对象有相同的返回值
                     * @throws Throwable
                     */
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // 提供增强的代码
                        Object returnValue = null;
                        // 1.获取方法的执行参数
                        Float money = (Float) args[0];

                        // 2.判断当前方法是不是销售方法
                        if ("saleProduct".equals(method.getName())){
                            returnValue = method.invoke(producer, money * 0.8f);
                        }
                        return returnValue;
                    }
                });
        proxyProduct.saleProduct(1000f);// 销售产品,拿到钱800.0
    }
}


复制代码

1.2 基于子类的动态代理

create 方法的参数:

  • Class:字节码。用于指定被代理对象的字节码。
  • Callback:用于提供增强的代码,类似于基于接口的动态代理的invoke方法。一般写的是该接口的子接口实现类 MethodInterceptor
  • create 参数中 MethodInterceptor 的方法参数:
    • o :代理对象的引用
    • method :当前执行的方法
    • objects:当前执行方法所需的参数
    • methodProxy:当前执行方法的代理对象

代码示例:

public class Client {

    final Producer producer = new Producer();

    public static void main(String[] args) {

        final Producer producer = new Producer();

        producer.saleProduct(1000f);// 售卖商品,得到钱1000.0
        System.out.println("对方法进行增强后。。。。。");
        Producer cglibProducer = (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
            /**
             * 执行任何被处理对象的任何方法都会经过该方法
             * @param o           代理对象的引用
             * @param method      当前的执行方法
             * @param objects     当前执行方法所需的参数
             * @param methodProxy 当前执行方法的代理对象
             * @return
             * @throws Throwable
             */
            @Override
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                // 提供增强的方法
                Object returnValue = null;

                // 1.获取当前方法的执行参数
                Float money = (Float) objects[0];
                // 2.判断当前的方法是不是销售动作
                if ("saleProduct".equals(method.getName())){
                    returnValue = method.invoke(producer, money * 0.8f);
                }
                return returnValue;
            }
        });
        cglibProducer.saleProduct(1000f);// 售卖商品,得到钱800.0
    }
}


复制代码

1.3 动态代理总结

动态代理的一般使用方式:

  • 获取被代理类对象(被代理对象的字节码、被代理类对象的类加载器等信息)
  • 在代理类提供的方法中对被代理类中的方法进行增强

2. spring中的AOP

spring中的AOP是通过配置的方式实现动态代理

2.1 spring中的相关术语:

Joinpoint 连接点指被拦截到的点。在spring中这些点指的是方法,因为spring只支持方法类型的连接点。可以理解为业务层中所有的方法。

Pointcut 切入点指需要对那些Joinpoint进行拦截的定义。可以理解为被增强的方法。

Advice 通知/增强指拦截到Joinpoint后需要做的事情。通知类型:前置通知,后置通知,异常通知,最终通知,环绕通知。

  • 前置通知:在执行业务层方法前的通知;
  • 后置通知:在执行业务层方法后的通知;
  • 异常通知:catch 中的通知;
  • 最终通知:在 finally 中的通知;
  • 环绕通知:整个 invoke 方法执行就是环绕通知;

image-20200821090205755

Introduction 引介一种特殊的通知在不修改类代码的前提下,Introduction可以在运行期为类动态的添加一些方法或Field。

Target 目标对象:代理的目标对象。

Weaving 织入指把增强应用到目标对象来创建代理对象的过程。spring是动态代理织入的,而AspectJ采用编译期织入和类装载期织入。

Proxy 代理一个类被AOP织入增强后,就产生一个结果代理类。

Aspect 切面是切入点和通知(引介)的结合。

2.2 spring中AOP

开发阶段:

编写核心业务代码(主线开发,熟悉业务代码即可进行开发)

把公共代码提取出来,制作成通知。(开发最后阶段)

在配置文件中声明切入点与通知之间的关系,即切面。

运行阶段:

spring框架监控切入点的方法执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。

spring中的AOP会根据目标是否实现了接口来决定采用哪种动态代理的方式

3.基于XML的AOP配置

3.1 将通知类交由IoC容器管理

将通知类注册到spring的IoC容器中

<bean id="" class="">
	<property name="" ref=""></property>
</bean>


复制代码

3.2 使用 <aop:config> 标签进行AOP配置

用于声明aop配置

<aop:config>
	<!-- 配置的代码写在此处 -->
</aop:config>


复制代码

3.3 使用 <aop:aspect> 配置切面

用于配置切面

属性:

① id属性:是给切面提供一个唯一标识

② ref属性:是指定通知类bean的Id。

<aop:aspect id="" ref="">
    <!-- 在这里配置通知类型 -->
</aop:aspect>
复制代码
  • <aop:before>:用于配置前置通知

  • <aop:after-return>:用于配置后置通知

  • <aop:after-throwing>:用于配置异常通知

  • <aop:after>:用于配置最终通知

  • <aop: around>:用于配置环绕通知

    ① method属性:用于指定Logger类中哪个方法是前置通知

    ② pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强

    ③ pointcut-ref属性:用于指定切入点表达式的id

3.4 使用 <aop:pointcut> 配置切入点表达式

用于配置切入点表达式,就是指定对那些类进行的那些方法进行增强

属性:

① id属性:用于指定切入点的唯一标识

② expression属性:用于配置切入点表达式

<aop:pointcut id="" expression="execution()"/>
复制代码

代码示例:

<!-- 配置service对象 -->
<bean id="accountService" class="cn.bruce.service.impl.AccountServiceImpl"></bean>
<bean id="testService" class="cn.bruce.service.impl.TestServiceImpl"></bean>


<!-- 配置Logger类 -->
<bean id="logger" class="cn.bruce.utils.Logger"></bean>

<!-- 配置AOP -->
<aop:config>
    <!-- 配置切面 -->
    <aop:aspect id="logAdvice" ref="logger">
        <!-- 配置通知类型,建立通知方法和切入点方法的关联 -->
        <aop:before method="printLog" pointcut="execution(* cn.bruce.service.impl.*.*(..))"></aop:before>
        <aop:after method="printLog" pointcut="execution(* cn..impl.Test*.*(cn.bruce.domain.Account))"></aop:after>
    </aop:aspect>
</aop:config>
复制代码

4. 切入点表达式

关键字:execution("表达式")

表达式写法:访问修饰符 返回值 包名.***.包名.类名.方法名(参数列表)

标准写法:public void cn.bruce.service.impl.AccountServiceImpl.saveAccount()

  • 访问修饰符可以省略(访问权限不能写 *),表示匹配任意类型的访问权限,但Spring现在只支持public权限;

    void cn.bruce.service.impl.AccountServiceImpl.saveAccount()

  • 返回值可以使用通配符,表示任意返回值;

    * cn.bruce.service.impl.AccountServiceImpl.saveAccount()

  • 包名可以使用通配符,表示任意包,有几级包就要写几个 *

    * *.*.*.*.AccountServiceImpl.saveAccount()

  • 包名可以使用 .. 表示当前包及其子包

    * cn..AccountServiceImpl.saveAccount()

  • 类名和方法名都可以使用通配符代替

    * *..*.*()

**参数列表:**直接写数据类型

  • 基本数据类型直接写名称,如:int long double boolean
  • 引用数据类型要写全类名,如:cn.bruce.domain.Accout
  • 可以使用通配符 * 表示任意类型,但是必须有参数
  • 可以使用通配符 * 进行占位,如:* *..*.*(*, int)
  • 可以使用 .. 表示有无参数均可,有参数可以是任意类型 * *..*.*(..)

全通配写法:* *..*.*(..)

开发中切入点表达式的通常写法:如:切到业务层实现类下的所有方法 * cn.bruce.service.impl.*.*(..)

5. 常用通知类型

前置通知 <aop:before>:在切入点方法执行之前执行

后置通知 <aop:after-returning>:在切入点方法执行之后执行。后置通知和异常通知永远只能执行一个

异常通知 <aop:after-throwing>:在切入点方法执行产生异常后执行。异常通知和后置通知永远只能执行一个

最终通知 <aop:after>:无论切入点方法是否正常执行,它都会在其后面执行

环绕通知 <aop:around>:是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。

代码示例:

<!-- 配置AOP -->
<aop:config>
    <!-- 配置切入点表达式,id属性是表达式的唯一标识,expression属性用于指定表达式内容
             此标签可以写在aop:aspect标签内部只能当前切面使用
             写在aop:aspect外面,此时表示所有的切面可用
         -->
    <aop:pointcut id="loggerPointCut" expression="execution(* cn..impl.Account*.*(..))"/>

    <!-- 配置切面 -->
    <aop:aspect id="logAdvice" ref="logger">
        <!-- 前置通知:在切入点方法执行之前执行 -->
        <aop:before method="beforePrintLog" pointcut-ref="loggerPointCut"></aop:before>

        <!-- 后置通知:在切入点方法执行之后执行。后置通知和异常通知永远只能执行一个 -->
        <aop:after-returning method="afterReturningPrintLog" pointcut-ref="loggerPointCut"></aop:after-returning>

        <!-- 异常通知:在切入点方法执行产生异常后执行。异常通知和后置通知永远只能执行一个 -->
        <aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="loggerPointCut"></aop:after-throwing>

        <!-- 最终通知:无论切入点方法是否正常执行,它都会在其后面执行 -->
        <aop:after method="afterPrintLog" pointcut-ref="loggerPointCut"></aop:after>

        <!-- 环绕通知:是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。 -->
        <aop:around method="aroundPrintLog" pointcut-ref="loggerPointCut"></aop:around>
    </aop:aspect>
</aop:config>


复制代码

6. 基于注解的AOP配置

配置步骤:

①导入maven坐标

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.8.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.6</version>
    </dependency>
</dependencies>


复制代码

② 书写spring配置类,开启包扫描和注解支持

@configuration
@ComponentScan("cn.bruce") // 开启包扫描,配置需要扫描的包
@EnableAspectJAutoProxy(proxyTargetClass = true) // 开启注解驱动
public class SpringConfiguration {
}


复制代码

③ 将业务层实体类交由IoC容器管理

@Service("testService")
public class TestServiceImpl implements TestService {

    @Override
    public void testOfVoid() {
        System.out.println("testOfVoid is running......");
    }

    @Override
    public void testOfInt(int i) {
        System.out.println("testOfInt is running......number is" + i);
    }

    @Override
    public void testOfInteger(Integer i) {
//        i = 1/0;
        System.out.println("testOfInteger is running......number is" + i);

    }

    @Override
    public void testOfAccount(Account account) {
        int i = 1/0;
        System.out.println("testOfInt is running......number is" + account);

    }
}


复制代码

④ 书写切面类,声明为切面类并设置切入点和通知类型

@Component("logger")
@Aspect // 表示此类为切面类
public class Logger {

    @Pointcut("execution(* cn..impl.*.*(..))") // 指定切入点表达式
    private void pointcut(){}

    /**
     * 前置通知
     */
    @Before("execution(* cn..impl.*.*(int))")
    public  void beforePrintLog(){
        System.out.println("前置通知Logger类中的beforePrintLog方法开始记录日志了。。。");
    }

    /**
     * 后置通知
     */
    @AfterReturning("execution(* cn..impl.*.*(Integer))")
    public  void afterReturningPrintLog(){
        System.out.println("后置通知Logger类中的afterReturningPrintLog方法开始记录日志了。。。");
    }
    /**
     * 异常通知
     */
    @AfterThrowing("pointcut()")
    public  void afterThrowingPrintLog(){
        System.out.println("异常通知Logger类中的afterThrowingPrintLog方法开始记录日志了。。。");
    }

    /**
     * 最终通知
     */
    @After("execution(* cn..impl.*.*())")
    public  void afterPrintLog(){
        System.out.println("最终通知Logger类中的afterPrintLog方法开始记录日志了。。。");
    }

    /**
     * 环绕通知
     */
    @Around("execution(* cn..impl.*.*(cn.bruce.domain.Account))")
    public Object aroundPringLog(ProceedingJoinPoint pjp){
        Object rtValue = null;
        try{
            //得到方法执行所需的参数
            Object[] args = pjp.getArgs();

            System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。前置");

            //明确调用业务层方法(切入点方法)
            rtValue = pjp.proceed(args);

            System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。后置");

            return rtValue;
        }catch (Throwable t){
            System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。异常");
            throw new RuntimeException(t);
        }finally {
            System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。最终");
        }
    }

}


复制代码

⑤ 书写测试类进行测试

public class TestAOP {
    public static void main(String[] args) {
        ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);
        TestService testService = (TestService) ac.getBean("testService");
        testService.testOfInt(133);
        System.out.println("-----------");
        testService.testOfInteger(112);
        System.out.println("-----------");
        testService.testOfVoid();
        System.out.println("-----------");

        Account account = (Account) ac.getBean("account");
        account.setName("Bruce");
        account.setAge(112);
        testService.testOfAccount(account);
    }
}


复制代码

关于注解这部分还有很多内容需要补充,后续会开个坑继续写的O(∩_∩)O




觉得文章还不错?,点我收藏



如果文章侵犯到您的版权,请联系我:buaq.net[#]pm.me