javascript
框架复习(一):不如写个tiny-Spring?(完整版)
- 框架復習(一):不如寫個tiny-Spring?
- 項目來源
- IOC
- 為什么要有IOC?
- IOC部分要實現什么功能?
- 第0步:下載項目
- 第1步:最基本的容器
- 第2步:將bean創建放入工廠
- 第3步:為Bean注入屬性
- 第4步:讀取xml配置來初始化bean
- 第5步:為bean注入bean
- 第6步:ApplicationContext登場
- AOP
- 理解動態代理設計模式
- 理解AOP
- Spring AOP與Aspect
- 第7步:使用JDK動態代理實現AOP織入
- 第8步:使用AspectJ管理切面
- 第9.1步:完善Bean的生命周期
- 第9.2步:將AOP融入Bean的創建過程
- 第9.3步:目前還存在的問題
- 原作者代碼中的一個錯誤
- 修正錯誤后帶來的新問題
- 只能對接口代理
- 第9.4步:萬惡之源
- 第10步:使用CGLib進行類的織入
- 如何使用CGLib實現動態代理
- 第11步:通過三級緩存徹底解決循環依賴
- 小結
框架復習(一):不如寫個tiny-Spring?
項目來源
https://github.com/code4craft/tiny-spring
IOC
https://mp.weixin.qq.com/s?__biz=MzI4Njg5MDA5NA==&mid=2247484247&idx=1&sn=e228e29e344559e469ac3ecfa9715217&chksm=ebd74256dca0cb40059f3f627fc9450f916c1e1b39ba741842d91774f5bb7f518063e5acf5a0#rd
為什么要有IOC?
https://www.zhihu.com/question/23277575
- 什么是依賴?
依賴,可以粗暴地理解為import,如果代碼中import了某個類,那這段代碼就依賴了這個類。面向接口編程時,邏輯都是接口邏輯(例如接口IA,有方法doX,doY,接口邏輯例如是main中實例化了IA后,順序執行了doX和doY),但具體實例化的對象是IA接口的實現(例如類CA實現了IA,重寫了方法doX,doY)。如果不用工廠,直接new,那么main文件里面就必須import了CA,也就是main“依賴”了CA這個實現。而面向接口編程中,main應該跟CA解耦(就是不直接依賴CA,不會看到import CA)。工廠方法就是解決這種import CA的解決途徑之一,簡單工廠為例,原本main里面的IA a = new CA(),就變成了IA a = AFactory.getA(“CA”),并且getA的具體實現中,可以通過如果是字符串“CA”就new CA()返回了。這樣子的話,main里面就不用import CA了(但是要import AFactory),也即是不“依賴”CA,與CA解耦了。依賴注入,就是把上面的工廠,獲取CA對象的方式,變成反射(也還是根據字符串來生成對象,不過就不用簡單工廠if-else那么粗暴了,多一個if又要改一遍工廠的實現,多累啊),根據配置來生成對象。不用import某個實際類,但是也把依賴(邏輯過程實際執行還是CA來做的)給注入(放到main中)了。(上述的main指代任意一個邏輯執行過程,不一定是main函數)
- 依賴注入,把底層類作為參數傳入上層類,實現上層類對下層類的控制。A類中:@Autowird B b;那不論B類怎么改變,都不需要改變A類中的代碼。比如構造函數創建A(B b),還有Setter創建。反轉A類中不應該B b=new b(),而是應該從外界注入。
- 從哪個外界注入呢?Spring設計的是IOC容器,相當于是框架本身管理注入過程。相當于A需要B b的時候,框架就getBean(“b”)給A類。
- 如果A需要b,B需要a,怎么注入?控制反轉,交給IOC容器去解決。tiny-spring的實現思路:先根據xml獲得全部bean標簽內容,然后在getBean的時候再lazy-init。這樣A需要b時,會先創建A,當遇到b時轉而去創建b,最后在創建出完成的A。整個類似于一個遞歸(dfs)的過程。
- IOC,控制反轉,通過配置文件/注解自動對對象進行初始化
- 控制反轉解決了對象層級嵌套的問題,在創建一個對象時可以自動創建依賴對象并注入,Spring的IOC容器實現了從xml或注解中進行自動初始化。
- 控制反轉容器因為是自上而下創建實例的,因此不需要知道其依賴類的創建方法,屏蔽了內部的細節,從外部看像一個工廠。
IOC部分要實現什么功能?
第0步:下載項目
https://github.com/code4craft/tiny-spring
- 請用git clone下載,這樣才能夠通過git checkout step-1-container-register-and-get一步一步的查看不同版本。
第1步:最基本的容器
- 最基本的容器是指BeanFactory和BeanDefinition。前者有一個ConcurrentHashMap<String,BeanDefinition>,因為實現xml中字符串id對對象實例的映射。BeanDefinition包裝了Bean。
第2步:將bean創建放入工廠
-
Spring中Bean實例的生成是由容器控制的,而不是由用戶,因此Bean對象的創建要放在BeanFactory中。為了仿照Spring,因此抽象出FactoryBean接口,AbstractBeanFactory模板類。模板類中最重要的是protected doCreateBean()。
-
在注冊的時候通過反射調用doCreateBean方法創建對象,并放入BeanDefinition包裝類中。doCreateBean相當于是個動態工廠,根據string類型的全類名反射出一個Object對象。
public void registerBeanDefinition(String name,BeanDefinition beanDefinition){Object bean = doCreateBean(beanDefinition);beanDefinition.setBean(bean);beanDefinitionMap.put(name,beanDefinition); } -
到這一步就實現了BeanFactory的實現類可以通過全類名創建一個對象。
public class BeanFactoryTest {@Testpublic void test(){BeanFactory beanFactory = new AutowireCapableBeanFactory();BeanDefinition beanDefinition = new BeanDefinition();//創建一個包裝類beanDefinition.setBeanClassName("beans.Car");//通過反射創建,要求必須有無參構造函數beanFactory.registerBeanDefinition("audi",beanDefinition);//注冊到hashmap中,注冊之前先調用doCreateObject方法創建對象,實現了在Facoty中創建對象System.out.println((Car)beanFactory.getBean("audi"));} } -
在看上述代碼,我們要傳給factory什么?1.全類名,即beans.Car。2.實例化后的實例名稱,即audi。這兩項顯然我們都能在配置的xml中獲取,這在第四步中完成。其次,我們目前創建出的對象還是一個依靠無參構造函數創建的,因此內部成員變量均為null,所以下一步是對成員變量進行賦值。
第3步:為Bean注入屬性
-
這一步有兩個類,PropertyValues和PropertyValue。PV類相當于是C++中的Pair<String fieldName,Object Value>類,保存字段和字段對應的值。PVS中保存了一個對象中所有字段和值的對應關系,即保存了一個List。每個BeanDefinition中都有一個PVS,因此每個BeanDefinition在創建完空Bean后可以遍歷PVS,通過反射實現Setter。
protected void applyPropertyValues(Object bean,BeanDefinition mbd) throws NoSuchFieldException, IllegalAccessException {for(PropertyValue propertyValue:mbd.getPropertyValues().getPropertyValues()){Field declaredField = bean.getClass().getDeclaredField(propertyValue.getName());declaredField.setAccessible(true);declaredField.set(bean,propertyValue.getValue());} }
第4步:讀取xml配置來初始化bean
- 解決獲取IO流的問題?URL類定位xml文件,url.openConnect().connect()即可定位并打開文件,利用getInputStream獲得文件輸入流。
- 通過XMLBeanDefinitionReader類和DocumentBuilder對xml進行解析。先根據bean定位到所有的bean,根據類名和實例名構建一個空實例,然后每一個bean中定位property,利用PVS類和PV類實現對bean屬性的賦值
- 官方結構
第5步:為bean注入bean
- 核心解決三個問題1.ref怎么實現?2.怎么解決xml中順序問題?2.怎么避免循環依賴?
怎么實現ref?
-
這個問題好解決。判斷xml中是ref還是value,如果是value(本項目目前value如果是基本類型,只允許是String)則直接用PV(PropertyValue)封裝,如果是ref,就用BeanReference{name,bean}封裝一下然后再用PV封裝。
private void processProperty(Element ele, BeanDefinition beanDefinition) {NodeList propertyNode = ele.getElementsByTagName("property");for (int i = 0; i < propertyNode.getLength(); i++) {Node node = propertyNode.item(i);if (node instanceof Element) {Element propertyEle = (Element) node;String name = propertyEle.getAttribute("name");String value = propertyEle.getAttribute("value");if (value != null && value.length() > 0) {beanDefinition.getPropertyValues().addPropertyValue(new PropertyValue(name, value));} else {String ref = propertyEle.getAttribute("ref");if (ref == null || ref.length() == 0) {throw new IllegalArgumentException("Configuration problem: <property> element for property '"+ name + "' must specify a ref or value");}BeanReference beanReference = new BeanReference(ref);beanDefinition.getPropertyValues().addPropertyValue(new PropertyValue(name, beanReference));}}} } -
在調用applyPropertyValues()方法——通過反射裝填實例的成員變量時,如果該變量是BeanReference,則該變量有可能需要創建一下。
protected void applyPropertyValues(Object bean, BeanDefinition mbd) throws Exception {for (PropertyValue propertyValue : mbd.getPropertyValues().getPropertyValues()) {Field declaredField = bean.getClass().getDeclaredField(propertyValue.getName());declaredField.setAccessible(true);Object value = propertyValue.getValue();if (value instanceof BeanReference) {BeanReference beanReference = (BeanReference) value;value = getBean(beanReference.getName());}declaredField.set(bean, value);} } -
注意上述代碼中的value=getBean(beanReference.getName())。實例的創建過程有可能就在此刻完成。這里需要明確的是下圖:
BeanFactory beanFactory = new AutowireCapableBeanFactory(); for(Map.Entry<String,BeanDefinition> beanDefinitionEntry:xmlBeanDefinitionReader.getRegistry().entrySet()){beanFactory.registerBeanDefinition(beanDefinitionEntry.getKey(),beanDefinitionEntry.getValue()); } ((AutowireCapableBeanFactory) beanFactory).preInstantiateSingletons();
讀取xml后,所有的類信息都在XmlBeanDefinitionReader實例中,但是XmlBDFR中的beanDefinition們并沒有創建實例,即空有類信息(className,PropertyValues),但是bean為null。此時,如果遇到A實例a的b字段ref C實例c,但是此刻C實例c還未初始化,在裝配A實例a的b字段的時候,就會用getBean創建c。(為什么能創建c呢?因為在創建工廠后,緊接著的操作就是把xmlBDFR中的所有beanDefinition寫入工廠的ConcurrentHashMap中,即工廠也有了全部的信息,因此可以創建c。)
通過getBean時創建實例的這種lazy-init方式,實現了不依靠xml中順序。這樣再創建實例的時候如果實例的依賴還沒有創建,就先創建依賴。
所謂循環依賴是類似以下的情況
<bean name="outputService" class="com.sonihr.beans.OutputService"><property name="helloWorldService" ref="helloWorldService"></property> </bean><bean name="helloWorldService" class="com.sonihr.beans.HelloWorldServiceImpl"><property name="text" value="Hello World!"></property><property name="outputService" ref="outputService"></property> </bean>在doCreateBean中,創建完空的bean(空的bean表示空構造函數構造出的bean)后,就放入beanDefinition中,這樣a ref b,b ref a時,a ref b因此b先創建并指向a,此時的a還不是完全體,但是引用已經連上了,然后創建好了b。然后b ref a的時候,a已經創建完畢。
第6步:ApplicationContext登場
- 這一步就是用ApplicationContext包裝之前的代碼public void refresh() throws Exception {XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(new ResourceLoader());xmlBeanDefinitionReader.loadBeanDefinitions(configLocation);for (Map.Entry<String, BeanDefinition> beanDefinitionEntry : xmlBeanDefinitionReader.getRegistry().entrySet()) {beanFactory.registerBeanDefinition(beanDefinitionEntry.getKey(), beanDefinitionEntry.getValue());} }
- 這樣只要如下調用即可 ApplicationContext applicationContext = new ClassPathXmlApplicationContext("tinyioc.xml"); ((ClassPathXmlApplicationContext) applicationContext).refresh(); System.out.println(applicationContext.getBean("car2"));
AOP
https://mp.weixin.qq.com/s?__biz=MzI4Njg5MDA5NA==&mid=2247483954&idx=1&sn=b34e385ed716edf6f58998ec329f9867&chksm=ebd74333dca0ca257a77c02ab458300ef982adff3cf37eb6d8d2f985f11df5cc07ef17f659d4#rd
理解動態代理設計模式
-
靜態代理模式
通過構造函數注入的方式,將被代理類B的實例b注入Proxy中,然后Proxy實現A接口a方法時,在調用b.a()之前之后都可以寫自己的代理邏輯代碼。
-
動態代理模式
將接口A的字節碼文件+一個構造器,這個構造器繼承自Proxy,就構成了代理類的基本字節碼。Prxoy構造器中必然依賴InvocationHandler實例,這個InovocationHandler實例要重寫invoke方法以實現1.Proxy中所有A接口方法全部使用handler.invoke。2.hanler.invoke()調用被代理實例的a(),并且可以在其中寫代理邏輯。3.Proxy的a方法調用的invoke,則內部就代理a方法。
分解操作:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {//指定被代理類實例Car target = new Car();//指定類加載器和接口Class carClazz = Proxy.getProxyClass(target.getClass().getClassLoader(),Drivebale.class);//創建構造函數Constructor constructor = carClazz.getConstructor(InvocationHandler.class);//反射創建代理類實例Drivebale car = (Drivebale) constructor.newInstance(new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("前");method.invoke(target,args);System.out.println("后");return null;}});car.running(); }一句話版本:
public static void main(String[] args) {Car target = new Car();Drivebale car = (Drivebale) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("這就是JDK動態代理的正確用法");method.invoke(target,args);System.out.println("結束");return null;}});car.running(); } -
靜態代理和動態代理的區別
本質區別,靜態代理是在運行期之前就編譯好的class文件,動態代理是運行期中生成的class文件。
-
代理模式和裝飾模式的區別
- 相同點都是返回一個功能更豐富的類。
- 代理模式強調與被代理類無關的功能,比如被代理類是核心業務邏輯代碼,代理模式增加日志等輔助功能。包裝模式強調對被包裝類進行功能性的加強。
- 代理模式控制對方法的訪問,可以不讓訪問者知道被代理的對象,Thread(Runnable target),MyBatis的Mapper。裝飾著模式為方法添加額外的行為,一般通過構造函數注入,IO流。
-
代理模式的應用
-
靜態代理:Thread implements Runnable,Thread(Runnable target),thread.run{target.run}。我們只用關心Runnable的業務邏輯,而不用關系線程創建,銷毀等具體的事情。
-
動態代理:MyBatis中的Mapper。
https://blog.csdn.net/xiaokang123456kao/article/details/76228684
SqlSession session = sqlSessionFactory.openSession(); //獲取mapper接口的代理對象 UserMapper userMapper = session.getMapper(UserMapper.class); //調用代理對象方法 User user = userMapper.findUserById(27);比如UserMapper這個接口,如果要用靜態代理,就必須手動寫一個實現該接口的代理類,如果你有很多個接口,就要寫很多個代理類,工作量很大。但是采用動態代理后,XXXMapperProxy通過反射實現XxxMapper接口內方法并創建構造函數,創建后在invoke中實現邏輯。
-
理解AOP
https://blog.csdn.net/javazejian/article/details/56267036
-
為什么要有AOP?
- 在面向對象編程的這些年,我們遇到了一些問題。
參數檢查,日志,異常處理,事務開始和提交,這些都是重復代碼,怎么解決呢?面向切面編程,將這些功能抽取出來,然后定義point cut(切入點),在point cut上進功能的weaving織入,從而形成一個aspect切面。 -
專屬名詞
-
join point:Spring中每個方法都可以是join point
-
point cut:我們想要切入的那些join point
-
advice:通知,即代理邏輯代碼
-
aspect:point cut+advice等于一個切面
-
weaving:切面應用到目標函數的過程
-
Spring AOP與Aspect
https://zhuanlan.zhihu.com/p/24565766
-
Spring aop和Aspect不是一個東西
-
Aspect是一套獨立的面向切面編程的實現方案,通過編譯器實現靜態織入.
-
Spring AOP基于動態代理設計模式的動態織入,基礎技術為jdk動態代理和CGLIB技術,前者基于反射技術且只應用于接口,后者基于繼承了用于類。
-
Spring AOP使用了Aspect的部分內容(主要是實現XML配置解析和類似 Aspectj 注解方式的時候,借用了 aspectjweaver.jar 中定義的一些annotation 和 class),但是并沒有使用其編譯器和織入器,可以認為是Aspect風格的,但是實現完全不同。
-
AOP Alliance 是AOP的接口標準,定義了 AOP 中的基礎概念(Advice、CutPoint、Advisor等),目標是為各種AOP實現提供統一的接口,本身并不是一種 AOP 的實現。Spring AOP, GUICE等都采用了AOP Alliance中定義的接口,因而這些lib都需要依賴 aopalliance.jar。
第7步:使用JDK動態代理實現AOP織入
-
這一步我們就是利用之前說到的動態代理模式,幾乎一模一樣的完成織入。想一下,我們實現動態代理要用Proxy.newInstance,我們可以封裝一個動態代理類,就叫做JdkDynamicAopProxy implements InvocationHandler。由之前的動態代理知識可知,實現了InvocationHandler就必須實現invoke方法,那我們這樣寫:
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// MethodInterceptor methodInterceptor = advised.getMethodInterceptor();// return methodInterceptor.invoke(new ReflectiveMethodInvocation(advised.getTargetSource().getTarget(),method,args));System.out.println("方法開始");Object result = method.invoke(advised.getTargetSource().getTarget(),args);System.out.println("方法結束");return result; }這樣就可以將所有代理方法前后打印兩句話了。我們通過getProxy返回構造好的代理類:return Proxy(getClass.getClassLoader,new Class[]{target.getClass},this。因為本類就是InvocationHandler的實現類,因此最后一個用this即可。
-
我們知道想成功代理一個實例需要2個要素1.被代理的實例2.被代理的接口。我們用AdvisedSupport進行封裝,包括target、targetClass(其實應該是targetInterface)(前兩個被封裝進TargetSource,而TargetSource被封裝進AdvisedSupport)、methodInterceptor.等一下,methodInterceptor是個什么吊參數?
-
聰明的小朋友已經發現了,按照我們這樣寫,功能只能是對被代理的方法前后加一句話而已,那有沒有一種方式能讓我們能定制對方法調用時進行的控制?有,就是MethodInterceptor,即方法攔截器類。
http://aopalliance.sourceforge.net/doc/org/aopalliance/intercept/MethodInterceptor.html
- MethodInterceptor,環繞切點進行織入
- MethodBeforeAdvice,切點前側織入
- MethodAfterAdvice,切點后側織入
- ThrowsAdvice,切點的目標方法出現異常時調用
-
與上文采用的動態代理不同,我們可以通過配置攔截器來配置不同的代理邏輯。但是注意methodInterceptor.invoke方法中還有個methodInvovation,這個類用于調用我們的target的方法,因此這個類需要target實例,method和args。
-
所以其實啊MethodInvocation就是point cut,而MethodInterceptor就是advice,Invocation負責調用target方法即切點方法,Interceptor負責代理邏輯即advice。
-
這一步到此為止可以做到:1.寫一個實現MethodInterceptor的實現類,實現增強功能。2.實現對接口方法的代理。
// 1. 設置被代理對象(Joinpoint) AdvisedSupport advisedSupport = new AdvisedSupport(); TargetSource targetSource = new TargetSource(car,Driveable.class); advisedSupport.setTargetSource(targetSource);// 2. 設置攔截器(Advice) TimerInterceptor timerInterceptor = new TimerInterceptor(); advisedSupport.setMethodInterceptor(timerInterceptor);// 3. 創建代理(Proxy) JdkDynamicAopProxy jdkDynamicAopProxy = new JdkDynamicAopProxy(advisedSupport); Driveable carProxy = (Driveable)jdkDynamicAopProxy.getProxy();// 4. 基于AOP的調用 carProxy.running(); -
給出一個AOP采用的動態代理方式的小demo
class ReflectMethodInvocation implements MethodInvocation{private Method method;private Object target;private Object[] args;public ReflectMethodInvocation(Method method, Object target, Object[] args) {this.method = method;this.target = target;this.args = args;}@Overridepublic Method getMethod() {return method;}@Overridepublic Object[] getArguments() {return args;}@Overridepublic Object proceed() throws Throwable {return method.invoke(target,args);}@Overridepublic Object getThis() {return target;}@Overridepublic AccessibleObject getStaticPart() {return method;} }public class JdkAopNew {public static void main(String[] args) {Car car = new Car();MethodInterceptor methodInterceptor = new MethodInterceptor() {@Overridepublic Object invoke(MethodInvocation methodInvocation) throws Throwable {System.out.println("攔截器方式動態代理前");methodInvocation.proceed();System.out.println("后");return null;}};Drivebale drivebale = (Drivebale) Proxy.newProxyInstance(car.getClass().getClassLoader(), car.getClass().getInterfaces(), new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {return methodInterceptor.invoke(new ReflectMethodInvocation(method,car,args));}});drivebale.running();} }
第8步:使用AspectJ管理切面
-
第7步解決了怎么織入的問題,下面就是在哪里織入?Spring采用了AspectJ風格的標示性語句來表示在哪些位置進行織入,即哪些位置是point cut。類似下面的語句<aop:pointcut id="pointcut" expression="execution(public int aopxml.Calculator.*(int, int ))"/>。Spring可以對類和方法做插入,因此我們也要實現對類和方法表示point cut的功能。
-
先寫出ClassFilter接口和MethodMathcer接口,望文生義的說前者是類過濾器,后者是方法匹配器。具體怎么匹配呢?就在我們的AspectJExpressionPointcut中。
-
AspectJExpressionPintcut中要做這樣幾件事1.獲得String expression即AspectJ風格表達式2.創建PonitcutParser,即解析AspectJ風格表達式的解析器。3.expression被解析后就變成了pointcutExpression。即expression是輸入,pointcutParser是輸出,pointcutParser是解析器,將輸入解析成輸出。這個解析器怎么創建呢?直接new一個行不行啊?不行。正確的創建方式為:pointcutParser = PointcutParser.getPointcutParserSupportingSpecifiedPrimitivesAndUsingContextClassloaderForResolution(supportedPrimitives);后面的supportedPrimitives指的是執行的AspectJ語言風格的關鍵字,是一個set。那請問支持哪些關鍵字呢?去org.aspectj.weaver.tools包內的PointPrimitive就可以看奧。
可以看出pointcutExpression是對expression的封裝。
-
pointcutExpression是創建好了,但是有什么用呢?這個類可以用于匹配方法和類。
//匹配類 pointcutExpression.couldMatchJoinPointsInType(targetClass); //匹配方法 ShadowMatch shadowMatch = pointcutExpression.matchesMethodExecution(method); -
所以其實AspectJ包已經幫你做好了解析和匹配的事兒,只不過你不會用他的編譯器,你用動態代理的方式實現了織入。
-
AspectJExpressionPointcutAdvisor封裝了pointcut和advice,實現了一個完整的切面,切面=切點+advice。p.s.advice就是代理邏輯代碼。
第9.1步:完善Bean的生命周期
- 生命周期,最后還有一個destroy沒有顯示出來。
- BeanPostProcessor接口(下稱BPP接口)是AOP在Bean創建方面的應用——根據Spring的生命周期,BeanPostProcessor是在創建Bean的構造函數,setter方法后。并且所有BPP接口實例都不會受到BPP影響,即BPP的實例過程不會有before和after的影響。
- BPP接口實例要率先被實例化,并且實例化過程幾乎不會存在依賴ref。
- 一般實例的創建過程
第9.2步:將AOP融入Bean的創建過程
-
第7和第8步我們已經完成了AOP的point識別和識別后的織入,但是兩個功能沒有整合,同時也沒有和Spring的IOC整合起來。目的是為了,IOC給我們的容器已經不再是我們自己寫的實例,而是被織入了advice的實例——如果該類在pointcut則返回new JdkDynamicAopProxy,否則返回bean。
public Object postProcessAfterInitialization(Object bean, String beanName) throws Exception {if (bean instanceof AspectJExpressionPointcutAdvisor) {return bean;}if (bean instanceof MethodInterceptor) {return bean;}List<AspectJExpressionPointcutAdvisor> advisors = beanFactory.getBeansForType(AspectJExpressionPointcutAdvisor.class);for (AspectJExpressionPointcutAdvisor advisor : advisors) {if (advisor.getPointcut().getClassFilter().matches(bean.getClass())) {AdvisedSupport advisedSupport = new AdvisedSupport();advisedSupport.setMethodInterceptor((MethodInterceptor) advisor.getAdvice());advisedSupport.setMethodMatcher(advisor.getPointcut().getMethodMatcher());TargetSource targetSource = new TargetSource(bean, bean.getClass().getInterfaces());advisedSupport.setTargetSource(targetSource);return new JdkDynamicAopProxy(advisedSupport).getProxy();}}return bean; } -
第一幕:和9.1非常類似的,僅有標紅出不同。因為AspectJAwareAdvisorAutoProxyCreator implements BBP,BeanFactoryWare,因此不同僅僅是,因為實現了BeanFactoryAware接口,因此調用setFactory方法。這一步的目的是為了是的AspectAwareAdvisorAutoProxyCreator中具有beanFactory,方便從中獲取AspectJExpressionPointcutAdvisor.class類的實例。
-
第二幕:這一幕是目前為止最復雜也最重量級的。相當于把第9.1步和7,8兩步合起來了,歸納如下:
- 首先因為autoProxyCreator implements BBP,BeanFactoryAware,因此其必然先于所有一般實例和AOP實例創建,而且所有一般實例和AOP實例都必然要經過autoProxyCreator的before和after處理。
- 當實例化一般實例和AOP實例時,after中對實例進行檢查,如果其肯定不需要代理,比如說是提供代理pointcut與advice的AspectJExpressionPointcutAdvisor或是提供advice的methodInterceptor。如果和expression給出的表達式不匹配的類也不進行代理。對那些對expression匹配的類,就返回proxy類實例代替原來的bean。
- 小結:1.先通過BBP接口特性實現每個非BBP實例都必須經過BBP實例的before和after方法。2.正是因為BBP的這種特性,因此after方法中對非BBP實例進行檢查,如果和expression表示的point cut匹配則返回代理對象,否則返回原對象。
-
第一幕創建BBP實例,以編譯對所有非BBP實例進行before、after操作。第二幕通過判斷該類是否為point cut從而確認返回原實例還是代理實例,到第二幕已經將實例創建完畢。第三幕指的是,當實例調用接口方法時,如果該方法是pointcut,則會調用如下流程:
invocationHandler.invoke(proxy,method,args)調用methodInterceptor.invoke(methodInvocation),methodInterceptor內部進行1.代理編碼的實現2.函數參數methodInvocation調用proceed,從而執行被代理實例的method方法。因為methodInvocation要可以調用被代理實例的method,因此methodInvocation當你想要實現這個接口時,必須要指定被代理實例target,被代理實例的方法method和參數args。
第9.3步:目前還存在的問題
原作者代碼中的一個錯誤
- 來自Github原項目的Issues中:
https://github.com/code4craft/tiny-spring/issues/10
問題是:在進行測試的時候,發現調用非BPP實例的接口方法時,并沒有被代理。 - 什么原因呢?原Issues里面也說了,要加上一句beanDefinition.setBean(bean).這句話是不是有些眼熟?邏輯是這樣的:
- 先給出非AOP實例(即實例沒有pointcut)情況下,這部分的詳細邏輯
- 再給出有AOP(即有pointcut,需要代理)情況下,這部分的詳細邏輯
可以看到,第一次setBean實現了將beanDefinition.bean指向內存空間a。此時bean和beanDefinition.bean指向了同一塊內存區域,因此對bean的操作本質上是對內存空間a的操作,而beanDefinition.bean也指向這塊內存區域,因此對這塊區域propertyvalue的賦值不影響beanDefinition.bean的引用關系。
**但是!**當return new之后,bean已經指向了不同的內存空間b,beanDefinition.bean仍然指向內存空間a,因此需要重新set。
修正錯誤后帶來的新問題
-
來自Github原項目的Issues中:
https://github.com/code4craft/tiny-spring/issues/17
問題是:如果a ref b,b ref a,且順序也是這樣。 -
這個問題很奇怪。我們看看在實際Spring中的實驗效果:
- 實驗準備:
這些類望文知意我就不解釋了。
而且確實是循環依賴的
-
實驗過程:
-
實驗一:
可以看出,單獨的calculator和book都可以被正常代理。當然,在TinySpring中也是符合的。
-
實驗二:
在Spring中,接口實現類根本不用考慮這個問題,因為根本無法運行。邏輯在于,你獲得的A和B本質上都是代理類,代理類只實現了代理接口,因此無法強轉為某一個具體的實現類。所以A.B.b()和B.A.a()從本質上根本就不會強轉成功。
-
實驗三:
怎么樣才能正確進行這個實驗呢?上一個回答說到,不能進行實驗的本質是因為只能代理接口而不能代理類,所以Spring通過Cglib實現類代理。
通過proxy-target-class標記為true后,強制開啟cglib,此時再看實驗結果。
成功!
-
小結論實驗三證明,強制開啟Chlib后,可以進行本實驗,且Spring解決了循環依賴的問題。那原作者的tiny spring是不是進行第10步之后,就解決了呢?答案是沒有,因為Cglib只是讓我們的實驗可以正常進行,不代表能解決這個問題。Spring是通過三級緩存解決的。
上圖是第10步做完后的效果,發現問題還未解決。
只能對接口代理
- 只能對接口代理,為了對這個問題有深入的認識,我們舉出以下兩個例子:
- 例子1:CA implements A。CA類中出了有A接口的a()以外,還有c(),當動態代理后,返回的CA類實例是proxy,因此只能轉換為A類型,所以永遠無法使用c()。這要求,CA中所有方法必須實現A。
- 例子2:CA implements A,CB implements B。CA中有CB類型的成員變量,CB中有CA類型的成員變量。抱歉,不行。為什么?因為CA類型實例正在創建的過程中因為ca ref cb會先創建cb,但是cb返回的是proxy實例而不是CB實例,因此proxy實例無法賦值給cb。
第9.4步:萬惡之源
- 萬惡之源就是,AOP如果用動態代理實現,從根本上就意味著只能代理接口方法。有沒有一種方式可以代理類,而不僅僅是借口呢?抱歉,還真的有。
第10步:使用CGLib進行類的織入
如何使用CGLib實現動態代理
-
CGlib的原理是通過對字節碼的操作,可以動態的生成一個目標實例類的子類,這個子類和目標實例的子類相同的基礎上,還增加了代理代碼或者叫advice。代理類 = 被代理類+增強邏輯
- CGlib動態代理
- JDK動態代理。
-
與JDK動態代理的區別
- 原理上JDK沒有修改字節碼,而是采用$proxyn extend Proxy implements InterfaceXXX的方式創建了一個被代理接口的實現類,然后在運行期寫class文件,再用classloader加載。而CGlib卻是操作字節碼,將被代理類的字節碼增強成一個子類,因此要導入ASM包。
- 操作上,JDK動態代理創建為Proxy類實例,且必須要傳入InvocationHandler類,而Cglib創建為Enhancer類實例,且必須傳入MethodInterceptor類(注意包的問題,這個MethodInterceptor是CGlib中的)。
- Advice即代理代碼的實現上,JDK動態代理可以在InvocationHandler中重寫invoke實現,或者在InvocationHandler.invoke中調用methodInterceptor.invoke(methodInvocation),將代理的業務代碼交給methodInterceptor去做,被代理實例方法的運行通過參數methodInvocation.proceed()實現。而在CGlib中,通過methodInterceptor.intercept()實現代理增強,值得注意的是,這個方法內部有四個參數,包括一個被代理實例,而JDK的InvocationHandler.invoke卻不包含被代理實例。
- 在運行方法上,JDK代理類實例.a()的運行流程為先運行InvocationHandler.invoke,在invoke中運行methodInterceptor.invoke,在這個invoke中有代理邏輯代碼和methodInvocation.proceed(),從而實現代理邏輯與被代理實例方法的兩開花。而CGlib則是直接運行methodInterceptor.interceptor方法。注意,這一條很重要。
-
為什么說運行方法上的差異很重要呢,因為這會導致步驟9的代碼不可復用。因為我們原來寫的都是JDK代理類實例的那一套代碼,如果用CGlib的話,就無法通過注入org.aopalliance.intercept.MethodInterceptor的方式實現增強,而是注入cglib的MethodInterceptor,通過setCallback可以設置不同methodInterceptor。有沒有一種辦法,讓我們配置一種org.aopalliance.intercept.MethodInterceptor,在CGlib的情況下也可以調用它呢?
-
有啊,只要我們在cglib的methodInterceptor接口實現的intercept方法中調用org.aopalliance.intercept.MethodInterceptor不就好了。
private static class DynamicAdvisedInterceptor implements MethodInterceptor {private AdvisedSupport advised;private org.aopalliance.intercept.MethodInterceptor delegateMethodInterceptor;private DynamicAdvisedInterceptor(AdvisedSupport advised) {this.advised = advised;this.delegateMethodInterceptor = advised.getMethodInterceptor();}@Overridepublic Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {if (advised.getMethodMatcher() == null|| advised.getMethodMatcher().matches(method, advised.getTargetSource().getTargetClass())) {// return delegateMethodInterceptor.invoke(new ReflectiveMethodInvocation(advised.getTargetSource().getTarget(),method, args));return delegateMethodInterceptor.invoke(new CglibMethodInvocation(advised.getTargetSource().getTarget(), method, args, proxy));}// return method.invoke(advised.getTargetSource().getTarget(),args);可以這么寫,return new CglibMethodInvocation(advised.getTargetSource().getTarget(), method, args, proxy).proceed();} }現在只剩下一個疑問了,因為么要寫一個ReflectMothodInvocation的子類?因為intercept有4個入參,所以我們交給下一步處理的時候也要有4個入參,相當于增強了一下功能,當然你這邊不改也沒問題,就當做JDK那個版本就行。
第11步:通過三級緩存徹底解決循環依賴
-
廢話少說,先看結果
@Test public void testXuhuanyilai() throws Exception {// --------- helloWorldService without AOPApplicationContext applicationContext = new ClassPathXmlApplicationContext("tinyioc.xml");Car car = (Car) applicationContext.getBean("car");car.getAddress().living();Address address = (Address)applicationContext.getBean("address");address.getCar().running(); } //測試結果 Invocation of Method setCar start! Invocation of Method setCar end! takes 123111 nanoseconds. Invocation of Method setAddress start! Invocation of Method setAddress end! takes 38666 nanoseconds. Invocation of Method running start! car is running Invocation of Method running end! takes 45777 nanoseconds. Invocation of Method living start! address is living Invocation of Method living end! takes 56000 nanoseconds.實驗結果表示,我已經解決了第9.3步中說到的AOP情況下,循環依賴導致a ref b, b ref a時,創建實例時,b.a指向的是空實例a,而不是代理實例a。
-
解決方法。
-
三層緩存。
protected Map<String,Object> secondCache = new HashMap(); protected Map<String,Object> thirdCache = new HashMap<>(); protected Map<String,Object> firstCache = new HashMap<>(); -
thirdCache是當空構造函數創建一個實例時,就放入其中。
protected Object doCreateBean(String name,BeanDefinition beanDefinition) throws Exception {Object bean = createBeanInstance(beanDefinition);thirdCache.put(name,bean);//thirdCache中放置的全是空構造方法構造出的實例beanDefinition.setBean(bean);applyPropertyValues(bean,beanDefinition);return bean; } -
a ref b, b ref a情況下,在b創建時,a還只是空構造實例,因此用secondCache去保存所有field中指向空實例的那些實例,即保存b。
for(PropertyValue propertyValue:mbd.getPropertyValues().getPropertyValues()){ Object value = propertyValue.getValue(); if(value instanceof BeanReference){//如果是ref,就創建這個refBeanReference beanReference = (BeanReference)value;value = getBean(beanReference.getName());String refName = beanReference.getName();if(thirdCache.containsKey(refName)&&!firstCache.containsKey(refName)){//說明當前是循環依賴狀態secondCache.put(beanReference.getName(),bean);//標注a ref b,b ref a中,b是后被循環引用的} } -
firstCache用于保存所有最終被生成的實例.
initializeBean(): if(thirdCache.containsKey(name)){//空構造實例如果被AOP成代理實例,則放入三級緩存,說明已經構建完畢firstCache.put(name,bean); } -
因此,當執行完方法beanFactory.preInstantiateSingletons();后,thirdCache保存了所有空構造實例及名稱,secondCache保存了所有可能需要重新設置ref的實例及名稱,first保存了所有最終生成的實例和名稱。在firstcache與third中,必然存放了所有的bean,在second中只存放因循環依賴所以創建時ref了不完整對象的那些。在創建了所有實力后,通過checkoutAll方法對secondCache中的實例進行重置依賴。
protected void onRefresh() throws Exception{beanFactory.preInstantiateSingletons();checkoutAll(); }private void checkoutAll(){Map<String,Object> secondCache = beanFactory.getSecondCache();Map<String,BeanDefinition> beanDefinitionMap = beanFactory.getBeanDefinitionMap();for(Map.Entry<String,Object> entry:secondCache.entrySet()){String invokeBeanName = entry.getKey();BeanDefinition beanDefinition = beanDefinitionMap.get(invokeBeanName);try {resetReference(invokeBeanName,beanDefinition);} catch (Exception e) {e.printStackTrace();}} } //重置依賴,這邊用到了動態類型轉換。因為原類型的setter在代理類中已經無法使用了。 private void resetReference(String invokeBeanName,BeanDefinition beanDefinition) throws Exception {Map<String,Object> thirdCache = beanFactory.getThirdCache();Map<String,Object> secondCache = beanFactory.getSecondCache();Map<String,Object> firstCache = beanFactory.getFirstCache();Map<String,BeanDefinition> beanDefinitionMap = beanFactory.getBeanDefinitionMap();for (PropertyValue propertyValue : beanDefinition.getPropertyValues().getPropertyValues()) {String refName = propertyValue.getName();if (firstCache.containsKey(refName)) {//如果是ref,就創建這個refObject exceptedValue = firstCache.get(refName);Object invokeBean = beanDefinition.getBean();Object realClassInvokeBean = thirdCache.get(invokeBeanName);Object realClassRefBean = thirdCache.get(refName);try{Method declaredMethod = realClassInvokeBean.getClass().getDeclaredMethod("set" + propertyValue.getName().substring(0, 1).toUpperCase()+ propertyValue.getName().substring(1), realClassRefBean.getClass());declaredMethod.setAccessible(true);declaredMethod.invoke((realClassInvokeBean.getClass().cast(invokeBean)), (realClassRefBean.getClass().cast(exceptedValue)));}catch (NoSuchMethodException e){Field declaredField = realClassInvokeBean.getClass().getDeclaredField(propertyValue.getName());System.out.println(declaredField);declaredField.setAccessible(true);declaredField.set((realClassInvokeBean.getClass().cast(invokeBean)), (realClassRefBean.getClass().cast(exceptedValue)));}}} }
-
-
正如在9.3中說的那樣,只有在開啟全局cglib的情況下才可以完成本實驗,如果開啟jdk代理模式或者jdk代理+cglib都不會解決本bug。
小結
- IOC中通過讀xml用一個map,讀完才賦值給beanFactory的map的方式避免了xml因順序問題而導致的注入失敗。
- IOC中通過getBean懶加載+先空構造器創建實例的方式解決了循環依賴的問題(簡單解決而已,還未能解決增加AOP功能后循環依賴的問題。)。
- IOC本身1.因為都是注入,而不是在某一個類中new,因此系統耦合降低,所有的創建交給第三方容器,類似工廠模式2.IOC類的提供只需要xml注冊,創建的具體細節不需要你知道,程序更易維護和使用,因為你寫的代碼別人只要xml里注冊一下就能用你的實例。3.解決了對象層級嵌套的問題,a ref b,b ref c,c ref a,d ref b這樣復雜的嵌套關系,應該如何初始化?交給Spring!
- AOP中通過jdk動態代理模式實現了被代理實例代理方法的織入。
- AOP中通過AspectJ包完成了對AspectJ風格expression的解析,進一步完成了對類和方法ponitcut的判斷。
- AOP中通過BeanPostProcessor接口實現了一個完成的bean的生命周期中after和before的工作,這個并不是通過AOP完成的,而是通過邏輯代碼的流程控制完成的:確保所有實現BeanPostProcessor接口的實例都率先實例化。
- AOP中,所有ProxyCreator都實現BeanPostProcessor接口和BeanFactoryAware接口。前者接口保證自己率先被實例化,以保證對非AOP實例的before和after處理,后者接口保證在初始化自己的時候,會setBeanFactory,以用于后面獲取切面。
- AOP中,所有非AOP實例都必須經過ProxyCreator的after方法,proxyCreator中已經有了beanFactory,因此可以獲得所有expression對應的類pointcut,只要實例對應的類匹配類pointcut,就返回代理類實例而不是原實例。至此,全部實例創建工作完畢。
- AOP中,所有非AOP實例運行接口方法時,會按照invocationHandler.invoke(methodInterceptor.invoke(methodInvocation))邏輯進行調用,從而實現織入。
- AOP中,因為JDK代理只能針對接口,因此引入Cglib技術,實現類的動態代理。通過在cglib包的methodInterceptor中調用org.aopalliance.intercept.MethodInterceptor,實現了xml中配置的methodInterceptor對接口和類都可以使用。
- AOP中,最終通過三級緩存徹底解決了單例setter注入下的循環依賴。
- 沒有多少人有耐心看到這里的,我知道。晚安。
總結
以上是生活随笔為你收集整理的框架复习(一):不如写个tiny-Spring?(完整版)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: wps字体
- 下一篇: 【经济学视频课程】需求弹性的推导…