Spring5学习笔记(二、IOC工厂)

注入(Injection)

注入:通过Spring工厂及配置文件,为所创建对象的成员变量赋值。

为什么需要注入

通过编码的方式,为成员变量进行赋值,存在耦合。
在这里插入图片描述

如何进行注入[开发步骤]

  1. 为成员变量提供set get方法
  2. 配置spring的配置文件
    <bean id="person" class="com.angenin.basic.Person">
        <property name="id">
            <value>10</value>
        </property>
        <property name="name">
            <value>angenin</value>
        </property>
    </bean>
    
    在这里插入图片描述

注入的好处

解耦,注入后,只需要配置文件即可。

Spring注入的原理分析(简易版)

Spring通过底层调用对象属性对应的set方法,完成成员变量的赋值,这种方式也称之为set注入
在这里插入图片描述

Set注入详解

Set注入:Spring调用Set方法,通过配置文件为成员变量赋值。
针对不同类型的成员变量,< property >标签需要嵌套不同的标签。

在这里插入图片描述

1. JDK内置类型

  1. String+8种基本类型
    <property name="name">
        <value>angenin</value>
    </property>
    
  2. 数组
    <property name="emails">
       	<array>
       		<!--因为是String数组,所以这里使用的是value标签,如果是其他类型的,则用对应的标签-->
           	<value>angein1@qq.com</value>
           	<value>angein2@qq.com</value>
       	</array>
    </property>
    
  3. Set
    <property name="tels">
      	<set>
          	<!--因为是String类型的Set,所以这里使用的是value标签,如果是其他类型的,则用对应的标签-->
           	<value>aaa</value>
            <value>bbb</value>
            <value>ccc</value>
            <!--重复数据插入无效,所以只有三个-->
            <value>ccc</value>
        </set>
    </property>
    
  4. List
    <property name="addresses">
        <list>
        	<!--因为是String类型的List,所以这里使用的是value标签,如果是其他类型的,则用对应的标签-->
            <value>ddd</value>
            <value>eee</value>
            <value>eee</value>
        </list>
    </property>
    
  5. Map
    键值对有对应的标签entry,key有对应的标签key,而value没有,直接根据对应的类型用对应的标签即可。
    <property name="qqs">
        <map>
            <!--entry代表一个键值对-->
            <entry>
                <!--因为这里定义的key是String,所以需要嵌套value标签,如果是其他类型的,需要改成对应的标签-->
                <key><value>k1</value></key>
                <!--如果键值对的value不是String或基本类型的,需要改成对应的标签,如ref-->
                <value>v1</value>
            </entry>
            <!--如下-->
    <!--    <entry>-->
    <!--        <key><ref bean=""/> </key>-->
    <!--        <ref bean=""/>-->
    <!--    </entry>-->
        </map>
    </property>
    
  6. Properties
    是特殊的Map,key和value都是String类型。
    <property name="p">
        <props>
            <prop key="k2">v2</prop>
            <prop key="k3">v3</prop>
        </props>
    </property>
    
  7. 复杂的JDK类型(Date)
    需要定义自定义类型转换器进行处理。

2. 用户自定义类型

  • 第一种方式:

    • 为成员变量提供set get方法
    • 配置文件中进行注入(赋值)
    <bean id="userService" class="com.angenin.basic.UserServiceImpl">
        <property name="userDao">
            <!--因为只使用一次,所以不需要id属性-->
            <bean class="com.angenin.basic.UserDaoImpl"/>
        </property>
    </bean>
    
  • 第二种方式:

    第一种赋值方式存在的问题

    1. 配置文件代码冗余(如果多个bean对象都使用到userDao,那么userDao的匿名bean会被多次书写),
    2. 被注入的对象(userDao),多次创建,浪费(JVM)内存资源。
    • 为成员变量提供set get方法
    • 配置文件中进行配置
    <bean id="userDao" class="com.angenin.basic.UserDaoImpl"/>
    
    <bean id="userService" class="com.angenin.basic.UserServiceImpl">
        <property name="userDao">
            <!--引用bean-->
            <ref bean="userDao"/>
        </property>
    </bean>
    <!--Spring4.x废除的<ref local=""/>基本等效于<ref bean=""/>-->
    

Set注入的简化写法

1. 基于属性简化

JDK类型注入
<property name="name">
    <value>angenin</value>
</property>

简化后
<property name="name" value="angenin"/>
注意:value属性只能简化8种基本类型+String的value标签。
map的entry标签简化
<map>
	<entry>
	    <key><value>k1</value></key>
	    <value>v1</value>
	</entry>
</map>

简化后
<map>
	<entry key="k1" value="v1"/>
	<!--<entry key-ref="" value-ref=""/>-->
</map>
注意:只有单个才能简化,map、set、list都不能简化。
用户自定义类型
<property name="userDao">
    <ref bean="userDao"/>
</property>

简化后
<property name="userDao" ref="userDao"/>

2. 基于p命名空间简化

在bean标签里输入p:,然后把光标放到p:中间,按alt键+回车键,创建p命名空间。
在这里插入图片描述
在这里插入图片描述

<bean id="person" class="com.angenin.basic.Person" p:id="1" p:name="angenin">
    <property name="id" value="1"/>
    <property name="name" value="angenin"/>
</bean>

简化后
<bean id="person" class="com.angenin.basic.Person" p:id="1" p:name="angenin"/>
这里的p代表property
用户自定义类型
<bean id="userService" class="com.angenin.basic.UserServiceImpl">
    <property name="userDao" ref="userDao"/>
</bean>

简化后
<bean id="userService" class="com.angenin.basic.UserServiceImpl" p:userDao-ref="userDao"/>

构造注入

注入:通过Spring的配置文件,为成员变量赋值。
Set注入:Spring调用Set方法,通过配置文件为成员变量赋值。
构造注入:Spring的调用构造方法,通过配置文件为成员变量赋值。

开发步骤

  • 提供有参构造方法
    public class Customer implements Serializable {
        private String name;
        private int age;
    
        public Customer(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        toString();	//这里简写,节约空间
    }
    
  • Spring的配置文件
    <bean id="customer" class="com.angenin.constructor.Customer">
    <!--property标签对应的是set方法,而这里我们并没有定义set方法-->
        <!--constructor-arg标签对应有参构造方法的参数,有参构造方法有几个,就需要几个constructor-arg标签,
        并且需要按照有参构造方法参数的顺序进行赋值-->
        <constructor-arg>
            <value>angenin</value>
        </constructor-arg>
        <constructor-arg>
            <value>100</value>
        </constructor-arg>
    </bean>
    

构造方法重载

  1. 参数个数不同时,通过控制constructor-arg标签的数量进行区分。
    public Customer(String name) {
        this.name = name;
    }
    
    <bean id="customer" class="com.angenin.constructor.Customer">
        <constructor-arg>
            <value>angenin</value>
        </constructor-arg>
    </bean>
    <!--只为一个成员变量赋值,其他成员变量为初始值。-->
    
  2. 构造参数个数相同时:用type属性指定类型进行区分。(参数个数相同,类型相同不能重载)
    public Customer(int age) {
        this.age = age;
    }
    
    <bean id="customer" class="com.angenin.constructor.Customer">
    	<!--在constructor-arg标签里用type属性指定类型-->
        <constructor-arg type="int">
            <value>100</value>
        </constructor-arg>
    </bean>
    

注入总结

未来实战中,应用set注入还是构造注入?
答:set注入更多

  1. 构造注入麻烦,因为有重载。
  2. Spring框架底层,大量应用了set注入。
    在这里插入图片描述

反转控制 与 依赖注入

反转(转移)控制(IOC Inverse of Control)

控制:对于成员变量赋值的控制权。
反转控制:把对于成员变量赋值的控制权,从代码中反转(转移)到Spring工厂和配置文件中完成。好处:解耦。
底层实现:工厂设计模式

在这里插入图片描述

什么是IOC/控制反转?
控制反转就是把对成员变量赋值的控制权转移到Spring工厂的配置文件中完成,好处是解耦,底层由工厂设计模式实现。

依赖注入(Dependency Injection DI)

注入:通过Spring的工厂及配置文件,为对象(bean、组件)的成员变量赋值。
依赖注入:当一个类需要另一个类时,就意味着依赖,一旦出现依赖,就可以把另一个类作为本类的成员变量,最终通过Spring配置文件进行注入(赋值)。
好处:解耦。

在这里插入图片描述

Spring工厂创建复杂对象

在这里插入图片描述

1. FactoryBean接口

在这里插入图片描述
在这里插入图片描述
引入mysql驱动jar包

  <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.18</version>
  </dependency>

开发步骤

实现FactoryBean接口

public class ConnectionFactoryBean implements FactoryBean<Connection> {
    //用于书写创建复杂对象的代码
    @Override
    public Connection getObject() throws Exception {
        Class.forName("com.mysql.cj.jdbc.Driver");
       Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/book?serverTimezone=Asia/Shanghai",
                "root", "123456");
        return conn;
    }

    //返回创建的复杂对象的class对象
    @Override
    public Class<?> getObjectType() {
        return Connection.class;
    }

    //返回true,只需要创建一次
    //返回false,每次调用都需要创建一个新的复杂对象
    @Override
    public boolean isSingleton() {
        return false;
    }
}

Spring配置文件的配置

如果bean标签的class属性中指定的类型是Factory Bean接口的实现类,那么通过id值获得的是这个类所创建的复杂对象Connection,而不是这个实现类的对象。
在这里插入图片描述
在这里插入图片描述

<bean id="conn" class="com.angenin.factorybean.ConnectionFactoryBean"/>

细节

  • 如果就是想获取FactoryBean类型的对象,使用ctx.getBean("&conn"),加上一个&,获取的就是ConnectionFactoryBean对象。
  • isSingleton方法,返回true只会创建一次复杂对象,返回fase每次都会创建新的对象。这里的conn对象因为事务的缘故,所以不能共享,需要返回false,所以返回true还是false需要根据这个对象的特点,再决定返回true(如SqlSessionFactory)还是false(如Connection)。
  • mysql8连接创建时,需要指定SSL证书,并且需要解决时区问题,解决办法:
    url = "jdbc:mysql://localhost:3306/book?useSSL=false&serverTimezone=Asia/Shanghai"
    
  • 依赖注入的体会(DI):把ConnectionFactoryBean中依赖的4个字符串信息,进行配置文件的注入。好处:解耦。
    修改ConnectionFactoryBean
    private String driverClassName;
    private String url;
    private String username;
    private String password;
    
    get() set()...
    
    //用于书写创建复杂对象的代码
    @Override
    public Connection getObject() throws Exception {
        Class.forName(driverClassName);
        Connection conn = DriverManager.getConnection(url, username, password);
        return conn;
    }
    
    修改配置文件中的bean
    <bean id="conn" class="com.angenin.factorybean.ConnectionFactoryBean">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/book?useSSL=false&amp;serverTimezone=Asia/Shanghai"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </bean>
    

FactoryBean的实现原理(简易版)

接口回调

  1. 为什么Spring规定FactoryBean接口让我们去实现,并且需要把创建复杂对象的代码写在getObject()方法中?
  2. ctx.getBean(“conn”) 获得的是复杂对象Connection,而没有获得ConnectionFactoryBean对象呢?

Spring内部运行流程

  1. 通过conn获得 ConnectionFactoryBean类的对象,进而通过instanceof判断出是FactoryBean接口的实现类。
  2. Spring按照规定 getobject() --> Connection
  3. 返回 Connection

在这里插入图片描述
FactoryBean总结:Spring中用于创建复杂对象的一种方式,也是 Spring原生提供的,Spring整合其他框架时,会大量应用FactoryBean。

2. 实例工厂

为什么应用实例工厂?

  1. 避免Spring框架的侵入。
  2. 整合遗留系统。

开发步骤

  • 自定义工厂类
    public class ConnectionFactory {
        public Connection getConnection() {
            Connection conn = null;
            try {
                Class.forName("com.mysql.cj.jdbc.Driver");
                conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/book?useSSL=false&serverTimezone=Asia/Shanghai",
                        "root", "123456");
            } catch (Exception e) {
                e.printStackTrace();
            }
            return conn;
        }
    }
    
  • Spring配置文件的配置
    <bean id="connFactory" class="com.angenin.factorybean.ConnectionFactory"/>
    <!--factory-bean:指定工厂bean,factory-method:指定调用的工厂类的方法-->
    <bean id="conn" factory-bean="connFactory" factory-method="getConnection"/>
    

3. 静态工厂

开发步骤

  • 自定义静态工厂类
    public class StaticConnectionFactory {
        public static Connection getConnection() {
            Connection conn = null;
            try {
                Class.forName("com.mysql.cj.jdbc.Driver");
                conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/book?useSSL=false&serverTimezone=Asia/Shanghai",
                        "root", "123456");
            } catch (Exception e) {
                e.printStackTrace();
            }
            return conn;
        }
    }
    
  • Spring配置文件的配置
    <!--静态工厂不需要创建对象,所以不需要给StaticConnectionFactory创建bean-->
    <bean id="conn" class="com.angenin.factorybean.StaticConnectionFactory" factory-method="getConnection"/>
    

总结

在这里插入图片描述

控制Spring工厂创建对象的次数

1. 控制简单对象的创建次数

<!--使用scope属性来控制对象创建的次数-->
<!--单例(默认singleton)-->
<bean id="account" scope="singleton" class="com.angenin.scope.Account"/>
<!--多例-->
<bean id="account" scope="prototype" class="com.angenin.scope.Account"/>

2. 控制复杂对象的创建次数

通过FactoryBean的实现类中的isSingleton方法的返回值进行控制,返回true创建一次,返回false每次都创建。如果没有isSingleton方法,还是通过scope属性进行对象创建次数的控制。

为什么要控制创建的次数?

好处:节省不必要的内存浪费。

  • 什么样的对象只创建一次?

    1. SqlSessionFactory
    2. DAO
    3. Service
    4. 线程安全的
  • 什么样的对象每一次都要创建新的?

    1. Connection
    2. SqlSession / Session
    3. 线程不安全的

对象的生命周期

指的是一个对象创建、存活、消亡的一个完整过程。使用Spring后,由Spring负责的创建、存活、销毁,了解生命周期,有利于我们使用好Spring为我们创建的对象。

生命周期的3个阶段

1. 创建阶段

Spring工厂何时创建对象。

  • scope=“singleton”
    Spring工厂创建的同时创建对象。
    如果想获取对象时再创建对象,只需要在bean标签里加上lazy-init="true",启用懒加载机制。

    <bean id="product" class="com.angenin.life.Product" lazy-init="true"/>
    
  • scope=“prototype”
    Spring工厂会在获取对象的同时创建对象。(ctx.getBean(""))

2. 初始化阶段

Spring工厂在创建完对象后,调用对象的初始化方法,完成对应的初始化操作。

  1. 初始化方法提供:程序员根据需求,提供初始化方法,最终完成初始化操作。
  2. 初始化方法调用:Spring工厂进行调用。

定义初始化方法的两种途径:

  1. 实现InitializingBean接口,通过afterPropertiesSet方法进行初始化操作

    public class Product implements InitializingBean {
        //初始化方法,做一些初始化操作,由Spring调用
        @Override
        public void afterPropertiesSet() throws Exception {
            //初始化操作
        }
        ...
    }
    
  2. 对象中提供一个普通的方法,然后在配置文件的bean标签中使用init-method属性指定初始化方法。

    public class Product {
    	public void myInit(){...}
    	...
    }
    
    <bean id="product" class="com.angenin.life.Product" init-method="myInit"/>
    
细节分析
  1. 如果一个对象即实现了InitializingBean接口同时又指定了普通的初始化方法,那么会先执行afterPropertiesSet方法,再执行自定义初始化方法。
  2. 注入一定是发生在初始化操作的前面,会先set(或构造)注入后,再执行afterPropertiesSet方法、自定义初始化方法。
    在这里插入图片描述
  3. 什么叫初始化操作,资源的初始化(如数据库、IO、网络),初始化操作用得比较少,了解即可。

3. 销毁阶段

Spring销毁对象前,会调用对象的销毁方法,完成销毁操作。

Spring什么时候销毁所创建的对象?
工厂关闭的时候(ctx.close();)。

  1. 销毁方法提供:程序员根据需求,定义销毁方法,完成销毁操作。
  2. 销毁方法调用:Spring工厂进行调用。

定义销毁方法的两种途径:

  1. 实现DisposableBean接口,通过destroy方法进行销毁操作
    public class Product implements InitializingBean, DisposableBean {
        //初始化方法,做一些初始化操作,由Spring调用
        @Override
        public void afterPropertiesSet() throws Exception {
    	    //初始化操作
        }
        //销毁方法:销毁操作(资源释放的操作)
        @Override
        public void destroy() throws Exception {
            //销毁操作
        }
        ...
    }
    
  2. 对象中提供一个普通的方法,然后在配置文件的bean标签中使用destroy-method属性指定销毁方法。
    public void myDestroy() throws Exception {
        //销毁操作
    }
    
    <bean id="product" class="com.angenin.life.Product" init-method="myInit" destroy-method="myDestroy">
    
    和初始化方法的顺序一样,都是先执行Spring的销毁方法后,再执行自定义的销毁方法。
细节分析
  1. 销毁方法的操作只适用于scope=“singleton”。因为Spring对于多例,只负责创建,不负责销毁,只有单例的对象,Spring才负责创建和销毁。
  2. 什么叫销毁操作,资源的释放(如数据库、IO、网络),同样,销毁操作用得比较少,了解即可。

总结

在这里插入图片描述

配置文件参数化

把Spring配置文件中需要经常修改的字符串信息,转移到一个更小的配置文件中。

  1. Spring配置文件中存在需要经常修改的字符串吗?
    答案:存在,以 数据库连接相关的参数 为代表。
  2. 经常变化的字符串,在Spring的配置文件中,直接去修改的话,容易出错,也不利于项目的维护(修改)。
  3. 转移到一个小的配置文件(xxx.properties),利于维护(修改)。

配置文件参数化:利于Spring配置文件的维护(修改)。

开发步骤

  • 提供一个小的配置文件(.properties)

    对于这个配置文件的名字和位置都没要求。

    db.properties

    #格式:key=value
    jdbc.driverClassName=com.mysql.cj.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/book?useSSL=false&serverTimezone=Asia/Shanghai
    jdbc.username=root
    jdbc.password=123456
    
  • Spring配置文件和小配置文件进行整合
    在这里插入图片描述
    输入<context:后会自动引入context命名空间,专门用于整合配置文件。

    <!--指定配置文件的位置-->
    <context:property-placeholder location="classpath:/db.properties"/>
    
  • 在Spring配置文件中通过${key}获取小配置文件中对应的值。

    <bean id="conn" class="com.angenin.factorybean.ConnectionFactoryBean">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
    

自定义类型转换器

类型转换器

作用:Spring通过类型转换器把配置文件中字符串类型的数据转换成了对象中成员变量对应类型的数,进而完成了注入。

在这里插入图片描述

自定义类型转换器

原因:当Spring内部没有提供特定类型转换器时,而程序员在应用的过程中还需要使用,那么就需要程序员自己定义类型转换器。

开发步骤

  • 实现Converter接口
    //Converter接口的引用不要导错了
    import org.springframework.core.convert.converter.Converter;
    
    //Converter接口的两个泛型:第一个为原数据类型,第二个为转换后的类型
    public class MyDateConverter implements Converter<String, Date> {
    
        //convert方法作用:String --> Date
        //参数s:配置文件中value的日期字符串
        //返回值:当把转换好的Date作为convert方法的返回值后,Spring自动的为birthday属性进行注入(赋值)
        @Override
        public Date convert(String s) {
            SimpleDateFormat sdf = new SimpleDateFormat("yyy-MM-dd");
            Date date = null;
            try {
                date = sdf.parse(s);
            } catch (ParseException e) {
                e.printStackTrace();
            }
            return date;
        }
    }
    
  • 在Spring的配置文件中进行配置
    <!--创建MyDateConverter对象-->
    <bean id="myDateConverter" class="com.angenin.converter.MyDateConverter"/>
    
    <!--类型转换器的注册,告知Spring框架,MyDateConverter是一个类型转换器-->
    <!--  ConversionServiceFactoryBean:用于类型转换器的注册  -->
    <!--id必须是conversionService,否则会报错-->
    <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
        <!--converters为set类型-->
        <property name="converters">
            <set>
                <ref bean="myDateConverter"/>
            </set>
        </property>
    </bean>
    

细节

  • MyDateConverter中的日期格式,通过依赖注入的方式,由配置文件完成赋值。
    public class MyDateConverter implements Converter<String, Date> {
        private String pattern;
    
        //get() set()
        
        @Override
        public Date convert(String s) {
            SimpleDateFormat sdf = new SimpleDateFormat(pattern);
            ...
        }
    }
    
    <!--如果有需要,也可以放到小配置文件里-->
    <bean id="myDateConverter" class="com.angenin.converter.MyDateConverter">
        <property name="pattern" value="yyyy-MM-dd"/>
    </bean>
    
  • ConversionServiceFactoryBean定义的id属性必须是conversionService,大小写都要一致。
  • Spring框架内置日期类型的转换器,日期格式要求以/分隔,如2020/10/19。

后置处理Bean

BeanPostProcessor作用:对Spring工厂所创建的对象,进行再加工。
AOP底层实现。

工厂创建对象过程图
在这里插入图片描述
BeanPostProcessor后置处理bean的运行原理分析
在这里插入图片描述
先完成注入,然后调用BeanPostProcessor的before方法,再调用初始化方法,自定义初始化方法,最后调用BeanPostProcessor的after方法。

程序员实现BeanPostProcessor接口,使用接口里的方法:

  1. Object postProcessBeforeInitiallization(Object bean, String beanName)
    作用:Spring创建完对象,并进行注入后,可以运行 Before方法进行加工。
    获得Spring创建好的对象:通过方法的参数。
    最终通过返回值把bean对象交还给Spring框架。
  2. Object postProcessAfterInitiallization(object bean, String beanName)
    作用: Spring执行完对象的初始化操作后,可以运行After方法进行加工。
    获得Spring创建好的对象:通过方法的参数。
    最终通过返回值把bean对象交还给Spring框架。

实战中很少处理Spring的初始化操作,所以没有必要区分Before、After,只需要使用其中一个即可。
即使postProcessBeforeInitiallization方法不做任何操作,也需要把bean对象返回。

BeanPostProcessor的开发步骤

  • 实现BeanPostProcessor接口
    public class MyBeanPostProcessor implements BeanPostProcessor {
    
        //注入后,调用初始化方法前调用
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            //before不操作也需要把bean返回
            return bean;
        }
    
        //初始化方法调用后调用
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    		//把id修改为1
            Category category = (Category) bean;
            category.setId(1);
    
            return bean;
        }
    }
    
  • Spring的配置文件中进行配置
    <bean id="c" class="com.angenin.beanpost.Category">
        <property name="id" value="100"/>
        <property name="name" value="angenin"/>
    </bean>
    
    <bean id="myBeanPostProcessor" class="com.angenin.beanpost.MyBeanPostProcessor"/>
    

BeanPostProcessor细节

BeanPostProcessor会对Spring工厂中所有创建的对象进行加工。

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
       	//因为对所有的对象都生效,所以需要先判断
        if (bean instanceof Category) {
            Category category = (Category) bean;
            category.setId(1);
        }

        return bean;
    }

下一篇:Spring5学习笔记(三、AOP开发)

学习视频(p25-p62):https://www.bilibili.com/video/BV185411477k?p=25

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值