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

文章目录

前言

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

相关文章推荐

第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体系结构

在这里插入图片描述
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依赖:

<!--Spring框架依赖-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.5.RELEASE</version>
</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 扩展名。

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

    <!--
        1.beans:是根标签
        2.spring-beans.xsd:是约束文件,与mybatis中的dtd约束文件一样
                            用来约束该文件中允许出现的内容
    -->

    <!--
        注册bean操作:
        告诉Spring我们要创建某个类的对象
        id:对象的自定义名称,唯一值。Spring通过这个id值找到对象
        class:类的全限定名称(不能是接口,因为Spring是反射机制创建对象)

        Spring完成 SomeService someService = new SomeServiceImpl();
        Spring框架中有一个map用于存放对象,Spring把创建好的对象放入到map中
            springMap.put(id值,对象);
            例如:stringMap.put("someService",new SomeServiceImpl());

        一个bean标签只能声明一个对象
    -->
    <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 的配置文件,在程序中充当“容器”的角色。其实现类有两个。

在这里插入图片描述

(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 对象

在这里插入图片描述

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对象
<!--
    注册Student对象
    DI:给属性赋值,注入就是赋值的意思
    简单类型:Spring中规定java的基本数据类型(包括包装类)和String都是简单类型

    1.set注入:Spring容器调用类的set方法
              我们可以在set方法中完成属性的赋值
        (1)简单类型的set注入
            <bean id="xx" class="xx">
                一个property只能给一个属性赋值
                <property name="属性名字" value="属性的值"/>
            </bean>
-->
<bean id="myStudent" class="com.xu.pojo.Student">
    <property name="name" value="李四"/> <!--setName("李四")-->
    <property name="age" value="20"/> <!--setAge(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对象
<!--
    注册Student对象
    DI:给属性赋值,注入就是赋值的意思

    1.set注入:Spring容器调用类的set方法
              我们可以在set方法中完成属性的赋值
        (2)引用类型的set注入:Spring调用类的set方法
            <bean id="xx" class="xx">
                <property name="属性名称" ref="bean的id"/>
            </bean>
-->
<bean id="myStudent" class="com.xu.pojo.Student">
    <property name="name" value="李四"/>
    <property name="age" value="20"/>
    <!--引用类型的赋值-->
    <property name="school" ref="mySchool"/><!--setSchool(mySchool)-->
</bean>

<!--注册School对象-->
<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对象
<!--
    构造注入:Spring调用类的有参构造方法,在创建对象的同时,在构造方法中给属性赋值
    构造注入使用 <constructor-arg> 标签
    一个 <constructor-arg> 标签表示构造方法的一个参数

    <constructor-arg> 标签属性:
        name:表示构造方法的形参名
        index:表示构造方法的形参位置,参数从左到右分别是 0,1,2...
        value:构造方法的形参类型是简单类型使用value
        ref:构造方法的形参类型是引用类型使用ref
-->

<!--使用name属性实现构造注入-->
<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>

<!--使用index属性实现构造注入-->
<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>

<!--注册School对象-->
<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 进行比较而实现自动注入的。

举例:

<!--
    引用类型属性自动注入:Spring框架可以根据某些规则可以给引用类型赋值
    使用规则常用的是:byName,byType

    1.byName(按名称注入):java类中引用类型的属性名和Spring容器中(配置文件) <bean>的id名称一样,
               			且数据类型一致,这样的容器中的bean,Spring能够赋值给引用类型
        语法:
        <bean id="xx" class="xx" autowire="byName">
            简单类型属性赋值
        </bean>
-->
<bean id="myStudent" class="com.xu.pojo.Student" autowire="byName">
    <property name="name" value="李四"/>
    <property name="age" value="21"/>
</bean>

<!--注册School对象-->
<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 只能有一个。多于一个,容器就不知该匹配哪一个了。

举例:

<!--
    引用类型属性自动注入:Spring框架可以根据某些规则可以给引用类型赋值
    使用规则常用的是:byName,byType

    2.byType(按类型注入):java类中引用类型的数据类型和Spring容器中(配置文件) <bean>的class属性
                        是同源关系,这样的bean能够赋值给引用类型
    同源就是一类的意思:
        1.java类中引用类型的数据类型和bean的class的值是一样的
        2.java类中引用类型的数据类型和bean的class的值是父子类关系
        3.java类中引用类型的数据类型和bean的class的值是接口和实现类关系

        语法:
        <bean id="xx" class="xx" autowire="byType">
            简单类型属性赋值
        </bean>

        注意:在byType中,在Spring容器中声明bean只能有一个符合条件,
             多于一个是错误的
-->
<bean id="myStudent" class="com.xu.pojo.Student" autowire="byName">
    <property name="name" value="李四"/>
    <property name="age" value="21"/>
</bean>

<!--注册School对象-->
<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 代码如下,分为两种方式:

<!--
    包含关系的配置文件:
    spring-total表示主配置文件:包含其他配置文件,主配置文件一般是不定义对象的

    语法:<import resource="其他配置文件的路径"/>
    关键字:"classpath:" 表示类路径(class文件所在的目录)
           在Spring配置文件中要指定其他文件的位置
           需要使用classpath告诉Spring到哪里去加载读取文件
-->

<!--加载的是文件列表-->
<import resource="classpath:spring-student.xml"/>
<import resource="classpath:spring-school.xml"/>

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

<!--
    在包含关系的配置文件中,可以使用通配符(*:表示任意字符)
    注意:主配置文件名称不能包含在通配符的范围内(不能叫做spring-total.xml)
    	 否则将出现 循环递归包含
         如果使用通配符必须保证其他配置文件在同一个目录下
         需要导入的配置文件不能直接放在resources目录下
         直接放在resources目录下使用通配符方式是读取不到的!
-->
<import resource="classpath:spring04/spring-*.xml"/>

2.3 基于注解的DI

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

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

applicationContext.xml 配置文件:

<!--
    声明组件扫描器(component-scan),组件就是Java对象
    base-package:指定注解在项目中的包名

    component-scan工作方式:
        Spring会扫描遍历base-package指定的包
        找到包和子包中所有类的注解
        按照注解功能创建对象,或给属性赋值

    加入了component-scan标签,配置文件的变化:
        1.加入一个新的约束文件 spring-context.xsd
        2.给这个新的约束文件起个命名空间的名称
-->
<context:component-scan base-package="com.xu.pojo"/>

扫描多个包的三种方式:

<!--扫描多个包的三种方式-->
<!--1.使用多次组件扫描器,扫描不同的包-->
<context:component-scan base-package="com.xu.bao1"/>
<context:component-scan base-package="com.xu.bao2"/>

<!--2.使用分隔符(,或;)分隔多个包名-->
<context:component-scan base-package="com.xu.bao1;com.xu.bao2"/>

<!--3.扫描父级包-->
<context:component-scan base-package="com.xu"/>

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

2.3.1 定义 Bean 的注解@Component

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

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

//注解中的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 减少重复代码,专注业务实现:

在这里插入图片描述

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 表达式中明显就是方法的签名。

注意,表达式中黑色文字表示可省略部分,各部分间用空格分开。在其中可以使用以下符号:
在这里插入图片描述

举例:

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>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13</version>
</dependency>
<!--Spring框架依赖-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.0.RELEASE</version>
</dependency>
<!--aspectJ框架依赖-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.6</version>
</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 定义的切面类,再由切面类根据切入点找到目标类的目标方法,再由通知类型找到切入的时间点。

<!--
    声明自动代理生成器:使用aspectJ框架内部的功能,创建目标对象的代理对象
    创建代理对象是在内存中实现的,修改目标对象在内存中的结构,创建为代理对象
    所以,目标对象就是被修改后的代理对象
    aspectj-autoproxy:会把Spring容器中所有的目标对象,一次性生成代理对象
-->
<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 接口对象。其主要用于完成事务的提交、回滚,及获取事务的状态信息。

在这里插入图片描述

(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()方法会创建一个事务,并在其中执行。

在这里插入图片描述

PROPAGATION_SUPPORTS

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

在这里插入图片描述

PROPAGATION_REQUIRES_NEW

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

在这里插入图片描述

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

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

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

4.3 事务演示案例环境搭建

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

4.3.1 创建数据库表

创建两个数据库表 sale , goods

sale销售表:
在这里插入图片描述

goods商品表:
在这里插入图片描述

goods表中数据:
在这里插入图片描述

4.3.2 导入Maven依赖

<dependencies>
    <!--单元测试-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13</version>
    </dependency>
    <!--Spring框架依赖-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.5.RELEASE</version>
    </dependency>
    <!--做Spring事务用到的依赖-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
        <version>5.2.5.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.2.5.RELEASE</version>
    </dependency>
    <!--Mybatis框架依赖-->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.1</version>
    </dependency>
    <!--Mybatis和Spring集成依赖-->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>1.3.1</version>
    </dependency>
    <!--MySQL驱动依赖-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.9</version>
    </dependency>
    <!--阿里巴巴的数据库连接池-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.12</version>
    </dependency>
</dependencies>

<!--解决Maven资源过滤问题-->
<build>
    <resources>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.xml</include>
            </includes>
            <filtering>false</filtering>
        </resource>
        <resource>
            <directory>src/main/resources</directory>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.xml</include>
            </includes>
            <filtering>false</filtering>
        </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:

<?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="com.xu.mapper.SaleMapper">
    <insert id="insertSale">
        insert into sale(gid,nums) values (#{gid},#{nums});
    </insert>
</mapper>

GoodsMapper.xml:

<?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="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核心配置文件

<?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>
    <!--设置别名-->
    <typeAliases>
        <package name="com.xu.pojo"/>
    </typeAliases>

    <!--绑定mapper映射文件-->
    <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配置文件

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

    <!--声明组件扫描器(component-scan)-->
    <context:component-scan base-package="com.xu.service"/>

    <!--注册数据源 DataSource,作用是:连接数据库-->
    <bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <!--set注入:给 DruidDataSource 提供数据库连接信息-->
        <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>

    <!--声明Mybatis中提供的SqlSessionFactory类,该类内部创建SqlSessionFactory-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!--set注入,把数据库的连接池注入到dataSource属性-->
        <property name="dataSource" ref="myDataSource"/>
        <!--Mybatis核心配置文件位置-->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
    </bean>

    <!--
        使用SqlSession的getMapper()创建接口的实现类对象
        MapperScannerConfigurer:在内部调用getMapper()生成每个mapper接口的代理对象
    -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!--指定SqlSessionFactory对象的id-->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
        <!--
            指定包名,包名是mapper接口所在包名
            MapperScannerConfigurer会扫描这个包中的所有接口,
            把每个接口都执行一次getMapper(),得到每个接口的代理对象
            所有代理对象都放入到Spring容器中,对象的默认名称是接口名首字母小写
        -->
        <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容器中添加事务管理器

<!--使用Spring事务处理-->
<!--声明事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!--连接的数据库,指定数据源-->
    <property name="dataSource" ref="myDataSource"/>
</bean>

4.4.2 开启事务注解驱动

<!--
    开启事务注解驱动,告诉Spring使用注解管理事务,创建代理对象
    transaction-manager:事务管理器对象的id
-->
<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依赖

<!--aspectJ依赖-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.2.5.RELEASE</version>
</dependency>

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

<!--使用Spring事务处理-->
<!--声明事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!--连接的数据库,指定数据源-->
    <property name="dataSource" ref="myDataSource"/>
</bean>

4.5.3 配置事务通知

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

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

<!--
    声明业务方法的事务属性:隔离级别,传播行为,超时时间
    transaction-manager:事务管理器对象的id
-->
<tx:advice id="myAdvice" transaction-manager="transactionManager">
    <!--表示配置事物的属性-->
    <tx:attributes>
        <!--
            表示给具体的方法配置事务属性,method可以有多个,分别给不同的方法设置事务属性
            name:方法名称,1.完整的方法名称,不带有包和类
                          2.方法可以使用通配符
            propagation:传播行为,枚举值
            isolation:隔离级别
            rollback-for:指定的异常类名,全限定类名。发生异常一定回滚
        -->
        <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-->
<aop:config>
    <!--
        配置切入点表达式:指定哪些包中的类,要使用事务
        expression:切入点表达式,指定哪些类要使用事务,aspectj会创建代理对象
    -->
    <aop:pointcut id="servicePt" expression="execution(* *..service..*.*(..))"/>

    <!--配置增强器:关联advice和pointcut-->
    <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);
    }
}
  • 8
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值