相關背景及資源:html
曹工說Spring Boot源碼(1)-- Bean Definition究竟是什麼,附spring思惟導圖分享java
曹工說Spring Boot源碼(2)-- Bean Definition究竟是什麼,我們對着接口,逐個方法講解git
曹工說Spring Boot源碼(3)-- 手動註冊Bean Definition不比遊戲好玩嗎,咱們來試一下面試
曹工說Spring Boot源碼(4)-- 我是怎麼自定義ApplicationContext,從json文件讀取bean definition的?spring
曹工說Spring Boot源碼(5)-- 怎麼從properties文件讀取bean數據庫
曹工說Spring Boot源碼(6)-- Spring怎麼從xml文件裏解析bean的json
曹工說Spring Boot源碼(7)-- Spring解析xml文件,到底從中獲得了什麼(上)緩存
曹工說Spring Boot源碼(8)-- Spring解析xml文件,到底從中獲得了什麼(util命名空間)app
曹工說Spring Boot源碼(9)-- Spring解析xml文件,到底從中獲得了什麼(context命名空間上)框架
曹工說Spring Boot源碼(10)-- Spring解析xml文件,到底從中獲得了什麼(context:annotation-config 解析)
曹工說Spring Boot源碼(11)-- context:component-scan,你真的會用嗎(此次來講說它的奇技淫巧)
曹工說Spring Boot源碼(12)-- Spring解析xml文件,到底從中獲得了什麼(context:component-scan完整解析)
曹工說Spring Boot源碼(13)-- AspectJ的運行時織入(Load-Time-Weaving),基本內容是講清楚了(附源碼)
曹工說Spring Boot源碼(14)-- AspectJ的Load-Time-Weaving的兩種實現方式細細講解,以及怎麼和Spring Instrumentation集成
曹工說Spring Boot源碼(15)-- Spring從xml文件裏到底獲得了什麼(context:load-time-weaver 完整解析)
曹工說Spring Boot源碼(16)-- Spring從xml文件裏到底獲得了什麼(aop:config完整解析【上】)
曹工說Spring Boot源碼(17)-- Spring從xml文件裏到底獲得了什麼(aop:config完整解析【中】)
曹工說Spring Boot源碼(18)-- Spring AOP源碼分析三部曲,終於快講完了 (aop:config完整解析【下】)
工程結構圖:
本篇是接着前三篇講的,可是本篇相對獨立,即便不使用spring aop 和spring ioc,咱們也能夠利用今天要講的ProxyFactory爲咱們所用。
曹工說Spring Boot源碼(16)-- Spring從xml文件裏到底獲得了什麼(aop:config完整解析【上】)
曹工說Spring Boot源碼(17)-- Spring從xml文件裏到底獲得了什麼(aop:config完整解析【中】)
曹工說Spring Boot源碼(18)-- Spring AOP源碼分析三部曲,終於快講完了 (aop:config完整解析【下】)
前面幾篇說到,spring如何實現aop,即將匹配切點的bean,生成動態代理,並將生成的動態代理放到ioc容器,來替換原先的bean,一系列騷操做,完成"代理換真身"的操做。
比較老套的話題,可是,我問你們幾個問題,看看你們是否真的足夠了解他呢?
package foo; public class Performer implements Perform { @Override public void sing() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("男孩在唱歌"); } public void eat() { System.out.println("男孩在吃飯"); } }
能夠看到,咱們sing是實現了接口中的方法,而eat不在接口中定義。
那麼,以下代碼,結果會是啥:
@Test public void createJdkDynamicProxyManual() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { ClassLoader loader = Thread.currentThread().getContextClassLoader(); Object generatedProxy = Proxy.newProxyInstance(loader, new Class[]{Perform.class}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("proxy:" + proxy.getClass()); return "hahh"; } }); Method eat = Perform.class.getMethod("eat"); eat.setAccessible(true); eat.invoke(generatedProxy,null); }
代碼中,咱們建立了一個代理對象:generatedProxy;而後,調用了其eat方法,結果會是啥呢?
java.lang.NoSuchMethodException: foo.Perform.eat() at java.lang.Class.getMethod(Class.java:1665) at java.lang.Class.getMethod(Class.java:1665)
爲啥會這樣呢?由於咱們建立代理對象時,是在Perform.class這個接口上建立的。你們能夠再仔細看看。
這個問題,有人思考過嗎?簡單來講,其實有3個步驟。
我剛仔細看了Proxy.newProxyInstance的方法註釋:
Returns an instance of a proxy class for the specified interfaces that dispatches method invocations to the specified invocation handler. This method is equivalent to: Proxy.getProxyClass(loader, interfaces). // 對應步驟1和2 getConstructor(new Class[] { InvocationHandler.class }). // 對應步驟3 newInstance(new Object[] { handler }); // 對應步驟3
其中,第一步,細問一下,class是怎麼生成的,不少人估計又答不上了。我們這裏就看一下:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException { /* * Look up or generate the designated proxy class. */ Class<?> cl = getProxyClass0(loader, interfaces); /* * Invoke its constructor with the designated invocation handler. */ return newInstance(cons, ih); }
能夠看到,主要的獲取Class,是getProxyClass0方法,這個方法裏面代碼很多,去掉非核心的緩存等部分,核心的部分以下:
String proxyName = proxyPkg + "$Proxy" + num; /* * Generate the specified proxy class. */ byte[] proxyClassFile = ProxyGenerator.generateProxyClass( proxyName, interfaces); proxyClass = defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length);
這其中,ProxyGenerator.generateProxyClass 負責生成class的字節流,對應咱們前面講到的步驟1;defineClass0對應類加載。咱們仔細說說:
字節流生成
這部分呢,其實就是調用了ProxyGenerator.generateProxyClass,咱們跟蹤發現,它的全名爲:sun.misc.ProxyGenerator,是sun包下的。這部分無法看源碼,還好我以前下載過openjdk的源碼,這裏我給你們全文貼一下:
/** * Generate a class file for the proxy class. This method drives the * class file generation process. */ private byte[] generateClassFile() { /* ============================================================ * Step 1: Assemble ProxyMethod objects for all methods to * generate proxy dispatching code for. */ /* * Record that proxy methods are needed for the hashCode, equals, * and toString methods of java.lang.Object. This is done before * the methods from the proxy interfaces so that the methods from * java.lang.Object take precedence over duplicate methods in the * proxy interfaces. */ addProxyMethod(hashCodeMethod, Object.class); addProxyMethod(equalsMethod, Object.class); addProxyMethod(toStringMethod, Object.class); /* * Now record all of the methods from the proxy interfaces, giving * earlier interfaces precedence over later ones with duplicate * methods. */ for (int i = 0; i < interfaces.length; i++) { Method[] methods = interfaces[i].getMethods(); for (int j = 0; j < methods.length; j++) { addProxyMethod(methods[j], interfaces[i]); } } /* * For each set of proxy methods with the same signature, * verify that the methods' return types are compatible. */ for (List<ProxyMethod> sigmethods : proxyMethods.values()) { checkReturnTypes(sigmethods); } /* ============================================================ * Step 2: Assemble FieldInfo and MethodInfo structs for all of * fields and methods in the class we are generating. */ try { methods.add(generateConstructor()); for (List<ProxyMethod> sigmethods : proxyMethods.values()) { for (ProxyMethod pm : sigmethods) { // add static field for method's Method object fields.add(new FieldInfo(pm.methodFieldName, "Ljava/lang/reflect/Method;", ACC_PRIVATE | ACC_STATIC)); // generate code for proxy method and add it methods.add(pm.generateMethod()); } } methods.add(generateStaticInitializer()); } catch (IOException e) { throw new InternalError("unexpected I/O Exception"); } if (methods.size() > 65535) { throw new IllegalArgumentException("method limit exceeded"); } if (fields.size() > 65535) { throw new IllegalArgumentException("field limit exceeded"); } /* ============================================================ * Step 3: Write the final class file. */ /* * Make sure that constant pool indexes are reserved for the * following items before starting to write the final class file. */ cp.getClass(dotToSlash(className)); cp.getClass(superclassName); for (int i = 0; i < interfaces.length; i++) { cp.getClass(dotToSlash(interfaces[i].getName())); } /* * Disallow new constant pool additions beyond this point, since * we are about to write the final constant pool table. */ cp.setReadOnly(); ByteArrayOutputStream bout = new ByteArrayOutputStream(); DataOutputStream dout = new DataOutputStream(bout); try { /* * Write all the items of the "ClassFile" structure. * See JVMS section 4.1. */ // u4 magic; dout.writeInt(0xCAFEBABE); // u2 minor_version; dout.writeShort(CLASSFILE_MINOR_VERSION); // u2 major_version; dout.writeShort(CLASSFILE_MAJOR_VERSION); cp.write(dout); // (write constant pool) // u2 access_flags; dout.writeShort(ACC_PUBLIC | ACC_FINAL | ACC_SUPER); // u2 this_class; dout.writeShort(cp.getClass(dotToSlash(className))); // u2 super_class; dout.writeShort(cp.getClass(superclassName)); // u2 interfaces_count; dout.writeShort(interfaces.length); // u2 interfaces[interfaces_count]; for (int i = 0; i < interfaces.length; i++) { dout.writeShort(cp.getClass( dotToSlash(interfaces[i].getName()))); } // u2 fields_count; dout.writeShort(fields.size()); // field_info fields[fields_count]; for (FieldInfo f : fields) { f.write(dout); } // u2 methods_count; dout.writeShort(methods.size()); // method_info methods[methods_count]; for (MethodInfo m : methods) { m.write(dout); } // u2 attributes_count; dout.writeShort(0); // (no ClassFile attributes for proxy classes) } catch (IOException e) { throw new InternalError("unexpected I/O Exception"); } return bout.toByteArray(); }
這裏實際上是有面試題的,我以前還被問過,問我用的什麼技術來生成class字節流,這裏實際上是沒有用任何第三方工具的,這個類的import語句部分,也沒有asm、javaasist等工具。
import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.Array; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import sun.security.action.GetBooleanAction;
加載class字節流爲Class
這部分的代碼即爲前面提到的:
try { proxyClass = defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length); }
其中,defineClass0 是一個native方法:
java.lang.reflect.Proxy#defineClass0 private static native Class defineClass0(ClassLoader loader, String name, byte[] b, int off, int len);
讓我比較驚訝的是,這個native方法,是在Proxy類裏,且除了此處的調用,沒有被其餘代碼調用。
我去看了Classloader這個類的代碼,裏面也有幾個native的defineClass的方法:
private native Class defineClass0(String name, byte[] b, int off, int len, ProtectionDomain pd); private native Class defineClass1(String name, byte[] b, int off, int len, ProtectionDomain pd, String source); private native Class defineClass2(String name, java.nio.ByteBuffer b, int off, int len, ProtectionDomain pd, String source);
看來,Proxy是本身自立門戶啊,沒有使用Classloader類下面的defineClass等方法。
若是你們想看生成的class的文件的內容,能夠加這個虛擬機啓動參數:
-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
或者main最前面,加這個:
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
反射生成代理對象
這步就沒啥好說的了,通過上面第二步,已經拿到Class對象了。反射對於你們,也是輕車熟路了。
Constructor<?> cons = cl.getConstructor({ InvocationHandler.class }); final InvocationHandler ih = h; newInstance(cons, ih); private static Object newInstance(Constructor<?> cons, InvocationHandler h) { return cons.newInstance(new Object[] {h} ); }
這裏,咱們看到,獲取的構造函數,就是要接收一個InvocationHandler對象的。拿到了構造函數後,接下來,就調用了構造函數的newInstance,來生成代理對象。
具體的調用就不說了,反正你調用的任何方法(只能調用接口裏有的那些),都會轉到invocationHandler的invoke方法。
其實,你們看到上面,會想下面這個問題不?如今在代理對象上,調用方法,最終都會進入到:
java.lang.reflect.InvocationHandler#invoke public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
我若是想在這個邏輯裏面,去調用原始目標的方法,怎麼辦呢?
咱們看看傳給咱們的幾個參數:
1. proxy,代理對象;這個沒辦法拿到原始對象 2. method,是被調用的方法,也拿不到原始對象 3. args,給method的參數,也拿不到原始對象。
這就迷離了。那我咋辦呢?
答案是,在建立InvocationHandler時,把原始對象傳進去,以及其餘一切必要的信息,都傳進去。
固然,你也能夠不傳進去,在invoke方法裏,隨心所欲,好比下面的方法:
ClassLoader loader = Thread.currentThread().getContextClassLoader(); Object generatedProxy = Proxy.newProxyInstance(loader, new Class[]{Perform.class}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("到我這爲止,不會調用target了"); return null; } }); // 這裏,雖然調用了sing,但裏面的邏輯也不會執行。 ((Perform)generatedProxy).sing();
其實,這個代理,已經至關因而Perform接口的另外一個實現了;和以前的實現類,沒有半毛錢關係。
若是要讓它實施代理的工做,能夠這樣作:
@Test public void createJdkDynamicProxyManual() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { Performer performer = new Performer(); MyCustomInvocationHandler myCustomInvocationHandler = new MyCustomInvocationHandler(performer); ClassLoader loader = Thread.currentThread().getContextClassLoader(); Object generatedProxy = Proxy.newProxyInstance(loader, new Class[]{Perform.class}, myCustomInvocationHandler); ((Perform)generatedProxy).sing(); } public static class MyCustomInvocationHandler implements InvocationHandler { Performer performer; public MyCustomInvocationHandler(Performer performer) { this.performer = performer; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("我是一個稱職的代理"); return method.invoke(performer,args); } }
上面這個代碼,就沒問題了。會輸出以下:
我是一個稱職的代理
男孩在唱歌
咱們上面說了怎麼樣正確地實現代理的思路,就是要把target/原始bean,在new invocationHandler的時候,傳遞給它,後續在invoke裏再使用。咱們看看框架對invocationHandler的其餘實現,是怎麼作的吧?
我在project裏找了下InvocationHandler的實現類,發現了jdbc中的一個實現類。
org.springframework.jdbc.datasource.ConnectionProxy public interface ConnectionProxy extends Connection { /** * Return the target Connection of this proxy. * <p>This will typically be the native driver Connection * or a wrapper from a connection pool. * @return the underlying Connection (never {@code null}) */ Connection getTargetConnection(); }
這個是Connection的子接口,經過這個接口,獲取真正的數據庫鏈接。咱們看看其代理實現:
org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy#getConnection(java.lang.String, java.lang.String) public Connection getConnection(String username, String password) throws SQLException { return (Connection) Proxy.newProxyInstance( ConnectionProxy.class.getClassLoader(), new Class[] {ConnectionProxy.class}, new LazyConnectionInvocationHandler(username, password)); }
這個代理實現,主要是延遲獲取數據庫鏈接,等到使用的時候,纔去獲取鏈接;而不是啓動時,即創建鏈接池。
private class LazyConnectionInvocationHandler implements InvocationHandler { private String username; private String password; private Boolean readOnly = Boolean.FALSE; private Integer transactionIsolation; private Boolean autoCommit; private boolean closed = false; private Connection target; public LazyConnectionInvocationHandler() { this.autoCommit = defaultAutoCommit(); this.transactionIsolation = defaultTransactionIsolation(); } public LazyConnectionInvocationHandler(String username, String password) { this(); this.username = username; this.password = password; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // Invocation on ConnectionProxy interface coming in... if (method.getName().equals("equals")) { // We must avoid fetching a target Connection for "equals". // Only consider equal when proxies are identical. return (proxy == args[0]); } ... else if (method.getName().equals("getTargetConnection")) { // Handle getTargetConnection method: return underlying connection. return getTargetConnection(method); } ... }
這裏呢,若是方法爲getTargetConnection
,則調用瞭如下方法:
private Connection getTargetConnection(Method operation) throws SQLException { if (this.target == null) { // No target Connection held -> fetch one. if (logger.isDebugEnabled()) { logger.debug("Connecting to database for operation '" + operation.getName() + "'"); } // 經過用戶名,密碼去獲取數據庫鏈接 this.target = (this.username != null) ? getTargetDataSource().getConnection(this.username, this.password) : getTargetDataSource().getConnection(); // If we still lack default connection properties, check them now. checkDefaultConnectionProperties(this.target); // Apply kept transaction settings, if any. if (this.readOnly) { try { this.target.setReadOnly(this.readOnly); } catch (Exception ex) { // "read-only not supported" -> ignore, it's just a hint anyway logger.debug("Could not set JDBC Connection read-only", ex); } } if (this.transactionIsolation != null && !this.transactionIsolation.equals(defaultTransactionIsolation())) { this.target.setTransactionIsolation(this.transactionIsolation); } if (this.autoCommit != null && this.autoCommit != this.target.getAutoCommit()) { this.target.setAutoCommit(this.autoCommit); } } return this.target; } }
你們從上面代碼,能夠看到,是有經過用戶名密碼去獲取數據庫鏈接的。
因此,看來,正確的實現代理的思路就是,在構造proxy的時候,把你須要的東西,都經過構造函數或setter,傳遞給invocationHandler。而後再在invoke方法內去使用這些東西,來完成你的邏輯。
你們看了上面,以爲生成代理,簡單,仍是複雜呢?也許還不是很難。但若是是使用cglib的方式去建立代理,代碼可就要多好一些了。(這個留到後面講)
其實,spring裏給咱們提供了神器的,即咱們要說的:ProxyFactory。其註釋以下,意思是,aop代理工廠,不用經過bean factory,能夠直接使用。這個類提供一個簡單的獲取和配置aop代理的方式。
* Factory for AOP proxies for programmatic use, rather than via a bean * factory. This class provides a simple way of obtaining and configuring * AOP proxies in code.
意思是,咱們平時,實現aop,主要依靠spring的aop,即經過註解或者xml的方式,聲明式地建立aop(好比配置事務時)。這裏的意思是,咱們能夠經過代碼方式來實現一樣的效果,即,建立代理。
你們把這個類,理解爲代理工廠便可,工廠嘛,就是給它東西,它給你返回產品,這個產品,就是代理對象。
咱們看看,把它當成黑盒的話,如何利用它,來簡化咱們建立代理的過程:
@Test public void createJdkDynamicProxy() { ProxyFactory proxyFactory = new ProxyFactory(); // Performer performer = new Performer(); // proxyFactory.setTarget(performer); proxyFactory.addInterface(Perform.class); Perform proxy = (Perform) proxyFactory.getProxy(); log.info("proxy class:{}",proxy.getClass().getName()); proxy.sing(); log.info("proxy:{}",proxy); }
正常狀況下,按照咱們前面對jdk動態代理的理解,上面這樣就夠了。可是,上面代碼會報錯,說沒有指定target 對象。因此,咱們實際上,須要把上面那兩行註釋給放開,不然報以下錯誤。
org.springframework.aop.framework.AopConfigException: No advisors and no TargetSource specified at org.springframework.aop.framework.JdkDynamicAopProxy.<init>(JdkDynamicAopProxy.java:103) at org.springframework.aop.framework.DefaultAopProxyFactory.createAopProxy(DefaultAopProxyFactory.java:65) at org.springframework.aop.framework.ProxyCreatorSupport.createAopProxy(ProxyCreatorSupport.java:105) at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:98) at ProxyFactoryTest.createJdkDynamicProxy(ProxyFactoryTest.java:44)
上面放開那個註釋代碼後,默認就會去調用target的對應方法,會有以下輸出:
2020-02-25 08:32:29.828 [main] INFO ProxyFactoryTest - proxy class:com.sun.proxy.$Proxy5 男孩在唱歌 2020-02-25 08:32:30.910 [main] INFO ProxyFactoryTest - proxy:foo.Performer@502775a1
咱們上面只是建立了代理,默認去調用了target的對應方法,假設咱們要切一下,怎麼辦?
不慌!
@Test public void createJdkDynamicProxyWithAdvisor() { ProxyFactory proxyFactory = new ProxyFactory(); Performer performer = new Performer(); proxyFactory.setTarget(performer); proxyFactory.addInterface(Perform.class); DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(); advisor.setAdvice(new MethodInterceptor() { @Override public Object invoke(MethodInvocation invocation) throws Throwable { Object result = invocation.proceed(); System.out.println("男孩唱完要行禮"); return result; } }); proxyFactory.addAdvisor(advisor); Perform proxy = (Perform) proxyFactory.getProxy(); ProxyFactoryTest.log.info("proxy class:{}",proxy.getClass().getName()); proxy.sing(); }
這裏的重點代碼就是:
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(); advisor.setAdvice(new MethodInterceptor() { @Override public Object invoke(MethodInvocation invocation) throws Throwable { Object result = invocation.proceed(); System.out.println("男孩唱完要行禮"); return result; } }); proxyFactory.addAdvisor(advisor);
這上面的幾行代碼,主要是建立了一個advisor,一個advisor 幾乎等於切點+通知。
advisor的setAdvice呢,主要接受一個Advice類型的參數。而MethodInterceptor就是它的子接口。
固然了,其實advice的實現不少,包括spring裏都有不少內部實現。我這裏找了一個,對方法執行耗時,進行監測的。
我把上面的代碼改動了一行:
advisor.setAdvice(new PerformanceMonitorInterceptor());
這個類,的繼承關係以下:
主要功能就是記錄耗時,此時,輸出以下:
2020-02-25 08:40:06.825 [main] INFO ProxyFactoryTest - proxy class:com.sun.proxy.$Proxy5
男孩在唱歌
2020-02-25 08:40:07.868 [main] TRACE o.s.aop.interceptor.PerformanceMonitorInterceptor - StopWatch 'foo.Perform.sing': running time (millis) = 1006
今天大概講了jdk動態代理的原理,和ProxyFactory的使用。下一講,繼續aop之旅,主要講解ProxyFactory的原理。