Spring框架入门41-67:AOP入门

开始时间:2022-01-09
课程链接:动力节点Spring框架教程

AOP 面向切面编程

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

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

面向切面编程,就是将交叉业务逻辑封装成切面,利用 AOP 容器的功能将切面织入到主业务逻辑中。所谓交叉业务逻辑是指,通用的、==与主业务逻辑无关的代码,==如安全检查、事务、日志、缓存等。(做机械的厂、做互联网的厂、做房地产的厂,都会有财务部门,后勤部门,这些和主业务逻辑无关)

面向切面编程的好处

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

术语

  • Aspect:切面,表示增强的功能, 就是一堆代码,完成某个一个功能。非业务功能,
    常见的切面功能有日志, 事务, 统计信息, 参数检查, 权限验证。

  • JoinPoint:连接点 ,连接业务方法和切面的位置。 就某类中的业务方法(例如我们判断是否是我们需要执行的方法的if语句,就是一个连接点)

  • Pointcut : 切入点 ,指多个连接点方法的集合。多个方法

  • 目标对象: 给哪个类的方法增加功能, 这个类就是目标对象

  • Advice:通知,通知表示切面功能执行的时间。

切面三要素

1)切面的功能代码,切面干什么 (增加什么功能)
2)切面的执行位置,使用Pointcut表示切面执行的位置(在哪些业务中增加功能)
3)切面的执行时间,使用Advice表示时间,在目标方法之前,还是目标方法之后。

AOP的实现

aspectJ:

一个开源的专门做aop的框架。spring框架中集成了aspectj框架,通过spring就能使用aspectj的功能。

aspectJ框架实现aop有两种方式:
1.使用xml的配置文件 : 配置全局事务
2.使用注解,我们在项目中要做aop功能,一般都使用注解, aspectj有5个注解。

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

(1)前置通知
(2)后置通知
(3)环绕通知
(4)异常通知
(5)最终通知

  • 切面的执行时间, 这个执行时间在规范中叫做Advice(通知,增强):五种
    1)@Before
    2)@AfterReturning
    3)@Around
    4)@AfterThrowing
    5)@After
    在aspectj框架中使用注解表示的。也可以使用xml配置文件中的标签
  • 切面的执行位置,
    AspectJ 定义了专门的表达式用于指定切入点。表达式的原型是:
execution(modifiers-pattern? ret-type-pattern
declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
execution(访问权限 方法返回值 方法声明(参数) 异常类型)

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

符号意义备注
*0至多个任意字符通配符,和正则表达式差不多
··用在方法参数中,表示任意多个参数,用在包名后,表示当前包及其子路径像是文件夹的表示方法
+用在类名后,表示当前类及其子类;用在接口后,表示当前接口及其实现类

看几个切入点表达式的例子

举例:

execution(public * *(..))

两个空格,说明有三个参数,对应位置找一下
指定切入点为:任意公共方法。

execution(* set*(..))

一个空格,两个参数
指定切入点为:任何一个以“set”开始的方法。

execution(* com.xyz.service.*.*(..))

一个空格,两个参数
指定切入点为:定义在 service 包里的任意类的任意方法。

在Maven中使用AspectJ

ch06-aop-aspectj: 使用aspectj框架实现aop。
使用aop:目的是给已经存在的一些类和方法,增加额外的功能。 前提是不改变原来的类的代码。

使用aspectj实现aop的基本步骤:
1.新建maven项目
2.加入依赖
1)spring依赖
2)aspectj依赖
3)junit单元测试
3.创建目标类:接口和他的实现类。
要做的是给类中的方法增加功能

4.创建切面类:普通类
1)在类的上面加入 @Aspect
2)在类中定义方法, 方法就是切面要执行的功能代码
在方法的上面加入aspectj中的通知注解,例如@Before
有需要指定切入点表达式execution()

5.创建spring的配置文件:声明对象,把对象交给容器统一管理
声明对象你可以使用注解或者xml配置文件<bean>
1)声明目标对象
2)声明切面类对象
3)声明aspectj框架中的自动代理生成器标签。
自动代理生成器:用来完成代理对象的自动创建功能的。

6.创建测试类,从spring容器中获取目标对象(实际就是代理对象)。
通过代理执行方法,实现aop的功能增强。

2.添加依赖

  <!--spring和aspectJ的依赖-->
    <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>

3.创建目标类和接口类
创建接口 (在idea中,在接口后面的接口名上 按 enter+alt 可以直接选 自动创建该接口的实现类)

package BUPT.ba01;

public interface SomeService {
    void doSome(String name,Integer age);
    void doOther();
}

实现类

package BUPT.ba01;

public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome(String name,Integer age) {
		System.out.println("目标方法doSome的name:" + name + "以及age:" + age);
    }

    @Override
    public void doOther() {

    }
}

4.创建切面类

package BUPT.ba01;

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

import java.util.Date;

/*
表示当前类是切面类
 */
@Aspect
public class MyAspect {
    /**
     * 定义方法,方法是实现切面功能的。
     * 方法的定义要求:
     * 1.公共方法 public
     * 2.方法没有返回值
     * 3.方法名称自定义
     * 4.方法可以有参数,也可以没有参数。
     */
    //给切面表达式
    @Before(value = "execution(public void BUPT.ba01.SomeServiceImpl.doSome(String,Integer))")
    public void myBefore() {
        //就是你切面要执行的功能代码
        System.out.println("前置通知, 切面功能:在目标方法之前输出执行时间:" + new Date());
    }

}

5.创建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">
    <!--把对象交给spring容器,由spring容器统一创建,管理对象-->
    <!--声明目标对象-->
    <bean id="someService" class="BUPT.ba01.SomeServiceImpl" />

    <!--声明切面类对象-->
    <bean id="myAspect" class="BUPT.ba01.MyAspect" />
    
   <!--声明自动代理生成器:使用aspectj框架内部的功能,创建目标对象的代理对象。
        创建代理对象是在内存中实现的, 修改目标对象的内存中的结构。 创建为代理对象
        所以目标对象就是被修改后的代理对象.

        aspectj-autoproxy:会把spring容器中的所有的目标对象,一次性都生成代理对象。
    -->
    <aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>

== 6 测试类==

public class AppTest {
    @Test
    public void test01() {
        String config = "applicationContext.xml";
        ApplicationContext AC = new ClassPathXmlApplicationContext(config);
        //从目标容器中获取目标对象
        SomeService proxy = (SomeService) AC.getBean("someService");
        //通过代理的对象执行方法
        proxy.doSome("JDH", 23);
    }
}

输出就可以了

前置通知, 切面功能:在目标方法之前输出执行时间:Sun Jan 09 11:29:07 CST 2022
目标方法doSome的name:JDH以及age:23

注解+切入表达式的结合才能实现相互的耦合和精准定位

我们再来看看JoinPoint的使用
在MyAspect里面添加
这里的切入表达式把两个方法都弄进来了,doSome和doOther

@Before(value = "execution(void *..SomeServiceImpl.do*(..))")
    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("2=====前置通知, 切面功能:在目标方法之前输出执行时间:"+ new Date());
    }

此时的测试类输出

public void test01() {
        String config = "applicationContext.xml";
        ApplicationContext AC = new ClassPathXmlApplicationContext(config);
        //从目标容器中获取目标对象
        SomeService proxy = (SomeService) AC.getBean("someService");
        //通过代理的对象执行方法
        System.out.println("======执行doSome之前的输出======");
        proxy.doSome("JDH", 23);

        System.out.println();
        System.out.println("==============doSome和doOther的分割线===============");
        System.out.println();
        proxy.doOther();
    }
方法的签名(定义)=void BUPT.ba01.SomeServiceImpl.doSome(String,Integer)
方法的名称=doSome
参数=JDH
参数=23
2=====前置通知, 切面功能:在目标方法之前输出执行时间:Sun Jan 09 16:18:27 CST 2022
目标方法SomeServiceImpl的doSome的name:JDH以及age:23

==============doSome和doOther的分割线===============

方法的签名(定义)=void BUPT.ba01.SomeServiceImpl.doOther()
方法的名称=doOther
2=====前置通知, 切面功能:在目标方法之前输出执行时间:Sun Jan 09 16:18:27 CST 2022
目标方法是SomeServiceImpl的doOther

也就是说,通过JoinPoint我们能够拿到所执行方法的相关参数,这类似于反射的功能

后置通知@AfterReturning

看一个例子

* 定义方法,方法是实现切面功能的。
     * 方法的定义要求:
     * 1.公共方法 public
     * 2.方法没有返回值
     * 3.方法名称自定义
     * 4.方法必须有参数
     */
    //给切面表达式
    //returning用来表示目标方法的返回值,返回值和形参的返回值要一致

定义切面类

package BUPT.ba02;

import ...;

@Aspect
public class MyAspect {
   
    //给切面表达式
    //returning用来表示目标方法的返回值,返回值和形参的返回值要一致
    @AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))",
            returning = "res")
    public void myAfterReturing(JoinPoint jp, Object res) {
        // Object res:是目标方法执行后的返回值,根据返回值做你的切面的功能处理
        System.out.println("后置通知:方法的定义" + jp.getSignature());
        System.out.println("后置通知:在目标方法之后执行的,获取的返回值是:" + res);
        if (res.equals("BUPT")) {
            //做一些功能
            System.out.println("确认输出了BUPT,这里的位置可以用来实现增强功能");
        } else {
            System.out.println("没有输出BUPT,这里的位置可以用来实现增强其他功能");
        }
    }
}

我们可以通过在MyAspect类里面 修改res,然后返回,这样就能改变输出了,但是这里改了也不会变
因为这里返回的是String类,String类的传递方式比较特殊,有一个字符池的因素,具体可以再百度。
下面的环绕通知能够修改,是因为有一个return的过程,这里是没有的。

如果是其他引用类型,进行引用传递,地址不变,但值被修改了,那么返回值也会随之变化的。

定义接口实现类

package BUPT.ba02;

public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome(String name, Integer age) {
        System.out.println("目标方法SomeServiceImpl的doSome的name:" + name + "以及age:" + age);
    }

    @Override
    public String doOther(String name, Integer age) {
        System.out.println("目标方法是SomeServiceImpl的doOther");
        return name;
    }
}

定义测试类

package BUPT;

import ...;

public class MyTest02 {
    @Test
    public void MyTest() {
        String config = "applicationContext.xml";
        ApplicationContext AC = new ClassPathXmlApplicationContext(config);
        //从目标容器中获取目标对象
        SomeService proxy = (SomeService) AC.getBean("someService");
        //通过代理的对象执行方法

        String str = proxy.doOther("bupt", 25);
        System.out.println("在Test里面获取了返回值str:"+str);
    }
}

目标方法是SomeServiceImpl的doOther
后置通知:方法的定义String BUPT.ba02.SomeServiceImpl.doOther(String,Integer)
后置通知:在目标方法之后执行的,获取的返回值是:bupt
没有输出BUPT,这里的位置可以用来实现增强其他功能
在Test里面获取了返回值str:bupt

环绕通知@Around

MyAspect类

定义方法,方法是实现切面功能的。
方法的定义要求:
1.公共方法 public
2.方法必须有返回值,推荐Object
3.方法名称自定义
4.方法必须有参数 固定参数

@Around: 环绕通知 属性:value 切入点表达式 位置:在方法的定义什么 特点:
1.它是功能最强的通知
2.在目标方法的前和后都能增强功能。
3.控制目标方法是否被调用执行
4.修改原来的目标方法的执行结果。 影响最后的调用结果

package BUPT.ba03;

import ...;

@Aspect
public class MyAspect {
    
    @Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
    public Object myAround(ProceedingJoinPoint pjp) throws Throwable {

        String name = "";
        //获取第一个参数值
        Object args[] = pjp.getArgs();
        if (args != null && args.length > 1) {
            Object arg = args[0];
            name = (String) arg;
        }

        //实现环绕通知
        Object result = null;
        System.out.println("环绕通知:在目标方法之前,输出时间:" + new Date());
        //1.目标方法调用
        if ("BUPT".equals(name)) {
            //符合条件,调用目标方法
            result = pjp.proceed(); //method.invoke(); Object result = doFirst();
        }

        System.out.println("环绕通知:在目标方法之后,提交事务");
        //2.在目标方法后加入功能

        //修改目标方法的执行结果, 影响方法最后的调用结果
        if (result != null) {
            result = "Hello AspectJ AOP";
        }

        //返回目标方法的执行结果
        return result;
    }
}

实现类添加一个接口的实现方式

    @Override
    public String doFirst(String name, Integer age) {
        System.out.println("====业务方法doFirst()====");
        return "doFirst";
    }

测试类

package BUPT;

import ...;

public class MyTest03 {
    @Test
    public void myTest() {
        String config = "applicationContext.xml";
        ApplicationContext AC = new ClassPathXmlApplicationContext(config);
        //从目标容器中获取目标对象
        SomeService proxy = (SomeService) AC.getBean("someService");
        //通过代理的对象执行方法

        String str = proxy.doFirst("BUPT", 25);
        System.out.println("在Test里面获取了返回值str:"+str);
    }
}

输出结果

环绕通知:在目标方法之前,输出时间:Sun Jan 09 17:16:34 CST 2022
====业务方法doFirst()====
环绕通知:在目标方法之后,提交事务
在Test里面获取了返回值str:Hello AspectJ AOP

这里能修改String,是因为多了一个return的过程

PointCut注解

  /**
     * @Pointcut: 定义和管理切入点, 如果你的项目中有多个切入点表达式是重复的,可以复用的。
     *            可以使用@Pointcut
     *    属性:value 切入点表达式
     *    位置:在自定义的方法上面
     * 特点:
     *   当使用@Pointcut定义在一个方法的上面 ,此时这个方法的名称就是切入点表达式的别名。
     *   其它的通知中,value属性就可以使用这个方法名称,代替切入点表达式了
     */

MyAspect类

package BUPT.ba04;

import ..

/**
 *  @Aspect : 是aspectj框架中的注解。
 *     作用:表示当前类是切面类。
 *     切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
 *     位置:在类定义的上面
 */
@Aspect
public class MyAspect {

    @After(value = "mypt()")
    public  void  myAfter(){
        System.out.println("执行最终通知,总是会被执行的代码");
        //一般做资源清除工作的。
    }

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

    @Pointcut(value = "execution(* *..SomeServiceImpl.doSome(..))" )
    private void mypt(){
        //无需代码,
    }

}

测试方法

package BUPT;

import BUPT.ba04.SomeService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MyTest03 {
    @Test
    public void myTest() {
        String config = "applicationContext.xml";
        ApplicationContext AC = new ClassPathXmlApplicationContext(config);
        //从目标容器中获取目标对象
        SomeService proxy = (SomeService) AC.getBean("someService");
        //通过代理的对象执行方法

        proxy.doSome("BUPT", 25);
    }
}

输出

前置通知,在目标方法之前先执行的
目标方法SomeServiceImpl的doSome的name:BUPT以及age:25
执行最终通知,总是会被执行的代码

cglib动态代理

jdk动态代理,必须有接口,目标类必须实现接口, 没有接口时,需要使用cglib动态代理

把接口类去掉
重写Test方法
注意proxy对应的是实现类而不是接口了

package BUPT;

import ...;

public class MyTest03 {
    @Test
    public void myTest() {
        String config = "applicationContext.xml";
        ApplicationContext AC = new ClassPathXmlApplicationContext(config);
        //从目标容器中获取目标对象
        SomeServiceImpl proxy = (SomeServiceImpl) AC.getBean("someService");
        //目标类没有接口,使用cglib动态代理,

        //通过代理的对象执行方法
        //得到BUPT.ba05.SomeServiceImpl$$EnhancerBySpringCGLIB$$fccf21be
        System.out.println(proxy.getClass().getName());
        //如果是proxy代理,则是com.sun.proxy.$Proxy14
        proxy.doSome("BUPT", 25);
    }
}

这样就是cglib方式的动态代理了
但有接口也可以用cglib
在xml中配置

    <!--
       如果你期望目标类有接口,使用cglib代理
       proxy-target-class="true":告诉框架,要使用cglib动态代理
    -->
    <aop:aspectj-autoproxy proxy-target-class="true"/>

这样还是用的cglib方式

结束时间:2022-01-10

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值