日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > windows >内容正文

windows

使用Mybatis自定义插件实现不侵入业务的公共参数自动追加

發布時間:2023/12/29 windows 34 coder
生活随笔 收集整理的這篇文章主要介紹了 使用Mybatis自定义插件实现不侵入业务的公共参数自动追加 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

背景

后臺業務開發的過程中,往往會遇到這種場景:需要記錄每條記錄產生時間、修改時間、修改人及添加人,在查詢時查詢出來。
以往的做法通常是手動在每個業務邏輯里耦合上這么一塊代碼,也有更優雅一點的做法是寫一個攔截器,然后在Mybatis攔截器中為實體對象中的公共參數進行賦值,但最終依然需要在業務SQL上手動添加上這幾個參數,很多開源后臺項目都有類似做法。

這種做法往往不夠靈活,新增或修改字段時每處業務邏輯都需要同步修改,業務量大的話這么改非常麻煩。

最近在我自己的項目中寫了一個Mybatis插件,這個插件能夠實現不修改任何業務邏輯就能實現添加或修改時數據庫公共字段的賦值,并能在查詢時自動查詢出來。

實現原理

Mybatis提供了一系列的攔截器,用于實現在Mybatis執行的各個階段允許插入或修改自定義邏輯。

Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)

我這里用的是Executor,它能做到在所有數據庫操作前后執行一些邏輯,甚至可以修改Mybatis的上下文參數后繼續執行。
在Mybaits的攔截器中,可以拿到MappedStatement對象,這里面包含了一次數據庫操作的原始SQL以及實體對象與結果集的映射關系,為了實現公共參數自動攜帶,我們就需要在攔截器中修改原始SQL:

  1. Insert操作:自動為Insert語句添加公共字段并賦值
  2. Update操作:自動為Update語句添加公共字段并賦值
  3. Select操作:自動為Select語句的查詢參數上添加上公共字段

以及修改實體對象與結果集的映射關系,做到自動修改查詢語句添加公共字段后能夠使Mybatis將查出的公共字段值賦給實體類。

簡單來說就是修改MappedStatement中的SqlSource以及ResultMap

修改SqlSource

在SqlSource中,包含了原始待執行的SQL,需要將它修改為攜帶公共參數的SQL。
需要注意的是Mybatis的SqlSource、ResultMap中的屬性僅允許初次構造SqlSource對象時進行賦值,后續如果需要修改只能通過反射或者新構造一個對象替換舊對象的方式進行內部參數修改。

直接貼出來代碼,這里新構造了SqlSource對象,在里面實現了原始SQL的解析修改:
SQL的動態修改使用了JSQLParser將原始SQL解析為AST抽象語法樹后做參數追加,之后重新解析為SQL,使用自定義SqlSource返回修改后的SQL實現SQL修改

static class ModifiedSqlSourceV2 implements SqlSource {
        private final MappedStatement mappedStatement;
        private final Configuration configuration;

        public ModifiedSqlSourceV2(MappedStatement mappedStatement, Configuration configuration) {
            this.mappedStatement = mappedStatement;
            this.configuration = configuration;
        }

        @Override
        public BoundSql getBoundSql(Object parameterObject) {
            // 獲取原始的 BoundSql 對象
            BoundSql originalBoundSql = mappedStatement.getSqlSource().getBoundSql(parameterObject);

            // 獲取原始的 SQL 字符串
            String originalSql = originalBoundSql.getSql();
            log.debug("公共參數添加 - 修改前SQL:{}", originalSql);

            // 創建新的 BoundSql 對象
            String modifiedSql;
            try {
                modifiedSql = buildSql(originalSql);
                log.debug("公共參數添加 - 修改后SQL:{}", modifiedSql);
            } catch (JSQLParserException e) {
                log.error("JSQLParser解析修改SQL添加公共參數失敗, 繼續使用原始SQL執行" , e);
                modifiedSql = originalSql;
            }
            BoundSql modifiedBoundSql = new BoundSql(configuration, modifiedSql,
                    originalBoundSql.getParameterMappings(), parameterObject);
            // 復制其他屬性
            originalBoundSql.getAdditionalParameters().forEach(modifiedBoundSql::setAdditionalParameter);
            modifiedBoundSql.setAdditionalParameter("_parameter", parameterObject);

            return modifiedBoundSql;
        }

        private String buildSql(String originalSql) throws JSQLParserException {
            Statement statement = CCJSqlParserUtil.parse(originalSql);

            switch(mappedStatement.getSqlCommandType()) {
                case INSERT -> {
                    if(statement instanceof Insert insert) {
                        insert.addColumns(new Column(CREATE_BY_COLUMN), new Column(CREATE_TIME_COLUMN));
                        ExpressionList expressionList = insert.getItemsList(ExpressionList.class);
                        Timestamp currentTimeStamp = new Timestamp(System.currentTimeMillis());

                        if (!expressionList.getExpressions().isEmpty()) {
                            // 多行插入 行構造器解析
                            if (expressionList.getExpressions().get(0) instanceof RowConstructor) {
                                expressionList.getExpressions().forEach((expression -> {
                                    if (expression instanceof RowConstructor rowConstructor) {
                                        rowConstructor.getExprList().getExpressions().add(new StringValue(getCurrentUser()));
                                        rowConstructor.getExprList().getExpressions().add(new TimestampValue().withValue(currentTimeStamp));
                                    }
                                }));
                            } else {
                                // 其余默認單行插入
                                expressionList.addExpressions(new StringValue(getCurrentUser()), new TimestampValue().withValue(currentTimeStamp));
                            }
                        }

                        return insert.toString();
                    }
                }
                case UPDATE -> {
                    if(statement instanceof Update update) {
                        List<UpdateSet> updateSetList = update.getUpdateSets();
                        UpdateSet updateBy = new UpdateSet(new Column(UPDATE_BY_COLUMN), new StringValue(getCurrentUser()));
                        Timestamp currentTimeStamp = new Timestamp(System.currentTimeMillis());
                        UpdateSet updateTime = new UpdateSet(new Column(UPDATE_TIME_COLUMN), new TimestampValue().withValue(currentTimeStamp));
                        updateSetList.add(updateBy);
                        updateSetList.add(updateTime);

                        return update.toString();
                    }
                }
                case SELECT -> {
                    if(statement instanceof Select select) {
                        SelectBody selectBody = select.getSelectBody();
                        if(selectBody instanceof PlainSelect plainSelect) {
                            TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();
                            List<String> tableNames = tablesNamesFinder.getTableList(select);

                            List<SelectItem> selectItems = plainSelect.getSelectItems();
                            tableNames.forEach((tableName) -> {
                                String lowerCaseTableName = tableName.toLowerCase();
                                selectItems.add(new SelectExpressionItem().withExpression(new Column(new Table(tableName), CREATE_BY_COLUMN)).withAlias(new Alias(lowerCaseTableName + "_" + CREATE_BY_COLUMN)));
                                selectItems.add(new SelectExpressionItem().withExpression(new Column(new Table(tableName), CREATE_TIME_COLUMN)).withAlias(new Alias(lowerCaseTableName + "_" + CREATE_TIME_COLUMN)));
                                selectItems.add(new SelectExpressionItem().withExpression(new Column(new Table(tableName), UPDATE_BY_COLUMN)).withAlias(new Alias(lowerCaseTableName + "_" + UPDATE_BY_COLUMN)));
                                selectItems.add(new SelectExpressionItem().withExpression(new Column(new Table(tableName), UPDATE_TIME_COLUMN)).withAlias(new Alias(lowerCaseTableName + "_" + UPDATE_TIME_COLUMN)));
                            });

                            return select.toString();
                        }
                    }
                }
                default -> {
                    return originalSql;
                }
            }
            return originalSql;
        }
}

修改ResultMap

ResultMap中存放了結果列與映射實體類屬性的對應關系,這里為了自動生成公共屬性的結果映射,直接根據當前ResultMap中存儲的結果映射實體類的名稱作為表名,自動建立與結果列的映射關系。

就是說數據庫表對應的實體類的名字需要與數據庫表保持一致(但是實體類名可以是數據庫表的名字的駝峰命名,如表user_role的實體類需要命名為UserRole),只要遵守這個命名規則即可實現查詢結果中自動攜帶公共參數值
如下為添加公共參數結果映射的代碼

private static List<ResultMapping> addResultMappingProperty(Configuration configuration, List<ResultMapping> resultMappingList, Class<?> mappedType) {
        // resultMappingList為不可修改對象
        List<ResultMapping> modifiableResultMappingList = new ArrayList<>(resultMappingList);

        String []checkList = {CREATE_BY_PROPERTY, CREATE_TIME_PROPERTY, UPDATE_BY_PROPERTY, UPDATE_TIME_PROPERTY};
        boolean hasAnyTargetProperty = Arrays.stream(checkList).anyMatch((property) -> ReflectionUtils.findField(mappedType, property) != null);

        // 用于防止映射目標為基本類型卻被添加映射 導致列名規則 表名_列名 無法與映射的列名的添加規則 映射類型名_列名 相照應
        // 從而導致映射類型為基本類型時會生成出類似與string_column1的映射名 而產生找不到映射列名與實際結果列相照應的列名導致mybatis產生錯誤
        // 規則: 僅映射類型中包含如上四個字段其一時才會添加映射
        if(hasAnyTargetProperty) {
            // 支持類型使用駝峰命名
            String currentTable = upperCamelToLowerUnderscore(mappedType.getSimpleName());

            // 映射方式 表名_公共字段名 在實體中 表名與實體名相同 則可完成映射
            modifiableResultMappingList.add(new ResultMapping.Builder(configuration, CREATE_BY_PROPERTY, currentTable + "_" + CREATE_BY_COLUMN, String.class).build());
            modifiableResultMappingList.add(new ResultMapping.Builder(configuration, CREATE_TIME_PROPERTY, currentTable + "_" + CREATE_TIME_COLUMN, Timestamp.class).build());
            modifiableResultMappingList.add(new ResultMapping.Builder(configuration, UPDATE_BY_PROPERTY, currentTable + "_" + UPDATE_BY_COLUMN, String.class).build());
            modifiableResultMappingList.add(new ResultMapping.Builder(configuration, UPDATE_TIME_PROPERTY, currentTable + "_" + UPDATE_TIME_COLUMN, Timestamp.class).build());
        }

        return modifiableResultMappingList;
}

構建MappedStatement

原本的由Mybatis創建的MappedStatement無法直接修改,因此這里手動通過ResultMap.Builder()構造一個新的MappedStatement,同時保持其余參數不變,只替換SqlSource、ResultMap為先前重新創建的對象。

public MappedStatement buildMappedStatement(Configuration newModifiedConfiguration, MappedStatement mappedStatement) {
        SqlSource modifiedSqlSource = new ModifiedSqlSourceV2(mappedStatement, newModifiedConfiguration);

        List<ResultMap> modifiedResultMaps = mappedStatement.getResultMaps().stream().map((resultMap) -> {
            List<ResultMapping> resultMappingList = resultMap.getResultMappings();
            // 為每個resultMap中的resultMappingList添加公共參數映射
            List<ResultMapping> modifiedResultMappingList = addResultMappingProperty(newModifiedConfiguration, resultMappingList, resultMap.getType());

            return new ResultMap.Builder(newModifiedConfiguration, resultMap.getId(), resultMap.getType(), modifiedResultMappingList, resultMap.getAutoMapping()).build();
        }).toList();

        // 構造新MappedStatement 替換SqlSource、ResultMap、Configuration
        MappedStatement.Builder newMappedStatementBuilder = new MappedStatement.Builder(newModifiedConfiguration, mappedStatement.getId(), modifiedSqlSource, mappedStatement.getSqlCommandType())
                .cache(mappedStatement.getCache()).databaseId(mappedStatement.getDatabaseId()).dirtySelect(mappedStatement.isDirtySelect()).fetchSize(mappedStatement.getFetchSize())
                .flushCacheRequired(mappedStatement.isFlushCacheRequired())
                .keyGenerator(mappedStatement.getKeyGenerator())
                .lang(mappedStatement.getLang()).parameterMap(mappedStatement.getParameterMap()).resource(mappedStatement.getResource()).resultMaps(modifiedResultMaps)
                .resultOrdered(mappedStatement.isResultOrdered())
                .resultSetType(mappedStatement.getResultSetType()).statementType(mappedStatement.getStatementType()).timeout(mappedStatement.getTimeout()).useCache(mappedStatement.isUseCache());
        if(mappedStatement.getKeyColumns() != null) {
            newMappedStatementBuilder.keyColumn(StringUtils.collectionToDelimitedString(Arrays.asList(mappedStatement.getKeyColumns()), ","));
        }
        if(mappedStatement.getKeyProperties() != null) {
            newMappedStatementBuilder.keyProperty(StringUtils.collectionToDelimitedString(Arrays.asList(mappedStatement.getKeyProperties()), ","));
        }
        if(mappedStatement.getResultSets() != null) {
            newMappedStatementBuilder.resultSets(StringUtils.collectionToDelimitedString(Arrays.asList(mappedStatement.getResultSets()), ","));
        }
        return newMappedStatementBuilder.build();
}

到這里為止,已經完全實現了修改原始SQL、修改結果映射的工作了,將修改后的MappedStatement對象往下傳入到invoke()即可但是還能改進。

改進

在Mybatis攔截器中可以通過MappedStatement.getConfiguration()拿到整個Mybatis的上下文,在這個里面可以拿到所有Mybatis的所有SQL操作的映射結果以及SQL,可以一次性修改完后,將Configuration作為一個緩存使用,每次有請求進入攔截器后就從Configuration獲取被修改的MappedStatement后直接invoke,效率會提升不少。
經給改進后,除了應用啟動后執行的第一個SQL請求由于需要構建Configuration會慢一些,之后的請求幾乎沒有產生性能方面的影響。

現在唯一的性能消耗是每次執行請求前Mybatis會調用我們自己重新定義的SqlSource.getBoundSql()將原始SQL解析為AST后重新構建生成新SQL的過程了,這點開銷幾乎可忽略不計。如果想更進一步的優化,可以考慮將原始SQL做key,使用Caffeine、Guava緩存工具等方式將重新構建后的查詢SQL緩存起來(Update/Insert由于追加有時間參數的原因,不能被緩存),避免多次重復構建SQL帶來的開銷

完整實現

經過優化后,整個插件已經比較完善了,能夠滿足日常使用,無論是單表查詢,還是多表聯查,嵌套查詢都能夠實現無侵入的參數追加,目前僅實現了創建人、創建時間、修改人、修改時間的參數追加&映射綁定,如有需要的可以自行修改。

我把它放到了GitHub上,并附帶有示例項目:https://github.com/Random-pro/ExtParamInterctptor
覺得好用的歡迎點點Star

使用的人多的話,后續會將追加哪些參數做成動態可配置的,等你們反饋

插件使用示例

所有的新增操作均會被自動添加創建人、創建時間。更新操作則會被自動添加更新人、更新時間。正常使用Mybatis操作即可,與原先無任何差別就不在這里給出示例了,如果需要示例請前往我在GitHub上的示例項目。

  1. 單表查詢

    // 實體類Child(類名對應具體的表名 使用駝峰命名法,如表名為user_role,則類名應寫為UserRole)
    @Data
    public class Child extends BaseDomain {
      private int childId;
      private int parentId;
      private String childName;
      private String path;
    }
    
    // 公共字段
    @Data
    public class BaseDomain {
      private String createBy;
      private Date createTime;
      private String updateBy;
      private Date updateTime;
    }
    
    // Mapper接口
    @Mapper
    public interface TestMapper {
      @Select("SELECT id as childId, name as childName, parent_id as parentId, path FROM child")
      List<Child> getChildList();
    }
    
    // Controller
    @RestController
    @RequestMapping("user")
    public record UserController(TestMapper testMapper) {
      @GetMapping("getChildList")
      public List<Child> getChildList() {
        return testMapper.getChildList();
      }
    }
    

    訪問user/getChildList獲取結果:

    [
        {
            "createBy": "sun11",
            "createTime": "2023-12-18T07:58:58.000+00:00",
            "updateBy": "random",
            "updateTime": "2023-12-18T07:59:19.000+00:00",
            "childId": 1,
            "parentId": 1,
            "childName": "childName1_1",
            "path": "childPath1_1"
        },
        {
            "createBy": "sun12",
            "createTime": "2023-12-18T07:58:59.000+00:00",
            "updateBy": "RANDOM",
            "updateTime": "2023-12-18T07:59:20.000+00:00",
            "childId": 2,
            "parentId": 1,
            "childName": "childName1_2",
            "path": "childPath1_2"
        },
        {
            "createBy": "sun21",
            "createTime": "2023-12-18T07:59:00.000+00:00",
            "updateBy": "randompro",
            "updateTime": "2023-12-18T07:59:21.000+00:00",
            "childId": 3,
            "parentId": 2,
            "childName": "childName2_1",
            "path": "childPath2_2"
        }
    ]
    
  2. 多表查詢

    // 實體類Base(類名對應具體的表名 使用駝峰命名法,如表名為user_role,則類名應寫為UserRole) 注意:當關聯多個表時,需要取哪個表里的公共字段(創建人、創建時間等字段)則將映射實體類名命名為該表的表名
    @Data
    public class Base extends BaseDomain {
      private int id;
      private String baseName;
      private String basePath;
      private List<Child> pathChildList;
    }
    
    @Data
    public class Child extends BaseDomain {
      private int childId;
      private int parentId;
      private String childName;
      private String path;
    }
    
    // 公共字段
    @Data
    public class BaseDomain {
      private String createBy;
      private Date createTime;
      private String updateBy;
      private Date updateTime;
    }
    
    // Mapper接口
    @Mapper
    public interface TestMapper {
      @Select("SELECT BASE.ID as id , BASE.BASE_NAME as baseName, CHILD.PATH as basePath FROM BASE, CHILD WHERE BASE.ID = CHILD.PARENT_ID")
      List<Base> getBaseAndChildPath();
    }
    
    // Controller
    @RestController
    @RequestMapping("user")
    public record UserController(TestMapper testMapper) {
      @GetMapping("getBaseAndChildPath")
      public List<Base> getBaseAndChildPath() {
        return testMapper.getBaseAndChildPath();
      }
    }
    

    訪問user/getBaseAndChildPath獲取結果:

    [
        {
            "createBy": "sun_base",
            "createTime": "2023-12-18T07:59:29.000+00:00",
            "updateBy": "random_base",
            "updateTime": "2023-12-18T08:00:09.000+00:00",
            "id": 1,
            "baseName": "baseName1",
            "basePath": "childPath1_1",
            "pathChildList": null
        },
        {
            "createBy": "sun_base",
            "createTime": "2023-12-18T07:59:29.000+00:00",
            "updateBy": "random_base",
            "updateTime": "2023-12-18T08:00:09.000+00:00",
            "id": 1,
            "baseName": "baseName1",
            "basePath": "childPath1_2",
            "pathChildList": null
        },
        {
            "createBy": "sun2_base",
            "createTime": "2023-12-18T07:59:30.000+00:00",
            "updateBy": "randompro_base",
            "updateTime": "2023-12-18T08:00:09.000+00:00",
            "id": 2,
            "baseName": "baseName2",
            "basePath": "childPath2_2",
            "pathChildList": null
        }
    ]
    
  3. 多表嵌套查詢

    // 實體類Base(類名對應具體的表名 使用駝峰命名法,如表名為user_role,則類名應寫為UserRole) 嵌套查詢中使用到的多個實體若均可映射到對應表中的如上四個字段的值(只要該實體通過繼承、直接添加的方式獲取到了以上聲明的四個實體屬性的getter/setter方法即可)
    @Data
    public class Base extends BaseDomain {
      private int id;
      private String baseName;
      private String basePath;
      private List<Child> pathChildList;
    }
    
    @Data
    public class Child extends BaseDomain {
      private int childId;
      private int parentId;
      private String childName;
      private String path;
    }
    
    // 公共字段
    @Data
    public class BaseDomain {
      private String createBy;
      private Date createTime;
      private String updateBy;
      private Date updateTime;
    }
    
    // Mapper接口
    @Mapper
    public interface TestMapper {
      List<Base> getPathList();
    }
    
    // Controller
    @RestController
    @RequestMapping("user")
    public record UserController(TestMapper testMapper) {
      @GetMapping("getPathList")
      public List<Base> getPathList() {
        return testMapper.getPathList();
      }
    }
    

    Mapper.xml:

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.live.mapper.TestMapper">
    
        <resultMap type="com.live.domian.Base" id="PathDomainMap">
            <result property="id"    column="id"    />
            <result property="baseName" column="base_name"/>
            <result property="basePath" column="base_path"/>
    
            <collection property="pathChildList" ofType="com.live.domian.Child">
                <id property="childId" column="child_id"/>
                <result property="parentId" column="parent_id"/>
                <result property="childName" column="child_name"/>
                <result property="path" column="path"/>
            </collection>
        </resultMap>
    
        <select id="getPathList" resultMap="PathDomainMap">
            SELECT base.id, base.base_name, base.base_path, child.id AS child_id, child.name AS child_name,
                  child.path, child.parent_id FROM base LEFT JOIN child ON base.id = child.parent_id
        </select>
    </mapper>
    

    訪問user/getPathList獲取結果,可見嵌套查詢中每個層次都取到了公共字段createBy、createTime、updateBy、updateTime的值:

    [
        {
            "createBy": "sun_base",
            "createTime": "2023-12-18T07:59:29.000+00:00",
            "updateBy": "random_base",
            "updateTime": "2023-12-18T08:00:09.000+00:00",
            "id": 1,
            "baseName": "baseName1",
            "basePath": "basePath1",
            "pathChildList": [
                {
                    "createBy": "sun12",
                    "createTime": "2023-12-18T07:58:59.000+00:00",
                    "updateBy": "RANDOM",
                    "updateTime": "2023-12-18T07:59:20.000+00:00",
                    "childId": 2,
                    "parentId": 1,
                    "childName": "childName1_2",
                    "path": "childPath1_2"
                },
                {
                    "createBy": "sun11",
                    "createTime": "2023-12-18T07:58:58.000+00:00",
                    "updateBy": "random",
                    "updateTime": "2023-12-18T07:59:19.000+00:00",
                    "childId": 1,
                    "parentId": 1,
                    "childName": "childName1_1",
                    "path": "childPath1_1"
                }
            ]
        },
        {
            "createBy": "sun2_base",
            "createTime": "2023-12-18T07:59:30.000+00:00",
            "updateBy": "randompro_base",
            "updateTime": "2023-12-18T08:00:09.000+00:00",
            "id": 2,
            "baseName": "baseName2",
            "basePath": "basePath2",
            "pathChildList": [
                {
                    "createBy": "sun21",
                    "createTime": "2023-12-18T07:59:00.000+00:00",
                    "updateBy": "randompro",
                    "updateTime": "2023-12-18T07:59:21.000+00:00",
                    "childId": 3,
                    "parentId": 2,
                    "childName": "childName2_1",
                    "path": "childPath2_2"
                }
            ]
        }
    ]
    

    嵌套查詢中,如果只希望獲取到特定的表的那四個公共屬性,則把不希望獲取公共屬性的表對應的實體類中的四個映射屬性去掉(若使用BaseDomain繼承來的四個屬性的的話去掉繼承BaseDomain)即可

總結

以上是生活随笔為你收集整理的使用Mybatis自定义插件实现不侵入业务的公共参数自动追加的全部內容,希望文章能夠幫你解決所遇到的問題。

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

主站蜘蛛池模板: 裸体毛片 | 欧美成人a∨高清免费观看 国产精品999视频 | 丰满少妇一区二区三区视频 | 亚色视频在线观看 | av网站导航 | 亚洲视频自拍 | 国产精品国产三级国产aⅴ 欧美bbbbbbbbbbbb18av | 五十路在线观看 | 日本超碰 | 日韩欧美有码 | 日本不卡一区二区 | 99精品视频一区二区 | 久久久久久久久久久91 | 黑人巨大精品欧美一区二区免费 | 日韩香蕉视频 | 大牛影视剧免费播放在线 | 妞干网av| 国产aⅴ片 | bl动漫在线观看 | 婷婷调教口舌奴ⅴk | 日本色网址 | 在线成人中文字幕 | 欧美一区免费 | 国产精品人成在线观看免费 | 国产aaaaaa | 中文在线免费视频 | 国产精品丝袜黑色高跟鞋的设计特点 | 高潮毛片| 国产做爰免费观看视频 | 最近中文字幕无免费 | 天天操天天干天天摸 | 免费亚洲一区二区 | 国产亚洲久一区二区 | 夜夜爽夜夜操 | 欧美少妇一级片 | 国产午夜啪啪 | 亚洲欧美中文日韩在线v日本 | 越南少妇做受xxx片 亚洲av综合色区无码一二三区 | 国产在线v | 免费裸体视频女性 | 男女深夜福利 | 一级真人毛片 | 精品国产精品三级精品av网址 | 日韩欧美亚洲一区 | 美丽的姑娘观看在线播放 | 91桃色视频| 免费在线观看视频 | 日本免费不卡 | 大屁股白浆一区二区三区 | 欧美v日韩 | 秋霞欧美一区二区三区视频免费 | 久久精品第一页 | 三男一女吃奶添下面 | 伊人久久中文字幕 | 99久久免费看精品国产一区 | 综合色久| 女仆裸体打屁屁羞羞免费 | 狠狠干美女 | www黄色com| 好男人影视www | 久久成人在线观看 | 在线观看9.1 | 色婷五月| 秋霞影院午夜老牛影院 | 国产精品久久久影院 | 中文字幕亚洲欧美 | 一区二区三区国产精品视频 | 青娱乐99| 亚洲av成人无码一区二区三区在线观看 | 亚洲 欧美 日韩 综合 | 精品成人18 | 日韩久久一区 | 伊人网站在线观看 | 亚洲亚洲人成综合网络 | 中文字幕av免费在线观看 | 制服丝袜av一区二区三区下载 | 日韩精品一区二区三 | 91丨porny丨成人蝌蚪 | 果冻av在线 | 美人被强行糟蹋np各种play | 亚洲av永久无码精品一区二区国产 | 亚洲熟女少妇一区 | 亚州三级| 成人777 | 九色一区 | 亚洲狠狠丁香婷婷综合久久久 | 一本久道在线 | 欧美孕交视频 | 中文字幕第一页在线 | 在线观看福利片 | 狠狠干狠狠操 | 国产老女人乱淫免费 | 美国黄色片网站 | 亚洲美女自拍偷拍 | 波多野结衣二区三区 | 音影先锋av资源 | 国产96在线 | 国产一区二区三区免费观看 | 在线黄网 |