Spring 02 控制反转

简介

IOC

IOC(Inversion of Control),即控制反转。

这不是一项技术,而是一种思想。

其根本就是对象创建的控制权由使用它的对象转变为第三方的容器,即控制权的反转。

DI

DI(Dependency Injection),即依赖注入,IOC 的实现方式。

所谓依赖注入,就是由 IOC 容器在运行期间,动态地将某种依赖关系注入到对象之中。

优点

对象之间的解耦

对象之间不会相互影响,减少程序出错的可能性,提高了代码的灵活性和可维护性。

缺点

生成对象的步骤变得更为复杂

会增加团队成员学习和认识的培训成本。

运行效率有一定的损耗

由于IOC容器生成对象是通过反射方式,相较于传统的创建对象方式效率是要低一些的。

需要大量的配置工作

由于生成对象交给了第三方容器,对第三方容器的配置是较为繁琐的。

体验

创建一个普通的 Maven 项目,引入 spring-context 依赖。

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
    </dependency>
</dependencies>

在 resources 目录下创建一个 spring 的配置文件。

官方推荐命名为 applicationContext.xml

一定要先添加依赖,后创建配置文件,否则创建配置文件时,没有模板选项。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
                           http://www.springframework.org/schema/beans/spring-beans.xsd">

</beans>

配置需要注册到 Spring 容器的 Bean。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean class="org.javaboy.Book" id="book"/>
</beans>

class 表示需要注册的 bean 的全路径。

id 表示 bean 的唯一标记。

也可以用 name 属性作为 bean 的标记,它们之间有一定的区别,后面会介绍。

加载配置文件

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");

从容器中去获取对象

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
Book book = ctx.getBean("book", Book.class);

ClassPathXmlApplicationContext 是去 classpath 下查找配置文件的加载方式。

也可以使用 FileSystemXmlApplicationContext ,它会从操作系统路径下去寻找配置文件。

FileSystemXmlApplicationContext ctx = new FileSystemXmlApplicationContext("D:\\Work\\Project\\training\\src\\main\\resources");
Book book = (Book) ctx.getBean("book");

这种方式要写较长的系统路径,且如果项目路径变更就需要修改,不推荐使用。

通过 Class 获取 Bean

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
Book book = ctx.getBean(Book.class);

这种方式有一个很大的弊端:如果存在多个实例,这种方式就不可用。

例如,xml 文件中存在两个同 class 的 Bean。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean class="org.javaboy.Book" id="book"/>
    <bean class="org.javaboy.Book" id="book2"/>
</beans>

此时,如果通过 Class 去查找 Bean,会报如下错误:

3-2-1.png

所以,一般建议使用 name 或者 id 去获取 Bean 的实例。

属性注入

构造方法

通过 Bean 的构造方法给 Bean 的属性注入值

给 Bean 添加对应的构造方法:

public class Book implements Serializable {

    private static final long serialVersionUID = 5492270562431552420L;

    private Integer id;

    private String name;

    private Double price;

    public Book() {
    }

    public Book(Integer id, String name, Double price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }
}

在 xml 文件中注入 Bean

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--构造方法注入:不能清晰看到定义的哪个属性,不推荐使用-->
    <bean id="book" class="cn.sail.training.spring.ioc.Book">
        <constructor-arg index="0" value="1"/>
        <constructor-arg index="1" value="红楼梦"/>
        <constructor-arg index="2" value="33"/>
    </bean>
    
</beans>

constructor-arg 中的 index 和 Book 中的构造方法参数一一对应。

写的顺序可以颠倒,但是 index 的值和 value 要一一对应。

另一种构造方法中的属性注入,则是通过直接指定参数名来注入

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--构造方法注入:参数名指定,可以较直观的看到定义的属性,推荐使用-->
    <bean id="book1" class="cn.sail.training.spring.ioc.Book">
        <constructor-arg name="id" value="2"/>
        <constructor-arg name="name" value="西游记"/>
        <constructor-arg name="price" value="40"/>
    </bean>
    
</beans>

如果有多个构造方法,则会根据给出参数个数以及参数类型,自动匹配到对应的构造方法上,进而初始化一个对象。

set 方法

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--set方法注入:可以较直观的看到定义的属性,推荐使用-->
    <bean id="user" class="cn.sail.training.spring.ioc.User">
        <property name="id" value="1"/>
        <property name="age" value="27"/>
        <property name="name" value="哈哈"/>
    </bean>
    
</beans>

set 方法注入,不是根据属性名对应的值,而是根据 get/set 方法分析出来的属性名。

如果改变set方法的名称,就算属性名不变,也会找不到值而报错。

p 名称空间

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="book2" class="cn.sail.training.spring.ioc.Book" p:id="2" p:name="西游记" p:price="33"/>

</beans>

这种方式本质上也是调用了set方法,但层次结构不清晰,且需要额外引进标签值,不推荐使用。

静态工厂

提供一个 OkHttpClient 的静态工厂

import okhttp3.OkHttpClient;

public class OkHttpUtils {

    private static OkHttpClient okHttpClient;

    public static OkHttpClient getInstance() {
        if (okHttpClient == null) {
            okHttpClient = new OkHttpClient.Builder().build();
        }
        return okHttpClient;
    }

}

在 xml 文件中配置该静态工厂

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--静态工厂注入-->
    <bean id="okHttpUtils" class="cn.sail.training.spring.ioc.OkHttpUtils" factory-method="getInstance"/>

</beans>

这个配置表示 OkHttpUtils 类中的 getInstance 是我们需要的实例,实例的名字就叫 okHttpClient。

然后,在 Java 代码中,获取到这个实例,就可以直接使用了。

实例工厂

实例工厂就是工厂方法是一个实例方法,这样,工厂类必须实例化之后才可以调用工厂方法。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--实例工厂注入-->
    <bean id="okHttpUtils" class="cn.sail.training.spring.ioc.OkHttpUtils" factory-method="getInstance"/>

    <bean id="okHttpClient1" class="okhttp3.OkHttpClient" factory-bean="okHttpUtils"/>

</beans>

复杂属性注入

对象

可以通过 xml 注入对象,通过 ref 来引用一个对象。

<bean id="user1" class="cn.sail.training.spring.ioc.User">
    <property name="cat" ref="cat"/>
</bean>

<bean id="cat" class="cn.sail.training.spring.ioc.Cat">
    <property name="name" value="小白"/>
    <property name="color" value="白色"/>
</bean>

数组

<bean id="user2" class="cn.sail.training.spring.ioc.User">
    <property name="favorites">
        <array>
            <value>足球</value>
            <value>篮球</value>
            <value>乒乓球</value>
        </array>
    </property>
</bean>

集合

即可以通过 ref 使用外部定义好的 Bean,也可以直接在 list 或者 array 节点中定义 bean。

<bean id="user3" class="cn.sail.training.spring.ioc.User">
    <property name="cats">
        <list>
            <ref bean="cat"/>
            <bean id="cat2" class="cn.sail.training.spring.ioc.Cat">
                <property name="name" value="小黑"/>
                <property name="color" value="黑色"/>
            </bean>
        </list>
    </property>
</bean>

Map

<bean id="user4" class="cn.sail.training.spring.ioc.User">
    <property name="map">
        <map>
            <entry key="name" value="sail"/>
            <entry key="age" value="27"/>
        </map>
    </property>
</bean>

Properties

<bean id="user5" class="cn.sail.training.spring.ioc.User">
    <property name="info">
        <props>
            <prop key="name">sail</prop>
            <prop key="age">27</prop>
        </props>
    </property>
</bean>

Java 配置

在 Spring 中,想要将一个 Bean 注册到 Spring 容器中,整体上来说,有三种不同的方式

  • XML 注入
  • Java 配置(通过 Java 代码将 Bean 注册到 Spring 容器中)
  • 自动化扫描

Java 配置的方式注册有以下步骤:

配置类上加 @Configuration 注解

表示这个类是一个配置类,它的作用相当于 applicationContext.xml

定义方法,方法返回对象,方法上添加 @Bean 注解

表示将这个方法的返回值注入的Spring容器中去。

也就是说,@Bean 所对应的方法,就相当于 applicationContext.xml 中的 bean 节点。

@Configuration
public class Config {

    @Bean
    User user() {
        return new User();
    }

}

配置加载

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
User user = ctx.getBean(User.class);

Bean 的默认名称是方法名。如果想自定义方法名,直接在 @Bean 中进行配置。

如下配置表示修改 Bean 的名字为 sail。

@Configuration
public class Config {

    @Bean("sail")
    User user() {
        return new User();
    }

}

配置加载

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
User user = ctx.getBean("sail", User.class);

配置类需要在项目启动时加载才生效。

自动化配置

自动化配置既可以通过 Java 配置来实现,也可以通过 xml 配置来实现。

类自动化扫描

类自动化扫描的注解一共有四个

  • @Component
  • @Repository
  • @Service
  • @Controller

这四个中,另外三个都是基于 @Component 做出来的,从目前的源码来看,功能也是一致的,使用另外三个的目的主要是区分业务。

  • 在 Service 层上,添加注解时,使用 @Service。
  • 在 Dao 层,添加注解时,使用 @Repository。
  • 在 Controller 层,添加注解时,使用 @Controller。
  • 在其他组件上添加注解时,使用 @Component。

比如:

@Service
public class UserService {
}

添加完成后,自动化扫描有两种方式

通过 Java 代码配置自动化扫描。

通过 xml 文件来配置自动化扫描。

Java代码配置自动扫描

@Configuration
@ComponentScan(basePackages = "cn.sail.training.spring.ioc.service")
public class JavaConfig {
}

然后,在项目启动中加载配置类,在配置类中,通过 @ComponentScan 注解指定要扫描的包,然后就可以获取 UserService 的实例了。

如果不指定,默认情况下扫描的是配置类所在的包下面的 Bean 以及配置类所在的包下的子包下的类。

Bean 的名字叫什么

默认情况下,Bean 的名字是类名首字母小写。例如上面的 UserService,它的实例名,默认就是 userService。如果开发者想要自定义名字,就直接在 @Service 注解中添加即可。

有几种扫描方式

上面的配置,我们是按照包的位置来扫描的。也就是说,Bean 必须放在指定的扫描位置,否则,即使你有 @Service 注解,也扫描不到。

除了按照包的位置来扫描,还有另外一种方式,就是根据注解来扫描。例如如下配置:

@Configuration
@ComponentScan(basePackages = "cn.sail.training.spring.ioc", useDefaultFilters = true, excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class)})
public class JavaConfig {
}

这个配置表示扫描指定包下的所有 Bean,但是除了 Controller。

XML 配置自动化扫描

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/spring-context.xsd
">

    <!--配置自动化扫描-->
    <context:component-scan base-package="cn.sail.training.spring.ioc.service"/>

</beans>

这行配置表示扫描指定包下的所有 Bean。也可以按照类来扫描。

也可以在 XML 配置中按照注解的类型进行扫描:

<!--按照注解的类型进行扫描-->
<context:component-scan base-package="cn.sail.training.spring.ioc" use-default-filters="true">
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

对象注入

自动扫描时的对象注入有三种方式:

  • @Autowired

  • @Resource

  • @Injected

@Autowired 是根据类型去查找,然后赋值,这就有一个要求,这个类型只可以有一个对象,否则就会报错。

@Resources 是根据名称去查找,默认情况下,定义的变量名,就是查找的名称,也可以在注解中手动指定。如果名称找不到,会再根据类型去找,类型也找不到才会报错。

所以,如果一个类存在多个实例,那么就应该使用 @Resources 去注入。

如果非要使用 @Autowired,需要配合另外一个注解 @Qualifier

在 @Qualifier 中可以指定变量名,两个一起用就可以实现通过变量名查找到变量。

@Service
public class UserService {

    @Autowired
    private User user;

    public String hello() {
        return user.getName();
    }

}

条件注解

条件注解就是在满足某一个条件的情况下,生效的配置。

获取 Windows 和 Linux 下的不同文件目录查看命令

定义一个显示文件夹目录的接口。

public interface ShowCmd {

    String showCmd();

}

分别实现 Windows 下的实例和 Linux 下的实例。

public class WinShowCmd implements ShowCmd{

    @Override
    public String showCmd() {
        return "dir";
    }

}
public class LinuxShowCmd implements ShowCmd{

    @Override
    public String showCmd() {
        return "ls";
    }

}

定义两个条件,一个是 Windows 下的条件,另一个是 Linux 下的条件。

public class WindowsCondition implements Condition {

    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        return conditionContext.getEnvironment().getProperty("os.name").toLowerCase().contains("windows");
    }

}
public class LinuxCondition implements Condition {

    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        return conditionContext.getEnvironment().getProperty("os.name").toLowerCase().matches("linux");
    }
    
}

在定义 Bean 的时候,就可以去配置条件注解了。

这里一定要给两个 Bean 取相同的名字,这样在调用时,才可以自动匹配。

给每一个 Bean 加上条件注解,当条件中的 matches 方法返回 true 的时候,这个 Bean 的定义就会生效。

@Bean("showCmd")
@Conditional(WindowsCondition.class)
ShowCmd winCmd() {
    return new WinShowCmd();
}

@Bean("showCmd")
@Conditional(LinuxCondition.class)
ShowCmd linuxCmd() {
    return new LinuxShowCmd();
}

多环境切换

研发中,如何在开发、生产、测试环境之间进行快速切换是很重要的。

Spring 中提供了 Profile 来解决这个问题。

Profile 的底层就是条件注解。

这个从 @Profile 注解的定义就可以看出来。

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional({ProfileCondition.class})
public @interface Profile {
    String[] value();
}

其中的 ProfileCondition 就通过实现 Condition 和重写 matches 来进行条件判断:

class ProfileCondition implements Condition {
    ProfileCondition() {
    }

    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
        if (attrs != null) {
            Iterator var4 = ((List)attrs.get("value")).iterator();

            Object value;
            do {
                if (!var4.hasNext()) {
                    return false;
                }

                value = var4.next();
            } while(!context.getEnvironment().acceptsProfiles(Profiles.of((String[])((String[])value))));

            return true;
        } else {
            return true;
        }
    }
}

定义一个 DataSource

public class DataSource implements Serializable {

    private static final long serialVersionUID = 3802892946419732473L;

    private String url;

    private String username;

    private String password;

    @Override
    public String toString() {
        return "DataSource{" +
                "url='" + url + '\'' +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

}

在配置 Bean 时,通过 @Profile 注解指定不同的环境

@Bean("ds")
@Profile("dev")
DataSource devDataSource() {
    DataSource dataSource = new DataSource();
    dataSource.setUrl("devUrl");
    dataSource.setUsername("devUserName");
    dataSource.setPassword("devPassWord");
    return dataSource;
}

@Bean("ds")
@Profile("prod")
DataSource prodDataSource() {
    DataSource dataSource = new DataSource();
    dataSource.setUrl("prodUrl");
    dataSource.setUsername("prodUserName");
    dataSource.setPassword("prodPassWord");
    return dataSource;
}

加载配置类

需要先设置当前环境,然后再去加载配置类。

AnnotationConfigApplicationContext不能先加载配置类,否则refresh方法会报错。

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("dev");
ctx.register(JavaConfig.class);
ctx.refresh();
DataSource dataSource = ctx.getBean("ds", DataSource.class);
System.out.println(dataSource);

环境的切换,也可以在 XML 文件中配置。

如下配置在 XML 文件中,必须放在其他节点后面,否则会层级混乱而报错。

<beans profile="dev">
    <bean id="dataSource" class="cn.sail.training.spring.ioc.conditionAnnotation.DataSource">
        <property name="url" value="devUrl"/>
        <property name="username" value="devUserName"/>
        <property name="password" value="devPassWord"/>
    </bean>
</beans>

<beans profile="prod">
    <bean id="dataSource" class="cn.sail.training.spring.ioc.conditionAnnotation.DataSource">
        <property name="url" value="prodUrl"/>
        <property name="username" value="prodUserName"/>
        <property name="password" value="prodPassWord"/>
    </bean>
</beans>

启动类中设置当前环境并加载配置

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext();
ctx.getEnvironment().setActiveProfiles("prod");
ctx.setConfigLocation("applicationContext.xml");
ctx.refresh();
DataSource dataSource = ctx.getBean("dataSource", DataSource.class);
System.out.println(dataSource);

Bean 的作用域

从 Spring 容器中多次获取同一个 Bean,默认情况下,获取到的实际上是同一个实例。

XML 配置

通过设置 scope 属性,我们可以调整默认的实例个数。

<bean id="book" class="cn.sail.training.spring.ioc.Book" scope="prototype">
    <constructor-arg index="0" value="1"/>
    <constructor-arg index="1" value="红楼梦"/>
    <constructor-arg index="2" value="33"/>
</bean>

scope 的值为 singleton(默认),表示这个 Bean 在 Spring 容器中,是以单例的形式存在。

如果值为 prototype,表示这个 Bean 在 Spring 容器中不是单例,多次获取将拿到多个不同的实例。

还有两个取值,requestsession,这两个取值在 web 环境下有效。

Java配置

@Bean("sail")
@Scope("prototype")
User user() {
    return new User();
}

在 Java 代码中,我们可以通过 @Scope 注解指定 Bean 的作用域。

在自动扫描配置中,也可以指定 Bean 的作用域。

id 和 name 的区别

name 支持取多个值

多个 name 之间,用 , 隔开。

<bean name="book3,book4,book5" class="cn.sail.training.spring.ioc.Book" scope="prototype">
    <constructor-arg index="0" value="1"/>
    <constructor-arg index="1" value="红楼梦"/>
    <constructor-arg index="2" value="33"/>
</bean>

此时,通过 book3book4book5 都可以获取到当前对象。

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
Book book3 = ctx.getBean("book3", Book.class);
Book book4 = ctx.getBean("book4", Book.class);
Book book5 = ctx.getBean("book5", Book.class);

id 不支持有多个值

如果强行用 , 隔开,它还是一个值。

<bean id="user6,user7,user8" class="cn.sail.training.spring.ioc.User">
    <property name="id" value="1"/>
    <property name="age" value="27"/>
    <property name="name" value="廖航"/>
</bean>

这个配置表示 Bean 的名字为 user6,user7,user8,具体调用如下。

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
User user6 = ctx.getBean("user6,user7,user8", User.class);

混合配置

混合配置就是 Java 配置 + XML 配置。

混用的话,可以在 Java 配置中引入 XML 配置。

@Configuration
@ImportResource("classpath:applicationContext.xml")
public class JavaConfig {
    
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

天航星

感谢你的鼓励和认可

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

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

打赏作者

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

抵扣说明:

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

余额充值