AOP 编程

目录

​编辑一、AOP 编程

1、AOP 概念

2、AOP 编程的开发步骤

3、切面的名词解释

二、AOP 的底层实现原理

1、核心问题

2、动态代理类的创建

(1)JDK 的动态代理创建

(2)CGlib 的动态代理

(3)总结

3、Spring 工厂如何加工原始对象

三、基于注解的 AOP 编程

1、基于注解的 AOP 编程的开发步骤

2、细节

(1)切入点复用

(2)动态代理的创建方式

四、AOP 开发中的坑

五、AOP 阶段知识总结


一、AOP 编程

1、AOP 概念

AOP (Aspect Oriented Programing)面向切面编程

以切面为基本单位的程序开发,通过切面间的彼此协同,相互调用,完成程序的构建

面向切面编程 = Spring 动态代理开发

切面 = 切入点 + 额外功能

OOP (Object Oriented Programing)面向对象编程

以对象为基本单位的程序开发,通过对象间的彼此协同,相互调用,完成程序的构建 

POP (Producer Oriented Programing)面向过程编程

以过程为基本单位的程序开发,通过过程间的彼此协同,相互调用,完成程序的构建

AOP 的概念:本质就是 Spring 动态代理开发,通过代理类为原始类(目标类)增加额外功能,利于原始类的维护

注意:AOP 编程不可能代理 OOP 编程


2、AOP 编程的开发步骤

AOP 本质上就是 Spring 的动态代理开发,那么它的开发步骤和 Spring 的动态代理开发是完全一样的:

1、原始对象

2、额外功能(MethodInterceptor)

3、切入点

4、组装切面(额外功能 + 切入点)


3、名词解释

(1)切面(Aspect)

切面 = 切入点 + 额外功能

通俗的理解:在程序中就是一个处理某方具体问题的一个类,类里面包含了很多方法,这些方法就是切点和通知

为什么把这两个的组合,叫做切面呢?

在几何学上:面 = 点 + 相同的性质

切面也是由 点 来构成的,它们所具有的相同性质,就是 额外功能

(2)切点(Pointcut)

用来进行主动拦截的规则(配置)

(3)通知(Advice)

程序中被拦截请求触发的具体动作(做什么)就是在通知中实现的具体业务代码。

AOP 具体的具体执行动作

a.前置通知: @Before,在执行目标方法之前执行的方法就叫前置通知

b.后置通知: @After,在执行了目标方法之后执行了的方法就叫后置通知

c.异常通知@AfterReturning,在执行了目标方法之后出现了异常执行的通知就叫异常通知

d.返回通知: @AfterThrowing,目标方法执行了返回数据 ( return ) 时,执行的通知

e.环绕通知@Around,在目标方法执行的周期范围内(执行之前、执行中、执行后),都可以执行的方法

(4)连接点(join point)

可能会触发 AOP 规则的所有点(所有请求)


二、AOP 的底层实现原理

1、核心问题

1、AOP 如何创建动态代理类?

前面我们学过,所谓的动态代理类,是依附于动态字节码技术,那么动态字节码技术到底是怎么通过编码,让我们把动态代理创建出来的呢?

2、Spring 工厂如何加工如何加工创建代理对象

Spring 是如何实现 通过原始对象的 id 值,最终获得的是代理对象 的呢?


2、动态代理类的创建

对于 Spring 来讲,在动态代理类的创建过程中有两种方式:

1、JDK 的动态代理创建

2、CGlib 的动态代理

(1)JDK 的动态代理创建

Proxy.newProxyInstance 方法参数详解 

类加载器的作用:

1、通过类加载器把对应类的字节码文件加载到 JVM 中

2、通过类加载器,创建类的 Class 对象,进而创建这个类的对象

比如:如果我们想创建一个 User 类的  user 对象

那么我们首先先要通过类加载器创建一个 User 类的 Class 对象,进而才可以通过 new User() 的方法创建 user 对象

类加载器的运行过程:

假设此时我们想创建 User 类的对象,那么第一步就得开发这个 User 类,也就是创建它的 .java 文件,之后我们会对它进行相应的编译,最后编译成 .class 文件,而 .class 文件里存放的就是它所对应的字节码文件

我们要想创建 User 类对应的对象,就得把 User 类对应的字节码加载到 JVM 虚拟机当中,这个加载实际上就是类加载器完成的(类加载器的第一个作用)

然后,我们先得创建 User 类的 Class 对象(类加载器的第二个作用), 然后我们就可以根据之前的知识,去通过 new 对象来创建 User 对象

如何获得 类加载器?

虚拟机会为每一个类的 .class 文件,自动分配与之对应的类加载器

动态代理:

动态代理,实际上也是在虚拟机当中去获得动态代理类,进而创建代理对象

但是动态代理类是没有源文件,没有字节码文件的,那么动态代理类是怎么获取这个类所对应的字节码来创建对象的呢?

动态代理,是通过动态字节码技术,来创建字节码的

我们之前学到的 Proxy.newProxyInstance (classloader , interfaces , invocationhandler) 就是动态字节码技术

生成了对应动态代理的字节码之后,就直接把字节码写在了  JVM 虚拟机里面

要想创建代理类的对象,就必须得先获得代理类的 class 对象,这个过程就需要类加载器的介入

但是此时没有 .class 文件,JVM 虚拟机就不会分配类加载器,但是我们又需要类加载器

所以我们可以借用一个 类加载器

所以这也是为什么我们在创建动态代理的时候,要指定 类加载器,这个类加载器是借用的,目的是完成动态代理类 Class 对象的创建

public class TestJDKProxy {
    public static void main(String[] args) {
        //1、创建原始对象
        UserService userService = new UserServiceImpl();

        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("-------log---------");
                
                //原始对象的方法运行
                Object ret = method.invoke(userService,args);
                //          方法本身    方法属于哪个对象    方法的参数
                
                return ret;
            }
        };

        //2、JDK 创建动态代理
        UserService userService1Proxy = (UserService) Proxy.newProxyInstance(TestJDKProxy.class.getClassLoader(),userService.getClass().getInterfaces(),handler);

        userService1Proxy.login("xiaoahei","123456");
        userService1Proxy.register(new User());
    }
}

(2)CGlib 的动态代理

我们先来看看 JDK 的动态代理:

首先,得提供我们所说的原始对象,而原始对象在 JDK 动态代理的过程中,我们必须得让它实现一个接口

当接口有了之后,我们得去实现这个接口

JDK 的动态代理中,原始对象和代理对象必须实现相同的接口

原因:

1、保证 代理类 和 原始类 方法一致,

2、代理类中可以提供新的实现:额外功能 + 对应原始方法


再来看看 CGlib 的动态代理:

假设此时有一个原始类,它没有实现任何的接口,我们想为这个没有实现任何接口的原始类,去创建它所对应的代理类,此时我们该怎么做呢?

CGlib 要求所创建的代理类,要去继承原始类

此时,也能让代理类和原始类有相同的原始方法,从而在 login 和 register 中加入额外功能

CGlib 创建动态代理的原理:通过父子继承关系创建代理对象,原始类作为父类,代理类作为子类,这样既可以保证二者方法一致,同时还可以在代理类中,提供新的实现(额外功能 + 原始方法)

C3Glib 的编码:

public class TestCglib extends UserService{
    public static void main(String[] args) {
        //1、创建原始对象
        UserService userService = new UserService();
        
        //2、通过 CGlib 的方法,创建动态代理对象

        Enhancer enhancer = new Enhancer();
        enhancer.setClassLoader(TestCglib.class.getClassLoader());//类加载器
        enhancer.setSuperclass(userService.getClass());//父类

        MethodInterceptor interceptor = new MethodInterceptor() {
            //等同于 invocationhandler 中的 invoke 方法
            @Override
            public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                System.out.println("-----cglib log--------");
                Object ret = method.invoke(userService,args);
                
                return ret;
            }
        };
        
        enhancer.setCallback(interceptor);//额外功能
        
        UserService userService1Proxy = (UserService) enhancer.create();
        
        userService1Proxy.login("xiaohei","123456");
        userService1Proxy.register(new User());
        
    }
}

(3)总结

JDK 代理 依附 Proxy.newProxyInstance( ) ,通过接口创建代理的实现类

CGlib 代理 依附  Enhance ,通过继承父类创建的代理类


3、Spring 工厂如何加工原始对象

我们先来回顾一下 BeanPostProcessor 

在 Spring 创建一个对象的时候,比如:User ,那么创建完对象的时候,Spring 工厂可以通过 BeanPostProcessor 来对我们所创建的对象进行加工,加工完成之后,把最终加工好了的对象返回给调用者,调用者就享受到了 Spring 为我们加工的 User 对象

实际上,动态代理的创建,实际上也是通过 BeanPostProcessor  进行加工的

我们再来看看,动态代理结合了 BeanPostProcessor  之后,我们这个程序变成了什么样:

创建代理过程当中,Spring 通过 BeanPostProcessor  完成了对 UserService 这个原始对象的加工

Spring 通过 userService 获得到了代理对象,实际上这个过程中,就涉及到了对 UserServiceImpl 的一个加工过程,这个加工还是通过 BeanPostProcessor  来完成的

创建了 UserService 的原始对象,调用了 UserService 的构造方法,然后 Spring 把 userService 对象创建出来了,接下类对其进行初始化操作,再交给 after  方法进行加工,其中调用了 Proxy.newProxyInstance( ) 方法,加工成我们最终所需要的代理对象了

编码:

public class ProxyBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                System.out.println("-------- new log --------");
                Object ret = method.invoke(bean,args);
                
                return ret;
            }
        };
        
        return Proxy.newProxyInstance(ProxyBeanPostProcessor.class.getClassLoader(),bean.getClass().getInterfaces(),handler);
        
    }
}

三、基于注解的 AOP 编程

1、基于注解的 AOP 编程的开发步骤

既然是 AOP 编程,那么就应该遵从AOP 的开发步骤;

1、原始对象

2、额外功能

3、切入点

4、组装切面

我们可以把切面想象成一个类,所以在基于注解的 AOP 编程中,所对应的切面就是一个切面类

要想表达一个类是切面类,就要为其加上 @Aspect 注解

@Aspect
public class MyAspect {

    /*
    加上 @Around 就相当于 MethodInterceptor
    此时,around 方法就相当于 invoke 方法
    ProceedingJoinPoint joinPoint 就等同于 MethodInvocation invocation,代表的是原始方法
     */
    @Around("execution(* login(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {

        System.out.println("----aspect log----");
        Object ret = joinPoint.proceed();

        return ret;
    }
}

 

定义好了这个切面是切面类,最后用的一定是它的对象,所以后续我们还得在 Spring 的配置文件当中,通过 Spring 的工厂来创建这个类的对象

 <bean id="arround" class="aspect.MyAspect"/>

此时这个切面里面,既体现了额外功能,又体现了切入点

最后一个环节:此时我们要告诉 Spring ,我们现在要基于注解的形式,来进行 AOP 编程了,所以此时我们要增加一个新的标签:

<aop:aspectj-autoproxy />

2、细节

(1)切入点复用

    @Around("execution(* login(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {

        System.out.println("----aspect log----");
        Object ret = joinPoint.proceed();

        return ret;
    }
    
    @Around("execution(* login(..))")
    public Object around1(ProceedingJoinPoint joinPoint) throws Throwable {

        System.out.println("----aspect tx----");
        Object ret = joinPoint.proceed();

        return ret;
    }

虽然它可以达到我们想要的效果

在这个切面当中,实际上我们为我们的 login 方法加额外功能 的过程中,这个切入点表达式实际上会存在冗余的,冗余会有两个问题:

1、同样的东西,我们写了两次,会影响到开发效率

2、后续维护的过程中,两边都得变,所以不方便修改

切入点复用,就可以解决上述问题

所谓的切入点复用,就是把我们的切入点的配置,提取到一个独立的函数上

切入点复用:在切面类中,定义一个函数,该函数上面加上 @Pointcut 注解,定义切入点表达式,后面更加有利于切入点的复用 

 @Pointcut("execution(* login(..))")
    public void myPointCut(){};

    @Around(value = "myPointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {

        System.out.println("----aspect log----");
        Object ret = joinPoint.proceed();

        return ret;
    }

    @Around(value = "myPointCut()")
    public Object around1(ProceedingJoinPoint joinPoint) throws Throwable {

        System.out.println("----aspect tx----");
        Object ret = joinPoint.proceed();

        return ret;
    }

(2)动态代理的创建方式

AOP 底层实现中有两种代理创建方式:

JDK  通过实现接口来做新的实现类的方式,来创建代理对象

CGlib 通过继承父类的方式,来做新的子类,来创建最终的代理对象的

默认情况下,AOP 编程底层应用的是 JDK 的动态代理创建方式

如果切换成 CGlib ,应该怎么办呢?

基于注解 AOP 开发

 <aop:aspectj-autoproxy  proxy-target-class="true"/>

proxy-target-class 默认情况下是 false(JDK 的动态代理),当我们改成 true 的时候,就可以更改成 CGlib 的方式了

基于传统的 AOP 开发:

    <aop:config proxy-target-class="true">
        <!--        所有的方法,都作为切入点,加入额外功能 -->
        <aop:pointcut id = "pc" expression = "execution(* proxy.UserServiceImpl.*(..))"/>

        <!--        组装:把切入点和额外功能进行整合 -->
        <aop:advisor advice-ref="arround" pointcut-ref = "pc"/>
    </aop:config>

四、AOP 开发中的坑

在 同一个业务类中 ,进行业务方法间的相互调用,只有最外层的方法,才是加入了额外功能的方法(内部的方法,通过普通的方式调用,都调用的是原始方法),如果想让内层的方法也调用代理对象的方法,就要通过 ApplicationContextAware 来获得工厂,进而获得代理对象

    @Override
    public void login(String name, String password) {
        System.out.println("UserServiceImpl.login");
    }

    @Override
    public void register(User user) {
        System.out.println("UserServiceImpl.register");

        //调用的是原始对象的 login 方法,就只能完成核心功能
        //但是我们真正的设计目的是:想调用代理对象的 login 方法 ---> 额外功能

        this.login("xiaohei","123456");
    }

由于 Spring 工厂是重量级资源,所以一个应用中,我们只能创建一个工厂

因此此处我们不该再创建一个工厂,而是从 测试类 中获取到已经创建好了的工厂,直接使用即可

那么怎么拿到已经创建好了的工厂呢?

让当前类再实现 ApplicationContextAware 接口,并通过接口中的方法获取到 Spring 工厂 

public class UserServiceImpl implements UserService, ApplicationContextAware {
    private ApplicationContext ctx ;
    
    @Override
    public void login(String name, String password) {
        System.out.println("UserServiceImpl.login");
    }

    @Override
    public void register(User user) {
        System.out.println("UserServiceImpl.register");

        //调用的是原始对象的 login 方法,就只能完成核心功能
        //但是我们真正的设计目的是:想调用代理对象的 login 方法 ---> 额外功能

        proxy.UserService userService = (proxy.UserService) ctx.getBean("userService");
        userService.login("xiaohei","123456");
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.ctx = applicationContext;
    }
}

五、AOP 阶段知识总结

  • 8
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值