代理模式的定義:爲其它對象提供一種代理以控制對這個對象的訪問。html
這句話很是簡潔,一會兒可能感覺不到代理模式的強大,直到接觸大量業務後才能體會它的應用之豐富:java
監控:在原始方法先後進行記錄,能夠度量方法的處理時間、qps、參數分佈等;apache
限流:依據監控的數據對該方法的不一樣請求進行限流,特定一段時間內僅容許必定的訪問次數;segmentfault
重定向:依據參數判斷本地是否應該處理這個請求,是則處理,不然返回重定向信息;設計模式
......api
因此代理模式的精髓就在「控制」二字上面,而控制是有着很是豐富的含義的,下次須要控制的時候就能夠考慮一下代理了。oracle
假設咱們有一個服務app
public interface BusinessService { /** * 業務邏輯 * @param uid 用戶ID * @return 業務處理結果 */ String doJob(int uid); }
最開始實現以下,依據用戶ID和當前進程ID返回一段字符串dom
public class BusinessImpl implements BusinessService { public static int pid; static { String name = ManagementFactory.getRuntimeMXBean().getName(); String pids = name.split("@")[0]; pid = Integer.valueOf(pids); } @Override public String doJob(int uid){ return "businessLogic of user: "+uid+" processed by "+pid; } }
如今咱們想對doJob方法進行用戶分流:只有用戶ID和進程ID模5的結果相同,本進程才處理請求,不然給用戶返回失敗信息讓其尋找其它服務節點。怎麼實現呢?jvm
所謂靜態代理是指:定義接口或者父類,使得被代理對象與代理對象實現相同的接口或者是繼承相同父類,這種代理方式下,class文件是進程啓動前靜態生成(由java文件編譯而來)的,代理類以下:
public class BusinessImplNew implements BusinessService { // 被代理的原始類 private BusinessService business; public BusinessImplNew(BusinessService businessimpl){ this.business = businessimpl; } @Override public String doJob(int uid){ int pid = BusinessImpl.pid; if (pid % 5 == uid % 5){ return business.doJob(uid); }else { return "user "+uid+" not in this section "+pid; } } }
客戶代碼調用以下:
public class Main { public static void main(String[] args) throws InterruptedException { Random random = new Random(); // 原始 // BusinessService businessService = new BusinessImpl(); // 靜態代理 BusinessService businessService = new BusinessImplNew(new BusinessImpl()); while (true){ int uid = Math.abs(random.nextInt()); System.out.println(businessService.doJob(uid)); Thread.sleep(1000); } } } }
只需聲明一個service,代理類和原始類只是不一樣的實現而已,在Spring裏面能夠輕易的經過@Qualifier來實現不修改客戶代碼的代理改造。
這種代理的原理很是簡單,就是簡單的接口實現與委託,若是非要挖一挖,那就是多態了,方法實現是在運行時而非編譯時肯定的,具體能夠了解下 itable和vtable。
靜態代理有2個問題:一是必須提早寫好代理類,會致使大量的冗餘java代碼,若是不想寫那麼多代理類咋辦?二是代理類必須在被代理類和接口以後產生,若是代理邏輯對大量的業務類都適用,那麼我是否是能夠在全部業務類實現以前完成代理邏輯呢?
是動態代理上場的時候了。
import java.lang.reflect.Proxy; public class ProxyFactory { private Object target; public ProxyFactory(Object target) { this.target = target; } public Object getProxyInstance() { return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxy, method, args) -> { // 只攔截這一個方法 if (method.getName().equals("doJob")){ int pid = BusinessImpl.pid; int uid = (int) args[0]; if (pid % 5 == uid % 5) { return method.invoke(target, args); } else { return "user " + uid + " not in this section " + pid; } } return method.invoke(target,args); } ); } }
客戶代碼:
// 原始 // BusinessService businessService = new BusinessImpl(); // 動態代理 BusinessService businessService = (BusinessService) new ProxyFactory(new BusinessImpl()).getProxyInstance(); while (true){ int uid = Math.abs(random.nextInt()); System.out.println(businessService.doJob(uid)); Thread.sleep(1000); }
這種代理的原理就是動態生成與原有類繼承相同接口的類的字節碼,若是咱們在客戶代碼加入屬性
System._getProperties_().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
那麼生成的字節碼就會保存下來,咱們能夠直接用idea打開該字節碼,idea會幫咱們反編譯成Java代碼
package com.sun.proxy; public final class $Proxy0 extends Proxy implements BusinessService { private static Method m0; public $Proxy0(InvocationHandler var1) throws { super(var1); } public final String doJob(int var1) throws { try { return (String)super.h.invoke(this, m3, new Object[]{var1}); } catch (RuntimeException | Error var3) { throw var3; } catch (Throwable var4) { throw new UndeclaredThrowableException(var4); } } static { try { m3 = Class.forName("cn.mageek.BusinessService").getMethod("doJob", Integer.TYPE); } catch (NoSuchMethodException var2) { throw new NoSuchMethodError(var2.getMessage()); } catch (ClassNotFoundException var3) { throw new NoClassDefFoundError(var3.getMessage()); } } }
可見生成的類的確是實現了BusinessService,還繼承了來自Object的方法(這裏篇幅緣由省略了)。這裏doJob方法的實現就是咱們在newProxyInstance中傳入的InvocationHandler的invoke方法(這裏用Lambda簡化了),咱們觀察一下ProxyGenerator(代理類字節碼產生的邏輯)源碼便可和上面的結果驗證。
private byte[] generateClassFile() { visit(V14, accessFlags, dotToSlash(className), null, JLR_PROXY, typeNames(interfaces)); /* * Add proxy methods 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. 的確是都會生成這3個方法 */ addProxyMethod(hashCodeMethod); addProxyMethod(equalsMethod); addProxyMethod(toStringMethod); /* * Accumulate all of the methods from the proxy interfaces. */ for (Class<?> intf : interfaces) { for (Method m : intf.getMethods()) { if (!Modifier.isStatic(m.getModifiers())) { addProxyMethod(m, intf); } } } /* * 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); } generateConstructor(); for (List<ProxyMethod> sigmethods : proxyMethods.values()) { for (ProxyMethod pm : sigmethods) { // add static field for the Method object visitField(Modifier.PRIVATE | Modifier.STATIC, pm.methodFieldName, LJLR_METHOD, null, null); // Generate code for proxy method pm.generateMethod(this, className); } } generateStaticInitializer(); return toByteArray(); }
最後提一下,咱們能夠在InvocationHandler中依據方法/參數等的不一樣來制定各類個樣的代理策略(工廠方法+策略模式),而不須要寫大量的代理類。好比下面的例子就是對「doJob」方法採起uid模5的分流策略,對「doHardJob」方法採起uid後3位尾數模8的分流策略。
public class ProxyFactory { private Object target; /** * 須要攔截的方法及其具體攔截策略 */ private Map<String, InterceptionStrategy> methodInterceptionMap = new HashMap<>(); public ProxyFactory(Object target) { this.target = target; methodInterceptionMap.put("doJob",new Interception5()); methodInterceptionMap.put("doHardJob",new Interception8()); } public Object getProxyInstance() { return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxy, method, args) -> { InterceptionStrategy interception = methodInterceptionMap.get(method.getName()); // 不攔截 if (interception == null){ return method.invoke(target,args); } return interception.work(target,proxy,method,args); } ); } } interface InterceptionStrategy { Object work(Object target, Object proxy, Method method, Object[] args) throws Exception; } class Interception5 implements InterceptionStrategy { @Override public Object work(Object target, Object proxy, Method method, Object[] args) throws Exception { int pid = BusinessImpl.pid; int uid = (int) args[0]; if (pid % 5 == uid % 5) { return method.invoke(target, args); } else { return "user " + uid + " not in this section " + pid; } } } class Interception8 implements InterceptionStrategy { @Override public Object work(Object target, Object proxy, Method method, Object[] args) throws Exception { int pid = BusinessImpl.pid; int uid = (int) args[0] % 1000; if (pid % 8 == uid % 8) { return method.invoke(target, args); } else { return "user " + uid + " not in this section " + pid; } } }
靜態代理和動態代理的問題是必須依賴接口的存在,若是業務邏輯是這樣聲明的,它們就行不通了
public class BusinessImpl { ....
這時候就該Cglib上場了
import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class BusinessProxy implements MethodInterceptor { private Object target; public BusinessProxy(Object target) { this.target = target; } public Object getProxyInstance(){ Enhancer en = new Enhancer(); en.setSuperclass(target.getClass()); en.setCallback(this); return en.create(); } @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { // 只攔截這一個方法 if (method.getName().equals("doJob")){ int pid = BusinessImpl.pid; Integer uid = (Integer) args[0]; if (pid % 5 == uid % 5){ return method.invoke(target, args); }else { return "user "+uid+" not in this section "+pid; } }else { return method.invoke(target, args); } } }
客戶代碼
Random random = new Random(); // 原始無接口 BusinessImpl businessService = new BusinessImpl(); // Cglib代理 businessService = (BusinessImpl) new BusinessProxy(businessService).getProxyInstance(); while (true){ int uid = Math.abs(random.nextInt()); System.out.println(businessService.doJob(uid)); Thread.sleep(1000); }
這種代理的本質是經過繼承原有類/接口,對外呈現一致的相似於接口的行爲,從而實現代理。
一樣能夠經過參數設置,保存生成的class文件
System._setProperty_(DebuggingClassWriter._DEBUG_LOCATION_PROPERTY_, "./");
這個文件關鍵部分以下
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // public class BusinessImpl$$EnhancerByCGLIB$$869bd7f5 extends BusinessImpl implements Factory { private MethodInterceptor CGLIB$CALLBACK_0; public final String doJob(int var1) { MethodInterceptor var10000 = this.CGLIB$CALLBACK_0; if (this.CGLIB$CALLBACK_0 == null) { CGLIB$BIND_CALLBACKS(this); var10000 = this.CGLIB$CALLBACK_0; } return var10000 != null ? (String)var10000.intercept(this, CGLIB$doJob$0$Method, new Object[]{new Integer(var1)}, CGLIB$doJob$0$Proxy) : super.doJob(var1); } }
若是BusinessImpl聲明成final
public final class BusinessImpl { ......
上面幾種方法都無效了,由於既不能經過接口代理也不能經過繼承代理,這時候就是instrumentation上場的時候了,咱們能夠直接改變類的字節碼,從而改變類的行爲,天然就能夠實現代理的方法,嚴格意義上已經不能叫代理了,能夠作的事情更多。
修改字節碼不像修改java代碼那麼容易,是須要時機和工具的。
關於時機,JDK5開始,提供了premain,支持在jvm啓動前修改類的字節碼。
首先新建一個module或者項目
<groupId>**.**</groupId> <artifactId>agent</artifactId> <version>1.0-SNAPSHOT</version> <build> <!-- 包含資源 --> <resources> <resource> <directory>src/main/resources</directory> <includes> <!--包含文件夾以及子文件夾下全部資源--> <include>**/*.*</include> </includes> </resource> </resources> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.0.2</version> <configuration> <archive> <!--使用manifestFile屬性配置自定義的參數文件所在的--> <manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile> </archive> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <!--JDK6及以上才能用--> <configuration> <source>6</source> <target>6</target> </configuration> </plugin> <plugin> <!-- 依賴打包 --> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>2.10</version> <executions> <execution> <id>copy-dependencies</id> <phase>package</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <outputDirectory>${project.build.directory}/lib</outputDirectory> </configuration> </execution> </executions> </plugin> </plugins> </build> <!-- 後面修改字節碼要用到的依賴 --> <dependencies> <dependency> <groupId>javassist</groupId> <artifactId>javassist</artifactId> <version>RELEASE</version> <scope>compile</scope> </dependency> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>RELEASE</version> <scope>compile</scope> </dependency> </dependencies>
聲明premain:
public class PreMain { public static void premain(String agentArgs, Instrumentation inst){ System.out.println("agentArgs : " + agentArgs); inst.addTransformer(new Transformer(), true); System.out.println("Agent pre Done"); } }
在/src/main/resources/MEAT-INF/MANIFEST.MF中指明premain的相關參數:
Manifest-Version: 1.0 Can-Redefine-Classes: true Can-Retransform-Classes: true Premain-Class: **.**.PreMain
重寫一個BusinessImpl類:
public class BusinessImpl { public static int pid; static { // 耗時的操做 String name = ManagementFactory.getRuntimeMXBean().getName(); String pids = name.split("@")[0]; pid = Integer.valueOf(pids); } public String doJob(int uid){ if (pid % 5 == uid % 5){ return "agent businessLogic of user: "+uid+" processed by "+pid; }else { return "user "+uid+" not in this section "+pid; } } }
定義轉換類:
class Transformer implements ClassFileTransformer { // 防止遞歸替換 final AtomicBoolean first = new AtomicBoolean(true); @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { // 待修改的類 String toBeChanged = "BusinessImpl"; if (className.contains(toBeChanged) && first.get()){ System.out.println(className+",change behaviors"); first.set(false); // 用從新定義的類編譯的字節碼替換原有類的字節碼 return getBytesFromFile(toBeChanged); }else { // null 表示不更改字節碼 return null; } } byte[] getBytesFromFile(String className) { try { File file = new File("/Users/**/workspace/*/target/classes/*/"+className+".class"); InputStream is = new FileInputStream(file); long length = file.length(); byte[] bytes = new byte[(int) length]; int offset = 0; int numRead; while (offset <bytes.length && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) { offset += numRead; } if (offset < bytes.length) { throw new IOException("Could not completely read file " + file.getName()); } is.close(); return bytes; } catch (Exception e) { System.out.println("error occurs in _ClassTransformer!" + e.getClass().getName()); return null; } } }
將上述模塊打包(順便也就把新寫的BusinessImpl編譯成了class文件)。
而後再看客戶類:沒什麼變化,依然像之前同樣調用業務代碼:
Random random = new Random(); BusinessImpl businessService = new BusinessImpl(); while (true){ int uid = Math.abs(random.nextInt()); System.out.println(businessService.doJob(uid)); Thread.sleep(1000); }
只是啓動的時候要配置參數,指明agent所在的jar:
-javaagent:/Users/*/workspace/*/target/agent-1.0-SNAPSHOT.jar=version=1
這種代理的本質就是修改字節碼,天然能實現代理(甚至直接修改類的行爲)。
原理上,當目標jvm啓動並指明瞭 -javaagent參數,在加載類以前,agent中的premain方法就會首先被調用(先於目標jvm的main方法,從字面也能看出來含義),而後就會調用transform方法修改類的字節碼。
刨根問底,有人確定想問了,類是final的,而且jvm已經啓動了,怎麼進行代理?
JDK6開始提供了agentmain,支持在jvm啓動後修改類的字節碼。
一樣的在MANIFEST.MF中指明agentmain的相關參數:
Manifest-Version: 1.0 Can-Redefine-Classes: true Can-Retransform-Classes: true Agent-Class: **.**.AgentMain
而後修改BusinessImpl類,與premain不一樣的是,agentmain修改類有較多限制:
之因此有這麼多限制,按我理解,Java是一種強類型靜態語言。舉個例子,原本服務定義以下:
class BusinessImpl implements BusinessService; class BusinessImplNew implements BusinessService;
而後客戶也是多態地用:
BusinessService business = new BusinessImpl(); business.doJob; ...... BusinessService businessNew = new BusinessImplNew(); businessNew.doJob;
用戶程序跑的好好地,結果你寫個agent,把類聲明改了
class BusinessImpl
這時候客戶代碼第1行的存在就是一個矛盾,首先Java是強類型的,因此不容許不一樣類型進行隱式轉換,其次Java是靜態類型,因此客戶代碼的錯誤應該在編譯時就報錯了,但這時候程序已經運行,因此就矛盾了,爲了杜絕這種矛盾,天然不容許你作這種類的修改。
總結起來,運行時作類的修改,能夠修改實現,可是類自己對其它類暴露的"接口"應該在修改先後保持一致,否則就會把本屬於編譯時的錯誤引入到運行時,與Java的設計理念相違背,天然會被杜絕。這裏"接口"不僅是Java語言裏面的接口(implements 或者 extends),而是指對外部的行爲。
The retransformation may change method bodies, the constant pool and attributes. The retransformation must not add, remove or rename fields or methods, change the signatures of methods, or change inheritance. These restrictions maybe be lifted in future versions.
HotSpot的實現就沒有徹底遵照規範,其jvmtiRedefineClasses有以下判斷,能夠添加private static/final 方法。
static bool can_add_or_delete(Method* m) { // Compatibility mode return (AllowRedefinitionToAddDeleteMethods && (m->is_private() && (m->is_static() || m->is_final()))); }
爲何支持這個添加我也沒整明白,可是我理解這個是不該該的,由於即便private,對內部類也是可見的。估計官方也考慮到這點,因此在JDK13後加上了AllowRedefinitionToAddDeleteMethods參數,而且默認是false
回到具體操做上來,定義agentmain方法
public class AgentMain { public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException, InterruptedException { System.out.println("agentArgs : " + agentArgs); inst.addTransformer(new Transformer(), true); inst.retransformClasses(BusinessImpl.class); System.out.println("Agent Main Done"); } }
將該模塊打包成jar。
再看客戶代碼(就是是main方法所在類),依然正常使用以前的業務邏輯方式。
而後咱們新起一個jvm來修改目標jvm中想被代理的類的代碼:
public class Attach { public static void main(String[] args){ try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } // Lists the Java virtual machines known to this provider. List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list) { // 找到目標jvm if (vmd.displayName().endsWith("Main")) { try { VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); // 加載agent來修改字節碼 virtualMachine.loadAgent("/Users/*/workspace/*/target/agent-1.0-SNAPSHOT.jar", "version=2"); virtualMachine.detach(); } catch (AgentLoadException | AgentInitializationException | IOException | AttachNotSupportedException e) { e.printStackTrace(); } } } } }
此時能看到目標jvm的業務發生了變化:
businessLogic of user: 133033937 processed by 78344 businessLogic of user: 2033106653 processed by 78344 agentArgs : version=2 //BusinessImpl,change behaviors Agent Main Done user 1594461603 not in this section 78344 agent businessLogic of user: 1837400384 processed by 78344 user 563122492 not in this section 78344 user 2005311866 not in this section 78344 agent businessLogic of user: 631373574 processed by 78344 agent businessLogic of user: 1439396359 processed by 78344
那麼問題又來了
1.上面咱們看得出字節碼修改的邏輯是在目標jvm發生的,若是這段邏輯拋了異常對目標jvm有影響嗎?修改transform方法:
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { System.out.println("load Class :" + className); // 待修改的類 String toBeChanged = "BusinessImpl"; if (className.contains(toBeChanged) && first.get()){ System.out.println(className+",change behaviors,type:"+type); // 直接拋異常 System.out.println("throw rt exception"); throw new RuntimeException("can wen crash?"); } return null; }
結果以下,可見拋出異常致使修改失敗了,可是對原有代碼邏輯沒有影響,等於沒修改(premain也同樣)。
businessLogic of user: 1277419295 processed by 26152 businessLogic of user: 1554140834 processed by 26152 agentArgs : version=2 load Class :/BusinessImpl type:2 /BusinessImpl,change behaviors,type:2 throw rt exception Agent Main Done businessLogic of user: 348179760 processed by 26152 businessLogic of user: 337107270 processed by 26152
2.若是在修改的代碼中拋出異常會咋樣?修改方法
private String createJavaString() { StringBuilder sb = new StringBuilder(); sb.append("{if (pid % 5 != uid % 5){"); sb.append("throw new RuntimeException(\"can wen crash?\");}}"); return sb.toString(); }
結果以下,不出所料,既然你改了目標jvm的代碼,那麼拋出的異常確定是能感知到的(premain天然也同樣)。
businessLogic of user: 2049987640 processed by 26182 businessLogic of user: 1456301228 processed by 26182 agentArgs : version=2 load Class :cn/mageek/BusinessImpl cn/mageek/BusinessImpl,change behaviors insert string: {if (pid % 5 != uid % 5){throw new RuntimeException("can wen crash?");}} Agent Main Done ...... Exception in thread "main" java.lang.RuntimeException: can wen crash?
關於異常premain和agentmain不同的地方在於,若是premian這個agent不規範(好比字節碼錯誤)或者premain方法自己拋出了異常,目標jvm直接就不能啓動;而agentmain的目標jvm則會直接忽略之,特別地,當agentmain方法在inst.retransformClasses(BusinessImpl.class);以後拋出異常,修改仍是成功的。
上面講了修改字節碼的時機,下面來說講修改字節碼的工具。
也就是上面用的方式,經過重寫這個類,加上攔截邏輯,而後覆蓋掉原有類的字節碼,這種方式須要重寫原有代碼業務邏輯,顯然只能看成demo看看,不具有實際可行性。
javasist支持Java代碼和字節碼兩種級別的修改方式,修改Java代碼的方式更加簡單易懂也更流行,以下:
byte[] javaAsistCode(String className,byte[] classfileBuffer){ ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ByteArrayClassPath(className,classfileBuffer)); CtClass cclass; try { cclass = pool.get(className.replaceAll("/", ".")); } catch (NotFoundException e) { System.out.println("no class:"+className); e.printStackTrace(); return null; } String insert = "empty"; if (!cclass.isFrozen()) { for (CtMethod currentMethod : cclass.getDeclaredMethods()) { if (currentMethod.getName().equals("doJob")){ try { insert = createJavaString(); currentMethod.insertBefore(insert); } catch (CannotCompileException e) { e.printStackTrace(); } } } }else { System.out.println("class isFrozen:"+className); } System.out.println("insert string: "+insert); try { return cclass.toBytecode(); } catch (IOException e) { e.printStackTrace(); } catch (CannotCompileException e) { e.printStackTrace(); } return null; } private String createJavaString() { StringBuilder sb = new StringBuilder(); sb.append("{if (pid % 5 != uid % 5){"); sb.append("return \"user \"+uid+\" not in this section \"+pid;}}"); return sb.toString(); }
修改transform方法
@Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { // 待修改的類 String toBeChanged = "BusinessImpl"; if (className.contains(toBeChanged) && first.get()){ System.out.println(className+",change behaviors"); first.set(false); // 用加強的字節碼替換原有類的字節碼 return javaAsistCode(className,classfileBuffer); } return null; }
還能夠用Cglib直接進行字節碼修改,這種方式的問題是須要對字節碼很是瞭解,我這裏就沒有繼續追究下去了。
假設咱們有一個服務,想對他進行代理:
若是應用還處於開發階段
若是應用已經開發完畢並上線