MyBatis3源码深度解析(十二)MyBatis的核心组件(一)Configuration

第四章 MyBatis的核心组件

4.1 使用MyBatis操作数据库

在研究MyBatis的核心组件之前,有必要了解一下如何使用MyBatis操作数据库。本文使用maven创建一个简单的示例项目来演示。

(1)准备数据库环境:使用MySQL数据库,创建一个新的database:mybatis_demo,新的表user,并插入数据。

CREATE DATABASE mybatis_demo;
USE mybatis_demo;
CREATE TABLE `user` (
  `id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `name` VARCHAR(20) DEFAULT NULL,
  `age` INT(11) DEFAULT NULL,
  `phone` VARCHAR(20) DEFAULT NULL,
  `birthday` DATETIME DEFAULT NULL
)
insert into user (name, age, phone, birthday) values('孙悟空', 1500, '18705464523', '0000-01-01');
insert into user (name, age, phone, birthday) values('猪八戒', 1000, '15235468789', '0500-03-10');

(2)创建maven项目,导入相关依赖。

<dependencies>
    <!--单元测试-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>compile</scope>
    </dependency>
    <!--mybatis-->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.15</version>
    </dependency>
    <!--mysql-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>
</dependencies>

(3)编写MyBatis主配置文件mybatis-config.xml。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC
        "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
    <settings>
        <!--支持返回自动生成主键-->
        <setting name="useGeneratedKeys" value="true"/>
        <!--支持实体名驼峰原则-->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    <!--别名处理-->
    <typeAliases>
        <package name="com.star.mybatis.entity"/>
    </typeAliases>
    <!--环境信息-->
    <environments default="dev">
        <environment id="dev">
            <!--事务管理器-->
            <transactionManager type="JDBC"/>
            <!--数据源:用户名和密码需要改成自己定义的-->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis_demo"/>
                <property name="username" value="******"/>
                <property name="password" value="******"/>
            </dataSource>
        </environment>
    </environments>
    <!--Mapper映射文件-->
    <mappers>
        <mapper resource="mapper/UserMapper.xml"/>
    </mappers>
</configuration>

MyBatis主配置文件用于配置MyBatis框架的配置信息,这些参数会改变MyBatis的运行时行为。

(4)编写Java实体User。

public class User {

    private Integer id;
    private String name;
    private Integer age;
    private String phone;
    private Date birthday;
    
    // constructor getter setter ...
}

(5)编写UserMapper接口。

public interface UserMapper {

    List<User> selectAll();

    @Select("select * from user where id = #{id, jdbcType=INTEGER}")
    User selectById(@Param("id") Integer id);

}

为了演示方便,这里使用两种方式配置SQL语句,selectAll()方法用XML的方式,selectById()方法用注解的方式。

(6)编写映射文件UserMapper.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.star.mybatis.mapper.UserMapper">

    <select id="selectAll" resultType="User">
        select * from user
    </select>

</mapper>

(7)编写单元测试。

@Test
public void testMybatis() throws IOException {
    // 读取配置文件
    Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
    // 创建会话
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
    SqlSession sqlSession = sqlSessionFactory.openSession();
    // 获取Mapper接口
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    // 操作数据库
    List<User> userList = userMapper.selectAll();
    userList.forEach(System.out::println);
    System.out.println("---------");
    User user = userMapper.selectById(1);
    System.out.println(user.toString());
}

(8)执行单元测试,控制台打印结果。

User{id=1, name='孙悟空', age=1500, phone='18705464523', birthday=Thu Jan 01 00:00:00 CST 1}
User{id=2, name='猪八戒', age=1000, phone='15235468789', birthday=Fri Mar 10 00:00:00 CST 500}
---------
User{id=1, name='孙悟空', age=1500, phone='18705464523', birthday=Thu Jan 01 00:00:00 CST 1}

如示例代码所示,SqlSession是MyBatis提供的与数据库交互的接口,其实例通过工厂模式创建。

为了创建SqlSession对象,首先需要创建SqlSessionFactory对象,而SqlSessionFactory对象的创建又依赖于SqlSessionFactoryBuilder类,该类提供了一系列重载的build()方法,将主配置文件的输入流作为参数传入SqlSessionFactoryBuilder对象的build()方法,则可以返回一个SqlSessionFactory对象。

有了SqlSessionFactory对象后,调用其openSession()方法即可获得一个与数据库建立连接的SqlSession实例。

调用SqlSession对象的getMapper()方法创建一个Mapper接口的动态代理对象,然后调用代理实例的方法即可完成与数据库的交互。

需要注意的是,MyBatis来源于iBatis项目,依然保留了iBatis执行Mapper的方式,例如:

// 兼容iBatis执行Mapper的方式
List<User> userList = sqlSession.selectList("com.star.mybatis.mapper.UserMapper.selectAll");

另外,MyBatis还提供了一个SqlSessionManager类,同样可以用于与数据库的交互,例如可以把单元测试方法修改成如下:

@Test
public void testSqlSessionManager() throws IOException {
    Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
    // 创建SqlSessionManager对象
    SqlSessionManager sqlSessionManager = SqlSessionManager.newInstance(reader);
    sqlSessionManager.startManagedSession();
    // 通过SqlSessionManager获取Mapper接口
    UserMapper userMapper = sqlSessionManager.getMapper(UserMapper.class);
    List<User> userList = userMapper.selectAll();
    userList.forEach(System.out::println);
    System.out.println("---------");
    User user = userMapper.selectById(1);
    System.out.println(user.toString());
}

如示例代码所示,SqlSessionManager使用了单例模式,在整个应用程序中只存在一个实例。通过该类的静态方法newInstance()即可获取SqlSessionManager的实例。

SqlSessionManager实现了SqlSessionFactory和SqlSession接口,因此可以替代SqlSession完成与数据库的交互。

4.2 MyBatis核心组件

在上面的示例项目中,有一个比较核心的MyBatis组件——SqlSession,它是面向用户的操作数据库的API。除此之外,MyBatis还有一些其他的核心组件,其执行流程如下图所示:

这些核心组件的作用大致描述如下:

  • Configuration

用于描述MyBatis的主配置信息。其他组件需要获取配置信息时,可以直接通过Configuration对象获取。MyBatis在应用启动时,会读取主配置文件mybatis-config.xml中的配置,并将这些配置注册到Configuration组件中,其他组件需要这些配置时,可以直接通过Configuration对象获取。

  • MappedStatement

用于描述Mapper中的SQL配置信息,是对Mapper XML配置文件中的、、、等标签或者@Select、@Update、@Delete、@Insert等注解的封装。

  • SqlSession

是MyBatis提供的面向用户的API,表示和数据库交互时的会话对象,用于完成数据库的增删改查功能。SqlSession是Executor组件的外观,目的是对外提供易于理解和使用的数据库操作接口。

  • Executor

Executor是MyBatis的SQL执行器,MyBatis中对数据库的所有增删改查操作都是由Executor组件完成的。

  • StatementHandler

StatementHandler封装了对JDBC的Statement对象的操作,比如为Statement对象设置参数、调用Statement接口提供的方法与数据库交互等。

  • ParameterHandler

当MyBatis使用的Statement类型为PreparedStatement和CallableStatement时,ParameterHandler用于为Statement对象的参数占位符设置值。

  • ResultSetHandler

ResultSetHandler封装了对JDBC中的ResultSet对象的操作,当执行SQL类型为SELECT语句时,ResultSetHandler用于将查询结果转换为Java对象。

  • TypeHandler

TypeHandler是MyBatis的类型处理器,用于处理Java类型与JDBC类型之间的映射。它能够根据Java类型调用PreparedStatement或CallableStatement对象的setXXX()方法为Statement对象设置值,而且能够根据Java类型调用ResultSet对象的getXXX()方法获取SQL执行结果。

MyBatis操作数据库的过程大致如下:

  1. SqlSession是用户层面的API,它是Executor组件的外观,目的是为用户提供更友好的数据库操作接口。
  2. 真正执行SQL操作的是Executor组件,它是SQL执行器,会使用StatementHandler组件对JDBC的Statement对象进行操作。
  3. 当Statement类型为PreparedStatement和CallableStatement时,会通过ParameterHandler组件为参数占位符赋值。
  4. ParameterHandler组件会根据Java类型找到对应的TypeHandler对象,TypeHandler中会通过Statement对象提供的setXXX()方法为Statement对象中的参数占位符设置值。
  5. StatementHandler组件使用JDBC中的Statement对象与数据库完成交互后,当SQL语句类型为SELECT时,通过ResultSetHandler组件从Statement对象中获取ResultSet对象,然后将ResultSet对象转换为Java对象。

4.3 Configuration组件

MyBatis框架的配置信息有两种,一种是配置MyBatis框架属性的主配置文件,另一种是配置执行SQL语句的Mapper配置文件。

Configuration组件的作用是描述MyBatis主配置文件的信息,即mybatis-config.xml文件。

4.3.1 属性

在mybatis-config.xml文件中,属性使用标签进行配置,其特点是可外部配置可动态替换。例如:

<!-- mybatis-config.xml -->
<properties resource="C:\workspace\mybatis_demo2\config\config.properties">
    <property name="username" value="dev_user"/>
    <property name="password" value="*****"/>
</properties>

可外部配置的意思是,属性既可以在标签下使用标签直接配置,也可以通过resource属性引用外部的一个properties文件。如果两种方式都配置了属性,则优先加载标签下的属性,再加载resource路径下properties文件中的属性,并覆盖已有的同名属性。

可动态替换的意思是,标签下设置的属性,可以在整个配置文件中用于替换需要动态配置的属性值。例如,在配置文件的其它位置使用<property name="username" value="${username}"/>时,${username}会被替换为dev_user

在Configuration组件中,组合了一个Properties对象,用于保存属性配置。

源码1org.apache.ibatis.session.Configuration

protected Properties variables = new Properties();

借助Debug工具,可以看到属性均被加载到Configuration组件中:

4.3.2 设置

在mybatis-config.xml文件中,属性使用标签进行配置,这些设置属性的值会改变MyBatis运行时的行为,因此非常重要。

相对应的,Configuration组件定义了一系列属性来保存这些设置属性。例如:

<!-- mybatis-config.xml -->
<settings>
    <!--支持返回自动生成主键-->
    <setting name="useGeneratedKeys" value="true"/>
    <!--支持自动驼峰命名规则-->
    <setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>

对应到Configuration组件定义的属性是:

源码2org.apache.ibatis.session.Configuration

public class Configuration {
    protected boolean mapUnderscoreToCamelCase;
    protected boolean useGeneratedKeys;
    // ......
}

除了 源码1 中列出的2个设置属性,还有一系列属性,这些属性的作用、默认值等可以参见 W3Cschool的文档-MyBatis教程-XML配置 中的一张表。

借助Debug工具,可以看到设置属性被加载到Configuration组件中:

在这里插入图片描述

4.3.3 类型别名

类型别名就是为Java类型设置一个短的名字,用来减少类的完全限定名的冗余。例如使用User作为com.star.mybatis.entity.User的别名:

在mybatis-config.xml文件中,类型别名使用标签进行配置。例如:

<!-- mybatis-config.xml -->
<typeAliases>
    <typeAlias alias="User" type="com.star.mybatis.entity.User"/>
</typeAliases>

也可以指定一个包名,MyBatis会在包下面搜索需要的Java对象,例如:

<!-- mybatis-config.xml -->
<typeAliases>
    <package name="com.star.mybatis.entity"/>
</typeAliases>

在没有@Alias注解的情况下,MyBatis会使用Java对象的首字母小写的非限定类名来作为它的别名,例如com.star.mybatis.entity包下的User类的别名为user;若该类有@Alias注解,则别名为其注解值。

@Alias("user")
public class User {...}

MyBatis本身已经为许多常见的Java类型内置了相应的类型别名,如下表:

在这里插入图片描述

对应到Configuration组件,组合了一个类型别名注册器,用于注册所有的类型别名。其源码如下:

源码3org.apache.ibatis.session.Configuration

// 用于注册所有的类型别名
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();

借助Debug工具,可以看到加载到Configuration组件中的别名有81个,其中包括自定义的User类的别名:

4.3.3 类型处理器

类型处理器的作用在于,MyBatis在预处理语句(PreparedStatement)中设置一个参数时,或者从ResulrSet结果集中取出一个值时,都会用类型处理器将获取的值以合适的方式转换成JDBC类型或Java类型。

在mybatis-config.xml文件中,类型别名使用标签进行配置。例如:

<!-- mybatis-config.xml -->
<typeHandlers>
    <typeHandler handler="com.star.mybatis.handler.ExampleTypeHandler"
        javaType="String"
        jdbcType="VARCHAR"/>
</typeHandlers>

MyBatis有许多默认的类型处理器实现,基本涵盖Java全部基本数据类型,如下表所示:

另外,类型处理器支持重写,或创建自定义的类型处理器来处理不支持的或非标准的类型

具体做法是:实现 org.apache.ibatis.type.TypeHandler接口, 或继承org.apache.ibatis.type.BaseTypeHandler,然后在主配置文件mybatis-config.xml中使用标签进行配置。例如:

@MappedTypes({String.class})
@MappedJdbcTypes({JdbcType.VARCHAR})
public class ExampleTypeHandler extends BaseTypeHandler {...}

那如何确定这个自定义的类型处理器是处理什么类型的呢?

设置类型处理器处理的Java类型:

  • 在类型处理器的配置元素上增加一个javaType属性,如:<typeHandler javaType="String"/>
  • 在类型处理器的类上增加一个@MappedTypes注解,如:@MappedTypes({String.class})。 如果同时配置javaType属性和@MappedTypes注解,则注解方式将被忽略。

设置类型处理器处理的JDBC类型:

  • 在类型处理器的配置元素上增加一个jdbcType属性,如:<typeHandler jdbcType="VARCHAR"/>
  • 在类型处理器的类上增加一个@MappedJdbcTypes注解,如:@MappedJdbcTypes({JdbcType.VARCHAR})。 如果同时配置jdbcType属性和@MappedJdbcTypes注解,则注解方式将被忽略。

最后,还可以通过设置包名让MyBatis自动查找类型处理器,此时只能通过注解方式来指定类型处理器要处理的Java类型和JDBC类型。例如:

<!-- mybatis-config.xml -->
<typeHandlers>
    <package name="com.star.mybatis.handler"/>
</typeHandlers>

在Configuration组件中,组合了一个类型处理器的注册器,用于注册所有的类型处理器。

源码4org.apache.ibatis.session.Configuration

// 用于注册所有的类型处理器
protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(this);

借助Debug可知,Configuration组件中最终注册的类型处理器一共有41个,包括1个自定义的ExampleTypeHandler:

4.3.5 对象工厂

MyBatis每次创建结果映射对象的新实例时,会使用一个对象工厂(ObjectFactory)实例来完成。 默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认构造方法,要么在参数映射存在的时候通过参数构造方法来实例化。

在mybatis-config.xml文件中,对象工厂使用标签进行配置。例如:

<!-- mybatis-config.xml -->
<objectFactory type="com.star.mybatis.factory.ExampleObjectFactory">
    <property name="name" value="test"/>
</objectFactory>

通过创建自定义的对象工厂,可以覆盖对象工厂的默认行为。例如:

public class ExampleObjectFactory extends DefaultObjectFactory {

    // 处理默认构造方法
    @Override
    public <T> T create(Class<T> type) {
        return super.create(type);
    }

    // 处理带参数的构造方法
    @Override
    public <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
        return super.create(type, constructorArgTypes, constructorArgs);
    }

    // 配置ObjectFactory属性
    @Override
    public void setProperties(Properties properties) {

    }

}

在Configuration组件中,组合了一个ObjectFactory对象,用于保存对象工厂,默认实现类为DefaultObjectFactory。

源码5org.apache.ibatis.session.Configuration

protected ObjectFactory objectFactory = new DefaultObjectFactory();

借助Debug可知,Configuration组件最终会将自定义的对象工厂注册到Configuration组件中:

4.3.6 插件

MyBatis允许在SQL语句执行过程中的某一点使用插件进行拦截调用。默认情况下,允许使用插件来拦截的方法包括:

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

自定义一个插件比较简单,只需实现Interceptor接口,并指定了想要拦截的方法:

@Intercepts({@Signature(
        type= Executor.class,
        method = "update",
        args = {MappedStatement.class, Object.class})})
public class ExamplePlugin implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        return invocation.proceed();
    }

}

在mybatis-config.xml文件中,插件使用标签进行配置。例如:

<!-- mybatis-config.xml -->
<plugins>
    <plugin interceptor="com.star.mybatis.plugin.ExamplePlugin">
        <property name="name" value="ExamplePlugin"/>
    </plugin>
</plugins>

在Configuration组件中,组合了一个拦截器链,用于存放自定义的插件。

源码6org.apache.ibatis.session.Configuration

protected final InterceptorChain interceptorChain = new InterceptorChain();

借助Debug可知,Configuration组件最终会注册自定义的插件:

4.3.7 配置环境

MyBatis可以配置成适应多种环境,例如,开发、测试和生产环境有不同的配置。

但要注意,尽管可以配置多个环境,但每个SqlSessionFactory实例只能选择其中一个环境。因此,有几个数据库需要连接,就需要创建几个SqlSessionFactory实例。

在mybatis-config.xml文件中,配置环境使用标签进行配置。例如:

<!-- mybatis-config.xml -->
<environments default="dev">
    <environment id="dev">
        <transactionManager type="JDBC">
            <property name="..." value="..."/>
        </transactionManager>
        <dataSource type="POOLED">
            <property name="driver" value="${driver}"/>
            <property name="url" value="${url}"/>
            <property name="username" value="${username}"/>
            <property name="password" value="${password}"/>
        </dataSource>
    </environment>
    <environment id="test">
        ...
    </environment>
    <environment id="prod">
        ...
    </environment>
    ...
</environments>

如例子所示,标签的default属性要和标签的id属性相匹配,选择出当前生效的环境,生效的环境才会注册到Configuration组件中的环境配置。

  • 事务管理器:

在MyBatis中,有两种事务管理器:JDBC、MANAGED。

(1)JDBC

这种事务管理器直接使用JDBC的提交和回滚设置,它依赖于从数据源得到的连接来管理事务。

(2)MANAGED

这种事务管理器相当于没有事务管理器,因为它从不提交或回滚一个连接,而是全权交给了容器来管理。

  • 数据源:

注意,这里使用${...}的方式,就是取用【4.3.1 属性】中配置的属性。

标签的type属性是指数据源类型,它的取值包括UNPOOLED、POOLED、JNDI。

(1)UNPOOLED

即非“池”类型的数据源,它的实现会在每次请求时打开和关闭连接。相比连接池,它虽然有一点慢,但对于没有性能要求的简单应用程序是一个很好的选择。

UNPOOLED类型的数据源仅需要配置以下5种属性:

  • driver– JDBC驱动的Java类的完全限定名。
  • url– 数据库的JDBC URL地址。
  • username– 登录数据库的用户名。
  • password– 登录数据库的密码。
  • defaultTransactionIsolationLevel– 默认的连接事务隔离级别。

(2)POOLED

即“池”类型的数据源,它的实现会将JDBC连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。这是一种使得并发Web应用快速响应请求的流行处理方式。

除了上述提到的5种属性,还有更多属性用来配置POOLED类型的数据源:

  • poolMaximumActiveConnections– 最大活动连接数,默认值:10。
  • poolMaximumIdleConnections– 最大空闲连接数。
  • poolMaximumCheckoutTime– 池中连接最大被检出时间,超时将被强制返回,默认值:20000毫秒。
  • poolTimeToWait– 获取连接如果超过这个时间,则会给连接池打印状态日志并重新尝试获取一个连接(避免在误配置的情况下一直失败但没有任何提示),默认值:20000毫秒。
  • poolPingQuery– 发送到数据库的心跳查询SQL语句,用来检验连接是否处在正常状态并准备接受请求。默认是NO PING QUERY SET,当数据库驱动失败时,返回一个错误消息。
  • poolPingEnabled– 是否启用心跳查询。若开启,必须使用一个可执行的SQL语句设置poolPingQuery属性(最好是一个非常快的SQL语句),默认值:false。
  • poolPingConnectionsNotUsedFor– 配置poolPingQuery的使用频度。可以设置成数据库连接超时时间,来避免不必要的检测,默认值:0(即所有连接每一时刻都被检测 — 当然仅当poolPingEnabled为true时适用)。

(3)JNDI

这种数据源的实现是为了能在如EJB或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个JNDI上下文的引用。

这种数据源配置只需要2个属性:

  • initial_context– 用来在InitialContext中寻找上下文。这是个可选属性。
  • data_source– 引用数据源实例位置的上下文的路径。提供了initial_context配置时会在其返回的上下文中进行查找,没有提供时则直接在InitialContext中查找。

在Configuration组件中,组合了一个Environment对象,用于存放被选中的环境配置。

源码7org.apache.ibatis.session.Configuration

protected Environment environment;

借助Debug可知,Configuration组件最终会注册一个Environment对象:

4.3.8 映射器

映射器用于配置SQL映射文件的位置,也就是告诉MyBatis到哪里去找SQL映射文件。

在mybatis-config.xml文件中,映射器使用标签进行配置。例如:

    <!--映射文件-->
    <mappers>
        <!--通过指定XML文件的类路径来注册-->
        <mapper resource="mapper/UserMapper.xml"/>
        <!--通过指定XML文件的完全限定资源定位符来注册-->
        <!--<mapper url="file:///C:\workspace\mybatis_demo2\src\main\resources\mapper\UserMapper.xml"/>-->
        <!--通过Mapper接口的类路径来注册-->
        <!--<mapper class="com.star.mybatis.mapper.UserMapper"/>-->
        <!--通过Mapper接口所在包路径类注册-->
        <!--<package name="com.star.mybatis.mapper"/>-->
    </mappers>

如上面的配置所示,配置SQL映射文件的位置有4种方法是:第一是使用XML文件的类路径;第二是使用XML文件的完全限定资源定位符(即包括 file:///的URL);第三是使用Mapper接口的类路径;第四是使用Mapper接口所在包路径。

在Configuration组件中,组合了一个Mapper注册器,用于注册Mapper接口信息:

源码:

protected final MapperRegistry mapperRegistry;

借助Debug可知,Configuration组件最终会注册配置好的映射器:

本节完,更多内容请查阅分类专栏:MyBatis3源码深度解析

  • 29
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

灰色孤星A

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值