springboot IOC实战应用

目录

1、一个例子看懂spring IOC

a 首先定义一个Java简单对象(Plain Ordinary Java Object,POJO),然后再定义一个Java配置文件AppConfig.java

b 下面使用AnnotationConfigApplicationContext来构建自己的IoC容器

2、装配bean

a 修改user类 加入注解@Component 和 @Value

3、@ComponentScan的使用方法总结

a 扫描某个包下面全部包和类

b 指定多个包

c 指定某个类

d  excludeFilters过滤

4、自定义第三方bean

5、依赖注入

6、探讨@Autowired

7、消除歧义的注解:@Primary和@Quelifier

8、@Autowired注解对构造方法的参数进行注入

9、使用属性文件application.properties

a 属性文件的maven配置依赖

b application.properties配置属性清单

c 两种引用application.properties的方法

c1 通过@Value注解,使用${......}这样的占位符读取配置在属性文件的内容

c2 使用注解@ConfigurationProperties,通过它使得配置上有所减少

d 数据库的属性可以配置在jdbc.properties中

10、使用注解@Conditional进行条件装配Bean

11、bean的作用域

12、使用@Profile注解实现各个环境之间的切换


1、一个例子看懂spring IOC

Spring IoC容器是一个管理Bean的容器,在Spring的定义中,它要求所有的IoC容器都需要实现接口BeanFactory,它是一个顶级容器接口。

a 首先定义一个Java简单对象(Plain Ordinary Java Object,POJO),然后再定义一个Java配置文件AppConfig.java

User.java

package com.springboot.chapter3.pojo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

public class User {

	private Long id;
	private String userName;
	private String note;

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getUserName() {
		return userName;
	}

	public void setUserName(String userName) {
		this.userName = userName;
	}

	public String getNote() {
		return note;
	}

	public void setNote(String note) {
		this.note = note;
	}
}

AppConfig.java

package com.springboot.chapter3.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
//@Configuration代表这是一个Java配置文件,Spring的容器会根据它来生成IoC容器去装配Bean

public class AppConfig {

	@Bean(name="user")
//@Bean 
//代表将initUser方法返回的POJO装配到IoC容器中,而其属性name定义这个Bean的名称,如果没有配置
//它,则将方法名称“initUser”作为Bean的名称保存到Spring IoC容器中
	public User initUser() {
		
		User user = new User();

		user.setId(1L);
		user.setUsername("course1");
		user.setNote("note1");
		
		return user;
	}
}

@Configuration 

代表这是一个Java配置文件,Spring的容器会根据它来生成IoC容器去装配Bean

@Bean 

代表将initUser方法返回的POJO装配到IoC容器中,而其属性name定义这个Bean的名称,如果没有配置它,则将方法名称“initUser”作为Bean的名称保存到Spring IoC容器中

b 下面使用AnnotationConfigApplicationContext来构建自己的IoC容器


package com.springboot.chapter3.config;

import org.apache.log4j.Logger;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import com.springboot.course1.config.AppConfig;
import com.springboot.course1.config.User;

public class IocTest {

	private static Logger log = Logger.getLogger(IocTest.class);
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
		User user = ctx.getBean(User.class);
		log.info(user.getUsername());
	}

}

配置在配置文件中的名称为user的Bean已经被装配到IoC容器中,并且可以通过getBean方法获取对应的Bean,并将Bean的属性信息输出出来

当然这只是很简单的方法,而注解@Bean也不是唯一创建Bean的方法,还有其他的方法可以让IoC容器装配Bean,而且Bean之间还有依赖的关系需要进一步处理

2、装配bean

Spring中允许我们通过XML或者Java配置文件装配Bean,但是Spring Boot是基于注解方式的

如果一个个的Bean使用注解@Bean注入Spring IoC容器中,那将是一件很麻烦的事情

Spring还允许我们进行扫描装配Bean到IoC容器中,对于扫描装配而言使用的注解是@Component和@ComponentScan

@Component是标明哪个类被扫描进入Spring IoC容器,而@ComponentScan则是标明采用何种策略去扫描装配Bean

a 修改user类 加入注解@Component 和 @Value

package com.springboot.chapter3.pojo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component("user")
//@Component表明这个类将被Spring IoC容器扫描装配
//其中配置的“user”则是作为Bean的名称,当然你也可以不配置这个字符串,那么IoC容器就会把类名第一个
//字母作为小写,其他不变作为Bean名称放入到IoC容器中;注解@Value则是指定具体的值,使得Spring 
//IoC给予对应的属性注入对应的值
public class User {
	@Value("1")
	private Long id;
	@Value("user_name_1")
	private String userName;
	@Value("note_1")
	private String note;
	
	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getUserName() {
		return userName;
	}

	public void setUserName(String userName) {
		this.userName = userName;
	}

	public String getNote() {
		return note;
	}

	public void setNote(String note) {
		this.note = note;
	}

}

b 改造类AppConfig 加入注解@ComponentScan

package com.springboot.chapter3.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
//@Configuration代表这是一个Java配置文件,Spring的容器会根据它来生成IoC容器去装配Bean
@ComponentScan()
//这里加入了@ComponentScan,意味着它会进行扫描,但是它只会扫描类AppConfig所在的当前包和其子包
public class AppConfig {

//@Bean(name="user")
//@Bean 
//代表将initUser方法返回的POJO装配到IoC容器中,而其属性name定义这个Bean的名称,如果没有配置
//它,则将方法名称“initUser”作为Bean的名称保存到Spring IoC容器中
	public User initUser() {
		
		//User user = new User();
		//user.setId(1L);
		//user.setUsername("course1");
		//user.setNote("note1");		
		//return user;
	}
}

再运行原来的IocTest类进行测试,可以获取到bean

3、@ComponentScan的使用方法总结

a 扫描某个包下面全部包和类

@ComponentScan({“ com.springboot.chapter3.*”)

b 指定多个包

@ComponentScan( basePackage = {“com.springboot.chapter3.pojo”,“com.springboot.chapter3.config”})

c 指定某个类

@ComponentScan( basePackageClasses = { User.class })

d  excludeFilters过滤

UserService类,为了标注它为服务类,将类标注@Service(该标准注入了@Component,所以在默认的情况下它会被Spring扫描装配到IoC容器中),加入了excludeFilters的配置,使标注了@Service的类将不被IoC容器扫描注入,这样就可以把UserService类排除到Spring IoC容器中了

@ComponentScan(basePackages = {"com.springboot.chapter3.*"},excludeFilters = {@Filter(classes=Service.class)})

4、自定义第三方bean

现实的Java的应用往往需要引入许多来自第三方的包,并且很有可能希望把第三方包的类对象也放入到Spring IoC容器中,这时@Bean注解就可以发挥作用了

例子:引入一个DBCP数据源

pom.xml上加入项目所需要DBCP包和数据库MySQL驱动程序的依赖:

		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-dbcp2</artifactId>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>

 通过@Bean定义了其配置项name为“dataSource”,那么Spring就会把它返回的对象用名称“dataSource”保存在IoC容器中

AppConfig.java代码:

package com.springboot.chapter3.config;
import java.util.Properties;

import javax.sql.DataSource;

import org.apache.commons.dbcp2.BasicDataSourceFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.context.annotation.Profile;


@Configuration
@ComponentScan(basePackages = "com.springboot.chapter3.*")
public class AppConfig {
	
	@Bean(name = "dataSource", destroyMethod = "close")
	public DataSource getTestDataSource() {
		Properties props = new Properties();
		props.setProperty("driver", "com.mysql.jdbc.Driver");
		props.setProperty("url", "jdbc:mysql://localhost:3306/test_spring_boot");
		props.setProperty("username", "root");
		props.setProperty("password", "123456");
		DataSource dataSource = null;
		try {
			dataSource = BasicDataSourceFactory.createDataSource(props);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return dataSource;
	}
}

5、依赖注入

Bean之间的依赖,在Spring IoC的概念中,我们称为依赖注入(Dependency Injection,DI)

例子:

首先来定义两个接口,一个是人类(Person),另外一个是动物(Animal)。人类是通过动物去提供一些特殊服务的

package com.springboot.chapter3.pojo.definition;

public interface Person {
	
	public void service();
	
	public void setAnimal(Animal animal);
	
}
package com.springboot.chapter3.pojo.definition;

public interface Animal {
	public void use();
}

接下来我们需要两个实现类

package com.springboot.chapter3.pojo;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;


import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
import com.springboot.chapter3.pojo.definition.Animal;
import com.springboot.chapter3.pojo.definition.Person;


@Component
public class BussinessPerson implements Person{

    @Autowired
    private Animal animal = null;

    @Override
    public void service() {
        this.animal.use();
    }

    @Override
    public void setAnimal(Animal animal) {
        this.animal = animal;
    }

}
package com.springboot.chapter3.pojo;

import org.springframework.stereotype.Component;
import com.springboot.chapter3.pojo.definition.Animal;


@Component
public class Dog implements Animal {

	@Override
	public void use() {
		System.out.println("狗【" + Dog.class.getSimpleName()+"】是看门用的。");
	}

}

注意看 animal属性的注解@Autowired,这也是我们在Spring中最常用的注解之一,十分重要,它会根据属性的类型(by type)找到对应的Bean进行注入

Dog类是动物的一种,所以Spring IoC容器会把Dog的实例注入BussinessPerson中

6、探讨@Autowired

@Autowired是我们使用得最多的注解之一

我们创建一个猫的类,继承接口animal

package com.springboot.chapter3.pojo;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

import com.springboot.chapter3.pojo.definition.Animal;
@Component
@Primary
public class Cat implements Animal {

	@Override
	public void use() {
		System.out.println("猫【" + Cat.class.getSimpleName()+"】是抓老鼠。");
	}

}

提出问题:在的BussinessPerson类中,因为这个类只是定义了一个动物属性(Animal),而我们却有两个动物,一个狗,一个猫,Spring IoC如何注入呢?

如果你还进行测试,很快你就可以看到IoC容器抛出异常

原因:Spring IoC容器并不能知道你需要注入什么动物(是狗?是猫?)给BussinessPerson类对象,从而引起错误的发生

解决:

① 这里,我们只是将属性的名称从animal 修改为了dog,那么我们再测试的时候,你可以看到是采用狗来提供服务的

@Autowired
private Animal animal = null;

改为

@Autowired
private Animal dog = null;

 注意事项:

注意的是@Autowired是一个默认必须找到对应Bean的注解,如果不能确定其标注属性一定会存在并且允许这个被标注的属性为null,那么你可以配置@Autowired属性required为false

@Autowired(required = false)

 除了可以标注属性外,还可以标注方法,如setAnimal方法

7、消除歧义的注解:@Primary和@Quelifier

注解@Primary,它是一个修改优先权的注解,当我们有猫有狗的时候,假设这次需要使用猫,那么只需要在猫类的定义上加入@Primary就可以了

package com.springboot.chapter3.pojo;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

import com.springboot.chapter3.pojo.definition.Animal;
@Component
@Primary
public class Cat implements Animal {

	@Override
	public void use() {
		System.out.println("猫【" + Cat.class.getSimpleName()+"】是抓老鼠。");
	}

}

@Quelifier:它的配置项value需要一个字符串去定义,它将与@Autowired组合在一起,通过类型和名称一起找到Bean。我们知道Bean名称在Spring IoC容器中是唯一的标识,通过这个就可以消除歧义性了


    @Autowired
    @Qualifier("dog")
    public void setAnimal(Animal animal) {
        this.animal = animal;
    }

 Spring IoC将会以类型和名称去寻找对应的Bean进行注入

8、@Autowired注解对构造方法的参数进行注入

package com.springboot.chapter3.pojo;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import com.springboot.chapter3.pojo.definition.Animal;
import com.springboot.chapter3.pojo.definition.Person;
import org.springframework.beans.factory.DisposableBean;

@Component
public class BussinessPerson implements Person {

    private Animal animal = null;

    public BussinessPerson(@Autowired @Qualifier("dog") Animal animal){
        this.animal = animal;
    }

    @Override
    public void service() {
        this.animal.use();
    }

    @Override
    public void setAnimal(Animal animal) {
        this.animal = animal;
    }
}

代码中取消了@Autowired对属性和方法的标注。在参数上加入了@Autowired和@Qualifier注解,使得它能够注入进来

9、使用属性文件application.properties

a 属性文件的maven配置依赖

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
			<optional>true</optional>
		</dependency>

有了依赖,就可以直接使用application.properties文件工作了

b application.properties配置属性清单

database.driverName=com.mysql.jdbc.Driver
database.url=jdbc:mysql://localhost:3306/chapter3
database.username=root
database.password=123456

c 两种引用application.properties的方法

c1 通过@Value注解,使用${......}这样的占位符读取配置在属性文件的内容

@Value注解,既可以加载属性,也可以加在方法上

package com.springboot.chapter3.pojo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/******** imports ********/
@Component
public class DataBaseProperties {
	@Value("${database.driverName}")
	private String driverName = null;

	@Value("${database.url}")
	private String url = null;

	private String username = null;

	private String password = null;

	public void setDriverName(String driverName) {
		System.out.println(driverName);
		this.driverName = driverName;
	}

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

	@Value("${database.username}")
	public void setUsername(String username) {
		System.out.println(username);
		this.username = username;
	}

	@Value("${database.password}")
	public void setPassword(String password) {
		System.out.println(password);
		this.password = password;
	}
	/**** getters ****/

	public String getDriverName() {
		return driverName;
	}

	public String getUrl() {
		return url;
	}

	public String getUsername() {
		return username;
	}

	public String getPassword() {
		return password;
	}
}

c2 使用注解@ConfigurationProperties,通过它使得配置上有所减少

注解@ConfigurationProperties中配置的字符串database,将与POJO的属性名称组成属性的全限定名去配置文件里查找,这样就能将对应的属性读入到POJO当中

package com.springboot.chapter3.pojo;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties( "database")
public class DataBaseProperties {
	
	private String driverName = null;
	
	private String url = null;
	
	private String username = null;
	
	private String password = null;
	
	public void setDriverName(String driverName) {
		System.out.println(driverName);
		this.driverName = driverName;
	}
	
	public void setUrl(String url) {
		System.out.println(url);
		this.url = url;
	}
	
	
	public void setUsername(String username) {
		System.out.println(username);
		this.username = username;
	}
	
	public void setPassword(String password) {
		System.out.println(password);
		this.password = password;
	}
	
	public String getDriverName() {
		return driverName;
	}

	public String getUrl() {
		return url;
	}

	public String getUsername() {
		return username;
	}

	public String getPassword() {
		return password;
	}
}

d 数据库的属性可以配置在jdbc.properties中

配置从application.properties中迁移到jdbc.properties中

然后使用@PropertySource去定义对应的属性文件,把它加载到Spring的上下文中

jdbc.properties

database.driverName=com.mysql.jdbc.Driver
database.url=jdbc:mysql://localhost:3306/chapter3
database.username=root
database.password=123456

使用@PropertySource去定义对应的属性文件:

package com.springboot.chapter3.main;



import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ImportResource;
import org.springframework.context.annotation.PropertySource;

@SpringBootApplication
@ComponentScan(basePackages = {"com.springboot.chapter3"})
@PropertySource(value={"classpath:jdbc.properties"}, ignoreResourceNotFound=true)
@ImportResource(value = {"classpath:spring-other.xml"})
public class Chapter3Application {

	public static void main(String[] args) {
		SpringApplication.run(Chapter3Application.class, args);
	}
}

value可以配置多个配置文件。使用classpath前缀,意味着去类文件路径下找到属性文件;ignoreResourceNotFound则是是否忽略配置文件找不到的问题。ignoreResourceNotFound的默认值为false,也就是没有找到属性文件,就会报错;这里配置为true,也就是找不到就忽略掉,不会报错

10、使用注解@Conditional进行条件装配Bean

有时候某些客观的因素会使一些Bean无法进行初始化

比如 :在数据库连接池的配置中漏掉一些配置会造成数据源不能连接上。在这样的情况下,IoC容器如果还进行数据源的装配,则系统将会抛出异常,导致应用无法继续

Spring提供了@Conditional注解处理这种场景

@Conditional注解需要配合另外一个接口

Condition(org.springframework.context.annotation.Condition

来完成对应的功能

使用属性初始化数据库连接池

package com.springboot.chapter3.config;
import java.util.Properties;

import javax.sql.DataSource;

import org.apache.commons.dbcp2.BasicDataSourceFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.context.annotation.Profile;


@Configuration
@ComponentScan(basePackages = "com.springboot.chapter3.*")
@ImportResource(value = {"classpath:spring-other.xml"})
public class AppConfig {
	
	@Bean(name = "dataSource", destroyMethod = "close")
	@Conditional(DatabaseConditional.class)
	public DataSource getDataSource(
			@Value("${database.driverName}") String driver,
			@Value("${database.url}") String url,
			@Value("${database.username}") String username, 
			@Value("${database.password}") String password
			) {
		Properties props = new Properties();
		props.setProperty("driver", driver);
		props.setProperty("url", url);
	    props.setProperty("username", username);
		props.setProperty("password", password);
		DataSource dataSource = null;
		try {
			dataSource = BasicDataSourceFactory.createDataSource(props);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return dataSource;
    }
}

加入了@Conditional注解

并且配置了类DatabaseConditional,那么这个类就必须实现Condition接口。对于Condition接口则要求实现matches方法

DatabaseConditional:

package com.springboot.chapter3.condition;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class DatabaseConditional implements Condition {

	@Override
	/*
	 * 
	 * @param context 条件上下文
	 * @param 
	 */
	public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		Environment env = context.getEnvironment();
		return 
         env.containsProperty("database.driverName") 
         &&env.containsProperty("database.url") 
         &&env.containsProperty("database.username")
         &&env.containsProperty("database.password");
	}

}

matches方法首先读取其上下文环境,然后判定是否已经配置了对应的数据库信息。这样,当这些都已经配置好后则返回true。这个时候Spring会装配数据库连接池的Bean,否则是不装配的

11、bean的作用域

在一般的容器中,Bean都会存在单例(Singleton)和原型(Prototype)两种作用域

在Web容器中,存在页面(page)、请求(request)、会话(session)和应用(application)4种作用域

12、使用@Profile注解实现各个环境之间的切换

项目往往要面临开发环境、测试环境、准生产环境(用于模拟真实生产环境部署所用)和生产环境的切换,这样在一个互联网企业中往往需要有4套环境,而每一套环境的上下文是不一样的

Spring还提供了Profile机制,使我们可以很方便地实现各个环境之间的切换。

假设存在dev_spring_boot和test_spring_boot两个数据库,这样可以使用注解@Profile定义两个Bean

package com.springboot.chapter3.config;
import java.util.Properties;

import javax.sql.DataSource;

import org.apache.commons.dbcp2.BasicDataSourceFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.context.annotation.Profile;


@Configuration
@ComponentScan(basePackages = "com.springboot.chapter3.*")
@ImportResource(value = {"classpath:spring-other.xml"})
public class AppConfig {
	
//	@Bean(name = "dataSource", destroyMethod = "close")
//	@Conditional(DatabaseConditional.class)
//	public DataSource getDataSource(
//			@Value("${database.driverName}") String driver,
//			@Value("${database.url}") String url,
//			@Value("${database.username}") String username, 
//			@Value("${database.password}") String password
//			) {
//		Properties props = new Properties();
//		props.setProperty("driver", driver);
//		props.setProperty("url", url);
//		props.setProperty("username", username);
//		props.setProperty("password", password);
//		DataSource dataSource = null;
//		try {
//			dataSource = BasicDataSourceFactory.createDataSource(props);
//		} catch (Exception e) {
//			e.printStackTrace();
//		}
//		return dataSource;
//	}
	
	
	@Bean(name = "dataSource", destroyMethod = "close")
	@Profile("dev")
	public DataSource getDevDataSource() {
		Properties props = new Properties();
		props.setProperty("driver", "com.mysql.jdbc.Driver");
		props.setProperty("url", "jdbc:mysql://localhost:3306/dev_spring_boot");
		props.setProperty("username", "root");
		props.setProperty("password", "123456");
		DataSource dataSource = null;
		try {
			dataSource = BasicDataSourceFactory.createDataSource(props);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return dataSource;
	}
	
	
	@Bean(name = "dataSource", destroyMethod = "close")
	@Profile("test")
	public DataSource getTestDataSource() {
		Properties props = new Properties();
		props.setProperty("driver", "com.mysql.jdbc.Driver");
		props.setProperty("url", "jdbc:mysql://localhost:3306/test_spring_boot");
		props.setProperty("username", "root");
		props.setProperty("password", "123456");
		DataSource dataSource = null;
		try {
			dataSource = BasicDataSourceFactory.createDataSource(props);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return dataSource;
	}
}

待续........  2022-07-26

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值