Spring

Spring——控制反转、面向切面编程,整合MyBatis

概述

  • Spring 是分层的 JavaSE/EE 应用轻量级开源框架,以 IoC(Inverse Of Control:控制反转)和 AOP(Aspect Oriented Programming:面向切面编程)为内核,提供了view层 Spring MVC、持久层 Spring JDBC 等技术

  • POJO、PO、DTO、DAO、BO、VO:

    • POJO:Plain Ordinary Java Object,即简单普通的java对象,没有从任何类继承、也没有实现任何接口,更没有被其它框架侵入的java对象。一般用在数据层映射到数据库表的类

    • PO:Persistant Object,即持久化对象。可以理解为数据库中的一条数据即一个PO对象,或者为POJO经过持久化后的对象

    • DTO:Data Transfer Object,即数据传输对象。一般用于向数据层外围提供仅需的数据,例如一个表有50个字段,界面或服务只需要其中某些字段,DTO包装送出去的对象

    • DAO:Data Access Object,即数据访问对象。DAO层用于连接数据库与外层之间的桥梁,并持久化数据层对象

    • BO:Business Object,即业务对象。用在业务层,当业务复杂、用到比较多的业务对象时,用BO类组合封装所有的对象一并传递

    • VO:Value Object,或者View Object,即值对象或页面对象。用于web层封装并向view层提供需要展现的数据

  • 特性:

    • 声明式:一些功能只需声明需求即可由框架代为实现
    • 控制反转:翻转资源获取方向。把“自己创建资源、向环境索取资源”变成“环境将资源准备好”,通过依赖注入的方式实现
    • 面向切面编程:在不修改源代码的基础上增强代码功能
    • 容器:IOC 是一个容器,包含并管理组件对象的生命周期
    • 非侵入式:Spring 对应用程序本身的结构影响小
    • 组件化:方便集成其他框架
  • 功能模块:

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

控制反转IOC

  • IOC概念:
    • Java 对象需要调用另一个 Java 对象(被依赖的对象)时, 传统模式下常会用 new 被调用者的代码来创建对象——增加调用者与被调用者的耦合性
    • 对象的实例不再由调用者创建,而由 Spring 容器创建。 Spring 容器控制程序之间的关系,此时控制权由调用者转移到 Spring 容器,控制权发生反转
    • Spring 容器将被依赖对象,赋值给调用对象的成员变量,相当于为调用者注入它所依赖的实例,即为依赖注入——当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方(IOC容器生成对象是通过反射方式。反射:根据给出的类名(字符串方式)来动态地生成对象。让对象在生成时才决定到底是哪一种对象
    • IoC 容器帮助我们实例化对象并且管理它,我们需要哪个对象,去问 IoC 容器要即可——解决对象之间的耦合问题,使用者不需要了解该对象应当如何创建和初始化
  • IOC 是一种通过描述(可以是 xml 或者注解)并通过第三方产生或获取特定对象的方式,Spring 中实现控制反转的是 IOC 容器,实现方法为依赖注入
  • IOC 容器中创建、装配、配置的应用组件对象,称为 bean(类的代理?)
  • IOC 容器的实现主要基于两个接口:
    • BeanFactory:IOC 容器的基本实现,Spring 内部使用的接口(不提供给开发人员)——创建实例时,需要提供 XML 文件的绝对路径
    • ApplicationContext:BeanFactory 子接口,面向 Spring 的使用者。几个实现类:
      • ClassPathXmlApplicationContext:通过读取类路径下的 XML 配置文件创建 IOC 容器对象
      • FileSystemXmlApplicationContext:通过文件系统路径读取 XML 配置文件创建 IOC 容器对象
      • ConfigurableApplicationContext:ApplicationContext 的子接口,包含一些扩展方法 refresh() 和 close() ,增加启动、 关闭和刷新上下文的能力
      • WebApplicationContext:为 Web 应用准备,基于 Web 环境创建 IOC 容器对象,并将对象引入存入 ServletContext 域中

解耦合

工厂模式解耦合

  • 耦合指的是对象之间的依赖性

  • 原始的创建对象方法:

    1
    AccountService as = new AccountServiceImpl();
  • 传入全限定类名字符串,通过反射创建对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public static Object getBean(String beanName) {
    Object bean = null;
    try {
    String beanPath = props.getProperty(beanName);
    bean = Class.forName(beanPath).newInstance(); //默认构造函数创建对象
    } catch (Exception e) {
    e.printStackTrace();
    }
    return bean;
    }
  • 进一步,读取配置文件的键值对<String, Object>,通过配置文件的值反射创建对象存入HashMap。之后需要使用时,通过调用GetBean函数,返回一个对应的对象——形成一个单例设计模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    import java.io.InputStream;
    import java.util.Enumeration;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Properties;
    public class BeanFactory {
    private static final Properties props;
    //定义一个Map,用于存放要创建的对象。把它称之为容器
    private static final Map<String, Object> beans;

    //静态代码块为Properties对象赋值
    static {
    try {
    props = new Properties();
    //获取properties文件的流对象
    InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
    props.load(in);
    beans = new HashMap<String, Object>(); //实例化容器
    //取出配置文件中所有的Key
    Enumeration keys = props.keys();

    while (keys.hasMoreElements()) {
    //取出每个Key,获取value,创建反射对象,存入容器
    String key = keys.nextElement().toString();
    String beanPath = props.getProperty(key);
    Object value = Class.forName(beanPath).newInstance();
    beans.put(key, value);
    }
    } catch (Exception e) {
    throw new ExceptionInInitializerError("初始化properties失败!");
    }
    }
    /**
    * 根据bean的名称获取对象
    */
    public static Object getBean(String beanName) {
    return beans.get(beanName);
    }
    }
  • 工厂:负责向调度方从容器中获取指定被调度对象的类,此时调度方想要获取对象,工厂为调度方查找或创建对象

IOC 解耦合

  • 创建xml配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--将对象的创建交给Spring来管理-->
    <!--id自定义,class设置bean所对应类型的全类名-->
    <bean id="demo1" class="IOC.demo1.hello"/>
    </beans>
  • 创建被反射的测试类

    1
    2
    3
    4
    5
    6
    7
    package IOC.demo1;

    public class hello {
    public void Hello(){
    System.out.println("hello");
    }
    }
  • 测试:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    package testIOC.testDemo1;

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

    import IOC.demo1.hello;

    public class testdemo1 {
    @Test
    public void testHelloWorld(){
    ApplicationContext ac = new ClassPathXmlApplicationContext("demo1.xml");
    hello helloworld = (hello) ac.getBean("demo1");
    helloworld.Hello();
    }
    }

Bean标签

  • Spring 默认通过反射调用组件类的无参构造函数,来创建组件对象
  • 属性:
    • id:给对象在容器中提供一个唯一标识,用于获取对象
    • class:类的全限定类名。用于反射创建对象。默认情况下调用无参构造函数
    • scope:指定对象的作用范围
      • singleton:默认值,单例
      • prototype:多例
      • request:WEB项目中
      • session:WEB项目中
      • globalsession:WEB项目中。如果没有Portlet环境,则相当于session
    • init-method:指定类中的初始化方法名
    • destroy-method:指定类中析构方法名

获取Bean

  • 根据 bean 标签的 id 属性可以精确获取到一个组件对象(如IOC解耦合中的例子)

  • 根据类型获取——此时容器中指定类型的bean有且只能有一个:

    1
    hello helloworld = ac.getBean(hello.class);
  • 根据类型和id获取:

    1
    hello helloworld = ac.getBean("demo1", hello.class);
  • 如果一个组件类实现了某接口,并且 bean 唯一,可以根据接口获取该 bean 的

Bean的实例化

  • 直接使用构造函数实例化 Bean

    1
    <bean id="demo1" class="IOC.demo1.hello"/>
  • 使用静态工厂的方法创建对象,静态工厂由 Spring 管理——这里需要体会下工厂的含义,只要我调用它,它就返回给我一个实例

    • 工厂类

      1
      2
      3
      4
      5
      6
      7
      package IOC.demo1;

      public class helloStaticFactory {
      public static hello createHello(){
      return new hello();
      }
      }
    • 配置文件

      1
      <bean id="helloStatic" class="IOC.demo1.helloStaticFactory" factory-method="createHello" />
      • class属性:指定静态工厂的全限定类名
      • factory-method属性:指定生产对象的静态方法
  • 使用实例工厂的方法创建对象,实例工厂由 Spring 管理——不是静态方法,要使用先创建工厂类,再创建实际对象

    • 工厂类

      1
      2
      3
      4
      5
      6
      7
      package IOC.demo1;

      public class helloFactory {
      public hello createHello(){
      return new hello();
      }
      }
    • 配置文件

      1
      2
      <bean id="instanceFactory" class="IOC.demo1.helloFactory"/>
      <bean id="hello" factory-bean="instanceFactory" factory-method="createHello"/>
      • factory-bean属性:指定实例工厂bean的id
      • factory-method属性:指定实例工厂中创建对象的方法。

Bean的作用范围与生命周期

  • 作用域:

    • singleton(默认):IOC 容器中,该 bean 的对象始终为单实例,在 IOC 容器初始化时创建——一个应用只有一个实例
    • prototype:bean 在 IOC 容器中有多个实例,用户获取bean时创建——每次访问时,都会重新创建实例
    • WebApplicationContext环境下:
      • request:在一个请求范围内有效
      • session:在一个会话范围内有效
  • 生命周期(主要指singleton bean,prototype作用域的bean,在创建后就不会再管理后续生命周期):

    • 实例化、属性赋值、初始化、销毁

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {
      BeanWrapper instanceWrapper = null;
      if (mbd.isSingleton()) {
      instanceWrapper = (BeanWrapper)this.factoryBeanInstanceCache.remove(beanName);
      }

      if (instanceWrapper == null) {
      // 实例化阶段
      instanceWrapper = this.createBeanInstance(beanName, mbd, args);
      }

      ...

      Object exposedObject = bean;

      try {
      // 属性赋值阶段
      this.populateBean(beanName, mbd, instanceWrapper);
      // 初始化阶段
      exposedObject = this.initializeBean(beanName, exposedObject, mbd);
      } catch (Throwable var18) {
      ...
      }

      ...
      }
      // 销毁在容器关闭时调用的,ConfigurableApplicationContext.close()
      • 实例化:为 Bean 对象分配内存空间(调用无参构造器)
      • 属性赋值:根据 Spring 上下文对实例化的 Bean 进行依赖注入,即对 Bean 的属性进行初始化
      • 初始化:执行一些用户自定义的初始化逻辑
    • bean对象初始化之前的操作(由bean的后置处理器负责)

    • bean对象初始化(需在配置bean时指定初始化方法)

    • bean对象初始化之后的操作(由bean的后置处理器负责)

    • bean对象就绪,可以使用

    • bean对象销毁(需在配置bean时指定销毁方法)

    • IOC容器关闭

      image-20220907162203789
    • 一个测试例子:

      • 测试类:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        package IOC.lifeCycle;

        public class life {
        private Integer life;

        public life() {
        System.out.println("周期1 创建对象");
        }

        public void setLife(Integer life) {
        this.life = life;
        System.out.println("周期2 属性注入");
        }

        public void init() {
        System.out.println("周期3 初始化");
        }

        public void destory() {
        System.out.println("周期5 销毁");
        }
        }
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13

        * 配置文件与测试:

        ```xml
        <?xml version="1.0" encoding="UTF-8"?>
        <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
        <bean id="lifeCycle" class="IOC.lifeCycle.life" init-method="init" destroy-method="destory">
        <!--依赖注入,见下文-->
        <property name="life" value="100"/>
        </bean>
        </beans>
        1
        2
        3
        4
        5
        6
        7
        @Test
        public void testLifeCycle() {
        ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("lifeCycle.xml");
        life test = ac.getBean("lifeCycle", life.class);
        System.out.println("周期4 通过IOC获取bean");
        ac.close();
        }

依赖注入

  • 即,在 IOC 在依赖注入时,初始化组件类对象的属性

  • 属性setter方法注入

    • 测试类:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      package IOC.Inject;

      public class Emp {
      private Integer id;
      private String name;
      private Double salary;

      public Emp() {
      }
      public void setId(Integer id) {
      this.id = id;
      }
      public void setName(String name) {
      this.name = name;
      }
      public void setSalary(Double salary) {
      this.salary = salary;
      }
      public void setDepart(Department depart) {
      this.depart = depart;
      }
      }
    • 配置bean时为属性赋值:

      1
      2
      3
      4
      5
      <bean id="emp" class="IOC.Inject.Emp">
      <property name="id" value="100"/>
      <property name="name" value="Tim"/>
      <property name="salary" value="20.0"/>
      </bean>
      • property标签:通过组件类的setXxx()方法给组件对象设置属性
      • name属性:指定属性名(属性名是getXxx()、setXxx()方法定义的,和成员变量无关)
      • value属性:指定属性值
      • ref属性:引用IOC容器中某个bean的id,将所对应的bean为属性赋值(见下文复杂属性的注入)
      • 如果不赋值,则为默认值
  • 构造方法注入

    • 添加有参构造函数

      1
      2
      3
      4
      5
      public Emp(Integer id, String name, Double salary) {
      this.id = id;
      this.name = name;
      this.salary = salary;
      }
    • Bean 配置:

      1
      2
      3
      4
      5
      <bean id="empConstructor" class="IOC.Inject.Emp">
      <constructor-arg name="id" value="100"/>
      <constructor-arg name="salary" value="22.0"/>
      <constructor-arg name="name" value="Tim"/>
      </bean>
      • index属性:指定参数所在位置的索引(从0开始)
      • name属性:指定参数名(注意到,bean配置里的参数顺序可以和构造函数的参数不同),并且name的值为形式参数名
  • 复杂属性的注入

    • 注入类类型属性(即,有一个属性为外部类的对象)

      • 在上述类中添加一个测试类Department,Emp类中添加属性depart。二者添加override的toString方法

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        public class Emp {
        ...
        private Department depart; // 类的注入
        ...
        }
        public class Department {
        private Integer id;
        private String name;
        public void setId(Integer id) {
        this.id = id;
        }
        public void setName(String name) {
        this.name = name;
        }
        }
      • 引用外部已声明的bean

        • 配置Department类型的Bean

          1
          2
          3
          4
          <bean id="departmentSetter" class="IOC.Inject.Department">
          <property name="id" value="1111"/>
          <property name="name" value="财务"/>
          </bean>
        • 赋值Emp的depart属性(使用ref)

          1
          2
          3
          4
          5
          6
          <bean id="empSetterDepart" class="IOC.Inject.Emp">
          <property name="id" value="102"/>
          <property name="name" value="Tim"/>
          <property name="salary" value="20.0"/>
          <property name="depart" ref="departmentSetter"/>
          </bean>
      • 内部bean

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        <bean id="empSetterInnner" class="IOC.Inject.Emp">
        <property name="id" value="103"/>
        <property name="name" value="Tim"/>
        <property name="salary" value="30.0"/>
        <property name="depart">
        <!-- 在一个bean中再声明一个bean就是内部bean -->
        <!-- 内部bean只能用于给属性赋值,不能在外部通过IOC容器获取,因此可以省略id属性 -->
        <bean class="Inject.Department">
        <property name="id" value="2222"/>
        <property name="name" value="人力"/>
        </bean>
        </property>
        </bean>
      • 级联属性赋值

        1
        2
        3
        4
        5
        6
        7
        8
        9
        <bean id="empSetterCascade" class="IOC.Inject.Emp">
        <property name="id" value="104"/>
        <property name="name" value="Tim"/>
        <property name="salary" value="40.0"/>
        <!-- 一定先引用某个bean为属性赋值,才可以使用级联方式更新属性 -->
        <property name="depart" ref="departmentSetter"/>
        <property name="depart.id" value="3333"/>
        <property name="depart.name" value="人力"/>
        </bean>
      • 测试:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
            @Test
        public void testClassAttribute() {
        ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DependencyInjection.xml");
        // 方法1:
        Emp emp1 = ac.getBean("empSetterDepart", Emp.class);
        System.out.println(emp1);
        // 方法2:
        Emp emp2 = ac.getBean("empSetterInnner", Emp.class);
        System.out.println(emp2);
        // 方法3:
        Emp emp3 = ac.getBean("empSetterCascade", Emp.class);
        System.out.println(emp3);
        }
        // Emp{id=102, name='Tim', salary=20.0, depart=Department{id=3333, name='人力', emp_list=null}}
        // Emp{id=103, name='Tim', salary=30.0, depart=Department{id=2222, name='人力', emp_list=null}}
        // Emp{id=104, name='Tim', salary=40.0, depart=Department{id=3333, name='人力', emp_list=null}}
        • 这里之所以第一个输出和第三个输出相同,是因为此时spring中对象Department为singleton作用域!

        • 如果改为<bean id="departmentSetter" class="Inject.Department" scope="prototype">,则输出为

          1
          2
          3
          Emp{id=102, name='Tim', salary=20.0, depart=Department{id=1111, name='财务', emp_list=null}}
          Emp{id=103, name='Tim', salary=30.0, depart=Department{id=2222, name='人力', emp_list=null}}
          Emp{id=104, name='Tim', salary=40.0, depart=Department{id=3333, name='人力', emp_list=null}}
    • 注入特殊值

      • null:<property name="name"> <null /> </property>。如果为<property name="name" value="null"></property>则name的值为字符串null
      • 小于号:小于号在xml中定义了标签的开始,因此不能直接使用
        • <property name="expression" value="a &lt; b"/>
        • <property name="expression"> <value><![CDATA[a < b]]></value> </property>——这里CDATA表示纯文本数据,xml解析器看到CDATA时,会将其内容作为纯文本处理,因此该节中写什么符号都随意
    • 赋值数组类型属性

      • 类Emp添加属性kpis:

        1
        private double[] kpis;
      • Bean 配置:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        <bean id="empSetterArray" class="IOC.Inject.Emp">
        <property name="id" value="105"/>
        <property name="name" value="Tim"/>
        <property name="salary" value="40.0"/>
        <property name="kpis">
        <array>
        <value>0.1</value>
        <value>0.2</value>
        <value>0.3</value>
        <value>0.4</value>
        </array>
        </property>
        </bean>
    • 赋值集合类型属性

      • Emp中添加List属性、Set属性、Map属性和Properties属性

        1
        2
        3
        4
        private List<String> myList;
        private Set<Integer> mySet;
        private Map<Integer, Department> myMap;
        private Properties myProperty;
      • Bean 配置

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        <bean id="empSetterCollection" class="IOC.Inject.Emp">
        <!--给list注入数据-->
        <property name="myList">
        <list>
        <value>AAA</value>
        <value>BBB</value>
        <value>CCC</value>
        </list>
        </property>
        <!--给Set注入数据-->
        <property name="mySet">
        <set>
        <value>1</value>
        <value>2</value>
        <value>3</value>
        </set>
        </property>
        <!--给Property注入数据-->
        <property name="myProperty">
        <props>
        <prop key="keya">aaa</prop>
        <prop key="keyb">bbb</prop>
        </props>
        </property>
        <!--注入map数据-->
        <property name="myMap">
        <map>
        <entry>
        <key>
        <value>0</value>
        </key>
        <ref bean="departOne"/>
        </entry>
        <entry>
        <key>
        <value>1</value>
        </key>
        <ref bean="departTwo"/>
        </entry>
        </map>
        </property>
        </bean>
        <bean id="departOne" class="IOC.Inject.Department">
        <property name="id" value="555"/>
        <property name="name" value="资源"/>
        </bean>
        <bean id="departTwo" class="IOC.Inject.Department">
        <property name="id" value="666"/>
        <property name="name" value="HR"/>
        </bean>
    • 或者引用集合类型的bean:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      <beans ...
      xmlns:util="http://www.springframework.org/schema/util"
      ...>
      <util:map id="departMap">
      <entry>
      <key>
      <value>0</value>
      </key>
      <ref bean="departOne"></ref>
      </entry>
      <entry>
      <key>
      <value>1</value>
      </key>
      <ref bean="departTwo"></ref>
      </entry>
      </util:map>
      ...
      <property name="myMap" ref="departMap"></property>
      ...
  • p命名空间注入——和setter方法类似(但需要引入p命名空间依赖)

    1
    2
    3
    4
    5
    <beans ...
    xmlns:p="http://www.springframework.org/schema/p"
    ...>
    ...
    <bean id="empPSpace" class="IOC.Inject.Emp" p:id="107" p:name="Tom" p:depart-ref="departOne" p:myMap-ref="departMap"/>
  • 外部文件注入(略)

  • 自动装配:bean标签的autowire属性设置自动装配

    • byType:根据类型匹配IOC容器中的某个兼容类型的bean,为属性自动赋值。如果有多个符合条件的bean,则会报错
    • byName:将需要自动装配的属性的属性名,作为bean的id在IOC容器中匹配相对应的bean,进行赋值(例如,类Emp需要自动装配属性depart,则会在容器中查找id为depart的bean对象,set给属性depart)
  • 注:

    • bean 的实例化,和 bean 的依赖注入的区别,个人感觉 bean 的实例化更侧重于控制反转的过程,而 bean 的依赖注入可以认为是属性注入
    • bean 的实例化使得调用者获得被调用对象实例,bean 的依赖注入使得被调用实例的属性不需要调用者来设置

基于注解的IOC配置

  • 使用注解来管理Bean
  • 注解:注解本身不能执行,它只是做一个标记,具体功能的实现上,需要框架检测到注解标记的位置,针对这个位置按照注解标记的功能来执行具体操作——Spring通过扫描的方式进行检测

配置

  • 使用@Component注解配置管理的资源

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    package IOC.Annotation;

    import org.springframework.stereotype.Controller;
    @Controller
    public class UserController { // 控制层组件
    }

    import org.springframework.stereotype.Service;
    @Service
    public class UserServiceImpl implements UserService { // 业务层组件
    }

    import org.springframework.stereotype.Repository;
    @Repository
    public class UserDaoImpl implements UserDao { // 持久层组件
    }
  • XML配置文件开启对注解的支持:(即,确定扫描的文件范围)

    • 全局

      1
      <context:component-scan base-package="IOC.Annotation"/>
    • 指定要排除的组件

      1
      2
      3
      4
      <context:component-scan base-package="IOC.Annotation">
      <!--<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>-->
      <context:exclude-filter type="assignable" expression="IOC.Annotation.UserController"/>
      </context:component-scan>
      • context:exclude-filter标签:指定排除规则
      • type:设置排除或包含的依据
        • annotation:根据注解排除,标注了指定注解的组件不要扫描。expression中设置要排除的注解的全类名
        • assignable”,根据类型排除。expression中设置要排除的类型的全类名
    • 仅扫描指定组件

      1
      2
      3
      4
      <context:component-scan base-package="IOC.Annotation" use-default-filters="false">
      <!--<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>-->
      <context:include-filter type="assignable" expression="IOC.Annotation.UserController"/>
      </context:component-scan>
      • context:include-filter标签:在原有扫描规则的基础上追加规则
      • use-default-filters属性:取值false表示关闭默认扫描规则——默认规则是扫描指定包下所有类
  • bean的id:

    • 先前使用XML方式管理 bean 的时候,每个 bean 都有一个唯一id,使用注解管理同样会给每个组件配置一个bean id
    • 默认:类名首字母小写就是 bean 的id。例如:UserController类对应的 bean id 是userController(此时,注解只是最简单的@Controller)
    • 自定义:通过注解的value属性自定义 bean id。例如:@Service("userService")

常用注解

  • 用于对象创建:

    • @Component:将类标识为普通组件
    • @Controller:将类标识为控制层组件
    • @Service:将类标 识为业务层组件
    • @Repository:将类标识为持久层组件
    • 后三者只是在@Component注解的基础上起了三个新的名字,提高可读性,对于Spring使用IOC容器管理来说没有区别
  • 用于注入数据:

    • @Autowired:对类成员变量、方法以及构造方法进行标注,自动按照类型注入(使用要注入的对象变量名称作为bean的id),此时set方法可以省略,只能注入其他bean类型
    • @Qualifier:给字段注入时不能独立使用,必须和@Autowire一起使用;给方法参数注入时,可以独立使用。属性value用于指定bean的id
    • @Resource:直接按照Bean的id注入
    • (以上只能注入其他Bean类型的数据)
    • @Value:注入基本数据类型和 String 类型数据,属性value用于指定具体的值
    • 集合类型的注入只能通过XML来实现
    • @Scope:指定bean的作用范围,属性value指定范围的值
  • 自动装配:在成员变量上直接标记@Autowired注解即可完成自动装配(也可以标注在构造器和set方法)

    1
    2
    3
    4
    5
    6
    @Service
    public class UserServiceImpl implements UserService {
    @Autowired
    @Qualifier("userDaoImpl")
    private UserDao userDao;
    }
    • autowired流程:根据需要的组件类型到 IOC 容器中查找(例如,查找UserDao)
      • 如果和所需类型匹配的bean不止一个:
        • 没有@Qualifier注解:根据@Autowired标记位置成员变量的变量名作为bean的id进行匹配
        • 用@Qualifier注解:根据@Qualifier注解中指定的名称作为bean的id进行匹配
    image-20220908193038036

XML与注解

image-20220908173149488

面向切面编程AOP

  • 面向切面编程(Aspect Oriented Programming)

代理模式

  • 一些附加的功能代码,和业务功能代码混杂(例如,父类“计算器”具有业务功能“加减乘除”,分别由四个函数实现,子类“日志计算器”在每个功能内部添加了日志打印功能)。此时要解耦,要将附加功能的代码抽取出来,但抽取的代码在函数的内部

  • 提供一个代理类,在调用目标类的方法时,不直接调用目标类的方法,而通过代理类间接调用。此时附加功能的代码会剥离出来,成为代理类的方法,让不属于目标方法核心逻辑 的代码从目标方法中剥离出来——解耦

  • 调用目标类的方法时,先调用代理对象的方法

    image-20220908205410108

  • 静态代理:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class CalculatorStaticProxy implements Calculator {
    // 将被代理的目标对象声明为成员变量
    private Calculator target;
    public CalculatorStaticProxy(Calculator target) {
    this.target = target;
    }
    @Override
    public int add(int i, int j) {
    // 附加功能由代理类中的代理方法来实现
    System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
    // 通过目标对象来实现核心业务逻辑
    int addResult = target.add(i, j);
    System.out.println("[日志] add 方法结束了,结果是:" + addResult);
    return addResult;
    }
    }
  • 生成代理对象的工厂类(动态代理):利用反射,在运行时创建一个实现某些给定接口的新类(“动态代理类”)及其实例,代理的是接口而不是类

    • 可以实现AOP编程
    • Proxy类的代码被固定下来,不会因为业务的逐渐庞大而庞大(静态代理中,如果目标类方法数量越来越多,其代码量也越来越多)
    • (是否可以认为,动态代理类将目标类的所有方法,都用invoke重写了?即,每个目标类的方法,都添加了相同的代码?)
    • 参考文章:java动态代理Proxy.newProxyInstancejava动态代理中的invoke方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    public class ProxyFactory {
    private Object target; // 目标对象,即被代理的类
    public ProxyFactory(Object target) {
    this.target = target;
    }
    public Object getProxy(){
    /**
    * newProxyInstance():创建一个代理实例
    * 其中有三个参数:
    * 1、classLoader:加载动态生成的代理类的类加载器
    * 2、interfaces:目标对象实现的所有接口的class对象所组成的数组(动态代理类需要实现的接口)
    * 3、invocationHandler:设置代理对象实现目标对象方法的过程,即代理类中如何重写接口中的抽象方法
    */
    ClassLoader classLoader = target.getClass().getClassLoader();
    Class<?>[] interfaces = target.getClass().getInterfaces();
    InvocationHandler invocationHandler = new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    /**
    * proxy:代理对象
    * method:代理对象需要实现的方法,即其中需要重写的方法
    * args:method所对应方法的参数
    */
    Object result = null;
    try {
    System.out.println("[动态代理][日志] "+method.getName()+",参数:"+ Arrays.toString(args));
    result = method.invoke(target, args);
    System.out.println("[动态代理][日志] "+method.getName()+",结果:"+ result);
    } catch (Exception e) {
    e.printStackTrace();
    System.out.println("[动态代理][日志] "+method.getName()+",异常:"+e.getMessage());
    } finally {
    System.out.println("[动态代理][日志] "+method.getName()+",方法执行完毕");
    }
    return result;
    }
    };
    return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
    }
    }
    • 测试:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      @Test
      public void test() {
      ProxyFactory factory = new ProxyFactory(new CalculatorImp());
      Calculator proxy = (Calculator) factory.getProxy();
      System.out.println(proxy.getClass().getName());

      Method[] methods = proxy.getClass().getMethods();
      for (Method m: methods) {
      System.out.println(m.getName());
      }

      System.out.println(Arrays.toString(proxy.getClass().getInterfaces()));

      proxy.div(0, 1);
      proxy.add(0, 1);
      }
      • Calculator为接口,CalculatorImp为接口的实现类
      • 此时接口中每个方法都被添加了日志输出功能

AOP概念

  • 通过预编译方式和运行期动态代理方式,在不修改源代码的情况下给程序动态统一添加额外功能

  • 横切关注点:

    • 从每一个方法中抽取出来的同一类非核心功能——例如,上面例子中的日志打印功能
    • 根据附加功能的逻辑进行区分:有多个附加功能,就有多个横切关注点
  • 通知:每一个横切关注点上的功能,都需要一个方法来实现。该方法称为通知方法(例如,日志打印)

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

    • 返回通知:在被代理的目标方法成功结束后执行

    • 异常通知:在被代理的目标方法异常结束后执行

    • 后置通知:在被代理的目标方法最终结束后执行

    • 环绕通知:使用try…catch…finally结构,围绕整个被代理的目标方法

    • 顺序:

      • Spring版本5.3.x以前:前置通知 目标操作 后置通知 返回通知或异常通知
      • Spring版本5.3.x以后:前置通知 目标操作 返回通知或异常通知 后置通知
      image-20220908205850483
  • 切面:封装通知方法的类

  • 目标:被代理的目标对象

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

  • 连接点:一个逻辑概念。将要进行抽取的方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴交叉点是连接点

    image-20220908210035699
  • 切入点:定位连接点的方式

    • 每个类的方法都包含多个连接点
    • Spring 的 AOP 技术通过切入点定位到特定的连接点。
    • 切入点通过 org.springframework.aop.Pointcut 接口描述,使用类和方法作为连接点的查询条件
  • 作用:

    • 简化代码:方法中固定位置的重复代码抽取出来
    • 代码增强:特定的功能封装到切面类中,哪里有需要就往上套,相关的方法被切面增强

基于注解的AOP

结构

image-20220908215459348
  • 动态代理:被代理的目标类必须实现某个接口。代理对象和目标对象实现同样的接口
  • cglib:继承被代理的目标类实现代理,此时不需要目标类实现接口
  • AspectJ:本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,最终效果是动态的。weaver是织入器。Spring只是借用了AspectJ中的注解

范例

  • 依赖:

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.2.14.RELEASE</version>
    </dependency>
  • 目标类(被代理的类)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    package AOP.Aspect;

    public interface Calculator {
    public int add(int a, int b);
    public int sub(int a, int b);
    public int mul(int a, int b);
    public int div(int a, int b);
    }


    public class CalculatorImp implements Calculator {
    @Override
    public int add(int a, int b) {
    int result = a + b;
    return result;
    }
    @Override
    public int sub(int a, int b) {
    int result = a - b;
    return result;
    }
    @Override
    public int mul(int a, int b) {
    int result = a * b;
    return result;
    }
    @Override
    public int div(int a, int b) {
    int result = a / b;
    return result;
    }
    }
  • 切面类:(例如,日志功能的切面类)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    package AOP.Aspect;

    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.springframework.stereotype.Component;

    import java.util.Arrays;

    @Aspect // Aspect表示这个类是一个切面类
    @Component // 切面类放入IOC容器
    public class LoggerAspect {

    @Before("execution(public int AOP.Aspect.CalculatorImp.add(int, int))")
    // 本方法标识为前置通知
    public void beforeMethod(JoinPoint joinPoint){
    String methodName = joinPoint.getSignature().getName();
    String args = Arrays.toString(joinPoint.getArgs());
    System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
    }

    @After("execution(* AOP.Aspect.CalculatorImp.*(..))")
    // 不在乎返回值,不在乎权限修饰符,不在乎是哪个方法,不在乎方法的参数
    public void afterMethod(JoinPoint joinPoint){
    String methodName = joinPoint.getSignature().getName();
    System.out.println("Logger-->后置通知,方法名:"+methodName);
    }

    // 重用切入点表达式
    @Pointcut(value = "execution(* AOP.Aspect.*.sub(..))")
    public void beforePointCut(){}

    @Before("beforePointCut()")
    public void beforeMethodReUse(JoinPoint joinPoint){
    String methodName = joinPoint.getSignature().getName();
    String args = Arrays.toString(joinPoint.getArgs());
    System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
    }

    @AfterReturning(value = "execution(* AOP.Aspect.CalculatorImp.*(..))", returning = "result")
    public void afterReturningMethod(JoinPoint joinPoint, Object result){
    String methodName = joinPoint.getSignature().getName();
    System.out.println("Logger-->返回通知,方法名:"+methodName+",结果:"+result);
    }

    @AfterThrowing(value = "execution(* AOP.Aspect.CalculatorImp.*(..))", throwing = "ex")
    public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){
    String methodName = joinPoint.getSignature().getName();
    System.out.println("Logger-->异常通知,方法名:"+methodName+",异常:"+ex);
    }

    @Around("execution(* AOP.Aspect.CalculatorImp.mul(..))")
    public Object aroundMethod(ProceedingJoinPoint joinPoint){
    String methodName = joinPoint.getSignature().getName();
    String args = Arrays.toString(joinPoint.getArgs());
    Object result = null;
    try {
    System.out.println("环绕通知-->目标对象方法执行之前");
    //目标对象(连接点)方法的执行
    result = joinPoint.proceed();
    System.out.println("环绕通知-->目标对象方法返回值之后");
    } catch (Throwable throwable) {
    throwable.printStackTrace();
    System.out.println("环绕通知-->目标对象方法出现异常时");
    } finally {
    System.out.println("环绕通知-->目标对象方法执行完毕");
    }
    return result;
    }
    }
    • 切面中,需要通过指定的注解,将切面中的方法标识为通知方法

    • 切入点表达式(设置在注解的value属性中):通过切入点表达式,将抽取出来的通知方法,重新套入到目标方法中

      • 通过切入点表达式,定位到连接点(通知要作用到的位置)
      • 表达式格式:
        • 目标方法权限 + 目标方法返回类型 + 目标方法所在的全类名 + 目标方法的参数列表(只需要类型)
        • 例如: @Before("execution(public int AOP.Aspect.CalculatorImp.add(int, int))")表示,接下来的方法,是目标方法add的前置通知
        • 如果不在乎权限修饰符、返回类型,则直接用 * 代替。同样的,如果不在乎哪个包、类,则都可以用 * 代替
    • 在通知方法的参数中,添加 joinPoint,可以获得连接点对应方法(目标方法)的信息

    • 重用切入点表达式:

      • 此时注解中value的值为方法名
      • 该方法为空,将该方法的注解的value作为当前切入点表达式的value
    • 注解Before:在目标对象方法执行之前运行

    • 注解After:在目标对象方法的finally子句中执行(参考动态代理工厂类)

    • 注解AfterReturning:在目标对象方法运行完成执行(没有异常时才执行),参考动态代理工厂类中的:

      1
      2
      result = method.invoke(target, args);
      System.out.println("[动态代理][日志] "+method.getName()+",结果:"+ result);
      • 注解中的returning属性的值,将通知方法的某个参数指定为接收目标方法返回值的参数
    • 注解AfterThrowing:在目标对象方法的catch子句中执行(参考动态代理工厂类)。其中的属性throwing含义同上

    • 注解Around:环绕通知。需要指定目标方法进行操作的位置,并且返回值是目标方法的返回值——一般而言,要么只有一个位置的通知,要么直接用环绕通知

    • 可以重新看看前面的代理工厂类,体会下这里不同通知对应的位置在哪里

  • 将目标对象和切面类交给IOC管理(通过注解)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <?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"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <context:component-scan base-package="AOP.Aspect"> </context:component-scan>
    <aop:aspectj-autoproxy /> <!--开启AspectJ的自动代理,为目标对象自动生成代理-->
    </beans>
  • 测试:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Test
    public void test() {
    ApplicationContext ac = new ClassPathXmlApplicationContext("aop-annotation.xml");
    Calculator calculator = ac.getBean(Calculator.class);
    calculator.add(1, 1);
    calculator.sub(1, 1);
    calculator.mul(1, 0);
    calculator.div(1, 0);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    Logger-->前置通知,方法名:add,参数:[1, 1]
    Logger-->返回通知,方法名:add,结果:2
    Logger-->后置通知,方法名:add
    Logger-->前置通知,方法名:sub,参数:[1, 1]
    Logger-->返回通知,方法名:sub,结果:0
    Logger-->后置通知,方法名:sub
    环绕通知-->目标对象方法执行之前
    Logger-->返回通知,方法名:mul,结果:0
    Logger-->后置通知,方法名:mul
    环绕通知-->目标对象方法返回值之后
    环绕通知-->目标对象方法执行完毕
    Logger-->异常通知,方法名:div,异常:java.lang.ArithmeticException: / by zero
    Logger-->后置通知,方法名:div

    java.lang.ArithmeticException: / by zero
  • 切面的优先级

    • 当一个目标方法具有多个切面类时,需要给定切面执行的优先顺序

    • 每个切面都有一个优先级(int值),值越小,优先级越高。默认为int的最大值

      1
      2
      3
      4
      5
      6
      7
      import org.springframework.core.annotation.Order;

      @Aspect // Aspect表示这个类是一个切面类
      @Component // 切面类放入IOC容器
      @Order(1)
      public class LoggerAspect {
      ...

基于XML的AOP

声明式事务

JdbcTemplate

  • Spring 使用 Spring jdbc 操作数据库(通过JdbcTemplate这个类来实现增删改查操作)——实际上,可以用 MyBatis 代替 Spring jdbc,以面向对象的方式来操作持久层对象

声明式事务

  • 编程式事务:事务功能的相关操作全部通过代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    Connection conn = ...;
    try {
    // 开启事务:关闭事务的自动提交
    conn.setAutoCommit(false);
    // 核心操作
    // 提交事务
    conn.commit();
    }catch(Exception e){
    // 回滚事务
    conn.rollBack();
    }finally{
    // 释放数据库连接
    conn.close();
    }
  • 声明式事务:将事务控制的代码抽取出来(AOP)封装

  • 配置文件中添加配置,开启事务的注解驱动

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
        xmlns:tx="http://www.springframework.org/schema/tx"
    ...
    <!--扫描组件-->
    <context:component-scan base-package="Transcation"> </context:component-scan>
    <!-- 导入外部属性文件 -->
    <context:property-placeholder location="classpath:jdbc.properties" />

    <!--数据库连接池-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="${jdbc.driver}"></property>
    <property name="url" value="${jdbc.url}"></property>
    <property name="username" value="${jdbc.username}"></property>
    <property name="password" value="${jdbc.password}"></property>
    </bean>

    <!--配置事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"></property>
    </bean>
    <tx:annotation-driven transaction-manager="transactionManager" />
  • Service 层添加@Transactional 注解(通过 Service 层进行事务管理,service层中一个方法表示一个功能)

    • 同一个类中,一个没有事务的方法A,去调用另一个有事务的方法B时,因为是直接调用,而不是调用代理类,此时事务不起作用

    • @Transactional标识在方法上,只会影响该方法,标识在类上则会影响类中所有的 public 方法

    • 事务属性:

      • 只读:(便于针对查询操作来进行优化)

        1
        2
        @Transactional(readOnly = true)
        public void ...
      • 超时回滚:

        1
        2
        @Transactional(timeout = 3)
        public void ...
      • 回滚策略:直接使用@Transactional不指定rollbackFor时,Exception及其子类都不会触发回滚

        • rollbackFor属性:需要设置一个Class类型的对象——指定能够触发事务回滚的异常类型,可以指定多个,用逗号分隔

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

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

        • noRollbackForClassName属性:需要设置一个字符串类型的全类名——设置不需要进行回滚的异常类名称数组

          1
          2
          3
          @Transactional(noRollbackFor = ArithmeticException.class)
          //@Transactional(noRollbackForClassName = "java.lang.ArithmeticException")
          public void ...
      • 事务隔离级别:

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

        • 读已提交:READ COMMITTED,Transaction01只能读取Transaction02已提交的修改(Oracle 默认)

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

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

          image-20220909203211837

          • 脏读:一事务对数据进行了增删改,但未提交,另一事务可以读取到未提交的数据。如果第一个事务这时候回滚了,那么第二个事务就读到了脏数据

          • 不可重复读:一个事务中发生了两次读操作,第一次读操作和第二次操作之间,另外一个事务对数据进行了修改,这时候两次读取的数据是不一致的

          • 幻读:第一个事务对一定范围的数据进行批量修改,第二个事务在这个范围增加一条数据,第一个事务会丢失对新增数据的修改

        1
        2
        3
        4
        5
        @Transactional(isolation = Isolation.DEFAULT) //使用数据库默认的隔离级别
        @Transactional(isolation = Isolation.READ_UNCOMMITTED) //读未提交
        @Transactional(isolation = Isolation.READ_COMMITTED) //读已提交
        @Transactional(isolation = Isolation.REPEATABLE_READ) //可重复读
        @Transactional(isolation = Isolation.SERIALIZABLE) //串行化
      • 事务传播:当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如,可能继续在现有事务中运行,也可能开启一个新事务运行

        • @Transactional的propagation属性设置事务传播行为

          • @Transactional(propagation = Propagation.REQUIRED):默认情况,表示如果当前线程上有已经开启的事务,则在这个事务中运行,此时如果有一个事务不满足,则这个链上的所有操作都进行回滚

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

          • 以买书为例,用户有100元,进行一个事务:买多本书。而买一本书也是一个事务。此时如果为前者,则会在当前买多本书的事务中运行,如果最后余额不足,回滚会导致一本书也没有买到;如果为后者,则每次买书都是在“买一本书”这个事务上进行,因此只有余额不足时才回滚买一本书的事务

            1
            2
            3
            4
            5
            6
            7
            8
            9
            10
            11
            12
            13
            14
            15
            16
            17
            18
            19
            20
            21
            22
            23
            @Transactional
            //一次购买多本图书
            public void checkout(Integer[] bookIds, Integer userId) {
            for (Integer bookId : bookIds) {
            bookService.buyBook(bookId, userId);
            }
            }

            @Transactional(timeout = 3)
            public void buyBook(Integer bookId, Integer userId) {
            try {
            TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
            e.printStackTrace();
            }
            //查询图书的价格
            Integer price = bookDao.getPriceByBookId(bookId);
            //更新图书的库存
            bookDao.updateStock(bookId);
            //更新用户的余额
            bookDao.updateBalance(userId, price);
            //System.out.println(1/0);
            }
    • 参考:@Transaction必知必会

基于XML的声明式事务

Spring与MyBatis的整合

  • 在 Spring 环境中使用 MyBatis 更加简单,甚至可以不用 SqlSessionFactory、 SqlSession 等对象,只需要定义mapper接口,配置对应的mapper.xml,这样就可以直接通过mapper接口直接执行数据库操作
  • MyBatis中,想获得dao对象 –> sqlSession对象 –> sqlSessionFactory对象调用opensession()方法 –> 读取mybatis的主配置文件
  • Spring整合:
    • 独立的连接池类的对象
    • SqlSessionFactory对象
    • DAO对象

整合

  • 使用之前MyBatis的数据库和表Emp

  • 依赖:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    <dependencies>
    <!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 -->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.14.RELEASE</version>
    </dependency>

    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.2.14.RELEASE</version>
    </dependency>

    <!--事务-->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>5.2.14.RELEASE</version>
    </dependency>

    <!--事务相关-->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.2.14.RELEASE</version>
    </dependency>

    <!--mybatis依赖-->
    <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.7</version>
    </dependency>

    <!--mybatis与spring集成的依赖-->
    <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>2.0.4</version>
    </dependency>

    <!--德鲁伊连接池-->
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.23</version>
    </dependency>

    <!--驱动-->
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.3</version>
    </dependency>

    <!--spring-test(对JUnit的封装)-->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.2.14.RELEASE</version>
    <scope>test</scope>
    </dependency>

    <!-- junit测试 -->
    <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
    </dependency>
    </dependencies>

    <build>
    <!--目的是把src/main/java目录中的xml文件包含到输出结果中。输出到classes目录中-->
    <resources>
    <resource>
    <directory>src/main/java</directory><!--所在的目录 -->
    <includes>
    <!--包括目录下的.properties、.xml文件都会扫描-->
    <include>**/*.properties</include>
    <include>**/*.xml</include>
    </includes>
    <filtering>false</filtering>
    </resource>
    </resources>
    </build>
  • 创建Service接口和实现类(main/java/CombineMyBatis/Service/

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    package CombineMyBatis.Service;

    import CombineMyBatis.Pojo.Emp;

    import java.util.List;

    public interface EmpService {

    List<Emp> findAllEmp();

    void insertEmp(Emp emp);

    void update(Emp emp);

    void deleteEmp(Emp emp);
    }

    public class EmpServiceImp implements EmpService {

    private EmpDao empDao;

    public void setEmpDao(EmpDao empDao) {
    this.empDao = empDao;
    }

    @Override
    public List<Emp> findAllEmp() {
    return empDao.findAll();
    }

    @Override
    public void insertEmp(Emp emp) {
    empDao.insertEmp(emp);
    }

    @Override
    public void update(Emp emp) {
    empDao.updateEmp(emp);
    }

    @Override
    public void deleteEmp(Emp emp) {
    empDao.deleteEmp(emp);
    }
    }
  • 创建PoJo对象(main/java/CombineMyBatis/Pojo/

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package CombineMyBatis.Pojo;

    public class Emp {
    private Integer id;
    private String name;
    private String job;
    private Double salary;

    // (get、set方法)

    @Override
    public String toString() {
    return "Emp{" +
    "id=" + id +
    ", name='" + name + '\'' +
    ", job='" + job + '\'' +
    ", salary=" + salary +
    '}';
    }
    }
  • 创建Dao接口(main/java/CombineMyBatis/Dao/

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package CombineMyBatis.Dao;

    import CombineMyBatis.Pojo.Emp;

    import java.util.List;

    public interface EmpDao {
    List<Emp> findAll();

    void insertEmp(Emp emp);

    void updateEmp(Emp emp);

    void deleteEmp(Emp emp);
    }
  • 四个配置文件:

    • jdbc.properties(main/resources/jdbc.properties

      1
      2
      3
      4
      prop.url=jdbc:mysql://localhost:3306/testMyBatis?characterEncoding=utf8
      prop.userName=root
      prop.password=123456
      prop.maxActive=10
    • mybatis配置(main/resources/mybatis-config.xml

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      <?xml version="1.0" encoding="UTF-8"?>
      <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
      "http://mybatis.org/dtd/mybatis-3-config.dtd">
      <configuration>

      <!--settings:控制mybatis全局行为-->
      <settings>
      <setting name="logImpl" value="STDOUT_LOGGING"/>
      </settings>

      <!--设置别名-->
      <typeAliases>
      <!--name:实体类所在的包名-->
      <package name="CombineMyBatis.Pojo"/>
      </typeAliases>

      <!--sql mapper(sql映射文件)的位置 -->
      <mappers>
      <!--name:是包名,这个包中的所有mapper.xml一次都能加载-->
      <package name="CombineMyBatis.Dao"/>
      </mappers>
      </configuration>
    • Dao接口对应的mapper配置(main/java/CombineMyBatis/Dao/EmpDao.xml

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      <?xml version="1.0" encoding="UTF-8"?>
      <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
      "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
      <mapper namespace="CombineMyBatis.Dao.EmpDao">

      <insert id="insertEmp">
      insert into emp values (#{id}, #{name}, #{job}, #{salary})
      </insert>

      <select id="findAll" resultType="CombineMyBatis.Pojo.Emp">
      select * from emp;
      </select>

      <select id="updateEmp">
      update emp set job=#{job}, salary=#{job} where id=#{id}
      </select>

      <update id="deleteEmp">
      delete from emp where id=#{id}
      </update>

      </mapper>
    • Spring配置(main/resources/applicationContext.xml

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      <?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"
      xmlns:aop="http://www.springframework.org/schema/aop"
      xmlns:tx="http://www.springframework.org/schema/tx"
      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/util http://www.springframework.org/schema/util/spring-util.xsd
      http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
      http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
      http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

      <!--开启组件扫描(全包扫描的方式)-->
      <context:component-scan base-package="CombineMyBatis"/>

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

      <!--声明数据源,配置连接池-->
      <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
      init-method="init" destroy-method="close">
      <!-- driverClassName可以不要(官方明确说明)-->
      <!-- <property name="driverClassName" value="${prop.driverClass}"></property>-->
      <property name="url" value="${prop.url}"/>
      <property name="username" value="${prop.userName}"/>
      <property name="password" value="${prop.password}"/>
      <property name="maxActive" value="${prop.maxActive}"/>
      </bean>

      <!--声明的是mybatis中提供的SqlSessionFactoryBean类,这个类内部创建SqlSessionFactory的-->
      <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
      <!--set注入,把数据库连接池赋给dataSource属性-->
      <property name="dataSource" ref="dataSource"/>
      <!--mybatis主配置文件的位置
      configLocation属性是Resource类型,读取配置文件
      value指定文件的路径,使用classpath:表示文件的位置-->
      <property name="configLocation" value="classpath:mybatis-config.xml"/>
      </bean>

      <!--创建dao对象,使用sqlSession的getMapper (StudentDao.class )
      MapperScannerConfigurer:在内部调用getMapper()生成每个dao接口的代理对象-->
      <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
      <!--指定SqlSess ionFactory对象的-->
      <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
      <!--指定包名,包名是dao接口所在的包名。
      MapperScannerConfigurer会扫描这个包中的所有接口,对每个接口执行一次getMapper()方法,得到每个接口的dao对象。
      创建好的dao对象放入 IOC 容器中。xml文件名和Dao接口名相同-->
      <property name="basePackage" value="CombineMyBatis.Dao"/>
      </bean>

      <!--声明Service-->
      <bean id="empServiceImpl" class="CombineMyBatis.Service.EmpServiceImp">
      <property name="empDao" ref="empDao"/>
      </bean>

      <!--配置事务管理器-->
      <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
      <property name="dataSource" ref="dataSource"/>
      </bean>
      <!--使用注解式事务-->
      <tx:annotation-driven transaction-manager="transactionManager" />

      <!--基于注解的AOP-->
      <context:component-scan base-package="CombineMyBatis"> </context:component-scan>
      <aop:aspectj-autoproxy />
      </beans>
    • 测试:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      package testMyBatis;

      ...

      @RunWith(SpringJUnit4ClassRunner.class) //单元测试框架
      @ContextConfiguration("classpath:applicationContext.xml") //加载配置文件
      public class testMyBatis {
      @Autowired //注入
      private EmpServiceImp empService;

      @Test //查询所有数据
      public void findAll() {
      List<Emp> list = empService.findAllEmp();
      ...
      }
      @Test //添加一条数据
      public void add() {
      Emp emp = new Emp();

      emp.setId(100);
      emp.setName("xxx");
      emp.setSalary(109.73);
      empService.insertEmp(emp);
      ...
      }
      }
  • 关于ClassPath

    • 项目打包为jar包:一个应用通常分成几个单元,每个单元用jar包封装,并相互依赖

      1
      2
      3
      4
      5
      6
      7
      8
      9
      tools.jar 
      | resource.xml // 资源配置文件
      | other.xml
      |
      |— META-INF
      MANIFEST.MF // jar包的描述文件
      |— com // 类的包目录
      |—test
      util.class // java类文件
    • 项目打包为war包:Java web项目是打成WAR包发布,因此classpath会指定在编译后的classes文件中找相关的xml文件

      image-20220910152937323
    • classpath*:**/mapper/mapping/*Mapper.xml

      • classpath*:这种前缀,可以代表多个匹配的文件
      • **/mapper/mapping/*Mapper.xml,双星号**表示在任意目录下,即WEB-INF/classes/下任意层的目录,只要符合后面的文件路径,都会被作为资源文件找到
  • 参考:

    Spring整合Mybatis(配置事务两种方式)

    【MyBatis】Spring整合原理

    Spring整合Mybatis的原理(IOC) 思路 及演示示例

    Mybatis 和 Spring 是怎么整合的?

    spring+mybatis事务配置