Spring AOP实现原理,从代理说起

本文详细介绍了Java代理模式,静态代理与动态代理的区别,重点讲解了Spring AOP如何通过配置实现目标方法的增强,以及AOP在解耦和代码重用中的优势。通过实例演示了基于接口的JDK动态代理和CGLIB动态代理,以及Spring AOP的配置和工作原理。
摘要由CSDN通过智能技术生成

前言

为了理解Spring AOP,我们先来了解一下Java的代理模式

什么是代理?

举个例子来说明代理的作用:

假设我们想邀请一位明星,那么并不是直接联系明星,而是联系明星的经纪人,来达到同样的目的.明星就是一个目标对象,他只要负责活动中的节目,而其他琐碎的事情就交给他的代理人(经纪人)来解决.这就是代理思想在现实中的一个例子

用图表示如下:
在这里插入图片描述
代理模式的关键点是:代理对象与目标对象.代理对象是对目标对象的扩展,并会调用目标对象

代理的类型

  1. 静态代理
  2. 动态代理

动态代理又包括

  1. 基于接口的动态代理
  2. 基于子类的动态代理

静态代理

一个简单的例子:

Service里的业务逻辑我们称之为需要执行的目标方法;

开启事务,提交事务这些我们就可以称之为对目标方法的增强。

“需要执行的目标方法”单独写一个类(目标类),“对目标方法的增强”单独写一个类(增强类),最后再写一个类(代理类),把它两者结合到一起。

 public interface PersonService {
	
	public void savePerson();
	
	public void updatePerson();
	
	public void deletePerson();
	
}
public class PersonServiceImpl implements PersonService{
 
	@Override
	public void savePerson() {
		System.out.println("添加");
	}
 
	@Override
	public void updatePerson() {
		System.out.println("修改");
	}
 
	@Override
	public void deletePerson() {
		System.out.println("删除");
	}
 
}

增强类:

public class Transaction {
	public void beginTransaction(){
		System.out.println("开启事务 ");
	}
	public void commit(){
		System.out.println("提交事务");
	}
}

代理类:

public class PersonServiceProxy implements PersonService{
	
	//目标类
	private PersonService personService;
	
	//增强类
	private Transaction transaction;
	
	//利用构造函数将目标类和增强类注入
	public PersonServiceProxy(PersonService personService,Transaction transaction){
		this.personService = personService;
		this.transaction = transaction;
	}
	
	@Override
	public void savePerson() {
		transaction.beginTransaction();
		personService.savePerson();
		transaction.commit();
	}
 
	@Override
	public void updatePerson() {
		transaction.beginTransaction();
		personService.updatePerson();
		transaction.commit();
	}
 
	@Override
	public void deletePerson() {
		transaction.beginTransaction();
		personService.deletePerson();
		transaction.commit();
	}
 
}

测试:

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
public class ProxyTest {
	@Test
	public void test(){
		ApplicationContext context = new ClassPathXmlApplicationContext("com/cj/study/proxy/applicationContext-proxy.xml");
		PersonService personService = (PersonService)context.getBean("personServiceProxy");
		personService.savePerson();
	}
}

bean.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
    
	<bean id="personServie" class="com.cj.study.proxy.PersonServiceImpl"></bean>
	
	<bean id="transaction" class="com.cj.study.proxy.Transaction"></bean>
	
	<bean id="personServiceProxy" class="com.cj.study.proxy.PersonServiceProxy">
		<constructor-arg index="0" ref="personServie"></constructor-arg>
		<constructor-arg index="1" ref="transaction"></constructor-arg>
	</bean>
	
</beans>

执行结果:

开启事务
添加
提交事务

静态代理是在程序运行前,代理类的.class文件就已经存在了

静态代理的缺点

  1. 假设一个系统中有100个Service,则需要创建100个代理对象
  2. 如果一个Service中有很多方法需要事务(增强动作),发现代理对象的方法中还是有很多重复的代码
  3. 由第一点和第二点可以得出:静态代理的重用性不强

那么该如何解决?这就轮到了我们动态代理的登场

动态代理

动态代理实现的目的和静态代理一样,都是对目标方法进行增强,而且让增强的动作和目标动作分开,达到解耦的目的

动态代理分为基于接口的动态代理和基于子类的动态代理

基于接口的动态代理
基于接口的动态代理:
    涉及的类:Proxy
    提供者:JDK官方
如何创建代理对象:
    使用Proxy类中的newProxyInstance方法
创建代理对象的要求:
    被代理类最少实现一个接口,如果没有则不能使用

例子:

package com.fym.cglib;

import com.fym.proxy.IProducer;

/**
 * @Author fym
 * @Date 2020/9/5 21:35
 *
 * 一个生产者
 */
public class Producer{
    //销售
    public void saleProduct(float money){
        System.out.println("销售产品,并拿到钱"+money);

    }

    //售后
    public void afterService(float money){
        System.out.println("提供售后服务,并拿到钱"+money);
    }

}
package com.fym.proxy;

/**
 * @Author fym
 * @Date 2020/9/5 21:37
 *
 * 对生产厂家要求的接口
 */
public interface IProducer {
    //销售
    public void saleProduct(float money);

    //售后
    public void afterService(float money);
}
如何创建代理对象:
    使用Proxy类中的newProxyInstance方法
创建代理对象的要求:
    被代理类最少实现一个接口,如果没有则不能使用
newProxyInstance方法的参数
    ClassLoader:类加载器
        它是用于加载代理对象字节码的,和被代理对象使用相同的类加载器
        写法是固定写法,代理的是谁就写Proxy.newProxyInstance(xxx.getClass().getClassLoader())
    Class[]:字节码数组
        它是用于让代理对象和被代理对象有相同方法
        代理的是谁就写(xxx.getClass().getInterfaces());
    InvocationHandler:用于提供增强的代码
        它是让我们写如何代理 我们一般都是写一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的
        此接口的实现类都是谁用谁写,就是谁要对方法增强就谁写
package com.fym.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * @Author fym
 * @Date 2020/9/5 21:38
 *
 * 模拟一个消费者
 */
public class Client {
    public static void main(String[] args) {
        final Producer producer=new Producer();
        IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(),
                new InvocationHandler() {
            		/**
                     * 执行被代理对象的任何接口方法都会经过该方法
                     * @param proxy 代理对象的引用
                     * @param method 当前执行的方法
                     * @param args 当前执行方法所需的参数
                     * @return 和被代理对象方法有相同的返回值
                     * @throws Throwable
                     */
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        //提供增强的代码
                        Object returnValue=null;
                        //1.获取方法执行的参数
                        Float money=(Float)args[0];
                        //2.判断当前方法是不是销售
                        if ("saleProduct".equals(method.getName())){
                            returnValue= method.invoke(producer,money*0.8f);
                        }
                            return returnValue;
                    }
                }
        );
        proxyProducer.saleProduct(1000f);
    }
}

在这里插入图片描述

基于子类的动态代理
基于子类的动态代理:
    涉及的类:Enhancer
    提供者:第三方cglib库
如何创建代理对象:
    使用Enhancer类中的create方法
创建代理对象的要求:
    被代理类不能是最终类
package com.fym.cglib;

import com.fym.proxy.IProducer;

/**
 * @Author fym
 * @Date 2020/9/5 21:35
 *
 * 一个生产者
 */
public class Producer{
    //销售
    public void saleProduct(float money){
        System.out.println("销售产品,并拿到钱"+money);

    }

    //售后
    public void afterService(float money){
        System.out.println("提供售后服务,并拿到钱"+money);
    }

}
package com.fym.cglib;

import com.fym.proxy.IProducer;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * @Author fym
 * @Date 2020/9/5 21:38
 *
 * 模拟一个消费者
 */
public class Client {
    public static void main(String[] args) {

        final Producer producer=new Producer();
        /**
        create方法的参数
            Class:字节码
            它是用于指定被代理对象的字节码
            写法是固定写法,代理的是谁就写Proxy.newProxyInstance(xxx.getClass().getClassLoader());
    	Callback:用于提供增强的代码
            他是让我们写如何代理,我们一般都是写一个接口的实现类,通常情况下都是匿名内部类,但不是必须的。
            此接口的实现类都是谁用谁写
            我们一般写的都是该接口的子接口的实现类:MethodIntercepter
        */
        Producer producer1= (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
            /**
             * 执行被代理对象的任何方法都会经过该方法
             * methodProxy是当前执行方法的代理对象
             * */
            public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                //提供增强的代码
                Object returnValue=null;
                //1.获取方法执行的参数
                Float money=(Float)args[0];
                //2.判断当前方法是不是销售
                if ("saleProduct".equals(method.getName())){
                    returnValue= method.invoke(producer,money*0.8f);
                }
                return returnValue;
            }
        });
        producer1.saleProduct(10000f);
    }
}

执行结果与基于接口的动态代理一致。

两个类型代理的小总结
  1. JDK动态代理,要求目标类实现接口,但是有时候目标类直接一个单独的对象,并没有实现任何的接口,这时就得使用CGLib动态代理
  2. JDK动态代理是JDK里自带的,CGLib动态代理需要引入第三方的jar包
  3. CGLib动态代理,它是在内存中构建一个子类对象,从而实现对目标对象功能的扩展
  4. CGLib动态代理,是基于继承来实现代理,所以无法对final类、private方法和static方法进行代理

我们用动态代理的做法去实现目标方法的增强,实现代码的解耦,是没有问题的,但是还是需要自己去生成代理对象,自己手写拦截器,在拦截器里自己手动的去把要增强的内容和目标方法结合起来,这用起来还是有点繁琐,那么解决方法应该是什么呢?

我们最终的主角Sring AOP登场!

Spring AOP

什么是Spring AOP

AOP(Aspect-OrientedProgramming,面向切面(方面)编程),可以说是OOP(Object-Oriented Programing,面向对象编程)的补充和完善。我们知道OOP的特点就是封装、继承、多态。我们很容易就能建立对象的层次结构,也就是说OOP允许你定义从上到下的关系,但不适合定义从左到右的关系,什么是从左到右的关系?例如日志功能。日志代码往往水平地散布在所有对象层次中,而它所散布到的对象的核心功能毫无关系。对于其他类型的代码,如安全性、异常处理和透明的持续性也是如此。这种散布在各处的无关的代码被称为横切(cross-cutting)代码,在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

而AOP技术则恰恰相反,它利用一种称为**“横切”**的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面/切面。所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。AOP代表的是一个横向的关系,如果说“对象”是一个空心的圆柱体,其中封装的是对象的属性和行为;那么面向切面编程的方法,就仿佛一把利刃,将这些空心圆柱体剖开,以获得其内部的消息。而剖开的切面,也就是所谓的“方面”了。然后它又以巧夺天功的妙手将这些剖开的切面复原,不留痕迹。

比如:我们写一段业务代码,例如进行一些简单的增删查改,需要日志功能,那么我们的日志类就是一个“切面”,那我们这个日志类根本就没有影响到我们自己的业务逻辑,也不会让我们整个操作有大量重复代码。

为什么使用Spring AOP

有了Spring的AOP后,我们就不用自己去写方法的增强了,例如上面的动态代理什么的,只需要在配置文件里进行配置,告诉Spring你的哪些类需要生成代理类、你的哪个类是增强类、是在目标方法执行之前增强还是目标方法执行后增强。配置好这些后Spring按照你的配置去帮你生成代理对象,按照你的配置把增强的内容和目标方法结合起来。就相当于自己写代码也能实现和AOP类似的功能,但是有了Spring AOP以后有些事情Spring帮你做了,而且人家Spring做成了可配置化,用起来非常简单而且很灵活

上边用的JDK动态代理和cglib动态代理,这两种在Spring的AOP里都有用到,Spring是根据不同的情况去决定是使用JDK的动态代理生成代理对象,还是使用cglib去生成代理对象,是不是很智能呢~

Spring AOP里的关键名词

**切面类:**就是要执行的增强的方法所在的类

**通知:**切面类里的增强方法

**目标方法:**要执行的目标方法,其实也可以说是切入点方法

**织入:**把通知和目标方法进行结合,形成代理对象的过程就叫织入

代理对象的方法=目标方法+通知

说这些概念,我们也不知道具体啥意思啊?下面以一个简单的例子来看看Spring AOP的简单应用

下面就是简单的业务类,保存、更新、删除操作,这些都是我们的目标方法

package com.fym.service;

/**
 * @Author fym
 * @Date 2020/9/6 14:21
 */
public interface IAccountService {
    //模拟保存账户
    void saveAccount();

    //模拟更新账户
    void updateAccount(int i);

    //删除账户
    int deleteAccount();

}
package com.fym.service.impl;

import com.fym.service.IAccountService;

/**
 * @Author fym
 * @Date 2020/9/6 14:23
 */
public class AccountServiceImpl implements IAccountService {
    @Override
    public void saveAccount() {
        System.out.println("执行了保存");
    }

    @Override
    public void updateAccount(int i) {
        System.out.println("执行了更新");
    }

    @Override
    public int deleteAccount() {
        System.out.println("执行了删除");
        return 0;
    }
}

这是日志类,也就是我们前面说的切面类。因为一个操作要记录一下日志嘛,我们不可能在业务类那里每一个操作方法里面都打印一句“开始记录日志了。。。”,而且这只是一个小例子随便打印一句话,真正的切面类可不一定像我们的例子那么简单,可想而知如果我们不设置切面类代码会有多冗杂。

那这个切面类里面的printLog就是通知咯,因为它是切面类里面的增强方法

package com.fym.utils;

/**
 * @Author fym
 * @Date 2020/9/6 14:37
 *
 * 用于记录日志的工具类。它里面提供了公共的代码
 */
public class Logger {
    //用于打印日志,计划让其在切入点方法执行前执行(切入点方法就是业务层方法)
    public void printLog(){
        System.out.println("Logger类的printLog方法开始记录日志了。。。");
    }
}

bean.xml

spring中基于xml的aop配置步骤
  1. 把通知Bean也交给spring来管理
  2. 使用aop:config标签表明开始aop配置
  3. 使用aop:aspect标签表明开始配置切面
    id属性;是给切面提供一个唯一标识
    ref属性:是指定通知类bean的id
  4. 在aop:aspect标签的内部使用对应的标签来配置通知的类型
    我们现在的示例是让pringLog方法在切入点方法执行之前执行
    aop:before :表示配置前置通知
    method属性:用于指定Logger类中哪个方法是前置通知
    pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强
<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
    <!--配置spring的ioc把service对象配置进来-->
    <bean id="accountService" class="com.fym.service.impl.AccountServiceImpl"></bean>
    <!--配置logger类-->
    <bean id="logger" class="com.fym.utils.Logger"></bean>
    <!--配置aop-->
    <aop:config>
        <!--配置切面-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--配置通知的类型并且建立通知方法和切入点方法的关联-->
            <aop:before method="printLog" pointcut="execution(* *..*.*(..))"></aop:before>
        </aop:aspect>
    </aop:config>

</beans>

我们可能会疑惑这个execution是个啥啊里面那么多星号还有点点、括号什么的,一脸懵逼

这里解释一下,这是切入点表达式的写法

切入点表达式的写法:
关键字:execution(表达式)
表达式:
访问修饰符 返回值 包名.包名…类名.方法名(参数列表)
标准的表达式写法:
public void com.fym.service.impl.AccountServiceImpl.saveAccount()

这么说是不是还是有点儿不懂,我们一步步来看
在这里插入图片描述
参数列表:
可以直接写数据类型:
基本类型直接写名称 int
引用类型写包名.类名的方式 java.lang.String
可以使用通配符表示任意类型,但是必须有参数
可以使用…表示有无参数均可,有参数可以是任意类型

全通配写法:
* *..*.*(..)

实际开发中切入点表达式通常写法:
切到业务层实现类下的所有方法
针对这个例子就是:

* com.fym.service.impl.*.*(..)

好啦,那都配置好了之后,我们写一个测试类来看看效果吧

package com.fym.test;

import com.fym.service.IAccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * @Author fym
 * @Date 2020/9/6 15:03
 * 测试aop的配置
 */
public class AOPTest {
    public static void main(String[] args) {
        //1.获取容器
        ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
        //2.获取对象
        IAccountService as=(IAccountService)ac.getBean("accountService");
        //3.执行方法
        as.saveAccount();
        as.updateAccount(1);
        as.deleteAccount();
    }
}

运行结果如下
在这里插入图片描述

Spring AOP的具体加载步骤
   1、当spring容器启动的时候,加载了spring的配置文件
   2、为配置文件中所有的bean创建对象
   3、spring容器在创建对象的时候它会解析aop:config的配置 
       	    解析切入点表达式,用切入点表达式和纳入spring容器中的bean做匹配
            如果匹配成功,则会为该bean创建代理对象,代理对象的方法=目标方法+通知
            如果匹配不成功,则为该bean创建正常的对象
	其实就是你通过表达式告诉Spring哪些bean需要它帮你生成代理对象而不是生成原有的正常对象
      理解这一点相当重要!
   4、在客户端利用context.getBean获取对象时,如果该对象有代理对象则返回代理对象,如果没有代理对象,则返回目标对象
 
说明:如果目标类实现了接口,spring容器会采用jdk的动态代理产生代理对象,产生的代理类和目标类实现了相同的接口;
如果目标类没实现接口,spring容器会采用cglib的方式产生代理对象,产生的代理类是目标类的子类

对于Spring AOP的原理我们都知道是动态代理,但是具体的不太清楚,通过本文从代理–>静态代理–>动态代理–>AOP,大概基本清晰了一点吧~

参考文章

静态代理:

https://caoju.blog.csdn.net/article/details/90713500

AOP部分参考:

https://caoju.blog.csdn.net/article/details/90719595

https://blog.csdn.net/moreevan/article/details/11977115/

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值