MyBatis的简介和核心的组件(映射器、执行器、SqlSession及其工厂)

Java知识点总结:想看的可以从这里进入

1、简介


1.1、原始JDBC

在以前我们都是使用原始的JDBC,但是这种连接会造成创建、释放频繁造成系统资源的浪费从而影响性能,当sql变化时需要去改变Java代码,不符合开闭的原则,在查询时要手动将结果集封装到实体,插入时手动的提取实体到占位符,增加了代码量。

  • 导入相关的 jar 包 或者依赖;
  • 加载驱动,获取Connection
  • 准备SQL语句,获取执行sql语句的Statement对象;
  • 通过Statement对象执行SQL,返回结果到ResultSet对象;
  • 使用ResultSet读取数据,然后通过代码转化为具体的对象;
  • 释放资源
public List<User> selectAllUser(){
 	//原始的JDBC手动创建连接
    Connection conn = null;
    PreparedStatement ps = null;
    ResultSet rs = null;
    try {
        Class.forName("com.mysql.cj.jdbc.Driver");
        String url = "jdbc:mysql://localhost:3306/库名?useUnicode=true&amp&characterEncoding=utf-8&serverTimezone=Asia/Shanghai";
        String root = "root";
        String password = "密码";
        conn = DriverManager.getConnection(url,root,password);
        ps = conn.prepareStatement("select * from `user`");
        rs = ps.executeQuery();
        List<User> list = new ArrayList<>();
        while (rs.next()){
            //获取数据
            User user = new User();
            user.setId(rs.getInt("id"));
            user.setUsername(rs.getString("username"));
            user.setPassword(rs.getString("password"));
            //添加到集合中
            list.add(user);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        try {
            conn.close();
            ps.close();
            rs.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }   
}

1.2、SpringJDBC

SpringJDBC是对原始JDBC做了封装,它将原始的JDBC的步骤做了封装,我们只需要完成相关配置,就可以直接连接数据库,在操作中只需要关注JdbcTemplate 对象即可。

<!--导入数据文件database.properties -->
<context:property-placeholder location="classpath:database.properties"/>
<!--配置德鲁伊数据库连接池 -->
<!-- 配置druid 连接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
    <property name="driverClassName" value="${jdbc.driver}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
    <!--初始连接数-->
    <property name="initialSize" value="${jdbc.initialSize}"/>
    <!--最小空闲连接数-->
    <property name="minIdle" value="${jdbc.minIdle}"/>
    <!--最大活动连接数-->
    <property name="maxActive" value="${jdbc.maxActive}"/>
    <!--最大等待时间-->
    <property name="maxWait" value="${jdbc.maxWait}"/>
</bean>
<!--jdbcTemplate连接数据库-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource"/>
</bean>
@Resource
private JdbcTemplate jdbcTemplate;
@Test
public void jdbcTest(){
    String sql = "select * from `user`";
    List<User> users = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
    users.forEach(System.out::println);
}

1.3、MyBatis

原始的JDBC操作繁琐,每次执行SQL时每次都需要获取:Connection、PreparedStatement、ResultSet 这些数据库资源,然后就需要大量的try、catch、finally语句捕捉异常、关闭数据库资源。而SpringJDBC虽然对JDBC做了封装,大大的简化了操作流程,但是它的操作还是直接在业务层面的,这样会导致耦合性较强,且有sql注入的危险。所以就产生了第三方的框架,比如MyBatis。官网

MyBatis 是 apache的一个开源项目iBatis,2010年6月这个项目由Apache Software Foundation迁移到了Google Code后,正式更名为MyBatis。代码于2013年11月迁移到Github。

它是一种半自动的对象关系映射(ORM)框架,它在Java对象和数据库的表之间建立映射关系(类对应表,属性对应字段),内部封装了JDBC相关操作,所以不用再去关注注册驱动、获取连接、执行 SQL、释放资源等操作,极大简化了持久层开发工作。其中SQL语句需要我们自己编写,虽然相对于自动的映射框架来说,增加了不少代码量,但是它有较高的灵活性,可以根据需要,自由地对SQL进行定制。

什么是ORM?即Object Relation Mapping,对象关系映射。对象指的是Java对象,关系指的是数据库中的关系模型,对象关系映射,指的就是在Java对象和数据库的关系模型之间建立一种对应关系,就是数据库的表对应Java类,数据库字段对应Java属性,数据的一条数据对应Java一个对象。

全自动的ORM不需要编写SQL语句,只需要定义好映射关系,就可以直接进行CRUD操作了。而半自动的ORM,需要手动编写SQL语句。虽然这种半自动的ORM在操作上会麻烦很多,但是相对的它也更加灵活,可以根据需要,自由地对SQL进行定制。

MyBatis 是通过 XML 或注解配置Statement对象,并通过Java对象和 Statement中的SQL进行映射生成最终执行的SQL语句,最后由MyBatis框架执行SQL并将结果映射成Java对象并返回。

在MyBatis 中有三个基本要素:

  • 核心组件(核心的类和接口)
  • MyBatis核心配置文件(mybatis-config.xml)
  • SQL映射器(mapper.xml)

MyBatis还有一种增强的工具版本 Mybatis-Plus,它是在Mybatis的基础上做了一些增强,在不改变MyBatis的基础上,简化了开发。

Mybatis的使用过程:

  • 添加mybatis的依赖

    <!--mysql8依赖-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.25</version>
    </dependency>
    <!--mybatis依赖 -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.7</version>
    </dependency>
    <!-- junit的依赖,测试 -->
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-api</artifactId>
      <version>5.9.1</version>
      <scope>test</scope>
    </dependency>
    <!-- log4j 日志-->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.20.0</version>
    </dependency>
    
  • MyBatis 默认使用 log4j 输出日志信息,所以可以配置日志文件 log4j.properties

    ### 设置日志级别和输出位置 ###
    log4j.rootLogger=DEBUG,console,logFile
    log4j.additivity.org.apache=true
    
    # 输出到控制台(console)
    log4j.appender.console=org.apache.log4j.ConsoleAppender
    # 输出级别最低为DEBUG
    log4j.appender.console.Threshold=DEBUG		
    # 表示所有消息都会被立即输出
    log4j.appender.console.ImmediateFlush=true
    #输出日志到控制台的方式
    log4j.appender.console.Target= System.out
    # 灵活布局输出日志
    log4j.appender.console.layout=org.apache.log4j.PatternLayout
    #设定以怎样的格式显示消息。
    log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%t] [%c] [%p] -%m%n
    
  • 创建mybatis-config.xml配置文件

  • 编写,加载映射文件mapper.xml

  • 加载配置文件,创建SqlSessionFactory工厂,获取SqlSession

  • 调用mapper映射文件中的SQL语句来执行CRUD操作

  • 返回结果集的映射

2、核心组件


2.1、组件介绍

mybatis的核心组件主要有4部分

  • SqlSessionFactoryBuilder(构造器):它的作用仅仅用来生成 SqlSessionFactory,创建成功后就失去作用,所以用在生成 SqlSessionFactory 的方法中作为局部变量使用,用完即可回收。

    它通过内部的build(InputStream inputStream, String environment, Properties properties) 创建SqlSessionFactory,使用流读取配置文件。

  • SqlSessionFactory:相当于数据库的连接池,主要是用 openSession() 创建SqlSession接口,所以需要存在MyBatis的整个应用中。一般是创建一个单例模式,生命周期等同MyBatis的生命周期

  • SqlSession:相当于Connection对象,用于业务请求,用完关闭。存在于一次业务请求中,处理完请求后使用close方法关闭。

  • SQL Mapper(映射器):Mapper是通过SqlSession获取的,所以Mapper的生命周期小于SqlSession,它相当于一个请求中的业务处理。

2.2、SqlSessionFactory

SqlSessionFactory是 Mybatis的核心组件,每个MyBatis都是以一个SqlSessionFactory实例为中心,而它的作用就是生产SqlSession。

使用MyBatis需要配置 SqlSessionFactory 对象,通常使用 SqlSessionFactoryBuilder 的 build() 方法,SqlSessionFactory作为一个工厂接口,唯一的作用就是用来创建SqlSession的,所以我们一般会用单例模式来处理它。而一般选用通过XML配置的方式来生成 SqlSessionFactory。

  • 创建database.properties文件

    jdbc.driver=com.mysql.cj.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/库名?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    jdbc.username=root
    jdbc.password=密码
    
  • 创建一个映射器文件

    <?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接口 -->
    <mapper namespace="com.yu.mybatis.mapper.UserMapper">
          <!--数据库字段和Java属性之间的映射关系 -->
          <resultMap id="BaseResultMap" type="com.yu.mybatis.entity.User">
                <id column="user_id" jdbcType="INTEGER" property="userId" />
                <result column="username" jdbcType="VARCHAR" property="username" />
                <result column="password" jdbcType="VARCHAR" property="password" />
                <result column="deleted" jdbcType="BIT" property="deleted" />
          </resultMap>
        <!--数据库的字段名字 -->
          <sql id="Base_Column_List">
            	user_id, username, `password`, deleted
          </sql>
        <!--根据id获取数据 -->
          <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
                select 
                <include refid="Base_Column_List" />
                from `user`
                where user_id = #{userId,jdbcType=INTEGER}
          </select>
    </mapper>
    
  • 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>
    	<!--指定外部 properties 文件-->
        <properties resource="database.properties"/>
    
        <!--配置数据库连接,可以配置多种环境,默认使用的环境(比如:default="development") -->
        <environments default="development">
            <environment id="development">
                <!--事务管理器的配置(比如:type="JDBC")。
                  JDBC – 这个配置直接使用了 JDBC 的提交和回滚设施,它依赖从数据源获得的连接来管理事务作用域。
                  MANAGED – 这个配置几乎没做什么。它从不提交或回滚一个连接,而是让容器来管理事务的整个生命周期
                -->
                <transactionManager type="JDBC"/>
                <dataSource type="POOLED">
                    <property name="driver" value="${jdbc.driver}"/>
                    <property name="url" value="${jdbc.url}"/>
                    <property name="username" value="${jdbc.username}"/>
                    <property name="password" value="${jdbc.password}"/>
                </dataSource>
            </environment>
        </environments>
    
        <!--指定映射器XML文件的路径-->
        <mappers>
            <mapper resource="mapping/UserMapper.xml"/>
        </mappers>
    </configuration>
    
  • 创建一个帮助类,读取 xml 配置文件,生成SqlSessionFactory

    public class SqlSessionUtil {
        private static SqlSessionFactory factory = null;
        static {
            try {
                InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
                factory = new SqlSessionFactoryBuilder().build(in);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        public static SqlSessionFactory getFactory(){
            return factory;
        }
    }
    

2.3、SqlSession

SqlSession是MyBatis的核心接口,其有两个实现类:其中DefaultSqlSession是单线程,SqlSessionManager是多线程。

它就相当于Connection对象,代表一个连接数据库的资源,每次需要获取和关闭。它提供了面向数据库执行 SQL 命令所需的所有方法,可以通过 SqlSession 实例直接运行已映射的 SQL 语句。

//SqlSession的获取,
//通过SqlSession对象所操作的sql都必须手动提交或回滚事务
SqlSession sqlSession = SqlSessionUtil.getFactory().openSession();
//此时通过SqlSession对象所操作的sql都会自动提交
SqlSession sqlSession = SqlSessionUtil.getFactory().openSession(true);

SqlSession执行SQL的两种方式

  • 直接发送SQL给数据库,利用XML映射器

    String statement=“XML映射文件的namespace属性值(Mapper接口全限定名) + XML映射文件方法的id名(mapper接口的方法名)”
    Object parameter:插入所需的参数,通常是对象或者Map
    RowBounds rowBounds:用于分页,它的两个属性: offset指查询的当前页数; limit指当前页显示多少条数据。
    String mapKey:执查询结果会被封装成一个Map集合返回,key就是参数mapKey传入的列名,value 是封装的对象。
    ResultHandler handler:ResultHandler对象用来处理查询返回的复杂结果集,通常用于多表查询。

    public interface SqlSession extends Closeable {
    
      /**查询方法,查询结果只有一条数据时才使用。*/
      <T> T selectOne(String statement);
      <T> T selectOne(String statement, Object parameter);
    
      /**查询方法,返回执行SQL话句查询结果的泛型对象的集合。。*/
      <E> List<E> selectList(String statement);
      <E> List<E> selectList(String statement, Object parameter);
      <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds);
    
      /**查询方法,根据结果对象中的一个属性将结果列表转换为 Map */
      <K, V> Map<K, V> selectMap(String statement, String mapKey);
      <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey);
      <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds);
    
      /**Cursor 提供与 List 相同的结果,只是它使用 Iterator 懒惰地获取数据。*/
      <T> Cursor<T> selectCursor(String statement);
      <T> Cursor<T> selectCursor(String statement, Object parameter);
      <T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds);
    
      /**使用ResultHandler检索从语句键和参数映射的单行。 */
      void select(String statement, Object parameter, ResultHandler handler);
      void select(String statement, ResultHandler handler);
      void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler);
    
      /**插入方法,返回执行SQL语句所影响的行数。。*/
      int insert(String statement);
      int insert(String statement, Object parameter);
    
      /**执行更新语句。将返回受影响的行数 */
      int update(String statement);
      int update(String statement, Object parameter);
    
      /**执行删除语句。将返回受影响的行数。*/
      int delete(String statement);
      int delete(String statement, Object parameter);
    
      /**提交事务。 */
      void commit();
      void commit(boolean force);
    
      /**回滚事务 */
      void rollback();
      void rollback(boolean force);
    
      /**刷新批处理语句*/
      List<BatchResult> flushStatements();
    
      /**关闭会话 */
      @Override
      void close();
    
      /**清除本地会话缓存。*/
      void clearCache();
    
      /**检索当前配置。 */
      Configuration getConfiguration();
    
      /**检索映射器*/
      <T> T getMapper(Class<T> type);
    
      /**检索内部数据库连接 */
      Connection getConnection();
    }
    
    

    image-20230304142316789

  • 获取Mapper接口,操作SQL(建议使用)

    sqlSession.getMapper(***Mapper.class);	//创建的Mapper接口
    

    在使用Mybatis时,会根据映射文件创建相应的mapper接口,可以用接口的方法

    public interface UserMapper {
        User selectByPrimaryKey(Integer userId);
    }
    
    <!--对应mapper接口-->
    <mapper namespace="com.yu.mybatis.mapper.UserMapper">
        <!-- id对应接口内方法名-->
      <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
        <!--@mbg.generated-->
        select 
        <include refid="Base_Column_List" />
        from `user`
        where user_id = #{userId,jdbcType=INTEGER}
      </select>
    </mapper>
    

    image-20230304142719906

2.4、映射器

MyBatis中最重要也是最复杂的组件,MyBatis是由一个Mapper接口,和一个XML映射文件(注解),对应以前的dao接口和一个dao实现类,它主要的作用是:

  • 描述映射规则
  • 提供SQL语句
  • 配置缓存
  • 提供动态SQL
2.4.1 接口和XML映射

在MyBatis中数据的一张表,分别对应一个实体类,一个Mapper接口、和一个XML的映射文件。

在IDEA中可以引入插件MyBatisCodeHelperPro,使用插件辅助创建所需的类。

image-20210716120801177 image-20210716120411230
  • 实体类:根据数据表对应的实体类会自动创建好,并生成get和det方法。

    image-20220918163654592
  • mapper:接口,编写查询所有的方法,如果映射的路径正确,点击创建,会自动在xml文件中创建好

    image-20220918163038319
  • mapping:mapper的实现类,xml形式配置。

    它的映射规则是:

    先通过 mapper namespace=“com.yu.mapper.UserMapper” 映射到对应的接口

    然后通过select、update、delete、inser这些标签的 id 属性值映射得到接口对应的方法上

    再通过 resultMap、parameterType这两个属性可以将获得的结果集,映射到对应的实体类上进行自动装配。

    <?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是相应的Mapper接口的全限定名-->
    <mapper namespace="com.yu.mapper.UserMapper">
    
        <!--数据库的表和是实体类的映射关系,可以自己配置属性是另外一个实体的情况 -->
        <resultMap id="BaseResultMap" type="com.yu.pojo.User">
            <id column="user_id" jdbcType="INTEGER" property="userId" />
            <result column="username" jdbcType="VARCHAR" property="username" />
            <result column="password" jdbcType="VARCHAR" property="password" />
            <result column="deleted" jdbcType="BIT" property="deleted" />
        </resultMap>
    
        <!--数据表的字段,在本XML中可直接引用-->
        <sql id="Base_Column_List">
            user_id, username, `password`, deleted
        </sql>
    
        <!--和Mapper接口中的方法对应,select为查询属性, id为方法名, resultType为返回值-->
        <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
            <!--@mbg.generated-->
            select 
            <include refid="Base_Column_List" />
            from `user`
            where user_id = #{userId,jdbcType=INTEGER}
        </select>
    
        <!--    插入-->
        <insert id="insert" keyColumn="user_id" keyProperty="userId" parameterType="com.yu.pojo.User" useGeneratedKeys="true">
            <!--@mbg.generated-->
            insert into `user` (username, `password`, deleted)
            values (#{username,jdbcType=VARCHAR}, #{password,jdbcType=VARCHAR}, #{deleted,jdbcType=BIT})
        </insert>
    
        <!--删除-->
        <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer">
            delete from `user`
            where user_id = #{userId,jdbcType=INTEGER}
        </delete>
    
        <!--修改-->
        <update id="updateByPrimaryKeySelective" parameterType="com.yu.pojo.User">
            update `user`
            <set>
                <if test="username != null">
                    username = #{username,jdbcType=VARCHAR},
                </if>
                <if test="password != null">
                    `password` = #{password,jdbcType=VARCHAR},
                </if>
                <if test="deleted != null">
                    deleted = #{deleted,jdbcType=BIT},
                </if>
            </set>
            where user_id = #{userId,jdbcType=INTEGER}
        </update>
    </mapper>
    
2.4.2 发送SQL

通过SqlSession可以获取Mapper接口,它对比SqlSession发送的SQL语句,有很高的可读性,因为SqlSession需要使用自己的方法通过SQL的id属性去匹配相应的SQL,而Mapper则是直接使用自己定义的方法。

public class SqlSessionTest {
    @Test
    public void test(){
        SqlSession sqlSession = SqlSessionUtil.getFactoryBean().openSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User user = mapper.selectByPrimaryKey(1);
        sqlSession.commit();
        System.out.println(user.getUsername());

    }
}

配置的日志打印的信息:

image-20220918163428829

查到了数据

image-20220918163507889

2.4.3、注解的形式

映射器中也有相应的注解来代替XML映射文件,来实现SQL语句,如果和XML同时定义,XML会覆盖掉注解.

public interface UserMapper {
    @Select("select user_id, username, `password`, deleted from `user` where user_id =#{userId}")
    User selectByPrimaryKey(Integer userId);

}

上面的这条注解语句可以代替下面这个:

<select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    <!--@mbg.generated-->
    select 
    <include refid="Base_Column_List" />
    from `user`
    where user_id = #{userId,jdbcType=INTEGER}
</select>

虽然注解的形式要简单很多,但是很多的 sql语句的复杂性是很高的,这种复杂的sql语句放在XML文件中更适合,而且他还可能牵扯到一些动态 SQL等问题,所以还是推荐使用XML的方式。

2.5、执行器

MyBatis整体操作无非就是:连接数据库、编译生成SQL语句、执行SQL、获取数据进行封装等几个步骤,其中执行器 Executor就是用来执行SQL语句的,存在于SqlSession中。

image-20220918170024484

MyBatis有三种基本的执行器:

  1. 简单执行器SimpleExecutor:每次执行update或select都会开启一个statement对象,用完立刻关闭statement对象
  2. 可重用执行器ReuseExecutor:执行update或select前,以sql作为key查找Statement对象,存在就使用不存在就创建,用完后不关闭,放置在Map中供下一次使用。(重复使用)
  3. 批量执行器BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
        executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
        executor = new ReuseExecutor(this, transaction);
    } else {
        executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
        executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

2.6、mybatis流程

image-20220717165229940
  1. 读取MyBatis的核心配置文件:读取配置数据库的连接、属性、类别名、映射器等信息。
  2. 加载映射文件:Mapping.xml映射文件,用来配置操作数据库的SQL语句,可以通过xml和注解获取
  3. 构造会话工厂获取SqlSessionFactory:通过SqlSessionFactoryBuilder对象构建
  4. 创建会话对象SqlSession:包含了执行SQL的一些方法
  5. Executor执行器:mybatis的核心,用来SQL语句的生成和查询缓存的维护
  6. Statement对象:对SQL语句的封装,一个对象对应XML文件中的一个sql语句标签。
  7. 输入参数映射:传递参数,进行预编译处理
  8. 封装结果集:将SQL语句产生的数据进行封装
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

辰 羽

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

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

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

打赏作者

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

抵扣说明:

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

余额充值