日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > javascript >内容正文

javascript

Spring 事务管理高级应用难点剖析--转

發布時間:2025/4/5 javascript 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Spring 事务管理高级应用难点剖析--转 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

第 1 部分

http://www.ibm.com/search/csass/search/?q=%E4%BA%8B%E5%8A%A1&sn=dw&lang=zh&cc=CN&en=utf&hpp=20&dws=cndw&lo=zh

概述

Spring 最成功,最吸引人的地方莫過于輕量級的聲明式事務管理,僅此一點,它就宣告了重量級 EJB 容器的覆滅。Spring 聲明式事務管理將開發者從繁復的事務管理代碼中解脫出來,專注于業務邏輯的開發上,這是一件可以被拿來頂禮膜拜的事情。但是,世界并未從此消停,開發人員需要面對的是層出不窮的應用場景,這些場景往往逾越了普通 Spring 技術書籍的理想界定。因此,隨著應用開發的深入,在使用經過 Spring 層層封裝的聲明式事務時,開發人員越來越覺得自己墜入了迷霧,陷入了沼澤,體會不到外界所宣稱的那種暢快淋漓。本系列文章的目標旨在整理并剖析實際應用中種種讓我們迷茫的場景,讓陽光照進云遮霧障的山頭。

DAO 和事務管理的牽絆

很少有使用 Spring 但不使用 Spring 事務管理器的應用,因此常常有人會問:是否用了 Spring,就一定要用 Spring 事務管理器,否則就無法進行數據的持久化操作呢?事務管理器和 DAO 是什么關系呢?

也許是 DAO 和事務管理如影隨行的緣故吧,這個看似簡單的問題實實在在地存在著,從初學者心中涌出,縈繞在開發老手的腦際。答案當然是否定的!我們都知道:事務管理是保證數據操作的事務性(即原子性、一致性、隔離性、持久性,也即所謂的 ACID),脫離了事務性,DAO 照樣可以順利地進行數據的操作。

下面,我們來看一段使用 Spring JDBC 進行數據訪問的代碼:

清單 1. UserJdbcWithoutTransManagerService.java
package user.withouttm;import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.apache.commons.dbcp.BasicDataSource;@Service("service1") public class UserJdbcWithoutTransManagerService {@Autowiredprivate JdbcTemplate jdbcTemplate;public void addScore(String userName,int toAdd){String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?";jdbcTemplate.update(sql,toAdd,userName);}public static void main(String[] args) {ApplicationContext ctx = new ClassPathXmlApplicationContext("user/withouttm/jdbcWithoutTransManager.xml");UserJdbcWithoutTransManagerService service = (UserJdbcWithoutTransManagerService)ctx.getBean("service1");JdbcTemplate jdbcTemplate = (JdbcTemplate)ctx.getBean("jdbcTemplate");BasicDataSource basicDataSource = (BasicDataSource)jdbcTemplate.getDataSource();//①.檢查數據源autoCommit的設置System.out.println("autoCommit:"+ basicDataSource.getDefaultAutoCommit());//②.插入一條記錄,初始分數為10jdbcTemplate.execute("INSERT INTO t_user(user_name,password,score) VALUES('tom','123456',10)");//③.調用工作在無事務環境下的服務類方法,將分數添加20分service.addScore("tom",20);//④.查看此時用戶的分數int score = jdbcTemplate.queryForInt("SELECT score FROM t_user WHERE user_name ='tom'");System.out.println("score:"+score);jdbcTemplate.execute("DELETE FROM t_user WHERE user_name='tom'");} }

jdbcWithoutTransManager.xml 的配置文件如下所示:

清單 2. jdbcWithoutTransManager.xml
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xmlns:p="http://www.springframework.org/schema/p"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"><context:component-scan base-package="user.withouttm"/><!-- 數據源默認將autoCommit設置為true --><bean id="dataSource"class="org.apache.commons.dbcp.BasicDataSource"destroy-method="close"p:driverClassName="oracle.jdbc.driver.OracleDriver"p:url="jdbc:oracle:thin:@localhost:1521:orcl"p:username="test"p:password="test"/><bean id="jdbcTemplate"class="org.springframework.jdbc.core.JdbcTemplate"p:dataSource-ref="dataSource"/> </beans>

運行 UserJdbcWithoutTransManagerService,在控制臺上打出如下的結果:

defaultAutoCommit:true score:30

在 jdbcWithoutTransManager.xml 中,沒有配置任何事務管理器,但是數據已經成功持久化到數據庫中。在默認情況下,dataSource 數據源的 autoCommit 被設置為 true ―― 這也意謂著所有通過 JdbcTemplate 執行的語句馬上提交,沒有事務。如果將 dataSource 的 defaultAutoCommit 設置為 false,再次運行 UserJdbcWithoutTransManagerService,將拋出錯誤,原因是新增及更改數據的操作都沒有提交到數據庫,所以 ④ 處的語句因無法從數據庫中查詢到匹配的記錄而引發異常。

對于強調讀速度的應用,數據庫本身可能就不支持事務,如使用 MyISAM 引擎的 MySQL 數據庫。這時,無須在 Spring 應用中配置事務管理器,因為即使配置了,也是沒有實際用處的。

不過,對于 Hibernate 來說,情況就有點復雜了。因為 Hibernate 的事務管理擁有其自身的意義,它和 Hibernate 一級緩存有密切的關系:當我們調用 Session 的 save、update 等方法時,Hibernate 并不直接向數據庫發送 SQL 語句,而是在提交事務(commit)或 flush 一級緩存時才真正向數據庫發送 SQL。所以,即使底層數據庫不支持事務,Hibernate 的事務管理也是有一定好處的,不會對數據操作的效率造成負面影響。所以,如果是使用 Hibernate 數據訪問技術,沒有理由不配置 HibernateTransactionManager 事務管理器。

但是,不使用 Hibernate 事務管理器,在 Spring 中,Hibernate 照樣也可以工作,來看下面的例子:

清單 3.UserHibernateWithoutTransManagerService.java
package user.withouttm;import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.orm.hibernate3.HibernateTemplate; import org.apache.commons.dbcp.BasicDataSource; import user.User;@Service("service2") public class UserHibernateWithoutTransManagerService {@Autowiredprivate HibernateTemplate hibernateTemplate;public void addScore(String userName,int toAdd){User user = (User)hibernateTemplate.get(User.class,userName);user.setScore(user.getScore()+toAdd);hibernateTemplate.update(user);}public static void main(String[] args) {//參考UserJdbcWithoutTransManagerService相應代碼…} }

此時,采用 hiberWithoutTransManager.xml 的配置文件,其配置內容如下:

清單 4.hiberWithoutTransManager.xml
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xmlns:p="http://www.springframework.org/schema/p"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context-3.0.xsd"><!--省略掉包掃描,數據源,JdbcTemplate配置部分,參見jdbcWithoutTransManager.xml -->…<bean id="sessionFactory"class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"p:dataSource-ref="dataSource"><property name="annotatedClasses"><list><value>user.User</value></list></property><property name="hibernateProperties"><props><prop key="hibernate.dialect">org.hibernate.dialect.Oracle10gDialect</prop><prop key="hibernate.show_sql">true</prop></props></property></bean><bean id="hibernateTemplate"class="org.springframework.orm.hibernate3.HibernateTemplate"p:sessionFactory-ref="sessionFactory"/> </beans>

運行 UserHibernateWithoutTransManagerService,程序正確執行,并得到類似于 UserJdbcWithoutTransManagerService 的執行結果,這說明 Hibernate 在 Spring 中,在沒有事務管理器的情況下,依然可以正常地進行數據的訪問。

應用分層的迷惑

Web、Service 及 DAO 三層劃分就像西方國家的立法、行政、司法三權分立一樣被奉為金科玉律,甚至有開發人員認為如果要使用 Spring 的事務管理就一定先要進行三層的劃分。這個看似荒唐的論調在開發人員中頗有市場。更有甚者,認為每層必須先定義一個接口,然后再定義一個實現類。其結果是:一個很簡單的功能,也至少需要 3 個接口,3 個類,再加上視圖層的 JSP 和 JS 等,打牌都可以轉上兩桌了,這種誤解貽害不淺。

對將“面向接口編程”奉為圭臬,認為放之四海而皆準的論調,筆者深不以為然。是的,“面向接口編程”是 Martin Fowler,Rod Johnson 這些大師提倡的行事原則。如果拿這條原則去開發架構,開發產品,怎么強調都不為過。但是,對于我們一般的開發人員來說,做的最多的是普通工程項目,往往最多的只是一些對數據庫增、刪、查、改的功能。此時,“面向接口編程”除了帶來更多的類文件外,看不到更多其它的好處。

Spring 框架提供的所有附加的好處(AOP、注解增強、注解 MVC 等)唯一的前提就是讓 POJO 的類變成一個受 Spring 容器管理的 Bean,除此以外沒有其它任何的要求。下面的實例用一個 POJO 完成所有的功能,既是 Controller,又是 Service,還是 DAO:

清單 5. MixLayerUserService.java
package user.mixlayer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; //①.將POJO類通過注解變成Spring MVC的Controller @Controller public class MixLayerUserService {//②.自動注入JdbcTemplate@Autowiredprivate JdbcTemplate jdbcTemplate;//③.通過Spring MVC注解映URL請求@RequestMapping("/logon.do") public String logon(String userName,String password){if(isRightUser(userName,password)){String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?";jdbcTemplate.update(sql,20,userName);return "success";}else{return "fail";}}private boolean isRightUser(String userName,String password){//do sth...return true;} }

通過 @Controller 注解將 MixLayerUserService 變成 Web 層的 Controller,同時也是 Service 層的服務類。此外,由于直接使用 JdbcTemplate 訪問數據,所以 MixLayerUserService 還是一個 DAO。來看一下對應的 Spring 配置文件:

清單 6.applicationContext.xml
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop"xmlns:tx="http://www.springframework.org/schema/tx"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"><!--掃描Web類包,通過注釋生成Bean--><context:component-scan base-package="user.mixlayer"/><!--①.啟動Spring MVC的注解功能,完成請求和注解POJO的映射--><bean class="org.springframework.web.servlet.mvc.annotation .AnnotationMethodHandlerAdapter"/><!--模型視圖名稱的解析,即在模型視圖名稱添加前后綴 --><bean class="org.springframework.web.servlet.view .InternalResourceViewResolver"p:prefix="/WEB-INF/jsp/" p:suffix=".jsp"/><!--普通數據源 --><bean id="dataSource"class="org.apache.commons.dbcp.BasicDataSource"destroy-method="close"p:driverClassName="oracle.jdbc.driver.OracleDriver"p:url="jdbc:oracle:thin:@localhost:1521:orcl"p:username="test"p:password="test"/><bean id="jdbcTemplate"class="org.springframework.jdbc.core.JdbcTemplate"p:dataSource-ref="dataSource"/><!--事務管理器 --><bean id="jdbcManager"class="org.springframework.jdbc.datasource.DataSourceTransactionManager"p:dataSource-ref="dataSource"/><!--②使用aop和tx命名空間語法為MixLayerUserService所有公用方法添加事務增強 --><aop:config proxy-target-class="true"><aop:pointcut id="serviceJdbcMethod"expression="execution(public * user.mixlayer.MixLayerUserService.*(..))"/><aop:advisor pointcut-ref="serviceJdbcMethod" advice-ref="jdbcAdvice" order="0"/></aop:config><tx:advice id="jdbcAdvice" transaction-manager="jdbcManager"><tx:attributes><tx:method name="*"/></tx:attributes></tx:advice> </beans>

在 ① 處,我們定義配置了 AnnotationMethodHandlerAdapter,以便啟用 Spring MVC 的注解驅動功能。而②和③處通過 Spring 的 aop 及 tx 命名空間,以及 Aspject 的切點表達式語法進行事務增強的定義,對 MixLayerUserService 的所有公有方法進行事務增強。要使程序能夠運行起來還必須進行 web.xml 的相關配置:

清單 7.web.xml
<?xml version="1.0" encoding="GB2312"?> <web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://java.sun.com/xml/ns/j2eehttp://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"><context-param><param-name>contextConfigLocation</param-name><param-value>classpath*:user/mixlayer/applicationContext.xml</param-value></context-param><context-param><param-name>log4jConfigLocation</param-name><param-value>/WEB-INF/classes/log4j.properties</param-value></context-param><listener><listener-class>org.springframework.web.util.Log4jConfigListener</listener-class></listener><listener><listener-class>org.springframework.web.context.ContextLoaderListener</listener-class></listener><servlet><servlet-name>user</servlet-name><servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class><!--①通過contextConfigLocation參數指定Spring配置文件的位置 --><init-param><param-name>contextConfigLocation</param-name><param-value>classpath:user/mixlayer/applicationContext.xml</param-value></init-param><load-on-startup>1</load-on-startup></servlet><servlet-mapping><servlet-name>user</servlet-name><url-pattern>*.do</url-pattern></servlet-mapping> </web-app>

這個配置文件很簡單,唯一需要注意的是 DispatcherServlet 的配置。默認情況下 Spring MVC 根據 Servlet 的名字查找 WEB-INF 下的 <servletName>-servlet.xml 作為 Spring MVC 的配置文件,在此,我們通過 contextConfigLocation 參數顯式指定 Spring MVC 配置文件的確切位置。

將 org.springframework.jdbc 及 org.springframework.transaction 的日志級別設置為 DEBUG,啟動項目,并訪問 http://localhost:8088/logon.do?userName=tom 應用,MixLayerUserService#logon 方法將作出響應,查看后臺輸出日志:

清單 8 執行日志
13:24:22,625 DEBUG (AbstractPlatformTransactionManager.java:365) - Creating new transaction with name [user.mixlayer.MixLayerUserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT 13:24:22,906 DEBUG (DataSourceTransactionManager.java:205) - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@6e1cbf] for JDBC transaction 13:24:22,921 DEBUG (DataSourceTransactionManager.java:222) - Switching JDBC Connection [org.apache.commons.dbcp.PoolableConnection@6e1cbf] to manual commit 13:24:22,921 DEBUG (JdbcTemplate.java:785) - Executing prepared SQL update 13:24:22,921 DEBUG (JdbcTemplate.java:569) - Executing prepared SQL statement [UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?] 13:24:23,140 DEBUG (JdbcTemplate.java:794) - SQL update affected 0 rows 13:24:23,140 DEBUG (AbstractPlatformTransactionManager.java:752) - Initiating transaction commit 13:24:23,140 DEBUG (DataSourceTransactionManager.java:265) - Committing JDBC transaction on Connection [org.apache.commons.dbcp.PoolableConnection@6e1cbf] 13:24:23,140 DEBUG (DataSourceTransactionManager.java:323) - Releasing JDBC Connection [org.apache.commons.dbcp.PoolableConnection@6e1cbf] after transaction 13:24:23,156 DEBUG (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource

日志中粗體部分說明了 MixLayerUserService#logon 方法已經正確運行在事務上下文中。

Spring 框架本身不應該是復雜化代碼的理由,使用 Spring 的開發者應該是無拘無束的:從實際應用出發,去除掉那些所謂原則性的接口,去除掉強制分層的束縛,簡單才是硬道理。

事務方法嵌套調用的迷茫

Spring 事務一個被訛傳很廣說法是:一個事務方法不應該調用另一個事務方法,否則將產生兩個事務。結果造成開發人員在設計事務方法時束手束腳,生怕一不小心就踩到地雷。

其實這種是不認識 Spring 事務傳播機制而造成的誤解,Spring 對事務控制的支持統一在 TransactionDefinition 類中描述,該類有以下幾個重要的接口方法:

  • int getPropagationBehavior():事務的傳播行為
  • int getIsolationLevel():事務的隔離級別
  • int getTimeout():事務的過期時間
  • boolean isReadOnly():事務的讀寫特性。

很明顯,除了事務的傳播行為外,事務的其它特性 Spring 是借助底層資源的功能來完成的,Spring 無非只充當個代理的角色。但是事務的傳播行為卻是 Spring 憑借自身的框架提供的功能,是 Spring 提供給開發者最珍貴的禮物,訛傳的說法玷污了 Spring 事務框架最美麗的光環。

所謂事務傳播行為就是多個事務方法相互調用時,事務如何在這些方法間傳播。Spring 支持 7 種事務傳播行為:

  • PROPAGATION_REQUIRED 如果當前沒有事務,就新建一個事務,如果已經存在一個事務中,加入到這個事務中。這是最常見的選擇。
  • PROPAGATION_SUPPORTS 支持當前事務,如果當前沒有事務,就以非事務方式執行。
  • PROPAGATION_MANDATORY 使用當前的事務,如果當前沒有事務,就拋出異常。
  • PROPAGATION_REQUIRES_NEW 新建事務,如果當前存在事務,把當前事務掛起。
  • PROPAGATION_NOT_SUPPORTED 以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
  • PROPAGATION_NEVER 以非事務方式執行,如果當前存在事務,則拋出異常。
  • PROPAGATION_NESTED 如果當前存在事務,則在嵌套事務內執行。如果當前沒有事務,則執行與 PROPAGATION_REQUIRED 類似的操作。

Spring 默認的事務傳播行為是 PROPAGATION_REQUIRED,它適合于絕大多數的情況。假設 ServiveX#methodX() 都工作在事務環境下(即都被 Spring 事務增強了),假設程序中存在如下的調用鏈:Service1#method1()->Service2#method2()->Service3#method3(),那么這 3 個服務類的 3 個方法通過 Spring 的事務傳播機制都工作在同一個事務中。

下面,我們來看一下實例,UserService#logon() 方法內部調用了 UserService#updateLastLogonTime() 和 ScoreService#addScore() 方法,這兩個類都繼承于 BaseService。它們之間的類結構說明如下:

圖 1. UserService 和 ScoreService

具體的代碼如下所示:

清單 9 UserService.java
@Service("userService") public class UserService extends BaseService {@Autowiredprivate JdbcTemplate jdbcTemplate;@Autowiredprivate ScoreService scoreService;public void logon(String userName) {updateLastLogonTime(userName);scoreService.addScore(userName, 20);}public void updateLastLogonTime(String userName) {String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";jdbcTemplate.update(sql, System.currentTimeMillis(), userName);} }

UserService 中注入了 ScoreService 的 Bean,ScoreService 的代碼如下所示:

清單 10 ScoreService.java
@Service("scoreUserService") public class ScoreService extends BaseService{@Autowiredprivate JdbcTemplate jdbcTemplate;public void addScore(String userName, int toAdd) {String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?";jdbcTemplate.update(sql, toAdd, userName);} }

通過 Spring 的事務配置為 ScoreService 及 UserService 中所有公有方法都添加事務增強,讓這些方法都工作于事務環境下。下面是關鍵的配置代碼:

清單 11 事務增強配置
<!-- 添加Spring事務增強 --> <aop:config proxy-target-class="true"><aop:pointcut id="serviceJdbcMethod"<!-- 所有繼承于BaseService類的子孫類的public方法都進行事務增強-->expression="within(user.nestcall.BaseService+)"/><aop:advisor pointcut-ref="serviceJdbcMethod" advice-ref="jdbcAdvice" order="0"/> </aop:config> <tx:advice id="jdbcAdvice" transaction-manager="jdbcManager"><tx:attributes><tx:method name="*"/></tx:attributes> </tx:advice>

將日志級別設置為 DEBUG,啟動 Spring 容器并執行 UserService#logon() 的方法,仔細觀察如下的輸出日志:

清單 12 執行日志
16:25:04,765 DEBUG (AbstractPlatformTransactionManager.java:365) - Creating new transaction with name [user.nestcall.UserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT ①為UserService#logon方法啟動一個事務16:25:04,765 DEBUG (DataSourceTransactionManager.java:205) - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@32bd65] for JDBC transactionlogon method...updateLastLogonTime... ②直接執行updateLastLogonTime方法16:25:04,781 DEBUG (JdbcTemplate.java:785) - Executing prepared SQL update16:25:04,781 DEBUG (JdbcTemplate.java:569) - Executing prepared SQL statement [UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?]16:25:04,828 DEBUG (JdbcTemplate.java:794) - SQL update affected 0 rows16:25:04,828 DEBUG (AbstractPlatformTransactionManager.java:470) - Participating in existing transaction ③ScoreService#addScore方法加入到UserService#logon的事務中addScore...16:25:04,828 DEBUG (JdbcTemplate.java:785) - Executing prepared SQL update16:25:04,828 DEBUG (JdbcTemplate.java:569) - Executing prepared SQL statement [UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?]16:25:04,828 DEBUG (JdbcTemplate.java:794) - SQL update affected 0 rows16:25:04,828 DEBUG (AbstractPlatformTransactionManager.java:752) - Initiating transaction commit④提交事務16:25:04,828 DEBUG (DataSourceTransactionManager.java:265) - Committing JDBC transactionon Connection [org.apache.commons.dbcp.PoolableConnection@32bd65]16:25:04,828 DEBUG (DataSourceTransactionManager.java:323) - Releasing JDBC Connection [org.apache.commons.dbcp.PoolableConnection@32bd65] after transaction16:25:04,828 DEBUG (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource

從上面的輸入日志中,可以清楚地看到 Spring 為 UserService#logon() 方法啟動了一個新的事務,而 UserSerive#updateLastLogonTime() 和 UserService#logon() 是在相同的類中,沒有觀察到有事務傳播行為的發生,其代碼塊好像“直接合并”到 UserService#logon() 中。接著,當執行到 ScoreService#addScore() 方法時,我們就觀察到了發生了事務傳播的行為:Participating in existing transaction,這說明 ScoreService#addScore() 添加到 UserService#logon() 的事務上下文中,兩者共享同一個事務。所以最終的結果是 UserService 的 logon(), updateLastLogonTime() 以及 ScoreService 的 addScore 都工作于同一事務中。

多線程的困惑

由于 Spring 的事務管理器是通過線程相關的 ThreadLocal 來保存數據訪問基礎設施,再結合 IOC 和 AOP 實現高級聲明式事務的功能,所以 Spring 的事務天然地和線程有著千絲萬縷的聯系。

我們知道 Web 容器本身就是多線程的,Web 容器為一個 Http 請求創建一個獨立的線程,所以由此請求所牽涉到的 Spring 容器中的 Bean 也是運行于多線程的環境下。在絕大多數情況下,Spring 的 Bean 都是單實例的(singleton),單實例 Bean 的最大的好處是線程無關性,不存在多線程并發訪問的問題,也即是線程安全的。

一個類能夠以單實例的方式運行的前提是“無狀態”:即一個類不能擁有狀態化的成員變量。我們知道,在傳統的編程中,DAO 必須執有一個 Connection,而 Connection 即是狀態化的對象。所以傳統的 DAO 不能做成單實例的,每次要用時都必須 new 一個新的實例。傳統的 Service 由于將有狀態的 DAO 作為成員變量,所以傳統的 Service 本身也是有狀態的。

但是在 Spring 中,DAO 和 Service 都以單實例的方式存在。Spring 是通過 ThreadLocal 將有狀態的變量(如 Connection 等)本地線程化,達到另一個層面上的“線程無關”,從而實現線程安全。Spring 不遺余力地將狀態化的對象無狀態化,就是要達到單實例化 Bean 的目的。

由于 Spring 已經通過 ThreadLocal 的設施將 Bean 無狀態化,所以 Spring 中單實例 Bean 對線程安全問題擁有了一種天生的免疫能力。不但單實例的 Service 可以成功運行于多線程環境中,Service 本身還可以自由地啟動獨立線程以執行其它的 Service。下面,通過一個實例對此進行描述:

清單 13 UserService.java 在事務方法中啟動獨立線程運行另一個事務方法
@Service("userService") public class UserService extends BaseService {@Autowiredprivate JdbcTemplate jdbcTemplate;@Autowiredprivate ScoreService scoreService;//① 在logon方法體中啟動一個獨立的線程,在該獨立的線程中執行ScoreService#addScore()方法public void logon(String userName) {System.out.println("logon method...");updateLastLogonTime(userName);Thread myThread = new MyThread(this.scoreService,userName,20);myThread.start();}public void updateLastLogonTime(String userName) {System.out.println("updateLastLogonTime...");String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";jdbcTemplate.update(sql, System.currentTimeMillis(), userName);}//② 封裝ScoreService#addScore()的線程private class MyThread extends Thread{private ScoreService scoreService;private String userName;private int toAdd;private MyThread(ScoreService scoreService,String userName,int toAdd) {this.scoreService = scoreService;this.userName = userName;this.toAdd = toAdd;}public void run() {scoreService.addScore(userName,toAdd);}} }

將日志級別設置為 DEBUG,執行 UserService#logon() 方法,觀察以下輸出的日志:

清單 14 執行日志
[main] (AbstractPlatformTransactionManager.java:365) - Creating new transaction with name[user.multithread.UserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT ①[main] (DataSourceTransactionManager.java:205) - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@1353249] for JDBC transactionlogon method...updateLastLogonTime...[main] (JdbcTemplate.java:785) - Executing prepared SQL update[main] (JdbcTemplate.java:569) - Executing prepared SQL statement [UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?][main] (JdbcTemplate.java:794) - SQL update affected 0 rows[main] (AbstractPlatformTransactionManager.java:752) - Initiating transaction commit[Thread-2](AbstractPlatformTransactionManager.java:365) - Creating new transaction with name [user.multithread.ScoreService.addScore]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT ②[main] (DataSourceTransactionManager.java:265) - Committing JDBC transactionon Connection [org.apache.commons.dbcp.PoolableConnection@1353249] ③[main] (DataSourceTransactionManager.java:323) - Releasing JDBC Connection [org.apache.commons.dbcp.PoolableConnection@1353249] after transaction[main] (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource[Thread-2] (DataSourceTransactionManager.java:205) - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@10dc656] for JDBC transactionaddScore...[main] (JdbcTemplate.java:416) - Executing SQL statement [DELETE FROM t_user WHERE user_name='tom'][main] (DataSourceUtils.java:112) - Fetching JDBC Connection from DataSource[Thread-2] (JdbcTemplate.java:785) - Executing prepared SQL update[Thread-2] (JdbcTemplate.java:569) - Executing prepared SQL statement [UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?][main] (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource[Thread-2] (JdbcTemplate.java:794) - SQL update affected 0 rows[Thread-2] (AbstractPlatformTransactionManager.java:752) - Initiating transaction commit[Thread-2] (DataSourceTransactionManager.java:265) - Committing JDBC transaction on Connection [org.apache.commons.dbcp.PoolableConnection@10dc656] ④[Thread-2] (DataSourceTransactionManager.java:323) - Releasing JDBC Connection [org.apache.commons.dbcp.PoolableConnection@10dc656] after transaction

在 ① 處,在主線程(main)執行的 UserService#logon() 方法的事務啟動,在 ③ 處,其對應的事務提交,而在子線程(Thread-2)執行的 ScoreService#addScore() 方法的事務在 ② 處啟動,在 ④ 處對應的事務提交。

所以,我們可以得出這樣的結論:在?相同線程中進行相互嵌套調用的事務方法工作于相同的事務中。如果這些相互嵌套調用的方法工作在不同的線程中,不同線程下的事務方法工作在獨立的事務中。

小結

Spring 聲明式事務是 Spring 最核心,最常用的功能。由于 Spring 通過 IOC 和 AOP 的功能非常透明地實現了聲明式事務的功能,一般的開發者基本上無須了解 Spring 聲明式事務的內部細節,僅需要懂得如何配置就可以了。

但是在實際應用開發過程中,Spring 的這種透明的高階封裝在帶來便利的同時,也給我們帶來了迷惑。就像通過流言傳播的消息,最終聽眾已經不清楚事情的真相了,而這對于應用開發來說是很危險的。本系列文章通過剖析實際應用中給開發者造成迷惑的各種難點,通過分析 Spring 事務管理的內部運作機制將真相還原出來。

在本文中,我們通過剖析了解到以下的真相:

  • 在沒有事務管理的情況下,DAO 照樣可以順利進行數據操作;
  • 將應用分成 Web,Service 及 DAO 層只是一種參考的開發模式,并非是事務管理工作的前提條件;
  • Spring 通過事務傳播機制可以很好地應對事務方法嵌套調用的情況,開發者無須為了事務管理而刻意改變服務方法的設計;
  • 由于單實例的對象不存在線程安全問題,所以進行事務管理增強的 Bean 可以很好地工作在多線程環境下。

在?下一篇?文章中,筆者將繼續分析 Spring 事務管理的以下難點:

  • 混合使用多種數據訪問技術(如 Spring JDBC + Hibernate)的事務管理問題;
  • 進行 Spring AOP 增強的 Bean 存在哪些特殊的情況。

第 2 部分

http://www.ibm.com/developerworks/cn/java/j-lo-spring-ts2/

聯合軍種作戰的混亂

Spring 抽象的 DAO 體系兼容多種數據訪問技術,它們各有特色,各有千秋。像 Hibernate 是非常優秀的 ORM 實現方案,但對底層 SQL 的控制不太方便;而 iBatis 則通過模板化技術讓您方便地控制 SQL,但沒有 Hibernate 那樣高的開發效率;自由度最高的當然是直接使用 Spring JDBC 莫屬了,但是它也是最底層的,靈活的代價是代碼的繁復。很難說哪種數據訪問技術是最優秀的,只有在某種特定的場景下,才能給出答案。所以在一個應用中,往往采用多個數據訪問技術:一般是兩種,一種采用 ORM 技術框架,而另一種采用偏 JDBC 的底層技術,兩者珠聯璧合,形成聯合軍種,共同御敵。

但是,這種聯合軍種如何應對事務管理的問題呢?我們知道 Spring 為每種數據訪問技術提供了相應的事務管理器,難道需要分別為它們配置對應的事務管理器嗎?它們到底是如何協作,如何工作的呢?這些層出不窮的問題往往壓制了開發人員使用聯合軍種的想法。

其實,在這個問題上,我們低估了 Spring 事務管理的能力。如果您采用了一個高端 ORM 技術(Hibernate,JPA,JDO),同時采用一個 JDBC 技術(Spring JDBC,iBatis),由于前者的會話(Session)是對后者連接(Connection)的封裝,Spring 會“足夠智能地”在同一個事務線程讓前者的會話封裝后者的連接。所以,我們只要直接采用前者的事務管理器就可以了。下表給出了混合數據訪問技術所對應的事務管理器:

表 1. 混合數據訪問技術的事務管理器
混合數據訪問技術事務管理器
ORM 技術框架JDBC 技術框架
HibernateSpring JDBC 或 iBatisHibernateTransactionManager
JPASpring JDBC 或 iBatisJpaTransactionManager
JDOSpring JDBC 或 iBatisJdoTransactionManager

由于一般不會出現同時使用多個 ORM 框架的情況(如 Hibernate + JPA),我們不擬對此命題展開論述,只重點研究 ORM 框架 + JDBC 框架的情況。Hibernate + Spring JDBC 可能是被使用得最多的組合,下面我們通過實例觀察事務管理的運作情況。

清單 1.User.java:使用了注解聲明的實體類
import javax.persistence.Entity; import javax.persistence.Table; import javax.persistence.Column; import javax.persistence.Id; import java.io.Serializable; @Entity @Table(name="T_USER") public class User implements Serializable{ @Id@Column(name = "USER_NAME") private String userName; private String password; private int score; @Column(name = "LAST_LOGON_TIME")private long lastLogonTime = 0; }

再來看下 UserService 的關鍵代碼:

清單 2.UserService.java:使用 Hibernate 數據訪問技術
package user.mixdao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.stereotype.Service; import org.springframework.orm.hibernate3.HibernateTemplate; import org.apache.commons.dbcp.BasicDataSource; import user.User;@Service("userService") public class UserService extends BaseService {@Autowiredprivate HibernateTemplate hibernateTemplate;@Autowiredprivate ScoreService scoreService;public void logon(String userName) {System.out.println("logon method...");updateLastLogonTime(userName); //①使用Hibernate數據訪問技術scoreService.addScore(userName, 20); //②使用Spring JDBC數據訪問技術}public void updateLastLogonTime(String userName) {System.out.println("updateLastLogonTime...");User user = hibernateTemplate.get(User.class,userName);user.setLastLogonTime(System.currentTimeMillis());hibernateTemplate.flush(); //③請看下文的分析} }

在①處,使用 Hibernate 操作數據,而在②處調用 ScoreService#addScore(),該方法內部使用 Spring JDBC 操作數據。

在③處,我們顯式調用了 flush() 方法,將 Session 中的緩存同步到數據庫中,這個操作將即時向數據庫發送一條更新記錄的 SQL 語句。之所以要在此顯式執行 flush() 方法,原因是:默認情況下,Hibernate 要在事務提交時才將數據的更改同步到數據庫中,而事務提交發生在 logon() 方法返回前。如果所有針對數據庫的更改都使用 Hibernate,這種數據同步延遲的機制不會產生任何問題。但是,我們在 logon() 方法中同時采用了 Hibernate 和 Spring JDBC 混合數據訪問技術。Spring JDBC 無法自動感知 Hibernate 一級緩存,所以如果不及時調用 flush() 方法將數據更改同步到數據庫,則②處通過 Spring JDBC 進行數據更改的結果將被 Hibernate 一級緩存中的更改覆蓋掉,因為,一級緩存在 logon() 方法返回前才同步到數據庫!

ScoreService 使用 Spring JDBC 數據訪問技術,其代碼如下:

清單 3.ScoreService.java:使用 Spring JDBC 數據訪問技術
package user.mixdao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.apache.commons.dbcp.BasicDataSource;@Service("scoreUserService") public class ScoreService extends BaseService{@Autowiredprivate JdbcTemplate jdbcTemplate;public void addScore(String userName, int toAdd) {System.out.println("addScore...");String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?";jdbcTemplate.update(sql, toAdd, userName);//① 查看此處數據庫激活的連接數BasicDataSource basicDataSource = (BasicDataSource) jdbcTemplate.getDataSource();System.out.println("激活連接數量:"+basicDataSource.getNumActive());} }

Spring 關鍵的配置文件代碼如下所示:

清單 4. applicationContext.xml 事務配置代碼部分
<!-- 使用Hibernate事務管理器 --> <bean id="hiberManager"class="org.springframework.orm.hibernate3.HibernateTransactionManager"p:sessionFactory-ref="sessionFactory"/><!-- 對所有繼承BaseService類的公用方法實施事務增強 --> <aop:config proxy-target-class="true"><aop:pointcut id="serviceJdbcMethod"expression="within(user.mixdao.BaseService+)"/><aop:advisor pointcut-ref="serviceJdbcMethod"advice-ref="hiberAdvice"/> </aop:config><tx:advice id="hiberAdvice" transaction-manager="hiberManager"><tx:attributes><tx:method name="*"/></tx:attributes> </tx:advice>

啟動 Spring 容器,執行 UserService#logon() 方法,可以查看到如下的執行日志:

清單 5. 代碼運行日志
12:38:57,062 (AbstractPlatformTransactionManager.java:365) - Creating new transaction with name [user.mixdao.UserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT12:38:57,093 (SessionImpl.java:220) - opened session at timestamp: 1266640737012:38:57,093 (HibernateTransactionManager.java:493) - Opened new Session [org.hibernate.impl.SessionImpl@83020] for Hibernate transaction ①12:38:57,093 (HibernateTransactionManager.java:504) - Preparing JDBC Connection of Hibernate Session [org.hibernate.impl.SessionImpl@83020]12:38:57,109 (JDBCTransaction.java:54) - begin…logon method... updateLastLogonTime... …12:38:57,109 (AbstractBatcher.java:401) - select user0_.USER_NAME as USER1_0_0_, user0_.LAST_LOGON_TIME as LAST2_0_0_, user0_.password as password0_0_, user0_.score as score0_0_ from T_USER user0_ where user0_.USER_NAME=?Hibernate: select user0_.USER_NAME as USER1_0_0_, user0_.LAST_LOGON_TIME as LAST2_0_0_, user0_.password as password0_0_, user0_.score as score0_0_ from T_USER user0_ where user0_.USER_NAME=?…12:38:57,187 (HibernateTemplate.java:422) - Not closing pre-bound Hibernate Session after HibernateTemplate12:38:57,187 (HibernateTemplate.java:397) - Found thread-bound Sessionfor HibernateTemplateHibernate: update T_USER set LAST_LOGON_TIME=?, password=?, score=? where USER_NAME=?…2010-02-20 12:38:57,203 DEBUG [main] (AbstractPlatformTransactionManager.java:470) - Participating in existing transaction ② addScore...2010-02-20 12:38:57,203 DEBUG [main] (JdbcTemplate.java:785) - Executing prepared SQL update2010-02-20 12:38:57,203 DEBUG [main] (JdbcTemplate.java:569)- Executing prepared SQL statement [UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?]2010-02-20 12:38:57,203 DEBUG [main] (JdbcTemplate.java:794) - SQL update affected 1 rows激活連接數量:1 ③ 2010-02-20 12:38:57,203 DEBUG [main] (AbstractPlatformTransactionManager.java:752) - Initiating transaction commit 2010-02-20 12:38:57,203 DEBUG [main] (HibernateTransactionManager.java:652) - Committing Hibernate transaction on Session [org.hibernate.impl.SessionImpl@83020] ④2010-02-20 12:38:57,203 DEBUG [main] (JDBCTransaction.java:103) - commit ⑤

仔細觀察這段輸出日志,在①處 UserService#logon() 開啟一個新的事務,在②處 ScoreService#addScore() 方法加入到①處開啟的事務上下文中。③處的輸出是 ScoreService#addScore() 方法內部的輸出,匯報此時數據源激活的連接數為 1,這清楚地告訴我們 Hibernate 和 JDBC 這兩種數據訪問技術在同一事務上下文中“共用”一個連接。在④處,提交 Hibernate 事務,接著在⑤處觸發調用底層的 Connection 提交事務。

從以上的運行結果,我們可以得出這樣的結論:使用 Hibernate 事務管理器后,可以混合使用 Hibernate 和 Spring JDBC 數據訪問技術,它們將工作于同一事務上下文中。但是使用 Spring JDBC 訪問數據時,Hibernate 的一級或二級緩存得不到同步,此外,一級緩存延遲數據同步機制可能會覆蓋 Spring JDBC 數據更改的結果。

由于混合數據訪問技術的方案的事務同步而緩存不同步的情況,所以最好用 Hibernate 完成讀寫操作,而用 Spring JDBC 完成讀的操作。如用 Spring JDBC 進行簡要列表的查詢,而用 Hibernate 對查詢出的數據進行維護。如果確實要同時使用 Hibernate 和 Spring JDBC 讀寫數據,則必須充分考慮到 Hibernate 緩存機制引發的問題:必須充分分析數據維護邏輯,根據需要,及時調用 Hibernate 的 flush() 方法,以免覆蓋 Spring JDBC 的更改,在 Spring JDBC 更改數據庫時,維護 Hibernate 的緩存。

可以將以上結論推廣到其它混合數據訪問技術的方案中,如 Hibernate+iBatis,JPA+Spring JDBC,JDO+Spring JDBC 等。

特殊方法成漏網之魚

由于 Spring 事務管理是基于接口代理或動態字節碼技術,通過 AOP 實施事務增強的。雖然,Spring 還支持 AspectJ LTW 在類加載期實施增強,但這種方法很少使用,所以我們不予關注。

對于基于接口動態代理的 AOP 事務增強來說,由于接口的方法是 public 的,這就要求實現類的實現方法必須是 public 的(不能是 protected,private 等),同時不能使用 static 的修飾符。所以,可以實施接口動態代理的方法只能是使用“public”或“public final”修飾符的方法,其它方法不可能被動態代理,相應的也就不能實施 AOP 增強,也不能進行 Spring 事務增強了。

基于 CGLib 字節碼動態代理的方案是通過擴展被增強類,動態創建子類的方式進行 AOP 增強植入的。由于使用 final、static、private 修飾符的方法都不能被子類覆蓋,相應的,這些方法將不能被實施 AOP 增強。所以,必須特別注意這些修飾符的使用,以免不小心成為事務管理的漏網之魚。

下面通過具體的實例說明基于 CGLib 字節碼動態代理無法享受 Spring AOP 事務增強的特殊方法。

清單 6.UserService.java:4 個不同修飾符的方法
package user.special; import org.springframework.stereotype.Service;@Service("userService") public class UserService {//① private方法因訪問權限的限制,無法被子類覆蓋private void method1() {System.out.println("method1");}//② final方法無法被子類覆蓋public final void method2() {System.out.println("method2");}//③ static是類級別的方法,無法被子類覆蓋public static void method3() {System.out.println("method3");}//④ public方法可以被子類覆蓋,因此可以被動態字節碼增強public void method4() {System.out.println("method4");} }

Spring 通過 CGLib 動態代理技術對 UserService Bean 實施 AOP 事務增強的配置如下所示:

清單 7.applicationContext.xml:對 UserService 用 CGLib 實施事務增強
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop"xmlns:tx="http://www.springframework.org/schema/tx"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"><!-- 省略聲明數據源及DataSourceTransactionManager事務管理器-->…<aop:config proxy-target-class="true"><!-- ①顯式使用CGLib動態代理 --><!-- ②希望對UserService所有方法實施事務增強 --><aop:pointcut id="serviceJdbcMethod"expression="execution(* user.special.UserService.*(..))"/><aop:advisor pointcut-ref="serviceJdbcMethod" advice-ref="jdbcAdvice" order="0"/></aop:config><tx:advice id="jdbcAdvice" transaction-manager="jdbcManager"><tx:attributes><tx:method name="*"/></tx:attributes></tx:advice> </beans>

在 ① 處,我們通過 proxy-target-class="true"顯式使用 CGLib 動態代理技術,在 ② 處通過 AspjectJ 切點表達式表達 UserService 所有的方法,希望對 UserService 所有方法都實施 Spring AOP 事務增強。

在 UserService 添加一個可執行的方法,如下所示:

清單 8.UserService.java 添加 main 方法
package user.special; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.stereotype.Service;@Service("userService") public class UserService {…public static void main(String[] args) {ApplicationContext ctx = new ClassPathXmlApplicationContext("user/special/applicationContext.xml");UserService service = (UserService) ctx.getBean("userService");System.out.println("before method1");service.method1();System.out.println("after method1");System.out.println("before method2");service.method2();System.out.println("after method2");System.out.println("before method3");service.method3();System.out.println("after method3");System.out.println("before method4");service.method4();System.out.println("after method4");} }

在運行 UserService 之前,將 Log4J 日志級別設置為 DEBUG,運行以上代碼查看輸出日志,如下所示:

17:24:10,953 (AbstractBeanFactory.java:241) - Returning cached instance of singleton bean 'userService'before method1 method1 after method1 before method2 method2 after method2 before method3 method3 after method3 before method417:24:10,953 (AbstractPlatformTransactionManager.java:365) - Creating new transaction with name [user.special.UserService.method4]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT17:24:11,109 (DataSourceTransactionManager.java:205) - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@165b7e] for JDBC transaction…17:24:11,109 (DataSourceTransactionManager.java:265) - Committing JDBC transaction on Connection [org.apache.commons.dbcp.PoolableConnection@165b7e]17:24:11,125 (DataSourceTransactionManager.java:323) - Releasing JDBC Connection [org.apache.commons.dbcp.PoolableConnection@165b7e] after transaction17:24:11,125 (DataSourceUtils.java:312) - Returning JDBC Connection to DataSourceafter method4

觀察以上輸出日志,很容易發現 method1~method3 這 3 個方法都沒有被實施 Spring 的事務增強,只有 method4 被實施了事務增強。這個結果剛才驗證了我們前面的論述。

我們通過下表描述哪些特殊方法將成為 Spring AOP 事務增強的漏網之魚:

表 2. 不能被 Spring AOP 事務增強的方法
動態代理策略不能被事務增強的方法
基于接口的動態代理除 public 外的其它所有的方法,此外 public static 也不能被增強
基于 CGLib 的動態代理private、static、final 的方法

不過,需要特別指出的是,這些不能被 Spring 事務增強的特殊方法并非就不工作在事務環境下。只要它們被外層的事務方法調用了,由于 Spring 的事務管理的傳播特殊,內部方法也可以工作在外部方法所啟動的事務上下文中。我們說,這些方法不能被 Spring 進行 AOP 事務增強,是指這些方法不能啟動事務,但是外層方法的事務上下文依就可以順利地傳播到這些方法中。

這些不能被 Spring 事務增強的方法和可被 Spring 事務增強的方法唯一的區別在?“是否可以主動啟動一個新事務”:前者不能而后者可以。對于事務傳播行為來說,二者是完全相同的,前者也和后者一樣不會造成數據連接的泄漏問題。換句話說,如果這些“特殊方法”被無事務上下文的方法調用,則它們就工作在無事務上下文中;反之,如果被具有事務上下文的方法調用,則它們就工作在事務上下文中。

對于 private 的方法,由于最終都會被 public 方法封裝后再開放給外部調用,而 public 方法是可以被事務增強的,所以基本上沒有什么問題。在實際開發中,最容易造成隱患的是基于 CGLib 的動態代理時的“public static”和“public final”這兩種特殊方法。原因是它們本身是 public 的,所以可以直接被外部類(如 Web 層的 Controller 類)調用,只要調用者沒有事務上下文,這些特殊方法也就以無事務的方式運作。

小結

在本文中,我們通過剖析了解到以下的真相:

  • 混合使用多個數據訪問技術框架的最佳組合是一個 ORM 技術框架(如 Hibernate 或 JPA 等)+ 一個 JDBC 技術框架(如 Spring JDBC 或 iBatis)。直接使用 ORM 技術框架對應的事務管理器就可以了,但必須考慮 ORM 緩存同步的問題;
  • Spring AOP 增強有兩個方案:其一為基于接口的動態代理,其二為基于 CGLib 動態生成子類的代理。由于 Java 語法的特性,有些特殊方法不能被 Spring AOP 代理,所以也就無法享受 AOP 織入帶來的事務增強。

在下一篇文章中,筆者將繼續分析 Spring 事務管理的以下難點:

  • 直接獲取 Connection 時,哪些情況會造成數據連接的泄漏,以及如何應對;
  • 除 Spring JDBC 外,其它數據訪問技術數據連接泄漏的應對方案。

第 3 部分

http://www.ibm.com/developerworks/cn/java/j-lo-spring-ts3/

概述

對于應用開發者來說,數據連接泄漏無疑是一個可怕的夢魘。如果存在數據連接泄漏問題,應用程序將因數據連接資源的耗盡而崩潰,甚至還可能引起數據庫的崩潰。數據連接泄漏像黑洞一樣讓開發者避之唯恐不及。

Spring DAO 對所有支持的數據訪問技術框架都使用模板化技術進行了薄層的封裝。只要您的程序都使用 Spring DAO 模板(如 JdbcTemplate、HibernateTemplate 等)進行數據訪問,一定不會存在數據連接泄漏的問題 ―― 這是 Spring 給予我們鄭重的承諾!因此,我們無需關注數據連接(Connection)及其衍生品(Hibernate 的 Session 等)的獲取和釋放的操作,模板類已經通過其內部流程替我們完成了,且對開發者是透明的。

但是由于集成第三方產品,整合遺產代碼等原因,可能需要直接訪問數據源或直接獲取數據連接及其衍生品。這時,如果使用不當,就可能在無意中創造出一個魔鬼般的連接泄漏問題。

我們知道:當 Spring 事務方法運行時,就產生一個事務上下文,該上下文在本事務執行線程中針對同一個數據源綁定了一個唯一的數據連接(或其衍生品),所有被該事務上下文傳播的方法都共享這個數據連接。這個數據連接從數據源獲取及返回給數據源都在 Spring 掌控之中,不會發生問題。如果在需要數據連接時,能夠獲取這個被 Spring 管控的數據連接,則使用者可以放心使用,無需關注連接釋放的問題。

那么,如何獲取這些被 Spring 管控的數據連接呢? Spring 提供了兩種方法:其一是使用數據資源獲取工具類,其二是對數據源(或其衍生品如 Hibernate SessionFactory)進行代理。在具體介紹這些方法之前,讓我們先來看一下各種引發數據連接泄漏的場景。

Spring JDBC 數據連接泄漏

如果直接從數據源獲取連接,且在使用完成后不主動歸還給數據源(調用 Connection#close()),則將造成數據連接泄漏的問題。

一個具體的實例

下面,來看一個具體的實例:

清單 1.JdbcUserService.java:主體代碼
package user.connleak; import org.apache.commons.dbcp.BasicDataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import java.sql.Connection; @Service("jdbcUserService") public class JdbcUserService { @Autowired private JdbcTemplate jdbcTemplate; public void logon(String userName) { try { // ①直接從數據源獲取連接,后續程序沒有顯式釋放該連接Connection conn = jdbcTemplate.getDataSource().getConnection(); String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?"; jdbcTemplate.update(sql, System.currentTimeMillis(), userName); Thread.sleep(1000);// ②模擬程序代碼的執行時間} catch (Exception e) { e.printStackTrace(); } } }

JdbcUserService 通過 Spring AOP 事務增強的配置,讓所有 public 方法都工作在事務環境中。即讓 logon() 和 updateLastLogonTime() 方法擁有事務功能。在 logon() 方法內部,我們在①處通過調用?jdbcTemplate.getDataSource().getConnection()顯式獲取一個連接,這個連接不是 logon() 方法事務上下文線程綁定的連接,所以如果開發者如果沒有手工釋放這連接(顯式調用 Connection#close() 方法),則這個連接將永久被占用(處于 active 狀態),造成連接泄漏!下面,我們編寫模擬運行的代碼,查看方法執行對數據連接的實際占用情況:

清單 2.JdbcUserService.java:模擬運行代碼
… @Service("jdbcUserService") public class JdbcUserService {…//①以異步線程的方式執行JdbcUserService#logon()方法,以模擬多線程的環境public static void asynchrLogon(JdbcUserService userService, String userName) {UserServiceRunner runner = new UserServiceRunner(userService, userName);runner.start();}private static class UserServiceRunner extends Thread {private JdbcUserService userService;private String userName;public UserServiceRunner(JdbcUserService userService, String userName) {this.userService = userService;this.userName = userName;}public void run() {userService.logon(userName);}}//② 讓主執行線程睡眠一段指定的時間public static void sleep(long time) {try {Thread.sleep(time);} catch (InterruptedException e) {e.printStackTrace();}}//③ 匯報數據源的連接占用情況public static void reportConn(BasicDataSource basicDataSource) {System.out.println("連接數[active:idle]-[" +basicDataSource.getNumActive()+":"+basicDataSource.getNumIdle()+"]");}public static void main(String[] args) {ApplicationContext ctx = new ClassPathXmlApplicationContext("user/connleak/applicatonContext.xml");JdbcUserService userService = (JdbcUserService) ctx.getBean("jdbcUserService");BasicDataSource basicDataSource = (BasicDataSource) ctx.getBean("dataSource");//④匯報數據源初始連接占用情況JdbcUserService.reportConn(basicDataSource);JdbcUserService.asynchrLogon(userService, "tom");JdbcUserService.sleep(500);//⑤此時線程A正在執行JdbcUserService#logon()方法JdbcUserService.reportConn(basicDataSource); JdbcUserService.sleep(2000);//⑥此時線程A所執行的JdbcUserService#logon()方法已經執行完畢JdbcUserService.reportConn(basicDataSource);JdbcUserService.asynchrLogon(userService, "john");JdbcUserService.sleep(500);//⑦此時線程B正在執行JdbcUserService#logon()方法JdbcUserService.reportConn(basicDataSource);JdbcUserService.sleep(2000);//⑧此時線程A和B都已完成JdbcUserService#logon()方法的執行JdbcUserService.reportConn(basicDataSource);}

在 JdbcUserService 中添加一個可異步執行 logon() 方法的 asynchrLogon() 方法,我們通過異步執行 logon() 以及讓主線程睡眠的方式模擬多線程環境下的執行場景。在不同的執行點,通過 reportConn() 方法匯報數據源連接的占用情況。

使用如下的 Spring 配置文件對 JdbcUserServie 的方法進行事務增強:

清單 3.applicationContext.xml
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xmlns:p="http://www.springframework.org/schema/p"xmlns:aop="http://www.springframework.org/schema/aop"xmlns:tx="http://www.springframework.org/schema/tx"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"><context:component-scan base-package="user.connleak"/><bean id="dataSource"class="org.apache.commons.dbcp.BasicDataSource"destroy-method="close"p:driverClassName="oracle.jdbc.driver.OracleDriver"p:url="jdbc:oracle:thin:@localhost:1521:orcl"p:username="test"p:password="test"p:defaultAutoCommit="false"/><bean id="jdbcTemplate"class="org.springframework.jdbc.core.JdbcTemplate"p:dataSource-ref="dataSource"/><bean id="jdbcManager"class="org.springframework.jdbc.datasource.DataSourceTransactionManager"p:dataSource-ref="dataSource"/><!-- 對JdbcUserService的所有方法實施事務增強 --><aop:config proxy-target-class="true"><aop:pointcut id="serviceJdbcMethod"expression="within(user.connleak.JdbcUserService+)"/><aop:advisor pointcut-ref="serviceJdbcMethod" advice-ref="jdbcAdvice" order="0"/></aop:config><tx:advice id="jdbcAdvice" transaction-manager="jdbcManager"><tx:attributes><tx:method name="*"/></tx:attributes></tx:advice> </beans>

保證 BasicDataSource 數據源的配置默認連接為 0,運行以上程序代碼,在控制臺中將輸出以下的信息:

清單 4. 輸出日志
連接數 [active:idle]-[0:0] 連接數 [active:idle]-[2:0] 連接數 [active:idle]-[1:1] 連接數 [active:idle]-[3:0] 連接數 [active:idle]-[2:1]

我們通過下表對數據源連接的占用和泄漏情況進行描述:

表 1. 執行過程數據源連接占用情況
時間執行線程 1執行線程 2數據源連接activeidleleak
T0未啟動未啟動000
T1正在執行方法未啟動200
T2執行完畢未啟動111
T3執行完畢正式執行方法301
T4執行完畢執行完畢212

可見在執行線程 1 執行完畢后,只釋放了一個數據連接,還有一個數據連處于 active 狀態,說明泄漏了一個連接。相似的,執行線程 2 執行完畢后,也泄漏了一個連接:原因是直接通過數據源獲取連接(jdbcTemplate.getDataSource().getConnection())而沒有顯式釋放造成的。

通過 DataSourceUtils 獲取數據連接

Spring 提供了一個能從當前事務上下文中獲取綁定的數據連接的工具類,那就是 DataSourceUtils。Spring 強調必須使用 DataSourceUtils 工具類獲取數據連接,Spring 的 JdbcTemplate 內部也是通過 DataSourceUtils 來獲取連接的。DataSourceUtils 提供了若干獲取和釋放數據連接的靜態方法,說明如下:

  • static Connection doGetConnection(DataSource dataSource):首先嘗試從事務上下文中獲取連接,失敗后再從數據源獲取連接;
  • static Connection getConnection(DataSource dataSource):和 doGetConnection 方法的功能一樣,實際上,它內部就是調用 doGetConnection 方法獲取連接的;
  • static void doReleaseConnection(Connection con, DataSource dataSource):釋放連接,放回到連接池中;
  • static void releaseConnection(Connection con, DataSource dataSource):和 doReleaseConnection 方法的功能一樣,實際上,它內部就是調用 doReleaseConnection 方法獲取連接的;

來看一下 DataSourceUtils 從數據源獲取連接的關鍵代碼:

清單 5. DataSourceUtils.java 獲取連接的工具類
public abstract class DataSourceUtils {…public static Connection doGetConnection(DataSource dataSource) throws SQLException {Assert.notNull(dataSource, "No DataSource specified");//①首先嘗試從事務同步管理器中獲取數據連接ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) { conHolder.requested();if (!conHolder.hasConnection()) {logger.debug("Fetching resumed JDBC Connection from DataSource");conHolder.setConnection(dataSource.getConnection());}return conHolder.getConnection();}//②如果獲取不到,則直接從數據源中獲取連接Connection con = dataSource.getConnection();//③如果擁有事務上下文,則將連接綁定到事務上下文中if (TransactionSynchronizationManager.isSynchronizationActive()) {ConnectionHolder holderToUse = conHolder;if (holderToUse == null) {holderToUse = new ConnectionHolder(con);}else {holderToUse.setConnection(con);}holderToUse.requested();TransactionSynchronizationManager.registerSynchronization(new ConnectionSynchronization(holderToUse, dataSource));holderToUse.setSynchronizedWithTransaction(true);if (holderToUse != conHolder) {TransactionSynchronizationManager.bindResource(dataSource, holderToUse);}}return con;}… }

它首先查看當前是否存在事務管理上下文,并嘗試從事務管理上下文獲取連接,如果獲取失敗,直接從數據源中獲取連接。在獲取連接后,如果當前擁有事務上下文,則將連接綁定到事務上下文中。

我們在清單 1 的 JdbcUserService 中,使用 DataSourceUtils.getConnection() 替換直接從數據源中獲取連接的代碼:

清單 6. JdbcUserService.java:使用 DataSourceUtils 獲取數據連接
public void logon(String userName) {try {//Connection conn = jdbcTemplate.getDataSource().getConnection();//①使用DataSourceUtils獲取數據連接Connection conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource());String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?";jdbcTemplate.update(sql, System.currentTimeMillis(), userName);Thread.sleep(1000); } catch (Exception e) {e.printStackTrace();} }

重新運行代碼,得到如下的執行結果:

清單 7. 輸出日志
連接數 [active:idle]-[0:0] 連接數 [active:idle]-[1:0] 連接數 [active:idle]-[0:1] 連接數 [active:idle]-[1:0] 連接數 [active:idle]-[0:1]

對照清單 4 的輸出日志,我們可以看到已經沒有連接泄漏的現象了。一個執行線程在運行 JdbcUserService#logon() 方法時,只占用一個連接,而且方法執行完畢后,該連接馬上釋放。這說明通過 DataSourceUtils.getConnection() 方法確實獲取了方法所在事務上下文綁定的那個連接,而不是像原來那樣從數據源中獲取一個新的連接。

使用 DataSourceUtils 獲取數據連接也可能造成泄漏!

是否使用 DataSourceUtils 獲取數據連接就可以高枕無憂了呢?理想很美好,但現實很殘酷:如果 DataSourceUtils 在沒有事務上下文的方法中使用 getConnection() 獲取連接,依然會造成數據連接泄漏!

保持代碼清單 6 的代碼不變,調整 Spring 配置文件,將清單 3 中 Spring AOP 事務增強配置的代碼注釋掉,重新運行清單 6 的代碼,將得到如下的輸出日志:

清單 8. 輸出日志
連接數 [active:idle]-[0:0] 連接數 [active:idle]-[1:1] 連接數 [active:idle]-[1:1] 連接數 [active:idle]-[2:1] 連接數 [active:idle]-[2:1]

我們通過下表對數據源連接的占用和泄漏情況進行描述:

表 2. 執行過程數據源連接占用情況
時間執行線程 1執行線程 2數據源連接activeidleleak
T0未啟動未啟動000
T1正在執行方法未啟動110
T2執行完畢未啟動111
T3執行完畢正式執行方法211
T4執行完畢執行完畢212

仔細對照表 1 的執行過程,我們發現在 T1 時,有事務上下文時的 active 為 2,idle 為 0,而此時由于沒有事務管理,則 active 為 1 而 idle 也為 1。這說明有事務上下文時,需要等到整個事務方法(即 logon())返回后,事務上下文綁定的連接才釋放。但在沒有事務上下文時,logon() 調用 JdbcTemplate 執行完數據操作后,馬上就釋放連接。

在 T2 執行線程完成 logon() 方法的執行后,有一個連接沒有被釋放(active),所以發生了連接泄漏。到 T4 時,兩個執行線程都完成了 logon() 方法的調用,但是出現了兩個未釋放的連接。

要堵上這個連接泄漏的漏洞,需要對 logon() 方法進行如下的改造:

清單 9.JdbcUserService.java:手工釋放獲取的連接
public void logon(String userName) {Connection conn = null;try {conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource());String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?";jdbcTemplate.update(sql, System.currentTimeMillis(), userName);Thread.sleep(1000);// ①} catch (Exception e) {e.printStackTrace();}finally {// ②顯式使用DataSourceUtils釋放連接DataSourceUtils.releaseConnection(conn,jdbcTemplate.getDataSource());} }

在 ② 處顯式調用?DataSourceUtils.releaseConnection()?方法釋放獲取的連接。特別需要指出的是:一定不能在 ① 處釋放連接!因為如果 logon() 在獲取連接后,① 處代碼前這段代碼執行時發生異常,則①處釋放連接的動作將得不到執行。這將是一個非常具有隱蔽性的連接泄漏的隱患點。

JdbcTemplate 如何做到對連接泄漏的免疫

分析 JdbcTemplate 的代碼,我們可以清楚地看到它開放的每個數據操作方法,首先都使用 DataSourceUtils 獲取連接,在方法返回之前使用 DataSourceUtils 釋放連接。

來看一下 JdbcTemplate 最核心的一個數據操作方法 execute():

清單 10.JdbcTemplate#execute()
public <T> T execute(StatementCallback<T> action) throws DataAccessException {//① 首先根據DataSourceUtils獲取數據連接Connection con = DataSourceUtils.getConnection(getDataSource());Statement stmt = null;try {Connection conToUse = con;…handleWarnings(stmt);return result;}catch (SQLException ex) {JdbcUtils.closeStatement(stmt);stmt = null;DataSourceUtils.releaseConnection(con, getDataSource());con = null;throw getExceptionTranslator().translate("StatementCallback", getSql(action), ex);}finally {JdbcUtils.closeStatement(stmt);//② 最后根據DataSourceUtils釋放數據連接DataSourceUtils.releaseConnection(con, getDataSource());} }

在 ① 處通過 DataSourceUtils.getConnection() 獲取連接,在 ② 處通過 DataSourceUtils.releaseConnection() 釋放連接。所有 JdbcTemplate 開放的數據訪問方法最終都是通過?execute(StatementCallback<T> action)執行數據訪問操作的,因此這個方法代表了 JdbcTemplate 數據操作的最終實現方式。

正是因為 JdbcTemplate 嚴謹的獲取連接,釋放連接的模式化流程保證了 JdbcTemplate 對數據連接泄漏問題的免疫性。所以,如有可能盡量使用 JdbcTemplate,HibernateTemplate 等這些模板進行數據訪問操作,避免直接獲取數據連接的操作。

使用 TransactionAwareDataSourceProxy

如果不得已要顯式獲取數據連接,除了使用 DataSourceUtils 獲取事務上下文綁定的連接外,還可以通過 TransactionAwareDataSourceProxy 對數據源進行代理。數據源對象被代理后就具有了事務上下文感知的能力,通過代理數據源的 getConnection() 方法獲取的連接和使用 DataSourceUtils.getConnection() 獲取連接的效果是一樣的。

下面是使用 TransactionAwareDataSourceProxy 對數據源進行代理的配置:

清單 11.applicationContext.xml:對數據源進行代理
<bean id="dataSource"class="org.apache.commons.dbcp.BasicDataSource"destroy-method="close"p:driverClassName="oracle.jdbc.driver.OracleDriver"p:url="jdbc:oracle:thin:@localhost:1521:orcl"p:username="test"p:password="test"p:defaultAutoCommit="false"/><!-- ①對數據源進行代理--> <bean id="dataSourceProxy" class="org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy"p:targetDataSource-ref="dataSource"/><!-- ②直接使用數據源的代理對象--> <bean id="jdbcTemplate"class="org.springframework.jdbc.core.JdbcTemplate"p:dataSource-ref="dataSourceProxy"/><!-- ③直接使用數據源的代理對象--> <bean id="jdbcManager"class="org.springframework.jdbc.datasource.DataSourceTransactionManager"p:dataSource-ref="dataSourceProxy"/>

對數據源進行代理后,我們就可以通過數據源代理對象的 getConnection() 獲取事務上下文中綁定的數據連接了。

因此,如果數據源已經進行了 TransactionAwareDataSourceProxy 的代理,而且方法存在事務上下文,那么清單 1 的代碼也不會生產連接泄漏的問題。

其它數據訪問技術的等價類

理解了 Spring JDBC 的數據連接泄漏問題,其中的道理可以平滑地推廣到其它框架中去。Spring 為每個數據訪問技術框架都提供了一個獲取事務上下文綁定的數據連接(或其衍生品)的工具類和數據源(或其衍生品)的代理類。

DataSourceUtils 的等價類

下表列出了不同數據訪問技術對應 DataSourceUtils 的等價類:

表 3. 不同數據訪問框架 DataSourceUtils 的等價類
數據訪問技術框架連接 ( 或衍生品 ) 獲取工具類
Spring JDBCorg.springframework.jdbc.datasource.DataSourceUtils
Hibernateorg.springframework.orm.hibernate3.SessionFactoryUtils
iBatisorg.springframework.jdbc.datasource.DataSourceUtils
JPAorg.springframework.orm.jpa.EntityManagerFactoryUtils
JDOorg.springframework.orm.jdo.PersistenceManagerFactoryUtils

TransactionAwareDataSourceProxy 的等價類

下表列出了不同數據訪問技術框架下 TransactionAwareDataSourceProxy 的等價類:

表 4. 不同數據訪問框架 TransactionAwareDataSourceProxy 的等價類
數據訪問技術框架連接 ( 或衍生品 ) 獲取工具類
Spring JDBCorg.springframework.jdbc.datasource.TransactionAwareDataSourceProxy
Hibernateorg.springframework.orm.hibernate3.LocalSessionFactoryBean
iBatisorg.springframework.jdbc.datasource.TransactionAwareDataSourceProxy
JPA
JDOorg.springframework.orm.jdo.
TransactionAwarePersistenceManagerFactoryProxy

小結

在本文中,我們通過剖析了解到以下的真相:

    • 使用 Spring JDBC 時如果直接獲取 Connection,可能會造成連接泄漏。為降低連接泄漏的可能,盡量使用 DataSourceUtils 獲取數據連接。也可以對數據源進行代理,以便將其擁有事務上下文的感知能力;
    • 可以將 Spring JDBC 防止連接泄漏的解決方案平滑應用到其它的數據訪問技術框架中。

?

?

?

?

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

總結

以上是生活随笔為你收集整理的Spring 事务管理高级应用难点剖析--转的全部內容,希望文章能夠幫你解決所遇到的問題。

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

国产高清免费在线观看 | 91九色porny蝌蚪视频 | 日韩高清在线一区二区 | 成人a级免费视频 | 97视频在线免费 | 日本美女xx | 国产一区二区在线视频观看 | 最新色站 | 欧美亚洲成人xxx | 91av网址 | av丁香 | 在线一级片| 久久综合色播五月 | 国产偷在线 | 91综合色 | 中文字幕第一页在线视频 | 日本三级吹潮在线 | 狠狠操导航 | 玖玖国产精品视频 | 免费一级片视频 | 亚洲最大av网站 | 中文字幕精品一区久久久久 | 亚洲高清视频一区二区三区 | 久久久久激情 | 中文字幕永久免费 | 蜜臀av一区 | 在线观看黄色免费视频 | av网站在线观看免费 | 国产手机视频 | 97小视频| 国产无套精品久久久久久 | 久久在线观看视频 | 色噜噜日韩精品欧美一区二区 | 91av视频免费在线观看 | 国产综合精品一区二区三区 | 狠狠躁夜夜躁人人爽超碰97香蕉 | 一区二区久久久久 | www91在线观看| 国产精品亚洲片夜色在线 | 精品亚洲在线 | 日韩网站在线播放 | 91精品久久久久久久91蜜桃 | 国模一区二区三区四区 | 色婷婷综合久久久久 | 日韩av影视在线观看 | 91毛片视频 | 久久久影院一区二区三区 | 国产人成看黄久久久久久久久 | 日韩久久一区二区 | 久久中文字幕视频 | 久草视频资源 | 亚洲国产片色 | 亚洲精品免费在线观看 | 国产字幕在线看 | 日韩中文字幕在线观看 | 欧美亚洲三级 | 日日夜夜噜 | 国产精品中文久久久久久久 | 国产视频色| 福利视频导航网址 | 成人一级片免费看 | 奇米影音四色 | 国产精品一区二区美女视频免费看 | 亚洲mv大片欧洲mv大片免费 | 精品国内自产拍在线观看视频 | 97国产精品 | 狠狠综合久久av | 久久久亚洲成人 | 久久久综合九色合综国产精品 | 免费不卡中文字幕视频 | 久久免费视频5 | 超碰人人91 | 在线成人国产 | 人人爽人人爽人人片av免 | 国产青草视频在线观看 | 人人爱人人舔 | 91人人射 | 成人午夜剧场在线观看 | 色综合久久久久久中文网 | 中文字幕在线免费观看 | 91久久精品一区二区二区 | 丁香花中文字幕 | 亚洲国产日韩一区 | 久久久久久久看片 | 中文字幕亚洲精品在线观看 | 国产在线播放一区二区三区 | 超碰人人在线观看 | 91免费观看网站 | av免费在线观看网站 | 爱av在线网 | 在线观看久草 | 成年人在线观看 | 国产黄色大全 | 久久久一本精品99久久精品66 | 奇米影视四色8888 | 日韩 精品 一区 国产 麻豆 | 婷婷久久五月天 | 超碰在线97观看 | 色综合久久中文字幕综合网 | 99 久久久久 | 国产黄色观看 | 免费在线91 | 最新国产精品视频 | 国产精品一区二区美女视频免费看 | 国模一二三区 | 日本xxxx.com | 国产亚洲在| 国产精品一区二区久久国产 | 在线观看日本高清mv视频 | 色综合天天狠狠 | 91免费看黄 | 高清中文字幕 | 射射射综合网 | 可以免费观看的av片 | 久草视频在线资源 | 九九精品毛片 | 欧美 国产 视频 | 97**国产露脸精品国产 | 麻豆精品视频在线 | 日韩成人在线免费观看 | 成人av观看| 国产精品剧情在线亚洲 | 天堂久色 | 国精产品999国精产品视频 | 青草视频在线免费 | 狠狠色丁香 | 国产精品9999 | 亚洲成年人在线播放 | 人人爽人人爽人人片av免 | 丁香国产视频 | 激情综合啪 | 亚洲性xxxx | 免费在线激情电影 | 国产精品入口久久 | 黄色av网站在线观看 | 夜夜干天天操 | 亚欧洲精品视频在线观看 | 亚洲综合视频在线观看 | av在线看网站 | 射九九 | 黄色com| 欧美孕妇视频 | 欧美日韩在线看 | 中文字幕欧美三区 | 国产打女人屁股调教97 | 婷婷五月在线视频 | av成人免费在线观看 | 国产又粗又猛又黄 | 中文字幕一区二区在线观看 | 亚州精品视频 | 日日摸日日添夜夜爽97 | 国产精品18久久久久久vr | 国产精品一区二区免费 | 亚洲欧美综合精品久久成人 | 欧美日韩免费网站 | 日韩av电影中文字幕 | 91久色蝌蚪 | 在线观看亚洲国产精品 | 中文字幕人成人 | 日韩超碰 | 精品久久久精品 | 亚洲欧洲国产精品 | 首页中文字幕 | 91最新国产 | 亚洲永久精品在线观看 | 亚洲精品国产欧美在线观看 | 欧美日韩视频在线一区 | 在线观看视频三级 | 欧美一区免费在线观看 | 免费a视频在线 | 国产亚洲资源 | 高清av网| 毛片.com | 最近日本韩国中文字幕 | 丰满少妇久久久 | 亚洲国产精品免费 | 亚洲激情综合 | japanese黑人亚洲人4k | 99色在线播放 | 国内精品亚洲 | 亚洲国产精品va在线看黑人动漫 | 国产中文字幕在线视频 | 久久涩视频 | 欧美在线观看视频免费 | 色婷婷激情电影 | 日韩欧美不卡 | 欧美中文字幕第一页 | 亚欧日韩成人h片 | 99久久综合狠狠综合久久 | 在线观看视频你懂 | 成人一级电影在线观看 | 免费看的黄色小视频 | 丝袜美女在线观看 | 亚洲国产中文在线观看 | 深爱开心激情网 | 成人va天堂 | av短片在线| 91在线小视频 | 亚洲黄色片一级 | 国产精品久久久久久一区二区三区 | 免费视频久久久久久久 | 国产又黄又爽又猛视频日本 | 婷婷久久综合网 | 在线观看中文字幕一区二区 | 在线观看久久久久久 | 欧美日韩国产伦理 | 精品国产免费av | 不卡视频国产 | 久艹视频在线免费观看 | 久久精品这里都是精品 | 久久一及片| 国产 av 日韩| av专区在线 | 少妇bbb搡bbbb搡bbbb | 国际精品久久 | 中文字幕在线观看视频网站 | 中文字幕av专区 | 国产精品视频在线观看 | 欧美精品一级视频 | 高清有码中文字幕 | 国产中文字幕在线视频 | 欧美在线视频一区二区三区 | 成人羞羞免费 | 视频一区视频二区在线观看 | 免费黄色网址网站 | 日日碰狠狠添天天爽超碰97久久 | 国产免费中文字幕 | 国产在线精品一区二区三区 | 中文在线a天堂 | 免费在线播放视频 | 国产精品美女久久久免费 | 国产黄色观看 | 最新国产中文字幕 | 免费a网 | 97超级碰碰碰碰久久久久 | 精品综合久久 | 欧美资源在线观看 | 国产日韩精品在线观看 | 99精品视频免费看 | 在线看一级片 | 婷婷综合视频 | 一区二区三区四区在线 | 黄色官网在线观看 | 韩国精品一区二区三区六区色诱 | 色婷婷亚洲精品 | 久久综合九色综合欧美就去吻 | av解说在线观看 | 日韩试看 | 黄色一级影院 | 黄网站a | 久久毛片网站 | 在线精品观看国产 | 日韩美一区二区三区 | 在线免费色视频 | 97在线观看免费高清 | av电影亚洲| 99视频一区二区 | a黄色大片 | 91视频电影| 中文字幕在线免费看线人 | 国产日本在线播放 | 欧美片网站yy | 久久a级片| 国产玖玖精品视频 | 亚洲精品美女免费 | 亚洲另类视频在线 | 欧美在线观看视频免费 | 国产精品区免费视频 | 欧美精品久久久久久久久老牛影院 | 国产精品免费麻豆入口 | 国产在线小视频 | 亚洲精品456在线播放第一页 | 天天综合日日夜夜 | 色噜噜噜 | 中文字幕日本特黄aa毛片 | 操操操干干干 | 黄色精品网站 | 色噜噜在线观看 | av大全在线免费观看 | 在线观看国产一区 | 青青视频一区 | 久久精品爱爱视频 | 超碰国产人人 | 精品福利网 | 91精品国自产在线 | 久久久91精品国产一区二区三区 | 色综合久久精品 | 狠狠色伊人亚洲综合网站野外 | 国产精品久久久久999 | 久久精品免费电影 | а天堂中文最新一区二区三区 | 97免费视频在线播放 | 国产福利a | 日韩免费成人 | 天天干天天综合 | 亚洲欧美日本A∨在线观看 青青河边草观看完整版高清 | 丁香资源影视免费观看 | 亚洲国产成人精品在线 | 久久久久欠精品国产毛片国产毛生 | 日本最大色倩网站www | 一级α片免费看 | 特黄特色特刺激视频免费播放 | 亚洲国产精品人久久电影 | 国产精品午夜在线 | 999久久国精品免费观看网站 | 在线午夜av | 在线看国产精品 | 精品国产资源 | 欧美aaa视频 | 免费观看av网站 | 丁香高清视频在线看看 | 亚洲视频精品在线 | 日产乱码一二三区别免费 | 人人看看人人 | 在线观看www视频 | 超碰最新网址 | 欧美日韩国产伦理 | 中文字幕av在线不卡 | 成人av在线影院 | 超碰伊人网 | 狠狠夜夜| 色av男人的天堂免费在线 | 成人亚洲精品久久久久 | 久久免费播放视频 | 国产在线精品一区二区三区 | 日本午夜免费福利视频 | 国产在线观看,日本 | 欧美日性视频 | 黄色小视频在线观看免费 | 天天插视频 | 久草视频在线资源 | 日本中文一区二区 | 日本中文字幕在线 | 久久九九精品久久 | 亚洲成人av在线电影 | 欧美在线观看视频免费 | 午夜视频在线网站 | 成人国产电影在线观看 | 8x成人免费视频 | 特级毛片在线 | 在线欧美小视频 | 日韩久久久久久久久 | 91亚洲精品久久久久图片蜜桃 | 美女久久久久久久久久 | 丰满少妇麻豆av | 91九色综合 | 成人在线网站观看 | 精品国产91亚洲一区二区三区www | 国产人成精品一区二区三 | 欧美日韩一区二区久久 | 91探花在线 | 人人玩人人爽 | 人人澡人人干 | 激情欧美一区二区免费视频 | 成人黄色电影视频 | 欧洲激情在线 | 手机看片国产日韩 | 亚洲精品视频免费 | 久久精品导航 | 97超碰人人澡人人爱 | 久草视频免费看 | 99亚洲视频 | 青青网视频 | 国产精品久久久久久影院 | 色天天综合网 | 亚洲精品国产拍在线 | 在线免费观看国产视频 | 五月婷婷激情综合网 | 男女激情网址 | 偷拍久久久 | 免费观看完整版无人区 | 99精品视频免费全部在线 | 国产精品久久久久久久久免费看 | 欧美在线观看视频一区二区三区 | 91理论片午午伦夜理片久久 | 日韩精品视频免费看 | 五月婷网站| 日韩在线观看 | 在线视频欧美亚洲 | 成年人在线免费视频观看 | 久久免费视频在线 | 在线观看国产永久免费视频 | 一区二区三区不卡在线 | 人人精久| 91av在线播放视频 | 999久久国精品免费观看网站 | 成人在线播放免费观看 | 日韩二区三区 | 亚洲美女精品视频 | 亚州av网站| 草久视频在线观看 | 久久婷婷一区二区三区 | 国产精品高潮呻吟久久久久 | 最近2019中文免费高清视频观看www99 | 国产精品入口a级 | www.亚洲精品在线 | 黄色网免费| 99精品欧美一区二区 | 又黄又刺激的网站 | 国产99区 | 亚洲一区二区三区四区在线视频 | 开心婷婷色 | 操高跟美女 | 91精选在线观看 | 欧美精品中文在线免费观看 | 日韩精品视频在线观看网址 | 黄色a视频免费 | av在线免费在线 | 精品久久91| 日韩黄色免费看 | 色综合久久久久综合99 | 一区二区欧美日韩 | 久久综合久色欧美综合狠狠 | 日韩在线免费小视频 | 久久久免费电影 | 亚洲色图27p | 中文在线免费观看 | 国产精品国产三级国产不产一地 | 免费看特级毛片 | 国内揄拍国产精品 | 视频一区二区免费 | 91在线看视频 | 中文字幕中文字幕 | 国产一区视频导航 | 六月激情久久 | 欧美一区二区三区特黄 | 亚洲国产免费看 | 欧美日韩一区二区三区不卡 | 色婷婷丁香| 国产精品久久久久久69 | 日韩国产精品久久久久久亚洲 | 久久国产精品免费一区二区三区 | 亚洲精品国产精品国自产在线 | 久久久午夜剧场 | 91欧美国产| 中文字幕免费播放 | 午夜色性片 | 国产成人久久精品亚洲 | 激情影院在线观看 | 欧美a视频在线观看 | 日韩网站一区 | 最近中文字幕免费av | 视频在线播放国产 | 久久免费视频这里只有精品 | 久久视频这里有久久精品视频11 | 亚洲va欧美va国产va黑人 | 日韩一区二区三 | 日韩精品视频网站 | 久久99精品久久久久蜜臀 | 久久99精品国产一区二区三区 | 久久精品波多野结衣 | 欧美日韩国产在线 | 伊人亚洲综合 | 丁香视频 | 91精品天码美女少妇 | 在线观看 国产 | 91在线免费播放 | 狠狠色丁香婷婷 | 国产人成看黄久久久久久久久 | 亚洲最新在线 | 久久久久免费电影 | 国产麻豆视频在线观看 | 中文字幕在线视频一区 | 在线观看日韩av | 日韩成人精品一区二区三区 | 国产高清免费av | 蜜臀aⅴ国产精品久久久国产 | 欧美国产高清 | 亚洲中字幕 | 成人免费网站在线观看 | 香蕉日日 | 黄色免费网站 | www.久久91| 国产高清不卡在线 | 91网在线| 亚洲天堂网视频在线观看 | 一区三区视频在线观看 | 精品视频免费观看 | 久久精品电影 | 久久人人爽| 日韩丝袜在线观看 | 久久免费视频4 | 精品成人a区在线观看 | 最近最新最好看中文视频 | 最新高清无码专区 | 亚洲最大av | 欧美日韩国产精品久久 | 天天干天天操天天操 | av在线播放快速免费阴 | 精品国产一区二区三区四区在线观看 | 日韩精品免费一区二区 | 手机在线欧美 | 久久夜靖品 | 免费影视大全推荐 | 啪啪动态视频 | 日韩美女av在线 | 91在线公开视频 | 精品久久久成人 | 亚洲精品久久久久中文字幕m男 | 天天草天天摸 | 18国产精品白浆在线观看免费 | 亚洲一级影院 | 超碰在线97观看 | 久久久一本精品99久久精品66 | 亚洲精品视频在线免费播放 | 亚洲国产免费 | 91激情视频在线观看 | 日韩一级片大全 | 天天草av| 激情综合五月天 | 久草香蕉在线视频 | 久久人人97超碰精品888 | 日精品在线观看 | 一区二区三区视频在线 | 国产一级黄色电影 | 天天综合区 | 亚州av成人 | 亚洲美女在线一区 | 亚洲国产精品va在线看黑人动漫 | av中文字幕网 | 夜夜躁天天躁很躁波 | 最近日本mv字幕免费观看 | 一级性av | 日本二区三区在线 | 欧美一级片在线免费观看 | 在线观看中文字幕2021 | 久久艹99 | 日韩成人黄色 | 最近中文字幕第一页 | 日韩欧美一区二区三区视频 | 激情丁香综合五月 | 中文字幕刺激在线 | 国产精品中文字幕av | 午夜视频色 | 人人爽人人| 91在线中文字幕 | 日韩欧美一区二区三区在线观看 | 在线免费国产 | 97超碰资源 | 成人天堂网 | 亚洲成人黄色在线 | 国产女人免费看a级丨片 | 国产在线精品二区 | 99色精品视频 | a成人v | 精品欧美一区二区精品久久 | 啪啪激情网 | 中文字幕一区二 | 欧美亚洲xxx | 在线观看韩日电影免费 | 亚洲成人欧美 | 999成人精品 | 成人永久在线 | 国产成人精品一区二区 | 亚洲91精品 | av手机在线播放 | 九九免费精品视频在线观看 | 国产高清第一页 | 色午夜影院 | 亚洲成色777777在线观看影院 | 人人干人人干人人干 | 香蕉成人在线视频 | 黄色视屏av | 激情欧美一区二区三区免费看 | 香蕉网址 | 日日干天天射 | 免费av大全| 日韩高清在线观看 | 丰满少妇在线观看 | 久久久精品国产一区二区电影四季 | 中文字幕一区二区三区在线播放 | 天天综合日 | 九九热中文字幕 | 在线激情影院一区 | 九色91在线 | a国产精品| 精品视频免费看 | 乱男乱女www7788 | 五月婷婷激情综合网 | 麻豆91视频| 久久久99精品免费观看app | 久久久久国产一区二区三区四区 | 亚洲精品在线国产 | 久久久久99999| 91视频免费 | 网站免费黄 | 亚洲精品国精品久久99热一 | 日本性xxx| 久久久黄视频 | 日韩大片免费观看 | 日韩电影一区二区在线 | 麻豆传媒视频在线播放 | 亚洲爱av | 在线免费高清视频 | 婷婷丁香在线视频 | 免费a视频| 亚洲激情综合 | 国产精品视频线看 | 精品v亚洲v欧美v高清v | 中文电影网| 97人人爽人人 | 色吊丝在线永久观看最新版本 | 欧美一区二区精美视频 | 在线视频久 | 国产免码va在线观看免费 | av 在线观看| 国产精品免费在线播放 | 国产日韩在线视频 | 日韩成人看片 | 毛片一区二区 | 日韩性久久| 91精品在线播放 | 激情综合中文娱乐网 | 国产小视频在线免费观看 | 怡红院av久久久久久久 | 99精品系列 | 日韩性片 | 日韩系列 | 97精品国产aⅴ | 国产综合精品久久 | av中文电影| 色资源在线 | 西西人体4444www高清视频 | 狠狠色丁香久久婷婷综合五月 | 精品少妇一区二区三区在线 | 亚洲jizzjizz日本少妇 | 婷婷五综合 | 午夜精品久久久久久久久久 | 国产一二三在线视频 | 中文字幕日韩免费视频 | 国产精品久久久久久久久久 | 久久久久亚洲最大xxxx | 99r在线观看 | 国产麻豆精品久久 | 永久免费精品视频网站 | 91中文字幕在线播放 | 国产精品高清一区二区三区 | 日日夜夜91 | 在线观看av中文字幕 | 中文字幕在线观看一区二区三区 | 超碰在线色 | 国内免费的中文字幕 | 亚洲精品午夜国产va久久成人 | 国产精品毛片久久久久久久久久99999999 | 欧美成人h版 | 亚洲va欧美va人人爽春色影视 | 天天草天天干天天 | 国产精品久久久一区二区 | 天天操天天艹 | 国产精品美女在线 | 欧美亚洲三级 | 久久在线免费观看 | 91九色视频在线 | 欧美午夜a | 免费在线一区二区 | 国产精品日韩 | 在线观看的av | 日韩欧美大片免费观看 | 狠狠色丁香久久综合网 | 中文字幕观看av | 91精品久久久久久久久久入口 | 国产精品一区二区在线观看 | 亚洲第一成网站 | 99免费在线 | 亚洲国产精品女人久久久 | 国产精品网在线观看 | 欧美日韩xxxxx | 成人免费视频a | 久久久这里有精品 | 日日骑 | 五月婷婷在线播放 | 国产亚洲精品久久久久久移动网络 | 欧美巨大荫蒂茸毛毛人妖 | 亚洲免费a | 日本少妇久久久 | 精品视频成人 | 免费毛片一区二区三区久久久 | 六月丁香婷婷网 | 国产精品第54页 | 久久综合狠狠狠色97 | 久草在线免 | 一本到在线 | 日韩成人在线免费观看 | 激情五月六月婷婷 | 国产精品激情偷乱一区二区∴ | 欧美日韩在线视频一区 | 日韩一二三在线 | 2019精品手机国产品在线 | 久久久久久久久久久免费 | 国产精品欧美久久久久天天影视 | 婷婷亚洲五月 | 特级西西444www大胆高清无视频 | 日本在线视频网址 | 中文字幕a∨在线乱码免费看 | 激情五月激情综合网 | 97国产超碰在线 | 美女黄视频免费看 | 人人揉人人揉人人揉人人揉97 | 国产福利91精品一区二区三区 | 毛片视频网址 | 伊人久久影视 | 久色小说 | 亚洲在线网址 | 天天在线操 | 免费a级大片 | 国产精品久久久久久久久久免费 | 久二影院 | 亚洲在线色 | 国产黄色一级片在线 | 日本免费一二三区 | 91视频91色 | 国产韩国日本高清视频 | 狠狠色丁香久久婷婷综合五月 | 麻花传媒mv免费观看 | 99在线精品观看 | 国产福利中文字幕 | 中文字幕一区二区三区四区在线视频 | 97视频总站 | 欧美看片 | 国产亚洲一区二区三区 | 毛片一区二区 | 久久精品这里热有精品 | 在线免费观看国产视频 | 国产精品不卡一区 | 久久久国产精品亚洲一区 | 欧美一区中文字幕 | 在线国产视频 | 日韩大片在线播放 | 丁香六月在线 | 亚洲91中文字幕无线码三区 | 欧美成人999 | 欧美日韩中文在线 | 天天色草| 视频国产一区二区三区 | 伊人婷婷网 | 国产97在线视频 | 久久午夜电影网 | 最近免费中文字幕mv在线视频3 | h网站免费在线观看 | 久久天天操 | 亚洲精品国产品国语在线 | 亚洲 欧洲av | 四虎最新域名 | 六月丁香综合网 | 国产精品大片免费观看 | 四虎成人在线 | 国产精品手机看片 | 亚洲激情一区二区三区 | 首页国产精品 | 天天操天天干天天综合网 | 狠狠干网址 | 91九色最新地址 | 久久,天天综合 | 四虎国产精品成人免费影视 | 欧美精品国产综合久久 | 日本精品一二区 | 麻豆视频大全 | 在线小视频 | 超碰人人舔 | 国产精品资源在线观看 | 亚洲成成品网站 | 91看片淫黄大片在线播放 | 六月婷婷网 | 亚洲视频在线观看免费 | 久久伊人综合 | 99在线观看免费视频精品观看 | 免费在线激情视频 | 欧美精品亚洲精品 | 五月天亚洲综合小说网 | 久久久久免费电影 | 久99久在线 | 亚洲国产精品传媒在线观看 | 9999精品 | 三级av免费 | www狠狠操 | 黄色av电影在线观看 | 亚洲国产免费看 | 五月天天天操 | 成人影音在线 | 最近乱久中文字幕 | 四虎成人免费影院 | 亚洲精品一区中文字幕乱码 | 日本性xxxxx| 91精品色| 超碰在线97观看 | 九九九视频在线 | 四虎视频 | 久久久久99精品国产片 | 欧美性护士| 国产精品国产毛片 | 中文字幕免费成人 | 天天干天天做 | 国产手机在线视频 | 日韩中文字幕免费电影 | 天堂在线一区二区 | 精品国产一区二区三区久久久蜜臀 | 国产精品99久久久久久小说 | 亚洲精品国偷拍自产在线观看蜜桃 | 丰满少妇在线观看 | 视频 国产区 | 蜜臀av一区二区 | 国产黄网站在线观看 | 日韩av在线一区二区 | 欧美黑人xxxx猛性大交 | www.成人精品| 国产一区二三区好的 | 丝袜+亚洲+另类+欧美+变态 | 九九热国产视频 | 日韩在线观看第一页 | 国产99免费 | 亚洲va男人天堂 | 国产精品嫩草影院9 | 久久毛片视频 | 黄色av一级片 | 久久婷婷综合激情 | 久久精品站 | 欧美一级特黄aaaaaa大片在线观看 | 伊人色综合久久天天 | 在线国产精品一区 | 免费看一级特黄a大片 | 欧美日韩视频在线播放 | 麻豆va一区二区三区久久浪 | 国产精品乱码久久 | 欧美色图狠狠干 | 国产福利中文字幕 | 可以免费看av | 免费高清在线观看电视网站 | 国产91学生| 亚洲一区二区视频 | 中文字幕资源网 | 亚洲欧美日本国产 | 人人舔人人爱 | 91av在线免费看 | 看片黄网站 | 黄色小网站在线 | 国产资源网站 | 亚洲免费婷婷 | 婷婷丁香导航 | 久久久久国产一区二区三区四区 | 91av99| 日韩免费成人 | 久久久精品在线观看 | 国产一区在线免费 | 午夜影院一级片 | 不卡av电影在线观看 | 国产白浆在线观看 | 日韩免费网址 | av高清影院| 日日爱夜夜爱 | 国产精品a成v人在线播放 | 国产精品久久久久久a | 国产精品视频全国免费观看 | 97超碰超碰久久福利超碰 | 亚洲一二三在线 | 国产一级做a | 日韩毛片在线一区二区毛片 | www.av在线.com| 国产精品久久久久影视 | 国产精品免费成人 | 久久99精品久久久久蜜臀 | 国产麻豆精品久久一二三 | 免费日韩一区二区三区 | 天天干天天做天天操 | 久久精品在线免费观看 | av超碰在线 | 国产一区二区三区四区大秀 | 日韩免费在线网站 | 久久成人麻豆午夜电影 | 国产小视频福利在线 | 日韩视频免费播放 | 少妇性bbb搡bbb爽爽爽欧美 | 国产一级视频在线免费观看 | aaa亚洲精品一二三区 | 最新成人av | 最近最新中文字幕 | 国产精品丝袜久久久久久久不卡 | 天天操天天色天天射 | 午夜精品久久久久久久99热影院 | 亚洲 中文 在线 精品 | 99久热精品| 中文字幕在线视频一区二区三区 | 999久久久免费精品国产 | 中文字幕高清有码 | 亚洲天天| 在线网站黄 | 国产黄色电影 | 午夜久久成人 | 操操日| 狠狠干夜夜操天天爽 | 国产一级性生活视频 | 96久久欧美麻豆网站 | 国产资源中文字幕 | 国产高清免费在线观看 | 国产精品mv | 天天综合网久久综合网 | 成人va视频 | 日韩av一区二区三区四区 | 久久这里| 天天弄天天干 | 国产美女无遮挡永久免费 | 日韩电影在线观看中文字幕 | 久草资源在线观看 | 欧美一级大片在线观看 | 香蕉精品在线观看 | 亚洲免费激情 | 91大神电影 | 国产精久久久久久妇女av | 亚洲成人av在线 | 久久综合影音 | 国产亚洲在线观看 | 中文字幕在线观看2018 | 在线 国产一区 | 2019中文最近的2019中文在线 | 国产成人一区二区三区久久精品 | 亚洲闷骚少妇在线观看网站 | av在线com| 白丝av在线 | 免费观看一级 | 国产一级免费观看视频 | 黄av免费 | 国产精品午夜av | 日韩激情免费视频 | 国产成人亚洲在线电影 | 狠狠做六月爱婷婷综合aⅴ 日本高清免费中文字幕 | 国产视频亚洲 | 日韩在线观看一区二区 | 91精品国产91热久久久做人人 | 激情视频一区 | 国产黄色片一级三级 | 最近中文字幕免费视频 | 亚洲精品999 | 色综合天天爱 | 久久综合五月天婷婷伊人 | 91成人破解版 | 中文字幕首页 | 最近中文字幕第一页 | 国产精品99在线观看 | 免费视频在线观看网站 | 激情电影影院 | 99精品在线视频观看 | 深夜精品福利 | 国产一区二区三区午夜 | 欧美精品久久 | 国产精品美女久久久久久久 | 性色av香蕉一区二区 | 久久婷婷久久 | 四虎影视成人精品 | 九色精品 | 久久成人亚洲欧美电影 | 国产精品一区二区在线播放 | 最近中文国产在线视频 | 韩国av电影在线观看 | 精品久久久久久久久久久久久久久久久久 | 日韩激情三级 | japanese黑人亚洲人4k | 久久人人精品 | 国产专区在线视频 | 少妇bbw搡bbbb搡bbb | 欧美福利视频 | 久草在线播放视频 | 91九色九色| 免费看黄色大全 | 天天干亚洲 | 国产高清av免费在线观看 | 日韩在线视频观看免费 | 日韩欧美v | 国产一级精品绿帽视频 | 国产精品免费久久久久久久久久中文 | 免费看的黄色片 | 国产精品福利在线观看 | 久久人人做 | 午夜12点| 久久久久电影 | 久久久久久国产精品 | 在线免费黄色av | 免费看黄色大全 | 99视频偷窥在线精品国自产拍 | 天天操狠狠干 | 伊人网综合在线观看 | 中文字幕永久在线 | a黄在线观看 | 夜夜躁日日躁 | 亚洲国产影院av久久久久 | 99在线热播 | 又粗又长又大又爽又黄少妇毛片 | 深夜免费福利视频 | 91系列在线观看 | 国产午夜剧场 | 国内精品福利视频 | 色亚洲激情| 五月天婷婷在线观看视频 | 国产精品99久久久久人中文网介绍 | 嫩嫩影院理论片 | www99精品| 日韩av一区二区三区在线观看 | 日本中文乱码卡一卡二新区 | 又黄又网站 | 天天干,天天草 |