日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

AOP的实现机制--转

發布時間:2025/4/5 编程问答 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 AOP的实现机制--转 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

原文地址:http://www.iteye.com/topic/1116696

1 AOP各種的實現

?

AOP就是面向切面編程,我們可以從幾個層面來實現AOP。

?

?

在編譯器修改源代碼,在運行期字節碼加載前修改字節碼或字節碼加載后動態創建代理類的字節碼,以下是各種實現機制的比較。?

?

類別

機制

原理

優點

缺點

靜態AOP

靜態織入

在編譯期,切面直接以字節碼的形式編譯到目標字節碼文件中。

對系統無性能影響。

靈活性不夠。

動態AOP

動態代理

在運行期,目標類加載后,為接口動態生成代理類,將切面植入到代理類中。

相對于靜態AOP更加靈活。

切入的關注點需要實現接口。對系統有一點性能影響。

動態字節碼生成

在運行期,目標類加載后,動態構建字節碼文件生成目標類的子類,將切面邏輯加入到子類中。

沒有接口也可以織入。

擴展類的實例方法為final時,則無法進行織入。

自定義類加載器

在運行期,目標加載前,將切面邏輯加到目標字節碼里。

可以對絕大部分類進行織入。

代碼中如果使用了其他類加載器,則這些類將不會被織入。

字節碼轉換

在運行期,所有類加載器加載字節碼前,前進行攔截。

可以對所有類進行織入。

?



2 AOP里的公民??

  • Joinpoint:攔截點,如某個業務方法。
  • Pointcut:Joinpoint的表達式,表示攔截哪些方法。一個Pointcut對應多個Joinpoint。
  • Advice:? 要切入的邏輯。
  • Before Advice 在方法前切入。
  • After Advice 在方法后切入,拋出異常時也會切入。
  • After Returning Advice 在方法返回后切入,拋出異常則不會切入。
  • After Throwing Advice 在方法拋出異常時切入。
  • Around Advice 在方法執行前后切入,可以中斷或忽略原有流程的執行。 ?
  • 公民之間的關系?

    織入器通過在切面中定義pointcut來搜索目標(被代理類)的JoinPoint(切入點),然后把要切入的邏輯(Advice)織入到目標對象里,生成代理類。?

3 AOP的實現機制?
? 本章節將詳細介紹AOP有各種實現機制。


3.1 動態代理?
? Java在JDK1.3后引入的動態代理機制,使我們可以在運行期動態的創建代理類。使用動態代理實現AOP需要有四個角色:被代理的類,被代理類的接口,織入器,和InvocationHandler,而織入器使用接口反射機制生成一個代理類,然后在這個代理類中織入代碼。被代理的類是AOP里所說的目標,InvocationHandler是切面,它包含了Advice和Pointcut。?


3.1.1 使用動態代理?
? 那如何使用動態代理來實現AOP。下面的例子演示在方法執行前織入一段記錄日志的代碼,其中Business是代理類,LogInvocationHandler是記錄日志的切面,IBusiness, IBusiness2是代理類的接口,Proxy.newProxyInstance是織入器。?
清單一:動態代理的演示

Java代碼??
  • public?static?void?main(String[]?args)?{???
  • ????//需要代理的接口,被代理類實現的多個接口都必須在這里定義???
  • ????Class[]?proxyInterface?=?new?Class[]?{?IBusiness.class,?IBusiness2.class?};???
  • ????//構建AOP的Advice,這里需要傳入業務類的實例???
  • ????LogInvocationHandler?handler?=?new?LogInvocationHandler(new?Business());???
  • ????//生成代理類的字節碼加載器???
  • ????ClassLoader?classLoader?=?DynamicProxyDemo.class.getClassLoader();???
  • ????//織入器,織入代碼并生成代理類???
  • ????IBusiness2?proxyBusiness?=?(IBusiness2)?Proxy.newProxyInstance(classLoader,?proxyInterface,?handler);???
  • ????//使用代理類的實例來調用方法。???
  • ????proxyBusiness.doSomeThing2();???
  • ????((IBusiness)?proxyBusiness).doSomeThing();???
  • }???
  • ??
  • /**??
  • *?打印日志的切面??
  • */???
  • public?static?class?LogInvocationHandler?implements?InvocationHandler?{???
  • ??
  • ????private?Object?target;?//目標對象???
  • ??
  • ????LogInvocationHandler(Object?target)?{???
  • ????????this.target?=?target;???
  • ????}???
  • ??
  • ????@Override???
  • ????public?Object?invoke(Object?proxy,?Method?method,?Object[]?args)?throws?Throwable?{???
  • ????????//執行原有邏輯???
  • ????????Object?rev?=?method.invoke(target,?args);???
  • ????????//執行織入的日志,你可以控制哪些方法執行切入邏輯???
  • ????????if?(method.getName().equals("doSomeThing2"))?{???
  • ????????????System.out.println("記錄日志");???
  • ????????}???
  • ????????return?rev;???
  • ????}???
  • }???
  • ??
  • 接口IBusiness和IBusiness2定義省略。???
  • ?

    ?? 業務類,需要代理的類。

    Java代碼??
  • public?class?Business?implements?IBusiness,?IBusiness2?{???
  • ??
  • ????@Override???
  • ????public?boolean?doSomeThing()?{???
  • ????????System.out.println("執行業務邏輯");???
  • ????????return?true;???
  • ????}???
  • ??
  • ????@Override???
  • ????public?void?doSomeThing2()?{???
  • ????????System.out.println("執行業務邏輯2");???
  • ????}???
  • ??
  • }???
  • ?

    ?? 輸出

    Java代碼??
  • 執行業務邏輯2???
  • 記錄日志???
  • 執行業務邏輯???
  • ?

    ? 可以看到“記錄日志”的邏輯切入到Business類的doSomeThing方法前了。


    ?

    3.1.2 動態代理原理?
    ??? 本節將結合動態代理的源代碼講解其實現原理。動態代理的核心其實就是代理對象的生成,即Proxy.newProxyInstance(classLoader, proxyInterface, handler)。讓我們進入newProxyInstance方法觀摩下,核心代碼其實就三行。?
    清單二:生成代理類

    Java代碼??
  • //獲取代理類???
  • Class?cl?=?getProxyClass(loader,?interfaces);???
  • //獲取帶有InvocationHandler參數的構造方法???
  • Constructor?cons?=?cl.getConstructor(constructorParams);???
  • //把handler傳入構造方法生成實例???
  • return?(Object)?cons.newInstance(new?Object[]?{?h?});?????
  • ?

    ??? 其中getProxyClass(loader, interfaces)方法用于獲取代理類,它主要做了三件事情:在當前類加載器的緩存里搜索是否有代理類,沒有則生成代理類并緩存在本地JVM里。清單三:查找代理類。

    Java代碼??
  • ?//?緩存的key使用接口名稱生成的List???
  • Object?key?=?Arrays.asList(interfaceNames);???
  • synchronized?(cache)?{???
  • ????do?{???
  • Object?value?=?cache.get(key);???
  • ?????????//?緩存里保存了代理類的引用???
  • if?(value?instanceof?Reference)?{???
  • ????proxyClass?=?(Class)?((Reference)?value).get();???
  • }???
  • if?(proxyClass?!=?null)?{???
  • //?代理類已經存在則返回???
  • ????return?proxyClass;???
  • }?else?if?(value?==?pendingGenerationMarker)?{???
  • ????//?如果代理類正在產生,則等待???
  • ????try?{???
  • cache.wait();???
  • ????}?catch?(InterruptedException?e)?{???
  • ????}???
  • ????continue;???
  • }?else?{???
  • ????//沒有代理類,則標記代理準備生成???
  • ????cache.put(key,?pendingGenerationMarker);???
  • ????break;???
  • }???
  • ????}?while?(true);???
  • }???
  • ??

    代理類的生成主要是以下這兩行代碼。 清單四:生成并加載代理類

    ?

    Java代碼??
  • //生成代理類的字節碼文件并保存到硬盤中(默認不保存到硬盤)???
  • proxyClassFile?=?ProxyGenerator.generateProxyClass(proxyName,?interfaces);???
  • //使用類加載器將字節碼加載到內存中???
  • proxyClass?=?defineClass0(loader,?proxyName,proxyClassFile,?0,?proxyClassFile.length);???
  • ?

    ? ProxyGenerator.generateProxyClass()方法屬于sun.misc包下,Oracle并沒有提供源代碼,但是我們可以使用JD-GUI這樣的反編譯軟件打開jre\lib\rt.jar來一探究竟,以下是其核心代碼的分析。?
    清單五:代理類的生成過程

    Java代碼??
  • //添加接口中定義的方法,此時方法體為空???
  • for?(int?i?=?0;?i?<?this.interfaces.length;?i++)?{???
  • ??localObject1?=?this.interfaces[i].getMethods();???
  • ??for?(int?k?=?0;?k?<?localObject1.length;?k++)?{???
  • ?????addProxyMethod(localObject1[k],?this.interfaces[i]);???
  • ??}???
  • }???
  • ??
  • //添加一個帶有InvocationHandler的構造方法???
  • MethodInfo?localMethodInfo?=?new?MethodInfo("<init>",?"(Ljava/lang/reflect/InvocationHandler;)V",?1);???
  • ??
  • //循環生成方法體代碼(省略)???
  • //方法體里生成調用InvocationHandler的invoke方法代碼。(此處有所省略)???
  • this.cp.getInterfaceMethodRef("InvocationHandler",?"invoke",?"Object;?Method;?Object;")???
  • ??
  • //將生成的字節碼,寫入硬盤,前面有個if判斷,默認情況下不保存到硬盤。???
  • localFileOutputStream?=?new?FileOutputStream(ProxyGenerator.access$000(this.val$name)?+?".class");???
  • localFileOutputStream.write(this.val$classFile);???
  • ?

    ? 那么通過以上分析,我們可以推出動態代理為我們生成了一個這樣的代理類。把方法doSomeThing的方法體修改為調用LogInvocationHandler的invoke方法。?
    清單六:生成的代理類源碼

    ?

    Java代碼??
  • public?class?ProxyBusiness?implements?IBusiness,?IBusiness2?{???
  • ??
  • private?LogInvocationHandler?h;???
  • ??
  • @Override???
  • public?void?doSomeThing2()?{???
  • ????try?{???
  • ????????Method?m?=?(h.target).getClass().getMethod("doSomeThing",?null);???
  • ????????h.invoke(this,?m,?null);???
  • ????}?catch?(Throwable?e)?{???
  • ????????//?異常處理(略)???
  • ????}???
  • }???
  • ??
  • @Override???
  • public?boolean?doSomeThing()?{???
  • ????try?{???
  • ???????Method?m?=?(h.target).getClass().getMethod("doSomeThing2",?null);???
  • ???????return?(Boolean)?h.invoke(this,?m,?null);???
  • ????}?catch?(Throwable?e)?{???
  • ????????//?異常處理(略)???
  • ????}???
  • ????return?false;???
  • }???
  • ??
  • public?ProxyBusiness(LogInvocationHandler?h)?{???
  • ????this.h?=?h;???
  • }???
  • ??
  • //測試用???
  • public?static?void?main(String[]?args)?{???
  • ????//構建AOP的Advice???
  • ????LogInvocationHandler?handler?=?new?LogInvocationHandler(new?Business());???
  • ????new?ProxyBusiness(handler).doSomeThing();???
  • ????new?ProxyBusiness(handler).doSomeThing2();???
  • }???
  • }???
  • ?

    3.1.3 小結?
    ??? 從前兩節的分析我們可以看出,動態代理在運行期通過接口動態生成代理類,這為其帶來了一定的靈活性,但這個靈活性卻帶來了兩個問題,第一代理類必須實現一個接口,如果沒實現接口會拋出一個異常。第二性能影響,因為動態代理使用反射的機制實現的,首先反射肯定比直接調用要慢,經過測試大概每個代理類比靜態代理多出10幾毫秒的消耗。其次使用反射大量生成類文件可能引起Full GC造成性能影響,因為字節碼文件加載后會存放在JVM運行時區的方法區(或者叫持久代)中,當方法區滿的時候,會引起Full GC,所以當你大量使用動態代理時,可以將持久代設置大一些,減少Full GC次數。?

    3.2 動態字節碼生成?
    ?? 使用動態字節碼生成技術實現AOP原理是在運行期間目標字節碼加載后,生成目標類的子類,將切面邏輯加入到子類中,所以使用Cglib實現AOP不需要基于接口。

    ?


    ??? 本節介紹如何使用Cglib來實現動態字節碼技術。Cglib是一個強大的,高性能的Code生成類庫,它可以在運行期間擴展Java類和實現Java接口,它封裝了Asm,所以使用Cglib前需要引入Asm的jar。 清單七:使用CGLib實現AOP

    Java代碼??
  • public?static?void?main(String[]?args)?{???
  • ????????byteCodeGe();???
  • ????}???
  • ??
  • ????public?static?void?byteCodeGe()?{???
  • ????????//創建一個織入器???
  • ????????Enhancer?enhancer?=?new?Enhancer();???
  • ????????//設置父類???
  • ????????enhancer.setSuperclass(Business.class);???
  • ????????//設置需要織入的邏輯???
  • ????????enhancer.setCallback(new?LogIntercept());???
  • ????????//使用織入器創建子類???
  • ????????IBusiness2?newBusiness?=?(IBusiness2)?enhancer.create();???
  • ????????newBusiness.doSomeThing2();???
  • ????}???
  • ??
  • ????/**??
  • ?????*?記錄日志??
  • ?????*/???
  • ????public?static?class?LogIntercept?implements?MethodInterceptor?{???
  • ??
  • ????????@Override???
  • ????????public?Object?intercept(Object?target,?Method?method,?Object[]?args,?MethodProxy?proxy)?throws?Throwable?{???
  • ????????????//執行原有邏輯,注意這里是invokeSuper???
  • ????????????Object?rev?=?proxy.invokeSuper(target,?args);???
  • ????????????//執行織入的日志???
  • ????????????if?(method.getName().equals("doSomeThing2"))?{???
  • ????????????????System.out.println("記錄日志");???
  • ????????????}???
  • ????????????return?rev;???
  • ????????}???
  • ????}???
  • ?

    ?

    3.3 自定義類加載器?
    ?? 如果我們實現了一個自定義類加載器,在類加載到JVM之前直接修改某些類的方法,并將切入邏輯織入到這個方法里,然后將修改后的字節碼文件交給虛擬機運行,那豈不是更直接。

    ?



    Javassist是一個編輯字節碼的框架,可以讓你很簡單地操作字節碼。它可以在運行期定義或修改Class。使用Javassist實現AOP的原理是在字節碼加載前直接修改需要切入的方法。這比使用Cglib實現AOP更加高效,并且沒太多限制,實現原理如下圖:?






    ??? 我們使用系統類加載器啟動我們自定義的類加載器,在這個類加載器里加一個類加載監聽器,監聽器發現目標類被加載時就織入切入邏輯,咱們再看看使用Javassist實現AOP的代碼:?
    清單八:啟動自定義的類加載器

    Java代碼??
  • //獲取存放CtClass的容器ClassPool???
  • ClassPool?cp?=?ClassPool.getDefault();???
  • //創建一個類加載器???
  • Loader?cl?=?new?Loader();???
  • //增加一個轉換器???
  • cl.addTranslator(cp,?new?MyTranslator());???
  • //啟動MyTranslator的main函數???
  • cl.run("jsvassist.JavassistAopDemo$MyTranslator",?args);???
  • ?清單九:類加載監聽器

    Java代碼??
  • public?static?class?MyTranslator?implements?Translator?{???
  • ??
  • ????????public?void?start(ClassPool?pool)?throws?NotFoundException,?CannotCompileException?{???
  • ????????}???
  • ??
  • ????????/*?*??
  • ?????????*?類裝載到JVM前進行代碼織入??
  • ?????????*/???
  • ????????public?void?onLoad(ClassPool?pool,?String?classname)?{???
  • ????????????if?(!"model$Business".equals(classname))?{???
  • ????????????????return;???
  • ????????????}???
  • ????????????//通過獲取類文件???
  • ????????????try?{???
  • ????????????????CtClass??cc?=?pool.get(classname);???
  • ????????????????//獲得指定方法名的方法???
  • ????????????????CtMethod?m?=?cc.getDeclaredMethod("doSomeThing");???
  • ????????????????//在方法執行前插入代碼???
  • ????????????????m.insertBefore("{?System.out.println(\"記錄日志\");?}");???
  • ????????????}?catch?(NotFoundException?e)?{???
  • ????????????}?catch?(CannotCompileException?e)?{???
  • ????????????}???
  • ????????}???
  • ??
  • ????????public?static?void?main(String[]?args)?{???
  • ????????????Business?b?=?new?Business();???
  • ????????????b.doSomeThing2();???
  • ????????????b.doSomeThing();???
  • ????????}???
  • ????}???
  • ?輸出:?

    Java代碼??
  • 執行業務邏輯2???
  • 記錄日志???
  • 執行業務邏輯??
  • ??
    ??? 其中Bussiness類在本文的清單一中定義。看起來是不是特別簡單,CtClass是一個class文件的抽象描述。咱們也可以使用insertAfter()在方法的末尾插入代碼,使用insertAt()在指定行插入代碼。?

    3.3.1 小結?
    ??? 從本節中可知,使用自定義的類加載器實現AOP在性能上要優于動態代理和Cglib,因為它不會產生新類,但是它仍然存在一個問題,就是如果其他的類加載器來加載類的話,這些類將不會被攔截。?

    3.4 字節碼轉換?
    ??? 自定義的類加載器實現AOP只能攔截自己加載的字節碼,那么有沒有一種方式能夠監控所有類加載器加載字節碼呢?有,使用Instrumentation,它是 Java 5 提供的新特性,使用 Instrumentation,開發者可以構建一個字節碼轉換器,在字節碼加載前進行轉換。本節使用Instrumentation和javassist來實現AOP。?

    3.4.1 構建字節碼轉換器?
    ??? 首先需要創建字節碼轉換器,該轉換器負責攔截Business類,并在Business類的doSomeThing方法前使用javassist加入記錄日志的代碼。

    Java代碼??
  • public?class?MyClassFileTransformer?implements?ClassFileTransformer?{???
  • ??
  • ????/**??
  • ?????*?字節碼加載到虛擬機前會進入這個方法??
  • ?????*/???
  • ????@Override???
  • ????public?byte[]?transform(ClassLoader?loader,?String?className,?Class<?>?classBeingRedefined,???
  • ????????????????????????????ProtectionDomain?protectionDomain,?byte[]?classfileBuffer)???
  • ????????????throws?IllegalClassFormatException?{???
  • ????????System.out.println(className);???
  • ????????//如果加載Business類才攔截???
  • ????????if?(!"model/Business".equals(className))?{???
  • ????????????return?null;???
  • ????????}???
  • ??
  • ????????//javassist的包名是用點分割的,需要轉換下???
  • ????????if?(className.indexOf("/")?!=?-1)?{???
  • ????????????className?=?className.replaceAll("/",?".");???
  • ????????}???
  • ????????try?{???
  • ????????????//通過包名獲取類文件???
  • ????????????CtClass?cc?=?ClassPool.getDefault().get(className);???
  • ????????????//獲得指定方法名的方法???
  • ????????????CtMethod?m?=?cc.getDeclaredMethod("doSomeThing");???
  • ????????????//在方法執行前插入代碼???
  • ????????????m.insertBefore("{?System.out.println(\"記錄日志\");?}");???
  • ????????????return?cc.toBytecode();???
  • ????????}?catch?(NotFoundException?e)?{???
  • ????????}?catch?(CannotCompileException?e)?{???
  • ????????}?catch?(IOException?e)?{???
  • ????????????//忽略異常處理???
  • ????????}???
  • ????????return?null;???
  • }???
  • ?

    3.4.2 注冊轉換器?
    ??? 使用premain函數注冊字節碼轉換器,該方法在main函數之前執行。

    Java代碼??
  • public?class?MyClassFileTransformer?implements?ClassFileTransformer?{???
  • ????public?static?void?premain(String?options,?Instrumentation?ins)?{???
  • ????????//注冊我自己的字節碼轉換器???
  • ????????ins.addTransformer(new?MyClassFileTransformer());???
  • }???
  • }???
  • ?

    3.4.3 配置和執行?
    ??? 需要告訴JVM在啟動main函數之前,需要先執行premain函數。首先需要將premain函數所在的類打成jar包。并修改該jar包里的META-INF\MANIFEST.MF 文件。?

    Java代碼??
  • Manifest-Version:?1.0???
  • Premain-Class:?bci.?MyClassFileTransformer??
  • ???? 然后在JVM的啟動參數里加上。-javaagent:D:\java\projects\opencometProject\Aop\lib\aop.jar?

    ???????????? 3.4.4 輸出

    ????執行main函數,你會發現切入的代碼無侵入性的織入進去了。

    Java代碼??
  • public?static?void?main(String[]?args)?{???
  • ???new?Business().doSomeThing();???
  • ???new?Business().doSomeThing2();???
  • }???
  • ???
  • ?? 輸出

    Java代碼??
  • model/Business???
  • sun/misc/Cleaner???
  • java/lang/Enum???
  • model/IBusiness???
  • model/IBusiness2???
  • 記錄日志???
  • 執行業務邏輯???
  • 執行業務邏輯2???
  • java/lang/Shutdown???
  • java/lang/Shutdown$Lock???
  • ??

    ?從輸出中可以看到系統類加載器加載的類也經過了這里。

    ?

    4 AOP實戰?
    說了這么多理論,那AOP到底能做什么呢? AOP能做的事情非常多。

    • 性能監控,在方法調用前后記錄調用時間,方法執行太長或超時報警。
    • 緩存代理,緩存某方法的返回值,下次執行該方法時,直接從緩存里獲取。
    • 軟件破解,使用AOP修改軟件的驗證類的判斷邏輯。
    • 記錄日志,在方法執行前后記錄系統日志。
    • 工作流系統,工作流系統需要將業務代碼和流程引擎代碼混合在一起執行,那么我們可以使用AOP將其分離,并動態掛接業務。
    • 權限驗證,方法執行前驗證是否有權限執行當前方法,沒有則拋出沒有權限執行異常,由業務代碼捕捉。?

    4.1 Spring的AOP?
    ??? Spring默認采取的動態代理機制實現AOP,當動態代理不可用時(代理類無接口)會使用CGlib機制。但Spring的AOP有一定的缺點,第一個只能對方法進行切入,不能對接口,字段,靜態代碼塊進行切入(切入接口的某個方法,則該接口下所有實現類的該方法將被切入)。第二個同類中的互相調用方法將不會使用代理類。因為要使用代理類必須從Spring容器中獲取Bean。第三個性能不是最好的,從3.3章節我們得知使用自定義類加載器,性能要優于動態代理和CGlib。?
    可以獲取代理類

    Java代碼??
  • public?IMsgFilterService?getThis()???
  • {???
  • ????????return?(IMsgFilterService)?AopContext.currentProxy();???
  • }???
  • ??
  • public?boolean?evaluateMsg?()?{???
  • ???//?執行此方法將織入切入邏輯???
  • return?getThis().evaluateMsg(String?message);???
  • }???
  • ??
  • @MethodInvokeTimesMonitor("KEY_FILTER_NUM")???
  • public?boolean?evaluateMsg(String?message)?{???
  • ?不能獲取代理類

    Java代碼??
  • public?boolean?evaluateMsg?()?{???
  • ???//?執行此方法將不會織入切入邏輯???
  • return?evaluateMsg(String?message);???
  • }???
  • ??
  • @MethodInvokeTimesMonitor("KEY_FILTER_NUM")???
  • public?boolean?evaluateMsg(String?message)?{???
  • ?

    ?4.2 參考資料

      • Java 動態代理機制分析及擴展
      • CGlib的官方網站
      • ASM官方網站
      • JbossAOP
      • Java5特性Instrumenttation實踐?

    轉載于:https://www.cnblogs.com/davidwang456/p/5732607.html

    總結

    以上是生活随笔為你收集整理的AOP的实现机制--转的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。