1. Spring
官网:https://spring.io/projects/spring-framework/
文档:https://docs.spring.io/spring-framework/docs/current/reference/html/index.html
三大核心:
控制反转(ioc)-----> Inversion of Control
依赖注入(di)---- >Dependency Injection
面向切面编程(AOP)---->Aspect Oriented Programming
- 测试依赖
1 | <dependency> |
- 测试代码写在/src/test/java目录下,最好包结构也和待测试代码一致。
- 测试代码中main函数可加但是不需要加测试注解,如果要测试其他函数则mian函数不能加测试注解
1 | import com.ahulearn.pojo.People; |
2. IOC思想
文档:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html
- UserDao接口:面向接口编程
- UserDaoImpl实现类
- UserService业务接口:业务层,就是用来调用Dao层
- UserService也算实现类
- 用户测试类
- 工厂模式
2.1 为什么需要IOC
下方案例,如果用户获取用户的需求发生改变,则需要做
- 添加一个新的UserDao实现类
- 修改UserServiceI实现类的方法,调用新的的UserDao类
这只是一个需求需要更改的情况,且项目本身很小。对于大项目需要修改的内容十分繁杂。如果项目代码需要每一次根据用户需求的改变或增加而大量修改代码显然是不合理的。
- UserDao接口
1 | package com.ahulearn.dao; |
- UserDaoImpl实现类
1 | package com.ahulearn.dao; |
- UserService业务接口
1 | package com.ahulearn.service; |
- UserServiceImpl实现类
1 | package com.ahulearn.service; |
- 用户测试类
1 | package com.ahulearn; |
解决方法:利用set方法,实现动态的创建值的注入
方法很简单,但思想很深刻
- UserServiceImpl实现类
1 | package com.ahulearn.service; |
- 用户测试类: 利用泛型的思想,根据用户需求将不同的实现类传给service层,不需要对service层进行修改,只需要在业务层增加相应的实现即可。
- 存在一个问题就是,用户层需要接触dao层的具体实现类
1 | package com.ahulearn; |
- 之前,对象是程序主动创建的,控制权在程序员的手上。
- 使用set注入后,程序不再具有主动性,而是变成被动的接受对象!
这种思想,从本质上解决问题,程序员不需要再去管理对象的创建。系统的耦合性降低,可以更加专注业务的实现上。
这是IOC的原型,并不是真正的IOC。
IOC的本质
控制反转(Inversion of Control,缩写为IoC),是面向对象编中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。
3. HelloSpring
控制反转中初始化对象属性是依赖对象中的set方法实现的。
- 对象
1 | package com.ahulearn.pojo; |
- beans.xml
1 |
|
- 测试类
1 | package com.ahulearn.pojo; |
- 重写IOC思想案例测试类
1 | package com.ahulearn.pojo; |
beans.xml
1 |
|
注意:运行出错可能是代码没有重新编译
此时修改实现只需要修改xml配置文件,配置文件可以通过配置选修修改。或者手动修改配置文件。但不需要修改代码了。用户和服务都不需要修改。
4. IOC创建对象
- 依赖包
1 | <dependency> |
Spring在创建容器后,容器中注册管理的所有对象都被初始化了。
-
默认使用无参构造对象!
-
要使用有参构造创建对象
- 下标赋值
1
2
3
4<!--通过参数索引,有参构造-->
<bean id="user1" class="com.ahulearn.pojo.User">
<constructor-arg index="0" value="sl"/>
</bean>- 参数类型
1
2
3
4<!--通过参数类型,有参构造-->
<bean id="user2" class="com.ahulearn.pojo.User">
<constructor-arg type="java.lang.String" value="lz"/>
</bean>- 引用方式
1
2
3
4<!--主流方法,通过形参名,有参构造-->
<bean id="user3" class="com.ahulearn.pojo.User">
<constructor-arg name="name" value="令章"/>
</bean>
5. Spring配置
5.1 别名
1 | <bean id="user3" class="com.ahulearn.pojo.User"> |
5.2 bean
id: bean的唯一标识符,相当于对象名
class: bean对象对应的全限定命名:包名+类型
name: 别名,相当于用alias去定义别名, 且可以同时取多个别名
1 | <!--name创建别名,可以创建多个别名,别名间通过逗号,分号或空格分隔--> |
5.3 import
import一般用于团队开发使用,他可以将多个配置文件,导入合并为一个:applicationContext.xml
假设,现在项目中有多个人开发,两个人负责不同的类开发,不同的类需要注册在不同的bean中,可以利用import将所有人的beans.xml合并为一个总的配置文件,使用的时候只需要使用总的配置文件即可。
如果两个文件中,存在相同的id, 则后导入的会覆盖之前导入的bean.
1 |
|
6. 依赖注入
6.1构造器注入
上方第5节已经使用并介绍
6.2 set注入【重点】
- 依赖注入
- 依赖:bean对象的创建依赖于容器!
- 注入:bean对象的所有属性,由容器来注入!
【环境搭建】
- 复杂类型
1 | package com.ahulearn.pojo; |
1 | public class Student { |
- 真实测试对象
1 | package com.ahulearn.pojo; |
- beans.xml
1 |
|
- 测试类
1 | import com.ahulearn.pojo.Student; |
6.3 拓展方式注入
p命名空间注入,对应set方式注入
1 |
|
c命名空间注入,对应构造器注入
1 |
|
注意点:p命名和c命名不能直接使用,需要导入相应的xml约束
1 | xmlns:p="http://www.springframework.org/schema/p" |
6.4 Bean Scopes 作用域
文档:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-scopes
6种作用域,后两种类似
Scope | Description |
---|---|
singleton | (Default) Scopes a single bean definition to a single object instance for each Spring IoC container. |
prototype | Scopes a single bean definition to any number of object instances. |
request | Scopes a single bean definition to the lifecycle of a single HTTP request. That is, each HTTP request has its own instance of a bean created off the back of a single bean definition. Only valid in the context of a web-aware Spring ApplicationContext . |
session | Scopes a single bean definition to the lifecycle of an HTTP Session . Only valid in the context of a web-aware Spring ApplicationContext . |
application | Scopes a single bean definition to the lifecycle of a ServletContext . Only valid in the context of a web-aware Spring ApplicationContext . |
websocket | Scopes a single bean definition to the lifecycle of a WebSocket . Only valid in the context of a web-aware Spring ApplicationContext . |
-
单例模式singleton (Spring默认机制)
每次从容器中get, 同一个id取出的是同一个对象
1 | <!--单例模式--> |
-
原型模式prototype (多线程可能有用)
每次从容器中get, 同一个id都会产生一个新对象!
1 | <!--原型模式--> |
-
其余的request、session、application,这些只能在web开发中使用到!
request: 在一次请求中存活,session: 在一个会话中存活,application:全局有效
7. Bean的自动装配
- 自动装配是Spring满足bean依赖的一种方式!
- Spring会在上下文中自动寻找,并自动给bean装配属性!
在Spring中由三种装配方式
- 在xml中显示的配置
- 在java中显示的配置
- 隐式的自动装配bean【重点】
7.1 测试
环境搭建:一个人有两个宠物,看到一句话要立马反应出有几个对象
1 |
|
7.2 byName自动装配
要求bean ID要和set方法名一致,比如setCat方法装配则找id为cat的bean
1 |
|
7.3 byType自动装配
要求beans中的类型必须全局唯一,用于装配的bean甚至可以需要拥有bean-id
1 |
|
7.4 使用注解自动装配
JDK1.5支持注解,Spring2.5开始支持注解
要使用注解须知:
- 导入约束:
xmlns:context="http://www.springframework.org/schema/context"
- 配置注解的支持:
<context:annotation-config/>
1 |
|
- 注解依赖:
1 | <dependency> |
- 导入包:
import org.springframework.beans.factory.annotation.Autowired;
7.4.1 @Autowired
注解可以在属性上用,或者set方法上用,一般写在属性上。需要在IOC容器中注册相应的id.
如果写在属性上,set方法都不用再写,因为注解是通过反射实现的,但get方法不能省略。
注解优先按类型查找,没有类型匹配则报错;找到多个则按name匹配,如果没有name匹配的则报错。
1 | package com.ahulearn.pojo; |
- @Autowired(required=false) : 默认是true,如果设置成false则允许字段为空,即在beans可以不注册该对象。跳过装配。
1 | public class People { |
可以在xml不添加cat的bean,但必须添加dog的bean
1 |
|
@Nullable: 允许标记的字段为空
1 | import org.springframework.lang.Nullable; |
7.4.2 @Qualifier
- 如果自动装配环境比较复杂,在beans注册对象中,通过类型可以匹配超过一个对象,但name都不匹配,可以配合@Qualifier(value="cat2")注解指定要装配的id
1 | package com.ahulearn.pojo; |
7.4.3 @Resource
javax包下的注解,功能类似spring中的@Autowired注解,首先通过类型匹配,之后通过name匹配,可以手动指定name
- @Resource(name="cat2")
- 依赖:javax下的包属于java拓展包,没有包含在java核心包中,需要自己添加依赖
1 | <dependency> |
1 | package com.ahulearn.pojo; |
8. 使用自动装配注解Component开发
在Spring4之后,要使用注解开发,必须导入aop包
1 | <!-- https://mvnrepository.com/artifact/org.springframework/spring-aop --> |
8.1 bean
- 导入约束applicationContext.xml
1 |
|
1 | package com.ahulearn.pojo; |
8.2 属性如何注入
1 | package com.ahulearn.pojo; |
- 测试类
1 | import com.ahulearn.pojo.User; |
8.3 衍生注解
@Component有几个衍生注解,作用相同,只是名称不同,用在不同的模块中。我们在web开发中,会按照mvc架构分层
- dao 【@Repository】
1 | package com.ahulearn.dao; |
- service【@Service】
1 | package com.ahulearn.service; |
- controller【@Controller】
1 | package com.ahulearn.controller; |
这四个注解功能一样,都是代表将某个类注册到Spring中,装配Bean
- benas.xml
1 |
|
8.4 自动装配
1 | @Autowired |
8.5 作用域
-
导入依赖
1
import org.springframework.context.annotation.Scope;
-
@Scope
- 单例模式@Scope("singleton") 等同于beans配置中的属性scope = "singleton"
- 多例模式@Scope("prototype") 等同于beans配置中的属性scope = "prototype"
1 | package com.ahulearn.pojo; |
8.6 小结
xml与注解
- xml功能更强,适合任何场合!维护简单方便,配置都在同一个文件中
- 注解不是自己的类使用不了,维护相对复杂
xml与注解最佳实践
- xml用来管理bean
- 注解只负责完成属性的注入
- 我们在使用的过程中,只需要注意一个问题:必须让注解生效,就需要开启注解支持
1 | <!--指定要扫描的包,只有指定的包下的Component注解才会生效--> |
9. 使用java的方式配置Spring
JavaConfig原是Spring的子项目,在Spring4之后成为核心功能
完全不使用Spring的xml配置,全权交给java来做
文档:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-java
在一个类上加@Configuration类似于写一个配置文件:beans
- 配置类:AppConfig.java
1 | package com.ahulearn.config; |
等价于
1 | <beans> |
- 实体类:User
1 | package com.ahulearn.pojo; |
- 测试类
1 | import com.ahulearn.config.AppConfig; |
纯java的配置方式,在Springboot中随处可见
10. AOP-代理模式
为什么学习代码模式?【因为这就是SpringAOP的底层】
代理模式分类:
- 静态代理
- 动态代理
使用代理避免修改原有的业务代码(被代理角色),可以方便的添加公用的业务操作。
10.1 静态代理
角色分析:
- 抽象角色:一般会使用接口或抽象类
1 | package com.ahulearn.demo01; |
- 真实角色:被代理的角色
1 | package com.ahulearn.demo01; |
- 代理角色:代理真实角色,代理真实角色后,一般还会做附属操作
1 | package com.ahulearn.demo01; |
- 客户角色:访问代理对象的人
1 | package com.ahulearn.demo01; |
代理模式的好处:
- 可以使真实角色的操作更加纯粹!不用去关注一些公共的业务
- 公共业务交给代理角色,实现业务分工
- 公共业务发生扩展的时候,方便集中管理!
缺点:
- 一个真实角色就会产生一个代理角色;代码量会翻倍开发效率变低
10.2 静态代理例子-加深理解
代码对应 spring-08-proxy-demo02
- 抽象角色
1 | package com.ahulearn.demo02; |
- 真实角色
1 | package com.ahulearn.demo02; |
- 代理角色
1 | package com.ahulearn.demo02; |
- 客户角色:访问代理对象的人
1 | package com.ahulearn.demo02; |
10.3 动态代理
底层:反射
- 动态代理和静态代理角色一样
- 动态代理的类是动态生成的,不是直接写好的!
- 动态代理分为两大类:基于接口的动态代理,基于类的动态代理
- 基于接口--JDK的动态接口【本节使用的方法】
- 基于来:cglib
- 基于字节码:JAVAssist 目前用于JBoss 应用服务器项目
需要了解两个类:
- Proxy: 代理
提供了创建动态代理类和示例的静态方法
1 | //方法调用句柄 |
-
InvocationHandler: 调用处理程序接口
代理实例的 调用处理程序 的接口
只有一个invoke接口方法,该方法使用反射的方式调用另一个方法。
1
Object invoke(Object proxy, Method method, Object[] args)
实验环境:
- 抽象角色:接口
1 | package com.ahulearn.demo03; |
- 真实角色:实体类
1 | package com.ahulearn.demo03; |
- 代理角色
1 | package com.ahulearn.demo03; |
- 用户角色
1 | package com.ahulearn.demo03; |
动态代理的好处:
-
可以使真实角色的操作更加纯粹!不用去关注一些公共的业务
-
公共业务交给代理角色,实现业务分工
-
公共业务发生扩展的时候,方便集中管理!
-
一个动态代理类代理的是一个接口,一般就是对应一类业务
-
一个动态代理类可以代理多个类,只要是实现了同一个接口即可!
11. AOP
11.1 什么是AOP?
AOP(Aspect Oriented Programming)意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
AOP可以说是OOP(Object-Oriented Programing,面向对象编程)的补充和完善。OOP引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需 要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。对于其他类型的代码,如安全性、异常处理和透明的持续性也是如此。这种散布在各处的无关的代码被称为横切(cross-cutting)代码,在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
而AOP技术则恰恰相反,它利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为 “Aspect”,即方面。所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低 模块间的耦合度,并有利于未来的可操作性和可维护性。AOP代表的是一个横向的关系,如果说“对象”是一个空心的圆柱体,其中封装的是对象的属性和行为; 那么面向方面编程的方法,就仿佛一把利刃,将这些空心圆柱体剖开,以获得其内部的消息。而剖开的切面,也就是所谓的“方面”了。然后它又以巧夺天功的妙手将这些剖开的切面复原,不留痕迹。
11.2 AOP在Spring中的作用
- 提供声明式事务;允许用户自定义切面
以下名词需要了解:
名称 | 说明 |
---|---|
Joinpoint(连接点) | 指那些被拦截到的点,在 Spring 中,可以被动态代理拦截目标类的方法。 |
Pointcut(切入点) | 指要对哪些 Joinpoint 进行拦截,即被拦截的连接点。 |
Advice(通知) | 指拦截到 Joinpoint 之后要做的事情,即对切入点增强的内容。 |
Target(目标) | 指代理的目标对象。 |
Weaving(植入) | 指把增强代码应用到目标上,生成代理对象的过程。 |
Proxy(代理) | 指生成的代理对象。 |
Aspect(切面) | 切入点和通知的结合。 |
SpringAOP中,通过Advice定义横切逻辑,Spring中支持5种类型的Advice:
即 Aop 在 不改变原有代码的情况下 , 去增加新的功能 .
11.3 使用Spring实现Aop
【重点】使用AOP织入包,需要导入一个依赖包!
1 | <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver --> |
11.3.1 第一种方式:通过 Spring API 实现
首先编写我们的业务接口和实现类
1 | package com.ahulearn.service; |
1 | package com.ahulearn.service; |
然后去写我们的增强类 , 我们编写两个 , 一个前置增强 一个后置增强
1 | package com.ahulearn.log; |
1 | package com.ahulearn.log; |
最后去spring的文件中注册 , 并实现aop切入实现 , 注意导入约束 .
1 |
|
测试类
1 | import com.ahulearn.service.UserService; |
Aop的重要性 : 很重要 . 一定要理解其中的思路 , 主要是思想的理解 .
Spring的Aop就是将公共的业务 (日志 , 安全等) 和领域业务结合起来 , 当执行领域业务时 , 将会把公共业务加进来 . 实现公共业务的重复利用 . 领域业务更纯粹 , 程序猿专注领域业务 , 其本质还是动态代理 .
11.3.2 第二种方式:自定义类来实现
目标业务类不变依旧是userServiceImpl
第一步 : 写我们自己的一个切入类
1 | package com.ahulearn.diy; |
去spring中配置
1 |
|
测试:
1 | import com.ahulearn.service.UserService; |
11.3.3 第三种方式: 注解实现
第一步:编写一个注解实现的增强类
1 | package com.ahulearn.diy; |
第二步:在Spring配置文件中,注册bean,并增加支持注解的配置
1 |
|
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动态代理。
12. 整合Mybatis
步骤:
- 导入相关jar包
- junit
- mybatis
- mysql数据库
- spring相关
- aop织入
- mybatis-spring 【新包】
1 | <dependencies> |
-
编写配置文件
-
测试
12.1 回顾mybatis
连接数据库:右侧边栏:Database打开数据库栏;+号点Data Source; 选择MySQL
- 编写实体类 User.java
1 | package com.ahulearn.pojo; |
- 编写核心配置文件: mybatis-config.xml
1 |
|
- 编写接口: UserMapper.java
1 | package com.ahulearn.mapper; |
- 编写指令配置:UserMapper.xml
1 |
|
- 测试
1 | import com.ahulearn.mapper.UserMapper; |
12.2 Spring整合mybatis方式一
- 保留上方回顾mybatis中的
com.ahulearn.pojo.User
、com.ahulearn.mapper.UserMapper
、com.ahulearn.mapper.UserMapper.xml
- 配置数据源替换mybaits的数据源:
spring-mapper.xml
1 |
|
原mybatis配置文件简化为下面的形式:mybatis-config.xml
1 |
|
- sqlSessionFactory:beans.xml中
1 | <!--sqlSessionFactory: 工厂--> |
- sqlSessionTemplate:beans.xml中
1 | <!--SqlSession模板--> |
- 接口实现类: UserMapperImpl
1 | package com.ahulearn.mapper; |
- 将实现类注入到Spring中
1 | <!--注入接口实现类,执行的sql语句--> |
- 测试使用
1 | //使用mybatis-spring |
12.3 Spring整合mybatis方式二
- 修改UserMapperImpl
1 | package com.ahulearn.mapper; |
- 修改beans.xml
1 |
|
- 测试
1 |
|
13. 声明式事务
1. 回顾事务
- 把一组业务当成一个业务来做,要么都成功,要么失败
- 事务在项目开发中,十分重要,涉及到数据的一致性问题!
- 确保完整行和一致性
事务四个属性ACID
-
原子性(atomicity)
-
- 事务是原子性操作,由一系列动作组成,事务的原子性确保动作要么全部完成,要么完全不起作用
-
一致性(consistency)
-
- 一旦所有事务动作完成,事务就要被提交。数据和资源处于一种满足业务规则的一致性状态中
-
隔离性(isolation)
-
- 可能多个事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏
-
持久性(durability)
-
- 事务一旦完成,无论系统发生什么错误,结果都不会受到影响。通常情况下,事务的结果被写到持久化存储器中
2. spring 中的事务管理
Spring在不同的事务管理API之上定义了一个抽象层,使得开发人员不必了解底层的事务管理API就可以使用Spring的事务管理机制。Spring支持编程式事务管理和声明式的事务管理。
编程式事务管理
- 将事务管理代码嵌到业务方法中来控制事务的提交和回滚
- 缺点:必须在每个事务操作业务逻辑中包含额外的事务管理代码
声明式事务管理
- 一般情况下比编程式事务好用。
- 将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。
- 将事务管理作为横切关注点,通过aop方法模块化。Spring中通过Spring AOP框架支持声明式事务管理。
使用Spring管理事务,注意头文件的约束导入 : tx
1 | xmlns:tx="http://www.springframework.org/schema/tx" |
导入aop
1 | xmlns:aop="http://www.springframework.org/schema/aop" |
事务管理器
- 无论使用Spring的哪种事务管理策略(编程式或者声明式)事务管理器都是必须的。
- 就是 Spring的核心事务管理抽象,管理封装了一组独立于技术的方法。
JDBC事务
1 | <!--配置声明式事务--> |
配置好事务管理器后我们需要去配置事务的通知
1 | <!--配置事务通知--> |
spring事务传播特性:
事务传播行为就是多个事务方法相互调用时,事务如何在这些方法间传播。spring支持7种事务传播行为:
- propagation_requierd:如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,这是最常见的选择。
- propagation_supports:支持当前事务,如果没有当前事务,就以非事务方法执行。
- propagation_mandatory:使用当前事务,如果没有当前事务,就抛出异常。
- propagation_required_new:新建事务,如果当前存在事务,把当前事务挂起。
- propagation_not_supported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
- propagation_never:以非事务方式执行操作,如果当前事务存在则抛出异常。
- propagation_nested:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与propagation_required类似的操作
Spring 默认的事务传播行为是 PROPAGATION_REQUIRED,它适合于绝大多数的情况。
假设 ServiveX#methodX() 都工作在事务环境下(即都被 Spring 事务增强了),假设程序中存在如下的调用链:Service1#method1()->Service2#method2()->Service3#method3(),那么这 3 个服务类的 3 个方法通过 Spring 的事务传播机制都工作在同一个事务中。
就好比,我们刚才的几个方法存在调用,所以会被放在一组事务当中!
配置AOP
导入aop的头文件!
1 | <!--配置aop织入事务--> |
进行测试
删掉刚才插入的数据,再次测试!
1 |
|
为什么需要配置事务?
- 如果不配置,就需要我们手动提交控制事务;
- 事务在项目开发过程非常重要,涉及到数据的一致性的问题,不容马虎!