Spring

简介

Spring 框架是由于软件开发的复杂性而创建的。Spring 使用的是基本的 JavaBean 来完成以前只可能由EJB完成的事情。然而,Spring 的用途不仅仅限于服务器端的开发。从简单性、可测试性和松耦合性角度而言,绝大部分 Java 应用都可以从 Spring 中受益。

  • 目的:解决企业应用开发的复杂性

  • 功能:使用基本的 JavaBean 代替 EJB,并提供了更多的企业应用功能

  • 范围:任何 Java 应用

Spring 是一个轻量级控制反转(IoC)和面向切面(AOP)的容器框架。

Spring 理念:使现有技术更加实用 . 本身就是一个大杂烩 , 整合现有的框架技术

早些年使用框架 SSH:Struct2 + Spring + Hibernate

现在 SSM : SpringMVC +Spring + Mybatis

maven 配置的话:

1
2
3
4
5
6
7
<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.3</version>
</dependency>

优点

  • Spring 是开源的免费的框架(容器)

  • 轻量级、非侵入式的框架(也就是不会破坏之前的架构,引入后反而会使得编程更简单了)

  • (核心)具有控制反转(IOC),面向切面编程(AOP)的特点(核心)

  • 支持事务的处理(由于 AOP)

    ps:事务是对一系列数据库进行统一的提交和回滚

总结:Spring 是一个轻量级的控制反转和面向切面编程的框架

组成

参考:https://www.cnblogs.com/xiaobaizhiqian/p/7616453.html

Spring 分为七大组成模块

image-20210125160108693

  • 核心容器(Spring Core)

核心容器提供 Spring 框架的基本功能。Spring 以 bean 的方式组织和管理 Java 应用中的各个组件及其关系。Spring 使用 BeanFactory 来产生和管理 Bean,它是工厂模式的实现。BeanFactory 使用控制反转(IoC)模式将应用的配置和依赖性规范与实际的应用程序代码分开。

  • 应用上下文(Spring Context)

Spring 上下文是一个配置文件,向 Spring 框架提供上下文信息。Spring 上下文包括企业服务,如 JNDI、EJB、电子邮件、国际化、校验和调度功能。

  • Spring 面向切面编程(Spring AOP)

通过配置管理特性,Spring AOP 模块直接将面向方面的编程功能集成到了 Spring 框架中。所以,可以很容易地使 Spring 框架管理的任何对象支持 AOP。Spring AOP 模块为基于 Spring 的应用程序中的对象提供了事务管理服务。通过使用 Spring AOP,不用依赖 EJB 组件,就可以将声明性事务管理集成到应用程序中。

  • JDBC 和 DAO 模块(Spring DAO)

JDBC、DAO 的抽象层提供了有意义的异常层次结构,可用该结构来管理异常处理,和不同数据库供应商所抛出的错误信息。异常层次结构简化了错误处理,并且极大的降低了需要编写的代码数量,比如打开和关闭链接。

  • 对象实体映射(Spring ORM)

Spring 框架插入了若干个 ORM 框架,从而提供了 ORM 对象的关系工具,其中包括了 Hibernate、JDO 和 IBatis SQL Map 等,所有这些都遵从 Spring 的通用事物和 DAO 异常层次结构。

  • Web 模块(Spring Web)

Web 上下文模块建立在应用程序上下文模块之上,为基于 web 的应用程序提供了上下文。所以 Spring 框架支持与 Struts 集成,web 模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作。

  • MVC 模块(Spring Web MVC)

MVC 框架是一个全功能的构建 Web 应用程序的 MVC 实现。通过策略接口,MVC 框架变成为高度可配置的。MVC 容纳了大量视图技术,其中包括 JSP、POI 等,模型来有 JavaBean 来构成,存放于 m 当中,而视图是一个街口,负责实现模型,控制器表示逻辑代码,由 c 的事情。Spring 框架的功能可以用在任何 J2EE 服务器当中,大多数功能也适用于不受管理的环境。Spring 的核心要点就是支持不绑定到特定 J2EE 服务的可重用业务和数据的访问的对象,毫无疑问这样的对象可以在不同的 J2EE 环境,独立应用程序和测试环境之间重用。

拓展

image-20210125160937644

Spring Boot 与 Spring Cloud

  • Spring Boot 是 Spring 的一套快速配置脚手架,可以基于 Spring Boot 快速开发单个微服务;

    ps:需要记住约定大于配置;

  • Spring Cloud 是基于 Spring Boot 实现的;

  • Spring Boot 专注于快速、方便集成的单个微服务个体,Spring Cloud 关注全局的服务治理框架;

    什么是微服务

  • Spring Boot 使用了约束优于配置的理念,很多集成方案已经帮你选择好了,能不配置就不配置 , Spring Cloud 很大的一部分是基于 Spring Boot 来实现Spring Boot 可以离开 Spring Cloud 独立使用开发项目但是 Spring Cloud 离不开 Spring Boot,属于依赖的关系

  • SpringBoot 在 SpringClound 中起到了承上启下的作用,如果你要学习 SpringCloud 必须要学习 SpringBoot。

  • Springboot 是非常重要的,因为大多数公司都在用,然而学习 Springboot 就需要完全掌握好 Spring 和 SpringMVC 以做到承上启下

IOC

IOC 理论

  • IOC 理论背景:

在之前所学的面向对象开发中,所有的组件都是对象,一般都是通过一个对象中 new 另一个对象,然后调用方法,对象合作然后去实现业务逻辑;

如下图:就像一组齿轮,一个转带动另一个转,然后再带动另一个转;

img

然而这种开发的思想其实有点危险,就是对象与对象之间耦合度有点太高了。一个出问题,其他可能也会动不了了。

img

因此我们开发终极目标:降低对象间耦合度(如果能完全解耦那再好不过了)

因此Michael Mattson提出IOC 理论


  • 下面以一个开发实例进行理解:

原先开发流程:

Dao 接口 → DaoImpl → Service 接口 → ServiceImpl

我们先用我们原来的方式写一段代码 .

1、先写一个 UserDao 接口

1
2
3
public interface UserDao {
public void getUser();
}

2、再去写 Dao 的实现类

1
2
3
4
5
6
public class UserDaoImpl implements UserDao {
@Override
public void getUser() {
System.out.println("获取用户数据");
}
}

3、然后去写 UserService 的接口

1
2
3
public interface UserService {
public void getUser();
}

4、最后写 Service 的实现类

注意看,我 service 实现类一般都会 new 一个对象,这就是导致耦合的原因

1
2
3
4
5
6
7
8
public class UserServiceImpl implements UserService {
private UserDao userDao = new UserDaoImpl();

@Override
public void getUser() {
userDao.getUser();
}
}

5、测试一下(这里每次更换不同接口都需要在service层更改new的接口)

1
2
3
4
5
@Test
public void test(){
UserService service = new UserServiceImpl();
service.getUser();
}

这是我们原来的方式 , 开始大家也都是这么去写的对吧 . 那我们现在修改一下 .

把 Userdao 的实现类增加一个 .

1
2
3
4
5
6
public class UserDaoMySqlImpl implements UserDao {
@Override
public void getUser() {
System.out.println("MySql获取用户数据");
}
}

紧接着我们要去使用 MySql 的话 , 我们就需要去 service 实现类里面修改对应的实现

1
2
3
4
5
6
7
8
public class UserServiceImpl implements UserService {
private UserDao userDao = new UserDaoMySqlImpl();

@Override
public void getUser() {
userDao.getUser();
}
}

再假设, 我们再增加一个 Userdao 的实现类 .

1
2
3
4
5
6
public class UserDaoOracleImpl implements UserDao {
@Override
public void getUser() {
System.out.println("Oracle获取用户数据");
}
}

那么我们要使用 Oracle , 又需要去 service 实现类里面修改对应的实现 .

假设我们的这种需求非常大 , 这种方式就根本不适用了, , 每次变动 , 都需要修改大量代码 .

这种设计的耦合性太高了, 牵一发而动全身 .

那我们如何去解决呢 ?

(有的人会说 工厂模式!策略模式,这当然也 ok;)

在这里, 我们不去 new 它出来 , 而是留出一个接口 , 利用 set 方法去实现我们的目的

相当于解耦了,它上一层怎么样和我无关了,我写代码只需要 focus 在这一层即可了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class UserServiceImpl implements UserService{
//控制反转就是
//原来是把这里的 private UserDao userDao = new UserDaoImpl();(原来是写死了的)
//然后需要用哪个接口 就把new后面的改了
//变更为以下:
private UserDao userDao;
//利用set实现动态实现值的注入
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}


@Override
public void getUser() {
userDao.getUser();
}
}

现在去我们的测试类里 , 进行测试 ;(有了set方法就可以在调用的时候由用户选择调用的接口)

把这里想象成用户的使用界面,用户点击不同按钮就相当于创建了不同的对象,去使用对应的方法

我们的底层代码根本没有进行修改,但是能够对相应选择做出相应的回应(结果)

1
2
3
4
5
6
7
8
9
10
11
12
public class MyTest {
@Test
public void test() {
UserServiceImpl service = new UserServiceImpl();
service.setUserDao( new UserDaoMySqlImpl() );
service.getUser();
//想使用Oracle当然也可以
service.setUserDao( new UserDaoOracleImpl() );
service.getUser();
}
}

image-20210125180219993

由此我们发现区别:

  • 在之前,程序是占据主动的一方,也就是说,功能的应用、控制权在程序员手中,只有程序员能够对 Service 进行修改,然后才能调用相应的方法
  • 而现在,用了 set 方法后,程序就写死了,不用修改,控制权在用户手中;

这种思想从本质上解决了问题,我们程序员不用再去管理对象的创建问题了,这样系统的耦合性就降低了,我们开发就可以专注于业务实现,而不用老去进行修改没必要的接口代码了。

通俗来说就是,我们只用注重相应功能开发,至于用哪个接口,留给用户自己去选择了。

IOC 本质

image-20210128152909864

控制反转 IoC(Inversion of Control),是一种设计思想,DI(依赖注入)是实现 IoC 的一种方法,也有人认为 DI 只是 IoC 的另一种说法。

没有 IoC 的程序中 , 我们使用面向对象编程 , 对象的创建与对象间的依赖关系完全硬编码在程序中,对象的创建由程序自己控制

然而,控制反转后将对象的创建转移给第三方,因此控制反转即:获得依赖对象的方式反转了。

img

  • 那么 下面就详解一下控制反转 IOC

控制反转/控制反向/控制倒置,IOC(Inversion of Control)

反转反转,我们到底反转了什么呢?答案是:反转了控制权

简单来说,就是通过引入第三方,将复杂系统 → 可相互合作的对象,同时,这些对象的内部实现对外部是透明

ps:什么叫透明的呢?意思是说,你不用管其中运行的原理,只用知道他能实现这个功能就行啦。

同时可以降低解决问题的复杂度(不用改之前的代码),我们写的组件就可以灵活的被重用以及扩展

img

IOC 容器充当了第三方的角色,使得 ABCD 四个对象无耦合了,齿轮的转动依靠 IOC 容器,ABCD 四个对象的控制权给了 IOC 容器

下面我们把容器去掉,发现 ABCD 四个对象达到了 0 耦合。

假设 1 2 3 4 号程序员分别去开发 A B C D 四个轮子,各程序员只需要 focus 开发自己的类,不用管别人,至于别人什么时候要用到自己这个轮子,根本就不是自己应该管的事情了。

举个例子,在没有引入这个思想之前,在我们上述 ServiceImpl 中去创建了 UserDaoImpl 对象去调用相应方法对吧

这样对象 A 与对象 B 就有了直接的联系。

然而,软件系统引入了 IOC 容器后,我们就不用这样了,在上述代码示例中,那个接口实际上就是一个 IOC 容器了

让对象之间没有了直接的联系,当我们对象 A 的代码跑着跑着发现,哦,我需要 B 对象了,这个时候接口(IOC 容器)就会创建一个对象 B 注入到对象 A 需要的地方(由之前的主动创建,变成了被动的注入,控制权反转了),因此这思想/行为称为控制反转

img

HelloSpring(第一个 Spring 程序)

整体目录结构:

image-20210128154915378

首先需要导入 jar 包:

在 maven 中搜 spring 会出现很多个包含 spring 的包,这里我们使用了 webmvc 的;

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.3.RELEASE</version>
</dependency>
  1. 编写 Hello 实体类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package com.hpg.pojo;

    public class Hello {
    private String id;

    public String getId() {
    return id;
    }

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

    @Override
    public String toString() {
    return "Hello{" +
    "id='" + id + '\'' +
    '}';
    }
    }
  2. 编写 spring 文件

    可以这么理解:java 中 创建对象方式为 类型 变量名 = new 类型();

    • id → 变量名
    • class → 类型();
    • name → 类的属性
    • value → 属性的值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <?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
    https://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--使用Spring来创建对象,在Spring之中这些称为Bean-->
    <!--类型 变量名 = new 类型();-->
    <!--这里的id等价于变量名 class相当于全路径,相当于要new的对象-->
    <bean id="hello" class="com.hpg.pojo.Hello">
    <!--这句话的意思是 Hello 这个类有个属性 叫 id,它的值是 HelloSpring-->
    <property name="id" value="HelloSpring"/>
    </bean>

    </beans>

    在上述例子中,创建了一个 Hello 类的对象,取名为 hello,将其属性【id】设置为了 HelloSpring

  3. 通过 Junit 进行单元测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import com.hpg.pojo.Hello;
    import org.junit.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;

    public class MyTest {
    @Test
    public void test(){

    //configlocation:配置文件地址,可以传多个
    //获取Spring的上下文对象方法
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    //在执行getBean的时候, Hello这个类已经创建好了 , 通过无参构造
    //这里的getBean参数与id相同
    Hello hello = (Hello) context.getBean("hello");
    //调用对象的方法 .
    System.out.println(hello.toString());
    }

    }

    结果:

    image-20210128155536466

  • Hello 对象是谁创建的 ? 由 Spring 创建
  • Hello 对象的属性是怎么设置的 ?由 Spring 容器设置

这个过程就叫控制反转 :

  • 控制 : 谁来控制对象的创建 , 传统应用程序的对象是由程序本身控制创建的 , 使用 Spring 后 , 对象是由 Spring 来创建的
  • 反转 : 程序本身不创建对象 , 而变成被动的接收对象 .

依赖注入 : 就是利用 set 方法来进行注入的.

IOC 是一种编程思想,由主动的创建变成被动的接收

既然我们学习了 Spring 的初步入门了,下面把之前的第一个案例改一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.hpg.service;

import com.hpg.dao.UserDao;
import com.hpg.dao.UserDaoImpl;

public class UserServiceImpl implements UserService{
//控制反转就是
//原来是把这里的 private UserDao userDao = new UserDaoImpl();(原来是写死了的)
//然后需要用哪个接口 就把new后面的改了
//变更为以下:
private UserDao userDao;
//根据传入参数 动态改变
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}

@Override
public void getUser() {
userDao.getUser();
}
}

原先利用 set 方法模拟了 IOC,现在使用 xml 进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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="MysqlImpl" class="com.hpg.dao.UserDaoMySqlImpl"/>
<bean id="OracleImpl" class="com.hpg.dao.UserDaoOracleImpl"/>

<bean id="ServiceImpl" class="com.hpg.service.UserServiceImpl">
<!--注意: 这里的name并不是属性 , 而是set方法后面的那部分 , 首字母小写-->
<!--引用另外一个bean , 不是用value 而是用 ref-->
<property name="userDao" ref="OracleImpl"/>
</bean>

</beans>

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import com.hpg.dao.UserDaoMySqlImpl;
import com.hpg.dao.UserDaoOracleImpl;
import com.hpg.service.UserServiceImpl;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;


public class MyTest {
@Test
public void test() {
//获取ApplicationContext 拿到Spring的容器
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
//通过容器去获取类(bean)
//记住 这里getBean的参数填的是配置文件中的id,而不是类的名字哦
UserServiceImpl serviceImpl =(UserServiceImpl) context.getBean("ServiceImpl");
//执行
serviceImpl.getUser();

}
}

由于我们 ref 处导向的是 Oracle,因此这里的输出:

image-20210128174939981

假如修改了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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="MysqlImpl" class="com.hpg.dao.UserDaoMySqlImpl"/>
<bean id="OracleImpl" class="com.hpg.dao.UserDaoOracleImpl"/>

<bean id="ServiceImpl" class="com.hpg.service.UserServiceImpl">
<!--注意: 这里的name并不是属性 , 而是set方法后面的那部分 , 首字母小写-->
<!--引用另外一个bean , 不是用value 而是用 ref-->
<property name="userDao" ref="MysqlImpl"/>
</bean>

</beans>

导向 Mysql,那么结果:

image-20210128175037525

我们根本就不需要去改动原来写的类,只需要在配置文件处修改一下 ref 导向就可以动态改变结果了。

IOC 创建对象方式

同时根据结果,我们发现整个程序没有出现new的字样,但是对象确实是实实在在的被创建了,那么究竟是怎么做到的?

而且还有另一个提示,我们所有的变更都是在配置文件中进行的;

学过 JavaSE 的朋友肯定知道,这种动态创建对象的方法 + 与配置文件进行配合 必定是用到了反射这个技术;

关于反射,我们之后再聊,我们首先需要知道 IOC 构建对象的方式

  1. 是使用无参构造创建对象的,这个是默认的构造方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    package com.hpg.pojo;
    public class User {

    private String name;

    public User() {
    System.out.println("user无参构造方法");
    }

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

    public void show(){
    System.out.println("name="+ name );
    }

    }

    配置文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <?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="userwucan" class="com.hpg.pojo.User">
    <property name="name" value="apple"/>
    </bean>

    </beans>
    1
    2
    3
    4
    5
    6
    7
    8
    @Test
    public void test(){
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    //在执行getBean的时候, user已经创建好了 , 通过无参构造
    User user = (User) context.getBean("user");
    //调用对象的方法 .
    user.show();
    }

    测试结果:

    image-20210228192702643

  2. 那么要使用有参构造的话,可以使用如下几种方式:

    首先需要在 User.java 中构造一个有参构造方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    package com.hpg.pojo;

    public class User {
    private String name;

    public User() {
    System.out.println("执行无参构造");
    }

    //构造一个有参构造方式
    public User(String name) {
    this.name = name;
    }

    public String getName() {
    return name;
    }

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

    public void show() {
    System.out.println(this.name);
    }
    }

    下标赋值

1
2
3
4
5
<!-- 第一种根据index参数下标设置 -->
<bean id="userT" class="com.hpg.pojo.User">
<!-- index指构造方法 , 下标从0开始 -->
<constructor-arg index="0" value="小黄"/>
</bean>

测试:

image-20210131144831408

image-20210131144836910

  1. 参数名字设置(一般用这个)

    需要注意的是,创建对象与 java 遵循规则一样,不能取同名的,Spring 并不会覆盖创建的对象

    image-20210131145545858

    参数匹配的方式代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
        <!-- 第二种根据参数名字设置 -->
    <bean id="userT2" class="com.hpg.pojo.User">
    <!-- name指参数名 -->
    <constructor-arg name="name" value="小黄2"/>
    </bean>

    <!-- 对比一下一开始使用的-->
    <bean id="user" class="com.hpg.pojo.User">
    <property name="name" value="hpg"/>
    </bean>

    测试结果:

    image-20210131145641385

  2. 根据类型匹配设置(不建议使用)

    1
    2
    3
    4
    <!-- 第三种根据参数类型设置 -->
    <bean id="userT3" class="com.hpg.pojo.User">
    <constructor-arg type="java.lang.String" value="小黄3"/>
    </bean>

    测试结果:

    image-20210131145956312

同时,我们进行一个另外的测试:

我们再创建一个类 UserTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.hpg.pojo;

public class UserTest {
private String name;

public UserTest() {
System.out.println("执行UserTest无参构造");
}

//构造一个有参构造方式
public UserTest(String name) {
this.name = name;
}

public String getName() {
return name;
}

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

public void show() {
System.out.println(this.name);
}
}

我们在 beans 中装载

1
2
3
<bean id="userTest" class="com.hpg.pojo.UserTest">

</bean>

测试:

image-20210131151146247

通过结果我们发现,我们并没有去使用这个 UserTest 类,但是却仍旧执行了 UserTest 的无参构造

也就是说,在配置文件加载的时候,Spring 管理的对象就全部被初始化了,我们需要用的时候只需要从中取出来即可;

Spring 配置说明

别名 Alias

  • 意思就是给一个对象取一个别名,通过这个别名也可以取出对应的对象
  • id 是 bean 的唯一标识符,而别名可以取很多个

比如:

1
2
3
4
5
<bean id="user" class="com.hpg.pojo.User">
<property name="name" value="hpg"/>
</bean>

<alias name="user" alias="uuuuuu"/>

进行测试:

image-20210202154111750

Bean 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--bean就是java对象,由Spring创建和管理-->

<!--
id :bean的标识符,要唯一,相当于对象名
class:bean对象对应的限定名—— 包名+类型
name:相当于别名,而且可以设置多个别名,可以用逗号,分号,空格隔开
如果没有配置id,name就是默认标识符
如果配置id,又配置了name,那么name是别名
如果不配置id和name,可以根据applicationContext.getBean(.class)获取对象;
-->
<bean id="userTest2" name="u1,u2,u3" class="com.hpg.pojo.UserTest">
<property name="name" value="HHHPPPGGG"/>
</bean>

执行结果:

  • 用 id 执行:

image-20210205164756955

  • 用 name 执行:

image-20210205164716758

结果是一样的,说明只不过参数是 name 别名和 id 唯一标识符的区别罢了;

Import

  • 团队开发使用,可以将多个配置文件,通过 import 导入合并成一个配置文件

使用场景:项目多个人开发,每个人负责不同的实体类(bean)的开发,利用import将所有人的 beans.xml 合并成总的

image-20210205173824383

这样子 在

1
2
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(applicationContext.xml);
//这个地方就只需要填入总的xml配置文件即可

DI - 依赖注入

  • DI (Dependency Injection)依赖注入

构造器注入

就是通过构造函数(参数的输入)进行依赖注入;

官方文档例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SimpleMovieLister {

// the SimpleMovieLister has a dependency on a MovieFinder
private MovieFinder movieFinder;

// a constructor so that the Spring container can inject a MovieFinder
//这就是一个构造器注入方式
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}

// business logic that actually uses the injected MovieFinder is omitted...
}

关于构造器注入方式,还有构造函数参数类型匹配,根据下标(索引)赋值,根据参数名称赋值等都在前文提及过了

Setter 方式注入

  • 依赖注入方式:使用了 Set 方法

    • 依赖:代表着 bean 对象的创建是依赖于容器
    • 注入:bean 对象的所有属性,通过容器进行注入
  • 测试:

    包结构:

    image-20210228193323011

    • Address 类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      package com.hpg.pojo;

      public class Address {

      private String address;

      public String getAddress() {
      return address;
      }

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

      @Override
      public String toString() {
      return "Address{" +
      "address='" + address + '\'' +
      '}';
      }
      }
    • Student 类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      package com.hpg.pojo;

      import java.util.*;

      public class Student {

      private String name;
      private Address address;
      private String[] books;
      private List<String> hobbys;
      private Map<String,String> card;
      private Set<String> games;
      private String wife;
      private Properties info;

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

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

      public void setBooks(String[] books) {
      this.books = books;
      }

      public void setHobbys(List<String> hobbys) {
      this.hobbys = hobbys;
      }

      public void setCard(Map<String, String> card) {
      this.card = card;
      }

      public void setGames(Set<String> games) {
      this.games = games;
      }

      public void setWife(String wife) {
      this.wife = wife;
      }

      public void setInfo(Properties info) {
      this.info = info;
      }

      public String getName() {
      return name;
      }

      public Address getAddress() {
      return address;
      }

      public String[] getBooks() {
      return books;
      }

      public List<String> getHobbys() {
      return hobbys;
      }

      public Map<String, String> getCard() {
      return card;
      }

      public Set<String> getGames() {
      return games;
      }

      public String getWife() {
      return wife;
      }

      public Properties getInfo() {
      return info;
      }

      @Override
      public String toString() {
      return "Student{" +
      "name='" + name + '\'' +
      ", address=" + address.toString() +
      ", books=" + Arrays.toString(books) +
      ", hobbys=" + hobbys +
      ", card=" + card +
      ", games=" + games +
      ", wife='" + wife + '\'' +
      ", info=" + info +
      '}';
      }

      public void show(){
      System.out.println("name="+ name
      + ",address="+ address.getAddress()
      + ",books="
      );
      for (String book:books){
      System.out.print("<<"+book+">>\t");
      }
      System.out.println("\n爱好:"+hobbys);

      System.out.println("card:"+card);

      System.out.println("games:"+games);

      System.out.println("wife:"+wife);

      System.out.println("info:"+info);

      }
      }
    • beans.xml

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      <?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
      https://www.springframework.org/schema/beans/spring-beans.xsd">

      <!--常量注入-->
      <bean id="s1" class="com.hpg.pojo.Student">
      <property name="name" value="小黄"/>
      </bean>

      </beans>
    • 测试:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      public class MyTest {
      @Test
      public void test() {
      //获取上下文对象先
      ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
      //根据上下文对象获取实例化的对象
      Student student =(Student) context.getBean("s1");
      //再根据这个实例化的对象获取属性
      String name = student.getName();
      System.out.println(name);

      }
      }

    • 结果

      ![image-20210228193535103](

      https://er11.oss-cn-shenzhen.aliyuncs.com/img/image-20210228193535103.png)

注入实例

首先实例化 Address 对象

1
2
3
4
5
<bean id="Address1" class="com.hpg.pojo.Address">
<property name="address" value="深圳"/>
</bean>

<bean id="Address2" class="com.hpg.pojo.Address"/>

普通值注入

1
2
3
4
<bean id="s1" class="com.hpg.pojo.Student">
<!--1.普通值注入-->
<property name="name" value="小黄"/>
</bean>

Bean 注入

1
2
<!--Bean注入,使用ref-->
<property name="address" ref="Address1"/>

数组注入

1
2
3
4
5
6
7
8
<!--数组注入-->
<property name="books">
<array>
<value>西游记</value>
<value>红楼梦</value>
<value>水浒传</value>
</array>
</property>

List 注入

1
2
3
4
5
6
7
8
<!--List注入-->
<property name="hobbys">
<list>
<value>听歌</value>
<value>看电影</value>
<value>吃肉</value>
</list>
</property>

Map 注入

1
2
3
4
5
6
7
<!--Map注入-->
<property name="card">
<map>
<entry key="中国邮政" value="456456456465456"/>
<entry key="建设" value="1456682255511"/>
</map>
</property>

Set 注入

1
2
3
4
5
6
7
8
<!--set注入-->
<property name="games">
<set>
<value>LOL</value>
<value>csgo</value>
<value>Dota2</value>
</set>
</property>

null 注入

1
2
3
4
<!--null注入-->
<property name="wife">
<null/>
</property>

Properties 注入

1
2
3
4
5
6
7
8
<!--Properties注入-->
<property name="info">
<props>
<prop key="学号">20190604</prop>
<prop key="性别"></prop>
<prop key="姓名">小黄</prop>
</props>
</property>

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.hpg.pojo.Student;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import javax.management.MBeanAttributeInfo;

public class MyTest {
@Test
public void test() {
//获取上下文对象先
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
//根据上下文对象获取实例化的对象
Student student =(Student) context.getBean("s1");
//测试
System.out.println(student.toString());
student.show();

}
}

结果:

image-20210228210140837

(拓展方式注入)p 命名空间和 c 命名空间

这两种方式不能直接使用,需要引用 xml 约束

1
2
3
导入约束 : xmlns:p="http://www.springframework.org/schema/p"

导入约束 : xmlns:c="http://www.springframework.org/schema/c"

User 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.hpg.pojo;

public class User {
private String name;
private int age;

public User() {
}

public User(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}

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

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

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

P 命名空间

  • 本质是 setter 注入 这里的p代表着 properties
  • 要注意的是,要使用 p 命名空间,就必须有无参构造!!不然会报错
1
2
<!--P(属性: properties)命名空间 , 属性依然要设置set方法-->
<bean id="user" class="com.hpg.pojo.User" p:name="小黄" p:age="18"/>

C 命名空间

  • 本质是构造器注入 这里的c代表着 constructor
  • 意味着这里的实体类必须要有带参构造
1
2
<!--C(构造: Constructor)命名空间 , 属性依然要设置set方法-->
<bean id="user2" class="com.hpg.pojo.User" c:name="小小黄" c:age="18"/>

假如没有带参构造,直接使用这种方式的话:

就会报错

image-20210228213626464

测试:

1
2
3
4
5
6
7
8
@Test
public void test02(){
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("userbeans.xml");
User user1 = (User) context.getBean("user1");
User user2 = (User) context.getBean("user2");
System.out.println(user1);
System.out.println(user2);
}

image-20210228214653421

Bean 作用域

分为六种(五种 有的人把后两种合并为一种了):

  • singleton单例模式,在整个 Spring IoC 容器中,使用 singleton 定义的 Bean 将只有一个实例(Spring 默认机制)

  • prototype原型模式,每次通过容器的 getBean 方法获取 prototype 定义的 Bean 时,都将产生一个新的 Bean 实例

  • request:对于每次 HTTP 请求,使用 request 定义的 Bean 都将产生一个新实例,即每次 HTTP 请求将会产生不同的 Bean 实例。只有在 Web 应用中使用 Spring 时,该作用域才有效

  • session:对于每次 HTTP Session,使用 session 定义的 Bean 都将产生一个新实例。同样只有在 Web 应用中使用 Spring 时,该作用域才有效

  • globalsession:每个全局的 HTTP Session,使用 session 定义的 Bean 都将产生一个新实例。典型情况下,仅在使用 portlet context 的时候有效。同样只有在 Web 应用中使用 Spring 时,该作用域才有效

image-20210301204421750

单例模式

  • 定义一个 bean定义并且其作用域为单例时,Spring IoC 容器将为该 bean 定义定义的对象创建一个实例(定义 Bean 的时候,IOC 就会给这个对象创建一个实例了)。

  • 该单个实例存储在此类单例 bean 的高速缓存中,并且对该命名 bean 的所有后续请求和引用都返回该高速缓存的对象

image-20210301205835365

  • 单例模式中使用的是同一个对象

  • hashcode 值相同

原型模式

  • Bean 部署的非单一原型范围会在每次请求该特定 Bean 时创建一个新的 Bean 实例

  • 也就是说,将 Bean 注入到另一个 Bean 中,或者您可以通过getBean()容器上的方法调用来请求它。通常,应将原型作用域用于所有有状态 Bean,将单例作用域用于无状态 Bean。

image-20210301205850123

原型模式使用的是拷贝的对象

hashcode 值不同

Bean 自动装配

  • 自动装配是使用 spring 满足 bean 依赖的一种方法
  • spring 会在应用上下文中为某个 bean 寻找其依赖的 bean。

Spring 中 bean 有三种装配机制,分别是:

  1. 在 xml 中显式配置;
  2. 在 java 中显式配置;
  3. 隐式的 bean 发现机制和自动装配。

这里我们主要讲第三种:自动化的装配 bean。

Spring 的自动装配需要从两个角度来实现,或者说是两个操作:

  1. 组件扫描(component scanning):spring 会自动发现应用上下文中所创建的 bean;
  2. 自动装配(autowiring):spring 自动满足 bean 之间的依赖,也就是我们说的 IoC/DI;

组件扫描和自动装配组合发挥巨大威力,使得显示的配置降低到最少。

推荐不使用自动装配 xml 配置 , 而使用注解 .

搭建环境测试

目录包结构

image-20210301201900920

  • Cat 类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    package com.hpg.pojo;

    public class Cat {

    public void shout() {
    System.out.println("miao");
    }
    }

  • Dog 类

    1
    2
    3
    4
    5
    6
    7
    package com.hpg.pojo;

    public class Dog {
    public void shout() {
    System.out.println("wang");
    }
    }
  • People 类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    public class People {
    private Cat cat;
    private Dog dog;
    private String name;

    public Cat getCat() {
    return cat;
    }

    public void setCat(Cat cat) {
    this.cat = cat;
    }

    public Dog getDog() {
    return dog;
    }

    public void setDog(Dog dog) {
    this.dog = dog;
    }

    public String getName() {
    return name;
    }

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

  • 测试类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import com.hpg.pojo.People;
    import org.junit.Test;
    import org.springframework.context.support.ClassPathXmlApplicationContext;

    public class MyTest {
    @Test
    public void test() {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");

    People people = (People) context.getBean("p1");

    people.getCat().shout();
    people.getDog().shout();

    }
    }

ByName 自动装配

  • 假如使用 ByName 进行自动装配的话

    要注意:bean 的 id 填写是取决于 set 方法后的字符小写

    也就是说:假如在 people 类中,写的是:

    1
    2
    3
    public void setDog1(Dog dog) {
    this.dog = dog;
    }

    那么此时的 bean 的 id 应该填写:

    1
    <bean id="dog1" class="com.hpg.pojo.Dog"/>
  • 同时,需要保证所有 bean 的 id 是唯一的,不能有重复;

ByType 自动装配

  • 假如使用 ByType 进行自动装配的话,则需要保证 bean 的 class 是唯一的,并且这个 bean 要与自动注入的属性一致;

    什么意思呢?就是假如在上述的 bean 中,再加入一个同类型的:

1
2
3
4
5
6
7
<bean id="cat" class="com.hpg.pojo.Cat"/>
<bean id="dog" class="com.hpg.pojo.Dog"/>
<bean id="cat2" class="com.hpg.pojo.Cat"/>

<bean id="p1" class="com.hpg.pojo.People" autowire="byType">
<property name="name" value="hpg"/>
</bean>

就不行,会报错,因为已经有一个 Cat 类了,就算取不同名也不行;

使用注解实现自动装配

这是原来的约束:

1
2
3
4
5
6
7
<?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:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">

</beans>

想要使用注解的话必须修改约束:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">

<!--别忘了这行代码 开启注解的支持-->
<context:annotation-config/>

</beans>

@Autowired

对于这个注解,放在属性上或者是 set 方法上即可,通常来说放在属性上;

默认通过 BytType 方式去查找 bean,如果有多个类型相同的,再通过 ByName;

假如还是有重复的,就需要配合@Qualifier 使用

@Qualifier

用于指定一个 bean;配合 Autowired 使用

Test:

Cat 类和 Dog 类没有改变,改变了 People 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.hpg.pojo;

import org.springframework.beans.factory.annotation.Autowired;

public class People {
@Autowired
private Cat cat;
@Autowired
private Dog dog;
private String name;

public Cat getCat() {
return cat;
}

public void setCat(Cat cat) {
this.cat = cat;
}

public Dog getDog() {
return dog;
}

public void setDog(Dog dog) {
this.dog = dog;
}

public String getName() {
return name;
}

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

beans.xml 文件修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">

<context:annotation-config/>

<bean id="cat" class="com.hpg.pojo.Cat"/>
<bean id="dog" class="com.hpg.pojo.Dog"/>


<bean id="p1" class="com.hpg.pojo.People" >
<property name="name" value="hpg"/>
</bean>

</beans>

在这里,我不用再去写 autowire 了;

测试结果:

image-20210301220908740

使用注解开发

  • 注解@Component 相当于 ,把普通的 pojo 类实例化到了 Spring 容器中

    使用的时候放在实体类上

  • 要注意,如果属性是私有的,则需要通过 get 方法去获取(意味着需要有 set 方法配合);其实设置私有成员属性是通过反射访问的,关键代码:

    1
    field.setAccessible(true);

Bean 实现

要注意,如果@Component 后不加括号,即:@Component 形式而非@Component(“xxx”)形式的话,默认的 name 是类名的小写,即如果类名是 User111,那么 getBean 的时候 name 就得写 user111

如果填入了 id,name 就得写那个 id,且区分大小写;

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.hpg.pojo;

import org.springframework.stereotype.Component;
//@Component("user")等价于<bean id="user" class="com.hpg.pojo.User"/>
@Component
public class User {
private String name = "apple";

public String getName() {
return name;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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
https://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.hpg.pojo"/>

<!--别忘了这行代码 开启注解的支持-->
<context:annotation-config/>


</beans>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import com.hpg.pojo.User;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.web.bind.annotation.RestController;

public class MyTest {
@Test
public void test() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
User user = context.getBean("user", User.class);
System.out.println(user.getName());

}
}

测试结果:

image-20210302171125811

属性注入

现在假如我不写死属性的值,而是想随时变化

就需要使用@Value 注解

如果使用这个注解了:

  1. 若提供Set 方法,就在 set 方法上添加@Value(“xxx”)达到注入的效果

    但我试了,就算提供 set 方法,在属性名上添加@Value(“xxx”)同样也行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    package com.hpg.pojo;

    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    //@Component("u")等价于<bean id="u" class="com.hpg.pojo.User"/>
    @Component("u")
    public class User {
    public String name = "apple";
    @Value("xxx")
    public void setName(String name) {
    this.name = name;
    }
    }

  2. 假如没提供Set 方法,就在属性名上添加@Value(“xxx”)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package com.hpg.pojo;

    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    //@Component("u")等价于<bean id="u" class="com.hpg.pojo.User"/>
    @Component("u")
    public class User {
    @Value("xxx")
    public String name = "apple";

    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import com.hpg.pojo.User;
    import org.junit.Test;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    import org.springframework.web.bind.annotation.RestController;

    public class MyTest {
    @Test
    public void test() {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    User user = context.getBean("u", User.class);
    System.out.println(user.name);

    }
    }

    image-20210302172308644

    从结果可以看出,注解的优先级高于赋予的值;

衍生注解

@Component 三个衍生注解

为了更好的进行分层,Spring 可以使用其它三个注解,功能一样,目前使用哪一个功能都一样。

  • @Controller:web 层
  • @Service:service 层
  • @Repository:dao 层

写上这些注解,就相当于将这个类交给 Spring 管理装配了!

注解与 xml 区别

注解:是一种分散式的元数据,与源代码紧绑定。

xml:是一种集中式的元数据,与源代码无绑定。

因此注解和 XML 的选择上可以从两个角度来看:分散还是集中,源代码绑定/无绑定。

注解的缺点:

1、很多朋友比如在使用 spring 注解时,会发现注解分散到很多类中,不好管理和维护;这个其实要借助工具,我目前使用的是IDEA,它在这方面表现的非常好;当然现在还有Spring 的 STS,也是不错的; 所以借助工具,能解决这个问题;

2、注解的开启/关闭必须修改源代码,因为注解是源代码绑定的,如果要修改,需要改源码,这个有这个问题,所以如果是这种情况,还是使用 XML 配置方式;比如数据源;

3、注解还一个缺点就是灵活性,比如在之前翻译的 Spring Framework 4.0 M1: WebSocket 支持;在实现复杂的逻辑上,没有 XML 来的更加强大;注解就是要么用,要么不用,比如之前的 jpa bean validation,要么全,要么没;遇到这种情况很痛苦;

4、还一种就是约定大于配置,但是在处理一些复杂的情况下,注解还是需要的(如 Spring 的数据验证/数据绑定注解很强大);

5、通用配置还是走 XML 吧,比如事务配置,比如数据库连接池等等,即通用的配置集中化,而不是分散化,如很多人使用@Transactional 来配置事务,在很多情况下这是一种太分散化的配置;

6、XML 方式比注解的可扩展性和复杂性维护上好的多,比如需要哪些组件,不需要哪些;在面对这种情况,注解扫描机制比较逊色,因为规则很难去写或根本不可能写出来;

注解的好处: 数据绑定用注解,很少改变的用注解,类型安全,

1、XML 配置起来有时候冗长,此时注解可能是更好的选择,如 jpa 的实体映射;注解在处理一些不变的元数据时有时候比 XML 方便的多,比如 springmvc 的数据绑定,如果用 xml 写的代码会多的多;

2、注解最大的好处就是简化了 XML 配置;其实大部分注解一定确定后很少会改变,所以在一些中小项目中使用注解反而提供了开发效率,所以没必要一头走到黑;

3、注解相对于 XML 的另一个好处是类型安全的,XML 只能在运行期才能发现问题。

注解也好,XML 也好,我们还是需要一些开关/替换机制来控制特殊需求,以改变那种要么全部 要么没有的方案。。

还一种呼声就是约定大于配置,这种方案可能在某些场景下是最优的,但是遇到一些复杂的情况可能并不能解决问题,所以此时注解也是一个不错的方案。尤其在使用 springmvc 时,好处是能体会的出的。

不管使用注解还是 XML,做的事情还是那些事情,但注解和 XML 都不是万能的,满足自己的需求且已一种更简单的方式解决掉问题即可。

就像探讨一下技术问题,很多人都带有很强的个人喜好来评判一个东西的好坏,这种探讨没有任何意义,我们最终的目的是解决方案,所以我们应该探讨的是能不能解决问题,能不能以更容易理解的方式解决问题,能不能更简单的解决问题。

不管是约定大于配置、注解还是 XML 配置也好,没有哪个是最优的,在合适的场景选择合适的解决方案这才是重要的。就像设计模式一样:是对特定环境中重复出现的特定问题的一个经过前人验证了的解决方案。

使用 Java 方式配置 Spring

包结构:

image-20210302193809401

配置文件类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.hpg.config;


import com.hpg.pojo.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration //这个注解表示这个类是个配置类
public class HpgConfig {
@Bean //这个注解 表示通过以下方法注册一个bean, 方法名为bean的id 在这里,bean的id = User
public User User() {
return new User();
}
}

User 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.hpg.pojo;

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

@Component //这里去掉这个注解也一样能跑的
public class User {
@Value("apple")
private String name;

public String getName() {
return name;
}

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

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

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.hpg.config.HpgConfig;
import com.hpg.pojo.User;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MyTest {
@Test
public void test() {
//在这里需要的就不熟之前的ClassPathXml..了 就得用注解的
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(HpgConfig.class);
//这里填入的“User”对应的是Config类中的方法名 User
// public User User() {
// return new User();
// }
User user = context.getBean("User", User.class);
System.out.println(user.getName());



}
}

测试结果:

image-20210302193921636

整个过程,我们都没有去配置 xml 文件,而是用纯 java 实现的;

@Component 和@Bean 区别

看了上述例子有点奇怪,明明有@Component 注解了 能够去注册一个 bean 了,还需要这个@Bean 干嘛呢

(这个知识点还存疑,之后要着重复习下)

相关博客:https://shanhy.blog.csdn.net/article/details/97533038

注解作用

  • @Component 注解表明一个类会作为组件类,并告知 Spring 要为这个类创建 bean。
  • @Bean 注解告诉 Spring 这个方法将会返回一个对象,这个对象要注册为 Spring 应用上下文中的 bean。

两者对比

  • 相同点:两者的结果都是为 spring 容器注册 Bean,也就是说将 bean 添加到了 Spring 的上下文中

  • 不同点:@Component 通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中。

    @Bean 注解通常是我们在标有该注解的方法中定义产生这个 bean 的逻辑

    @Bean 注解比@Component 注解的自定义性更强,而且很多地方只能通过@Bean 注解来注册 bean

    比如当引用第三方库的类需要装配到 Spring 容器的时候,就只能通过@Bean 注解来实现。

    作用对象不同: @Component 注解作用于类,而@Bean 注解作用于方法。

    @Bean 需要配合@Configuration 注解一起用


理解

这两个功能确实是一样,都是创建 bean 实例,并让 spring 容器管理其生命周期。

但是一般他们的使用情况不太一样。一般@Confiuration 配合@Bean 使用,写在配置文件中,返回的是某一个对象的实例;而@Component 是对应某一个类,配合@ComponentScan 使用,然后让 spring ioc 容器实例化。

而且使用@Confiugraion 和@Bean, 会进行cglib 动态代理增强,拿到的是一个代理对象;@Component 拿到的是一个普通的 java 对象。

至于为什么?我并没有找到答案。不过两个进行对比以及我的猜测,是因为@Confiugraion 和@Bean 使用中返回的是一个已经定义的类的对象,可能后续要对它实例化的过程中添加一些逻辑,所以要用 cglib 进行动态代理;而@Component 是由 Spring 容器进行实例化的。

AOP

概念

  • AOP 面向切面编程 利用这技术可以使得业务逻辑各部分耦合度降低,提高程序可重用性,提高开发效率
  • 基本思想就是:在不通过修改源代码的方式去添加新的模块(功能)

底层原理

关于代理相关知识:

https://www.cnblogs.com/teach/p/10763845.html

https://www.cnblogs.com/baizhanshi/p/6611164.html

Spring 使用的是动态代理去实现

  • 为什么不用静态代理呢?

    静态代理的本质是在实现接口的基础上去实现额外功能,因此还是会有大量的代码重复

    同时,静态代理只能对固定接口的实现类进行代理,不灵活;

    静态怎么理解呢?是指将代理类生成了源代码再进行编译,也就是程序跑之前这个代理类的.class 文件就存在,而动态代理中的动态是利用了反射,代理类是在程序运行时用反射机制动态创建的

动态代理分两种:JDK 动态代理(有接口)和 CGLIB 动态代理(无接口)

  • JDK 动态代理:创建接口实现类的代理对象,然后通过该代理对象增强功能

    image-20210302211105958

  • CGLIB 动态代理:创建当前类的子类的代理对象,通过该代理对象去增强功能

    image-20210302211118766

JDK 动态代理

使用的是 Proxy 类中的方法去创建代理对象:

1
2
3
4
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h) {
Objects.requireNonNull(h);

参数分析:

  • 第一个参数,类加载器

  • 第二个参数,增强方法所在的类,这个类实现的接口,支持多个接口

  • 第三个参数,实现这个接口 InvocationHandler,创建代理对象,写增强的部分

包结构:

image-20210302221808392

UserDao 接口:

1
2
3
4
5
6
7
8
9
package com.hpg.spring;

public interface UserDao {

public int add(int a, int b);

public String update(String id);
}

UserDao 实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.hpg.spring;

public class UserDaoImpl implements UserDao{
@Override
public int add(int a, int b) {
System.out.println("执行add方法");
return a + b;
}

@Override
public String update(String id) {
System.out.println("执行update方法");
return id;
}
}

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import com.hpg.spring.UserDao;
import com.hpg.spring.UserDaoImpl;
import org.junit.Test;

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

public class MyTest {
@Test
public void test() {
Class[] interfaces = {UserDao.class};
//下面的方法是采取匿名内部类 比较冗杂
// Proxy.newProxyInstance(MyTest.class.getClassLoader(), interfaces, new InvocationHandler() {
// @Override
// public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// return null;
// }
// })
UserDaoImpl userdaoimpl = new UserDaoImpl();
UserDao dao = (UserDao) Proxy.newProxyInstance(MyTest.class.getClassLoader(), interfaces, new UserDaoProxy(userdaoimpl));
int res = dao.add(1, 2);
System.out.println("res = " + res);
}
}
//这个类是增强类
class UserDaoProxy implements InvocationHandler {
private Object obj;

//既然是增强 就需要把原对象传过来
public UserDaoProxy(Object obj) {
this.obj = obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//方法之前进行处理
System.out.println("方法之前执行" + method.getName() + "传递的参数" + Arrays.toString(args));
//被增强的方法
Object res = method.invoke(obj, args);
//方法之后进行处理
System.out.println("方法之后执行" + obj );
return res;
}
}

测试结果:

image-20210302221945907

AOP 术语

  • 连接点

    类中哪些方法可以被增强,那么这些方法就叫做连接点(理论上可以被增强的方法,不一定真要去增强)

  • 切入点

    实际被真正增强的方法,称为切入点

  • 通知(增强)

    实际增强的逻辑部分称为通知(增强)

    • 通知分为多种类型

      • 前置通知

        在被增强的方法前执行

      • 后置通知

        在被增强的方法后执行

      • 环绕通知

        在被增强的方法前后都执行

      • 异常通知

        当被增强的方法发生了异常时执行

      • 最终通知

        类似 java 中 try catch finally 中的 finally 被增强方法可能发生了异常,然后就不执行后置通知,但会执行最终通知;

  • 切面

    这是一个动词,而非名词;

    代表着把通知应用到了切入点这一过程

AOP 操作

准备工作

Spring 框架一般是基于AspectJ实现 AOP 操作

  • 什么是AspectJ呢?

他不属于 Spring 的组成部分,是一个独立的 AOP 框架;通常将 AspectJ 与 Spring 一起使用,来完成 AOP 操作

  • 如何基于 AspectJ 实现 AOP 操作呢?
  1. 基于 xml 配置文件实现
  2. 基于注解方式实现
  • 做项目工程中引入 AOP 相关依赖

依赖(pom.xml 中引入):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>

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

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/aopalliance/aopalliance -->
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>

命名空间(beans.xml 中引入):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop"//这个为aop
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
http://www.springframework.org/schema/aop //这个为aop的
http://www.springframework.org/schema/aop/spring-aop.xsd //这个为aop的
">
</beans>

切入点表达式

  • 作用:根据这个表达式,我们可以知道对哪个类里面的哪个方法进行增强
  • 语法结构:execution( 权限修饰符 返回类型 包.类.方法名(参数列表) throws 异常 )

括号中的内容其实就是声明一个方法。通常”修饰符“和”throw 异常“省略不写;

返回值

1
2
基本类型,对象
*//(匹配任意返回值)

:用限定名。可以使用通配符如:

1
2
3
4
com.abc.service//表示固定包
com.abc.crm.*.service//表示crm包下任意子包的service包
com.abc.crm..//表示crm包下面的所有子包(包括自己)
“..”可以表示自己本身和自己下面的多级包

:可以使用固定名称,和统配符号

1
2
3
*Impl //以Impl结尾
User* //以User开头的
* //任意

方法名

1
2
3
addUser//固定方法
add*//以add开头
*Do//以Do结尾

参数

1
2
3
4
()//无参
(int)//一个整型
(int,int)//两个
(..)//参数任意

例子

1
2
3
4
5
6
execution(* *.someServiceImpl.*(..))
//所有的包下面(限定一级包)someServiceImpl类的所有任意参数的方法
execution(* *..someServiceImpl.*(..))
//所有的包下面(可以是多级包)someServiceImpl类的所有任意参数的方法
execution(* com.abc.crm.*.service..*.*(..))
//service下自己以及自己的子包的任意类的任意方法

切入点表达式使用例子:

eg1:对 com.hpg.dao.UserDao 类中的 add 方法进行增强的写法:

  • execution(* com.hpg.dao.UserDao.add(..))
    • 第一个*表示所有的返回值类型
    • ..表示所有参数

eg2:execution(_ com.hpg.service._ . *(..))

  • 第一个*表示所有返回值类型
  • 第二个*表示所有的类
  • 第三个*表示类所有的方法
  • ..表示所有参数

因此这行代码表示:

对 com.hpg.service 包下所有类和类中的所有方法进行增强

AOP 操作 - AspectJ 注解实现

  1. 创建类,类中定义方法

    1
    2
    3
    4
    5
    6
    7
    8
    package com.hpg.spring.AopAnno;

    public class User {
    public void add() {
    System.out.println("执行add方法");
    }
    }

  2. 创建增强类(编写增强逻辑)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package com.hpg.spring.AopAnno;

    public class UserProxy {

    //前置通知
    public void before() {
    System.out.println("前置通知执行");
    }

    }

  3. 进行通知的配置

    1. 配置好 Spring 中的命名空间相关信息

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      <beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:p="http://www.springframework.org/schema/p"
      xmlns:aop="http://www.springframework.org/schema/aop"
      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

      http://www.springframework.org/schema/aop
      http://www.springframework.org/schema/aop/spring-aop.xsd
      ">

      <!--开启注解/组件扫描-->
      <context:component-scan base-package="com.hpg.spring.AopAnno"></context:component-scan>
      </beans>
    2. 使用注解创建 User 和 UserProxy 对象(@Component)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      package com.hpg.spring.AopAnno;

      import org.springframework.stereotype.Component;

      @Component
      public class User {
      public void add() {
      System.out.println("执行add方法");
      }
      }

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      package com.hpg.spring.AopAnno;

      import org.springframework.stereotype.Component;

      @Component
      public class UserProxy {

      //前置通知
      public void before() {
      System.out.println("前置通知执行");
      }

      }

    3. 在增强类上面添加注解@Aspect

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      package com.hpg.spring.AopAnno;

      import org.aspectj.lang.annotation.Aspect;
      import org.springframework.stereotype.Component;

      @Component
      @Aspect
      public class UserProxy {

      //前置通知
      public void before() {
      System.out.println("before.....");
      }

      }

    4. 在 Spring 配置文件中开启生成代理对象

      1
      2
      <!--开启Aspect生成代理对象-->
      <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
  4. 配置不同类型的通知

    在增强类的里面,在作为通知方法上面添加【通知类型注解】,使用切入点表达式配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    package com.hpg.spring.AopAnno;

    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.springframework.stereotype.Component;

    @Component
    @Aspect
    public class UserProxy {

    //前置通知
    @Before(value = "execution(* com.hpg.spring.AopAnno.User.add(..))")
    public void before() {
    System.out.println("前置通知执行");
    }

    //后置通知(返回通知)
    @AfterReturning(value = "execution(* com.hpg.spring.AopAnno.User.add(..))")
    public void afterReturning() {
    System.out.println("后置通知执行");
    }

    //最终通知
    @After(value = "execution(* com.hpg.spring.AopAnno.User.add(..))")
    public void after() {
    System.out.println("最终通知执行");
    }

    //异常通知
    @AfterThrowing(value = "execution(* com.hpg.spring.AopAnno.User.add(..))")
    public void afterThrowing() {
    System.out.println("异常通知执行");
    }

    //环绕通知
    @Around(value = "execution(* com.hpg.spring.AopAnno.User.add(..))")
    public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    System.out.println("环绕中的前置通知执行");

    //被增强的方法执行
    proceedingJoinPoint.proceed();

    System.out.println("环绕之中的后置通知执行");
    }

    }

  5. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import com.hpg.spring.AopAnno.User;
    import org.junit.Test;
    import org.springframework.context.support.ClassPathXmlApplicationContext;

    public class AopTest {
    @Test
    public void test() {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    User user = context.getBean("user", User.class);
    user.add();
    }
    }

    image-20210303213806207

    这是没有异常的情况,假如模拟一下异常,则打印结果是:

    image-20210303214042098

    可以发现环绕中的后置通知 和 后置(返回)通知 是不执行的

  6. 相同切入点抽取

    我们发现,切入点大多数重复的,因此可以进行一个共性抽取 通过注解@Pointcut达到效果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //相同切入点抽取
    @Pointcut(value = "execution(* com.hpg.spring.AopAnno.User.add(..))")
    public void GetPoint() {

    }

    //前置通知
    @Before(value = "GetPoint()")
    public void before() {
    System.out.println("前置通知执行");
    }
  7. 多个增强类对同个方法进行增强,设置增强类优先级

    通过在增强类上添加注解@Order(数字类型值),其中值越小优先级越高

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package com.hpg.spring.AopAnno;

    import org.aspectj.lang.annotation.AfterReturning;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.springframework.core.annotation.Order;
    import org.springframework.stereotype.Component;

    @Component
    @Aspect
    @Order(1)
    public class AnotherProxy {
    //后置通知(返回通知)
    @Before(value = "execution(* com.hpg.spring.AopAnno.User.add(..))")
    public void before() {
    System.out.println("另一个前置通知执行");
    }
    }


    给另一个增强类设为 1,之前的增强类设置为 2

    打印结果是:

    image-20210303215857414

  8. 完全使用注解开发,则需要创建配置类,不需要 xml 配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package com.hpg.spring.config;

    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;

    @Configuration
    @ComponentScan(basePackages = {"com.hpg"}) //用于代替 开启注解扫描: <context:component-scan base-package="com.hpg.spring.AopAnno"></context:component-scan>
    @EnableAspectJAutoProxy(proxyTargetClass = true) //用于代替 开启切面生成代理对象: <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
    public class ConfigAop {
    }

AOP 操作 - AspectJ XML 实现

  1. 创建两个类 【增强类 + 被增强类】创建方法

    1
    2
    3
    4
    5
    6
    7
    8
    package com.hpg.spring.AopXml;

    public class Book {
    public void buy() {
    System.out.println("buy");
    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    package com.hpg.spring.AopXml;


    public class BookProxy {
    public void before() {
    System.out.println("执行前置通知");
    }
    }

  2. 在 Spring 配置文件中创建两个类的对象

    1
    2
    3
    4
    <!--创建对象-->
    <bean id="book" class="com.hpg.spring.AopXml.Book"></bean>

    <bean id="bookProxy" class="com.hpg.spring.AopXml.BookProxy"></bean>
  3. 在 Spring 配置文件中配置切入点

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <!--创建对象-->
    <bean id="book" class="com.hpg.spring.AopXml.Book"></bean>

    <bean id="bookProxy" class="com.hpg.spring.AopXml.BookProxy"></bean>
    <!--3、在 spring 配置文件中配置切入点-->
    <!--配置 aop 增强-->
    <aop:config>
    <!--切入点-->
    <aop:pointcut id="p" expression="execution(* com.hpg.spring.AopXml.Book.buy(..))"/>
    <!--配置切面-->
    <aop:aspect ref="bookProxy">
    <!--增强作用在具体的方法上 method指向方法, pointcut-ref指向切入点-->
    <aop:before method="beforeMethod" pointcut-ref="p"/>
    </aop:aspect>
    </aop:config>



  4. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import com.hpg.spring.AopXml.Book;
    import org.junit.Test;
    import org.springframework.context.support.ClassPathXmlApplicationContext;

    public class AopXmlTest {
    @Test
    public void test() {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans2.xml");
    Book book = context.getBean("book", Book.class);
    book.buy();
    }
    }

    image-20210303222535354

JDBCTemplate

  • JdbcTemplate 是什么?Spring 框架对 JDBC 进行了封装,使用这个JdbcTemplate方便实现对数据库的操作

下面进行一下测试:

准备工作

  1. 配置相关依赖(百度找的 不知道全不全)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.0.2.RELEASE</version>
    </dependency>

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

    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>5.0.2.RELEASE</version>
    </dependency>

    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.0.9</version>
    </dependency>

    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.16</version>
    </dependency>

    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-orm</artifactId>
    <version>5.3.2</version>
    </dependency>
    <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
    </dependency>

  2. 在 Spring 配置文件中配置数据库连接池

    1
    2
    3
    4
    5
    6
    7
    8
    <!--数据库连接池-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
    destroy-method="close">
    <property name="url" value="jdbc:mysql://localhost:3306/user_db?serverTimezone=UTC" />
    <property name="username" value="root" />
    <property name="password" value="123456" />
    <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
    </bean>
  3. 配置 JdbcTemplate 对象 注入 DataSource

    1
    2
    3
    4
    5
    6
    <!--JdbcTemplate对象-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <!--注入dataSource-->
    <property name="dataSource" ref="dataSource"></property>

    </bean>
  4. 创建数据库 创建表 创建对应实体类

    创建 Service 类,创建 Dao 类,并且在 Dao 中注入 JdbcTemplate 对象

    建表语句:

    1
    2
    3
    4
    5
    6
    7
    CREATE TABLE `t_book` (
    `book_id` bigint NOT NULL,
    `bookname` varchar(100) CHARACTER SET utf8 COLLATE utf8_croatian_ci NOT NULL,
    `bookstatus` varchar(50) CHARACTER SET utf8 COLLATE utf8_croatian_ci NOT NULL,
    PRIMARY KEY (`book_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_croatian_ci

    实体类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    package com.hpg.Pojo;

    public class Book {
    //用户ID
    private String BookID;
    //用户名
    private String BookName;
    //用户状态
    private String BookStatus;

    public String getBookID() {
    return BookID;
    }

    public void setBookID(String bookID) {
    BookID = bookID;
    }

    public String getBookName() {
    return BookName;
    }

    public void setBookName(String bookName) {
    BookName = bookName;
    }

    public String getBookStatus() {
    return BookStatus;
    }

    public void setBookStatus(String bookStatus) {
    BookStatus = bookStatus;
    }
    }

    组件扫描:

    1
    2
    <!--组件扫描-->
    <context:component-scan base-package="com.hpg"></context:component-scan>

    Service:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    package com.hpg.Service;

    import com.hpg.Dao.BookDao;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;

    @Service
    public class BookService {

    //注入Dao
    @Autowired
    private BookDao bookdao;
    }

    Dao:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    package com.hpg.Dao;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Repository;

    @Repository
    public class BookDaoImpl implements BookDao{

    //注入模板对象
    @Autowired
    private JdbcTemplate jdbcTemplate;
    }

操作数据库

添加

  1. 编写 Service 和 Dao

    1. 在 Dao 类中进行数据库添加操作

      1
      2
      3
      4
      5
      6
      7
      8
      9
      package com.hpg.Dao;

      import com.hpg.Pojo.Book;

      public interface BookDao {
      //添加的方法
      void add(Book book);
      }

    2. 在 Dao 实现类中调用 JDBCTemplate 对象里面的update 方法去实现添加操作

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      package com.hpg.Dao;

      import com.hpg.Pojo.Book;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.jdbc.core.JdbcTemplate;
      import org.springframework.stereotype.Repository;

      @Repository
      public class BookDaoImpl implements BookDao{

      //注入模板对象
      @Autowired
      private JdbcTemplate jdbcTemplate;

      public void add(Book book) {
      //创建sql语句
      String sql = "insert into t_book values(?,?,?)";
      Object[] params = {book.getBookID(), book.getBookName(), book.getBookStatus()};
      int update = jdbcTemplate.update(sql, params);
      System.out.println(update);
      }

      }
      ![image-20210304170002544](

      https://er11.oss-cn-shenzhen.aliyuncs.com/img/image-20210304170002544.png)

      1. 第一个参数:sql语句
      2. 第二个参数:可变参数,设置sql语句值
    3. 在 Service 层的类中调用 Dao 类的方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      package com.hpg.Service;

      import com.hpg.Dao.BookDao;
      import com.hpg.Pojo.Book;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.stereotype.Service;

      @Service
      public class BookService {

      //注入Dao
      @Autowired
      private BookDao bookdao;

      //添加的方法
      public void addBook(Book book) {
      bookdao.add(book);
      }
      }

    4. 测试

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      import com.hpg.Pojo.Book;
      import com.hpg.Service.BookService;
      import org.junit.Test;
      import org.springframework.context.support.ClassPathXmlApplicationContext;

      public class MyTest {
      @Test
      public void AddTest() {
      ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
      BookService bookService = context.getBean("bookService", BookService.class);

      Book book = new Book();
      book.setBookID("1");
      book.setBookName("test");
      book.setBookStatus("free");

      bookService.addBook(book);
      }
      }


      ![](

      https://er11.oss-cn-shenzhen.aliyuncs.com/img/image-20210304174445833.png)

      ![image-20210304174527504](

      https://er11.oss-cn-shenzhen.aliyuncs.com/img/image-20210304174527504.png)

ps:如果执行不成功,记得看数据库的用户名和密码输入有没有正确,还有注意 url 的填写问题以及数据库连接的版本一致性问题

修改删除

其实大体都一样,就只有 sql 语句是不同的:

  • 修改

    1
    2
    3
    4
    5
    6
    7
    8
    //1、修改
    @Override
    public void updateBook(Book book) {
    String sql = "update t_book set bookname=?,bookstatus=? where book_id=?";
    Object[] args = {book.getUsername(), book.getUstatus(),book.getUserId()};
    int update = jdbcTemplate.update(sql, args);
    System.out.println(update);
    }
  • 删除

    1
    2
    3
    4
    5
    6
    7
    8
    //2、删除
    @Override
    public void delete(String id) {
    String sql = "delete from t_book where book_id=?";
    int update = jdbcTemplate.update(sql, id);
    System.out.println(update);
    }
    //使用JdbcTemplate 模板所实现的 “增删改” 都是调用了同一个 “update” 方法

查询

查询分为:

  1. 查询返回某个值

    image-20210304202952200

    1. 第一个参数:sql 语句
    2. 第二个参数:返回类型 Class
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //查询表记录数
    @Override
    public int selectCount() {
    String sql = "select count(*) from t_book";
    //queryForObject方法中:第一个参数代表--sql语句;第二个参数代表--返回类型class
    Integer count = jdbcTemplate.queryForObject(sql, Integer.class);
    return count;
    }
    JdbcTemplate 操作数据库(

  2. 查询返回对象

    image-20210304203359106

    1. 第一个参数:sql 语句

    2. 第二个参数:RowMapper

      ![image-20210304203923691](

      https://er11.oss-cn-shenzhen.aliyuncs.com/img/image-20210304203923691.png)

      这是一个接口,可以将数据中的每一行数据封装成用户定义的类
      
      简单来说就是吧数据库中的列字段和java bean实体类的属性对应起来
      
      作用形如:`bean.setName(rs.getString("name");`
    3. 第三个参数:sql 语句中的参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //查询返回对象
    @Override
    public Book findBookInfo(String id) {
    String sql = "select * from t_book where user_id=?";
    //调用方法
    /*
    queryForObject方法中:
    第一个参数:sql语句
    第二个参数:RowMapper 是接口,针对返回不同类型数据,使用这个接口里面 实现类 完成数据封装
    第三个参数:sql 语句值
    */
    Book book = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<Book>(Book.class), id);
    return book;
    }

  3. 查询返回集合

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //所用场景:查询图书列表分页、、
    //查询返回集合
    @Override
    public List<Book> findAllBook() {
    String sql = "select * from t_book";
    //调用方法
    List<Book> bookList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<Book>(Book.class));
    return bookList;
    }

批量操作

顾名思义,就是操作表里面的多条记录

批量增加

1
2
3
4
5
public void batchBook(List<Object[]> batchArgs) {
String sql = "insert into t_book values(?,?,?)";
int[] ints = jdbcTemplate.batchUpdate(sql, batchArgs);
System.out.println(Arrays.toString(ints));
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void batchAddTest() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
BookService bookService = context.getBean("bookService", BookService.class);

//批量添加测试
List<Object[]> batchArgs = new ArrayList<>();
Object[] o1 = {"3","java","a"};
Object[] o2 = {"4","c++","b"};
Object[] o3 = {"5","MySQL","c"};

batchArgs.add(o1);
batchArgs.add(o2);
batchArgs.add(o3);

bookService.batchAdd(batchArgs);
}

测试结果:[1,1,1] 代表着三组结果都成功了

image-20210304210854309

image-20210304210944633

批量修改

1
2
3
4
5
6
7
8
9
//批量修改(同批量添加一样,调用同一个方法)
@Override
public void batchUpdateBook(List<Object[]> batchArgs) {
String sql = "update t_book set bookname=?,bookstatus=? where book_id=?";
int[] ints = jdbcTemplate.batchUpdate(sql, batchArgs);
System.out.println(Arrays.toString(ints));

}

事务

  • 事务是数据库操作的最基本单元,逻辑上来说:一组操作要么都成功,若有一个失败则所有操作都失败,

    其本质就是一个具有一定特性的工作单元,为什么服务呢?为了访问或者更新数据库中各数据项。

  • 事务的四个特性(ACID)

    • 原子性

      一个事务要么全部提交成功,要么全部失败回滚,不能只执行其中的一部分操作,这就是事务的原子性

    • 一致性

      事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行之前和执行之后,数据库都必须处于一致性状态。

      如果数据库系统在运行过程中发生故障,有些事务尚未完成就被迫中断,这些未完成的事务对数据库所作的修改有一部分已写入物理数据库,这是数据库就处于一种不正确的状态,也就是不一致的状态

    • 隔离性

      事务的隔离性是指在并发环境中,并发的事务时相互隔离的,一个事务的执行不能不被其他事务干扰。不同的事务并发操作相同的数据时,每个事务都有各自完成的数据空间,即一个事务内部的操作及使用的数据对其他并发事务时隔离的,并发执行的各个事务之间不能相互干扰。

    • 持久性

      一旦事务提交,那么它对数据库中的对应数据的状态的变更就会永久保存到数据库中。–即使发生系统崩溃或机器宕机等故障,只要数据库能够重新启动,那么一定能够将其恢复到事务成功结束的状态

转账 - 模拟事务

Dao 层:创建 多钱 少钱 2 个方法

Service 层:创建转账的方法 —— 分别调用 Dao 层 2 个方法 实现转账

搭建环境并测试

  • 创建表:
1
2
3
4
5
6
7
CREATE TABLE `t_account` (
`id` varchar(20) COLLATE utf8_croatian_ci NOT NULL,
`username` varchar(50) COLLATE utf8_croatian_ci DEFAULT NULL,
`money` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_croatian_ci

  • 加入两条数据:

image-20210304213014230

  • 完成配置文件处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:aop="http://www.springframework.org/schema/aop"
    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

    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop.xsd
    ">

    <!--组件扫描-->
    <context:component-scan base-package="com.hpg"></context:component-scan>

    <!--数据库连接池-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
    destroy-method="close">
    <property name="url" value="jdbc:mysql://localhost:3306/user_db?serverTimezone=UTC" />
    <property name="username" value="root" />
    <property name="password" value="123456" />
    <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
    </bean>

    <!--JdbcTemplate对象-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <!--注入dataSource-->
    <property name="dataSource" ref="dataSource"></property>

    </bean>


    </beans>
  • 创建 Service,Dao 完成对象创建和注入关系

    • service 注入 Dao,Dao 中又注入 JdbcTemplate,JdbcTempate 中注入 DataSource
    1
    2
    3
    4
    5
    package com.hpg.dao;

    public interface UserDao {
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package com.hpg.dao;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;

    public class UserDaoImpl implements UserDao{

    @Autowired
    private JdbcTemplate jdbcTemplate;

    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package com.hpg.service;

    import com.hpg.dao.UserDao;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;

    @Service
    public class UserService {

    //注入Dao
    @Autowired
    private UserDao userDao;

    }

  • 创建相应的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package com.hpg.dao;

    public interface UserDao {

    //减少钱
    public void reduceMoney();

    //增加钱
    public void addMoney();
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    package com.hpg.dao;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Repository;

    @Repository
    public class UserDaoImpl implements UserDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    //lucy 转账 100 给 mary
    //少钱
    @Override
    public void reduceMoney() {
    String sql = "update t_account set money=money-? where username=?";
    jdbcTemplate.update(sql,100,"lucy");
    }
    //多钱
    @Override
    public void addMoney() {
    String sql = "update t_account set money=money+? where username=?";
    jdbcTemplate.update(sql,100,"mary");
    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package com.hpg.service;

    import com.hpg.dao.UserDao;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;

    @Service
    public class UserService {
    //注入 dao
    @Autowired
    private UserDao userDao;
    //转账的方法
    public void accountMoney() {
    //lucy 少 100
    userDao.reduceMoney();
    //mary 多 100
    userDao.addMoney();
    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import com.hpg.service.UserService;
    import org.junit.Test;
    import org.springframework.context.support.ClassPathXmlApplicationContext;

    public class MyTest {
    @Test
    public void AccountTest() {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    UserService userService = context.getBean("userService", UserService.class);

    userService.accountMoney();
    }
    }

    测试结果(没有发生异常的)

    image-20210304215213380

引入异常

假如现在有一个异常:

1
2
3
4
5
6
7
8
9
10
11
@Service
public class UserService {
//这里执行后将会产生错误(异常):lucy明明少了钱,但是mary却并没有多钱
private UserDao userDao;
//转账方法
public void accountMoney(){
userDao.reduceMoney();//lucy 少 100
int x=10/0;
userDao.addMoney(); //mary 多 100
}
}

java 的方法来说,使用 try catch 抓异常 —— 编程式的事务管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void accountMoney() {
try {
//第一步 开启事务

//第二步 进行业务操作
//lucy少100
userDao.reduceMoney();

//模拟异常
int i = 10/0;

//mary多100
userDao.addMoney();

//第三步 没有发生异常,提交事务
}catch(Exception e) {
//第四步 出现异常,事务回滚
}
}

Spring 事务操作

  • 事务添加到 JavaEE 三层架构中的Service 层

  • 在 Spring 中进行事务管理操作主要有 2 种方式:编程式事务管理 & 声明式事务管理

    • 编程式事务管理
      • 在实际应用中很少使用
      • 通过TransactionTemplate手动管理事务
    • 声明式事务管理
      • 代码侵入性小,推荐使用
      • Spring 中声明事务通过AOP 实现
      • 基于注解/xml 配置文件方式

Spring 事务 API

提供了一个接口,代表着事务管理器,这个接口针对不同的框架提供不同的实现类

image-20210304221853695

注解 声明式事务管理

  1. 在 Spring 配置文件中配置事务管理器

    1
    2
    3
    4
    5
    <!--创建事务管理器-->
    <bean id="TransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!--注入数据源-->
    <property name="dataSource" ref="dataSource"></property>
    </bean>
  2. 在 Spring 配置文件中开启事务注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    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

    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop.xsd

    http://www.springframework.org/schema/tx
    http://www.springframework.org/schema/tx/spring-tx.xsd
    ">
    1
    2
    3
    4
    5
    <!--组件扫描-->
    <context:component-scan base-package="com.hpg"></context:component-scan>

    <!--开启事务注解-->
    <tx:annotation-driven transaction-manager="TransactionManager"></tx:annotation-driven>

    image-20210305165809201

    看到这个绿了 才代表事务注解开启

  3. 在 Service 类上面(或者 Service 类的具体方法上面)添加事务注解@Transactional

    1. 如果把这个注解添加到类上,代表类所有方法都添加事务
    2. 如果添加到方法上,只为这个方法添加事务
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    package com.hpg.service;

    import com.hpg.dao.UserDao;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    @Service
    @Transactional
    public class UserService {
    //注入 dao
    @Autowired
    private UserDao userDao;
    //转账的方法
    public void accountMoney() {
    //lucy 少 100
    userDao.reduceMoney();
    //模拟异常
    int i = 10 / 0;
    //mary 多 100
    userDao.addMoney();
    }
    }

    测试结果:

    image-20210305165244504

    数据库之前设置回了两个 1000,现在的结果仍然不变,证明事务回滚了

    image-20210305165311118

声明式事务管理参数配置

在 service 类上面添加注解@Transactional,在这个注解里面可以配置事务相关参数

  • Transactional 注解的参数如下:

image-20210305170329832

propagation:事务传播行为

事务方法直接进行调用,表示这个过程中事务是如何进行管理的

事务方法:指对数据表数据进行变化的操作 ,事务方法 ≠ 事务

事务传播行为解释如下图:

在一个事务中调用另一个事务,称为事务传播行为

image-20210305171415695

事务传播行为:

image-20210305171607613

  • REQUIRED:以上述图为例:如果 add 方法本身有事务,调用 update 方法后,update 使用当前 add 方法里面的事务;(这也是默认的 propagation 参数)

    如果 add 方法本身没有事务,调用 update 方法后,创建新的事务

  • REQUIRED_NEW:同样,以上述图为例:使用 add 调用 update 方法,不论 add 方法是否有事务,都会创建新的事务

solation:事务隔离级别

事务具有一个特性,我们称为:隔离性 —— 多事务操作之间不会产生影响

假如我们不考虑隔离性,就会有很多问题,包括:脏读、不可重复读、幻读

  1. 脏读: 一个未提交事务读取到另一个未提交事务的数据

    以下图为例:A B 都想对同一个数据的值进行修改,A 首先进行了修改,B 这时读到了修改后的数据,但 A 发生了事务回滚,数据回到了一开始未修改的状态,但是 A 仍然读到的是修改后的数据,这就是脏读

    image-20210305173122516

  2. 不可重复读: 一个未提交事务读取到另一提交事务中修改的数据

    以下图为例:A B 对同一个数据值进行查询。A 首先看到数据为 5000,B 将数据修改成了 900,并进行了提交。虽然 A 并未提交数据,但是此时再查看数据的时候发现数据改变了,这就叫不可重复读

    image-20210305185250438

  3. 幻读: 一个未提交事务读取到另一提交事务中添加的数据

    幻读和不可重复读的区别在于,后者是修改,而前者是添加

    A B 对一个表单进行查询数据,假设一开始只有一条数据。A 此时查表,发现只有一条数据,而后 B 添加了一条数据,A 再查数据发现多了一条,这与之前的查询结果不一样,这就叫幻读

既然有上述问题出现,那么如何解决呢?就通过设置事务隔离性去解决读的问题

其中假如我们不设置隔离级别,Mysql 默认设置的是:REAPETABLE READ 即 可重复读

image-20210305190003173

timeout:超时时间

首先需要明白的是:事务需要在一定的时间内进行提交,如果不提交则需要进行回滚(rollback)

如果不设置超时时间,则默认的超时时间是 -1,设置时间以秒为单位进行计算

readOnly:是否只读
  • :指的是查询操作

  • :指的是增添 删除 和修改操作

  • 默认值 :false,代表着可以进行查询、添加、修改和删除操作

  • 设置成 true 后:只能进行查询,而不能进行增添、修改和删除操作

rollbackFor:回滚

表示:出现了哪些异常进行事务回滚

noRollbackFor:不回滚

表示:出现了哪些异常不进行事务回滚

noRollbackForClassName:不回滚 参数 String[]类型,默认为空数组。

配置文件 声明式事务管理

  1. 在 spring 配置文件中进行配置

    1. 配置事务管理器

      1
      2
      3
      4
      5
      <!--1.创建事务管理器-->
      <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
      <!--注入数据源-->
      <property name="dataSource" ref="dataSource"></property>
      </bean>
    2. 配置通知

      1
      2
      3
      4
      5
      6
      7
      8
      9
          <!--2.配置通知-->
      <tx:advice id="txadvice" >
      <!--配置事务参数-->
      <tx:attributes>
      <!--指定哪种规则的方法上面添加事务-->
      <tx:method name="accountMoney" propagation="REQUIRED"/>
      <!-- <tx:method name="account*"/> &lt;!&ndash;表示所有以account开头的方法&ndash;&gt;-->
      </tx:attributes>
      </tx:advice>
    3. 配置切入点和切面

      1
      2
      3
      4
      5
      6
      7
      8
      <!--3.配置切入点和切面-->
      <aop:config>
      <!--配置切入点-->
      <aop:pointcut id="pt" expression="execution(* com.hpg.service.UserService.*(..))"/>
      <!--配置切面-->
      <!--这行代码的意思是:把txadvice设置的通知 配置到了PT对应的方法上-->
      <aop:advisor advice-ref="txadvice" pointcut-ref="pt"/>
      </aop:config>

完全注解开发

工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.hpg.config;


import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@Configuration//代表这是一个配置类
@ComponentScan(basePackages = "com.hpg") //组件扫描
@EnableTransactionManagement //开启事务
public class TxConfig {
//创建数据库连接池
@Bean
public DruidDataSource getDruidDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/user_db?serverTimezone=UTC");
dataSource.setUsername("root");
dataSource.setPassword("123456");
return dataSource;
}
//创建 JdbcTemplate 对象
@Bean
public JdbcTemplate getJdbcTemplate(DataSource dataSource) {//从IOC容器中拿到配置注入的数据源
//到 ioc 容器中根据类型找到 dataSource
JdbcTemplate jdbcTemplate = new JdbcTemplate();
//注入 dataSource
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
//创建事务管理器
@Bean
public DataSourceTransactionManager
getDataSourceTransactionManager(DataSource dataSource) {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}

同时,Service 类也要开启注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.hpg.service;

import com.hpg.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)
public class UserService {
//注入 dao
@Autowired
private UserDao userDao;
//转账的方法
public void accountMoney() {
//lucy 少 100
userDao.reduceMoney();
//模拟异常
int i = 10 / 0;
//mary 多 100
userDao.addMoney();
}
}

测试:

1
2
3
4
5
6
7
@Test
public void AccountTest3() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TxConfig.class);

UserService userService = context.getBean("userService", UserService.class);
userService.accountMoney();
}

模拟异常:

image-20210305202923965

数据库没有发生变化:

image-20210305202935032