Spring学习文档

172 阅读9分钟

Spring

Spring 是最受欢迎的企业级 Java 应用程序开发框架,数以百万的来自世界各地的开发人员使用 Spring 框架来创建性能好、易于测试、可重用的代码。

Spring 框架是一个开源的 Java 平台,它最初是由 Rod Johnson 编写的,并且于 2003 年 6 月首 次在 Apache 2.0 许可下发布。 Spring 是轻量级的框架,其基础版本只有 2 MB 左右的大小。

Spring 框架的核心特性是可以用于开发任何 Java 应用程序,但是在 Java EE 平台上构建 web 应用程序是需要扩展的。 Spring 框架的目标是使 J2EE 开发变得更容易使用,通过启用基于 POJO 编程模型来促进良好的编程实践。

1.3、Spring Framework
Spring 基础框架,可以视为 Spring 基础设施,基本上任何其他 Spring 项目都是以 Spring Framework
为基础的。
1.3.1、Spring Framework特性
非侵入式:
    使用 Spring Framework 开发应用程序时,Spring 对应用程序本身的结构影响非常
小。对领域模型可以做到零污染;对功能性组件也只需要使用几个简单的注解进行标记,完全不会
破坏原有结构,反而能将组件结构进一步简化。这就使得基于 Spring Framework 开发应用程序
时结构清晰、简洁优雅。
    
控制反转:
    IOC——Inversion of Control,翻转资源获取方向。把自己创建资源、向环境索取资源
变成环境将资源准备好,我们享受资源注入。
面向切面编程:AOP——Aspect Oriented Programming,在不修改源代码的基础上增强代码功
能。
    
容器:
    Spring IOC 是一个容器,因为它包含并且管理组件对象的生命周期。组件享受到了容器化
的管理,替程序员屏蔽了组件创建过程中的大量细节,极大的降低了使用门槛,大幅度提高了开发
效率。
    
组件化:
    Spring 实现了使用简单的组件配置组合成一个复杂的应用。在 Spring 中可以使用 XML
和 Java 注解组合这些对象。这使得我们可以基于一个个功能明确、边界清晰的组件有条不紊的搭
建超大型复杂应用系统。
    
声明式:
    很多以前需要编写代码才能实现的功能,现在只需要声明需求即可由框架代为实现。
一站式:在 IOC 和 AOP 的基础上可以整合各种企业应用的开源框架和优秀的第三方类库。而且
Spring 旗下的项目已经覆盖了广泛领域,很多方面的功能性需求可以在 Spring Framework 的基
础上全部使用 Spring 来实现。

Spring Framework五大功能

功能模块功能介绍
Core Container核心容器,在 Spring 环境下使用任何功能都必须基于 IOC 容器。
AOP&Aspects面向切面编程
Testing提供了对 junit 或 TestNG 测试框架的整合。
Data Access/Integration提供了对数据访问/集成的功能。
Spring MVC提供了面向Web应用程序的集成功能。

IOC容器

2.1、IOC容器
2.1.1、IOC思想
IOC:Inversion of Control,翻译过来是反转控制。
①获取资源的传统方式
    自己做饭:买菜、洗菜、择菜、改刀、炒菜,全过程参与,费时费力,必须清楚了解资源创建整个过程
    中的全部细节且熟练掌握。
    在应用程序中的组件需要获取资源时,传统的方式是组件主动的从容器中获取所需要的资源,在这样的
    模式下开发人员往往需要知道在具体容器中特定资源的获取方式,增加了学习成本,同时降低了开发效率。
    
②反转控制方式获取资源
    点外卖:下单、等、吃,省时省力,不必关心资源创建过程的所有细节。
    反转控制的思想完全颠覆了应用程序组件获取资源的传统方式:反转了资源的获取方向——改由容器主
    动的将资源推送给需要的组件,开发人员不需要知道容器是如何创建资源对象的,只需要提供接收资源
    的方式即可,极大的降低了学习成本,提高了开发的效率。这种行为也称为查找的被动形式。
    
③DI
    DI:Dependency Injection,翻译过来是依赖注入。
    DI 是 IOC 的另一种表述方式:即组件以一些预先定义好的方式(例如:setter 方法)接受来自于容器
    的资源注入。相对于IOC而言,这种表述更直接。
    所以结论是:IOC 就是一种反转控制的思想, 而 DI 是对 IOC 的一种具体实现。

IOC容器在Spring中的实现

​ **Spring 的 IOC 容器就是 IOC 思想的一个落地的产品实现。**IOC 容器中管理的组件也叫做 bean。在创建bean 之前,首先需要创建 IOC 容器。

Spring 提供了 IOC 容器的两种实现方式:

  • BeanFactory 这是 IOC 容器的基本实现,是 Spring 内部使用的接口。面向 Spring 本身,不提供给开发人员使用。
  • ApplicationContext BeanFactory 的子接口,提供了更多高级特性。面向 Spring 的使用者,几乎所有场合都使用 ApplicationContext 而不是底层的 BeanFactory

ApplicationContext的主要实现类

类型名简介简介
ClassPathXmlApplicationContext通过读取类路径下的 XML 格式的配置文件创建 IOC 容器 对象
FileSystemXmlApplicationContext通过文件系统路径读取 XML 格式的配置文件创建 IOC 容 器对象
ConfigurableApplicationContextApplicationContext 的子接口,包含一些扩展方法 refresh() 和 close() ,让 ApplicationContext 具有启动、 关闭和刷新上下文的能力。
WebApplicationContext专门为 Web 应用准备,基于 Web 环境创建 IOC 容器对 象,并将对象引入存入 ServletContext 域中。

基于XML管理bean

导入依赖

<dependencies>
<!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.1</version>
</dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>RELEASE</version>
        <scope>test</scope>
    </dependency>

    <!-- MySQL驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.16</version>
    </dependency>
    <!-- 数据源 -->
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.10</version>
    </dependency>
</dependencies>

创建bean测试

/**
 * Created by KingsLanding on 2022/7/31 18:37
 */
public class HelloWorldSpring {
    public void satHello(){
        System.out.println("helloWorldSpring");
    }
}

在Spring的配置文件中配置bean

    <!--
    配置HelloWorld所对应的bean,即将HelloWorld的对象交给Spring的IOC容器管理
        通过bean标签配置IOC容器所管理的bean
        属性:
        id:设置bean的唯一标识
        class:设置bean所对应类型的全类名
-->
    <bean id="helloWorld" class="com.spring.pojo.HelloWorldSpring"></bean>

方法测试

@Test
 public void testHello(){
     //获取IOC容器
    ApplicationContext ioc = new ClassPathXmlApplicationContext("HelloSpring.xml");
    HelloWorldSpring helloWorld = (HelloWorldSpring) ioc.getBean("helloWorld");
    helloWorld.satHello();
}

bean的获取方式


获取bean的三种方式

  • 1、根据bean的id获取

  • 2、根据bean的类型获取

    注意:根据类型获取bean时,要求IOC容器中有且只有一个类型匹配的bean根据类型获取匹配的bean

    如果没找到此时抛出异常:NoUniqueBeanDefinitionException

    若有多个类型匹配的bean,此时抛出异常: NoUniqueBeanDefinitionException

  • 3、根据bean的id和类型获取

    根据类型来获取bean时,在满足bean唯一性的前提下 其实只是看: [对象 instanceof 指定的类型]的返回结果只要返回的是true就可以认定为和类型匹配,能够获取到

    即通过bean的类型,bean所继承的类的类型,bean所实现的接口的类型都可以获取bean

    前提

    如果组件类实现了接口,根据接口类型可以获取 bean 吗?

    可以,前提是bean唯一 如果一个接口有多个实现类,这些实现类都配置了 bean

    根据接口类型可以获取 bean 吗?

    不行,因为bean不唯一

   @Test
   public void testMassageIOC(){
      //获取IOC容器
      ApplicationContext ioc = new ClassPathXmlApplicationContext("MassageSpring.xml");
      //获取bean各方法
      //通过bean的id获取
      Massage iocBean0 = (Massage) ioc.getBean("MassageOne");
      //通过bean类型获取
      Massage iocBean1 = ioc.getBean(Massage.class);
      //通过bean的id和类型获取
      Massage iocBean2 = ioc.getBean("MassageOne", Massage.class);
      //通过bean所实现的接口类型获取
      MassageInterface iocBean3 = ioc.getBean(MassageInterface.class);
      System.out.println(iocBean3);
   }

配置bean时为属性赋值

set()方法赋值

property标签:通过组件类的setXxx()方法给组件对象设置属性 name属性:指定属性名(这个属性名是getXxx()setXxx()方法定义的,和成员变量无关) value属性:指定属性值

<!--set()方法赋值-->
<bean id="MassageOne" class="com.spring.pojo.Massage">
    <!--
        property标签:通过组件类的setXxx()方法给组件对象设置属性
        name属性:指定属性名(这个属性名是getXxx()、setXxx()方法定义的,和成员变量无关)
        value属性:指定属性值
    -->
    <property name="id" value="1"/>
    <property name="age" value="22"/>
    <property name="name" value="zmj"/>
    <property name="phone" value="123123"/>
</bean>
@Test
public void testMassageDI(){
   ApplicationContext ioc = new ClassPathXmlApplicationContext("MassageSpring.xml");
   Massage iocBean1 = ioc.getBean("MassageOne",Massage.class);
   System.out.println(iocBean1);//Massage{id=12, name='zmj', age=22, phone='123123'}
}

构造器方式赋值

<!--构造器方式赋值-->
<bean id="MassageTwo" class="pojo.Massage">
    <constructor-arg value="2" name="id"></constructor-arg>
    <constructor-arg value="22" name="age"></constructor-arg>
    <constructor-arg value="zmj" name="name"></constructor-arg>
    <constructor-arg value="123123" name="phone"></constructor-arg>
</bean>

#### null字面量的赋值方式

<!--null字面量的赋值方式-->
<bean id="MassageThere" class="com.spring.pojo.Massage">
    <property name="id" value="1"/>
    <property name="age" value="22"/>
    <property name="name" value="zmj"/>
    <property name="phone">
        <null></null>
    </property>
</bean>

为类类型的属性赋值的方式一

private Department department;
<!--为类类型的属性赋值的方式一-->
<bean id="MassageFour" class="com.spring.pojo.Massage">
    <property name="id" value="1"/>
    <property name="age" value="22"/>
    <property name="name" value="zmj"/>
    <property name="phone" value="123123"/>
    <!--ref:引用IOC容器中的某个bean的id-->
    <property name="department" ref="DepartmentOne"/>
</bean>
<bean id="DepartmentOne" class="com.spring.pojo.Department">
    <property name="dId" value="1"/>
    <property name="dName" value="测试"/>
    <property name="dSector" value="测试"/>
</bean>

测试结果

@Test
public void testMassageDI03(){
   ApplicationContext ioc = new ClassPathXmlApplicationContext("MassageSpring.xml");
   Massage iocBean1 = ioc.getBean("MassageFour",Massage.class);
   System.out.println(iocBean1);//Massage{id=1, name='zmj', age=22, phone='123123', department=Department{dId=1,
                                 // dName='测试', dSector='测试'}}
}

#### 为类类型的属性赋值的方式二

<!--为类类型的属性赋值的方式二-->
<bean id="MassageFour02" class="com.spring.pojo.Massage">
    <property name="id" value="1"/>
    <property name="age" value="22"/>
    <property name="name" value="zmj"/>
    <property name="phone" value="123123"/>
    <!--内部bean,只能在当前的bean的内部使用,不能直接通过IOC容器获取-->
    <property name="department">
        <bean id="departmentInner" class="com.spring.pojo.Department">
            <property name="dId" value="2"/>
            <property name="dName" value="测试2"/>
            <property name="dSector" value="测试2"/>
        </bean>
    </property>
</bean>

测试结果

@Test
public void testMassageDI04(){
   ApplicationContext ioc = new ClassPathXmlApplicationContext("MassageSpring.xml");
   Massage iocBean1 = ioc.getBean("MassageFour02",Massage.class);
   System.out.println(iocBean1);//Massage{id=1, name='zmj', age=22, phone='123123',
                                 // department=Department{dId=2, dName='测试2', dSector='测试2'}}
}

特殊标签的赋值方式

<!--
    特殊标签的赋值方式
    <:&lt
    >:&gt
    CDATA节中的内容可以原样解析<![CDATA[...]]
    CDATA节是xml中一个特殊的标签,因此不能写在一个属性里
-->
<!--特殊标签的赋值方式-->
<!--Massage{id=1, name='zmj', age=22, phone='<123123>'}-->
<bean id="MassageFive" class="com.spring.pojo.Massage">
    <property name="id" value="1"/>
    <property name="age" value="22"/>
    <property name="name" value="zmj"/>
    <property name="phone">
        <value><![CDATA[<123123>]]></value>
    </property>
</bean>

数组类性属性赋值

<!--数组类性属性赋值-->
<bean id="ListTest01" class="com.spring.pojo.ListTest">
    <property name="strings">
        <array>
            <value>数组数据1</value>
            <value>数组数据2</value>
            <value>数组数据3</value>
        </array>
    </property>
</bean>

List集合类型属性赋值方式一

<!--List集合类型属性赋值方式一-->
<bean id="ListTest02" class="com.spring.pojo.ListTest">
    <property name="strings">
        <array>
            <value>数组数据1</value>
            <value>数组数据2</value>
            <value>数组数据3</value>
        </array>
    </property>
    <property name="list">
        <list>
            <ref bean="ListTestList01"/>
            <ref bean="ListTestList02"/>
        </list>
    </property>
</bean>
<bean id="ListTestList01" class="com.spring.pojo.Department">
    <property name="dId" value="01"/>
    <property name="dName" value="测试"/>
    <property name="dSector" value="测试"/>
</bean>
<bean id="ListTestList02" class="com.spring.pojo.Department">
    <property name="dId" value="01"/>
    <property name="dName" value="测试"/>
    <property name="dSector" value="测试"/>
</bean>

测试结果

@Test
public void testListTestDI02(){
   ApplicationContext ioc = new ClassPathXmlApplicationContext("MassageSpring.xml");
   ListTest listTest02 = ioc.getBean("ListTest02", ListTest.class);
   System.out.println(listTest02);
   /*
   ListTest{strings=[数组数据1, 数组数据2, 数组数据3],
   list=[Department{dId=1, dName='测试', dSector='测试'},
         Department{dId=1, dName='测试', dSector='测试'}]}
    */
}

List集合类型属性赋值方式二

必须加入util约束

<!--List集合类型属性赋值方式二 必须加入util约束-->
<bean id="ListTest03" class="com.spring.pojo.ListTest">
<property name="strings">
    <array>
        <value>数组数据1</value>
        <value>数组数据2</value>
        <value>数组数据3</value>
    </array>
</property>
<property name="list" ref="listTest03"></property>
</bean>
<util:list id="listTest03">
    <ref bean="ListTestList01"/>
    <ref bean="ListTestList02"/>
</util:list>

测试结果

@Test
public void testListTestDI03() {
   ApplicationContext ioc = new ClassPathXmlApplicationContext("MassageSpring.xml");
   ListTest listTest03 = ioc.getBean("ListTest03", ListTest.class);
   System.out.println(listTest03);
   /*
   ListTest{strings=[数组数据1, 数组数据2, 数组数据3],
   list=[Department{dId=1, dName='测试', dSector='测试'},
         Department{dId=1, dName='测试', dSector='测试'}]}
    */
}

Map集合类型属性赋值方式一

<!--Map集合类型属性赋值方式一-->
<bean id="ListTest04" class="com.spring.pojo.ListTest">
    <property name="strings">
        <array>
            <value>数组数据1</value>
            <value>数组数据2</value>
            <value>数组数据3</value>
        </array>
    </property>
    <property name="list" ref="listTest03"></property>
    <property name="deptMap">
        <map>
            <entry key="01" value-ref="ListTestList01"/>
            <entry key="02" value-ref="ListTestList02"/>
        </map>
    </property>
</bean>

测试结果

@Test
public void testListTestDI04() {
   ApplicationContext ioc = new ClassPathXmlApplicationContext("MassageSpring.xml");
   ListTest listTest04 = ioc.getBean("ListTest04", ListTest.class);
   System.out.println(listTest04);
   /*
   ListTest{strings=[数组数据1, 数组数据2, 数组数据3],
   list=[Department{dId=1, dName='测试', dSector='测试'}, Department{dId=1, dName='测试', dSector='测试'}],
   deptMap={01=Department{dId=1, dName='测试', dSector='测试'}, 02=Department{dId=1, dName='测试', dSector='测试'}}}
    */
}

Map集合类型属性赋值方式二

<!--Map集合类型属性赋值方式二-->
<bean id="ListTest05" class="com.spring.pojo.ListTest">
    <property name="strings">
        <array>
            <value>数组数据1</value>
            <value>数组数据2</value>
            <value>数组数据3</value>
        </array>
    </property>
    <property name="list" ref="listTest03"></property>
    <property name="deptMap" ref="deptMapTest"></property>
</bean>
<util:map id="deptMapTest">
    <entry key="01" value-ref="ListTestList01"/>
    <entry key="02" value-ref="ListTestList02"/>
</util:map>

测试结果

@Test
public void testListTestDI05() {
   ApplicationContext ioc = new ClassPathXmlApplicationContext("MassageSpring.xml");
   ListTest listTest05 = ioc.getBean("ListTest05", ListTest.class);
   System.out.println(listTest05);
   /*
   ListTest{strings=[数组数据1, 数组数据2, 数组数据3],
   list=[Department{dId=1, dName='测试', dSector='测试'}, Department{dId=1, dName='测试', dSector='测试'}],
   deptMap={01=Department{dId=1, dName='测试', dSector='测试'}, 02=Department{dId=1, dName='测试', dSector='测试'}}}
    */
}

P命名空间

引入p命名空间后,可以通过以下方式为bean的各个属性赋值

<!--引入p命名空间-->
<bean id="pNamespace" class="com.spring.pojo.Department"
      p:dId="01" p:dName="测试01" p:dSector="测试01">

</bean>

测试结果

@Test
public void testPNamespace(){
   ApplicationContext ioc = new ClassPathXmlApplicationContext("MassageSpring.xml");
   Department pNamespace = ioc.getBean("pNamespace", Department.class);
   System.out.println(pNamespace);
   //Department{dId=1, dName='测试01', dSector='测试01'}
}

引入外部属性文件

外部文件druid.properties

#这个文件得放到mybatis核心文件同级
#为避免Spring中命名冲突,加上jdbc.前缀
jdbc.username=root
jdbc.password=zmj.666.999
jdbc.url=jdbc:mysql://localhost:3306/jdbctest?serverTimezone=UTC
jdbc.driverClass=com.mysql.cj.jdbc.Driver

引入druid.properties

<!--引入druid.properties 就可以通过${key}方式获取value-->
<context:property-placeholder location="druid.properties"/>
<bean id="DruidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="driverClassName" value="${jdbc.driverClass}"/>
</bean>

测试结果

@Test
public void testDruidDataSource() throws SQLException {
    ApplicationContext ioc = new ClassPathXmlApplicationContext("DruidDataSource.xml");
    DataSource druidDataSource = (DataSource) ioc.getBean("DruidDataSource");
    System.out.println(druidDataSource.getConnection());//com.mysql.cj.jdbc.ConnectionImpl@5bfa9431

}

scope:设置bean作用域

<!--
    scope:设置bean作用域
    prototype:表示获取该bean所对应的对象都是同一个
    singleton:表示获取该bean所对应的对象都不是同一个
-->
<!--
scope属性:取值singleton(默认值),bean在IOC容器中只有一个实例,IOC容器初始化时创建对象
scope属性:取值prototype,bean在IOC容器中可以有多个实例,getBean()时创建对象
-->

<bean id="department" class="com.spring.pojo.Department" scope="singleton">
    <property name="dId" value="01"/>
    <property name="dName" value="测试"/>
    <property name="dSector" value="测试"/>
</bean>

测试

@Test
public void testBeanScope(){
    ApplicationContext ioc = new ClassPathXmlApplicationContext("BeanScope.xml");
    Department bean1 = ioc.getBean(Department.class);
    Department bean2 = ioc.getBean(Department.class);
    System.out.println(bean1==bean2);//单例返回true,多例返回false
}

bean的生命周期

具体的生命周期过程

  • bean对象创建(调用无参构造器)
  • 给bean对象设置属性
  • bean对象初始化之前操作(由bean的后置处理器负责)
  • bean对象初始化(需在配置bean时指定初始化方法)
  • bean对象初始化之后操作(由bean的后置处理器负责)
  • bean对象就绪可以使用
  • bean对象销毁(需在配置bean时指定销毁方法)
  • IOC容器关闭

使用init-method属性指定初始化方法;使用destroy-method属性指定销毁方法

<!-- 使用init-method属性指定初始化方法 -->
<!-- 使用destroy-method属性指定销毁方法 -->
<bean class="com.spring.pojo.User" scope="prototype" init-method="initMethod"
      destroy-method="destroyMethod">
    <property name="id" value="1001"></property>
    <property name="username" value="admin"></property>
    <property name="password" value="123456"></property>
    <property name="age" value="23"></property>
</bean>

bean的后置处理器

bean的后置处理器会在生命周期的初始化前后添加额外的操作,需要实现BeanPostProcessor接口,且配置到IOC容器中,需要注意的是,bean后置处理器不是单独针对某一个bean生效,而是针对IOC容器中所有bean都会执行

<!--
bean的后置处理器会在生命周期的初始化前后添加额外的操作,需要实现BeanPostProcessor接口,
且配置到IOC容器中,需要注意的是,bean后置处理器不是单独针对某一个bean生效,而是针对IOC容
器中所有bean都会执行
-->
<!-- bean的后置处理器要放入IOC容器才能生效 -->
<bean id="myBeanProcessor" class="com.spring.pojo.MyBeanProcessor"/>

测试

bean

/**
 * Created by KingsLanding on 2022/8/1 15:19
 */
public class User {
    private Integer id;
    private String username;
    private String password;
    private Integer age;
    public User() {
        System.out.println("生命周期:1、创建对象");
    }
    public User(Integer id, String username, String password, Integer age) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.age = age;
    }
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        System.out.println("生命周期:2、依赖注入");
        this.id = id;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public Integer getAge() {
        return age;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
    public void initMethod(){
        System.out.println("生命周期:3、初始化");
    }
    public void destroyMethod(){
        System.out.println("生命周期:5、销毁");
    }
    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", age=" + age +
                '}';
    }
}

bean的后置处理器

/**
 * Created by KingsLanding on 2022/8/1 15:51
 */
public class MyBeanProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName)
            throws BeansException {
        System.out.println("后置处理器" + beanName + " = " + bean);
        return bean;
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName)
            throws BeansException {
        System.out.println("后置处理器" + beanName + " = " + bean);
        return bean;
    }

}

测试结果

  • 注意:

若bean的作用域为单例(scope="singleton")时,生命周期的前三个步骤会在获取IOC容器时执行,且此时才会自动调用bean的销毁方法

若bean的作用域为多例(scope="properties")时,生命周期的前三个步骤会在获取bean时执行,此时不会自动调用销毁方法

/**
 * Created by KingsLanding on 2022/8/1 15:22
 */
public class SpringBeanCycle {
    /*
    注意:
    若bean的作用域为单例(scope="singleton")时,生命周期的前三个步骤会在获取IOC容器时执行,且此时才会自动调用bean的销毁方法
    若bean的作用域为多例(scope="properties")时,生命周期的前三个步骤会在获取bean时执行,此时不会自动调用销毁方法

     */
    @Test
    public void testLife(){
        ClassPathXmlApplicationContext ac = new
                ClassPathXmlApplicationContext("BeanLifeCycle.xml");
        User bean = ac.getBean(User.class);
        System.out.println("生命周期:4、通过IOC容器获取bean并使用");
        ac.close();
        /*
        生命周期:1、创建对象
        生命周期:2、依赖注入
        后置处理器com.spring.pojo.User#0 = User{id=1001, username='admin', password='123456', age=23}
        生命周期:3、初始化
        后置处理器com.spring.pojo.User#0 = User{id=1001, username='admin', password='123456', age=23}
        生命周期:4、通过IOC容器获取bean并使用
        生命周期:5、销毁
         */
    }

}

FactoryBean

FactoryBean是Spring提供的一种整合第三方框架的常用机制。和普通的bean不同,配置一个FactoryBean类型的bean,在获取bean的时候得到的并不是class属性中配置的这个类的对象,而是getObject()方法的返回值。通过这种机制,Spring可以帮我们把复杂组件创建的详细过程和繁琐细节都屏蔽起来,只把最简洁的使用界面展示给我们。

将来我们整合Mybatis时,Spring就是通过FactoryBean机制来帮我们创建SqlSessionFactory对象的。
  • FactoryBean是一个接口,需要创建一个类实现该接口 其中有三个方法: getObject():通过一个对象交给IOC容器管理 getObjectType():设置所提供对象的类型 isSingleton():所提供的对象是否单例 当把FactoryBean的实现类配置为bean时,会将当前类getObject()所返回的对象交给IOC容器管理
/**
 * Created by KingsLanding on 2022/8/1 16:05
 *
 *
 * FactoryBean是一个接口,需要创建一个类实现该接口
 * 其中有三个方法:
 * getObject():通过一个对象交给IOC容器管理
 * getObjectType():设置所提供对象的类型
 * isSingleton():所提供的对象是否单例
 * 当把FactoryBean的实现类配置为bean时,会将当前类getObject()所返回的对象交给I0C容器管理
 */
public class UserFactoryBean implements FactoryBean<User> {
    @Override
    public User getObject() throws Exception {

        return new User();
    }

    @Override
    public Class<?> getObjectType() {
        return User.class;
    }
}

测试

@Test
public void testFactory(){
    ApplicationContext ioc = new ClassPathXmlApplicationContext("Spring-Factory.xml");
    User userFactoryBean = (User) ioc.getBean("userFactoryBean");
    System.out.println(userFactoryBean);
}

基于XML的自动装配

非自动装配

<!--非自动装配-->
<bean class="controller.MassageController">
    <property name="massageServiceImpl" ref="massageService"></property>
</bean>
<bean id="massageService" class="service.MassageServiceImpl">
    <property name="massageDaoImpl" ref="massageDao"></property>
</bean>
<bean id="massageDao" class="dao.MassageDaoImpl"></bean>

自动装配

根据指定的策略,在IOC 容器中匹配某个bean,自动为bean 中的类类型的属性或接口类型的属性賦值 可以通过bean标签中的autowire属性设置自动装配的策略

自动装配的策略: no, default: 表示不装配,bean 中的属性不会自动匹配某个bean为属性赋值,此时属性使用默认值 byType:根据要赋值的属性的类型,在I0C 容器中匹配某个bean,为属性赋值

注意: a>若通过类型没有找到任何一个类型匹配的bean,此时不装配,属性使用默认值null b>若通过类型找到了多个类型匹配的bean,此时会抛出异常: NoUniqueBeanDefinitionException 总结:当使用byType 实现自动装配时,IOC 容器中有且只有一个类型匹配的bean能够为属性赋时

byName:将自动装配的属性的属性名,作为bean的id在IOC容器中匹配相对应的bean进行赋值

<!--
自动装配:
根据指定的策略,在IOC 容器中匹配某个bean,自动为bean 中的类类型的属性或接口类型的属性賦值
可以通过bean标签中的autowire属性设置自动装配的策略
自动装配的策略:
no, default: 表示不装配,bean 中的属性不会自动匹配某个bean为属性赋值,此时属性使用默认值
byType:根据要赋值的属性的类型,在I0C 容器中匹配某个bean,为属性赋值
注意:
a>若通过类型没有找到任何一个类型匹配的bean,此时不装配,属性使用默认值
b>若通过类型找到了多个类型匹配的bean,此时会抛出异常: NoUniqueBeanDefinitionException
总结:当使用byType 实现自动装配时,IOC 容器中有且只有一个类型匹配的bean能够为属性赋时

byName:将自动装配的属性的属性名,作为bean的id在IOC容器中匹配相对应的bean进行赋值
-->
<bean id="massageController" class="com.spring.MassageController" autowire="byType"></bean>

<bean id="massageService" class="com.spring.MassageServiceImpl" autowire="byType"></bean>

<bean id="massageDao" class="com.spring.dao.MassageDaoImpl" autowire="byType"></bean>

byName:

<bean id="userController"
class="com.atguigu.autowire.xml.controller.UserController" autowire="byName">
</bean>
<bean id="userService"
class="com.atguigu.autowire.xml.service.impl.UserServiceImpl" autowire="byName">
</bean>
<bean id="userServiceImpl"
class="com.atguigu.autowire.xml.service.impl.UserServiceImpl" autowire="byName">
</bean>
<bean id="userDao" class="com.atguigu.autowire.xml.dao.impl.UserDaoImpl"></bean>
<bean id="userDaoImpl" class="com.atguigu.autowire.xml.dao.impl.UserDaoImpl">
</bean>

基于注解管理bean

标识组件的常用注解

@Component:将类标识为普通组件 @Controller:将类标识为控制层组件 @Service:将类标识为业务层组件 @Repository:将类标识为持久层组件

扫描组件

基本的扫描方式

<context:component-scan base-package="com.spring"></context:component-scan>

特殊扫描

context:include-filter标签:指定在原有扫描规则的基础上追加的规则 use-default-filters属性:取值false表示关闭默认扫描规则 此时必须设置use-default-filters="false",因为默认规则即扫描指定包下所有类 type:设置排除或包含的依据 type="annotation",根据注解排除,expression中设置要排除的注解的全类名 type="assignable",根据类型排除,expression中设置要排除的类型的全类名

<context:component-scan base-package="com.spring">
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"></context:exclude-filter>
    <context:exclude-filter type="assignable" expression="com.spring.controller.MassageController"></context:exclude-filter>
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"></context:include-filter>
</context:component-scan>

@Repository

/**
 * Created by KingsLanding on 2022/8/1 18:09
 */
@Repository
public class MassageDaoImpl implements MassageDao{
    @Override
    public void saveMassage() {
        System.out.println("基于XML的自动装配测试");
    }
}

默认id:

类名首字母小写就是bean的id。例如:UserController类对应的bean的id就是userController。

自定义bean的id:

可通过标识组件的注解的value属性设置自定义的bean的id

/**
 * Created by KingsLanding on 2022/8/1 18:12
 */
@Controller("controller")
public class MassageController {
    @Autowired
    private MassageService massageServiceImpl;

    public void setMassageServiceImpl(MassageService massageServiceImpl) {
        this.massageServiceImpl = massageServiceImpl;
    }

    public void saveMassage(){
        massageServiceImpl.saveMassage();
    }
}
MassageController massageController = ioc.getBean("controller",MassageController.class);
System.out.println(massageController);
massageController.saveMassage();

基于注解的自动装配

@Autowired:实现自动装配功能的注解

  1. @Autowired注解能够标识的位置 a>标识在成员变量上,此时不需要设置成员变量的set方法(常用) b>标识在set方法上 c>标识在为当前成员变量赋值的有参构造上
/**
 * Created by KingsLanding on 2022/8/1 18:12
 */
@Controller("controller")
public class MassageController {
    @Autowired
    private MassageService massageServiceImpl;

    public void setMassageServiceImpl(MassageService massageServiceImpl) {
        this.massageServiceImpl = massageServiceImpl;
    }

    public void saveMassage(){
        massageServiceImpl.saveMassage();
    }
}

@Autowired工作流程

  • 首先根据所需要的组件类型到IOC容器中查找
    • 能够找到唯一的bean:直接执行装配
    • 如果完全找不到匹配这个类型的bean:装配失败
    • 和所需类型匹配的bean不止一个
      • 没有@Qualifier注解:根据@Autowired标记位置成员变量的变量名作为bean的id进行匹配
        • 能够找到:执行装配
        • 找不到:装配失败
      • 使用@Qualifier注解:根据@Qualifier注解中指定的名称作为bean的id进行匹配
        • 能够找到:执行装配
        • 找不到:装配失败
@Service
public class MassageServiceImpl implements MassageService {

    @Autowired
    @Qualifier("massageDaoImpl")
    private MassageDao massageDaoImpl;

    public void setMassageDaoImpl(MassageDao massageDaoImpl) {
        this.massageDaoImpl = massageDaoImpl;
    }

    @Override
    public void saveMassage() {
        massageDaoImpl.saveMassage();
    }
}

@Autowired中有属性required,默认值为true,因此在自动装配无法找到相应的bean时,会装配失败

可以将属性required的值设置为true,则表示能装就装,装不上就不装,此时自动装配的属性为默认值

但是实际开发时,基本上所有需要装配组件的地方都是必须装配的,用不上这个属性。

AOP

面向切面编程

代理模式

二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标 方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑 的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调 用和打扰,同时让附加功能能够集中在一起也有利于统一维护。

动态代理

/**
 * Created by KingsLanding on 2022/8/2 15:39
 */
public interface Calculator {
    int add(int i, int j);
    int sub(int i, int j);
    int mul(int i, int j);
    int div(int i, int j);
}

动态生成代理对象

CalculatorProxy

/**
 * Created by KingsLanding on 2022/8/2 16:23
 */
public class CalculatorProxy implements InvocationHandler {

    private Object obj;

    public CalculatorProxy(Object obj) {
        this.obj = obj;
    }

    public Object getProxy(){

        CalculatorProxy handler = new CalculatorProxy(obj);
        Object proxyInstance = Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), handler);

        return proxyInstance;
    }


    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("测试1");
        Object invoke = method.invoke(obj, args);
        System.out.println("测试2"+ Arrays.toString(args));
        return invoke;
    }

}

CalculatorPureImpl

/**
 * Created by KingsLanding on 2022/8/2 15:40
 */
public class CalculatorPureImpl implements Calculator {
    @Override
    public int add(int i, int j) {
        int result = i + j;
        System.out.println("方法内部 result = " + result);
        return result;
    }
    @Override
    public int sub(int i, int j) {
        int result = i - j;
        System.out.println("方法内部 result = " + result);
        return result;
    }
    @Override
    public int mul(int i, int j) {
        int result = i * j;
        System.out.println("方法内部 result = " + result);
        return result;
    }
    @Override
    public int div(int i, int j) {
        int result = i / j;
        System.out.println("方法内部 result = " + result);
        return result;
    }
}

测试结果

@Test
public void testProxy(){
    CalculatorProxy calculatorProxy = new CalculatorProxy(new CalculatorPureImpl());
    Calculator proxy = (Calculator) calculatorProxy.getProxy();
    proxy.add(1,2);
    /*
    测试1
    方法内部 result = 3
    测试2[1, 2]
     */
}

AOP概念及相关术语

AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面 向对象编程的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现在不修改源代码的情况 下给程序动态统一添加额外功能的一种技术。

相关术语

横切关注点

从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方 法进行多个不同方面的增强。 这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横 切关注点。

通知

每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。

前置通知:在被代理的目标方法前执行

返回通知:在被代理的目标方法成功结束后执行(寿终正寝)

异常通知:在被代理的目标方法异常结束后执行(死于非命)

后置通知:在被代理的目标方法最终结束后执行(盖棺定论)

环绕通知:使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所 有位置

切面

封装通知方法的类。

目标

被代理的目标对象。

代理

向目标对象应用通知之后创建的代理对象。

连接点

这也是一个纯逻辑概念,不是语法定义的。 把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉 点就是连接点。

切入点

定位连接点的方式。

每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。 如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。

Spring 的 AOP 技术可以通过切入点定位到特定的连接点。 切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。

作用

简化代码:把方法中固定位置的重复的代码抽取出来,让被抽取的方法更专注于自己的核心功能, 提高内聚性。

代码增强:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑的方法就 被切面给增强了。

相关依赖

<!-- spring-aspects会帮我们传递过来aspectjweaver -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.1</version>

AOP注意事项

  • 切面类和目标类都需要交给IOC容器管理
  • 开启AspectJ的自动代理,为目标对象自动生成代理
  • 切面类必须通过@Aspect注解标识为一个切面
<context:component-scan base-package="com.Spring.AOP"></context:component-scan>

    <!--开启基于注解的AOP-->
    <aop:aspectj-autoproxy/>

方法测试

/**
 * Created by KingsLanding on 2022/8/2 15:39
 */
public interface Calculator {
    int add(int i, int j);
    int sub(int i, int j);
    int mul(int i, int j);
    int div(int i, int j);
}
/**
 * Created by KingsLanding on 2022/8/2 15:40
 */
@Component
public class CalculatorPureImpl implements Calculator {
    @Override
    public int add(int i, int j) {
        int result = i + j;
        System.out.println("方法内部 result = " + result);
        return result;
    }
    @Override
    public int sub(int i, int j) {
        int result = i - j;
        System.out.println("方法内部 result = " + result);
        return result;
    }
    @Override
    public int mul(int i, int j) {
        int result = i * j;
        System.out.println("方法内部 result = " + result);
        return result;
    }
    @Override
    public int div(int i, int j) {
        int result = i / j;
        System.out.println("方法内部 result = " + result);
        return result;
    }
}

1、@Aspect标注此组件为切面类

@Before("pointCut()"):前置通知 @After("pointCut()"):后置通知,在目标对象的final中执行 @AfterReturning(value = "pointCut()",returning = "returning"):返回通知,在目标对象的方法返回值之后执行 @AfterThrowing(value = "pointCut()",throwing = "ex"):异常通知,在目标对象方法抛出异常catch时执行


2、在切面中,需要通过指定的注解将方法标识为通知方法

@Before:前置通知,在目标对象方法执行之前执行 @Before("execution(public int com.Spring.AOP.CalculatorPureImpl.add(int , int))") @Before("execution(* com.Spring.AOP.CalculatorPureImpl.*(..))") 第一个表示任意的访问修饰符和返回值类型;第二个表示任意方法 .. 表示任意的参数

* 也可以在类或包下使用


3、重用的切入点表达式

@Pointcut("execution(* com.Spring.AOP.CalculatorPureImpl.*(..))")
    public void pointCut(){ 
        
    }

4、获取连接点所对应的方法的签名信息

Signature signature = joinPoint.getSignature(); //获取连接点所对应方法的参数 Object[] args = joinPoint.getArgs();


5、切面优先级

​ 可以通过@Order注解的value属性设置优先级,默认值是Integer的最大值 ​ @Order注解的value属性值越小,优先级越高

/**
 * Created by KingsLanding on 2022/8/2 21:35
 */
@Component
@Aspect
@Order(1)
public class OrderAspect {

    @Before("execution(* com.Spring.AOP.CalculatorPureImpl.*(..))")
    public void BeforeOrderMethod(){
        System.out.println("前置通知--优先级测试");
    }
}

前置通知

   /*
        在切面中,需要通过指定的注解将方法标识为通知方法
        @Before:前置通知,在目标对象方法执行之前执行
     */
//    @Before("execution(public int com.Spring.AOP.CalculatorPureImpl.add(int , int))")
//    @Before("execution(* com.Spring.AOP.CalculatorPureImpl.*(..))")
    @Before("pointCut()")
    public void BeforeMethod(JoinPoint joinPoint){
        //获取连接点所对应的方法的签名信息(就是方法体的全部信息)
        Signature signature = joinPoint.getSignature();
        //获取连接点所对应方法的参数
        Object[] args = joinPoint.getArgs();
        System.out.println("前置通知 方法:"+signature.getName()+"参数:"+ Arrays.toString(args));
    }

后置通知

@After("pointCut()")
public void AfterFinalAdviceMethod(JoinPoint joinPoint){
    //获取连接点所对应的方法的签名信息(就是方法体的全部信息)
    Signature signature = joinPoint.getSignature();

    System.out.println("后置通知 方法:"+signature.getName()+"执行完毕");
}

返回通知

/*
    在返回通知中若要获取目标对象方法的返回值
    需要通过@AfterReturning注解的returning属性设置接收返回值的参数
    并且在通知方法的某个参数指定为接收目标对象方法的返回值参数
 */
@AfterReturning(value = "pointCut()",returning = "returning")
public void AfterReturningAdviceMethod(JoinPoint joinPoint,Object returning){
    //获取连接点所对应的方法的签名信息(就是方法体的全部信息)
    Signature signature = joinPoint.getSignature();

    System.out.println("返回通知 方法:"+signature.getName()+"返回值:"+returning);
}

异常通知

/*
    在异常通知中若要获取目标对象方法的异常
    需要在@AfterThrowing注解的throwing属性设置接收异常的参数
    通过通知方法的某个异常类型的参数获取目标对象抛出的异常参数
 */
@AfterThrowing(value = "pointCut()",throwing = "ex")
public void AfterThrowingAdviceMethod(JoinPoint joinPoint,Exception ex){
    //获取连接点所对应的方法的签名信息(就是方法体的全部信息)
    Signature signature = joinPoint.getSignature();

    System.out.println("异常通知 方法:"+signature.getName()+"异常值:"+ex);
}

环绕通知

/*
    环绕通知:
    使用环绕通知时,就像代理模式的invoke;目标对象方法由joinPoint.proceed();代理执行
 */
@Around("pointCut()")
//环绕通知的返回值必须与目标对象的返回值类型一致,当然这里的目标对象已经由joinPoint.proceed();代理
public Object AroundAdviceMethod(ProceedingJoinPoint joinPoint){

    Object result = null;
    try {
        System.out.println("环绕通知-->前置通知");

        result = joinPoint.proceed();
        System.out.println("环绕通知-->返回通知");
    } catch (Throwable throwable) {
        throwable.printStackTrace();
        System.out.println("环绕通知-->异常通知" + throwable);
    } finally {
        System.out.println("环绕通知-->后置通知");
    }

    return result;
}

测试结果

/**
 * Created by KingsLanding on 2022/8/2 18:49
 */
public class testBean {

    @Test
    public void testBean(){
        ApplicationContext ioc = new ClassPathXmlApplicationContext("Spring-AOP.xml");

//        CalculatorPureImpl calculatorPure = ioc.getBean(CalculatorPureImpl.class);
//        calculatorPure.add(1,3);
        Calculator calculator = ioc.getBean(Calculator.class);
        calculator.div(1,1);
        /*
        前置通知--优先级测试
        环绕通知-->前置通知
        前置通知 方法:div参数:[1, 1]
        方法内部 result = 1
        返回通知 方法:div返回值:1
        后置通知 方法:div执行完毕
        环绕通知-->返回通知
        环绕通知-->后置通知
         */
    }
}

基于XML的AOP(了解)

<context:component-scan base-package="com.atguigu.aop.xml"></context:componentscan>
<aop:config>
<!--配置切面类-->
<aop:aspect ref="loggerAspect">
<aop:pointcut id="pointCut" expression="execution(*
com.atguigu.aop.xml.CalculatorImpl.*(..))"/>
<aop:before method="beforeMethod" pointcut-ref="pointCut"></aop:before>
<aop:after method="afterMethod" pointcut-ref="pointCut"></aop:after>
<aop:after-returning method="afterReturningMethod" returning="result"
pointcut-ref="pointCut"></aop:after-returning>
<aop:after-throwing method="afterThrowingMethod" throwing="ex" pointcutref="pointCut"></aop:after-throwing>
<aop:around method="aroundMethod" pointcut-ref="pointCut"></aop:around>
</aop:aspect>
<aop:aspect ref="validateAspect" order="1">
<aop:before method="validateBeforeMethod" pointcut-ref="pointCut">
</aop:before>
</aop:aspect>
</aop:config>

声明式事务

Spring 框架对 JDBC 进行封装,使用 JdbcTemplate 方便实现对数据库操作

加入依赖

<dependencies>
    <!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.1</version>
    </dependency>
    <!-- Spring 持久化层支持jar包 --> <!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中,需要使用orm、jdbc、tx三个 jar包 --> <!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-orm</artifactId>
        <version>5.3.1</version>
    </dependency>
    <!-- Spring 测试相关 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.3.1</version>
    </dependency>
    <!-- junit测试 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <!-- MySQL驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.16</version>
    </dependency>
    <!-- 数据源 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.0.31</version>
    </dependency>
</dependencies>

JdbcTemplate

#这个文件得放到mybatis核心文件同级
#为避免Spring中命名冲突,加上jdbc.前缀
jdbc.username=root
jdbc.password=zmj.666.999
jdbc.url=jdbc:mysql://localhost:3306/jdbcspring?serverTimezone=UTC
jdbc.driverClass=com.mysql.cj.jdbc.Driver
<!-- 导入外部属性文件 -->
<context:property-placeholder location="druid.properties"/>

<!-- 配置数据源 -->
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="${jdbc.driverClass}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

<!-- 配置 JdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <!-- 装配数据源 -->
    <property name="dataSource" ref="druidDataSource"></property>
</bean>

测试

/**
 * Created by KingsLanding on 2022/8/3 12:38
 */
//指定当前测试类在Spring的测试环境中执行,此时就可以通过注入的方式直接获取IOC容器中的bean
@RunWith(SpringJUnit4ClassRunner.class)
//设置Spring测试环境的配置文件
@ContextConfiguration("classpath:Spring-jdbcTemplate.xml")
public class TestJdbcTemplate {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void testJdbcTemplate(){
        String sql="insert into user value(?,?,?)";
        jdbcTemplate.update(sql,1,2,3);
    }

    @Test
    public void testQuery(){
        String sql="select * from massage";
        BeanPropertyRowMapper<Massage> mapper = new BeanPropertyRowMapper<>(Massage.class);
        List<Massage> massages = jdbcTemplate.query(sql, mapper);
        System.out.println(massages);
    }
}

声明式事务概念

编程式事务的实现方式存在缺陷:

细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。

代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。

Connection conn = ...;
try {
// 开启事务:关闭事务的自动提交
conn.setAutoCommit(false);
// 核心操作
// 提交事务
conn.commit();
}catch(Exception e){
// 回滚事务
conn.rollBack();
}finally{
    // 释放数据库连接
conn.close();
}

#### 声明式事务的优势

既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。 封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。

  • 好处1:提高开发效率

  • 好处2:消除了冗余的代码

  • 好处3:框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性性能等各个方面的优化

所以,我们可以总结下面两个概念:

  • 编程式:自己写代码实现功能

  • 声明式:通过配置让框架实现功能

声明式事务的创建

1、在Spring配置文件中配置文件中配置事务管理器


2、开启事务的注解驱动 之后在需要别事务管理的方法或者是类上添加@Transactional,该方法就会被事务管理

  • 如果标识在方法上,那么该方法被事务管理器管理
  • 如果表示在类上,那么该类的所有方法都会被事务管理器管理
   <!-- 导入外部属性文件 -->
    <context:property-placeholder location="druid.properties"/>

    <!-- 配置数据源 -->
    <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driverClass}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <!-- 配置 JdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!-- 装配数据源 -->
        <property name="dataSource" ref="druidDataSource"></property>
    </bean>

    <!--扫描spring包下的bean-->
    <context:component-scan base-package="com.spring"></context:component-scan>

    <!--配置事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!--设置数据源-->
        <property name="dataSource" ref="druidDataSource"></property>
    </bean>
    <!--
        开启事务的注解驱动
        将使用@Transaction注解所标识的方法或类中所有的方法使用事务进行管理
        transaction-manager属性设置事务管理器的id
        若事务管理器的bean的id默认为transactionManager,那么transaction-manager可以不设置
    -->
    <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>

@Transactional

@Transactional管理的应该是一个逻辑整体,在这个逻辑整体中管理事务,也就是说这个逻辑整体的其中部分出现问题,那么就会由事务管理器进行管理

##### @Transactional中value的值

设置只读readOnly = true

  • 对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这 样数据库就能够针对查询操作来进行优化。

  • 对增删改操作设置只读会抛出下面异常: Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed


超时timeout = 3

事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间 占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。 此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常 程序可以执行。

概括来说就是一句话:超时回滚,释放资源。


事务属性:回滚策略

声明式事务默认只针对运行时异常回滚,编译时异常不回滚。 可以通过@Transactional中相关属性设置回滚策略

rollbackFor属性:需要设置一个Class类型的对象

rollbackForClassName属性:需要设置一个字符串类型的全类名

noRollbackFor属性:需要设置一个Class类型的对象

rollbackFor属性:需要设置一个字符串类型的全类名


propagation事务的传播性

@Transactional(propagation = Propagation.REQUIRED),默认情况,表示如果当前线程上有已经开启的事务可用,那么就在这个事务中运行

@Transactional(propagation = Propagation.REQUIRES_NEW),表示不管当前线程上是否有已经开启的事务,都要开启新事务。

REQUIRED的含义是,支持当前已经存在的事务,如果还没有事务,就创建一个新事务。在上面这个例子中,假设调用aMethod前不存在任何事务,那么执行aMethod时会自动开启一个事务,而由aMethod调用bMethod时,由于事务已经存在,因此会使用已经存在的事务(也就是执行aMethod之前创建的那个事务,不会使用bMethod配置的事务)。

对于这样的配置,如果bMethod过程中发生异常需要回滚,那么aMethod中所进行的所有数据库操作也将同时被回滚,因为这两个方法使用了同一个事务,这样说应该很容易理解吧。

SUPPORTS的含义是,支持当前事务,如果没有事务那么就不在事务中运行。SUPPORTS传播性的逻辑含义比较模糊,因此一般是不推荐使用的。

MANDATORY的含义是,支持当前已经存在的事务,如果还没有事务,就抛出一个异常。如果上例中aMethod的传播性配置为MANDATORY,我们就无法在没有事务的情况下调用aMethod,会抛出异常,因此,传播性为MANDATORY的方法必定是一个其他事务的子事务,当逻辑上独立存在没有意义或者可能违反数据、事务完整性的时候,就可以考虑设置这样的传播性设置。

REQUIRES_NEW的含义是,挂起当前事务,创建一个新事务,new嘛,如果还没有事务,就简单地创建一个新事务,反正是要新建一个事务。

NOT_SUPPORTED的含义是,强制不在事务中运行,如果当前存在一个事务,则挂起该事务,从英文意思上也容易理解,即不需要事务。

NEVER的含义很简单,就是强制要求不在事务中运行,如果当前存在一个事务,则抛出异常,因此如果bMethod传播性是NEVER,则一定抛出异常,从不需要事务。

NESTED的含义是,在当前事务中创建一个嵌套事务,如果还没有事务,那么就简单地创建一个新事务
    @Transactional(
            //只读
//            readOnly = true
            //超时
//            timeout = 3
            //回滚策略
//            noRollbackFor = ArithmeticException.class
            noRollbackForClassName = "java.lang.ArithmeticException",
/*
@Transactional(propagation = Propagation.REQUIRED),默认情况,表示如果当前线程上有已经开启的事务可用,那么就在这个事务中运行。经过观察,购买图书的方buyBook()在checkout()中被调用,checkout()上有事务注解,因此在此事务中执行。所购买的两本图书的价格为80和50,而用户的余额为100,因此在购买第二本图书时余额不足失败,导致整个checkout()回滚,即只要有一本书买不了,就都买不了

@Transactional(propagation = Propagation.REQUIRES_NEW),表示不管当前线程上是否有已经开启的事务,都要开启新事务。同样的场景,每次购买图书都是在buyBook()的事务中执行,因此第一本图书购买成功,事务结束,第二本图书购买失败,只在第二次的buyBook()中回滚,购买第一本图书不受影响,即能买几本就买几本
*/
            propagation = Propagation.REQUIRES_NEW

    )
    public void buyBook(Integer bookId, Integer userId) {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //查询图书的价格
        Integer price = bookDao.getPriceByBookId(bookId);
        //更新图书的库存
        bookDao.updateStock(bookId);
        //更新用户的余额
        bookDao.updateBalance(userId, price);

        System.out.println(1/0);
    }

使用测试

BookDao

/**
 * Created by KingsLanding on 2022/8/3 15:26
 */
public interface BookDao {
    Integer getPriceByBookId(Integer bookId);
    void updateStock(Integer bookId);
    void updateBalance(Integer userId, Integer price);
}

BookDaoImpl

/**
 * Created by KingsLanding on 2022/8/3 15:26
 */
@Repository
public class BookDaoImpl implements BookDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Override
    public Integer getPriceByBookId(Integer bookId) {
        String sql = "select price from t_book where book_id = ?";
        return jdbcTemplate.queryForObject(sql, Integer.class, bookId);
    }
    @Override
    public void updateStock(Integer bookId) {
        String sql = "update t_book set stock = stock - 1 where book_id = ?";
        jdbcTemplate.update(sql, bookId);
    }
    @Override
    public void updateBalance(Integer userId, Integer price) {

        String sql = "update t_user set balance = balance - ? where user_id =?";

        jdbcTemplate.update(sql, price, userId);
    }
}

BookService

/**
 * Created by KingsLanding on 2022/8/3 15:25
 */
public interface BookService {
    void buyBook(Integer bookId, Integer userId);
}

BookServiceImpl

/**
 * Created by KingsLanding on 2022/8/3 15:25
 *
 * 声明式事务的配置步骤
 * 1、在Spring配置文件中配置文件中配置事务管理器
 *     <!--配置事务管理器-->
 *     <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
 *         <!--设置数据源-->
 *         <property name="dataSource" ref="druidDataSource"></property>
 *     </bean>
 * 2、开启事务的注解驱动
 *    之后在需要别事务管理的方法或者是类上添加@Transactional,该方法就会被事务管理
 *    如果标识在方法上,那么该方法被事务管理器管理
 *    如果表示在类上,那么该类的所有方法都会被事务管理器管理
 * @Transactional管理的应该是一个逻辑整体,在这个逻辑整体中管理事务,也就是说这个逻辑整体的其中
 * 部分出现问题,那么就会由事务管理器进行管理
 */
@Service
public class BookServiceImpl implements BookService {
    @Autowired
    private BookDao bookDao;

    @Override
    @Transactional(
            //只读
//            readOnly = true
            //超时
//            timeout = 3
            //回滚策略
//            noRollbackFor = ArithmeticException.class
            noRollbackForClassName = "java.lang.ArithmeticException",
/*
    @Transactional(propagation = Propagation.REQUIRED),默认情况,表示如果当前线程上有已经开
    启的事务可用,那么就在这个事务中运行。经过观察,购买图书的方法buyBook()在checkout()中被调
    用,checkout()上有事务注解,因此在此事务中执行。所购买的两本图书的价格为80和50,而用户的余
    额为100,因此在购买第二本图书时余额不足失败,导致整个checkout()回滚,即只要有一本书买不
    了,就都买不了

    @Transactional(propagation = Propagation.REQUIRES_NEW),表示不管当前线程上是否有已经开启
    的事务,都要开启新事务。同样的场景,每次购买图书都是在buyBook()的事务中执行,因此第一本图
    书购买成功,事务结束,第二本图书购买失败,只在第二次的buyBook()中回滚,购买第一本图书不受
    影响,即能买几本就买几本
 */
            propagation = Propagation.REQUIRES_NEW

    )
    public void buyBook(Integer bookId, Integer userId) {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //查询图书的价格
        Integer price = bookDao.getPriceByBookId(bookId);
        //更新图书的库存
        bookDao.updateStock(bookId);
        //更新用户的余额
        bookDao.updateBalance(userId, price);

        System.out.println(1/0);
    }

}

BookController

/**
 * Created by KingsLanding on 2022/8/3 15:24
 */
@Controller

public class BookController {
    @Autowired
    private BookService bookService;
    public void buyBook(Integer bookId, Integer userId){
        bookService.buyBook(bookId, userId);
    }

    @Autowired
    private ChickService chickService;
    public void checkout(Integer[] bookIds,Integer userId){
        chickService.checkout(bookIds,userId);
    }
}

测试结果

/**
 * Created by KingsLanding on 2022/8/3 15:27
 */
//指定当前测试类在Spring的测试环境中执行,此时就可以通过注入的方式直接获取IOC容器中的bean
@RunWith(SpringJUnit4ClassRunner.class)
//设置Spring测试环境的配置文件
@ContextConfiguration("classpath:Spring-tx.xml")
public class TestNotEx {
    @Autowired
    private BookController bookController;

    @Test
    public void testNotEx(){
        bookController.buyBook(1,1);
    }
 }

事务的传播性测试

ChickService

/**
 * Created by KingsLanding on 2022/8/3 17:49
 */
public interface ChickService {

    void checkout(Integer[] bookIds, Integer userId);

}

ChickServiceImpl

/**
 * Created by KingsLanding on 2022/8/3 17:50
 */
@Service
public class ChickServiceImpl implements ChickService{

    @Autowired
    private BookService bookService;
    @Override
    @Transactional
    public void checkout(Integer[] bookIds, Integer userId) {
        for (Integer bookId : bookIds) {
            bookService.buyBook(bookId,userId);
        }
    }
}

测试结果

@Test
public void testCheckout(){
    bookController.checkout(new Integer[]{1,2},1);
}

事务属性:事务隔离级别

数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事 务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同 的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。

隔离级别一共有四种:

读未提交:READ UNCOMMITTED 允许Transaction01读取Transaction02未提交的修改。

读已提交:READ COMMITTED、 要求Transaction01只能读取Transaction02已提交的修改。

可重复读:REPEATABLE READ 确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它 事务对这个字段进行更新。

串行化:SERIALIZABLE 确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它 事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。

事务的隔离级别.png

使用方式

@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别
@Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交
@Transactional(isolation = Isolation.READ_COMMITTED)//读已提交
@Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读
@Transactional(isolation = Isolation.SERIALIZABLE)//串行化

基于XML的声明式事务

将Spring配置文件中去掉tx:annotation-driven 标签,并添加配置:

<!--扫描spring包下的bean-->
<context:component-scan base-package="com.spring"></context:component-scan>

<!-- 导入外部属性文件 -->
<context:property-placeholder location="druid.properties"/>

<!-- 配置数据源 -->
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="${jdbc.driverClass}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

<!-- 配置 JdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <!-- 装配数据源 -->
    <property name="dataSource" ref="druidDataSource"></property>
</bean>

<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!--设置数据源-->
    <property name="dataSource" ref="druidDataSource"></property>
</bean>

<aop:config>
    <!-- 配置事务通知和切入点表达式 -->
    <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.Spring.*.*(..))"></aop:advisor>
</aop:config>

<!-- tx:advice标签:配置事务通知 -->
<!-- id属性:给事务通知标签设置唯一标识,便于引用 -->
<!-- transaction-manager属性:关联事务管理器 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <!-- tx:method标签:配置具体的事务方法 -->
        <!-- name属性:指定方法名,可以使用星号代表多个字符 -->
        <tx:method name="get*" read-only="true"/>
        <tx:method name="query*" read-only="true"/>
        <tx:method name="find*" read-only="true"/>
        <!-- read-only属性:设置只读属性 -->
        <!-- rollback-for属性:设置回滚的异常 -->
        <!-- no-rollback-for属性:设置不回滚的异常 -->
        <!-- isolation属性:设置事务的隔离级别 -->
        <!-- timeout属性:设置事务的超时属性 -->
        <!-- propagation属性:设置事务的传播行为 -->
        <tx:method name="save*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
        <tx:method name="update*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
        <tx:method name="delete*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
    </tx:attributes>
</tx:advice>