掃二維碼與項(xiàng)目經(jīng)理溝通
我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問(wèn)/技術(shù)咨詢(xún)/運(yùn)營(yíng)咨詢(xún)/技術(shù)建議/互聯(lián)網(wǎng)交流
前言

創(chuàng)新互聯(lián)建站是一家集網(wǎng)站建設(shè),臨海企業(yè)網(wǎng)站建設(shè),臨海品牌網(wǎng)站建設(shè),網(wǎng)站定制,臨海網(wǎng)站建設(shè)報(bào)價(jià),網(wǎng)絡(luò)營(yíng)銷(xiāo),網(wǎng)絡(luò)優(yōu)化,臨海網(wǎng)站推廣為一體的創(chuàng)新建站企業(yè),幫助傳統(tǒng)企業(yè)提升企業(yè)形象加強(qiáng)企業(yè)競(jìng)爭(zhēng)力。可充分滿(mǎn)足這一群體相比中小企業(yè)更為豐富、高端、多元的互聯(lián)網(wǎng)需求。同時(shí)我們時(shí)刻保持專(zhuān)業(yè)、時(shí)尚、前沿,時(shí)刻以成就客戶(hù)成長(zhǎng)自我,堅(jiān)持不斷學(xué)習(xí)、思考、沉淀、凈化自己,讓我們?yōu)楦嗟钠髽I(yè)打造出實(shí)用型網(wǎng)站。
前幾篇文章本打算寫(xiě)spring aop的,但是強(qiáng)忍著沒(méi)有寫(xiě)(旁白:也有可能是沒(méi)想好該怎么寫(xiě)),就是為了今天整個(gè)專(zhuān)題,因?yàn)樗莝pring中最核心的技術(shù)之一,實(shí)在太重要了。
關(guān)于spring aop的文章網(wǎng)上一搜一大堆,但我想寫(xiě)點(diǎn)不一樣的東西,嘗試一種全新的寫(xiě)作風(fēng)格,希望您會(huì)喜歡。
從實(shí)戰(zhàn)出發(fā)
很多文章講spring aop的時(shí)候,一開(kāi)始就整一堆概念,等我們看得差不多要暈的時(shí)候,才真正進(jìn)入主題。。。
我卻相反,沒(méi)錯(cuò),先從實(shí)戰(zhàn)出發(fā)。
在spring aop還沒(méi)出現(xiàn)之前,想要在目標(biāo)方法之前先后加上日志打印的功能,我們一般是這樣做的:
- @Service
- public class TestService {
- public void doSomething1() {
- beforeLog();
- System.out.println("==doSomething1==");
- afterLog();
- }
- public void doSomething2() {
- beforeLog();
- System.out.println("==doSomething1==");
- afterLog();
- }
- public void doSomething3() {
- beforeLog();
- System.out.println("==doSomething1==");
- afterLog();
- }
- public void beforeLog() {
- System.out.println("打印請(qǐng)求日志");
- }
- public void afterLog() {
- System.out.println("打印響應(yīng)日志");
- }
- }
如果加了新doSomethingXXX方法,就需要在新方法前后手動(dòng)加beforeLog和afterLog方法。
原本相安無(wú)事的,但長(zhǎng)此以往,總有會(huì)出現(xiàn)幾個(gè)刺頭青。
刺頭青A說(shuō):每加一個(gè)新方法,都需要加兩行重復(fù)的代碼,是不是很麻煩?
刺頭青B說(shuō):業(yè)務(wù)代碼和公共代碼是不是耦合在一起了?
刺頭青C說(shuō):如果有幾千個(gè)類(lèi)中加了公共代碼,而有一天我需要?jiǎng)h除,是不是要瘋了?
spring大師們說(shuō):我們提供一套spring的aop機(jī)制,你們可以閉嘴了。
下面看看用spring aop(偷偷說(shuō)一句,還用了aspectj)是如何打印日志的:
- @Service
- public class TestService {
- public void doSomething1() {
- System.out.println("==doSomething1==");
- }
- public void doSomething2() {
- System.out.println("==doSomething1==");
- }
- public void doSomething3() {
- System.out.println("==doSomething1==");
- }
- }
- @Component
- @Aspect
- public class LogAspect {
- @Pointcut("execution(public * com.sue.cache.service.*.*(..))")
- public void pointcut() {
- }
- @Before("pointcut()")
- public void beforeLog() {
- System.out.println("打印請(qǐng)求日志");
- }
- @After("pointcut()")
- public void afterLog() {
- System.out.println("打印響應(yīng)日志");
- }
- }
增加了LogAspect類(lèi),在類(lèi)上加了@Aspect注解。先在類(lèi)中使用@Pointcut注解定義了pointcut方法,然后將beforeLog和afterLog方法移到這個(gè)類(lèi)中,分別加上@Before和@After注解。
改造后,業(yè)務(wù)方法在TestService類(lèi)中,而公共方法在LogAspect類(lèi)中,是分離的。如果要新加一個(gè)業(yè)務(wù)方法,直接加就好,LogAspect類(lèi)不用改任何代碼,新加的業(yè)務(wù)方法就自動(dòng)擁有打印日志的功能,是不是很神奇?
spring aop其實(shí)是一種橫切的思想,通過(guò)動(dòng)態(tài)代理技術(shù)將公共代碼織入到業(yè)務(wù)方法中。
這里出于5毛錢(qián)的友情,有必要溫馨提醒一下。aop是一種思想,不是spring獨(dú)有的,目前市面上比較出名的有:
我們現(xiàn)在主流的做法是將spring aop和aspectj結(jié)合使用,spring借鑒了AspectJ的切面,以提供注解驅(qū)動(dòng)的AOP。
此時(shí),一個(gè)黑影一閃而過(guò)。
刺頭青D問(wèn):你說(shuō)的“橫切”,“動(dòng)態(tài)代理”,“織入” 是什么東東?
幾個(gè)重要的概念
根據(jù)上面spring aop的代碼,用一張圖聊聊幾個(gè)重要的概念:
還是那個(gè)刺頭青D說(shuō)(旁邊:這位仁兄比較好學(xué)):spring aop概念弄明白了,挺簡(jiǎn)單的。@Pointcut注解的execution表達(dá)式剛剛看得我一臉懵逼,可以再說(shuō)說(shuō)嗎,我請(qǐng)你吃飯?
切入點(diǎn)表達(dá)式
@Pointcut注解的execution切入點(diǎn)表達(dá),看似簡(jiǎn)單,里面還是有些內(nèi)容的。為了更直觀一些,還是用張圖來(lái)總結(jié)一下:
該表達(dá)式的含義是:匹配訪問(wèn)權(quán)限是public,任意返回值,包名為:com.sue.cache.service,下面的所有類(lèi)所有方法和所有參數(shù)類(lèi)型。圖中所有用*表示,比如圖中類(lèi)名用.*表示的是所有類(lèi)。如果具體匹配某個(gè)類(lèi),比如:TestService,則表達(dá)式可以換成:
- @Pointcut("execution(public * com.sue.cache.service.TestService.*(..))")
其實(shí)spring支持9種表達(dá)式,execution只是其中一種。
有哪些入口?
先說(shuō)說(shuō)我為什么會(huì)問(wèn)這樣一個(gè)問(wèn)題?
spring aop有哪些入口?說(shuō)人話就是在問(wèn):spring中有哪些場(chǎng)景需要調(diào)用aop生成代理對(duì)象,難道你不好奇嗎?
入口1
AbstractAutowireCapableBeanFactory類(lèi)的createBean方法中,有這樣一段代碼:
它通過(guò)BeanPostProcessor提供了一個(gè)生成代理對(duì)象的機(jī)會(huì)。具體邏輯在AbstractAutoProxyCreator類(lèi)的postProcessBeforeInstantiation方法中:
說(shuō)白了,需要實(shí)現(xiàn)TargetSource才有可能會(huì)生成代理對(duì)象。該接口是對(duì)Target目標(biāo)對(duì)象的封裝,通過(guò)該接口可以獲取到目標(biāo)對(duì)象的實(shí)例。
不出意外,這時(shí),又會(huì)冒出一個(gè)黑影。
刺頭青F說(shuō):這里生成代理對(duì)象有什么用呢?
有時(shí)我們想自己控制bean的創(chuàng)建和初始化,而不需要通過(guò)spring容器,這時(shí)就可以通過(guò)實(shí)現(xiàn)TargetSource滿(mǎn)足要求。只是創(chuàng)建單純的實(shí)例還好,如果我們想使用代理該怎么辦呢?這時(shí)候,入口1的作用就體現(xiàn)出來(lái)了。
入口2
AbstractAutowireCapableBeanFactory類(lèi)的doCreateBean方法中,有這樣一段代碼:
它主要作用是為了解決對(duì)象的循環(huán)依賴(lài)問(wèn)題,核心思路是提前暴露singletonFactory到緩存中。
通過(guò)getEarlyBeanReference方法生成代理對(duì)象:
它又會(huì)調(diào)用wrapIfNecessary方法:
這里有你想看到的生成代理的邏輯。
這時(shí)。。。。,你猜錯(cuò)了,黑影去吃飯了。。。
入口3
AbstractAutowireCapableBeanFactory類(lèi)的initializeBean方法中,有這樣一段代碼:
它會(huì)調(diào)用到AbstractAutoProxyCreator類(lèi)postProcessAfterInitialization方法:
該方法中能看到我們熟悉的面孔:wrapIfNecessary方法。從上面得知該方法里面包含了真正生成代理對(duì)象的邏輯。
這個(gè)入口,是為了給普通bean能夠生成代理用的,是spring最常見(jiàn)并且使用最多的入口。
下面為了加深印象,用一張圖總結(jié)一下:
jdk動(dòng)態(tài)代理 vs cglib
我猜你們對(duì)jdk動(dòng)態(tài)代理和cglib是知道的(即使猜錯(cuò)了也不會(huì)少塊肉),但為了照顧一下新朋友,還是有必要把這兩種生成代理的方式拿出來(lái)說(shuō)說(shuō)。
jdk動(dòng)態(tài)代理
jdk動(dòng)態(tài)代理是通過(guò)反射技術(shù)實(shí)現(xiàn)的,生成代理的代碼如下:
- public interface IUser {
- void add();
- }
- public class User implements IUser{
- @Override
- public void add() {
- System.out.println("===add===");
- }
- }
- public class JdkProxy implements InvocationHandler {
- private Object target;
- public Object getProxy(Object target) {
- this.target = target;
- return Proxy.newProxyInstance(this.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
- }
- @Override
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- before();
- Object result = method.invoke(target,args);
- after();
- return result;
- }
- private void before() {
- System.out.println("===before===");
- }
- private void after() {
- System.out.println("===after===");
- }
- }
- public class Test {
- public static void main(String[] args) {
- User user = new User();
- JdkProxy jdkProxy = new JdkProxy();
- IUser proxy = (IUser)jdkProxy.getProxy(user);
- proxy.add();
- }
- }
首先要定義一個(gè)接口IUser,然后定義接口實(shí)現(xiàn)類(lèi)User,再定義類(lèi)JdkProxy實(shí)現(xiàn)InvocationHandler接口,重寫(xiě)invoke方法,該方法中實(shí)現(xiàn)額外的邏輯。當(dāng)然,別忘了在getProxy方法中,用Proxy.newProxyInstance方法創(chuàng)建一個(gè)代理對(duì)象。
jdk動(dòng)態(tài)代理三個(gè)要素:
cglib
cglib底層是通過(guò)asm字節(jié)碼技術(shù)實(shí)現(xiàn)的,生成代理的代碼如下:
- public class User {
- public void add() {
- System.out.println("===add===");
- }
- }
- public class CglibProxy implements MethodInterceptor {
- private Object target;
- public Object getProxy(Object target) {
- this.target = target;
- Enhancer enhancer = new Enhancer();
- enhancer.setSuperclass(target.getClass());
- enhancer.setCallback(this);
- return enhancer.create();
- }
- @Override
- public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
- before();
- Object result = method.invoke(target,objects);
- after();
- return result;
- }
- private void before() {
- System.out.println("===before===");
- }
- private void after() {
- System.out.println("===after===");
- }
- }
- public class Test {
- public static void main(String[] args) {
- User user = new User();
- CglibProxy cglibProxy = new CglibProxy();
- IUser proxy = (IUser)cglibProxy.getProxy(user);
- proxy.add();
- }
- }
這里不需要定義接口,直接定義目標(biāo)類(lèi)User,然后實(shí)現(xiàn)MethodInterceptor接口,重寫(xiě)intercept方法,該方法中實(shí)現(xiàn)額外的邏輯。當(dāng)然,別忘了在getProxy方法中,通過(guò)Enhancer創(chuàng)建代理對(duì)象。
cglib兩個(gè)要素:
spring中如何用的?
DefaultAopProxyFactory類(lèi)的createAopProxy方法中,有這樣一段代碼:
它里面包含:
JdkDynamicAopProxy類(lèi)的invoke方法生成的代理對(duì)象。而ObjenesisCglibAopProxy類(lèi)的父類(lèi):CglibAopProxy,它的getProxy方法生成的代理對(duì)象。
哪個(gè)更好?
我猜,不是刺頭青,是你,可能會(huì)來(lái)自靈魂深處的一問(wèn):jdk動(dòng)態(tài)代理和cglib哪個(gè)更好?
其實(shí)這個(gè)問(wèn)題沒(méi)有標(biāo)準(zhǔn)答案,要看具體的業(yè)務(wù)場(chǎng)景:
沒(méi)有定義接口,只能使用cglib,不說(shuō)它好不行。
定義了接口,需要?jiǎng)?chuàng)建單例或少量對(duì)象,調(diào)用多次時(shí),可以使用jdk動(dòng)態(tài)代理,因?yàn)樗鼊?chuàng)建時(shí)更耗時(shí),但調(diào)用時(shí)速度更快。
定義了接口,需要?jiǎng)?chuàng)建多個(gè)對(duì)象時(shí),可以使用cglib,因?yàn)樗鼊?chuàng)建速度更快。
出于人道主義關(guān)懷,免費(fèi)贈(zèng)送一條有用經(jīng)驗(yàn):如果要強(qiáng)制使用cglib,可以通過(guò)以下兩種方式:
五種通知
spring默認(rèn)提供了五種通知:
按照國(guó)際慣例,不,按照我個(gè)人習(xí)慣,先看看他們是怎么用的。
前置通知
該通知在方法執(zhí)行之前執(zhí)行,只需在公共方法上加@Before注解,就能定義前置通知:
- @Before("pointcut()")
- public void beforeLog(JoinPoint joinPoint) {
- System.out.println("打印請(qǐng)求日志");
- }
后置通知
該通知在方法執(zhí)行之后執(zhí)行,只需在公共方法上加@After注解,就能定義后置通知:
- @After("pointcut()")
- public void afterLog(JoinPoint joinPoint) {
- System.out.println("打印響應(yīng)日志");
- }
環(huán)繞通知
該通知在方法執(zhí)行前后執(zhí)行,只需在公共方法上加@Round注解,就能定義環(huán)繞通知:
- @Around("pointcut()")
- public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
- System.out.println("打印請(qǐng)求日志");
- Object result = joinPoint.proceed();
- System.out.println("打印響應(yīng)日志");
- return result;
- }
結(jié)果通知
該通知在方法結(jié)束后執(zhí)行,能夠獲取方法返回結(jié)果,只需在公共方法上加@AfterReturning注解,就能定義結(jié)果通知:
- @AfterReturning(pointcut = "pointcut()",returning = "retVal")
- public void afterReturning(JoinPoint joinPoint, Object retVal) {
- System.out.println("獲取結(jié)果:"+retVal);
- }
異常通知
該通知在方法拋出異常之后執(zhí)行,只需在公共方法上加@AfterThrowing注解,就能定義異常通知:
- @AfterThrowing(pointcut = "pointcut()", throwing = "e")
- public void afterThrowing(JoinPoint joinPoint, Throwable e) {
- System.out.println("異常:"+e);
- }
spring aop給這五種通知,分別分配了一個(gè)xxxAdvice類(lèi)。在ReflectiveAspectJAdvisorFactory類(lèi)的getAdvice方法中可以看得到:
下面用一張圖總結(jié)一下對(duì)應(yīng)關(guān)系:
這五種xxxAdvice類(lèi)都實(shí)現(xiàn)了Advice接口,但是有些差異。
下面三個(gè)xxxAdvice類(lèi)實(shí)現(xiàn)了MethodInterceptor接口:
而另外兩個(gè)類(lèi):AspectJMethodBeforeAdvice 和 AspectJAfterReturningAdvice 沒(méi)有實(shí)現(xiàn)上面的接口,這是為什么?
(這里留點(diǎn)懸念,后面的文章會(huì)揭曉謎題,敬請(qǐng)期待。)
一個(gè)猝不及防,依然是那個(gè)刺頭青D,放下碗沖過(guò)來(lái)問(wèn)了句:這五種通知的執(zhí)行順序是怎么樣的?
單個(gè)切面正常情況
單個(gè)切面異常情況
多個(gè)切面正常情況
多個(gè)切面異常情況
為什么使用鏈?zhǔn)秸{(diào)用?
這個(gè)問(wèn)題沒(méi)人問(wèn),是我自己想聊聊(旁白:因?yàn)槲议L(zhǎng)得帥,有點(diǎn)自戀了)。
先看看spring是如何使用鏈?zhǔn)秸{(diào)用的,在ReflectiveMethodInvocation的proceed方法中,有這樣一段代碼:
下面用一張圖捋一捋上面的邏輯:
圖中包含了一個(gè)遞歸的鏈?zhǔn)秸{(diào)用,為什么要這樣設(shè)計(jì)呢?
假如不這樣設(shè)計(jì),我們代碼中是不是需要寫(xiě)很多if...else,根據(jù)不同的切面和通知單獨(dú)處理?
而spring巧妙的使用責(zé)任鏈模式消除了原本需要大量的if...else判斷,讓代碼的擴(kuò)展性更好,很好的體現(xiàn)了開(kāi)閉原則:對(duì)擴(kuò)展開(kāi)放,對(duì)修改關(guān)閉。
緩存中存的原始還是代理對(duì)象?
我們知道spring中為了性能考慮是有緩存的,通常說(shuō)包含了三級(jí)緩存:
說(shuō)時(shí)遲那時(shí)快,刺頭青D的兄弟,刺頭青F忍不住趕過(guò)來(lái)問(wèn)了句:緩存中存的原始還是代理對(duì)象?
我竟然被問(wèn)得一時(shí)語(yǔ)塞,仔細(xì)捋了捋,要從三個(gè)方面回答:
singletonFactories(三級(jí)緩存)
AbstractAutowireCapableBeanFactory類(lèi)的doCreateBean方法中,有這樣一段代碼:
其實(shí)之前已經(jīng)說(shuō)過(guò),它是為了解決循環(huán)依賴(lài)問(wèn)題。這次要說(shuō)的是addSingletonFactory方法:
它里面保存的是singletonFactory對(duì)象,所以是原始對(duì)象。
earlySingletonObjects(二級(jí)緩存)
AbstractBeanFactory類(lèi)的doGetBean方法中,有這樣一段代碼:
在調(diào)用getBean方法獲取bean實(shí)例時(shí),會(huì)調(diào)用getSingleton嘗試先從緩存中看能否獲取到,如果能獲取到則直接返回。
這段代碼會(huì)先從一級(jí)緩存中獲取bean,如果沒(méi)有再?gòu)亩?jí)緩存中獲取,如果還是沒(méi)有則從三級(jí)緩存中獲取singletonFactory,通過(guò)getObject方法獲取實(shí)例,將該實(shí)例放入到二級(jí)緩存中。
答案的謎底就聚焦在getObject方法中,而這個(gè)方法又是在哪來(lái)定義的呢?
其實(shí)就是上面的getEarlyBeanReference方法,我們知道這個(gè)方法生成的是代理對(duì)象,所以二級(jí)緩存中存的是代理對(duì)象。
singletonObjects(一級(jí)緩存)
DefaultSingletonBeanRegistry類(lèi)的getSingleton方法中,有這樣一段代碼:
此時(shí)的bean創(chuàng)建、注入和初始化完成了,判斷是如果新的單例對(duì)象,則會(huì)加入到一級(jí)緩存中,具體代碼如下:
出于一塊錢(qián)的友誼,有必要溫馨提醒一下:這里是DefaultSingletonBeanRegistry類(lèi)的getSingleton方法,跟上面說(shuō)的AbstractBeanFactory類(lèi)getSingleton方法不一樣。
幾個(gè)常見(jiàn)的坑
我是一個(gè)樂(lè)于分享的人,雖說(shuō)有時(shí)話比較少(旁邊:屬于人狠話不多的角色,別惹我)。為了表現(xiàn)我的share精神,給大家總結(jié)幾個(gè)我之前使用spring aop遇過(guò)的坑。
我們幾乎每天都在用spring aop。
“什么?我怎么不知道?” 你可能會(huì)問(wèn)。
如果你每天在用spring事務(wù)的話,就是每天在用spring aop,因?yàn)閟pring事務(wù)的底層就用到了spring aop。
坑1:直接方法調(diào)用
使用spring事務(wù)時(shí),直接方法調(diào)用:
- @Service
- public class UserService {
- @Autowired
- private UserMapper userMapper;
- public void add(UserModel userModel) {
- userMapper.queryUser(userModel);
- save(userModel);
- }
- @Transactional
- public void save(UserModel userModel) {
- System.out.println("保存數(shù)據(jù)");
- }
- }
這種情況直接方法調(diào)用spring aop無(wú)法生成代理對(duì)象,事務(wù)會(huì)失效。這個(gè)問(wèn)題的解決辦法有很多:
坑2:訪問(wèn)權(quán)限錯(cuò)誤
- @Service
- public class UserService {
- @Autowired
- private UserService userService;
- @Autowired
- private UserMapper userMapper;
- public void add(UserModel userModel) {
- userMapper.queryUser(userModel);
- userService.save(userModel);
- }
- @Transactional
- private void save(UserModel userModel) {
- System.out.println("保存數(shù)據(jù)");
- }
- }
上面用 UserService類(lèi)中@Autowired注入自己的實(shí)例userService的方式解決事務(wù)失效問(wèn)題,如果不出意外的話,是可以的。
但是恰恰出現(xiàn)了意外,save方法被定義成了private的,這時(shí)也無(wú)法生成代理對(duì)象,事務(wù)同樣會(huì)失效。
所以,我們應(yīng)該拿個(gè)小本本記一下,目標(biāo)方法一定不能定義成private的。
坑3:目標(biāo)類(lèi)用final修飾
- @Service
- public class UserService {
- @Autowired
- private UserService userService;
- @Autowired
- private UserMapper userMapper;
- public void add(UserModel userModel) {
- userMapper.queryUser(userModel);
- userService.save(userModel);
- }
- @Transactional
- public final void save(UserModel userModel) {
- System.out.println("保存數(shù)據(jù)");
- }
- }
這種情況spring aop生成代理對(duì)象,重寫(xiě)save方法時(shí),發(fā)現(xiàn)的final的,重寫(xiě)不了,也會(huì)導(dǎo)致事務(wù)失效。
小本本需要再加一條,目標(biāo)方法一定不能定義成final的。
坑4:循環(huán)依賴(lài)問(wèn)題
在使用@Async注解開(kāi)啟異步功能的場(chǎng)景,它會(huì)通過(guò)AOP自動(dòng)生成代理對(duì)象。
- @Service
- public class TestService1 {
- @Autowired
- private TestService2 testService2;
- @Async
- public void test1() {
- }
- }
- @Service
- public class TestService2 {
- @Autowired
- private TestService1 testService1;
- public void test2() {
- }
- }
啟動(dòng)服務(wù)會(huì)報(bào)錯(cuò):
- org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'testService1': Bean with name 'testService1' has been injected into other beans [testService2] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.
至于為什么會(huì)報(bào)錯(cuò),我在這里不過(guò)多解釋了,在我的《spring:我是如何解決循環(huán)依賴(lài)的?》這篇文章中寫(xiě)的很詳細(xì)。

我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問(wèn)/技術(shù)咨詢(xún)/運(yùn)營(yíng)咨詢(xún)/技術(shù)建議/互聯(lián)網(wǎng)交流