Spring事件驅(qū)動模型

事件驅(qū)動模型簡介

事件驅(qū)動模型也就是我們常說的觀察者,或者發(fā)布-訂閱模型抵碟;理解它的幾個關鍵點:

  • 1.首先是一種對象間的一對多的關系锭硼;最簡單的如交通信號燈,信號燈是目標(一方)队塘,行人注視著信號燈(多方);
  • 2.當目標發(fā)送改變(發(fā)布)宜鸯,觀察者(訂閱者)就可以接收到改變憔古;
  • 3.觀察者如何處理(如行人如何走,是快走/慢走/不走淋袖,目標不會管的)鸿市,目標無需干涉;所以就松散耦合了它們之間的關系即碗。

接下來先看一個用戶注冊的例子:

用戶注冊

用戶注冊成功后焰情,需要做這么多事:

1、加積分
2拜姿、發(fā)確認郵件
3烙样、如果是游戲帳戶,可能贈送游戲大禮包
4蕊肥、索引用戶數(shù)據(jù)


問題:

  1. UserService和其他Service耦合嚴重谒获,增刪功能比較麻煩;

  2. 有些功能可能需要調(diào)用第三方系統(tǒng)壁却,如增加積分/索引用戶批狱,速度可能比較慢,此時需要異步支持展东;這個如果使用Spring赔硫,可以輕松解決,后邊再介紹;

從如上例子可以看出,應該使用一個觀察者來解耦這些Service之間的依賴關系套么,如圖:

注冊功能設計結構

增加了一個Listener來解耦UserService和其他服務嚎京,即注冊成功后诅需,只需要通知相關的監(jiān)聽器,不需要關系它們?nèi)绾翁幚怼T鰟h功能非常容易。

這就是一個典型的事件處理模型/觀察者耘成,解耦目標對象和它的依賴對象,目標只需要通知它的依賴對象,具體怎么處理瘪菌,依賴對象自己決定撒会。比如是異步還是同步,延遲還是非延遲等师妙。

上邊其實也使用了DIP(依賴倒置原則)诵肛,依賴于抽象,而不是具體疆栏。

還是就是使用了IoC思想曾掂,即以前主動去創(chuàng)建它依賴的Service惫谤,現(xiàn)在只是被動等待別人注冊進來壁顶。

其他的例子還有如GUI中的按鈕和動作的關系,按鈕和動作本身都是一種抽象溜歪,每個不同的按鈕的動作可能不一樣若专;如“文件-->新建”打開新建窗口;點擊“關閉”按鈕關閉窗口等等蝴猪。

主要目的是:松散耦合對象間的一對多的依賴關系调衰,如按鈕和動作的關系;

如何實現(xiàn)呢自阱?面向接口編程(即面向抽象編程)嚎莉,而非面向?qū)崿F(xiàn)。即按鈕和動作可以定義為接口沛豌,這樣它倆的依賴是最小的(如在Java中趋箩,沒有比接口更抽象的了)。

有朋友會問加派,我剛開始學的時候也是這樣:抽象類不也行嗎叫确?記住一個原則:接口目的是抽象,抽象類目的是復用芍锦;所以如果接觸過servlet/struts2/spring等框架竹勉,大家都應該知道:
Servlet<-----GenericServlet<-----HttpServlet<------我們自己的
Action<------ActionSupport<------我們自己的
DaoInterface<------××DaoSupport<-----我們自己的
從上邊大家應該能體會出接口、抽象類的主要目的了÷α穑現(xiàn)在想想其實很簡單次乓。

在Java中接口還一個非常重要的好處:接口是可以多實現(xiàn)的,類/抽象類只能單繼承孽水,所以使用接口可以非常容易擴展新功能(還可以實現(xiàn)所謂的mixin)票腰,類/抽象類辦不到。

Java GUI事件驅(qū)動模型/觀察者

扯遠了匈棘,再來看看Java GUI世界里的事件驅(qū)動模型吧:

如果寫過AWT/Swing程序丧慈,應該知道其所有組件都繼承自java.awt.Component抽象類,其內(nèi)部提供了addXXXListener(XXXListener l) 注冊監(jiān)聽器的方法,即Component與實際動作之間依賴于XXXListener抽象逃默。

比如獲取焦點事件鹃愤,很多組件都可以有這個事件,是我們知道組件獲取到焦點后需要一個處理完域,雖然每個組件如何處理是特定的(具體的)软吐,但我們可以抽象一個FocusListener,讓所有具體實現(xiàn)它然后提供具體動作吟税,這樣組件只需依賴于FocusListener抽象凹耙,而不是具體。

還有如java.awt.Button肠仪,提供了一個addActionListener(ActionListener l)肖抱,用于注冊點擊后觸發(fā)的ActionListener實現(xiàn)。

組件是一個抽象類异旧,其好處主要是復用意述,比如復用這些監(jiān)聽器的觸發(fā)及管理等。

JavaBean規(guī)范的事件驅(qū)動模型/觀察者

JavaBean規(guī)范提供了JavaBean的PropertyEditorSupport及PropertyChangeListener支持吮蛹。

PropertyEditorSupport就是目標荤崇,而PropertyChangeListener就是監(jiān)聽器,大家可以google搜索下潮针,具體網(wǎng)上有很多例子术荤。

Java提供的事件驅(qū)動模型/觀察者抽象

JDK內(nèi)部直接提供了觀察者模式的抽象:
目標:java.util.Observable,提供了目標需要的關鍵抽象:addObserver/deleteObserver/notifyObservers()等每篷,具體請參考javadoc瓣戚。
觀察者:java.util.Observer,提供了觀察者需要的主要抽象:update(Observable o, Object arg)雳攘,此處還提供了一種推模型(目標主動把數(shù)據(jù)通過arg推到觀察者)/拉模型(目標需要根據(jù)o自己去拉數(shù)據(jù)带兜,arg為null)。

因為網(wǎng)上介紹的非常多了吨灭,請google搜索了解如何使用這個抽象及推/拉模型的優(yōu)缺點刚照。

接下來是我們的重點:spring提供的事件驅(qū)動模型。

Spring提供的事件驅(qū)動模型/觀察者抽象

首先看一下Spring提供的事件驅(qū)動模型體系圖:

Spring事件驅(qū)動模型

事件

具體代表者是:ApplicationEvent:

1喧兄、其繼承自JDK的EventObject无畔,JDK要求所有事件將繼承它,并通過source得到事件源吠冤,比如我們的AWT事件體系也是繼承自它浑彰;

2、系統(tǒng)默認提供了如下ApplicationEvent事件實現(xiàn):

事件體系

只有一個ApplicationContextEvent拯辙,表示ApplicationContext容器事件郭变,且其又有如下實現(xiàn):

  • ContextStartedEvent:ApplicationContext啟動后觸發(fā)的事件颜价;(目前版本沒有任何作用)
  • ContextStoppedEvent:ApplicationContext停止后觸發(fā)的事件;(目前版本沒有任何作用)
  • ContextRefreshedEvent:ApplicationContext初始化或刷新完成后觸發(fā)的事件诉濒;(容器初始化完成后調(diào)用)
  • ContextClosedEvent:ApplicationContext關閉后觸發(fā)的事件周伦;(如web容器關閉時自動會觸發(fā)spring容器的關閉,如果是普通java應用未荒,需要調(diào)用ctx.registerShutdownHook();注冊虛擬機關閉時的鉤子才行)

注:org.springframework.context.support.AbstractApplicationContext抽象類實現(xiàn)了LifeCycle的start和stop回調(diào)并發(fā)布ContextStartedEvent和ContextStoppedEvent事件专挪;但是無任何實現(xiàn)調(diào)用它,所以目前無任何作用片排。

目標(發(fā)布事件者)

具體代表者是:ApplicationEventPublisher及ApplicationEventMulticaster寨腔,系統(tǒng)默認提供了如下實現(xiàn):


事件發(fā)布體系

1、ApplicationContext接口繼承了ApplicationEventPublisher率寡,并在AbstractApplicationContext實現(xiàn)了具體代碼迫卢,實際執(zhí)行是委托給ApplicationEventMulticaster(可以認為是多播):

public void publishEvent(ApplicationEvent event) {
    Assert.notNull(event, "Event must not be null");
    if (logger.isTraceEnabled()) {
        logger.trace("Publishing event in " + getDisplayName() + ": " + event);
    }
    getApplicationEventMulticaster().multicastEvent(event);
    if (this.parent != null) {
        this.parent.publishEvent(event);
    }
}

我們常用的ApplicationContext都繼承自AbstractApplicationContext,如ClassPathXmlApplicationContext勇劣、XmlWebApplicationContext等靖避。所以自動擁有這個功能潭枣。

2比默、ApplicationContext自動到本地容器里找一個名字為ApplicationEventMulticaster的實現(xiàn),如果沒有自己new一個SimpleApplicationEventMulticaster盆犁。其中SimpleApplicationEventMulticaster發(fā)布事件的代碼如下:

public void multicastEvent(final ApplicationEvent event) {
    for (final ApplicationListener listener : getApplicationListeners(event)) {
        Executor executor = getTaskExecutor();
        if (executor != null) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    listener.onApplicationEvent(event);
                }
            });
        }
        else {
            listener.onApplicationEvent(event);
        }
    }
}

大家可以看到如果給它一個executor(java.util.concurrent.Executor)命咐,它就可以異步支持發(fā)布事件了。否則就是同步發(fā)送谐岁。

所以我們發(fā)送事件只需要通過ApplicationContext.publishEvent即可醋奠,沒必要再創(chuàng)建自己的實現(xiàn)了。除非有必要伊佃。

監(jiān)聽器

具體代表者是:ApplicationListener
1窜司、其繼承自JDK的EventListener,JDK要求所有監(jiān)聽器將繼承它航揉,比如我們的AWT事件體系也是繼承自它塞祈;
2、ApplicationListener接口:

public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
    void onApplicationEvent(E event);
}

其只提供了onApplicationEvent方法帅涂,我們需要在該方法實現(xiàn)內(nèi)部判斷事件類型來處理议薪,也沒有提供按順序觸發(fā)監(jiān)聽器的語義,所以Spring提供了另一個接口媳友,SmartApplicationListener:

public interface SmartApplicationListener extends ApplicationListener<ApplicationEvent>, Ordered {

    /**
     * 如果實現(xiàn)支持該事件類型 那么返回true  
     */
    boolean supportsEventType(Class<? extends ApplicationEvent> eventType);
    /**
     * 如果實現(xiàn)支持“目標”類型斯议,那么返回true 
     */
    boolean supportsSourceType(Class<?> sourceType);
    /**
     * 順序,即監(jiān)聽器執(zhí)行的順序醇锚,值越小優(yōu)先級越高
     */
    int getOrder(); 
}

該接口可方便實現(xiàn)去判斷支持的事件類型哼御、目標類型,及執(zhí)行順序。

Spring事件機制的簡單例子

本例子模擬一個給多個人發(fā)送內(nèi)容(類似于報紙新聞)的例子恋昼。

1尿扯、定義事件

Java代碼

public class ContentEvent extends ApplicationEvent {  
    public ContentEvent(final String content) {  
        super(content);  
    }  
}

非常簡單,如果用戶發(fā)送內(nèi)容焰雕,只需要通過構造器傳入內(nèi)容衷笋,然后通過getSource即可獲取。

2矩屁、定義無序監(jiān)聽器

之所以說無序辟宗,類似于AOP機制,順序是無法確定的吝秕。

@Component  
public class LisiListener implements ApplicationListener<ApplicationEvent> {  
        @Override  
        public void onApplicationEvent(final ApplicationEvent event) {  
            if(event instanceof ContentEvent) {  
            System.out.println("李四收到了新的內(nèi)容:" + event.getSource());  
        }  
    }  
} 

1泊脐、使用@Compoent注冊Bean即可;
2烁峭、在實現(xiàn)中需要判斷event類型是ContentEvent才可以處理容客;

更簡單的辦法是通過泛型指定類型,如下所示

@Component  
public class ZhangsanListener implements ApplicationListener<ContentEvent> {  
    @Override  
    public void onApplicationEvent(final ContentEvent event) {  
        System.out.println("張三收到了新的內(nèi)容:" + event.getSource());  
    }  
}

3约郁、定義有序監(jiān)聽器

實現(xiàn)SmartApplicationListener接口即可缩挑。

WangwuListener.java

@Component  
public class WangwuListener implements SmartApplicationListener {  

    @Override  
    public boolean supportsEventType(final Class<? extends ApplicationEvent> eventType) {  
        return eventType == ContentEvent.class;  
    }  
    @Override  
    public boolean supportsSourceType(final Class<?> sourceType) {  
        return sourceType == String.class;  
    }  
    @Override  
    public void onApplicationEvent(final ApplicationEvent event) {  
        System.out.println("王五在孫六之前收到新的內(nèi)容:" + event.getSource());  
    }  
    @Override  
    public int getOrder() {  
        return 1;  
    }  
} 

SunliuListener.java

@Component  
public class SunliuListener implements SmartApplicationListener {  

    @Override  
    public boolean supportsEventType(final Class<? extends ApplicationEvent> eventType) {  
        return eventType == ContentEvent.class;  
    }  

    @Override  
    public boolean supportsSourceType(final Class<?> sourceType) {  
         return sourceType == String.class;  
    }  

    @Override  
    public void onApplicationEvent(final ApplicationEvent event) {  
        System.out.println("孫六在王五之后收到新的內(nèi)容:" + event.getSource());  
    }  

    @Override  
    public int getOrder() {  
        return 2;  
    }  
}  

1.supportsEventType:用于指定支持的事件類型,只有支持的才調(diào)用onApplicationEvent鬓梅;
2.supportsSourceType:支持的目標類型供置,只有支持的才調(diào)用onApplicationEvent;
3.getOrder:即順序绽快,越小優(yōu)先級越高

4芥丧、測試

4.1、配置文件
<context:component-scan base-package="com.xxx"/> 

就一句話坊罢,自動掃描注解Bean续担。

4.2、測試類
@RunWith(SpringJUnit4ClassRunner.class)  
@ContextConfiguration(locations={"classpath:applicationContext.xml"})  
public class HelloIT {  

    @Autowired  
    private ApplicationContext applicationContext;  
    @Test  
    public void testPublishEvent() {  
        applicationContext.publishEvent(new ContentEvent("test......"));  
    }  
}  

接著會輸出:

王五在孫六之前收到新的內(nèi)容:test......
孫六在王五之后收到新的內(nèi)容:test......
李四收到了新的內(nèi)容:test......
張三收到了新的內(nèi)容:test......

一個簡單的測試例子就演示完畢活孩,而且我們使用spring的事件機制去寫相關代碼會非常簡單物遇。

Spring 對Event的注解支持

上述的幾個接口已經(jīng)非常清爽了,如果習慣使用注解诱鞠,Spring也提供了挎挖,不再需要顯示實現(xiàn)

注解式的事件發(fā)布者

@Service
public class UserService {
    public void register(String name) {
        System.out.println("用戶:" + name + " 已注冊!");
        applicationEventPublisher.publishEvent(new UserRegisterEvent(name));
    }
    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;
}

Spring4.2之后航夺,ApplicationEventPublisher自動被注入到容器中蕉朵,采用Autowired即可獲取。

注解式的事件訂閱者

@Service
public class EmailService {
    @EventListener
    public void listenUserRegisterEvent(UserRegisterEvent userRegisterEvent) {
        System.out.println("郵件服務接到通知阳掐,給 " + userRegisterEvent.getSource() + " 發(fā)送郵件...");
    }
}

@EventListener注解完成了ApplicationListener<E extends ApplicationEvent>接口的使命始衅。

Spring事件機制實現(xiàn)之前提到的注冊流程

用戶注冊

這里講解一下Spring對異步事件機制的支持冷蚂,實現(xiàn)方式有兩種:

1、全局異步

即只要是觸發(fā)事件都是以異步執(zhí)行汛闸,具體配置(spring-config-register.xml)如下:

<task:executor id="executor" pool-size="10" />  
<!--名字必須是applicationEventMulticaster和messageSource是一樣的蝙茶,默認找這個名字的對象 
名字必須是applicationEventMulticaster,因為AbstractApplicationContext默認找個
如果找不到就new一個诸老,但不是異步調(diào)用而是同步調(diào)用 -->  
<bean id="applicationEventMulticaster"     class="org.springframework.context.event.SimpleApplicationEventMulticaster">  
<!-- 注入任務執(zhí)行器 這樣就實現(xiàn)了異步調(diào)用(缺點是全局的隆夯,要么全部異步,要么全部同步(刪除這個屬性即是同步))  -->  
    <property name="taskExecutor" ref="executor"/>  
</bean> 

通過注入taskExecutor來完成異步調(diào)用别伏。具體實現(xiàn)可參考之前的代碼介紹蹄衷。這種方式的缺點很明顯:要么大家都是異步,要么大家都不是厘肮。所以不推薦使用這種方式愧口。

2、更靈活的異步支持

spring3提供了@Aync注解來完成異步調(diào)用类茂。此時我們可以使用這個新特性來完成異步調(diào)用耍属。不僅支持異步調(diào)用,還支持簡單的任務調(diào)度巩检,比如我的項目就去掉Quartz依賴厚骗,直接使用spring3這個新特性,具體可參考spring-config.xml碴巾。

2.1溯捆、開啟異步調(diào)用支持
<!-- 開啟@AspectJ AOP代理 -->  
<aop:aspectj-autoproxy proxy-target-class="true"/>  
<!-- 任務調(diào)度器 -->  
<task:scheduler id="scheduler" pool-size="10"/>  
<!-- 任務執(zhí)行器 -->  
<task:executor id="executor" pool-size="10"/>  
<!--開啟注解調(diào)度支持 @Async @Scheduled-->  
<task:annotation-driven executor="executor" scheduler="scheduler" proxy-target-class="true"/>  
2.2、配置監(jiān)聽器讓其支持異步調(diào)用
@Component  
public class EmailRegisterListener implements ApplicationListener<RegisterEvent> {  
    @Async  
    @Override  
    public void onApplicationEvent(final RegisterEvent event) {  
        System.out.println("注冊成功厦瓢,發(fā)送確認郵件給:" + ((User)event.getSource()).getUsername());  
    }  
}  

使用@Async注解即可,非常簡單啤月。

這樣不僅可以支持通過調(diào)用煮仇,也支持異步調(diào)用,非常的靈活谎仲,實際應用推薦大家使用這種方式浙垫。

通過如上,大體了解了Spring的事件機制郑诺,可以使用該機制非常簡單的完成如注冊流程夹姥,而且對于比較耗時的調(diào)用,可以直接使用Spring自身的異步支持來優(yōu)化辙诞。

代碼地址:https://gitee.com/algernoon/event.git

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末辙售,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子飞涂,更是在濱河造成了極大的恐慌旦部,老刑警劉巖祈搜,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異士八,居然都是意外死亡容燕,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進店門婚度,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蘸秘,“玉大人,你說我怎么就攤上這事蝗茁∶匮” “怎么了?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵评甜,是天一觀的道長灰粮。 經(jīng)常有香客問我,道長忍坷,這世上最難降的妖魔是什么粘舟? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮佩研,結果婚禮上柑肴,老公的妹妹穿的比我還像新娘。我一直安慰自己旬薯,他們只是感情好晰骑,可當我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著绊序,像睡著了一般硕舆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上骤公,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天抚官,我揣著相機與錄音,去河邊找鬼阶捆。 笑死凌节,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的洒试。 我是一名探鬼主播倍奢,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼垒棋!你這毒婦竟也來了卒煞?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤捕犬,失蹤者是張志新(化名)和其女友劉穎跷坝,沒想到半個月后酵镜,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡柴钻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年淮韭,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贴届。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡靠粪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出毫蚓,到底是詐尸還是另有隱情占键,我是刑警寧澤,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布元潘,位于F島的核電站畔乙,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏翩概。R本人自食惡果不足惜牲距,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望钥庇。 院中可真熱鬧牍鞠,春花似錦、人聲如沸评姨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吐句。三九已至胁后,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蕴侧,已是汗流浹背择同。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留净宵,地道東北人。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓裹纳,卻偏偏與公主長得像择葡,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子剃氧,可洞房花燭夜當晚...
    茶點故事閱讀 43,514評論 2 348

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理敏储,服務發(fā)現(xiàn),斷路器朋鞍,智...
    卡卡羅2017閱讀 134,633評論 18 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,773評論 6 342
  • 正則表達式是一種用來匹配字符串的強有力的武器已添。它的設計思想是用一種描述性的語言來給字符串定義一個規(guī)則妥箕,凡是符合規(guī)則...
    XYZ7閱讀 2,997評論 0 0
  • 第一次知道簡書,是在課上老師打開了簡書的網(wǎng)站更舞,當時就被簡書的設計風格給迷住了畦幢。不同一些其他網(wǎng)站的雜而亂,而...
    dearestlala閱讀 202評論 0 0
  • 一缆蝉、完成的事 1.看《黑鏡》 第三季完結宇葱,絕對神劇,漲知識了 2.看《白夜行》 耶耶耶刊头,完結黍瞧,知道真相的我心情有些...
    媚兒大人閱讀 66評論 0 0