springboot面试题

springboot自动装配原理

springboot启动类加@SpringBootApplication注解

@SpringBootApplication //启动自动装配功能
public class SpringbootApplication {}

@SpringBootApplication是个复合注解,引入了@EnableAutoConfiguration

//也就是@Configuration,将当前类申明为配置类
@SpringBootConfiguration
//启用自动配置
@EnableAutoConfiguration
//扫描当前包以及子包,将有@Component,@Controller,@Service,@Repository等注解的类注册到容器中
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {}

@EnableAutoConfiguration引入了@Import

//@Import是Spring注解之一,用于在配置类中导入其他配置类或者普通的Java类
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {}

@Import导入了AutoConfigurationImportSelector类,AutoConfigurationImportSelector类实现了DeferredImportSelector接口

public class AutoConfigurationImportSelector implements DeferredImportSelector, 
BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {}

重写了selectImports(),读取所有的spring.factories文件,过滤出AutoConfiguration类型的类

public String[] selectImports(AnnotationMetadata annotationMetadata) {
        Enumeration urls = classLoader.getResources("META-INF/spring.factories");
    }

spring.factories文件

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
省略

通过@ConditionalOn排除无效的自动配置类

@ConditionalOnClass({RabbitTemplate.class, Channel.class})
public class RabbitAutoConfiguration {}

spring整合mybatis

需要程序员手动将SqlSessionFactoryBean注入spring,由spring管理

@Configuration
public class MybatisConfiguration {
	//创建SqlSessionFactoryBean对象,设置形参,Spring会自动去调用IOC容器中已有的数据源
	@Bean
	public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource){
	    SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
	    sqlSessionFactoryBean.setDataSource(dataSource);
	    return sqlSessionFactoryBean;
	}
}

springboot整合mybatis

mybatis-spring-boot-autoconfigure-2.2.2.jar,META-INF/spring.factories中

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
......,\org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

springboot会自动装配需要的bean比如SqlSessionFactory,不需要程序员再一个个手动注入spring容器中
@ConditionalOnClass判断是否真正用到了mybatis,虽然可以自动装配SqlSessionFactory,但是只有当代码中真正用到mybatis时才会返回相应的bean

@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
public class MybatisAutoConfiguration implements InitializingBean {
	@Bean
	@ConditionalOnMissingBean
	public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {}
	}

springboot启动原理?

9千字长文带你了解SpringBoot启动过程–史上最详细 SpringBoot启动流程-图文并茂
1,当启动springboot应用程序时,会先创建springApplication对象,在对象的构造方法中会进行某些参数初始化工作,主要是判断当前应用程序类型默认使用的是Servlet 容器,以及加载所有的初始化器和监听器,在这个过程中会加载整个应用程序中spring.factories文件,将文件内容放到缓存对象中,方便后续获取。
2,springApplication对象创建完成之后,开始执行run方法,来完成整个启动,启动过程中最主要的有两个方法,prepareContext和refreshContext,前面的处理逻辑包含了上下文对象的创建,banner的打印,实例化异常报告器等各种准备工作,方便后续来进行调用。
3,在prepareContext方法中完成上下文对象的初始化操作,包括属性值的设置,比如环境对象,其中load()将当前启动类作为一个beanDefintion注册到ioc容器中,方便后续在进行BeanFactoryPostProcessor调用执行的时候,找到对应的主类,来完成@SpringBootApplication,@EnableAutoConfiguration等注解解析工作。
4,在refreshContext方法中会进行整个容器刷新过程,来完成整个spring应用程序的启动,在自动装配过程中,在执行invokeBeanFactoryPostProcessors解析处理各种注解,包括@PropertySource,@ComponentScan,@Bean,@Import等注解,最主要的是@Import注解的解析
5,解析@Import注解的时候,会有一个getImports方法,从主类开始递归解析注解,把所有包含@Import的注解都解析到,调用deferredImportSelectorHandle中的process方法,来加载EnableAutoConfiguration

springboot启动原理分析

-----------------------创建springbootApplication对象--------------------
1,创建springbootApplication对象springboot容器初始化操作
2,获取当前应用的启动类型。
	注1:通过判断当前classpath是否加载servlet类,返回servlet web启动方式。
	注2:webApplicationType三种类型:
		1.reactive:响应式启动(spring5新特性)
		2.none:即不嵌入web容器启动(springboot放在外部服务器运行 )
		3.servlet:基于web容器进行启动
3,读取springboot下的META-INFO/spring.factories文件,获取对应的ApplicationContextInitializer装配到集合
4,读取springboot下的META-INFO/spring.factories文件,获取对应的ApplicationListener装配到集合
5,mainApplicationClass,获取当前运行的主函数
------------------调用springbootApplication对象的run方法,实现启动,返回当前容器的上下文-------------------------
6,调用run方法启动
7,StopWatch stopWatch = new StopWatch(),记录项目启动时间
8,getRunListeners,读取META-INF/spring.factores,将SpringApplicationRunListeners类型存到集合中
监听器实现了SpringApplicationRunListener的类,用来加载配置文件
9,listeners.starting();循环调用starting方法
10,prepareEnvironment(listeners, applicationArguments);将配置文件读取到容器中读取多数据源:classpath:/,classpath:/config/,file:./,file:./config/底下。其中classpath是读取编译后的,file是读取编译的支持yml,yaml,xml,properties
11,Banner printedBanner = printBanner(environment);开始打印banner图,就是sprongboot启动最开头的图案
12,初始化AnnotationConfigServletWebServerApplicationContext对象
13,刷新上下文,调用注解,refreshContext(context);
14,创建tomcat
15,加载springmvc
16,刷新后的方法,空方法,给用户自定义重写afterRefresh()
17,stopWatch.stop();结束计时
18,使用广播和回调机制告诉监听者springboot容器已经启动化成功,listeners.started(context);
19,返回上下文

springboot启动原理详细步骤

1,运行 SpringApplication.run() 方法

@SpringBootApplication
public class App  {
    public static void main(String[] args) {
        // 启动springboot
        ConfigurableApplicationContext run = SpringApplication.run(App.class, args);
    }
}

2,new 一个SpringApplication 对象,创建这个对象的构造函数做了一些准备工作

public static ConfigurableApplicationContext run(Class<?>[] primarySources,String[] args) {
	return new SpringApplication(primarySources).run(args);
}

2.1,确定应用程序类型
在SpringApplication的构造方法内,首先会通过WebApplicationType.deduceFromClasspath()方法判断当前应用程序的容器,默认使用的是Servlet 容器,除了servlet之外,还有NONE 和 REACTIVE

static WebApplicationType deduceFromClasspath() {
	if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
			&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
		return WebApplicationType.REACTIVE;
	}
	for (String className : SERVLET_INDICATOR_CLASSES) {
		if (!ClassUtils.isPresent(className, null)) {
			return WebApplicationType.NONE;
		}
	}
	return WebApplicationType.SERVLET;
}

2.2,加载所有的初始化器
从META-INF/spring.factories配置文件中加载初始化器

setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));

META-INF/spring.factories

# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

2.3,加载所有的监听器
加载监听器也是从META-INF/spring.factories 配置文件中加载的,与初始化器不同的是,监听器加载的是实现了ApplicationListener 接口的类

setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

META-INF/spring.factories

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

2.4,设置程序运行的主类
仅仅是找到main方法所在的类,为后面的扫包作准备

private Class<?> deduceMainApplicationClass() {
	try {
		StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
		for (StackTraceElement stackTraceElement : stackTrace) {
			if ("main".equals(stackTraceElement.getMethodName())) {
				return Class.forName(stackTraceElement.getClassName());
			}
		}
	}
	catch (ClassNotFoundException ex) {
		// Swallow and continue
	}
	return null;
}

3,调用run()方法
3.1,开启计时器

// 实例化计时器
StopWatch stopWatch = new StopWatch(); 
// 开始计时
stopWatch.start();

3.2,将java.awt.headless设置为true
这里将java.awt.headless设置为true,表示运行在服务器端,在没有显示器器和鼠标键盘的模式下照样可以工作

private void configureHeadlessProperty() {
	System.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS,
			System.getProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, Boolean.toString(this.headless)));
}

3.3,获取并启用监听器
创建所有 Spring 运行监听器并发布应用启动事件,启用监听器

SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();

3.4,设置应用程序参数
将执行run方法时传入的参数封装成一个对象

ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

3.5,准备环境变量
准备环境变量,包含系统属性和用户配置的属性,将maven和系统的环境变量、yaml文件、property文件也加载进来了

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
	ApplicationArguments applicationArguments) {
	// Create and configure the environment
	ConfigurableEnvironment environment = getOrCreateEnvironment();
	configureEnvironment(environment, applicationArguments.getSourceArgs());
	ConfigurationPropertySources.attach(environment);
	listeners.environmentPrepared(environment);
	bindToSpringApplication(environment);
	if (!this.isCustomEnvironment) {
		environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
				deduceEnvironmentClass());
	}
	ConfigurationPropertySources.attach(environment);
	return environment;
}

3.6,忽略bean信息

configureIgnoreBeanInfo(environment);

3.7,打印banner信息

Banner printedBanner = printBanner(environment);

打印效果

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.4.5)

3.8,创建应用程序的上下文
实例化应用程序的上下文, 调用 createApplicationContext() 方法,这里就是用反射创建上下文对象

protected ConfigurableApplicationContext createApplicationContext() {
	Class<?> contextClass = this.applicationContextClass;
	if (contextClass == null) {
		try {
			switch (this.webApplicationType) {
			case SERVLET:
				contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
				break;
			case REACTIVE:
				contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
				break;
			default:
				contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
			}
		}
		catch (ClassNotFoundException ex) {
			throw new IllegalStateException(
					"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
		}
	}
	return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}

3.9,实例化异常报告器
异常报告器是用来捕捉启动过程抛出的异常使用的,当springboot应用程序在发生异常时,异常报告器会将其捕捉并做相应处理,在spring.factories 文件里配置了默认的异常报告器

Collection<SpringBootExceptionReporter> exceptionReporters = 
	getSpringFactoriesInstances(SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);

META-INF/spring.factories

# Error Reporters
org.springframework.boot.SpringBootExceptionReporter=\
org.springframework.boot.diagnostics.FailureAnalyzers

3.10,准备上下文环境
将启动类作为配置类进行读取,主要是加载启动类上注解,将xml配置、注解的bean注册为BeanDefinition(bean的定义信息,在通过反射创建对象之前定义BeanDefinition,设置bean的属性信息)

BeanDefinition例如包含了以下属性值
<BeanDefinition id=? name=? classname=? depends-on=? initmethod=?>
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
		SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
	context.setEnvironment(environment);
	postProcessApplicationContext(context);
	applyInitializers(context);
	listeners.contextPrepared(context);
	if (this.logStartupInfo) {
		logStartupInfo(context.getParent() == null);
		logStartupProfileInfo(context);
	}
	// Add boot specific singleton beans
	ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
	beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
	if (printedBanner != null) {
		beanFactory.registerSingleton("springBootBanner", printedBanner);
	}
	if (beanFactory instanceof DefaultListableBeanFactory) {
		((DefaultListableBeanFactory) beanFactory)
				.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
	}
	if (this.lazyInitialization) {
		context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
	}
	// Load the sources
	Set<Object> sources = getAllSources();
	Assert.notEmpty(sources, "Sources must not be empty");
	load(context, sources.toArray(new Object[0]));
	listeners.contextLoaded(context);
}

3.11,实例化单例的beanName生成器
在 postProcessApplicationContext(context)方法里面。使用单例模式创建了BeanNameGenerator 对象,其实就是beanName生成器,用来生成bean对象的名称
3.12,执行初始化方法
执行2.2加载出来的所有初始化器,这些初始化器都实现了ApplicationContextInitializer接口的类
3.13,将启动参数注册到容器中
这里将启动参数以单例的模式注册到容器中,是为了以后方便拿来使用,参数的beanName 为 :springApplicationArguments
3.14,刷新上下文refresh
刷新上下文已经是spring的范畴了,自动装配和启动 tomcat就是在这个方法里面完成的,invokeBeanFactoryPostProcessor解析@Import加载所有的自动配置类

@Override
public void refresh() throws BeansException, IllegalStateException {
	synchronized (this.startupShutdownMonitor) {
		// Prepare this context for refreshing.
		prepareRefresh();
		// Tell the subclass to refresh the internal bean factory.
		ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
		// Prepare the bean factory for use in this context.
		prepareBeanFactory(beanFactory);
		try {
			// Allows post-processing of the bean factory in context subclasses.
			postProcessBeanFactory(beanFactory);
			// Invoke factory processors registered as beans in the context.
			invokeBeanFactoryPostProcessors(beanFactory);
			// Register bean processors that intercept bean creation.
			registerBeanPostProcessors(beanFactory);
			// Initialize message source for this context.
			initMessageSource();
			// Initialize event multicaster for this context.
			initApplicationEventMulticaster();
			// Initialize other special beans in specific context subclasses.
			onRefresh();
			// Check for listener beans and register them.
			registerListeners();
			// Instantiate all remaining (non-lazy-init) singletons.
			finishBeanFactoryInitialization(beanFactory);
			// Last step: publish corresponding event.
			finishRefresh();
		}
		catch (BeansException ex) {
			if (logger.isWarnEnabled()) {
				logger.warn("Exception encountered during context initialization - " +
						"cancelling refresh attempt: " + ex);
			}
			// Destroy already created singletons to avoid dangling resources.
			destroyBeans();
			// Reset 'active' flag.
			cancelRefresh(ex);
			// Propagate exception to caller.
			throw ex;
		}
		finally {
			// Reset common introspection caches in Spring's core, since we
			// might not ever need metadata for singleton beans anymore...
			resetCommonCaches();
		}
	}
}

3.15,刷新上下文后置处理
afterRefresh 方法是启动后的一些处理,留给用户扩展使用,目前这个方法里面是空的
3.16,结束计时器
计时器会打印启动springboot的时长
3.17,发布上下文准备就绪事件
告诉应用程序,我已经准备好了,可以开始工作了
3.18,执行自定义的run方法
可以在启动完成后执行自定义的run方法,实现 ApplicationRunner 接口或实现 CommandLineRunner 接口

/**
 * 自定义run方法的2种方式
 */
@Component
public class MyRunner implements ApplicationRunner, CommandLineRunner {
    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println(" 我是自定义的run方法1,实现 ApplicationRunner 接口既可运行"        );
    }
 
    @Override
    public void run(String... args) throws Exception {
        System.out.println(" 我是自定义的run方法2,实现 CommandLineRunner 接口既可运行"        );
    }
}

springboot的jar可以直接运行?

1,springboot提供了一个插件spring-boot-maven-plugin用于将程序打包成一个可执行jar

<plugin>
   <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
</plugin>

2,java -jar会找jar中mainfest文件,在里面找真正的启动类
3,jar启动main函数,创建一个LaunchedURLClassLoader来加载boot-lib下边的jar。

springboot内置Tomcat启动原理?

1,当我们添加了spring-boot-starter-web依赖时,在spring.factories中配置ServletWebServerFactoryAutoConfiguration配置类,通过@ConditionalOn相关注解选择到底应用哪个web容器,默认是tomcat
spring.factories:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
//省略
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\
@Import({ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class, 
EmbeddedTomcat.class, 
EmbeddedJetty.class, 
EmbeddedUndertow.class})
public class ServletWebServerFactoryAutoConfiguration {}

2,SpringBoot启动的时候(SpringApplication.run)会创建Spring容器AnnotationConfigServletWebServerApplicationContext。
3,调用容器的refresh方法,加载ioc容器。解析spring.factories中自动配置类。
4,通过refresh里面调用getWebServerFactory方法。获得tomcat服务工厂。这个方法的作用就是去获取之前自动配置类当中配置的这个TomcatServletWebServerFactory。获取到之后就创建Bean,那么就有Bean实例了,之后返回。

@ConditionalOnClass({Servlet.class, Undertow.class, SslClientAuthMode.class})
@ConditionalOnMissingBean(
    value = {ServletWebServerFactory.class},
    search = SearchStrategy.CURRENT
)
static class EmbeddedTomcat {
    @Bean
    TomcatServletWebServerFactory tomcatServletWebServerFactory(ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers, ObjectProvider<TomcatContextCustomizer> contextCustomizers, ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
        //省略
        return factory;
    }
}

5,调用getWebServer,这个方法就会启动内嵌的Tomcat。Tomcat挂起等待请求。

public WebServer getWebServer(ServletContextInitializer... initializers) {
	//实例化 Tomcat,可以理解为 Server 组件
    Tomcat tomcat = new Tomcat();
    //启动tomcat并返回TomcatWebServer对象
    return this.getTomcatWebServer(tomcat);
}

springboot核心注解?

1,@SpringBootApplication:这个注解标识了一个springboot工程,实际上他是一个复合注解。
2,@SpringBootConfiguration:这个注解实际就是一个@Configuration,表示启动类也是一个配置类
3,@EnableAutoConfiguration:向spring容器中导入了一个selector,用来加载classpath下springFactories中所定义的自动配置类,将这些自动加载到spring容器中。
4,@ConditionalOn也很关键,如果没有我们无法再自定义应用中进行订制开发

注解含义
@ConditionalOnBean当指定的bean在应用程序上下文存在时,才会启动被注解的组件或配置。用于基于其他bean的存在与否决定是否启动特定功能
@ConditionalOnMissingBean当指定bean在应用程序上下文不存在时,才会启动被注解的组件或配置。用于提供默认实现或避免重复创建bean
@ConditionalOnClass当指定类位于类路径上时,才会启动被注解的组件或配置。用于根据类的可用性来决定是否启动某个特定功能
@ConditionalOnMissingClass当指定类不在类路径时,才会启动被注解的组件或配置。用于在某些类不可用时应用备用实现
@ConditionalOnExpression当指定表达式计算结果为true时,才会启动被注解的组件或配置。用于更复杂条件判断
@ConditionalOnProperty当指定属性满足条件时,才会启动被注解的组件或配置。用于基于配置属性的值来决定是否启动特定功能

springboot有哪些特性?

springboot是快速开发spring应用的一个脚手架,其设计目的是用来简化spring应用的初始搭建以及开发过程。
springboot提供了很多内置的starter结合自动配置,对主流框架无配置集成、开箱即用。
springboot简化了开发,采用JavaConfig的方式使用零xml的方式进行开发。
springboot内置web容器无需依赖外部web服务器,省略了web.xml,直接运行jar文件就可以启动web应用。
springboot帮我们管理常用的第三方依赖的版本,减少出现版本冲突的问题。
springboot自带的监控功能,可以监控应用程序的运行状况,或者内存、线程池、http请求统计等,同时还提供了优雅关闭应用程序等功能。

springboot读取配置文件原理?加载顺序?

通过事件监听的方式读取配置文件,ConfigFileApplicationListener
优先级:
resources/application.yml,
resources/config/application.yml,
resources/config/xxx/application.yml
classpath:application.yml

springboot默认日志框架?

logback

springboot为什么默认使用cglib动态代理?

1,虽然jdk动态代理在某些情况下可能比cglib动态代理略快,但差距通常不大。
2,jdk动态代理要求目标类必须实现一个接口,而cglib动态代理不需要。使得cglib动态代理更适用于没有接口的类,从而扩展了动态代理的适用范围。
3,springboot选择cglib动态代理可以使得类无需实现任何接口或继承特定的类,从而减少了对源代码的侵入性。
4,springboot默认提供了cglib的依赖,从而使应用程序使用cglib动态代理非常方便。
5,springboot设计上尽可能降低配置复杂度,使用cglib可以避免开发者在配置文件中显式声明要使用的代理类型。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值