【4万字详解】一篇文章搞定Spring框架

文章目录

  • 前言
  • 第1章 Spring 概述
    • 1.1 Spring框架是什么
    • 1.2 Spring优点是什么
        • (1)轻量
        • (2)针对接口编程,解耦合
        • (3)AOP编程的支持
        • (4)方便集成各种优秀框架
    • 1.3 Spring体系结构
  • 第2章 IoC 控制反转
    • 2.1 Spring的第一个程序
      • 2.1.1 导入依赖jar包
      • 2.1.2 定义接口与实体类
      • 2.1.3 创建Spring配置文件
      • 2.1.4 进行测试
      • 2.1.5 获取容器中对象的信息
      • 2.1.6 使用Spring创建非自定义类对象
      • 2.1.7 ApplicationContext接口(容器)
        • (1)配置文件在类路径下
        • (2)ApplicationContext 容器中对象的装配时机
        • (3) 使用 Spring 容器创建的 java 对象
    • 2.2 基于XML的DI
      • 2.2.1 注入分类
        • (1)set注入
        • (2)构造注入
      • 2.2.2 引用类型属性自动注入
        • (1)byName方式自动注入
        • (2)byType方式自动注入
      • 2.2.3 指定多个 Spring 配置文件
    • 2.3 基于注解的DI
      • 2.3.1 定义 Bean 的注解@Component
      • 2.3.2 简单类型属性注入@Value
      • 2.3.3 byType 自动注入@Autowired
      • 2.3.4 byName 自动注入@Autowired 与@Qualifier
      • 2.3.5 JDK 注解 @Resource 自动注入
        • (1)byType 注入引用类型属性
        • (2)byName 注入引用类型属性
      • 2.3.6 注解 与 XML 的对比
  • 第3章 AOP 面向切面编程
    • 3.1 不使用 AOP 的开发方式
    • 3.2 AOP 简介
    • 3.3 AOP 好处
    • 3.4 AOP 编程术语
        • (1)切面(Aspect)
        • (2)连接点(JoinPoint)
        • (3)切入点(Pointcut)
        • (4)目标对象(Target)
        • (5)通知(Advice)
    • 3.5 AspectJ 对 AOP 的实现
      • 3.5.1 AspectJ 的通知类型
      • 3.5.2 AspectJ 的切入点表达式
      • 3.5.3 AspectJ 基于注解的 AOP 实现
        • (1)实现步骤
        • (2)@Before 前置通知
        • (3)@AfterReturning 后置通知
        • (4)@Around 环绕通知
        • (5)@Pointcut 定义切入点
  • 第4章 Spring 事务
    • 4.1 Spring 的事务管理
    • 4.2 Spring 事务管理 API
      • 4.2.1 事务管理器接口
        • (1)常用的两个实现类
        • (2)Spring 的回滚方式
      • 4.2.2 事务定义接口
        • (1)定义五个事务隔离级别常量
        • (2)定义七个事务传播行为常量
        • (3) 定义默认事务超时时限
    • 4.3 事务演示案例环境搭建
      • 4.3.1 创建数据库表
      • 4.3.2 导入Maven依赖
      • 4.3.3 创建实体类
      • 4.3.4 创建mapper接口
      • 4.3.5 创建mapper映射文件
      • 4.3.6 创建Mybatis核心配置文件
      • 4.3.7 创建异常类
      • 4.3.8 创建Service接口
      • 4.3.9 创建Service接口的实现类
      • 4.3.10 创建Spring配置文件
      • 4.3.11 创建测试类
    • 4.4 使用 Spring 的事务注解管理事务
      • 4.4.1 在Spring容器中添加事务管理器
      • 4.4.2 开启事务注解驱动
      • 4.4.3 业务层 public 方法上加入事务属性
    • 4.5 使用 AspectJ 的 AOP 配置管理事务
      • 4.5.1 导入Maven依赖
      • 4.5.2 在Spring容器中添加事务管理器
      • 4.5.3 配置事务通知
      • 4.5.4 配置增强器
      • 4.5.5 进行测试

前言

该技术博客是关于动力节点Spring教程的笔记总结,方便自己学习的同时希望能为大家带来帮助!

相关文章推荐

  • 【3万字详解】一篇文章搞定Mybatis框架
  • 【步骤详细】手把手整合Spring + Mybatis

第1章 Spring 概述

1.1 Spring框架是什么

Spring 是于 2003 年兴起的一个轻量级的 Java 开发框架,它是为了解决企业应用开发的复杂性而创建的。Spring 的核心是控制反转(IoC)和面向切面编程(AOP)。Spring 是可以在 Java SE/EE 中使用的轻量级开源框架。

Spring 的主要作用就是为代码“解耦”,降低代码间的耦合度。就是让对象和对象(模块和模块)之间关系不是使用代码关联,而是通过配置来说明。即在 Spring 中说明对象(模块)的关系。

Spring 根据代码的功能特点,使用 Ioc 降低业务对象之间耦合度。IoC 使得主业务在相互调用过程中,不用再自己维护关系了,即不用再自己创建要使用的对象了。而是由 Spring 容器统一管理,自动“注入”,注入即赋值。 而 AOP 使得系统级服务得到了最大复用,且不用再由程序员手工将系统级服务“混杂”到主业务逻辑中了,而是由 Spring 容器统一完成“织入”。

Spring官方网址

1.2 Spring优点是什么

Spring 是一个框架,是一个半成品的软件。有 20 个模块组成。它是一个容器管理对象,容器是装东西的,Spring 容器不装文本,数字。装的是对象。Spring 是存储对象的容器。

(1)轻量

Spring 框架使用的 jar 都比较小,一般在 1M 以下或者几百 kb。Spring 核心功能的所需的 jar 总共在 3M 左右。

Spring 框架运行占用的资源少,运行效率高。不依赖其他 jar。

(2)针对接口编程,解耦合

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

(3)AOP编程的支持

通过 Spring 提供的 AOP 功能,方便进行面向切面的编程,许多不容易用传统 OOP 实现的功能可以通过 AOP 轻松应付

在 Spring 中,开发人员可以从繁杂的事务管理代码中解脱出来,通过声明式方式灵活地进行事务的管理,提高开发效率和质量。

(4)方便集成各种优秀框架

Spring 不排斥各种优秀的开源框架,相反 Spring 可以降低各种框架的使用难度,Spring提供了对各种优秀框架(如 Struts,Hibernate、MyBatis)等的直接支持。简化框架的使用。Spring 像插线板一样,其他框架是插头,可以容易的组合到一起。需要使用哪个框架,就把这个插头放入插线板。不需要可以轻易的移除。

1.3 Spring体系结构

【4万字详解】一篇文章搞定Spring框架_第1张图片
Spring 由 20 多个模块组成,它们可以分为数据访问/集成(Data Access/Integration)、Web、面向切面编程(AOP, Aspects)、提供JVM的代理(Instrumentation)、消息发送(Messaging)、核心容器(Core Container)和测试(Test)

第2章 IoC 控制反转

2.1 Spring的第一个程序

2.1.1 导入依赖jar包

创建一个普通的Maven项目,导入Maven依赖:


<dependency>
    <groupId>org.springframeworkgroupId>
    <artifactId>spring-contextartifactId>
    <version>5.2.5.RELEASEversion>
dependency>

2.1.2 定义接口与实体类

//接口
public interface SomeService {
    void doSome();
}

//实体类
public class SomeServiceImpl implements SomeService {
    public void doSome() {
        System.out.println("执行了实现类的doSome()方法");
    }
}

2.1.3 创建Spring配置文件

在 src/main/resources/目录现创建一个 xml 文件,文件名可以随意,但 Spring 建议的名称为 applicationContext.xml

spring 配置中需要加入约束文件才能正常使用,约束文件是 xsd 扩展名。


<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="someService" class="com.xu.service.impl.SomeServiceImpl"/>
beans>

2.1.4 进行测试

/**
  * Spring默认创建对象的时间:在创建Spring容器时,会创建配置文件中所有的对象
  * Spring创建对象:默认调用的是无参构造
  */
 @Test
 public void test01() {
     //使用Spring容器创建对象
     //1.指定Spring配置文件名称
     String config = "applicationContext.xml";

     //2.创建Spring容器对象:ApplicationContext
     //ApplicationContext:表示Spring容器,我们可以通过Spring容器获取对象
     //ClassPathXmlApplicationContext:表示从类路径中加载Spring的配置文件
     ApplicationContext context = new ClassPathXmlApplicationContext(config);

     //3.从Spring容器中获取对象,调用对象的方法
     //getBean("配置文件中bean的id值");
     SomeService someService = (SomeService) context.getBean("someService");
     someService.doSome();
 }

2.1.5 获取容器中对象的信息

/**
  * 获取Spring容器中java对象的信息
  */
@Test
public void test2() {
    //通过Spring配置文件,获取Spring容器
    String config = "applicationContext.xml";
    ApplicationContext context = new ClassPathXmlApplicationContext(config);

    //使用Spring提供的方法获取,获取容器中对象的个数
    int counts = context.getBeanDefinitionCount();
    System.out.println("Spring容器中对象的数量为:" + counts);

    //获取容器中每个对象定义的id名称
    String[] names = context.getBeanDefinitionNames();
    for (String name : names) {
        System.out.println(name);
    }
}

2.1.6 使用Spring创建非自定义类对象

applicationContext.xml 配置文件注册 java.util.Date:

<bean id="mydate" class="java.util.Date"/>

进行测试:

@Test
public void test3() {
    String config = "applicationContext.xml";
    ApplicationContext context = new ClassPathXmlApplicationContext(config);
    Date date = (Date) context.getBean("mydate");
    System.out.println(date);
}

2.1.7 ApplicationContext接口(容器)

ApplicationContext 用于加载 Spring 的配置文件,在程序中充当“容器”的角色。其实现类有两个。

【4万字详解】一篇文章搞定Spring框架_第2张图片

(1)配置文件在类路径下

若 Spring 配置文件存放在项目的类路径下,则使用 ClassPathXmlApplicationContext 实现
类进行加载。

//使用Spring容器创建对象
//1.指定Spring配置文件名称
String config = "applicationContext.xml";

//2.创建Spring容器对象:ApplicationContext
//ApplicationContext:表示Spring容器,我们可以通过Spring容器获取对象
//ClassPathXmlApplicationContext:表示从类路径中加载Spring的配置文件
ApplicationContext context = new ClassPathXmlApplicationContext(config);

(2)ApplicationContext 容器中对象的装配时机

ApplicationContext 容器,会在容器对象初始化时,将其中的所有对象一次性全部装配好。

以后代码中若要使用到这些对象,只需从内存中直接获取即可。执行效率较高,但占用内存。

//创建Spring容器:此时容器中所有对象均已创建完毕
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

(3) 使用 Spring 容器创建的 java 对象

【4万字详解】一篇文章搞定Spring框架_第3张图片

2.2 基于XML的DI

2.2.1 注入分类

bean 实例在调用无参构造器创建对象后,就要对 bean 对象的属性进行初始化。初始化是由容器自动完成的,称为注入(DI)

根据注入方式的不同,常用的有两类:

  1. set注入
  2. 构造注入

(1)set注入

set 注入也叫设值注入,是指通过 setter 方法传入被调用者的实例。这种注入方式简单、直观,因而在 Spring 的依赖注入中大量使用。

简单类型:

  1. 创建Student实体类
public class Student {
    private String name;
    private int age;
	//set、toString方法省略
}
  1. 在Spring容器中注册Student对象

<bean id="myStudent" class="com.xu.pojo.Student">
    <property name="name" value="李四"/> 
    <property name="age" value="20"/> 
bean>
  1. 进行测试
@Test
public void test4() {
    //创建Spring容器
    String config = "applicationContext1.xml";
    ApplicationContext context = new ClassPathXmlApplicationContext(config);

    //从容器中获取Student对象
    Student student = (Student) context.getBean("myStudent");
    System.out.println(student);
}

引用类型:
当指定 bean 的某属性值为另一 bean 的实例时,通过 ref 指定它们间的引用关系。ref 的值必须为某 bean 的 id 值。

  1. 创建School实体类
public class School {
    private String name;
    private String address;
	//set、toString方法省略
}
  1. 将School类作为属性添加到Student类中
public class Student {
    private String name;
    private int age;
    private School school;
	//set、toString方法省略
}
  1. 在Spring容器中注册Student、School对象

<bean id="myStudent" class="com.xu.pojo.Student">
    <property name="name" value="李四"/>
    <property name="age" value="20"/>
    
    <property name="school" ref="mySchool"/>
bean>


<bean id="mySchool" class="com.xu.pojo.School">
    <property name="name" value="北京大学"/>
    <property name="address" value="北京的海淀区"/>
bean>
  1. 进行测试
@Test
public void test4() {
    //创建Spring容器
    String config = "applicationContext1.xml";
    ApplicationContext context = new ClassPathXmlApplicationContext(config);

    //从容器中获取Student对象
    Student student = (Student) context.getBean("myStudent");
    System.out.println(student);
}

(2)构造注入

构造注入是指,在构造调用者实例的同时,完成被调用者的实例化。即,使用构造器设置依赖关系。

举例:

  1. 在Student类中创建一个有参构造方法
//创建有参构造方法
public Student(String name, int age, School school) {
    this.name = name;
    this.age = age;
    this.school = school;
}
  1. 在Spring容器中注册Student、School对象



<bean id="myStudent" class="com.xu.pojo.Student">
    <constructor-arg name="name" value="张三"/>
    <constructor-arg name="age" value="20"/>
    <constructor-arg name="school" ref="mySchool"/>
bean>


<bean id="myStudent2" class="com.xu.pojo.Student">
    <constructor-arg index="0" value="李四"/>
    <constructor-arg index="1" value="21"/>
    <constructor-arg index="2" ref="mySchool"/>
bean>


<bean id="mySchool" class="com.xu.pojo.School">
    <property name="name" value="北京大学"/>
    <property name="address" value="北京的海淀区"/>
bean>
  1. 进行测试
@Test
public void test4() {
    //创建Spring容器
    String config = "applicationContext1.xml";
    ApplicationContext context = new ClassPathXmlApplicationContext(config);

    //从容器中获取Student对象
    Student student = (Student) context.getBean("myStudent");
    System.out.println(student);
}

@Test
public void test5() {
    //创建Spring容器
    String config = "applicationContext1.xml";
    ApplicationContext context = new ClassPathXmlApplicationContext(config);

    //从容器中获取Student对象
    Student student = (Student) context.getBean("myStudent2");
    System.out.println(student);
}

2.2.2 引用类型属性自动注入

对于引用类型属性的注入,也可不在配置文件中显示的注入。可以通过为标签设置 autowire 属性值,为引用类型属性进行隐式自动注入(默认不自动注入引用类型属性)。根据自动注入判断标准的不同,可以分为两种:

  1. byName:根据名称自动注入
  2. byType: 根据类型自动注入

(1)byName方式自动注入

当配置文件中被调用者 bean 的 id 值与代码中调用者 bean 类的属性名相同时,可使用byName 方式,让容器自动将被调用者 bean 注入给调用者 bean。容器是通过调用者的 bean 类的属性名与配置文件的被调用者 bean 的 id 进行比较而实现自动注入的。

举例:


<bean id="myStudent" class="com.xu.pojo.Student" autowire="byName">
    <property name="name" value="李四"/>
    <property name="age" value="21"/>
bean>


<bean id="school" class="com.xu.pojo.School">
    <property name="name" value="北京大学"/>
    <property name="address" value="北京的海淀区"/>
bean>

(2)byType方式自动注入

使用 byType 方式自动注入,要求:配置文件中被调用者 bean 的 class 属性指定的类,要与代码中调用者 bean 类的某引用类型属性类型同源。即要么相同,要么有 is-a 关系(子类,或是实现类)。但这样的同源的被调用 bean 只能有一个。多于一个,容器就不知该匹配哪一个了。

举例:


<bean id="myStudent" class="com.xu.pojo.Student" autowire="byName">
    <property name="name" value="李四"/>
    <property name="age" value="21"/>
bean>


<bean id="school" class="com.xu.pojo.School">
    <property name="name" value="北京大学"/>
    <property name="address" value="北京的海淀区"/>
bean>

2.2.3 指定多个 Spring 配置文件

在实际应用里,随着应用规模的增加,系统中 Bean 数量也大量增加,导致配置文件变得非常庞大、臃肿。为了避免这种情况的产生,提高配置文件的可读性与可维护性,可以将 Spring 配置文件分解成多个配置文件。

包含关系的配置文件:
多个配置文件中有一个总文件,总配置文件将各其它子文件通过 import 引入。在 Java 代码中只需要使用总配置文件对容器进行初始化即可。

举例:
在这里插入图片描述
主配置文件 total.xml 代码如下,分为两种方式:




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

=====================================================================================


<import resource="classpath:spring04/spring-*.xml"/>

2.3 基于注解的DI

对于 DI 使用注解,将不再需要在 Spring 配置文件中声明 bean 实例。Spring 中使用注解,需要在原有 Spring 运行环境基础上再做一些改变。

需要在 Spring 配置文件中配置组件扫描器,用于在指定的基本包中扫描注解。

applicationContext.xml 配置文件:


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

扫描多个包的三种方式:



<context:component-scan base-package="com.xu.bao1"/>
<context:component-scan base-package="com.xu.bao2"/>


<context:component-scan base-package="com.xu.bao1;com.xu.bao2"/>


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

但不建议使用顶级的父包(com包),扫描的路径比较多,导致容器启动时间变慢。指定到目标包才合适。也就是注解所在包全路径。

2.3.1 定义 Bean 的注解@Component

需要在类上使用注解@Component,该注解的 value 属性用于指定该 bean 的 id 值。

/**
 * @Component:该注解用于创建对象,等同于的功能
 * 属性(value):就是对象的名称,也就是bean的id值
 *             value的值是唯一的,创建的对象在Spring容器中只有一个
 *             位置在类的上面
 *
 * @Component(value = "myStudent")等同于
 * 
 */

//注解中的value可以省略
@Component(value = "myStudent")
public class Student {
    private String name;
    private Integer age;
	//set、toString方法已省略
}

另外,Spring 还提供了 3 个创建对象的注解:

  1. @Repository 用于对 DAO 实现类进行注解
  2. @Service 用于对 Service 实现类进行注解
  3. @Controller 用于对 Controller 实现类进行注解

这三个注解与@Component 都可以创建对象,但这三个注解还有其他的含义,@Service创建业务层对象,业务层对象可以加入事务功能,@Controller 注解创建的对象可以作为处理器接收用户的请求。

@Repository,@Service,@Controller 是对@Component 注解的细化,标注不同层的对象。即持久层对象,业务层对象,控制层对象。

如果 @Component 不指定 value 属性,bean 的 id 是类名的首字母小写。

2.3.2 简单类型属性注入@Value

需要在属性上使用注解@Value,该注解的 value 属性用于指定要注入的值。

使用该注解完成属性注入时,类中无需 setter。当然,若属性有 setter,则也可将其加到 setter 上。

举例:

@Component("myStudent")
public class Student {
    /**
     * @Value:简单类型的属性赋值
     *  属性:value是String类型的,表示简单类型的属性值
     *  位置:1.在成员变量的上面,无需set方法(推荐使用)
     *       2.在set方法的上面
     */

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

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

2.3.3 byType 自动注入@Autowired

需要在引用属性上使用注解@Autowired,该注解默认使用按类型自动装配 Bean 的方式。

使用该注解完成属性注入时,类中无需 setter。当然,若属性有 setter,则也可将其加到 setter 上。

举例:

@Component("mySchool")
public class School {
    @Value("北京大学")
    private String name;
    @Value("北京的海淀区")
    private String address;
}

======================================================================

@Component("myStudent")
public class Student {
    @Value(value = "张飞")
    private String name;
    @Value("29")
    private Integer age;

    /**
     * 引用类型
     * @Autowired:Spring框架提供的注解,实现引用类型的赋值
     * Spring中通过注解给引用类型赋值,使用的是自动注入原理,支持byName,byType
     * @Autowired:默认使用的是byType自动注入
     *
     * 位置:1)在成员变量的上面,无需set方法(推荐使用)
     *      2)在set方法
     */
    @Autowired
    private School school;
}

2.3.4 byName 自动注入@Autowired 与@Qualifier

需要在引用属性上联合使用注解@Autowired 与@Qualifier。@Qualifier 的 value 属性用于指定要匹配的 Bean 的 id 值。类中无需 set 方法,也可加到 set 方法上。

举例:

@Component("myStudent")
public class Student {
    /**
     * 引用类型
     * @Autowired:Spring框架提供的注解,实现引用类型的赋值
     * Spring中通过注解给引用类型赋值,使用的是自动注入原理,支持byName,byType
     * @Autowired:默认使用的是byType自动注入
     *
     * 位置:1)在成员变量的上面,无需set方法(推荐使用)
     *      2)在set方法
     *
     * 如果要使用byName方式,需要进行如下操作:
     *      1.在成员变量上面加入@Autowired
     *      2.在成员变量上面加入@Qualifier(value = "bean的id")
     *        表示使用指定名称的bean完成赋值
     */

    //byName自动注入
    @Autowired
    @Qualifier("mySchool")
    private School school;
}

@Autowired 还有一个属性 required,默认值为 true,表示当匹配失败后,会终止程序运行。若将其值设置为 false,则匹配失败,将被忽略,未匹配的属性值为 null。

@Component("myStudent")
public class Student {
    /**
     * 引用类型
     * @Autowired:Spring框架提供的注解,实现引用类型的赋值
     * Spring中通过注解给引用类型赋值,使用的是自动注入原理,支持byName,byType
     * @Autowired:默认使用的是byType自动注入
     *
     * 属性:required是boolean类型,默认值为true
     *      required=true表示引用类型如果赋值失败,程序报错,并终止执行
     *      required=false表示引用类型如果赋值失败,程序正常执行,引用类型为null
     */

    @Autowired(required = false)
    @Qualifier("mySchool1")
    private School school;
}

2.3.5 JDK 注解 @Resource 自动注入

Spring提供了对 jdk中@Resource注解的支持。@Resource 注解既可以按名称匹配Bean,也可以按类型匹配 Bean。默认是按名称注入。使用该注解,要求 JDK 必须是 6 及以上版本。@Resource 可在属性上,也可在 set 方法上。

(1)byType 注入引用类型属性

@Resource 注解若不带任何参数,采用默认按名称的方式注入,按名称不能注入 bean,则会按照类型进行 Bean 的匹配注入。

举例:

@Component("myStudent")
public class Student {
    /**
     * 引用类型
     * @Resource:来自jdk中的注解,Spring框架提供了对这个注解的功能支持
     *            可以使用该注解给引用类型的属性赋值
     *            也是自动注入原理,支持byName,byType(默认是byName)
     *
     * 位置:1.在成员变量的上面,无需set方法(推荐使用)
     *      2.在set方法上面
     */

    //默认是byName:先使用byName自动注入,如果byName赋值失败,再使用byType
    @Resource
    private School school;
}

(2)byName 注入引用类型属性

@Resource 注解指定其 name 属性,则 name 的值即为按照名称进行匹配的 Bean 的 id。

举例:

@Component("myStudent")
public class Student {
    /**
     * 引用类型
     * @Resource:来自jdk中的注解,Spring框架提供了对这个注解的功能支持
     *            可以使用该注解给引用类型的属性赋值
     *            也是自动注入原理,支持byName,byType(默认是byName)
     *
     * 位置:1.在成员变量的上面,无需set方法(推荐使用)
     *      2.在set方法上面
     *
     * @Resource如果只使用byName方式,需要增加一个属性:name
     * name的值是bean的id名称
     */

    //只使用byName方式
    @Resource(name = "mySchool")
    private School school;
}

2.3.6 注解 与 XML 的对比

注解优点:

  • 方便
  • 直观
  • 高效(代码少,没有配置文件的书写那么复杂)

注解缺点:以硬编码的方式写入到 Java 代码中,修改是需要重新编译代码的。


XML优点:

  • 配置和代码是分离的
  • 在 xml 中做修改,无需编译代码,只需重启服务器即可将新的配置加载。

XML缺点:编写麻烦,效率低,大型项目过于复杂。

第3章 AOP 面向切面编程

3.1 不使用 AOP 的开发方式

方式一

先定义好接口与一个实现类,该实现类中除了要实现接口中的方法外,还要再写两个非业务方法。非业务方法也称为交叉业务逻辑:

  1. doTransaction():用于事务处理
  2. doLog():用于日志处理

然后,再使接口方法调用它们。接口方法也称为主业务逻辑。

SomeService接口:

public interface SomeService {
    void doSome();
    void doOther();
}

SomeService接口实现类:

public class SomeServiceImpl implements SomeService {
    //重写接口中doSome方法
    public void doSome() {
        doLog();
        System.out.println("执行业务方法doSome");
        doTrans();
    }

    //重写接口中doOther方法
    public void doOther() {
        doLog();
        System.out.println("执行业务方法doOther");
        doTrans();
    }

    //日志功能方法
    public void doLog(){
        System.out.println("非业务功能,日志功能,在方法开始时输出日志");
    }

    //事务功能方法
    public void doTrans(){
        System.out.println("非业务功能,在业务方法执行之后,加入事务");
    }
}

方式二

当然,也可以有另一种解决方案:将这些交叉业务逻辑代码放到专门的工具类或处理类中,由主业务逻辑调用。

创建一个工具类:

public class ServiceTools {
    public static void doLog(){
        System.out.println("非业务功能,日志功能,在方法开始时输出日志");
    }

    public static void doTrans(){
        System.out.println("非业务功能,在业务方法执行之后,加入事务");
    }
}

修改SomeServiceImpl类:

public class SomeServiceImpl implements SomeService {
    //重写接口中doSome方法
    public void doSome() {
        ServiceTools.doLog();
        System.out.println("执行业务方法doSome");
        ServiceTools.doTrans();
    }

    //重写接口中doOther方法
    public void doOther() {
        ServiceTools.doLog();
        System.out.println("执行业务方法doOther");
        ServiceTools.doTrans();
    }
}

方式三

以上的解决方案,还是存在弊端:交叉业务与主业务深度耦合在一起。当交叉业务逻辑较多时,在主业务代码中会出现大量的交叉业务逻辑代码调用语句,大大影响了主业务逻辑的可读性,降低了代码的可维护性,同时也增加了开发难度。

所以,可以采用动态代理方式。在不修改主业务逻辑的前提下,扩展和增强其功能。

功能增强

public class MyInvocationHandler implements InvocationHandler {

    //目标对象:SomeServiceImpl类
    private Object target;

    public MyInvocationHandler(Object target) {
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //通过代理对象执行方法时,会调用这个invoke()
        Object res = null;
        ServiceTools.doLog();
        //执行目标类的方法(doSome,doOther),通过Method类实现
        res = method.invoke(target,args);
        ServiceTools.doTrans();
        //返回目标方法执行结果
        return res;
    }
}

进行测试:

public class MyTest {
    public static void main(String[] args) {
        //使用jdk的Proxy创建代理对象
        //创建目标对象
        SomeService target = new SomeServiceImpl();

        //创建InvocationHandler对象
        InvocationHandler handler = new MyInvocationHandler(target);

        //使用Proxy创建代理
        SomeService proxy = (SomeService) Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),handler);

        //通过代理执行方法,会调用handler中的invoke()
        proxy.doSome();
    }
}

3.2 AOP 简介

AOP(Aspect Orient Programming),面向切面编程。面向切面编程是从动态角度考虑程序运行过程。

AOP 底层,就是采用动态代理模式实现的。采用了两种代理:

  1. JDK 的动态代理
  2. CGLIB的动态代理

AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程,可通过运行期动态代理实现程序功能的统一维护的一种技术。AOP 是 Spring 框架中的一个重要内容。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

面向切面编程,就是将交叉业务逻辑封装成切面,利用 AOP 容器的功能将切面织入到主业务逻辑中。所谓交叉业务逻辑是指,通用的、与主业务逻辑无关的代码,如安全检查、事务、日志、缓存等。

若不使用 AOP,则会出现代码纠缠,即交叉业务逻辑与主业务逻辑混合在一起。这样,会使主业务逻辑变的混杂不清。

例如,转账,在真正转账业务逻辑前后,需要权限控制、日志记录、加载事务、结束事务等交叉业务逻辑,而这些业务逻辑与主业务逻辑间并无直接关系。但,它们的代码量所占比重能达到总代码量的一半甚至还多。它们的存在,不仅产生了大量的“冗余”代码,还大大干扰了主业务逻辑—转账。

3.3 AOP 好处

  1. 减少重复代码
  2. 专注业务

注意:面向切面编程只是面向对象编程的一种补充。

使用 AOP 减少重复代码,专注业务实现:

【4万字详解】一篇文章搞定Spring框架_第4张图片

3.4 AOP 编程术语

(1)切面(Aspect)

切面表示增强的功能, 就是一堆代码,完成某个一个非业务功能。

常见的切面功能:日志, 事务, 统计信息, 参数检查, 权限验证。

(2)连接点(JoinPoint)

连接业务方法和切面的位置。说白了就是某个类中的业务方法。

(3)切入点(Pointcut)

切入点指声明的一个或多个连接点的集合。通过切入点指定一组方法。

被标记为 final 的方法是不能作为连接点与切入点的。因为最终的是不能被修改的,不能被增强的。

(4)目标对象(Target)

目 标 对 象 指 将 要 被 增 强 的 对 象 。 即 包 含 主 业 务 逻 辑 的 类 的 对 象 。 上 例 中 的 StudentServiceImpl 的对象若被增强,则该类称为目标类,该类对象称为目标对象。当然,不被增强,也就无所谓目标不目标了。

(5)通知(Advice)

通知表示切面的执行时间,Advice 也叫增强。上例中的 MyInvocationHandler 就可以理解为是一种通知。换个角度来说,通知定义了增强代码切入到目标代码的时间点,是目标方法执行之前执行,还是之后执行等。通知类型不同,切入时间不同。

切入点定义切入的位置,通知定义切入的时间

3.5 AspectJ 对 AOP 的实现

对于 AOP 这种编程思想,很多框架都进行了实现。Spring 就是其中之一,可以完成面向切面编程。然而,AspectJ 也实现了 AOP 的功能,且其实现方式更为简捷,使用更为方便,而且还支持注解式开发。所以,Spring 又将 AspectJ 的对于 AOP 的实现也引入到了自己的框架中。

在 Spring 中使用 AOP 开发时,一般使用 AspectJ 的实现方式。

AspectJ 是一个开源的专门用于做aop的框架。
是一个优秀面向切面的框架,它扩展了 Java 语言,提供了强大的切面实现。

3.5.1 AspectJ 的通知类型

AspectJ 中常用的通知有五种类型:

  1. @Before 前置通知
  2. @AfterReturning 后置通知
  3. @Around 环绕通知
  4. @AfterThrowing 异常通知
  5. @After 最终通知

3.5.2 AspectJ 的切入点表达式

AspectJ 定义了专门的表达式用于指定切入点。表达式的原型是:

/**
 * modifiers-pattern] 访问权限类型
 * ret-type-pattern 返回值类型
 * declaring-type-pattern 包名类名
 * name-pattern(param-pattern) 方法名(参数类型和参数个数)
 * throws-pattern 抛出异常类型
 * ?表示可选的部分
 */
execution(modifiers-pattern? ret-type-pattern
         declaring-type-pattern?name-pattern(param-pattern)
   		 throws-pattern?)

以上表达式共 4 个部分:

execution(访问权限 方法返回值 方法声明(参数) 异常类型)
注意:黄色字体为必选项

切入点表达式要匹配的对象就是目标方法的方法名。所以,execution 表达式中明显就是方法的签名。

注意,表达式中黑色文字表示可省略部分,各部分间用空格分开。在其中可以使用以下符号:
【4万字详解】一篇文章搞定Spring框架_第5张图片

举例:

execution(public * *(..)) 
//指定切入点为:任意公共方法。

execution(* set*(..)) 
//指定切入点为:任何一个以“set”开始的方法。

execution(* com.xyz.service.*.*(..)) 
//指定切入点为:定义在 service 包里的任意类的任意方法。

execution(* com.xyz.service..*.*(..))
//指定切入点为:定义在 service 包或者子包里的任意类的任意方法。“..”出现在类名中时,
//后面必须跟 *,表示包、子包下的所有类。

execution(* *..service.*.*(..))
//指定所有包下的 serivce 子包下所有类(接口)中所有方法为切入点

3.5.3 AspectJ 基于注解的 AOP 实现

AspectJ 提供了以注解方式对于 AOP 的实现。

(1)实现步骤

Step1:导入Maven依赖


<dependency>
    <groupId>junitgroupId>
    <artifactId>junitartifactId>
    <version>4.13version>
dependency>

<dependency>
    <groupId>org.springframeworkgroupId>
    <artifactId>spring-contextartifactId>
    <version>5.2.0.RELEASEversion>
dependency>

<dependency>
    <groupId>org.springframeworkgroupId>
    <artifactId>spring-aspectsartifactId>
    <version>5.3.6version>
dependency>

Step2:定义业务接口与实现类

public interface SomeService {
    void doSome(String name,Integer age);
}

======================================================================

//目标类
public class SomeServiceImpl implements SomeService{
    //给doSome方法增加一个功能,在doSome()执行之前,输出方法的执行时间
    public void doSome(String name, Integer age) {
        System.out.println("=====目标方法doSome()=====");
    }
}

Step3:定义切面类

类中定义了若干普通方法,将作为不同的通知方法,用来增强功能。

/**
 * @Aspect:是aspectj框架中的注解
 *     作用:表示当前类是切面类
 *     切面类:用来给业务方法增加功能的类,在这个类中有切面的功能代码
 */
@Aspect
public class MyAspect {
    /**
     * 定义方法用于实现切面功能
     *
     * 方法定义的要求:
     *  1.公共方法 public
     *  2.方法没有返回值
     *  3.方法名称自定义
     *  4.方法可以有参数,也可以没有
     *      如果有参数,参数不可以自定义,有几种参数类型可以使用
     */


    /**
     * @before:前置通知注解
     * 属性:value,是切入点表达式,表示切面功能执行的位置
     * 特点:1.在目标方法之前先执行
     *      2.不会影响目标方法的执行
     */
    @Before(value = "execution(public void com.xu.service.SomeServiceImpl.doSome(String,Integer))")
    public void myBefore(){
        //就是切面要执行的功能代码
        System.out.println("前置通知:在目标方法执行之前输出执行时间:" + new Date());
    }
}

Step4:Spring容器中注册目标对象和切面类对象


<bean id="someService" class="com.xu.service.SomeServiceImpl"/>

<bean id="myAspect" class="com.xu.service.MyAspect"/>

Step5:注册 AspectJ 的自动代理

在定义好切面 Aspect 后,需要通知 Spring 容器,让容器生成“目标类+ 切面”的代理对象。这个代理是由容器自动生成的。只需要在 Spring 配置文件中注册一个基于 aspectj 的自动代理生成器,其就会自动扫描到@Aspect 注解,并按通知类型与切入点,将其织入,并生成代理。

aop:aspectj-autoproxy 的底层是由 AnnotationAwareAspectJAutoProxyCreator 实现的。从其类名就可看出,是基于 AspectJ 的注解适配自动代理生成器。

其工作原理是,aop:aspectj-autoproxy 通过扫描找到@Aspect 定义的切面类,再由切面类根据切入点找到目标类的目标方法,再由通知类型找到切入的时间点。


<aop:aspectj-autoproxy/>

Step6:进行测试

@Test
public void test01(){
    String config = "applicationContext.xml";
    ApplicationContext context = new ClassPathXmlApplicationContext(config);
    //从容器中获取目标对象(代理对象)
    SomeService proxy = (SomeService) context.getBean("someService");
    //通过代理对象执行方法,实现目标方法执行时,功能的增强
    proxy.doSome("lisi",20);
}

(2)@Before 前置通知

在目标方法执行之前执行。被注解为前置通知的方法,可以包含一个 JoinPoint 类型参数。该类型的对象本身就是切入点表达式。通过该参数,可获取切入点表达式、方法签名、目标对象等。

不光前置通知的方法,可以包含一个 JoinPoint 类型参数,所有的通知方法均可包含该参数。

@Aspect
public class MyAspect {
    /**
     * 指定通知方法中的参数:JoinPoint
     * JoinPoint:代表要加入切面功能的业务方法
     * 作用是:可以在通知方法中获取方法执行时的信息,例如:方法名称,方法实参
     * 如果你的切面功能中需要用到方法的信息,就加入JoinPoint参数
     * 参数的值是由框架赋予的,必须是第一个位置的参数
     */
    @Before(value = "execution(void *..SomeServiceImpl.doSome(String,Integer))")
    public void myBefore(JoinPoint jp){
        System.out.println("方法的定义:" + jp.getSignature());
        System.out.println("方法的名称:" + jp.getSignature().getName());
        //获取方法参数信息
        Object[] args = jp.getArgs();
        for (Object arg : args) {
            System.out.println(arg);
        }
        //就是切面要执行的功能代码
        System.out.println("前置通知:在目标方法执行之前输出执行时间:" + new Date());
    }
}

(3)@AfterReturning 后置通知

在目标方法执行之后执行。由于是目标方法之后执行,所以可以获取到目标方法的返回值。该注解的 returning 属性就是用于指定接收方法返回值的变量名的。所以,被注解为后置通知的方法,除了可以包含 JoinPoint 参数外,还可以包含用于接收返回值的变量。该变量最好为 Object 类型,因为目标方法的返回值可能是任何类型。

接口方法:

public interface SomeService {
    String doOther(String name,Integer age);
}

实现方法:

public class SomeServiceImpl implements SomeService{
    public String doOther(String name, Integer age) {
        System.out.println("=====目标方法doOther()=====");
        return "abcd";
    }
}

定义切面:

@Aspect
public class MyAspect {
    /**
     * 后置通知定义方法:方法有参数,推荐是Object
     * @AfterReturning:后置通知
     *      属性:1.value 切入点表达式
     *           2.returning 自定义的变量,表示目标方法的返回值
     *                       自定义的变量名必须和通知方法的形参名一样
     *
     *      特点:1.在目标方法之后执行
     *           2.能够获取目标方法的返回值,可以根据这个返回值做不同的处理功能
     *              Object res = doOther();
     *           3.可以修改返回值
     *
     *      后置通知的执行:
     *           Object res = doOther();
     *           myAfterReturning(res);
     */
    @AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))",
                    returning = "res")
    public void myAfterReturning(Object res){
        //Object res:是目标方法执行之后的返回值,根据返回值做切面的功能处理
        System.out.println("后置通知:在目标方法执行之后执行,获取到返回值是:" + res);

        //修改目标方法的执行结果
        if (res != null){
            res = "Hello AspectJ";
        }
    }
}

(4)@Around 环绕通知

在目标方法执行之前之后执行。被注解为环绕增强的方法要有返回值,Object 类型。并且方法可以包含一个 ProceedingJoinPoint 类型的参数。接口 ProceedingJoinPoint 其有一个proceed()方法,用于执行目标方法。若目标方法有返回值,则该方法的返回值就是目标方法的返回值。最后,环绕增强方法将其返回值返回。该增强方法实际是拦截了目标方法的执行。

接口方法:

public interface SomeService {
    String doFirst(String name,Integer age);
}

实现方法:

public class SomeServiceImpl implements SomeService{
    public String doFirst(String name, Integer age) {
        System.out.println("=====目标方法doFirst()=====");
        return "doFirst";
    }
}

定义切面:

@Aspect
public class MyAspect {
    /**
     * 环绕通知方法的定义格式
     *      1.public
     *      2.必须有返回值,推荐使用Object
     *      3.方法名称自定义
     *      4.方法有固定的参数:ProceedingJoinPoint
     *
     * @Around:环绕通知
     *      属性:value 切入点表达式
     *      特点:1.它是功能最强的通知
     *           2.在目标方法的前后都能增强功能
     *           3.控制目标方法是否被调用执行
     *           4.修改原来目标方法的执行结果。影响最后的调用结果
     *
     *      环绕通知,等同于JDK的动态代理,InvocationHandler接口
     *
     *      参数:ProceedingJoinPoint 等同于 Method
     *           作用:执行目标方法
     *      返回值:就是目标方法的执行结果,可以被修改
     *
     *      环绕通知:经常用于事务操作,在目标方法之前开启事务,执行目标方法
     *              在目标方法之后提交事务
     */
    @Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
    public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
        //实现环绕通知
        Object result = null;
        System.out.println("环绕通知:在目标方法之前,输出时间:" + new Date());
        //目标方法调用,在目标方法前或者后加入功能
        result = pjp.proceed(); //等同于 method.invoke()
        System.out.println("环绕通知:在目标方法之后,提交事务");
        //返回目标方法的执行结果
        return result;
    }
}

(5)@Pointcut 定义切入点

当较多的通知增强方法使用相同的 execution 切入点表达式时,编写、维护均较为麻烦。AspectJ 提供了@Pointcut 注解,用于定义 execution 切入点表达式。

其用法是,将@Pointcut 注解在一个方法之上,以后所有的 execution 的 value 属性值均可使用该方法名作为切入点。代表的就是@Pointcut 定义的切入点。这个使用@Pointcut 注解的方法一般使用 private 的标识方法,即没有实际作用的方法。

@Aspect
public class MyAspect {
    @After(value = "mypt()")
    public void myAfter() {
        System.out.println("最终通知:总是会被执行的代码");
    }

    @Before(value = "mypt()")
    public void myBefore() {
        System.out.println("前置通知:在目标方法之前先执行");
    }

    /**
     * @Pointcut:定义和管理切入点,如果项目中有多个切入点表达式是重复的、k可以复用的
     *            可以使用该注解
     *
     *     属性:value 切入点表达式
     *     特点:当使用该注解定义在方法上,此时这个方法的名称就是切入点表达式的别名
     *          其他的通知中,value属性就可以使用这个方法的名称代替切入点表达式了
     */
    @Pointcut(value = "execution(* *..SomeServiceImpl.doOther(..))")
    private void mypt(){
        //无需代码
    }
}

第4章 Spring 事务

4.1 Spring 的事务管理

事务原本是数据库中的概念,在 Dao 层。但一般情况下,需要将事务提升到业务层,即 Service 层。这样做是为了能够使用事务的特性来管理具体的业务。

在 Spring 中通常可以通过以下两种方式来实现对事务的管理:

  1. 使用 Spring 的事务注解管理事务
  2. 使用 AspectJ 的 AOP 配置管理事务

4.2 Spring 事务管理 API

4.2.1 事务管理器接口

事务管理器是 PlatformTransactionManager 接口对象。其主要用于完成事务的提交、回滚,及获取事务的状态信息。

【4万字详解】一篇文章搞定Spring框架_第6张图片

(1)常用的两个实现类

PlatformTransactionManager 接口有两个常用的实现类:

  1. DataSourceTransactionManager:使用 JDBC 或 MyBatis 进行数据库操作时使用。
  2. HibernateTransactionManager:使用 Hibernate 进行持久化数据时使用。

(2)Spring 的回滚方式

Spring 事务的默认回滚方式是:发生运行时异常和 error 时回滚,发生受查(编译)异常时提交。不过,对于受查异常,程序员也可以手工设置其回滚方式。

4.2.2 事务定义接口

事务定义接口 TransactionDefinition 中定义了事务描述相关的三类常量:事务隔离级别、事务传播行为、事务默认超时时限,及对它们的操作。

(1)定义五个事务隔离级别常量

这些常量均是以 ISOLATION_开头。即形如 ISOLATION_XXX。

  1. DEFAULT:采用 DB 默认的事务隔离级别。MySql 的默认为 REPEATABLE_READ; Oracle
    默认为 READ_COMMITTED。
  2. READ_UNCOMMITTED:读未提交。未解决任何并发问题。
  3. READ_COMMITTED:读已提交。解决脏读,存在不可重复读与幻读。
  4. REPEATABLE_READ:可重复读。解决脏读、不可重复读,存在幻读
  5. SERIALIZABLE:串行化。不存在并发问题。

(2)定义七个事务传播行为常量

所谓事务传播行为是指,处于不同事务中的方法在相互调用时,执行期间事务的维护情况。如,A 事务中的方法 doSome()调用 B 事务中的方法 doOther(),在调用执行期间事务的维护情况,就称为事务传播行为。事务传播行为是加在方法上的。

事务传播行为常量都是以 PROPAGATION_ 开头,形如 PROPAGATION_XXX。

PROPAGATION_REQUIRED
PROPAGATION_REQUIRES_NEW
PROPAGATION_SUPPORTS

PROPAGATION_MANDATORY
PROPAGATION_NESTED
PROPAGATION_NEVER
PROPAGATION_NOT_SUPPORTED


PROPAGATION_REQUIRED

指定的方法必须在事务内执行。若当前存在事务,就加入到当前事务中;若当前没有事务,则创建一个新事务。这种传播行为是最常见的选择,也是 Spring 默认的事务传播行为。

如该传播行为加在 doOther()方法上。若 doSome()方法在调用 doOther()方法时就是在事务内运行的,则 doOther()方法的执行也加入到该事务内执行。若 doSome()方法在调用doOther()方法时没有在事务内执行,则 doOther()方法会创建一个事务,并在其中执行。

【4万字详解】一篇文章搞定Spring框架_第7张图片

PROPAGATION_SUPPORTS

指定的方法支持当前事务,但若当前没有事务,也可以以非事务方式执行。

【4万字详解】一篇文章搞定Spring框架_第8张图片

PROPAGATION_REQUIRES_NEW

总是新建一个事务,若当前存在事务,就将当前事务挂起,直到新事务执行完毕。

【4万字详解】一篇文章搞定Spring框架_第9张图片

(3) 定义默认事务超时时限

常量 TIMEOUT_DEFAULT 定义了事务底层默认的超时时限,sql 语句的执行时长。

注意,事务的超时时限起作用的条件比较多,且超时的时间计算点较复杂。所以,该值一般就使用默认值即可。

4.3 事务演示案例环境搭建

该案例要实现:购买商品,模拟用户下订单,向订单表添加销售记录,从商品表减少库存。

4.3.1 创建数据库表

创建两个数据库表 sale , goods

sale销售表:
【4万字详解】一篇文章搞定Spring框架_第10张图片

goods商品表:
【4万字详解】一篇文章搞定Spring框架_第11张图片

goods表中数据:
【4万字详解】一篇文章搞定Spring框架_第12张图片

4.3.2 导入Maven依赖

<dependencies>
    
    <dependency>
        <groupId>junitgroupId>
        <artifactId>junitartifactId>
        <version>4.13version>
    dependency>
    
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-contextartifactId>
        <version>5.2.5.RELEASEversion>
    dependency>
    
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-txartifactId>
        <version>5.2.5.RELEASEversion>
    dependency>
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-jdbcartifactId>
        <version>5.2.5.RELEASEversion>
    dependency>
    
    <dependency>
        <groupId>org.mybatisgroupId>
        <artifactId>mybatisartifactId>
        <version>3.5.1version>
    dependency>
    
    <dependency>
        <groupId>org.mybatisgroupId>
        <artifactId>mybatis-springartifactId>
        <version>1.3.1version>
    dependency>
    
    <dependency>
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
        <version>5.1.9version>
    dependency>
    
    <dependency>
        <groupId>com.alibabagroupId>
        <artifactId>druidartifactId>
        <version>1.1.12version>
    dependency>
dependencies>


<build>
    <resources>
        <resource>
            <directory>src/main/javadirectory>
            <includes>
                <include>**/*.propertiesinclude>
                <include>**/*.xmlinclude>
            includes>
            <filtering>falsefiltering>
        resource>
        <resource>
            <directory>src/main/resourcesdirectory>
            <includes>
                <include>**/*.propertiesinclude>
                <include>**/*.xmlinclude>
            includes>
            <filtering>falsefiltering>
        resource>
    resources>
build>

4.3.3 创建实体类

根据两张表,分别创建Goods类 与 Sale类

public class Goods {
    private Integer id;
    private String name;
    private Integer amount;
    private Float price;
    //get、set方法已省略
}
public class Sale {
    private Integer id;
    private Integer gid;
    private Integer nums;
	//get、set方法已省略
}

4.3.4 创建mapper接口

GoodsMapper接口:

public interface GoodsMapper {
    //更新库存,goods表示本次购买的商品信息
    int updateGoods(Goods goods);

    //查询商品的信息
    Goods selectGoods(Integer id);
}

SaleMapper接口:

public interface SaleMapper {
    //插入销售记录
    int insertSale(Sale sale);
}

4.3.5 创建mapper映射文件

SaleMapper.xml:


DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.xu.mapper.SaleMapper">
    <insert id="insertSale">
        insert into sale(gid,nums) values (#{gid},#{nums});
    insert>
mapper>

GoodsMapper.xml:


DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.xu.mapper.GoodsMapper">
    <update id="updateGoods">
        update set amount = amount - #{amount} where id = #{id};
    update>

    <select id="selectGoods" resultType="com.xu.pojo.Goods">
        select id,name,amount,price from goods where id = #{id};
    select>
mapper>

4.3.6 创建Mybatis核心配置文件


DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
    
    <typeAliases>
        <package name="com.xu.pojo"/>
    typeAliases>

    
    <mappers>
        <package name="com.xu.mapper"/>
    mappers>
configuration>

4.3.7 创建异常类

定义 service 层可能会抛出的异常类 NotEnoughException

//自定义的运行时异常
public class NotEnoughException extends RuntimeException{
    public NotEnoughException() {
        super();
    }

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

4.3.8 创建Service接口

public interface BugGoodsService {
    //购买商品,参数分别是:购买商品的编号、数量
    void buy(Integer goodsId,Integer nums);
}

4.3.9 创建Service接口的实现类

@Component("buyService")
public class BuyGoodsServiceImpl implements BugGoodsService {
    @Autowired
    private GoodsMapper goodsMapper;
    @Autowired
    private SaleMapper saleMapper;

    public void buy(Integer goodsId, Integer nums) {
        //记录销售的信息,向sale表添加记录
        Sale sale = new Sale();
        sale.setGid(goodsId);
        sale.setNums(nums);
        saleMapper.insertSale(sale);

        //更新库存
        Goods goods = goodsMapper.selectGoods(goodsId);
        if (goods == null) {
            //商品不存在
            throw new NullPointerException("编号为:" + goodsId + "商品不存在");
        } else if (goods.getAmount() < nums) {
            //库存不足
            throw new NotEnoughException("编号为:" + goodsId + "商品库存不足");
        }

        //修改库存
        Goods buyGoods = new Goods();
        buyGoods.setId(goodsId);
        buyGoods.setAmount(nums);
        goodsMapper.updateGoods(buyGoods);
    }
}

4.3.10 创建Spring配置文件


<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:component-scan base-package="com.xu.service"/>

    
    <bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        
        <property name="url" value="jdbc:mysql://localhost:3306/ssm"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
        <property name="maxActive" value="20"/>
    bean>

    
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        
        <property name="dataSource" ref="myDataSource"/>
        
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
    bean>

    
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
        
        <property name="basePackage" value="com.xu.mapper"/>
    bean>
beans>

4.3.11 创建测试类

现在就可以在无事务代理的情况下运行了

public class MyTest {
    @Test
    public void test01(){
        String config = "applicationContext.xml";
        ApplicationContext context = new ClassPathXmlApplicationContext(config);
        BuyGoodsServiceImpl service = (BuyGoodsServiceImpl) context.getBean("buySerivce");
        //调用方法
        service.buy(1001,10);
    }
}

4.4 使用 Spring 的事务注解管理事务

通过@Transactional 注解方式,可将事务织入到相应 public 方法中,实现事务管理。

@Transactional 的所有可选属性如下所示:

propagation:用于设置事务传播属性。该属性类型为 Propagation 枚举,默认值为Propagation.REQUIRED
isolation:用于设置事务的隔离级别。该属性类型为 Isolation 枚举,默认值为 Isolation.DEFAULT
readOnly:用于设置该方法对数据库的操作是否是只读的。该属性为 boolean,默认值为 false
timeout:用于设置本操作与数据库连接的超时时限。单位为秒,类型为 int,默认值为 -1,即没有时限
rollbackFor:指定需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组
rollbackForClassName:指定需要回滚的异常类类名。类型为 String[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组
noRollbackFor:指定不需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组
noRollbackForClassName:指定不需要回滚的异常类类名。类型为 String[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。

需要注意的是,@Transactional 若用在方法上,只能用于 public 方法上。对于其他非 public 方法,如果加上了注解@Transactional,虽然 Spring 不会报错,但不会将指定事务织入到该方法中。因为 Spring 会忽略掉所有非 public 方法上的@Transaction 注解。

若@Transaction 注解在类上,则表示该类上所有的方法均将在执行时织入事务。


实现步骤如下:

4.4.1 在Spring容器中添加事务管理器



<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    
    <property name="dataSource" ref="myDataSource"/>
bean>

4.4.2 开启事务注解驱动


<tx:annotation-driven transaction-manager="transactionManager"/>

4.4.3 业务层 public 方法上加入事务属性

/**
 * rollbackFor:表示发生指定的异常一定回滚
 *
 * 处理逻辑:
 *       1.Spring框架首先会检查方法抛出的异常是不是在rollbackFor的属性值中
 *       如果异常在rollbackFor的列表中,不管是什么类型的异常,一定回滚
 *       2.如果抛出的异常不在rollbackFor的列表中,Spring会判断异常是不是RuntimeException
 *       如果是,一定回滚
 */
@Transactional(
        propagation = Propagation.REQUIRED,
        isolation = Isolation.DEFAULT,
        readOnly = false,
        rollbackFor = {
                NullPointerException.class,
                NotEnoughException.class
        }
)

======================================================================

//使用事务控制的默认值,默认的传播行为是 REQUIRED ,默认的隔离级别是 DEFAULT
//默认抛出运行时异常,回滚事务
@Transactional

4.5 使用 AspectJ 的 AOP 配置管理事务

使用 XML 配置事务代理的方式的不足是,每个目标类都需要配置事务代理。当目标类较多,配置文件会变得非常臃肿。

使用 XML 配置顾问方式可以自动为每个符合切入点表达式的类生成事务代理。其用法很简单,只需将前面代码中关于事务代理的配置删除,再替换为如下内容即可。

4.5.1 导入Maven依赖


<dependency>
    <groupId>org.springframeworkgroupId>
    <artifactId>spring-aspectsartifactId>
    <version>5.2.5.RELEASEversion>
dependency>

4.5.2 在Spring容器中添加事务管理器



<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    
    <property name="dataSource" ref="myDataSource"/>
bean>

4.5.3 配置事务通知

为事务通知设置相关属性。用于指定要将事务以什么方式织入给哪些方法。

例如,应用到 buy 方法上的事务要求是必须的,且当 buy 方法发生异常后要回滚业务。


<tx:advice id="myAdvice" transaction-manager="transactionManager">
    
    <tx:attributes>
        
        <tx:method name="buy" propagation="REQUIRED" isolation="DEFAULT"
                   rollback-for="java.lang.NullPointerException,com.xu.excep.NotEnoughException"/>
        
        <tx:method name="add*" propagation="REQUIRES_NEW"/>
    tx:attributes>
tx:advice>

4.5.4 配置增强器

指定将配置好的事务通知,织入给谁。


<aop:config>
    
    <aop:pointcut id="servicePt" expression="execution(* *..service..*.*(..))"/>

    
    <aop:advisor advice-ref="myAdvice" pointcut-ref="servicePt"/>
aop:config>

4.5.5 进行测试

测试类中要从容器中获取的是目标对象。

public class MyTest {
    @Test
    public void test01(){
        String config = "applicationContext.xml";
        ApplicationContext context = new ClassPathXmlApplicationContext(config);
        BuyGoodsServiceImpl service = (BuyGoodsServiceImpl) context.getBean("buySerivce");
        //调用方法
        service.buy(1001,10);
    }
}

你可能感兴趣的:(spring,ioc,aop,mybatis,mysql)