二十一、Spring框架一之Spring基本概念(1)

本文介绍了Spring框架的基本概念及其特点,详细探讨了控制反转IoC的原理与实现思路,并通过具体案例展示了如何使用Spring进行依赖注入DI。此外,还介绍了Spring与MyBatis的整合过程,包括配置与测试。

第一章 概述

1 关于框架

框架:框架是一个程序的半成品,里面有基础代码,在基础代码的基础上进行开发,节约开发时间,提高开发效率。
spring(包括springmvc)和mybatis是企业开发中最基础的两个框架。

2 Spring概述

官网地址:https://spring.io/

2.1 Spring简介

Spring 是分层的 Java SE/EE 应用 full-stack 轻量级开源框架,以 IOC(Inverse Of Control:反转控制)和AOP(Aspect Oriented Programming:面向切面编程)为内核,提供了展现层 SpringMVC 和持久层 Spring JDBC 以及业务层事务管理等众多的企业级应用技术,还能整合开源世界众多著名的第三方框架和类库,逐渐成为使用最多的 Java EE 企业应用开源框架 。

2.2 Spring框架特点

Spring框架:面向service层,但是也能整合web、dao以及其他的框架,都要受到它的管理。

  • 方便解耦,简化开发

通过 Spring 提供的 IoC 容器,可以将对象间的依赖关系交由 Spring 进行控制,避免硬编码所造成的过度程序耦合。用户也不必再为单例模式类、属性文件解析等这些很底层的需求编写代码,可以更专注于上层的应用。

  • AOP 编程的支持
    通过 Spring 的 AOP 功能,方便进行面向切面的编程,许多不容易用传统 OOP 实现的功能可以通过 AOP 轻松应付。

  • 声明式事务的支持
    可以将我们从单调烦闷的事务管理代码中解脱出来,通过声明式方式灵活的进行事务的管理,提高开发效率和质量。

  • 方便集成各种优秀框架
    Spring 可以降低各种框架的使用难度,提供了对各种优秀框架( Struts、 Hibernate、 Hessian、 Quartz等)的直接支持。

第二章 控制反转IoC - Inverse Of Control

1 工厂模式解耦

1.1 内聚和耦合

原则:高内聚 低耦合
高内聚:比如用户的登录、注册等属性和方法,都应该放到用户类中,而不是多个不相关的类中。
低耦合:比如用户下订单:涉及到用户模块和订单模块,各个模块交换数据通过接口文件来实现。

1.2 解耦的思路

解耦,顾名思义,即解除耦合,消除依赖关系。但是在程序开发中,如果两个模块协同工作,则必然存在耦合。

思路:在我们使用一些不是频繁创建的对象时,采用反射的方式创建对象显然更加合理。而反射创建时需要提供创建类的全限定类名,这个名称如果写在java代码中,造成的结果就是修改仍然避免不了修改源码。所以,我们需要使用配置文件,把要创建类的全限定类名用配置文件配置起来。

总结:

  • 第一:使用反射创建对象
  • 第二:创建对象用到的全限定类名用配置文件配置起来

1.3 工厂模式解耦

工厂模式:工厂模式是我们最常用的实例化对象模式了,它是用工厂中的方法代替new创建对象的一种设计模式。

SqlSessionFactory factory = new SqlSessionFactoryBuilder().build("SqlMapConfig.xml");
SqlSession sqlSession = factory.openSession();

Mybatis框架在使用时为我们提供了SqlSessionFactory工厂类,通过openSession()方法获取到SqlSession对象。openSession()同时方法有很多重载,用于实现不同的需求。它支持传入Connection参数来保证连接的一致性;支持传入true|false来保证事务的提交时机等等。

###1.4 运用工厂模式解耦
在service层中:
在我们使用三层架构作为开发基础时,层和层之间的调用,如果使用具体实现类就会出现内容依赖。
在需要更换实现时,就需要修改源码,造成当前业务层功能的独立性很差。同时给我们维护带来极大不便。

/**
* 账户的业务层实现类
*/
public class AccountServiceImpl implements AccountService {
    //此处的依赖关系有待解决
    //private AccountDao accountDao = new AccountDaoImpl();
    
    //使用工厂模式获取dao对象
    private AccountDao accountDao = (AccountDao)BeanFactory.getBean("AccountDao");
    
    @Override
    public void saveAccount() {
    	accountDao.saveAccount();
    }
}

使用Bean工厂来创建对象:
通过解析配置文件 beans.xml,获取id和类型。

<?xml version="1.0" encoding="UTF-8"?>
<beans>
    <!-- 配置 service -->
    <bean id="AccountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>
    <!-- 配置 dao -->
    <bean id="AccountDao" class="com.itheima.dao.impl.AccountDaoImpl"></bean>
</beans>

Bean工厂类,解析该配置文件,使用反射创建对象,并使用一个map容器存放该对象。用户需要该对象的时候,通过容器去取就行了(getBean获取对象)。

package com.itheima.factory;

/**
 * Bean工厂:创建对象
 * 步骤:
 *  1. 解析xml,获取所有对象的全限定类型(全名称)
 *  2. 反射创建对象,存储到容器中
 *      容器:有查找的需求,使用map集合
 *  3. 用户需要对象时,从容器中根据id直接获取
 */
public class BeanFactory {
    // String: id , Object : 反射创建好的对象
    private static Map<String,Object> map = new HashMap<>();
    //静态代码块:类加载时执行,只会执行一次
    static {
        //解析xml,获取所有对象的全限定类型(全名称)
        SAXReader reader = new SAXReader();
        try {
            //获取文档对象
            Document doc = reader.read(BeanFactory.class.getClassLoader().getResourceAsStream("beans.xml"));
            //获取根节点
            Element rootElement = doc.getRootElement();
            //获取根节点下的所有的子节点
            List<Element> elements = rootElement.elements();
            //遍历
            for (Element element : elements) {
                String idValue = element.attributeValue("id");
                String classValue = element.attributeValue("class");
                //反射创建对象,存储到容器中
                Class clazz = Class.forName(classValue);
                Object obj = clazz.newInstance();
                //存储到容器中
                map.put(idValue, obj);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static Object  getBean(String id){
        return map.get(id);
    }
}

2 关于IOC

2.1 什么是IOC

它的全称是Inversion Of Control,意为控制反转。它不是一个技术,而是一种思想。其作用是用于削减代码间的耦合。它的实现思想就是利用了工厂设计模式,把创建对象代码从具体类中剥离出去,交由工厂来完成,从而降低代码间的依赖关系。

2.2 IoC的实现思路分析

在这里插入图片描述
IoC的作用
明确:它是用于降低我们代码间的依赖关系,削减程序中的耦合。

第三章 Spring的IOC入门

1 案例的前置说明

在使用Spring中IoC的配置时,它支持纯XML配置或者纯注解配置以及XML和注解混合配置这三种方式,我们首先以纯XML配置方式为spring入门案例的技术实现。————这就是我们的Spring配置技术选型

本案例要解决 账户的业务层和持久层的依赖关系。

2 Spring基于XML的入门

创建业务层的接口和接口实现类
创建持久层的接口和接口实现类

Spring基于XML的的配置
步骤1: 导入jar包依赖 pom.xml
步骤2: 创建Spring的配置文件 beans.xml (文件名可任意起名)
步骤3: 让Spring管理资源, 在配置文件中配置service和dao

<!-- bean 标签:用于配置让 spring 创建对象,并且存入 ioc 容器之中
    id 属性:对象的唯一标识。
    class 属性:指定要创建对象的全限定类名
-->
<!-- 配置 service -->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>
<!-- 配置 dao -->
<bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl"></bean>

步骤4:测试配置是否成功

/**
 * 测试spring基于xml的Ioc入门案例
 */
public class SpringIocTest {

    /**
     * 步骤分析:
     *    第一步:创建容器
     *    第二步:根据bean的唯一标识获取对象
     *    第三步:执行方法
     */
    public static void main(String[] args) {
        //1.使用 ApplicationContext 接口,就是在获取 spring 容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml");
        //2.根据 bean 的 id 获取对象
        AccountService accountService = (AccountService) ac.getBean("accountService");
        System.out.println(accountService);

        AccountDao accountDao = (AccountDao) ac.getBean("accountDao");
        System.out.println(accountDao);
    }
}

3 基于XML的IOC深入

3.1 Spring中工厂类结构图

在这里插入图片描述
BeanFactory和ApplicationContext的区别:

  • 通过类视图我们可以看出,BeanFactory是Spring中IoC容器的顶层接口,而ApplicationContext是它的一个子接口,所以ApplicationContext具备BeanFactory提供的全部功能。

    通常情况下,我们称BeanFactory是Spring的IoC基础容器。而ApplicationContext是容器的高级接口,它比BeanFactory多了很多重要的功能。例如,父子容器的概念(在SpringMVC课程中讲解),AOP的支持,消息发布机制,事件处理机制,国际化和资源访问等等。

  • BeanFactory 和 ApplicationContext 的区别:

    • 创建对象的时间点不一样。
      • ApplicationContext:只要一读取配置文件, 默认情况下就会创建对象。
      • BeanFactory:什么使用什么时候创建对象

ApplicationContext 接口的实现类

  • ClassPathXmlApplicationContext:
    • 它是从类的根路径下加载配置文件,推荐使用这种
  • FileSystemXmlApplicationContext:
    • 它是从磁盘路径上加载配置文件,配置文件可以在磁盘的任意位置
  • AnnotationConfigApplicationContext :
    • 当我们使用注解配置容器对象时,需要使用此类来创建 spring 容器。它用来读取注解

代码演示

// 创建容器对象,加载配置文件
ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml");
//从容器获取对象
Object obj = ac.getBean("accountDao");
System.out.println(obj);

3.2 IOC中Bean标签和管理对象细节

Bean标签的对象获取: getBean方法

public class SpringIocTest {
    public static void main(String[] args) {
        //1.使用 ApplicationContext 接口,就是在获取 spring 容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml");
        //2.根据 bean 的 id 获取对象
        AccountDao accountDao = (AccountDao) ac.getBean("accountDao");
        System.out.println(accountDao);
        accountDao.saveAccount();

        //方式2 : 根据字节码获取对象(常用),不适合接口有多个实现类的情况下
        AccountDao accountDao2 = ac.getBean(AccountDao.class);
        System.out.println(accountDao2);
        accountDao2.saveAccount();
        
        
        //方式3 : (一个接口有多个实现类时)根据字节码与bean的id 进行确定获取指定Bean对象
        //若AccountDao有多个实现类,那么仅凭接口名无法确定哪个接口的实现类
        AccountDao accountDao3 = ac.getBean("accountDao", AccountDao.class);
        System.out.println(accountDao3);
        accountDao3.saveAccount();
    }
}

Bean标签的作用和属性

  • 作用:

    • 用于配置对象让 spring 来创建的。
    • 默认情况下它调用的是类中的无参构造函数。如果没有无参构造函数则不能创建成功。
  • 属性:

    • id: 给对象在容器中提供一个唯一标识。用于获取对象。

    • class: 指定类的全限定类名。用于反射创建对象。默认情况下调用无参构造函数。

    • scope: 指定对象的作用范围。

      - singleton :默认值,单例的.每次通过getBean获取对象,都是同一个对象,只初始化一次
      - prototype :多例的.
      - request :WEB 项目中,Spring 创建一个 Bean 的对象,将对象存入到 request 域中.
      - session :WEB 项目中,Spring 创建一个 Bean 的对象,将对象存入到 session 域中. 
      
    • init方法: 指定类中的初始化方法名称:对象的初始化工作

    • destroy方法: 指定类中销毁方法名称 :对象销毁前执行的方法,用于资源的释放

bean标签的生命周期

  • 单例对象: scope=“singleton”

    • 一个应用只有一个对象的实例。它的作用范围就是整个引用。

    • 生命周期:

      对象出生: 当应用加载,创建容器时,对象就被创建了。
      
      对象活着: 只要容器在,对象一直活着。
      
      对象死亡: 当应用卸载,销毁容器时,对象就被销毁了。
      
    • 一句话总结:单例模式的bean对象生命周期与容器相同。

  • 多例对象: scope=“prototype”

    • 每次访问对象时,都会重新创建对象实例。

    • 生命周期:

      对象出生: 当使用对象时,创建新的对象实例。
      
      对象活着: 只要对象在使用中,就一直活着。
      
      对象死亡: 当对象长时间不用时,被 java 的垃圾回收器回收了。
      
    • 一句话总结:多例模式的bean对象,spring框架只负责创建,不负责销毁。即使容器销毁了,对象还是存在的。

      不配置scope:默认是单例的

实例化Bean的三种方式:
方式1: Spring 使用默认无参构造函数(掌握,工作开发中使用第一种)

在默认情况下, Spring会根据默认无参构造函数来创建类对象。如果 bean 中没有默认无参构造函数,将会创建失败。

  • 步骤1: 编写beans.xml
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"/>

方式二和方式三不推荐使用,不进行介绍

4 Spring的依赖注入(DI)

4.1 什么是依赖注入

依赖注入(DI): Dependency Injection。就是让spring框架给Bean对象的属性进行赋值. 它是 spring 框架核心 ioc 的具体实现.

  • 我们的程序在编写时, 通过控制反转,把对象的创建交给了 spring,但是代码中不可能出现没有依赖的情况。ioc 解耦只是降低他们的依赖关系,但不会消除。 例如:我们的业务层仍会调用持久层的方法。那这种业务层和持久层的依赖关系, 在使用 spring 之后, 就让 spring 来维护了。
  • 简单的说,依赖注入(DI)就是坐等框架把持久层对象传入业务层,而不用我们自己去获取

4.2 使用构造方法方式注入

顾名思义,就是使用类中的构造函数,给成员变量赋值。

注意,赋值的操作不是我们自己做的,而是通过配置的方式,让 spring 框架来为我们注入。具体代码如下

步骤1: 编写AccountServiceImpl
public class AccountServiceImpl implements AccountService {
    private String name;
    private Integer age;
    private Date birthday;
    
    public AccountServiceImpl(String name, Integer age, Date birthday) {
        this.name = name;
        this.age = age;
        this.birthday = birthday;
    }
    
    @Override
    public void saveAccount() {
    	System.out.println(name+","+age+","+birthday);
    }
}
步骤2: 编写beans.xml

使用构造函数的方式,给pojo中的属性传值
要求:

  • 类中需要提供一个对应参数列表的构造函数。

涉及的标签:

  • constructor-arg

    • 属性:
    index:指定参数在构造函数参数列表的索引位置
    type:指定参数在构造函数中的数据类型
    name:指定参数在构造函数中的名称 用这个找给谁赋值
    
    ========上面三个都是找给谁赋值,下面两个指的是赋什么值的========
    
    value:它能赋的值是基本数据类型和 String 类型
    ref:它能赋的值是其他 bean 类型,也就是说,必须得是在配置文件中配置过的 bean ()
    

编写beans.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">

    <bean id="birthdayId" class="java.util.Date"></bean>
    <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
        <!--使用构造方法的方式1,通过构造方法中索引位置来赋值,不推荐-->
        <!--<constructor-arg index="0" value="zhang3"></constructor-arg>-->
        <!--<constructor-arg index="1" value="25"></constructor-arg>-->
        <!--<constructor-arg index="2" ref="birthdayId"></constructor-arg>-->

        <!--使用构造方法的方式2,通过构造方法中参数的类型来赋值,不推荐-->
        <!--<constructor-arg type="java.lang.String" value="zhang3"></constructor-arg>-->
        <!--<constructor-arg type="java.lang.Integer" value="25"></constructor-arg>-->
        <!--<constructor-arg type="java.util.Date" ref="birthdayId"></constructor-arg>-->

        <!--使用构造方法的方式3,通过构造方法中形参的名称来赋值 推荐-->
        <constructor-arg name="name" value="zhang3"></constructor-arg>
        <constructor-arg name="age" value="25"></constructor-arg>
        <constructor-arg name="birthday" ref="birthdayId"></constructor-arg>
    </bean>

</beans>

4.3 使用set方法方式注入

区分一般的数据类型,引用数据类型,集合类型的方式

**顾名思义,就是在类中提供需要注入成员的 set 方法。**实际开发中,此种方式用的较多

具体代码如下

步骤1: 编写AccountServiceImpl
public class AccountServiceImpl implements AccountService {
    private String name;
    private Integer age;
    private Date birthday;
    
    public void setName(String name) {
    	this.name = name;
    }
    public void setAge(Integer age) {
    	this.age = age;
    }
    public void setBirthday(Date birthday) {
    	this.birthday = birthday;
    }
    
    @Override
    public void saveAccount() {
    	System.out.println(name+","+age+","+birthday);
    }
}
步骤2: 编写beans.xml(普通的属性)

通过配置文件给 bean 中的属性传值:使用 set 方法的方式
涉及的标签:

  • property

    • 属性:
    name: 找的是类中 set 方法后面的部分
    value: 给属性赋值是基本数据类型和 string 类型的
    ref: 给属性赋值是其他 bean 类型的
    
<bean id="birthdayId" class="java.util.Date"></bean>
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
    <property name="name" value="lisi"></property>
    <property name="age" value="30"></property>
    <property name="birthday" ref="birthdayId"></property>
</bean>
步骤2: 编写beans.xml(集合属性)

注入集合数据, 在注入集合数据时,只要结构相同,标签可以互换

  • List 结构的:array,list,set

  • Map 结构的: map,entry; props,prop

<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
    <!--数组-->
    <property name="myStrs">
        <array>
            <value>aaa</value>
            <value>bbb</value>
            <value>ccc</value>
        </array>
    </property>
    <!--list集合-->
    <property name="myList">
        <list>
            <value>javase</value>
            <value>mysql</value>
            <value>javaweb</value>
        </list>
    </property>
    <!--set集合-->
    <property name="mySet">
        <set>
            <value>北京</value>
            <value>上海</value>
            <value>南京</value>
        </set>
    </property>

    <!--map集合-->
    <property name="myMap">
        <map>
            <entry key="aaa" value="你好"></entry>
            <entry key="bbb" value="hello"></entry>
            <entry key="ccc" value="Hi"></entry>
        </map>
    </property>
    <!--properties集合-->
    <property name="myProps">
        <props>
            <prop key="111">哈哈</prop>
            <prop key="222">嘿嘿</prop>
            <prop key="333">呵呵</prop>
        </props>
    </property>
</bean>
</bean>
  • 在List结构的集合数据注入时:
    • array,list,set这三个标签通用,另外注值的value标签内部可以直接写值,也可以使用bean标签配置一个对象,或者用ref标签引用一个已经配合的bean的唯一标识。
  • 在Map结构的集合数据注入时:
    • map标签使用entry子标签实现数据注入,entry标签可以使用keyvalue属性指定存入map中的数据。
    • 使用value-ref属性指定已经配置好的bean的引用。
    • 同时entry标签中也可以使用ref标签,但是不能使用bean标签。
    • property标签不能中不能使用ref或者bean标签引用对象

第四章 Spring整合Mybatis开发

1 整合思路

Spring和Mybatis都是框架,整合到底是Mybatis接管Spring,还是Spring接管Mybatis呢?

通过早期的学习,我们知道Mybatis框架是一个持久层ORM框架,而今天我们学习的Spring则是一个综合性的框架。所以整合是Mybatis往Spring上整合。就是让Spring框架接管Mybatis中的SqlSessionFactory工厂的创建,同时再通过读取mapper配置内容创建dao的代理实现类,并把他们都存入IoC容器。

Spring和Mybatis他们都有独立的配置文件,我们在整合时,有两种选择。第一种是保留两个框架的配置文件,第二种是只保留Spring的配置文件,把Mybatis相关的配置都写在Spring的配置文件中。这两种方式的结果是第二种看起来更为简洁明了。

2 Mybatis框架搭建

在整合之前,我们先要分析整合步骤。我们的思路是,一个框架一个框架的去搭建环境,这样的好处是当出现问题能很快定位到哪里出了问题。

1 创建数据库表
2 创建pom文件
3 创建pojo
4 实现Dao层:
		创建Dao接口:增删改查的方法签名。
		创建接口的实现类(不用我们自己实现,mybatis框架):原理:使用sql的映射文件
			创建dao下的资源目录用于存放接口对应的Mapper映射文件
			编写Mapper映射文件,每条sql语句对应接口中定义的方法
			
		创建Mybatis的核心配置文件:
			分离一个连接数据库的properties文件,存放连接数据库的配置信息,
			加载SQL的映射文件,package自动扫描包下的所有配置文件
			设置pojo的别名映射,简化sql映射文件中的参数名

5 测试:
	导入单元测试包4.12以上,用了spring
	为dao接口中的每个方法设置测试方法
	抽取@before 创建连接池

3 Spring框架搭建及整合Mybatis

6 实现Service层:
		创建Service接口:对应dao中接口的方法
		创建Service的接口对应的实现类:
				Service中需要dao的实现类对象,使用依赖注入:set或构造方法的方式,Spring容器注入了一个对象
7 创建Spring的核心配置文件ApplicationContext.xml
	配置Service层代码,依赖注入的配置:bean标签service的接口实现类,
		property中配置的ref的类型是dao实现类对象的类型,所以配置dao层的代码
		
	配置dao层的代码(Mybatis框架与Spring框架的整合)
		配置数据源Druid
		配置SQLSessionFactory
		配置扫描Dao层代码,实现对接口与SQL文件进行加载,SpringIOC容器会自动创建dao接口的接口实现类对象(动态代理),存到Map集合
		
		Mybatis的配置文件全部整合到Spring中,Mybatis的配置文件可以删除了

配置service层bean对象:

<!--service层开始-->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
	<!--此时还没有配置扫描accountDao,会报错,先不管-->
    <property name="accountDao" ref="accountDao"></property>
</bean>
<!--service层结束-->

配置dao层bean对象:

<!--dao层开始-->
<!--配置properties文件的位置-->
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>

<!--配置数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="${jdbc.driverClassName}"></property>
    <property name="url" value="${jdbc.url}"></property>
    <property name="username" value="${jdbc.username}"></property>
    <property name="password" value="${jdbc.password}"></property>
</bean>
<!--配置mybatis的SqlSessionFactory工厂-->
<bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
    <!--注入数据源-->
    <property name="dataSource" ref="dataSource"></property>
    <!--配置别名-->
    <property name="typeAliasesPackage" value="com.itheima.pojo"></property>
</bean>
<!--配置创建dao代理实现类的扫描器-->
<bean id="mapperScanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.itheima.dao"></property>
</bean>
<!--dao层结束-->

4 MyBatis框架测试

public class TestSpring {
    public static void main(String[] args) {
        ApplicationContext ac = new ClassPathXmlApplicationContext("ApplicationContext.xml");
        AccountService accountService = ac.getBean(AccountService.class);
        Account account = accountService.findByName("热巴");
        System.out.println("account = " + account);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值