3.AOP面向切面编程

Posted 小马Mark

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了3.AOP面向切面编程相关的知识,希望对你有一定的参考价值。

动态代理

动态代理的实现方式常用的有两种:使用JDK的Proxy与通过CGLIB生成代理。

动态代理的作用:

  • 在目标类源代码不改变的情况下,增强功能
  • 减少代码的重复
  • 专注业务逻辑代码
  • 解耦合,让你的业务功能和日志,事务非业务功能分离。

JDK动态代理

jdk动态代理要求目标对象必须实现接口,这是java设计上的要求。

从jdk1.3以来,java语言通过java.lang.reflect包提供三个类支持代理模式Proxy,Method和 InovcationHandler。

CGLIB动态代理

CGLIB(Code Generation Library)是一个开源项目。是一个强大的,高性能,高质量的Code 生成类库,它可以在运行期扩展Java类与实现Java接口。它广泛的被许多AOP的框架使用,例如Spring AOP。

cglib是第三方的工具库,创建代理对象。

cglib的原理是继承,cglib通过继承目标类,创建它的子类,在子类中重写父类中同名的方法,实现功能的修改。

CGLIB代理的生成原理是生成目标类的子类,而子类是增强过的,这个子类对象就是代理对象。所以,使用CGLIB生成动态代理要求自标类必须能够被继承,即不能是 final的类。

对比:

  • 使用JDK的 Proxy实现代理,要求目标类与代理类实现相同的接口。若目标类不存在接口,则无法使用该方式实现
  • 对于无接口的类,要为其创建动态代理,就要使用CGLIB来实现。
  • cglib经常被应用在框架中,例如Spring ,Hibernate,mybatis等。
  • cglib 的代理效率高于jdk。对于cglib一般的开发中并不使用。

AOP概述

AOP简介

AOP(Aspect Orient Programming),面向切面编程。面向切面编程是从动态角度考虑程序运行过程。

AOP 底层,就是采用动态代理模式实现的。采用了两种代理:JDK 的动态代理,与 CGLIB的动态代理

AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程,可通过运行期动态代理实现程序功能的统一维护的一种技术。

AOP 是 Spring 框架中的一个重要内容。利用 AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

面向切面编程,就是将交叉业务逻辑封装成切面,利用 AOP 容器的功能将切面织入到主业务逻辑中。所谓交叉业务逻辑是指,通用的、与主业务逻辑无关的代码,如安全检查、事务、日志、缓存等。

若不使用 AOP,则会出现代码纠缠,即交叉业务逻辑与主业务逻辑混合在一起。这样,会使主业务逻辑变的混杂不清。例如,转账,在真正转账业务逻辑前后,需要权限控制、日志记录、加载事务、结束事务等交叉业务逻辑,而这些业务逻辑与主业务逻辑间并无直接关系。但,它们的代码量所占比重能达到总代码量的一半甚至还多。它们的存在,不仅产生了大量的“冗余”代码,还大大干扰了主业务逻辑——转账。

面向切面编程的好处

  1. 减少重复;
  2. 专注业务;
  3. 注意:面向切面编程只是面向对象编程的一种补充。

AOP编程术语

  1. 切面(Aspect)

    切面泛指交叉业务逻辑。事务处理、日志处理就可以理解为切面。

    常用的切面是通知(Advice),实际就是对主业务逻辑的一种增强。

  2. 连接点(JoinPoint)

    连接点指可以被切面织入的具体方法。

    通常业务接口中的方法均为连接点。

  3. 切入点(Pointcut)

    切入点指声明的一个或多个连接点的集合。通过切入点指定一组方法。

    被标记为 final 的方法是不能作为连接点与切入点的。因为最终的是不能被修改的,不能被增强的。

  4. 目标对象(Target)

    目标对象指将要被增强的对象。即包含主业务逻辑的类的对象。

  5. 通知(Advice)

    通知表示切面的执行时间,Advice 也叫增强。

    换个角度来说,通知定义了增强代码切入到目标代码的时间点,是目标方法执行之前执行,还是之后执行等。

    通知类型不同,切入时间不同。切入点定义切入的位置,通知定义切入的时间。

AspectJ对AOP的实现

对于 AOP 这种编程思想,很多框架都进行了实现。Spring 就是其中之一,可以完成面向切面编程。然而,AspectJ 也实现了 AOP 的功能,且其实现方式更为简捷,使用更为方便,而且还支持注解式开发。所以,Spring 又将 AspectJ 的对于 AOP 的实现也引入到了自己的框架中。

在 Spring 中使用 AOP 开发时,一般使用 AspectJ 的实现方式。

AspectJ 简介

AspectJ 是一个优秀面向切面的框架,它扩展了 Java 语言,提供了强大的切面实现。

AspectJ全称是Eclipse AspectJ

官网地址:http://www.eclipse.org/aspectj/

a seamless aspect-oriented extension to the Javatm programming language(一种基于 Java 平台的面向切面编程的语言)
Java platform compatible(兼容 Java 平台,可以无缝扩展)
easy to learn and use(易学易用)

AspectJ框架实现AOP有两种方式:

  • 使用xml配置文件:配置全局事务
  • 使用注解:在项目中要用AOP功能,一般都使用注解,AspectJ常用5个注解。

AspectJ框架的使用

AspectJ的通知类型

切面的执行时间,这个执行时间在规范中叫做 Advice(通知,增强)。

AspectJ中常用的通知有5种类型:(常使用注解,也可以使用xml)

  1. 前置通知:@Before
  2. 后置通知:@AfterReturning
  3. 环绕通知:@Around
  4. 异常通知:@AfterThrowing
  5. 最终通知:@After

用法见实现步骤

AspectJ 的切入点表达式

AspectJ 定义了专门的表达式用于指定切入点。

表达式的原型是:

execution(modifiers-pattern? 
		  ret-type-pattern 
		  declaring-type-pattern?name-pattern(param-pattern) 
		  throws-pattern?)

解释:
	modifiers-pattern] 访问权限类型
	ret-type-pattern 返回值类型
	declaring-type-pattern 包名类名
	name-pattern(param-pattern) 方法名(参数类型和参数个数)
	throws-pattern 抛出异常类型
	?表示可选的部分

以上表达式共 4 个部分。

语法: execution(访问权限? 方法返回值 包名类名?.方法声明(参数) 异常类型?)

切入点表达式要匹配的对象就是目标方法的方法名。execution 表达式中明显就是方法的签名。

在其中可以使用以下符号:
在这里插入图片描述

举例:

execution(public * *(..)) 
指定切入点为:任意公共方法。

execution(* set*(..)) 
指定切入点为:任何一个以“set”开始的方法。

execution(* com.xyz.service.*.*(..)) 
指定切入点为:定义在 service 包里的任意类的任意方法。

execution(* com.xyz.service..*.*(..))
指定切入点为:定义在 service 包或者子包里的任意类的任意方法。“..”出现在类名中时,后面必须跟“*”,表示包、子包下的所有类。

execution(* *..service.*.*(..))
指定所有包下的 serivce 子包下所有类(接口)中所有方法为切入点

AspectJ的aop实现步骤

使用aop的目的是在不改变原来的类和代码前提下,给已经是存在的一些类和方法,增加额外的功能。

基本步骤:

  1. 新建maven项目

  2. 加入依赖:spring依赖、aspectj依赖、Junit单元测试依赖

  3. 创建目标类:接口和实现类(将给类的方法增加功能)

  4. 创建切面类:普通类

    • 在类的上面加入@Aspect
    • 在类中定义方法,方法就是切面要执行的功能代码
    • 在方法上面加入aspect中的通知(Advice)注解,例如:@Before等,还有需要指定切入点表达是execution()
  5. 创建spring配置文件:声明对象、把对象交给容器管理。

    (声明对象可以用注解或者xml文件的<bean>标签)

    • 声明目标对象
    • 声明切面类对象
    • 声明aspect框架中的自动代理生成器标签。(自动代理生成器:用来完成代理对象的自动创建功能)

补充:

  • 在定义好切面 Aspect 后,需要通知 Spring 容器,让容器生成“目标类+ 切面”的代理对象。这个代理是由容器自动生成的。只需要在 Spring 配置文件中注册一个基于 aspectj 的自动代理生成器,其就会自动扫描到@Aspect 注解,并按通知类型与切入点,将其织入,并生成代理。
  • <aop:aspectj-autoproxy/>的底层是由 AnnotationAwareAspectJAutoProxyCreator 实现的。从其类名就可看出,是基于 AspectJ 的注解适配自动代理生成器。
    • 其工作原理是,<aop:aspectj-autoproxy/>通过扫描找到@Aspect 定义的切面类,再由切
      面类根据切入点找到目标类的目标方法,再由通知类型找到切入的时间点。
  1. 创建测试类,从spring容器中获取目标对象(实际就代理对象),通过代理执行方法,实现aop的功能增强。

示例:

导入依赖:

<!-- 单元测试 -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
    <scope>test</scope>
</dependency>

<!--  spring框架依赖  -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.5.RELEASE</version>
</dependency>
<!--  aspect依赖  -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.2.5.RELEASE</version>
</dependency>

创建一个目标类接口和目标实现类:

package com.maj.ba01;


// 目标类接口
public interface SomeService {
    void doSome(String name, Integer age);
}
package com.maj.ba01;


// 目标类
public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome(String name, Integer age) {


        System.out.println("-------目标方法doSome()-------");
    }
}

创建一个切面类:

package com.maj.ba01;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

import java.util.Date;

@Aspect  // 这是aspect框架中的注解,用来表示当前类是切面类,增强功能的类
public class MyAspect {
    /*
    * 定义方法:用来实现切面功能的
    * 定以方法的要求:
    *       1.公共方法 public
    *       2.方法没有返回值 void
    *       3.方法名自定义
    *       4.方法可以无参数,可以有参数
    *           若有参数,参数不是自定义的,有几个参数可以使用
    *
    * */

    /*
    * @Before: 前置通知注解
    *   属性:value="execution(....)" 切入点表达式
    * */
    @Before(value = "execution(public void com.maj.ba01.SomeServiceImpl.doSome(String, Integer))")
    public void myBefore(){
        // 切面要执行的功能代码
        System.out.println("前置通知,在目标方法之前输出执行时间:"+ new Date());

    }
}

spring配置文件:

<?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
       https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--  声明目标类对象  -->
    <bean id="someService" class="com.maj.ba01.SomeServiceImpl" />

    <!--  声明切面类对象  -->
    <bean id="myAspect" class="com.maj.ba01.MyAspect" />

    <!--
          声明自动代理生产器:
                使用aspect框架内部的功能,创建目标对象的代理对象。
                创建代理对象是在内存中实现的,修改目标对象在内存中的结构。
                所以,目标对象就是被修改后的代理对象。
          aspectj-autoproxy: 会把spring容器中的所有的目标对象一次性都生成代理对象
      -->

    <aop:aspectj-autoproxy />
    <!-- <aop:aspectj-autoproxy proxy-target-class="true" /> -->
    <!-- 
		proxy-target-class属性:
			
		默认不写时,则是有接口的实现类用jdk动态代理,无接口就用cglib动态代理
		proxy-target-class属性值为true,则是强制使用cglib动态代理 

	-->
    
</beans>

测试类:

package com.maj;

import com.maj.ba01.SomeService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MyText01 {

    @Test
    public void text01(){
        String config = "applicationContext.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(config);

        // 从容器中获取目标对象(这里虽然看着是拿到了目标对象,实际是代理对象)
        SomeService proxy = (SomeService) ac.getBean("someService");
        // 通过代理对象执行方法,实现目标方法执行时就增强了功能
        proxy.doSome("何鼎东",3);
    }
}

运行结果:

前置通知,在目标方法之前输出执行时间:Wed Jul 14 22:23:18 CST 2021
-------目标方法doSome()-------

前置通知

注解:@Before

  • 在目标方法执行之前执行。
  • 被注解为前置通知的方法,可以包含一个 JoinPoint 类型参数。
    • 用来获取要加入切面功能的目标方法(例如,doSome(String name, Integer age)方法,可以获取其中的所有相关信息)
    • 该类型的对象本身就是切入点表达式。
    • 通过该参数,可获取切入点表达式、方法签名(定义)、 目标对象等。
    • JoinPoint参数的值是由框架赋予,且必须放在参数中第一个位置
  • 不光前置通知的方法,可以包含一个 JoinPoint 类型参数,所有的通知方法均可包含该参数。
package com.maj.ba01;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

import java.util.Date;

@Aspect 
public class MyAspect {

    @Before(value = "execution(public void com.maj.ba01.SomeServiceImpl.doSome(String, Integer))")
    public void myBefore(JoinPoint jp){  
        // 获取方法的完整定义
        System.out.println("方法的签名(定义):" + jp.getSignature());
        System.out.println("方法的名称:" + jp.getSignature().getName());
        
        // 获取方法的实参
        Object[] args = jp.getArgs();
        for (Object arg:args){
            System.out.println("参数值:" + arg);
        }

        // 切面要执行的功能代码
        System.out.println("前置通知,在目标方法之前输出执行时间:"+ new Date());

    }
}

后置通知

注解:@afterReturning

  • 在目标方法执行之后执行。

  • 由于是目标方法之后执行,所以可以获取到目标方法的返回值。

  • 该注解的 returning 属性就是用于指定接收方法返回值的变量名的。

  • 被注解为后置通知的方法,除了可以包含 JoinPoint 参数外,还可以包含用于接收返回值的变量。该变
    量最好为 Object 类型,因为目标方法的返回值可能是任何类型。

示例:

package com.maj.ba02;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;

@Aspect  // 这是aspect框架中的注解,用来表示当前类是切面类,增强功能的类
public class MyAspect {

    /*
     *  后置通知定义方法:
     *      方法定义的要求:
     *          1. 公共方法public
     *          2. 方法没有返回值
     *          3. 方法名称自定义
     *          4. 方法有参数的,推荐是Object, 参数名自定义
     */

    /**
     * @AfterReturning: 后置通知
     *      属性:1. value 切入点表达式
     *            2. returning  定义的变量
     *      位置: 在方法定义上面
     *      特点: 1. 在目标方法之后执行的。
     *            2. 能够获取到目标方法的返回值,可以根据这个返回值做不同的处理
     *            3. 可以修改这个返回值
     */

    @AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))", returning = "res")
    public void myAfterReturning(JoinPoint joinPoint, Object res){
        /*
            Object res: 是目标方法执行后的返回值,根据返回值做你的切面的功能处理
                         这里的参数名必须与returning的值相同。
        */

        System.out.println("后置通知:在目标方法之后执行, 获取的目标方法的返回值是:" + res);   
    }
}

环绕通知

注解:@Around

  • 环绕通知就等同于jdk动态代理,(InvocationHandler接口)
  • 在目标方法执行之前之后执行。
  • 被注解为环绕增强的方法要有返回值,Object 类型。并且方法可以包含一个 ProceedingJoinPoint 类型的参数
  • 接口 ProceedingJoinPoint 其有一个proceed()方法,用于执行目标方法。若目标方法有返回值,则该方法的返回值就是目标方法的返回值。
    • ProceedingJoinPoint 是继承的JoinPoint ,所以也可以用后者的方法
  • 环绕增强方法将其返回值返回。
  • 该增强方法实际是拦截了目标方法的执行。
  • 环绕通知经常是用来做事务的

示例:

package com.maj.ba03;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

import java.util.Date;

@Aspect  // 这是aspect框架中的注解,用来表示当前类是切面类,增强功能的类
public class MyAspect {
    /*
    * 环绕通知:用来实现切面功能的
    * 定以方法的要求:
    *       1.公共方法 public
    *       2.方法必须有一个返回值, 推荐使用Object
    *       3.方法名自定义
    *       4.方法有参数,固定的参数:ProceedingJoinPoint
    *
    * */

    /**
     * @Around: 环绕通知
     *      属性:value="切入点表达式"
     *      特点:
     *          1.功能最强的通知
     *          2. 在目标方法的前后都可以增强功能
     *          3.控制目标方法是否被调用执行
     *          4.可以修改原来的目标方法的执行结果,影响最后调用的结果
     *
 *          参数ProceedingJoinPoint的作用:执行目标方法
     *      返回值: 就是目标方法的执行结果,可以被修改。
     */
    @Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
    public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
        Object result = null;
        System.out.println("环绕通知:在目标方法之前做事");

        // 执行目标方法
        result = pjp.proceed();

        System.out.println("环绕通知:在目标方法之后做事");

        // 返回目标方法的返回值(这里可以是修改后的值)
        return result;
    }
}

异常通知

注解:@myAfterTrowing

在目标方法抛出异常后执行。

该注解的 throwing 属性用于指定所发生的异常类对象。

被注解为异常通知的方法可以包含一个参数 Throwable,参数名称为 throwing 指定的名称,表示发生的异常对象。

package com.maj.ba04;

import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;

@Aspect  // 这是aspect框架中的注解,用来表示当前类是切面类,增强功能的类
public class MyAspect {
    /*
    * 异常通知:用来实现切面功能的
    * 定以方法的要求:
    *       1.公共方法 public
    *       2.方法没有返回值 void
    *       3.方法名自定义
    *       4.方法有参数Exception,和JoinPoint
    *
    * */


    /**
     * @myAfterTrowing:异常通知
     *      属性:1.value="切入点表达式"
     *           2.throwing 自定义的变量,表示目标方法抛出的异常对象
     *                      变量名必须与参数名一样
*           特点:
     *           1.在目标方法抛出异常时执行的
     *           2.可以做异常的监控程序,监控目标方法执行时是否有异常
     * @param ex
     */

    @AfterThrowing(value = "execution(* *..SomeServiceImpl.doSecond(..))", throwing = "ex")
    public void myAfterTrowing(Exception ex){
        System.out.println("异常通知:方法发生异常时执行");
        System.out.println("发送信息或者邮件给开发人员,程序有异常了,异常信息:"+ex.getMessage());

    }
}

最终通知

注解:@After

  • 无论目标方法是否抛出异常,该增强均会被执行。
package com.maj.ba05;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;

@Aspect  // 这是aspect框架中的注解,用来表示当前类是切面类,增强功能的类
public class MyAspect {
    /*
    * 最终 通知:用来实现切面功能的
    * 定以方法的要求:
    *       1.公共方法 public
    *       2.方法没有返回值 void
    *       3.方法名自定义
    *       4.方法没有自己特有的参数,只有JoinPoint参数
    *
    * */


    /**
     * doThird:最终通知
     *      属性:value="切入点表达式"
     *      位置:在方法的上面
     * 特点:1. 总是会执行
     *      2. 在目标方法之后执行
     *      3.一般多是做资源清除工作的
     */
    @After(value = "execution(* *..SomeServiceImpl.doThird(..))")
    public void myAfter(){
        System.out.println("执行最终通知:无论是否有异常,总是会被执行");
    }
}

@Pointcut 定义切入点

类似于给切入点表达起别名

当较多的通知增强方法使用相同的 execution 切入点表达式时,编写、维护均较为麻烦。

AspectJ 提供了@Pointcut 注解,用于定义 execution 切入点表达式。

用法:

  • 将@Pointcut 注解在一个方法之上
  • 以后所有的 execution 的 value 属性值均可使用该方法名作为切入点。
  • 代表的就是@Pointcut 定义的切入点。

这个使用@Pointcut 注解的方法一般使用 private 的标识方法,即没有实际作用的方法。

示例:

package com.maj.ba06;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect  // 这是aspect框架中的注解,用来表示当前类是切面类,增强功能的类
public class MyAspect {
    /**
     * doThird:最终通知
     *      属性:value="切入点表达式"
     *      位置:在方法的上面
     * 特点:1. 总是会执行
     *      2. 在目标方法之后执行
     *      3.一般多是做资源清除工作的
     */
    @After(value = "mypt()")
    public void myAfter(){
        System.out.println("执行最终通知:无论是否有异常,总是会被执行");
    }

    @Before(value = "mypt()")
    public void myBefore(){
        System.out.println("前置通知:在目标方法之前执行");
    }

    /**
     *@Pointcut: 定义和管理切入点
     *      属性:1.value="切入点表达式"
     *      位置:在自定义的方法上
     * 特点:1.给切入点表达取别名
     *      2.在其他的通知中,如果想要用这个切入点表达式,直接在value属性的值上调用次方法
     */
    @Pointcut(value = "execution(* *..SomeServiceImpl.doThird(..))")
    private void mypt(){
        // 无需代码
    }
}

以上是关于3.AOP面向切面编程的主要内容,如果未能解决你的问题,请参考以下文章

3.AOP面向切面编程

3.AOP面向切面编程

Spring总结 3.AOP(xml)

面向切面编程(aop)

Spring企业级程序设计 • 第3章 面向切面编程

细聊AOP理论