玩命加载中 . . .

Spring(02)


AOP 面向切面编程

作用

  1. 在目标类不增加代码的情况下,给目标类增加功能。
  2. 减少重复的代码。
  3. 让开发人员更加专注于业务逻辑的实现。
  4. 解耦合:将业务功能和日志、事务等非业务功能解耦。

使用

  1. 当需要修改系统中某个类的功能,原有类的功能不完善,而又没有源代码的情况。
  2. 当你需要给项目中多个类增加相同的功能时。
  3. 给业务方法增加事务、日志输出等功能时。

实现原理

Spring底层实现了两种方式,一种是实现了JDK动态代理方式,另一种是实现了cglib框架(此框架是专门为了实现AOP而做的)。

JDK动态代理

使用的是JDK中的InvocationHandler、Method、Proxy类来创建动态代理从而实现动态代理。

InvocationHandler:实现InvocationHandler下面的invoke方法从而实现动态代理。

Method:使用Method执行目标类中的方法。

Proxy:用于创建代理对象。

注意:使用JDK动态代理需要使用到接口,如果没有接口可以使用cjlib框架,此框架不需要使用接口,底层使用的是继承来实现动态代理。

AspectJ

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

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

AspectJ通知类型

AspectJ 中常用的通知有五种类型。

前置通知

  1. 在目标方法执行之前先执行的。
  2. 不会改变目标方法执行的结果。
  3. 不会影响目标方法的执行。

后置通知

  1. 在目标方法之后执行。
  2. 能够获取到目标方法的返回值,可以根据这个返回值做不同的处理功能。
  3. 可以修改这个返回值(但是不会影响目标方法的最终返回值,只能改变扩展方法中的返回值)。

环绕通知

  1. 它是功能最强的一个通知。
  2. 在目标方法的前和后都能增强功能。
  3. 控制目标方法是否被调用执行。
  4. 修改原来的目标方法的执行结果,影响最后的调用结果。
  5. 此通知类似于JDK底层的动态代理功能。

异常通知

  1. 在目标方法抛出异常时执行的。
  2. 可以做异常的监控程序,监控该方法执行时是不是有异常,如果有异常,可以发送邮件,短信通知。

最终通知

  1. 无论目标方法是否执行成功,通知方法一定会执行。
  2. 在目标方法之后执行的。

AspectJ切入点表达式

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

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

切入点表达式要匹配的对象就是目标方法的方法名,所以execution 表达式中明显就是方法的签名。以上表达式共 4 个部分:execution(访问权限 方法返回值 方法声明(参数) 异常类型)

表达式说明
modifiers-pattern访问权限类型,例(protect、public…)
ret-type-pattern返回值类型
declaring-type-pattern包名类名
name-pattern(param-pattern)方法名(参数类型和参数个数)
throws-pattern抛出异常类型

注意:以上表达式中加粗文字表示不可省略部分,各部分间用空格分开。在其中可以使用以下符号:

符号说明
*0至多个任意字符
..用在方法参数中,表示任意多个参数 用在包名后,表示当前包及其子包路径
+用在类名后,表示当前类及其子类 用在接口后,表示当前接口及其实现类

例子:

execution(public * *(..))
指定切入点为:任意公共方法。
execution(* set*(..))
指定切入点为:任何一个以“set”开始的方法。
execution(* com.xyz.service.*.*(..))
指定切入点为:定义在 service 包里的任意类的任意方法。
execution(* com.xyz.service..*.*(..))
指定切入点为:定义在 service 包或者子包里的任意类的任意方法。“..”出现在类名中时,后
面必须跟“*”,表示包、子包下的所有类。
execution(* *..service.*.*(..))
指定所有包下的 serivce 子包下所有类(接口)中所有方法为切入点
execution(* *.service.*.*(..))
指定只有一级包下的 serivce 子包下所有类(接口) 中所有方法为切入点
execution(* *.ISomeService.*(..))
指定只有一级包下的 ISomeSerivce 接口中所有方法为切入点
execution(* joke(Object+)))
指定切入点为:所有的 joke()方法,方法拥有一个参数,且参数是 Object 类型或该类的子类。
不仅 joke(Object ob)是, joke(String s)和 joke(User u)也是。

Maven相关依赖

<!--相关依赖-->
</dependency>
    <!--Spring依赖-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.3.14</version>
    </dependency>
    <!--aspectj依赖AOP框架-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
      <version>5.3.14</version>
</dependency>

AspectJ 基于注解的 AOP 实现

实现步骤

定义业务接口与实现类

//业务接口
package com.xrebirth.bean01;

public interface SomeService {
    void doSome(String name,Integer age);
}
//业务接口实现类
package com.xrebirth.bean01.impl;
import com.xrebirth.bean01.SomeService;
//目标类
public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome(String name,Integer age) {
        /*
            需求:
            在doSome执行之前增加执行时间
         */
        System.out.println("====目标方法doSome()====");
        
    }
}

定义切面类

package com.xrebirth.bean01;

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

import java.util.Date;

/**
 * @Aspect: 是aspectj框架中的注解.
 *          作用:表示当前类是切面类.
 *          切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
 *          使用位置:在类定义上面
 */
@Aspect
public class MyAspect {
    /**
     * 指定通知方法中的参数
     * 参数类型: JoinPoint (注:JoinPoint中的P为大写)
     * JoinPoint:业务方法要加入切面功能的业务方法.
     *      作用: 可以在通知方法中获取方法执行时的信息,例如实际调用方法的名称及方法的实参
     *            如果切面功能中需要使用实际调用方法中的方法信息就在通知中加入JoinPoint信息
     *            这个JoinPoint参数的值是由框架赋予的,必须是通知方法中第一个参数!
     */
    @Before(value = "execution(public void com.xrebirth.bean01.impl.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("1--前置通知(在目标方法执行之前执行):切面功能:在目标方法之前输出执行时间:" + new Date());
    }
    //注意:这里方法可以创建多个方法。

在Spring配置文件中注册 AspectJ 的自动代理

在定义好切面 Aspect 后,需要通知 Spring 容器,让容器生成“目标类+ 切面”的代理对象。这个代理是由容器自动生成的。只需要在 Spring 配置文件中注册一个基于 aspectj 的自动代理生成器,其就会自动扫描到@Aspect 注解,并按通知类型与切入点,将其织入,并
生成代理。

    <!--将对象交给Spring容器,由Spring容器统一创建.管理对象-->
    <!--声名目标对象-->
    <bean id="someService" class="com.xrebirth.bean08.impl.SomeServiceImpl" />

    <!--声名切面类对象-->
    <bean id="myAspect" class="com.xrebirth.bean08.MyAspect" />

    <!--声名自动代理生成器: 使用aspectj框架内部的功能,创建目标对象的代理对象
        创建代理对象是在内存中实现的,修改目标对象的内存中的结构.创建为代理对象
        所以目标对象就是被修改后的代理对象
    -->
    <!--
        在添加aop:aspectj-autoproxy标签之后idea自动添加xml中的spring-aop.xsd约束文件

        aspectj-autoproxy:会把spring容器中的所有的目标对象一次性都生成代理对象
    -->
    <!--<aop:aspectj-autoproxy />-->

    <!--
        在目标对象有接口情况下使用CGLIB接口
        proxy-target-class:代表即使目标类使用的接口实现也要使用CGLIB接口实现动态代理
    -->
    <aop:aspectj-autoproxy proxy-target-class="true" />

测试方法使用目标对象中的id

package com.xrebirth;
import static org.junit.Assert.assertTrue;
import com.xrebirth.bean01.SomeService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MyTest01 {
    
    @Test
    public void test01() {
        String config = "applicationContext.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(config);
        //从容器中获取目标对象
        SomeService proxy = (SomeService) ac.getBean("someService");
        //通过代理的对象执行方法,实现目标方法执行时增强了功能
        proxy.doSome("张三", 20);
    }
}

具体通知中的详细参数见:spring项目中LearnSpring09-AOP-aspectj模块相关代码

@Pointcut 定义切入点

当较多的通知增强方法使用相同的 execution 切入点表达式时,编写、维护均较为麻烦。AspectJ 提供了@Pointcut 注解,用于定义 execution 切入点表达式。其用法是,将@Pointcut 注解在一个方法之上,以后所有的 execution 的 value 属性值均可使用该方法名作为切入点。代表的就是@Pointcut 定义的切入点。这个使用@Pointcut 注解的方法一般使用 private 的标识方法,即没有实际作用的方法。

//切面类
package com.xrebirth.bean08;
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: 是aspectj框架中的注解.
 *          作用:表示当前类是切面类.
 *          切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
 *          使用位置:在类定义上面
 */
@Aspect
public class MyAspect {
    @Before("pointcut())")
    public void myBefore() {
        System.out.println("前置通知:在方法执行之前执行");
    }
    @After("pointcut()")
    public void myAfter() {
        System.out.println("执行最终通知,一定会被执行");
        //一般用于资源清除工作
    }
    /**
     * @Pointcut: 定义和管理切入点,如果项目中有多个切入点表达式需要管理,则可以使用这个注解是可以复用的
     */
    @Pointcut(value = "execution(* *.doThird(..))")
    public void pointcut() {
        //无需代码
    }
}

文章作者: 小靳同学
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 小靳同学 !
评论
 上一篇
Spring 集成 MyBatis(03) Spring 集成 MyBatis(03)
Spring 集成 MyBatis将 MyBatis 与 Spring 进行整合,主要解决的问题就是将 SqlSessionFactory
2022-01-10
下一篇 
Spring(01) Spring(01)
IOC控制反转概述:Spring 的主要作用就是为代码“解耦”,降低代码间的耦合度。 就是让对象和对象(模块和模块)之间关系不是使用代码关联
2022-01-04
  目录