JavaWeb_14_Spring


本文介绍Spring全家桶(Spring、SpringMVC、Spring Boot、Spring Cloud)中最基础的框架——Spring。

1. Spring概述

Spring框架主要负责三层架构中的业务逻辑层。该框架出现在2002年左右,解决企业级项目开发的难度。减轻对项目模块之间的管理、类和类之间的管理,帮助开发人员创建对象,管理对象之间的关系。

因为企业级项目比较庞大,功能以及对象的模块比较多,这样单纯的手工管理模块以及模块的对应关系比较复杂,因此出现了该框架。Spring框架中两个最核心的技术是IoC和AOP。能够实现模块之间、类之间的解耦合,使得程序之间修改变得相对容易。

Spring官网上可以看到Spring全家桶,Spring框架指的就是Spring Frame这个项目。点击该项目进入到详细页面。

spring_001.png (1479×807) (gitee.io)

可以看到Spring框架中的特点以及关键技术等等,其中依赖注入指的就是IoC,ORM指的是数据访问,可以和MyBatis集成。

spring_002.png (1050×753) (gitee.io)

在“LEARN”按钮部分,Reference Doc.指的是该版本的所有模块的参考文档,API Doc.指的是具体API方法的帮助文档,日常使用中主要是参考这两个文档

spring_003.png (1107×758) (gitee.io)

1.1 Spring优点

Spring是一个框架,是一个半成品的软件。有20多个模块组成,它是一个容器管理对象,而容器是装东西的,Spring容器不装文本、数字等等,装的是对象,即Spring是存储对象的容器。具有以下优点:

  1. 轻量

    Spring框架使用的jar包都比较小,框架运行占用的资源少,效率较高,不依赖其他jar。

  2. 针对接口编程,解耦合

    Spring提供了IoC控制反转,由容器管理对象、对象的依赖关系。原来在代码中的对象创建方式,现在由容器完成,对象之间的依赖解耦合。

  3. AOP编程的支持

    通过Spring提供的AOP功能,方便进行面向切面编程,许多不容易用传统OOP实现的功能可以通过AOP轻松应付。在Spring中,开发人员可以从繁杂的事务管理代码中解脱出来,通过声明式方式灵活地进行事务的管理,提高开发效率和质量。

  4. 方便集成各种优秀框架

    Spring可以集成各种框架,降低各种框架的使用难度。

1.2 Spring体系结构

Spring框架的体系结构如下所示:

spring_004.png (720×534) (gitee.io)

其中左上方是数据集成模块,可以用JDBC,也可以使用ORM来集成MyBatis框架来访问数据。右上方是Web开发,集成SpringMVC框架来实现。

中间是AOP、Aspects,右侧两个是集成功能,如消息、邮件等等。

下面是核心容器(主要涉及到IoC)。最下面是test模块,类似maven中的junit。

总体来看分为以下几个模块:

  1. 数据访问模块
  2. Web应用模块
  3. AOP模块
  4. 集成功能模块(略过)
  5. 核心容器模块
  6. 测试模块(略过)

本文自底向上介绍Spring框架的各个模块。

2. IoC控制反转

2.1 IoC概述

控制反转(IoC,Inversion of Control)是一个概念,是一种思想。指的是将对象的创建、赋值以及管理工作都交给代码之外的容器实现,也就是对象的创建等工作不再是由我们自己编写代码来实现,而是由其他外部资源来完成的。即对象的控制权不再是程序,而是其他资源,这就是控制反转

进一步讲,控制反转分为两部分:控制和反转。

  1. 控制:指的是创建对象,对象的属性赋值以及对象之间的关系管理。

  2. 反转:即把原来的权限转移给代码之外的容器来实现,由容器来代替开发人员进行操作。

    (正转:由开发人员在代码中,使用new构造方法创建对象,开发人员主动管理对象)

那么为什么要这样做呢?这样做的优点是什么:

它的目的是:在减少对代码改动的基础上,也能实现不同的功能。也就是说,将分离出的代码部分以其他方式实现,而剩余核心部分则是以代码的形式展现,对于分离出的“代码部分”,即使修改,也不会参与编译(类似配置文件),这样就可以通过修改分离的部分来应对不同的业务,然后直接运行程序,不再需要编译。

个人理解:感觉和MyBatis类似,MyBatis是将数据库的相关操作交给了其他资源,使得程序和数据操作分离。

而这里则是将对象的基本管理操作交给代码之外的其他资源,使得程序更加注重逻辑功能,相当于对程序的分工进一步细化,更加保持核心功能。

并且这个分离出的功能在不修改源代码的基础上,修改该文件来实现该功能。

目前为止,Java创建对象的方式主要有以下几种:

  1. 构造方法,new Student()
  2. 反射
  3. 序列化
  4. 克隆
  5. IoC:容器创建对象
  6. 动态代理

注意,IoC和以上其他方法都不一样,IoC不需要我们手动创建对象。目前为止,学过的技术中,体现IoC的一个机制就是Servlet

  1. 创建类继承HttpServlet
  2. 在web.xml文件中注册servlet,使用<servlet-name>以及<servlet-class>注册
  3. 但是我们并没有自己 new Servlet对象
  4. 而是Servlet是Web容器(如Tomcat服务器)帮助创建的,并且它自己实现了调用,也就是说,这个web工作的流程已经设计好了,我们只需要完成具体的功能即可。Tomcat也被称为Web容器,就是因为里面存放了Servlet对象、监听器、过滤器对象等等。

那么Spring也有同样的容器作用,帮助我们创建对象,供我们使用,我们只需要在配置文件中声明该类即可。

IoC作为一种思想,必然有针对该思想的技术实现,具体的技术实现就是依赖注入(DI,Dependency Injection)我们只需要在程序中提供要使用的对象名称就可以,至于对象如何在程序中创建、赋值、查找等都由容器内部实现。

Spring就是使用DI实现了IoC的功能,底层创建对象仍然使用的是反射机制。

注意,Spring是一个容器,用来管理对象以及给对象属性赋值,底层是通过反射创建对象。

2.2 Spring的第一个程序

注意,Spring只是容器,用来管理对象等操作,并不是web项目容器。因此可以采用普通的maven工程即可。

步骤如下所示:

  1. 创建maven项目

    这个不用多说,直接maven项目即可

  2. 加入maven依赖:

    1. Spring依赖

      spring-context和spring-webmvc是Spring中的两个模块。

      • spring-context:是ioc功能,用来创建对象。
      • spring-webmvc:做web开发会使用,是servlet的升级,当然这也会用到spring-context中创建对象的功能。
      1
      2
      3
      4
      5
      6
      7
      <!-- Spring -->
      <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
      <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.3.16</version>
      </dependency>
    2. 单元测试junit依赖(方便测试)

      1
      2
      3
      4
      5
      6
      7
      <!-- 单元测试 -->
      <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
      </dependency>
  3. 创建类(接口和它的实现类)

    和没有使用框架一样,就是普通的类。注意,Spring只是帮助创建对象,但是其类仍然需要自己定义。创建的接口和类如下所示:

    1
    2
    3
    public interface SomeService {
    void doSome();
    }
    1
    2
    3
    4
    5
    6
    public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome() {
    System.out.println("执行了SomeService中的doSome()方法");
    }
    }
  4. 创建Spring需要使用的配置文件

    声明类的信息,这些类由Spring创建和管理。即将上述类的信息,交给Spring,即类似Servlet的配置文件来注册类。IDEA提供了创建Spring配置文件的快捷方式。我们在resource目录下创建即可,名字自定义(一个规范是ApplicationContext.xml),这里起名为beans.xml

    spring_005.png (997×604) (gitee.io)

    配置文件如下所示:

    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
    <?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">

    <!--
    底层就是类似这样: SomeService ss = new org.hianian.service.impl.SomeServiceImpl();
    将对象创建好后,放入到Map中,springMap.put(id值, 对象)。
    之后,仅需要id值就可以获取到创建好的对象了。
    -->
    <bean id="someService" class="org.hianian.service.impl.SomeServiceImpl"></bean>

    </beans>


    <!--
    Spring配置文件:
    1. beans:是根标签,Spring把Java对象称为Bean。所以根便签中必定有若干个<bean>子标签
    2. 上面的 .xsd 文件约束文件,和MyBatis中的 .dtd 文件类似,用来限制本文件中出现的标签和属性的。可以直接在浏览器中打开查看。

    3. <bean id="" class="" /> 标签用来告诉Spring创建对象
    这个标签的意义是 声明bean,就是告诉Spring要创建某个类的对象
    1. id属性是对象的自定义名称,唯一值,符合Java命名规则即可。后续Spring通过这个定位对象,类似Servlet中的<servlet-name>中的值。
    2. class是类的全限定名称,不能是接口,只能是类,因为是通过反射机制创建对象,Spring通过这个创建对象,
    dicen j

    4. 注意,一个bean标签只能声明一个对象。
    -->
  5. 测试Spring创建对象。

    下面是Spring框架和传统方法的对比:

    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
    public class TestSomeService {
    @Test
    public void testDoSome(){

    // 不用Spring框架时的写法
    SomeService ss = new SomeServiceImpl();
    ss.doSome();

    }

    @Test
    public void testSpringDoSome(){

    // 用Spring框架时的写法

    // 1. 指定Spring配置文件的名称
    String config = "beans.xml";

    // 2. 创建表示Spring容器的对象:ApplicationContext,
    // 这个ApplicationContext就是表示Spring容器,可以通过该容器获取到对象
    // ApplicationContext是一个接口,其常用的实现类有:
    // FileSystemXmlApplicationContext 是从其他盘符中读取配置文件(较少使用)
    // ClassPathXmlApplicationContext 是从 类路径 中读取配置文件。
    // 读取配置文件信息,当读取到bean标签的时候,就会完成创建对应对象的工作,将对象放入map中。
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);

    // 从容器中获取某个对象,ApplicationContext.getBean(),有多个重载方法,一般用id来获取.
    // 返回类型是Object,这里需要强转,才能使用对象的方法以及后续的对象操作等等。
    SomeService someService = (SomeService) ac.getBean("someService");

    someService.doSome();
    }
    }

    spring_006.png (788×347) (gitee.io)

注意,Spring默认创建对象的时间:在创建Spring容器ApplicationContext时,会创建配置文件中的所有的bean标签对象。

注意,创建对象,默认调用的是无参构造方法。

另外,似乎一个容器对应一个配置文件,那么也就是说,我们可以创建多个配置文件,并据此创建多个容器。

2.2.1 获取Spring容器中的对象信息

方法名 描述
ac.getBeanDefinitionCount() 获取容器中对象的数量
ac.getBeanDefinitionNames() 获取容器中对象的名称
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void testSpring(){

String config = "beans.xml";

ApplicationContext ac = new ClassPathXmlApplicationContext(config);

// 获取容器中定义的对象的数量
int cnt = ac.getBeanDefinitionCount();

System.out.println("Spring容器中的对象数量:" + cnt);

// 获取容器中定义的对象的名称
String[] names = ac.getBeanDefinitionNames();

for(String name: names){
System.out.println("容器中的对象有:" + name);
}
}

2.2.2 创建非自定义对象

我们知道,通知Spring创建对象需要在配置文件中声明对象。此时,如果想要创建已有的类(Java内置类,比如String、ArrayList等等),和前面一样,只需要在声明的时候填写全限定名称即可,如

1
<bean id="myDate" class="java.util.Date"></bean>
1
2
3
4
5
6
7
8
9
@Test
public void testInnerClass(){
String config = "beans.xml";

ApplicationContext ac = new ClassPathXmlApplicationContext(config);

Date myDate = (Date) ac.getBean("myDate");
System.out.println(myDate);
}

获取到对象之后,接下来就是给对象属性赋值了(DI),主要有下面两种方法实现:基于配置文件和基于注解。

DI有两种语法

  • set注入(设值注入):使用Spring调用 该类的set方法,在set方法中可以实现属性的赋值,大部分是这种用法。
  • 构造注入:使用Spring调用 该类的有参构造方法,创建对象。在构造方法中完成赋值。

注意,Spring中规定Java的基本数据类型(以及其包装类)和String数据类型为简单数据类型。

2.3 基于XML的DI

这种方式是在Spring配置文件中,使用标签和属性给Java对象的属性赋值。

2.3.1 set注入

  1. 简单数据类型的注入

    1
    2
    3
    4
    5
    <bean id="" class="">
    <!-- 注意,一个property只能给一个属性赋值 -->
    <!-- 底层在创建完对象后,会调用set方法进行赋值,可以在set方法输出进行测试 -->
    <property name="属性名字" value="要赋的值"></property>
    </bean>

    经过测试:set方法名必须符合命名规范,即set属性名,否则就会找不到该方法,报异常:NotWritablePropertyExceptionBean property 'name' is not writable or has an invalid setter method

    另外,Spring只是负责调用set方法,至于方法的内部实现,没有约束。并且,仅仅是根据name的值来进行set拼接找到对应的set方法,不会检查到底有没有name这个属性。那么此时,可以利用set方法的命名规则,来执行对应的方法,而无需考虑到底有没有该属性。

    还有,value值一定要双引号括起来,无论是什么数据类型,统一为字符串类型,这是xml文件的规则。

    案例如下所示:

    1
    2
    3
    4
    <bean id="myStudent01" class="org.hianian.ba02.Student">
    <property name="name" value="张三"></property>
    <property name="age" value="23"></property>
    </bean>
  2. 引用数据类型的注入

    1
    2
    3
    4
    5
    6
    <bean id="" class="">
    <!-- 我们知道,引用数据类型就是指向的是对象,所以引用数据类型的注入,需要有该对象,而该对象可以用
    <bean>标签创建,所以利用ref属性将对象传入即可,而bean标签的id可以唯一标识对象。
    -->
    <property name="属性名字" ref="bean的id"></property>
    </bean>

    案例如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <bean id="mySchool" class="org.hianian.ba02.School">
    <property name="name" value="动力节点"></property>
    <property name="address" value="北京市大兴区亦庄"></property>
    </bean>

    <bean id="myStudent" class="org.hianian.ba02.Student">
    <property name="name" value="张三"></property>
    <property name="age" value="24"></property>
    <property name="school" ref="mySchool"></property>
    </bean>

2.3.2 构造注入

构造注入则是调用构造方法在创建对象的时候来设置属性值。默认情况下,Spring调用无参构造方法创建对象,需要使用<constructor-arg>标签来设置使用构造方法,这个标签和<property>标签类似。

  • name属性表示构造方法的形参名
  • index表示构造方法的参数的位置,从左到右依次是0,1,2的顺序(可以替代name属性)
  • value表示如果构造方法的形参是简单类型,则使用value赋值
  • ref表示如果构造方法的形参是引用类型,则使用ref赋值

案例如下所示:

1
2
3
4
5
6
7
8
9
10
<bean id="myStudent" class="org.hianian.ba03.Student">
<constructor-arg name="name" value="张三"></constructor-arg>
<constructor-arg name="age" value="23"></constructor-arg>
<constructor-arg name="school" ref="mySchool"></constructor-arg>
</bean>

<bean id="mySchool" class="org.hianian.ba03.School">
<property name="name" value="动力节点"></property>
<property name="address" value="北京市大兴区亦庄asdf"></property>
</bean>

2.3.3 引用数据类型属性的自动注入

上述两种方法,无论是基本数据类型还是引用数据类型,均是从配置文件中通过标签来注入属性。由于在大型项目中,类的属性可能涉及到很多个引用数据类型,因此就需要多个<bean>标签,代码比较繁琐,因此Spring允许自动注入引用数据类型,即自动注入。

自动注入指的是:Spring框架根据某些规则可以给引用类型赋值(注意,只针对引用类型)。常用的语法有如下两种:

  • byName

    即按名称注入。Java类中引用类型的属性名和Spring容器中(配置文件)<property>标签的id名称一样,且数据类型一致的,这样的容器中的bean,Spring能够赋值给引用类型。

    就是说,因为需要给引用类型的属性赋值,所以必须要有该对象,即在配置文件中必定有<bean>标签声明该对象。此时,如果需要给该属性赋值,我们可以用byName来赋值,将id为x的<bean>对象赋值给属性x,而不需要<property>标签了。

    所以,bean标签的id必须要和类属性名一致。而且该方法对所有引用数据类型有效。即会检查该类中所有的引用数据类型属性,然后在配置文件中找对应的bean标签,最后赋值。

    语法规则如下:

    1
    2
    3
    4
    <bean id="" class="" aotuwire="byName">
    <!--简单类型属性赋值-->
    <property name="" value="">
    <\bean>

    案例如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <bean id="myStudent" class="org.hianian.ba04.Student" autowire="byName">
    <property name="name" value="张三"></property>
    <property name="age" value="24"></property>
    </bean>

    <!-- 注意,id名和属性名一致 -->
    <bean id="school" class="org.hianian.ba04.School">
    <property name="name" value="清华大学"></property>
    <property name="address" value="北京市大兴区亦庄plus"></property>
    </bean>
  • byType

    即按类型注入。Java类中引用类型的属性名和Spring容器中(配置文件)<bean>标签的class属性是同源关系的,这样的bean能够赋值给引用类型。同源指的是同一类,有以下三类:

    • Java类中引用类型的数据类型和bean的class的值是一样的。
    • java类中引用类型的数据类型(父)和bean的class的值(子)是父子类关系。
    • Java类中引用类型的数据类型(接口)和bean的class的值(实现类)是接口和实现类关系。

    语法规则如下:

    1
    2
    3
    4
    <bean id="" class="" aotuwire="byType">
    <!--简单类型属性赋值-->
    <property name="" value="">
    <\bean>

    案例如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <bean id="myStudent" class="org.hianian.ba05.Student" autowire="byType">
    <property name="name" value="张三"></property>
    <property name="age" value="24"></property>
    </bean>

    <bean id="mySchool" class="org.hianian.ba05.School">
    <property name="name" value="人民大学plus"></property>
    <property name="address" value="北京市大兴区亦庄plus"></property>
    </bean>

    在注入的时候,首先创建对象,对基本数据类型赋值,然后发现是byType,所以查找对象中的引用数据类型的类型,然后在配置文件中找对应的同源对象,并对其赋值。

    针对其他类型这里不再测试。因为同源类型有三种,那么如果这三种同时出现会怎么样呢?Spring有优先级吗?

    答案是没有的,如果出现多个同源关系,会报错。oUniqueBeanDefinitionException: No qualifying bean of type 'org.hianian.ba05.School' available: expected single matching bean but found 2: mySchool,myPrimarySchool

2.4 基于注解的DI(掌握)

这种方式是使用注解完成属性赋值。使用注解的步骤如下:

  1. 加入maven的依赖spring-context。实际上注解需要使用spring-aop依赖,而在加入spring-context的同时,会间接自动加入spring-aop的依赖。

  2. 在类中加入spring的注解(spring提供了多个不同功能的注解)

  3. 在spring的配置文件中,加入一个组件扫描器的标签,指明注解在项目中的位置。组件扫描器的语法如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <?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">

    <!-- 声明组件扫描器 属性base-package用来指定注解在项目中的包名,实际上就是指明在哪里扫描组件-->
    <!-- 这里的组件指的就是Java对象 -->
    <!--
    component-scan工作方式:spring会扫描遍历base-package指定的包,把包和子包中的所有类扫描,找到类中的注解
    按照注解的功能进行操作,比如创建对象或者给属性赋值等等。
    -->
    <context:component-scan base-package="org.hianian.ba01"></context:component-scan>
    </beans>

    注意,在加入组件扫描标签后,上面自动新增了约束文件:spring-context.xsd,并起了一个命名空间的名称:xmlns:context=”http://www.springframework.org/schema/context"。

    另外,有时候不只有一个包,所以,需要指定扫描多个包,有如下三种方式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!-- 使用多个组件扫描器 -->
    <context:component-scan base-package="org.hianian.ba01"></context:component-scan>
    <context:component-scan base-package="org.hianian.ba02"></context:component-scan>

    <!-- 使用分隔符分号或者逗号分隔多个包名 -->
    <context:component-scan base-package="org.hianian.ba01; org.hianian.ba02"></context:component-scan>

    <!-- 指定这些多个包的父包 -->
    <context:component-scan base-package="org.hianian"></context:component-scan>

主要有以下几个注解:

  1. @Component

    该注解用于创建对象,相当于<bean>标签。有一个属性value,表示对象的名称,相当于bean标签的id值,是唯一的。这个注解用于修饰类,只能在类上面写,相当于创建某类的对象。注意,一个注解创建的对象,在整个Spring容器中就一个。

  2. @Repository

    该注解也是用于创建对象,只不过一般放在DAO的实现类上面,即用于持久层类对象。使用语法和Component一样,用来访问数据库。

  3. @Service

    该注解也是用于创建对象,只不过一般放在Service的实现类上面,用于业务层类对象。使用语法和Component一样,用来处理业务逻辑。

  4. @Controller

    该注解也是用于创建对象,只不过一般放在控制器的实现类上面,用于控制层类对象。使用语法和Component一样,用于接收请求,显示处理结果。

  5. @Value

    用于给简单数据类型属性赋值。该注解有一个属性value,是String类型的,表示简单类型的属性值。有两个位置用法,一个是在属性定义的上面,无需set方法(推荐使用);第二个是在set方法上面使用。注意,即使自己定义了set方法,但是对于第一种用法来说,并不会调用自定义的set方法。

  6. @Autowired

    用于给引用类型属性赋值。该注解底层是Spring的自动注入原理。支持byType(默认)和byName。有两个位置用于,一个是在属性定义的上面,无序set方法(推荐使用);第二个是在set方法上面使用。注意,在使用之前,一定要先用创建对应的对象(注解方式或配置文件方式均可,都会在容器中保存)。此时spring才会根据自动注入原理找同源类型对象,否则会找不到赋值为空。

    如果想要采用byName方式,则除了添加@Autowired之外,还需要添加@Qualifier(value=”bean的id”)注解,表示使用指定名称的bean完成赋值。(注意,两个注解没有先后顺序。

    @Autowired注解有一个required属性,该属性是布尔类型,默认为true,表示当引用类型属性赋值失败时(比如同源类型对象找不到),程序就报错,终止执行。如果设为false,则表示找不到就赋值为null,程序正常执行。

  7. @Resource

    该注解和Autowired注解的作用一致,语法和Resource类似,只不过该注解是JDK提供的,Spring提供了对该注解的支持。也是采用自动注入的原理,支持byName、byType。默认是byName。但是,该注解默认情况下@Resource,当使用byName赋值失败时,会使用byType进行尝试赋值。可以采用@Resource(name=”bean的id值”)来限制只采用byName。

注意,上述@Repository、@Service、@Controller除了创建对象之外,还有对应的扩展角色功能,实现了对项目的对象分层处理。所以说,如果该类不是上述三类,就采用@Component注解修饰创建对象。

案例如下所示:

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
// 采用注解的方式创建对象,并赋值
@Component(value = "myStudent")
public class Student {

@Value(value = "张飞")
private String name;

@Value(value = "25")
private int age;

@Autowired()
@Qualifier(value = "mySchool")
private School school;

public void setName(String name) {
System.out.println("自定义set方法执行");
this.name = name;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", school=" + school +
'}';
}
}
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
// 采用注解的方式创建对象,并赋值
@Component(value = "mySchool")
public class School {

@Value(value = "清华大学")
private String name;

@Value(value = "北京市海淀区")
private String address;

public void setName(String name) {
this.name = name;
}

public void setAddress(String address) {
this.address = address;
}

@Override
public String toString() {
return "School{" +
"name='" + name + '\'' +
", address='" + address + '\'' +
'}';
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 测试类
public class MyTest01 {

@Test
public void testZhujie(){

String config = "applicaitonContext.xml";

ApplicationContext ac = new ClassPathXmlApplicationContext(config);

Student myStudent = (Student) ac.getBean("myStudent");
System.out.println(myStudent);
}
}

2.5 补充

什么样的对象放入容器中?

DAO类、service类、controller类、工具类。Spring中的对象默认都是单例的,即在容器中叫这个名称的对象只有一个。

不放入Spring容器中的对象?

实体类对象,因为实体类对象的数据(属性值)来自数据库,而数据库是需要访问查询才能获得,只能是在程序运行过程中创建。

servlet、监听器、过滤器等,因为这些对象是web容器(如Tomcat)管理的,而不是spring容器来管理。

使用多配置文件

一方面,大型项目肯定涉及到很多个类对象,此时如果将类对象都声明再一个配置文件中,不方便管理。

另外,文件的大小也会随着bean标签的增多而变大。读取效率也会降低。

并且,只有一个文件,那么多人协同开发,就可能会影响文件的版本更新问题,不同人之间的修改可能会覆盖。

多文件的分配方式如下:

  • 按功能模块,一个模块一个配置文件
  • 按类的功能,数据库相关的配置采用一个配置文件,做事务的相关配置采用一个配置文件,做service功能的采用一个配置文件。

那么,如果某个文件中的对象,用到另一个文件中的对象该怎么办呢?比如前面的引用类型赋值。

此时,除了上述的分模块配置文件,我们还设置一个主配置文件,用于将各模块配置文件关联起来。即import,这样,就相当于所有的配置文件都处于一个总的配置文件中。语法规则如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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">

<!-- 主配置文件,用来包含其他模块的配置文件,也就是将各个文件关联起来。一般情况下,本文件中不定义任何对象。 -->

<import resource="classpath:ba06/sprint-student.xml"></import>
<import resource="classpath:ba06/sprint-school.xml"></import>

<!-- 或者使用通配符 -->
<import resources="classpath:ba06/*.xml"></import>
</beans>

基于配置文件和基础注解的依赖注入有什么优缺点?

配置文件的优点:

  1. 和代码分离,修改配置文件不需要重新编译源码。

配置文件的缺点:

  1. 文件比较多,需要编写xml代码,而且不容易看清对象的内部逻辑。

注解的优点:

  1. 和代码在一起,容器看清对象的逻辑关系。

注解的缺点:

  1. 和代码在一起,修改注解需要重新编译源码。

总体来说,注解适合不经常修改对象属性的情况。配置文件适合经常修改对象属性的情况。一般情况下,主要采用注解的方式,辅助采用配置文件的方式。

总体来看,IoC能够实现业务对象之间的解耦合。例如Service层和dao对象之间的解耦合。二者对象的赋值可以通过配置文件进行修改,对象之间没有必然的联系,使得程序更加松散。

3. AOP面向切面编程

AOP涉及到动态代理,而动态代理有JDK(内置类)和CGLIB(第三方库)两种方式,JDK要求除了有目标类之外还必须有接口。而CGLIB则不要求有接口,但是要求类和方法不能是final修饰,主要通过继承目标类实现代理和功能增强。CGLIB经常被应用在框架中,如Spring等,效率比JDK高。这里不再展开叙述,可参考前面的文章。总之,动态代理,一个是代理,进行了功能增强;一个是动态,代码比较灵活,减少代码量,可以在不修改源码的基础上进行代理其他功能。

总体来说动态代理有如下作用:

  • 在目标类源码不改变的情况下,增加功能
  • 减少代码的重复
  • 专注业务逻辑代码
  • 解耦合,让业务功能和日志、事务等非事务功能分离

什么时候考虑使用AOP技术?

  1. 当要给一个系统中存在的类增加功能,但是不能修改源码,可以使用AOP增加功能。
  2. 给项目中的多个类增加一个相同的功能,可以使用AOP。
  3. 给业务方法增加事务,如日志输出。

AOP也被称为面向切面编程(Aspect Orient Programming),基于动态代理。本质上说就是动态代理的规范化,把动态代理的实现步骤以及方式都定义好了,让开发人员用一种统一的方式使用动态代理。即既使用动态代理,也使得步骤变得统一,易于多个开发人员统一开发,易于维护。(因为动态代理本身比较灵活,可以任意开发,但是对于多人开发来说,必须形成统一规范,这样才方面协同)

一般情况下,切面指的是给目标类增加的功能(即功能增强)。切面一般是非业务方法,可以独立使用,不影响主体业务功能。

面向切面编程和面向对象编程类似,以切面为核心,分析

  1. 什么样的功能可以以切面的形式使用?
  2. 什么时候使用?(连接点/切入点,执行时间)
  3. 给谁用?(目标对象)

AOP的术语以下几个:

  1. Aspect:切面,表示增强的功能,就是一堆代码,完成某个功能(非业务功能)。常见的切面有:日志,事务,统计信息,参数检查,权限验证。
  2. JoinPoint:连接点,连接业务方法和切面的位置。实际上就是某类中的业务方法。(比如在该方法执行前计时,在该方法执行后打日志,这里这个方法就是连接点,计时和打日志就是切面。本质上就是通过该方法将切面和整体业务逻辑连接起来)
  3. Pointcut:切入点,指多个连接点方法的集合(即多个连接点的统称,比如这些方法都要接入切面,那么这些方法就被称为切入点)。
  4. 目标对象:给哪个类的方法增加功能,这个类就是目标对象。
  5. Advice:通知(或者说增强),表示切面功能执行的时间。

一个切面有三个关键要素:

  1. 切面的功能代码,切面干什么
  2. 切面的执行位置,使用Pointcut表示切面执行的位置
  3. 切面的执行时间,使用Advice表示时间,如在目标方法之前,还是目标方法之后使用。

AOP的技术实现框架有以下几个:

  1. Spring框架,主要在事务处理时使用AOP,这是它自己实现的,比较笨重繁琐,我们在项目开发时很少使用自带的AOP。
  2. aspectJ框架,这是一个开源的专门做AOP的框架,在业界比较权威,在实际开发中用的比较多,而且Spring框架集成了该框架。官网。另外,aspectJ框架实现AOP有两种方式:xml配置文件和注解。在开发过程中一般主要采用注解的形式,对于xml配置文件的方式主要是做事务时使用。

3.1 aspectJ框架

我们知道切面

主要有三要素:

  • 功能代码,这个没什么可说的,就是代码实现。

  • 切面的执行时间,也被称为Advice(翻译为通知、增强),在aspectJ框架中使用注解或xml配置文件来表示。主要的注解有以下五种:

    1. @Before(前置通知,掌握
      1. 在目标方法之前执行
      2. 不会改变目标方法的执行结果
      3. 不会影响目标方法的执行
    2. @AfterReturning(后置通知,掌握
      1. 在目标方法之后执行
      2. 能够获取到目标方法的返回值,可以根据这个返回值做不同的处理功能。
      3. 因为能够获取到该值,所以可以“修改”返回值。
    3. @Around(环绕通知,掌握
    4. @AfterThrowing(异常通知,了解)
    5. @After(最终通知,了解)
    6. @PointCut(定义切入点)
  • 切面的执行位置(切入点),aspectJ框架使用的是切入点表达式来表示。我们知道切入点就是目标类方法(连接点),因此,需要指明该方法(权限修饰符、返回值类型、所在包名类名、方法名、参数列表(主需要参数类型,形参名不需要)、抛出异常等等),注意,方法返回值和方法声明(参数)这两部分是必须的

    spring_007.png (874×519) (gitee.io)

    spring_008.png (918×321) (gitee.io)

    spring_009.png (769×265) (gitee.io)

    spring_010.png (879×207) (gitee.io)

因此,总体来说,首先知道切入点,即在哪里插入切面(切入点表达式)。然后要知道切面的执行时间,即在什么时候(注解、xml配置文件)。最后还要实现切面的具体功能(代码)。

3.2 aspectJ框架的第一个程序

在现有的某个类的方法基础上,在不改变原来的类的代码的基础上,进行功能增强,比如在某个方法执行之前输出时间。使用aspectJ实现AOP的基本步骤:

  1. 新建Maven项目

  2. 加入Spring和aspectJ依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <!-- Spring -->
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.16</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.15</version>
    </dependency>
  3. 创建目标类:接口和实现类。(本程序的目标是给类中的方法增加功能)

    1
    2
    3
    4
    5
    package org.hianian.ba01;
    // 接口
    public interface SomeService {
    void doSome(String name, Integer age);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package org.hianian.ba01.impl;
    // 目标类以及业务方法
    public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome(String name, Integer age) {

    // 给doSome方法增加一个功能,在doSome()执行之前,输出方法的执行时间
    // 业务方法
    System.out.println("=====目标方法doSome()=====");
    }
    }
  4. 创建切面类(其实就是普通类,目的就是将普通类和实现类中的方法关联起来)

    1. 在类的上面加入注解:@Aspect

    2. 在类中定义方法(增强方法、通知方法),方法就是切面要执行的功能代码。在方法的上面加入aspectJ中的通知注解,例如@Before。另外还需要指定切入点表达式execution()。

      注意,方法的定义有如下要求:

      1. 公共方法public
      2. 方法没有返回值
      3. 方法名称自定义
      4. 方法可以有参数,也可以没有参数。(如果有参数,参数不是自定义的,只能使用特定的参数类型如JointPoint、Object等等。)
        1. 该参数表示目标方法。
        2. 通过该方法我们可以获取目标方法执行时的信息,例如方法名称、方法实参等等。
        3. 如果在通知方法中需要使用上述信息,可以加入JointPoint参数。
        4. 这个参数是由框架赋值的,要求必须在第一个参数位置

      @Before注解的属性value的值为切面表达式,即指明切面的执行位置。Before本身指定了执行时间。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    package org.hianian.ba01.acpect;

    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;

    import java.util.Date;

    // 切面类
    @Aspect
    public class MyAspect {

    // 定义实现切面功能的方法,注意方法的定义要求
    // 注意,切入点表达式,可以简化。Before注解表示在方法执行前执行本增强方法。
    @Before(value = "execution(public void org.hianian.ba01.impl.SomeServiceImpl.doSome(String, Integer))")
    public void myBefore(){
    System.out.println("前置通知,切面功能:在目标方法之前输出执行时间:" + new Date());
    }
    }

    在IDEA中可以看到,此时会自动提示该方法为增强方法(Navigate To Advised Methods)。

    测试一下方法参数JointPoint。

    1
    2
    3
    4
    5
    6
    7
    @Before(value = "execution(public void org.hianian.ba01.impl.SomeServiceImpl.doSome(String, Integer))")
    public void myBefore(JoinPoint jp){

    System.out.println("目标方法的方法定义:" + jp.getSignature());
    System.out.println("目标方法的方法名:" + jp.getSignature().getName());
    System.out.println("前置通知,切面功能:在目标方法之前输出执行时间:" + new Date());
    }

    结果如下所示:

    1
    2
    3
    4
    5
    目标方法的方法定义:void org.hianian.ba01.SomeService.doSome(String,Integer)
    目标方法的方法名:doSome
    前置通知,切面功能:在目标方法之前输出执行时间:Tue Mar 29 11:28:30 CST 2022
    =====目标方法doSome()=====
    代理对象的类型:com.sun.proxy.$Proxy8
  5. 创建Spring的配置文件,声明对象,把对象交给容器统一管理。

    1. 声明目标对象
    2. 声明切面类对象
    3. 声明aspectJ框架中的自动代理生成器标签(<aop:aspectj-autoproxy></aop:aspectj-autoproxy>)。自动代理生成器对象是用来完成代理对象的自动创建功能的,不再需要手动创建代理类对象了。
      1. 注意,自动代理生成器使用的是aspectJ框架内部的功能,来创建目标对象的代理对象。创建代理对象是在内存中实现的,修改目标对象的内存中的结构,将目标对象修改为代理对象。所以此时目标对象就是修改后的代理对象(本质上似乎并没有创建新对象,猜一下,是将切面类对象作为一个属性加入到目标类对象中,在执行目标类对象目标方法的时候,调用切面类对象的增强方法)。
      2. 自动代理生成器对象会将Spring容器中的所有的符合条件(切入点表达式)的目标对象,一次性都生成代理对象。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/aop
    https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 把对象交给Spring容器,由Spring容器统一创建,管理对象。 -->

    <!-- 声明目标对象 -->
    <bean id="someService" class="org.hianian.ba01.impl.SomeServiceImpl"></bean>

    <!-- 声明切面对象 -->
    <bean id="myAspect" class="org.hianian.ba01.acpect.MyAspect"></bean>

    <!-- 注意,自动代理生成器使用的是aspectJ框架内部的功能,来创建目标对象的代理对象。
    创建代理对象是在内存中实现的,修改目标对象的内存中的结构,
    将目标对象修改为代理对象。所以此时目标对象就是修改后的代理对象(本质上似乎并没有创建新对象)。 -->

    <!-- 同样,在加入该标签后,IDEA自动添加了若干个约束文件,如上xmlns所示 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
    </beans>
  6. 创建测试类,从Spring容器中获取目标对象(实际上就是代理对象),通过代理执行方法,实现AOP的功能增强。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class Test01 {

    @Test
    public void testProxy(){
    String config = "applicationContext.xml";

    ApplicationContext ac = new ClassPathXmlApplicationContext(config);

    // 从容器中获取目标对象(注意,此时已经经过了自动代理生成器对象在内存中转换,所以,获取到的目标对象就是代理对象)
    SomeService proxy = (SomeService) ac.getBean("someService");

    // 通过代理对象执行目标方法,实现增强功能以及原有功能。
    proxy.doSome("张三", 23);

    // 因为目标类实现了接口,所以aspectJ采用的是JDK的方式实现动态代理
    // com.sun.proxy.$Proxy8
    System.out.println("代理对象的类型:" + proxy.getClass().getName());
    }
    }

从上面的步骤来看,Spring主要做的是控制反转,创建容器来管理对象,以及支持AOP框架。实现AOP功能则需要有切面类以及AOP框架的自动代理生成器对象。该对象通过搜索spring容器中对象,通过@Aspect注解来找到切面类,进而找到其增强方法,根据方法上面的注解找到什么时候执行以及在哪里执行(切入点表达式,找到目标类)。

3.3 @AfterReturning

上面的案例中讲解了前置通知注解,这里讲解一下后置通知,顾名思义就是在目标方法执行之后执行此通知方法(增强方法)。其属性主要有两个:

  1. value:即切入点表达式
  2. returning:自定义的变量,表示目标方法的返回值,注意变量名必须和增强方法的参数名一致。

后置通知修饰的方法和前置通知类似,因为是后置通知,所以必须获得目标方法的执行结果,因此,通知方法中的形参可以表示目标方法的执行结果返回值,类型规定为Object。

因此,后置通知主要有以下几个特点:

  1. 在目标方法之后执行
  2. 能够获取到目标方法的返回值,可以根据这个返回值做不同的处理功能。
  3. 因为能够获取到该值,所以可以“修改”返回值。(注意,经过测试,对于引用数据类型来说,是可以修改其值的,并且会影响最终方法获得的结果。)即执行顺序如下所示:
    1. Object res = doOther();
    2. myAfterReturning(res); // 如果是引用数据类型,则会修改值,影响下面的返回结果。
    3. return res;

案例如下所示(为了测试引用数据类型传参,采用了引用数据类型):

1
2
3
4
5
6
7
8
9
10
// 接口
public interface SomeService {

void doSome(String name, Integer age);

String doOther(String name, Integer age);

// 测试引用数据类型传参,切面会不会影响最终返回结果。
Student doOthers(String name, Integer age);
}
1
2
3
4
5
6
7
8
9
10
11
// 目标类以及业务方法
public class SomeServiceImpl implements SomeService {

@Override
public Student doOthers(String name, Integer age) {
System.out.println("=====目标方法doOthers()=====");
Student student = new Student("张三", 25);
System.out.println("目标方法:" + student);
return student;
}
}
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
// 切面类
@Aspect
public class MyAspect {
// 后置通知

/**
* @AfterReturning 后置通知
* 属性:
* 1. value:切入点表达式
* 2. returning:自定义的变量,表示目标方法的返回值。
* 注意,变量名必须和通知方法(增强方法)的形参名一样。
* 3. 位置:在方法定义的上面
*
* 特点:
* 1. 在目标方法之后执行
* 2. 能够获取到目标方法的返回值,可以根据这个返回值做不同的处理功能
* 3. 因为能够获取到该值,所以可以修改返回值。
*/

@AfterReturning(value = "execution(public org.hianian.ba03.Student org.hianian.ba03.impl.SomeServiceImpl.doOthers(String, Integer))",
returning = "res")
public void myAfterReturning(Object res) {
// Object res 是目标方法执行之后的返回值,可以调用它来执行相关操作。
System.out.println("后置通知:在目标方法执行之后执行的,获取的返回值是:" + res);

// 修改目标方法的返回值,验证增强方法是否会影响目标方法的返回结果。
if(res != null) {
((Student) res).setAge(16);
System.out.println("增强方法修改后的结果是:" + res);
}

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Test01 {

@Test
public void testProxy(){
String config = "applicationContext.xml";

ApplicationContext ac = new ClassPathXmlApplicationContext(config);

// 从容器中获取目标对象(注意,此时已经经过了自动代理生成器对象在内存中转换,所以,获取到的目标对象就是代理对象)
SomeService proxy = (SomeService) ac.getBean("someService");

// 通过代理对象执行目标方法,实现增强功能以及原有功能。
Student result = proxy.doOthers("张三", 23);
System.out.println("最终结果:" + result);

// 因为目标类实现了接口,所以aspectJ采用的是JDK的方式实现动态代理
// com.sun.proxy.$Proxy8
System.out.println("代理对象的类型:" + proxy.getClass().getName());
}
}

3.4 @Around

环绕通知是最强的通知。对修饰的方法有如下要求:

  1. public
  2. 必须有一个返回值,推荐使用Object类型
  3. 方法名称自定义
  4. 方法有参数,固定的参数:ProceedingJoinPoint类型(注意,该类型继承了JoinPoint,所以有JoinPoint的一些方法调用)

环绕通知的注解属性有value,值仍然是切入点表达式。环绕通知注解有以下特点:

  1. 它是功能最强的通知(主要用来做事务
  2. 在目标方法的执行前后都能增强功能
  3. 控制目标方法是否被调用执行(因为手动调用,所以可控制执行条件)
  4. 可以修改目标方法的执行结果,影响最后的调用结果。(和引用数据类型修改不一样,这个似乎是将增强方法的返回结果返回给了调用者)

实际上,环绕通知等同于JDK动态代理的InvocationHandler接口的invoke方法。增强方法的参数ProceedingJoinPoint就等同于invoke方法中的Method参数,用于执行目标方法。增强方法的返回值就是目标方法的执行结果,可以被修改。总体上说,环绕通知就是使得我们可以更加灵活地手动调用目标方法,以及调整增强功能的位置。

测试案例如下所示,其他代码和前面类似,这里不再展示:

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
// 切面类
@Aspect
public class MyAspect {

@Around(value = "execution(public String org.hianian.ba04.impl.SomeServiceImpl.doFirst(String, Integer))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {

// 功能增强
System.out.println("环绕通知,在目标方法之前,输出时间:" + new Date());

Object result = null;
// 目标方法调用
// 等同于 method.invoke(),并且自动将调用的实参传递给目标方法了。
// result = pjp.proceed();

// 测试一下,根据调用的实参来执行目标方法
Object[] args = pjp.getArgs();
if(args != null && args.length > 1) {
if("张三".equals(args[0])) {
result = pjp.proceed();
}
}

// 功能增强
System.out.println("环绕通知,在目标方法之后,提交事务");

// 修改返回结果,本质上,环绕通知是将增强方法的执行结果返回给了调用者。
return result + "asdf";
}
}

3.5 @AfterThrowing

异常通知,主要用于目标方法在抛出异常时执行。异常通知修饰的方法要求如下:

  1. public
  2. 没有返回值
  3. 方法名称自定义
  4. 方法有一个Exception类型参数(表示目标方法抛出的异常),如果还有就是JoinPoint

该注解的属性有两个:

  1. value,即切入点表达式
  2. throwing,自定义的变量,表示目标方法抛出的异常对象,变量名必须和增强方法的参数名一样。

特点是:

  1. 在目标方法抛出异常时执行的
  2. 可以做异常的监控程序,监控目标方法执行时是不是有异常,如果有异常,可以进行相关操作等等。

该注解用的较少,简单测试一下:

1
2
3
4
5
6
7
8
9
10
11
// 目标类以及业务方法
public class SomeServiceImpl implements SomeService {

@Override
public void doSecond() {
// 为了测试异常,设置除数为0
System.out.println("=====业务方法doSecond()=====" + (10 / 0));

System.out.println("asdf");
}
}
1
2
3
4
5
6
7
8
9
10
11
// 切面类
@Aspect
public class MyAspect {

@AfterThrowing(value = "execution(public void org.hianian.ba05.impl.SomeServiceImpl.doSecond())", throwing = "ex")
public void myAfterThrowing(Exception ex) {
System.out.println("发生异常:" + ex.getMessage());

// 进行其他操作等等
}
}

相当于try{}catch(){}中的catch。

3.6 @After

最终通知修饰的方法有如下要求:

  1. public
  2. 没有返回值
  3. 方法名称自定义
  4. 方法没有参数,如果有就是JoinPoint

最终通知注解只有一个属性value,值为切入点表达式。修饰位置在方法的上面,特点是总是会执行(即使目标方法出现异常,增强方法也会执行),在目标方法之后执行。一般用来做资源清除工作。相当于try{}catch(){}finally{}中的finally。

1
2
3
4
5
6
7
8
9
10
// 切面类
@Aspect
public class MyAspect {

// 即使目标方法中有异常发生,该增强方法也会执行
@After(value = "execution(public void org.hianian.ba06.impl.SomeServiceImpl.doThird())")
public void myAfterThrowing() {
System.out.println("最终通知");
}
}

3.7 @PointCut

该注解是作为一个辅助出现的,用来定义和管理切入点。如果项目中有多个切入点表达式是重复的,可以复用的,可以使用PointCut。属性有一个value,值为切入点表达式,注解的使用位置是在自定义的方法上面。

特点:

  1. 当使用@PointCut定义在一个方法的上面,此时这个方法的名称就是切入点表达式的别名。在其他的通知中,value属性就可以使用这个方法名称(需要加括号),代替切入点表达式了。

因为有时候,针对一个目标方法,有时候会做多个增强,或者前置和后置通知等等,此时就需要多个增强方法,而就会写多个同样的切入点表达式。这时候,就可以使用@PointCut注解来定义别名。

似乎对该注解修饰的方法没有过多的要求。代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 切面类
@Aspect
public class MyAspect {

@After(value = "myPc()")
public void myAfterThrowing() {
System.out.println("最终通知");
}

// 为该切入点表达式定义别名,后续其他注解的相同的切入点表达式可以使用该别名
@Pointcut(value = "execution(public void org.hianian.ba07.impl.SomeServiceImpl.doThird())")
private void myPc(){}
}

3.8 JDK和CGLIB

前面测试过,如果目标类实现了接口,那么aspectJ默认采用的就是JDK的方式实现动态代理。可以进行测试,如果没有实现接口,那么就采用的是CGLIB的方法实现动态代理。如下所示,可以看到CGLIB的字眼。

org.hianian.ba07.impl.SomeServiceImpl$$EnhancerBySpringCGLIB$$8c0c98aa

实际上,即使目标类实现了接口,仍然可以采用CGLIB方式实现动态代理。需要在配置文件中的自动代理生成器标签中加入proxy-target-class="true"属性值。即:

1
<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>

4. Spring集成MyBatis

前面讲解过MyBatis,将SQL语句和程序分离,使得耦合度降低。而Spring依据IoC和AOP进一步解耦合。这是两个不同应用功能的框架,MyBatis更加具体,负责数据库,而Spring则是负责整体业务逻辑层面。很明显,MyBatis相当于一个子集。

可以把MyBatis集成到Spring中,像一个框架那样使用。IoC是管理对象的,而AOP则是负责业务逻辑的。因此可以用IoC来管理MyBatis框架的相关对象,比如SqlSessionFactory对象等等,这样开发人员可以从Spring容器中获取对象,这样开发人员就不用同时面对多个框架了,只需面对Spring框架即可。

我们知道,MyBatis框架的使用,有以下几个步骤:

  1. 定义DAO接口,
  2. 创建SQL映射文件,xml
  3. 创建MyBatis主配置文件
  4. 根据动态代理,利用SqlSession.getMapper()获取到DAO接口的代理对象
  5. 根据代理对象执行数据库操作

本质上主要用到的对象有:SqlSessionSqlSessionFactory(需要读取主配置文件)。而主配置文件主要有数据库连接信息以及SQL映射文件信息。

在MyBatis中,采用自带的数据库连接池来进行连接对象,而该连接池性能较弱。因此在Spring中采用独立的连接池(如阿里的druid)来连接数据库。

因此,通过以上说明,我们需要让Spring创建以下对象(以目前掌握的知识来看,只能采用xml配置文件来创建,因为没有源码,所以无法采用注解方式创建):

  1. 独立的连接池类的对象
  2. SqlSessionFactory对象
  3. DAO对象

4.1 案例

Spring集成MyBatis有如下步骤:

  1. 新建Maven项目

  2. 加入Maven依赖

    1. spring依赖
    2. mybatis依赖
    3. mysql驱动
    4. spring的事务依赖
    5. mybatis和spring集成的依赖。该依赖是MyBatis官方提供的,用来在Spring项目中创建MyBatis的SqlSessionFactory、dao对象的。
    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
    <dependencies>

    <!-- 单元测试 -->
    <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
    <scope>test</scope>
    </dependency>

    <!-- Spring核心 IoC -->
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.16</version>
    </dependency>

    <!-- AspectJ 外部 AOP -->
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.15</version>
    </dependency>

    <!-- Spring事务 -->
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-tx -->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>5.3.14</version>
    </dependency>

    <!-- Spring事务(访问数据库) -->
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.3.14</version>
    </dependency>

    <!-- mybatis依赖 -->
    <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
    <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.6</version>
    </dependency>

    <!-- mybatis和spring集成的依赖 -->
    <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
    <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>2.0.6</version>
    </dependency>


    <!-- mysql驱动依赖 -->
    <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.27</version>
    </dependency>

    <!-- 阿里提供的数据库连接池 -->
    <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.8</version>
    </dependency>
    </dependencies>
  3. 创建实体类

    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
    public class User {

    private Integer no;
    private String login;
    private String password;
    private String name;

    public User() {
    }

    public User(Integer no, String login, String password, String name) {
    this.no = no;
    this.login = login;
    this.password = password;
    this.name = name;
    }

    public Integer getNo() {
    return no;
    }

    public void setNo(Integer no) {
    this.no = no;
    }

    public String getLogin() {
    return login;
    }

    public void setLogin(String login) {
    this.login = login;
    }

    public String getPassword() {
    return password;
    }

    public void setPassword(String password) {
    this.password = password;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    @Override
    public String toString() {
    return "user{" +
    "no=" + no +
    ", login='" + login + '\'' +
    ", password='" + password + '\'' +
    ", name='" + name + '\'' +
    '}';
    }
    }
  4. 创建dao接口和SQL映射文件(mapper文件)

    1
    2
    3
    4
    5
    public interface UserDao {

    int insertUser(User user);
    List<User> selectUser();
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <?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="org.hianian.dao.UserDao">

    <insert id="insertUser">
    insert into t_user values(#{no}, #{login}, #{password}, #{name})
    </insert>

    <select id="selectUser" resultType="org.hianian.entity.User">
    select no, login, password, name from t_user order by no desc
    </select>

    </mapper>
  5. 创建mybatis主配置文件

    对于主配置文件,由于连接池采用第三方工具,因此<environments>等数据连接标签就不再需要了(这些对象交给druid第三方工具以及Spring框架来创建)。主配置文件只需要设置一些别名、日志以及映射文件即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <?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>

    <!-- 指定properties文件的位置,从类路径根下开始找 -->
    <properties resource="jdbc.properties"></properties>

    <!-- 控制MyBatis的全局行为 -->
    <!-- 在主配置文件的configuration标签内,加入如下标签 -->
    <settings>
    <!-- 控制输出日志,输出为标准控制台输出 -->
    <setting name="logImpl" value="STDOUT_LOGGING"></setting>
    </settings>

    <mappers>
    <mapper resource="org/hianian/dao/UserDao.xml"/>
    </mappers>
    </configuration>
  6. 创建Sevice接口和实现类,属性是dao。(dao接口对象本来是由Service对象来调用,因此这里创建Service接口及实现类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class UserServiceImpl implements UserService {

    private UserDao userDao;

    // 采用set注入的方式给属性赋值
    public void setUserDao(UserDao userDao) {
    this.userDao = userDao;
    }

    // 为了简单起见,这里的Service方法只是简单地调用Dao接口对象中的方法,并没有其他复杂操作。
    @Override
    public List<User> queryUser() {
    List<User> users = userDao.selectUser();
    return users;
    }

    @Override
    public int addUser(User user) {

    int result = userDao.insertUser(user);
    return result;
    }
    }
  7. 创建spring的配置文件:声明mybatis的对象讲给spring创建

    1. 数据源(datasource,就是上面的连接池)

      采用第三方工具alibaba/druid: 阿里云计算平台DataWorks(https://help.aliyun.com/document_detail/137663.html) 团队出品,为监控而生的数据库连接池 (github.com),可以参考官方文档说明。如下所示(Spring中创建DruidDataSource对象):

      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
      <!-- 构造方法执行之后,会调用init初始化方法。然后在销毁对象的时候,会调用close方法 -->
      <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">

      <!-- 给对象的各种属性赋值, 通常配置url、username、password这三项即可-->
      <!-- 另外也可手动配置maxActive最大连接数量 -->
      <!-- 注意,druid会根据url来识别数据库驱动类名,因此不需要提供数据库驱动名 -->
      <!-- 如果连接的数据库非常见数据库,可以配置属性driverClassName -->
      <!-- 其他的属性可以不用写 -->
      <property name="url" value="${jdbc_url}" />
      <property name="username" value="${jdbc_user}" />
      <property name="password" value="${jdbc_password}" />

      <property name="filters" value="stat" />

      <property name="maxActive" value="20" />
      <property name="initialSize" value="1" />
      <property name="maxWait" value="6000" />
      <property name="minIdle" value="1" />

      <property name="timeBetweenEvictionRunsMillis" value="60000" />
      <property name="minEvictableIdleTimeMillis" value="300000" />

      <property name="testWhileIdle" value="true" />
      <property name="testOnBorrow" value="false" />
      <property name="testOnReturn" value="false" />

      <property name="poolPreparedStatements" value="true" />
      <property name="maxOpenPreparedStatements" value="20" />

      <property name="asyncInit" value="true" />
      </bean>
    2. SqlSessionFactory对象

    3. dao对象

    4. 声明自定义的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
    46
    47
    <?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="classpath:jdbc.properties"></context:property-placeholder>

    <!-- 声明数据源DataSource对象以及连接对象Connection,作用是连接数据库,代替MyBatis主配置文件中的数据库连接 -->
    <bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
    <property name="url" value="${jdbc.url}"></property>
    <property name="username" value="${jdbc.username}"></property>
    <property name="password" value="${jdbc.password}"></property>
    <property name="maxActive" value="${jdbc.maxActive}"></property>
    </bean>

    <!-- 声明MyBatis所提供的SqlSessionFactoryBean类,这个类内部会创建SqlSessionFactory对象。 -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <!-- 通过之前的MyBatis主配置文件知道,创建SqlSessionFactory对象需要数据库连接信息以及主配置文件 -->
    <!-- 连接数据库就是上面的myDataSource,即下面的属性 -->
    <property name="dataSource" ref="myDataSource"></property>

    <!-- 主配置文件就是resources/MyBatis.xml -->
    <property name="configLocation" value="classpath:MyBatis.xml"></property>
    </bean>

    <!-- 声明Dao对象,使用SqlSession的getMapper(.class)方法来创建对象 -->
    <!-- 这个声明类对象不需要id(不需要手动调用),MapperScannerConfigurer会在内部调用getMapper()生成每个dao接口的代理对象。 -->
    <!-- 因为需要SqlSession对象,以及接口的class名。所以需要将这两个信息赋给属性 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <!-- 指定SqlSessionFactory对象的id,会自动通过SqlSessionFactory对象创建SqlSession对象 -->
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>

    <!-- 指定代理对象的接口的class (只需要指定dao接口所在包名即可,会自动扫描接口类型)-->
    <!-- MapperScannerConfigurer会扫描指定包中所有的接口,把每个接口都执行一次getMapper()方法,得到每个接口生成的dao对象 -->
    <!-- 创建好的dao对象放入到Spring容器中。对象名称就是Dao接口的首字母小写 -->
    <!-- 如果是多个包,那么就用逗号分隔 -->
    <property name="basePackage" value="org.hianian.dao"></property>
    </bean>

    <!-- 声明service对象,为用户提供服务 -->
    <bean id="userService" class="org.hianian.Service.impl.UserServiceImpl">
    <!-- 给属性赋值,注意,因为是引用数据类型,采用ref属性,值就是上面内部创建好的Dao对象 -->
    <property name="userDao" ref="userDao"></property>
    </bean>
    </beans>

    经过上述步骤,可以在测试类中测试一下Spring中有没有SqlSessionFactory对象。

  8. 创建测试类,获取Service对象,通过Service调用dao对象完成数据库的访问。

    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
    public class MyTest {

    @Test
    public void testMB() throws IOException {

    // SqlSession sqlSession = MyBatisUtils.getSqlSession();
    //
    // UserDao userDao = sqlSession.getMapper(UserDao.class);
    //
    // List<User> result = userDao.selectUser();
    //
    // for(User user: result) {
    // System.out.println(user);
    // }

    String config = "applicationContext.xml";

    ApplicationContext ac = new ClassPathXmlApplicationContext(config);

    // SqlSessionFactory sqlSessionFactory = (SqlSessionFactory) ac.getBean("sqlSessionFactory");
    //
    // System.out.println(sqlSessionFactory);

    String[] names = ac.getBeanDefinitionNames();

    for(String name: names) {
    System.out.println(name + "=====" + ac.getBean(name));
    }

    }

    @Test
    public void testMB3() throws IOException {


    String config = "applicationContext.xml";

    ApplicationContext ac = new ClassPathXmlApplicationContext(config);

    // 通过Service对象为用户提供服务(不只有访问数据库,这里为了简单起见,Service接口只调用了数据库操作。)
    UserService userService = (UserService) ac.getBean("userService");
    List<User> users = userService.queryUser();
    for(User user: users) {
    System.out.println(user);
    }

    }

    @Test
    public void testMB4() throws IOException {


    String config = "applicationContext.xml";

    ApplicationContext ac = new ClassPathXmlApplicationContext(config);

    // 通过Service对象为用户提供服务(不只有访问数据库,这里为了简单起见,Service接口只调用了数据库操作。)
    UserService userService = (UserService) ac.getBean("userService");

    User user = new User(16, "hh", "hh132", "huhuhu");

    int i = userService.addUser(user);

    System.out.println(i);
    }
    }

可以看到,前面的几个步骤仍然是MyBatis的步骤,只是后面的创建对象步骤交给Spring框架来做。注意,MyBatis集成到Spring框架中,事务是自动提交的,无需执行SqlSession.commit()。

另外,本质上仍然是三部分:实体类、Dao类、Service类。只不过其中这些对象的创建放到了Spring容器中自动创建,我们只需要获取对象即可。

5. Spring事务

在讲解Spring事务之前,先看下面几个问题:

  1. 什么是事务?

    最早出现在数据库中,指的是一系列SQL语句的集合(就是多个SQL操作),这些操作完成最终的任务,这个任务就被称为事务。

  2. 在什么时候想到使用事务?

    当我们希望这个任务完成或者不完成,但是这个任务分为多个SQL操作,所以说,这些操作这么全部完成,要么都不完成,不能只完成一部分。此时就需要使用事务,最经典的就是转账操作,涉及到多个表,从A账户中减少,从B账户中增加数据。这两个操作需要都完成,这个转账操作才算完成。

    那么在Java代码中,为了方便控制事务,会在Service类的业务方法中采用事务,因为业务方法会调用多个dao方法,执行多个SQL语句。

  3. 通常使用JDBC访问数据库以及MyBatis访问数据库,怎么处理事务?

    1. JDBC中的Connection.commit()Connection.rollback()
    2. MyBatis中的SqlSession.commit()SqlSession.rollback()
  4. 问题3中事务的处理方式,有什么不足?

    1. 不同的数据库访问技术,处理事务的对象、方法不同。需要了解不同数据库访问技术使用事务的原理。
    2. 掌握多种数据库事务的处理逻辑。什么时候提交事务,什么时候回滚事务。
    3. 处理事务的多种方法。

    总之就是:多种数据库的访问技术,有着不同的事务处理的机制、对象和方法。

  5. 怎么解决上述不足?

    Spring提供了一种处理事务的统一模型,能使用统一步骤,完成多种不同数据库访问技术的事务处理。比如使用Spring的事务处理机制,可以完成MyBatis、Hibernate等访问数据库的事务处理。

    Spring采用的是声明式事务,即把事务相关的资源和内容都提供给Spring,Spring就能处理事务提交、回滚。

    spring_011.png (1665×644) (gitee.io)

  6. 处理事务,需要怎么做,做什么?

    Spring处理事务的模型,使用的步骤都是固定的,我们只需要把使用的信息提供给Spring就可以了。主要有以下内容。

    1. 事务内部提交、回滚事务,使用的是事务管理器对象,代替程序员手动commit、rollback。事务管理器指的是一个接口和他的众多实现类(了解)。

      1. 接口:PlatformTransactionManager,定义了事务的重要方法,比如commit()、rollback()。
      2. 实现类:Spring把每一种数据库访问技术都创建了对应的事务处理类。比如MyBatis对应的事务处理类就是DataSourceTransactionManager,Hibernate对应的事务处理类就是HibernateTransactionManager。

      因此,我们只需要告诉Spring采用哪种数据库访问技术即可,Spring就会创建对应的事务管理对象进行事务的提交和回滚。那么这么告诉Spring呢?在主配置文件中声明对应的事务处理对象即可。

    2. 业务方法需要什么样的事务,需要说明具体的事务类型。主要有以下方面内容:

      1. 事务的隔离级别:

        1. 读未提交(ISOLATION_READ_UNCOMMITTED):为解决任何并发问题
        2. 读已提交(ISOLATION_READ_COMMITTED):解决脏读,存在不可重复读与幻读
        3. 可重复度(ISOLATION_REPEATABLE_READ):解决脏读、不可重复读,存在幻读
        4. 串行化(ISOLATION_SERIALIZABLE):不存在任何并发问题
      2. 事务的超时时间(一般情况下不需要考虑,因为时间影响因素太多,比如网络状况等。

        表示一个方法最长的执行时间(单位为秒),如果方法执行时超过了这个时间,事务就回滚。

      3. 事务的传播行为(重要,掌握)

        控制业务方法是不是有事务,是什么样的事务。有以下七个传播行为,表示业务方法在调用时,事务在方法之间是如何使用的:

        传播行为 描述
        PROPAGATION_REQUIRED(掌握) 指定的方法必须在事务内部执行。若当前调用处存在事务,就加入到当前事务中;若当前没有事务,则创建一个新事物。这是最常见的传播行为,也是Spring默认的。
        PROPAFATION_REQUIRES_NEW(掌握) 总是新建一个事务,若当前调用处存在事务,就将当前事务挂起,直到新事务执行完毕。
        PROPAGATION_SUPPORTS(掌握) 指定的方法支持调用处的当前事务,如果没有事务,也可以以非事务方式执行。(比如查询操作)
        PROPAGATION_MANDATORY
        PROPAGATION_NESTED
        PROPAGATION_NEVER
        PROPAGATION_NOT_SUPPORTED
    3. 提交事务、回滚事务的时机

      1. 当业务方法执行成功,没有异常抛出,当方法执行完毕,Spring在方法执行后自动提交事务,即调用事务管理器的commit()方法。
      2. 当业务方法抛出运行时异常或ERROR,Spring自动执行回滚,即调用事务管理器的rollback()方法。
      3. 当业务方法抛出运行时异常,默认也会自动提交事务。

    总体来说,Spring处理事务需要我们告诉它:

    • 指定要使用的事务管理器实现类
    • 指定哪些类,哪些方法需要加入事务的功能
    • 指定方法需要的隔离级别、传播行为、超时等等。

5.1 具体案例

购买商品trans_sale项目

本例要实现购买商品,模拟用户下订单,向订单中添加销售记录,从商品表中减少库存(注意,首先要查询余量是否满足需求)。

即购买商品涉及到多个SQL语句,将其看成一个事务。

步骤如下面所示。

5.1.1 创建数据库表

1
2
3
4
5
6
7
8
9
10
11
12
13
drop table if exists t_goods;

create table t_goods (

id int primary key auto_increment,
name varchar(100),
amount int,
price float
);


insert into t_goods values (1001, 'notebook', 20, 5000.0);
insert into t_goods values (1002, 'phone', 20, 3000.0);
1
2
3
4
5
6
7
8
drop table if exists t_sales;

create table t_sales (

id int primary key auto_increment,
gid int not null,
nums int
);

5.1.2 创建maven项目

这个不必多说,注意添加依赖即可。

spring_012.png (402×709) (gitee.io)

5.1.3 创建实体类

有两张表,则创建两个实体类。

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
public class Goods {
private int id;
private String name;
private int amount;
private float price;

public Goods(){}

public Goods(int id, String name, int amount, float price) {
this.id = id;
this.name = name;
this.amount = amount;
this.price = price;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAmount() {
return amount;
}

public void setAmount(int amount) {
this.amount = amount;
}

public float getPrice() {
return price;
}

public void setPrice(float price) {
this.price = price;
}

@Override
public String toString() {
return "Goods{" +
"id=" + id +
", name='" + name + '\'' +
", amount=" + amount +
", price=" + price +
'}';
}
}
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
public class Sales {

private int id;
private int gid;
private int nums;

public Sales(){}

public Sales(int id, int gid, int nums) {
this.id = id;
this.gid = gid;
this.nums = nums;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public int getGid() {
return gid;
}

public void setGid(int gid) {
this.gid = gid;
}

public int getNums() {
return nums;
}

public void setNums(int nums) {
this.nums = nums;
}

@Override
public String toString() {
return "Sales{" +
"id=" + id +
", gid=" + gid +
", nums=" + nums +
'}';
}
}

5.1.4 定义dao接口

有两张表,所以也要定义两个用于访问该表的接口。

1
2
3
4
5
6
7
8
public interface GoodsDao {

// 用于查询商品信息,查看库存是否充足
Goods selectGood(int id);

// 用于更新库存变动
int updateGood(Goods good);
}
1
2
3
4
5
6
7
public interface SalesDao {

List<Sales> selectAll();

// 用于插入商品购买信息
int insertSale(Sales sales);
}

5.1.5 定义dao接口对应的SQL映射文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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="org.hianian.dao.GoodsDao">

<select id="selectGood" resultType="org.hianian.entity.Goods">
select id, name, amount, price from t_goods where id=${id}
</select>

<update id="updateGood" parameterType="org.hianian.entity.Goods">
update t_goods set amount=${amount} where id=${id}
</update>
</mapper>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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="org.hianian.dao.SalesDao">

<select id="selectAll" resultType="org.hianian.entity.Sales">
select id, gid, nums from t_sales orderby id asc
</select>

<insert id="insertSale" parameterType="org.hianian.entity.Sales">
insert into t_sales(gid, nums) values (${gid}, ${nums})
</insert>
</mapper>

5.1.6 定义异常类

前面提到过,Spring中的事务,面对运行时异常以及ERROR时,会自动回滚。为了方便测试,这里定义一个异常类,用于后续方法中手动抛出。

1
2
3
4
5
6
7
8
9
10
11
12
package org.hianian.excep;

// 自定义运行时异常,重写无参和有参构造方法
public class NotEnoughException extends RuntimeException {
public NotEnoughException() {
super();
}

public NotEnoughException(String message) {
super(message);
}
}

5.1.7 定义Service接口

给了实现业务方法,这里定义接口。

1
2
3
4
5
public interface UserService {

// 指定购买商品id以及购买数量
void buyGoods(int id, int num);
}

5.1.8 定义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
46
public class UserServiceImpl implements UserService {

private SalesDao salesDao;
private GoodsDao goodsDao;

// set方式注入属性赋值
public void setSalesDao(SalesDao salesDao) {
this.salesDao = salesDao;
}

public void setGoodsDao(GoodsDao goodsDao) {
this.goodsDao = goodsDao;
}

@Override
public void buyGoods(int id, int num) {

// 为了方便抛出异常,这里先插入销售表,再查询库存以及更新库存
Goods goods = goodsDao.selectGood(id);

// 添加销售表
Sales sales = new Sales(100, id, num);
int insertResult = salesDao.insertSale(sales);
System.out.println("新增销售表插入成功:" + insertResult);
System.out.println("购买成功");


// 查询数据是否满足要求
if(goods == null) {
throw new NotEnoughException("抱歉,您购买商品不存在");

} else if(goods.getAmount() < num) {
// 库存不足
throw new NotEnoughException("抱歉,您购买商品的余量不足");
} else {

// 更新库存,并添加销售表
goods.setAmount(goods.getAmount() - num);

int updateResult = goodsDao.updateGood(goods);
System.out.println("库存更新成功:" + updateResult);

}

}
}

可以看到,如果出现异常,那么就会导致两张表的更新不一致,即事务“只完成了一部分”。此时就应该添加事务,保证SQL语句要么都执行成功,要么都执行失败。那么怎么加入事务呢?给谁加入事务呢?当然是给该方法加入事务,事务作为一个附加功能,和主体业务逻辑没有太大的关系,因此可以采用AOP的方式加入事务。

具体的描述如5.1.11补充所示,代码如下所示:

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
public class UserServiceImpl implements UserService {

private SalesDao salesDao;
private GoodsDao goodsDao;

// set方式注入属性赋值
public void setSalesDao(SalesDao salesDao) {
this.salesDao = salesDao;
}

public void setGoodsDao(GoodsDao goodsDao) {
this.goodsDao = goodsDao;
}

// 在方法上加入事务注解(可以看到,都是默认值,所以可以省略属性,直接@Transactional即可)
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.DEFAULT,
readOnly = false,
rollbackFor = {
NullPointerException.class,
NotEnoughException.class
}
)
@Override
public void buyGoods(int id, int num) {

// 为了方便抛出异常,这里先插入销售表,再查询库存以及更新库存
Goods goods = goodsDao.selectGood(id);

// 添加销售表
Sales sales = new Sales(100, id, num);
int insertResult = salesDao.insertSale(sales);
System.out.println("新增销售表插入成功:" + insertResult);
System.out.println("购买成功");


// 查询数据是否满足要求
if(goods == null) {
throw new NullPointerException("抱歉,您购买商品不存在");

} else if(goods.getAmount() < num) {
// 库存不足
throw new NotEnoughException("抱歉,您购买商品的余量不足");
} else {

// 更新库存,并添加销售表
goods.setAmount(goods.getAmount() - num);

int updateResult = goodsDao.updateGood(goods);
System.out.println("库存更新成功:" + updateResult);

}

}
}

5.1.9 修改Spring配置文件内容

5.1.9.1 Spring自带的事务注解
  1. 声明事务管理器对象
  2. 开启事务注解驱动
1
2
3
4
5
6
7
8
9
10
11
<!-- 使用Spring的事务管理 -->
<!-- 声明事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 事务管理器,肯定要指明哪个数据库,因此将上面的数据源赋予属性 -->
<property name="dataSource" ref="myDataSource"></property>
</bean>

<!-- 开启事务注解驱动,告诉Spring使用注解管理事务,创建代理对象 -->
<!-- 注意,annotation-driven有多个,一定要选择结尾连接为tx的。 -->
<!-- transaction-manager属性值为事务管理器的id -->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>

在要添加事务的方法上添加注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 在方法上加入事务注解(可以看到,都是默认值,所以可以省略属性,直接@Transactional即可)
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.DEFAULT,
readOnly = false,
rollbackFor = {
NullPointerException.class,
NotEnoughException.class
}
)
@Override
public void buyGoods(int id, int num) {
...
}
5.1.9.2 AspectJ中的事务注解
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
<!-- AspectJ框架进行事务管理 -->
<!-- 声明事务管理器对象 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="myDataSource"></property>
</bean>

<!-- 声明业务方法的事务属性(隔离级别,传播行为,超时时间) -->
<!-- id是自定义名称,表示这对标签内中的内容,transaction-manager表示事务管理器对象的id -->
<!-- 本标签用于规定对哪些方法配置哪些事务 -->
<tx:advice id="myAdvice" transaction-manager="transactionManager">

<!-- 配置事务的属性 -->
<tx:attributes>
<!-- method用于指定方法,一个method指定一个方法,可以有多个method标签 -->
<!-- name表示方法名,可以使用通配符,也可以是完整名称,后面的就是具体的事务属性,比如船舶行为 -->
<!-- 注意,rollback-for中的异常类型必须是全限定名称 -->
<tx:method name="buyGoods" propagation="REQUIRED" isolation="DEFAULT"
rollback-for="java.lang.NullPointerException, org.hianian.excep.NotEnoughException"/>
</tx:attributes>
</tx:advice>

<!-- 配置aop,确定哪个类(上面只是指明了方法,但是没有指明哪个类) -->
<aop:config>
<!-- 配置切入点表达式:指定哪些包中的类,要使用事务 -->
<!-- id是切入点表达式的名称,expression是具体的表达式,指定哪些类要使用事务,aspectJ会创建对应的代理对象 -->
<aop:pointcut id="servicePt" expression="execution(public void org.hianian.service.impl.UserServiceImpl.buyGoods(int, int))"/>

<!-- 配置增强器,即将此处的切入点表达式和上面的事务关联起来 -->
<aop:advisor advice-ref="myAdvice" pointcut-ref="servicePt"></aop:advisor>

</aop:config>

可以看到本方法不需要设计代码修改,使得配置文件和代码完全分离。

5.1.10 定义测试类

注意,观察获得的service对象是不是代理对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyTest {

@Test
public void testCo(){

String config = "applicationContext.xml";

ApplicationContext ac = new ClassPathXmlApplicationContext(config);
UserService userService = (UserService) ac.getBean("userService");

// 测试一下加入注解之后的对象是不是代理对象
System.out.println(userService.getClass());

userService.buyGoods(1002, 500);
}
}

5.1.11 补充

所以说,事务原本是数据库中的概念,在Dao层。但一般情况下,需要将事务提升到业务层,即Service层。这样做是为了能够使用事务的特定来管理具体的业务。在Spring中通常可以通过以下两种方式来实现对事务的管理

  1. 使用Spring的事务注解管理事务。(适合中小项目)

    该方法使用的是Spring自带的注解@Transactional,修饰public方法,实现事务管理。其属性有以下几种:

    1. propagation

      用于设置事务传播属性,该属性类型为Propagation枚举,默认值为Propagation.REQUIRED。

    2. isolation

      用于设置事务的隔离级别,该属性类型为Isolation枚举,默认值为Isolation.DEFAULT。

    3. readOnly

      用于设置所修饰的方法对数据库的操作是否是只读的,该属性类型为Boolean,默认值为false。

    4. timeout(一般不用设置)

      用于设置本操作与数据库连接的超时时限。单位为秒,类型为int,默认值为-1,即没有时限。

    5. rollbackFor

      指定需要回滚的异常类,即抛什么异常时会回滚。类型为Class[],默认值为空数组。如果只有一个异常类时,可以不用数组。

      注意,Spring框架会首先检查方法抛出的异常是不是在数组中,如果在,则一定回滚。如果不在,那么会判断异常是不是运行时异常,如果是,则一定回滚。

    6. rollbackForClassName

      指定需要回滚的异常类类名。(和上面类似,只不过本属性类型为数组)类型为String[]。

    7. noRollbackFor

      指定不需要回滚的异常类类名。类型为Class[]。

    8. noRollbackForClassName

      指定不需要回滚的异常类类名。类型为String[]。

    使用该注解方式添加事务的步骤:

    1. 需要声明事务管理器对象

    2. 开启事务注解驱动,即告诉Spring框架,使用注解的方式管理事务。

      Spring内部会使用AOP机制,创建@Transactional所在的类的代理对象,给修饰的方法加入事务的功能。Spring给业务方法加入事务:在业务方法执行之前,先开启事务,在业务方法之后提交或回滚事务,使用AOP的环绕通知,即try{}catch(){},如果发生异常,则调用rollback,否则commit()。

    3. 在方法的上面加入注解@Transactional。

    案例可以参考上面5.1.9.1部分,前两步在配置文件中声明,第三步在代码中声明注意,这个注解内部自己实现了环绕通知,所以无需我们自己手动使用AOP采用环绕通知。

  2. 使用AspectJ的AOP配置管理事务。(适合大型项目)

    上面方法存在一些不足,如果目标类较多,就会一一添加注解。可以使用AspectJ框架,在Spring配置文件中声明类、方法需要的事务,这种声明式方法使得业务方法和事务配置完全分离。步骤如下所示:

    1. 加入AspectJ依赖
    2. 声明事务管理器对象
    3. 声明方法需要的事务类型(配置方法的事务属性:隔离级别、传播行为、超时)
    4. 配置AOP:指定哪些类要创建代理。

    案例可以参考上面5.1.9.2部分。这种方法完全在配置文件中使用,不涉及到代码部分。

6. Spring与Web

这部分讲解如何在Web项目中使用Spring框架,首先要解决在web层(Servlet)中获取到Spring容器的问题。只要在Web层获取到了Spring容器,就可以从容器中获取到Service对象。

通过上面的例子可以知道,我们通过在 main方法/测试方法 中创建容器对象,从而可以获取到容器中的各种对象。而Servlet中,我们只是在doGet()等方法中编写代码,Web容器自动调用对象的doGet方法。那么我们应该在doGet()方法中手动创建Spring容器对象吗?答案是否定的,因为用户每请求一次,就会调用一次方法,就会创建一个Spring容器对象,并且容器也会创建内部所有的对象,这肯定是不合理的。

实际上,应该创建一份对象就好了,并且在每个Servlet对象中都可以使用。所以应该将该Spring容器对象放入到全局作用域ServletContext中。所以可以用监听器,当全局作用域对象被创建时,创建Spring容器,并将其放入到ServletContext中。

监听器的作用主要有两个:一是创建容器对象,而是将容器对象放入到ServletContext中。可以自己手写监听器实现上述步骤,也可以采用已经实现好的ContextLoaderListener,自动监听,实现将Spring容器创建并放入到ServletContext中,需要添加spring-web依赖。

注意,ContextLoaderListener会创建WebApplicationContext对象,该对象继承了ApplicationContext类。放入到ServletContext中,key为WebApplicationContext.ROOT_WEB_APPLICATION__CONTEXT_ATTRIBUTE

因此,我们既可以通过ServletContext对象来获取ApplicationContext对象,也可以通过WebApplicationContextUtils工具类中的getRequiredWebApplicationContext(ServletContext sc)方法来获取。

1
2
3
4
5
6
7
8
9
10
<!-- Spring相关配置 -->
<!-- 注册Spring监听器 ,并指定自定义的配置文件位置(就是说,创建容器的时候,扫描哪个文件,创建里面的对象)-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

7. 总结

8. 备注

参考B站《动力节点》。


文章作者: 浮云
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 浮云 !
  目录