【mybatis plus源码解析】(二)详解SQL注入器底层原理,mybatis plus是如何实现自动注入CRUD操作

8 篇文章 4 订阅
7 篇文章 4 订阅

系列文章目录

【mybatis plus源码解析】mybatis plus执行原理
【mybatis plus源码解析】(二)详解自定义SQL注入器底层原理


前言

上篇文章介绍了【mybatis plus源码解析】mybatis plus执行原理,这回接着上篇文章,建议看完上篇文章再来看这篇,这里主要介绍相关类

一、ISqlInjector SQL自动注入器接口的相关uml图

uml图

ISqlInjector顶级接口,只做一件事

在这里插入图片描述

再来看看AbstractSqlInjector抽象类

AbstractSqlInjector类实现了inspectInject注入方法

/**
 * SQL 自动注入器
 *
 * @author hubin
 * @since 2018-04-07
 */
public abstract class AbstractSqlInjector implements ISqlInjector {

    protected final Log logger = LogFactory.getLog(this.getClass());

    @Override
    public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
    	//通过反射获取实体类对象
        Class<?> modelClass = ReflectionKit.getSuperClassGenericType(mapperClass, Mapper.class, 0);
        if (modelClass != null) {
            String className = mapperClass.toString();
            //获取mapperRegistry缓存用于对比,防止覆盖
            Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
            if (!mapperRegistryCache.contains(className)) {
            //通过实体类对象反射方式获取信息(比如实体类上的注解)封装成TableInfo存储表信息对象
                TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
                List<AbstractMethod> methodList = this.getMethodList(mapperClass, tableInfo);//获取要注入的方法集合
                if (CollectionUtils.isNotEmpty(methodList)) {
                    // 循环注入自定义方法
                    methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo));
                } else {
                    logger.debug(mapperClass.toString() + ", No effective injection method was found.");
                }
                mapperRegistryCache.add(className);
            }
        }
    }

    /**
     * <p>
     * 获取 注入的方法
     * </p>
     *
     * @param mapperClass 当前mapper
     * @return 注入的方法集合
     * @since 3.1.2 add  mapperClass
     */
    public abstract List<AbstractMethod> getMethodList(Class<?> mapperClass,TableInfo tableInfo);

}

DefaultSqlInjector默认SQL 注入器

DefaultSqlInjector实现了AbstractSqlInjector类的getMethodList方法,规定哪些方法可以实现自动注入

/**
 * SQL 默认注入器
 *
 * @author hubin
 * @since 2018-04-10
 */
public class DefaultSqlInjector extends AbstractSqlInjector {

    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
        Stream.Builder<AbstractMethod> builder = Stream.<AbstractMethod>builder()
            .add(new Insert())
            .add(new Delete())
            .add(new DeleteByMap())
            .add(new Update())
            .add(new SelectByMap())
            .add(new SelectCount())
            .add(new SelectMaps())
            .add(new SelectMapsPage())
            .add(new SelectObjs())
            .add(new SelectList())
            .add(new SelectPage());
        if (tableInfo.havePK()) {
            builder.add(new DeleteById())
                .add(new DeleteBatchByIds())
                .add(new UpdateById())
                .add(new SelectById())
                .add(new SelectBatchByIds());
        } else {
            logger.warn(String.format("%s ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method.",
                tableInfo.getEntityType()));
        }
        return builder.build().collect(toList());
    }
}

默认的注入器类治理介绍完了,里面其实做了一件事就是调用注入mapper默认实现的CRUD方法

二、注入方法相关的类

先看看UML图

在这里插入图片描述

AbstractMethod抽象方法类

/**
 * 抽象的注入方法类
 *
 * @author hubin
 * @since 2018-04-06
 */
@SuppressWarnings("serial")
public abstract class AbstractMethod implements Constants {
    protected static final Log logger = LogFactory.getLog(AbstractMethod.class);

    protected Configuration configuration;
    protected LanguageDriver languageDriver;
    protected MapperBuilderAssistant builderAssistant;

    /**
     * 方法名称
     * @since 3.5.0
     */
    protected final String methodName;

    /**
     * @see AbstractMethod#AbstractMethod(java.lang.String)
     * @since 3.5.0
     */
    @Deprecated
    public AbstractMethod() {
        methodName = null;
    }

    /**
     * @param methodName 方法名
     * @since 3.5.0
     */
    protected AbstractMethod(String methodName) {
        Assert.notNull(methodName, "方法名不能为空");
        this.methodName = methodName;
    }

    /**
     * 注入自定义方法
     */
    public void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        this.configuration = builderAssistant.getConfiguration();
        this.builderAssistant = builderAssistant;
        this.languageDriver = configuration.getDefaultScriptingLanguageInstance();
        /* 注入自定义方法 */
        injectMappedStatement(mapperClass, modelClass, tableInfo);
    }

    /**
     * 是否已经存在MappedStatement
     *
     * @param mappedStatement MappedStatement
     * @return true or false
     */
    private boolean hasMappedStatement(String mappedStatement) {
        return configuration.hasStatement(mappedStatement, false);
    }

    /**
     * SQL 更新 set 语句
     *
     * @param table 表信息
     * @return sql set 片段
     */
    protected String sqlLogicSet(TableInfo table) {
        return "SET " + table.getLogicDeleteSql(false, false);
    }

    /**
     * SQL 更新 set 语句
     *
     * @param logic  是否逻辑删除注入器
     * @param ew     是否存在 UpdateWrapper 条件
     * @param table  表信息
     * @param alias  别名
     * @param prefix 前缀
     * @return sql
     */
    protected String sqlSet(boolean logic, boolean ew, TableInfo table, boolean judgeAliasNull, final String alias,
                            final String prefix) {
        String sqlScript = table.getAllSqlSet(logic, prefix);
        if (judgeAliasNull) {
            sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", alias), true);
        }
        if (ew) {
            sqlScript += NEWLINE;
            sqlScript += convertIfEwParam(U_WRAPPER_SQL_SET, false);
        }
        sqlScript = SqlScriptUtils.convertSet(sqlScript);
        return sqlScript;
    }

    /**
     * SQL 注释
     *
     * @return sql
     */
    protected String sqlComment() {
        return convertIfEwParam(Q_WRAPPER_SQL_COMMENT, true);
    }

    /**
     * SQL 注释
     *
     * @return sql
     */
    protected String sqlFirst() {
        return convertIfEwParam(Q_WRAPPER_SQL_FIRST, true);
    }

    protected String convertIfEwParam(final String param, final boolean newLine) {
        return SqlScriptUtils.convertIf(SqlScriptUtils.unSafeParam(param),
            String.format("%s != null and %s != null", WRAPPER, param), newLine);
    }

    /**
     * SQL 查询所有表字段
     *
     * @param table        表信息
     * @param queryWrapper 是否为使用 queryWrapper 查询
     * @return sql 脚本
     */
    protected String sqlSelectColumns(TableInfo table, boolean queryWrapper) {
        /* 假设存在用户自定义的 resultMap 映射返回 */
        String selectColumns = ASTERISK;
        if (table.getResultMap() == null || table.isAutoInitResultMap()) {
            /* 未设置 resultMap 或者 resultMap 是自动构建的,视为属于mp的规则范围内 */
            selectColumns = table.getAllSqlSelect();
        }
        if (!queryWrapper) {
            return selectColumns;
        }
        return convertChooseEwSelect(selectColumns);
    }

    /**
     * SQL 查询记录行数
     *
     * @return count sql 脚本
     */
    protected String sqlCount() {
        return convertChooseEwSelect(ASTERISK);
    }

    /**
     * SQL 设置selectObj sql select
     *
     * @param table 表信息
     */
    protected String sqlSelectObjsColumns(TableInfo table) {
        return convertChooseEwSelect(table.getAllSqlSelect());
    }

    protected String convertChooseEwSelect(final String otherwise) {
        return SqlScriptUtils.convertChoose(String.format("%s != null and %s != null", WRAPPER, Q_WRAPPER_SQL_SELECT),
            SqlScriptUtils.unSafeParam(Q_WRAPPER_SQL_SELECT), otherwise);
    }

    /**
     * SQL map 查询条件
     */
    protected String sqlWhereByMap(TableInfo table) {
        if (table.isWithLogicDelete()) {
            // 逻辑删除
            String sqlScript = SqlScriptUtils.convertChoose("v == null", " ${k} IS NULL ",
                " ${k} = #{v} ");
            sqlScript = SqlScriptUtils.convertForeach(sqlScript, COLUMN_MAP, "k", "v", "AND");
            sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null and !%s.isEmpty", COLUMN_MAP, COLUMN_MAP), true);
            sqlScript += (NEWLINE + table.getLogicDeleteSql(true, true));
            sqlScript = SqlScriptUtils.convertWhere(sqlScript);
            return sqlScript;
        } else {
            String sqlScript = SqlScriptUtils.convertChoose("v == null", " ${k} IS NULL ",
                " ${k} = #{v} ");
            sqlScript = SqlScriptUtils.convertForeach(sqlScript, COLUMN_MAP, "k", "v", "AND");
            sqlScript = SqlScriptUtils.convertWhere(sqlScript);
            sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null and !%s", COLUMN_MAP,
                COLUMN_MAP_IS_EMPTY), true);
            return sqlScript;
        }
    }

    /**
     * EntityWrapper方式获取select where
     *
     * @param newLine 是否提到下一行
     * @param table   表信息
     * @return String
     */
    protected String sqlWhereEntityWrapper(boolean newLine, TableInfo table) {
        if (table.isWithLogicDelete()) {
            String sqlScript = table.getAllSqlWhere(true, true, WRAPPER_ENTITY_DOT);
            sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", WRAPPER_ENTITY),
                true);
            sqlScript += (NEWLINE + table.getLogicDeleteSql(true, true) + NEWLINE);
            String normalSqlScript = SqlScriptUtils.convertIf(String.format("AND ${%s}", WRAPPER_SQLSEGMENT),
                String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT,
                    WRAPPER_NONEMPTYOFNORMAL), true);
            normalSqlScript += NEWLINE;
            normalSqlScript += SqlScriptUtils.convertIf(String.format(" ${%s}", WRAPPER_SQLSEGMENT),
                String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT,
                    WRAPPER_EMPTYOFNORMAL), true);
            sqlScript += normalSqlScript;
            sqlScript = SqlScriptUtils.convertChoose(String.format("%s != null", WRAPPER), sqlScript,
                table.getLogicDeleteSql(false, true));
            sqlScript = SqlScriptUtils.convertWhere(sqlScript);
            return newLine ? NEWLINE + sqlScript : sqlScript;
        } else {
            String sqlScript = table.getAllSqlWhere(false, true, WRAPPER_ENTITY_DOT);
            sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", WRAPPER_ENTITY), true);
            sqlScript += NEWLINE;
            sqlScript += SqlScriptUtils.convertIf(String.format(SqlScriptUtils.convertIf(" AND", String.format("%s and %s", WRAPPER_NONEMPTYOFENTITY, WRAPPER_NONEMPTYOFNORMAL), false) + " ${%s}", WRAPPER_SQLSEGMENT),
                String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT,
                    WRAPPER_NONEMPTYOFWHERE), true);
            sqlScript = SqlScriptUtils.convertWhere(sqlScript) + NEWLINE;
            sqlScript += SqlScriptUtils.convertIf(String.format(" ${%s}", WRAPPER_SQLSEGMENT),
                String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT,
                    WRAPPER_EMPTYOFWHERE), true);
            sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", WRAPPER), true);
            return newLine ? NEWLINE + sqlScript : sqlScript;
        }
    }

    protected String sqlOrderBy(TableInfo tableInfo) {
        /* 不存在排序字段,直接返回空 */
        List<TableFieldInfo> orderByFields = tableInfo.getOrderByFields();
        if (CollectionUtils.isEmpty(orderByFields)) {
            return StringPool.EMPTY;
        }
        orderByFields.sort(Comparator.comparingInt(TableFieldInfo::getOrderBySort));
        StringBuilder sql = new StringBuilder();
        sql.append(NEWLINE).append(" ORDER BY ");
        sql.append(orderByFields.stream().map(tfi -> String.format("%s %s", tfi.getColumn(),
            tfi.getOrderByType())).collect(joining(",")));
        /* 当wrapper中传递了orderBy属性,@orderBy注解失效 */
        return SqlScriptUtils.convertIf(sql.toString(), String.format("%s == null or %s", WRAPPER,
            WRAPPER_EXPRESSION_ORDER), true);
    }

    /**
     * 过滤 TableFieldInfo 集合, join 成字符串
     */
    protected String filterTableFieldInfo(List<TableFieldInfo> fieldList, Predicate<TableFieldInfo> predicate,
                                          Function<TableFieldInfo, String> function, String joiningVal) {
        Stream<TableFieldInfo> infoStream = fieldList.stream();
        if (predicate != null) {
            return infoStream.filter(predicate).map(function).collect(joining(joiningVal));
        }
        return infoStream.map(function).collect(joining(joiningVal));
    }

    /**
     * 获取乐观锁相关
     *
     * @param tableInfo 表信息
     * @return String
     */
    protected String optlockVersion(TableInfo tableInfo) {
        if (tableInfo.isWithVersion()) {
            return tableInfo.getVersionFieldInfo().getVersionOli(ENTITY, ENTITY_DOT);
        }
        return EMPTY;
    }

    /**
     * 查询
     */
    protected MappedStatement addSelectMappedStatementForTable(Class<?> mapperClass, String id, SqlSource sqlSource,
                                                               TableInfo table) {
        String resultMap = table.getResultMap();
        if (null != resultMap) {
            /* 返回 resultMap 映射结果集 */
            return addMappedStatement(mapperClass, id, sqlSource, SqlCommandType.SELECT, null,
                resultMap, null, NoKeyGenerator.INSTANCE, null, null);
        } else {
            /* 普通查询 */
            return addSelectMappedStatementForOther(mapperClass, id, sqlSource, table.getEntityType());
        }
    }

    /**
     * 查询
     * @since 3.5.0
     */
    protected MappedStatement addSelectMappedStatementForTable(Class<?> mapperClass, SqlSource sqlSource, TableInfo table) {
        return addSelectMappedStatementForTable(mapperClass, this.methodName, sqlSource, table);
    }

    /**
     * 查询
     */
    protected MappedStatement addSelectMappedStatementForOther(Class<?> mapperClass, String id, SqlSource sqlSource,
                                                               Class<?> resultType) {
        return addMappedStatement(mapperClass, id, sqlSource, SqlCommandType.SELECT, null,
            null, resultType, NoKeyGenerator.INSTANCE, null, null);
    }

    /**
     * 查询
     *
     * @since 3.5.0
     */
    protected MappedStatement addSelectMappedStatementForOther(Class<?> mapperClass, SqlSource sqlSource, Class<?> resultType) {
        return addSelectMappedStatementForOther(mapperClass, this.methodName, sqlSource, resultType);
    }

    /**
     * 插入
     */
    protected MappedStatement addInsertMappedStatement(Class<?> mapperClass, Class<?> parameterType, String id,
                                                       SqlSource sqlSource, KeyGenerator keyGenerator,
                                                       String keyProperty, String keyColumn) {
        return addMappedStatement(mapperClass, id, sqlSource, SqlCommandType.INSERT, parameterType, null,
            Integer.class, keyGenerator, keyProperty, keyColumn);
    }

    /**
     * 插入
     * @since 3.5.0
     */
    protected MappedStatement addInsertMappedStatement(Class<?> mapperClass, Class<?> parameterType,
                                                       SqlSource sqlSource, KeyGenerator keyGenerator,
                                                       String keyProperty, String keyColumn) {
        return addInsertMappedStatement(mapperClass, parameterType, this.methodName, sqlSource, keyGenerator, keyProperty, keyColumn);
    }


    /**
     * 删除
     */
    protected MappedStatement addDeleteMappedStatement(Class<?> mapperClass, String id, SqlSource sqlSource) {
        return addMappedStatement(mapperClass, id, sqlSource, SqlCommandType.DELETE, null,
            null, Integer.class, NoKeyGenerator.INSTANCE, null, null);
    }

    /**
     * @since 3.5.0
     */
    protected MappedStatement addDeleteMappedStatement(Class<?> mapperClass, SqlSource sqlSource) {
        return addDeleteMappedStatement(mapperClass, this.methodName, sqlSource);
    }

    /**
     * 更新
     */
    protected MappedStatement addUpdateMappedStatement(Class<?> mapperClass, Class<?> parameterType, String id,
                                                       SqlSource sqlSource) {
        return addMappedStatement(mapperClass, id, sqlSource, SqlCommandType.UPDATE, parameterType, null,
            Integer.class, NoKeyGenerator.INSTANCE, null, null);
    }

    /**
     * 更新
     *
     * @since 3.5.0
     */
    protected MappedStatement addUpdateMappedStatement(Class<?> mapperClass, Class<?> parameterType,
                                                       SqlSource sqlSource) {
        return addUpdateMappedStatement(mapperClass, parameterType, this.methodName, sqlSource);
    }

    /**
     * 添加 MappedStatement 到 Mybatis 容器
     */
    protected MappedStatement addMappedStatement(Class<?> mapperClass, String id, SqlSource sqlSource,
                                                 SqlCommandType sqlCommandType, Class<?> parameterType,
                                                 String resultMap, Class<?> resultType, KeyGenerator keyGenerator,
                                                 String keyProperty, String keyColumn) {
        String statementName = mapperClass.getName() + DOT + id;
        if (hasMappedStatement(statementName)) {
            logger.warn(LEFT_SQ_BRACKET + statementName + "] Has been loaded by XML or SqlProvider or Mybatis's Annotation, so ignoring this injection for [" + getClass() + RIGHT_SQ_BRACKET);
            return null;
        }
        /* 缓存逻辑处理 */
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
        return builderAssistant.addMappedStatement(id, sqlSource, StatementType.PREPARED, sqlCommandType,
            null, null, null, parameterType, resultMap, resultType,
            null, !isSelect, isSelect, false, keyGenerator, keyProperty, keyColumn,
            configuration.getDatabaseId(), languageDriver, null);
    }

    /**
     * @since 3.5.0
     */
    protected MappedStatement addMappedStatement(Class<?> mapperClass, SqlSource sqlSource,
                                                 SqlCommandType sqlCommandType, Class<?> parameterType,
                                                 String resultMap, Class<?> resultType, KeyGenerator keyGenerator,
                                                 String keyProperty, String keyColumn) {
        return addMappedStatement(mapperClass, this.methodName, sqlSource, sqlCommandType, parameterType, resultMap, resultType, keyGenerator, keyProperty, keyColumn);
    }

    /**
     * 注入自定义 MappedStatement
     *
     * @param mapperClass mapper 接口
     * @param modelClass  mapper 泛型
     * @param tableInfo   数据库表反射信息
     * @return MappedStatement
     */
    public abstract MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo);

    /**
     * 获取自定义方法名,未设置采用默认方法名
     * https://gitee.com/baomidou/mybatis-plus/pulls/88
     *
     * @return method
     * @author 义陆无忧
     * @see AbstractMethod#AbstractMethod(java.lang.String)
     * @deprecated 3.5.0
     */
    @Deprecated
    public String getMethod(SqlMethod sqlMethod) {
        return StringUtils.isBlank(methodName) ? sqlMethod.getMethod() : this.methodName;
    }

}

简单看个SelectById类,以这个方法为例子

/**
 * 根据ID 查询一条数据
 *
 * @author hubin
 * @since 2018-04-06
 */
public class SelectById extends AbstractMethod {

    public SelectById() {
    	//给methodName属性赋值
        super(SqlMethod.SELECT_BY_ID.getMethod());
    }

    /**
     * @param name 方法名
     * @since 3.5.0
     */
    public SelectById(String name) {
    //给methodName属性赋值
        super(name);
    }

    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
    //获取SqlMethod类,这是一个枚举类,下面会有介绍
        SqlMethod sqlMethod = SqlMethod.SELECT_BY_ID;
        //生成最终的SqlSource对象
        SqlSource sqlSource = new RawSqlSource(configuration, String.format(sqlMethod.getSql(),
                sqlSelectColumns(tableInfo, false),
                tableInfo.getTableName(), tableInfo.getKeyColumn(), tableInfo.getKeyProperty(),
                tableInfo.getLogicDeleteSql(true, true)), Object.class);
         //将最终封装好的MappedStatement对象加入配置类对象中
        return this.addSelectMappedStatementForTable(mapperClass, getMethod(sqlMethod), sqlSource, tableInfo);
    }
}

在这里插入图片描述

断点看看
在这里插入图片描述
可以看到sqlMethod枚举类封装了相关sql语句,短点往下
在这里插入图片描述
最终生成了封装好sql语句和参数的SqlSource对象,这里生成的是StaticSqlSource静态对象

了解完上面的类,现在回头看看初始化时注入方法,不了解的可以先看我的上篇文章。

在这里插入图片描述
来看看parserInjector这个方法
在这里插入图片描述
GlobalConfigUtils.getSqlInjector(configuration)这句代码其实就是返回配置文件中设置的SqlInjector注入器,说明我们可以自定义注入器,默认注入器是DefaultSqlInjector。官方文档也正好证明这一点
在这里插入图片描述
在这里插入图片描述
最终这个方法调用了AbstractSqlInjector的inspectInject方法将CRUD相关的sql对象注入到配置中去

总结

到这里基本就介绍完了。到这里其实就能想到可以通过自定义BaseMapper,并且编写响应method对象,自定义实现ISqlInjector注入器。这样我们就能够对mybatis-plus进行扩展来实现更多的CRUD方法。后面文章我会出一片详细的

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
MyBatis Plus是一个基于MyBatis的增强工具,它简化了MyBatis的使用,提供了更多的功能和便利性。它通过封装了常用的操作,如CRUD操作、分页查询、乐观锁、逻辑删除等,来简化编码过程并提高开发效率。此外,MyBatis Plus还提供了代码生成,可以根据数据库表生成对应的实体类、Mapper接口和XML映射文件,减少了手动编写这些代码的工作量。 该工具还实现了一些高级特性,如多租户、性能分析、逻辑删除等。同时,MyBatis Plus也保留了MyBatis的灵活性和强大的SQL编写能力,可以与MyBatis无缝集成和共存。 关于MyBatis Plus的源码解析,可以参考【mybatis plus源码解析】系列文章。其中第一篇介绍了MyBatis Plus的执行原理和自动注入CRUD操作实现,第详解SQL注入底层原理自动注入CRUD操作实现。这些文章可以帮助你更深入地理解MyBatis Plus的内部机制和实现原理。 如果你对MyBatis Plus的源码解析感兴趣,可以查看这些文章,它们会对你理解MyBatis Plus的工作原理和使用方式有所帮助。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [【mybatis plus源码解析】(一)mybatis plus执行原理,mybatis plus是如何实现自动注入CRUD操作](https://blog.csdn.net/qq_35270805/article/details/123825416)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值