Spring 学习笔记

简介

Spring 概述

​ Spring 是分层的 Java SE/EE 应用 full-stack 轻量级开源框架,以 IoC(Inverse Of Control)控制反转 和 AOP(Aspect Oriented Programming)面向切面编程为内核,提供了展现层 Spring MVC 和持久层 Spring JDBC 以及业务层事务管理等技术,还能整合开源的第三方框架和类库,是使用最多的 Java EE 企业应用开源框架。

​ 我们平常说的 Spring 指的是 Spring Framework,其为 Java 程序提供全面的基础架构支持,Spring 处理基础结构,使得我们可以专注于业务本身。是非入侵式框架(导入项目不会破坏原有项目代码)

​ Spring 之父:Rod Johnson

Framework Modules 组成

​ Spring Framework 由组成大约 20 个模块的 feature 组成,这些模块分为:

  • Core Container 核心容器

  • Date Access/Integration 数据访问/整合

  • Web

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

  • Instrumentation 检测

  • Messaging 消息

  • Test

img

IOC

导论

​ 在面向对象编程中,我们经常处理的问题就是解耦,程序的耦合性越低,表名这个程序的可读性以及可维护性越高。IoC(Inversion of Control) 控制反转,就是常用的面向对象编程的设计原则,使用这个原则我们可以降低耦合性。其中依赖注入是控制反转最常见的实现

什么是程序耦合

  • 耦合

    程序间的依赖关系

    ​ 包括:类之间的依赖

    ​ 方法之间的依赖

  • 解耦

    降低程序间的依赖关系

    我们在开发中,有些依赖关系是必须的,有些依赖关系可以通过优化代码来解除的。

所以实际开发中应做到:编译期不依赖,运行时才依赖

解耦思路:

  1. 使用反射来创建对象,而避免使用 new 关键词
  2. 通过读取配置文件来获取要创建的对象全限定类名

范例:JDBC 连接数据库

public static void main(String[] args) throws Exception{
    //1.注册驱动
    /*使用new对象的方式注册驱动
    DriverManager.registerDriver(new com.mysql.jdbc.Driver());*/
    /*使用反射方式创建对象注册驱动,此时配置内容只作为一个字符串传递
    Class.forName("com.mysql.jdbc.Driver");*/
    
    //而通过读取配置文件的方式,解决上面将字符串在代码中写死的问题,便于修改配置
    Properties properties = new Properties();
    properties.load(new FileInputStream("src/main/resources/data.properties"));
    //略...
    //2.获取连接
    Connection conn = DriverManager.getConnection(url,user,password);
    //3.获取操作数据库的预处理对象
    PrepareStatement ps = conn.prepareStatement("select * from tb_students");
    //4.执行SQL,获取结果集
    result = ps.executeQuery();
    //5.遍历结果集
    while(result.next()){
        int no = result.getInt("no");
        String name = result.getString("name");
		System.out.println(no + "," + name);
    //6.释放资源
    result.close();
    ps.close();
    conn.close();
}

​ 传统的 JDBC 获取连接方式也是为了解耦而使用读取配置文件的方式配置数据源。

耦合性(Coupling),也叫耦合度,是对模块间关联程度的度量。耦合的强弱取决于模块间接口的复杂性、调 用模块的方式以及通过界面传送数据的多少。模块间的耦合度是指模块之间的依赖关系,包括控制关系、调用关系、数据传递关系。模块间联系越多,其耦合性越强,同时表明其独立性越差( 降低耦合性,可以提高其独立 性)。耦合性存在于各个领域,而非软件设计中独有的,但是我们只讨论软件工程中的耦合。

在软件工程中,耦合指的就是就是对象之间的依赖性。对象之间的耦合越高,维护成本越高。因此对象的设计应使类和构件之间的耦合最小。软件设计中通常用耦合度和内聚度作为衡量模块独立程度的标准。划分模块的一个 准则就是高内聚低耦合

它有如下分类:

​ (1) 内容耦合。当一个模块直接修改或操作另一个模块的数据时,或一个模块不通过正常入口而转入另 一个模块时,这样的耦合被称为内容耦合。内容耦合是最高程度的耦合,应该避免使用之。 (2) 公共耦合。两个或两个以上的模块共同引用一个全局数据项,这种耦合被称为公共耦合。在具有大 量公共耦合的结构中,确定究竟是哪个模块给全局变量赋了一个特定的值是十分困难的。 (3) 外部耦合 。一组模块都访问同一全局简单变量而不是同一全局数据结构,而且不是通过参数表传 递该全局变量的信息,则称之为外部耦合。

​ (4) 控制耦合 。一个模块通过接口向另一个模块传递一个控制信号,接受信号的模块根据信号值而进 行适当的动作,这种耦合被称为控制耦合。

​ (5) 标记耦合 。若一个模块 A 通过接口向两个模块 B 和 C 传递一个公共参数,那么称模块 B 和 C 之间 存在一个标记耦合。

​ (6) 数据耦合。模块之间通过参数来传递数据,那么被称为数据耦合。数据耦合是最低的一种耦合形 式,系统中一般都存在这种类型的耦合,因为为了完成一些有意义的功能,往往需要将某些模块的输出数据作为另
一些模块的输入数据。

​ (7) 非直接耦合 。两个模块之间没有直接关系,它们之间的联系完全是通过主模块的控制和调用来实 现的。

总结: 耦合是影响软件复杂程度和设计质量的一个重要因素,在设计上我们应采用以下原则:如果模块间必须 存在耦合,就尽量使用数据耦合,少用控制耦合,限制公共耦合的范围,尽量避免使用内容耦合。

内聚与耦合

​ 内聚标志一个模块内各个元素彼此结合的紧密程度,它是信息隐蔽和局部化概念的自然扩展。内聚是从 功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事。它描述的是模块内的功能联系。耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及通过接口的数据。 程序讲究的是低耦合,高内聚。就是同一个模块内的各个元素之间要高度紧密,但是各个模块之 间的相互依存度却要不那么紧密。

​ 内聚和耦合是密切相关的,同其他模块存在高耦合的模块意味着低内聚,而高内聚的模块意味着该模块同其他模块之间是低耦合。在进行软件设计时,应力争做到高内聚,低耦合

​ 具体到项目中,带来了哪些依赖问题呢:

使用工厂模式解耦

​ 先了解一下工厂模式解耦的思想,会给下面 Spring 控制反转使用带来启发。

​ 在实际开发中我们可以把三层的对象都使用配置文件配置起来,当启动服务器应用加载的时候,让一个类中的方法通过读取配置文件,把这些对象创建出来并存起来。在接下来的使用的时候,可以直接拿过来用。

​ 那么,这个读取配置文件,创建和获取三层对象的类就是工厂(Factory)

范例:

项目结构:

image-20200507222036404

对应代码:以表现层 - 业务层 - 持久层 - 工厂 顺序

表现层代码:

package com.yh.view;

import com.yh.factory.BeanFactory;
import com.yh.service.INameService;

/**
 * 模拟一个表现层用于调用业务层
 * @author YH
 * @create 2020-05-07 16:19
 */
public class Cilent {
    public static void main(String[] args){
        //想调用业务层方法依赖与其实现类对象
//        INameService service = new NameServiceImpl();
        INameService service = (INameService)BeanFactory.getBean("nameService");
        System.out.println("表现层后台代码执行调用业务逻辑层:1");
        service.method();
    }
}

业务层代码:

package com.yh.service;

/**
 * 业务逻辑层接口
 * @author YH
 * @create 2020-05-07 16:17
 */
public interface INameService {
    void method();
}
package com.yh.service.impl;

import com.yh.dao.INameDao;
import com.yh.dao.impl.NameDaoImpl;
import com.yh.factory.BeanFactory;
import com.yh.service.INameService;

/**
 * 模拟业务逻辑层调用持久层
 * @author YH
 * @create 2020-05-07 16:18
 */
public class NameServiceImpl implements INameService {
    @Override
    public void method() {
        //想调用持久层方法依赖与其实现类对象
//        INameDao nameDao = new NameDaoImpl();
        INameDao nameDao = (INameDao) BeanFactory.getBean("nameDao");
        System.out.println("业务逻辑层实现类执行调用持久层:2");
        nameDao.method();
    }
}

持久层代码:

package com.yh.dao;

/**
 * 持久层接口
 * @author YH
 * @create 2020-05-07 16:14
 */
public interface INameDao {
    void method();
}
package com.yh.dao.impl;

import com.yh.dao.INameDao;

/**
 * 模拟持久层
 * @author YH
 * @create 2020-05-07 16:15
 */
public class NameDaoImpl implements INameDao {
    @Override
    public void method() {
        System.out.println("持久层dao执行 3");
    }
}

工厂:

package com.yh.factory;

import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * 一个创建Bean对象的工厂
 * Bean:在计算机英语中,有可重用组件的含义
 * JavaBean:用java语言编写的可重用组件
 * 注意:JavaBean不等于实体类,且包含实体类,即 JavaBean > 实体类
 *
 * 创建service和dao对象
 *
 * 1.需要通过配置文件读取配置,可用两种方式: xml 或 properties
 *      配置的内容:唯一标识=全限定类名(key-value)
 * 2.再通过读取配置文件中配置的内容,反射创建对象
 * @author YH
 * @create 2020-05-07 17:14
 */
public class BeanFactory {
    private static Properties props;

    /**
     * 定义一个Map,作为存储对象的容器,存放我们要创建的对象
     */
    private static Map<String,Object> beans = null;

    /**
     * 静态代码块只执行一次,保证了从始至终只生成配置中对应的唯一一个实例
     */
    static {
        try {
            props = new Properties();
            InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("factory.properties");
            props.load(in);
            //实例化Map容器
            beans = new HashMap<>();
            //取出配置文件中所有的key
            Enumeration<Object> keys = props.keys();
            //遍历枚举
            while(keys.hasMoreElements()){
                //取出每个key
                String key = keys.nextElement().toString();
                //根据key从配置中读取value
                String beanPath = props.getProperty(key);
                //反射创建实例对象
                Object value = BeanFactory.class.forName(beanPath).newInstance();
                //把key和value存入容器中
                beans.put(key,value);
            }

        } catch (Exception e) {
            //读取配置文件出现异常那么后面的操作都无意义,所以直接声明一个错误终止程序
            throw new ExceptionInInitializerError("初始化properties时发生错误!");
        }
    }

    /**
     * 根据bean的名称获取bean对象
     * @param beanName
     * @return
     */
    public static Object getBean(String beanName){
        return beans.get(beanName);
    }

    /**
     * 传入key的名称寻找对应的value全类名 并创建对象返回
     * @param beanName
     * @return
     *//*
    public static Object getBean(String beanName){
        Object bean = null;
        try {
            String beanPath = props.getProperty(beanName);
            //每次都会调用默认构造函数创建对象
            bean = (Object) Class.forName(beanPath).newInstance();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
        return bean;
    }*/
}

小结:通过工厂类初始化加载就将配置文件中所代表的类创建并存储到 Map 中,需要使用时调用工厂方法即可,避免了 new,即避免了反复创建对象,也降低了程序的耦合度

控制反转 IOC

​ 控制反转(Inversion Of Control)把创建对象的权利交给框架,是框架的重要特征,并非面向对象编程的专用术语。它包括依赖注入(DI)和依赖查找(DL)

作用:消减计算机程序的耦合(解除我们代码中的依赖关系)

以上面小节为例:

​ 我们通过工厂创建对象,将对象存储在容器中,提供获取对象的方法。在这个过程中:

​ 获取对象的方式发生了改变:

​ 以前:获取对象,采用 new 的方式,是主动的

image-20200508120420272

​ 现在:通过工厂获取对象,工厂为我们查找或者创建对象,是被动的

image-20200508120555456

使用 Spring 的 IOC 解决程序耦合

  1. 准备 spring 的开发包

    • spring 目录结构:
      • docs:API 和开发规范
      • libs:jar 包和源码
      • schema:约束
  2. 以上一节工厂解耦改为使用 spring

第一步:向项目的 pro.xml 文件中加入配置,将 spring 的 jar 包导入工程:

<!--设置打包方式-->
<packaging>jar</packaging>

<dependencies>
    <!--        导入spring-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
</dependencies>

第二步:在资源目录下创建一个 xml 文件

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

    <!-- bean标签:用于配置让spring创建对象,并且存入IOC容器之中
         id属性:对象的唯一标识
         class属性:指定要创建对象的全限定类名
    -->
    <bean id="dao" class="yh.dao.impl.NameDaoImpl"></bean>
    <bean id="service" class="yh.service.impl.NameServiceImpl"></bean>
</beans>

第三步:让 spring 管理资源,在配置文件中配置 service 和 dao

public class Client {
    /**
     * 获取spring的核心容器 并根据id获取对象
     * @param args
     */
    public static void main(String[] args){
        //1.获取核心容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.根据id获取bean对象
        INameDao dao = (INameDao)ac.getBean("dao");
        INameService service = ac.getBean("service",INameService.class);

        System.out.println(dao);
        System.out.println(service);
    }
}

测试配置是否成功:

image-20200508142735850

Spring 中工厂的类结构:

image-20200508143643321

image-20200508150247755

​ 可以看出 BeanFactory 是 Spring 容器中的顶层接口,ApplicationContext 是其子接口,它们创建对象的时间点的区别:

​ ApplicationContext:只要一读取配置文件,默认情况下就会创建对象(即时创建),可以推断:即时创建对象适合使用在单例模式的场景,对象只创建一次

​ BeanFactory:什么时候使用对象了,才会创建对象(延迟创建),同理:延迟创建对象适合于多例模式的场景,节省性能开销

ApplicationContext 的三个常用实现类:

  • ClassPathXmlApplicationContext:加载类路径下的配置文件,要求配置文件必须在类路径下
  • FileSystemXmlApplication:加载磁盘任意路径下的配置文件(必须有访问权限)
  • AnnotationConfigApplicationContext:用于读取注解创建容器

Spring 基于 XML 的 IOC 细节

IOC 本质

控制反转 IoC,是一种设计思想,DI(依赖注入)是实现 IoC 的一种方法。没有 IoC 的程序中,使用面向对象编程,对象的创建与对象的依赖关系完全硬编码在程序中,对象的创建由程序自己控制,控制反转后将对象的创建转移给第三方,即获得依赖对象的方式反转了。

image-20200521135312514

IoC 是 Spring 框架的核心内容,使用多种方式完美实现了 IoC,可以使用 XML 配置,也可以使用注解,新版本的 Spring 也可以零配置实现 IoC。Spring 容器在初始化时先读取配置文件,根据配置文件或元数据创建与组织对象存入容器中,程序使用时再从 IoC 容器中取出需要的对象。

image-20200521143116286

控制反转是一种通过描述(XML 或注解)并通过第三方去生产或获取特定对象的方式,在 Spring 中实现控制反转的是 IoC 容器,其实现方法是依赖注入(Dependency Injection,DI)。

所谓控制反转,就是应用本身不负责依赖对象的创建及维护,依赖对象的创建及维护是由外部容器负责的。其中依赖注入是控制反转最常见的实现。

​ 那我们来先搞清这个依赖对象是什么,下面是传统三层架构的代码示例:

持久层:

//持久层接口
public interface IUserDao {
    void daoMethod();
}
//持久层接口实现1
public class UserDaoImpl implements IUserDao {
    public void daoMethod() {
        System.out.println("数据库连接1");
    }
}
//持久层接口实现2
public class UserDaoImpl2 implements IUserDao {
    public void daoMethod() {
        System.out.println("数据库连接2");
    }
}

持久层即数据访问层(DAL 层),其功能主要是负责数据库的访问,实现对数据表的 CEUD 等操作。

​ 可能会有变更接口实现的需求(如 MySQL 换为 Oracle)

业务逻辑层:

//业务逻辑层接口
public interface IUserService {
    void serviceMethod();
}
//业务逻辑层接口实现
public class UserServiceImpl implements IUserService {
    //业务层需要或许持久层对象,调用其方法
    IUserDao dao = new UserDaoImpl();
    public void serviceMethod() {
        dao.daoMethod();
    }
}

三层架构的核心,其关注点是业务规则的制定、业务流程的实现等与业务需求有关的系统设计。

视图层(表示层):

@Test
public void test1(){
    //程序入口要获取业务层对象来调用功能
    IUserService service = new UserServiceImpl();
    service.serviceMethod();
}

表示层主要作用是与用户进行交互,显示数据(如打印到控制台的信息)和接收传输用户的数据,提供用户操作界面等。

运行结果:image-20200520195731474

​ 这就是传统三层架构的一个调用流程,可以看出作为三层核心的业务层起的一个承上启下的作用。表示层与用户交互,要执行功能那么就需要先货到控制层的对象,调用相关功能。即没有业务层对象就没法实现操作,则表示层依赖于业务逻辑层,没它不行;同样的,业务逻辑层作为一个指挥全局的头,需要指挥小弟来办事,所以他先得有个小弟,那么就获取一个持久层对象了,同样是没有这个小弟没法办事,而且加入要办另外一件事需要另一个小弟,那业务层大哥也要做相应的调整(改代码)。此时业务逻辑层依赖于持久层。

真是世间美好与你环环相扣,变强了,头也就秃了(手动**)

​ 针对变更持久层实现需要修改业务层代码的问题做一个优化,使用 set 方法注入方式获取对象,如下:

业务层实现类:

public class UserServiceImpl implements IUserService {
    /**
     * 对象注入
     */
    private IUserDao dao;
    public void set(IUserDao dao){
        this.dao = dao;
    }

    public void serviceMethod() {
        dao.daoMethod();
    }
}

​ 利用多态的特性可接收任何其实现对象,外部根据不同的需求传递不同的实现对象参数,从而避免了二次修改业务层代码。

测试代码:

@Test
    public void test1(){
        //程序入口要获取业务层对象来调用功能
        IUserService service = new UserServiceImpl();

//        service.setDaoImpl(new UserDaoImpl());
        service.setDaoImpl(new UserDaoImpl2());

        service.serviceMethod();
    }

传入不同的实现参数,获取不同的连接:

image-20200521105738040

image-20200521105701107

​ 对比:

​ 之前,程序主动创建对象,由程序员决定使用的功能(更改代码)

​ 使用 set 注入后,程序变成被动接受对象,由使用者决定使用的功能(传递对应的参数)

这种让程序员不再管理对象创建的思想,使得程序系统的耦合性大大降低,让程序员可以更加专注于业务的实现上,这就是 IoC 的原型。

​ 对,是原型,起关键作用的就是 set 方法,它是得以注入的关键,下面就使用 Spring IoC 来建立第一个程序:

JavaBean:

public class Hello {
    private String name;

    //注意此set方法
    public void setName(String name){
        this.name = name;
    }
    public void run(){
        System.out.println("Hello!" + name);
    }
}

使用 XML 方式进行配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--配置元数据
	bean的配置:
    使用Spring创建对象,对象都用Bean表示
    对比原有的new创建对象方式:
        Heelo hello = new Hello() 即 类型 变量名 = new 类型()
        - id 指定对象变量名 -> 变量名
        - class 指定要创建的对象的类
        - property 指定对象的属性
             name 指定属性名
             value 指定属性值
    -->
    <bean id="hello" class="yh.pojo.Hello">
        <property name="name" value="熊大"/>
    </bean>
</beans>

测试代码:

@Test
public void test1(){
    //获取Spring的上下文对象
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    //我们的对象现在都在spring中管理了,我们要使用,直接去里面取出来即可(取Bean)
    Hello hello = context.getBean("hello", Hello.class);
    //Hello hello = (Hello)context.getBean("hello");
    hello.run();
}

结果:image-20200521153102644

整个过程中:

​ hello 对象有 Spring 创建

​ hello 对象的属性也由 Spring 容器设置

这就是控制反转:

  • 控制:传统程序的对象是由程序本身控制创建的,使用 Spring 后,对象是由 Spring 创建的。

  • 反转:程序本身不创建对象,而变成被动地接收对象。

  • 依赖注入:就是利用 set 方法进行注入。

  • IOC 就是一种编程思想,由主动的编程编编程被动的接收。

至此,我们彻底不用去程序中改动了,要实现不同的操作,只需要在 xml 配置文件中进行修改,对象由 Spring 来创建、管理、装配。

​ 现在我们来修改最开始的那个传统实例,看看用 IoC 如何实现它:

配置文件:

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

    <bean id="daoImpl" class="yh.dao.impl.UserDaoImpl"/>
    <bean id="daoImpl2" class="yh.dao.impl.UserDaoImpl2"/>

    <bean id="service" class="yh.service.impl.UserServiceImpl">
        <!--
        ref:引用spring容器中创建好的对象
        value:具体的值类型数据
        -->
        <property name="dao" ref="daoImpl"/>
    </bean>
</beans>

​ 由于业务层实现中原本就设置了 set 方法,所以可以直接配置注入属性的信息

注意:set 方法命名一定要按照规范,否则无法识别注入

其他地方都不用修改,直接进行测试:

@Test
public void test1(){
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    IUserService service = context.getBean("service", IUserService.class);
    service.serviceMethod();
}

结果:

image-20200521161949574

如需更改配置,直接修改配置文件中的 dao 属性值(配置文件不属于程序代码),如下:

<bean id="service" class="yh.service.impl.UserServiceImpl">
    <!--更改引用值-->
        <property name="dao" ref="daoImpl2"/>
    </bean>

image-20200521161914292

​ 增加实现,或换实现原都可以通过元数据完成了。

IOC 中 bean 标签和管理对象细节

bean 标签

作用:用于配置对象让 spring 来创建

​ 默认情况下他调用的是类中的无参构造器,如果没有无参构造器则不能创建成功

属性

​ id:给对象在容器中提供一个唯一标识,用于获取对象

​ class:指定类的全限定类名,用于反射创建对象。默认情况下调用无参构造器

​ scope:指定对象的作用范围

​ init-method:指定类中的初始化方法名称

​ destroy-method:指定类中销毁方法名称

实例化 Bean 的三种方式

第一种方式:使用构造器

  • 使用默认无参构造函数(bean 对象需要设置 set 方法)
<!--在默认情况下:
    他会根据默认无参构造函数来创建类对象,如果bean中没有默认无参构造函数,将会创建失败-->
<bean id="service" class="yh.service.impl.NameServiceImpl"></bean>
  • 使用有参构造器(即用构造器代替 set 方法给属性注入值)
<bean id="hello" class="yh.pojo.Hello">
    <constructor-arg name="name" value="Spring"/>
</bean>

第二种方式:spring管理实例工厂,使用实例工厂的方法创建对象

<!--先把工厂的创建交给spring来管理,然后使用工厂bean来调用里面的方法(先创建工厂对象,再用其获取service对象)
    factory-bean 属性:用于指定实例工厂bean的id
    factory-method 属性:用于指定实例工厂中创建对象的方法
-->
    <bean id="instanceFactory" class="yh.factory.InstanceFactory"></bean>
    <bean id="nameService" factory-bean="instanceFactory" factory-method="createNameService"></bean>

第三种方式:spring管理静态工厂,使用静态工厂的方法创建对象

<!--使用StaticFactory类中的静态方法创建对象,并存入spring容器
    id:指定bean的id,用于从容器中获取
    class:指定静态工厂的全限定类名
    factory-method 属性:指定生成对象的工厂静态方法
-->
    <bean id="nameService" class="yh.factory.StaticFactory" factory-method="createNameService"></bean>

调用类:

package yh.view;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import yh.service.INameService;

/**
 * 模拟一个表现层用于调用业务层
 * @author YH
 * @create 2020-05-07 16:19
 */
public class Client {
    /**
     * 获取spring的核心容器 并根据id获取对象
     * @param args
     */
    public static void main(String[] args){
        //1.获取核心容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.根据id获取bean对象
        INameService service = ac.getBean("nameService",INameService.class);

        System.out.println(service);
    }
}

测试结果都能获取到对象:

image-20200508174144220

bean 的作用范围和生命周期

在配置文件加载的时候,容器中管理的对象(Bean)就已经初始化了,需要哪个对象通过 Spring 上下文对象直接获取即可(getBean())。

参考官方介绍:

范围 描述
singleton (默认)为每个 Spring IoC 容器的单个 object 实例定义单个 bean 定义。
prototype 为任意数量的 object 实例定义单个 bean 定义。
request 将单个 bean 定义范围限定为单个 HTTP 请求的生命周期。也就是说,每个 HTTP 请求都有自己的 bean 实例,该实例是在单个 bean 定义的后面创建的。仅在 web-aware Spring ApplicationContext的 context 中有效。
session 将单个 bean 定义范围限定为 HTTP Session的生命周期。仅在 web-aware Spring ApplicationContext的 context 中有效。
application 将单个 bean 定义范围限定为ServletContext的生命周期。仅在 web-aware Spring ApplicationContext的 context 中有效。
websocket 将单个 bean 定义范围限定为WebSocket的生命周期。仅在 web-aware Spring ApplicationContext的 context 中有效。

bean 对象的作用范围:

​ 使用 scope 属性指定对象的作用范围,参数:

singleton:单例的(默认值)

prototype:多例的

​ request:WEB 项目中 Spring 创建一个 Bean 的对象,将对象存入到 request 域中

​ session:WEB 项目中 Spring 创建一个 Bean 的对象,将对象存入到 session 域中

​ global session:作用于集群环境的会话范围(全局会话范围),不是集群它就是 session

global session(全局变量)应用场景:

​ 一个web工程可能有多个服务器分流,用户首次发送请求访问 web 时所连接的服务器和提交登录所请求的服务器可能不一同一个服务器,但是验证码生成首先是从第一次访问时的服务器获取的,并保存在独有 session 中,提交登录时肯定需要比较验证码正确性,由于可能不在一个服务器无法验证,所以就需要 global session 这个全局变量,无论在哪个服务器都可以验证

示意图:

image-20200508200937130

生命周期:

单例对象:scope="singleton"

​ 一个应用只有一个对象的实例,它的作用范围就是整个应用

​ 对象出生:当应用加载,创建容器时,对象就被创建了

​ 对象活着:只要容器在,对象一直活着

​ 对象死亡:当应用卸载,容器销毁时,对象也被销毁

多例对象:scope="prototype"

​ 每次访问时,都会重新创建对象实例

​ 对象出生:当使用对象时,创建新的对象实例

​ 对象活着:对象使用期间一直活着

​ 对象死亡:当对象长时间不用,被java的垃圾回收机制回收了

Spring 的依赖注入

依赖注入的概念

​ 依赖注入:Dependdency Injection。它是 spring 框架核心 IOC 的具体实现

​ 我们的程序在编写时,通过控制反转,把对象的创建交给了 spring,但是代码中不可能出现没有依赖的情况。ioc 解耦只是降低他们的依赖关系,但不会消除。例如:我们的业务层仍会调用持久层的方法。

​ 那这种业务层和持久的依赖关系,在使用 spring 之后,就让 spring 来维护了;

​ 简单的说,就是坐等框架把持久层对象传入业务层,而不用我们自己去获取

构造函数注入

​ 顾名思义,就是使用类中的构造函数,给成员变量赋值

  • 构造函数注入

要求:
类中需要提供一个对应的带参构造器
涉及的标签:
constructor-arg
属性:
index:指定要注入的数据给构造函数中指定索引位置的参数赋值,索引从0开始
type:指定要注入数据的数据类型,该类型也是某个或某些参数的类型
name:指定给构造器中指定名称的参数赋值
---------------以上三个属性用于指定要给哪个参数赋值---------------
value:用于提供基本类型和String类型的数据
ref:用于指定其他的bean类型数据(即在spring的IOC核心容器中出现过的bean对象
- 优势:
在获取bean对象时,注入数据时必须的操作,否则对象无法创建成功
- 弊端:
改变了bean对象的实例化方式,调用有参构造器,使我们在创建对象时,不管需不需要这些数据,也必须提供

xml 文件配置:

<!--使用构造函数的方式,给service中的属性传值-->
    <bean id="nameService" class="yh.service.impl.NameServiceImpl">
        <constructor-arg name="name" value="云翯"></constructor-arg>
        <constructor-arg name="age" value="18"></constructor-arg>
        <constructor-arg name="birthday" ref="now"></constructor-arg>
    </bean>
<!--    配置一个日期对象-->
    <bean id="now" class="java.util.Date"></bean>

实现类提供有参构造器:

public class NameServiceImpl implements INameService {
    private String name;
    private Integer age;
    private Date birthday;

    public NameServiceImpl(String name, Integer age, Date birthday) {
        this.name = name;
        this.age = age;
        this.birthday = birthday;
    }

    @Override
    public void method() {
        System.out.println(name + "," + age + "," + birthday);
    }
}

调用类:

public class Client {
    /**
     * 获取spring的核心容器 并根据id获取对象
     * @param args
     */
    public static void main(String[] args){
        //1.获取核心容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.根据id获取bean对象
        INameService service = ac.getBean("nameService",INameService.class);
        service.method();
    }
}

测试结果:

image-20200509090209538

set 方法注入(常用)

涉及的标签:property
出现的位置:bean标签内部
属性:
name:指定所用的set方法名称
value:指定基本类型和String类型的数据
ref:指定其他bean类型数据(即spring的IOC核心容器中出现过的bean对象)

  • 优势:
    创建对象时没有明确的限制,可以直接使用默认构造函数
  • 弊端:
    因为是先创建对象再通过set赋值,假如某个成员必须有值,而获取对象时有可能set方法还没有执行

顾名思义,实现类中需要提供set方法。范例:

xml 配置文件:

<bean id="nameService1" class="yh.service.impl.NameServiceImpl1">
    <property name="name" value="云翯1"></property>
    <property name="age" value="19"></property>
    <property name="birthday" ref="now"></property>
</bean>
<!--    配置一个日期对象-->
    <bean id="now" class="java.util.Date"></bean>

带有 set() 方法的实现类:

public class NameServiceImpl1 implements INameService {
    private String name;
    private Integer age;
    private Date birthday;

    public NameServiceImpl1() {}

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

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

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    @Override
    public void method() {
        System.out.println(name + "," + age + "," + birthday);
    }
}

调用类:

public class Client {
    /**
     * 获取spring的核心容器 并根据id获取对象
     * @param args
     */
    public static void main(String[] args){
        //1.获取核心容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.根据id获取bean对象
        INameService service = ac.getBean("nameService1",INameService.class);
        service.method();
    }
}

测试结果:

image-20200509091958836

  • 注入集合数据(复杂类型注入)

    ​ 顾名思义,就是给集合成员传值,他用的也是set方法注入的方式,只不过变量的数据类型都是集合。

    用于给 List 结构集合注入的标签
    list、array、set
    用于给 Map 结构集合注入的标签
    map、props
    结构相同,标签可以互用

    范例:

    xml 配置文件:

    <!-- 复杂类型的注入/集合类型的注入-->
        <bean id="nameService2" class="yh.service.impl.NameServiceImpl3">
            <property name="myStrs">
                <array>
                    <value>AAA</value>
                    <value>BBB</value>
                    <value>CCC</value>
                </array>
            </property>
            <property name="myList">
                <list>
                    <value>AAA</value>
                    <value>BBB</value>
                    <value>CCC</value>
                </list>
            </property>
            <property name="mySet">
                <set>
                    <value>AAA</value>
                    <value>BBB</value>
                    <value>CCC</value>
                </set>
            </property>
            <property name="myMap">
                <map>
                    <entry key="testA" value="aaa"></entry>
                    <entry key="testB" value="bbb"></entry>
                    <entry key="testC" value="ccc"></entry>
                </map>
            </property>
            <property name="myProps">
                <props>
                    <prop key="testA">aaa</prop>
                    <prop key="testB">bbb</prop>
                </props>
            </property>
        </bean>
    

    集合等复杂类型的属性,同样使用set方法赋值:

public class NameServiceImpl3 implements INameService {
    private String[] myStrs;
    private List<String> myList;
    private Set<String> mySet;
    private Map<String,String> myMap;
    private Properties myProps;

    public void setMyStrs(String[] myStrs) {
        this.myStrs = myStrs;
    }

    public void setMyList(List<String> myList) {
        this.myList = myList;
    }

    public void setMySet(Set<String> mySet) {
        this.mySet = mySet;
    }

    public void setMyMap(Map<String,String> myMap) {
        this.myMap = myMap;
    }

    public void setMyProps(Properties myProps) {
        this.myProps = myProps;
    }

    @Override
    public void method() {
        System.out.println(myStrs);
        System.out.println(myList);
        System.out.println(mySet);
        System.out.println(myMap);
        System.out.println(myProps);
    }
}

调用类:

public class Client {
    /**
     * 获取spring的核心容器 并根据id获取对象
     * @param args
     */
    public static void main(String[] args){
        //1.获取核心容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.根据id获取bean对象
        INameService service = ac.getBean("nameService2",INameService.class);
        service.method();
    }
}

测试结果:

image-20200509101235696

命名空间注入

​ 我们可以使用 p 命名空间和 c 命名空间,进行注入

<?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:p="http://www.springframework.org/schema/p"
       xmlns:c="http://www.springframework.org/schema/c"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
	<!--p命名空间注入,可以直接注入属性的值-->
    <bean id="user" class="yh.pojo.User" p:name="无问西东" p:age="18"/>
    
    <!--c命名空间注入,可以直接注入构造器的值-->
    <bean id="user2" class="yh.pojo.User" c:name="无问西东" c:age="18"/>
</beans>

​ p 和 c 命名空间允许 bean 元素通过属性(而不是嵌套的子元素)来描述注入的属性值。但是不能直接使用,需要导入 XML 约束:

xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"

案例:使用 spring Ioc(XML)实现的 CRUD

结构:

image-20200522192401585

Account 类:

public class Account {
    private int id;
    private String name;
    private float money;
    //标准JavaBean,剩余代码略...
}

dao 接口:

public interface IAccountDao {
    /**
     * 查询所有
     * @return
     */
    List<Account> findAccounts();

    /**
     * 查询一个
     * @param account
     * @return
     */
    Account findAccountById(Integer account);

    /**
     * 保存
     * @param account
     */
    void saveAccount(Account account);

    /**
     * 更新
     * @param account
     */
    void updateAccount(Account account);

    /**
     * 删除
     * @param id
     */
    void deleteAccountById(Integer id);
}

dao 接口实现:

public class AccountDaoImpl implements IAccountDao {
    private QueryRunner runner;

    public void setRunner(QueryRunner runner) {
        this.runner = runner;
    }

    public List<Account> findAccounts() {
        try {
            return runner.query("select * from account",new BeanListHandler<Account>(Account.class));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public Account findAccountById(Integer account) {
        try {
            return runner.query("select * from account where id=?",new BeanHandler<Account>(Account.class),account);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void saveAccount(Account account) {
        try {
            runner.update("insert into account(name,money) values(?,?)",account.getName(),account.getMoney());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void updateAccount(Account account) {
        try {
            runner.update("update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void deleteAccountById(Integer id) {
        try {
            runner.update("delete from account where id=?",id);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

service 层:

public interface IAccountService {
    /**
     * 查询所有
     * @return
     */
    List<Account> findAccounts();

    /**
     * 查询一个
     * @param account
     * @return
     */
    Account findAccountById(Integer account);

    /**
     * 保存
     * @param account
     */
    void saveAccount(Account account);

    /**
     * 更新
     * @param account
     */
    void updateAccount(Account account);

    /**
     * 删除
     * @param id
     */
    void deleteAccountById(Integer id);
}

service 接口实现:

public class AccountServiceImpl implements IAccountService {
    private IAccountDao accountDao;

    /**
     * set注入
     * @param accountDao
     */
    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }

    /**
     * 获取所有账户信息
     * @return
     */
    public List<Account> findAccounts() {
        return accountDao.findAccounts();
    }

    public Account findAccountById(Integer account) {
        return accountDao.findAccountById(account);
    }

    public void saveAccount(Account account) {
        accountDao.saveAccount(account);
    }

    public void updateAccount(Account account) {
        accountDao.updateAccount(account);
    }

    public void deleteAccountById(Integer id) {
        accountDao.deleteAccountById(id);
    }
}

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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--配置service-->
    <bean id="accountService" class="yh.service.impl.AccountServiceImpl">
        <property name="accountDao" ref="accountDao"/>
    </bean>
    <!--配置dao-->
    <bean id="accountDao" class="yh.dao.impl.AccountDaoImpl">
        <property name="runner" ref="dbutils"/>
    </bean>
    <!--配置dbutils 避免多线程干扰,设此bean设为多例-->
    <bean id="dbutils" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
        <constructor-arg name="ds" ref="dateScore"/>
    </bean>
    <!--配置数据源-->
    <bean id="dateScore" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--连接数据库的基本信息-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis?SSl=true&amp;useUnicode=true&amp;characterEncoding=utf8"/>
        <property name="user" value="root"/>
        <property name="password" value="root"/>
    </bean>
</beans>

测试代码:

public class Mytest {
    @Test
    public void testFindAll(){
        //获取容器
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        //获取业务层对象
        IAccountService service = context.getBean("accountService", IAccountService.class);
        //调用方法
        List<Account> accounts = service.findAccounts();
        for (Account account : accounts) {
            System.out.println(account.toString());
        }
    }
    @Test
    public void testFindOne(){
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        IAccountService service = context.getBean("accountService", IAccountService.class);
        Account account = service.findAccountById(2);
        System.out.println(account.toString());
    }
    @Test
    public void testSave(){
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        IAccountService service = context.getBean("accountService", IAccountService.class);
        service.saveAccount(new Account(5,"ddd",999));
    }
    @Test
    public void testUpdate(){
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        IAccountService service = context.getBean("accountService", IAccountService.class);
        service.updateAccount(new Account(2,"bbb2",999));
    }
    @Test
    public void testDelete(){
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        IAccountService service = context.getBean("accountService", IAccountService.class);
        service.deleteAccountById(2);
    }
}

基于注解的 IOC 配置

Java 注解(Annotation)

​ 从 JDK 5.0 开始,java 增加了对元数据(MetaData)的支持,也就是 Annotation(注解)

​ 注解是代码里的特殊标记,可以在编译、类加载、运行时被读取,并执行相应的处理,通过使用注解,我们可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充信息。代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证或进行部署。

​ 注解可以像修饰符一样被使用,用来修饰包、类、构造器、方法、成员变量、参数、局部变量的声明,这些信息被保存在 Annation 的 name=value 对中。

框架 = 注解 + 反射 + 设计模式

  • 自定义注解

    ​ 使用 @interface 关键字如:public @interface testAnnotation,自定义注解自动继承了 java.lang.annotation.Annotation 接口;

    ​ 注解的成员变量在定义时以无参方法的形式来声明(如:String[] value()),其方法名和返回值定义了该成员变量的名字和类型,此为配置参数,类型只能是八种基本数据类型、String、Class、enum、Annotation 这几个类型的数组(有多个 value 值);

    ​ 可以在定义注解的成员变量时使用 default 关键字,为其指定初始值。如果只有一个参数成员,建议设置参数名为 value

    ​ 如果定义的注解有配置参数,那么使用时必须指定参数值,除非它有默认值。格式:参数名 = 参数值,如果只有一个参数成员,且名称为 value,可以省略 value = 参数值,直接写参数值即可;

    ​ 没有成员定义的注解称为标记(如:@Override)包含成员变量的注解称为元数据注解

    注意:自定义注解必须配上注解的信息处理流程(使用反射)才有意义。

  • JDK 中的元注解

    元注解:对现有注解进行解释说明的注解。

    jdk 提供的 4 中元注解:

    • @Retention:用于修饰一个 Annotation 定义,指定其生命周期,包含一个 RetentionPolicy 类型的成员变量,使用时需指定 value 的值:

      RetentionPolicy.SOURCE:在源文件中有效(即源文件保留),编译器直接丢弃这种策略的注释;

      RetentionPolicy.CLASS:在 class 文件中有效(即 class 保留),当运行 Java 程序时,JVM 不会保留注释。这是默认值

      RetentionPolicy.RUNTIME:在运行时有效(即运行时保留),当运行 Java 程序时,JVM 会保留注释。程序可以通过反射获取该注释

    image-20200509135250937

    • @Target:用于指定被修饰的 Annotation 能用于修饰哪些程序元素
    • Documented:表示所修饰的注解在被Javadoc解析时,保留下来
    • Inherited:被其修饰的注解将有继承性(子类继承父类的注解)
    • jdk 8 新增:可重复注解 和 类型注解

扩展:元数据,是指对数据进行修饰的数据。如:在String name = "YunHe";"YunHe"为数据,而 String name = 就为元数据

基于注解的 IOC 配置

​ 配置注解与配置 xml 文件要实现的功能是一样的,都是要降低程序间的耦合,只是配置的形式不一样 。

与 xml 配置对应,可将注解简单分为:

用于创建对象的:

相当于:<bean id="" class="">
	@Component:
        作用:用于把当前类对象存入 spring 容器中
        属性:
            value:用于指定 bean 的 id。默认值为当前类名首字母小写
	@Controller:一般用在表现层
    @Service:一般用在业务层
	@Repository:一般用在持久层
		以上三个注解作用和属性与 @Component 一样(父子关系),是 spring 框架提供明确的三层使用注解,
    使我们的三层对象更加清晰

用于注入数据的:

相当于:<property name="" ref="">  /  <property name="" value="">
	@Autowired
       	作用:自动按照类型注入(自动装配)。当使用注解注入属性时,set方法可以省略。自动将spring容器中
    的 bean 注入到类型匹配的带有此注解的属性。当有多个类型匹配时,配合@Qualifier指定要注入的bean
	@Qualifier
		作用:在自动按照类型注入的基础之上,再按照bean的id注入(解决自动注入存在多个同类型的 bean
    所产生的歧义问题),它在给字段注入时不能独立使用,必须和 @Autowire 一起使用;但是给方法参数注
    入时,可以独立使用(指定形参所要接收的bean的id名)。
		属性:
			value:指定bean的id
	@Resource
		作用:直接按照bean的id注入,它也只能注入其他bean类型
		属性:
			name:指定bean的id
    @Value
    	作用:注入基本数据类型和 String 类型数据
    	属性:
    		value:用于指定值

用于改变作用范围的:

相当于:<bean id="" class="" scope="">
	@Scope
    作用:指定 bean 的作用范围
    属性:
    	value:指定范围的值
    		取值:singleton/prototype/request/session/globalsession

声明周期相关:

相当于:<bean id="" class="" init-method="" destroy-method="">
	@PostConstruct
	作用:用于指定初始化方法
	
	@PreDestroy
	作用:用于指定销毁方法

自动按照类型注入示意图:

image-20200511173755370

注意:spring 识别 bean 的范围时需通过 xml 配置设置 spring 创建容器时要扫描的包。

使用注解方式修改上例 CRUD 程序

这里就贴上修改的部分代码(改动太小了)

dao 实现类:(两个注解)

@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {
    @Autowired
    private QueryRunner runner;
//...
}

service 实现类:(两个注解)

@Service("accountService")
public class AccountServiceImpl implements IAccountService {
    @Autowired
    private IAccountDao accountDao;
//...
}

使用注解的 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
        http://www.springframework.org/schema/context/spring-context.xsd">

    <!--    告知 spring 创建容器时要扫描的包-->
    <context:component-scan base-package="yh"/>

    <bean id="dbutils" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
        <constructor-arg name="ds" ref="dateScore"/>
    </bean>
    <bean id="dateScore" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis?SSl=true&amp;useUnicode=true&amp;characterEncoding=utf8"/>
        <property name="user" value="root"/>
        <property name="password" value="root"/>
    </bean>
</beans>

​ 这样就完成配置了,但是 DBUtils 及 c3p0 的配置能不能也转换成注解形式呢?答案是当然可以,这就要新引入一个配置类的概念。

​ 在项目中新建一个结构如下:

image-20200523120534361

并新建配置类如下:

/**
 * 该类是一个配置类它的作用和bean.xml是一样的
 * spring中的新注解:
 *
 * Configuration 注解
 *      作用:指定当前类是一个配置类
 * ComponentScan 注解
 *      作用:指定Spring在创建容器时扫描配置的包
 *      属性:value,指定要包
 *      效果同xml配置中的<context:component-scan base-package=""/>一样
 * Bean 注解
 *      作用:用于把当前方法的返回值作为bean对象存入spring的IoC容器中
 *      属性:
 *          name:用于指定bean的id,当不写时,默认为方法名
 *      细节:
 *          当我们使用注解配置方法时,如果方法有参数,spring框架会去容器中查找有没有可用的bean对象
 *          查找的方式和Autowired注解的作用一样
 * Import 注解
 *      作用:用于导入其他配置的类
 *      属性:
 *          value:用于指定其他配置类的字节码
 *          当我们使用Import的注解之后,使用Import注解的类就是父配置类,而导入的都是子配置类
 * @author YH
 * @create 2020-05-22 21:36
 */
@Configuration
@ComponentScan("yh")
@Import(JDBCConfig.class)
public class SpringConfiguration {
    /**
     * 用于创建一个QueryRunner对象
     * 细节:默认获取的是单例的,但runner对象我们需要多例的,所以可加上scope
     * @param dataSource
     * @return
     */
    @Bean(name="runner")
    @Scope("prototype")
    public QueryRunner createQueryRunner(DataSource dataSource){
        return new QueryRunner(dataSource);
    }
}

配置jdbc的配置类:

/**
 * 注解方式获取jdbc连接的配置类
 * PropertySource 注解
 *      作用:指定properties文件的位置
 *      属性:
 *          value 注解:指定文件的名称和路径(properties文件的key)
 *              关键字:classpath,便是类路径下
 * @author YH
 * @create 2020-05-23 9:47
 */
@PropertySource("classpath:data.properties")	//引入外部的properties属性文件
public class JDBCConfig {
    @Value("${jdbc.driver}")
    private String driver;

    @Value("${jdbc.url}")
    private String url;

    @Value("${jdbc.username}")
    private String username;

    @Value("${jdbc.password}")
    private String password;

    /**
     * 创建数据源
     * @return
     */
    @Bean(name="dataSource")
    public DataSource createDataSource(){
        try {
            ComboPooledDataSource ds = new ComboPooledDataSource();
            ds.setDriverClass(driver);
            ds.setJdbcUrl(url);
            ds.setUser(username);
            ds.setPassword(password);
            return ds;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

properties 配置的数据库参数:

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=utf8
#不要用username作为key(获取到了我程序的作者标记了)
jdbc.username=root
jdbc.password=root

​ 如上就是纯注解的配置形式,配置类的作用同 bean.xml 一样,所以相对的,也会有对应 xml 中配置的注解(往往能见名知意)。测试类原来是通过加载 xml 的方式也要变更为 Annotation 的,如下:

@Test
    public void testFindAll(){
//        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        //改为注解工厂
        ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfiguration.class);
        IAccountService service = context.getBean("accountService", IAccountService.class);
        List<Account> accounts = service.findAccounts();
        for (Account account : accounts) {
            System.out.println(account.toString());
        }
    }

​ 纯注解的形式配置的工作量也不小,所以合理与 xml 搭配使用方能体现效率。

Qualifier 注解

可以使用在类或属性上以及方法形参前,用于解决有多个同类型 bean 的自动注入问题,通过 @Qualifier() 指定 bean id 来确认哪个 bean 才是我们需要注入的(设置的 value 值需要与目标 bean id 名相同)

@Primary 注解也用于解决自动注入时多个相同类型 bean 的问题,它定义了首选项,除非另有说明,否则将优先使用与 @Primary 关联的 bean。

Spring 整合 Junit

在上面的测试代码中都会有以下两行代码:

ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
IAccountService service = context.getBean("accountService", IAccountService.class);

这两行代码的作用是获取容器,如果不写会提示空指针,所以不能轻易去掉。

​ 针对问题,我们需要 Spring 自动帮我们创建容器,我们就无序手动创建了,上面的问题也能解决。

​ 首先 Junit 实现底层是集成了 main 方法,它无法知晓我们是否使用了 Spring 框架,自然无可能帮我们创建容器,不过 junit 给我吗暴露了一个注解,可以让我们替换掉它的运行器。

​ 所以我们需要依赖 spring 框架,因为它提供了一个运行器,可以读取配置文件(或注解)来创建容器。我们只需要告诉它配置文件的位置即可。

配置步骤

  1. 添加 junit 必备的 jar 包依赖

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.2.6.RELEASE</version>
    </dependency>
    

    对于 Spring 5,需用 4.12 及以上 Junit jar 包

  2. 使用 @RunWith 注解替换原有运行器并使用@Autowired 给测试类中的变量注入数据

    /**
     * RunWith:替换原有运行器
     * @author YH
     * @create 2020-05-22 21:20
     */
    @RunWith(SpringJUnit4ClassRunner.class)
    public class MyTest {
        //由spring自动注入业务层对象
        @Autowired
        IAccountService service;
        
        @Test
        public void testFindAll(){
            List<Account> accounts = service.findAccounts();
            for (Account account : accounts) {
                System.out.println(account.toString());
            }
        }
    }
    
  3. 使用 @ContextConfiguration 指定 Spring 配置文件的位置

    /**
     * RunWith:替换原有运行器
     * ContextConfiguration
     *   属性:
     *       location属性:用于指定配置文件的位置,如果是类路径下,需要用classpath:表名
     *       classes属性:用于指定注解的类,当不使用xml配置时,需要用此属性指定注解类的位置
     * @author YH
     * @create 2020-05-22 21:20
     */
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes={SpringConfiguration.class}) //{}表示支持设置多个配置类
    public class MyTest {
        @Autowired
        ApplicationContext context;
        @Autowired
        IAccountService service;
        @Test
        public void testFindAll(){
            List<Account> accounts = service.findAccounts();
            for (Account account : accounts) {
                System.out.println(account.toString());
            }
        }
    }
    
  4. 其中@Autowired 会给测试类中的变量注入数据

为什么不把测试类配置到 xml 中

​ 首先,测试类配置到 xml 中肯定是可以实现的,但为什么不这样做?

原因:

  1. 当我们在 xml 中配置一个 bean ,spring 加载配置文件创建容器时,就会创建对象。

  2. 而测试仅仅起测试作用,在项目中它并不参与程序逻辑,也不会解决需求上的问题,所以创建完了,并没有使用,那么存在容器中就会造成资源的浪费。

    所以,基于以上两点,我们不应该把测试类配置到 xml 中。

AOP

AOP(Aspect Oriented Programming)面向切面编程,通过预编译的方式和运行期动态代理实现程序功能的统一维护的一种技术。将程序中重复的功能代码抽象出来,在需要执行的时候使用动态代理在不修改源码的基础上,对我们已有的方法进行增强。

从几个知识面作为学习 AOP 的突破口

一个转账案例

修改上面的 CRUD 案例,首先原案例代码中的事务由 connection 对象的 setAutocommit(true) 而被自动控制。此方式控制事务,一次只执行一条 sql 语句,没有问题,但执行多条 sql 就无法实现功能。原因是 sql 执行一次会获取一次数据库连接,统一 sql 语句的执行结果会被缓存,后面执行会直接读取缓存;而多条 sql 执行就需要各自或许连接并执行,持久层方法都是独立事务的,不符合事务的一致性,下面来探讨一下。

持久层代码:

package yh.dao.impl;

import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import yh.dao.IAccountDao;
import yh.pojo.Account;

import java.sql.SQLException;
import java.util.List;

/**
 * @author YH
 * @create 2020-07-01 8:59
 */
public class AccountDaoImpl implements IAccountDao {
    private QueryRunner runner;
    public void setRunner(QueryRunner runner){
        this.runner = runner;
    }
    @Override
    public Account findName(String name) {
        try {
            List<Account> accounts = runner.query("select * from mybatis.account where name=?", new BeanListHandler<Account>(Account.class), name);
            if(accounts == null || accounts.isEmpty()){
                return null;
            }
            if (accounts.size() > 1){
                throw new RuntimeException("结果集不唯一,数据有问题");
            }
            return accounts.get(0);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public void update(Account account) {
        try {
            runner.update("update mybatis.account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

业务层代码:

package yh.service.impl;

import yh.dao.impl.AccountDaoImpl;
import yh.pojo.Account;
import yh.service.IAccountService;

/**
 * @author YH
 * @create 2020-07-01 8:53
 */
public class AccountServiceImpl implements IAccountService {
    private AccountDaoImpl accountDao;

    public void setAccountDao(AccountDaoImpl accountDao){
        this.accountDao = accountDao;
    }
    @Override
    public void transfer(String sourceName, String targetName, Float money) {
        //根据账户信息获取账户对象
        Account source = accountDao.findName(sourceName);
        Account target = accountDao.findName(targetName);
        //转出账户减钱,转入账户加钱
        source.setMoney(source.getMoney() - money);
        target.setMoney(target.getMoney() + money);
        //提交更新
        accountDao.update(source);
        int i = 1/0;//模拟程序出错
        accountDao.update(target);
    }
}

理想情况下,程序正常运行,转账结果正确

一旦出错,前面执行后面的执行中断,即转出账户减钱了,而收款账户余额未增加,且事务无法回滚(因为它们有各自的事务)

下面就是新增在业务层的转账方法,每个执行方法都获取一次连接,都是独立的事务,一旦中途出现中断,就无法实现事务的回滚。

image-20200701150622969

解决办法:

​ 使用 ThreadLocal 对象把 Connection 和当前线程绑定,从而使一个线程中只有一个能控制事务的对象,原来的事务是在持久层,现需将事务应用在业务层。

image-20200702105954209

持久层代码:

public class AccountDaoImpl implements IAccountDao {
    private QueryRunner runner;
    private ConnectionUtils connectionUtils;

    public void setRunner(QueryRunner runner){
        this.runner = runner;
    }
    public void setConnectionUtils (ConnectionUtils connectionUtils){
        this.connectionUtils = connectionUtils;
    }

    @Override
    public Account findName(String name) {
        try {
            //使用与线程绑定的连接
            List<Account> accounts = runner.query(connectionUtils.getThreadConnection(),"select * from mybatis.account where name=?", new BeanListHandler<Account>(Account.class), name);
            if(accounts == null || accounts.isEmpty()){
                return null;
            }
            if (accounts.size() > 1){
                throw new RuntimeException("结果集不唯一,数据有问题");
            }
            return accounts.get(0);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public void update(Account account) {
        try {
            //使用与线程绑定的连接
            runner.update(connectionUtils.getThreadConnection(),"update mybatis.account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

业务层代码:

public class AccountServiceImpl implements IAccountService {
    private AccountDaoImpl accountDao;
    private TransactionManager transactionManager;

    public void setAccountDao(AccountDaoImpl accountDao){
        this.accountDao = accountDao;
    }
    public void setTransactionManager(TransactionManager transactionManager){
        this.transactionManager = transactionManager;
    }
    @Override
    public void transfer(String sourceName, String targetName, Float money) {
        //根据账户信息获取账户对象
        Account source = accountDao.findName(sourceName);
        Account target = accountDao.findName(targetName);
        try {//开启事务
            transactionManager.beginTransaction();
            //转出账户减钱,转入账户加钱
            source.setMoney(source.getMoney() - money);
            target.setMoney(target.getMoney() + money);
            //提交更新
            accountDao.update(source);
//            int i = 1 / 0;
            accountDao.update(target);

            //提交事务
            transactionManager.commit();
        } catch (Exception e){
            transactionManager.rollback();
            e.printStackTrace();
        }finally {
            //释放线程并解绑连接
            transactionManager.release();
        }
    }
}

连接工具类代码:

package yh.utils;

import javax.sql.DataSource;
import java.sql.Connection;

/**
 * 连接的工具类
 * 从数据源中获取连接,并实现和线程的绑定
 * @author YH
 * @create 2020-07-02 9:30
 */
public class ConnectionUtils {
    private ThreadLocal<Connection> tl = new ThreadLocal<>();
    private DataSource dataSource;

    public void setDataSource(DataSource dataSource){
        this.dataSource = dataSource;
    }

    /**
     * 获取当前线程上的连接
     * @return
     */
    public Connection getThreadConnection(){
        try {
            //1.先从Threadlocal上获取连接
            Connection conn = tl.get();
            //2.判断当前线程上是否有连接
            if (conn == null) {
                //3.如果ThreadLocal上没有连接,那么从数据源获取连接并存入ThreadLocal
                conn = dataSource.getConnection();
                tl.set(conn);
            }
            //4.返回当前线程连接
            return conn;
        } catch(Exception e){
            throw new RuntimeException(e);
        }
    }

    /**
     * 直接删除连接,让线程与连接解绑
     */
    public void removeConnection(){
        tl.remove();
    }
}

事务管理工具类的代码:

package yh.utils;

import java.sql.SQLException;

/**
 * 事务管理相关的工具类
 * 负责开启事务、提交事务、回滚事务、释放连接
 * @author YH
 * @create 2020-07-02 9:57
 */
public class TransactionManager {
    private ConnectionUtils connectionUtils;
    public void setConnectionUtils(ConnectionUtils connectionUtils){
        this.connectionUtils = connectionUtils;
    }

    /**
     * 开启事务
     */
    public void  beginTransaction(){
        try {
            connectionUtils.getThreadConnection().setAutoCommit(false);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 提交事务
     */
    public void commit(){
        try {
            connectionUtils.getThreadConnection().commit();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 回滚事务
     */
    public void rollback(){
        try {
            connectionUtils.getThreadConnection().rollback();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 释放资源 并 解绑线程和连接
     * 默认情况下线程回收到线程池其上依旧绑定了已经会受到连接池的连接,
     * 即连接时关闭的,再次启动线程时,能直接获取到连接,但这个连接显然
     * 无法使用,顾需在线程关闭后让其与连接解绑
     */
    public void release(){
        try {
            //回收到线程池
            connectionUtils.getThreadConnection().close();
            connectionUtils.removeConnection();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

线程回收到线程池,而线程绑定的连接也会回到连接池,如果该线程在此运行,那么此时获取它的连接是可以获取到的,但这个连接已经关闭回到连接池中,这样显然不行。所以在线程关闭前还需要做线程解绑操作。

解决事务问题后,发现我只是增加一个功能,就要对原有代码进行这么大的改动,而且业务层和持久层对两个工具类方法有很强的依赖,显然这就是问题,有什么解决办法呢?

一个动态代理的案例

场景:有生产者(被代理类)与经销商(代理方)。生产者可以售出产品,经销商也可以销售产品,但由经销商销售的产品经销商从中收取百分之20的金额。即要对被代理的工厂增加代理的代码,使得代理经销商能收益。如下:

基于接口的代理

image-20200702155111612

共同实现的接口:

/**
 * 定义一个代理类和被代理类共同要实现的接口
 * 从而实现基于接口的代理
 * @author YH
 * @create 2020-07-02 14:37
 */
public interface IProducer {
    /**
     * 销售产品
     * @param money
     */
    public void saleProduct(float money);

    /**
     * 产品售后
     * @param money
     */
    public void afterProduct(float money);
}

生产者(被代理对象):

public class Producer implements IProducer {
    public void saleProduct(float money){
        System.out.println("销售产品,并拿到钱:" + money);
    }
    public void afterProduct(float money){
        System.out.println("产品售后,并拿到钱:" + money);
    }
}

模拟消费(代理对象):

public class Client {
    public static void main(String[] args){
        //被代理对象(被内部类方法,需要声明为不可变的)
        final Producer producer = new Producer();
        /**
         * 动态代理:
         *  特点:字节码随意调用,随用随加载
         *  作用:不修改源码的基础上对方法增强
         *  分类:
         *      基于接口的动态代理
         *      基于子类的动态代理
         *  基于接口的动态代理:
         *      涉及的类:Proxy
         *      提供者:官方JDK
         *  如何创建代理对象:
         *      使用Proxy类的newProxyInstance方法
         *  创建代理对象的要求:
         *      被代理类至少实现一个接口,如果没有则不能使用
         *  newProxyInstance方法的参数:
         *      lassLoader:类加载器。用于加载代理对象字节码的,和被代理对象使用相同的类加载器。固定写法
         *      Class<?>[]:字节码数组。传递被代理对象实现的接口信息,使得代理对象和被代理对象具有相同的方法。固定写法
         *      InvocationHandler:用于提供增强的代码。用于说明如何代理(一般写一些接口的实现类,通常是匿名内部类)
         */
        IProducer proxyProducer = (IProducer)Proxy.newProxyInstance(producer.getClass().getClassLoader(),
                producer.getClass().getInterfaces(),
                new InvocationHandler(){
                    /**
                     * 执行被代理对象的任何方法都会经过这里
                     * @param o 代理对象的引用
                     * @param method 当前执行的方法
                     * @param objects 当前执行方法所需的参数
                     * @return 和被代理对象有相同的返回值
                     * @throws Throwable
                     */
                    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
                        //提供增强的代码
                        //1.获取方法执行的参数
                        Float money = (Float)objects[0];
                        //2.判断当前方法是不是销售
                        if("saleProduct".equals(method.getName())) {
                            return method.invoke(producer, money * 0.8f);
                        }
                        return null;
                    }
                });
        //测试调用被代理类的方法
        proxyProducer.saleProduct(10000f);
    }
}

最终实现了,在经销商处销售的商品工厂只能拿到8000。

基于接口的代理方式有一个缺陷就是必须要实现一个接口,无法实现接口要怎么办呢,那就是实现动态代理的另一种方式:基于子类的动态代理

这种方式需要有第三方 jar 包: cglib 的支持

增加 pom.xml 文件依赖:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

模拟消费(代理类)

/**
 * 基于子类的动态代理
 * @author YH
 * @create 2020-07-02 16:53
 */
public class Client {
    public static void main(String[] args){
        final Producer producer = new Producer();
        /**
         *  基于接口的动态代理:
         *      涉及的类:Enhancer
         *      提供者:第三方cglib
         *  如何创建代理对象:
         *      使用Enhancer类的create()方法
         *  创建代理对象的要求:
         *      被代理类不能是最终类
         *  create()方法的参数:
         *      class:字节码。用于指定被代理对象的字节码
         *      Callback:用于提供增强的代码。即如何代理,一般用该接口的子类接口的实现类 MethodInterceptor
         */
        Producer cglibProduct = (Producer   ) Enhancer.create(producer.getClass(), new MethodInterceptor() {
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                //提供增强的代码
                //1.获取方法执行的参数
                Float money = (Float)objects[0];
                //2.判断当前方法是不是销售
                if("saleProduct".equals(method.getName())) {
                    return method.invoke(producer, money * 0.8f);
                }
                return null;
            }
        });
        //测试调用方法
        cglibProduct.saleProduct(10000f);
    }
}

生产者(被代理类)

public class Producer implements IProducer {
    public void saleProduct(float money){
        System.out.println("销售产品,并拿到钱:" + money);
    }
    public void afterProduct(float money){
        System.out.println("产品售后,并拿到钱:" + money);
    }
}

结果相同:

image-20200702170930894

结合动态代理修改转账案例

image-20200703103354790

持久层代码不变

业务层代码(被代理对象):

public class AccountServiceImpl implements IAccountService {
    private AccountDaoImpl accountDao;

    public void setAccountDao(AccountDaoImpl accountDao){
        this.accountDao = accountDao;
    }
    @Override
    public void transfer(String sourceName, String targetName, Float money) {
        try {
            //根据账户信息获取账户对象
        	Account source = accountDao.findName(sourceName);
        	Account target = accountDao.findName(targetName);
            //转出账户减钱,转入账户加钱
            source.setMoney(source.getMoney() - money);
            target.setMoney(target.getMoney() + money);
            //提交更新
            accountDao.update(source);
//            int i = 1 / 0;
            accountDao.update(target);
        } catch (Exception e){
            //改为运行时异常,将异常抛给调用者(代理类)来处理,否则调用处后的回滚操作无法执行
            // (当然被代理类中也可以不捕获异常,代理类捕获)
            throw new RuntimeException(e);
        }
    }
}

代理工厂:

package yh.factory;

import yh.service.IAccountService;
import yh.utils.TransactionManager;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 用于创建Service的代理对象工厂
 * @author YH
 * @create 2020-07-03 8:55
 */
public class BeanFactory {
    private IAccountService accountService;
    private TransactionManager transactionManager;

    public void setAccountService(IAccountService accountService){
        this.accountService = accountService;
    }
    public void setTransactionManager(TransactionManager transactionManager){
        this.transactionManager = transactionManager;
    }

    public IAccountService getAccountService(){
        return (IAccountService)Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
                accountService.getClass().getInterfaces(),
                new InvocationHandler() {
                    /**
                     * 获取AccountService的代理对象
                     * @param proxy
                     * @param method
                     * @param args
                     * @return
                     * @throws Throwable
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Object returnValue = null;
                        try {
                            //1.开启事务
                            transactionManager.beginTransaction();
                            //2.执行操作
                            returnValue = method.invoke(accountService, args);
                            //3.提交事务
                            transactionManager.commit();
                            //4.返回被代理对象
                            return returnValue;
                        } catch(Exception e){
                            //5.回滚
                            transactionManager.rollback();
                            throw new RuntimeException(e);
                        } finally {
                            //6.释放资源
                            transactionManager.release();
                        }
                    }
                });
    }
}

被代理对象实现了一个接口,顾使用了基于接口的动态代理方式。

至此,无论业务层中有多少个方法,都会由代理类为其增加事务管理,而不是每个单独都要设置,在不增加业务类代码的情况下实现了功能的增强!

Spring AOP

使用 AOP 就可以通过配置的方式实现上面案例的功能,这也是通过案例引入 AOP 的原因。

  • AOP 相关术语

    • Joinpoint(连接点):

      指那些被拦截到的点。在 Spring 中,这些点指的是方法,因为 Spring 只支持方法类型的连接点

    • Pointcut(切点):

      切点的定义会匹配通知所要织入的一个或多个连接点,即定义拦截规则(通常使用明确的类和方法名称,可配合正则表达式使用)

    • Advice(通知/增强):

      拦截到 Joinpoint 之后要做的事情(新增的功能)

      通知的类型:前置通知、后置通知、异常通知、最终通知、环绕通知。对应到案例中如下:

      image-20200704070309875

    • Introduction(引入):

      一种特殊的通知。在不修改类代码的前提下,Introduction 可以在运行期为类动态地添加一些方法或属性

    • Target(目标对象):

      代理的目标对象

    • Weaving(织入):

      把增强应用到目标对象并创建新的代理对象的过程。

      Spring 采用动态代理织入(运行期);AspectJ 采用编译器织入和类装载期织入。

    • Proxy(代理):

      一个类被 AOP 织入增强后,就会产生一个结果代理类

    • Aspect(切面):

      切点和通知的结合

    小结

    ​ 通知包含了需要应用于多个对象的横切行为;连接点是程序执行过程中能够应用通知的所有点;切点定义了通知被应用的具体位置,即哪些连接点(方法),且定义了哪些连接点会得到通知。

  • 注意

    • 开发阶段(我们做的)

      • 核心业务代码,即开发主线由我们自己完成,熟悉也无需求;
      • 抽取出共用代码,制作成 aop 通知,开发阶段后最后再做。并在配置文件中声明切入点与通知间的关系,即切面。
    • 运行阶段(Spring 框架做的)

      • Spring 监控奇瑞乳垫方法的执行。一旦监控切入点方法执行,便使用代理机制,动态创建目标对象的代理对象。根据通知类别,在代理对象的对应位置,织入通知对应的功能,完成完整的代码逻辑运行。

    Spring 会根据目标类是否实现了接口来决定采用哪种动态代理方式。

    动态代理中用到的 invoke() 方法有拦截功能。

基于 xml 配置的 AOP 示例

image-20200704103124481

添加依赖

<!--用于解析Spring表达式-->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.5</version>
</dependency>

bean.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:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

<!--    ioc配置,将Service配置进来-->
    <bean id="accountService" class="yh.service.impl.AccountServiceImpl"/>


<!--    aop配置
    1.把通知bean也交给spring管理
    2.使用aop:config标签标示开始aop配置
    3.使用aop:aspect变迁配置切面
        id:给切面定义唯一的表示
        ref:指定切面的通知类bean的id
        4.内部标签中配置通知类型(前置通知为例)
            使用aop:before表示配置前置通知
                method:指定通知列中哪个方法用于通知
                pointcut:指定切入点表达式,表示对业务层中哪些方法进行增强
         切入点表达式写法:
            关键字:execution(表达式)
            标准写法 execution(public void 全类名.方法名(参数列表))
            其中权限修饰符可以省略,返回值类型、全类名、方法名、形参列表都可以用通配符代替
            全统配写法:* *..*.*(..)    多个包用 .. 表示一个包及其子包,形参列表.. 表示无参或多参
            但实际开发中只会对业务层的实现类方法进行统配,写法:* 业务层包路径.*.*(..)
-->
    <!-- 配置Logger类-->
    <bean id="logger" class="yh.utils.Logger"/>
    <!--配置aop-->
    <aop:config>
        <!--配置切面-->
        <aop:aspect id="loggerAdvice" ref="logger">
            <!--配置通知类型且定义通知方法和切入点方法的关联-->
            <aop:before method="printLog" pointcut="execution(public void yh.service.impl.AccountServiceImpl.saveAccount())"/>
        </aop:aspect>
    </aop:config>
</beans>

注意添加 aop 命名空间和约束

业务层接口

public interface IAccountService {
    /**
     * 模拟保存账户
     */
    void saveAccount();

    /**
     * 模式更新账户
     * @param i
     */
    void updateAccount(int i);

    /**
     * 模拟删除账户
     * @return
     */
    int deleteAccount();
}

业务层实现类

public class AccountServiceImpl implements IAccountService {
    public void saveAccount() {
        System.out.println("save account!");
    }

    public void updateAccount(int i) {
        System.out.println("update account!");
    }

    public int deleteAccount() {
        System.out.println("delete account!");
        return 1;
    }
}

通知类

public class Logger {
    /**
     * 输出日志:计划让其在切入点之前执行(即前置通知,在匹配的业务层方法前执行)
     */
    public void printLog(){
        System.out.println("输出日志...");
    }
}

测试

@Test
public void aopTest(){
    //1.获取容器
    ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
    //2.获取bean
    IAccountService as = (IAccountService)context.getBean("accountService");
    //3.执行方法
    as.saveAccount();
    //spring表达式所匹配的连接点方法才会被应用通知
    as.updateAccount(1);
    as.deleteAccount();
}

结果:image-20200704110858678

增加对应的方法,对四种通知进行配置:

<aop:config>
    <!--提取共用的表达式,供通知引用-->
    <aop:pointcut id="ref" expression="execution(* yh.service.impl.*.*(..))"/>
    <!--配置切面-->
    <aop:aspect id="loggerAdvice" ref="logger">
        <!--前置通知:切入点方法执行前通知-->
        <aop:before method="beforeAdvice" pointcut-ref="ref"/>

        <!--后置通知:切入点方法执行后通知-->
        <aop:after-returning method="afterAdvice" pointcut-ref="ref"/>

        <!--异常通知:切入点方法抛出异常时通知-->
        <aop:after-throwing method="exceptionAdvice" pointcut-ref="ref"/>

        <!--最终通知:无论切入点方法是否正常执行,都会执行-->
        <aop:after method="finallyAdvice" pointcut-ref="ref"/>
    </aop:aspect>
</aop:config>

image-20200704113819901

环绕通知配置

使用所编写的逻辑将被通知的目标方法完全包装起来(类似前面的动态代理对方法的增强),实现了一个方法中同时编写各类通知。

bean.xml中配置环绕通知

<!--配置环绕通知-->
<aop:around method="aroundAdvice" pointcut-ref="ref"/>

通知类中定义环绕通知的方法:

/**
 * 环绕通知
 * Spring提供了一个接口:ProceedingJoinPoint,改接口有一个 proceed() 方法,用于明确切入点方法
 * 改接口可作为环绕通知方法的参数使用,由Spring创建
 * 通过环绕通知我们可以手动控制被增强方法在通知中执行的位置
 */
public Object aroundAdvice(ProceedingJoinPoint pjp){
    Object returnValue = null;

    try {
        System.out.println("我是前置通知");

        //得到执行方法所需的参数
        Object[] args = pjp.getArgs();
        //执行切入点(业务类)方法
        returnValue = pjp.proceed(args);

        System.out.println("我是后置通知");
    } catch (Throwable throwable) {
        System.out.println("我是异常通知");
        throwable.printStackTrace();
    } finally {
        System.out.println("我是最终通知");
    }
    return returnValue;
}

类似代理类环绕增强被代理类,但明显更加简便明了,大多数事情被 spring 完成了,我们可以在被通知方法执行前后定义想要增加的功能,从而实现各类通知,结果如下:

image-20200704121351204

基于注解的配置

业务类要加上 @Service("accountService") 让 Spring 容器管理并指定标识 id

通知类

/**
 * 记录日志的工具类,定义通知的共用代码
 * @author YH
 * @create 2020-07-04 7:27
 * Component注解,指示Spring容器将创建管理当前类对象
 *      value:用于指定 bean 的 id。默认值为当前类名首字母小写
 *      (三层有各自的注解,但功能一样,是Component的子类)
 *  Aspect注解:表示当前类是一个切面
 */
@Component("logger")
@Aspect
public class Logger {
    /**
     * 通过注解定义可重用切点表达式,供通注解知引用
     */
    @Pointcut("execution(* yh.service.impl.*.*(..))")
    public void spe(){}

    /**
     * 前置通知
     */
    @Before("spe()")
    public void beforeAdvice(){
        System.out.println("前置通知...");
    }
    /**
     * 后置通知
     */
    @AfterReturning("spe()")
    public void afterAdvice(){
        System.out.println("后置通知...");
    }
    /**
     * 异常通知
     */
    @AfterThrowing("spe()")
    public void exceptionAdvice(){
        System.out.println("异常通知...");
    }
    /**
     * 最终通知
     */
    @After("spe()")
    public void finallyAdvice(){
        System.out.println("最终通知...");
    }

    /**
     * 环绕通知
     * Spring提供了一个接口:ProceedingJoinPoint,改接口有一个 proceed() 方法,用于明确切入点方法
     * 改接口可作为环绕通知方法的参数使用,由Spring创建
     * 通过环绕通知我们可以手动控制被增强方法在通知中执行的位置
     */
    @Around("spe()")
    public Object aroundAdvice(ProceedingJoinPoint pjp){
        Object returnValue = null;

        try {
            System.out.println("我是前置通知");

            //得到执行方法所需的参数
            Object[] args = pjp.getArgs();
            //执行切入点(业务类)方法
            returnValue = pjp.proceed(args);

            System.out.println("我是后置通知");
        } catch (Throwable throwable) {
            System.out.println("我是异常通知");
            throwable.printStackTrace();
        } finally {
            System.out.println("我是最终通知");
        }
        return returnValue;
    }
}

bean.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:aop="http://www.springframework.org/schema/aop"
       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/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

<!--    配置Spring创建容器时要扫描的包-->
    <context:component-scan base-package="yh"/>

<!--    开启注解aop的支持-->
    <aop:aspectj-autoproxy/>
</beans>

使用注解,命名空间和约束都需要设置

纯注解获取 Spring 容器方式与通过 xml 配合不一样,如下:

先定义一个 java 配置类:

@Configuration
@ComponentScan("yh") //指定扫描的包
@EnableAspectJAutoProxy //开启基于注解AOP的支持
public class SpringConfiguration {
}
/**
 * 测试纯注解配置
 */
@Test
public void annotationAopTest2(){
    ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfiguration.class);
    IAccountService service = (IAccountService)context.getBean("accountService");
    service.saveAccount();
}

基于注解配置通知时,建议应用于环绕通知。其他通知的顺序可能不是想要的结果(如后置通知在最终通知之前执行)

改造转账案例

  • 基于 XML 配置

    改动几乎都在 bean.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:aop="http://www.springframework.org/schema/aop"
           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/aop
            http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    
    <!--配置service-->
        <bean id="accountService" class="yh.service.impl.AccountServiceImpl">
            <property name="accountDao" ref="accountDao"/>
        </bean>
    
    <!--配置dao-->
        <bean id="accountDao" class="yh.dao.impl.AccountDaoImpl">
            <property name="runner" ref="runner"/>
            <property name="connectionUtils" ref="connectionUtils"/>
        </bean>
    <!--    配置QueryRunner-->
        <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
            <constructor-arg ref="dataSource"/>
        </bean>
    
    <!--配置数据源-->
        <!-- 读取数据源文件的位置-->
        <context:property-placeholder location="classpath:jdbc.properties"/>
        <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
            <property name="driverClassName" value="${driverClassName}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
        </bean>
    
        <!--    配置事务管理工具类-->
        <bean id="transactionManager" class="yh.utils.TransactionManager">
            <property name="connectionUtils" ref="connectionUtils"/>
        </bean>
        <!--    配置连接工具类-->
        <bean id="connectionUtils" class="yh.utils.ConnectionUtils">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    
    <!--    AOP配置-->
        <aop:config>
            <!--提取共用的表达式,供通知引用-->
            <aop:pointcut id="pe1" expression="execution(* yh.service.impl.AccountServiceImpl.transfer(..))"/>
            <!--配置切面-->
            <aop:aspect id="tr" ref="transactionManager">
                <!--确定通知类型,定义通知方法和切入点方法的关联-->
                <aop:before method="beginTransaction" pointcut-ref="pe1"/>
                <aop:after-throwing method="rollback" pointcut-ref="pe1"/>
                <aop:after-returning method="commit" pointcut-ref="pe1"/>
                <aop:after method="release" pointcut-ref="pe1"/>
    
            </aop:aspect>
        </aop:config>
    
    </beans>
    
  • 纯注解配置

    基于注解配置中,由于 Spring 原因,最终通知(@After)和后置通知(@AfterReturning)或异常通知(@AfterThrowing)的执行顺序无法控制,所以使用环绕通知:

    image-20200705151342804

    持久层、业务层等工具列类只用加上组件注解(@Component 注解之类)以及其成员属性的注入注解(@Autowired 注解)即可

    SpringConfiguration 配置类:

    @Configuration	//表名此类为配置类
    @EnableAspectJAutoProxy	//开启Spring AOP支持
    @ComponentScan("yh")	//指定spring创建容器要扫描的包
    @Import(JdbcConfig.class)	//导入子配置类
    public class SpringConfiguration {
        @Bean(name = "runner")	//将方法的返回值创建为bean 并存入Spring容器中
        public QueryRunner createQueryRunner(DataSource dataSource){//形参可自动注入
            return new QueryRunner(dataSource);
        }
    }
    

    JDBC配置类:

    @PropertySource("classpath:jdbc.properties")	//引入外部properties属性文件
    public class JdbcConfig {
        //@Value是@PropertySource的属性注解,用于读取配置文件中的key-value
        @Value("${driverClassName}")
        private String driver;
    
        @Value("${jdbc.url}")
        private String url;
    
        @Value("${jdbc.username}")
        private String username;
    
        @Value("${jdbc.password}")
        private String password;
    
        @Bean(name="dataSource")
        public DataSource createDataSource(){
            DruidDataSource ds = new DruidDataSource();
            ds.setDriverClassName(driver);
            ds.setUrl(url);
            ds.setUsername(username);
            ds.setPassword(password);
            return ds;
        }
    }
    

    通知类:

    /**
     * 事务管理相关的工具类
     * 负责开启事务、提交事务、回滚事务、释放连接
     * @author YH
     * @create 2020-07-02 9:57
     */
    @Component("txManager")
    @Aspect	//指示此类是切面
    public class TransactionManager {
        @Autowired
        private ConnectionUtils connectionUtils;
    
        @Pointcut("execution(* yh.service.impl.*.*(..))")
        public void spe(){}
    
        /**
         * 开启事务
         */
        public void  beginTransaction(){
            try {
                connectionUtils.getThreadConnection().setAutoCommit(false);
                System.out.println("开启事务");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 提交事务
         */
        public void commit(){
            try {
                connectionUtils.getThreadConnection().commit();
                System.out.println("提交事务");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 回滚事务
         */
        public void rollback(){
            try {
                connectionUtils.getThreadConnection().rollback();
                System.out.println("回滚事务");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 释放资源 并 解绑线程和连接
         * 默认情况下线程回收到线程池其上依旧绑定了已经会受到连接池的连接,
         * 即连接时关闭的,再次启动线程时,能直接获取到连接,但这个连接显然
         * 无法使用,顾需在线程关闭后让其与连接解绑
         */
        public void release(){
            try {
                //回收到线程池
                connectionUtils.getThreadConnection().close();
                connectionUtils.removeConnection();
                System.out.println("关闭资源");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        /**
        * 环绕通知,配置注解通知建议只使用环绕通知
        */
        @Around("spe()")
        public Object aroundAdvice(ProceedingJoinPoint pjp){
            Object returnValue = null;
    
            try {
                this.beginTransaction();
    
                Object[] args = pjp.getArgs();
                returnValue = pjp.proceed(args);
    
                this.commit();
            } catch (Throwable throwable) {
                this.rollback();
                throwable.printStackTrace();
            } finally {
                this.release();
            }
            return returnValue;
        }
    }
    

    properties属性文件:

    driverClassName=com.mysql.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/mybatis?ssl=true&amp;useUnicode=true&amp;characterEncoding=utf8
    jdbc.username=root
    jdbc.password=root
    

    xml 引入外部属性文件的两种方式:

     <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> 
     	<property name="location" value="classpath:jdbc.properties"/>
     </bean>
    
    <context:property-placeholder location="classpath:jdbc.properties"/>
    

Spring 中的 JdbcTemplate

概述

Spring 框架提供了很多的操作模板类

  • 操作关系型数据
    • JdbcTemplate
    • HibernateTemplate
  • 操作 nosql 数据库
    • RedisTemplate
  • 操作消息队列
    • JmsTemplate

应用

关键依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.2.6.RELEASE</version>
</dependency>

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

基本配置

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

<!--    配置JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

<!--    配置数据源-->
    <!--引入外部属性文件-->
    <context:property-placeholder location="data.properties"/>
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
</beans>

data.properties

driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?ssl=true&useUnicode=true&characterEncoding=utf8
jdbc.username=root
jdbc.password=root

简单的 CRUD

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import yh.domain.Account;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

/**
 * JdbcTemplate的简单用法
 * @author YH
 * @create 2020-07-05 17:25
 */
public class JdbcTemplate1 {
    public static void main(String[] args){
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
        JdbcTemplate jt = (JdbcTemplate)context.getBean("jdbcTemplate");
        //保存
          jt.update("insert into mybatis.account(name,money) values(?,?)","zzz",2000);
        //修改
          jt.update("update mybatis.account set money=money+? where name=?",99,"aaa");
        //删除
          jt.update("delete from mybatis.account where id=?",7);
        //查询所有
        List<Account> accountList = jt.query("select * from mybatis.account", new AccountRowMapper());
        for(Account a : accountList){
            System.out.println(a);
        }
        //查询一个
        List<Account> accountList = jt.query("select * from mybatis.account where id=?",
                new AccountRowMapper(),3);
        System.out.println(accountList.isEmpty() ? "没有结果" : accountList.get(0));

        //查询返回一行一列,常用于分页中获取总记录数
        Integer total = jt.queryForObject("select count(*) from mybatis.account where id>?",
                Integer.class, 1);
        System.out.println(total);
    }

    /**
     * 处理查询结果集的封装
     */
    static class AccountRowMapper implements RowMapper<Account> {
        /**
         * @param resultSet 查询sql返回的结果集
         * @param i 所查询表的行数
         */
        @Override
        public Account mapRow(ResultSet resultSet, int i) throws SQLException {
            Account account = new Account();
            account.setId(resultSet.getInt("id"));
            account.setName(resultSet.getString("name"));
            account.setMoney(resultSet.getFloat("money"));

            return account;
        }
    }

在 Dao 中使用

dao 中使用 JdbcTemplate 有两种方式,普通做法,在 dao 中增加一个 JdbcTemplate 引用属性,交由 spring 注入,而后进行 update()、query() 调用。但当有多个 dao 时,每个 dao 内都要重复定义代码:private JdbcTemplate jdbcTemplate;

第二种方式:使用 Spring 提供的 JdbcDaoSupport 抽象类,其内部封装了 JdbcTemplate 属性,只需给予一个 DataSource 给它就可以获取 JdbcTemplate 对象,让我们的 dao 继承它就可以获取属性以及注入 DataSource:

image-20200706051500764

持久层接口:

public interface IAccountDao {
    /**
     * 通过Id查账户
     * @param id
     * @return
     */
    public Account findAccountById(Integer id);

    /**
     * 通过Id查账户
     * @param name
     * @return
     */
    public Account findAccountByName(String name);

    /**
     * 修改账户
     * @param account
     */
    public void updateAccount(Account account);
}

持久层实现类:

public class AccountDaoImpl extends JdbcDaoSupport implements IAccountDao {
    /*注:继承父类所获得的属性可进行注入,数据源就是通过此特性注入(见bean.xml)*/

    @Override
    public Account findAccountById(Integer id) {
        JdbcTemplate jt = getJdbcTemplate();
        List<Account> list = jt.query("select * from mybatis.account where id=?",
                new AccountRowMapper(), id);
        return list.isEmpty() ? null : list.get(0);
    }

    @Override
    public Account findAccountByName(String name) {
        JdbcTemplate jt = getJdbcTemplate();
        List<Account> list = jt.query("select * from mybatis.account where name=?",
                new AccountRowMapper(), name);
        if(list.size() > 1){
            throw new RuntimeException("结果集不唯一,查询的对象有多个");
        }
        return list.isEmpty() ? null : list.get(0);
    }

    @Override
    public void updateAccount(Account account) {
        JdbcTemplate jt = getJdbcTemplate();
        jt.update("update mybatis.account set name=?,money=? where id=?",
                account.getName(),account.getMoney(),account.getId());
    }

封装查询结果集的工具类:

public class AccountRowMapper implements RowMapper<Account> {
    @Override
    public Account mapRow(ResultSet resultSet, int i) throws SQLException {
        Account account = new Account();
        account.setId(resultSet.getInt("id"));
        account.setName(resultSet.getString("name"));
        account.setMoney(resultSet.getFloat("money"));
        return account;
    }
}

bean.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">

<!--    配置dao-->
    <bean id="accountDao" class="yh.dao.impl.AccountDaoImpl">
        <!--给所继承的父类的属性注入值-->
        <property name="dataSource" ref="dataSource"/>
    </bean>

<!--    配置数据源-->
    <!--引入外部属性文件-->
    <context:property-placeholder location="data.properties"/>
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
</beans>

data.properties

driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?ssl=true&useUnicode=true&characterEncoding=utf8
jdbc.username=root
jdbc.password=root

注意:第一种方式可以使用注解或者 xml 配置;但第二种方式只能用 xml 配置

Spring 中的事务控制

JavaEE 体系进行分层开发,事务处理位于业务层,Spring 提供了分层设计业务层的事务处理解决方案。Spring 提供了一组基于 AOP 的事务控制接口 ,可以通过编程或配置方式实现。

  • PlatformTransactionManager 接口提供了三个方法:
//获取事务状态信息
TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
//提交事务
void commit(TransactionStatus var1) throws TransactionException;
//回滚事务
void rollback(TransactionStatus var1) throws TransactionException;

开发中使用的是它额实现类对象对事务进行管理:

//使用 Spring JDBC 或 iBatis 进行持久化数据时使用 
org.springframework.jdbc.datasource.DataSourceTransactionManager
    
//使用 Hibernate 版本进行持久化数据时使用
org.springframework.orm.hibernate5.HibernateTransactionManager
  • 事务的定义信息对象 TransactionDefinition:
//获取事务对象的名称
String getName();
//获取事务隔离级别
int getIsolationLevel();
//获取事务传播行为
int getPropagationBehavior();
//获取事务超时时间
int getTimeout();
//获取事务是否只读
boolean isReadOnly();

读写型事务:增加、删除、修改开启事务;

只读型事务:执行查询时,也会开启事务。

  1. 事务隔离级别

    事务隔离级别反应了事务提交并发访问时的处理态度

    • ISOLATION_DEFAULT:默认级别,归属下列某一类
    • ISOLATION_READ_UNCOMMITTED:可以读取未提交数据
    • ISOLATION_READ_COMMITTED:只能读取已提交数据,解决脏读问题(Oracle 默认级别)
    • ISOLATION_REPEATABLE_READ:是否读取其他事务提交修改后的数据,解决不可重复读取问题(MySQL默认级别)
    • ISOLATION_SERIALIZABLE:是否读取其他事务提交添加后的数据,解决幻影读问题
  2. 事务的传播行为

    REQUIRED:如果当前没有事务,就新建一个事务;如果已经存在一个事务,加入到这个事务中(默认值)

    SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方法执行(没有事务)

    MANDATORY:使用当前的事务,如果当前没有事务,就抛出异常

    REQUERS_NEW:新建事务,如果当前在事务中,把当前事务挂起。

    NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起

    NEVER:以非事务方式运行,如果当前存在事务,抛出异常

    NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行 REQUIRED 类似的操作

  3. 超时时间

    默认值是 -1,没有超时限制;如需有,以秒为单位进行设置

  4. 是否是只读事务

    建议查询时设置为只读

  • TransactionStatus 接口

    /**
    * TransactionStatus接口描述了某个时间点上事务对象的状态信息,包含有6个具体的操作
    */
    //刷新事务
    void flush();
    //获取是否存在存储点
    boolean hasSavepoint();
    //获取事务是否完成
    boolean isCompleted();
    //获取事务是否为新的事务
    boolean isNewTransaction();
    //获取事务是否回滚
    boolean isRollbackOnly();
    //设置事务回滚
    void setRollbackOnly();
    

基于 XML 的声明式事务控制(配置方式)

image-20200706133549093

环境搭建

  • 必备依赖:spring-jdbc-xxx 和 spring-tx-xxx 等

  • 创建 spring 的配置文件并导入约束

  • 准备数据库表和实体类

  • 编写业务层接口和实现类

  • 编写 Dao 接口和实现类

    以上按照项目需求编写,关键是配置,个人理解是上面所写的 AOP 事务的更强形式

  • 编写 bean.xml 配置

    • 各层级配置
    • 事务管理器配置
    <!--配置一个事务管理器-->
        <bean id="transactionManager"
              class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <!--注入数据源-->
            <property name="dataSource" ref="dataSource"/>
        </bean>
    <!--配置事务-->
        <!--配置事务的通知并引用事务管理器(用于管理事务)-->
        <tx:advice id="txAdvice" transaction-manager="transactionManager">
            <!--配置事务的属性-->
            <tx:attributes>
                <!--指定方法名称:是核心业务方法
                 read-only:是否只读事务,默认false
                 isolation:指定隔离级别,默认值是使用的数据库默认隔离级别
                 propagation:指定事务的传播行为
                 timeout:指定超时时间,默认值-1表示永不超时
                 rollback-for:指定会进行回滚的异常类型,未指定表示任何异常都回滚
                 no-rollback-for:指定不进行回滚的异常类型,未指定表示任何异常都回滚
                -->
                <tx:method name="*" read-only="false" propagation="REQUIRED"/>
                <tx:method name="find*" read-only="true" propagation="SUPPORTS"/>
            </tx:attributes>
        </tx:advice>
    
    <!--aop切点表达式-->
        <aop:config>
            <!--配置切入点表达式-->
            <aop:pointcut id="pt1" expression="execution(* yh.service.impl.*.*(..))"/>
            <!--配置切入点表达和事务通知的关系-->
            <aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"/>
        </aop:config>
    

    对比前面指定通知位置或者使用环绕通知都或多或少需要手动去处理代理逻辑,从而控制控制事务的方法的执行顺序。

    而使用 Spring 事务控制器,配置一个事务通知后,我们只需关联切入点表达式和事务通知即可。

基于注解的配置方式

image-20200706170145152

  • 必备依赖:spring-jdbc-xxx 和 spring-tx-xxx 等

  • 创建 spring 的配置文件并导入约束

  • 准备数据库表和实体类

  • 创建业务层接口及其实现类,并使用符合语义的注解让 spring 进行管理

  • 创建持久层接口及其实现类,并使用符合语义的注解让 spring 进行管理

  • 配置步骤

    • 总 JavaConfig 类

      @Configuration
      @Import(value={jdbcConfig.class,JdbcTemplateConfig.class,TransactionManager.class})
      @ComponentScan("yh")	//创建spring容器时扫描的包
      @EnableTransactionManagement    //开启基于注解的事务管理功能(与开启aop支持不要混淆)
      public class SpringConfiguration {
      }
      
    • 创建事务管理器配置类并注入数据源

      public class TransactionManager {
          @Bean(name="txManager")
          public PlatformTransactionManager createTxManager(DataSource dataSource){
              return new DataSourceTransactionManager(dataSource);
          }
      }
      
    • 数据源、JdbcTemplate 的 JavaConfig:

      @PropertySource("classpath:jdbc.properties")	//引入外部的properties属性文件
      public class jdbcConfig {
          @Value("${driverClassName}")
          private String driver;
      
          @Value("${jdbc.url}")
          private String url;
      
          @Value("${jdbc.username}")
          private String username;
      
          @Value("${jdbc.password}")
          private String password;
      
          @Bean(name="dataSource")
          public DataSource createDataSource(){
              DruidDataSource ds = new DruidDataSource();
              ds.setDriverClassName(driver);
              ds.setUrl(url);
              ds.setUsername(username);
              ds.setPassword(password);
              return ds;
          }
      }
      

      jdbc.properties 属性文件:

      driverClassName=com.mysql.jdbc.Driver
      jdbc.url=jdbc:mysql://localhost:3306?ssl=true&useUnicode=true&characterEncoding=utf8
      jdbc.username=root
      jdbc.password=root
      
      public class JdbcTemplateConfig {
          @Bean(name="jdbcTemplate")
          public JdbcTemplate ceeateJdbcTemplate(DataSource dataSource){
              return new JdbcTemplate(dataSource);
          }
      }
      
    • 在业务层使用 @Transactional 注解

      @Service("accountService")
      @Transactional(readOnly = true,propagation = Propagation.SUPPORTS)
      public class AccountServiceImpl implements IAccountService {
          /**
           * 获取dao对象
           */
          @Autowired
          private IAccountDao accountDao;
      
          /**
          * 转账方法
          * Transactional注解与<tx:Advice/>标签含义相同,配置事务通知
       	* 可用在接口、类、方法上,表示其支持事务
       	* 三个位置的优先级 方法>类>接口
       	*/
          @Override
          @Transactional(readOnly = false,propagation = Propagation.REQUIRED)
          public void transferAccount(String sourceName, String targetName, float money) {
              //获取账户
              Account source = accountDao.findByName(sourceName);
              Account target = accountDao.findByName(targetName);
              //修改账户金额
              source.setMoney(source.getMoney()-money);
              target.setMoney(target.getMoney()+money);
              //将修改后的账户更新至数据库
              accountDao.updateAccount(source);
      //        int i = 1/0;//模拟异常
              accountDao.updateAccount(target);
          }
      }
      
  • 测试

    @RunWith(SpringJUnit4ClassRunner.class)	//替换原有运行器
    @ContextConfiguration(classes = SpringConfiguration.class) //指定容器配置来源
    public class MyTest {
        @Autowired
        private IAccountService service;
        @Test
        public void test1(){
            service.transferAccount("aaa","ccc",100);
        }
    }
    

    基于纯注解配置以上。

    使用 Spring 事务管理,业务代码全称躺着任由摆布,各层级没有代码侵入问题。

原文地址:https://www.cnblogs.com/csyh/p/13257164.html