国产片侵犯亲女视频播放_亚洲精品二区_在线免费国产视频_欧美精品一区二区三区在线_少妇久久久_在线观看av不卡

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

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術|正則表達式|C/C++|IOS|C#|Swift|Android|VB|R語言|JavaScript|易語言|vb.net|

服務器之家 - 編程語言 - Java教程 - 深入學習MyBatis中的參數(推薦)

深入學習MyBatis中的參數(推薦)

2020-11-17 10:52isea533 Java教程

大家日常使用MyBatis經常會遇到一些異常,想要避免參數引起的錯誤,我們需要深入了解參數。想了解參數,我們首先看MyBatis處理參數和使用參數的全部過程。下面這篇文章主要給大家介紹了MyBatis中參數的的相關資料,需要的朋友

前言

相信很多人可能都遇到過下面這些異常:

  • "Parameter 'xxx' not found. Available parameters are [...]"
  • "Could not get property 'xxx' from xxxClass. Cause:
  • "The expression 'xxx' evaluated to a null value."
  • "Error evaluating expression 'xxx'. Return value (xxxxx) was not iterable."

不只是上面提到的這幾個,我認為有很多的錯誤都產生在和參數有關的地方。

想要避免參數引起的錯誤,我們需要深入了解參數。

想了解參數,我們首先看MyBatis處理參數和使用參數的全部過程。

本篇由于為了便于理解和深入,使用了大量的源碼,因此篇幅較長,需要一定的耐心看完,本文一定會對你起到很大的幫助。

參數處理過程

處理接口形式的入參

在使用MyBatis時,有兩種使用方法。一種是使用的接口形式,另一種是通過SqlSession調用命名空間。這兩種方式在傳遞參數時是不一樣的,命名空間的方式更直接,但是多個參數時需要我們自己創建Map作為入參。相比而言,使用接口形式更簡單。

接口形式的參數是由MyBatis自己處理的。如果使用接口調用,入參需要經過額外的步驟處理入參,之后就和命名空間方式一樣了。

在MapperMethod.Java會首先經過下面方法來轉換參數:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Object convertArgsToSqlCommandParam(Object[] args) {
 final int paramCount = params.size();
 if (args == null || paramCount == 0) {
 return null;
 } else if (!hasNamedParameters && paramCount == 1) {
 return args[params.keySet().iterator().next()];
 } else {
 final Map<String, Object> param = new ParamMap<Object>();
 int i = 0;
 for (Map.Entry<Integer, String> entry : params.entrySet()) {
 param.put(entry.getValue(), args[entry.getKey()]);
 // issue #71, add param names as param1, param2...but ensure backward compatibility
 final String genericParamName = "param" + String.valueOf(i + 1);
 if (!param.containsKey(genericParamName)) {
 param.put(genericParamName, args[entry.getKey()]);
 }
 i++;
 }
 return param;
 }
}

在這里有個很關鍵的params,這個參數類型為Map<Integer, String> ,他會根據接口方法按順序記錄下接口參數的定義的名字,如果使用@Param指定了名字,就會記錄這個名字,如果沒有記錄,那么就會使用它的序號作為名字。

例如有如下接口:

?
1
List<User> select(@Param('sex')String sex,Integer age);

那么他對應的params如下:

?
1
2
3
4
{
 0:'sex',
 1:'1'
}

繼續看上面的convertArgsToSqlCommandParam方法,這里簡要說明3種情況:

  • 入參為null或沒有時,參數轉換為null
  • 沒有使用@Param注解并且只有一個參數時,返回這一個參數
  • 使用了@Param注解或有多個參數時,將參數轉換為Map1類型,并且還根據參數順序存儲了key為param1,param2的參數。

注意:從第3種情況來看,建議各位有多個入參的時候通過@Param指定參數名,方便后面(動態sql)的使用。

經過上面方法的處理后,在MapperMethod中會繼續往下調用命名空間方式的方法:

?
1
2
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.<E>selectList(command.getName(), param);

從這之后開始按照統一的方式繼續處理入參。

處理集合

不管是selectOne還是selectMap方法,歸根結底都是通過selectList進行查詢的,不管是delete還是insert方法,都是通過update方法操作的。在selectList和update中所有參數的都進行了統一的處理。

在DefaultSqlSession.java中的wrapCollection方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private Object wrapCollection(final Object object) {
 if (object instanceof Collection) {
 StrictMap<Object> map = new StrictMap<Object>();
 map.put("collection", object);
 if (object instanceof List) {
 map.put("list", object);
 }
 return map;
 } else if (object != null && object.getClass().isArray()) {
 StrictMap<Object> map = new StrictMap<Object>();
 map.put("array", object);
 return map;
 }
 return object;
}

這里特別需要注意的一個地方是map.put("collection", object) ,這個設計是為了支持Set類型,需要等到MyBatis 3.3.0版本才能使用。

wrapCollection處理的是只有一個參數時,集合和數組的類型轉換成Map2類型,并且有默認的Key,從這里你能大概看到為什么<foreach>中默認情況下寫的array和list(Map類型沒有默認值map)。

參數的使用

參數的使用分為兩部分:

  • 第一種就是常見#{username}或者${username}
  • 第二種就是在動態SQL中作為條件,例如<if test="username!=null and username !=''">

下面對這兩種進行詳細講解,為了方便理解,先講解第二種情況。

在動態SQL條件中使用參數

關于動態SQL的基礎內容可以查看官方文檔

動態SQL為什么會處理參數呢?

主要是因為動態SQL中的<if> , <bind> , <foreache>都會用到表達式,表達式中會用到屬性名,屬性名對應的屬性值如何獲取呢?獲取方式就在這關鍵的一步。不知道多少人遇到Could not get property xxx from xxxClass或: Parameter ‘xxx' not found. Available parameters are[…] ,都是不懂這里引起的。

在DynamicContext.java中,從構造方法看起:

?
1
2
3
4
5
6
7
8
9
10
public DynamicContext(Configuration configuration, Object parameterObject) {
 if (parameterObject != null && !(parameterObject instanceof Map)) {
 MetaObject metaObject = configuration.newMetaObject(parameterObject);
 bindings = new ContextMap(metaObject);
 } else {
 bindings = new ContextMap(null);
 }
 bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
 bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}

這里的Object parameterObject就是我們經過前面兩步處理后的參數。這個參數經過前面兩步處理后,到這里的時候,他只有下面三種情況:

  • null,如果沒有入參或者入參是null,到這里也是null。
  • Map類型,除了null之外,前面兩步主要是封裝成Map類型。
  • 數組、集合和Map以外的Object類型,可以是基本類型或者實體類。

看上面構造方法,如果參數是1,2情況時,執行代碼bindings = new ContextMap(null);參數是3情況時執行if中的代碼。我們看看ContextMap類,這是一個內部靜態類,代碼如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static class ContextMap extends HashMap<String, Object> {
 private MetaObject parameterMetaObject;
 public ContextMap(MetaObject parameterMetaObject) {
 this.parameterMetaObject = parameterMetaObject;
 }
 public Object get(Object key) {
 String strKey = (String) key;
 if (super.containsKey(strKey)) {
 return super.get(strKey);
 }
 if (parameterMetaObject != null) {
 // issue #61 do not modify the context when reading
 return parameterMetaObject.getValue(strKey);
 }
 return null;
 }
}

我們先繼續看DynamicContext的構造方法,在if/else之后還有兩行:

?
1
2
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());

其中兩個Key分別為:

?
1
2
public static final String PARAMETER_OBJECT_KEY = "_parameter";
public static final String DATABASE_ID_KEY = "_databaseId";

也就是說1,2兩種情況的時候,參數值只存在于"_parameter"的鍵值中。3情況的時候,參數值存在于"_parameter"的鍵值中,也存在于bindings本身。

當動態SQL取值的時候會通過OGNL從bindings中獲取值。MyBatis在OGNL中注冊了ContextMap:

?
1
2
3
static {
 OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
}

當從ContextMap取值的時候,會執行ContextAccessor中的如下方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public Object getProperty(Map context, Object target, Object name)
 throws OgnlException {
 Map map = (Map) target;
 
 Object result = map.get(name);
 if (map.containsKey(name) || result != null) {
 return result;
 }
 
 Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
 if (parameterObject instanceof Map) {
 return ((Map)parameterObject).get(name);
 }
 
 return null;
}

參數中的target就是ContextMap類型的,所以可以直接強轉為Map類型。

參數中的name就是我們寫在動態SQL中的屬性名。

下面舉例說明這三種情況:

1、null的時候:

不管name是什么(name="_databaseId"除外,可能會有值),此時Object result = map.get(name);得到的result=null

Object parameterObject = map.get(PARAMETER_OBJECT_KEY);parameterObject=null,因此最后返回的結果是null。

在這種情況下,不管寫什么樣的屬性,值都會是null,并且不管屬性是否存在,都不會出錯。

2、Map類型:

此時Object result = map.get(name);一般也不會有值,因為參數值只存在于"_parameter"的鍵值中。

然后到Object parameterObject = map.get(PARAMETER_OBJECT_KEY); ,此時獲取到我們的參數值。

在從參數值((Map)parameterObject).get(name)根據name來獲取屬性值。

在這一步的時候,如果name屬性不存在,就會報錯:

?
1
throw new BindingException("Parameter '" + key + "' not found. Available parameters are " + keySet());

name屬性是什么呢,有什么可選值呢?這就是處理接口形式的入參和處理集合處理后所擁有的Key。

如果你遇到過類似異常,相信看到這兒就明白原因了。

3、數組、集合和Map以外的Object類型:

這種類型經過了下面的處理:

?
1
2
MetaObject metaObject = configuration.newMetaObject(parameterObject);
bindings = new ContextMap(metaObject);

MetaObject是MyBatis的一個反射類,可以很方便的通過getValue方法獲取對象的各種屬性(支持集合數組和Map,可以多級屬性點.訪問,如user.username,user.roles[1].rolename)

現在分析這種情況。

首先通過name獲取屬性時Object result = map.get(name); ,根據上面ContextMap類中的get方法:

?
1
2
3
4
5
6
7
8
9
10
public Object get(Object key) {
String strKey = (String) key;
if (super.containsKey(strKey)) {
 return super.get(strKey);
}
if (parameterMetaObject != null) {
 return parameterMetaObject.getValue(strKey);
}
return null;
}

可以看到這里會優先從Map中取該屬性的值,如果不存在,那么一定會執行到下面這行代碼:

?
1
return parameterMetaObject.getValue(strKey)

如果name剛好是對象的一個屬性值,那么通過MetaObject反射可以獲取該屬性值。如果該對象不包含name屬性的值,就會報錯:

?
1
throw new ReflectionException("Could not get property '" + prop.getName() + "' from " + object.getClass() + ". Cause: " + t.toString(), t);

理解這三種情況后,使用動態SQL應該不會有參數名方面的問題了。

在SQL語句中使用參數

SQL中的兩種形式#{username}或者${username} ,雖然看著差不多,但是實際處理過程差別很大,而且很容易出現莫名其妙的錯誤。

${username}的使用方式為OGNL方式獲取值,和上面的動態SQL一樣,這里先說這種情況。

${propertyName}參數

在TextSqlNode.java中有一個內部的靜態類BindingTokenParser,現在只看其中的handleToken方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public String handleToken(String content) {
 Object parameter = context.getBindings().get("_parameter");
 if (parameter == null) {
 context.getBindings().put("value", null);
 } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
 context.getBindings().put("value", parameter);
 }
 Object value = OgnlCache.getValue(content, context.getBindings());
 String srtValue = (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
 checkInjection(srtValue);
 return srtValue;
}

put("value"這個地方可以看出來,MyBatis會創建一個默認為"value"的值,也就是說,在xml中的SQL中可以直接使用${value},從else if可以看出來,只有是簡單類型的時候,才會有值。

關于這點,舉個簡單例子,如果接口為List<User> selectOrderby(String column) ,如果xml內容為:

?
1
2
3
<select id="selectOrderby" resultType="User">
select * from user order by ${value}
</select>

這種情況下,雖然沒有指定一個value屬性,但是MyBatis會自動把參數column賦值進去。

再往下的代碼:

?
1
2
Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = (value == null ? "" : String.valueOf(value));

這里和動態SQL就一樣了,通過OGNL方式來獲取值。

看到這里使用OGNL這種方式時,你有沒有別的想法?

特殊用法:你是否在SQL查詢中使用過某些固定的碼值?一旦碼值改變的時候需要改動很多地方,但是你又不想把碼值作為參數傳進來,怎么解決呢?你可能已經明白了。

就是通過OGNL的方式,例如有如下一個碼值類:

?
1
2
3
4
5
package com.abel533.mybatis;
public interface Code{
 public static final String ENABLE = "1";
 public static final String DISABLE = "0";
}

如果在xml,可以這么使用:

?
1
2
3
<select id="selectUser" resultType="User">
 select * from user where enable = ${@com.abel533.mybatis.Code@ENABLE}
</select>

除了碼值之外,你可以使用OGNL支持的各種方法,如調用靜態方法。

#{propertyName}參數

這種方式比較簡單,復雜屬性的時候使用的MyBatis的MetaObject。

在DefaultParameterHandler.java中:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public void setParameters(PreparedStatement ps) throws SQLException {
 ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
 if (parameterMappings != null) {
 for (int i = 0; i < parameterMappings.size(); i++) {
  ParameterMapping parameterMapping = parameterMappings.get(i);
  if (parameterMapping.getMode() != ParameterMode.OUT) {
  Object value;
  String propertyName = parameterMapping.getProperty();
  if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
   value = boundSql.getAdditionalParameter(propertyName);
  } else if (parameterObject == null) {
   value = null;
  } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
   value = parameterObject;
  } else {
   MetaObject metaObject = configuration.newMetaObject(parameterObject);
   value = metaObject.getValue(propertyName);
  }
  TypeHandler typeHandler = parameterMapping.getTypeHandler();
  JdbcType jdbcType = parameterMapping.getJdbcType();
  if (value == null && jdbcType == null) {
   jdbcType = configuration.getJdbcTypeForNull();
  }
  typeHandler.setParameter(ps, i + 1, value, jdbcType);
  }
 }
 }
}

上面這段代碼就是從參數中取#{propertyName}值的方法,這段代碼的主要邏輯就是if/else判斷的地方,單獨拿出來分析:

?
1
2
3
4
5
6
7
8
9
10
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
 value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
 value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
 value = parameterObject;
} else {
 MetaObject metaObject = configuration.newMetaObject(parameterObject);
 value = metaObject.getValue(propertyName);
}

首先看第一個if,當使用<foreach>的時候,MyBatis會自動生成額外的動態參數,如果propertyName是動態參數,就會從動態參數中取值。

第二個if,如果參數是null,不管屬性名是什么,都會返回null。

第三個if,如果參數是一個簡單類型,或者是一個注冊了typeHandler的對象類型,就會直接使用該參數作為返回值,和屬性名無關。

最后一個else,這種情況下是復雜對象或者Map類型,通過反射方便的取值。

下面我們說明上面四種情況下的參數名注意事項。

1、動態參數,這里的參數名和值都由MyBatis動態生成的,因此我們沒法直接接觸,也不需要管這兒的命名。但是我們可以了解一下這兒的命名規則,當以后錯誤信息看到的時候,我們可以確定出錯的地方。
在ForEachSqlNode.java中:

?
1
2
3
private static String itemizeItem(String item, int i) {
return new StringBuilder(ITEM_PREFIX).append(item).append("_").append(i).toString();
}

其中ITEM_PRFIX為public static final String ITEM_PREFIX = "__frch_";

如果在<foreach>中的collection="userList" item="user" , 那么對userList循環產生的動態參數名就是:

?
1
__frch_user_0,__frch_user_1,__frch_user_2…

如果訪問動態參數的屬性,如user.username會被處理成__frch_user_0.username ,這種參數值的處理過程在更早之前解析SQL的時候就已經獲取了對應的參數值。具體內容看下面有關<foreach>的詳細內容。

2、參數為null,由于這里的判斷和參數名無關,因此入參null的時候,在xml中寫的#{name}不管name寫什么,都不會出錯,值都是null。

3、可以直接使用typeHandler處理的類型。最常見的就是基本類型,例如有這樣一個接口方法User selectById(@Param("id")Integer id) ,在xml中使用id的時候,我們可以隨便使用屬性名,不管用什么樣的屬性名,值都是id。

4、復雜對象或者Map類型一般都是我們需要注意的地方,這種情況下,就必須保證入參包含這些屬性,如果沒有就會報錯。這一點和可以參考上面有關MetaObject的地方。

<foreach>詳解

所有動態SQL類型中, <foreach>似乎是遇到問題最多的一個。

例如有下面的方法:

?
1
2
3
4
5
6
7
<insert id="insertUserList">
 INSERT INTO user(username,password)
 VALUES
 <foreach collection="userList" item="user" separator=",">
 (#{user.username},#{user.password})
 </foreach>
</insert>

對應的接口:

?
1
int insertUserList(@Param("userList")List<User> list);

我們通過foreach源碼,看看MyBatis如何處理上面這個例子。

在ForEachSqlNode.java中的apply方法中的前兩行:

?
1
2
Map<String, Object> bindings = context.getBindings();
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);

這里的bindings參數熟悉嗎?上面提到過很多。經過一系列的參數處理后,這兒的bindings如下:

?
1
2
3
4
5
6
7
{
 "_parameter":{
 "param1":list,
 "userList":list
 },
 "_databaseId":null,
}

collectionExpression就是collection="userList"的值userList。

我們看看evaluator.evaluateIterable如何處理這個參數,在ExpressionEvaluator.java中的evaluateIterable方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Iterable<?> evaluateIterable(String expression, Object parameterObject) {
 Object value = OgnlCache.getValue(expression, parameterObject);
 if (value == null) {
  throw new BuilderException("The expression '" + expression + "' evaluated to a null value.");
 }
 if (value instanceof Iterable) {
  return (Iterable<?>) value;
 }
 if (value.getClass().isArray()) {
  int size = Array.getLength(value);
  List<Object> answer = new ArrayList<Object>();
  for (int i = 0; i < size; i++) {
   Object o = Array.get(value, i);
   answer.add(o);
  }
  return answer;
 }
 if (value instanceof Map) {
  return ((Map) value).entrySet();
 }
 throw new BuilderException("Error evaluating expression '" + expression + "'. Return value (" + value + ") was not iterable.");
}

首先通過看第一行代碼:

?
1
Object value = OgnlCache.getValue(expression, parameterObject);

這里通過OGNL獲取到了userList的值。獲取userList值的時候可能出現異常,具體可以參考上面動態SQL部分的內容。

userList的值分四種情況。

  • value == null,這種情況直接拋出異常BuilderException。
  • value instanceof Iterable,實現Iterable接口的直接返回,如Collection的所有子類,通常是List。
  • value.getClass().isArray()數組的情況,這種情況會轉換為List返回。
  • value instanceof Map如果是Map,通過((Map) value).entrySet()返回一個Set類型的參數。

通過上面處理后,返回的值,是一個Iterable類型的值,這個值可以使用for (Object o : iterable)這種形式循環。

在ForEachSqlNode中對iterable循環的時候,有一段需要關注的代碼:

?
1
2
3
4
5
6
7
8
9
if (o instanceof Map.Entry) {
 @SuppressWarnings("unchecked")
 Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
 applyIndex(context, mapEntry.getKey(), uniqueNumber);
 applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
 applyIndex(context, i, uniqueNumber);
 applyItem(context, o, uniqueNumber);
}

如果是通過((Map) value).entrySet()返回的Set,那么循環取得的子元素都是Map.Entry類型,這個時候會將mapEntry.getKey()存儲到index中,mapEntry.getValue()存儲到item中。

如果是List,那么會將序號i存到index中,mapEntry.getValue()存儲到item中。

<foreach>常見錯誤補充

collection="userList"的值userList中的User是一個繼承自Map的類型時,你需要保證<foreach>循環中用到的所有對象的屬性必須存在,Map類型存在的問題通常是,如果某個值是null,一般是不存在相應的key,這種情況會導致<foreach>出錯,會報找不到__frch_user_x參數。所以這種情況下,就是值是null,你也需要map.put(key,null)

總結

以上就是這篇文章的全部內容了,這篇文章真的非常有用,如果你對Mybatis有一定的了解,這篇文章幾乎是必讀的一篇。希望本文的內容對大家的學習或者工作能帶來一定的幫助,如有疑問大家可以留言交流,謝謝大家對服務器之家的支持。

原文鏈接:http://blog.csdn.net/isea533/article/details/44002219

延伸 · 閱讀

精彩推薦
Weibo Article 1 Weibo Article 2 Weibo Article 3 Weibo Article 4 Weibo Article 5 Weibo Article 6 Weibo Article 7 Weibo Article 8 Weibo Article 9 Weibo Article 10 Weibo Article 11 Weibo Article 12 Weibo Article 13 Weibo Article 14 Weibo Article 15 Weibo Article 16 Weibo Article 17 Weibo Article 18 Weibo Article 19 Weibo Article 20 Weibo Article 21 Weibo Article 22 Weibo Article 23 Weibo Article 24 Weibo Article 25 Weibo Article 26 Weibo Article 27 Weibo Article 28 Weibo Article 29 Weibo Article 30 Weibo Article 31 Weibo Article 32 Weibo Article 33 Weibo Article 34 Weibo Article 35 Weibo Article 36 Weibo Article 37 Weibo Article 38 Weibo Article 39 Weibo Article 40
主站蜘蛛池模板: 国产色视频一区 | 亚洲综合精品 | 免费一级毛片免费播放 | 激情欧美日韩一区二区 | 国产精品视频久久 | 亚洲一区二区av | aaa级大片 | 国产脚交av在线一区二区 | 亚洲香蕉视频 | av手机在线播放 | 精品视频一区二区三区四区 | 亚洲一区二区三区免费观看 | 久久国产精品无码网站 | 日日操夜夜操免费视频 | 天天干夜干 | 国产一区二区三区免费看 | 亚洲不卡高清视频 | 精品久久久久久久久久久下田 | 成人欧美一区二区三区白人 | 亚洲 欧美 自拍偷拍 | 99国产精品99久久久久久 | 毛片视频网站在线观看 | 成人精品网站在线观看 | 青青在线精品视频 | 亚洲欧洲精品成人久久奇米网 | 欧美亚洲天堂 | 欧美专区在线观看 | 国产综合亚洲精品一区二 | 久久久久国产 | av瑟瑟| 欧美啪啪一区二区 | 亚洲va国产天堂va久久 en | 日本福利一区二区 | 中文av在线播放 | 在线成人免费电影 | 国产精品欧美一区二区三区不卡 | 综合自拍| 91免费在线 | 亚洲免费视频大全 | 黄色短视频在线观看 | 成人在线一区二区三区 |