一区二区三区在线-一区二区三区亚洲视频-一区二区三区亚洲-一区二区三区午夜-一区二区三区四区在线视频-一区二区三区四区在线免费观看

服務器之家:專注于服務器技術及軟件下載分享
分類導航

PHP教程|ASP.NET教程|JAVA教程|ASP教程|

服務器之家 - 編程語言 - JAVA教程 - 使用Java8實現觀察者模式的方法(上)

使用Java8實現觀察者模式的方法(上)

2020-04-03 14:59mrr JAVA教程

本文給大家介紹使用java8實現觀察者模式的方法,涉及到java8觀察者模式相關知識,對此感興趣的朋友一起學習吧

觀察者(Observer)模式又名發布-訂閱(Publish/Subscribe)模式,是四人組(GoF,即 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides)在1994合著的《設計模式:可復用面向對象軟件的基礎》中提出的(詳見書中293-313頁)。盡管這種模式已經有相當長的歷史,它仍然廣泛適用于各種場景,甚至成為了標準Java庫的一個組成部分。目前雖然已經有大量關于觀察者模式的文章,但它們都專注于在 Java 中的實現,卻忽視了開發者在Java中使用觀察者模式時遇到的各種問題。

本文的寫作初衷就是為了填補這一空白:本文主要介紹通過使用 Java8 架構實現觀察者模式,并在此基礎上進一步探討關于經典模式的復雜問題,包括匿名內部類、lambda 表達式、線程安全以及非平凡耗時長的觀察者實現。本文內容雖然并不全面,很多這種模式所涉及的復雜問題,遠不是一篇文章就能說清的。但是讀完本文,讀者能了解什么是觀察者模式,它在Java中的通用性以及如何處理在 Java 中實現觀察者模式時的一些常見問題。

觀察者模式

根據 GoF 提出的經典定義,觀察者模式的主旨是:

定義對象間的一種一對多的依賴關系,當一個對象的狀態發生改變時,所有依賴于它的對象都得到通知并被自動更新。

什么意思呢?很多軟件應用中,對象之間的狀態都是互相依賴的。例如,如果一個應用專注于數值數據加工,這個數據也許會通過圖形用戶界面(GUI)的表格或圖表來展現或者兩者同時使用,也就是說,當底層數據更新時,相應的 GUI 組件也要更新。問題的關鍵在于如何做到底層數據更新時 GUI 組件也隨之更新,同時盡量減小 GUI 組件和底層數據的耦合度。

一種簡單且不可擴展的解決方案是給管理這些底層數據的對象該表格和圖像 GUI 組件的引用,使得對象可以在底層數據變化時能夠通知 GUI 組件。顯然,對于處理有更多 GUI 組件的復雜應用,這個簡單的解決方案很快顯示出其不足。例如,有20個 GUI 組件都依賴于底層數據,那么管理底層數據的對象就需要維護指向這20個組件的引用。隨著依賴于相關數據的對象數量的增加,數據管理和對象之間的耦合度也變得難以控制。

另一個更好的解決方案是允許對象注冊獲取感興趣數據更新的權限,當數據變化時,數據管理器就會通知這些對象。通俗地說就是,讓感興趣的數據對象告訴管理器:“當數據變化時請通知我”。此外,這些對象不僅可以注冊獲取更新通知,也可以取消注冊,保證數據管理器在數據變化時不再通知該對象。在 GoF 的原始定義中,注冊獲取更新的對象叫作“觀察者”(observer),對應的數據管理器叫作“目標”(Subject),觀察者感興趣的數據叫作“目標狀態”,注冊過程叫“添加”(attach),撤銷觀察的過程叫“移除”(detach)。前文已經提到觀察者模式又叫發布-訂閱模式,可以理解為客戶訂閱關于目標的觀察者,當目標狀態更新時,目標把這些更新發布給訂閱者(這種設計模式擴展為通用架構,稱為發布——訂閱架構)。這些概念可以用下面的類圖表示:

使用Java8實現觀察者模式的方法(上)

具體觀察者(ConcereteObserver)用來接收更新的狀態變化,同時將指向具體主題(ConcereteSubject)的引用傳遞給它的構造函數。這為具體觀察者提供了指向具體主題的引用,在狀態變化時可由此獲得更新。簡單來說,具體觀察者會被告知主題更新,同時用其構造函數中的引用來獲取具體主題的狀態,最后將這些檢索狀態對象存儲在具體觀察者的觀察狀態(observerState)屬性下。這一過程如下面的序列圖所示:

使用Java8實現觀察者模式的方法(上)

經典模式的專業化

盡管觀察者模式是通用的,但也有很多專業化的模式,最常見是以下兩種:

為State對象提供一個參數,傳給觀察者調用的Update方法。在經典模式下,當觀察者被通知Subject狀態發生變化后,會直接從Subject獲得其更新后狀態。這要求觀察者保存指向獲取狀態的對象引用。這樣就形成了一個循環引用,ConcreteSubject的引用指向其觀察者列表,ConcreteObserver的引用指向能獲得主題狀態的ConcreteSubject。除了獲得更新的狀態,觀察者和其注冊監聽的Subject間并沒有聯系,觀察者關心的是State對象,而非Subject本身。也就是說,很多情況下都將ConcreteObserver和ConcreteSubject強行聯系一起,相反,當ConcreteSubject調用Update函數時,將State對象傳遞給ConcreteObserver,二者就無需關聯。ConcreteObserver和State對象之間關聯減小了觀察者和State之間的依賴程度(關聯和依賴的更多區別請參見Martin Fowler's的文章)。

將Subject抽象類和ConcreteSubject合并到一個 singleSubject類中。多數情況下,Subject使用抽象類并不會提升程序的靈活性和可擴展性,因此,將這一抽象類和具體類合并簡化了設計。

這兩個專業化的模式組合后,其簡化類圖如下:

使用Java8實現觀察者模式的方法(上)

在這些專業化的模式中,靜態類結構大大簡化,類之間的相互作用也得以簡化。此時的序列圖如下:

使用Java8實現觀察者模式的方法(上)

專業化模式另一特點是刪除了 ConcreteObserver 的成員變量 observerState。有時候具體觀察者并不需要保存Subject的最新狀態,而只需要監測狀態更新時 Subject 的狀態。例如,如果觀察者將成員變量的值更新到標準輸出上,就可以刪除 observerState,這樣一來就刪除了ConcreteObserver和State類之間的關聯。

更常見的命名規則

經典模式甚至是前文提到的專業化模式都用的是attach,detach和observer等術語,而Java實現中很多都是用的不同的詞典,包括register,unregister,listener等。值得一提的是State是listener需要監測變化的所有對象的統稱,狀態對象的具體名稱需要看觀察者模式用到的場景。例如,在listener監聽事件發生場景下的觀察者模式,已注冊的listener將會在事件發生時收到通知,此時的狀態對象就是event,也就是事件是否發生。

平時實際應用中目標的命名很少包含Subject。例如,創建一個關于動物園的應用,注冊多個監聽器用于觀察Zoo類,并在新動物進入動物園時收到通知。該案例中的目標是Zoo類,為了和所給問題域保持術語一致,將不會用到Subject這樣的詞匯,也就是說Zoo類不會命名為ZooSubject。

監聽器的命名一般都會跟著Listener后綴,例如前文提到的監測新動物加入的監聽器會命名為AnimalAddedListener。類似的,register,、unregister和notify等函數命名常會以其對應的監聽器名作后綴,例如AnimalAddedListener的register、unregister、notify函數會被命名為registerAnimalAddedListener、 unregisterAnimalAddedListener和notifyAnimalAddedListeners,需要注意的是notify函數名的s,因為notify函數處理的是多個而非單一監聽器。

這種命名方式會顯得冗長,而且通常一個subject會注冊多個類型的監聽器,如前面提到的動物園的例子,Zoo內除了注冊監聽動物新增的監聽器,還需注冊監聽動物減少監聽器,此時就會有兩種register函數:(registerAnimalAddedListener和 registerAnimalRemovedListener,這種方式處理,監聽器的類型作為一個限定符,表示其應觀察者的類型。另一解決方案是創建一個registerListener函數然后重載,但是方案一能更方便的知道哪個監聽器正在監聽,重載是比較小眾的做法。

另一慣用語法是用on前綴而不是update,例如update函數命名為onAnimalAdded而不是updateAnimalAdded。這種情況在監聽器獲得一個序列的通知時更常見,如向list中新增一個動物,但很少用于更新一個單獨的數據,比如動物的名字。

接下來本文將使用Java的符號規則,雖然符號規則不會改變系統的真實設計和實現,但是使用其他開發者都熟悉的術語是很重要的開發準則,因此要熟悉上文描述的Java中的觀察者模式符號規則。下文將在Java8環境下用一個簡單例子來闡述上述概念。

一個簡單的實例

還是前面提到的動物園的例子,使用Java8的API接口實現一個簡單的系統,說明觀察者模式的基本原理。問題描述為:

創建一個系統zoo,允許用戶監聽和撤銷監聽添加新對象animal的狀態,另外再創建一個具體監聽器,負責輸出新增動物的name。

根據前面對觀察者模式的學習知道實現這樣的應用需要創建4個類,具體是:

Zoo類:即模式中的主題,負責存儲動物園中的所有動物,并在新動物加入時通知所有已注冊的監聽器。

Animal類:代表動物對象。

AnimalAddedListener類:即觀察者接口。

PrintNameAnimalAddedListener:具體的觀察者類,負責輸出新增動物的name。

首先我們創建一個Animal類,它是一個包含name成員變量、構造函數、getter和setter方法的簡單Java對象,代碼如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
public class Animal {
private String name;
public Animal (String name) {
this.name = name;
}
public String getName () {
return this.name;
}
public void setName (String name) {
this.name = name;
}
}

用這個類代表動物對象,接下來就可以創建AnimalAddedListener接口了:

?
1
2
3
public interface AnimalAddedListener {
public void onAnimalAdded (Animal animal);
}

前面兩個類很簡單,就不再詳細介紹,接下來創建Zoo類:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Zoo {
private List<Animal> animals = new ArrayList<>();
private List<AnimalAddedListener> listeners = new ArrayList<>();
public void addAnimal (Animal animal) {
// Add the animal to the list of animals
this.animals.add(animal);
// Notify the list of registered listeners
this.notifyAnimalAddedListeners(animal);
}
public void registerAnimalAddedListener (AnimalAddedListener listener) {
// Add the listener to the list of registered listeners
this.listeners.add(listener);
}
public void unregisterAnimalAddedListener (AnimalAddedListener listener) {
// Remove the listener from the list of the registered listeners
this.listeners.remove(listener);
}
protected void notifyAnimalAddedListeners (Animal animal) {
// Notify each of the listeners in the list of registered listeners
this.listeners.forEach(listener -> listener.updateAnimalAdded(animal));
}
}

這個類比前面兩個都復雜,其包含兩個list,一個用來存儲動物園中所有動物,另一個用來存儲所有的監聽器,鑒于animals和listener集合存儲的對象都很簡單,本文選擇了ArrayList來存儲。存儲監聽器的具體數據結構要視問題而定,比如對于這里的動物園問題,如果監聽器有優先級,那就應該選擇其他的數據結構,或者重寫監聽器的register算法。

注冊和移除的實現都是簡單的委托方式:各個監聽器作為參數從監聽者的監聽列表增加或者移除。notify函數的實現與觀察者模式的標準格式稍微偏離,它包括輸入參數:新增加的animal,這樣一來notify函數就可以把新增加的animal引用傳遞給監聽器了。用streams API的forEach函數遍歷監聽器,對每個監聽器執行theonAnimalAdded函數。

在addAnimal函數中,新增的animal對象和監聽器各自添加到對應list。如果不考慮通知過程的復雜性,這一邏輯應包含在方便調用的方法中,只需要傳入指向新增animal對象的引用即可,這就是通知監聽器的邏輯實現封裝在notifyAnimalAddedListeners函數中的原因,這一點在addAnimal的實現中也提到過。

除了notify函數的邏輯問題,需要強調一下對notify函數可見性的爭議問題。在經典的觀察者模型中,如GoF在設計模式一書中第301頁所說,notify函數是public型的,然而盡管在經典模式中用到,這并不意味著必須是public的。選擇可見性應該基于應用,例如本文的動物園的例子,notify函數是protected類型,并不要求每個對象都可以發起一個注冊觀察者的通知,只需保證對象能從父類繼承該功能即可。當然,也并非完全如此,需要弄清楚哪些類可以激活notify函數,然后再由此確定函數的可見性。

接下來需要實現PrintNameAnimalAddedListener類,這個類用System.out.println方法將新增動物的name輸出,具體代碼如下:

?
1
2
3
4
5
6
7
public class PrintNameAnimalAddedListener implements AnimalAddedListener {
@Override
public void updateAnimalAdded (Animal animal) {
// Print the name of the newly added animal
System.out.println("Added a new animal with name '" + animal.getName() + "'");
}
}

最后要實現驅動應用的主函數:

?
1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main (String[] args) {
// Create the zoo to store animals
Zoo zoo = new Zoo();
// Register a listener to be notified when an animal is added
zoo.registerAnimalAddedListener(new PrintNameAnimalAddedListener());
// Add an animal notify the registered listeners
zoo.addAnimal(new Animal("Tiger"));
}
}

主函數只是簡單的創建了一個zoo對象,注冊了一個輸出動物name的監聽器,并新建了一個animal對象以觸發已注冊的監聽器,最后的輸出為:

Added a new animal with name 'Tiger'

新增監聽器

當監聽器重新建立并將其添加到Subject時,觀察者模式的優勢就充分顯示出來。例如,想添加一個計算動物園中動物總數的監聽器,只需要新建一個具體的監聽器類并注冊到Zoo類即可,而無需對zoo類做任何修改。添加計數監聽器CountingAnimalAddedListener代碼如下:

?
1
2
3
4
5
6
7
8
9
10
public class CountingAnimalAddedListener implements AnimalAddedListener {
private static int animalsAddedCount = 0;
@Override
public void updateAnimalAdded (Animal animal) {
// Increment the number of animals
animalsAddedCount++;
// Print the number of animals
System.out.println("Total animals added: " + animalsAddedCount);
}
}

修改后的main函數如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main (String[] args) {
// Create the zoo to store animals
Zoo zoo = new Zoo();
// Register listeners to be notified when an animal is added
zoo.registerAnimalAddedListener(new PrintNameAnimalAddedListener());
zoo.registerAnimalAddedListener(new CountingAnimalAddedListener());
// Add an animal notify the registered listeners
zoo.addAnimal(new Animal("Tiger"));
zoo.addAnimal(new Animal("Lion"));
zoo.addAnimal(new Animal("Bear"));
}
}

輸出結果為:

?
1
2
3
4
5
6
Added a new animal with name 'Tiger'
Total animals added: 1
Added a new animal with name 'Lion'
Total animals added: 2
Added a new animal with name 'Bear'
Total animals added: 3

使用者可在僅修改監聽器注冊代碼的情況下,創建任意監聽器。具有此可擴展性主要是因為Subject和觀察者接口關聯,而不是直接和ConcreteObserver關聯。只要接口不被修改,調用接口的Subject就無需修改。

匿名內部類,Lambda函數和監聽器注冊

Java8的一大改進是增加了功能特性,如增加了lambda函數。在引進lambda函數之前,Java通過匿名內部類提供了類似的功能,這些類在很多已有的應用中仍在使用。在觀察者模式下,隨時可以創建新的監聽器而無需創建具體觀察者類,例如,PrintNameAnimalAddedListener類可以在main函數中用匿名內部類實現,具體實現代碼如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Main {
public static void main (String[] args) {
// Create the zoo to store animals
Zoo zoo = new Zoo();
// Register listeners to be notified when an animal is added
zoo.registerAnimalAddedListener(new AnimalAddedListener() {
@Override
public void updateAnimalAdded (Animal animal) {
// Print the name of the newly added animal
System.out.println("Added a new animal with name '" + animal.getName() + "'");
}
});
// Add an animal notify the registered listeners
zoo.addAnimal(new Animal("Tiger"));
}
}

類似的,lambda函數也可以用以完成此類任務:

?
1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main (String[] args) {
// Create the zoo to store animals
Zoo zoo = new Zoo();
// Register listeners to be notified when an animal is added
zoo.registerAnimalAddedListener(
(animal) -> System.out.println("Added a new animal with name '" + animal.getName() + "'")
);
// Add an animal notify the registered listeners
zoo.addAnimal(new Animal("Tiger"));
}
}

需要注意的是lambda函數僅適用于監聽器接口只有一個函數的情況,這個要求雖然看起來嚴格,但實際上很多監聽器都是單一函數的,如示例中的AnimalAddedListener。如果接口有多個函數,可以選擇使用匿名內部類。

隱式注冊創建的監聽器存在此類問題:由于對象是在注冊調用的范圍內創建的,所以不可能將引用存儲一個到具體監聽器。這意味著,通過lambda函數或者匿名內部類注冊的監聽器不可以撤銷注冊,因為撤銷函數需要傳入已經注冊監聽器的引用。解決這個問題的一個簡單方法是在registerAnimalAddedListener函數中返回注冊監聽器的引用。如此一來,就可以撤銷注冊用lambda函數或匿名內部類創建的監聽器,改進后的方法代碼如下:

?
1
2
3
4
5
public AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) {
// Add the listener to the list of registered listeners
this.listeners.add(listener);
return listener;
}

重新設計的函數交互的客戶端代碼如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Main {
public static void main (String[] args) {
// Create the zoo to store animals
Zoo zoo = new Zoo();
// Register listeners to be notified when an animal is added
AnimalAddedListener listener = zoo.registerAnimalAddedListener(
(animal) -> System.out.println("Added a new animal with name '" + animal.getName() + "'")
);
// Add an animal notify the registered listeners
zoo.addAnimal(new Animal("Tiger"));
// Unregister the listener
zoo.unregisterAnimalAddedListener(listener);
// Add another animal, which will not print the name, since the listener
// has been previously unregistered
zoo.addAnimal(new Animal("Lion"));
}
}

此時的結果輸出只有Added a new animal with name 'Tiger',因為在第二個animal加入之前監聽器已經撤銷了:

Added a new animal with name 'Tiger'

如果采用更復雜的解決方案,register函數也可以返回receipt類,以便unregister監聽器調用,例如:

?
1
2
3
4
5
6
7
8
9
public class AnimalAddedListenerReceipt {
private final AnimalAddedListener listener;
public AnimalAddedListenerReceipt (AnimalAddedListener listener) {
this.listener = listener;
}
public final AnimalAddedListener getListener () {
return this.listener;
}
}

receipt會作為注冊函數的返回值,以及撤銷注冊函數輸入參數,此時的zoo實現如下所示:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
public class ZooUsingReceipt {
// ...Existing attributes and constructor...
public AnimalAddedListenerReceipt registerAnimalAddedListener (AnimalAddedListener listener) {
// Add the listener to the list of registered listeners
this.listeners.add(listener);
return new AnimalAddedListenerReceipt(listener);
}
public void unregisterAnimalAddedListener (AnimalAddedListenerReceipt receipt) {
// Remove the listener from the list of the registered listeners
this.listeners.remove(receipt.getListener());
}
// ...Existing notification method...
}

上面描述的接收實現機制允許保存信息供監聽器撤銷時調用的,也就是說如果撤銷注冊算法依賴于Subject注冊監聽器時的狀態,則此狀態將被保存,如果撤銷注冊只需要指向之前注冊監聽器的引用,這樣的話接收技術則顯得麻煩,不推薦使用。

除了特別復雜的具體監聽器,最常見的注冊監聽器的方法是通過lambda函數或通過匿名內部類注冊。當然,也有例外,那就是包含subject實現觀察者接口的類和注冊一個包含調用該引用目標的監聽器。如下面代碼所示的案例:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ZooContainer implements AnimalAddedListener {
private Zoo zoo = new Zoo();
public ZooContainer () {
// Register this object as a listener
this.zoo.registerAnimalAddedListener(this);
}
public Zoo getZoo () {
return this.zoo;
}
@Override
public void updateAnimalAdded (Animal animal) {
System.out.println("Added animal with name '" + animal.getName() + "'");
}
public static void main (String[] args) {
// Create the zoo container
ZooContainer zooContainer = new ZooContainer();
// Add an animal notify the innerally notified listener
zooContainer.getZoo().addAnimal(new Animal("Tiger"));
}
}

這種方法只適用于簡單情況而且代碼看起來不夠專業,盡管如此,它還是深受現代Java開發人員的喜愛,因此了解這個例子的工作原理很有必要。因為ZooContainer實現了AnimalAddedListener接口,那么ZooContainer的實例(或者說對象)就可以注冊為AnimalAddedListener。ZooContainer類中,該引用代表當前對象即ZooContainer的一個實例,所以可以被用作AnimalAddedListener。

通常,不是要求所有的container類都實現此類功能,而且實現監聽器接口的container類只能調用Subject的注冊函數,只是簡單把該引用作為監聽器的對象傳給register函數。在接下來的章節中,將介紹多線程環境的常見問題和解決方案。

OneAPM 為您提供端到端的Java 應用性能解決方案,我們支持所有常見的 Java 框架及應用服務器,助您快速發現系統瓶頸,定位異常根本原因。分鐘級部署,即刻體驗,Java 監控從來沒有如此簡單。想閱讀更多技術文章,請訪問OneAPM 官方技術博客。

以上內容給大家介紹了使用Java8實現觀察者模式的方法(上)的相關內容,下篇文章給大家介紹使用java8實現觀察者模式的方法(下),感興趣的朋友繼續學習吧,希望對大家有所幫助!

延伸 · 閱讀

精彩推薦
  • JAVA教程Java實現驗證碼具體代碼

    Java實現驗證碼具體代碼

    這篇文章主要介紹了Java實現驗證碼具體代碼,有需要的朋友可以參考一下 ...

    java教程網2632019-10-24
  • JAVA教程java實現文件重命名的方法

    java實現文件重命名的方法

    這篇文章主要介紹了java實現文件重命名的方法,涉及java針對文件的重命名操作技巧,具有一定參考借鑒價值,需要的朋友可以參考下 ...

    zzt06051932019-12-27
  • JAVA教程java實現圖片裁切的工具類實例

    java實現圖片裁切的工具類實例

    這篇文章主要介紹了java實現圖片裁切的工具類實例,涉及Java針對圖片的讀取、修改等相關操作技巧,具有一定參考借鑒價值,需要的朋友可以參考下 ...

    5iasp3722020-01-15
  • JAVA教程解析Java編程中對于包結構的命名和訪問

    解析Java編程中對于包結構的命名和訪問

    這篇文章主要介紹了Java編程中對于包結構的命名和訪問,是Java入門學習中的基礎知識,需要的朋友可以參考下 ...

    爪哇小博2572020-03-13
  • JAVA教程初識JAVA數組

    初識JAVA數組

    java語言中,數組是一種最簡單的復合數據類型。數組是有序數據的集合,數組中的每個元素具有相同的數據類型,可以用一個統一的數組名和下標來唯一地...

    hebedich1452019-11-27
  • JAVA教程java計算自冪數和水仙花數

    java計算自冪數和水仙花數

    對于一個正整數而言,長度是n,如果它的各位上的數字的n次方之和正好等于它本身,那么我們稱這樣的數為自冪數,下面使用JAVA實現這個方法 ...

    java教程網4892019-11-13
  • JAVA教程Java正則表達式匹配電話格式

    Java正則表達式匹配電話格式

    正則表達式是由普通的字符以及特殊字符組成的文字模式,用來在查找文字主體時待匹配的一個或多個字符串。本文給大家介紹java正則表達式匹配電話格式...

    wangdnbre1442020-03-04
  • JAVA教程Java封裝好的mail包發送電子郵件的類

    Java封裝好的mail包發送電子郵件的類

    本文給大家分享了2個java封裝好的mail包發送電子郵件的類,并附上使用方法,小伙伴們可以根據自己的需求自由選擇。 ...

    hebedich3702020-03-21
主站蜘蛛池模板: 韩国最新理论片奇忧影院 | 国产婷婷综合丁香亚洲欧洲 | 美女用屁股把人吞进肚子 | 青青色在线观看 | 亚洲国产一区 | 性bbbbwwbbbb| 日本无遮挡吸乳视频看看 | 91大神在线精品播放 | 国产成人久久 | 日本动漫xxxxxx | 亚洲同性男男gay1069 | 亚洲精品国产在线网站 | 超时空要爱国语完整版在线 | 女同学用白丝脚玩我的故事 | 国产精品 视频一区 二区三区 | 麻豆资源 | 女教师的一级毛片 | 日韩欧美国产综合精品 | 美女又爽又黄免费 | 日韩免费在线视频 | 精精国产www视频在线观看免费 | 色帝国亚洲欧美在线蜜汁tv | 调教校花浣肠开菊 | 99久久伊人精品波多野结衣 | 91视频破解版 | 日本午夜大片免费观看视频 | 久久99热狠狠色AV蜜臀 | 日本b站一卡二不卡三卡四卡 | 色色色色色色网 | 性鸥美 | 欧美xxoo做爰猛烈视频 | 3d欧美人禽交| 国产香蕉视频在线观看 | 国产网站免费在线观看 | 亚洲天堂网站在线 | 99热影视| 91噜噜噜噜色 | 精品国产欧美一区二区五十路 | 美日韩一区二区三区 | 国产91网站在线观看 | 午夜精品久久久久久中宇 |