Spring学习笔记

Spring学习笔记

1.Spring概述

1.Spring是一个轻量级的JavaEE框架

2.它是为了解决企业应用开发的复杂性而创建的

3.Spring有两个核心部分:IoC和Aop

  1. IoC:控制反转,把创建对象的过程交给Spring来进行管理
  2. Aop:面向切面,在不修改源码的情况下进行功能增强

4.Spring的特点

  • 1)轻量

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

    • Spring框架运行占用的资源少,运行效率高。

    • Spring框架可以独立运行,不依赖其他jar包。

  • 2)方便解耦

    • Spring提供了IoC控制反转,由容器来管理对象和对象之间的依赖关系。
    • 原来在程序代码中创建对象,现在用容器来完成对象的创建,不需要修改代码。对象之间解耦和。
  • 3)AOP编程的支持

    • Spring提供了Aop面向切面的支持,可以在不修改源码的情况下进行功能增强
  • 4)方便整合各种框架

    • Spring提供了各种优秀框架如Mybatis等的直接支持,同时简化框架的使用。
    • Spring就像插座一样,其他框架就像插头,可以很容易地组合到一起,也可以很容易地移除。
  • 5)方便程序测试

  • 6)方便进行事务操作

  • 7)降低API开发难度

5.本文使用的Spring版本是5.2.6

6.Spring体系结构

2.入门案例

创建一个普通java类,在这个类中编写一个方法。

演示在不手动new这个类的对象的情况下,使用Spring得到这个类的对象并调用其方法。

2.1获取Spring

方式1:从官网下载jar包

地址:

http://repo.spring.io/release/org/springframework/spring/

由于是外网,可能进的比较慢。

方式2:添加依赖

<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.6.RELEASE</version>
</dependency>

2.2创建一个普通maven项目

1.创建父工程

创建好后删除src目录。

2.在pom.xml中添加spring依赖以及其他基本设置

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.tsccg</groupId>
  <artifactId>spring-project</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
    <!--单元测试依赖-->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
    <!--spring依赖-->
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.2.6.RELEASE</version>
    </dependency>
  </dependencies>

</project>

3.创建子module

2.3创建一个普通java类

在/main/java目录下新建一个普通java类User,编写一个方法doSome

package com.tsccg.service;

/**
 * @Author: TSCCG
 * @Date: 2021/09/13 18:19
 */
public class User {
    public void doSome(){
        System.out.println("执行doSome方法");
    }
}

2.4创建Spring配置文件,配置创建对象

1.在/main/resources目录下创建一个Spring配置文件

2.在配置文件的beans标签内,添加bean标签

在bean标签里添加id和class两个属性

  1. id:自定义的名称,作为唯一标识
  2. class:目标类的全限定名
<?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">
    <!--配置User对象的创建-->
    <bean id="user" class="com.tsccg.service.User"/>
</beans>

2.5编写测试代码

在/test/java目录下新建一个测试类TestUser,编写测试方法

public class TestUser {
    /**
     * 使用spring获取User对象,调用其方法
     */
    @Test
    public void testDoSome() {
        //1.加载配置文件
        String config = "beans.xml";//beans.xml所在路径
        ApplicationContext ac = new ClassPathXmlApplicationContext(config);
        //2.获取配置创建的对象,强转为User类型
		//User user = ac.getBean("user",User.class);
        User user = (User)ac.getBean("user");//这里需要输入配置文件里bean的id值
        System.out.println(user);
        //调用对象的方法
        user.doSome();
    }
}

测试结果:

com.tsccg.service.User@32d2fa64
执行doSome方法

3.IoC控制反转

3.1什么是IoC

​ 控制反转(Inversion of Control,简称IoC):是面向对象编程中的一种设计原则。是指将传统上由程序代码直接操控的对象调用权交给容器,通过容器来实现对象的装配和管理。

​ IoC的实现:当前最常见的方式叫做依赖注入(Dependency Injection,简称DI),程序代码不做定位查询,这些工作由容器自行完成。

依赖注入是指程序运行过程中,若需要调用另一个对象协助时,无需在代码中创建被调用者,而是依赖于外部容器,由外部容器创建后传递给程序。

​ Spring的依赖注入对调用者和被调用者几乎没有任何要求,完全支持对象之间依赖关系的管理。

​ Spring容器是一个对象工厂,负责创建、管理所有的java对象,这些java对象被称为Bean。Spring容器管理着Bean之间的依赖关系。

使用IoC可以降低对象之间的耦合度。

通俗地讲:

  1. IoC就是把对象的创建和对象之间的调用过程,都交给Spring来管理。
  2. 使用IoC的目的:就是为了降低耦合度。

上面的入门案例就是使用IoC来实现的。

3.2IoC底层原理

IoC底层主要使用了xml解析、工厂模式、反射三种技术。

下面画图演示:

现在有两个类,UserService和User。

我想在UserService的execute()方法里调用User的doSome()方法。实现方式如下:

第一种方式:在execute()方法里new对象

在UserService类的execute()方法里new一个User对象,然后用这个对象调用doSome()方法

这种方式最为简单,但使得UserService类和User类紧紧关联在了一起,耦合度很高。

什么是耦合?

  • 当一个事物改变了,另一个事物也要随之改变。
  • 两个事物之间依赖关系的强弱就是耦合度。
  • 理论上耦合度不会消失,我们只能尽可能地降低。

如果耦合度过高,比如上面的关联方式,就会出现如下情况:

  1. 当User类的路径变了,UserService类也要跟着变;
  2. 当User类的doSome方法变了,UserService类也要跟着变。
  3. 假如说现在有1000个UserService类,当User类改变时,这1000个UserService类都要跟着变。牵一发而动全身。

我们开发时追求的是低耦合高内聚,高耦合不利于程序拓展。

我们可以使用一些技术来降低耦合度。

第二种方式:使用工厂模式

工厂模式就是创建一个第三方类,实现目标类对象的创建,然后将对象交给调用它的地方。

工厂模式的目的就是为了降低耦合度。

工厂模式虽然降低了UserService类和User类之间的耦合度,但是仍没有降到最低。

为了进一步降低耦合度,我们需要使用到IoC,控制反转。

第三种方式:IoC

IoC就是在工厂模式的基础上,将创建对象的过程进一步解耦:

  1. 首先,创建一个xml配置文件,在该文件里的class属性里写明目标类的全限定名,也就是目标类编译后的class文件所在位置。
  2. 然后在工厂类里通过xml解析读取配置文件里的class属性值,得到User类的全限定名。
  3. 最后通过反射创建该类的对象。

IoC将对象间的耦合度大大降低了,假如说现在User类的路径变了,我们只需要在配置文件里修改路径即可,不需要更改源代码。

3.3容器接口及其实现类

3.3.1ApplicationContext接口(容器)

ApplicationContext用于加载Spring的配置文件,在程序中充当"容器"的角色。

ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml");
User user = (User)ac.getBean("user");

3.3.2ApplicationContext接口的实现类

ApplicationContext接口有两个实现类

这两个实现类的区别是:

1)当使用FileSystemXmlApplicationContext类时,需要传入xml配置文件编译后的绝对路径

ApplicationContext ac = new FileSystemXmlApplicationContext("D:\\code\\常用框架\\04-Spring\\spring-project\\spring-01\\target\\classes\\beans.xml");
User user = (User)ac.getBean("user");

2)当使用ClassPathXmlApplicationContext类时,需要传入xml配置文件编译后的相对路径

ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml");
User user = (User)ac.getBean("user");

3.3.3Application容器中对象的装配时机

ApplicationContext容器,会在容器对象初始化时,将其中所有的对象一次性装配好。以后代码中如果要用这些对象,只需要从内存中直接获取即可。执行效率高,但占用内存。

也就是说,在加载配置文件的时候就会创建配置文件里所有的对象。

//1.加载配置文件
String config = "beans.xml";//beans.xml所在路径
//获取容器:此时容器中所有对象都创建好了
ApplicationContext ac = new ClassPathXmlApplicationContext(config);

3.4依赖注入(DI)

为了实现在Spring的配置文件中,给java对象的属性赋值,需要使用DI,依赖注入。

DI是基于创建对象来实现的。

DI有两种实现:

  1. 基于xml的DI实现:在Spring的配置文件中,使用标签和属性来完成属性赋值
  2. 基于注解的DI实现:使用Spring中的注解,完成属性赋值

3.5基于XML的DI

3.5.1DI的语法分类

1)set注入(设值注入):Spring调用类的set方法,在set方法中可以实现属性的赋值

2)构造注入:Spring调用类的有参构造方法创建对象,在构造方法中完成赋值

3.5.2简单类型的set注入

简单类型的set注入格式:

简单类型:Spring中规定java的基本数据类型和String都是简单类型

<!-- 一个Bean标签代表一个对象 -->
<bean id="自定义标识" class="目标类全限定名">
    <!-- 一个property只能给一个属性赋值 -->
	<property name="简单类型形参名1" value="此属性值"></property>
    <property name="简单类型形参名2" value="此属性值"></property>
</bean>

1.创建一个普通java类Student,设置两个属性,name和age,编写这两个属性的set方法以及重写toString方法。

package com.tsccg.set;

/**
 * @Author: TSCCG
 * @Date: 2021/09/14 22:51
 */
public class Student {
    //声明简单类型
    private String name;
    private Integer age;

    public void setName(String name) {
        this.name = name;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

2.在/main/resources/set目录下创建xml配置文件beans.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 声明Student对象 -->
    <bean id="student" class="com.tsccg.set.Student">
        <property name="name" value="张三"/>
        <property name="age" value="20"/>
    </bean>
</beans>

3.编写测试代码

在/test/java目录下新建测试类TestStudent

import com.tsccg.set.Student;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * @Author: TSCCG
 * @Date: 2021/09/14 22:54
 */
public class TestStudent {
    /**
     * 测试set注入
     */
    @Test
    public void testSet() {
        String config = "set/beans.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(config);
        Student student = (Student)ac.getBean("student");
        System.out.println(student);
    }
}

测试结果:

Student{name='张三', age=20}

需要注意的是:set注入执行的是目标类的set方法

  1. 如果没有set方法,程序会报错
  2. 如果set方法里没有赋值操作,得到的对象就没有属性值
<bean id="student" class="com.tsccg.set.Student">
    <property name="name" value="张三"/><!-- setName("张三") -->
    <property name="age" value="20"/><!-- setAge(20) -->
</bean>

3.5.3引用类型的set注入

引用类型的set注入格式:

假如现在需要在目标类中声明另一个类的引用

<!-- 一个Bean标签代表一个对象 -->
<!--创建目标类对象-->
<bean id="目标对象自定义标识" class="目标类全限定名">
	<property name="简单类型形参名" value="属性值"></property>
    <property name="引用类型形参名" ref="另一个类的bean标签的id值"></property>
</bean>

<!--创建引用类的对象-->
<bean id="自定义标识" class="引用类型的全限定名">
	<property name="简单类型形参名" value="属性值"></property>
</bean>

1.创建一个普通java类School,有两个属性name和address

package com.tsccg.set2;

/**
 * @Author: TSCCG
 * @Date: 2021/09/15 17:45
 */
public class School {
    private String name;
    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 + '\'' +
                '}';
    }
}

2.创建一个普通java对象StudentPlus,在其中声明一个School类型引用,并为其编写set方法

package com.tsccg.set2;

/**
 * @Author: TSCCG
 * @Date: 2021/09/14 22:51
 */
public class StudentPlus {
    //声明简单类型
    private String name;
    private Integer age;
    //声明一个引用类型
    private School school;
    
    public void setName(String name) {
        this.name = name;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
    //给引用类型编写一个set方法
    public void setSchool(School school) {
        this.school = school;
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", school=" + school +
                '}';
    }
}

3.创建Beans.xml,配置StudentPlus对象的创建和属性的注入

<?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">
    <!--创建StudentPlus对象-->
    <bean id="myStudent" class="com.tsccg.set2.StudentPlus">
        <property name="name" value="李四"/><!-- setName("张三") -->
        <property name="age" value="26"/><!-- setAge(20) -->
        <!-- 为School类型的引用赋值 -->
        <property name="school" ref="mySchool"/><!-- setSchool(mySchool) -->
    </bean>
    <!-- 创建School对象 -->
    <bean id="mySchool" class="com.tsccg.set2.School">
        <property name="name" value="南阳理工"/><!--setName("南阳理工")-->
        <property name="address" value="南阳"/><!--setAddress("南阳")-->
    </bean>
</beans>

4.编写测试代码

/**
 * 测试引用类型set注入
 */
@Test
public void testSet2() {
    ApplicationContext ac = new ClassPathXmlApplicationContext("set2/beans.xml");
    StudentPlus myStudent = (StudentPlus) ac.getBean("myStudent");
    System.out.println(myStudent);
}

测试结果:

Student{name='李四', age=26, school=School{name='南阳理工', address='南阳'}}

3.5.4构造注入

构造注入:Spring调用目标类的有参构造方法,在创建对象的同时,在构造方法中给属性赋值。

构造注入需要使用<constructor-arg >标签

<constructor-arg >标签属性:

  1. name:表示构造方法的形参名
  2. index:表示构造方法的参数位置,从左到右,以0开始
  3. value:给简单类型赋值
  4. ref:给引用类型赋值

一般使用name属性,因为name属性可读性更高。

格式:

<!--使用name属性-->
<bean id="studentPro1" class="com.tsccg.construct.StudentPro">
    <constructor-arg name="简单类型形参名" value="属性值"/>
    <constructor-arg name="引用类型形参名" ref="bean的id值"/>
</bean>
<!--使用index属性-->
<bean id="studentPro2" class="com.tsccg.construct.StudentPro">
    <constructor-arg index="1" ref="bean的id值"/>
    <constructor-arg index="0" value="属性值"/>
</bean>
<!--当不使用name或index时,默认按index,赋值顺序必须按照构造方法中形参列表的顺序-->
<bean id="studentPro3" class="com.tsccg.construct.StudentPro">
    <constructor-arg  value="属性值"/>
    <constructor-arg  ref="bean的id值"/>
</bean>

演示:

1.创建一个普通java类StudentPro和SchoolPro,分别编写无参构造方法和有参构造方法,并重写toString方法。

package com.tsccg.construct;

/**
 * @Author: TSCCG
 * @Date: 2021/09/14 22:51
 * StudentPro类
 */
public class StudentPro {
    private String name;
    private Integer age;
    private SchoolPro schoolPro;
    /**
     * 无参构造
     */
    public StudentPro() {
        System.out.println("StudentPro----执行无参构造方法----");
    }
    /**
     * 有参构造
     */
    public StudentPro(String myName, Integer myAge, SchoolPro mySchoolPro) {
        System.out.println("StudentPro----执行有参构造方法----");
        this.name = myName;
        this.age = myAge;
        this.schoolPro = mySchoolPro;
    }
    @Override
    public String toString() {
        return "StudentPro{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", schoolPro=" + schoolPro +
                '}';
    }
}

/**
 * SchoolPro类
 */
class SchoolPro {
    private String name;
    private String address;

    /**
     * 无参构造方法
     */
    public SchoolPro() {
        System.out.println("SchoolPro----执行无参构造方法----");
    }
    /**
     * 有参构造方法
     */
    public SchoolPro(String myName, String myAddress) {
        System.out.println("SchoolPro----执行有参构造方法----");
        this.name = myName;
        this.address = myAddress;
    }
    @Override
    public String toString() {
        return "School{" +
                "name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}

2.创建bean.xml,配置创建对象

<constructor-arg >标签属性:

  1. name:表示构造方法的形参名
  2. index:表示构造方法的参数位置,从左到右,以0开始
  3. value:给简单类型赋值
  4. ref:给引用类型赋值

分别使用name属性和index属性进行构造注入

<?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">
    <!--创建StudentPlus对象-->
    <!-- 使用name -->
    <bean id="studentPro1" class="com.tsccg.construct.StudentPro">
        <constructor-arg name="myName" value="王五"/>
        <constructor-arg name="myAge" value="28"/>
        <constructor-arg name="mySchoolPro" ref="schoolPro"/>
    </bean>
    <!-- 使用index -->
    <bean id="studentPro2" class="com.tsccg.construct.StudentPro">
        <constructor-arg index="0" value="王五五"/>
        <constructor-arg index="2" ref="schoolPro"/>
        <constructor-arg index="1" value="30"/>
    </bean>
    <!-- 省略index 
 		如果不使用name和index,默认按index,必须按照形参顺序赋值
	-->
    <bean id="studentPro3" class="com.tsccg.construct.StudentPro">
        <constructor-arg value="王六六"/>
        <constructor-arg value="35"/>
        <constructor-arg ref="schoolPro"/>
    </bean>
    <!-- 创建School对象 -->
    <bean id="schoolPro" class="com.tsccg.construct.SchoolPro">
        <constructor-arg name="myName" value="清华大学"/>
        <constructor-arg name="myAddress" value="北京"/>
    </bean>
</beans>

3.编写测试代码

/**
 * 测试构造注入
 */
@Test
public void testCon1() {
    String config = "con1/beans.xml";
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);
    //使用name属性
    StudentPro studentPro1 = (StudentPro)ac.getBean("studentPro1");
    System.out.println("使用name:" + studentPro1);
    //使用index属性
    StudentPro studentPro2 = (StudentPro)ac.getBean("studentPro2");
    System.out.println("使用index:" + studentPro2);
    //不使用name和index属性
    StudentPro studentPro3 = (StudentPro)ac.getBean("studentPro3");
    System.out.println("不使用name和index:" + studentPro3);
}

测试结果:

SchoolPro----执行有参构造方法----
StudentPro----执行有参构造方法----
StudentPro----执行有参构造方法----
StudentPro----执行有参构造方法----//使用ApplicationContext,在加载配置文件时会创建所有对象
使用name:StudentPro{name='王五', age=28, schoolPro=School{name='清华大学', address='北京'}}
使用index:StudentPro{name='王五五', age=30, schoolPro=School{name='清华大学', address='北京'}}
不使用name和index:StudentPro{name='王六六', age=35, schoolPro=School{name='清华大学', address='北京'}}

3.5.5引用类型的自动注入

引用类型的自动注入:Spring框架根据某些规则可以自动给引用类型赋值。不需要手动给引用类型赋值。

使用的规则常用的是byName、byType

1.byName(按名称注入):java类中引用类型的属性名和配置文件中<bean>标签的id值一样,且数据类型是一致的,这样的bean,spring能够赋值给引用类型。

语法:

<bean id="xxx" class="ooo" autowire="byName">
	给简单类型属性赋值(必须使用set注入)
</bean>

2.byType(按类型)注入:java类中引用类型的数据类型和配置文件<bean >标签的class属性是同源关系的,这样的bean,spring能够赋值给引用类型。

同源就是一类的关系:

  1. java类中引用类型的数据类型和bean的class的值是一样的。
  2. java类中引用类型的数据类型和bean的class的值是父子类关系的。
  3. java类中引用类型的数据类型和bean的class的值是接口与实现类关系的。

语法:

<bean id="xxx" class="ooo" autowire="byType">
    给简单类型赋值(必须使用set注入)
</bean>
3.5.5.1演示自动注入ByName

1.创建一个Student类和一个School类,在Student类里声明一个School类型对象school,编写set方法

package com.tsccg.autowair1;

/**
 * @Author: TSCCG
 * @Date: 2021/09/16 13:49
 */
public class Student {
    private  String name;
    private  Integer age;
    School school;

    public Student() {
        System.out.println("创建Student对象");
    }

    public void setName(String name) {
        this.name = name;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
    public void setSchool(School school) {
        System.out.println("school:" + school);
        this.school = school;
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", school=" + school +
                '}';
    }
}

class School {
    private  String name;
    private  String address;

    public School() {
        System.out.println("创建School对象");
    }
    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 + '\'' +
                '}';
    }
}

2.创建beans.xml,使用byName方式完成school的自动注入

使用byName后,就不需要在Student的bean标签中引入School的bean。

当程序开始执行后,Spring会遍历所有的bean标签的id属性,找到和Student类里声明的School类型引用名相同的值,将其注入到Student对象的School类型引用里。

<?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">
    <!--创建Student对象-->
    <bean id="myStudent" class="com.tsccg.autowair1.Student" autowire="byName"><!--自动注入byName-->
        <property name="name" value="赵六"/>
        <property name="age" value="19"/>
<!--        <property name="school" ref="school"/>--><!--不需要在这里引入School的bean-->
    </bean>
    <!-- 创建School对象 -->
    <bean id="school" class="com.tsccg.autowair1.School">
        <property name="name" value="小清华"/>
        <property name="address" value="南阳"/>
    </bean>
</beans>

3.编写测试代码

/**
 * 测试自动注入byName
 */
@Test
public void testAuto1() {
    String config = "auto1/beans.xml";
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);
    Student student = (Student)ac.getBean("myStudent");
    System.out.println(student);
}

测试结果:

创建Student对象
创建School对象
school:School{name='小清华', address='南阳'}
Student{name='赵六', age=19, school=School{name='小清华', address='南阳'}}
3.5.5.2演示自动注入ByType

1.还是使用上面的Student和School类,然后创建一个普通java类PrimarySchool,继承School类

package com.tsccg.autowair2;

/**
 * @Author: TSCCG
 * @Date: 2021/09/16 13:49
 */
public class Student {
    private  String name;
    private  Integer age;
    School school;

    public Student() {
        System.out.println("创建Student对象");
    }
    public void setName(String name) {
        this.name = name;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
    public void setSchool(School school) {
        System.out.println("school:" + school);
        this.school = school;
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", school=" + school +
                '}';
    }
}

/**
 * 定义School类
 */
class School {
    private String name;
    private String address;
    public School() {
        System.out.println("创建School对象");
    }
    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 + '\'' +
                '}';
    }
}
/**
 * 定义PrimarySchool,继承School
 */
class PrimarySchool extends School {
    private String name;
    private String address;
    public PrimarySchool() {
        System.out.println("创建PrimarySchool对象");
    }
    @Override
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public void setAddress(String address) {
        this.address = address;
    }
    @Override
    public String toString() {
        return "PrimarySchool{" +
                "name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}

2.创建beans.xml,使用byType完成自动注入引用类型属性

<?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">
    <!--创建Student对象-->
    <bean id="myStudent" class="com.tsccg.autowair2.Student" autowire="byType"><!--自动注入byType-->
        <property name="name" value="赵六"/>
        <property name="age" value="22"/>
    </bean>
    <!-- 创建School对象 -->
    <bean id="school" class="com.tsccg.autowair2.School">
        <property name="name" value="家里蹲大学"/>
        <property name="address" value="地球"/>
    </bean>
</beans>

3.编写测试代码

/**
 * 测试自动注入byType
 */
@Test
public void testAutoByType() {
    String config = "auto2/beans.xml";
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);
    Student student = (Student)ac.getBean("myStudent");
    System.out.println(student);
}

测试结果:

创建Student对象
创建School对象
school:School{name='家里蹲大学', address='地球'}
Student{name='赵六', age=22, school=School{name='家里蹲大学', address='地球'}}

4.在beans.xml配置文件里,将School替换为PrimarySchool,继续测试

<?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">
    <!--创建Student对象-->
    <bean id="myStudent" class="com.tsccg.autowair2.Student" autowire="byType"><!--自动注入byType-->
        <property name="name" value="赵六"/>
        <property name="age" value="22"/>
    </bean>
    <!-- 创建School对象 -->
<!--    <bean id="school" class="com.tsccg.autowair2.School">-->
<!--        <property name="name" value="家里蹲大学"/>-->
<!--        <property name="address" value="地球"/>-->
<!--    </bean>-->
    <!-- 创建PrimarySchool对象 -->
    <bean id="primarySchool" class="com.tsccg.autowair2.PrimarySchool">
        <property name="name" value="家里蹲小学"/>
        <property name="address" value="地球"/>
    </bean>
</beans>

测试结果:

创建Student对象
创建School对象
创建PrimarySchool对象
school:PrimarySchool{name='家里蹲小学', address='地球'}
Student{name='赵六', age=22, school=PrimarySchool{name='家里蹲小学', address='地球'}}

PrimarySchool是School的子类,属于同源关系。当在Student类里声明一个School类型属性时,假如Spring在配置文件没找到School类型的bean标签,就会找School的同源类完成注入。

注意:在使用byType时,xml配置文件里声明的bean只能有一个符合条件的,不然会报错。也就是说,同源类的bean不能同时出现在同一个配置文件里。

3.5.6多个配置文件

1.多个配置文件的优势
  1. 多个配置文件比一个配置文件效率高:
    1. 假如项目中有几百个类。
    2. 如果全部写入一个配置文件里,那么这个文件就会很大,执行效率很慢。
    3. 如果将所有类划分到多个配置文件里,那么单个配置文件就会很小,执行单个配置文件的效率就会很高。
  2. 多个配置文件可以避免多人开发带来的冲突:
    1. 假如项目中有多个模块,每个模块都由不同的人负责开发。
    2. 如果将所有模块的类都放到一个配置文件里,那么可能造成代码的冲突。
    3. 如果一个模块一个配置文件,即能避免冲突,又能方便操作。
2.多个配置文件的分配方式
  1. 按功能模块:一个模块一个配置文件
  2. 按类的功能:
    1. 数据库相关的配置放到一个配置文件
    2. 事务相关的功能放到一个配置文件
    3. service功能相关的放到一个配置文件等
3.多配置文件用法

以Student类和School类为例,分别为其创建一个配置文件,演示如何使用。

包含关系的配置文件

1)在main/resources目录下创建一个文件夹xmls,在其中新建三个配置文件:spring-student.xml、spring-school.xml、spring-total.xml。

  1. spring-student.xml:只声明Student对象,在其bean标签中,使用byType完成School类型引用的自动注入

    <?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">
        <!--创建Student对象-->
        <bean id="myStudent" class="com.tsccg.xmls.Student" autowire="byType">
            <property name="name" value="铁柱"/>
            <property name="age" value="24"/>
        </bean>
    </beans>
    
  2. spring-school.xml:只声明School对象

    <?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">
        <!-- 创建School对象 -->
        <bean id="school" class="com.tsccg.xmls.School">
            <property name="name" value="家里站大学"/>
            <property name="address" value="河南"/>
        </bean>
    </beans>
    
  3. spring-total.xml:主配置文件,在其中引入其它配置文件,主配置文件里一般不声明对象

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
        <!-- 
    		resource:其他配置文件的路径
    		classpath:告诉spring到项目编译后的位置去找配置文件 -->
        <import resource="classpath:xmls/spring-student.xml"/>
        <import resource="classpath:xmls/spring-school.xml"/>
    </beans>
    

2)测试

/**
 * 测试多配置文件使用
 */
@Test
public void testAutoByType() {
    //加载的是主配置文件
    String config = "xmls/spring-total.xml";
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);
    Student student = (Student)ac.getBean("myStudent");
    System.out.println(student);
}

测试结果:

school:School{name='家里站大学', address='河南'}
Student{name='铁柱', age=24, school=School{name='家里站大学', address='河南'}}

3)在包含关系的配置文件中,可以使用通配符(*:表示任意字符)引入一个目录下包含指定字符的所有配置文件

在主配置文件中进行如下修改:

<?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:xmls/spring-student.xml"/>-->
<!--    <import resource="classpath:xmls/spring-school.xml"/>-->
    <import resource="classpath:xmls/spring-*.xml"/><!--加载xmls目录下的所有以spring-开头的xml文件-->
</beans>

当然,在主配置文件中使用通配符后,主配置文件名不能包含指定字符,不然会将主配置文件一并加载,造成死循环。将主配置文件名改为:total.xml

重新进行测试,测试结果:

school:School{name='家里站大学', address='河南'}
Student{name='铁柱', age=24, school=School{name='家里站大学', address='河南'}}

注意:通配符加载配置文件时,配置文件上面至少要有一级目录,不然无法加载。也就是说在resources目录下,必须新建一个文件夹,在该文件夹里存放配置文件。

3.6基于注解的DI

通过注解完成java对象的创建和属性赋值。

3.6.1注解的使用步骤

1.添加spirng-context依赖:在加入spring-context依赖后,maven会将spring-aop的依赖一并添加,使用注解完成DI必须使用spring-aop

2.创建类,在类中加入spring的注解(多个不同功能的注解)

  1. @Component
  2. @Respotoy
  3. @Service
  4. @Controller
  5. @Value
  6. @Autowired
  7. @Resource

3.创建spring配置文件,在spring的配置文件中,加入一个组件扫描器的标签,指明注解在你的项目中的位置。

4.使用注解创建对象,创建容器ApplicationContext

3.6.2定义Bean的注解

@Component:创建对象,相当于<bean >的功能

  1. value属性:该注解的value属性用于指定该bean的id值。 vlaue的值是唯一的,创建的对象在整个Spring中独一个。
  2. 使用位置:类的上面。
  3. 使用格式:
    1. @Component(value="自定义对象名"):相当于<bean id="自定义对象名 class="类的全限定名"/>
    2. @Component("自定义对象名"):省略value,最常用。
    3. @Component:不指定对象名,由spring提供默认名。(类名首字母小写)

和@Component功能一致,可以创建对象的注解还有三个:

  1. @Repository:用于持久层类
    1. 放在dao的实现类上面,表示创建dao对象
    2. 额外功能:dao对象是用于数据访问的,具有访问数据库的功能
  2. @Service:用于业务层类
    1. 放在service的实现类上面,表示创建service对象
    2. 额外功能:service对象是做业务处理的,具有处理事务等功能
  3. @Controller:用于控制器类
    1. 放在控制器(处理器)类的上面,表示创建控制器对象
    2. 额外功能:控制器对象能够接收用户提交的参数,显示请求的处理结果

以上三个注解的使用语法和@Component一样。

注意:虽然这三个注解和@Component都可以创建对象,但是这三个注解都有额外的功能,不能一概而论。

@Repository,@Service,@Controller的作用是给项目中的对象分层。

演示:

1.新建一个子模块spring-03-DI-anno,由于父模块的pom中已经添加了spring-context依赖,所以不用再加了。

2.在main/java下创建com.tsccg.anno01.Student,在类名定义处添加@Component注解

package com.tsccg.anno01;

import org.springframework.stereotype.Component;

/**
 * @Author: TSCCG
 * @Date: 2021/09/17 15:04
 */
@Component("myStudent")//使用@Component注解
public class Student {
    private String name;
    private Integer age;

    public Student() {
        System.out.println("调用无参构造");
    }
    public Student(String name, Integer age) {
        System.out.println("调用有参构造");
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

3.创建Spring配置文件,在配置文件里添加组件扫描器的标签,指明注解在你的项目中的位置

在main/resources目录下新建applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns: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),组件就是java对象 
		base-package:指定注解在项目中所处包名
		component-scan工作方式:Spring会扫描遍历base-package指定的包及其子包中的所有类,找到类中的注解,按照注解的功能创建对象,或者给属性赋值。
	-->
    <context:component-scan  base-package="com.tsccg.anno01"/>
</beans>

加入了component-scan标签后,配置文件的变化:

  1. 加入一个新的约束文件spring-context.xsd
  2. 给这个新的约束文件起个命名空间的名称

4.使用注解创建对象

编写测试代码:

/**
 * 测试Component
 */
@Test
public void test01() {
    String config = "applicationContext.xml";
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);
    Student myStudent = ac.getBean("myStudent", Student.class);
    System.out.println(myStudent);
}

测试结果:

调用无参构造
Student{name='null', age=null}

由结果来看,使用注解创建对象时,调用的是类的无参构造方法。

3.6.3扫描多个包的三种方式

<!--指定多个包的三种方式-->
<!--  第一种方式  -->
<context:component-scan base-package="com.tsccg.anno01"/>
<context:component-scan base-package="com.tsccg.anno02"/>
<!--  第二种方式  -->
<context:component-scan base-package="com.tsccg.anno01;com.tsccg.anno02"/>
<!--  第三种方式  -->
<context:component-scan base-package="com.tsccg"/>

3.6.5@Value:简单类型属性注入

@Value:给简单类型的属性赋值

  1. 属性:value。是String类型的,表示简单类型的属性值,可省略不写。
  2. 使用位置:
    • 1)在属性定义的上面,不需要写set方法,推荐使用
    • 2)在set方法上面

实际使用:

package com.tsccg.anno01;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * @Author: TSCCG
 * @Date: 2021/09/17 15:04
 */
@Component("myStudent")//创建对象
public class Student {
    @Value("张三")//给name属性赋值
    private String name;
    @Value("25")//给age属性赋值
    private Integer age;

    public Student() {
        System.out.println("调用无参构造");
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

测试结果:

调用无参构造
Student{name='张三', age=25}

3.6.6@Autowired:引用类型属性注入(byType)

@Autowired:spring框架提供的注解,实现给引用类型的属性赋值。

  1. spring中通过注解给引用类型赋值,使用的是自动注入原理,支持byName和byType。
  2. @Autowired默认使用的是byType自动注入。
  3. 属性:required,是一个boolean类型的,默认为true
    1. true:表示当引用类型属性赋值失败时,程序报错,并终止执行
    2. false:表示当引用类型属性赋值失败时,程序仍正常执行,引用类型属性值为null
    3. 一般推荐使用true,便于暴露问题

使用位置:

  1. 在引用类型属性定义的上面,不需要写set方法,推荐使用
  2. 在set方法上面

实例演示:

1.创建一个School类,使用注解创建其对象以及实现简单类型属性赋值

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

    public School() {
        System.out.println("调用School的无参构造");
    }
    @Override
    public String toString() {
        return "School{" +
                "name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}

2.创建一个Student类,在Student类里声明一个School类型属性,使用注解创建其对象以及实现引用类型属性赋值

package com.tsccg.anno02;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * @Author: TSCCG
 * @Date: 2021/09/17 15:04
 */
@Component("myStudent")//创建对象
public class Student {
    @Value("张三")//给name属性赋值
    private String name;
    @Value("25")//给age属性赋值
    private Integer age;
    
    @Autowired//给引用类型属性赋值,默认byType
    private School school;//声明引用类型属性

    public Student() {
        System.out.println("调用Student的无参构造");
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", school=" + school +
                '}';
    }
}

3.创建spring配置文件applicationContext2.xml,告诉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">

    <context:component-scan  base-package="com.tsccg.anno02"/>
</beans>

4.编写测试代码

/**
 * 测试用注解给引用类型赋值
 */
@Test
public void test01() {
    String config = "applicationContext2.xml";
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);
    Student myStudent = ac.getBean("myStudent", Student.class);
    System.out.println(myStudent);
}

测试结果:

调用School的无参构造
调用Student的无参构造
Student{name='张三', age=25, school=School{name='北京大学', address='北京'}}

3.6.7@Autowired与@Qualifier:引用类型属性注入(byName)

如果要使用byName方式给引用类型赋值,那么就需要使用到@Qualifier注解。

  1. 在属性上面加入@Autowired
  2. 在属性上面再加入@Qualifier(value="bean的id值"):表示使用指定名称的bean对象完成赋值
    1. @Qualifier的 value 属性用于指定要匹配的bean的id值

如:

Student类:

@Component("myStudent")//创建对象
public class Student {
    @Value("张三")
    private String name;
    @Value("25")
    private Integer age;
    //使用自动注入给引用类型属性赋值
    @Autowired
    @Qualifier("mySchool")//设置为byName
    private School school;

School类:

@Component("mySchool")//mySchool:创建的对象名
public class School {
    @Value("北京大学")
    private String name;
    @Value("北京")
    private String address;

3.6.8JDK注解@Resource自动注入

@Resource是来自JDK中的注解,Spring框架提供了对这个注解的功能支持。

可以使用它给引用类型属性赋值,使用的同样是自动注入原理,同样支持byName和byType,默认是byName。

在无法通过byName方式找到bean时,会使用byType来找。

@Component("myStudent")//创建对象
public class Student {
    @Value("李四")
    private String name;
    @Value("30")
    private Integer age;
    //给引用类型属性赋值
	@Resource//默认byName,当使用byName方式找不到bean时,会转而使用byType找
    //@Resource(name="mySchool")//设置为只使用byName,找不到就报错
    private School school;

3.6.9注解与XML的对比

注解的优点:

  1. 方便
  2. 直观
  3. 高效:代码量少,没有配置文件的书写那么复杂

注解的缺点:注解是写到java代码里的,耦合度高,修改后需要重新编译代码。

xml的优点:

  1. 配置信息与代码是分离的,耦合度低
  2. 在xml中修改数据,无需重新编译代码,只需要重启服务器即可加载新的配置

xml的缺点:编写麻烦,效率低,开发大型项目时过于复杂。

总结:

  1. 当不需要经常修改配置信息时,使用注解
  2. 当需要经常修改配置信息时,使用xml
  3. 在实际开发中,能用注解就用注解。

4.AOP面向切面编程

4.1动态代理

动态代理就是为了在不修改目标类的基础上,实现调用目标方法和功能增强。

实现方式:

1.JDK动态代理:使用jdk中的Proxy,Method,InvocationHandler创建代理对象。

jdk动态代理要求目标类必须实现接口。

2.CGLIB动态代理:使用第三方的工具库,创建代理对象。原理是继承,通过继承目标类,创建其子类对象,这个子类对象就是代理对象,在代理对象中实现调用目标方法以及功能增强。

CGLIB要求目标类必须是可继承的,不能由final修饰,方法也不能是final的。

4.2JDK动态代理例子

现在有一个service接口,该接口有两个业务方法。(接口方法也称为主业务逻辑)

接口:

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

编写它的一个实现类,在该实现类中实现接口中的业务方法

实现类:

public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome() {
        System.out.println("执行业务方法doSome");
    }
    @Override
    public void doOther() {
        System.out.println("执行业务方法doOther");
    }
}

现在需要在两个业务方法前添加执行日期,在两个业务方法执行后添加事务提交处理。

执行日期和事务提交处理属于非业务方法(非业务方法也称为交叉业务逻辑)

我们可以将这些非业务方法放到一个工具类里实现,然后再在实现类里调用。

工具类:

public class SomeServiceUtil {
    public static void doLog() {
        System.out.println("非业务功能-->方法开始执行时间为:" + new Date());
    }
    public static void doTrans() {
        System.out.println("非业务功能-->事务提交");
    }
}

实现类:

public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome() {
        SomeServiceUtil.doLog();//调用工具类,显示执行日期
        System.out.println("执行业务方法doSome");
        SomeServiceUtil.doTrans();//提交事务
    }
    @Override
    public void doOther() {
        SomeServiceUtil.doLog();//显示执行日期
        System.out.println("执行业务方法doOther");
        SomeServiceUtil.doTrans();//提交事务
    }
}

测试代码:

@Test
public void testSomeService() {
    SomeService someService = new SomeServiceImpl();
    someService.doSome();
    System.out.println("================================");
    someService.doOther();
}

测试结果:

非业务功能-->方法开始执行时间为:Sat Sep 18 20:51:17 CST 2021
执行业务方法doSome
非业务功能-->业务提交
================================
非业务功能-->方法开始执行时间为:Sat Sep 18 20:51:17 CST 2021
执行业务方法doOther
非业务功能-->业务提交

我们分析以上程序,还是存在弊端:业务功能代码和非业务功能代码深度耦合在一起

当非业务代码较多时,在业务代码中会出现大量的非业务功能代码调用语句,大大影响了业务功能代码的可读性,降低了代码的可维护性,同时也增加了开发难度。
所以,可以采用动态代理方式,在不修改目标业务类源码的前提下,扩展和增强其功能。

service接口实现类:

public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome() {
        System.out.println("执行业务方法doSome");
    }
    @Override
    public void doOther() {
        System.out.println("执行业务方法doOther");
    }
}

编写InvocationHandler接口实现类

public class MyInvocationHandler implements InvocationHandler {
    //定义目标类
    private final Object target;

    public MyInvocationHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = null;
        //得到当前调用的方法名
        String methodName = method.getName();
        //判断当前调用的方法是否是doSome方法,如果是,则实现日志、事务的增强;如果不是,则只执行目标方法
        if ("doSome".equals(methodName)) {
            SomeServiceUtil.doLog();//在方法执行前输出日志
            result = method.invoke(target,args);//执行目标方法,执行target对象的方法
            SomeServiceUtil.doTrans();//在方法执行后,执行提交事务
        } else {
            result = method.invoke(target,args);//执行目标方法
        }
        return result;
    }
}

测试代码:

@Test
public void testSomeService() {
    //创建目标对象
    SomeService target = new SomeServiceImpl();
    InvocationHandler handler = new MyInvocationHandler(target);
    //创建代理对象
    SomeService proxy = (SomeService)Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),handler);
    //通过代理对象执行业务方法,实现日志、事务的增强
    proxy.doSome();
    System.out.println("================================");
    proxy.doOther();
}

测试结果:

非业务功能-->方法开始执行时间为:Sat Sep 18 20:55:36 CST 2021
执行业务方法doSome
非业务功能-->事务提交
================================
执行业务方法doOther

4.3动态代理的作用

  1. 在目标类源代码不改动的情况下,调用目标方法并增强功能
  2. 减少代码的重复
  3. 让开发者更专注于业务逻辑代码
  4. 解耦合,让业务功能代码和日志、事务等非业务功能代码分离

4.4AOP概述

4.4.1AOP基于动态代理

AOP就是面向切面编程,是基于动态代理的,可以使用jdk、cglib两种代理方式。

AOP就是动态代理的规范化,把动态代理的实现步骤、实现方式都定义好了,让开发人员用一种统一的方式去使用动态代理。

4.4.2什么是面向切面编程?

AOP(Aspect Orient Programming),面向切面编程

  1. Aspect:切面,给你的目标类增加的功能。就比如说上面例子中的日志和事务都是切面。
  2. Orient:面向,对着
  3. Programming:编程

好比OOP面向对象编程,面向对象编程就是在分析项目功能时,先考虑可以由哪些类来实现目标功能。

AOP面向切面编程就是分析项目功能时,先找出切面,然后合理地安排切面执行的时间、位置。(时间:在目标方法前还是后;位置:在哪个类、哪个方法上)

4.4.3AOP常见术语

  1. Aspect:切面,表示增强的功能。
    • 是非业务功能代码
    • 常见的切面功能有日志、事务、统计信息、参数检查和权限验证。
  2. JoinPoint:连接点,可以添加切面的具体方法。
    • 通常业务接口中的方法都是连接点
  3. Pointcut:切入点,指声明的一个或多个连接点的集合。
    • 通常切入点指定一组方法。
    • 被final标记的方法是不能作为连接点与切入点的,因为final意味着是最终的,是不能被修改的,不能被增强的。
  4. Target:目标对象,给哪个类的对象增强功能,哪个类的对象就是目标对象
    • 目标对象通常就是包含主业物逻辑的类的对象,其类叫目标类
  5. Advice:通知,表示切面功能执行的时间,也就是在目标方法之前执行,还是目标方法之后执行

4.4.4执行一个切面有三个要素

1.切面的功能代码:切面要实现的功能

在上面的例子中,工具类中的方法就是切面

public static void doLog() {
    System.out.println("非业务功能-->方法开始执行时间为:" + new Date());
}

2.切面的执行位置:使用Pointcut表示切面执行的位置

在上面的例子中,doSome方法就是JoinPoint连接点。又因为例子中只有一个连接点,所以doSome方法又是Pointcut切入点。

if ("doSome".equals(methodName)) {
    SomeServiceUtil.doLog();//执行切面
    result = method.invoke(target,args);//doSome方法----》连接点----》切入点
    SomeServiceUtil.doTrans();//执行切面
} else {
    result = method.invoke(target,args);
}

3.切面的执行时间:使用Advice表示时间,就是说切面执行在目标方法之前,还是目标方法之后

4.5AOP的实现框架:AspectJ

AOP是一种规范,是动态的一种规范化,是一个标准。

AOP的技术实现框架有:Spring、AspectJ

1.Spring:在内部实现了AOP规范,能处理AOP的工作

  • 但是AspectJ也实现了AOP的功能,且实现方式更为简捷,使用更加方便。
  • 所以,Spring又将AspectJ的对于AOP的实现也引入到了自己的框架中。
  • 在Spring中使用AOP开发时,一般使用AspectJ的实现方式。
  • Spring主要在事务处理时使用AOP。

2.AspectJ:一个开源的,专门做AOP的框架

  • Spring框架中集成了AspectJ框架,通过Spring就能使用AspectJ的功能。
  • AspectJ框架实现AOP有两种方式:
    1. 使用xml的配置文件:配置全局事务
    2. 使用注解:我们在项目中一般都使用注解来实现AOP功能。

4.6AspectJ框架的使用

4.6.1AspectJ的通知类型

切面的执行时间在AOP规范中也叫做Advice(通知,增强),在AspectJ框架有5个通知类型,都有相对应的注解:

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

当然,也可以使用xml配置文件中的标签来表示。

4.6.2AspectJ的切入点表达式

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

表达式原型:

execution(modifiers-pattern? ret-type-pattern 
          declaring-type-pattern?name-pattern(param-pattern) 
          throws-pattern?)

说明:(带有?的表示是可选部分)

  1. modifiers-pattern?:访问权限类型,可选
  2. ret-type-pattern:返回值类型
  3. declaring-type-pattern?:类的全限定名,可选
  4. name-pattern(param-pattern) :方法名(参数类型和参数个数),如doSome(String,Integer)
  5. throws-pattern?:抛出异常类型,可选

以上表达式共4个部分:

execution(访问权限 方法返回值 方法声明(参数) 异常类型)

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

注意:表达式中“访问权限”和“异常类型”是可省略部分,各个部分间用空格分开。一个表达式中只有方法返回值和方法声明是必须的。

切入点表达式支持通配符:

符号 意义
* 0到多个任意字符
.. 1.用在方法参数中时,表示任意多个参数
2.用在包名后时,表示当前包及其子包路径
+ 1.用在类名后,表示当前类及其子类
2.用在接口后,表示当前接口及其实现类

举例:

1.execution(public * *(..))

  • 第一个*表示任意方法返回值
  • 第二个*表示任意方法名
  • 方法后面小括号里的..表示任意多个参数

指定切入点为:任意公共方法。

2.execution(* set*(..))

指定切入点为:任何一个以“set”开始的方法。

3.execution(* com.tsccg.service.*.*(..))

指定切入点为:定义在service 包里的任意类的任意方法,不包含service包的子包中的类。

4.execution(* com.tsccg.service..*.*(..))

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

  1. execution(* *..service.*.*(..))

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

4.6.3使用AspectJ框架实现AOP的步骤

使用AOP的目的是:在不修改原有代码的情况下,给已经存在的一些类和方法增加额外的功能。

完整步骤:

  1. 新建maven项目
  2. 加入Spring和AspectJ等依赖
  3. 创建目标类:先定义一个接口,然后创建它的实现类
  4. 创建切面类:就是一个普通java类
    1. 在java类的上面添加@Aspect注解,表明这是一个切面类
    2. 在切面类中定义方法,方法就是切面,就是要增强的功能代码
    3. 在方法上面添加通知注解,如@Before,指定切面执行时间
    4. 在通知注解的value属性里编写切入点表达式execution(),指定切面执行位置
  5. 创建Spring的配置文件:声明Bean对象,把对象交给容器统一管理
    1. 可通过注解或xml配置文件<bean>来声明对象
    2. 声明目标对象和切面类对象
    3. 声明AspectJ框架中的自动代理生成器,用来完成代理对象的自动创建功能
  6. 编写测试代码,从Spring容器中获取目标对象(实际上已经是代理对象了),通过代理对象执行方法,发现已经实现了功能增强

实例演示:

1.创建一个maven子模块:spring-05-aop-aspectj

2.在父模块的pom.xml文件中添加AspectJ依赖,然后刷新

<!-- AspectJ -->
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aspects</artifactId>
  <version>5.2.9.RELEASE</version>
</dependency>

3.创建目标接口和目标类

目标接口:

package com.tsccg.service;

/**
 * @Author: TSCCG
 * @Date: 2021/09/19 21:30
 */
public interface SomeService {
    void doSome(String name,Integer age);
}

目标类:

package com.tsccg.service.impl01;

import com.tsccg.service.SomeService;

/**
 * @Author: TSCCG
 * @Date: 2021/09/19 21:30
 */
public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome(String name,Integer age) {
        System.out.println("执行doSome业务方法");
    }
}

4.创建切面类

package com.tsccg.aspectj;

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

import java.util.Date;

/**
 * @Author: TSCCG
 * @Date: 2021/09/19 21:33
 * 切面类:用来给业务方法增加功能的类,在这个类中有切面的功能代码
 */
/*
 @Aspect:AspectJ框架中的注解
  	作用:表示当前类是切面类
 	位置:在类定义的上面作用:表示当前类是切面类
*/
@Aspect
public class MyAspectJ {
    /*
     定义方法doLog,方法是实现切面功能的代码
     方法的定义要求:
    	1.必须为公共的方法:public
    	2.方法没有返回值:void
    	3.方法名自定义
    	4.方法可以有参也可以无参,如果有参数,参数不是自定义的,有几个参数类型可以使用
    */
    
    /*
     @Before:前置通知注解,指定切面的执行时间
     	属性:value,是切入点表达式,指定切面的执行位置
     	位置:在方法上面编写
     特点:
     	1.在目标方法前先执行切面
     	2.不会影响目标方法的执行
     	3.不会改变目标方法的执行结果
    */
    @Before(value="execution(public void com.tsccg.service.impl01.SomeServiceImpl.doSome(String,Integer))")
    public void doLog() {
        //切面要执行的增强功能代码
        System.out.println("前置通知,切面功能:在方法执行之前输出执行时间:" + new Date());
    }
}

5.创建Spring的配置文件:声明Bean对象,把对象交给容器统一管理

在resources目录下创建applicationContext.xml配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
	<!--通过xml配置文件的bean标签,把对象的创建和管理交给Spring容器-->
    <!--声明目标对象-->
    <bean id="mySomeServiceImpl" class="com.tsccg.service.impl01.SomeServiceImpl"/>
    <!--声明切面类对象-->
    <bean id="myAspect" class="com.tsccg.aspectj.MyAspectJ"/>
    <!--声明自动代理器:使用AspectJ框架内部的功能,创建目标对象的代理对象。
		创建代理对象是在内存中实现的,修改目标对象中的结构,创建为代理对象。
		故而从Spring中获取目标对象时,获取的实际上是被修改后的代理对象。
		
		aspectj-autoproxy:会把Spring容器中的所有目标对象,一次性都生成代理对象
	-->
    <aop:aspectj-autoproxy/>
</beans>

6.编写测试代码,从Spring容器中获取目标对象(实际上已经是代理对象了)

public class MyTest01 {
    @Test
    public void testSomeService() {
        String config="applicationContext.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(config);
        //从Spring容器中获取目标对象(实际上已经是代理对象了)
        SomeService proxy = (SomeService) ac.getBean("mySomeServiceImpl");
        //打印这个对象的类名
        System.out.println(proxy.getClass().getName());
        //调用代理对象调用目标方法
        proxy.doSome("张三",20);
    }
}

测试结果:

com.sun.proxy.$Proxy7//jdk动态代理
前置通知,切面功能:在方法执行之前输出执行时间:Mon Sep 20 22:45:51 CST 2021//增强的功能
执行doSome业务方法

4.6.4切入点表达式的多种写法

//不使用通配符
@Before(value="execution(public void com.tsccg.service.impl01.SomeServiceImpl.doSome(String,Integer))")
 public void doLog() {
     System.out.println("前置通知,切面功能:在方法执行之前输出执行时间:" + new Date());
 }
//省略访问权限
 @Before(value="execution(void com.tsccg.service.impl01.SomeServiceImpl.doSome(String,Integer))")
 public void doLog() {
     System.out.println("前置通知,切面功能:在方法执行之前输出执行时间:" + new Date());
 }
//用通配符代替返回值
 @Before(value="execution(* com.tsccg.service.impl01.SomeServiceImpl.doSome(String,Integer))")
 public void doLog() {
     System.out.println("前置通知,切面功能:在方法执行之前输出执行时间:" + new Date());
 }
//用通配符代替包名
 @Before(value="execution(* *..SomeServiceImpl.doSome(String,Integer))")
 public void doLog() {
     System.out.println("前置通知,切面功能:在方法执行之前输出执行时间:" + new Date());
 }
//用通配符代替部分类名
 @Before(value="execution(* *..Some*.doSome(String,Integer))")
 public void doLog() {
     System.out.println("前置通知,切面功能:在方法执行之前输出执行时间:" + new Date());
 }
//用通配符代替部分方法名
 @Before(value="execution(* *..SomeServiceImpl.do*(String,Integer))")
 public void doLog() {
     System.out.println("前置通知,切面功能:在方法执行之前输出执行时间:" + new Date());
 }
//省略类名,用通配符代替方法中参数
 @Before(value="execution(* doSome(..))")
 public void doLog() {
     System.out.println("前置通知,切面功能:在方法执行之前输出执行时间:" + new Date());
 }
 //用通配符代替方法名
 @Before(value="execution(* *..SomeServiceImpl.*(..))")
 public void doLog() {
     System.out.println("前置通知,切面功能:在方法执行之前输出执行时间:" + new Date());
 }

4.6.5通知方法中的参数:JoinPoint

通知方法:被通知注解所修饰的方法就是通知方法。

JoinPoint:连接点,指的是要加入切面功能的业务方法。

在通知方法中可以包含一个JoinPoint类型的参数,该类型的对象本身就是切入点表达式,通过该参数,可以在执行切面方法时,获取业务方法的信息,例如业务方法名称、业务方法的实参。

  1. 如果在切面功能中需要用到业务方法的信息,就需要加入JointPoint参数。
  2. 这个JointPoint参数的值由AspectJ框架赋予,使用时必须在参数列表第一位。

如:

切面类:

@Aspect
public class MyAspectJ {
    @Before(value="execution(public void com.tsccg.service.impl01.SomeServiceImpl.doSome(String,Integer))")
    public void doLog(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());
    }
}

测试结果:

com.sun.proxy.$Proxy7
业务方法的签名(定义):void com.tsccg.service.SomeService.doSome(String,Integer)
业务方法的名称:doSome
业务方法的参数:张三
业务方法的参数:20
前置通知,切面功能:在方法执行之前输出执行时间:Tue Sep 21 17:08:17 CST 2021
执行doSome业务方法

4.6.6前置通知@Before

@Before修饰的前置通知方法在目标方法执行前执行。

4.6.7后置通知@AfterReturning

被@AfterReturning修饰的后置通知方法在目标方法执行之后执行。

因为是在目标方法执行之后执行,所以可以获取到目标方法的返回值。该注解的returning属性就是专门用于接收目标方法返回值的变量名的。

同时,被注解为后置通知的方法,除了可以包含JoinPoint参数外,还可以包含用于接收返回值的变量。该变量最好为Object类型。因为目标方法的返回值可能是任何类型。

演示后置通知

1.在接口中添加方法

public interface SomeService {
    void doSome(String name,Integer age);
    //后置通知方法
    String doOther(String name,Integer age);
}

2.在实现类中实现方法

public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome(String name,Integer age) {...}
    
	//返回一个字符串
    @Override
    public String doOther(String name, Integer age) {
        System.out.println("执行doOther业务方法");
        return "abcd";
    }
}

3.使用后置通知定义方法

方法定义要求:

  1. 必须为公共方法:public
  2. 没有返回值:void
  3. 方法名自定义
  4. 方法可以有参数,推荐使用Object类型,参数名必须与后置通知的returning属性的值一致

@AfterReturning:

  1. 属性:
    1. value:切入点表达式
    2. returning:自定义的变量名,表示目标方法返回值,必须和通知方法的形参名一致
  2. 位置:在方法定义上面
  3. 特点:
    1. 在目标方法执行后执行
    2. 能获取到目标方法的返回值,可以根据这个返回值做不同的处理功能
@AfterReturning(value="execution(String *..SomeServiceImpl.doOther(..))",returning="res")
public void myAfterReturning(Object res) {//Object res:是目标方法执行后的返回值,根据返回值做切面的功能处理
    System.out.println("后置通知,切面功能:在目标方法执行之后获取返回值:" + res);
    /*
     对返回值进行修改
     如果返回值不为null,则将"Hello"字符串赋给res
    */
    if (res != null) {
        res = "Hello";
    }
    System.out.println("修改后:" + res);
}

4.编写测试方法

@Test
public void testSomeService() {
    String config="applicationContext1.xml";
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);
    SomeService proxy = (SomeService) ac.getBean("mySomeServiceImpl");
    System.out.println(proxy.getClass().getName());
    String str = proxy.doOther("李四", 30);
    System.out.println("str:" + str);
}

测试结果:

com.sun.proxy.$Proxy7
执行doOther业务方法
后置通知,切面功能:在目标方法执行之后获取返回值:abcd
修改后:Hello
str:abcd
分析

由结果可见,在执行目标方法后,执行了切面。但切面中对返回结果的修改并未成功,最终结果仍为abcd。

上面的例子中,目标方法返回值为String类型。当目标方法执行后,将"abcd"的地址复制一份给切面方法的res形参。

当给res重新指定一个新字符串时,修改的只是res形参所存储的地址,目标方法的返回值不会改变。

又由于String底层是一个被final修改的字符数组,故字符串本身不可被修改,故当目标方法返回结果为String类型时,不可被修改。

这代表着后置通知无法对返回值进行修改吗?当然不是。

当目标方法的返回值为一个自定义的类型比如Student,我们就可以在形参中通过set方法修改Student的属性值,从而实现修改返回结果。

演示修改返回结果

Student类:

public class Student {
    private String name;
    private Integer age;

    public Student() {
    }
    public Student(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public void setName(String name) {
        this.name = name;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

接口:

public interface SomeService {
    void doSome(String name,Integer age);
    String doOther(String name,Integer age);
    /**
     * 返回值为Student类型
     */
    Student doOther2(String name,Integer age);
}

实现类:

public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome(String name,Integer age) {
        System.out.println("执行doSome业务方法");
    }
    @Override
    public String doOther(String name, Integer age) {
        System.out.println("执行doOther业务方法");
        return "abcd";
    }
	/**
	 * 目标方法
	 */
    @Override
    public Student doOther2(String name, Integer age) {
        Student student = new Student(name,age);
        System.out.println("执行doOther2业务方法");
        return student;
    }
}

切面方法:

@AfterReturning(value="execution(* *..SomeServiceImpl.doOther2(..))",returning="res")
public void myAfterReturning2(Object res) {
    System.out.println("后置通知,切面功能:在目标方法执行之后获取返回值:" + res);
    if (res instanceof Student) {
        //res = new Student("张飞",30);//这种方式只会修改res形参的指向,不会修改返回结果
        //修改返回结果中的属性值
        Student student = (Student)res;
        student.setName("张飞");
        student.setAge(30);
    }
    System.out.println("修改后:" + res);
}

测试方法:

@Test
public void testSomeService() {
    String config="applicationContext1.xml";
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);
    SomeService proxy = (SomeService) ac.getBean("mySomeServiceImpl");
    System.out.println(proxy.getClass().getName());

    String str = proxy.doOther("李四", 30);
    System.out.println("str:" + str);
    System.out.println("===================================");
    Student student = proxy.doOther2("王五",23);
    System.out.println("student:" + student);
}

测试结果:

com.sun.proxy.$Proxy7
执行doOther业务方法
后置通知,切面功能:在目标方法执行之后获取返回值:abcd
修改后:Hello
str:abcd
===================================
执行doOther2业务方法
后置通知,切面功能:在目标方法执行之后获取返回值:Student{name='王五', age=23}
修改后:Student{name='张飞', age=30}
student:Student{name='张飞', age=30}

由结果来看,已成功修改返回结果。

4.6.8环绕通知@Around

环绕通知可以在目标方法执行前执行,也可以在目标方法执行后执行。

被注解为环绕通知的方法要有返回值,Object类型。

方法可以包含一个ProceedingJoinPoint类型的参数,ProceedingJoinPoint接口有一个方法proceed,用于执行目标方法。如果目标方法有返回值,那么该方法的返回值就是目标方法的返回值。最后,环绕通知方法将其返回值返回。

环绕通知方法实际上是拦截了目标方法的执行。

环绕通知一般都是做事务处理,在目标方法之前开启事务,执行目标方法,在目标方法之后提交事务。

演示环绕通知

1.在接口中添加方法doFirst

返回一个字符串

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

    String doOther(String name,Integer age);
    
    Student doOther2(String name,Integer age);
    //环绕通知方法
    String doFirst(String name,Integer age);
}

2.实现类中实现doFirst方法

返回"Hello"

public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome(String name,Integer age) {...}
    @Override
    public String doOther(String name, Integer age) {...}
    @Override
    public Student doOther2(String name, Integer age) {...}

    @Override
    public String doFirst(String name, Integer age) {
        System.out.println("执行doFirst业务方法");
        return "Hello";
    }
}

3.定义切面

环绕通知的方法定义格式:

  1. 必须为公共类:public
  2. 必须有一个返回值,推荐使用Object
  3. 方法名称自定义
  4. 方法可以包含一个ProceedingJoinPoint类型的参数

@Around:环绕通知注解

  1. 属性:value,切入点表达式
  2. 位置:在方法的定义上
  3. 特点:
    1. 它是功能最强的通知
    2. 在目标方法的前和后都能增强功能
    3. 可以控制目标方法是否被调用执行
    4. 可以修改原来的目标方法的执行结果
@Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
    //定义目标方法执行结果
    Object result = null;
    System.out.println("环绕通知:在目标方法执行前,输出时间:" + new Date());
    result = pjp.proceed();//执行目标方法
    System.out.println("环绕通知:在目标方法执行后,提交事务");
    return result;
}

环绕通知等同于jdk动态代理中的InvocationHandler接口实现类

InvocationHandler实现类:

public class MyInvocationHandler implements InvocationHandler {
    //定义目标类
    private final Object target;
    //有参构造方法
    public MyInvocationHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = null;
        String methodName = method.getName();
        if ("doSome".equals(methodName)) {
            SomeServiceUtil.doLog();//在目标方法执行前增强功能
            result = method.invoke(target,args);
            SomeServiceUtil.doTrans();//在目标方法执行后增强功能
        } else {
            result = method.invoke(target,args);
        }
        return result;
    }
}

环绕通知方法中的ProceedingJoinPoint类型参数相当于InvocationHandler接口实现类里的Method类型参数,作用是执行目标方法。

环绕通知方法的返回值就是执行目标方法的执行结果,可以被修改。

4.编写测试方法

@Test
public void testSomeService() {
    String config="applicationContext1.xml";
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);
    SomeService proxy = (SomeService) ac.getBean("mySomeServiceImpl");
    System.out.println(proxy.getClass().getName());
    //这里相当于执行String result = proxy.myAround();
    String result = proxy.doFirst("Tom",18);
    System.out.println("result:" + result);
}

测试结果:

com.sun.proxy.$Proxy8
环绕通知:在目标方法执行前,输出时间:Fri Sep 24 19:18:53 CST 2021
执行doFirst业务方法
环绕通知:在目标方法执行后,提交事务
result:Hello
环绕通知控制目标方法的执行

我们可以编写一个判断,当在测试方法里调用目标方法传入的name不是"Tom"时,不执行目标方法。

为此,我们需要在环绕通知方法里获取目标方法的实参。

我们可以查看ProceedingJoinPoint接口的源代码,可以发现,其继承了JoinPoint接口

public interface ProceedingJoinPoint extends JoinPoint {...}

JoinPoint类型的参数,可以在执行切面时,获取目标方法的信息,例如目标方法名称、目标方法的实参等。作为JoinPoint的子接口,一样可以获取目标方法的实参。

编写环绕通知方法:

@Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
public Object myAround2(ProceedingJoinPoint pjp) throws Throwable {
    //定义传入的name参数
    String name = null;
    //获取目标方法的所有实参
    Object[] args = pjp.getArgs();
    if (args != null && args.length > 1) {
        //获取第一个实参
        name = (String)args[0];
    }
    //定义目标方法执行结果
    Object result = null;
    System.out.println("环绕通知:在目标方法执行前,输出时间:" + new Date());
    //判断
    if ("Tom".equals(name)) {
        result = pjp.proceed();//执行目标方法
    }
    System.out.println("环绕通知:在目标方法执行后,提交事务");
    return result;
}

在测试方法中传入Jerroy:

@Test
public void testSomeService() {
    String config="applicationContext1.xml";
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);
    SomeService proxy = (SomeService) ac.getBean("mySomeServiceImpl");
    System.out.println(proxy.getClass().getName());
    String result = proxy.doFirst("Jerroy",18);
    System.out.println("result:" + result);
}

测试结果:

com.sun.proxy.$Proxy8
环绕通知:在目标方法执行前,输出时间:Fri Sep 24 20:17:03 CST 2021
环绕通知:在目标方法执行后,提交事务
result:null

可见,目标方法并未执行。

环绕通知控制目标方法的执行结果

在环绕通知方法返回目标类执行结果处,添加:

System.out.println("环绕通知:在目标方法执行后,提交事务");
//控制返回结果
if (result != null) {
    result = "修改后结果:ABCD";
}
return result;

执行测试方法,返回结果:

com.sun.proxy.$Proxy8
环绕通知:在目标方法执行前,输出时间:Fri Sep 24 20:27:51 CST 2021
执行doFirst业务方法
环绕通知:在目标方法执行后,提交事务
result:修改后结果:ABCD

4.6.9异常通知@AfterThrowing

异常通知是在目标方法抛出异常后执行。该注解的throwing属性用于指定所发生的异常类对象。

当然,被注解为异常通知的方法可以包含一个参数Throwable,参数名称为throwing指定的名称,表示发生的异常对象。

演示异常通知

1.在接口中添加业务方法

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

    String doOther(String name,Integer age);

    Student doOther2(String name,Integer age);

    String doFirst(String name,Integer age);
    //异常通知方法
    void doSecond();
}

2.实现业务方法

public class SomeServiceImpl implements SomeService {
    @Override
    public void doSecond() {
        System.out.println("执行doSecond业务方法");
    }
}

3.定义异常通知方法

异常通知方法的定义格式

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

@AfterThrowing:异常通知注解

  1. 属性:
    1. value:切入点表达式
    2. throwing:自定义的变量,表示目标方法抛出的异常对象。变量名必须和方法的参数名一致
  2. 特点:
    1. 在目标方法抛出异常时执行
    2. 可以做异常的监控程序,监控目标方法执行时是不是有异常。如果有异常,可以发送邮件、短信进行通知。
//异常通知方法
@AfterThrowing(value="execution(* *..SomeServiceImpl.doSecond(..))"
        ,throwing = "ex")
public void myAfterThrowing(Exception ex) {
    System.out.println("异常通知:在目标方法抛出异常时执行:" + ex.getMessage());
}

4.编写测试方法

@Test
public void testSomeService() {
    String config = "applicationContext1.xml";
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);
    SomeService proxy = (SomeService)ac.getBean("mySomeServiceImpl");
    System.out.println(proxy.getClass().getName());
    proxy.doSecond();
}

测试结果:

com.sun.proxy.$Proxy9
执行doSecond业务方法

在目标方法里添加一个异常:

@Override
public void doSecond() {
    System.out.println("执行doSecond业务方法" + 10/0);
}

重新执行测试:

异常通知就好比在try...catch语句中,被catch所包含的语句。当没有异常时不会执行,当有异常时才执行。

try {
	SomeServiceImpl.doSecond(..)
} catch (Exception ex) {
	//异常通知
    myAfterThrowing(Exception ex)
}

4.6.10最终通知@After

最终通知就是无论目标方法是否抛出异常,都会执行切面。

演示最终通知:

1.接口中添加方法

public interface SomeService {
    //最终通知方法
    void doThird();
}

2.实现方法

public class SomeServiceImpl implements SomeService {
    @Override
    public void doThird() {
        System.out.println("执行doThird业务方法");
    }
}

3.定义切面

最终通知方法的定义格式

  1. public
  2. 没有返回值
  3. 方法名称自定义
  4. 方法可以有JoinPoint类型的参数

@After:最终通知注解

  1. 属性:value,切入点表达式
  2. 位置:方法定义上面
  3. 特点:
    1. 无论有无异常发生,总是会被执行
    2. 在目标方法之后执行
@After("execution(* *..SomeServiceImpl.doThird(..))")
public void myAfter() {
    System.out.println("最终通知:总是会被执行的代码");
    //一般做资源清除工作
}

最终通知就好比try...catch...finally语句中,finally所包含的代码

try {
    SomeServiceImpl.doThird(..) 
} catch(Exception ex) {
    
} finally {
    myAfter()
}

4.测试方法

@Test
public void testSomeService() {
    String config = "applicationContext1.xml";
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);
    SomeService proxy = ac.getBean("mySomeServiceImpl", SomeService.class);
    proxy.doThird();
}

测试结果:

执行doThird业务方法
最终通知:总是会被执行的代码

现在在目标方法中添加一个异常:

@Override
public void doThird() {
    System.out.println("执行doThird业务方法" + 10/0);
}

重新执行测试方法:

4.6.11定义切入点@Pointcut

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

@Pointcut:定义和管理切入点,如果项目中有多个切入点表达式是重复的,可以使用@Pointcut

  1. 属性:value,切入点表达式
  2. 位置:在自定义的方法上面
  3. 特点:当使用@Pointcut注解在一个方法的上面时,这个方法的名称就是@Pointcut中所编写的切入点表达式的别名。在其他通知注解中,就可以直接使用这个方法的名称来代替切入点表达式了。
  4. @Pointcut注解的方法没有实际的作用,无需在方法体内编写代码,一般用private修饰。

用法:

  1. 定义:将@Pointcut注解在一个私有的空方法之上,然后编写切入点表达式
  2. 使用:在使用其他通知注解时,直接在注解的value属性里写空方法的名称代替切入点表达式
@Aspect
public class MyAspectJ {
    /**
     * 定义execution切入点表达式
     */
    @Pointcut(value="execution(* *..SomeServiceImpl.doThird(..))")
    private void myEx() {

    }
    //使用
    @After(value = "myEx()")
    public void myAfter() {
        System.out.println("最终通知:总是会被执行的代码");
    }
    @AfterThrowing(value="myEx()",throwing = "ex")
    public void myAfterThrowing(Exception ex) {
        System.out.println("异常通知:在目标方法抛出异常时执行:" + ex.getMessage());
    }
}

测试方法:

@Test
public void testSomeService() {
    String config = "applicationContext1.xml";
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);
    SomeService proxy = ac.getBean("mySomeServiceImpl", SomeService.class);
    proxy.doThird();
}

测试结果:

4.6.12当目标类没有接口时,使用cglib动态代理

定义一个普通java类,不继承任何接口:

package com.tsccg.service.impl02;

/**
 * @Author: TSCCG
 * @Date: 2021/09/19 21:30
 */
public class SomeServiceImpl {
    public void doSome(String name,Integer age) {
        System.out.println("执行doSome业务方法");
    }
}

定义切面:

package com.tsccg.aspectj.before02;

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

import java.util.Date;

/**
 * @Author: TSCCG
 * @Date: 2021/09/19 21:33
 */
@Aspect
public class MyAspectJ {
    @Before(value="execution(public void com.tsccg.service.impl02.SomeServiceImpl.doSome(String,Integer))")
    public void doLog() {
        System.out.println("前置通知,切面功能:在方法执行之前输出执行时间:" + new Date());
    }
}

定义Spring配置文件:

applicationContext1.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="mySomeServiceImpl" class="com.tsccg.service.impl02.SomeServiceImpl"/>
    <bean id="myAspect" class="com.tsccg.aspectj.before02.MyAspectJ"/>
    <aop:aspectj-autoproxy/>
</beans>

定义测试方法:

@Test
public void testSomeService() {
    String config="applicationContext1.xml";
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);
    SomeServiceImpl proxy = (SomeServiceImpl) ac.getBean("mySomeServiceImpl");
    System.out.println(proxy.getClass().getName());
    proxy.doSome("张三",20);
}

测试结果:

com.tsccg.service.impl02.SomeServiceImpl$$EnhancerBySpringCGLIB$$ea9fa087
前置通知,切面功能:在方法执行之前输出执行时间:Sat Sep 25 15:03:55 CST 2021
执行doSome业务方法

由结果可见,在目标对象没有接口的情况下,使用的是cglib动态代理。

4.6.13有接口的情况下,也可以是cglib动态代理

目标方法有接口的情况下,仍然可以使用cglib动态代理

如果在目标方法中有接口的情况下,也使用cglib动态代理,就需要在配置文件里的自动代理生成器中指定proxy-target-class属性值为true。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       ...
    <bean id="mySomeServiceImpl" class="com.tsccg.ba02.SomeServiceImpl"/>
    <bean id="myAspect" class="com.tsccg.ba02.MyAspectJ"/>
	<!-- 指定proxy-target-class属性值为true -->
    <aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>

接口:

package com.tsccg.ba02;

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

接口实现类:

public class SomeServiceImpl implements SomeService{
    @Override
    public void doSome(String name, Integer age) {
        System.out.println("执行doSome业务方法");
    }
}

定义切面:

@Aspect
public class MyAspectJ {
    @Before(value="execution(public void com.tsccg.ba02.SomeServiceImpl.doSome(String,Integer))")
    public void doLog() {
        System.out.println("前置通知,切面功能:在方法执行之前输出执行时间:" + new Date());
    }
}

配置文件:

applicationContext2.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="mySomeServiceImpl" class="com.tsccg.ba02.SomeServiceImpl"/>
    <bean id="myAspect" class="com.tsccg.ba02.MyAspectJ"/>
    <!-- 指定proxy-target-class属性值为true -->
    <aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>

测试方法:

@Test
public void testSomeService() {
    String config = "applicationContext2.xml";
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);
    SomeService proxy = ac.getBean("mySomeServiceImpl", SomeService.class);
    System.out.println(proxy.getClass().getName());
    proxy.doSome("张三",30);
}

测试结果:

com.tsccg.ba02.SomeServiceImpl$$EnhancerBySpringCGLIB$$99435831
前置通知,切面功能:在方法执行之前输出执行时间:Sat Sep 25 15:47:10 CST 2021
执行doSome业务方法

5.Spring整合MyBatis

5.1Spring整合MyBatis的思路

Spring整合MyBatis就是把Spring框架和MyBatis框架集成在一起,像一个框架一样来使用。

使用的技术是IoC。IoC可以创建对象,可以把MyBatis框架中的对象交给Spring统一创建,开发人员从Spring中获取对象。这样的好处就是开发人员不用同时面对两个或多个框架了,只需要面对Spring一个框架。

MyBatis使用基本步骤:

  1. 添加MyBatis和数据库驱动依赖

  2. 定义dao接口:StudentDao

  3. 创建mapper文件,编写sql语句:StudentDao.xml

  4. 创建MyBatis的主配置文件:mybatis.xml

  5. 创建dao的代理对象:StudentDao dao = sqlSession.getMapper(StudentDao.class);

    调用接口中的方法:List<Studetn> students = dao.selectStudents();

获取dao对象的步骤:

InputStream in = Resources.getResourceAsStream("mybatis.xml");//读取mybatis主配置文件
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(in);//创建SqlSessionFactory对象
SqlSession sqlSession = factory.openSession();//创建SqlSesion对象
StudentDao dao = sqlSession.getMapper(StudentDao.class);//获取dao对象
int count = dao.countStudent();//使用dao对象调用接口中的方法
sqlSession.close();
System.out.println("count:" + count);
  1. 读取mybatis主配置文件,通过SqlSessionFactoryBuilder对象调用build()方法创建SqlSessionFactory对象
  2. 使用SqlSessionFactory对象,调用openSession()方法,获取SqlSession对象
  3. 使用SqlSession对象,调用getMapper()方法,获取dao对象

要获取dao对象,需要SqlSession对象,就需要SqlSessionFactory对象来创建SqlSession对象,从而需要创建SqlSessionFactory对象,而创建SqlSessionFactory对象需要读取mybatis的主配置文件。

主配置文件中主要包括:

  1. 数据库信息:

    <environment id="mydev">
        <!-- transactionManager:mybatis的事务类型
             type:JDBC(表示使用JDBC中的Connection对象的commit,rollback做事务处理)
        -->
        <transactionManager type="JDBC"/>
        <!-- dataSource:表示数据源,连接数据库的
             type:表示数据源的类型,POOLED表示使用连接池
        -->
        <dataSource type="POOLED">
            <!-- driver,url,username,password都是固定的,不能自定义 -->
            <property name="driver" value="com.mysql.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql://localhost:3306/db_mybatis?useUnicode=true&amp;serverTimezone=GMT&amp;useSSL=false&amp;characterEncoding=utf-8"/>
            <property name="username" value="root"/>
            <property name="password" value="123456"/>
        </dataSource>
    </environment>
    
  2. mapper文件所在位置:

    <mappers>
        <!-- 一个mapper标签指定一个文件的位置
             从类路径(target/classes)开始的路径信息
         -->
        <mapper resource="com/tsccg/dao/StudentDao.xml"/>
    </mappers>
    

主配置文件中,连接数据库默认使用的是POOLED连接池,这个连接池是mybatis自带的,性能较弱,支撑不了大型项目的运作。

所以,我们在开发大型项目时,会使用性能更佳的独立的连接池类来替换原有的连接池,把连接池类也交给Spring创建。

综上,我们最终需要让Spring创建以下对象:

  1. 独立的连接池类对象,使用阿里的druid连接池
  2. SqlSessionFactory对象
  3. dao对象

我们学习Spring整合MyBatis主要就是学习以上三个对象的创建语法。

5.2Spring整合MyBatis的基本步骤说明

  1. 新建maven项目
  2. 添加所需依赖
    1. Spring依赖
    2. MyBatis依赖
    3. MySQL驱动依赖
    4. Spring的事务依赖
    5. MyBatis和Spring的集成依赖:MyBatis官方提供的,用来在Spring项目中创建MyBatis的SqlSessionFactory对象和dao对象
  3. 创建实体类
  4. 创建dao接口和mapper文件
  5. 创建MyBatis主配置文件
  6. 创建service接口和实现类,属性是dao,调用dao实现访问数据库
  7. 创建Spring的配置文件:在Spring配置文件中声明mybatis对象,把mybatis对象的管理交给Spring
    1. 独立的连接池类对象:数据源,用于代替MyBatis自带的连接池<dataSource type="POOLED">
    2. SqlSessionFactory对象
    3. Dao对象
    4. 自定义的service对象
  8. 创建测试类,获取service对象,通过service调用dao完成数据库的访问

5.3Spring整合MyBatis的步骤

5.3.1建表

drop database if exists `db_mybatis`;
create database `db_mybatis`;
use `db_mybatis`;
create table `t_student`(
  `id` int primary key,
  `name` varchar(255) default null,
  `email` varchar(255) default null,
  `age` int default null
)engine=InnoDB default charset=utf8;
insert into `t_student`(`id`,`name`,`email`,`age`) values
(1001,'张三','zhangsan@qq.com',20),
(1002,'李四','lisi@qq.com',21),
(1003,'王五','wangwu@qq.com',22);
commit;
select * from `t_student`;

5.3.2新建maven子模块

新建一个maven子模块spring-06-mybatis

5.3.3添加依赖

在父模块的pom.xml中添加所需的各种依赖

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
<!--指定自身坐标-->
  <groupId>com.tsccg</groupId>
  <artifactId>spring-project</artifactId>
  <packaging>pom</packaging>
  <version>1.0-SNAPSHOT</version>
<!--指定子模块-->
  <modules>
    <module>spring-01</module>
    <module>spring-04-dynamicAgent</module>
    <module>spring-06-mybatis</module>
  </modules>
<!--指定项目jdk版本-->
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>
<!--添加依赖-->
  <dependencies>
    <!-- 单元测试 -->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</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.2.6.RELEASE</version>
    </dependency>
    <!-- AspectJ:AOP -->
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
      <version>5.2.9.RELEASE</version>
    </dependency>
    <!-- 做Spring事务需要用到的 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>
    <!-- 做Spring事务需要用到的 -->
    <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集成的依赖,由mybatis提供 -->
    <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>
  <!--插件-->
  <build>
    <resources>
      <!-- 目的是把src/main/java目录中的mapper文件包含到输出结果中,输出到target/classes目录中 -->
      <resource>
        <directory>src/main/java</directory><!--mapper所在的目录-->
        <includes><!--包括目录下的.properties,.xml 文件都会扫描到-->
          <include>**/*.properties</include>
          <include>**/*.xml</include>
        </includes>
        <filtering>false</filtering>
      </resource>
      <!-- 目的是把src/main/resources目录中的xml文件包含到输出结果中,输出到target/classes目录中 -->
      <resource>
        <directory>src/main/resources</directory><!--主配置文件所在的目录-->
        <includes><!--包括目录下的.properties,.xml 文件都会扫描到-->
          <include>**/*.properties</include>
          <include>**/*.xml</include>
        </includes>
        <filtering>false</filtering>
      </resource>
    </resources>
  </build>
</project>

5.3.4创建数据库实体类

在main/java目录下新建【com.tsccg.entity.Student】实体类

package com.tsccg.entity;

/**
 * @Author: TSCCG
 * @Date: 2021/09/25 19:54
 * 数据库实体类
 */
public class Student {
    private Integer id;
    private String name;
    private String email;
    private Integer age;

    public Student() {
    }

    public Student(Integer id, String name, String email, Integer age) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.age = age;
    }

    public Integer getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

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

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Integer getAge() {
        return age;
    }

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

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

5.3.5创建dao接口和mapper文件

1.创建dao接口:

在main/java目录下新建【com.tsccg.dao.StudentDao】接口,定义三个抽象方法,分别是增删查

package com.tsccg.dao;

import com.tsccg.entity.Student;

import java.util.List;

/**
 * @Author: TSCCG
 * @Date: 2021/09/25 19:59
 */
public interface StudentDao {
    /**
     * 添加学生信息
     * @param student 包含学生信息的实体类对象
     * @return 返回处理结果数目
     */
    int insertStudent(Student student);
    /**
     * 查询所有学生信息
     * @return 返回包含所有学生信息的Student对象集合
     */
    List<Student> selectAllStudent();
    /**
     * 删除学生信息
     * @param id 学生id
     * @return 返回处理结果数目
     */
    int deleteStudent(@Param("id") Integer id);
}

2.创建mapper文件:

在StudentDao接口同级目录下,新建一个mapper文件【StudentDao.xml】文件,编写sql代码

<?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.tsccg.dao.StudentDao">
    <!--增-->
    <insert id="insertStudent">
        insert into t_student(id,name,email,age) values(#{id},#{name},#{email},#{age});
    </insert>
    <!--查-->
    <select id="selectAllStudent" resultType="com.tsccg.entity.Student">
        select id,name,email,age from t_student
    </select>
    <!--删-->
    <delete id="deleteStudent" >
        delete from t_student where id = #{id}
    </delete>
</mapper>

5.3.6创建mybatis主配置文件

在main/resources目录下新建mybatis主配置文件【mybatis.xml】

因为我们要使用阿里的druid连接池代替mybatis自带的连接池,且交给Spring来管理,所以在这里不需要添加dataSource标签。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- settings:控制mybatis全局行为 -->
    <settings>
        <!-- 控制mybatis输出日志 -->
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>
    <!--设置实体类别名-->
    <typeAliases>
        <!--name:实体类所在包名,设置之后,实体类的类名就是别名-->
        <package name="com.tsccg.entity"/>
    </typeAliases>

    <!-- 指定mapper文件的位置-->
    <mappers>
        <!-- name:包名,这个包名下所有的mapper.xml文件一次都能加载 -->
        <package name="com.tsccg.dao"/>
    </mappers>
</configuration>

5.3.7创建service接口和实现类

1.定义service接口

在main/java目录下新建【com.tsccg.service.StudentService】接口

package com.tsccg.service;

import com.tsccg.entity.Student;
import java.util.List;
/**
 * @Author: TSCCG
 * @Date: 2021/09/25 20:37
 */
public interface StudentService {
    int addStudent(Student student);
    
    List<Student> findAllStudent();
    
    int removeStudent(Integer id);
}

2.创建service接口实现类

在main/java目录下新建【com.tsccg.service.impl.StudentServiceImpl】,实现StudentService接口

package com.tsccg.service.impl01;

import com.tsccg.dao.StudentDao;
import com.tsccg.entity.Student;
import com.tsccg.service.StudentService;

import java.util.List;
/**
 * @Author: TSCCG
 * @Date: 2021/09/25 20:39
 */
public class StudentServiceImpl implements StudentService {
    //定义一个dao类型属性
    private StudentDao studentDao;
    /**
     * 定义set方法,使用set注入来给dao属性赋值
     */
    public void setStudentDao(StudentDao studentDao) {
        this.studentDao = studentDao;
    }
    
    @Override
    public int addStudent(Student student) {
        return studentDao.insertStudent(student);
    }
    @Override
    public List<Student> findAllStudent() {
        return studentDao.selectAllStudent();
    }
    @Override
    public int removeStudent(Integer id) {
        return studentDao.deleteStudent(id);
    }
}

5.3.8创建Spring配置文件

创建Spring的配置文件:在Spring配置文件中声明mybatis对象,把mybatis对象的管理交给Spring。

在main/resources目录下新建spring配置文件【applicationContext.xml】。

在spring配置文件中声明各种mybatis对象:

1.声明数据源DataSource

我们这里使用阿里的druid连接池来代替mybatis自带的POOLED

druid官方地址:https://github.com/alibaba/druid

druid官方中文帮助文档:https://github.com/alibaba/druid/wiki/常见问题

DruidDataSource基本配置:

  • id="myDataSource":自定义名称,作为bean对象中的唯一标识
  • class="com.alibaba.druid.pool.DruidDataSource":druid数据源类的全限定名称
  • init-method="init":初始方法,属性值是初始方法的名字。当Spring容器创建时,会自动调用init方法
  • destroy-method="close":销毁方法,属性值是销毁方法的名字。当Spring容器关闭时,会自动调用close方法来做资源的销毁
<bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> 
    <!--使用set注入给DruidDataSource提供连接数据库信息-->
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/db_mybatis?useSSL=false&amp;characterEncoding=utf8" />
    <property name="username" value="root" />
    <property name="password" value="123456" />
    <!--maxActive:连接池最多容纳的连接对象数目,默认20个-->
    <property name="maxActive" value="20" />
</bean>

注意:

  • 在上面的配置中,通常需要配置driverClassName、url、username、password、maxActive这五项属性
  • 虽然druid官方文档上说了能根据输入的url自动识别驱动名称,不用配置driverClassName。但亲测没用,该配还是得配...

使用属性配置文件保存连接数据库信息

在实际开发中,我们会将连接数据库的信息放到一个独立的属性配置文件里

1)在main/resources目录下新建一个属性配置文件jdbc.properties

jdbc.mysql.driver=com.mysql.jdbc.Driver
jdbc.mysql.url=jdbc:mysql://localhost:3306/db_mybatis?characterEncoding=utf8&useSSL=false
jdbc.mysql.username=root
jdbc.mysql.password=123456
jdbc.mysql.maxActive=20

2)在spring配置文件中指定属性配置文件的位置,添加如下语句:

<context:property-placeholder location="classpath:jdbc.properties"/>

3)在spring配置文件中读取属性配置文件信息

语法:${key}

<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- 声明数据源DataSource -->
<bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
    <!--使用set注入给DruidDataSource提供连接数据库信息-->
    <property name="driverClassName" value="${jdbc.mysql.driver}"/>
    <property name="url" value="${jdbc.mysql.url}" />
    <property name="username" value="${jdbc.mysql.username}" />
    <property name="password" value="${jdbc.mysql.password}" />
    <!--maxActive:连接池最多容纳的连接对象数目,默认20个-->
    <property name="maxActive" value="${jdbc.mysql.maxActive}" />
</bean>
2.声明SqlSessionFactory类对象

在这里声明的是mybatis提供的SqlSessionFactoryBean类,这个类内部创建SqlSessionFactory的对象。

创建SqlSessionFactory类对象需要读取mybatis主配置文件的信息,而mybatis主配置文件主要由数据源DataSource和指定mapper文件所在位置的路径组成的。

  1. 为此,我们需要将上面声明的DruidDataSource类型对象的bean的id赋给SqlSessionFactoryBean类的dataSource属性
  2. 同时把mybatis主配置文件的路径赋给其configLocation属性

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <!--set注入,把数据库连接池赋给了dataSource属性-->
    <property name="dataSource" ref="myDataSource"/>
    <!--指定mybatis主配置文件的位置
		configLocation属性是Resource类型,专门用于读取配置文件
 		使用value进行赋值,指定mybatis.xml文件编译后的路径,使用classpath:表示文件的位置
	-->
    <property name="configLocation" value="classpath:mybatis.xml"/>
</bean>
3.创建dao对象

创建dao对象,可以使用SqlSession类的getMapper()方法,同时需要传入dao接口的class属性值

StudentDao dao = sqlSession.getMapper(StudentDao.class);//获取dao代理对象

而在Spring配置文件中,可以通过声明MapperScannerConfigurer对象,在内部调用getMapper()方法来生成指定包中每个dao接口的代理对象。

<!--不需要指定id属性-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <!--指定上面SqlSessionFactory对象的bean的id-->
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
	<!--指定dao接口所在包名
		MapperScannerConfigurer会扫描这个包名下的所有接口,
		然后把每个接口都执行一次getMapper()方法,得到每个接口的dao对象,
		然后把创建好的dao对象都放入到Spring的容器中,dao对象的名字是各自对应dao接口的首字母小写
	-->
    <property name="basePackage" value="com.tsccg.dao"/>
    <!--<property name="basePackage" value="com.tsccg.dao,com.tsccg.dao2"/>-->
</bean>
4.声明service对象
<bean id="studentService" class="com.tsccg.service.impl01.StudentServiceImpl">
    <!--这里引用的是步骤3中自动创建的dao代理对象-->
    <property name="studentDao" ref="studentDao"/>
</bean>
5.完整配置文件
<?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"/>
    <!-- 声明数据源DataSource -->
    <bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <!--使用set注入给DruidDataSource提供连接数据库信息-->
        <property name="driverClassName" value="${jdbc.mysql.driver}"/>
        <property name="url" value="${jdbc.mysql.url}" />
        <property name="username" value="${jdbc.mysql.username}" />
        <property name="password" value="${jdbc.mysql.password}" />
        <!--maxActive:连接池最多容纳的连接对象数目,默认20个-->
        <property name="maxActive" value="${jdbc.mysql.maxActive}" />
    </bean>
    <!--声明SqlSessionFactory对象-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="myDataSource"/>
        <property name="configLocation" value="classpath:mybatis.xml"/>
    </bean>
    <!--声明dao代理对象-->
    <!--不需要指定id属性-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!--指定SqlSessionFactory对象的id-->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
        <!--指定dao接口所在包名
            MapperScannerConfigurer会扫描这个包名下的所有接口,
            然后把每个接口都执行一次getMapper()方法,得到每个接口的dao对象,
            然后把创建好的dao对象都放入到Spring的容器中,dao对象的名字是各自对应dao接口的首字母小写
        -->
        <property name="basePackage" value="com.tsccg.dao"/>
    </bean>
    <!--声明service对象-->
    <bean id="studentService" class="com.tsccg.service.impl01.StudentServiceImpl">
        <property name="studentDao" ref="studentDao"/>
    </bean>
</beans>

5.3.9创建测试类

编写测试代码,获取service对象,通过service调用dao完成数据库的访问

package com.tsccg;

import com.tsccg.dao.StudentDao;
import com.tsccg.entity.Student;
import com.tsccg.service.StudentService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.util.List;

/**
 * @Author: TSCCG
 * @Date: 2021/09/27 15:37
 */
public class Test01 {
    @Test
    public void testSelectAll() {
        String config = "applicationContext.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(config);
        //直接从Spring容器中拿dao代理对象
        StudentService service = ac.getBean("studentService", StudentService.class);
        List<Student> studentList = service.findAllStudent();
        for (Student student : studentList) {
            System.out.println(student);
        }
    }
    @Test
    public void testAdd() {
        String config = "applicationContext.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(config);
        StudentService service = ac.getBean("studentService", StudentService.class);
        Student student = new Student(1004,"赵六","zhaoliu@qq.com",28);
        //使用service类调用dao对象来访问数据库
        int result = service.addStudent(student);
        //会自动执行commit();
        System.out.println(result > 0 ? "插入成功":"插入失败");
    }
    @Test
    public void testRemove() {
        String config = "applicationContext.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(config);
        StudentService service = ac.getBean("studentService", StudentService.class);
        int result = service.removeStudent(1004);
        System.out.println(result > 0 ? "删除成功":"删除失败");
    }
}

测试结果:

测试查询方法:

测试插入方法:

测试删除方法:

6.Spring事务

6.1事务相关问题

6.1.1什么是事务

事务是一组sql语句的集合,其中,sql语句可能是insert、update、delete、select。

我们希望在一个事务中,所有的sql语句能同时执行成功或者同时执行失败。让这些sql语句是一致的,是作为一个整体来执行的。

6.1.2在什么时候需要用到事务

当访问数据库时,涉及到多个表,或者是多条不同的sql语句如insert、update和delete,我就需要保证这些sql语句全部执行成功才能实现我的功能,或者全部执行失败来保证原本数据库中的数据不受影响。

在java代码中编写程序,需要控制事务,那么事务应该在程序的哪里使用呢?

我们应该在service类的业务方法中使用事务,因为业务方法往往会调用多个dao方法,执行多条sql语句。

6.1.3使用不同数据库访问技术处理事务的方式

1.JDBC访问数据库,处理事务:

conn.commit();
conn.rollback();

2.MyBatis访问数据库,处理事务:

sqlSession.commit();
sqlSession.rollback();

3.hibernate访问数据库,处理事务:

session.commit();
session.rollback();

6.1.4上一步中,事务的处理方式有什么不足之处,该如何解决?

不足之处:

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

解决方法:

Spring提供了一种处理事务的统一模型,能够使用统一步骤,统一方式来完成不同数据库访问技术的事务处理。

6.2在Spring中处理事务

6.2.1在Spring中处理事务的方式

我们在Spring中,是使用Spring事务处理模型来处理事务的。

Spring事务处理模型其实就是Spring内部写好的一系列代码,使用步骤是固定的,我们只需要把处理事务需要用到的信息提供给Spring,Spring就能处理事务的提交、回滚了。几乎不用我们自己编写事务相关的代码。

Spring的事务管理,主要用到两个事务相关的接口

  1. 事务管理器接口(重点)
  2. 事务定义接口

6.2.2事务管理器接口(重点)

Spring事务处理模型中,通过事务管理器来完成事务的提交、回滚,以及事务的状态信息。

事务管理器是PlatformTransactionManager接口实现类对象,接口定义提交、回滚方法:

PlatformTransactionManager接口有两个常用的实现类:对应不同的数据库访问技术

  1. DataSourceTransactionManager:使用JDBC或MyBatis访问数据库时使用
  2. HibernateTransactionManager:使用Hibername访问数据库时使用

我们需要告诉Spring使用哪个事务管理器:

可以在Spring配置文件中,声明数据库访问技术对应的事务管理器接口实现类,使用<bean>声明即可,如:

<!--使用MyBatis数据库访问技术-->
<bean id="xxx" class="...DataSourceTransactionManager">
</bean>

6.2.3事务定义接口

我们还需要告诉Spring,业务方法需要什么样的事务,说明事务的类型。

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

1)定义事务隔离级别常量:有4个值

这些常量都是以ISOLATION_开头,如:ISOLATION_READ_COMMITTED

  1. READ_UNCOMMITTED:读未提交
    1. 能够读取另一个事务中尚未提交的数据
    2. 存在脏读、不可重复读、幻读等所有并发问题
  2. READ_COMMITTED:读已提交
    1. 能够读取另一个事务中已经提交的数据
    2. 解决脏读问题
    3. 存在不可重复读与幻读问题
  3. REPEATABLE_READ:可重复读
    1. 一个事务A开启后,只要不关闭,不管过多久,每一次在事务A中读取到的数据都是刚开启事务A时的数据。即使期间另一个事务B修改了事务A正在使用的数据,事务A读取到的数据也不会变。
    2. 解决脏读、不可重复读问题
    3. 存在幻读问题
  4. SERIALIZABLE:串行化/序列化
    1. 同一时间只能执行一个事务
    2. 最高隔离级别,效率最低
    3. 解决了所有并发问题
  5. DEFAULT:采用DB默认的事务隔离级别
    1. MySql:REPEATABLE_READ,可重复读
    2. Oracle:READ_COMMITTED,读已提交

2)定义事务传播行为:指的是当一个事务中的方法被另一个事务中的方法调用时,这个被调用的方法处理事务关系的方式

如:在A事务中的方法doSome()调用B事务中的方法doOther()时,doOther()是按事务A运行,还是为自己开启一个新事务B运行,这就是由doOther的事务传播行为所决定的。

事务传播行为是加在方法上的。

Spring定义了7个事务传播行为常量:只有前三个是常用的,其余了解即可

  1. PROPAGATION_REQUIRED
  2. PROPAGATION_SUPPORTS
  3. PROPAGATION_REQUIRES_NEW
  4. PROPAGATION_MANDATORY
  5. PROPAGATION_NESTED
  6. PROPAGATION_NEVER
  7. PROPAGATION_NOT_SUPPORTED

a)PROPAGATION_REQUIRED

表示指定的当前方法必须在事务中执行。如果当前存在事务,则使当前方法加入到当前事务中;如果当前没有事务,则使当前方法新建一个事务。

比如:现在有doSome()和doOther()两个方法:

  1. doSome()方法可能会在事务A中运行,也可能不在任何事务中运行
  2. doOther()方法单独运行时会开启事务B

现需要在doSome()方法中调用doOther()方法。

在doOther方法上添加PROPAGATION_REQUIRED传播行为。

  1. 如果在doSome()方法中调用doOther()方法时,doSome()方法是在事务A中运行的,那么doOther()也会在事务A中运行;【传播】
  2. 如果doSome()方法没有在任何事务中运行,那么doSome()方法就会开启一个新的事务B。

打个生活中的例子,就好比蹭基友热点,如果他开启了热点(有事务),我就连他的热点上网(传播);如果他没开热点(没有事务),我就开自己的流量上网(自己开启事务)。

b)PROPAGATION_SUPPORTS

表示指定的当前方法支持当前事务,但如果当前没有事务,就以非事务形式执行。

还是doSome()和doOther方法:用PROPAGATION_SUPPORTS指定doOther方法

  1. 如果在doSome()方法中调用doOther()方法时,doSome()方法在事务A中运行,那么doOther()方法也会在事务A中运行
  2. 如果doSome()方法没在任何事务中运行,那么doOther()方法同样在无事务下运行

You jump! I jump!

c)PROPAGATION_REQUIRES_NEW

表示指定的方法总是新开启一个事务,如果当前存在事务,则将当前事务挂起,直到新开启的事务执行完毕。

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

常量TIMEOUT_DEFAULT定义了事务底层默认的超时时限,表示一个事务方法最长的执行时间。

如果方法执行时超过了指定时限,那么事务就进行回滚。单位是秒,整数值,默认是-1。

注意:影响事务超时时限的因素比较多,且超时的时间计算点较复杂。所以,该值一般用默认的就行了,不用设置。

6.2.4Spring中事务提交、回滚的时机

  1. 当业务方法执行成功,没有抛出异常时,Spring会在方法执行后自动提交事务。(调用事务管理器的commit()方法)
  2. 当业务方法执行时,抛出运行时异常或者ERROR,Spring就会执行事务回滚。(调用事务管理器的rollback()方法)
    • 运行时异常:程序运行时抛出的异常,RuntimeException及其子类,如:NullPointException、NumberFormatException
  3. 当业务方法执行时,抛出受检异常,Spring就会提交事务
    • 受检异常:写代码时不许处理的异常,如:IOException、SQLException

6.2.5总结Spring的事务

1)管理事务的是事务管理器

2)Spring的事务管理是一个统一模型,使用步骤固定:

  1. 指定要使用哪一个事务管理器接口实现类,使用<bean id="xxx" class="...DataSourceTransactionManager"></bean>
  2. 指定哪些类、哪些方法需要加入事务
  3. 指定需要加入的事务类型
    1. 隔离级别
    2. 传播行为
    3. 超时时限

6.3实例演示Spring事务管理

实例项目:模拟用户购买商品

本项目中要实现模拟用户下订单,购买商品的功能。

分为两个步骤:

  1. 向订单表中添加销售记录
  2. 从商品表中减少相应商品库存量

6.3.1环境搭建

1.建表

本次项目需要创建两个数据库表:t_sale(销售表),t_commodity(商品表)

t_sale 销售表

drop table if exists `t_sale`;
create table `t_sale`(
	`id` int primary key auto_increment,#销售记录编号
    `cid` int not null,#商品编号
    `nums` int#购买商品数量
);

t_commodity 商品表

drop table if exists `t_commodity`;
create table `t_commodity`(
	`id` int primary key,#商品编号
    `name` varchar(255),#商品名称
    `count` int,#商品货存
    `price` float#商品价格
);
insert into `t_commodity`(id,name,count,price) values
(1001,'酱烧猪蹄',10,30),
(1002,'红烧牛肉',20,20);
select * from `t_commodity`;
2.新建maven项目,添加所需依赖

新建子maven模块spring-07-transaction,所需依赖在父模块中已添加过,如下:

<dependencies>
    <!-- 单元测试 -->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</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.2.6.RELEASE</version>
    </dependency>
    <!-- AspectJ:AOP -->
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
      <version>5.2.9.RELEASE</version>
    </dependency>
    <!-- 做Spring事务需要用到的 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>
    <!-- 做Spring事务需要用到的 -->
    <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集成的依赖,由mybatis提供 -->
    <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>
3.创建实体类

在main/java/com/tsccg/entity目录下,分别创建销售表和商品表对应的实体类

1)Sale:销售表实体类

package com.tsccg.entity;

/**
 * @Author: TSCCG
 * @Date: 2021/09/29 17:41
 * 销售表对应实体类
 */
public class Sale {
    private Integer id;//销售记录编号
    private Integer cid;//商品编号
    private Integer nums;//商品购买数量

    public Sale() {
    }

    public Sale(Integer id, Integer cid, Integer nums) {
        this.id = id;
        this.cid = cid;
        this.nums = nums;
    }
	...get、set方法
}

2)Commodity:商品表实体类

package com.tsccg.entity;

/**
 * @Author: TSCCG
 * @Date: 2021/09/29 17:43
 * 商品表对应实体类
 */
public class Commodity {
    private Integer id;//商品编号
    private String name;//商品名称
    private Integer count;//商品货存
    private Float price;//商品价格

    public Commodity() {
    }

    public Commodity(Integer id, String name, Integer count, Float price) {
        this.id = id;
        this.name = name;
        this.count = count;
        this.price = price;
    }
	...get、set方法
}
4.创建dao接口和mapper文件

在main/java/com/tsccg/dao目录下,分别创建SaleDao和CommodityDao接口,并分别为其创建mapper文件。

SaleDao接口:

public interface SaleDao {
    //添加销售记录
    public int insertSale(Sale sale);
    //删除销售记录
    public int deleteSale(Integer id);
}

SaleDao.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.tsccg.dao.SaleDao">
    <insert id="insertSale">
        insert into t_sale(cid,nums) values(#{cid},#{nums})
    </insert>
    <delete id="deleteSale">
        delete from t_sale where id = #{id}
    </delete>
</mapper>

CommodityDao接口:

public interface CommodityDao {
    /**
     * 根据商品编号查询指定商品信息
     * @param id 商品编号
     * @return 指定商品信息
     */
    Commodity selectCommodity(Integer id);
    /**
     * 更新库存
     * @param comm 表示本次用户购买的商品信息
     * @return 返回处理结果数
     */
    int updateCount(Commodity comm);
}

CommodityDao.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.tsccg.dao.CommodityDao">
    <select id="selectCommodity" resultType="com.tsccg.entity.Commodity">
        select id,name,count,price from t_commodity where id = #{id}
    </select>
    <update id="updateCount">
        update t_commodity set count = count - #{count} where id = #{id}
    </update>
</mapper>
5.创建MyBatis主配置文件

在main/resources目录下,新建MyBatis主配置文件:mybatis.xml

<?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>
    <!-- 指定mapper文件的位置-->
    <mappers>
        <!-- name:包名,这个包名下所有的mapper.xml文件一次都能加载 -->
        <package name="com.tsccg.dao"/>
    </mappers>
</configuration>
6.创建自定义异常类

在main/java/com/tsccg/exception目录下新建一个运行时异常类NotEnoughException

自定义异常类的作用是处理商品库存不足时的情况。

package com.tsccg.exception;

/**
 * @Author: TSCCG
 * @Date: 2021/09/29 19:32
 * 自定义运行时异常类
 * 当商品库存不足时,抛出该异常
 */
public class NotEnoughException extends RuntimeException{
    public NotEnoughException() {
    }
    public NotEnoughException(String message) {
        super(message);
    }
}
7.创建service接口及其实现类

在main/java/com/tsccg/service目录下新建BuyCommodityService接口

public interface BuyCommodityService {
    /**
     * 购买商品服务
     * @param cId 商品编号
     * @param nums 购买商品数量
     */
    void buy(Integer cId,Integer nums);
}

在同目录下新建impl01包,编写其实现类BuyCommodityServiceImpl

package com.tsccg.service.impl01;

import com.tsccg.dao.CommodityDao;
import com.tsccg.dao.SaleDao;
import com.tsccg.entity.Commodity;
import com.tsccg.entity.Sale;
import com.tsccg.exception.NotEnoughException;
import com.tsccg.service.BuyCommodityService;

/**
 * @Author: TSCCG
 * @Date: 2021/09/29 19:24
 */
public class BuyCommodityServiceImpl implements BuyCommodityService {
    //定义两个dao属性
    private SaleDao saleDao;
    private CommodityDao commDao;

    @Override
    public void buy(Integer cId, Integer nums) {
        //1.添加销售记录
        Sale sale = new Sale();//创建销售表实体类
        sale.setCid(cId);
        sale.setNums(nums);
        int result1 = saleDao.insertSale(sale);
        System.out.println(result1 > 0 ? "添加销售记录成功" : "添加销售记录失败");
        
        //2.进行验证
        Commodity oldComm = commDao.selectCommodity(cId);//查询指定编号的商品信息
        if (oldComm == null) {
            throw new NullPointerException("编号为" + cId + "的商品不存在");
        } else if (oldComm.getCount() < nums) {
            throw new NotEnoughException("编号为" + cId + "的商品库存不足");
        }
        
        //3.更新商品库存
        Commodity newComm = new Commodity();
        newComm.setId(cId);
        newComm.setCount(nums);
        int result2 = commDao.updateCount(newComm);
        System.out.println(result2 > 0 ? "更新商品库存成功" : "更新商品库存失败");
    }
    //通过set注入给saleDao属性赋值
    public void setSaleDao(SaleDao saleDao) {
        this.saleDao = saleDao;
    }
    //通过set注入给commDao属性赋值
    public void setCommDao(CommodityDao commDao) {
        this.commDao = commDao;
    }
}
8.创建Spring的配置文件

在main/resources目录下新建spring配置文件applicatonContext.xml和jdbc属性配置文件jdbc.properties

jdbc属性配置文件:jdbc.properties

jdbc.mysql.driver=com.mysql.jdbc.Driver
jdbc.mysql.url=jdbc:mysql://localhost:3306/db_mybatis?characterEncoding=utf8&useSSL=false
jdbc.mysql.username=root
jdbc.mysql.password=123456
jdbc.mysql.maxActive=20

spring配置文件:applicationContext.xml

在Spring配置文件中声明mybatis对象,把mybatis对象的管理交给Spring

  1. 独立的连接池类对象
  2. SqlSessionFactory对象
  3. Dao对象
  4. 自定义的service对象
<?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"/>
    <!-- 声明数据源DataSource -->
    <bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <!--使用set注入给DruidDataSource提供连接数据库信息-->
        <property name="driverClassName" value="${jdbc.mysql.driver}"/>
        <property name="url" value="${jdbc.mysql.url}"/>
        <property name="username" value="${jdbc.mysql.username}"/>
        <property name="password" value="${jdbc.mysql.password}"/>
        <!--maxActive:连接池最多容纳的连接对象数目,默认20个-->
        <property name="maxActive" value="${jdbc.mysql.maxActive}"/>
    </bean>
    <!--声明SqlSessionFactory对象-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="myDataSource"/>
        <property name="configLocation" value="classpath:mybatis.xml"/>
    </bean>
    <!--声明dao代理对象-->
    <!--不需要指定id属性值-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!--指定SqlSessionFactory对象的id-->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
        <!--指定dao接口所在包名
            MapperScannerConfigurer会扫描这个包名下的所有接口,
            然后把每个接口都执行一次getMapper()方法,得到每个接口的dao对象,
            然后把创建好的dao对象都放入到Spring的容器中,dao对象的名字是各自对应dao接口的首字母小写
        -->
        <property name="basePackage" value="com.tsccg.dao"/>
    </bean>
    <!--声明service对象-->
    <bean id="buyService" class="com.tsccg.service.impl01.BuyCommodityServiceImpl">
        <property name="saleDao" ref="saleDao"/>
        <property name="commDao" ref="commodityDao"/>
    </bean>
</beans>
9.编写测试代码

测试1:购买不存在的商品

@Test
public void test01() {
    String config = "applicationContext.xml";
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);
    BuyCommodityService buyService = ac.getBean("buyService", BuyCommodityService.class);
    buyService.buy(1003,5);
}

测试2:购买过量的商品

@Test
public void test01() {
    String config = "applicationContext.xml";
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);
    BuyCommodityService buyService = ac.getBean("buyService", BuyCommodityService.class);
    buyService.buy(1001,100);
}

测试3:购买合适数量的商品

@Test
public void test01() {
    String config = "applicationContext.xml";
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);
    BuyCommodityService buyService = ac.getBean("buyService", BuyCommodityService.class);
    buyService.buy(1001,5);
}

以上测试中,当发生异常时,商品货存不会更新,但销售记录仍会添加。也就是发生了“做假账”的行为。

为了避免这种情况发生,我们需要使用到事务来保证操作的一致性。同时,我们不希望在源代码上添加额外的非业务功能代码,所以,我们就要使用Spring的事务管理模型来处理事务。

Spring框架中提供了两种事务处理方案:

  1. 使用Spring的事务注解,适合中小项目使用
  2. 使用AspectJ的AOP配置,适合大型项目使用

6.3.2使用Spring事务注解管理事务

Spring框架使用自身的aop来给业务方法增加事务功能,使用@Transactional注解。适合中小项目。

1.@Transactional注解的属性
  1. propagation:用于设置事务的传播行为。该属性类型为Propagation枚举,默认值为:Propagation.REQUIRED
  2. isolation:用于设置事务的隔离级别。该属性类型为Isolation枚举,默认值为:Isolation.DEFAULT
  3. readOnly:用于设置该方法对数据库的操作是否是只读的。该属性类型为boolean,默认值为:false
  4. timeout:用于设置本次操作与数据库连接的超时时限。单位为秒,类型为int,默认值为-1,即没有时限限制
  5. rollbackFor:指定需要回滚的异常类
    1. 即抛出指定的异常类型的异常时进行回滚操作
    2. 类型为Class[],需要指定异常类的class属性值,默认为空数组。当然,如果只有一个异常类,可以不使用数组。
    3. 处理逻辑:Spring框架会先检查方法中抛出的异常是不是在rollbackFor指定的异常列表中
      1. 如果抛出的异常在指定的异常列表中,不管是什么类型的异常,都会回滚
      2. 如果抛出的异常不在指定的异常列表中,Spring会判断该异常是不是运行时异常,如果是则回滚,如果不是则提交事务。
  6. rollbackForClassName:指定需要回滚的异常类类名
    1. 和rollbackFor一样的功能,只不过是当抛出与该异常类的名称相同的异常时进行回滚操作。
    2. 类型为String[],默认为空数组
    3. 当然,如果只有一个异常类,可以不使用数组。
  7. noRollbackFor:指定不需要回滚的异常类。与rollbackFor功能相反,类型为Class[]。
  8. noRollbackForClassName:指定不需要回滚的异常类类名。与rollbackForClassName功能相反,类型为String[]。

注意:

  1. 若@Transactional注解在方法上,该方法必须是public的。Spring会忽略掉所有非public方法上的@Transactional注解,如果注解方法是非public的,那么虽然Spring不会报错,但是Spring不会将指定事务织入该方法中。
  2. 若@Transaction注解在类上,则表示该类上所有的方法均将在执行时织入事务。
2.使用事务注解方案的步骤

1.在spring配置文件中声明事务管理器对象

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

2.在spring配置文件中开启事务注解驱动

告诉Spring框架,使用注解的方式管理事务。

<!--开启事务注解驱动
    告诉Spring使用注解管理事务,创建代理对象
    annotation-driven:引入tx的那个
    transaction-manager:属性值为事务管理器bean的id
-->
<tx:annotation-driven transaction-manager="transactionManager"/>

开启之后,Spring会在内部使用AOP机制,创建@Transactional所在的类的代理对象,给业务方法加入事务的功能。

怎么加的呢?其实就是使用@Around环绕通知注解,在业务方法之前开启事务,在业务方法执行之后进行提交或者回滚事务。

@Around("切入点表达式")
Object myFunction(){
    Spirng开启事务
    try{
        buy(1001,5);
        事务管理器.commit();
    } catch(Exception e) {
        事务管理器.rollback();
    }
}

3.在业务方法上添加@Transactional注解

public class BuyCommodityServiceImpl implements BuyCommodityService {
    private SaleDao saleDao;
    private CommodityDao commDao;
    //添加注解
    @Transactional(
            //指定传播行为
            propagation = Propagation.REQUIRED,
            //指定隔离级别
            isolation = Isolation.DEFAULT,
            //指定需要回滚的异常
            rollbackFor = {NullPointerException.class,NotEnoughException.class}
    )
    @Override
    public void buy(Integer cId, Integer nums) {
        System.out.println("====buy方法执行开始====");
        //添加销售记录
        Sale sale = new Sale();
        sale.setCid(cId);
        sale.setNums(nums);
        int result1 = saleDao.insertSale(sale);
        System.out.println(result1 > 0 ? "添加销售记录成功" : "添加销售记录失败");

        //进行验证
        Commodity oldComm = commDao.selectCommodity(cId);
        if (oldComm == null) {
            throw new NullPointerException("编号为" + cId + "的商品不存在");
        } else if (oldComm.getCount() < nums) {
            throw new NotEnoughException("编号为" + cId + "的商品库存不足");
        }
        //更新商品库存
        Commodity newComm = new Commodity();
        newComm.setId(cId);
        newComm.setCount(nums);
        int result2 = commDao.updateCount(newComm);
        System.out.println(result2 > 0 ? "更新商品库存成功" : "更新商品库存失败");
        System.out.println("====buy方法执行结束====");
    }
    public void setSaleDao(SaleDao saleDao) {
        this.saleDao = saleDao;
    }

    public void setCommDao(CommodityDao commDao) {
        this.commDao = commDao;
    }
}

4.进行测试

1)商品编号和数量都合理,没有异常发生

@Test
public void test02() {
    String config = "applicationContext.xml";
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);
    BuyCommodityService buyService = ac.getBean("buyService", BuyCommodityService.class);
    System.out.println("buyService是代理对象:" + buyService.getClass().getName());
    buyService.buy(1002,5);
}

结果:

buyService是代理对象:com.sun.proxy.$Proxy17
====buy方法执行开始====
添加销售记录成功
更新商品库存成功
====buy方法执行结束====

功能正常

2)商品编号不存在,抛出异常

@Test
public void test02() {
    String config = "applicationContext.xml";
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);
    BuyCommodityService buyService = ac.getBean("buyService", BuyCommodityService.class);
    System.out.println("buyService是代理对象:" + buyService.getClass().getName());
    buyService.buy(1005,5);
}

结果:

buyService是代理对象:com.sun.proxy.$Proxy17
====buy方法执行开始====
添加销售记录成功

java.lang.NullPointerException: 编号为1005的商品不存在
    at ...
    at ...
    ...

由测试结果可见,当业务方法开始执行后:

  1. 先执行insert向销售表中插入了一条销售记录saleDao.insertSale(sale);
  2. 在向商品表中验证指定商品时,没有发现编号为1005的商品,抛出空指针异常
  3. 检验到异常抛出后,Spring调用事务管理器执行回滚操作,将insert操作rollback,销售表中无记录添加
3.佐证事务起了作用

思路:

  1. 销售表的id字段是设置为自增的auto_increment,每个数字只能使用一次。

  2. 在发生异常前,向销售表中插入了一条销售记录,占用了一个数字;

  3. 发生异常后,如果执行了回滚操作,那么插入操作会被撤销,所占用的数字却是不可再被复用的。

我们再来执行一次正常的购买操作:

@Test
public void test02() {
    String config = "applicationContext.xml";
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);
    BuyCommodityService buyService = ac.getBean("buyService", BuyCommodityService.class);
    System.out.println("buyService是代理对象:" + buyService.getClass().getName());
    buyService.buy(1001,5);
}

销售表中id为2的记录被使用过了,由此可证,事务起了作用

mysql> select * from t_sale;
+----+------+------+
| id | cid  | nums |
+----+------+------+
|  1 | 1002 |    5 |
|  3 | 1001 |    5 |
+----+------+------+
2 rows in set (0.00 sec)
4.@Transaction注解的属性可省略不写

当只有@Transaction注解,没有设定其属性值时:

  1. 默认的传播行为是REQUIRED
  2. 默认的隔离级别是DEFAULT
  3. 默认抛出运行时异常时,进行回滚操作
public class BuyCommodityServiceImpl implements BuyCommodityService {
    private SaleDao saleDao;
    private CommodityDao commDao;
//    @Transactional(
//            //传播行为
//            propagation = Propagation.REQUIRED,
//            //隔离级别
//            isolation = Isolation.DEFAULT,
//            //指定需要回滚的异常
//            rollbackFor = {NullPointerException.class,NotEnoughException.class}
//    )
    @Transactional//属性可省略不写,全部按默认
    @Override
    public void buy(Integer cId, Integer nums) {
        System.out.println("====buy方法执行开始====");
        //添加销售记录
        Sale sale = new Sale();
        sale.setCid(cId);
        sale.setNums(nums);
        int result1 = saleDao.insertSale(sale);
        System.out.println(result1 > 0 ? "添加销售记录成功" : "添加销售记录失败");
        ...
}

测试:购买过量商品

@Test
public void test02() {
    String config = "applicationContext.xml";
    ApplicationContext ac = new ClassPathXmlApplicationContext(config);
    BuyCommodityService buyService = ac.getBean("buyService", BuyCommodityService.class);
    System.out.println("buyService是代理对象:" + buyService.getClass().getName());
    buyService.buy(1001,500);//过量商品
}

由测试结果可见,当抛出我们自定义的运行时异常时,即使没有提前指定,也会执行回滚。

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

这种方案是用AspectJ框架实现的AOP功能,在Spirng配置文件中向指定业务方法织入事务功能。

这种方式能够使得业务方法和事务功能完全分离,耦合度低。

这种方案适合有很多类,很多方法,需要大量配置事务的大型项目。同时,由于每个目标类都需要配置事务代理,当目标类较多时,配置文件会非常臃肿。

实现步骤:

1.添加AspectJ依赖

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

2.声明事务管理器对象

不管使用哪种方案,都必须声明事务管理器对象

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

3.声明事务方法的事务属性(声明事务类型)

  1. 隔离级别
  2. 传播行为
  3. 需要执行回滚的异常等
<!--声明业务方法的事务类型
		tx:advice:
            tx:表示事务
            advice选末尾是tx那个
        id:自定义名称,表示当前<tx:advicd></tx:advice>之间的配置内容
        transaction-manager:属性值是事务管理器对象bean的id值
    -->
<tx:advice id="myAdvice" transaction-manager="transactionManager">
    <!--配置事务的属性-->
    <tx:attributes>
        <!--tx:method:给具体的某个方法指定事务属性,可以有多个
                name:完整的业务方法名,不含有包名和类名,可以使用通配符
		-->
        <tx:method name="buy"
                   propagation="REQUIRED"
                   isolation="DEFAULT"
                   rollback-for="java.lang.NullPointerException,
                                 com.tsccg.exception.NotEnoughException"/>
        
        <!--可以使用通配符指定多个业务方法的事务属性-->
        <!--指定所有增加方法:add-->
        <tx:method name="add*" propagation="SUPPORTS"/>
        <!--指定所有删除方法:remove delete-->
        <tx:method name="remove*" propagation="REQUIRES_NEW"/>
        <!--指定所有查询方法:search、find、query-->
        <tx:method name="search*" propagation="REQUIRES_NEW"/>
        <!--指定所有更新方法:update、modify-->
        <tx:method name="update*" propagation="SUPPORTS"/>
        <!--只有*:指定以上所有业务方法以外的方法-->
        <tx:method name="*" read-only="true"/>
    </tx:attributes>
</tx:advice>

4.配置AOP

指定将配置好的事务功能,织入给哪些类,哪些方法

<!--AOP配置:通知应用的切入点-->
<aop:config>
    <!--定义切入点表达式
        id:自定义切入点表达式的名称,唯一值
        expression:切入点表达式,指定哪些类哪些方法要使用事务,aspectJ会创建其代理对象

        com.service.AddService
        com.crm.service.RemoveService
        com.tsccg.service.impl01.BuyCommodityServiceImpl

        execution(* *..service..*.*(..)):指定所有service中的所有类的所有方法
    -->
    <aop:pointcut id="servicePt" expression="execution(* *..service..*.*(..))"/>
    <!--配置增强器:关联advice和pointcut,将配置好的事务功能,织入指定的业务方法
            advice-ref:上面声明的业务方法的事务类型
            pointcut-ref:切入点表达式的id
    -->
    <aop:advisor advice-ref="myAdvice" pointcut-ref="servicePt"/>
</aop:config>

5.测试

1)正常购买一件商品,无异常抛出

2)购买过量商品,抛出异常

7.在web项目中使用Spring

7.1在web项目中使用Spring的步骤

在web项目中使用Spring框架,首先要解决在Servlet中获取到Spring容器的问题。只要在Servlet中能获取到Spring容器,就能从容器中获取到service对象,从而调用dao,实现访问数据库。

下面将上面Spring整合MyBatis的例子修改为web项目

1.新建一个web项目​

新建一个web类型的maven子模块:spring-09-web,使用模板:maven-archetype-webapp

然后完善maven结构目录

main|--java
	|--resources
	|--webapp

2.添加依赖

复制Spring整合mybatis项目中的依赖和基本配置,并新增servlet和jsp依赖

完整pom.xml配置内容如下:

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.tsccg</groupId>
  <artifactId>spring-09-web</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>
<!--添加依赖-->
  <dependencies>
    <!-- 单元测试 -->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</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.2.6.RELEASE</version>
    </dependency>
    <!-- AspectJ:AOP -->
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
      <version>5.2.9.RELEASE</version>
    </dependency>
    <!-- 做Spring事务需要用到的 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>
    <!-- 做Spring事务需要用到的 -->
    <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集成的依赖,由mybatis提供 -->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>1.3.1</version>
    </dependency>
    <!-- mysql驱动 -->
    <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.49</version>
    </dependency>
    <!-- 阿里公司的数据库连接池:德鲁伊 -->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.1.12</version>
    </dependency>
      
    <!--新增servlet依赖 -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.1.0</version>
      <scope>provided</scope>
    </dependency>
    <!--新增jsp依赖 -->
    <dependency>
      <groupId>javax.servlet.jsp</groupId>
      <artifactId>javax.servlet.jsp-api</artifactId>
      <version>2.3.1</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>


  <!--配置插件-->
  <build>
    <resources>
      <!-- 目的是把src/main/java目录中的mapper文件包含到输出结果中,输出到target/classes目录中 -->
      <resource>
        <directory>src/main/java</directory><!--mapper所在的目录-->
        <includes><!--包括目录下的.properties,.xml 文件都会扫描到-->
          <include>**/*.properties</include>
          <include>**/*.xml</include>
        </includes>
        <filtering>false</filtering>
      </resource>
      <!-- 目的是把src/main/resources目录中的xml文件包含到输出结果中,输出到target/classes目录中 -->
      <resource>
        <directory>src/main/resources</directory><!--主配置文件所在的目录-->
        <includes><!--包括目录下的.properties,.xml 文件都会扫描到-->
          <include>**/*.properties</include>
          <include>**/*.xml</include>
        </includes>
        <filtering>false</filtering>
      </resource>
    </resources>
  </build>
</project>

3.复制Spring整合mybatis项目的代码

直接将Spring整合mybatis项目的java和resources文件夹复制到当前项目中覆盖

4.定义index页面

在webapp目录下新建一个index.jsp文件,编写一个表单页面,携带参数,申请访问后台的servlet

index.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>注册</title>
    <style type="text/css">
        * {
            font-size: 20px;
        }
    </style>
</head>
<body>
    <center>
        <h2>注册学生信息</h2>
        <form action="/MyWeb/register" method="get">
            <table>
                <tr>
                    <td>id:</td>
                    <td><input type="text" name="id" /></td>
                </tr>
                <tr>
                    <td>姓名:</td>
                    <td><input type="text" name="name" /></td>
                </tr>
                <tr>
                    <td>邮箱:</td>
                    <td><input type="text" name="email" /></td>
                </tr>
                <tr>
                    <td>年龄:</td>
                    <td><input type="text" name="age" /></td>
                </tr>
                <tr>
                    <td><input type="submit" value="注册" /></td>
                </tr>
            </table>
        </form>
    </center>
</body>
</html>

5.创建servlet

在创建servlet之前,先检查/webapp/WEB-INF目录下的web.xml文件,如果自动创建的内容如下,则需要更换为新的版本。

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>Archetype Created Web Application</display-name>
</web-app>

小技巧:在项目结构中,先删除原本的低版本web.xml配置文件,再创建一个高版本的web.xml配置文件。需要注意的是,创建时,需要修改一下名称,不然无法指定版本,创建完成后再修改回来。

新建一个servlet,全限定名为:com.tsccg.controller.RegisterServlet,别名为:register

然后在RegisterServlet类的doGet方法中,向数据库中插入一条记录并返回处理结果。

RegisterServlet:

public class RegisterServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //1.获取请求包中的参数信息
        String userId = request.getParameter("id").trim();
        String userName = request.getParameter("name").trim();
        String userEmail = request.getParameter("email").trim();
        String userAge = request.getParameter("age").trim();
        //2.创建Spring容器,获取service对象
        String config = "applicationContext.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(config);
        System.out.println("容器对象的信息:" + ac);
        StudentService service = ac.getBean("studentService", StudentService.class);
        //创建一个Student实体类对象,将参数赋给它
        Student student = new Student();
        student.setId(Integer.parseInt(userId));
        student.setName(userName);
        student.setEmail(userEmail);
        student.setAge(Integer.parseInt(userAge));
        //调用service,通过dao向数据库中插入该数据
        int result = service.addStudent(student);
        //3.将执行结果写入请求作用域对象中
        request.setAttribute("result",result > 0 ? "注册成功":"注册失败");
        //请求转发,调用result.jsp将响应结果写入响应协议包中
        request.getRequestDispatcher("/result.jsp").forward(request,response);
    }
}

6.创建响应页面

在webapp目录下,新建一个result.jsp文件,编写响应页面

result.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
    <style type="text/css">
        * {
            color:red;
            font-size: 20px;
        }
    </style>
</head>
<body>
    ${requestScope.result}
</body>
</html>

7.发布网站

指定网站别名为MyWeb

8.开启服务器,通过浏览器发送请求

7.2在web项目中使用Spring容器的问题

我们通过浏览器再发送一次请求:

可以发现,发送两次请求,创建了两个Spring容器:

Spring容器一次性就可以创建我们所需的所有对象,无需每次都创建,现在的这种情况是不被允许的。

解决方法:使用监听器,在监听器中创建一个Spring容器对象,然后将Spring容器对象放入全局作用域对象中。这样,我们就不需要在servlet中创建Spring容器了,需要时,直接从全局作用域对象中拿就可以了。

7.3使用Spring的监听器

监听器有两个作用:

  1. 创建Spring容器对象:执行ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
  2. 将容器对象放入全局作用域对象中:ServletContext.setAttribute(key,ac)

监听器可以自己创建,也可以使用Spring框架中提供的ContextLoaderListener。

7.3.1ContextLoaderListener源码分析

若要实现在ServletContext初始化时创建Spring容器,就需要使用监听器接口ServletContextListener对ServletContext进行监听。

Spring为该监听器接口定义了一个实现类:ContextLoaderListener,完成了两个很重要的工作:

  1. 创建监听器对象
  2. 将容器对象放入到ServletContext的空间中

使用时在web.xml中注册该监听器:

<!--注册监听器ContextLoaderListener-->
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

打开ContextLoaderListener的源码,可以看到有四个方法:

  1. ContextLoaderListener():无参构造
  2. ContextLoaderListener(WebApplicationContext):有参构造
  3. contextInitialized(ServletContextEvent):初始化方法【重要】
  4. contextDestroyed(ServletContextEvent):销毁方法

其中,初始化方法contextInitialized()比较重要,就是在其内部实现创建容器对象并放入全局作用域

追踪initWebApplicationContext()方法,可以看到其具体实现步骤

在将Spring容器对象放入全局作用域时,key值为一个常量:WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE

追踪此常量,可以发现,就是Spring容器对象的名字加上.ROOT的字符串

7.3.2使用Spring监听器步骤

1.添加Spring监听器依赖

在pom.xml文件中加入如下依赖,才可以使用Spring监听器对象

<!--Spring监听器-->
<dependency> 
    <groupId>org.springframework</groupId> 
    <artifactId>spring-web</artifactId> 
    <version>5.2.5.RELEASE</version> 
</dependency>
2.注册监听器

在web.xml中注册监听器:

<!--注册监听器ContextLoaderListener-->
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

当服务器启动时,会自动创建Spring监听器对象。在创建该对象时,默认会读取/webapp/WEB-INF/applicationContext.xml文件,也就是Spring配置文件。

为什么需要读取Spring配置文件?因为需要在监听器中创建ApplicationContext容器对象,需要加载Spring配置文件。

而我们的Spring配置文件路径为:/main/resources/applicationContext.xml,在运行程序时,监听器按照默认路径找不到配置文件,就会报异常。

我们可以使用<context-param>标签重新指定Spring配置文件的位置。

<!--自定义Spring配置文件的路径-->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext.xml</param-value>
</context-param>

web.xml完整配置:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <servlet>
        <servlet-name>RegisterServlet</servlet-name>
        <servlet-class>com.tsccg.controller.RegisterServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>RegisterServlet</servlet-name>
        <url-pattern>/register</url-pattern>
    </servlet-mapping>
    <!--自定义Spring配置文件的路径-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>
    <!--注册监听器ContextLoaderListener-->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
</web-app>
3.获取Spring容器对象

在Servlet中获取Spring容器对象的常用方式有两种:

1)直接从全局作用域中获取

从Spring提供的监听器ContextLoaderListener源码可知,容器对象在全局作用域中的key值为:WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE。所以,可以直接通过ServletContext的getAttribute()方法,根据key值获取容器对象。

WebApplicationContext ac = null;
String key = WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE;
Object attr = getServletContext().getAttribute(key);
if (attr != null) {
    ac = (WebApplicationContext) attr;
}

2)通过Spring提供的工具类获取

Spring提供了一个工具类WebApplicationContextUtils,其中有一个方法专门用来从ServletContext【全局作用域】中获取Spring容器对象:getRequiredWebApplicationContext(ServletContext sc)

调用方式:

//获取当前全局作用域对象
ServletContext sc = getServletContext();
//使用工具类获取Spring容器对象
WebApplicationContext ac = WebApplicationContextUtils.getRequiredWebApplicationContext(sc);

查其源码,看其调用关系,就可以发现,基本和前面获取Spring容器对象的方式一样

getRequiredWebApplicationContext(ServletContext sc)方法源码:

public static WebApplicationContext getRequiredWebApplicationContext(ServletContext sc) throws IllegalStateException {
    //调用getWebApplicationContext(ServletContext sc)方法,输入当前全局作用域对象,获取Spring容器对象
   WebApplicationContext wac = getWebApplicationContext(sc);
   if (wac == null) {
      throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener registered?");
   }
    //将Spring容器对象返回
   return wac;
}

getWebApplicationContext(ServletContext sc)方法源码:

@Nullable
public static WebApplicationContext getWebApplicationContext(ServletContext sc) {
    //调用getWebApplicationContext(ServletContext sc, String attrName)方法,输入全局作用域对象和Spring容器对象在其中的key值
    return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
}

getWebApplicationContext(ServletContext sc, String attrName)方法源码:

@Nullable
public static WebApplicationContext getWebApplicationContext(ServletContext sc, String attrName) {
    Assert.notNull(sc, "ServletContext must not be null");
    //根据key值,从全局作用域对象中获取Spring容器对象
    Object attr = sc.getAttribute(attrName);
    if (attr == null) {
        return null;
    }
	...
    //将Spring容器对象返回
    return (WebApplicationContext) attr;
}
4.测试能否起作用

开启服务器,连续发送两次请求

查看后台,发现两次使用的Spring容器对象是同一个,测试成功。

posted @ 2021-10-03 03:29  TSCCG  阅读(175)  评论(0编辑  收藏  举报