Java -- Spring学习笔记5、AOP面向切面编程

1、AspectJ对AOP的实现

AspectJ 是一个优秀面向切面的框架,它扩展了Java语言,提供了强大的切面实现、实现了AOP的功能,且其实现方式更为简捷,使用更为方便,而且还支持注解式开发。所以、在Spring中使用AOP开发时,一般使用AspectJ的实现方式。

2、AspectJ的通知类型

  • 常用的有五种类型:
    • 前置通知
    • 后置通知
    • 环绕通知
    • 异常通知
    • 最终通知

3、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 表达式中明显就是方法的签名。注意,表达式中黑色文字表示可省略部分,各部分间用空格分开。在其中可以使用以下符号:

  • *:0至多个任意字符
  • ..:用在方法参数中、表示任意参数。用在包名后表示当前包及其子包路径。
  • +:用在类名后、表示当前类及其子类。用在接口后、表示当前接口以及实现类。
  • 举例:
execution(public * *(..))
指定切入点为:任意公共方法。
execution(* set*(..))
指定切入点为:任何一个以“set”开始的方法。
execution(* com.rg.service.*.*(..))
指定切入点为:定义在 service 包里的任意类的任意方法。
execution(* com.rg.service..*.*(..))
指定切入点为:定义在 service 包或者子包里的任意类的任意方法。“..”出现在类名中时,后面必须跟“*”,表示包、子包下的所有类。
execution(* *..service.*.*(..))
指定所有包下的 serivce 子包下所有类(接口)中所有方法为切入点
........................

4、AspectJ的开发环境

  • 添加maven依赖:
<!-- 依赖 -->
<dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>
</dependencies>
<!-- 插件 -->
<build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.1</version>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
        </configuration>
      </plugin>
    </plugins>
</build>

5、AspectJ基于注解的AOP实现

5.1、前置通知@Before -- 目标方法执行之前执行

  • 在dao层定义接口和实现类、如下:
//定义接口
public interface SomeService
{
    void doSome();
}
//实现类
public class SomeServiceImpl implements SomeService
{
    @Override
    public void doSome()
    {
        System.out.println("...........执行了doSome()方法............");
    }
}
  • 定义切面类、添加前置通知增强方法、如下:
/**
 * @Aspect:AspectJ框架的注解、表示当前类是切面类
 */
@Aspect
public class MyAspect
{
    /**
     * @Before:前置通知、也就是在被切入方法之前执行
     * value:表示切入位置、这里的表达式指:任何返回值类型和任何包下的doSome()方法,参数类型和个数也是任意。
     * 位置:方法上边
     */
    @Before(value = "execution(* *..doSome(..))")
    public void before()
    {
        System.out.println("前置通知、在方法前执行........");
    }
}
  • 声明目标类对象和切面类对象、如下:
<!--目标类对象、接口实现类、因为在测试方法中需要调用该类中的业务方法-->
<bean id="someService" class="com.rg.dao.impl.SomeServiceImpl"/>
<!--切面类对象-->
<bean id="myAspect" class="com.rg.aspect.MyAspect"/>
  • 注册 AspectJ的自动代理、如下:
<!--
    定义好切面Aspect后,需要通知Spring容器,让容器生成“目标类+切面”的代理
    对象。代理是由容器自动生成的。只需要在 Spring 配置文件中注册一个基于 aspectj 的
    自动代理生成器,其就会自动扫描到@Aspect注解,并按通知类型与切入点,将其织入,并
    生成代理。

    其工作原理是,<aop:aspectj-autoproxy/>通过扫描找到@Aspect 定义的切面类,再由切
    面类根据切入点找到目标类的目标方法,再由通知类型找到切入的时间点。
-->
<aop:aspectj-autoproxy/>
  • 测试方法:
public class MyTest
{
    @Test
    public void test01()
    {
        //指定spring配置文件
        String config = "applicationContext.xml";
        //创建spring容器对象
        ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
        //从spring容器中获取对象、使用id
        SomeService someService = (SomeService) ctx.getBean("someService");
        //执行对象的业务方法
        someService.doSome();
    }
}
  • 控制台输出:
前置通知、在方法前执行........
...........执行了doSome()方法............

5.2、JoinPoint参数

所有的通知方法均可包含一个JoinPoint类型的参数、该类型的对象本身就是切入点表达式。通过该参数,可获取切入点表达式、方法签名、目标对象等。

  • 对上边接口和测试方法稍作修改:
//添加两个参数
void doSome(String name,int age);
  • 切面方法中通过JoinPoint获取参数信息:
/**
  * @Before:前置通知、也就是在被切入方法之前执行 value:表示切入位置、这里的表达式指:任何返回值类型和任何包下的doSome()方法,参数类型和个数也是任意。
  * 位置:方法上边
*/
@Before(value = "execution(* *..doSome(..))")
public void before(JoinPoint jp)
{
      System.out.println("被织入方法签名:" + jp.getSignature());
      System.out.println("被织入方法中的参数数量:" + jp.getArgs().length);

      Object argList[] = jp.getArgs();
      for (Object arg : argList)
      {
            System.out.println(arg);
      }

      System.out.println("前置通知、在方法前执行........");
}
  • 测试方法调用业务方法:
@Test
    public void test01()
    {
        //指定spring配置文件
        String config = "applicationContext.xml";
        //创建spring容器对象
        ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
        //从spring容器中获取对象、使用id
        SomeService someService = (SomeService) ctx.getBean("someService");
        //执行对象的业务方法
        someService.doSome("Jack",18);
    }
  • 控制台输出:
被织入方法签名:void com.rg.dao.SomeService.doSome(String,int)
被织入方法中的参数数量:2
Jack
18
前置通知、在方法前执行........
...........执行了doSome()方法............

5.3、后置通知@AfterReturning -- 目标方法执行之后执行

因为在方法后执行,所以可以获取方法的返回值信息、该注解有一个returning属性、就是用于指定接收方法返回值的变量名的、也就是说、在增强方法的参数列表中、除了可以包含JoinPoint参数外,还可以包含用于接收返回值的变量。该变量最好为Object类型,因为目标方法的返回值可能是任何类型。

  • 在接口中添加方法、如下:
public interface SomeService
{
    void doSome(String name,int age);
    String getName();
}
//实现类中实现该方法
@Override
    public String getName()
    {
        System.out.println("...........执行了getName()方法............");
        return "大卫";
    }
  • 在切面类中添加后置通知增强方法、如下:
/**
     * 后置通知
     * @param name:目标方法返回值
     */
    @AfterReturning(value = "execution(* *..getName(..))", returning = "name")
    public void after(Object name)
    {
        System.out.println("后置通知、在目标方法执行后执行、目标方法返回值是:" + name);
    }
  • 测试方法:
@Test
    public void test01()
    {
        //指定spring配置文件
        String config = "applicationContext.xml";
        //创建spring容器对象
        ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
        //从spring容器中获取对象、使用id
        SomeService someService = (SomeService) ctx.getBean("someService");
        //执行对象的业务方法
        someService.getName();
    }
  • 控制台输出:
...........执行了getName()方法............
后置通知、在目标方法执行后执行、目标方法返回值是:大卫

5.4、环绕通知@Around -- 目标方法执行之前之后执行

环绕通知、就是将目标方法拿到切面方法中、在增强方法中获取到的目标方法上边和下边加上需要的内容。方法有一个 ProceedingJoinPoint类型的参数、调用proceed()方法,也就是执行目标方法、若目标方法有返回值,则该方法的返回值就是目标方法的返回值。

  • 在接口中添加方法、如下:
public interface SomeService
{
    void doSome(String name,int age);
    String getName();
    String getAddress();
}
@Override
public String getAddress()
{
    System.out.println("...........执行了getAddress()方法............");
    return "中国上海";
}
  • 在切面类中添加环绕通知增强方法:
@Around(value = "execution(* *..getAddress(..))")
    public Object around(ProceedingJoinPoint pj) throws Throwable
    {
        Object res = null;
        System.out.println("前置功能.....");
        //proceed():目标方法、并接收目标方法的返回值
        res = pj.proceed();
        System.out.println("后置功能.....");
        return res;
    }
  • 测试方法:
@Test
    public void test01()
    {
        //指定spring配置文件
        String config = "applicationContext.xml";
        //创建spring容器对象
        ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
        //从spring容器中获取对象、使用id
        SomeService someService = (SomeService) ctx.getBean("someService");
        //执行对象的业务方法
        Object res = someService.getAddress();
        System.out.println(res);
    }
  • 控制台输出:
前置功能.....
...........执行了getAddress()方法............
后置功能.....
中国上海

5.5、@Pointcut定义切入点

如果一个execution切入点表达式可以被多个通知增强方法使用、就可以使用AspectJ提供@Pointcut注解,用于定义execution 切入点表达式、实现代码复用。

其用法是、将@Pointcut注解在一个方法之上,那么所有的 execution的value属性值均可使用该方法名作为切入点。这个使用@Pointcut注解的方法一般使用private,里边也不写代码、没用实际作用。

  • 对环绕通知的切面类中代码稍作修改、如下:
/**
     * myCut():下边的方法名就是myCut()
     * @param pj
     * @return
     * @throws Throwable
     */
    @Around(value = "myCut()")
    public Object around(ProceedingJoinPoint pj) throws Throwable
    {
        Object res = null;
        System.out.println("前置功能.....");
        //proceed():目标方法、并接收目标方法的返回值
        res = pj.proceed();
        System.out.println("后置功能.....");
        return res;
    }

    @Pointcut(value = "execution(* *..getAddress(..))")
    private void myCut()
    {
      //不写代码
    }
  • 测试方法同上、输出结果、如下:
前置功能.....
...........执行了getAddress()方法............
后置功能.....
中国上海

当然、不光有前置、后置、环绕、还有异常通知、最终通知,但是都不常用,了解就行。

原文地址:https://www.cnblogs.com/dcy521/p/14770869.html