动态代理和Spring AOP原理

AOP

什么是AOP?

  • 先来说个场景,如果说我们要在多个重要的用户访问的接口记录访问的日志,如果我们在业务类中加入日志记录的逻辑不仅使得代码显得臃肿而且容易出错,维护起来也不方便,那有没有简便的方法呢!这里就要提到本篇要讲的AOP,也就是切面编程,通过把切面的逻辑织入到业务逻辑中完成日志的记录。那么AOP是如何做到的呢?首先我们不妨来看个简单的例子,熟悉的配方熟悉的味道。
@Aspect
@Component
public class LogAspect {

    /**
     *  这里 一个 * 表示任意字符 两个 ** 表示一个或者多个目录 .. 表多0个或者多个参数
     */
    @Pointcut(value = "execution(* com.frank.spring.**.*Impl.*(..))")
    public void pointcut() {
    }

    @Around(value = "pointcut()")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint){
        try {
            System.out.println("执行方法之前");
            joinPoint.proceed(joinPoint.getArgs());
            System.out.println("执行方法之后");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return null;
    }
}

// 启动类,重点是要加上@EnableAspectJAutoProxy
@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "com.frank.spring.aop")
public class AopApplication {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext
                = new AnnotationConfigApplicationContext(AopApplication.class);
	// 获取Service对象,这里我直接获取了实现类
        UserServiceImpl userService = applicationContext.getBean(UserServiceImpl.class);
        userService.aopMethod();
    }
}

  • 执行结果如下,可以看出切面已经织入了方法执行的过程。那么Spring AOP是如何做到的呢?要了解这个过程,先来看看Spring对于AOP概念的定义
执行方法之前
=========执行方法逻辑=========
执行方法之后
  • AOP定义:Aspect-oriented Programming 面向切面编程。它主要操作的对象是切面,跟IOC是两个独立的功能,意味着可以单独使用,是对IOC的一种补充,主要用在Sping事务日志处理中。AOP几个重要的概念在这里我就不赘述了,请参考官网Spring AOP 概念
  • 如果对概念有个大致的了解之后再来梳理下。简单阐述我个人的理解。首先Method调用处,如果执行的方法是一个Join Point,这个时候就会把连接点织入到Aspect中去。Aspect定义有通知Advice,根据不同类型的通知在不同的阶段执行操作并回调Target方法,最后切面的方法执行完成后会返回到调用的地方继续后面的逻辑,这样就基本完成了一个AOP的流程,如下图所示。
    spring-aop流程
  • 对AOP的整体流程有了一个大致的了解之后,我们想要知道AOP是如何做到把连接点(调用的方法)和切面关联起来的,这就要说说动态代理的内容了

动态代理

Spring支持两种代理,jdk动态代理cglib动态代理

jdk动态代理

jdk的动态代理是通过java反射包中的Proxy类来实现的,最终生成的代理类继承了Proxy并且实现了目标接口,也就是说代理类只能在接口的基础上生成,这也就说明了为什么jdk动态代理为什么必须要通过接口来实现。下面具体来说说实现的方式

//接口
public interface Person {
    void sayHello();
}

// 实现类
public class Chinese implements Person{
    @Override
    public void say() {
        System.out.println("您好!");
    }
}

// 代理类handler
public class PersonInvocationHandler implements InvocationHandler {

    private Object object;

    public PersonInvocationHandler(Object object) {
        this.object = object;
    }

    /**
     *
     * @param proxy
     * @param method 代理的方法
     * @param args 方法的參數
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("調用之前");
        method.invoke(object, args);
        System.out.println("調用之後");
        return null;
    }
}

// main方法
public class JdkMain {

    public static void main(String[] args) {
    	// 开启生成代理类到根目录
        System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

        Chinese chinese = new Chinese();
        // 通过Proxy来创建代理对象
        Person proxyChinese = (Person) Proxy.newProxyInstance(JdkMain.class.getClassLoader(),
                new Class[]{Person.class},
                new PersonInvocationHandler(chinese));

        proxyChinese.say();
    }
}
  • 执行上面的main方法,会打印执行结果并在当前目录生成代理类
    • 1、打印下面的执行结果,说明切面已经生效
// 执行结果
調用之前
您好!
調用之後
  • 2、通过System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");这句代码开启生成代理类配置。执行main方法后,下面的代码截取了生成的文件名信息,可以看出,生成的代理类的文件名$Proxy0并且继承了Proxy同时实现了Person接口。这也说明了为什么Jdk动态代理必须要通过接口来实现
public final class $Proxy0 extends Proxy implements Person
cglib动态代理
  • cglib动态代理需要依赖cglib包,所以先引入cglib包。与JDK动态代理的最大区别是不需要通过接口来实现动态代理,而是通过字节码增强技术来创建代理类。那么cglib是如何来实现代理的呢?
// 普通类
public class Chinese {
    public void say() {
        System.out.println("你好!");
    }
}

// 方法拦截器
public class ChineseCglibInterceptor implements MethodInterceptor {

    public static Object generator(Class target, MethodInterceptor methodInterceptor) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(target);
        enhancer.setCallback(methodInterceptor);
        return enhancer.create();
    }

    /**
     * @param o 目标对象
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("調用cglib之前");
        methodProxy.invokeSuper(o, objects); // 通过methodProxy去调用目标对象的方法
        System.out.println("調用cglib之後");
        return null;
    }
}

// 调用main方法
public class CglibMain {
    public static void main(String[] args) throws Exception {
        // 开启生成动态代理类到代码根目录
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, System.getProperty("user.dir"));

        Chinese person = (Chinese) ChineseCglibInterceptor.generator(Chinese.class, new ChineseCglibInterceptor());
        person.say();
    }
}
  • 执行main方法,也是打印了执行结果并且生成了代理类文件。从下面的打印结果来看,说明代理已经生效
調用cglib之前
你好!
調用cglib之後
  • 通过配置System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, System.getProperty("user.dir"));来开启代理类生成文件的系统配置,在根目录下生成三个动态代理文件,文件名和具体分析如下
// 文件1、该类继承了目标对象,并且重写了方法,调用的时候就是通过这里进行拦截调用的
public class Chinese$$EnhancerByCGLIB$$50241a58 extends Chinese implements Factory {
	// 省略其它部分
	// 这里重写了父类的方法,并通过var10000.intercept来调用代理对象的方法
	public final void say() {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (var10000 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }

        if (var10000 != null) {
            var10000.intercept(this, CGLIB$say$0$Method, CGLIB$emptyArgs, CGLIB$say$0$Proxy);
        } else {
            super.say();
        }
    }
}
// 文件2
public class Chinese$$EnhancerByCGLIB$$50241a58$$FastClassByCGLIB$$6f5842fa extends FastClass
// 文件3
public class Chinese$$FastClassByCGLIB$$f1af7ddc extends FastClass

AOP的实现

通过在SpringBoot的配置@Configuration中引入注解@EnableAspectJAutoProxy就可以启用AOP的功能,那注解是如何做到呢?通过深入代码发现引入的注解的目的是为了在启动工程时候注册一个后置处理器AnnotationAwareAspectJAutoProxyCreator

@Aspect类被扫描
  • 启动Spring容器会创建对象ProxyTransactionManagementConfiguration,该对象初始化时调用后置处理器AnnotationAwareAspectJAutoProxyCreator,通过下面的代码分析,可以看出此后置处理会扫描@Aspect注解,通过AbstractAspectJAdvisorFactory来判断是否包含注解,并保存切面在缓存中
	// 在类AbstractAspectJAdvisorFactory判断是否有Aspect注解
	private boolean hasAspectAnnotation(Class<?> clazz) {
		return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null);
	}
// BeanFactoryAspectJAdvisorsBuilder#buildAspectJAdvisors方法中把注解额信息放入缓存Map去
public List<Advisor> buildAspectJAdvisors() {
	List<String> aspectNames = this.aspectBeanNames;

	if (aspectNames == null) {
		synchronized (this) {
							// 把构建的切面对象放入缓存中去,作为后面创建代理的对象的依据
							if (this.beanFactory.isSingleton(beanName)) {
								this.advisorsCache.put(beanName, classAdvisors);
							}
							else {
								this.aspectFactoryCache.put(beanName, factory);
							}
							advisors.addAll(classAdvisors);
						}
						
				}
				this.aspectBeanNames = aspectNames;
				return advisors;
			}
		}
	}
}
创建对象代理类
  • 我们知道后置处理器的作用之一是在Bean实例化之后对类进行包装从而生成新的对象。我们自己的对象在初始化的过程中,会调用一系列的BeanPostProcessor,其中就包含有AnnotationAwareAspectJAutoProxyCreator(前提是启用了@EnableAspectJAutoProxy),该后置处理器的作用就是生成动态代理类并替换原来的实例,最终缓存在singletonObjects中。如下在方法org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsAfterInitialization中就是生成代理类额逻辑(默认是cglib动态代理)
@Override
	public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
			throws BeansException {

		Object result = existingBean;
		// 执行一系列的后置处理器,就包含有 AnnotationAwareAspectJAutoProxyCreator 后置处理器
		for (BeanPostProcessor processor : getBeanPostProcessors()) {
			Object current = processor.postProcessAfterInitialization(result, beanName);
			if (current == null) {
				return result;
			}
			// 生成新的对象来替换原来的对象
			result = current;
		}
		return result;
	}
  • 生成代理对象之后,在程序执行的过程中,通过获取代理对象,在调用时就会进入切面逻辑,从而能够实现切面逻辑的调用。
  • 上面简单阐述了AOP的流程,大家知道AOP在Spring中扮演的角色非常非常重要,那重要性如何来体现?
    • 1、之前的文章中的降级熔断注解:@HystrixCommand 就是通过AOP的原理来实现的
    • 2、@Transactional的实现也是依赖于AOP,那么事务有哪些属性?它又是如何实现的?请听下回Spring 事务分解!

总结

  • OOP能够帮助我们实现逻辑的流程,但是如果在流程中要嵌套一些公共的逻辑,这个时候就需要AOP,通过两种方式的相辅相成使得日常的编码能够更加的简练高效职责更加单一
  • 实现AOP的几个重要的概念:切面、切点、通知、连接点、织入,其中切点实现方式有很多种,各种通知也比较多。这几个重要的组件相互配合共同完成了AOP

欢迎关注spring-cloud系列打怪升级系列性能优化系列spring系列
试试右下角一键三连你会有惊奇的发现:)你的点赞和关注是我创作的最大动力,有什么不足和错误的地方欢迎留言!可以微信搜索关注【小二说码

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值