spring面试题(2020最新版)

你对spring框架的理解

spring是一个轻量级的IOC和AOP框架,旨在提高开发人员的开发效率和对项目的维护,它的核心模块:

  • spring CORE:核心类库,提供IOC服务
  • spring AOP:提供AOP服务
  • spring context:提供框架式的bean访问方式
  • spring Dao:对jdbc进行了抽象封装,简化数据访问异常
  • spring ORM:提供对持久层框架支持
  • spring web:提供面向web的综合特性
  • spring mvc:提供面向web的model-view-controller实现

spring的优点

  • DI机制将对象之间的依赖关系交给容器来管理,减少各个组件之间的耦合性;
  • aop将通用的业务逻辑抽取出来提高代码的复用性;
  • spring提供了对主流框架很好的支持
  • spring属于低侵入式编程,对代码的污染极低;

谈谈你对IOC的理解

  • ioc即inversion of control,将对象之间的依赖关系交给spring容器来管理,减少各个组件之间的耦合性,DI和IOC是同一个概念只是不同的视角;
  • 以前创建对象的时机和动作都是自己把握(自己根据需要new一个对象),现在是交给spring来管理根据xml文件在运行时动态的去创建对象,使用反射机制。
  • ioc注入数据有三种方式:构造方法注入、set方法注入、注解注入

谈谈你对AOP的理解

aop即面向切面编程,在一个方法前面或者后面做一些额外的处理,比如:权限控制,日志记录等功能,将通用的业务逻辑抽取出来,提高代码的复用性。

aop中的术语

  • 连接点:类中可以被增强的方法
  • 切入点:哪些连接点可以被拦截
  • 增强:拦截之后要做的事情
  • 切面:切入点与增强的结合
  • 目标对象:代理的连接点对象
  • 织入:把增强应用到目标的过程
  • 引介:在运行期间动态的添加一些方法和属性

aop的实现方式:

  • JDK的动态代理:只能对接口的实现类生成代理对象,而不是针对类的,原理是通过在运行期间创建一个接口的实现类来完成代理对象
  • cglib:主要对类生成代理对象,对是否实现接口无要求,原理是对指定的类

在这里插入图片描述

IOC的初始化过程

  • Resource定位
  • BeanDefinition加载
  • BeanDefinition注册

ApplicationContext和BeanFactory的区别

applicationcontext和beanfactory是spring容器的两大核心接口,applicationcontext是beanfactory派生而来的,beanfactory包含了各种bean的定义,加载,实例化,依赖注入和生命周期管理。

ApplicationContextBeanFactory
可以同时加载多个配置文件加载一个配置文件
采用立即加载的方式,创建容器时,一口气创建完xml中的所有的bean,因此容器启动时我们就能发现spring配置是否存在问题采用按需加载的方式,根据需要调用getBean()创建对象,因此不能早早发现spring容器配置是否有问题,直到第一次调用getBean()才发现依赖没有注入
适用于单例模式适用于多例模式
因为提前预载入了很多bean实例,所以运行速度比较快,唯一的不足就是如果bean实例很多会很占用内存启动程序会很慢
继承MessageResource,支持国际化

BeanFactory和FactoryBean的区别

BeanFactoryFactoryBean
是spring容器的核心接口,包含了各种bean的定义、加载、实例化、依赖注入、生命周期用来产生其他bean实例

Spring中Bean的作用域有哪几种

  • singleton:默认作用域,容器中只能有一个bean实例;
  • prototype:每次创建都创建一个bean实例,容器中可以有多个bean实例
  • request:每个request请求都会创建一个bean实例,该作用域仅在web的spring applicationcontext情形下有效
  • session:每个session会话都会创建一个bean实例,该作用域仅在web的spring applicaitoncontext情形下有效
  • global-session:所有会话共享一个bena实例,仅在web的spring applicaitoncontext情形下有效

Spring框架中的Bean是线程安全的么?如果线程不安全,那么如何处理?

要分情况讨论:

  • 作用域如果是prototype,每次都创建一个bean实例,容器中可以有多个bean实例,多个线程使用自己的bean实例不存在线程安全问题。
  • 作用域如果是singleton,容器中只能有一个bean实例,多个线程共享一个bean实例就会存在线程安全问题。但是如果这个bean实例是无状态(只执行查询操作)的比如:controller、service、dao这种就是线程安全的;如果bean实例有状态的那就不是线程安全的如:model和view。
  • 最简单的办法就是将singleton变成prototype;或者使用ThreadLocal

Spring如何解决循环依赖问题

循环依赖问题在Spring中主要有三种情况:

  • 通过构造方法进行依赖注入时产生的循环依赖问题。
  • 通过set方法进行依赖注入且是在多例(原型)模式下产生的循环依赖问题。
  • 通过set方法进行依赖注入且是在单例模式下产生的循环依赖问题。

只有通过set()方法且单例模式的循环依赖问题被解决了,其他两种方式在遇到循环依赖问题时都会产生异常因为:

  • 通过构造方法注入依赖,new对象的时候就被堵塞住了;
  • set()且多例模式下的注入依赖,每一次getBean()都会都会产生一个bean实例,这样就会有越来越多的bean实例会导致内存出现问题。

set()且单例模式下的注入依赖,是通过spring的二级缓存和三级缓存解决的(详细原理)

基于注解的自动装配方式

使用@autowired、@resource注解来装配bena实例,在使用@autowired注解之前要在xml中开启注解功能<context:-annotation-config/>,且装配的bean要被注入spring容器中,使用@controller、@service、@repository、@component将bean注入容器中。

  • 如果查询结果刚好为一个,就按照@autowired类型注入到bea中
  • 如果查询的结果不止一个,就按照@autowired根据名称来查找;
  • 如果上述查找的结果为空,那么会抛出异常。解决方法时,使用required=false。

@Autowired和@Resource之间的区别:

  • @Resource一般用在成员变量上,默认是按照bean的id名称来装配注入的。
  • @Autowired一般用在成员变量上,默认是按照类型装配注入的
 - 如果注入类型在ioc中有一个类型,直接按照类型注入
 - 如果注入类型在ioc中有多个类型,按照变量名称去查找bean.id,然后注入
 - 如果注入类型在ioc中找不到,报异常可以使用requeied=false解决

Spring事务的实现方式和实现原理

spring事务的本质是数据库提供对事务的支持,spring自己并不提供事务功能,它只提供对事务管理的统一接口,由各数据库自己实现事务。

Spring事务的种类:

  • 编程式事务:在业务逻辑代码中嵌入事务声明的逻辑代码,通过TransanctionTemplate实现。
  • 声明式事务:在AOP的基础上对方法进行拦截,即在方法之前开启事务,在方法执行结束后提交事务或者回滚事务。

声明式事务的优点:不需要在业务逻辑代码中切入事务逻辑代码,通过在xml中声明事务规则或者通过@transanctional对事务进行声明,减少代码的污染符合spring的低侵入式编程,唯一的不足就是只能作用到方法级别,无法像编程式事务那样作用到代码块级别。

事务的传播行为

事务说明
PROPAGATION_REQUIRED默认传播行为,如果当前没有事务,就创建一个新事务;如果当前存在事务,就加入该事务。
PROPAGATION_REQUIRES_NEW无论当前存不存在事务,都创建新事务进行执行。
PROPAGATION_SUPPORTS如果当前存在事务,就加入该事务;如果当前不存在事务,就以非事务执行
PROPAGATION_NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
PROPAGATION_NESTED如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则按REQUIRED属性执行
PROPAGATION_MANDATORY如果当前存在事务,就加入该事务;如果当前不存在事务,就抛出异常。
PROPAGATION_NEVER以非事务方式执行,如果当前存在事务,则抛出异常。

Spring 框架中都用到了哪些设计模式?

Spring设计模式的详细使用案例可以阅读这篇文章:https://blog.csdn.net/a745233700/article/details/112598471

  • 工厂模式:Spring使用工厂模式,通过BeanFactory和ApplicationContext来创建对象
  • 单例模式:Bean默认为单例模式
  • 策略模式:例如Resource的实现类,针对不同的资源文件,实现了不同方式的资源获取策略
  • 代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术
  • 模板方法:可以将相同部分的代码放在父类中,而将不同的代码放入不同的子类中,用来解决代码重复的问题。比如RestTemplate,
    JmsTemplate, JpaTemplate
  • 适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式,Spring
    MVC中也是用到了适配器模式适配Controller
  • 观察者模式:Spring事件驱动模型就是观察者模式的一个经典应用。
  • 桥接模式:可以根据客户的需求能够动态切换不同的数据源。比如我们的项目需要连接多个数据库,客户在每次访问中根据需要会去访问不同的数据库

ApplicationContext的三个常用实现类

  • ClassPathXmlApplicationContext:它可以加载类路径下的配置文件,要求配置文件必须在类路径下。不在的话,加载不了。(更常用)
    如下图:
    在这里插入图片描述
  • FileSystemXmlApplicationContext:它可以加载磁盘任意路径下的配置文件(必须有访问权限)
  • AnnotationConfigApplicationContext:它是用于读取注解创建容器

spring中的配置文件命名空间和约束

bean名称空间和约束的配置文件bean.xml

基于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>

bean和context名称空间和约束的配置文件bean.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: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/context/spring-context.xsd">
</beans>

bean和aop名称空间和约束的配置文件bean.xml

基于xml配置文件前缀:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">
</beans>

bean、context和aop名称空间和约束的配置文件bean.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:aop="http://www.springframework.org/schema/aop"
       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/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
</beans>

把创建的对象注入spring容器中范例

项目结构:
在这里插入图片描述
配置文件:bean.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">
                        
    <!--把对象的创建交给spring来管理    -->
    <bean id="accountService" class="com.hunan.tabu.service.impl.AccountServiceImpl"></bean>
    <bean id="accountDao" class="com.hunan.tabu.dao.impl.AccountDaoImpl"></bean>
</beans>

client.java:

 public static void main(String[] args) {
        //1.获取核心容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");

        //2.根据id获取Bean对象
        IAccountService as  = (IAccountService)ac.getBean("accountService");
        IAccountDao adao = ac.getBean("accountDao",IAccountDao.class);

        System.out.println(as);
        System.out.println(adao);
    }

运行结果:
在这里插入图片描述

核心容器两个接口ApplicationContext和BeanFactory的区别(重点)

如下代码:bean.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">

    <!--把对象的创建交给spring来管理-->
    <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>

    <bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl"></bean>
</beans>

AccountServiceImpl .java:

public class AccountServiceImpl implements IAccountService {
    public AccountServiceImpl(){
        System.out.println("对象创建了");
    }
}
  • ApplicationContext:(更常用)

1.可以同时加载多个配置文件
2.一读取完配置文件,创建容器时,就会一口气创建配置文件中所有的对象,采用立即加载方式,有利于快速检查spring中是否存在配置错误。
3.适用于单例对象
优点:一开始就可以快速发现spring配置是否存在问题
缺点:一口气创建所有对象,占用内存较大

client.java:

    	//1.获取核心容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.根据id获取Bean对象
        IAccountService as  = (IAccountService)ac.getBean("accountService");
        IAccountDao adao = ac.getBean("accountDao",IAccountDao.class)

在运行完第2行之后,即读取完配置文件后,马上就创建配置文件中AccountServiceImpl类的对象。

运行结果:
在这里插入图片描述

  • BeanFactory:
    1.创建对象时采用的是延迟加载策略,什么时候需要对象的时候,才调用getBean()方法,才开始创建对象,不利于一开始发现spring是否存在配置错误
    2.适用于多例对象
    优点:应用启动时占用资源较少
    缺点:无法在容器创建时就发现spring中是否存在配置问题,也有可能会出现空指针异常。

Client.java:

		 //--------BeanFactory----------
        Resource resource = new ClassPathResource("bean.xml");
        BeanFactory factory = new XmlBeanFactory(resource);
        IAccountService as  = (IAccountService)factory.getBean("accountService");
        System.out.println(as);        

在运行完第4行后,即根据accountService获取对象后,开始创建配置文件中的对象
运行结果:
在这里插入图片描述

spring对bean的管理细节

创建bean的三种方式

  1. 方式一:使用默认构造函数创建对象

在spring的配置文件中使用bean标签,配以id和class属性之后,且没有其他属性和标签时,采用的就是默认构造函数创建bean对象。此时如果类中没有默认构造函数,则对象无法创建。

配置文件bean.xml:

 <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>

AccountServiceImpl.java:

public class AccountServiceImpl implements IAccountService {
	//默认构造函数
    public AccountServiceImpl(){
        System.out.println("对象创建了");
    }
}

运行结果:
center)
但是如下AccountServiceImpl.java无默认构造函数,无法创建bean对象:

public class AccountServiceImpl implements IAccountService {
	
	//因为创建了带参构造函数,所以系统不会再创建默认构造函数,此时类中没有默认构造函数
    public AccountServiceImpl(String name){
        System.out.println("对象创建了");
    }
}
  • 方式二:使用工厂中的普通方法创建对象(使用某个类中的方法创建对象,并存入spring容器)

InstanceFactory .java:

public class InstanceFactory {
    public IAccountService getAccountService(){
        return new AccountServiceImpl();
    }
}

bean.xml:

 <bean id="instanceFactory" class="com.itheima.factory.InstanceFactory"></bean>
 <bean id="accountService" factory-bean="instanceFactory" factory-method="getAccountService"></bean>

运行结果:
在这里插入图片描述
3. 方式三:使用工厂中的静态方法创建对象(使用某个类中的静态方法创建对象,并存入spring容器)

StaticFactory .java:

public class StaticFactory {
    public static IAccountService getAccountService(){
        return new AccountServiceImpl();
    }
}

bean.xml:

 <bean id="accountService" class="com.itheima.factory.StaticFactory" factory-method="getAccountService"></bean>

运行结果:
在这里插入图片描述

bean.xml:

 <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl" scope="prototype"></bean>

client.java:

 public static void main(String[] args) {
        //1.获取核心容器对象
//        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.根据id获取Bean对象
        IAccountService as  = (IAccountService)ac.getBean("accountService");
        IAccountService as2  = (IAccountService)ac.getBean("accountService");
        System.out.println(as);
        System.out.println(as2);

运行结果:
在这里插入图片描述

bean对象的生命周期

  • 单例对象:
    出生:当容器创建时对象出生
    活着:容器还在,对象一直活着
    死亡:容器销毁,对象死亡

AccountServiceImpl .java:

public class AccountServiceImpl implements IAccountService {
    public AccountServiceImpl(){
        System.out.println("对象创建了");
    }
    public void  saveAccount(){
        System.out.println("service中的saveAccount方法执行了。。。");
    }
    public void  init(){
        System.out.println("对象初始化了。。。");
    }
    public void  destroy(){
        System.out.println("对象销毁了。。。");
    }
}

bean.xml:

 <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"
          scope="singleton" init-method="init" destroy-method="destroy"></bean>

client.java:

  public static void main(String[] args) {
        //1.获取核心容器对象
        ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.根据id获取Bean对象
        IAccountService as  = (IAccountService)ac.getBean("accountService");
        as.saveAccount();
        //手动关闭容器
        ac.close();
    }

运行结果:
在这里插入图片描述

  • 多例对象:
    出生:当需要使用对象时,spring框架为我们创建
    活着:当只要在使用时,对象就一直活着
    死亡:当对象长时间不用时,且没有别的对象引用,垃圾回收器回收

bean.xml:

 <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"
          scope="prototype" init-method="init" destroy-method="destroy"></bean>

运行结果:
在这里插入图片描述

标签注入数据的三种方式

  • 方式一:构造函数注入:
    使用的标签:constructor-arg
    标签出现的位置:bean标签的内部
    标签中的属性:
    *******type:指定要注入的数据的数据类型,该数据类型也是构造函数中某些参数的类型
    *******index:指定要注入的数据对应构造函数中指定索引位置的参数赋值。索引的位置是从0开始
    *******name:指定构造函数中指定名称的参数赋值 (常用的)
    以上三个都是用于指定给构造函数中哪个参数赋值=================
    value:为基本类型和String类型的数据赋值
    ref:指定其他的bean类型数据。它指的就是在spring Ioc核心容器中出现过的bean对象

AccountServiceImpl .java:

public class AccountServiceImpl implements IAccountService {
    //如果是经常变化的数据,并不适用于注入的方式
    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;
    }

    public void  saveAccount(){
        System.out.println("service中的saveAccount方法执行了。。。"+name+","+age+","+birthday);
    }
}

bean.xml:

<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
        <constructor-arg name="name" value="泰斯特"></constructor-arg>
        <constructor-arg name="age" value="18"></constructor-arg>
        <constructor-arg name="birthday" ref="now"></constructor-arg>
 </bean>
 
  <!-- 配置一个日期对象 -->
  <bean id="now" class="java.util.Date"></bean>

client.java:

 public static void main(String[] args) {
        //1.获取核心容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.根据id获取Bean对象
        IAccountService as  = (IAccountService)ac.getBean("accountService");
         as.saveAccount();
        }

运行结果:
在这里插入图片描述

  • 方式二:set方法注入(常用方式
    使用的标签:property
    出现的位置:bean标签的内部
    标签的属性:
    ****name: 所调用的set方法后面的名称
    ******value:给基本类型和String类型的数据赋值
    ******ref:指定其他的bean类型数据。它指的就是在spring Ioc核心容器中出现过的bean对象

AccountServiceImpl2 .java:

public class AccountServiceImpl2 implements IAccountService {
    //如果是经常变化的数据,并不适用于注入的方式
    private String name;
    private Integer age;
    private Date birthday;
    
    public void setUserName(String name) {
        this.name = name;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }
    public void  saveAccount(){
        System.out.println("service中的saveAccount方法执行了。。。"+name+","+age+","+birthday);
    }
}

bean.xml:

 <bean id="accountService2" class="com.itheima.service.impl.AccountServiceImpl2">
        <property name="userName" value="TEST" ></property>
        <property name="age" value="21"></property>
        <property name="birthday" ref="now"></property>
 </bean>
 
 <!-- 配置一个日期对象 -->
 <bean id="now" class="java.util.Date"></bean>

运行结果:
在这里插入图片描述

  • 方式三:给集合类型的注入
    给List集合注入的标签:
    list array set
    给Map集合注入的标签:
    map props

AccountServiceImpl3 .java:

public class AccountServiceImpl3 implements IAccountService {
    private String[] myStrs;
    private List<String> myList;
    private Set<String> mySet;
    private Map<String,String> myMap;
    private Properties myProps;

    public void setMyStrs(String[] myStrs) {
        this.myStrs = myStrs;
    }
    public void setMyList(List<String> myList) {
        this.myList = myList;
    }
    public void setMySet(Set<String> mySet) {
        this.mySet = mySet;
    }
    public void setMyMap(Map<String, String> myMap) {
        this.myMap = myMap;
    }
    public void setMyProps(Properties myProps) {
        this.myProps = myProps;
    }
    
    public void  saveAccount(){
        System.out.println(Arrays.toString(myStrs));
        System.out.println(myList);
        System.out.println(mySet);
        System.out.println(myMap);
        System.out.println(myProps);
    }
}

bean.xml:

 <bean id="accountService3" class="com.itheima.service.impl.AccountServiceImpl3">
        <property name="myStrs">
            <set>
                <value>AAA</value>
                <value>BBB</value>
                <value>CCC</value>
            </set>
        </property>

        <property name="myList">
            <array>
                <value>AAA</value>
                <value>BBB</value>
                <value>CCC</value>
            </array>
        </property>

        <property name="mySet">
            <list>
                <value>AAA</value>
                <value>BBB</value>
                <value>CCC</value>
            </list>
        </property>

        <property name="myMap">
            <props>
                <prop key="testC">ccc</prop>
                <prop key="testD">ddd</prop>
            </props>
        </property>

        <property name="myProps">
            <map>
                <entry key="testA" value="aaa"></entry>
            </map>
        </property>
    </bean>

运行结果:
在这里插入图片描述

注解(重点)

用于创建对象的注解

  • @Component
    ***作用地方:类上
    ***作用:把当前类对象存入spring容器中
    ***属性:
    **********value:指定bean的id。当我们不写时,它的默认值是当前类名,且首字母改小写。

bean.xml:

 <!--创建容器时,扫描com.itheima下的包及子包中类和接口的注解-->
 <context:component-scan base-package="com.itheima"></context:component-scan>

AccountServiceImpl .java:

@Component(value = "accountService")
public class AccountServiceImpl implements IAccountService {
    public AccountServiceImpl() {
        System.out.println("对象创建了");
    }
}

client.java:

 public static void main(String[] args) {
        //1.获取核心容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.根据id获取Bean对象
        IAccountService as  = (IAccountService)ac.getBean("accountService");
        System.out.println(as);
  }

运行结果:
在这里插入图片描述

  • @Controller:一般用在表现层

  • @Service:一般用在业务层

  • @Repositrory:一般用在持久层
    以上三个注解他们的作用和属性与@Component是一模一样。他们三个是spring框架为我们提供明确的三层使用的注解,使我们的三层对象更加清晰

注入对象的注解

  • @Autowired:
    ******作用:自动按照类型注入。只要容器中有bean对象类型和要注入的对象类型匹配,就可以注入成功。
    ***** 出现位置: 可以是变量上,也可以是方法上
    ******细节: 在使用注解注入时,set方法就不是必须的了。

  • Resource
    ******作用:直接按照bean的id注入。它可以独立使用
    ******属性:
    ***********name:用于指定bean的id。

spring的IOC容器注入数据原理

单个注入类型匹配:
在这里插入图片描述多个注入类型匹配:
在这里插入图片描述运行结果:

NoUniqueBeanDefinitionException: No qualifying bean of type 'com.itheima.dao.IAccountDao' available: expected single matching bean but found 2: accountDao1,accountDao2

多个注入类型匹配解决方案:
在这里插入图片描述
匹配原则:
当注入的数据类型和容器中多个bean对象类型匹配时,使用变量名和bean对象中id进行匹配。

AccountServiceImpl .java:
在这里插入图片描述

AccountDaoImpl .java:

@Repository("accountDao1")
public class AccountDaoImpl implements IAccountDao {
    public  void saveAccount(){
        System.out.println("保存了账户1111111111111");
    }
}

AccountDaoImpl2 .java:

@Repository("accountDao2")
public class AccountDaoImpl2  implements IAccountDao {
    public  void saveAccount(){
        System.out.println("保存了账户2222222222222");
    }
}

运行结果:
在这里插入图片描述
使用@Qualifier注解解决多个类型注入:

  • @Qualifier:
    ***作用:给类成员注入时不能单独使用,要和@Autowired联合使用,但是给方法参数注入时可以。
    *** 属性:
    *********value:指定注入bean的id。

AccountServiceImpl .java:

@Component(value = "accountSer")
public class AccountServiceImpl implements IAccountService {
    @Autowired
    @Qualifier("accountDao1")
    private IAccountDao accountDao;

    public AccountServiceImpl() {
        System.out.println("对象创建了");
    }
    public void  saveAccount(){
        accountDao.saveAccount();
    }
}

AccountDaoImpl .java:

@Repository("accountDao1")
public class AccountDaoImpl implements IAccountDao {
    public  void saveAccount(){
        System.out.println("保存了账户1111111111111");
    }
}
  • Resource
    ******作用:直接按照bean的id注入。它可以独立使用
    ******属性:
    ***********name:用于指定bean的id。
@Component(value = "accountSer")
public class AccountServiceImpl implements IAccountService {
        @Resource(name = "accountDao2")
        private IAccountDao accountDao;

        public AccountServiceImpl() {
            System.out.println("对象创建了");
        }
        public void  saveAccount(){
            accountDao.saveAccount();
        }
    }

AccountDaoImpl2.java:

@Repository("accountDao2")
public class AccountDaoImpl2  implements IAccountDao {
    public  void saveAccount(){
        System.out.println("保存了账户2222222222222");
    }

以上三个注入都只能注入其他bean类型的数据,而基本类型和String类型无法使用上述注解实现。
另外,集合类型的注入只能通过XML来实现。

  • @Value
    ******作用:注入基本类型和String类型的数据
    ******属性:
    *********value:指定数据的值。它可以使用spring中SpEL(也就是spring的el表达式)

用于改变作用范围的

  • @ Scope
    *******作用:指定bean的作用范围
    *******使用地方:类上/方法上
    *******属性:
    ************value:范围的取值。常用取值:singleton—单例,prototype多例(默认是单例)

AccountServiceImpl .java:

@Scope("prototype")
@Component(value = "accountSer")
public class AccountServiceImpl implements IAccountService {
        @Autowired
        @Qualifier("accountDao1")
        private IAccountDao accountDao;

        public AccountServiceImpl() {
            System.out.println("对象创建了");
        }
        public void  saveAccount(){
            accountDao.saveAccount();
        }
    }

AccountDaoImpl .java:

@Repository("accountDao1")
public class AccountDaoImpl implements IAccountDao{
    public  void saveAccount(){
        System.out.println("保存了账户1111111111111");
    }
}

client.java:

   //1.获取核心容器对象
   ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
   //2.根据id获取Bean对象
    IAccountService as  = (IAccountService)ac.getBean("accountSer");
    IAccountService as2  = (IAccountService)ac.getBean("accountSer");
   System.out.println(as == as2);

运行结果:
在这里插入图片描述

案例分析

基于xml的spring的IOC案例

AccountServiceImpl .java:

public class AccountServiceImpl implements IAccountService{
    private IAccountDao accountDao;
    
    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }
}

AccountDaoImpl .java:

public class AccountDaoImpl implements IAccountDao {
    private QueryRunner runner;

    public void setRunner(QueryRunner runner) {
        this.runner = runner;
    }
}

bean.xml:

 	<!-- 配置Service -->
    <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
        <!-- 注入dao -->
        <property name="accountDao" ref="accountDao"></property>
    </bean>

    <!--配置Dao对象-->
    <bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl">
        <!-- 注入QueryRunner -->
        <property name="runner" ref="runner"></property>
    </bean>

    <!--配置QueryRunner-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
        <!--注入数据源-->
        <constructor-arg name="ds" ref="dataSource"></constructor-arg>
    </bean>

    <!-- 配置数据源 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--连接数据库的必备信息-->
        <property name="driverClass" value="com.mysql.cj.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mydb3?characterEncoding=utf8&amp;useSSL=false&amp;serverTimezone=GMT%2B8"></property>
        <property name="user" value="root"></property>
        <property name="password" value="636895"></property>
    </bean>

分析:创建业务层service对象,往其中注入持久层对象dao,创建持久层对象,往其中注入QueryRunner对象,创建QueryRunner对象,因为它是单例,防止多个dao进行访问,产生线程安全问题,设置成多例模式,往其中注入数据源对象dataSource,创建数据源对象,往其中注入数据库连接信息。

如下图:
在这里插入图片描述

基于注解的Spring的IOC案例

使用的注解(重点)

  1. @Configuration
    ********作用:指定当前类是一个配置类,相当于bean.xml
    ********细节:当该类的字节码文件,作为AnnotationConfigApplicationContext创建容器的参数时,该注解可以不写。因为它==使用了@Configuration
    ********属性:proxyBeanMethods,默认为true,保证依赖的组件始终是单实例的

有如下2种方式:

方式一:使用字节码文件的方式,SpringConfiguration作为主配置类,JdbcConfig作为次配置类,全部使用字节码文件作为参数,传入到AnnotationConfigApplicationContext创建对象中去,spring会自动识别他们是一个配置类。
SpringConfiguration.java:

//@Configuration
@ComponentScan("com.itheima")
public class SpringConfiguration {}

JdbcConfig.java:

public class JdbcConfig {

    @Bean(name="runner")
    @Scope("prototype")
    public QueryRunner createQueryRunner(@Qualifier("ds2") DataSource dataSource){
        return new QueryRunner(dataSource);
    }

    @Bean(name="ds2")
    public DataSource createDataSource(){
        try {
            ComboPooledDataSource ds = new ComboPooledDataSource();
            ds.setDriverClass(driver);
            ds.setJdbcUrl(url);
            ds.setUser(username);
            ds.setPassword(password);
            return ds;
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }
}

测试类:

ApplicationContext ac=new AnnotationConfigApplicationContext(SpringConfiguration.class,JdbcConfig.class);

方式二:使用扫描包的方式,这时JdbcConfig使用@Configuration注解,标志位它是一个配置类,才会对该类下的注解进行扫描,否则,无法扫描该类下的注解。
SpringConfiguration.java:

//@Configuration
@ComponentScan({"com.itheima","config"})
public class SpringConfiguration {}

config包下
JdbcConfig.java:

@Configuration
public class JdbcConfig {}

测试类:

ApplicationContext ac=new AnnotationConfigApplicationContext(SpringConfiguration.class);
  1. ComponentScan
    ********作用:指定spring在创建容器时要扫描的包
    ********属性:
    *************value:它和basePackages的作用是一样的,都是用于指定创建容器时要扫描的包。 我们使用此注解就等同于在xml中配置了:
    <context:component-scan base-package=“com.itheima”></context:component-scan>
  2. Bean
    ******作用:把当前方法的返回值作为bean对象,并存入spring的ioc容器中
    ******属性:
    ********** name:等于bean的id。当不写时,默认值是当前方法的名称
    ******细节:
    ***********当使用@Bean注解配置方法时,如果方法有参数,spring框架会去容器中查找有没有可用的bean对象。查找的方式和Autowired注解的作用是一样的
    在这里插入图片描述没有注入DataSource对象到Spring的Ioc的容器中,所以报错:
    在这里插入图片描述多个类型注入时:
    匹配原则:当注入的数据类型和容器中多个bean对象类型匹配时,使用变量名和bean对象中id进行匹配。
    在这里插入图片描述
    使用@Qualifier注解解决:
    @Qulifier注解作为方法参数使用:
public class JdbcConfig {
    /**
     * 用于创建一个QueryRunner对象
     * @param dataSource
     * @return
     */
    @Bean(name="runner")
    @Scope("prototype")
    public QueryRunner createQueryRunner(@Qualifier("ds2") DataSource dataSource){
        return new QueryRunner(dataSource);
    }

    /**
     * 创建数据源对象
     * @return
     */
    @Bean(name="ds2")
    public DataSource createDataSource(){
        try {
            ComboPooledDataSource ds = new ComboPooledDataSource();
            ds.setDriverClass(driver);
            ds.setJdbcUrl(url);
            ds.setUser(username);
            ds.setPassword(password);
            return ds;
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }

    @Bean(name="ds1")
    public DataSource createDataSource1(){
        try {
            ComboPooledDataSource ds = new ComboPooledDataSource();
            ds.setDriverClass(driver);
            ds.setJdbcUrl("jdbc:mysql://localhost:3306/eesy02");
            ds.setUser(username);
            ds.setPassword(password);
            return ds;
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }

  1. @Import
    ******作用:导入其他配置类的字节码文件,将该类的实例注入springIOC核心容器中,且bean的id就是该类的全限定类名。当我们使用Import的注解之后,有Import注解的类就父配置类,而导入的都是子配置类
    ****** 属性:
    ************value:指定其他配置类的字节码文件。

既不想加入扫描包,也不想加入@Configuration注解,可以使用@Import注解:
SpringConfiguration.java:

//@Configuration
@ComponentScan("com.itheima")
@Import(JdbcConfig.class)
public class SpringConfiguration {}

config包下
JdbcConfig.java:

public class JdbcConfig {}

测试类:

ApplicationContext ac=new AnnotationConfigApplicationContext(SpringConfiguration.class);
  1. @PropertySource
    ********属性:
    **************value:指定properties配置文件的位置
    ************* 关键字:classpath,表示类路径下

  2. @ImportResource
    作用地方:作用在配置类上
    作用:导入spring的配置文件(如:bean.xml)让springboot加载spring的xml配置文件。
    属性:locations表示xml配置文件路径,关键字classpth表示在当前类路径下

@ImportResource(locations={"classpath:bean.xml"})   
public class MyConfig{
}

完整案例:
AccountDaoImpl.java:

@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {
    @Autowired
    private QueryRunner runner;

    @Override
    public List<Account> findAllAccount() {
        try{
            return runner.query("select * from account",new BeanListHandler<Account>(Account.class));
        }catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public Account findAccountById(Integer accountId) {
        try{
            return runner.query("select * from account where id = ? ",new BeanHandler<Account>(Account.class),accountId);
        }catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void saveAccount(Account account) {
        try{
            runner.update("insert into account(name,money)values(?,?)",account.getName(),account.getMoney());
        }catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void updateAccount(Account account) {
        try{
            runner.update("update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
        }catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void deleteAccount(Integer accountId) {
        try{
            runner.update("delete from account where id=?",accountId);
        }catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

AccountServiceImpl.java:

@Service("accountService")
public class AccountServiceImpl implements IAccountService{
    @Autowired
    private IAccountDao accountDao;

    @Override
    public List<Account> findAllAccount() {
        return accountDao.findAllAccount();
    }

    @Override
    public Account findAccountById(Integer accountId) {
        return accountDao.findAccountById(accountId);
    }

    @Override
    public void saveAccount(Account account) {
        accountDao.saveAccount(account);
    }

    @Override
    public void updateAccount(Account account) {
        accountDao.updateAccount(account);
    }

    @Override
    public void deleteAccount(Integer acccountId) {
        accountDao.deleteAccount(acccountId);
    }
}

jdbcConfig.properties:

#key可以随便设置,value有规定写法
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/eesy
jdbc.username=root
jdbc.password=1234

SpringConfiguration.java:

@ComponentScan("com.itheima")
@Import(JdbcConfig.class)
@PropertySource("classpath:jdbcConfig.properties")
public class SpringConfiguration {}

JdbcConfig.java:

public class JdbcConfig {

    @Value("${jdbc.driver}")
    private String driver;

    @Value("${jdbc.url}")
    private String url;

    @Value("${jdbc.username}")
    private String username;

    @Value("${jdbc.password}")
    private String password;

    /**
     * 用于创建一个QueryRunner对象
     * @param dataSource
     * @return
     */
    @Bean(name="runner")
    @Scope("prototype")
    public QueryRunner createQueryRunner(@Qualifier("ds2") DataSource dataSource){
        return new QueryRunner(dataSource);
    }

    /**
     * 创建数据源对象
     * @return
     */
    @Bean(name="ds2")
    public DataSource createDataSource(){
        try {
            ComboPooledDataSource ds = new ComboPooledDataSource();
            ds.setDriverClass(driver);
            ds.setJdbcUrl(url);
            ds.setUser(username);
            ds.setPassword(password);
            return ds;
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }

    @Bean(name="ds1")
    public DataSource createDataSource1(){
        try {
            ComboPooledDataSource ds = new ComboPooledDataSource();
            ds.setDriverClass(driver);
            ds.setJdbcUrl("jdbc:mysql://localhost:3306/eesy02");
            ds.setUser(username);
            ds.setPassword(password);
            return ds;
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }
}

测试类AccountServiceTest.java:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfiguration.class)
public class AccountServiceTest {
    @Autowired
    private IAccountService as = null;

    @Test
    public void testFindAll() {
        //3.执行方法
        List<Account> accounts = as.findAllAccount();
        for(Account account : accounts){
            System.out.println(account);
        }
    }

    @Test
    public void testFindOne() {
        //3.执行方法
        Account account = as.findAccountById(1);
        System.out.println(account);
    }

    @Test
    public void testSave() {
        Account account = new Account();
        account.setName("test anno");
        account.setMoney(12345f);
        //3.执行方法
        as.saveAccount(account);
    }

    @Test
    public void testUpdate() {
        //3.执行方法
        Account account = as.findAccountById(4);
        account.setMoney(23456f);
        as.updateAccount(account);
    }

    @Test
    public void testDelete() {
        //3.执行方法
        as.deleteAccount(4);
    }
}

spring整合junit单元测试(重点)

1.导入spring整合junit的依赖:

 <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.0.2.RELEASE</version>
 </dependency>

2.使用@RunWith(SpringJUnit4ClassRunner.class)
***********作用:创建spring的IOC核心容器
3.使用@ContextConfiguration
***********作用:告知spring,spring的ioc核心容器创建的是基于xml配置文件还是注解类,并且说明位置
***********属性:
******************locations:指定xml配置文件的位置,加上classpath关键字,表示在类路径下
*******************classes:指定注解类所在的位置
4.细节:当我们使用spring 5.x版本的时候,要求junit的依赖必须是4.12及以上

AccountServiceTest.java:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfiguration.class)
public class AccountServiceTest {
    @Autowired
    private IAccountService as = null;
}

Spring中的AOP面向切面编程有了解吗?(重点)

aop指的是在一个方法之前或者之后做一些处理,包括:日志记录、权限控制等,可以在业务逻辑中将功能代码抽离出来。

aop中常用的术语

  • 连接点(joinpoint):类中可以被增强的方法
  • 切入点(pointcut):类中哪些方法可以被增强,然后就对它进行拦截
  • 通知/增强:拦截所做的具体事情
  • 切面:切入点和通知的结合

spring基于XML的AOP配置

步骤:
1.把通知Bean也交给spring来管理
2.使用aop:config标签开始AOP的配置
3.使用aop:aspect标签开始切面配置
*********id属性:给切面提供一个唯一标识
*********ref属性:指定通知类bean的Id。
4.在aop:aspect标签的内部使用对应标签来配置通知的类型
**我们现在示例是让printLog方法在切入点方法执行之前执行:所以是前置通知
*******aop:before:表示配置前置通知
*********************method属性:指定Logger类中哪个方法是前置通知
*********************pointcut属性:指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强

切入点表达式的写法:
*********关键字:execution(表达式)
*********表达式:访问修饰符 返回值 包名.包名.包名…类名.方法名(参数列表)
*********标准的表达式写法:
public void com.itheima.service.impl.AccountServiceImpl.saveAccount()

完整示例:
AccountServiceImpl.java:

public class AccountServiceImpl implements IAccountService{
    @Override
    public void saveAccount() {
        System.out.println("执行了保存");
    }

    @Override
    public void updateAccount(int i) {
        System.out.println("执行了更新"+i);
    }

    @Override
    public int deleteAccount() {
        System.out.println("执行了删除");
        return 0;
    }
}

Logger.java:

/**
 * 用于记录日志的工具类,它里面提供了公共的代码
 */
public class Logger {
    /**
     * 用于打印日志:计划让其在切入点方法执行之前执行(切入点方法就是业务层方法)
     */
    public  void printLog(){
        System.out.println("Logger类中的pringLog方法开始记录日志了。。。");
    }
}

bean.xml:

<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>

<!-- 配置Logger-->
<bean id="logger" class="com.itheima.utils.Logger"></bean>

 <!--配置AOP-->
    <aop:config>
        <!--配置切面 -->
        <aop:aspect id="logAdvice" ref="logger">
            <!-- 配置通知的类型,并且建立通知方法和切入点方法的关联-->
            <aop:before method="printLog" pointcut="execution(public void com.itheima.service.impl.AccountServiceImpl.saveAccount())"></aop:before>
        </aop:aspect>
    </aop:config>

测试类:

 public static void main(String[] args) {
        //1.获取容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.获取对象
        IAccountService as = (IAccountService)ac.getBean("accountService");
        //3.执行方法
        as.saveAccount();
    }

运行结果:(在业务层之前执行了)
在这里插入图片描述
切入点表达式的多种写法:

  1. 访问修饰符可以省略
  2. 返回值可以使用通配符,表示任意返回值
  3. 包名可以使用通配符,表示任意包。但是有几级包,就需要写几个*。另外包名也可以使用..表示当前包及其子包
  4. 类名和方法名都可以使用*来代替
  5. 参数列表:
    基本类型直接写名称 如: int
    引用类型写包名.类名的方式 如:java.lang.String
    可以使用..表示有无参数均可

实际开发中切入点表达式的通常写法:
切到业务层实现类下的所有方法:
*com.itheima.service.impl.*.*(..)

对业务成所有方法进行了增强:
bean.xml:

<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>

 <!-- 配置Logger-->
    <bean id="logger" class="com.itheima.utils.Logger"></bean>

    <!--配置AOP-->
    <aop:config>
        <!--配置切面 -->
        <aop:aspect id="logAdvice" ref="logger">
            <!-- 配置通知的类型,并且建立通知方法和切入点方法的关联-->
            <aop:before method="printLog" pointcut="execution(* com.itheima.service.impl.*.*(..))"></aop:before>
        </aop:aspect>
    </aop:config>

测试类:

 public static void main(String[] args) {
        //1.获取容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.获取对象
        IAccountService as = (IAccountService)ac.getBean("accountService");
        //3.执行方法
        as.saveAccount();
    }

运行结果:
在这里插入图片描述

四种通知类型

基于xml

AccountServiceImpl.java:

public class AccountServiceImpl implements IAccountService{
    @Override
    public void saveAccount() {
        System.out.println("执行了保存");
//        int i=1/0;
    }
}

Logger.java:

public class Logger {
    /**
     * 前置通知
     */
    public  void beforePrintLog(){
        System.out.println("前置通知Logger类中的beforePrintLog方法开始记录日志了。。。");
    }
    /**
     * 后置通知
     */
    public  void afterReturningPrintLog(){
        System.out.println("后置通知Logger类中的afterReturningPrintLog方法开始记录日志了。。。");
    }
    /**
     * 异常通知
     */
    public  void afterThrowingPrintLog(){
        System.out.println("异常通知Logger类中的afterThrowingPrintLog方法开始记录日志了。。。");
    }
    /**
     * 最终通知
     */
    public  void afterPrintLog(){
        System.out.println("最终通知Logger类中的afterPrintLog方法开始记录日志了。。。");
    }
}

bean.xml:

    <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>

    <!-- 配置Logger-->
    <bean id="logger" class="com.itheima.utils.Logger"></bean>

    <!--配置AOP-->
    <aop:config>
        <!--配置切面 -->
        <aop:aspect id="logAdvice" ref="logger">
            <!-- 配置前置通知:在切入点方法执行之前执行
            <aop:before method="beforePrintLog" pointcut="execution(* com.itheima.service.impl.*.*(..))" ></aop:before>-->

            <!-- 配置后置通知:在切入点方法正常执行之后值。后置通知和异常通知永远只能执行一个
            <aop:after-returning method="afterReturningPrintLog" pointcut="execution(* com.itheima.service.impl.*.*(..))"></aop:after-returning>-->

            <!-- 配置异常通知:在切入点方法执行产生异常之后执行。后置通知和异常通知永远只能执行一个
            <aop:after-throwing method="afterThrowingPrintLog" ppointcut="execution(* com.itheima.service.impl.*.*(..))"></aop:after-throwing>-->

            <!-- 配置最终通知:无论切入点方法是否正常执行它都会在其后面执行
            <aop:after method="afterPrintLog" pointcut="execution(* com.itheima.service.impl.*.*(..))"></aop:after>-->
        </aop:aspect>
    </aop:config>

测试类:

 public static void main(String[] args) {
        //1.获取容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.获取对象
        IAccountService as = (IAccountService)ac.getBean("accountService");
        //3.执行方法
        as.saveAccount();
    }

运行结果:
在这里插入图片描述

xml的aop事务控制(案例)

AccountServiceImpl.java:

public class AccountServiceImpl implements IAccountService{

    private IAccountDao accountDao;

    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }

    @Override
    public Account findAccountById(Integer accountId) {
        return accountDao.findAccountById(accountId);
    }

    @Override
    public void transfer(String sourceName, String targetName, Float money) {
        System.out.println("transfer....");
            //2.1根据名称查询转出账户
            Account source = accountDao.findAccountByName(sourceName);
            //2.2根据名称查询转入账户
            Account target = accountDao.findAccountByName(targetName);
            //2.3转出账户减钱
            source.setMoney(source.getMoney()-money);
            //2.4转入账户加钱
            target.setMoney(target.getMoney()+money);
            //2.5更新转出账户
            accountDao.updateAccount(source);

//            int i=1/0;

            //2.6更新转入账户
            accountDao.updateAccount(target);
    }
}

AccountDaoImpl.java:

public class AccountDaoImpl implements IAccountDao {

    private QueryRunner runner;
    private ConnectionUtils connectionUtils;

    public void setRunner(QueryRunner runner) {
        this.runner = runner;
    }

    public void setConnectionUtils(ConnectionUtils connectionUtils) {
        this.connectionUtils = connectionUtils;
    }

    @Override
    public List<Account> findAllAccount() {
        try{
            return runner.query(connectionUtils.getThreadConnection(),"select * from acount",new BeanListHandler<Account>(Account.class));
        }catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public Account findAccountById(Integer accountId) {
        try{
            return runner.query(connectionUtils.getThreadConnection(),"select * from acount where id = ? ",new BeanHandler<Account>(Account.class),accountId);
        }catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void saveAccount(Account account) {
        try{
            runner.update(connectionUtils.getThreadConnection(),"insert into acount(name,balance)values(?,?)",account.getName(),account.getMoney());
        }catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void updateAccount(Account account) {
        try{
            runner.update(connectionUtils.getThreadConnection(),"update acount set name=?,balance=? where id=?",account.getName(),account.getMoney(),account.getId());
        }catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void deleteAccount(Integer accountId) {
        try{
            runner.update(connectionUtils.getThreadConnection(),"delete from acount where id=?",accountId);
        }catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public Account findAccountByName(String accountName) {
        try{
            List<Account> accounts = runner.query(connectionUtils.getThreadConnection(),"select * from acount where name = ? ",new BeanListHandler<Account>(Account.class),accountName);
            if(accounts == null || accounts.size() == 0){
                return null;
            }
            if(accounts.size() > 1){
                throw new RuntimeException("结果集不唯一,数据有问题");
            }
            return accounts.get(0);
        }catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

TransactionManager.java:

public class TransactionManager {

    private ConnectionUtils connectionUtils;

    public void setConnectionUtils(ConnectionUtils connectionUtils) {
        this.connectionUtils = connectionUtils;
    }

    /**
     * 开启事务
     */
    public  void beginTransaction(){
        try {
            connectionUtils.getThreadConnection().setAutoCommit(false);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 提交事务
     */
    public  void commit(){
        try {
            connectionUtils.getThreadConnection().commit();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 回滚事务
     */
    public  void rollback(){
        try {
            connectionUtils.getThreadConnection().rollback();
        }catch (Exception e){
            e.printStackTrace();
        }
    }


    /**
     * 释放连接
     */
    public  void release(){
        try {
            connectionUtils.getThreadConnection().close();//还回连接池中
            connectionUtils.removeConnection();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

ConnectionUtils.java:

public class ConnectionUtils {

    private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();

    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /**
     * 获取当前线程上的连接
     * @return
     */
    public Connection getThreadConnection() {
        try{
            //1.先从ThreadLocal上获取
            Connection conn = tl.get();
            //2.判断当前线程上是否有连接
            if (conn == null) {
                //3.从数据源中获取一个连接,并且存入ThreadLocal中
                conn = dataSource.getConnection();
                tl.set(conn);
            }
            //4.返回当前线程上的连接
            return conn;
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }

    /**
     * 把连接和线程解绑
     */
    public void removeConnection(){
        tl.remove();
    }
}

bean.xml:

  <!-- 配置Service -->
    <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
        <!-- 注入dao -->
        <property name="accountDao" ref="accountDao"></property>
    </bean>

    <!--配置Dao对象-->
    <bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl">
        <!-- 注入QueryRunner -->
        <property name="runner" ref="runner"></property>
        <!-- 注入ConnectionUtils -->
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>

    <!--配置QueryRunner-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean>

    <!-- 配置数据源 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--连接数据库的必备信息-->
        <property name="driverClass" value="com.mysql.cj.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mydb3?serverTimezone=GMT%2B8&amp;useUnicode=true&amp;characterEncoding=utf-8&amp;useSSL=false"></property>
        <property name="user" value="root"></property>
        <property name="password" value="636895"></property>
    </bean>

    <!-- 配置Connection的工具类 ConnectionUtils -->
    <bean id="connectionUtils" class="com.itheima.utils.ConnectionUtils">
        <!-- 注入数据源-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!-- 配置事务管理器-->
    <bean id="txManager" class="com.itheima.utils.TransactionManager">
        <!-- 注入ConnectionUtils -->
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>

    <!--配置aop-->
    <aop:config>
        <!--配置通用切入点表达式-->
        <aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"></aop:pointcut>
        <aop:aspect id="txAdvice" ref="txManager">
            <!--配置前置通知:开启事务-->
            <aop:before method="beginTransaction" pointcut-ref="pt1"></aop:before>
            <!--配置后置通知:提交事务-->
            <aop:after-returning method="commit" pointcut-ref="pt1"></aop:after-returning>
            <!--配置异常通知:回滚事务-->
            <aop:after-throwing method="rollback" pointcut-ref="pt1"></aop:after-throwing>
            <!--配置最终通知:释放连接-->
            <aop:after method="release" pointcut-ref="pt1"></aop:after>
        </aop:aspect>
    </aop:config>

测试类:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccountServiceTest {

    @Autowired
    private  IAccountService as;

    @Test
    public  void testTransfer(){
        as.transfer("张无忌","谢逊",100f);

    }

}

运行结果:
注释 int i=1/0; 转账失败,事务进行回滚
放开注释,成功转账
在这里插入图片描述

基于xml配置环绕通知

AccountServiceImpl.java:

public class AccountServiceImpl implements IAccountService{
    @Override
    public void saveAccount() {
        System.out.println("执行了保存");
//        int i=1/0;
    }
}

bean.xml:

    <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>
    
 	<!-- 配置Logger-->
    <bean id="logger" class="com.itheima.utils.Logger"></bean>

    <!--配置AOP-->
    <aop:config>
        <!---配置通用切入点表达式->
        <aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"></aop:pointcut>
        <!--配置切面 -->
        <aop:aspect id="logAdvice" ref="logger">
            <!-- 配置环绕通知 详细的注释请看Logger类中-->
            <aop:around method="aroundPringLog" pointcut-ref="pt1"></aop:around>
        </aop:aspect>
    </aop:config>

Logger.java:
Spring框架为我们提供了一个接口:ProceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于明确调用切入点方法,因此切入点方法和通知方法都会执行。

public class Logger {
 public Object aroundPringLog(ProceedingJoinPoint pjp){
        Object rtValue = null;
        try{
            Object[] args = pjp.getArgs();//得到方法执行所需的参数

            System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。前置");

            rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)

            System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。后置");

            return rtValue;
        }catch (Throwable t){
            System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。异常");
            throw new RuntimeException(t);
        }finally {
            System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。最终");
        }
    }
}

测试类:

public class AOPTest {
    public static void main(String[] args) {
        //1.获取容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.获取对象
        IAccountService as = (IAccountService)ac.getBean("accountService");
        //3.执行方法
        as.saveAccount();
    }

运行结果:
在这里插入图片描述

基于注解配置环绕通知

注解(重点)

  • @Aspect
    作用:表示当前类是一个切面类
    使用地方:当前类上
  • @EnableAspectJAutoProxy
    作用:开启spring对注解aop的支持
  • @Pointcut
    作用:联结切入点方法和通知方法
    使用地方:方法上
    关键字:execution(表达式)
  • @Before
    作用:前置通知
    使用地方:方法上
  • @AfterReturning
    作用:后置通知
    使用地方:方法上
  • @AfterThrowing
    作用:异常通知
    使用地方:方法上
  • @After
    作用:最终通知
    使用地方:方法上
  • @Around
    作用:环绕通知
    使用地方:方法上

AccountServiceImpl.java:

@Service("accountService")
public class AccountServiceImpl implements IAccountService{

    @Override
    public void saveAccount() {
        System.out.println("执行了保存");
//        int i=1/0;
    }
}

通知类Logger.java:

@Component("logger")
@Aspect//表示当前类是一个切面类
public class Logger {
    @Pointcut("execution(* com.itheima.service.impl.*.*(..))")
    private void pt1(){}
    
 	@Around("pt1()")
    public Object aroundPringLog(ProceedingJoinPoint pjp){
        Object rtValue = null;
        try{
            Object[] args = pjp.getArgs();//得到方法执行所需的参数

            System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。前置");

            rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)

            System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。后置");

            return rtValue;
        }catch (Throwable t){
            System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。异常");
            throw new RuntimeException(t);
        }finally {
            System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。最终");
        }
    }
}

配置类SpringConfig.java:

//@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
public class SpringConfig {
}

测试类:

public static void main(String[] args) {
        //1.获取容器
//        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfig.class);
        //2.获取对象
        IAccountService as =ac.getBean("accountService",IAccountService.class);
        System.out.println(as);
        //3.执行方法
        as.saveAccount();
    }

运行结果:
在这里插入图片描述

Spring中的JdbcTemplate

JdbcTemplate的作用

spring提供和数据库进行交互,实现对表的CRUD操作

如何创建对象

常用方法

Spring的事务有了解吗?(重点)

spring的事务包括编程式事务控制和声明式事务控制两种。

  • 编程式事务控制:通过TransactionTemplate实现。
  • 声明式事务控制:建立在aop基础上,通过aop的功能,对目标方法进行拦截,将进行事务控制的代码插入到拦截方法中,也就是在目标方法前开启事务,在目标方法执行结束后提交事务或者回滚事务。
  • 声明式事务控制的优点:不需要在业务逻辑代码中掺杂进行事务控制的代码,只需要通过配置文件或者@Transactionl注解的方式将事务控制加入到业务逻辑代码中。

两种事务如何选择?(重点)

声明式事务控制优于编程式事务控制,它符合spring倡导的非侵入式开发方式,使业务逻辑代码不受污染,唯一不足就是声明式事务控制只能进行到方法级别,编程式事务控制可以作用到代码块级别

基于xml的声明式事务控制

步骤:

  1. 配置事务管理器
  2. 配置事务通知
    使用tx:advice标签配置事务通知
    属性:
    id:给事务通知起一个唯一标识
    transaction-manager:给事务通知提供一个事务管理器引用
  3. 配置AOP中的通用切入点表达式
  4. 建立事务通知和切入点表达式的对应关系
  5. 配置事务的属性
    是在事务通知tx:advice标签的内部
    使用tx:attributes标签,在其内部使用tx:method标签
    属性:
    name:对哪个方法进行
    isolation:指定事务的隔离级别。默认值是DEFAULT,表示使用数据库的默认隔离级别。
    propagation:指定事务的传播行为。默认值是REQUIRED,表示一定会有事务,增删改的选择。查询方法可以选择SUPPORTS。
    read-only:指定事务是否只读。只有查询方法才能设置为true。默认值是false,表示读写。
    timeout:指定事务的超时时间,默认值是-1,表示永不超时。如果指定了数值,以秒为单位。
    rollback-for:指定一个异常,当产生该异常时,事务回滚,产生其他异常时,事务不回滚。没有给值,表示任何异常都回滚。
    no-rollback-for:指定一个异常,当产生该异常时,事务不回滚,产生其他异常时事务回滚。没有给值,表示任何异常都回滚。

案例:
AccountServiceImpl.java:

public class AccountServiceImpl implements IAccountService{

    private IAccountDao accountDao;

    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }

    @Override
    public Account findAccountById(Integer accountId) {
        return accountDao.findAccountById(accountId);
    }

    @Override
    public void transfer(String sourceName, String targetName, Float money) {
        System.out.println("transfer....");
            //2.1根据名称查询转出账户
            Account source = accountDao.findAccountByName(sourceName);
            //2.2根据名称查询转入账户
            Account target = accountDao.findAccountByName(targetName);
            //2.3转出账户减钱
            source.setMoney(source.getMoney()-money);
            //2.4转入账户加钱
            target.setMoney(target.getMoney()+money);
            //2.5更新转出账户
            accountDao.updateAccount(source);

            int i=1/0;

            //2.6更新转入账户
            accountDao.updateAccount(target);
    }
}

bean.xml:

 	<!-- 1.配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    
 	<!-- 2.配置事务的通知-->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <!-- 5.配置事务的属性
                isolation:用于指定事务的隔离级别。默认值是DEFAULT,表示使用数据库的默认隔离级别。
                propagation:用于指定事务的传播行为。默认值是REQUIRED,表示一定会有事务,增删改的选择。查询方法可以选择SUPPORTS。
                read-only:用于指定事务是否只读。只有查询方法才能设置为true。默认值是false,表示读写。
                timeout:用于指定事务的超时时间,默认值是-1,表示永不超时。如果指定了数值,以秒为单位。
                rollback-for:用于指定一个异常,当产生该异常时,事务回滚,产生其他异常时,事务不回滚。没有默认值。表示任何异常都回滚。
                no-rollback-for:用于指定一个异常,当产生该异常时,事务不回滚,产生其他异常时事务回滚。没有默认值。表示任何异常都回滚。
        -->
        <tx:attributes>
            <!--*表示所有方法-->
            <tx:method name="*" propagation="REQUIRED" read-only="false"/>
            <!--find*表示所有查询方法   看自己怎么写-->
            <tx:method name="find*" propagation="SUPPORTS" read-only="true"></tx:method>
        </tx:attributes>
    </tx:advice>
    
 <!-- 配置aop-->
    <aop:config>
        <!--3. 配置切入点表达式-->
        <aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"></aop:pointcut>
        <!--4.建立切入点表达式和事务通知的对应关系 -->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"></aop:advisor>
    </aop:config>

AccountDaoImpl.java:

public class AccountDaoImpl extends JdbcDaoSupport implements IAccountDao {
    @Override
    public Account findAccountById(Integer accountId) {
        List<Account> accounts = super.getJdbcTemplate().query("select * from acount where id = ?",new BeanPropertyRowMapper<Account>(Account.class),accountId);
        return accounts.isEmpty()?null:accounts.get(0);
    }

    @Override
    public Account findAccountByName(String accountName) {
        List<Account> accounts = super.getJdbcTemplate().query("select * from acount where name = ?",new BeanPropertyRowMapper<Account>(Account.class),accountName);
        if(accounts.isEmpty()){
            return null;
        }
        if(accounts.size()>1){
            throw new RuntimeException("结果集不唯一");
        }
        return accounts.get(0);
    }

    @Override
    public void updateAccount(Account account) {
        super.getJdbcTemplate().update("update acount set name=?,balance=? where id=?",account.getName(),account.getMoney(),account.getId());
    }
}

测试类:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccountServiceTest {
    @Autowired
    private  IAccountService as;

    @Test
    public  void testTransfer(){
        as.transfer("张无忌","谢逊",100f);
    }
}

运行结果:
在这里插入图片描述

基于纯注解的声明式事务控制

  1. 配置事务管理器
  2. 开启spring对注解事务的支持
    @EnableTransactionManagement
  3. 在需要事务支持的地方使用@Transactional注解

AccountServiceImpl.java:

/**
 *
 * 事务控制应该都是在业务层
 */
@Service("accountService")
@Transactional(propagation= Propagation.SUPPORTS,readOnly=true)//只读型事务的配置,只能查询
public class AccountServiceImpl implements IAccountService{

    @Autowired
    private IAccountDao accountDao;

    @Override
    public Account findAccountById(Integer accountId) {
        return accountDao.findAccountById(accountId);
    }
    
    //上面进行只读型配置,但是这里需要的是读写型事务配置,所以继续进行配置
    @Transactional(propagation= Propagation.REQUIRED,readOnly=false)
    @Override
    public void transfer(String sourceName, String targetName, Float money) {
        System.out.println("transfer....");
            //2.1根据名称查询转出账户
            Account source = accountDao.findAccountByName(sourceName);
            //2.2根据名称查询转入账户
            Account target = accountDao.findAccountByName(targetName);
            //2.3转出账户减钱
            source.setMoney(source.getMoney()-money);
            //2.4转入账户加钱
            target.setMoney(target.getMoney()+money);
            //2.5更新转出账户
            accountDao.updateAccount(source);

            int i=1/0;

            //2.6更新转入账户
            accountDao.updateAccount(target);
    }
}

AccountDaoImpl.java:

@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public Account findAccountById(Integer accountId) {
        List<Account> accounts = jdbcTemplate.query("select * from account where id = ?",new BeanPropertyRowMapper<Account>(Account.class),accountId);
        return accounts.isEmpty()?null:accounts.get(0);
    }

    @Override
    public Account findAccountByName(String accountName) {
        List<Account> accounts = jdbcTemplate.query("select * from account where name = ?",new BeanPropertyRowMapper<Account>(Account.class),accountName);
        if(accounts.isEmpty()){
            return null;
        }
        if(accounts.size()>1){
            throw new RuntimeException("结果集不唯一");
        }
        return accounts.get(0);
    }

    @Override
    public void updateAccount(Account account) {
        jdbcTemplate.update("update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
    }
}

SpringConfiguration.java:

/**
 * spring的配置类,相当于bean.xml
 */
@Configuration
@ComponentScan("com.itheima")
@Import({JdbcConfig.class,TransactionConfig.class})
@PropertySource("jdbcConfig.properties")   
@EnableTransactionManagement    //开启spring对注解事务的支持
public class SpringConfiguration {
}

JdbcConfig.java:

/**
 * 和连接数据库相关的配置类
 */
public class JdbcConfig {
    @Value("${jdbc.driver}")
    private String driver;

    @Value("${jdbc.url}")
    private String url;

    @Value("${jdbc.username}")
    private String username;

    @Value("${jdbc.password}")
    private String password;

    /**
     * 创建JdbcTemplate
     * @param dataSource
     * @return
     */
    @Bean(name="jdbcTemplate")
    public JdbcTemplate createJdbcTemplate(DataSource dataSource){
        return new JdbcTemplate(dataSource);
    }

    /**
     * 创建数据源对象
     * @return
     */
    @Bean(name="dataSource")
    public DataSource createDataSource(){
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(username);
        ds.setPassword(password);
        return ds;
    }
}

TransactionConfig.java:

/**
 * 和事务相关的配置类
 */
public class TransactionConfig {

    /**
     * 用于创建事务管理器对象
     * @param dataSource
     * @return
     */
    @Bean(name="transactionManager")
    public PlatformTransactionManager createTransactionManager(DataSource dataSource){
        return new DataSourceTransactionManager(dataSource);
    }
}

jdbcConfig.properties:

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/eesy
jdbc.username=root
jdbc.password=1234

测试类:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes= SpringConfiguration.class)
public class AccountServiceTest {

    @Autowired
    private  IAccountService as;

    @Test
    public  void testTransfer(){
        as.transfer("aaa","bbb",100f);
    }
}

运行结果:
在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

前撤步登哥

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值