mybatis(4)Mybatis解析动态sql原理分析

MyBatis动态 SQL 的执行原理 MyBatis 的动态 SQL 提供了强大的功能,使得开发者能够灵活地生成复杂的 SQL 查询。通过使用。 阅读详情

出自:https://www.cnblogs.com/fangjian0423/p/mybaits-dynamic-sql-analysis.html

Mybatis解析动态sql原理分析

前言

废话不多说,直接进入文章。
我们在使用mybatis的时候,会在xml中编写sql语句。
比如这段动态sql代码:

<update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User">
    UPDATE users
    <trim prefix="SET" prefixOverrides=",">
        <if test="name != null and name != ''">
            name = #{name}
        </if>
        <if test="age != null and age != ''">
            , age = #{age}
        </if>
        <if test="birthday != null and birthday != ''">
            , birthday = #{birthday}
        </if>
    </trim>
    where id = ${id}
</update>

mybatis底层是如何构造这段sql的?
这方面的知识网上资料不多,于是就写了这么一篇文章。
下面带着这个疑问,我们一步一步分析。

介绍MyBatis中一些关于动态SQL的接口和类

SqlNode接口,简单理解就是xml中的每个标签,比如上述sql的update,trim,if标签:

public interface SqlNode {
  boolean apply(DynamicContext context);
}

img

SqlSource Sql源接口,代表从xml文件或注解映射的sql内容,主要就是用于创建BoundSql,有实现类DynamicSqlSource(动态Sql源),StaticSqlSource(静态Sql源)等:

public interface SqlSource {
  BoundSql getBoundSql(Object parameterObject);
}

img

BoundSql类,封装mybatis最终产生sql的类,包括sql语句,参数,参数源数据等参数:

img

XNode,一个Dom API中的Node接口的扩展类。

img

BaseBuilder接口及其实现类(属性,方法省略了,大家有兴趣的自己看),这些Builder的作用就是用于构造sql:

img

下面我们简单分析下其中4个Builder:

1 XMLConfigBuilder

解析mybatis中configLocation属性中的全局xml文件,内部会使用XMLMapperBuilder解析各个xml文件。

2 XMLMapperBuilder

遍历mybatis中mapperLocations属性中的xml文件中每个节点的Builder,比如user.xml,内部会使用XMLStatementBuilder处理xml中的每个节点。

3 XMLStatementBuilder

解析xml文件中各个节点,比如select,insert,update,delete节点,内部会使用XMLScriptBuilder处理节点的sql部分,遍历产生的数据会丢到Configuration的mappedStatements中。

4 XMLScriptBuilder

解析xml中各个节点sql部分的Builder。

LanguageDriver接口及其实现类(属性,方法省略了,大家有兴趣的自己看),该接口主要的作用就是构造sql:

img

简单分析下XMLLanguageDriver(处理xml中的sql,RawLanguageDriver处理静态sql):

XMLLanguageDriver内部会使用XMLScriptBuilder解析xml中的sql部分。

ok, 大部分比较重要的类我们都已经介绍了,下面源码分析走起。

源码分析走起

Spring与Mybatis整合的时候需要配置SqlSessionFactoryBean,该配置会加入数据源和mybatis xml配置文件路径等信息:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="configLocation" value="classpath:mybatisConfig.xml"/>
    <property name="mapperLocations" value="classpath*:org/format/dao/*.xml"/>
</bean>

我们就分析这一段配置背后的细节:

SqlSessionFactoryBean实现了Spring的InitializingBean接口,InitializingBean接口的afterPropertiesSet方法中会调用buildSqlSessionFactory方法

buildSqlSessionFactory方法内部会使用XMLConfigBuilder解析属性configLocation中配置的路径,还会使用XMLMapperBuilder属性解析mapperLocations属性中的各个xml文件。

部分源码如下:

img

由于XMLConfigBuilder内部也是使用XMLMapperBuilder,我们就看看XMLMapperBuilder的解析细节。

img

img

我们关注一下,增删改查节点的解析。

img

XMLStatementBuilder的解析:

img

默认会使用XMLLanguageDriver创建SqlSource(Configuration构造函数中设置)。

XMLLanguageDriver创建SqlSource:

img

XMLScriptBuilder解析sql:

img

得到SqlSource之后,会放到Configuration中,有了SqlSource,就能拿BoundSql了,BoundSql可以得到最终的sql。

实例分析

我以以下xml的解析大概说下parseDynamicTags的解析过程:

<update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User">
    UPDATE users
    <trim prefix="SET" prefixOverrides=",">
        <if test="name != null and name != ''">
            name = #{name}
        </if>
        <if test="age != null and age != ''">
            , age = #{age}
        </if>
        <if test="birthday != null and birthday != ''">
            , birthday = #{birthday}
        </if>
    </trim>
    where id = ${id}
</update>

在看这段解析之前,请先了解dom相关的知识,xml dom知识, dom博文

parseDynamicTags方法的返回值是一个List,也就是一个Sql节点集合。SqlNode本文一开始已经介绍,分析完解析过程之后会说一下各个SqlNode类型的作用。

1 首先根据update节点(Node)得到所有的子节点,分别是3个子节点

(1)文本节点 \n UPDATE users

(2)trim子节点 …

(3)文本节点 \n where id = #{id}

2 遍历各个子节点

(1) 如果节点类型是文本或者CDATA,构造一个TextSqlNode或StaticTextSqlNode

(2) 如果节点类型是元素,说明该update节点是个动态sql,然后会使用NodeHandler处理各个类型的子节点。这里的NodeHandler是XMLScriptBuilder的一个内部接口,其实现类包括TrimHandler、WhereHandler、SetHandler、IfHandler、ChooseHandler等。看类名也就明白了这个Handler的作用,比如我们分析的trim节点,对应的是TrimHandler;if节点,对应的是IfHandler…

这里子节点trim被TrimHandler处理,TrimHandler内部也使用parseDynamicTags方法解析节点

3 遇到子节点是元素的话,重复以上步骤

trim子节点内部有7个子节点,分别是文本节点、if节点、是文本节点、if节点、是文本节点、if节点、文本节点。文本节点跟之前一样处理,if节点使用IfHandler处理

遍历步骤如上所示,下面我们看下几个Handler的实现细节。

IfHandler处理方法也是使用parseDynamicTags方法,然后加上if标签必要的属性。

private class IfHandler implements NodeHandler {
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      List<SqlNode> contents = parseDynamicTags(nodeToHandle);
      MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
      String test = nodeToHandle.getStringAttribute("test");
      IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
      targetContents.add(ifSqlNode);
    }
}

TrimHandler处理方法也是使用parseDynamicTags方法,然后加上trim标签必要的属性。

private class TrimHandler implements NodeHandler {
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      List<SqlNode> contents = parseDynamicTags(nodeToHandle);
      MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
      String prefix = nodeToHandle.getStringAttribute("prefix");
      String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");
      String suffix = nodeToHandle.getStringAttribute("suffix");
      String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");
      TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
      targetContents.add(trim);
    }
}

以上update方法最终通过parseDynamicTags方法得到的SqlNode集合如下:

img

trim节点:

img

由于这个update方法是个动态节点,因此构造出了DynamicSqlSource。

DynamicSqlSource内部就可以构造sql了:

img

DynamicSqlSource内部的SqlNode属性是一个MixedSqlNode。

然后我们看看各个SqlNode实现类的apply方法

下面分析一下两个SqlNode实现类的apply方法实现:

MixedSqlNode:

public boolean apply(DynamicContext context) {
    for (SqlNode sqlNode : contents) {
      sqlNode.apply(context);
    }
    return true;
}

MixedSqlNode会遍历调用内部各个sqlNode的apply方法。

StaticTextSqlNode:

public boolean apply(DynamicContext context) {
    context.appendSql(text);
    return true;
}

直接append sql文本。

IfSqlNode:

public boolean apply(DynamicContext context) {
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      contents.apply(context);
      return true;
    }
    return false;
}

这里的evaluator是一个ExpressionEvaluator类型的实例,内部使用了OGNL处理表达式逻辑。

TrimSqlNode:

public boolean apply(DynamicContext context) {
    FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
    boolean result = contents.apply(filteredDynamicContext);
    filteredDynamicContext.applyAll();
    return result;
}

public void applyAll() {
  sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
  String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
  if (trimmedUppercaseSql.length() > 0) {
    applyPrefix(sqlBuffer, trimmedUppercaseSql);
    applySuffix(sqlBuffer, trimmedUppercaseSql);
  }
  delegate.appendSql(sqlBuffer.toString());
}

private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
  if (!prefixApplied) {
    prefixApplied = true;
    if (prefixesToOverride != null) {
      for (String toRemove : prefixesToOverride) {
        if (trimmedUppercaseSql.startsWith(toRemove)) {
          sql.delete(0, toRemove.trim().length());
          break;
        }
      }
    }
    if (prefix != null) {
      sql.insert(0, " ");
      sql.insert(0, prefix);
    }
  }
}

TrimSqlNode的apply方法也是调用属性contents(一般都是MixedSqlNode)的apply方法,按照实例也就是7个SqlNode,都是StaticTextSqlNode和IfSqlNode。 最后会使用FilteredDynamicContext过滤掉prefix和suffix。

总结

大致讲解了一下mybatis对动态sql语句的解析过程,其实回过头来看看不算复杂,还算蛮简单的。 之前接触mybaits的时候遇到刚才分析的那一段动态sql的时候总是很费解。

<update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User">
    UPDATE users
    <trim prefix="SET" prefixOverrides=",">
        <if test="name != null and name != ''">
            name = #{name}
        </if>
        <if test="age != null and age != ''">
            , age = #{age}
        </if>
        <if test="birthday != null and birthday != ''">
            , birthday = #{birthday}
        </if>
    </trim>
    where id = ${id}
</update>

想搞明白这个trim节点的prefixOverrides到底是什么意思(从字面上理解就是前缀覆盖),而且官方文档上也没这方面知识的说明。我将这段xml改成如下:

<update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User">
    UPDATE users
    <trim prefix="SET" prefixOverrides=",">
        <if test="name != null and name != ''">
            , name = #{name}
        </if>
        <if test="age != null and age != ''">
            , age = #{age}
        </if>
        <if test="birthday != null and birthday != ''">
            , birthday = #{birthday}
        </if>
    </trim>
    where id = ${id}
</update>

(第二段第一个if节点多了个逗号) 结果我发现这2段xml解析的结果是一样的,非常迫切地想知道这到底是为什么,然后这也促使了我去看源码的决心。最终还是看下来了。

MyBatis源码解析——动态SQL实现原理_mybatis注解动态sql provider(2) 如上面的代码所示,StaticSqlSource类的内容比较简单,只封装了Mapper解析后的SQL内容和Mapper参数映射信息。 阅读详情

相关推荐

MyBatis第七讲:MyBatis动态SQL优雅实践与原理

动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。

Jet_qi的博客 786

Mybatis自定义拦截器与插件开发

在Spring中我们经常会使用到拦截器,在登录验证、日志记录、性能监控等场景中,通过使用拦截器允许我们在不改动业务代码的情况下,执行拦截器的方法来增强现有的逻辑。在mybatis中,同样也有这样的业务场景,有时候需要我们在不侵入原有业务代码的情况下拦截sql,执行特定的某些逻辑。那么这个过程应该怎么实现呢,同样,在mybatis中也为开发者预留了拦截器接口,通过实现自定义拦截器这一功能,可以实现我们自己的插件,允许用户在不改动mybatis的原有逻辑的条件下,实现自己的逻辑扩展。 那么,在实现拦截器之前,首

1561

Mybatis | 动态 SQL 原理分析

日拱一卒无有尽,功不唐捐终入海。 如果 SQL 语句中包含 trim | where | set | foreach | if | choose | when | otherwise | bind 标签或者字符串替换符「 $ 」,MyBatis 就认为其为动态 SQL,在执行时会根据 Mapper 接口方法传入的参数动态计算 SQl 语句。

u010141779的博客 402

MyBatis - 分页之四大方式(数组、SQL、拦截器,RowBounds)

前言:学习hibernate &amp; mybatis等持久层框架的时候,不外乎对数据库的增删改查操作。而使用最多的当是数据库的查找操作, 而当数据库数据过多时,符合查找条件的数据可能也会是很庞大的数据。往往在这个时候,我们都不会希望一次性的将所有的数据一起性读取出来,并且显示在UI界面上。常用的操作,就是对查找到的数据进行分页,每次处理小部分数据。这样每次处理的数据量就会在可控的范围,UI的...

4130

Jetson Nano 【1】入手开箱、烧系统

类似于半开箱哈哈 怀着小期待的心情,屁颠屁颠跑到村口领快递,拿到了这个香喷喷的Jetson Nano 开发板 开箱: 由于太激动了忘记拍点啥,于是在最后留了张全家福 板子的结构: 我觉得这个有必要记录一下,于是去官方文档拿了张图来:官方文档传送 1=SD card 的卡槽,位于第二层板子的背部,也就是大风扇屁股位置 2=40针扩展接头(还没开用) 3=USB的供电口,根据大佬测试(某哩...

椰子奶糖的博客 4194

别再死记硬背了!用这5个Qt Design Studio小项目,彻底玩转核心组件

本文通过5个Qt Design Studio实战项目,帮助开发者深入理解核心组件的协作与应用。从动态登录界面到游戏化任务列表,每个项目都展示了组件属性与交互设计的巧妙结合,让你不再死记硬背,而是真正掌握Qt Design Studio的核心技能。

weixin_29202213的博客 271

MyBatis Plus updateById方法更新不了空字符串null

背景 在更新数据时使用updateById方法更新数据,但是发现为空的默认没有更新,在网上和官网搜索的许多方法都没有实现,最后查看底层实现才弄出来了,我使用的版本是MyBatis Plus3.3.0 解决方案 在实体类中,属性值上加上注解: @TableField(value = "STUID", updateStrategy = FieldStrategy.IGNORED, jdbcType = JdbcType.BIGINT) private Long stuid; ...

dinghongyu520的博客 523

从零到一:手把手教你用DJI E-Port套件为M350 RTK开发第三方负载(PSDK V3实战)

本文详细介绍了如何使用DJI E-Port套件为M350 RTK开发第三方负载,涵盖硬件配置、开发环境搭建及实战应用。通过PSDK V3开发套件,开发者可以高效实现负载与无人机的通信,适用于电力巡检等工业场景,提升开发效率与系统稳定性。

weixin_26841743的博客 701

Mybatis解析动态sql原理分析

前言 废话不多说,直接进入文章。 我们在使用mybatis的时候,会在xml中编写sql语句。 比如这段动态sql代码: "update" parameterType="org.format.dynamicproxy.mybatis.bean.User"> UPDATE users "SET" prefixOverrides=","> if test="

多看多听多总结 5975

GNU Radio系列教程(一):什么是GNU Radio?为什么我需要用GNU Radio?

目录 一、什么是软件无线电? 二、GNU Radio到底能做什么事情? 三、我如果不精通编程,还能用GNU Radio吗? 四、GNU Radio使用需要授权吗? 五、基于GNU Radio,人们已经做了取得了哪些成果? 六、数字信号处理、基带、时钟同步…这些名词是什么意思? 七、有用的链接和联系方式 GNU Radio是一个免费的开源软件工具包,该工具包提供了可用于软件无线电的信号处理模块。基于一些通用的射频前端设备,该工具包可以用来实现软件无线电系统。此外,该工具包也可以用于单纯...

无线通信实验室 1万+

Python+OpenCV实战:5分钟搞定机械臂手眼标定(眼在手上模式)

本文详细介绍了使用Python和OpenCV在5分钟内完成机械臂手眼标定(眼在手上模式)的实战教程。通过解析相机坐标系与机械臂基坐标系的转换关系,提供从数据采集到标定矩阵计算的全流程代码实现,帮助开发者快速解决AX=XB问题,提升机械臂视觉引导精度。

weixin_33670786的博客 496

【MyBatis】MyBatis解析动态SQL原理分析

MyBatis通过SqlNode、SqlSource等处理动态SQL,XMLMapperBuilder解析XML,NodeHandler(如IfHandler、TrimHandler)构造动态SQL逻辑,最终生成BoundSql执行。prefixOverrides用于trim标签中忽略特定前缀,精简SQL输出。

小七的博客 1576

EMC基础知识总结

电磁干扰(Electromagnetic Interference),简称EMI,有和两种。传导干扰主要是电子设备产生的干扰信号通过导电介质或公共电源线互相产生干扰;辐射干扰是指电子设备产生的干扰信号通过空间耦合把干扰信号传给另一个电网络或电子设备。

学海无涯的专栏 1364

MyBatis解析动态SQL原理分析

废话不多说,直接进入文章。 我们在使用mybatis的时候,会在xml中编写sql语句。 比如这段动态sql代码: <update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User"> UPDATE users <trim prefix="SET" prefixOverrides=","> <if test="name != null and n...

Xx__WangQi的博客 1226

【06】新国都——新国都 嵌入式笔试,校招,题目记录及解析

本文总结了新国都嵌入式校招笔试题目及解析,内容涵盖C语言基础、Linux系统、数据结构与算法等多个方面。主要包括:1)不定项选择题10道,涉及字符串常量、蓝牙与GPRS区别、编译错误识别等;2)判断题10道,考察Linux权限、循环控制、内存管理等;3)填空题5道,包括编码计算、逻辑运算、宏定义等;4)简答题2道,解析全局变量引用和进程线程区别。题目难度适中,重点考察嵌入式开发基础知识和编程能力,并提供详细解析和扩展知识链接,对准备嵌入式相关笔试具有参考价值。

程序员,他们想的是什么?他们想的永远都是技术,他们崇尚的也永远都是技术。 1431

STM32 HAL PWM (占空比) -电机软启动不到位解决

为了避免电机启动电压大造成设备损坏,或者供电不足导致重启,通常在对启动都需要一个PWM进行软启动,下面对PWM电机软启动详细介绍。

weixin_44597885的博客 866

Mybatis动态SQL原理解析

资料准备 官网 首先翻阅官网文档,(https://mybatis.org/mybatis-3/dynamic-sql.html)这页文档首先描述了Mybatis的动态SQL是多么牛逼,解决了哪些问题,又讲了如何使用,而我们想知道他是如何实现的,所以这不是我们想要的。 Dynamic SQL One of the most powerful features of MyB...

一瓶绿茶三元钱 3228

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

MyBatis -- 动态sql原理分析

动态sql原理分析Mybatis 动态 SQL 是做什么的动态 SQL 标签功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是必不可少的KaTeX数学公式新的甘特图功能,丰富你的文章UML 图表FLowchart流程图导出与导入导出导入 Mybatis 动态 SQL 是做什么的 Mybatis的 动态 SQL 是指在进行 SQL 操作的时候,

liyuan413050636的博客 1354

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

MyBatis中动态SQL执行原理

XML 配置解析:MyBatis 读取映射文件中的动态 SQL 标签(如<if><choose><foreach>等)并解析它们。条件判断:在执行时,MyBatis 根据传入的参数判断哪些 SQL 标签的条件满足,并决定是否将相关的 SQL 片段包含在最终的 SQL 中。生成 SQL:根据条件判断结果,MyBatis 动态生成 SQL 查询语句。参数绑定:MyBatis 将查询方法的参数绑定到 SQL 中的占位符。执行 SQL:最终生成的 SQL 被执行,结果返回给调用者。

午夜后是黎明,风雨后是骄阳 1565

Mybatis动态sql是做什么的?都有哪些动态sql?简述一下动态sql的执行原理?

Mybatis的动态sql的作用: 动态sql就是(在进行sql操作的时候)动态的根据属性值(所匹配的条件)来拼接数据库执行的sql语句,也就是多次查询或变更操作,根据传入的属性值不同,动态拼接出不同的可执行sql。包含判断为空、循环等; Mybatis动态sql(有哪些)标签: 1、<if>: if是为了判断传入的值是否符合某种规则,比如是否不为空; 2、<where>: where标签可以用来做动态拼接查询条件,当和if标签配合的时候,不用显示的声明类...

静默 1万+

Mybatis——Mybatis动态SQL原理

本博文主要介绍Mybatis动态SQL原理。帮助大家更好的学习和理解动态 SQL的原理,同时帮助大家如何配置和使用动态SQL。

庄小焱 1629

MyBatis 动态 SQL 底层原理分析

MyBatis 动态 SQL 底层原理分析 我们在使用mybatis的时候,会在xml中编写sql语句。 比如这段动态sql代码: <update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User"> UPDATE users <trim prefix=...

YunWisdom 588

详解MyBatis的动态SQL实现原理

MyBatis提供了强大的动态SQL语句生成功能,以应对复杂的业务场景,本篇文章将结合MyBatis解析SQL语句的过程对MyBatis中对,,等动态SQL标签的支持进行分析。MyBatis3.5.6MyBatis会为映射文件中的每个CURD标签节点里的SQL语句生成一个SqlSource如果是静态SQL语句,那么会生成;如果是动态SQL语句,则会生成。MyBatis在生成SqlSource时,会为CURD标签节点的每个子节点都生成一个SqlNode。

Java技术攻略的博客 967

深入理解Mybatis原理》MyBatis动态SQL原理

SqlSessionFactoryBean实现了Spring的InitializingBean接口,InitializingBean接口的afterPropertiesSet方法中会调用buildSqlSessionFactory方法 该方法内部会使用XMLConfigBuilder解析属性configLocation中配置的路径,还会使用XMLMapperBuilder属性解析mapperLocations属性中的各个xml文件。文本节点跟之前一样处理,if节点使用IfHandler处理。

sdgfafg_25的博客 1083

MyBatis源码解析——动态SQL实现原理

如果读者有过JDBC编程经验,肯定能体会到SQL语句拼接的痛苦。在有些情况下,我们需要根据不同的查询条件动态地拼接SQL语句,拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号,这个过程非常容易出错,导致我们需要在调试SQL语句的正确性上花费一定的时间。MyBatis的动态SQL特性能够彻底解决我们的烦恼,本章我们就来学习MyBatis动态SQL的使用及它的实现原理。

qq_16500963的博客 1324
上一篇: mybatis(3) MyBatis拦截器原理探究
下一篇: mybatis(5) 深入了解MyBatis参数
大神,快来碗里
博客等级 码龄15年 3075粉丝 640原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值