Spring 框架简介

什么是 Spring

Spring 框架是一个开源的轻量级的 DI 和 AOP 容器框架,致力于简化企业级应用开 发,让开发者使用简单的 Java Bean 来实现从前只有 EJB 才能实现的功能。

为什么要使用 Spring

Spring 堪称 Java 世界中最强大的框架,其亮点非常的多,主要体现在以下几个方 面。

(1)使用 Spring 可以实现 DI(依赖注入)。实现面向接口编程,以解决项目开发中 组件间 的解耦问题,让项目模块得以独立测试、灵活扩展和替换。

(2)使用 Spring 可以实现 AOP(面向切面)。AOP 可以在无需修改原有类源代码 的情况下为它们切入增强功能

(3)使用 Spring 可以实现声明式事务管理,无需在项目中写死事务处理边界,具有 更高灵活性。

(4)Spring 可与大部分的 Java 开源框架(如 Hibernate、MyBatis、Struts2 等)进行 整合,并进一步简化这些框架的编码。

(5)Spring 可以实现分布式远程调用、消息队列、安全验证等诸多大型应用所需的复杂功能,可以大大简化企业级系统的开发。在 Spring 出现之前,这些功能一般开发者都难以解决,只能依赖于大型厂商(如 IBM、Oracle、SAP 等)提供的昂贵的 EJB 容器;但现在,开源世界的 Spring 都可以做到,而且更为灵活和轻盈。

面向接口编程与容器框架

面向接口编程

Spring 首先是一个容器框架,用于管理系统中的 JavaBean。那么我们为何需要一个容器框架呢,这是由 Java 世界推崇的面向接口编程所决定的。JavaEE 平台的其中一个特点是倡导面向接口编程,JavaEE 本身就是由 SUN 提出的各种规范和接口构成的。 例如,JSP 技术中的 Servlet、Filter、Listener、ServletRequest,ServletResponse、 HttpSession 等对象统统都是接口。为什么要面向接口编程呢,面向接口,可以降低组件与组件之间的依赖,实现弱耦合,被依赖组件随时可以被替代。例如 Tomcat 服务器,不过是一组 JSP/Servlet 接口的实现容器,我们完全可以用其它实现同样接口的容器(如 Jetty)来替代它。此外,面向接口编程也使得组件的独立开发与测试提供了可能,否则开发上层模块的开发者就需要等待下层模块完成才能开工,各个模块无法并行开发。虽然面向接口编程的想法不错,但使用时却要解决一个核心问题——具体对象从何而来?

参考如下代码:“CategoryDao”是一个接口,而“CategoryDaoImpl”是它的实现类, 获取对象的常规方式是使用“new”调用实现类的构造方法。如果我们用这种方式来构 建具体使用对象,就谈不上面向接口编程了,因为具体实现类已被写死在调用代码中 了。

1
2
CategoryDao categoryDao = new CategoryDaoImpl(); 
categoryDao.save(categoryName);

工厂模式

在传统的面向对象编程中,对象是调用者创建(new)出来的,调用者和被调用者产生了强耦合,而工厂模式在可以在调用者中隐藏具体类型,解决这一问题。 参考如下代码:“ObjectFactory”是对象工厂,可以根据不同的 DAO 名称获取对应 的 DAO 实现类对象。具体可以使用 Java 反射技术与 XML 配置来实现。

1
2
CategoryDao categoryDao = (CategoryDao)ObjectFactory.getInstance("categoryDao");
categoryDao.save(categoryName);

IOC 控制反转

所谓依赖注入 DI(Dependency Injection),一些文献也称之控制反转 IOC(Inversionof Control):是一种松耦合的开发模式,指对象是被动地接收它的依赖类对象,而非 自己主动去查找或创建。在开发中 A 类依赖于 B 类(如业务对象依赖于数据访问对 象),往往是 A 类中直接代码创建 B 类对象使用(或使用 JNDI 查找 B 类对象)。在依赖注入中,A 类中的 B 对象不由 A 自身创建,而是由容器 C 在实例 化 A 类对象时主动将 A 所依赖的 B 对象注入给它。

通过 bean 元素的 property 子元素,可以通过 bean 对象的属性实现依赖注入。property 子元素中,name 属性用于声明属性名,ref 属性用于引用已声明的复杂类型 bean 对象,value 属性用于指定普通类型常量值。

导包

1
2
3
4
5
6
<!-- Spring DI 容器 --> 
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.6.RELEASE</version>
</dependency>

(2)在“类路径”下加 Spring 的 bean 配置文件“applicationContext.xml”,并配置 需要 Spring 管理的类对象。

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?> 
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="categoryDao" class="demo.dao.impl.CategoryDaoImpl" />
</beans>

Bean 的作用域。

Spring 默认使用单例模式管理 bean 对象,也就是说对于同一个类 Spring 中只保留一个实例,我们多次通过 Spring 的getBean()获取的是相同的一个实例。而一些实际的场合(单例模式,适用于无状态 Bean,不适用于有状态 Bean),单例模式不适 用,这时我们可以通过 bean 的 scope 属性来改变 Bean 的作用域。通过 bean 元素的 scope 属性,可以指定 Bean 对象在 Spring 容器中的作用域。其 取值如 下表所示。

scope 属性取值 含义
singleton 默认值,Spring 容器中对该 bean 做单例模式处理
prototype 每一次请求都会产生一个新的 bean 实例,相当与一个 new 的操作,对 于 prototype 作用域的 bean,有一点非常重要,那就是 Spring 不能对一个 prototype bean 的整个生命周期负责,容器在初始化、配置、装饰或者是装配完一个 prototype 实例后,将它交给客户端,随后就对该prototype 实例不闻不问了
request request 表示该针对每一次 HTTP 请求都会产生一个新的 bean,同 时该bean 仅在当前 HTTP request 内有效。
session session 作用域表示该针对每一次 HTTP 请求都会产生一个新的bean,同时该 bean 仅在当前 HTTP session 内有效
global session global session 作用域类似于标准的 HTTP Session 作用域,不过它仅仅在基于 portlet 的 web 应用中才有意义

示例:

1
<bean id="userDao" class="com.demo.dao.impl.UserDaoImpl" scope="prototype" />

注意:若配置为 request、session、global session 时,若使用的是 Servlet 2.4 及以上的web 容器,那么需要在 web 应用的 XML 声明文件 web.xml 中增加下述 ContextListener

声明:

1
2
3
4
5
<listener> 
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>

在 Web 环境中启动 Spring 容器

在 Web 环境中,应用程序是由 Web 服务器启动的,Spring 要作为对象容器(对象工厂)为各层提供依赖注入功能,就必须在 Web 服务器启动时创建 Spring 实例,并在整个应用程序生命周期中保持唯一。这时,我们就不能在 main 函数中随便创建ApplicationContext()对象了,因为 Web 应用程序并不是由 main 函数启动的。

针对这个问题,Spring 提供了 Web 服务器的监听程序,使用监听器监听 Web 应用程序的启动事件,并在事件处理函数中创建 Spring 实例并使用单例模式(放到 Web 应用程序上下文中)缓存起来。这样在 Web 程序的任意地方,就可以获取到唯一的 Spring实例并实现依赖注入了。

在 Web 环境下配置 Spring 容器的创建,要在 web.xml 中配置监听器,具体代码如下:

1
2
3
4
5
6
7
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

注入分类

bean 实例在调用无参构造器创建对象后,就要对 bean 对象的属性进行初始化。初始化是由容器自动完成的,称为注入。根据注入方式的不同,常用的有两类:set 注入、构造注入。

set 注入(掌握)

set 注入也叫设值注入是指,通过 setter 方法传入被调用者的实例。这种注入方式简单、直观,因而在 Spring 的依赖注入中大量使用。

简单注入

1
2
3
4
public class student {
private string name;
private int age;
}
1
2
3
4
<bean class="io.peng.student">
<property name="name" value="张三"/>
<property name="age" value="22" />
</bean>

还可以对系统类进行赋值

1
2
3
4
<bean id="myDate"class="java.util.Date">
<!--设置时间是2019-01-16 215140-->
<property name="time" value="1547646700353"/>
</bean>

引用类型

当指定 bean 的某属性值为另一 bean 的实例时,通过 ref 指定它们间的引用关系。ref 的值必须为某 bean 的 id 值

1
2
3
4
5
6
public class student {
private string name;
private int age;
// School 类型
private School school;
}
1
2
3
4
5
6
7
8
9
10
11
12
<!-- 声明School对象 -->
<bean id="myschool" class="io.peng.School">
<property name="name" value="北京大学"/>
<property name="address" value="海定区" />
</bean>

<bean class="io.peng.student">
<property name="name" value="张三"/>
<property name="age" value="22" />
<!-- 应用school的id -->
<property name="school" ref="myschool" />
</bean>

构造方法注入

构造注入是指,在构造调用者实例的同时,完成被调用者的实例化。即,使用构造器设置依赖关系。在实体类中必须提供相应参数的构造方法。
<constructor-arg />标签中用于指定参数的属性有:
name:指定参数名称。
index:指明该参数对应着构造器的第几个参数,从 0 开始。不过,该属性不要也行, 但要注意,若参数类型相同,或之间有包含关系,则需要保证赋值顺序要与构造器中的参数顺序一致。

使用构造方法的参数名称注入值

1
2
3
4
5
6
7
8
9
10
//提供有参的构造方法为进行注入值
public Student(String myname, int myage) {
this.name = myname;
this.age = myage;
}
public Student(String name, int age, School school) {
this.name = name;
this.age = age;
this.school = school;
}
1
2
3
4
5
6
7
8
9
10
11
<!--    创建学校对象,并赋值-->
<bean id="school" class="io.peng.School">
<constructor-arg name="name" value="清华大学"></constructor-arg>
<constructor-arg name="address" value="北京海淀区"></constructor-arg>
</bean>
<!-- 创建学生对象,通过构造方法参数名称注入值-->
<bean id="stu" class="io.peng.Student">
<constructor-arg name="age" value="22"></constructor-arg>
<constructor-arg name="name" value="张三"></constructor-arg>
<constructor-arg name="school" ref="school"></constructor-arg>
</bean>

使用构造方法的参数索引下标注入值

1
2
3
4
5
6
<!--    通过构造方法参数下标索引进入注入-->
<bean id="stuindex" class="io.peng.Student">
<constructor-arg index="1" value="22"></constructor-arg>
<constructor-arg index="0" value="李四"></constructor-arg>
<constructor-arg index="2" ref="school"></constructor-arg>
</bean>

不指定名称和下标索引的注入

1
2
3
4
5
6
7
<!--    通过构造方法参数进入注入,不指定参数名称和索引下标-->
<!-- 注意:此种方式的注入一定要按类中构造方法的参数的顺序来进行注入。 -->
<bean id="stuno" class="com.bjpowernode.pojo.s03.Student">
<constructor-arg value="李四"></constructor-arg>
<constructor-arg value="22"></constructor-arg>
<constructor-arg ref="school"></constructor-arg>
</bean>

byName 方式自动注入 autowire

当配置文件中被调用者 bean 的 id 值与代码中调用者 bean 类的属性名相同时,可使用byName 方式,让容器自动将被调用者 bean 注入给调用者 bean。容器是通过调用者的 bean类的属性名与配置文件的被调用者 bean 的 id 进行比较而实现自动注入的。

byType 方式自动注入 autowire

使用 byType 方式自动注入,要求:配置文件中被调用者 bean 的 class 属性指定的类, 要与代码中调用者 bean 类的某引用类型属性类型同源。即要么相同,要么有 is-a 关系(子类,或是实现类)。但这样的同源的被调用 bean 只能有一个。多于一个,容器就不知该匹配哪一个了。

注解DI

依赖注入:DI(Dependency Injection),对于 DI 使用注解,将不再需要在 Spring 配置文件中声明bean 实例。Spring 中使用注解, 需要在原有 Spring 运行环境基础上再做一些改变。需要在 Spring 配置文件中配置组件扫描器,用于在指定的基本包中扫描注解。

1
<context:component-scan base-package="io.peng.service.impl" />

指定 base-package 的值使用分隔符
分隔符可以使用逗号(,)或分号(;),还可以使用空格,不建议使用空格。

创建对象的注解

注解 描述
@Component 标记一个普通的 bean 被 Spring 管理,没有特定的语义
@Repository 标注一个 DAO 组件 bean
@Service 标注一个业务逻辑组件 bean
@Controller 标注一个控制器组件 bean

给对象赋值的注解

注解 描述
@Value 给简单类型赋值
@Autowired 按属性类型装配依赖对象(Spring 特有的注解)
@Qualifier 给引用类型按名称注入
@Resource 先按名称(bean id)装配依赖对象,没有匹配则按类型装配(Java EE 官方注解)

例子

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

1
2
3
4
5
6
7
@Component"myStudent"
public class student{
@Value"张三"
private String name;
@Value"21"
private int age;
}

需要在引用属性上使用注解@Autowired,该注解默认使用按类型自动装配 Bean 的方式。使用该注解完成属性注入时,类中无需 setter。当然,若属性有 setter,则也可将其加
到 setter 上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component"mystudent"
public class student{
@Value"张三"
private string name;
@Value"21"
private int age;

@Autowired
private School school;
}


@Component("myschool")
public class School{
@Value"清华大学"
private string name;
}

为应用指定多个 Spring 配置文件

拆分策略:
按模块拆分,例如用户模块applicationContext_user.xml,applicationContext_book.xml,每个xml文件中都包含相应的xxxController,xxxService,xxxDao的对象的创建。
按层拆分,例如拆分成applicationContext_controller.xml, applicationContext_service.xml,
applicationContext_dao.xml等,每个xml文件中有相关对象的创建,例如:applicationContext_controller.xml文件中包含userController,bookController等对象的创建。

拆分后整合
使用一个总的配置文件整合(专门导入配置文件的)
多个配置文件中有一个总文件,总配置文件将各其它子文件通过<import/>引入。在 Java
代码中只需要使用总配置文件对容器进行初始化即可。注意:可以使用通配符*进行批量整合。

1
2
3
4
5
6
7
<!--单个导入-->
<import resource="application_school.xml"></import>
<import resource="application_student.xml"></import>
<!--
使用通配符批量导入-->
<import resource="application_*.xmL"></import>
</beans>

Bean 的生命周期管理

在实例化 bean 时,有时有必要执行一些初始化代码来使它处于可用状态,或者在丢弃 bean 时需要执行一些清理工作。Spring 为这种需求提供了初始化方法 init-method 和 销毁方法 destory-method 配置,使得 bean 对象的生命周期管理更为细致。

1
2
3
4
5
6
7
8
9
10
11
12
public class UserDaoImpl implements UserDao {
public void init(){
System.out.println("执行了 UserDao 的初始化方法");
}
public void save() {
System.out.println("执行 UserDao,用户信息保存成功。");
}

public void destory(){
System.out.println("执行了 UserDao 的销毁化方法");
}
}

通过配置即可在适当时机调用初始化和销毁方法。

1
2
<bean id="userDao" class="com.demo.dao.impl.UserDaoImpl" init-method="init" 
destroy-method="destory" />

AOP 编程术语

(1) 切面(Aspect)
切面泛指交叉业务逻辑,或是公共的,通用的业务。上例中的事务处理、日志处理就可以理解为切面。常用的切面是通知(Advice)。实际就是对主业务逻辑的一种增强。

(2) 连接点(JoinPoint)
连接点指可以被切面织入的具体方法。通常业务接口中的方法均为连接点。

(3) 切入点(Pointcut)
切入点指声明的一个或多个连接点的集合。通过切入点指定一组方法。
被标记为 final 的方法是不能作为连接点与切入点的。因为最终的是不能被修改的,不能被增强的。

(4) 目标对象(Target)
目标对象指 将要被增强 的对象。 即包含主业 务逻辑的 类的对象。 上例中 的
BookServiceImpl 的对象若被增强,则该类称为目标类,该类对象称为目标对象。当然, 不被增强,也就无所谓目标不目标了。

(5) 通知(Advice)
通知表示切面的执行时间,Advice 也叫增强。上例中的 MyInvocationHandler 就可以理解为是一种通知。换个角度来说,通知定义了增强代码切入到目标代码的时间点,是目标方法执行之前执行,还是之后执行等。通知类型不同,切入时间不同。
切入点定义切入的位置,通知定义切入的时间。

Spring的切面AOP注解配置

1)在Spring配置文件(applicationContext.xml)中加入context声明和AOP声明

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

</beans>

(2)在Spring配置文件中配置注解标识业务类的扫描范围,配置切面类的扫描范围,开启Spring切面自动代理。

1
2
3
<context:component-scan base-package="com.demo.aspact" />
<context:component-scan base-package="com.demo.biz.impl" />
<aop:aspectj-autoproxy />

(3)定义切面类并使用注解标识切入点、通知类型。

​ 普通通知:可以事先写一个切点表达式然后重用,也可以在每个通知前直接写切点表达式。

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
@Component
@Aspect
public class NormalAdvice {
@Pointcut("execution(* *.save(..))")
public void pc1(){} //该方法无实际作用,仅作为切入点的名称被通知引用而已
@Before("pc1()") //前置通知
public void doBefore() { … }

@After("pc1()") //后置通知
public void doAfter() { … }
@AfterReturning("pc1()") //返回后通知

public void doAfterReturn() { … }
@AfterThrowing("pc1()") //抛出异常通知

public void doException() { … }
}

环绕通知:

@Component
@Aspect
public class AroundAdvice {
@Pointcut("execution(* *.save(..))")
public void pc2(){}
@Around("pc2()")
public Object doAround(ProceedingJoinPoint jp) throws Throwable {
System.out.println("环绕通知开始...");
Object result = jp.proceed();
System.out.println("环绕通知结束...");
return result;
}
}

事务注解

在spring的配置文件中,配置事务管理器,名称为transactionManager

1
2
3
4
<!-- 事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

在spring的配置文件中,配置使用事务注解

1
2
<!--事务启用注解-->
<tx:annotation-driven/>

在业务方法上或者类上面添加@Transactional注解,默认的事务管理器名字为transactionManager,事务传播机制默认为propagation = Propagation.REQUIRED

1
2
3
4
@Transactional
public int remove(Integer id) {
return categoryDao.delete(id);
}

导包

(1)MyBatis:mybatis、mybatis-springMyBatis与Spring整合包)
(2)数据库驱动包:
(3)Spring相关包:Core(Bean管理)、persistJdbc(数据源)、persistCore(事务)、aop(切面)

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
<!-- mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.2.5</version>
</dependency>

<!-- MyBatis与Spring整合包 ,整合Spring的关键 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.2.1</version>
</dependency>

<!-- dbcp 数据源(连接池),必须 -->
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.2.2</version>
</dependency>

<!-- Spring 容器 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>

<!-- Spring ORM -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>

核心类

SqlSessionFactoryBean:在MyBatis中,SqlSessionFactory是由SqlSessionFactoryBuilder创建的,在MyBatis-Spring中,使用SqlSessionFactoryBean来完成。

MapperFactoryBean: 该类实现了Spring的FactoryBean接口,通过MapperInterface属性注入接口。(使用Mapper接口调用数据访问)

SqlSessionTemplate: 模版,负责管理SqlSession。会保证SqlSession和当前Spring事务相关联的,它会管理会话的生命周期,包括必要的关闭,提交和回滚操作。

构建实体类

构建数据访问接口

1
2
3
4
5
6
7
8
public interface CategoryDao {
List<Category> getAll();
void add(Category category);
}

public interface MovieDao {
public List<Movie> getMovies(@Param("cid")int cid,@Param("title")String title);
}

创建SQL映射文件mybatis.xml

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 用于为(实体类的)完全限定类名设置简化的别名 -->
<typeAliases>
<typeAlias type="com.myCinema.entity.Category" alias="Category"/>
<typeAlias type="com.myCinema.entity.Movie" alias="Movie"/>
</typeAliases>
</configuration>

配置applicationContext.xml文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 配置数据源 (dbcp数据源,需要添加"commons-dbcp.jar"包) -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/MyCinema"/>
<property name="username" value="root"/>
<property name="password" value=" "/>
</bean>

<!-- 配置SqlSessionFactory,由Spring的 SqlSessionFactoryBean 提供-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<!-- 保留mybatis.xml主配置文件,配置实体类别名 -->
<property name="configLocation" value="classpath:mybatis.xml" />
<!-- 配置sqlMapper配置文件位置 -->
<property name="mapperLocations" value="classpath:mapper/*Mapper.xml" />
</bean>

如果使用Mapper接口访问,无需提供数据访问实现类。

MyBatis的一大亮点就是不用Dao的实现类。MyBatis-Spring提供了一个MapperFactoryBean可以将数据映射接口转化为SpringBean.

1
2
3
4
<bean id="categoryDao" class="org.mybatis.spring.mapper.MapperFactoryBean">
<property name="sqlSessionFactory" ref="sqlSessionFactory"></property>
<property name="mapperInterface" value="com.myCinema.dao.CategoryDao"/>
</bean>

使用MapperScannerConfigurer简化配置

如果数据访问映射接口很多,需要在Spring的配置文件对数据映射接口做配置。为了简化配置,MyBatis-Spring提供了一个转换器MapperScannerConfigurer,它可以将接口直接转换为Spring容器中的Bean(无需配置数据访问接口类)配置如下:

1
2
3
4
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactory" ref="sqlSessionFactory"/>
<property name="basePackage" value="com.mycinema.dao"/>
</bean>

声明式事务管理

在实际的数据访问开发中,在增删改中引入事务管理是非常必要的,只有在事务管理下,相关的数据才能保持一致性,否则有可能产生重大业务错误。但在 DAO 模式下,增删改数据往往是单表操作,而事务则常常包含多个 DAO 对象和方法,需要确保多个 DAO 方法在同一 Connection 和 Transaction 下执行变得非常复杂。
Spring 利用 AOP 切面技术,为数据访问提供了基于业务层(一个业务方法往往代表一个事务,可以包含多个 DAO 方法)的声明式事务管理,完全透明地解决了事务难题。所谓声明式的事务管理:即只需配置,无须编程,利用 AOP 技术,把事务代码横切织入到数据访问代码中。
Spring 针对不同的数据访问方式,提供了不同的事务管理器,如下所示:

使用 DataSource 的事务管理器

这里讨论的是 DataSource的事务管理器:org.springframework.jdbc.datasource.DataSourceTransactionManager

(1)导入所需要依赖。
这里需要用到 AOP 和切面描述,因此需要在原来基础上添加 Spring 的切面依赖。

1
2
3
4
5
6
<!-- Spring 切面,可用于配置事务切面 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${springframework.version}</version>
</dependency>

(2)在 Spring 配置文件的文档声明中加入 aop 和 tx(事务)配置声明。
在Spring配置文件的文档声明中加入aop和tx(事务)配置声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://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>

(3)配置DataSource事务管理器。

1
2
3
<bean id="txManager"  class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>

(4)配置AOP事务通知。

1
2
3
4
5
6
7
8
9
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="add*" propagation="REQUIRED" />
<tx:method name="update*" propagation="REQUIRED" />
<tx:method name="delete*" propagation="REQUIRED" />
<tx:method name="get*" read-only="true"/>
<tx:method name="fetch*" read-only="true"/>
</tx:attributes>
</tx:advice>

5)配置 AOP 切面(通知+切入点)

1
2
3
<aop:config>
<aop:advisor pointcut="execution(* mycinema.biz...(..))" advice-ref="txAdvice"/>
</aop:config>

理解事务参数。

​ 配置Spring声明式事务管理时,tx:method配置元素除用于声明业务方法名外,还提供了若干属性用于控制事务细节:propagation、isolaction、read-only、timeout等等。

(1)propagation(传播行为)。

​ 用于声明执行该业务方法时是否启用当前事务,还是启动一个新的事务。

取值 意义
Propagation.REQUIRED 如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。( 也就是说如果A方法和B方法都添加了注解,在默认传播模式下,A方法内部调用B方法,会把两个方法的事务合并为一个事务
Propagation.SUPPORTS 如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行
Propagation.MANDATORY 如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。
Propagation.REQUIRES_NEW 重新创建一个新的事务,如果当前存在事务,暂停当前的事务。( 当类A中的 a 方法用默认Propagation.REQUIRED模式,类B中的 b方法加上采用 Propagation.REQUIRES_NEW模式,然后在 a 方法中调用 b方法操作数据库,然而 a方法抛出异常后,b方法并没有进行回滚,因为Propagation.REQUIRES_NEW会暂停 a方法的事务 )
Propagation.NESTED 和 Propagation.REQUIRED 效果一样
Propagation.NOT_SUPPORTED 以非事务的方式运行,如果当前存在事务,暂停当前的事务。
Propagation.NEVER 以非事务的方式运行,如果当前存在事务,则抛出异常

(2)isolaction(隔离级别)。

TransactionDefinition取值 意义
ISOLATION_DEFAULT 使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别
ISOLATION_READ_UNCOMMITTED 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
ISOLATION_READ_COMMITTED 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
ISOLATION_REPEATABLE_READ 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生
ISOLATION_SERIALIZABLE 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别

​ 多个事务同时运行操作同一批数据会导致并发,有可能会导致以下问题:

A. 脏读(Dirty Reads):一个事务开始读取了某行数据,但是另外一个事务已经更新了此数据但没有能够及时提交。这是相当危险的,因为很可能所有的操作都被回滚。

学生A的原始成绩为50分,老师读取之后修改为60但未提交,学生A读取到60分,老师发现修改错了,回滚,学生A的成绩恢复为50分。

B. 幻读(Phantom Reads):事务在操作过程中进行两次查询,第二次查询的结果包含了第一次查询中未出现的数据或者缺少了第一次查询中出现的数据(这里并不要求两次查询的SQL语句相同)。这是因为在两次查询过程中有另外一个事务插入数据造成的。

A改后还未提交,B改其他的,A再查

老师A将60分以下的成绩改为60分但未提交

老师B将70分-60分的成绩改为50分

老师A再查询60分以下学生成绩还有一批数据

C. 不可重复读(Non-repeatable Reads):一个事务对同一行数据重复读取两次,但是却得到了不同的结果。

A先读,B再改,A再读

1.在事务A中,学生读取了自己的成绩为50分
2.在事务B中,老师修改了学生的成绩为60,并提交了事务.
3.在事务A中,学生 再次读取自己的成绩为60分

​ 理想状态下,事务之间应该是完全相互隔离的,但完全隔离会影响性能,因为隔离需要锁定数据库中的记。在实际中,并非所有应用都要求事务完全隔离,因此Spring提供了若干个隔离级别,以提高事务管理的灵活度。

(3)read-only(只读)

​ 若一个事务只需要对数据库执行读操作,那就应该把事务声明为只读,让Spring对该事务的执行实行优化策略。

(4) timeout(超时)

​ 为了确保不会造成死锁或长期等待过分增加数据库负担,可以为事务提供一个超时时间,让事务在超过设定的秒数后自动回滚事务。

(5) rollbackfor属性

rollbackFor属性配置当发生何种异常的时候会回滚。一般是配置的Exception异常大类。

(6) noRollbackFor属性

抛出指定的异常类型,不回滚事务,也可以指定多个异常类型

(7) 事务注解:Transactional失效场景

1.@Transactional应用在非public方法上

该注解只能应用于public方法。只要public方法回滚了,那么public方法内部的调用的方法也就都回滚了,只要一个回滚的入口就好了。

2.@Transactional注解属性propagation设置错误

这种失效是由于配置错导致的,若是错误的配置一下三种,事务将不会发生回滚。

TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。

TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。

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

3.@Transactional注解属性rollbackFor错误

rollbackFor属性配置当发生何种异常的时候会回滚。一般我们都是配置的Exception异常大类。Spring默认抛出未检查异常,继承自RuntimeException或者Error时才回滚事务;其他异常不会回滚。所以当我们希望发生我们指定的异常时回滚,那么就需要配置这个属性了

4.@Transactional在同一个方法中调用,导致@Transactional失效

开发中避免不了类中方法的调用,比如有一个A类,内有B、C两个public方法。B声明事务,C未声明事务。当B调用C,那么B失败则事务回滚;但是当C调用B的时候,C失败,那么事务不会回滚

5.异常被catch,导致回滚失败

当你对方法使用@Transactional的时候,但是你方法内部try-catch了异常。

A、B两个方法,A声明事务;A方法调用B方法并且try-catch了B方法,当B方法执行报错的时候,A不能回滚。

用到了哪些设计模式

  • 代理模式:在 AOP 和 Remoting 中被用的比较多。
  • 单例模式:在 Spring 配置文件中定义的 Bean 默认为单例模式。
  • 模板方法:用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。
  • 前端控制器:Spring 提供了 DispatcherServlet 来对请求进行分发。
  • 视图帮助(View Helper ):Spring 提供了一系列的 JSP 标签,高效宏来辅助将分散的代码整合在视图里。
  • 依赖注入:贯穿于 BeanFactory / ApplicationContext 接口的核心理念。
  • 工厂模式:BeanFactory 用来创建对象的实例。