Spring

轻量级开源的JavaEE的开源框架,主要是jar包比较小,解决了开发的复杂性
有两个核心的部分:

  • IOC:控制翻转,创建对象的过程交给Spring
  • AOP:面向切面,在不修改源代码的前提下进行功能增强

特点:

  • 便于解耦合,方便开发
  • 方便测试
  • 方便和其他框架使用
  • 降低Java EE API的使用
  • 提供了事务的支持,方便对事务进行操作

官网:Spring.io

下载地址:点击这里

image-20211018091829183

核心的jar包为:

  • spring-beans-5.3.11.jar
  • spring-context-5.3.11.jar
  • spring-core-5.3.11.jar
  • spring-expression-5.3.11.jar

这4个核心包依赖于commons-logging-1.2.jar

Spring配置文件

  • 首先创建一个类

  • 新建一个xml文件,选择spring配置

    • image-20211018092838127
  • 创建一个<bean></bean>标签,格式如下

    • <!--    class后的属性为全类名,id为这个标签的名字-->
          <bean class="com.company.User" id="user"></bean>
      

使用配置文件创建类:

  • 加载spring配置文件

    • //应用程序上下文
      ApplicationContext context = 
          //在类路径下读取xml文件,因此xml配置文件需要放在src下
          new ClassPathXmlApplicationContext("bean1.xml");
      
  • 通过配置创建对象

    • 使用第一步加载好配置文件的对象调用它的getBean("类名", 类名.class)
<!-- bean1.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">
<!--    class后的属性为全类名,id为这个标签的名字-->
    <bean class="com.company.User" id="user"></bean>
</beans>
//User类
package com.company;

public class User {
    public String name;
    public int age;
    public boolean sex;

    public void function(){
        System.out.println("hello spring");
    }
}
//测试类
package com.company;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestSpring {
    @Test
    public void test1(){
//        加载spring配置文件
        ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
//        获取配置创建的对象
        User user = context.getBean("user", User.class);
        user.function();
    }
}

IOC容器

全称为控制反转,英文全称为Inversion Of Control,用来降低代码的耦合度,对象的创建和对象之间的调用都交给Spring去做,使Spring进行管理

底层原理:

  • xml解析
  • 工厂设计模式
  • 反射

例如,使用普通的方式创建对象和调用方法的大体流程如下

image-20211018191503392

虽然能够实现,但耦合度太高了

可以使用工厂模式进行简单的解决

image-20211018191810036

使用工厂模式并没有将耦合度降至最低,可以使用IOC

image-20211018192456197

使用IOC的写法降低了耦合度,具体表现在当一个类中的路径被修改了,只需要改一下xml配置文件,其他的就不用管了

Spring中实现IOC容器的两种方式(两个接口):

  • BeanFactory:为IOC容器的基本实现,是Spring内部的实现接口,即spring自带的,一般不直接使用这种方式,在加载配置文件时不会直接创建,只有在获取创建的对象时才会创建

    • //        加载spring配置文件(此时不创建对象)
              BeanFactory context = new ClassPathXmlApplicationContext("bean1.xml");
      //        获取配置创建的对象(此时才创建对象)
              User user = context.getBean("user", User.class);
      
  • ApplicationContext:是BeanFactory的子接口,功能更强大,一般使用这种方式

    • //        加载spring配置文件(此时会创建对象)
              ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
      //        获取配置创建的对象
              User user = context.getBean("user", User.class);
      

      使用ApplicationContext方式最好,因为在WEB中,启动Tomcat时直接加载会节省很多的时间

ApplicationContext中,有两个主要的实现类,分别是:

  • FileSystemXmlApplicationContext:需要填写xml在本地磁盘的路径
  • ClassPathXmlApplicationContext:文件需要放在src下,填写在src下的路径

IOC Bean管理操作

Bean管理:

  • Spring创建对象
    • 编写xml文件
  • Spring注入属性
    • 在一个类中赋值

操作方式

分为:

基于xml配置文件和基于注解方式实现

基于xml配置文件
  • 之前在xml中所写的<bean class="com.company.User" id="user"></bean>就是基于xml进行操作的
  • bean中的属性:
    • id属性:获取对象的唯一个标识
    • class属性:类的全路径,包含类路径
    • name属性:id属性中,不能带有特殊字符,在name属性可以带有特殊字符,这个属性之前是为了兼容其他的框架而存在的(现在用的不多的一个属性)
  • 创建对象时,默认使用的时无参构造的方法实现的对象的创建
  • 基于XML方式注入属性:
    • DI:依赖注入,也就是注入属性
      • 原始的注入方式:使用提供的setXXX()的方法、有参构造进行注入

Spring本身也支持setXXX()的方法、有参构造进行注入

  • 使用setXXX()的方法进行注入:

    • 在xml文件中的<bean></bean>标签中写一个<property name = "变量名" value = "值"></property>

    • <bean class="com.company.User" id="user">
          <!-- 属性含义
              name = "变量名"
              value = "值"
          -->
          <property name="age" value="88"></property>
      
      
          <!--也可以写为-->
          <property name="age">
              <value>值</value>
          </property>
      
      
          <property name="name" value="名字"></property>
          <property name="sex" value="true"></property>
      </bean>
      
  • 使用有参构造的方法进行注入:

    • 修改配置文件,在bean中加入多组<constructor-arg name = "变量名" value = "值"></constructor-arg>标签

    • <bean class="com.company.User" id="user2">
          <!--使用有参构造,需要多组<constructor-arg></constructor-arg>标签,因为需要匹配到相关的构造器
              name = "变量名"
              value = "值" 或者 index = "值"
              也可以使用index属性,格式为index = "值",代表可以通过索引进行赋值,0代表有参构造的第一个参数,以此类推
          -->
          <constructor-arg name="age" value="99"></constructor-arg>
          <constructor-arg name="name" value="name2"></constructor-arg>
          <constructor-arg name="sex" value="false"></constructor-arg>
      </bean>
      

还有一种p名称空间注入,用的并不是很多,这里不再赘述

//有这么一个类,有对这个类提供的get和set方法
class User{
    public String name;
    public int age;
    public boolean sex;
}

注入其他类型的值:

  • 空值null

    • 以User类中的String为例,给String一个空值

    • <property></property>标签中,去掉value属性,只保留name属性,在标签中添加<null></null>标签

    • <!--赋一个空值-->
      <property name="name">
          <null></null>
      </property>
      
  • 值中含有特殊符号,例如<>

    • 解决办法1:使用xml中的转义字符,例如&lt&gt
    • 解决办法2:使用<![CDATA[内容]]>,idea有快捷的方式,在<value></value>标签中输入CD即可得到

注入外部bean:

  • 即如果一个类中有一个属性是一个对象,可以使用这种方式进行注入

  • 例如,有两个类

    • package com.company;
      
      public class Class1 {
          private Class2 class2;
          private String name;
      
          public Class1(Class2 class2, String name) {
              this.class2 = class2;
              this.name = name;
          }
      
          public void setClass2(Class2 class2) {
              this.class2 = class2;
          }
      
          public void setName(String name) {
              this.name = name;
          }
      
          public Class1() {
          }
      
          @Override
          public String toString() {
              return "Class1{" +
                      "class2=" + class2 +
                      ", name='" + name + '\'' +
                      '}';
          }
      }
      
    • package com.company;
      
      public class Class2 {
          private String name;
      
          public Class2(String name) {
              this.name = name;
          }
      
          @Override
          public String toString() {
              return "Class2{" +
                      "name='" + name + '\'' +
                      '}';
          }
      }
      
    • 可见,此时class1中有一个类型为class2的属性

  • 注入过程:

    • 需要有两个<bean></bean>标签,分别是关于class1和class2的标签

    • 在class1的<bean>标签的关于class2属性的<property></property>标签中填写格式如下

      • <property name = "变量名" ref = "class2的bean标签的id">
        </property>
        
    • <bean id="classOne" class="com.company.Class1">
          <property name="class2" ref="classTwo">
          </property>
      </bean>
      
      <bean id="classTwo" class="com.company.Class2">
          <constructor-arg index="0" value="hello">
          </constructor-arg>
      </bean>
      

注入内部bean

  • 相当于外部bean的套娃写法

  • 在上例中的两个类的写法可以采取内部bean的方式,具体写法为

    • ClassOne写一个<bean></bean>标签,bean标签中为需要的属性创建<property></property>标签,而在关于Class2属性的标签中,为Class2创建一个<bean>标签,在此标签内进行相关的操作

    •     <bean class="com.company.Class1" id="classOne">
              <property name="name">
                  <value><![CDATA[<<<内容>>>]]></value>
              </property>
              <property name="class2">
      <!--            在内部注入bean-->
                  <bean id="classTwo" class="com.company.Class2">
                      <constructor-arg name="name">
                          <value>world</value>
                      </constructor-arg>
                  </bean>
              </property>
          </bean>
      
  • 一般外部bean比较清晰,使用范围广

级联赋值:

  • 方式1:外部bean中写需要注入的bean时,在这个标签内写相关的赋值的标签

  • 方式2:是在外部bean的基础上进行的,需要给需要操作的对象提供好get方法,只有这样才能够获取到这个对象,从而对这个对象进行操作

    •     <bean id="classOne" class="com.company.Class1">
              <property name="class2" ref="classTwo">
              </property>
      <!--        级联赋值的第二种写法-->
              <property name="class2.name">
                  <value>gogog</value>
              </property>
          </bean>
      
          <bean id="classTwo" class="com.company.Class2">
              <constructor-arg index="0" value="hello">
              </constructor-arg>
          </bean>
      

注入数组:

  • <property></property>不要直接写value属性,在标签中写<array></array>标签,并且在<array></array>标签中写<value><value>,value标签可以有多组,一个value标签代表数组中的一个元素的值

  • <property name="需要注入的数组名">
        <array>
            <value>1</value>
            <value>1</value>
            <value>1</value>
            <value>1</value>
            <value>1</value>
            <value>1</value>
            <value>1</value>
        </array>
    </property>
    

List集合属性注入:

  • 整体写法和数组类似,只不过将<property></property>标签中的<array></array>标签换成了<list></list>标签,依旧要有<value></value>标签

  • <property name="list">
        <list>
            <value>2</value>
            <value>2</value>
            <value>2</value>
            <value>2</value>
            <value>2</value>
            <value>2</value>
            <value>2</value>
            <value>2</value>
        </list>
    </property>
    

MapSet的注入:

  • 注入方式都类似

    • Map是在<property></property>标签中写<map></map>,在<map></map>标签中写<entry key="key" value = "value"></entry>
    • Set是在<property></property>标签中写<set></set>,在<set></set>标签中写<value>值</value>标签
  • <!--        map-->
            <property name="map">
                <map>
                    <entry key="key1" value="value1"></entry>
                    <entry key="key2" value="value2"></entry>
                    <entry key="key3" value="value3"></entry>
                </map>
            </property>
    <!--        set-->
            <property name="set">
                <set>
                    <value>26</value>
                    <value>226</value>
                    <value>2634</value>
                </set>
            </property>
    

在集合中设置对象类型的值:

  • 对于List集合中的泛型填写一个对象的类型时,在<list></list>标签中加入多组<ref bean = "bean"></ref>标签

    • <bean id="classThree" class = "com.company.Class3">
          <property name="list">
              <ref bean="class2_1"></ref>
          </property>
      </bean>
      
      <bean id="class2_1" class="com.company.Class4">
          <property name="key">
              <value>555</value>
          </property>
          <property name="value">
              <value>84</value>
          </property>
      </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:util="http://www.springframework.org/schema/util"
             xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">
      
      
    • 设置和使用

      <?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:util="http://www.springframework.org/schema/util"
             xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">
          <bean id="classThree" class = "com.company.Class3">
              <property name="list" ref="publicList1">
              </property>
          </bean>
          <util:list id="publicList1">
              <!--        对于普通的数据类型,还是使用<value></value>标签-->
              <!--        对于对象的数据类型,还是使用<ref></ref>标签-->
              <value>555</value>
              <value>35</value>
              <value>54355</value>
              <value>5455</value>
              <value>5355</value>
          </util:list>
      </beans>
      
    • image-20211019200736976

工厂Bean(Factory Bean)

有两种bean,一种是普通的bean(之前自定义的),另外一种时工厂bean

两者的区别:普通bean定义的是什么类型,返回的就是什么类型,工厂bean定义的类型和返回的类型不一定相同

工厂bean的使用:

  • 创建类,让这个类作为工厂bean,实现接口FactroyBean

  • 实现接口中搞的方法,在实现的方法中定义返回的bean类型

  • package com.company;
    
    import org.springframework.beans.factory.FactoryBean;
    
    public class MyBean implements FactoryBean<Class1> {
    //    返回一个bean实例,此时定义的类型和返回的类型不同
        @Override
        public Class1 getObject() throws Exception {
            //新建一个类
            Class1 class1 = new Class1();
            //设置一个值
            class1.setA(new int[] {1,2,4,5});
            return class1;
        }
    
    //    返回类型
        @Override
        public Class<?> getObjectType() {
            return null;
        }
    
    //    是否是单例模式
        @Override
        public boolean isSingleton() {
            return FactoryBean.super.isSingleton();
        }
    }
    

    此时,如果直接按照之前的方式进行获取一个类,通过context.getBean()获取类的时候

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean2.xml");
        Class1 class1 = context.getBean("factory", Class1.class);
        System.out.println(class1);
    }
    

    此时返回的不是MyBean类,而是写在MyBean类中getObject()方法返回的类

工厂bean的作用域:

  • 默认为单实例

  • 单实例:只有一个对象

  • 多实例:每次执行getBean()方法都创建一个新的对象

  • 验证

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean2.xml");
        Class1 class1 = context.getBean("factory", Class1.class);
        Class1 class2 = context.getBean("factory", Class1.class);
        System.out.println("更改class2中的a数组之前,class1中的内容");
        System.out.println(class1);
        class2.setA(new int[]{99, 88, 8});
        System.out.println("更改class2中的a数组之后,class1中的内容");
        //此时class1和class2中的数组a中的值都被修改了
        System.out.println("class1 = " + class1);
        System.out.println("class2 = " + class2);
    }
    
  • 设置单实例或者多实例:

    • <bean></bean>标签中添加scope属性,属性值为prototypesingleton

    • scope中文为范围、眼界、广度,读音为skoʊp

    • prototype中文为原型、样板、榜样,读音为ˈproʊtətaɪp,设置此值为多实例,当设置为此值的时候,只有在执行getBean()方法时才会创建对象,加载配置文件时不会创建

    • singleton读音ˈsɪŋɡltən,设置此值为单实例

    • <bean id="factory" class="com.company.MyBean" scope="prototype">
      
      </bean>
      
Bean的生命周期

过程:

  • 创建bean实例

  • 为bean属性设置值和对其他bean的引用

  • 把bean实例传递给bean前置处理器的方法

    • process中文为过程,读音为prəˈses
    • processor中文为处理器,读音为ˈprɑːsesər
    • 新建一个类,使其实现BeanPostProcessor接口
    • 重写其中的postProcessBeforeInitialization方法
    • 将这个类绑定到xml配置文件中
  • 调用bean的初始化方法(需要配置)

    • <bean></bean>标签中添加init-method属性,属性值为这个类中需要被执行的一个方法名
  • 把bean实例传递给bean后置处理器的方法

    • 在实现类实现BeanPostProcessor接口的类中重写postProcessAfterInitialization
  • 此时bean获取到了,也就是可以使用了

  • 当容器关闭时,调用bean的销毁方法,销毁方法 需要进行配置

    • <bean></bean>标签中添加destroy-method属性,属性值为这个类中需要被执行的一个方法名
    • ClassPathXmlApplicationContext类中有一个close()方法,可以关闭容器
  • 当一个类实现了BeanPostProcessor接口,并将其加到xml配置文件中,默认的会给该配置文件中其他的类加上前置/后置处理器

  • 测试实例

  •     public static void main(String[] args) {
            ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("bean3.xml");
            Class5 classFive = context.getBean("classFive", Class5.class);
            System.out.println("第六步 获取到了bean");
            context.close();
        }
    
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
        <bean class="com.company.Class5" id="classFive" init-method="initMethod" destroy-method="destroyMethod">
            <property name="name">
                <value>names</value>
            </property>
        </bean>
    
        <bean id="myBeanPost" class="com.company.MyBeanPost">
    
        </bean>
    </beans>
    
package com.company;

public class Class5 {
    private String name;

    public Class5() {
        System.out.println("第一步 构造器被执行");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
        System.out.println("第二步 设置值");
    }

    public void initMethod(){
        System.out.println("第四步 执行初始化方法");
    }

    public void destroyMethod(){
        System.out.println("第七步 销毁方法");
    }
}

package com.company;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;

public class MyBeanPost implements BeanPostProcessor {
//    前置处理器
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("第三步 前置处理器");
        return bean;
    }

//    后置处理器
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("第五步 后置处理器");
        return bean;
    }
}

输出内容:

第一步 构造器被执行
第二步 设置值
第三步 前置处理器
第四步 执行初始化方法
第五步 后置处理器
第六步 获取到了bean
第七步 销毁方法
XML的自动装配

手动装配:在<property></property>标签中手动的添加属性和值,例如name=xxxvalue=xxx,而自动装配就不需要写这些值也可以做到值的注入

自动装配:是指根据指定的规则(属性名称或者类型),Spring会将属性和值自动的注入

可以根据类型或者属性名进行自动装配 在bean标签中添加autowire 属性值有两个 byNamebyType,当为属性名时,此时新建的需要被装配的类的<bean></bean>标签中的id的值必须和需要装配的属性名相同

<bean id="classSix" class="com.company.Class6" autowire="byName">

</bean>
<bean id="class7" class="com.company.Class7">

</bean>

例如以上xml是针对

public class Class6 {
    private Class7 class7;

    public Class7 getClass7() {
        return class7;
    }

    public void setClass7(Class7 class7) {
        this.class7 = class7;
    }

    @Override
    public String toString() {
        return "Class6{" +
                "class7=" + class7 +
                '}';
    }
}

进行编写的,如果将第2行修改为private Class7 classSeven;,那么此时,xml文件的内容将会变为

<bean id="classSix" class="com.company.Class6" autowire="byName">

</bean>
<bean id="classSeven" class="com.company.Class7">

</bean>

如果将autowire设为byType时,此时如果一个类有多个<bean>标签就会报错,因此此时找不到需要设置的标签,这种情况下只能通过byName进行解决

外部属性文件

当一个类的属性逐渐变多后配置文件会变得特别的繁杂,有一些固定的值(不会被修改的值)可以放入到xml文件中

例如Druid数据库,也可以放在IOC中

public class Main {

    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("dataBase.xml");
        DruidDataSource druid = context.getBean("druid", DruidDataSource.class);
        try {
            System.out.println(druid.getConnection());
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
<bean id="druid" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="url" value="jdbc:mysql://localhost:3306/test"/>
    <property name="username" value="用户名"/>
    <property name="password" value="密码"/>
</bean>

可以将以上的属性放到一个properties文件中

如果想要读取properties文件,需要引入一个名称空间(context),大致内容如下

<?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 https://www.springframework.org/schema/context/spring-context.xsd">

<context:property-placeholder location="properties文件名"/>

placeholder中文为占位符,使用${属性名}​读取properties中的相关属性,不知道为什么,用户名始终无法读取,只好写死

<?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 https://www.springframework.org/schema/context/spring-context.xsd">
    <context:property-placeholder location="jdbc.properties"/>

    <bean id="druid" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${url}"/>
        <property name="username" value="root"/>
        <property name="password" value="${password}"/>
    </bean>
</beans>
url=jdbc:mysql://localhost:3306/test
password=密码
基于注解方式

主要目的:简化xml繁琐的配置

注解:代码的标记方式,可以在类、方法、属性上加注解

注解格式:@注解名称(属性名称 = 属性值, ...., 属性名称 = 属性值)

spring提供了4个注解,分别是:

  • @Component,中文为组件,读音为kəmˈpoʊnənt,是spring提供的普通组件,用这个都可以创建对象
  • @Service,一般用于业务逻辑层和service层上
  • @Controller,一般用于web层
  • @Repository,中文为存储库、知识库,读音为rɪˈpɑːzətɔːri,一般用在dao层或者持久层上

以上注解可以随便用,作用范围、作用都相同

需要引入一个依赖,依赖的jar为spring-aop.jar

步骤:

  • 引入依赖(jar包)

  • 开启组件扫描

    • 告诉spring容器,要在哪个类加注解,让其扫描这个类
    • 需要新建一个xml配置文件,引入context命名空间
    • 使用<context:component-scan base-package="包名1,...., 包名n"></context:component-scan>
    • 设置需要组件扫描的包base-package=属性
  • 任选前边4个中的一个注解,格式@注解(value="bean名")或者省略括号中的内容,即@注解默认value的值为类名的首字母小写

    • package com.company;
      
      import org.springframework.stereotype.Component;
      
      @Component(value = "test") //等价于<bean id="test" class="com.company.Test"></bean>
      public class Test {
      }
      
      //或者
      @Component //等价于<bean id="test" class="com.company.Test"></bean>
      public class Test {
      }
      
  • 创建

    • public static void main(String[] args) {
          ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("annotation.xml");
          Test test = context.getBean("test", Test.class);
          test.method();
      }
      

也可以进行配置扫描的内容

<context:component-scan base-package="com.company" use-default-filters="false">
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/>
</context:component-scan>

代表关闭默认过滤器,配置过滤器过滤注解,只扫描@Component标识的类

<context:component-scan base-package="com.company" use-default-filters="false">
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/>
</context:component-scan>

代表关闭默认过滤器,配置过滤器过滤注解,不扫描@Component标识的类

注入属性:

  • 所需要的注解:

    • @Autowired:wired 中文为有线,读音为ˈwaɪərd,根据属性的类型自动注入
    • @Qualifier,读音为ˈkwɑːlɪfaɪər,根据属性名称注入
    • @Resource,可以根据类型注入,也可以根据名称注入
    • 前三个都针对对象类型
    • @Value,针对普通类型
  • @Autowire:直接在需要注入的属性上添加这个注解即可,需要保证被注入的类要被用注解标注过

  • @Qualifier:需要配合@Autowire在一起使用,例如有的类使用注解进行标识时,使用value属性指定过名字,如果此时直接使用@Autowire进行注入时,可能会找不到这个类,所以要使用@Qualifier(value="标注名")或者一个接口有多个实现类时,可以使用这个注解进行指定需要注入的类

    • 示例,当一个接口有多个实现类时,若只使用@Autowired会抛出异常

    • package com.company.class2;
      
      import org.springframework.stereotype.Component;
      
      @Component
      public interface InterfaceOne {
          void method();
      }
      
      package com.company.class2;
      
      import org.springframework.stereotype.Component;
      
      @Component
      public class InterfaceOneImpl1 implements InterfaceOne{
          @Override
          public void method() {
              System.out.println("实现类1");
          }
      }
      
      package com.company.class2;
      
      import org.springframework.stereotype.Component;
      
      @Component
      public class InterfaceOneImpl2 implements InterfaceOne{
          @Override
          public void method() {
              System.out.println("实现类2");
          }
      }
      
      package com.company;
      
      import com.company.class2.InterfaceOne;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.beans.factory.annotation.Qualifier;
      import org.springframework.stereotype.Component;
      import org.springframework.stereotype.Service;
      
      @Component
      public class Test {
          @Autowired
          @Qualifier("interfaceOneImpl2")
          InterfaceOne interfaceOne;
      
          @Override
          public String toString() {
              return "Test{" +
                      "interfaceOne=" + interfaceOne +
                      '}';
          }
      }
      
      package com.company;
      
      import org.springframework.context.support.ClassPathXmlApplicationContext;
      
      public class Main {
      
          public static void main(String[] args) {
              ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("annotation.xml");
              Test test = context.getBean("test", Test.class);
              test.interfaceOne.method();
          }
      }
      

      输出结果为:实现类2

  • @Resource

    • 依赖于javax.annotation.jar

    • 这个注解不是spring提供的

    • @Resource代表用根据类型注入

    • @Resourse(name="名称")根据名称进行注入

    • @Component
      public class Test {
          @Resource(name = "interfaceOneImpl1")
          InterfaceOne interfaceOne;
      
          @Override
          public String toString() {
              return "Test{" +
                      "interfaceOne=" + interfaceOne +
                      '}';
          }
      }
      
  • @Value

    • @Component
      public class Test {
          @Value("hello")
          private String s;
          @Value("222")
          private int v;
          @Resource(name = "interfaceOneImpl1")
          InterfaceOne interfaceOne;
      
          @Override
          public String toString() {
              return "Test{" +
                      "s='" + s + '\'' +
                      ", v=" + v +
                      ", interfaceOne=" + interfaceOne +
                      '}';
          }
      }
      

完全注解开发(使用配置类)

创建配置类,代替xml配置文件

在配置类前添加@Configuration注解,Configuratation读音为kənˌfɪɡjəˈreɪʃn

在之前的方法中,需要手动的加载配置文件,现在无需再加载配置文件,改为加载配置类,其他地方和之前都一样

public static void main(String[] args) {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(配置类名.class);
    Test test = context.getBean("test", Test.class);
    System.out.println(test);
}

动态代理

知识点部分属于JavaSE

使用一个对象将代理包装起来,用该代理对象取代原始对象,对于任何对原始对象的调用都要通过代理,代理对象决定是否以及何时将方法调用转到原始对象上,动态代理的底层是反射

也就是在运行期间动态的对一个接口创建类

首先是静态代理

/**
 * 工厂接口
 */
public interface Factory {
    void proxy();
}

/**
 * 代理类
 */
class ProxyClass{
    private Factory factory;

    public ProxyClass(Factory factory) {
//        使用被代理对象进行赋值,多态
        this.factory = factory;
    }

    public void useFactory(){
        System.out.println("代理工厂开始工作");
//        开始代理
        factory.proxy();
        System.out.println("代理工程结束工作");
    }
}

/**
 * 实现类,可以看作被代理类
 */
class FactoryImpl1 implements Factory{

    @Override
    public void proxy() {
        System.out.println("代理工厂实现类1");
    }
}
/**
 * 实现类,可以看作被代理类
 */
class FactoryImpl2 implements Factory{
    @Override
    public void proxy() {
        System.out.println("代理工厂实现类2");
    }
}

/**
 * 静态代理测试类
 */
class StaticProxyTest{
    public static void main(String[] args) {
//        实例化一个代理类
        ProxyClass proxy = new ProxyClass(new FactoryImpl1());
        proxy.useFactory();
    }
}

首先需要一个接口,一个接口有多个实现类,还有一个代理类,代理类负责执行被代理类,此时代码是写死的,由于是写死的,所以可能会导致类的数量翻倍

要想实现动态代理,需要解决以下问题:

  • 如何根据加载到内存的被代理类,动态的创建一个代理对象和代理类
  • 通过代理类调用方法时,如何通过被代理类调用同名的方法

动态代理

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 工厂接口
 */
public interface Factory {
    void proxy();
}

/**
 * 代理类
 */
class ProxyClass{
    private Factory factory;

    public ProxyClass(Factory factory) {
//        使用被代理对象进行赋值,多态
        this.factory = factory;
    }

    public void useFactory(){
        System.out.println("代理工厂开始工作");
//        开始代理
        factory.proxy();
        System.out.println("代理工程结束工作");
    }
}

/**
 * 实现类,可以看作被代理类
 */
class FactoryImpl1 implements Factory{

    @Override
    public void proxy() {
        System.out.println("代理工厂实现类1");
    }
}
/**
 * 实现类,可以看作被代理类
 */
class FactoryImpl2 implements Factory{
    @Override
    public void proxy() {
        System.out.println("代理工厂实现类2");
    }
}

/**
 * 静态代理测试类
 */
class StaticProxyTest{
    public static void main(String[] args) {
//        实例化一个代理类
        ProxyClass proxy = new ProxyClass(new FactoryImpl1());
        proxy.useFactory();
    }
}

/**
 * 代理类,用来解决被代理类加载到内存中时动态的创建一个代理对象和代理类
 */
class ProxyFactory{
    //因为可能还要代理其他类型,所以也不能写死,所以要使用Object类型
    public static Object getInstance(Object obj){ //obj为被代理的对象,作用是得到被代理的对象
        InvocationHandlerImpl invocation = new InvocationHandlerImpl();
//        将被代理类赋给obj
        invocation.bind(obj);
//        参数1为被代理类的加载器
//        参数2为被代理类实现的的接口的加载器
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), invocation);
    }
}

class InvocationHandlerImpl implements java.lang.reflect.InvocationHandler{
//    需要使用被代理类对象进行赋值
    private Object obj;

//    将被代理类赋给obj
    public void bind(Object obj){
        this.obj = obj;
    }
    /**
     * 当通过代理类的对象调用某个方法时,会自动调用invoke方法
     * <br>将被代理类要执行的方法的功能写在的invoke中
     * @param proxy-代理类的对象
     * @param method-代理类对象所调用的方法
     * @param args-方法的参数
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//        为代理类要调用的方法,obj为被代理的对象
        return method.invoke(obj,args);
    }
}

class Test{
    public static void main(String[] args) {
        FactoryImpl1 factoryImpl1 = new FactoryImpl1();
        Factory instance = (Factory) ProxyFactory.getInstance(factoryImpl1);
        instance.proxy();
    }
}

java.lang.reflect.Proxy类:

  • 这个类提供了创建对象的静态方法

  • 这个类中有一个静态方法newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)方法,该方法返回指定代理类的实例

  • 参数1:loader类的加载器

  • 参数2:interfaces 接口的class,一个类可能实现了多个接口,所以是数组

  • 参数3:InvocationHandler为一个接口,在这个接口中可以写一些东西,也可以用来增强原有的方法,invocation 中文为处理,handler为处理,使用这个生成的实例调用方法时,调用的时这个接口中的invoke方法,invoke()方法:处理实例中调用的方法并返回结果

AOP容器

AOP全称为Aspect-oriented programming,中文全称为面向切面编程,是面向对象的延续和扩展,主要是不通过修改源代码的方式实现在主干中添加新功能

底层采用动态代理实现的,有两种情况的动态代理:

  • 有接口的情况:使用JDK动态代理
  • 没有接口的情况:使用CGLIB动态代理

image-20211021121109223

一些专业术语

主要有四个,分别是:

  • 连接点
    • 一个类中的一些方法可以被增强(扩充),能被扩充的这些方法被称为连接点
  • 切入点
    • 虽然有的方法可以被扩充,但在实际中只会扩充部分方法,被扩充的这部分方法称为切入点
  • 通知(增强)
    • 一个方法被扩充后,扩充的部分称为通知(增强)
    • 通知的类型:
      • 前置通知
        • 在一个方法前扩充
      • 后置通知
        • 在一个方法后扩充
      • 环绕通知
        • 在一个方法前后都有扩充
      • 异常通知
        • 出现了异常才会执行的
      • 最终通知
        • 有没有异常都会执行
  • 切面
    • 把通知用于切入点的过程

实现

都是基于aspectJ

aspect中文为方面、面目、详情,读音为ˈæspekt

aspectJ并不是spring的组成部分,是一个独立的AOP框架,一般把aspectJ和spring配合使用

有两种操作方式:

  • xml配置文件方式
  • 注解方式

依赖于:

切入点表达式

确定对哪个类中的哪个方法进行增强

格式execution(权限修饰符 返回值类型 类的全路径 方法名称 (参数列表))

  • 权限修饰符可以省略
  • 返回值类型可以用*表示,代表全部匹配
  • 类的全路径和方法名称可以写一块,格式为全路径.方法名
  • 参数列表内的值可以用..表示
  • 类的全路径中也可以含有*代表全部匹配

注解方式实现

  • 创建xml文件(或者配置类)

  • 开启注解扫描

  • 开启aspect生成代理对象

    • <!--    开启aspect生成代理对象-->
      <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
      
  • 配置不同类型的通知

    • 首先再增强类上添加@Aspect注解
    • 前置通知:@before(value = "execution(表达式)"),该注解位于org.aspectj.lang.annotation
    • @After(value = "execution(表达式)")
      • 在被扩充的方法后执行,一般被称为最终通知
    • @AfterReturning(value = "execution(表达式)")
      • 在被扩充的方法返回值之后执行,被称为后置通知
    • @Around(value = "execution(表达式)")
      • 需要在增强的函数参数列表中添加一个参数,ProceedingJoinPoint类型的参数,在需要调用原有函数的位置,直接调用ProceedingJoinPoint类型的参数的proceed()方法
    • @AfterThrowing(value = "execution(表达式)"):异常通知,在抛出异常后给出通知
package AOP;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Component;

@Component
//被增强的类
public class Clazz {
    public void function(){
        System.out.println("function");
    }
    public void function2(){
        System.out.println("function2");
    }
    public void function3(){
        System.out.println("function3");
    }
    public void function4() throws Exception{
        System.out.println("function4,模拟异常");
        throw new Exception("手动模拟异常");
    }
}

//增强类
@Component
@Aspect //代表生成代理对象
class Clazz2{
//    前置通知
    @Before(value = "execution(* AOP.Clazz.function(..))")
    public void before(){
        System.out.println("前置通知");
    }
//    在执行完被扩充的通知后执行,一般被称为最终通知
    @After(value = "execution(* AOP.Clazz.function2(..))")
    public void after(){
        System.out.println("after,最终通知");
    }
//    环绕通知
    @Around(value = "execution(* AOP.Clazz.function3(..))")
    public void around(ProceedingJoinPoint proceedingJoinPoint){
        System.out.println("环绕前");
        try {
            proceedingJoinPoint.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        System.out.println("环绕后");
    }
//    返回值之后通知,通常称为后置通知
    @AfterReturning(value = "execution(* AOP.Clazz.function2(..))")
    public void returning(){
        System.out.println("返回值之后执行,后置通知");
    }
//    异常通知
    @AfterThrowing(value = "execution(* AOP.Clazz.function4(..))")
    public void afterThrowing(){
        System.out.println("抛出异常后执行的内容");
    }
}

class TestBefore{
    public static void main(String[] args) throws Exception{
        ApplicationContext context = new ClassPathXmlApplicationContext("/AOP/bean.xml");
        Clazz clazz = context.getBean("clazz", Clazz.class);
        clazz.function();
        System.out.println("----------------");
        clazz.function2();
        System.out.println("----------------");
        clazz.function3();
        System.out.println("----------------");
        clazz.function4();
    }
}

结果为

前置通知
function
----------------
function2
返回值之后执行,后置通知
after,最终通知
----------------
环绕前
function3
环绕后
----------------
function4,模拟异常
抛出异常后执行的内容
Exception in thread "main" java.lang.Exception: 手动模拟异常
	at AOP.Clazz.function4(Clazz.java:24)
	at AOP.Clazz$$FastClassBySpringCGLIB$$8e31332c.invoke(<generated>)
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:783)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753)
	at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:64)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:698)
	at AOP.Clazz$$EnhancerBySpringCGLIB$$17c35f49.function4(<generated>)
	at AOP.TestBefore.main(Clazz.java:75)

进程已结束,退出代码为 1

提取公共的切入点

由上例可知,上例中有多处出现了相同的execution表达式

只需要新建一个函数,为其添加@Pointcut(value=execution表达式),之后使用其他注解时,只需要@注解(value="被添加@pointcut的注解方法名")即可

例如

//提取公共的切入点
@Pointcut(value = "execution(* AOP.Clazz.function(..))")
public void publicPointFunction(){

}
//    前置通知
@Before(value = "publicPointFunction()")
public void before(){
    System.out.println("前置通知");
}

设置优先级

如果一个方法被多个增强类增强,可以设置优先级

在增强类上增加一个注解,注解为@Order(数字)

值越小,优先级越高

xml文件实现

也可以实现,但大多都是使用注解,视频链接

JDBC Template

template中文为模板,所以又称为JDBC模板

使用JDBC Template可以很方便的对数据库进行增删改查的操作

依赖于

  • MySQL提供的jar
  • druid德鲁伊的jar
  • spring-jdbc-5.3.11.jar关于JDBC操作的
  • spring-tx-5.3.11.jar关于事务的操作
  • spring-orm-5.3.11.jar如果整合了其他框架的操作时,必须要导入这个jar包,如果不整合其他框架,可以不引入

配置方式

  • 对于德鲁伊连接池,可以在xml文件中配置连接数据库的信息
  • 注入JdbcTemplate对象,需要注入DataSource对象,打开JdbcTemplate源码可以发现有两个构造器,两个有参,一个空参,可以通过有参的构造器传入DataSource,也可以通过set方法注入

JdbcTemplate定义了许多的方法

    • 对于增删改,可以使用update(String sql, Object... obj)方法,用法和dbUtil的方式差不多

      其中,sql语句也可以使用占位符

      package JDBC.sevice;
      
      import JDBC.bean.User;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.jdbc.core.JdbcTemplate;
      import org.springframework.stereotype.Service;
      
      @Service
      public class UserService {
          @Autowired
          private JdbcTemplate jdbcTemplate;
      
          public JdbcTemplate getJdbcTemplate() {
              return jdbcTemplate;
          }
      
          public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
              this.jdbcTemplate = jdbcTemplate;
          }
      
          public int addUser(User user) {
              String sql = "insert into user(name, password, address, phone) values(?, ?, ?, ?)";
              return jdbcTemplate.update(sql, user.getName(), user.getPassword(), user.getAddress(), user.getPhone());
          }
      
          public int updateUser(User user, int id) {
              String sql = "update user set name=?,password=?,address=?,phone=? where id = ?";
              return jdbcTemplate.update(sql, user.getName(), user.getPassword(), user.getAddress(), user.getPhone(), id);
          }
      
          public int deleteUser(int id){
              String sql = "delete from user where id = ?";
              return jdbcTemplate.update(sql, id);
          }
      }
      
    • 查比较复杂,大致分为以下几种情况:

    • 查询返回某个值

      • jdbcTemplate.queryForObject(String sql, Class<T> clazz)
        
      • 以上方法的返回值类型为<T> T

      • 第二个参数可以填写为一个类的.class

    • 查询返回对象

      • 使用jdbcTemplate.queryForObject(String sql, RowMapper<T> rowMapper, Object... obj)
      • 其中的RowMapper是一个接口,可以使用spring提供好的一个实现类,这个实现类为BeanPropertyRowMapper,这个实现类可以将得到的数据自动注入到一个对象中并返回
        • 具体用法new BeanPropertyRowMapper<类型>(类型.class);
    • 查询返回集合

      • 使用jdbcTemplate.query(String sql, RowMapper<T> rowMapper, Object... obj)方法即可,该方法返回List<T>
      • RowMapper的写法和以上写法一致

批量操作

批量的增删改,MySQL8可能需要手动开启,batch中文为批,读音为bætʃ

jdbcTemplate.batchUpdate(String sql, List<Object[]> args)

例如批量添加:

因为有占位符?,需要填充占位符?,之前的?都是手动进行填充的,每个Object[]中有需要填充的字段值,下标从0开始

    public int[] batchInsert(List<Object[]> args){
        String sql = "insert into user(name, password, address, phone) values(?,?,?,?)";
        return jdbcTemplate.batchUpdate(sql, args);
    }

传递过来的List<Object[]> args

		List<Object[]> arg = new ArrayList<>();
        arg.add(new Object[]{"aaa", "1234", "china", "9999"});
        arg.add(new Object[]{"bbbb", "1234", "china", "9999"});
        arg.add(new Object[]{"cccc", "1234", "china", "9999"});
        int[] results = userService.batchInsert(arg);

返回的执行结果为

[1, 1, 1]

插入后的数据为

+----+----------+-----------+-----------+-------------+
| id | name     | password  | address   | phone       |
+----+----------+-----------+-----------+-------------+
| 15 | aaa      | 1234      | china     | 9999        |
| 16 | bbbb     | 1234      | china     | 9999        |
| 17 | cccc     | 1234      | china     | 9999        |
+----+----------+-----------+-----------+-------------+

批量删除和修改与批量添加类似

批量删除

		List<Object[]> arg = new ArrayList<>();
        arg.add(new Object[]{15});
        arg.add(new Object[]{16});
        arg.add(new Object[]{17});
        int[] results = userService.batchDelete(arg);
        System.out.println("批量删除结果为:" + Arrays.toString(results));
	public int[] batchDelete(List<Object[]> args){
        String sql = "delete from user where id = ?";
        return jdbcTemplate.batchUpdate(sql, args);
    }

事务

在spring中,关于事务的操作有两种方式,分别是编程事务管理和声明式事务管理,在实际中,声明式事务管理使用的最多,编程式事务管理是指之前处理事务的方式,即使用大量的try-catch-finally

声明式事务管理也有两种方式,即注解和xml文件方式

声明式事务管理的底层使用的就是AOP

所有的事务管理都是实现了PlatformTransactionManager接口

基于注解方式的声明式事务管理

因为此时使用的是JDBC直接进行操作的,所以使用PlatformTranscactionManager接口实现类DatasourceTransactionManager进行操作

  • 在xml配置文件中配置事务管理器,即使用<bean></bean>标签对DatasourceTransactionManager进行操作,打开这个类的源码,可以发现有一个DataSource的属性,可以使用之前的Druid进行注入

  • 需要在配置文件中开启事务,需要在配置文件中,引入名称空间tx

    • <tx:annotation-driven transaction-manager="事务管理器的id"></tx:annotation-driven>
      
    • 此时的事务管理器的id为第一步创建的bean标签的id

  • 在需要用到事务的类或者方法前添加注解,注解为@Transactional注解

    • 如果注解添加到类上边,代表这个类中所有的方法都添加了事务
    • 如果注解添加到方法上边,代表给这个方法添加了事务
    • 该注解仅限于权限修饰符为public的方法使用
    • 对于无法被重写的方法,使用这个注解无效,例如static修饰的方法,static修饰的方法不能被重写(即不会实现多态,例如a中的静态方法a1,b继承于a,b中也有静态方法a1,使用a类new一个b类,调用a1时,调用的是a中的a1,而不是b中的a1),例如对main()方法无效

相关的代码

package JDBC.sevice;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class Bank {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    public int transferAccounts(int id, int amount){
        String sql = "update money set amount = amount + ? where id = ?";
        return jdbcTemplate.update(sql, amount, id);
    }

    public void operation(){
        System.out.println(this.transferAccounts(1, -100));
//        自定义异常
        int a = 1 / 0;
        System.out.println(this.transferAccounts(2, 100));
    }
    

    public static void main(String[] args){
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("JDBC/bean.xml");
        Bank bank = context.getBean("bank", Bank.class);
        bank.operation();
    }
}
<?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:content="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
    <content:property-placeholder location="/JDBC/jdbcConfig.properties"/>
    <bean class="com.alibaba.druid.pool.DruidDataSource" id="druid">
        <property name="username" value="root" />
        <property name="password" value="${password}" />
        <property name="url" value="${url}" />
    </bean>

    <bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
        <property name="dataSource" ref="druid" />
    </bean>
    <content:component-scan base-package="JDBC"/>
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="druid" />
    </bean>
<!--    开启事务注解-->
    <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
</beans>

事务的参数(传播行为)

@Transaction注解后可以填写的参数

参数有:

image-20211024185505472

主要的相关参数:

  • propagation:中文为传播,读音为ˌprɒpə'ɡeɪʃ(ə)n,事务的传播行为
    • 表示多事务方法的直接调用时,这个过程中的事务时怎么管理的
      • 例如一个有事务的方法调用一个没有事务的方法,两个有事务的方法之间的互相调用
      • image-20211024190806680
    • 用法:@Transactional(propagation = Propagation.属性值),例如@Transactional(propagation = Propagation.REQUIRED)
    • REQUIRED:读音rɪˈkwaɪərd,中文为必需的,如果a方法有事务,b方法没有事务,a方法调用b方法,此时的b方法使用a方法中的事务。如果a方法没有事务,此时调用b方法,会添加一个新的事务,也就是说,必须在事务中运行,有就不动,没有就添加一个,该项为默认值
    • REQUIRED_NEW:当方法a调用方法b时,无论方法a是否有事务,都会创建一个新事务,方法b在新的事务中运行,此时的方法a的是被被挂起了,即b方法单独运行在自己的事务中
    • 其他的如图所示
  • isolation:隔离,事务隔离级别
    • 一些问题
      • 脏读:有两个事务,两个事务均未提交数据,此时两个事务均能够相互读取到对方未提交的数据,如果事务1读取到事务2未提交的数据,事务2回滚后,事务1读取到的数据是无效的
      • 不可重复读:事务1未提交数据,事务2修改了数据并进行了提交,而此时的事务1(仍为提交)可以读取到事务2提交到的数据,在事务1未提交数据时,其他事务可能有多次修改数据进行的提交,而此时的事务1每次读取到的事务都不同
      • 幻读:和不可重复读类似,事务1未提交数据,事务2添加了数据并提交了,此时事务1读取到了事务2添加了的数据
      • 不可重复度和幻读的不同:不可重复读主要是读到另外一个事务修改之后的数据updatedelete),幻读主要是读取到新插入(insert)的数据
    • 通过设置事务的隔离性可以规避以上三种问题
    • 隔离级别有
      • READ_UNCOMMITTED:读未提交,会出现脏读、不可重复读、幻读现象
      • READ_COMMITTED:读已提交,解决了脏读,会出现不可重复读、幻读现象
      • REPEATABLE_READ:可重复读,解决了脏读、不可重复读,会出现幻读现象,repeatable中文为可重复,读音为rɪˈpiːtəblrepeat中文为重复,读音为rɪˈpiːt
      • SERIALIZABLE:序列化读,解决了脏读、不可重复读、幻读现象
    • 设置隔离级别:
      • 格式为@Transactional(isolation = Isolation.以上的隔离级别),例如@Transactional(isolation = Isolation.REPEATABLE_READ)
      • image-20211024202110024
  • timeout:超时时间
    • 事务在一定的时间范围内进行提交,单位是秒,否则将会自动回滚
    • 默认超时时间为-1,-1代表不会自动回滚
  • readOnly:是否只读
    • 默认值为false
    • 读:查询操作
    • 写:增删改
    • 设置为true只能进行读取操作,无法修改数据
  • rollbackFor:回滚
    • 设置出现哪些异常进行回滚,参数为异常的数组
  • noRollbackFor:不回滚
    • 设置出现哪些异常不进行回滚,参数为异常的数组

事务的完全注解开发

即采用配置类

对于一些引入的类,无法通过添加@Component(仅适用于自己编写的类)之类的注解,因此只能通过写一个方法并添加@Bean注解

格式:

@Bean
public 返回值(类) 方法名(){
    //其中还可以有一些赋值语句
    return 对象;
}
相当于
    <bean id = "名字" class = "类名"></bean>

配置类中的被标记为@Bean的类还可以给某个属性注入已经被放入到容器的对象,只需要在方法的参数中添加相应的类名,在方法体内赋值即可

开启事务的注解@EnableTransactionManagement

全部代码

package JDBC.sevice;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@Configuration
@ComponentScan(basePackages = "JDBC")
@EnableTransactionManagement //开启事务

public class TransactionConfig {
    @Bean
    public DruidDataSource getDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUrl("jdbc:mysql://localhost:3306/test");
        dataSource.setUsername("root");
        dataSource.setPassword("20020327");
        return dataSource;
    }

    //将参数写到这里后,spring会去ioc容器中寻找相关的datasource,找到后会自动注入
    @Bean
    public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }

    //事务管理器
    @Bean
    public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource) {
        DataSourceTransactionManager manager = new DataSourceTransactionManager(dataSource);
        manager.setDataSource(dataSource);
        return manager;
    }
}

使用时,还是和之前一样,使用

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TransactionConfig.class);

Spring5等用到之后再回头学吧

Q.E.D.


念念不忘,必有回响。