Spring Aop底层原理详解(利用spring后置处理器实现AOP)

Spring Aop底层原理详解(利用spring后置处理器实现AOP)写在前面:对于一个java程序员来说,相信绝大多数都有这样的面试经历,面试官问:你知道什么是aop吗?谈谈你是怎么理解aop的?等等诸如此类关于aop的问题。当然对于一些小白可能会一脸懵逼;对于一些工作一两年的,可能知道,哦!aop就是面向切面变成,打印日志啊,什么什么的,要是有点学习深度的呢可能会说aop底层实现利用了jdk动态代理,cglib啊什么的。很多时候可能面试就到此打住了,当然,然后也…

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

写在前面:对于一个java程序员来说,相信绝大多数都有这样的面试经历,面试官问:你知道什么是aop吗?谈谈你是怎么理解aop的?等等诸如此类关于aop的问题。当然对于一些小白可能会一脸懵逼;对于一些工作一两年的,可能知道,哦!aop就是面向切面变成,打印日志啊,什么什么的,要是有点学习深度的呢可能会说aop底层实现利用了jdk动态代理,cglib啊什么的。很多时候可能面试就到此打住了,当然,然后也就没有然后了(客气点的来句:回头有消息我会通知你的!)。
今天,通过这篇文章,我想带大家先了解下什么是spring后置处理器,然后利用spring的后置处理器我们自己来手写一个springAop,来完成和springAop一样的功能!让你可以对你的面试官说:你精通AOP!

在开始之前呢,我想先引入一个概念:spring扩展点和后置处理器

我们知道springIoc可以对我们应用程序中的java对象做一个集中化的管理,从而使我们从繁琐的new Object();中解脱出来。
其核心思想呢就是先创造出一个bean工厂,也就是我们的beanFactory,通过beanFactory来生产出我们应用程序中所需要的java对象,也就是我们的java bean。

今天呢我跟大家介绍的后置处理器呢,有三个
BeanFactoryPostProcessor : 可以插手beanFactory的生命周期
BeanPostProcessor :可以插手bean的生命周期
ImportSelector :借助@Import注解,可以动态实现将一个类是否交由spring管理,常用作开关操作

1,BeanFactoryPostProcessor(BeanDefinitionRegistryPostProcessor 有兴趣的可以自行查阅spring源码)
在这里插入图片描述
可以看到,该接口只定义了一个方法,具体实现的执行时机呢,我们可以通过spring源码得知:
在我们AnnotationConfigApplicationContext.refresh();的时候,从下源码可知,在我们beanFactory被创建出来后,相关准备工作做完后,会去执行invokeBeanFactoryPostProcessors(beanFactory);也就是去执行我们的BeanFactoryPostProcessor
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
从上面两处代码可以看出,spring在执行
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
的时候,会传入一个List beanFactoryPostProcessors;然后循环去执行list里面所有实现了
BeanFactoryPostProcessor和BeanDefinitionRegistryPostProcessor的对象的相关方法(spring处理逻辑比较严谨,我这只是大致描述,如想深入了解细节可以参考spring源码)。

2,BeanPostProcessor:可以看出,该接口定义了两个方法,分别在bean实例化之后放到我们的容器之前和之后去执行,方法的返回值为一个object,这个object呢就是我们存在于容器的对象了(所以这个位置我们是不是可以对我们的bean做一个动态的修改,替换等等操作,所以这也是我们spring的扩展点之一,后面结合我么自己手写aop来详细讲解这个扩展点的应用)
在这里插入图片描述
3, ImportSelector
在讲ImportSelector之前呢,我想先讲一下@Import这个注解。在spring处理我们的java类的时候,会分成四种情况去处理
1)普通类:就是我们家里@Component,@Service,@Repository等等的类
2)处理我们的import进来的类:
这里呢,又分为三种情况:
a)import一个普通类:@Import(A.class)
b)import一个Registrar:比如我们的aop @Import(AspectJAutoProxyRegistrar.class)
c)import一个ImportSelector:具体妙用见下文

至于spring在什么时候处理的呢,我大致叙述一下,有兴趣的可以自己去研究下spring源码:
对于普通类,spring在doScan的时候,就将扫描出来的java类转换成我们的BeanDefinition,然后放入一个BeanDefinitionMap中去
对于@import的三种情况,处理就在我们的ConfigurationClassPostProcessor(该类是我们BeanDefinitionRegistryPostProcessor后置处理器的一个实现,同时这也是我们spring内部自己维护的唯一实现类(排除内部类)),具体处理import的核心代码如下,if-else 很容易可以看出spring对于我们import三种类型的处理。

/**
	 * 处理我们的@Import注解,注意我们的@Import注解传入的参数,可能有三种类型
	 * 1,传入一个class类,直接解析
	 * 2,传入一个registrar,需要解析这个registrar
	 * 3,传入一个ImporterSelector,这时候会去解析ImporterSelector的实现方法中返回的数组的class
	 * 		
	 */
	private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
			Collection<SourceClass> importCandidates, boolean checkForCircularImports) {

		if (importCandidates.isEmpty()) {
			return;
		}

		if (checkForCircularImports && isChainedImportOnStack(configClass)) {
			this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
		}
		else {
			this.importStack.push(configClass);
			try {
				for (SourceClass candidate : importCandidates) {
					//处理我们的ImportSelector
					if (candidate.isAssignable(ImportSelector.class)) {
						// Candidate class is an ImportSelector -> delegate to it to determine imports
						Class<?> candidateClass = candidate.loadClass();
						ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class);
						ParserStrategyUtils.invokeAwareMethods(
								selector, this.environment, this.resourceLoader, this.registry);
						if (this.deferredImportSelectors != null && selector instanceof DeferredImportSelector) {
							this.deferredImportSelectors.add(
									new DeferredImportSelectorHolder(configClass, (DeferredImportSelector) selector));
						}
						else {
							String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
							Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
							//注意可能我们ImportSelector传入的类上还有可能会Import,所以这里,spring采用了
							//一个递归调用,解析所有的import
							processImports(configClass, currentSourceClass, importSourceClasses, false);
						}
					}
					//处理我们的Registrar
					else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
						// Candidate class is an ImportBeanDefinitionRegistrar ->
						// delegate to it to register additional bean definitions
						Class<?> candidateClass = candidate.loadClass();
						ImportBeanDefinitionRegistrar registrar =
								BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class);
						ParserStrategyUtils.invokeAwareMethods(
								registrar, this.environment, this.resourceLoader, this.registry);
						//添加的一个和Importselector方式不同的map中,sprig对两种方式传入的类注册方式不同
						configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
					}
					else {
						// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
						// process it as an @Configuration class
						
						//最后如果是普通类,传入importStack后交由processConfigurationClass进行注册处理
						this.importStack.registerImport(
								currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
						processConfigurationClass(candidate.asConfigClass(configClass));
					}
				}
			}
			catch (BeanDefinitionStoreException ex) {
				throw ex;
			}
			catch (Throwable ex) {
				throw new BeanDefinitionStoreException(
						"Failed to process import candidates for configuration class [" +
						configClass.getMetadata().getClassName() + "]", ex);
			}
			finally {
				this.importStack.pop();
			}
		}
	}

4,实现思路:在我们开发过程中,无非就是明确需求,列出需要解决的问题,然后针对问题,找出解决方案就OK!

同样根据如上原理,下面我们便可以来模拟我们的springAop,如果有点基础的可能应该会知道,spring是基于我们的动态代理实现的(先不考虑是cglib还是jdk动态代理),结合我们aop使用(没用过的好去百度了),那么我们就需要解决如下几个问题:
a)我们知道开启和关闭aop需要注解@EnableAspectJAutoProxy,如何实现,结合上文,我们可以使用@import(ImportSelector.class)
b)如何确定代理关系,即哪些是我们需要代理的目标对象和其中的目标方法,以及哪些方法是要增强到目标对象的目标方法上去的?
c)如何实现目标对象的替换,就是我们在getBean的时候,如何根据目标对象来获取到我们增强后的代理对象?

如上问题都解决了,那么也就实现了我们的AOP.

5,根据如上思路,构建工程如下:
在这里插入图片描述
具体工程说明如下:
annotation:放我们所有的自定义注解
holder:自定义数据结构,具体类后面说
processor:放我们所有的后置处理器及代理相关
selector:放我们的ImportSelector的实现
util:工具类

要模拟aop,那么我们就要结合我们怎么去使用aop:
对于AOP,我们知道有一个开关注解类 @EnableAspectJAutoProxy(同样我们定义个@EnableAop),
注解@Aspect,@Before,@After。。。(注意这些都不是spring的注解,是Aspectj的注解,只是我们spring直接引用了而已,同样我们也对于新建自定义注解@AopJ,@BeforeBaomw,@AfterBaomw,@AfterBaomw。。。)

针对问题2,由于BeanFactoryPostProcessor的所有实现会在beanFactory完成对由于bean的扫描后,在实例化之前执行,所以我们可以新建一类,实现这个接口,然后实现方法里面主要完成对有BeanDefinition的扫描,找出我们所有的通知类,然后循环里面的方法,找到所有的通知方法,然后根据注解判断切入类型(也就是前置,后置还是环绕),最后解析注解的内容,扫描出所有的目标类,放入我们定义好的容器中。
具体实现如下:
(1)定义holder,用于描述通知信息

/**
 * 描述:
 *  自定义数据结构
 *
 * @author baomw
 * @create 2018-11-19 下午 4:56
 */
public class ProxyBeanHolder {
	//通知类名称
    private volatile String className;
    //通知方法名称
    private volatile String methodName;
    //注解类名称
    private volatile String annotationName;
	...
 < get and setter>
}

(2)定义数据工具类,具体作用见注释

 /**
     * 描述:
     *
     * @author baomw
     * @create 2018-11-19 下午 1:48
     */
    public class ConfigurationUtil {
    
        /**
         * aop标识注解类
         */
        public static final String AOP_POINTCUT_ANNOTATION
                                                = "com.baomw.annotation.AopJ";
        /**
         * 前置通知注解类
         */
        public static final String BEFORE
                                                = "com.baomw.annotation.BeforeBaomw";
        /**
         * 后置通知注解类
         */
        public static final String AFTER
                                                = "com.baomw.annotation.AfterBaomw";
        /**
         * 环绕通知注解类
         */
        public static final String AROUND
                                                = "com.baomw.annotation.AroundBaomw";
        /**
         * 存放需代理的全部目标对象类
         */
        public static volatile Map<String,List<ProxyBeanHolder>> classzzProxyBeanHolder = new ConcurrentHashMap<>();
    
    }

(3)定义我们的注册类,用于注册我们的目标对象和通知对象之间的关系,其核心代码如下,首先实现BeanFactoryPostProcessor ,保证其实在对所有的bean完成扫描后,在bean的实例化之前执行,然后再其中按上述思路,scan出所有的目标对象,然后建立起目标对象和通知对象的关联关系,然后放入我们的Map中

/**
 * 描述:
 *
 * @author baomw
 * @create 2018-11-19 下午 1:59
 */
@Component
public class RegisterBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    /**
     * 存放需要代理的相关信息类
     */
    public static volatile List<ProxyBeanHolder> roxyBeanHolderList = new Vector<>();

    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        //获取所有的bdName
        String[] beanDefinitionNames = configurableListableBeanFactory.getBeanDefinitionNames();
        for (String beanDefinitionName:beanDefinitionNames){
            BeanDefinition beanDefinition
                    = configurableListableBeanFactory.getBeanDefinition(beanDefinitionName);
            //判断bd是否是一个注解bd
            if (beanDefinition instanceof AnnotatedBeanDefinition) {
                //取得bd上的所有注解
                AnnotationMetadata metadata = ((AnnotatedBeanDefinition) beanDefinition).getMetadata();
                Set<String> Annotations = metadata.getAnnotationTypes();
                //循环所有注解,找到aop切面注解类
                for (String annotation:Annotations)
                    if (annotation.equals(ConfigurationUtil.AOP_POINTCUT_ANNOTATION))
                        doScan((GenericBeanDefinition)beanDefinition);
            }
        }
    }

如此问题二就得到了完美 的解决

针对问题3,我们可以利用BeanPostProcessor,在bean实例化之后,在放入容器之前,进行一个条件过滤,如果当前对象是我们的目标对象(即在我们定义好的Map中),则对对象进行代理,将目标对象替换成代理对象返回即可
(注:spring实现aop采用cglib和jdk动态代理两种方式,@EnableAspectJAutoProxy(proxyTargetClass=true)可以加开关控制,如果不加,目标对象如果有实现接口,则使用jdk动态代理,如果没有就采用cglib(因为我们知道cglib是基于继承的))

我们这里实现,都简单粗暴一点,统一采用cglib代理,这样就可以完成对任意对象的代理了。

具体实现如下:

/**
 * 描述:
 * aop实现核心处理类
 *
 * @author baomw
 * @create 2018-11-18 下午 11:24
 */
public class RealizedAopBeanPostProcessor implements BeanPostProcessor {


    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        String targetClass = bean.getClass().getName();
        Object object = bean;
        if (ConfigurationUtil.classzzProxyBeanHolder.containsKey(targetClass)){
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(object.getClass());
            enhancer.setCallback(new CustomizedProxyInterceptor(ConfigurationUtil.classzzProxyBeanHolder.get(targetClass)));
            object =  enhancer.create();
        }
        return object;
    }

    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

}

如此我们第三个问题也就顺利解决了最后,还剩下我们的问题1,这时候就可以引出我们的@import(ImportSelector.class)了
ImportSelector 接口有一个实现方法,返回一个字符串类型的数组,里面可以放类名,在@import(ImportSelector.class)的时候,spring会把我们返回方法里面的类全部注册到BeanDefinitionMap中,继而将对象注册到Spring容器中

/**
 * 描述:
 * 自定义aop实现,提交给spring处理的类
 *
 * @author baomw
 * @create 2018-11-18 下午 11:29
 */
public class CustomizedAopImportSelector implements ImportSelector {

    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        return new String[]{RealizedAopBeanPostProcessor.class.getName(),RegisterBeanFactoryPostProcessor.class.getName()};
    }
}
/**
 * 描述:
 * aop开关注解
 *
 * @author baomw
 * @create 2018-11-18 下午 11:21
 */
@Retention(RetentionPolicy.RUNTIME)
@Import(CustomizedAopImportSelector.class)
public @interface EnableAop {
}

很明显,如果我的Appconfig上加了@EnableAop注解,则会将我们的后置处理器的实现类交给了spring管理,spring才能去扫描得到这个类,才能去执行我们的自定义的后置处理器里面的方法,才能实现我们的aop的代理,因此,我们的开关也就顺利完成了。下面编写测试方法
定义我们的通知,对我们com.baomw.dao下面的所有类的所有方法进行代理
在这里插入图片描述
在这里插入图片描述
可以看到,已经对我们dao下面的所有方法完成了代理,由此,我们便已经完成了我们的SpringAOP了,只不过我这aop是一个山寨版的,功能比较简单,但是具体的实现原理是跟springaop的实现大相庭径的,spring处理的逻辑更缜密严谨(毕竟是大师和小菜鸡的区别,你们懂的!)

下面我们不妨来看看spring是怎么来实现aop的:
(1)这里和我们一样,也就是我们的开关接口,不过他@Import进来的是一个registrar,前面我已经提到过,sping是可以对我们import进来的registrar进行扫描注册的
在这里插入图片描述
在这里插入图片描述
(2)下面为我们aop实现BeanPostProcesser实现的postProcessBeforeInstantiation方法,可以看到,他也是判断bean是否在
Set targetSourcedBeans里面,如果在呢,就取到targetSource,然后createProxy,创建我们的代理对象。最后将我们的代理对象返回出去。
可见,其实现跟我们自己实现aop的思路一模一样,只是spring处理的更严谨而已

@Override
	public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
		Object cacheKey = getCacheKey(beanClass, beanName);

		if (!StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) {
			if (this.advisedBeans.containsKey(cacheKey)) {
				return null;
			}
			if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) {
				this.advisedBeans.put(cacheKey, Boolean.FALSE);
				return null;
			}
		}

		// Create proxy here if we have a custom TargetSource.
		// Suppresses unnecessary default instantiation of the target bean:
		// The TargetSource will handle target instances in a custom fashion.
		TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
		if (targetSource != null) {
			if (StringUtils.hasLength(beanName)) {
				this.targetSourcedBeans.add(beanName);
			}
			Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
			Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
			this.proxyTypes.put(cacheKey, proxy.getClass());
			return proxy;
		}

		return null;
	}

到这里,你是不是对springaop有了一个更深层次的了解呢!希望对大家有所启发,谢谢!

【源码地址:】https://github.com/baomw/spring-customized.git

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/192952.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • JS获取当前时间(精确到秒)「建议收藏」

    JS获取当前时间(精确到秒)「建议收藏」js获取当前日期currentDate(){vard=newDate();varyear=d.getFullYear();varmonth=d.getMonth();month=month+1>12?1:month+1;month=month>9?month:”0″+month.toString();var

  • 什么是DrawCall?「建议收藏」

    前言游戏开发圈里的人一定听过优化游戏要降低DrawCall,这样到底什么是DrawCall呢?Unity中应该如何降低DrawCall,这里就来讲解一下关于DrawCall知识点。1.是谁拖了后腿?通俗的来说就是Cpu:(#`O′)喂你好,是Gpu吗?快点醒醒我这里又有画画的任务了(Cpu调用Gpu的次数),打一个比方比如上传很多文件到百度云或其他地方时,都会把它压缩到一个文件夹里…

  • 软件测试流程及主要用例设计方法[通俗易懂]

    软件测试流程及主要用例设计方法[通俗易懂]软件测试流程及主要用例设计方法测试新手人门,首先要掌握测试的流程和实际运作项目流程和基础的用例设计方法。掌握测试和项目流程是了解研发过程中测试的主要工作;掌握最主要的用例设计方法就是掌握测试岗位最基本最核心的技能—如何测试。1.软件测试流程1.1测试流程测试流程:需求分析和讨论>编写测试计划>测试设计>测试执行>缺陷管理>测试报告。1)需求分析和讨论:分析…

  • WLAN基本知识之802.11标准「建议收藏」

    WLAN基本知识之802.11标准「建议收藏」文章目录WLAN技术基础1.4802.11标准介绍1.4.1IEEE802.11协议族成员1.4.2IEEE802.11标准与WiFi的世代1.4.3802.11a/b/g差异1.4.4802.11n1.4.5802.11n关键技术1.4.6IEEE802.11ac标准1.4.7IEEE802.ax标准(又称WiFi6)1.4.8WiFi6理论速率计算WLAN技术基础1.4802.11标准介绍1.4.1IEEE802.11协议族成员IEEE805.11无线工

  • rc522 nfc_基于单片机的门禁系统

    rc522 nfc_基于单片机的门禁系统文章目录1.前言(包括一些个人理解)1.前言(包括一些个人理解)(2021/11/1编辑)在项目需要做一个NFC门禁功能的时候,突然发现有个RC522丢在我的桌面,甚至不知道它上面的引脚什么意思(还不会SPI通讯),搜索关键词“RC522”去看博客搜索资料,发现了很多都在说扇区,块,S50(M1)卡,然后就给代码,一开始我还以为S50是内嵌在这个模块里面的一个存储器,然后越看越怪,后面去淘宝搜索S50,才发现S50其实是我们的门禁卡,RC522是用来感应和判断的。…

  • 据说练就了一指禅神功的觅闻实时手机新闻网,正以每天2000+IP的用户量递增。有智能手机的能够当场进行体验,没有的就算了哈

    据说练就了一指禅神功的觅闻实时手机新闻网,正以每天2000+IP的用户量递增。有智能手机的能够当场进行体验,没有的就算了哈

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号