1、Spring简介

1.1、概述

Spring

Spring : 春天,它给软件行业带来了春天。现代化Java的开发,就是基于Spring的开发!

  • 2002年,Rod Jahnson首次推出了Spring框架雏形interface21框架。

  • 2004年3月24日,Spring框架以interface21框架为基础,经过重新设计,发布了1.0正式版。

很难想象Rod Johnson的学历 , 他是悉尼大学的博士,然而他的专业不是计算机,而是音乐学。

Spring理念 : 让现有技术更容易使用。它本身就是一个大杂烩,也是一种融合剂。它整合了现有的框架技术。

1.2、优点和缺点

(1)优点:

  • Spring是一个开源免费的容器(框架)。
  • Spring是一个轻量级的、非侵入式的框架。
  • 两大特性:控制反转(IOC)、面向切面编程(Aop)
  • 支持事务的处理。
  • 支持各种框架的整合。

**(2)缺点:**经过长期的发展,它的配置变得十分繁琐,违背了原来的理念。人称:”配置地狱“!直到SPring Boot的出现。

1.3、总结成一句话

一句话概括:Spring是一个轻量级的控制反转(IOC)和面向切面编程(AOP)的容器(框架)。

1.4、Spring框架组成

Spring框架是一个分层架构,它由7大模块组成。

Spring组成

Spring模块构建在核心容器(Spring Core)之上,核心容器定义了创建、配置和管理bean的方式 。

组成Spring框架的每个模块(或组件)都可以单独存在,或者与其他一个或多个模块联合实现。

每个模块的功能如下:

  • 核心容器:核心容器提供Spring框架的基本功能。核心容器的主要组件是BeanFactory,它是工厂模式的实现。BeanFactory使用控制反转(IOC) 模式将应用程序的配置和依赖性规范与实际的应用程序代码分开。
  • Spring AOP:通过配置管理特性,Spring AOP模块直接将面向切面的编程功能 , 集成到了Spring框架中。所以,可以很容易地使Spring框架管理任何支持AOP的对象。Spring AOP模块为基于Spring的应用程序中的对象提供了事务管理服务。通过使用Spring AOP,不用依赖组件,就可以将声明性事务管理集成到应用程序中。
  • Spring DAOJDBC DAO抽象层提供了有意义的异常层次结构,可用该结构来管理异常处理和不同数据库供应商抛出的错误消息。异常层次结构简化了错误处理,并且极大地降低了需要编写的异常代码数量(例如打开和关闭连接)。Spring DAO的面向JDBC的异常遵从通用的DAO异常层次结构。
  • Spring ORM(Object-Relational Mapping,对象关系映射):Spring 框架插入了若干个ORM框架,从而提供了ORM的对象关系工具,其中包括 JDOHibernateiBatis SQL Map。所有这些都遵从Spring的通用事务和DAO异常层次结构。
  • Spring上下文(Context)Spring 上下文是一个配置文件,向Spring框架提供上下文信息。Spring上下文包括企业服务,例如 JNDIEJB、电子邮件、国际化、校验和调度功能。
  • Spring Web 模块Web上下文模块建立在应用程序上下文模块之上,为基于Web的应用程序提供了上下文。所以,Spring框架支持与Jakarta Struts的集成。Web模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作。
  • Spring MVC 框架MVC框架是一个全功能的构建Web应用程序的MVC实现。通过策略接口,MVC框架变成为高度可配置的,MVC容纳了大量视图技术,其中包括 JSPVelocityTilesiTextPOI

1.5、Spring Boot与Spring Cloud

  • Spring Boot Spring的一套快速配置脚手架,我们可以基于Spring Boot快速开发单个微服务;
  • Spring Cloud是基于Spring Boot实现的;
  • Spring Boot专注于快速、方便集成的单个微服务个体,Spring Cloud关注全局的服务治理框架;
  • Spring Boot使用了约束优于配置的理念,很多集成方案已经帮你选择好了,能不配置就不配置 。Spring Cloud很大一部分是基于Spring Boot来实现,Spring Boot可以离开Spring Cloud独立使用开发项目,但是Spring Cloud离不开Spring Boot,属于依赖的关系。
  • SpringBootSpringClound中起到了承上启下的作用,如果你要学习SpringCloud必须要学习SpringBoot
  • 学好Spring Boot的前提是:需要完全掌握SpringSpring MVC

2、IOC理论推导(重点)

2.1、引例

2.1.1、以前实现一个业务的步骤

(1)新建一个普通的Maven项目,(具体步骤参考《Maven详解》),项目名称为Spring-Study

(2)删除系统自动生成的src目录,所得到的空项目就是我们的Maven父项目,我们可以在其中创建多个子项目(即:Module)。

(3)在父项目的pom.xml文件中导入父/子工程需要的Maven依赖。

pom.xml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 导入Maven依赖 -->
<dependencies>
<!-- Spring依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
<!-- junit依赖 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>

(4)在父项目中新建一个模块(Module),并在其中创建一个普通的Maven子项目,项目名称为spring-01-IOC1

(5)填写子项目名称和Maven项目GAV,点击Finish按钮完成子项目创建,等待Maven依赖包导入完毕。

(6)在子项目的java目录下新建一个com.atangbiji.dao包(用于存放我们自己编写的持久层代码),在该包下新建一个UserDao接口,并在其中创建接口函数。

UserDao.java文件:

1
2
3
4
5
6
7
package com.atangbiji.dao;

/****Dao层接口****/
public interface UserDao {
//获取用户的数据
void getUser();
}

(7)然后,在子项目的com.atangbiji.dao包下新建一个UserDaoImpl接口实现类,并在其中重写接口中的方法。

UserDaoImpl.java文件:

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

/****Dao层接口实现类****/
public class UserDaoImpl implements UserDao{
//重写Dao接口中的方法
@Override
public void getUser() {
System.out.println("获取用户数据");
}
}

(8)在子项目的java目录下新建一个com.atangbiji.service包(用于存放我们自己编写的业务层代码),在该包下新建一个UserService 接口,并在其中创建接口函数。

UserService.java文件:

1
2
3
4
5
6
7
package com.atangbiji.service;

/****业务层接口****/
public interface UserService {
//获取用户的数据
void getUser();
}

(9)然后,在子项目的com.atangbiji.service包下新建一个UserServiceImpl接口实现类,并在其中重写接口中的方法。

UserServiceImpl.java文件:

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

import com.atangbiji.dao.UserDao;
import com.atangbiji.dao.UserDaoImpl;

/****业务层接口实现类****/
public class UserServiceImpl implements UserService{

//1、组合(将Dao层接口引入业务层使用)
private UserDao userDao = new UserDaoImpl();
//重写Service接口中的方法
@Override
public void getUser() {
//2、在业务层调用Dao层接口方法
userDao.getUser();
}
}

注:此时,我们是在service接口实现类中直接新建(new)了一个Dao接口实现类的对象。

(10)在子项目的src/test/java目录下,新建一个MyTest测试类,用于用户调用业务层接口。

MyTest.java文件:

1
2
3
4
5
6
7
8
9
10
11
12
import com.atangbiji.service.UserService;
import com.atangbiji.service.UserServiceImpl;
import org.junit.Test;

public class MyTest {
@Test
public void test(){
//用户实际调用的是业务层,用户不需要接触Dao层方法
UserService service = new UserServiceImpl();
service.getUser();
}
}

运行测试程序,执行结果如下图所示:

用户调用业务层测试结果

以上就是我们以前处理一个业务的方式。

2.1.2、以前业务处理方式存在的问题

按照以前的业务处理方式,如果此时用户需要将需求修改为“从MySQL中获取用户数据”,那么程序员就需要修改Dao层和Service层代码,具体步骤如下:

(1)先在子项目的com.atangbiji.dao包下新建一个UserDaoMySqlImpl接口实现类,并在其中重写接口中的方法。

UserDaoMySqlImpl.java文件:

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

/****Dao层接口实现类****/
public class UserDaoMysqlImpl implements UserDao {
//重写Dao接口中的方法
@Override
public void getUser() {
System.out.println("MySql获取用户数据");
}
}

(2)然后,去service接口实现类中修改业务层对应的实现。

UserServiceImpl.java文件:

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

import com.atangbiji.dao.UserDao;
import com.atangbiji.dao.UserDaoMysqlImpl;

/****业务层接口实现类****/
public class UserServiceImpl implements UserService{
//1、组合(将Dao层接口引入业务层使用)
private UserDao userDao = new UserDaoMysqlImpl();
//重写Service接口中的方法
@Override
public void getUser() {
//2、在业务层调用Dao层接口方法
userDao.getUser();
}
}

(3)在不修改测试程序的情况下,重新运行测试程序,执行结果如下图所示:

用户调用新增业务测试结果

如果此时用户又需要将需求修改为“从Oracle中获取用户数据呢?”,那么程序员就又需要重复上述步骤,重新修改Dao层和Service层代码。

实际项目中,假设用户的这种需求非常多 , 程序员可能需要修改几十个地方,这种方式就很反人类。这种设计的耦合性太高了,程序不能适应用户的需求变化,牵一发而动全身。 因此,每次用户需求发生变化 , 程序员都需要修改大量service层代码,此时程序的控制权掌握在程序员手上。【虽然他不想掌握(改代码)!】

2.1.3、解决方式

为了解决上述问题,我们不再在service层直接创建(newDao层接口实现类的对象,而是将Dao接口类的对象定义为service接口实现类的成员变量,并通过Set的方式把接口暴露给用户,进而动态实现值得注入。这样,在不修改底层代码的情况下,用户就可以根据自己的需求灵活调用底层接口了。因此,我们只需对service接口实现类做如下修改:

UserServiceImpl.java文件:

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

import com.atangbiji.dao.UserDao;

/****业务层接口实现类****/
public class UserServiceImpl implements UserService{

//将Dao接口类的对象定义为service接口实现类的成员变量
private UserDao userDao;
//通过Set的方式把接口暴露给用户,进而动态实现值得注入
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}

//重写Service接口中的方法
@Override
public void getUser() {
//2、在业务层调用Dao层接口方法
userDao.getUser();
}
}

通过这种方式,用户若想在测试类中根据需要灵活调用各种业务层接口,就不需要程序员修改底层代码的了。如:

MyTest.java文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.atangbiji.dao.UserDaoImpl;
import com.atangbiji.dao.UserDaoMysqlImpl;
import com.atangbiji.service.UserServiceImpl;
import org.junit.Test;

public class MyTest {
@Test
public void test(){
//用户实际调用的是业务层,用户不需要接触Dao层方法
UserServiceImpl service = new UserServiceImpl();
service.setUserDao(new UserDaoImpl());
service.getUser();
//如果用户将需求修改为:从MySQL中获取用户数据
service.setUserDao(new UserDaoMysqlImpl());
service.getUser();
}
}

运行测试程序,执行结果如下图所示:

用户灵活调用底层接口测试结果

大家发现两种业务处理方式的区别没有 ? 可能很多人说没啥区别,但其实它们已经发生了根本性的变化。

  • 以前的业务处理方式中,用户的需求变化会影响我们原来的代码,当用户的需求发生变化时,程序员需要根据用户的需求去修改原来的代码。此时,业务层代码主动创建对象,程序的控制权掌握在程序员手上。

  • 而现在的业务处理方式中,用户的需求变化不会影响我们原来的代码,当用户的需求发生变化时,用户只需要在调用业务层接口时通过set方法注入(new)相应的Dao接口实现类的对象即可。此时,业务层代码不再具有主动性,程序的控制权反转到了用户(调用者)的手上

这种思想,从本质上解决了上述问题,我们程序员不再去管理对象的创建了 , 更多的去关注业务的实现。系统的耦合性大大降低,这也就是控制反转(IOC)的原型 !Spring的底层使用的都是这种控制反转(IOC)机制,底层接口如果没有set方法是无法正常运行的。

2.2、IOC容器和Bean

  • 所谓**IOC容器**,指的就是:Spring实现控制反转(IOC)的容器。

    IOC容器负责创建、定位、配置应用程序中的对象及建立这些对象间的依赖。这样,应用程序无需直接在代码中new相关的对象,应用程序由IOC容器进行组装。在Spring中BeanFactory是IOC容器的实际代表者。

  • 所谓**Bean**,指的就是:由IOC容器创建、组装和管理的那些组成我们应用程序的Java对象。

2.3、IOC容器的工作原理

IOC容器的工作原理如下图所示:

IOC容器

(1)Spring IOC容器在初始化时,首先通过读取**配置文件(或注解)**的方式获取其中的配置元数据;

(2)然后再通过元数据对应用中的各个对象(Bean)进行创建和组装,并存入到容器中。

(3)当程序需要使用相关对象时,直接从IOC容器中获取相关的对象(Bean)即可。

2.3、IOC的本质

控制反转IOC(Inversion of Control)是一种设计思想,其本质还是为了程序解耦

理想的系统希望各模块(层)之间相互独立,如下图3所示。

  • 在没有IOC的程序中 , 我们使用面向对象编程 , 对象的创建(new)与对象间的依赖关系完全硬编码在程序中,对象的创建由程序自己控制(如下图1所示)。
  • Spring通过IOC容器实现控制反转。实现控制反转后,我们便将对象的创建(new)转移给了第三方,即:IOC容器(如下图2所示)。

所谓控制反转就是:获得依赖对象的方式发生了反转。

IOC容器的作用

注:

  • 所谓IOC,对于Spring框架来说,就是由Spring的IOC容器来负责管理对象的生命周期和对象间的关系。

  • 控制反转的实现方式有很多种,DI(Dependency Injection,依赖注入)只是其中的一种。

  • IOCSpring框架的核心内容,Spring使用多种方式完美的实现了IOC。我们可以使用XML配置,也可以使用注解配置,新版本的Spring也可以零配置实现IOC

    • 采用XML方式配置Bean的时候,Bean的定义信息是和实现分离的;
    • 而采用注解的方式可以把两者合为一体,Bean的定义信息直接以注解的形式定义在实现类中,从而达到了零配置的目的。

综上所述:

  • 控制反转是一种通过描述(XML或注解)并通过第三方(Spring中IOC容器)去生产或获取特定对象的方式。

  • 在Spring中实现控制反转的是IOC容器,其实现方法是依赖注入(Dependency Injection,DI)。

3、HelloSpring

3.1、创建普通的Maven项目

(1)在父项目中新建一个模块(Module),并在其中创建一个普通的Maven子项目,项目名称为spring-02-helloSpring

(2)填写子项目名称和Maven项目GAV,点击Finish按钮完成子项目创建,等待Maven依赖包导入完毕。

3.2、创建实体类

(3)在子项目的java目录下新建一个com.atangbiji.pojo包(用于存放我们自己编写的实体类),并在该包下新建一个Hello实体类。

Hello.java文件:

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

//实体类
public class Hello {
private String str;

public String getStr() {
return str;
}

public void setStr(String str) {
this.str = str;
}

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

3.3、创建Beans的XML配置文件(重点)

(4)在子项目的resources目录下,新建一个beans.xml配置文件,用于配置Beans的元数据

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就是那些组成我们应用程序的Java对象,由Spring中的IOC容器创建、组装和管理-->
<!--bean标签用于创建Java对象。其中:id:bean的唯一标识;class:bean所对应对象的全限定类名-->
<bean id="hello" class="com.atangbiji.pojo.Hello">
<!--property标签用于给对象中的属性设置一个值-->
<property name="str" value="Hello Spring!"/>
</bean>
</beans>

注:

  • XML配置文件的名称可以自定义,官方建议使用applicationContext.xml
  • Spring给8种基本类型和String类型的成员变量赋值时用value
  • Spring给引用类型的成员变量赋值时用ref

3.4、测试

(5)在子项目的src/test/java目录下,新建一个MyTest测试类,用于测试Spring

MyTest.java文件:

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

public class MyTest {
@Test
public void test(){
//解析beans.xml文件,IOC容器生成并管理相应的Bean对象
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
//通过Spring的上下文对象,从IOC容器中获取Bean对象
Hello hello = (Hello) context.getBean("hello");
//调用Bean对象的方法
System.out.println(hello.toString());
}
}

运行测试程序,执行结果如下图所示:

HelloSpring测试结果

3.5、结果分析

分析上述代码,可以发现:

  • Hello类的对象是由Spring创建的。
  • Hello类对象的属性是由Spring容器设置的。

这个过程就叫做“控制反转” :

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

依赖注入:就是Spring实例化对象时通过实体类的set方法来进行数据注入的。

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

**注:**将xml配置文件配置为Application Context后,如果我们在xml配置文件中配置了Bean,那么与之对应实体类的旁边会出现相应的小图标(如下图所示),我们可以点击它们进行快速导航。

配置为ApplicationContext

快速导航

此时,若用户需求发生变化,我们只需要在xml配置文件中进行修改即可,程序员就彻底不用修改底层代码了。

结论:所谓IoC,一句话搞定就是:对象由Spring来创建、管理、装配 ! (以后我们再也不用通过new来创建对象了!)

4、IOC容器创建对象的方式

4.1、通过无参构造方法来创建

(1)在父项目中新建一个模块(Module),并在其中创建一个普通的Maven子项目,项目名称为spring-03-ioc2

(2)填写子项目名称和Maven项目GAV,点击Finish按钮完成子项目创建,等待Maven依赖包导入完毕。

(3)在子项目的java目录下新建一个com.atangbiji.pojo包(用于存放我们自己编写的实体类),并在该包下新建一个User实体类。

User.java文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.atangbiji.pojo;
//实体类
public class User {
private String name;
//无参构造函数
public User() {
System.out.println("user的无参构造方法!");
}

public String getName() {
return name;
}

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

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

(4)在子项目的resources目录下,新建一个beans.xml配置文件,用于配置Beans的元数据

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就是那些组成我们应用程序的Java对象,由Spring中的IOC容器创建、组装和管理-->
<!--bean标签用于创建Java对象。其中:id:bean的唯一标识;class:bean所对应对象的全限定类名-->
<bean id="User" class="com.atangbiji.pojo.User">
<!--property标签用于给对象中的属性设置一个值-->
<property name="name" value="阿汤笔迹"/>
</bean>
</beans>

(5)在子项目的src/test/java目录下,新建一个MyTest测试类。

MyTest.java文件:

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

public class MyTest {
@Test
public void test(){
//解析beans.xml文件,IOC容器生成并管理相应的Bean对象
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
//通过Spring的上下文对象,从IOC容器中获取Bean对象
User user = (User) context.getBean("User");
//调用Bean对象的方法
user.show();
}
}

运行测试程序,执行结果如下图所示:

通过无参构造方法创建对象

由此我们可以发现:在调用show方法之前,User对象已经通过无参构造初始化了!

4.2、通过有参构造方法来创建

(1)UserT.java文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.atangbiji.pojo;
//实体类
public class UserT {
private String name;
//有参构造函数
public UserT(String name) {
this.name = name;
System.out.println("user的有参构造方法!");
}

public String getName() {
return name;
}

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

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

(2)beans.xml中给有参构造函数传参的方式有以下几种:

方式一:根据构造函数参数索引index)给参数赋值。

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就是那些组成我们应用程序的Java对象,由Spring中的IOC容器创建、组装和管理-->
<!--bean标签用于创建Java对象。其中:id:bean的唯一标识;class:bean所对应对象的全限定类名-->
<bean id="UserT" class="com.atangbiji.pojo.UserT">
<!--根据构造函数参数索引(index)给参数赋值-->
<constructor-arg index="0" value="张三"/>
</bean>
</beans>

注:index从0开始。

方式二:通过构造函数参数类型匹配给参数赋值。(不推荐)

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就是那些组成我们应用程序的Java对象,由Spring中的IOC容器创建、组装和管理-->
<!--bean标签用于创建Java对象。其中:id:bean的唯一标识;class:bean所对应对象的全限定类名-->
<bean id="UserT" class="com.atangbiji.pojo.UserT">
<!--通过构造函数参数类型匹配给参数赋值-->
<constructor-arg type="java.lang.String" value="张三"/>
</bean>
</beans>

**注:**当构造函数有多个同类型的参数时,该方式将按照constructor-arg标签书写顺序依次给参数赋值。因此,为了防止出错,不推荐使用此方式。

方式三:通过构造函数参数名称给参数赋值。

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就是那些组成我们应用程序的Java对象,由Spring中的IOC容器创建、组装和管理-->
<!--bean标签用于创建Java对象。其中:id:bean的唯一标识;class:bean所对应对象的全限定类名-->
<bean id="UserT" class="com.atangbiji.pojo.UserT">
<!--通过构造函数参数名称给参数赋值-->
<constructor-arg name="name" value="张三"/>
</bean>
</beans>

附:当构造函数参数为引用类型时,constructor-arg标签使用ref属性给参数传值。如:

XXX.java文件:

1
2
3
4
5
6
public class ThingOne {
//有参构造函数
public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
// ...
}
}

beans.xml文件:

1
2
3
4
5
6
7
8
9
10
<beans>
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg ref="beanTwo"/>
<constructor-arg ref="beanThree"/>
</bean>

<bean id="beanTwo" class="x.y.ThingTwo"/>

<bean id="beanThree" class="x.y.ThingThree"/>
</beans>

(3)测试。

MyTest.java文件:

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

public class MyTest {
@Test
public void testT(){
//解析beans.xml文件,IOC容器生成并管理相应的Bean对象
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
//通过Spring的上下文对象,从IOC容器中获取Bean对象
UserT user = (UserT) context.getBean("UserT");
//调用Bean对象的方法
user.show();
}
}

运行测试程序,执行结果如下图所示:

通过有参构造方法创建对象

结论:在xml配置文件加载的时候,容器中配置的所有对象都已经被初始化了!

5、Spring配置

5.1、别名

我们可以通过alias 标签Bean设置别名 。这样,我们在获取Bean的时候便可以通过别名获取到这个对象了。如:

beans.xml文件:

1
2
<!--给Bean设置别名-->
<alias name="fromName" alias="toName"/>

**注:**别名区分大小写。

5.2、Bean的基本配置

**Bean就是java对象,由Spring创建和管理。**我们在Bean的XML配置文件中对其进行配置。如:

beans.xml文件:

1
2
3
4
5
6
7
8
<!--
(1)id:bean的唯一标识符。(相当于之前new的对象名)
(2)class:bean所对应对象的全限定类名。(全限定名 = 包名 + 类名)
(3)name:也是“别名”。
-->
<bean id="UserT" name="UserTwo user2,u3;u4" class="com.kuang.pojo.UserT">
<property name="name" value="Spring"/>
</bean>

注:

  • 如果我们没有给bean配置id,那么name就是其默认标识符。
  • 如果我们给bean既配置了id,又配置了name,那么name就是别名。
  • name中可以设置多个别名,可以用逗号、分号或空格隔开。
  • 如果不配置idname,那么我们可以通过applicationContext.getBean(.class)获取对象;

5.3、import(导入)

import标签一般用于团队开发,它可以将多个xml配置文件中的beans导入,合并为一个配置文件。如:

beans.xml文件:

1
2
3
4
5
6
7
8
9
<beans>
<!--导入其它xml配置文件中的Bean-->
<import resource="beans2.xml"/>
<import resource="resources/beans3.xml"/>
<import resource="/resources/beans4.xml"/>
<!--当前xml配置文件中的Bean-->
<bean id="bean1" class="..."/>
<bean id="bean2" class="..."/>
</beans>

**注:**注意xml配置文件的导入顺序。当不同配置文件中都定义了同一个bean时,后导入的会覆盖前面的。

至此,就算入门Spring了,认真体会Spring的好处吧!

6、依赖注入

依赖注入(Dependency InjectionDI)指的是Spring在创建对象时,IOC容器把对象依赖的属性注入到对象中的过程。

  • 依赖:Bean对象的创建依赖于容器。
  • 注入:Bean对象的所有属性由容器注入。

6.1、通过构造器注入

4、IOC容器创建对象的方式一节中已经介绍了。

6.2、通过Set方式注入(重点)

6.2.1、搭建测试环境

(1)在父项目中新建一个模块(Module),并在其中创建一个普通的Maven子项目,项目名称为spring-04-di

(2)填写子项目名称和Maven项目GAV,点击Finish按钮完成子项目创建,等待Maven依赖包导入完毕。

(3)在子项目的java目录下新建一个com.atangbiji.pojo包(用于存放我们自己编写的实体类),并在该包下新建两个名称分别为StudentAddress的实体类。

Address.java文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.atangbiji.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.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
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
package com.atangbiji.pojo;

import java.util.*;

//实体类
public class Student {
//成员变量
private String name;
private Address address;
private String[] books;
private List<String> hobbies;
private Map<String,String> card;//学生卡
private Set<String> games;
private String wife;//妻子(Null)
private Properties info;//属性

public String getName() {
return name;
}

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

public Address getAddress() {
return address;
}

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

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

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

public List<String> getHobbies() {
return hobbies;
}

public void setHobbies(List<String> hobbies) {
this.hobbies = hobbies;
}

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

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

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

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

public String getWife() {
return wife;
}

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

public Properties getInfo() {
return info;
}

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

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

(4)在子项目的resources目录下,新建一个beans.xml配置文件,用于配置Beans的元数据

(5)在子项目的src/test/java目录下,新建一个MyTest测试类。

6.2.2、常量注入

beans.xml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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就是那些组成我们应用程序的Java对象,由Spring中的IOC容器创建、组装和管理-->
<!--(1)id:bean的唯一标识符。(相当于之前new的对象名)-->
<!--(2)class:bean所对应对象的全限定类名。(全限定名=包名+类名)-->
<!--(3)name:也是“别名”。-->
<bean id="student" class="com.atangbiji.pojo.Student">
<!--常量注入-->
<property name="name" value="阿汤"/>
</bean>
</beans>

6.2.3、Bean注入

beans.xml文件:

1
2
3
4
5
6
7
<bean id="address" class="com.atangbiji.pojo.Address">
<property name="address" value="湖北省武汉市"/>
</bean>
<bean id="student" class="com.atangbiji.pojo.Student">
<!--Bean注入-->
<property name="address" ref="address"/>
</bean>

注:Bean注入时,value是一个引用,使用ref属性注入。

6.2.4、数组注入

beans.xml文件:

1
2
3
4
5
6
7
8
9
10
11
<bean id="student" class="com.atangbiji.pojo.Student">
<!--数组注入-->
<property name="books">
<array>
<value>红楼梦</value>
<value>西游记</value>
<value>水浒传</value>
<value>三国演义</value>
</array>
</property>
</bean>

6.2.5、List注入

beans.xml文件:

1
2
3
4
5
6
7
8
9
10
<bean id="student" class="com.atangbiji.pojo.Student">    
<!--List注入-->
<property name="hobbies">
<list>
<value>听歌</value>
<value>编程</value>
<value>看电影</value>
</list>
</property>
</bean>

6.2.6、Map注入

beans.xml文件:

1
2
3
4
5
6
7
8
9
<bean id="student" class="com.atangbiji.pojo.Student">
<!--Map注入-->
<property name="card">
<map>
<entry key="身份证" value="888888888"/>
<entry key="学生卡" value="123456789"/>
</map>
</property>
</bean>

6.2.7、Set注入

beans.xml文件:

1
2
3
4
5
6
7
8
9
10
<bean id="student" class="com.atangbiji.pojo.Student">
<!--Set注入-->
<property name="games">
<set>
<value>英雄联盟</value>
<value>王者荣耀</value>
<value>绝地求生</value>
</set>
</property>
</bean>

6.2.8、Null注入

beans.xml文件:

1
2
3
4
5
6
<bean id="student" class="com.atangbiji.pojo.Student">
<!--Null注入-->
<property name="wife">
<null/>
</property>
</bean>

6.2.9、Properties(属性)注入

beans.xml文件:

1
2
3
4
5
6
7
8
9
10
<bean id="student" class="com.atangbiji.pojo.Student">    
<!--Properties(属性)注入-->
<property name="info">
<props>
<prop key="学号">20221125</prop>
<prop key="用户名">root</prop>
<prop key="密码">123456</prop>
</props>
</property>
</bean>

6.2.10、测试结果

MyTest.java文件:

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

public class MyTest {
@Test
public void testDI(){
//解析beans.xml文件,IOC容器生成并管理相应的Bean对象
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
//通过Spring的上下文对象,从IOC容器中获取Bean对象
Student student = (Student)context.getBean("student");
//调用Bean对象的方法
System.out.println(student.toString());
}
}

运行测试程序,依赖注入成功。执行结果如下图所示:

依赖注入测试结果

6.3、p命名空间和c命名空间注入

首先,在子项目的com.atangbiji.pojo包下新建一个名称为User的实体类。

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
27
28
29
30
package com.atangbiji.pojo;
//实体类
public class User {
private String name;
private int age;

public String getName() {
return name;
}

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

public int getAge() {
return age;
}

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

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

注:此时User实体类没有有参构造器,各属性依然要设置set方法。

然后,再在子项目的resources目录下,新建一个userBeans.xml配置文件,用于配置Beans的元数据

6.3.1、p命名空间注入

通过Pproperties,属性)命名空间注入,可以直接注入属性的值。它对应的就是Set方式注入,具体使用步骤如下:

(1)在xml配置文件的头部导入约束。

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

(2)在bean标签中使用p命名空间注入。

userBeans.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"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!--bean就是那些组成我们应用程序的Java对象,由Spring中的IOC容器创建、组装和管理-->
<!--(1)id:bean的唯一标识符。(相当于之前new的对象名)-->
<!--(2)class:bean所对应对象的全限定类名。(全限定名=包名+类名)-->
<!--通过P(properties,属性)命名空间注入, 可以直接注入属性的值。-->
<bean id="user" class="com.atangbiji.pojo.User" p:name="阿汤" p:age="18"/>
</beans>

(3)测试。

MyTest.java文件:

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

public class MyTest {
@Test
public void testDI2(){
//userBeans.xml文件,IOC容器生成并管理相应的Bean对象
ApplicationContext context = new ClassPathXmlApplicationContext("userBeans.xml");
//通过Spring的上下文对象,从IOC容器中获取Bean对象
User user = (User)context.getBean("user");
//调用Bean对象的方法
System.out.println(user.toString());
}
}

运行测试程序testDI2,执行结果如下图所示:

p命名空间注入成功

6.3.2、c命名空间注入

通过CConstructor,构造器)命名空间注入,也可以直接注入属性的值。它对应的就是构造器注入,具体使用步骤如下:

(1)在实体类中添加有参构造函数。

User.java文件:

1
2
3
4
5
//有参构造函数
public User(String name, int age) {
this.name = name;
this.age = age;
}

注:c命名空间注入的对象,其实体类必须要有有参构造函数。

(2)在xml配置文件的头部导入约束。

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

(3)在bean标签中使用c命名空间注入。

userBeans.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"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!--bean就是那些组成我们应用程序的Java对象,由Spring中的IOC容器创建、组装和管理-->
<!--(1)id:bean的唯一标识符。(相当于之前new的对象名)-->
<!--(2)class:bean所对应对象的全限定类名。(全限定名=包名+类名)-->
<!--通过C(Constructor,构造器)命名空间注入, 可以直接注入属性的值。-->
<bean id="user" class="com.atangbiji.pojo.User" c:name="阿汤" c:age="20"/>
</beans>

(4)再次运行测试程序testDI2,执行结果如下图所示:

c命名空间注入成功

7、Bean的作用域

Spring中,那些组成应用程序的主体及由Spring IoC容器所管理的对象,被称之为bean。简单地讲,bean就是由IoC容器初始化、装配及管理的对象。

Spring中Bean的作用域有以下6种:

作用域 描述
singleton 每次从IOC容器中获取Bean时,返回的都是同一个实例。Bean以单例模式存在。Spring中的Bean默认以单例模式存在。
prototype 每次从IOC容器中获取Bean时,返回的都是一个新的实例。
request Bean的对象只在同一次HTTP请求中有效。(仅用于Web开发)
session Bean的对象只在同一次会话中有效。(仅用于Web开发)
application Bean的对象在**同一个Web应用(项目)**中有效,即服务器开启过程中一直有效。(仅用于Web开发)
websocket 类似于application作用域。

7.1、singleton(单例模式)

如果一个bean的作用域为Singleton,那么Spring IoC容器中只会存在一个共享的bean实例。当获取bean时,只要id与该bean相匹配,就会返回同一bean的实例。Singleton是单例模式,就是在创建容器时就同时自动创建了一个bean的对象,不管你是否使用,它都已经存在了,且每次获取到的对象都是同一个对象。

Singleton作用域是Springbean的默认作用域。我们也可以在XML中显示地将bean定义成singleton。如:

userBeans.xml文件:

1
2
3
4
<bean id="user" class="com.atangbiji.pojo.User" scope="singleton">
<property name="name" value="阿汤"/>
<property name="age" value="18"/>
</bean>

**附:**测试。

MyTest.java文件:

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

public class MyTest {
@Test
public void testDI3(){
//userBeans.xml文件,IOC容器生成并管理相应的Bean对象
ApplicationContext context = new ClassPathXmlApplicationContext("userBeans.xml");
//通过Spring的上下文对象,从IOC容器中获取Bean对象
User user1 = (User)context.getBean("user");
User user2 = (User)context.getBean("user");
//输出两个对象的HashCode,并判断两个对象是否相同
System.out.println(user1.hashCode());
System.out.println(user2.hashCode());
System.out.println(user1 == user2);
}
}

运行测试程序testDI3,执行结果如下图所示:

单例模式测试结果

7.2、prototype(原型模式)

若一个bean的作用域为Prototype,则表示一个bean的定义可以对应多个对象实例。Prototype作用域的bean会导致在每次对该bean请求(将其注入到另一个bean中,或者以程序的方式调用容器的getBean()方法)时都会创建一个新的bean实例。

Prototype是原型模式,它在我们创建容器的时候并没有实例化,而是当我们获取bean的时候才会去创建一个对象,而且我们每次获取到的对象都不是同一个对象。在XML中将bean定义成prototype,可以这样配置:

userBeans.xml文件:

1
2
3
4
<bean id="user" class="com.atangbiji.pojo.User" scope="prototype">
<property name="name" value="阿汤"/>
<property name="age" value="18"/>
</bean>

**附:**测试。重新运行测试程序testDI3,执行结果如下图所示:

原型模式测试结果

**注:**根据经验,对有状态的bean应该使用prototype作用域;而对无状态的bean则应该使用singleton作用域。

7.3、其它作用域

requestsessionapplicationwebsocket作用域仅在web开发中使用。

8、Bean的自动装配

8.1、自动装配说明

8.1.1、Bean的3种装配机制

Spring中,Bean3种装配机制,分别是:

  • 通过xml配置文件(或注解)的方式手动地显式配置;
  • java代码中显式配置;
  • 通过xml配置文件(或注解)的方式隐式地自动装配。【重点】

第一种,我们前面已经学习了,第二种会在以后的学习中再介绍。我们这里主要讲解的是第三种:自动化的装配bean。

8.1.2、什么是Bean的自动装配

所谓Bean的自动装配,即:Spring会在应用上下文中自动寻找,并自动给Bean装配属性的过程。

注:Spring中既可以通过xml配置文件的方式实现Bean的自动装配,又可以通过注解的方式实现Bean的自动装配。但由于在xml中实现自动装配可能会出现:我们以为bean的某些属性会被自动装配,但其实并没有,进而导致bean的这些属性忘记手动配置的情况。因此,一般情况下,建议使用注解的方式实现Bean的自动装配。

8.1.3、自动装配的两大步骤

Spring的自动装配过程需要分为两大步骤:

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

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

8.2、搭建测试环境

(1)在父项目中新建一个模块(Module),并在其中创建一个普通的Maven子项目,项目名称为spring-05-Autowired

(2)填写子项目名称和Maven项目GAV,点击Finish按钮完成子项目创建,等待Maven依赖包导入完毕。

(3)在子项目的java目录下新建一个com.atangbiji.pojo包(用于存放我们自己编写的实体类),并在该包下新建两个名称分别为CatDog的实体类。

Cat.java文件:

1
2
3
4
5
6
7
8
package com.atangbiji.pojo;
//实体类
public class Cat {
//猫叫
public void shout(){
System.out.println("喵喵!");
}
}

Dog.java文件:

1
2
3
4
5
6
7
8
package com.atangbiji.pojo;
//实体类
public class Dog {
//狗叫
public void shout(){
System.out.println("汪汪!");
}
}

(4)在子项目的com.atangbiji.pojo包下新建一个实体类User

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.atangbiji.pojo;
//实体类
public class User {
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;
}

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

(5)在子项目的resources目录下,新建一个beans.xml配置文件,用于配置Beans的元数据

beans.xml文件:

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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!--bean就是那些组成我们应用程序的Java对象,由Spring中的IOC容器创建、组装和管理-->
<!--(1)id:bean的唯一标识符。(相当于之前new的对象名)-->
<!--(2)class:bean所对应对象的全限定类名。(全限定名=包名+类名)-->
<bean id="cat" class="com.atangbiji.pojo.Cat"/>
<bean id="dog" class="com.atangbiji.pojo.Dog"/>

<bean id="user" class="com.atangbiji.pojo.User">
<property name="cat" ref="cat"/>
<property name="dog" ref="dog"/>
<property name="name" value="阿汤"/>
</bean>
</beans>

(6)在子项目的src/test/java目录下,新建一个MyTest测试类。

MyTest.java文件:

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

public class MyTest {
@Test
public void test(){
//解析beans.xml文件,IOC容器生成并管理相应的Bean对象
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
//通过Spring的上下文对象,从IOC容器中获取Bean对象
User user = (User)context.getBean("user");
//调用Bean对象的方法
user.getCat().shout();
user.getDog().shout();
}
}

运行测试程序test,执行结果如下图所示:

自动装配环境测试

8.3、方式一:在xml配置文件中实现自动装配

8.3.1、按名称自动装配(autowire byName)

由于在手动配置xml文件的过程中,经常出现字母缺漏和大小写出错等问题,而无法对其进行检查,使得开发效率降低。

采用自动装配可以避免这些问题,并且使配置简单化。具体实现如下:

(1)修改配置文件,在bean标签中增加一个属性autowire="byName"。这样,我们就不用手动注入属性CatDog了。

beans.xml文件:

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">
<!--bean就是那些组成我们应用程序的Java对象,由Spring中的IOC容器创建、组装和管理-->
<!--(1)id:bean的唯一标识符。(相当于之前new的对象名)-->
<!--(2)class:bean所对应对象的全限定类名。(全限定名=包名+类名)-->
<bean id="cat" class="com.atangbiji.pojo.Cat"/>
<bean id="dog" class="com.atangbiji.pojo.Dog"/>
<!--按名称自动装配-->
<bean id="user" class="com.atangbiji.pojo.User" autowire="byName">
<property name="name" value="阿汤"/>
</bean>
</beans>

(2)再次运行测试程序test,依赖依旧注入成功。执行结果如下图所示:

自动装配byName

**注:**若我们将catbean id修改为catXXX,再次运行测试程序test,则会报空指针异常java.lang.NullPointerException。这是因为按照byName规则找不对应的set方法,真正的setCat方法就没执行,对象就没有初始化,所以调用时就会报空指针异常的错误。

结论

当一个bean节点(标签)配置了autowire="byName"的属性时,Spring将会:

  1. 自动查找其类中所有的set方法名,例如setCat,并获得将set去掉并且首字母小写的字符串,即cat
  2. spring IOC容器上下文中查找是否有Beanid属性与该字符串相同。
  3. 如果有,就取出注入;如果没有,就报空指针异常。

8.3.2、按类型自动装配(autowire byType)

具体实现如下:

(1)修改配置文件,在bean标签中增加一个属性autowire="byType"。同样地,我们也不用手动注入属性CatDog了。

beans.xml文件:

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">
<!--bean就是那些组成我们应用程序的Java对象,由Spring中的IOC容器创建、组装和管理-->
<!--(1)id:bean的唯一标识符。(相当于之前new的对象名)-->
<!--(2)class:bean所对应对象的全限定类名。(全限定名=包名+类名)-->
<bean id="cat" class="com.atangbiji.pojo.Cat"/>
<bean id="dog" class="com.atangbiji.pojo.Dog"/>
<!--按类型自动装配-->
<bean id="user" class="com.atangbiji.pojo.User" autowire="byType">
<property name="name" value="阿汤"/>
</bean>
</beans>

(2)再次运行测试程序test,依赖依旧注入成功。执行结果如下图所示:

自动装配byType

注:

  • 按类型自动装配时,必须要保证:在spring容器(xml配置文件)上下文中一个实体类只对应一个bean对象(即:beanclass属性唯一)。否则,会报Bean不唯一(NoUniqueBeanDefinitionException)异常的错误。
  • 按类型自动装配时,即使没有配置beanid属性,bean同样可以完成自动装配。

8.4、方式二:使用注解实现自动装配

jdk1.5开始支持注解,spring2.5开始全面支持注解。

8.4.1、准备工作

若要通过注解的方式注入属性,则需要:

(1)在xml配置文件的头部引入context约束的文件头。

beans.xml文件:

1
2
3
4
xmlns:context="http://www.springframework.org/schema/context"

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

(2)开启属性注解支持!

beans.xml文件:

1
2
<!--开启属性注解支持-->
<context:annotation-config/>

8.4.2、使用@Autowired注解实现自动装配(重点)

@Autowired注解的执行过程为:

先按类型自动装配的方式查找是否存在class与其注解的实体类成员变量类型相同的bean

  • 若没找到,则直接报错。
  • 若找到多个该类型的bean对象,则再按名称自动装配的方式查找上述结果中是否存在id与其注解的实体类成员变量名称相同的bean;若仍然没找到,则直接报错。

测试步骤:

(1)在User类的bean属性上使用@Autowired注解。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.atangbiji.pojo;

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

//实体类
public class User {
@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;
}

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

(2)简化xml配置文件。

beans.xml文件:

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

<!--开启属性注解支持-->
<context:annotation-config/>

<!--bean就是那些组成我们应用程序的Java对象,由Spring中的IOC容器创建、组装和管理-->
<!--(1)id:bean的唯一标识符。(相当于之前new的对象名)-->
<!--(2)class:bean所对应对象的全限定类名。(全限定名=包名+类名)-->
<!--@Autowired注解按名称自动装配-->
<bean id="cat" class="com.atangbiji.pojo.Cat"/>
<bean id="cat2" class="com.atangbiji.pojo.Cat"/>
<!--@Autowired注解按类型自动装配-->
<bean id="dog2" class="com.atangbiji.pojo.Dog"/>
<bean id="user" class="com.atangbiji.pojo.User"/>
</beans>

(3)再次运行测试程序test,依赖依旧注入成功。执行结果如下图所示:

@Autowired测试结果

注:

  • @Autowired注解既可以在实体类的bean属性(依赖对象)上使用,也可以在实体类bean属性对应的Set方法上使用。

  • 使用@Autowired注解实体类的bean属性时,其对应的Set方法甚至可以省略不写。

  • 默认情况下,@Autowired注解要求依赖对象必须非空。如果要允许依赖对象为null,需要将它的required属性设置为false(默认为:true)。当然,从Spring5.0开始,我们也可以在依赖对象上使用@Nullable标签允许依赖对象为null。如:

    User.java文件:

    1
    2
    3
    4
    5
    6
    7
    //如果允许对象为null,设置required = false,默认为true
    @Autowired(required=false)
    private Cat cat;
    //如果允许对象为null,也可以使用@Nullable标签
    @Autowired
    @Nullable
    private Dog dog;
  • @Autowired注解默认是按类型自动装配的(属于spring规范)。

8.4.3、使用@Autowired和@Qualifier注解实现按名称自动装配

由于@Autowired注解默认是先按类型自动装配的。当一个实体类对应多个bean对象时,若在@Autowired注解后面再加上@Qualifier注解,并在其中指定要装配对象的id,则可实现按名称自动装配(autowire byName)。

注:@Qualifier不能单独使用。

测试步骤:

(1)修改配置文件内容,让一个实体类对应多个bean对象,且id不为实体类对应的属性名。

beans.xml文件:

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

<!--开启属性注解支持-->
<context:annotation-config/>

<!--bean就是那些组成我们应用程序的Java对象,由Spring中的IOC容器创建、组装和管理-->
<!--(1)id:bean的唯一标识符。(相当于之前new的对象名)-->
<!--(2)class:bean所对应对象的全限定类名。(全限定名=包名+类名)-->
<!--@Autowired注解按类型自动装配-->
<bean id="cat1" class="com.atangbiji.pojo.Cat"/>
<bean id="cat2" class="com.atangbiji.pojo.Cat"/>
<bean id="dog1" class="com.atangbiji.pojo.Dog"/>
<bean id="dog2" class="com.atangbiji.pojo.Dog"/>
<bean id="user" class="com.atangbiji.pojo.User"/>
</beans>

(2)若没有@Qualifier测试,则直接报错。

(3)若在User类的bean属性上再加上@Qualifier注解,并在其中指定要装配对象的id,则可实现按名称自动装配。

User.java文件:

1
2
3
4
5
6
@Autowired
@Qualifier(value = "cat1")
private Cat cat;
@Autowired
@Qualifier(value = "dog1")
private Dog dog;

再次运行测试程序test,依赖依旧注入成功。执行结果如下图所示:

@Qualifier测试结果

8.4.4、使用@Resource注解实现自动装配

@Resource注解的执行过程如下:

  • 如果@Resource指定了name属性,那么先根据name属性的值,按名称自动装配的方式查找与之对应的bean;若没找到,则直接报错。
  • 如果@Resource没有指定name属性,那么先按名称自动装配的方式查找是否存在id与其注解的实体类成员变量名称相同的bean;若没找到,则再按类型自动装配的方式查找,若仍然没找到或找到该类型的bean对象不唯一,则直接报错。

注:

  • 同样地,@Resource注解既可以在实体类的bean属性(依赖对象)上使用,也可以在实体类bean属性对应的Set方法上使用。

  • @ResourceJDK原生注解。在JDK8以上的版中,@Resource注解被取消了。此时若要继续使用该注解,则需要重新导入一个javax.annotation的依赖:

    1
    2
    3
    4
    5
    <dependency>
    <groupId>javax.annotation</groupId>
    <artifactId>javax.annotation-api</artifactId>
    <version>1.3.2</version>
    </dependency>

测试步骤:

(1)在User类的bean属性上使用@Resource注解。

User.java文件:

1
2
3
4
@Resource(name = "cat2")
private Cat cat;
@Resource
private Dog dog;

(2)修改配置文件内容。

beans.xml文件:

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

<!--开启属性注解支持-->
<context:annotation-config/>

<!--bean就是那些组成我们应用程序的Java对象,由Spring中的IOC容器创建、组装和管理-->
<!--(1)id:bean的唯一标识符。(相当于之前new的对象名)-->
<!--(2)class:bean所对应对象的全限定类名。(全限定名=包名+类名)-->
<bean id="cat1" class="com.atangbiji.pojo.Cat"/>
<!--@Resource注解按名称自动装配-->
<bean id="cat2" class="com.atangbiji.pojo.Cat"/>
<!--@Resource注解按类型自动装配-->
<bean id="dog2" class="com.atangbiji.pojo.Dog"/>
<bean id="user" class="com.atangbiji.pojo.User"/>
</beans>

(3)再次运行测试程序test,依赖依旧注入成功。执行结果如下图所示:

@Resource测试结果

9、使用注解开发

准备工作:

(1)在spring4之后,想要使用注解开发,必须要确保aop的包已经被导入。

确保导入AOP包

(2)此外,想要使用注解还需要在xml配置文件的头部引入一个context约束。

beans.xml文件:

1
2
3
4
xmlns:context="http://www.springframework.org/schema/context"

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

(3)开启属性注解支持!

beans.xml文件:

1
2
<!--开启属性注解支持-->
<context:annotation-config/>

9.1、搭建测试环境

(1)在父项目中新建一个模块(Module),并在其中创建一个普通的Maven子项目,项目名称为spring-06-Annotation

(2)填写子项目名称和Maven项目GAV,点击Finish按钮完成子项目创建,等待Maven依赖包导入完毕。

(3)在子项目的java目录下新建一个com.atangbiji.pojo包(用于存放我们自己编写的实体类),并在该包下新建一个User实体类。

User.java文件:

1
2
3
4
5
package com.atangbiji.pojo;
//实体类
public class User {
private String name;
}

(4)在子项目的resources目录下,新建一个applicationContext.xml配置文件,用于配置Beans的元数据

applicationContext.xml文件:

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"
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
http://www.springframework.org/schema/context/spring-context.xsd">
<!--开启属性注解支持-->
<context:annotation-config/>
</beans>

(5)在子项目的src/test/java目录下,新建一个MyTest测试类。

9.2、使用@Component注解注入Bean

我们之前都是使用bean的标签进行Bean注入,但是实际开发中,我们一般都会使用注解

(1)指定扫描哪些包下的注解。

applicationContext.xml文件:

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
http://www.springframework.org/schema/context/spring-context.xsd">
<!--开启属性注解支持-->
<context:annotation-config/>
<!--指定扫描哪些包下的注解-->
<context:component-scan base-package="com.atangbiji"/>
</beans>

**注:**配置扫描包是为了扫描类上的@Component注解,如果不配置扫描包,就需要手动配置bean

(2)在实体类上使用@Component(组件)注解。

User.java文件:

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

import org.springframework.stereotype.Component;

//实体类
@Component("user")
//相当于在xml配置文件中配置:<bean id="user" class="当前注解的类"/>
public class User {
private String name = "阿汤";

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

(3)测试。

MyTest.java文件:

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

public class MyTest {
@Test
public void testAnnotation(){
//解析beans.xml文件,IOC容器生成并管理相应的Bean对象
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
//通过Spring的上下文对象,从IOC容器中获取Bean对象
User user = context.getBean("user", User.class);
//调用Bean对象的方法
System.out.println(user.toString());
}
}

运行测试程序testAnnotation,执行结果如下图所示:

@Component测试结果

9.3、使用@Value注解注入属性

(1)实体类可以不用提供set方法,直接在属性名上添加@value("值")注解。

User.java文件:

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

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

//实体类
@Component("user")
//相当于在xml配置文件中配置:<bean id="user" class="当前注解的类"/>
public class User {
@Value("阿汤笔迹")
//相当于在xml配置文件中配置:<property name="name" value="阿汤笔迹"/>
private String name;

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

**注:**如果实体类提供了set方法,那么@value("值")注解同样可以在set方法上添加。

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
package com.atangbiji.pojo;

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

//实体类
@Component("user")
//相当于在xml配置文件中配置:<bean id="user" class="当前注解的类"/>
public class User {

private String name;

@Value("阿汤笔迹")
//相当于在xml配置文件中配置:<property name="name" value="阿汤笔迹"/>
public void setName(String name) {
this.name = name;
}

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

(2)测试。重新运行测试程序testAnnotation,执行结果如下图所示:

@Value测试结果

9.4、@Component的衍生注解

为了更好的进行分层,Spring提供了@Component注解的三个衍生注解,它们的功能和@Component一样:都表示将某一个实体类对应的bean对象装配到Spring IOC容器中,交由Spring管理了。

  • @Controllercontroller
  • @Serviceservice(业务)层
  • @Repository(仓库):dao

9.5、使用注解实现自动装配

Bean的自动装配中已经介绍,详见8.4节。

9.6、使用@scope注解配置Bean的作用域

直接在实体类上使用@scope(作用域)注解即可。

User.java文件:

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

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

//实体类
@Component("user")
@Scope("singleton")
//相当于在xml配置文件中配置:<bean id="user" class="当前注解的类"/>
public class User {
@Value("阿汤笔迹")
//相当于在xml配置文件中配置:<property name="name" value="阿汤笔迹"/>
private String name;

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

注:

  • singleton(单例模式):就是在创建容器时就同时自动创建了一个bean的对象,不管你是否使用,它都已经存在了,且每次获取到的对象都是同一个对象。Spring会采用单例模式创建这个对象。关闭工厂 ,所有的对象都会被销毁。(默认值!)
  • prototype(原型模式):它在我们创建容器的时候并没有实例化,而是当我们获取bean的时候才会去创建一个对象,而且我们每次获取到的对象都不是同一个对象。关闭工厂 ,所有的对象不会销毁,所有对象由内部的垃圾回收机制会回收。

9.7、小结

(1)XML与注解比较

  • xml可以适用任何场景 ,结构清晰,维护方便。
  • 注解只能在被其注解的实体类中使用,开发简单方便,维护相对复杂。

(2)最佳实践

实际项目中,建议:

  • 使用xml配置文件管理Bean
  • 注解只负责完成属性的注入。

10、基于Java的容器配置(重点)

Spring中,Bean除了前面学过的:通过**xml配置文件(或注解)的方式“手动地显式配置”和“隐式地自动装配**”之外,我们还可以通过java代码的方式对其进行显式地配置。

JavaConfig原来是Spring的一个子项目,它通过Java类的方式提供Bean的定义信息。在Spring 4之后的版本,JavaConfig已正式成为Spring的一个核心功能了。

10.1、实现步骤

(1)在父项目中新建一个模块(Module),并在其中创建一个普通的Maven子项目,项目名称为spring-07-AppConfig

(2)填写子项目名称和Maven项目GAV,点击Finish按钮完成子项目创建,等待Maven依赖包导入完毕。

(3)在子项目的java目录下新建一个com.atangbiji.pojo包(用于存放我们自己编写的实体类),并在该包下新建一个User实体类。

  • 通过@Component注解将该实体类标注为Spring的一个组件,放到IOC容器中。

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
27
28
package com.atangbiji.pojo;

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

//实体类
//通过@Component将该实体类标注为Spring的一个组件,放到IOC容器中!
@Component
public class User {
//使用@Value注解注入属性
@Value("阿汤笔迹")
private String name;

public String getName() {
return name;
}

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

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

(4)在子项目的com.atangbiji包下新建一个config(配置)包,并在该包下新建一个AppConfig类,用来配置Beans

  • 先通过@Configuration注解,我们可以将原本普通的AppConfig类配置成一个配置类
  • 然后再通过@ComponentScan注解指定扫描com.atangbiji包下的@Component注解。这样,我们便可以将该包下的所有被@Component注解标记的bean全部注入到IOC容器中了。

注:@ComponentScan注解往往需要和@Component注解在一起搭配使用。如果配置类只配置了@Configuration注解,但没有通过@ComponentScan注解指定扫描包的话,我们也可以通过@Bean注解把配置类的成员函数注册一个bean。此时,该成员函数的返回值就Bean的类型;该成员函数的方法名就是beanid。配置扫描包是为了扫描实体类上的@Component注解,如果不配置扫描包,就需要在配置类中通过@Bean注解手动配置bean。当然,@ComponentScan注解和@Bean注解也可以同时使用

AppConfig.java文件:

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

import com.atangbiji.pojo.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
//通过@Configuration将原本普通的AppConfig类配置成配置类
@Configuration
//通过@ComponentScan指定扫描包下的注解
@ComponentScan("com.atangbiji")
public class AppConfig {
//通过@Bean注解和配置类的成员函数注册一个bean。
//该成员函数的返回值就Bean的类型;该成员函数的方法名就是bean的id。
@Bean
public User user(){
return new User();
}
}

注:

  • 上述配置后的AppConfig类就等价于以前beans.xml配置文件中的:
1
2
3
<beans>
<bean id="user" class="com.atangbiji.pojo.User"/>
</beans>
  • @ComponentScan("com.atangbiji")注解就等价于以前beans.xml配置文件中的:
1
2
<!--指定扫描哪些包下的注解-->
<context:component-scan base-package="com.atangbiji"/>

(5)在子项目的src/test/java目录下,新建一个MyTest测试类。

MyTest.java文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.atangbiji.config.AppConfig;
import com.atangbiji.pojo.User;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MyTest {
@Test
public void test(){
//通过注解解析AppConfig类,IOC容器生成并管理相应的Bean对象
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
//通过Spring的上下文对象,从IOC容器中获取Bean对象
User user = context.getBean("user", User.class);
//调用Bean对象的方法
System.out.println(user.toString());
}
}

**注:**此时,我们不再通过解析beans.xml配置文件的方式获取ApplicationContext对象,而是通过注解解析AppConfig配置类的方式获取ApplicationContext对象。

运行测试程序,依赖注入成功。执行结果如下图所示:

基于Java的容器配置测试结果

10.2、导入多个配置类

同样地,当一个项目中有多个配置类时,我们可以通过@Import注解将多个配置类中的beans导入,合并为一个配置类。如:

(1)在上述子项目的com.atangbiji.config包下再新建一个配置类AppConfig2

AppConfig2.java文件:

1
2
3
4
5
6
7
8
package com.atangbiji.config;

import org.springframework.context.annotation.Configuration;

//通过@Configuration将原本普通的AppConfig2类配置成配置类
@Configuration
public class AppConfig2 {
}

(2)在之前的配置类中通过@Import注解导入其它配置类的class对象即可。

AppConfig.java文件:

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

import com.atangbiji.pojo.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

//通过@Configuration将原本普通的AppConfig类配置成配置类
@Configuration
//通过@ComponentScan指定扫描包下的注解
@ComponentScan("com.atangbiji")
//通过@Import注解导入其它配置类的class对象
@Import(AppConfig2.class)
public class AppConfig {
//通过@Bean注解和配置类的成员函数注册一个bean。
//该成员函数的返回值就Bean的类型;该成员函数的方法名就是bean的id。
@Bean
public User user(){
return new User();
}
}

注:@Import(AppConfig2.class)注解就等价于以前beans.xml配置文件中的:

1
2
3
4
5
6
<beans>
<!--导入其它xml配置文件中的Bean-->
<import resource="beans2.xml"/>
<import resource="resources/beans3.xml"/>
<import resource="/resources/beans4.xml"/>
</beans>

关于这种基于Java的容器配置方式,我们在之后的SpringBootSpringCloud中随处可见,现在我们只需要理解这些注解的作用即可!

11、代理模式

(1)为什么要学习代理模式?

因为AOP的底层机制就是动态代理!因此,学习AOP之前 , 我们要先学习一下代理模式!

(2)什么是代理模式?

代理模式就是:代理对象(如:中介)具备真实对象(如:房东)的功能,并代替真实对象完成相应操作(如:租房相关事宜),并能够在操作执行的前后,对操作进行增强处理。(为真实对象提供代理,然后供其他对象通过代理访问真实对象)

租房问题就是典型的代理模式:租客想找房东租房,但房东比较忙,一般都让中介代理完成租房相关事宜。

代理模式示意图

(3)代理模式的分类:

  • 静态代理
  • 动态代理

11.1、静态代理

11.1.1、静态代理角色分析

  • 抽象角色:一般使用接口或者抽象类来实现。
  • 真实角色:被代理的角色(如:房东)
  • 代理角色:代理真实角色(如:中介);代理真实角色后,一般会做一些附属的操作(如:收取代理费用等)。
  • 客户角色:使用代理角色来进行一些操作的人。(如:租客)

11.1.2、代码实现

(1)在父项目中新建一个模块(Module),并在其中创建一个普通的Maven子项目,项目名称为spring-08-Proxy

(2)填写子项目名称和Maven项目GAV,点击Finish按钮完成子项目创建,等待Maven依赖包导入完毕。

(3)在子项目的java目录下新建一个com.atangbiji.demo01包,并在该包下新建一个Rent接口,作为抽象角色。

Rent.java文件:

1
2
3
4
5
6
package com.atangbiji.demo01;
//抽象角色:租房的接口
public interface Rent {
//租房
public void rent();
}

(4)在子项目的com.atangbiji.demo01包下新建一个Host类,作为真实角色(房东)。

Host.java文件:

1
2
3
4
5
6
7
8
package com.atangbiji.demo01;
//真实角色:房东
public class Host implements Rent{
//房东出租房子
public void rent() {
System.out.println("房东想要出租房子!");
}
}

(5)在子项目的com.atangbiji.demo01包下新建一个Proxy类,作为代理角色(中介)。

Proxy.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
27
28
29
30
31
32
33
package com.atangbiji.demo01;
//代理角色:中介
public class Proxy implements Rent{
//组合被代理的真实角色(这样中介就可以代理房东了)
private Host host;
//无参构造函数
public Proxy() {
}
//有参构造函数
public Proxy(Host host) {
this.host = host;
}
//中介出租房子(一般会做一些附属的操作)
public void rent() {
seeHouse();
//中介帮房东出租房子
host.rent();
sign();
fare();
}
//看房
public void seeHouse(){
System.out.println("中介带你看房!");
}
//签合同
public void sign(){
System.out.println("中介与你签合同!");
}
//收中介费
public void fare(){
System.out.println("中介收取中介费!");
}
}

(6)在子项目的com.atangbiji.demo01包下新建一个Client类,作为客户角色(租客)。

Client.java文件:

1
2
3
4
5
6
7
8
9
10
11
12
package com.atangbiji.demo01;
//客户角色:租客
public class Client {
public static void main(String[] args) {
//房东要出租房子
Host host = new Host();
//新建一个代理类(通过构造函数注入被代理的真实角色)
Proxy proxy = new Proxy(host);
//租客通过中介租房子!
proxy.rent();
}
}

运行主程序,租客通过中介成功租到房东的房子。执行结果如下图所示:

静态代理测试结果

**分析:**在这个过程中,你直接接触的就是中介,就如同现实生活中的样子,你看不到房东,但是通过代理你依旧租到了房东的房子,这就是所谓的代理模式。程序源自于生活,所以学编程的人,一般能够更加抽象的看待生活中发生的事情。

11.1.3、静态代理的优缺点

(1)优点:

  • 可以使得我们的真实角色(如:房东)更加纯粹,不用再去关注一些公共的事情。
  • 公共的业务由代理来完成,进而实现了业务的分工。
  • 公共业务发生扩展时,更加方便,程序的耦合性更低。

(2)缺点:

  • 一个真实角色就会产生一个代理角色;如果真实角色太多,那么代码量将会变得非常大,进而导致开发效率降低。

**注:**我们想要静态代理的优点,又不想要静态代理的缺点,所以 , 就有了动态代理 !

11.1.4、静态代理再理解

为了加深对静态代理的理解,我们结合业务再来举一个例子。

(1)在上述子项目的java目录下新建一个com.atangbiji.demo02包,并在该包下新建一个UserService接口,作为抽象角色。

(如咱们平时做的用户业务,抽象出来就是增删改查!)

UserService.java文件:

1
2
3
4
5
6
7
8
package com.atangbiji.demo02;
//抽象角色:增删改查业务接口
public interface UserService {
public void add();
public void delete();
public void update();
public void query();
}

(2)在子项目的com.atangbiji.demo02包下新建一个UserServiceImpl类,作为真实角色,去实现这些增删改查操作。

UserServiceImpl.java文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.atangbiji.demo02;
//真实角色:去实现增删改查接口
public class UserServiceImpl implements UserService {
public void add() {
System.out.println("增加了一个用户!");
}

public void delete() {
System.out.println("删除了一个用户!");
}

public void update() {
System.out.println("更新了一个用户!");
}

public void query() {
System.out.println("查询了一个用户!");
}
}

那么,现在需求来了:我们需要增加删改查操作的日志功能,如何实现呢?

  • 思路1 :直接在接口的实现类(真实角色)中增加该功能 【改变了原来的代码,程序的耦合性高!】
  • 思路2:使用代理来做,能够在不改变原来的代码情况下,增加该功能【程序的耦合性低!】。具体过程如下:

(3)在子项目的com.atangbiji.demo02包下新建一个代理类UserServiceProxy ,作为代理角色,去实现增删改查操作中的日志功能。

UserServiceProxy.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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.atangbiji.demo02;
//代理角色:中介
public class UserServiceProxy implements UserService {
//组合被代理的真实角色(这样中介就可以代理真实角色了)
private UserServiceImpl userServiceImpl;
//通过Set注入UserServiceImpl对象
public void setUserServiceImpl(UserServiceImpl userServiceImpl) {
this.userServiceImpl = userServiceImpl;
}

public void add() {
//调用真实角色对应的方法
userServiceImpl.add();
//输出相应的日志
log("add");
}

public void delete() {
//调用真实角色对应的方法
userServiceImpl.delete();
//输出相应的日志
log("delete");
}

public void update() {
//调用真实角色对应的方法
userServiceImpl.update();
//输出相应的日志
log("update");
}

public void query() {
//调用真实角色对应的方法
userServiceImpl.query();
//输出相应的日志
log("query");
}

//通过代理实现输出日志的功能
public void log(String msg){
System.out.println("执行了" + msg + "方法!");
}
}

(4)在子项目的com.atangbiji.demo02包下新建一个Client类,作为客户角色。

Client.java文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.atangbiji.demo02;
//客户角色
public class Client {
public static void main(String[] args) {
//新建一个代理类
UserServiceProxy proxy = new UserServiceProxy();
//通过Set方式注入被代理的真实角色
proxy.setUserServiceImpl(new UserServiceImpl());
//通过代理实现类实现日志功能!
proxy.add();
proxy.delete();
proxy.update();
proxy.query();
}
}

运行主程序,通过代理实现类实现日志功能。执行结果如下图所示:

静态代理再理解测试结果

上述示例中,我们在不改变原来的代码的情况下,实现了对原有功能的增强。——这是AOP中最核心的思想。

11.2、动态代理(重点)

  • 动态代理的角色和静态代理的一样,也是:抽象角色、真实角色、代理角色和客户角色。
  • 动态代理的代理类是动态生成的;静态代理的代理类是我们提前写好的。

11.2.1、动态代理的分类

动态代理分为两大类:一类是基于接口的动态代理 , 一类是基于类的动态代理。

  • 基于接口的动态代理,例如:JDK原生的动态代理。
  • 基于类的动态代理,例如:CGLib (Code Generation Library) :一个强大的,高性能,高质量的Code生成类库。
  • 现在用的比较多的是Javassist(一个开源的分析、编辑和创建Java字节码的类库)来生成动态代理。
  • 我们这里使用JDK的原生代码来实现,其余的道理都是一样的。

11.2.2、JDK原生的动态代理

JDK中的动态代理指的是:Java通过动态生成的代理类(代理角色)代替被代理类(真实角色)处理业务的过程。

想要分析JDK的动态代理的实现过程,需要了解:Proxy类和InvocationHandler(调用处理程序)接口。

(1)通过Proxy类创建动态代理类

  • Proxy类是java.lang.reflect(反射包)下的一个类。
  • Proxy类提供了一系列创建动态代理类和实例静态方法,我们可以通过Proxy类动态生成代理类。
  • Proxy类是通过这些方法所创建的所有动态代理类的父类。
  • 每一个(动态生成的)动态代理类都有一个与之关联的调用处理程序(即:InvocationHandler接口实现类的对象)。当然,该InvocationHandler接口实现类也可以是动态代理类本身。

注:Proxy类中的newProxyInstance静态方法是动态生成代理类的核心方法。

1
2
3
4
5
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h){
……
}

该方法的输入参数为:

  • loader:(动态生成的)动态代理类的类加载器。
  • interfaces:(动态生成的)动态代理类所实现的接口列表,即:要代理的抽象角色。
  • h:指派(动态生成的)动态代理类对应的调用处理程序(即:InvocationHandler 接口实现类的对象)。

返回值为:指定接口(即:抽象角色)的动态代理类的实例。

(2)通过调用处理程序的invoke方法使用动态代理实例处理业务

InvocationHandlerjava.lang.reflect(反射包)下的一个接口,且该接口中只有一个invoke方法。

InvocationHandler.java文件:

1
2
3
4
5
6
package java.lang.reflect;

public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
  • 每一个(动态)代理实例都有一个关联的调用处理程序InvocationHandler接口实现类)。InvocationHandler 就是该(动态)代理实例所对应的调用处理程序需要实现的接口。

  • 动态代理所对应的InvocationHandler接口实现类可以是动态代理类本身。

  • **动态代理代替真实对象处理业务的底层是通过调用与之对应的InvocationHandler接口实现类的invoke方法实现的。**即:当我们想通过(动态)代理实例调用其中的一个方法时,将该方法调用将会被编码,并将其交给与之(即:该代理实例)对应的调用处理程序(InvocationHandler接口实现类)的invoke方法去处理。

  • 动态代理实例本身不执行代理方法,而是交由其调用处理程序的invoke方法去执行。

注:invoke方法的作用是:处理(动态)代理实例的方法调用(即:调用XXX代理实例的XXX方法),并返回结果

1
2
//调用处理函数:调用XXX代理实例的XXX方法并返回结果
public Object invoke(Object proxy, Method method, Object[] args)

该方法的输入参数为:

  • proxy:被调用方法对应的(动态)代理实例(即:你要用哪个代理?)
  • method:(动态)代理实例要调用的方法(即:你要调用代理的哪个方法?)
  • args:(动态)代理实例要调用方法的参数列表。(即:你通过代理要调用的方法有哪些参数?)

返回值为:代理实例调用相应方法的返回值。

11.2.3、代码实现

(1)在上述子项目的java目录下新建一个com.atangbiji.demo03包,并在该包下新建一个Rent接口,作为抽象角色。

Rent.java文件:

1
2
3
4
5
6
package com.atangbiji.demo03;
//抽象角色:租房的接口
public interface Rent {
//租房
public void rent();
}

(2)在子项目的com.atangbiji.demo03包下新建一个Host类,作为真实角色(房东)。

Host.java文件:

1
2
3
4
5
6
7
8
9
package com.atangbiji.demo03;

//真实角色:房东
public class Host implements Rent {
//房东出租房子
public void rent() {
System.out.println("房东想要出租房子!");
}
}

(3)在子项目的com.atangbiji.demo03包下新建一个ProxyInvocationHandler类,用于动态生成代理类,即:代理角色(中介)。

ProxyInvocationHandler.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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.atangbiji.demo03;

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

//代理角色(中介)的调用处理程序
public class ProxyInvocationHandler implements InvocationHandler {
//真实角色被代理的接口(这样中介就可以代理房东了)
private Rent rent;

public void setRent(Rent rent) {
this.rent = rent;
}

//获取动态生成的代理类
public Object getProxy(){
return Proxy.newProxyInstance(this.getClass().getClassLoader(), rent.getClass().getInterfaces(),this);
}
//处理(动态)代理实例的方法调用(即:调用XXX代理实例的XXX方法),并返回结果
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
seeHouse();
//核心:通过反射实现代理对象方法的调用
Object result = method.invoke(rent, args);
sign();
fare();
return result;
}
//看房
public void seeHouse(){
System.out.println("中介带你看房!");
}
//签合同
public void sign(){
System.out.println("中介与你签合同!");
}
//收中介费
public void fare(){
System.out.println("中介收取中介费!");
}
}

**注:**动态代理类本身也可以实现InvocationHandler接口,作为它自己的调用处理程序。

(4)在子项目的com.atangbiji.demo03包下新建一个Client类,作为客户角色(租客)。

Client.java文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.atangbiji.demo03;
//客户角色:租客
public class Client {
public static void main(String[] args) {
//真实角色
Host host = new Host();
//代理角色的调用处理程序
ProxyInvocationHandler proxyInvocationHandler = new ProxyInvocationHandler();
//设置要代理的真实角色
proxyInvocationHandler.setRent(host);
//动态生成代理类的实例
Rent proxy = (Rent)proxyInvocationHandler.getProxy();
//通过代理类实例实现租房功能!
proxy.rent();
}
}

运行主程序,租客通过中介(动态代理)成功租到房东的房子。执行结果如下图所示:

动态代理测试结果

11.2.4、动态代理再理解

demo03示例的基础上,我们只需将上述代理角色的调用处理程序中的Rent代理接口修改为Object,便可将其封装成一个通用的动态代理实现类。

ProxyInvocationHandler.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
27
28
29
30
31
32
package com.atangbiji.demo03;

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

//代理角色的调用处理程序
public class ProxyInvocationHandler implements InvocationHandler {
//真实角色被代理的接口
private Object target;

public void setTarget(Object target) {
this.target = target;
}

//获取动态生成的代理类
public Object getProxy(){
return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(),this);
}
//处理(动态)代理实例的方法调用(即:调用XXX代理实例的XXX方法),并返回结果
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//核心:通过反射实现代理对象方法的调用
Object result = method.invoke(target, args);
//通过反射获取方法名,并输出日志
log(method.getName());
return result;
}
//通过代理实现输出日志的功能
public void log(String msg){
System.out.println("执行了" + msg + "方法!");
}
}

**测试:**我们使用动态代理来代理demo02示例中的真实角色。

Client.java文件:

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.atangbiji.demo03;

import com.atangbiji.demo02.UserService;
import com.atangbiji.demo02.UserServiceImpl;

//客户角色
public class Client {
public static void main(String[] args) {
//真实角色
UserServiceImpl userService = new UserServiceImpl();
//代理角色的调用处理程序
ProxyInvocationHandler proxyInvocationHandler = new ProxyInvocationHandler();
//设置要代理的真实角色
proxyInvocationHandler.setTarget(userService);
//动态生成代理类的实例
UserService proxy = (UserService)proxyInvocationHandler.getProxy();
//通过代理类实例实现日志功能!
proxy.add();
proxy.delete();
proxy.update();
proxy.query();
}
}

重新运行主程序,执行结果如下图所示:

动态代理封装测试结果

11.2.5、动态代理的好处

静态代理有的它都有,静态代理没有的,它也有!即:

  • 可以使得我们的真实角色(如:房东)更加纯粹,不用再去关注一些公共的事情。
  • 公共的业务由代理来完成,进而实现了业务的分工。
  • 公共业务发生扩展时,更加方便,程序的耦合性更低。
  • 由于动态代理代理的是真实角色的接口(即:抽象角色),因此当多个真实角色都实现了同一个抽象角色时,一个动态代理便可以代理所有这些真实角色。

12、AOP

12.1、什么是AOP

AOPAspect Oriented Programming)意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护(即:在不修改源代码的情况下,给程序动态统一添加某种特定功能)的一种技术。AOPOOP(面向对象)的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

12.2、AOP的本质(重点)

IOC一样,AOP的本质还是为了程序解耦

AOP中最核心的思想就是:在不改变原来的代码的情况下,实现对原有功能的增强。

AOP的实现过程说白了就是:在原先**“纵向开发”的项目的基础上,“横向切入”添加新的功能。**

AOP的实现过程类似于给病人做一个外接手术,即:把病人(原来的代s码)的伤口(切入点)从四周(通知)切开,外接一个人造器官(切面)后再把伤口(连接点)缝(织入)起来。

12.3、AOP的术语

  • 横切关注点:跨越应用程序多个模块的方法或功能。即:与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点。如:日志、安全、缓存、事务等。
  • 切面Aspect):横切关注点被模块化的特殊对象,它是一个类。如:日志类等。
  • 通知Advice):切面必须要完成的工作,它是类中的一个方法。如:日志类中的方法。
  • 引入Introduction):允许开发者在现有的类中添加自定义的类和方法,以增强现有类的功能。
  • 切入点PointCut):切面通知执行的位置。
  • 连接点JointPoint):与切入点匹配的执行点。
  • 目标对象Target):被通知的对象。即:动态代理中的抽象角色。(Spring底层已经通过动态代理帮我们实现了!)
  • 代理Proxy):向目标对象应用通知之后创建的对象。即:动态生成的代理实例。(Spring底层已经通过动态代理帮我们实现了!)
  • 织入Weaving):是一个生成代理对象的过程。
AOP示意图

**注:**在Spring AOP中,通过Advice(通知)定义横切逻辑。Spring中支持5种类型的Advice(通知):

通知类型

12.4、Spring对AOP的支持

Spring的底层已经使用动态代理帮助我们实现了对AOP的支持。

Spring中AOP代理由Spring的IOC容器负责生成、管理,其依赖关系也由IOC容器负责管理。因此,AOP代理可以直接使用容器中的其它bean实例作为目标对象,这种关系可由IOC容器的依赖注入提供。

Spring创建动态代理的规则为:

(1)默认使用Java动态代理来创建AOP代理,这样就可以为任何接口实例创建动态代理了。

(2)当需要代理的类不是代理接口的时候,Spring会切换为使用CGLIB代理,也可强制使用CGLIB。

12.5、使用Spring实现AOP

需要先在父项目的pom.xml文件中导入AOP织入依赖包!

pom.xml文件:

1
2
3
4
5
6
<!-- AOP织入依赖 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>

12.5.1、方式一:通过Spring API实现AOP(早期)

(1)在父项目中新建一个模块(Module),并在其中创建一个普通的Maven子项目,项目名称为spring-09-AOP

(2)填写子项目名称和Maven项目GAV,点击Finish按钮完成子项目创建,等待Maven依赖包导入完毕。

(3)在子项目的java目录下新建一个com.atangbiji.service包,并在该包下新建一个UserService接口及其接口实现类UserServiceImpl

UserService.java文件:

1
2
3
4
5
6
7
8
package com.atangbiji.service;
//抽象角色:增删改查业务接口
public interface UserService {
public void add();
public void delete();
public void update();
public void select();
}

UserServiceImpl.java文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.atangbiji.service;
//真实角色:去实现增删改查接口
public class UserServiceImpl implements UserService {

public void add() {
System.out.println("增加了一个用户!");
}

public void delete() {
System.out.println("删除了一个用户!");
}

public void update() {
System.out.println("更新了一个用户!");
}

public void select() {
System.out.println("查询了一个用户!");
}
}

(4)在子项目的java目录下新建一个com.atangbiji.log包,并在该包下新建两个增强类:一个前置增强(Log类);一个后置增强(AfterLog类)。

Log.java文件:

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

import org.springframework.aop.MethodBeforeAdvice;

import java.lang.reflect.Method;
//日志类(即:切面)
//通知类型:前置通知
public class Log implements MethodBeforeAdvice {
// 输入参数:
//(1)method:要执行的目标对象的方法
//(2)args:被调用方法的参数
//(3)target:目标对象
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println(target.getClass().getName() + "类的" + method.getName() + "方法被执行了");
}
}

AfterLog.java文件:

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

import org.springframework.aop.AfterReturningAdvice;
import java.lang.reflect.Method;

//日志类(即:切面)
//通知类型:后置通知
public class AfterLog implements AfterReturningAdvice {
// 输入参数:
//(1)returnValue:返回值
//(2)method:要执行的目标对象的方法
//(3)args:被调用方法的参数
//(4)target:目标对象
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("执行了" + target.getClass().getName() + "类的" + method.getName() + "方法,返回值为:" + returnValue);
}
}

(5)在子项目的resource目录下新建一个applicationContext.xml配置文件,用于注册bean和配置AOP

applicationContext.xml文件:

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
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--bean就是那些组成我们应用程序的Java对象,由Spring中的IOC容器创建、组装和管理-->
<!--(1)id:bean的唯一标识符。(相当于之前new的对象名)-->
<!--(2)class:bean所对应对象的全限定类名。(全限定名=包名+类名)-->

<!--方式一:通过Spring API实现AOP-->
<!--注册bean(切面及通知类型)-->
<bean id="userServiceImpl" class="com.atangbiji.service.UserServiceImpl"/>
<bean id="log" class="com.atangbiji.log.Log"/>
<bean id="afterLog" class="com.atangbiji.log.AfterLog"/>

<!--配置AOP-->
<aop:config>
<!--切入点(通过expression:表达式匹配要切入的位置)-->
<aop:pointcut id="pointCut" expression="execution(* com.atangbiji.service.UserServiceImpl.*(..))"/>

<!--通知(把log类和afterLog类从pointCut切入点织入)-->
<!--(1)advice-ref:通知的引用;(2)pointcut-ref:切入点的引用-->
<aop:advisor advice-ref="log" pointcut-ref="pointCut"/>
<aop:advisor advice-ref="afterLog" pointcut-ref="pointCut"/>
</aop:config>
</beans>

**注:**注意在xml配置文件的头部导入AOP的约束。

1
2
3
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">

附:expression(表达式)的语法格式为:

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

其中:(以问号结束的部分都是可以省略

  • modifiers-pattern表示:方法的访问类型,如public等;

  • ret-type-pattern表示:方法的返回值类型,如String表示返回类型是String*表示所有的返回类型;

  • declaring-type-pattern表示:方法的声明类,如com.atangbiji..*表示com.atangbiji包及其子包下面的所有类型;

  • name-pattern表示:方法的名称,如add*表示所有以add开头的方法名;

  • param-pattern表示:方法的参数类型name-pattern(param-pattern)一起表示方法及对应的参数类型,如add()表示不带参数的add方法,add(*)表示带一个任意类型的参数的add方法,add(*,String)则表示带两个参数,且第二个参数是String类型的add方法;

  • throws-pattern表示:异常类型

(6)在子项目的src/test/java目录下,新建一个MyTest测试类。

MyTest.java文件:

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

public class MyTest {
@Test
public void test(){
//解析applicationContext.xml文件,IOC容器生成并管理相应的Bean对象
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
//通过Spring的上下文对象,从IOC容器中获取Bean对象
//注:由于动态代理代理的是接口,因此需要将UserServiceImpl类型强制转换为UserService类型
UserService userServiceImpl = (UserService)context.getBean("userServiceImpl");
//调用Bean对象的方法
userServiceImpl.add();
userServiceImpl.delete();
userServiceImpl.update();
userServiceImpl.select();
}
}

运行测试程序test,切面织入成功。执行结果如下图所示:

通过Spring API实现AOP测试结果

**注:**由于动态代理代理的是接口(抽象角色),因此需要将UserServiceImpl类型强制转换为UserService类型。

12.5.2、方式二:通过自定义类实现AOP(推荐)

(1)保持spring-09-AOP子项目com.atangbiji.service包下的UserService接口及其接口实现类UserServiceImpl不变。

注:UserService.java文件与UserServiceImpl.java文件参见方式一。

(2)在spring-09-AOP子项目的java目录下新建一个com.atangbiji.diy包,并在该包下新建一个DiyAspect类,用于自定义切面。

DiyAspect.java文件:

1
2
3
4
5
6
7
8
9
10
11
12
package com.atangbiji.diy;
//自定义切面
public class DiyAspect {
//通知1
public void before(){
System.out.println("---------方法执行前---------");
}
//通知2
public void after(){
System.out.println("---------方法执行后---------");
}
}

(3)在子项目resource目录下的applicationContext.xml配置文件中注册bean,并配置AOP

applicationContext.xml文件:

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
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--bean就是那些组成我们应用程序的Java对象,由Spring中的IOC容器创建、组装和管理-->
<!--(1)id:bean的唯一标识符。(相当于之前new的对象名)-->
<!--(2)class:bean所对应对象的全限定类名。(全限定名=包名+类名)-->

<!--方式二:通过自定义类实现AOP-->
<!--注册bean(切面及通知类型)-->
<bean id="userServiceImpl" class="com.atangbiji.service.UserServiceImpl"/>
<bean id="diy" class="com.atangbiji.diy.DiyAspect"/>

<!--配置AOP-->
<aop:config>
<!--自定义切面的引用-->
<aop:aspect ref="diy">
<!--切入点-->
<aop:pointcut id="diyPointcut" expression="execution(* com.atangbiji.service.UserServiceImpl.*(..))"/>
<!--通知类型(把diy类从diyPointcut切入点织入)-->
<!--(1)method:通知的引用;(2)pointcut-ref:切入点的引用-->
<aop:before method="before" pointcut-ref="diyPointcut"/>
<aop:after method="after" pointcut-ref="diyPointcut"/>
</aop:aspect>
</aop:config>
</beans>

(4)保持子项目的src/test/java目录下的MyTest测试类不变,重新运行测试程序test,切面同样织入成功。执行结果如下图所示:

通过自定义类实现AOP测试结果

12.5.3、方式三:通过注解实现AOP(主流)

(1)保持spring-09-AOP子项目com.atangbiji.service包下的UserService接口及其接口实现类UserServiceImpl不变。

注:UserService.java文件与UserServiceImpl.java文件参见方式一。

(2)在spring-09-AOP子项目的java目录下新建一个com.atangbiji.annotation包,并在该包下新建一个AnnotationAspect类,用于自定义切面。

AnnotationAspect.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
27
28
29
30
31
32
package com.atangbiji.annotation;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

//使用注解配置自定义切面
@Aspect
public class AnnotationAspect {
//使用注解配置(前置)通知。语法格式:@Before("切入点")
@Before("execution(* com.atangbiji.service.UserServiceImpl.*(..))")
public void before(){
System.out.println("===========方法执行前=========");
}

//使用注解配置(后置)通知。语法格式:@After("切入点")
@After("execution(* com.atangbiji.service.UserServiceImpl.*(..))")
public void after(){
System.out.println("===========方法执行后=========");
}

//使用注解配置(环绕)通知。语法格式:@Around("切入点")
@Around("execution(* com.atangbiji.service.UserServiceImpl.*(..))")
public void around(ProceedingJoinPoint jp) throws Throwable {
System.out.println("------------环绕前-----------");
//执行目标方法
jp.proceed();
System.out.println("------------环绕后-----------");
}
}

**注:**在环绕增强(通知)中,我们可以传入一个连接点参数,代表我们要获取处理切入的点。

(3)在子项目resource目录下的applicationContext.xml配置文件中注册bean,并开启注解支持。

applicationContext.xml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--bean就是那些组成我们应用程序的Java对象,由Spring中的IOC容器创建、组装和管理-->
<!--(1)id:bean的唯一标识符。(相当于之前new的对象名)-->
<!--(2)class:bean所对应对象的全限定类名。(全限定名=包名+类名)-->

<!--方式三:通过注解实现AOP-->
<!--注册bean(切面及通知类型)-->
<bean id="userServiceImpl" class="com.atangbiji.service.UserServiceImpl"/>
<bean id="annotationAspect" class="com.atangbiji.annotation.AnnotationAspect"/>
<!--开启注解支持-->
<aop:aspectj-autoproxy/>
</beans>

**注:**我们也可以通过注解方式注册bean,为了方便我们理解通过注解实现AOP,这里我们仍然使用xml配置文件的方式注册bean

(4)保持子项目的src/test/java目录下的MyTest测试类不变,重新运行测试程序test,切面同样织入成功。执行结果如下图所示:

通过注解实现AOP测试结果

附:aop:aspectj-autoproxy(开启注解支持)说明

  • 通过aop命名空间的<aop:aspectj-autoproxy />声明自动为spring容器中那些配置@aspectJ切面的bean创建代理,织入切面。当然,spring 在内部依旧采用AnnotationAwareAspectJAutoProxyCreator进行自动代理的创建工作,但具体实现的细节已经被<aop:aspectj-autoproxy />隐藏起来了。

  • <aop:aspectj-autoproxy />有一个proxy-target-class属性,默认为false,表示使用jdk动态代理织入增强,当配为<aop:aspectj-autoproxy poxy-target-class="true"/>时,表示使用CGLib动态代理技术织入增强。不过即使proxy-target-class设置为false,如果目标类没有声明接口,则spring将自动使用CGLib动态代理。

13、整合MyBatis

13.1、回顾MyBatis

(1)在父项目中新建一个模块(Module),并在其中创建一个普通的Maven子项目,项目名称为spring-10-MyBatis

(2)填写子项目名称和Maven项目GAV,点击Finish按钮完成子项目创建,等待Maven依赖包导入完毕。

(3)在父项目的pom.xml文件中导入父/子工程需要的Maven依赖。

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
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
<!-- 导入Maven依赖 -->
<dependencies>
<!-- MySQL驱动依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!-- MyBatis依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.2</version>
</dependency>
<!-- Spring依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
<!-- Spring-JDBC依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.1.10.RELEASE</version>
</dependency>
<!-- Spring整合MyBatis依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.2</version>
</dependency>
<!-- junit依赖 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!-- AOP织入依赖 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
<!--lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
</dependencies>

(4)在Maven子项目的src/main/resources目录下新建一个名称为mybatis-config.xml 的MyBatis核心配置文件。

mybatis-config.xml 文件:

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
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<!--MyBatis核心配置文件-->
<configuration>
<!--将包内的实体类全部设置别名-->
<typeAliases>
<package name="com.atangbiji.pojo"/>
</typeAliases>
<!--环境配置-->
<environments default="development">
<!--开发环境-->
<environment id="development">
<!--事务管理器-->
<transactionManager type="JDBC"/>
<!--数据源-->
<dataSource type="POOLED">
<!--驱动(固定写法)-->
<property name="driver" value="com.mysql.jdbc.Driver"/>
<!--连接信息(URL、用户名和密码)-->
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?useUnicode=true&amp;characterEncoding=utf8&amp;useSSL=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<!-- 将包内的映射器接口全部注册为映射器 -->
<mappers>
<package name="com.atangbiji.dao"/>
</mappers>
</configuration>

(5)在子项目的java目录下新建一个com.atangbiji.utils包(用于存放我们自己封装的MyBatis工具类),并在该包下新建一个MyBatisUtils类,用于获取SqlSession对象。

MyBatisUtils.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
27
28
29
30
31
32
33
package com.atangbiji.utils;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
//MyBatis工具类
public class MyBatisUtils {
private static SqlSessionFactory sqlSessionFactory;
static {
try {
//从XML中构建SqlSessionFactory对象
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}

//从SqlSessionFactory中获取SqlSession对象
public static SqlSession getSqlSession(){
return sqlSessionFactory.openSession();
}

//重载getSqlSession方法
//若autoCommit为true:则事务自动提交;若autoCommit为false:则需要手动提交事务。
public static SqlSession getSqlSession(boolean autoCommit){
return sqlSessionFactory.openSession(autoCommit);
}
}

(6)在子项目的java目录下新建一个com.atangbiji.pojo包(用于存放我们自己编写的实体类(pojo)),并在该包下新建一个User类,用于实现关系型数据库和业务实体间的映射。

User.java文件:

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

import lombok.Data;
//实体类
@Data
public class User {
private int id;
private String name;
private String pwd;
}

(7)在子项目的java目录下新建一个com.atangbiji.dao包(用于存放我们自己编写的持久层代码),并在该包下新建一个UserMapper映射器接口。

UserMapper.java文件:

1
2
3
4
5
6
7
8
9
package com.atangbiji.dao;

import com.atangbiji.pojo.User;
import java.util.List;
//映射器接口
public interface UserMapper {
//查询全部用户
public List<User> selectUser();
}

(8)在该子项目的com.atangbiji.dao包下新建一个映射器配置文件UserMapper.xml,并在其中使用标签实现SQL语句与映射器接口函数的映射。

UserMapper.xml文件:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--命名空间(绑定对应的映射器接口)-->
<mapper namespace="com.atangbiji.dao.UserMapper">
<!--查询全部用户-->
<select id="selectUser" resultType="user">
select * from mybatis.user
</select>
</mapper>

(9)在子项目的src/test/java目录下,新建一个MyTest,用于MyBatis功能测试。

MyTest.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
import com.atangbiji.dao.UserMapper;
import com.atangbiji.pojo.User;
import com.atangbiji.utils.MyBatisUtils;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;

import java.util.List;

public class MyTest {
@Test
public void test(){
//1、从SqlSessionFactory中获取SqlSession对象
try(SqlSession sqlSession = MyBatisUtils.getSqlSession()){
//2、执行SQL语句
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = mapper.selectUser();

for (User user : userList) {
//3、输出查询结果
System.out.println(user);
}
}
}
}

**注:**由于Maven的“约定大于配置”思想,我们的映射器配置文件*.xml写在了项目中的Java目录下,因此出现无法被导出或者无法生效的问题。该问题的解决方法是:在父项目(或子项目)的pom.xml<build>……</build>中配置resources,修改Maven约定的过滤条件,来防止我们的资源导出失败。

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
<!--在build中配置resources,来防止我们资源导出失败的问题-->
<build>
<resources>
<!--选择需要导出资源的路径,可以选择多个-->
<resource>
<directory>src/main/resources</directory>
<!--写入需要导出的什么类型的文件-->
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<!--不去过滤这些文件-->
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<!--不去过滤这些文件-->
<filtering>false</filtering>
</resource>
</resources>
</build>

重新加载Maven项目,运行测试程序,执行结果如下图所示:

Mybatis回顾测试结果

13.2、MyBatis-Spring介绍

13.2.1、什么是MyBatis-Spring?

MyBatis-Spring是帮助我们将MyBatis代码无缝地整合到Spring中的工具包。

MyBatis-Spring允许MyBatis参与到Spring的事务管理之中,创建映射器mapperSqlSession 并注入到bean中,以及将Mybatis的异常转换为SpringDataAccessException。 最终,可以做到应用代码不依赖于MyBatisSpringMyBatis-Spring

13.2.2、MyBatis-Spring的使用

在选择MyBatis-Spring的版本之前,我们需要先明确它与SpringMyBatis版本之间的对应关系。如下图所示:

MyBatis-Spring版本

如果要使用MyBatis-Spring,仅需要在项目的pom.xml文件中加入Spring整合MyBatis的依赖即可:

1
2
3
4
5
6
<!-- Spring整合MyBatis依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.2</version>
</dependency>

13.3、方式1:使用sqlSessionTemplate整合

具体整合步骤如下:

(1)引入Spring配置文件

spring-10-MyBatis子项目的resources目录下新建一个spring-dao.xml配置文件,用于配置与MyBatis相关Beans的元数据

spring-dao.xml文件:

1
2
3
4
5
6
7
8
9
<?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就是那些组成我们应用程序的Java对象,由Spring中的IOC容器创建、组装和管理-->
<!--(1)id:bean的唯一标识符。(相当于之前new的对象名)-->
<!--(2)class:bean所对应对象的全限定类名。(全限定名=包名+类名)-->
</beans>

**注:**我们可以使用import标签,将spring-dao.xml配置文件导入到我们的Spring主配置文件(applicationContext.xml)中去。

applicationContext.xml文件:

1
2
3
4
5
6
7
<beans>
<!--导入spring-dao.xml配置文件中的Bean-->
<import resource="spring-dao.xml"/>
<!--当前xml配置文件中的Bean-->
<bean id="bean1" class="..."/>
<bean id="bean2" class="..."/>
</beans>

(2)配置数据源

spring-dao.xml文件中使用Spring的数据源替换MyBatis核心配置文件(mybatis-config.xml)中的数据源配置。

spring-dao.xml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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就是那些组成我们应用程序的Java对象,由Spring中的IOC容器创建、组装和管理-->
<!--(1)id:bean的唯一标识符。(相当于之前new的对象名)-->
<!--(2)class:bean所对应对象的全限定类名。(全限定名=包名+类名)-->

<!--配置数据源:使用Spring的数据源替换MyBatis(核心配置文件中)的数据源配置-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<!--驱动(固定写法)-->
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<!--连接信息(URL、用户名和密码)-->
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?useUnicode=true&amp;characterEncoding=utf8&amp;useSSL=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
</beans>

**注:**数据源的种类有非常多,可以使用第三方的,也可使使用Spring的,我们这里使用Spring的提供的JDBC

(3)配置SqlSessionFactory,关联MyBatis

MyBatis-Spring中,可使用SqlSessionFactoryBean来创建 SqlSessionFactory。 要配置这个工厂bean,只需要把下面代码放在SpringXML配置文件中。

spring-dao.xml文件:

1
2
3
4
5
6
7
<!--配置SqlSessionFactory,关联MyBatis-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<!--绑定MyBatis配置文件,关联Mybatis-->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<property name="mapperLocations" value="classpath:com/atangbiji/dao/*.xml"/>
</bean>

注:

  • SqlSessionFactoryBean中绑定了MyBatis配置文件后,我们便可以将两者关联起来,一起配合使用了。
  • SqlSessionFactoryBean中配置了mapperLocations属性后,我们就不用再在MyBatis核心配置文件(mybatis-config.xml)中通过mappers标签注册所有的映射器了。

(4)注册sqlSessionTemplate,关联sqlSessionFactory(关键)

MyBatis中,我们使用 SqlSessionFactory 来创建 SqlSession。 一旦你获得一个session之后,我们可以使用它来执行映射了的SQL语句,提交或回滚连接,最后,当不再需要它的时候,我们需要关闭session

使用MyBatis-Spring之后,我们不需要再从SqlSessionFactory中获取SqlSession对象了,因为我们的bean可以被注入一个线程安全的 SqlSession(即:SqlSessionTemplate),它能基于Spring的事务配置来自动提交、回滚、关闭session

spring-dao.xml文件:

1
2
3
4
5
<!--注册sqlSessionTemplate,关联sqlSessionFactory-->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<!--通过有参构造方法注入sqlSessionFactory-->
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>

注:

  • SqlSessionTemplateSqlSession 的一个实现,使用它可以无缝代替我们之前代码中已经在使用的 SqlSession
  • SqlSessionTemplateMyBatis-Spring的核心。
  • SqlSessionTemplate 是线程安全的,可以被多个DAO或映射器所共享使用。
  • 我们可以使用 SqlSessionFactory 作为有参构造方法的参数来创建 SqlSessionTemplate 对象。

(5)增加Dao接口的实现类,私有化sqlSessionTemplate(关键)

在该子项目的com.atangbiji.dao包下新建一个映射器配置文件UserMapperImpl类,用于实现UserMapper接口;并在其中添加一个 SqlSessionTemplate(私有)属性及其set方法。

UserMapperImpl.java文件:

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

import com.atangbiji.pojo.User;
import org.mybatis.spring.SqlSessionTemplate;
import java.util.List;

//映射器接口实现类
public class UserMapperImpl implements UserMapper {
//sqlSession(SqlSessionTemplate对象)不需要我们自己创建了,它由Spring来管理
private SqlSessionTemplate sqlSession;

public void setSqlSession(SqlSessionTemplate sqlSession) {
this.sqlSession = sqlSession;
}

@Override
public List<User> selectUser() {
//使用sqlSession(SqlSessionTemplate对象)执行SQL语句,并返回
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
return mapper.selectUser();
}
}

**注:**为了能够在Spring中使用Dao接口,我们需要将Dao接口的实现类注入到IOC容器中。因此,我们需要手动实现Dao接口,并在其中使用sqlSessionSqlSessionTemplate对象)执行SQL语句,并将执行结果返回供用户调用。

(6)在SpringIOC容器中注册Dao接口的实现类

spring-dao.xml文件:

1
2
3
4
5
<!--注册Dao接口的实现类-->
<bean id="userMapper" class="com.atangbiji.dao.UserMapperImpl">
<!--通过set方法注入sqlSession(SqlSessionTemplate对象)-->
<property name="sqlSession" ref="sqlSession"/>
</bean>

(7)测试

修改MyTest测试类,用于MyBatis-Spring功能测试。

MyTest.java文件:

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

public class MyTest {
@Test
public void testMyBatisSpring(){
//解析spring-dao.xml文件,IOC容器生成并管理相应的Bean对象
ApplicationContext context = new ClassPathXmlApplicationContext("spring-dao.xml");
//通过Spring的上下文对象,从IOC容器中获取Bean对象
UserMapper userMapper = (UserMapper)context.getBean("userMapper");
//调用Bean对象的方法
for (User user : userMapper.selectUser()) {
System.out.println(user);
}
}
}

运行测试程序,SQL执行成功。执行结果如下图所示:

整合MyBatis方式1测试结果

**注:**这样我们便完成了SpringMybatis的整合。

  • 此时,我们不再需要之前封装的MyBatis工具类(MyBatisUtils类),可以予以删除。

  • 此时,MyBatis核心配置文件被简化为:

mybatis-config.xml文件:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<!--MyBatis核心配置文件-->
<configuration>
<!--将包内的实体类全部设置别名-->
<typeAliases>
<package name="com.atangbiji.pojo"/>
</typeAliases>
</configuration>

13.4、方式2:使用SqlSessionDaoSupport整合

mybatis-spring1.2.3以上版本,为我们提供了一个抽象的支持类——SqlSessionDaoSupport类,它为我们提供了 SqlSession。如果我们手动添加的Dao接口实现类(如:UserMapperImpl类)继承了该支持类,那么我们就不用再像方式1一样在Dao接口实现类中添加一个 SqlSessionTemplate(私有)属性及其set方法了。此时,我们直接调用其 getSqlSession() 方法,便可以得到一个 SqlSessionTemplate对象,然后再去执行 SQL 语句了。

注:

  • 与方式1相比,方式2不需要管理SqlSessionTemplate, 而且对事务的支持更加友好,可跟踪查看源码。
  • 方式2是对方式1的进一步优化。

具体整合步骤如下:

(1)增加Dao接口的实现类,继承SqlSessionDaoSupport(关键)

保持方式1步骤(1)(2)(3)不变的基础上,在上述子项目的com.atangbiji.dao包下新建一个映射器配置文件UserMapperImpl2类,用于实现UserMapper接口;并让该Dao接口实现类继承SqlSessionDaoSupport类。

**注:**方式1的步骤(4)可以省略,也可以保持不变。

UserMapperImpl.java文件:

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

import com.atangbiji.pojo.User;
import org.apache.ibatis.session.SqlSession;
import org.mybatis.spring.support.SqlSessionDaoSupport;
import java.util.List;

//映射器接口实现类(方式二)
public class UserMapperImpl2 extends SqlSessionDaoSupport implements UserMapper{
@Override
public List<User> selectUser() {
//直接调用getSqlSession方法,获取SqlSessionTemplate对象。
SqlSession sqlSession = getSqlSession();
//然后,使用sqlSession(SqlSessionTemplate对象)执行SQL语句,并返回
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
return mapper.selectUser();
}
}

(2)在SpringIOC容器中注册新的Dao接口实现类

spring-dao.xml文件:

1
2
3
4
5
<!--注册新的Dao接口的实现类-->
<bean id="userMapper2" class="com.atangbiji.dao.UserMapperImpl2">
<!--通过父类的有参构造方法注入sqlSessionFactory-->
<property name="sqlSessionFactory" ref="sqlSessionFactory"/>
</bean>

注:SqlSessionDaoSupport 需要设置一个 sqlSessionFactorySqlSessionTemplate属性。如果两个属性都被设置了,那么 SqlSessionFactory 将会被忽略。

(3)测试

修改MyTest测试类,用于MyBatis-Spring功能测试。

MyTest.java文件:

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

public class MyTest {
@Test
public void testMyBatisSpring2(){
//解析spring-dao.xml文件,IOC容器生成并管理相应的Bean对象
ApplicationContext context = new ClassPathXmlApplicationContext("spring-dao.xml");
//通过Spring的上下文对象,从IOC容器中获取Bean对象
UserMapper userMapper = (UserMapper)context.getBean("userMapper2");
//调用Bean对象的方法
for (User user : userMapper.selectUser()) {
System.out.println(user);
}
}
}

运行测试程序,SQL执行成功。执行结果如下图所示:

整合MyBatis方式2测试结果

**注:**除了这些方式可以实现MyBatis整合之外,我们还可以使用注解来实现,这个等我们后面学习SpringBoot的时候还会测试整合。

14、Spring声明式事务

  • 事务在项目开发过程非常重要,涉及到数据的一致性的问题,不容马虎!
  • 事务管理是企业级应用程序开发中必备技术,用来确保数据的完整性和一致性。

14.1、回顾事务

详见《事务》。

14.2、搭建测试环境

(1)在父项目中新建一个模块(Module),并在其中创建一个普通的Maven子项目,项目名称为spring-11-Transaction

(2)填写子项目名称和Maven项目GAV,点击Finish按钮完成子项目创建,等待Maven依赖包导入完毕。

(3)检测在父项目的pom.xml文件中是否导入父/子工程需要的Maven依赖。若未导入,则手动导入。

(4)在子项目的java目录下新建一个com.atangbiji.pojo包(用于存放我们自己编写的实体类(pojo)),并在该包下新建一个User类,用于实现关系型数据库和业务实体间的映射。

User.java文件:

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

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

//实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
}

(5)在子项目的java目录下新建一个com.atangbiji.dao包(用于存放我们自己编写的持久层代码),并在该包下新建一个UserMapper映射器接口。

UserMapper.java文件:

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

import com.atangbiji.pojo.User;
import java.util.List;

//映射器接口
public interface UserMapper {
//查询全部用户
public List<User> selectUser();
//添加一个用户
public int addUser(User user);
//删除一个用户
public int deleteUser(int id);
}

(6)在该子项目的com.atangbiji.dao包下新建一个映射器配置文件UserMapper.xml,并在其中使用标签实现SQL语句与映射器接口函数的映射。

UserMapper.xml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--命名空间(绑定对应的映射器接口)-->
<mapper namespace="com.atangbiji.dao.UserMapper">
<!--查询全部用户-->
<select id="selectUser" resultType="user">
select * from mybatis.user
</select>
<!--添加一个用户-->
<insert id="addUser" parameterType="user">
insert into mybatis.user (id, name, pwd) VALUES (#{id},#{name},#{pwd})
</insert>
<!--删除一个用户-->
<delete id="deleteUser" parameterType="int">
delete from mybatis.user where id = #{id}
</delete>
</mapper>

(7)在Maven子项目的src/main/resources目录下新建一个名称为mybatis-config.xml 的MyBatis核心配置文件。

mybatis-config.xml 文件:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<!--MyBatis核心配置文件-->
<configuration>
<!--将包内的实体类全部设置别名-->
<typeAliases>
<package name="com.atangbiji.pojo"/>
</typeAliases>
</configuration>

接下来,按照方式1整合MyBatis。具体地:

(8)在子项目的resources目录下新建一个spring-dao.xml配置文件,用于配置与MyBatis相关Beans的元数据

spring-dao.xml文件:

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
<?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就是那些组成我们应用程序的Java对象,由Spring中的IOC容器创建、组装和管理-->
<!--(1)id:bean的唯一标识符。(相当于之前new的对象名)-->
<!--(2)class:bean所对应对象的全限定类名。(全限定名=包名+类名)-->

<!--配置数据源:使用Spring的数据源替换MyBatis(核心配置文件中)的数据源配置-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<!--驱动(固定写法)-->
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<!--连接信息(URL、用户名和密码)-->
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?useUnicode=true&amp;characterEncoding=utf8&amp;useSSL=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>

<!--配置SqlSessionFactory,关联MyBatis-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<!--绑定MyBatis配置文件,关联Mybatis-->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<property name="mapperLocations" value="classpath:com/atangbiji/dao/*.xml"/>
</bean>
<!--注册sqlSessionTemplate,关联sqlSessionFactory-->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<!--通过有参构造方法注入sqlSessionFactory-->
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
</beans>

(9)在该子项目的com.atangbiji.dao包下新建一个映射器配置文件UserMapperImpl类,用于实现UserMapper接口;并让该Dao接口实现类继承SqlSessionDaoSupport类。

UserMapperImpl.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
27
28
29
30
31
32
33
34
35
36
package com.atangbiji.dao;

import com.atangbiji.pojo.User;
import org.apache.ibatis.session.SqlSession;
import org.mybatis.spring.support.SqlSessionDaoSupport;
import java.util.List;

//映射器接口实现类
public class UserMapperImpl extends SqlSessionDaoSupport implements UserMapper {

//查询全部用户
@Override
public List<User> selectUser() {
//直接调用getSqlSession方法,获取SqlSessionTemplate对象。
SqlSession sqlSession = getSqlSession();
//然后,使用sqlSession(SqlSessionTemplate对象)执行SQL语句,并返回
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
return mapper.selectUser();
}

//添加一个用户
@Override
public int addUser(User user) {
//直接调用getSqlSession方法,获取SqlSessionTemplate对象。
//然后,使用sqlSession(SqlSessionTemplate对象)执行SQL语句,并返回
return getSqlSession().getMapper(UserMapper.class).addUser(user);
}

//删除一个用户
@Override
public int deleteUser(int id) {
//直接调用getSqlSession方法,获取SqlSessionTemplate对象。
//然后,使用sqlSession(SqlSessionTemplate对象)执行SQL语句,并返回
return getSqlSession().getMapper(UserMapper.class).deleteUser(id);
}
}

(10)在子项目的resources目录下新建一个Spring applicationContext.xml配置文件。使用import标签,将spring-dao.xml配置文件导入到我们的Spring主配置文件(applicationContext.xml)中去。

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

<!--导入spring-dao.xml配置文件中的Bean-->
<import resource="spring-dao.xml"/>

<!--当前xml配置文件中的Bean-->
<!--bean就是那些组成我们应用程序的Java对象,由Spring中的IOC容器创建、组装和管理-->
<!--(1)id:bean的唯一标识符。(相当于之前new的对象名)-->
<!--(2)class:bean所对应对象的全限定类名。(全限定名=包名+类名)-->

<!--注册Dao接口的实现类-->
<bean id="userMapper" class="com.atangbiji.dao.UserMapperImpl">
<!--通过父类的有参构造方法注入sqlSessionFactory-->
<property name="sqlSessionFactory" ref="sqlSessionFactory"/>
</bean>
</beans>

(11)在子项目的src/test/java目录下,新建一个MyTest,用于MyBatis-Spring功能测试。

MyTest.java文件:

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

public class MyTest {
@Test
public void test(){
//解析applicationContext.xml文件,IOC容器生成并管理相应的Bean对象
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
//通过Spring的上下文对象,从IOC容器中获取Bean对象
UserMapper userMapper = (UserMapper)context.getBean("userMapper");
//调用Bean对象的方法
for (User user : userMapper.selectUser()) {
System.out.println(user);
}
}
}

运行测试程序,SQL执行成功。执行结果如下图所示:

声明式事务环境测试结果

14.3、为什么需要管理事务

为了通过演示说明事务管理的重要性,我们在上述测试环境的基础上:

(1)故意把UserMapper.xml文件中的deleteSQL语句的delete写错为deletes

UserMapper.xml文件:

1
2
3
4
<!--删除一个用户-->
<delete id="deleteUser" parameterType="int">
deletes from mybatis.user where id = #{id}
</delete>

(2)修改UserMapperImpl接口实现类的selectUser方法,在该方法中增加一个“先添加一个用户,再删除该用户”的操作,并且希望这两个操作要么都成功,要么都失败

UserMapperImpl.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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.atangbiji.dao;

import com.atangbiji.pojo.User;
import org.apache.ibatis.session.SqlSession;
import org.mybatis.spring.support.SqlSessionDaoSupport;
import java.util.List;

//映射器接口实现类
public class UserMapperImpl extends SqlSessionDaoSupport implements UserMapper {

//查询全部用户
@Override
public List<User> selectUser() {
//直接调用getSqlSession方法,获取SqlSessionTemplate对象。
SqlSession sqlSession = getSqlSession();
//然后,使用sqlSession(SqlSessionTemplate对象)执行SQL语句,并返回
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
//先添加一个用户,再删除该用户
User user = new User(4,"小明","123456");
mapper.addUser(user);
mapper.deleteUser(4);

return mapper.selectUser();
}

//添加一个用户
@Override
public int addUser(User user) {
//直接调用getSqlSession方法,获取SqlSessionTemplate对象。
//然后,使用sqlSession(SqlSessionTemplate对象)执行SQL语句,并返回
return getSqlSession().getMapper(UserMapper.class).addUser(user);
}

//删除一个用户
@Override
public int deleteUser(int id) {
//直接调用getSqlSession方法,获取SqlSessionTemplate对象。
//然后,使用sqlSession(SqlSessionTemplate对象)执行SQL语句,并返回
return getSqlSession().getMapper(UserMapper.class).deleteUser(id);
}
}

(3)测试。

再次运行测试程序test,我们可以发现:SQL删除语句执行出错,但新用户添加仍然成功。执行结果如下图所示:

数据删除出错

数据添加成功

测试结果分析:虽然我们希望“添加一个用户“和”删除新添加的用户”这两个操作要么都成功,要么都失败,但由于我们没有对其进行事务的管理,才会出现上述SQL删除语句执行出错,但新用户添加仍然成功的现象。这显然不是我们想要的结果!

如果我们想让它们都成功时才成功,有一个失败,就都失败,那么我们就需要把它们当做事务来处理!

以前我们都需要自己手动管理事务,十分麻烦!

但是**Spring给我们提供了事务管理,我们只需要配置即可。**一旦配置好了Spring的事务管理器,我们就可以在Spring中按照我们平时的方式来配置事务。并且支持 @Transactional 注解和 AOP 风格的配置。在事务处理期间,一个单独的 SqlSession 对象将会被创建和使用。当事务完成时,这个session会以合适的方式提交或回滚。

14.4、Spring中的事务管理

Spring在不同的事务管理API之上定义了一个抽象层,使得开发人员不必了解底层的事务管理API就可以使用Spring的事务管理机制。

Spring为我们提供了 DataSourceTransactionManager事务管理器)来实现事务管理。

Spring支持编程式事务管理声明式事务管理

14.4.1、编程式事务管理与声明式事务管理

(1)编程式事务管理:

  • 将事务管理代码嵌到业务方法中来控制事务的提交和回滚。
  • 缺点:必须在每个事务操作业务逻辑中包含额外的事务管理代码。

(2)声明式事务管理:(推荐)

  • 一般情况下比编程式事务好用。
  • 将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。
  • 将事务管理作为横切关注点,通过AOP方法模块化。
  • Spring中通过Spring AOP框架支持声明式事务管理。

注:

  • 无论使用Spring的哪种事务管理策略(编程式或者声明式)事务管理器都是必须的。
  • DataSourceTransactionManager (事务管理器)就是Spring的核心事务管理抽象,管理封装了一组独立于技术的方法。
  • 我们这里仅介绍声明式事务管理,若想编程式地控制事务,请参考 Spring官方文档(Data Access -Programmatic transaction management)

14.4.2、实现步骤

(1)开启Spring的事务处理功能

在上述子项目的基础上,开启Spring的事务处理功能,即:在Spring整合MyBatis的配置文件中创建一个 DataSourceTransactionManager (事务管理器)对象(Bean)。

spring-dao.xml文件:

1
2
3
4
5
<!--开启Spring的事务处理功能(即:注入事务管理器)-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--通过有参构造方法注入dataSource-->
<constructor-arg ref="dataSource" />
</bean>

接下来,使用AOP实现事务的织入。具体地:

(2)导入txAOP的约束

Spring整合MyBatis的配置文件头部导入txAOP的约束。

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

注:tx表示事务。

(3)配置事务的通知

Spring整合MyBatis的配置文件中配置事务通知对应的事务管理器,并在其中配置事务的传播特性,即:配置哪些方法使用什么样的事务。

spring-dao.xml文件:

1
2
3
4
5
6
7
8
9
10
11
<!--配置事务的通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!--配置事务的传播特性,即:配置哪些方法使用什么样的事务-->
<tx:attributes>
<tx:method name="add" propagation="REQUIRED"/>
<tx:method name="delete" propagation="REQUIRED"/>
<tx:method name="update" propagation="REQUIRED"/>
<tx:method name="select*"/>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>

附:spring事务的传播特性

**事务传播行为(propagation)**就是多个事务方法相互调用时,事务如何在这些方法之间传播。spring支持7种事务传播行为:

  • requierd:如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,这是最常见的选择。
  • supports:支持当前事务,如果没有当前事务,就以非事务方法执行。
  • mandatory:使用当前事务,如果没有当前事务,就抛出异常。
  • required_new:新建事务,如果当前存在事务,把当前事务挂起。
  • not_supported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • never:以非事务方式执行操作,如果当前事务存在则抛出异常。
  • nested:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与required类似的操作。

注:Spring默认的事务传播行为是:REQUIRED,它适合于绝大多数的情况。

假设ServiveX#methodX()都工作在事务环境下(即都被Spring事务增强了),假设程序中存在如下的调用链:Service1#method1()->Service2#method2()->Service3#method3(),那么这3个服务类的3个方法通过Spring的事务传播机制都工作在同一个事务中。

就好比,我们14.3节中的selectUser()方法调用了addUser()deleteUser()方法,所以它们会被Spring放在一组事务当中进行处理!

(4)配置AOP

Spring整合MyBatis的配置文件中,通过AOP把事务织入到com.atangbiji.dao包下所有类的所有方法中。

spring-dao.xml文件:

1
2
3
4
5
6
7
8
<!--配置AOP-->
<aop:config>
<!--切入点(通过expression:表达式匹配要切入的位置)-->
<aop:pointcut id="txPointCut" expression="execution(* com.atangbiji.dao.*.*(..))"/>
<!--通知(把事务从txPointCut切入点织入)-->
<!--(1)advice-ref:通知的引用;(2)pointcut-ref:切入点的引用-->
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>

**注:**注意在xml配置文件的头部导入AOP的约束。

(5)测试

删除刚才插入的用户数据,再次运行测试程序test,我们可以发现:SQL删除语句执行仍然出错,但此时新用户并未添加成功。执行结果如下图所示:

数据删除仍然出错

数据添加并未成功

通过测试结果我们可以发现:Spring通过声明式事务可以在不改变原有代码的基础上实现对事务的管理,将事务管理代码从业务方法中分离出来。

(本讲完,系列博文持续更新中…… )

阿汤笔迹微信公众平台

关注**“阿汤笔迹”** 微信公众号,获取更多学习笔记。
原文地址:http://www.atangbiji.com/2022/12/08/SpringInDetail
博主最新文章在个人博客 http://www.atangbiji.com/ 发布。