0、基本介绍
Spring
是对业务层的操作,同时可以整合Mybatis
框架和Spring MVC
框架。下图是MVC
结构:
耦合:简单理解为程序间的依赖关系
- 类之间的依赖
- 方法间的依赖
解耦:降低程序间的依赖关系
实际开发中编译期不依赖,运行时才依赖。
解耦的思路:
第一步:使用反射来创建对象,而避免使用new
关键字。
第二步:通过读取配置文件来获取要创建的对象全限定类名。
一个创建Bean
对象的工厂。
Bean
:含有可重用组件的含义。
JavaBean
:用Java
语言编写的可重用组件。
JavaBean > 实体类
JavaBean
就是创建service
和dao
对象的。
第一个:需要一个配置文件来配置我们的service
和dao
配置的内容:唯一标识=全限定类名(key=value
)
第二个:通过读取配置文件中配置的内容,反射创建对象。
配置文件可以是xml
,也可以是properties
。
使用步骤:
- 创建
Properties
对象,读取配置文件。 - 通过类加载器读取流。
以上两步通过static
静态代码块加载。
加载文件代码块:
1 | /** |
单例对象:从始至终只有一个对象。
- 只被创建一次,从而类中的成员也就只会初始化一次。
多例对象:对象被创建多次,执行效率没有单例对象高。
目的:我们需要创建单例对象,只会初始化一次。
在创建工厂时,我们创建一个容器,将配置文件中的所有对象都提前创建好,存入到容器中,这样我们每次获取对象时,都是从这个容器中获取对象,保证我们始终获取的都是同一个对象。这里的容器就可以是Map
集合。
- 在
BeanFactory
中,以容器装载对象。 – 当获取对象时,是从容器中获取对象 - 获取对象,使用工厂获取对象,避免了
new
关键字创建出的对象,并由于工厂中,对象存储在容器中,保证了这里的对象的单例的。
为了降低Java
开发的复杂性,Spring
采取了以下4种关键策略:
- 基于
POJO
的轻量级和最小侵入性编程 - 通过依赖注入和面向接口实现松耦合
- 基于切面和惯例进行声明式编程
- 通过切面和模板减少样板式代码
Spring
与很多框架不同的一个关键点在于:很多框继承通过强迫应用继承它们的类或实现它们的接口从而导致应用与框架绑死。而Spring
不会强迫你实现Spring
规范的接口或接口或继承Spring
规范的类。
一、IOC – 控制反转
将new
的自主控制权交给了工厂。工厂再通过全限定类名决定得到获取到的对象。此时类无法再确定所获得到对象是否是自己所需要的(降低了耦合)。
使用步骤一:
创建配置文件
1
2
3
4
5
6
7
8
9
10
<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">
<!-- 将对象的创建交给spring来管理 -->
<bean id="accountService" class="cn.lihzi.service.impl.AccountServiceImpl"></bean>
<bean id="accountDao" class="cn.lihzi.dao.impl.AccountDaoImpl"></bean>
</beans>获取容器 –
spring
的核心容器,并根据id
获取对象1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class Client {
/**
* 获取spring的IOC核心容器,并根据id获取对象
*
* @param args
* @throws IllegalAccessException
* @throws InstantiationException
* @throws ClassNotFoundException
*/
public static void main(String[] args) {
// 1. 获取核心容器对象
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
// 2. 根据id获取Bean对象
AccountService service = (AccountService) ac.getBean("accountService");
AccountDao dao = ac.getBean("accountDao", AccountDao.class);
System.out.println(service);
System.out.println(dao);
// service.save();
}
}
1.1 ApplicationContext的三个常用实现类:
ClassPathXmlApplicationContext
它可以加载类路径下的配置文件,要求配置文件必须在类路径下。不在的话,加载不了。
FileSystemXmlApplicationContext
它可以加载磁盘任意路径下的配置文件(必须有访问权限)
AnnotationConfigApplicationContext
它用于读取注解创建的容器
1.2 核心容器的两个接口引发出的问题
ApplicationContext
它在构建核心容器时,创建对象采取的策略是采用立即加载的方法。也就是说,只要一读取完配置文件马上就创建配置文件中配置的对象。
试用情况:单例对象适用,更多采用此接口
BeanFactory
它在构建核心容器时,创建对象采取的策略是采用延迟加载的方式。也就是说,什么时候根据
id
获取对象,什么时候才是真正的创建对象。试用情况:多例对象适用。
1.3 Bean对象的细节
- 创建
Bean
的三种方式:
- 第一种方式
使用默认构造函数创建。
在spring
的配置文件中使用bean标签,配以id
和class
属性之后,且没有其他属性和标签时。
采用的就是默认构造函数创建bean对象,此时如果类中没有默认构造函数,则对象无法创建。
1 | <bean id="accountService" class="cn.lihzi.service.impl.AccountServiceImpl"></bean> |
- 第二种方式
使用工厂中的普通方法创建对象(使用某个类中的方法创建对象,并存入spring容器)
1 | <bean id="instanceFactory" class="cn.lihzi.factory.InstanceFactory"></bean> |
通过factory-bean
找到id
得到其对象,再通过factory-method
得到其方法对象。而其中的id="accountService"
是对应到容器中的key
。
- 第三种方式
使用工厂中的静态方法创建对象(使用某个类中的静态方法创建对象,并存入spring容器)。
1 | <bean id="accountService" class="cn.lihzi.factory.StaticInstanceFactory" factory-method="getAccountService"/> |
其中,第二种、第三种方式可以用来获取到jar
包中的方法对象。
Bean
的作用范围
默认的作用范围是单例。
bean
标签的scope
属性:
作用:用于指定bean
的作用范围
取值:常用的就是单例的和多例的
singleto
:单例的(默认值)prototype
:多例的request
:作用于web
应用的请求范围session
:作用于web
应用的会话范围global-session
:作用于集群环境的会话范围(全局会话范围),当不会集群环境时,它就是session
bean
对象的生命周期
单例对象
出生:当容器创建时对象出生
活着:只要容器还在,对象一直活着
死亡:容器销毁,对象消亡
总结:单例对象的生命周期和容器相同
多例对象
出生:当我们使用对象时
spring
框架为我们创建活着:对象只要是在使用过程中就一直活着
死亡:当对象长时间不用,且没有别的对象引用时,由
Java
的垃圾回收器回收
bean
配置文件中分别是,init-method
;destory-method
指定初始化和销毁时使用的对象方法。
注:创建应用组件之间协作的行为通常称为装配(wiring)。Spring有多种装配bean的方式,采用
XML
是很常见的一种装配方式。
Spring
通过应用上下文(Application Context)装载Bean
的定义并把它们组装起来。Spring
应用上下文全权负责对象的创建和组装。Spring
自带了多种应用上下文的实现,它们之间主要的区别仅仅在于如何加载配置。
1.4 Spring的依赖注入
依赖注入: Dependency Injection
。依赖注入会将所依赖的关系自动交给目标对象,而不是让对象自己去获取依赖。通过DI
,对象的依赖关系将由系统中负责协调各对象的第三方组件(就是容器)在创建对象的时候进行设定,对象无需自行创建或管理它们的依赖关系,依赖关系将被自动注入到需要它们的对象当中去。
在进行测试时,可以使用Mock
测试。所谓的Mock测试就是指在测试过程中,模拟出那些不容易获取或者不容易构造出来的对象,比如HttpServletRequest对象需要在Servlet容器中构造出来。
使用mock
框架Mockito去创建一个Quest
接口的mock
实现,通过该mock
就可以创建新的所属实例,并通过构造器注入这个mock
创建出的实例对象。(即通过构造器注入,来达到松耦合)
IOC
作用:降低程序间的耦合(依赖关系)
依赖关系的管理
- 交给
spring
来维护
在当前类需要用到其他类的对象,由spring
为我们提供,我们只需要在配置文件中说明
依赖关系的维护就称之为依赖注入。
依赖注入:
- 能注入的数据:三类
- 基本数据类型和
String
- 其他
bean
类型(在配置文件中或者注解配置过的bean
) - 复杂类型/集合类型
- 基本数据类型和
- 注入的方式:三种
- 使用构造函数提供
- 使用
set
方法提供 - 使用注解提供
1.4.1 构造函数注入(构造器注入)
1 | <!-- 构造函数注入: |
类的代码:
1 | public class AccountServiceImpl implements AccountService { |
配置文件:
1 | <bean id="accountService" class="cn.lihzi.service.impl.AccountServiceImpl"> |
1.4.2 set方法注入 – 适用于存在空参的构造方法(更常用的set方法注入)
涉及的标签:property
出现的位置:bean
标签的内部
标签的属性
name
:用于指定注入时所调用的set
方法名称value
:用于提供基本类型和String
类型的数据ref
:用于指定其他的bean
类型数据。它指的就是在spring
的Ioc
核心容器中出现的bean
对象。
优势:
- 创建对象时没有明确的限制,可以直接使用默认构造函数
弊端:
- 如果有某个成员必须有值,则获取对象是有可能
set
方法没有执行。
配置文件:
1 | <!-- set --> |
1.4.3 复杂类型的注入/集合类型的注入
用于给List
结构集合注入的标签:list
、array
、set
用于给Map
结构集合注入的标签:map
、props
结构相同,标签可以互换。
实体类:
1 | public class AccountServiceImpl2 implements AccountService { |
Bean
配置文件:
1 | <!-- 复杂对象的封装使用 --> |
二、基于注解的方式 – IOC
xml
起始的配置
1 | <bean id="accountService" class="cn.lihzi.service.impl.AccountServiceImpl" scope="" init-method="" destory-method=""> |
注解类别:
用于创建对象的(创建的对象存放至
Spring
容器中)其作用就和在
XML
配置文件中编写一个<bean>
标签实现的功能是一样的@Component
作用:用于把当前类对象写入
spring
容器中。属性:
- `value`:用于指定`bean`的`id`。当我们不写时,它的默认值是当前类名,且首字母小写。
@Controller
:一般用于表现层@Service
:一般用在业务层@Respository
:一般用于持久层
以上三个注解他们的作用和属性与Component
是一模一样的。
他们三个是Spring
框架为我们提供明确的三层使用的注解,使我们的三层对象更加清晰。
用于注入数据的(注入的对象是从
Spring
容器中获取)- 其作用就和在
xml
配置文件中的bean
标签中写一个<property>
标签的作用是一样的 @Autowired
- 作用:自动按照类型注入。只要容器中有唯一的一个
bean
对象类型和要注入的变量类型匹配,就可以注入成功。 - 出现位置:可以是变量上,也可以是方法上。
- 细节:在使用注解注入时,
set
方法就不是必须的了。 - 如果
IOC
容器中有多个类型匹配时:先找到同类别的,再根据变量名称进行注入。
- 作用:自动按照类型注入。只要容器中有唯一的一个
通过在
Spring
容器中符合数据类型的value
,并将value
值赋给该引用,当出现同类型的引用时,再根据变量名称进行赋值。@Qualifier
- 作用:在按照类中注入的基础之上再按照名称注入。它在给类成员注入时不能单独使用。但是在给方法参数注入时可以。(要和
@Autowired
配合使用) - 属性
value
:用于指定注入bean
的id
。
- 作用:在按照类中注入的基础之上再按照名称注入。它在给类成员注入时不能单独使用。但是在给方法参数注入时可以。(要和
@Resource
- 作用:直接按照
bean
的id
注入。它可以独立使用。 - 属性
name
:用于指定bean
的id
。
- 作用:直接按照
以上三个注入都只能注入其他
bean
类型的数据,而基本类型和String
类型无法使用上述注解实现。另外集合只能通过
xml
来实现。@Value
- 作用:用于注入基本类型和
String
类型的数据。 - 属性:
value
:用于指定数据的值。它可以使用spring
中SpEL
(也就是spring
的el
表达式)SpEL
的写法:${表达式}
。注意:其表达式写在哪里(JSP
、Mybatis
、Spring
…),就是从哪里获值。
- 作用:用于注入基本类型和
- 其作用就和在
用于改变作用范围的
- 其作用就和在
bean
标签中使用scope
属性实现的功能是一样的 @Scope
- 作用:用于指定
bean
的作用范围 - 属性:
value
:指定范围的取值。常用取值:singleton
、prototype
(默认为singleton
)
- 作用:用于指定
- 其作用就和在
生命周期相关
- 其作用就和在
bean
标签中使用init-method
和destory-method
的作用是一样的。 @PreDestroy
- 作用:用于指定销毁方法
@PostConstruct
- 作用:用于指定初始化方法
- 其作用就和在
注意:需要告知spring
在创建容器时要扫描的包、配置所需要的标签不是在bean
的约束中,而是一个名称为context
名称空间和约束中。其配置文件形式为:
1 |
|
2.1 实例:简单的数据库增删改查
XML
方式
service
层中,提供setter
方法,共xml
配置使用。
配置:
- 业务层对象
- 持久层对象
JDBC
对象- 数据库连接池
注意数据源的单例、多例造成的线程混乱问题
1 |
|
测试方法:
1 | public class AccountServiceTest { |
- 基于注解的
IOC
配置
注意一点:当用注解方式对变量进行注入时,setter
方法就不是必要的了。
xml
配置文件:
1 |
|
2.2 使用注解不带有xml
配置文件的使用(纯注解)
创建配置类 —
config
例如:
SpringConfiguration
:其作用同bean.xml
相同spring
中的新注解:@Configuration
- 作用:指定当前类是一个配置类
- 细节:当配置类作为
AnnotationConfigApplicationContext
对象创建的参数时,该注解可以不写。(原因是参数传递的是配置的Class
对象,就能够读取到这个类)
@ComponentScan
作用:用于通过注解指定
spring
在创建容器时要扫描的包属性:
value
:它和basePackages
的作用是一样的,都是用于指定创建容器时要扫描的包。我们使用此注解就等同于在xml
中配置了:<context:component-scan base-package="cn.lizhi"></context:component-scan>
@Bean
作用:用于把当前方法的返回值作为
bean
对象存入spring
的ioc
容器中属性:
name
:用于指定bean
的id
。当不写时,默认值是当前方法的名称
细节:
当我们使用注解配置方法时,如果方法需要传递参数,
Spring
框架会去容器中查找有没有可用的对应类型的bean
对象。查找的方式和Autowired
注解的作用是一样的。
@Import
- 作用:用于导入其他的配置类
- 属性
value
:用于指定其他配置类的字节码。当我们使用import
的注解之后,有Import
注解的类就是父配置类,而导入的都是子配置类。
在主配置类下配置@import
,@import
中的参数为value
数组,内容填写子配置类。这样也可以不用在子配置类下配置@Configuration
注解。
@PropertySource
- 作用:用于指定
properties
文件的位置。 - 属性:
value
:指定文件的名称和路径。- 关键字:
classpath
,表示类路径下。- 例如:
@PropertySource(classpath:jdbcConfig.properties)
- 例如:
注解位置在主配置文件下。
xml
和注解配置选择问题:自己写的类,选择采用注解
的方式;存在于jar
包中的选择用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
"cn.lizhi") (
public class SpringConfiguration {
"runner") // 用于将返回值存入容器中 -- 其中id设置为runner (
"prototype") // 将数据源设置成多例 (
public QueryRunner getRunner(DataSource dataSource) { // 获取QueryRunner对象 -- 参数为dataSource,故进一步再得到dataSource对象,见下方
return new QueryRunner(dataSource);
}
"dataSource") -- 容器中id为dataSource (
public DataSource getDataSource() {
ComboPooledDataSource cpds = new ComboPooledDataSource();
try {
cpds.setDriverClass("com.mysql.jdbc.Driver");
cpds.setJdbcUrl("jdbc:mysql://url:3306/draft");
cpds.setUser("username");
cpds.setPassword("password");
} catch (PropertyVetoException e) {
e.printStackTrace();
}
return cpds;
}
}测试用例:
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
42public class AccountServiceTest {
// 1. 获取对象
// private ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
private ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);
// 2. 得到业务层对象
private AccountService service = ac.getBean("accountService", AccountService.class);
public void findAll() {
List<Account> accounts = service.findAll();
for (Account account : accounts) {
System.out.println(account);
}
}
public void findById() {
Account account = service.findById(1);
System.out.println(account);
}
public void insert() {
Account account = new Account();
account.setName("Tom");
account.setMoney(800.8f);
service.insert(account);
}
public void update() {
Account account = service.findById(4);
account.setMoney(1000.0f);
service.update(account,account.getId());
}
public void delete() {
service.delete(4);
}
}注意:此时接口的实现方法使用的是
AnnotationConfigApplicationContext
。- 作用:用于指定
抽取子配置类
JdbcConfig
- 子配置类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
31public class JdbcConfig {
"${jdbc.driver}") (
private String driver;
"${jdbc.url}") (
private String url;
"${jdbc.user}") (
private String user;
"${jdbc.password}") (
private String password;
"runner") (
"prototype") // 将数据源设置成多例 (
public QueryRunner getRunner(DataSource dataSource) {
return new QueryRunner(dataSource);
}
"dataSource") (
public DataSource getDataSource() {
ComboPooledDataSource cpds = new ComboPooledDataSource();
try {
cpds.setDriverClass(driver);
cpds.setJdbcUrl(url);
cpds.setUser(user);
cpds.setPassword(password);
} catch (PropertyVetoException e) {
e.printStackTrace();
}
return cpds;
}
}SpringConfiguration
- 父配置类1
2
3
4
5.class) (JdbcConfig
@ComponentScan("cn.lizhi")
"classpath:jdbcConfig.properties") (
public class SpringConfiguration {
}jdbcConfig.properties
- 配置文件1
2
3
4com.mysql.jdbc.Driver =
jdbc:mysql://url:3306/draft =
username =
password =
2.3 spring整合junit问题
整合原因:
- 应用程序的入口 – main 方法
junit
单元测试中,没有main
方法也能执行
junit
集成了一个main
方法- 该方法就会判断当前测试类中哪些方法有
@Test
注解junit
就让有Test
注解的方法执行junit
中无法探测出是否存在spring
框架
- 在执行测试方法时,
junit
无法得知我们是否使用了spring
框架- 因此也就不会为我们读取配置文件/配置类创建
spring
核心容器综上:在执行测试方式时,没有
IOC
容器,就算谢了Autowired
注解,也无法实现注入
Spring
整合junit
的配置
导入
spring
整合junit
的jar
(坐标)使用
junit
提供的一个注解把原有的main
方法替换了,替换成spring
提供的@RunWith
注解配置–SpringJUnit4ClassRunner.class
告知
spring
的运行器,spring
和ioc
创建是基于xml
还是基于注解
的,并且说明位置。@ContextConfiguration
locations
:指定xml
文件的位置,加上classpath
关键字,表示在类路径下。classes
:指定注解所在地位置。å例如:@ContextConfiguration(classes=SpringConfiguration.class)
当我们使用
spring 5.x
版本的时候,要求junit
的jar
包必须是4.12
以上。
三、AOP – 导读
AOP
允许将遍布应用各处的功能分离出来形成可重用的组件。即通过AOP,可以使用各种功能层去包裹核心业务层。这些层以声明的方式灵活地应用到系统中,核心应用甚至根本不知道它们的存在,即将安全、事务和日志关注点与核心业务逻辑相分离。
3.1 案例-transfer(银行转账案例)
在service
接口中定义转账方法(其参数列表为转出用户,转入用户,转账金额),dao
接口中定义根据用户名称查找用户的方法。
当在数据库数据进行更新时(转账过程中),如果出现异常,可能就会出现破坏数据库的一致性操作。故下面要进行对事物的控制。
事务控制在service
层。
以上的问题引发了一个思考,就是获取的connection
应该全部由同一个connection
进行控制,要成功就一起成功,如果失败就一起失败。
解决办法:需要使用ThreadLocal
对象把Connection
与当前线程绑定,从而使一个线程中只有一个能控制事务的对象。
事务控制应该都是在业务层。
注意:在web
工程中,当tomcat
服务器启动时,会初始化线程池,当我们对tomcat
服务器进行访问时,便会从线程池中获取线程,同时当使用数据库连接池时,在获取连接以后,当我们对线程访问完毕以后,需要对线程进行归还,此时归还到线程池中的线程还在绑定着数据库的连接,所以在归还连接(线程)前(无论是线程或数据库连接),都需要将线程与数据库连接进行解绑,否则当我们再获取这个线程时,因为它绑定着数据库连接池中的那个连接,再使用时是无法使用的,因为这个数据连接已经被close
(归还)了,需要我们重新获取连接并进行绑定。
通过创建service
的代理对象的工厂解决事务上方法的耦合问题。即对service
类中的方法进行增强(增强的内容就是加入事务的控制)。
事务解决的整个思路:
创建
ConnectionUtils
工具类,通过ThreadLocal
绑定数据库连接。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/**
* 线程绑定
*/
public class ConnectionUtils {
private ThreadLocal<Connection> tl = new ThreadLocal<Connection>(); // 绑定的对象 -- Connection
private DataSource dataSource; // 获取数据源
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public Connection getThreadConnection() {
Connection conn = tl.get();
if (conn == null) { // 如果TreadLocal未绑定有连接,则从连接池中获取连接,并对其进行绑定
try {
conn = dataSource.getConnection();
tl.set(conn);
} catch (SQLException e) {
e.printStackTrace();
}
}
return conn;
}
/**
* 对其进行解绑
*/
public void remove() {
Connection conn = tl.get();
if (conn != null) {
tl.remove();
}
}
}创建事务管理类,用于在业务层对
SQL
进行事务管理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/**
* 对事物进行管理的工具类
*/
public class TransactionManager {
private ConnectionUtils connectionUtils;
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
/**
* 开启事务
*/
public void beginTransaction() {
try {
connectionUtils.getThreadConnection().setAutoCommit(false); // 关闭自动提交 -- 即使用此方法为开启事务(关闭了自动提交事务)
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 提交事务
*/
public void commit() {
try {
connectionUtils.getThreadConnection().commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 回滚事务
*/
public void rollback() {
try {
connectionUtils.getThreadConnection().rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 释放连接
*/
public void release() {
try {
connectionUtils.getThreadConnection().close();
connectionUtils.remove(); // 释放连接时,将线程与数据库连接池中的连接进行解绑
} catch (SQLException e) {
e.printStackTrace();
}
}
}dao
层代码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
41public class AccountDaoImpl implements AccountDao {
// 通过spring配置获取QueryRunner对象
private QueryRunner runner;
// 通过spring配置获取连接
private ConnectionUtils connectionUtils;
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
public void setRunner(QueryRunner runner) {
this.runner = runner;
}
public void update(Account account, Integer id) {
try {
runner.update(connectionUtils.getThreadConnection(), "update account set name=?,money=? where id=?", account.getName(), account.getMoney(), id);
} catch (SQLException e) {
e.printStackTrace();
}
}
public Account findByName(String name) {
Account account = null;
try {
List<Account> accounts = runner.query( "select * from account where name=?", new BeanListHandler<Account>(Account.class), name);
if (accounts == null || accounts.size() == 0) {
throw new RuntimeException("该用户不存在");
} else if (accounts.size() > 1) {
throw new RuntimeException("用户存在异常,存在两个异常");
} else {
account = accounts.get(0);
}
} catch (SQLException e) {
e.printStackTrace();
}
return account;
}connectionUtils.getThreadConnection()
用于获取连接。创建工厂类-用于创建
service
的代理对象的工厂1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46public class BeanFactory {
private AccountService accountService;
private TransactionManager tsManager;
public final void setTsManager(TransactionManager tsManager) {
this.tsManager = tsManager;
}
public void setAccountService(AccountService accountService) {
this.accountService = accountService;
}
/**
* 获取代理对象,对方法进行增强
* @return
*/
public AccountService getAccountService() {
AccountService proxy_accountService = (AccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(), new InvocationHandler() {
/**
* 增强对事务的控制
**/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object obj = null;
try {
// 1. 开启事务
tsManager.beginTransaction();
// 2. 执行语句
obj = method.invoke(accountService, args);
// 3. 提交事务
tsManager.commit();
return obj;
} catch (Exception e) {
// 4. 回滚事务
tsManager.rollback();
throw new RuntimeException(e);
} finally {
// 5. 关闭连接
tsManager.release();
}
}
});
return proxy_accountService;
}
}这里被代理的对象为
accountService
,当我们使用它的方法时,我们其实是在使用其代理对象(通过BeanFactory
中getAccountService
方法创建出的对象)中增强后的方法。其中,创建工厂中普通方法的配置方法:1
2
3
4
5
6
7
8
9<!-- 创建工厂类对象 -->
<bean id="beanFactory" class="cn.lizhi.factory.BeanFactory">
<!-- 被代理对象 -->
<property name="accountService" ref="accountService"></property>
<!-- 注入事务管理器 -->
<property name="tsManager" ref="transaction"></property>
</bean>
<!-- 创建工厂类方法对象 -->
<bean id="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean>例如被代理对象(
AccountService
)中的一处方法为:1
2
3
4
5
6
7
8
9
10
11
12public void transfer(String startName, String endName, Float money) {
Account startAccount = dao.findByName(startName); // 出款人账号 -- 会获取连接
Account endAccount = dao.findByName(endName); // 收款人账号 -- 会获取连接
Float startMoney = startAccount.getMoney();
startAccount.setMoney(startMoney - money);
Float endMoney = endAccount.getMoney();
endAccount.setMoney(endMoney + money);
dao.update(startAccount, startAccount.getId()); // 会获取连接
int a = 3 / 0;
dao.update(endAccount, endAccount.getId()); // 会获取连接
}即通过代理对象增强后,对其方法进行了事务管理,即对以下方法的执行,在其上下添加事务的管理。
1
obj = method.invoke(accountService, args);
增强的方法等价于:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public void transfer(String startName, String endName, Float money) {
try {
// 1. 开启事务
tsManager.beginTransaction();
// 2. 执行操作
Account startAccount = dao.findByName(startName); // 出款人账号 -- 会获取连接
Account endAccount = dao.findByName(endName); // 收款人账号 -- 会获取连接
Float startMoney = startAccount.getMoney();
startAccount.setMoney(startMoney - money);
Float endMoney = endAccount.getMoney();
endAccount.setMoney(endMoney + money);
dao.update(startAccount, startAccount.getId()); // 会获取连接
int a = 3 / 0;
dao.update(endAccount, endAccount.getId()); // 会获取连接
// 3. 提交事务
tsManager.commit();
} catch (Exception e) {
// 4. 回滚事务
tsManager.rollback();
throw new RuntimeException(e);
} finally {
// 5. 关闭连接
tsManager.release();
}
}由以上的比较,故能够很清晰的看到,通过动态代理的方法,可以简化代码量以及降低方法间的耦合问题。
如果,采用上面一般的方式,即在业务层中所有的方法都要加入事务管理的相关代码,这样就增加了代码的冗余;其次,如果我们对
TransactionManager
类中关于事务管理的方法名进行修改,那么在业务层中相应调用事务的方法名也都要修改。如果,采用动态代理的方式:
- 我们关于事务管理的代码只需要写一次(工厂类中的代理对象)。
TransactionManage
类中事务管理相关的方法名修改后,只需要在代理对象中对增强的方法进行修改即可。
最后依赖注入的对象是代理对象
1
2
3
"proxyAccountService") (
private AccountService service = null;
四、Spring AOP
4.1 AOP基本介绍
定义:
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
作用:
- 在程序运行期间,不修改源码对已有方法进行增强。
优势:
- 减少重复代码
- 提高开发效率
- 维护方便
实现方式:
- 使用动态代理技术
4.2 Spring中的AOP介绍
4.2.1 AOP相关术语介绍及使用介绍
Joinpoint(连接点)
所谓连接点是指那些被拦截的点。在
Spring
中,这些点指的是方法,因为Spring
只支持方法类型的连接点。即:在业务逻辑中的全部方法。Pointcut(切入点)
所谓切入点是指我们要对哪些
Joinpoint
进行拦截的定义。即:在业务逻辑中,那些被增强的方法。所以,有些方法是连接点,但不是切入点,因为没有被增强。所以得出:所有的切入点都是连接点,但并不一定所有的连接点都是切入点。(只有被增强的连接点,才是切入点)
Advice(通知/增强)
所谓通知,是指拦截到
Joinpoint
之后所要做的事情就是通知。(即,想要增加的功能,事先定义好,然后在想要用的地方添加通知即可)通知的类型:
- 前置通知:在
method.invoke()
之前执行的方法 - 后置通知:在
method.invoke()
之后执行的方法 - 异常通知:
catch
中的代码 - 最终通知:
finally
中的代码 - 环绕通知:整个
invoke
(public Object invoke..
)方法在执行就是环绕通知,即在环绕通知中有明确的切入点方法调用。 – 最强大的一个通知
- 前置通知:在
Introduction(引介)
引介是一种特殊的通知在不修改类代码的前提下,
Introduction
可以在运行期为类动态地添加一些方法或Field
。Target(目标对象)
代理的目标对象。(被代理对象)
Weaving(织入)
是指把增强应用到目标对象来创建新的代理对象的过程。
Spring
采用动态代理织入,而AspectJ
采用编译期织入和类装载期织入。Proxy(代理)
一个类被
AOP
织入增强后,就产生一个结果代理类。Aspect(切面)
是切入点和通知(引介)的结合。
Spring
框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类型,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。
将公共代码作为通知(即把通知Bean
交给Spring
来管理),由AOP
配置织入到切入点。
Logger
类作为通知:
1 | /** |
以Logger
类为例,aop
相关配置步骤:
配置
Spring
的IOC
,把service
对象配置进来。spring
中基于XML
的aop
配置步骤:把通知
Bean
交给spring
来管理使用
aop:config
标签表明开始AOP
的配置使用
aop:aspect
标签表明配置切面id
属性:是给切面提供一个唯一的标识ref
属性:是指定通知类bean
的Id
在
aop:aspect
标签的内部使用对应标签来配置通知的类型在
Logger
类中的pringLog
方法是在切入点方法执行之前,所以是前置通知aop:before
表示配置前置通知method
属性:用于指定Logger
类中哪个方法是前置通知。pointcut
属性:用于指定切入点表达式,该表达式的含义是对业务层中哪些方法增强
切入点表达式的写法:
关键字:
execution(表达式)
表达式:
访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表)
根据以上说明,见下方配置:
1 |
|
切入点表达式的写法
全通配写法
* *..*.*(..)
原因:
访问修饰符可以省略
void cn.lizhi.service.impl.AccountServiceImpl.saveAccount()
返回值可以改成任意类型的返回值
* cn.lizhi.service.impl.AccountServiceImpl.saveAccount()
包名可以使用通配符,表示任意包。但是有几级包,就需要几个
*.
* *.*.*.*.AccountServiceImpl.saveAccount()
包名可以使用
..
表示当前包及其子包* *..AccountServiceImpl.saveAccount()
类名和方法名都可以使用
*
来实现通配* *..*.*()
参数列表:
可以直接写数据类型:
- 基本类型直接写名称 例如:
int
- 引用类型写包名.类名的方式 例如:
java.lang.String
- 基本类型直接写名称 例如:
可以使用通配符表示任意类型,但是必须有参数
可以使用
..
表示有无参数均可,有参数可以是任意类型* *..*.*(..)
实际开发中切入点表达式的通常写法:
切到业务层实现类下的所有方法
* cn.lizhi.service.impl.*.*(..)
总结:以上是正则表达式的应用,正则表达式中
*
代表0次或无限次扩展;.
代表任何单个字符。
4.1.2 四种常用通知类型
- 前置通知:在切入点方法执行之前执行,类比动态代理中的
method.invoke
执行之前的开启事务方法 –before
- 后置通知:在切入点方法正常执行之后执行。它和异常通知永远只能执行一个,类比事务中的
commit
–after-returning
- 异常通知:在切入点方法执行产生异常之后执行。它和后置通知永远只能执行一个,类比
catch
代码块中的rollback
。–after-throwing
- 最终通知:无论切入点方法是否正常执行,它都会在其后面执行,类比
finally
代码块。 –after
最终配置文件:
1 |
|
其中这个切入点表达式的配置
1 | <aop:pointcut id="pt1" expression="execution(* cn.lizhi.service.impl.*.*(..))"/> |
id
属性用于指定表达式的唯一标识。
expression
属性用于指定表达式内容。
同时,此标签写在aop:aspect
标签内部只能当前切面使用。
当它写在aop:aspect
外面时,此时就变成了所有切面可用。
步骤:
- 把需要切入的对象,声明一个
bean
- 在
<aop:aspect>
元素中引用该bean
,为了进一步定义切面 - 声明
<aop:before>
等标签,即声明前置通知、后置通知 pointcut-ref
属性都引用了名字为pt1
的切入点。该切入点是在前边的<pointcut>
元素中定义的,并配置expression
属性来选择所应用的通知。
通过少量的XML
配置,就可以把logger
声明为一个切面。
这样做的好处是,logger
类仍然是一个POJO
,代码没有任何的更改,但通过以上的修改,logger
可以作为一个切面进行使用了;同时,最重要的是,被切入的对象完全不知道Logger
的存在。(注意:需要使用的切面,仍然先需要定义成一个bean
)
4.1.3 环绕通知
4.1.3.1 问题
当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了。
4.1.3.2 分析
通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用,而我们的代码代码中没有。
4.1.3.3 解决
Spring
框架为我们提供了一个接口:ProceedingJoinPoint
。该接口有一个方法proceed()
,此方法就相当于明确调用切入点方法。
该接口可以作为环绕通知的方法参数,在程序执行时,spring
框架会为我们提供该接口的实现类供我们使用。
使用:
1 | public Object aroundPrintLog(ProceedingJoinPoint pjp) { |
从上面可以看出,环绕通知是Spring
框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。
4.2.2 基于注解的AOP配置
在
bean.xml
配置Spring
创建容器时要扫描的包1
2
3
4<!-- 配置扫描的包 -->
<context:component-scan base-package="cn.lizhi"></context:component-scan>
<!-- 配置Spring开启注解AOP的支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>在通知类中,开启注解
@Before
@AfterReturning
@AfterThrowing
@After
@Around
另外需要额外在通知类中,建立通知方法和切入点的关联,以上的通知注解中的参数就写关联方法的函数名:
1
2
3
4
5
6
7"execution(* cn.lizhi.service.impl.*.*(..))") (
private void pt1() {}
"pt1()") (
public void beforePrintLog() {
System.out.println("beforePrintLog记录了日志...");
}最后需要在配置文件中,开启
Spring
对注解AOP
的支持:1
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
或者采用纯注解的方式(定义配置类):
1
2
3
4
5
"cn.lizhi") (basePackages=
public class springConfiguration(){
}注意:使用注解的方式,操作四个普通类型的通知,可能会带来顺序错误的问题。例如:
beforePrintLog记录了日志…前置
模拟用户保存了…
afterPrintLog记录了日志…最终
afterReturningPrintLog记录了日志…后置注意:这里出现了
最终通知
在后置通知
之前出现的问题。这样会在事务控制中产生一个问题,以上面的银行转账为例,就是最终通知–release
操作会在后置通知
–commit
操作之前执行,那么会造成获取的连接不会是同一个连接(每次调用dao
方法时,都会获取连接,由于前面我们使用了ThreadLocal
对连接进行了绑定,所以此时获取的连接都是同一个连接,当对其进行release
之后,再次获取连接时,那么和之前的连接就都不是同一个连接,因为在release
方法中首先对数据库连接进行归还连接池操作,然后再将ThreadLocal
和数据库连接进行解绑),就没有对事务进行相应的控制。因为当你commit
时,会从连接池中重新获取连接再与线程进行绑定,而此时的连接并没有做任何的操作,所以commit
就是一个空的提交。因此,在使用注解的方式时,尽量使用
环绕通知
代替以上四个普通通知
,因为环绕通知中的代码执行一定是由我们自己控制编写的。
五、Spring事务相关
5.1 Spring中JdbcTemplate 介绍
5.1.1 Spring JdbcTemplate的内置数据源
1 | // 准备数据源,spring的内置数据源 |
通过配置文件,利用IOC
思想简化以上操作:
1 |
|
代码实现:
1 | public static void main(String[] args) { |
5.1.2 JdbcTemplate的使用 – CRUD
1 | public static void main(String[] args) { |
5.1.3 JdbcDaoSupport的使用
可以用于抽取dao
层中的重复代码块。
例如:
dao
层的接口:
1 | public interface AccountDao { |
实现类:
1 | public class AccountDaoImpl implements AccountDao { |
其配置文件bean.xml
中bean
配置为:
1 | <!--配置账户的持久层--> |
当我们有多个dao
的实现方法时,那么这里的重复代码块:
1 | private JdbcTemplate template = null; |
所以这里就引发了我们的一个思考,是否可以把这些重复的代码块进行抽取,作为一个单独的类,然后当我们的实现类去继承这个类,进而简化我们的代码。答案是可以的。
现在我们对这个类进行抽取:
1 | public class JdbcDaoSupport { |
那么我们原有的实现类就需要继承这个类:
1 | public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao { |
从上面的bean.xml
配置文件中,我们看到需要单独对JdbcTemplate
进行配置,我们是否可以将JdbcTemplate
和DataSource
进行统一配置呢。我们将DataSource
同样创建在JdbcDaoSupport
类中:
1 | public class JdbcDaoSupport { |
在这里我们创建了一个DataSource
对象,并改写了其set
方法。作用:如果我们直接传递template
对象,那么template
理所应当有值(有set
方法);如果我们没有传递template
对象,我们可以通过DataSource
来获取template
对象,由于我们改写了其set
方法,我们同样可以使template
有值(创建了有参构造函数的template
,并向其传递了dataSource
)。
此时bean.xml
配置文件中bean
的配置为:
1 | <!--配置账户的持久层--> |
从上面的bean.xml
文件也可以看出,在创建accountDao
时,我们是向其注入了dataSource
,所以进而就直接触发其set
方法(因为accountDaoImpl
继承了JdbcDaoSupport
),判断template
是否有值,如果没有值,就调用其父类中的createJdbcTemplate
方法,其中传递的参数dataSource
是来自于bean.xml
配置文件中的id=dataSource
的bean
依赖注入。
以上的操作其实Spring
已经帮我们实现了,不需要我们自己手动构建(也算是自己练习了一下源码)。截取其部分源码:
1 | public abstract class JdbcDaoSupport extends DaoSupport { |
是不是同样的存在createJdbcTemplate(DataSource dataSource)
和getJdbcTemplate()
方法。从上面的源码中也可以看出Spring
在使用set
依赖注入时,可以不用定义其变量,只要定义其set
属性方法即可。
最后,以上这种继承的方法适用于xml
配置文件的方法,不适用于注解开发的方式(因为无法在jar
包中的JdbcTemplate
上加注解,注入我们的数据类型)。
5.2 Spring中事务控制的API
5.2.1 Spring事务控制
JavaEE
体系进行分层开发,事务处理处于业务层,Spring
提供了分层设计业务层的事务处理解决方案。Spring
框架为我们提供了一组事务控制的接口Spring
的事务控制都是基于AOP
的,它既可以使用编程的方式实现,也可以使用配置的方法实现。
依赖导入:
1 | <dependency> |
5.2.2 Spring中事务控制的API介绍
PlatformTransactionManager
1
2
3
4
5
6
7public interface PlatformTransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
void commit(TransactionStatus var1) throws TransactionException;
void rollback(TransactionStatus var1) throws TransactionException;
}此接口存在
commit
、rollback
方法,即为通知bean
。PlatformTransactionManager
接口提供事务操作的方法,包含有3个具体的操作:
获取事务状态信息
TransactionStatus getTransaction(@Nullable TransactionDefinition var1)
- 提交事务
void commit(TransactionStatus var1)
- 回滚事务
void rollback(TransactionStatus var1)
我们需要使用的是其实现类:
org.springframework.jdbc.datasource.DataSourceTransactionManager
使用
Spring JDBC
或iBatis
进行持久化数据时使用org.springframework.orm.hibernate5.HibernateTransactionManager
使用
Hibernate
版本进行持久化数据时使用
- 提交事务
TransactionDefinition
获取事务对象名称
String getName()
获取事务隔离级别:有四个隔离级别,
Spring
默认使用的是数据库的隔离级别int getIsolationLevel()
获取事务传播行为:定义什么时候需要用事务(增、删、改),什么时候事务可有可无(查询)
int getPropagationBehavior()
获取事务超时时间
int getTimeout()
获取事务是否只读(建议查询方法下为只读)
boolean isReadOnly()
读写型事务:增加、删除、修改开启事务
只读型事务:执行查询时,也会开启事务
5.2.3 事务的隔离级别
事务隔离级别反映事务提交并发访问时的处理态度
ISOLATION_DEFAULT
- 默认级别,归属下列某一种
ISOLATION_READ_UNCOMMITTED
- 可以读取未提交数据
ISOLATION_READ_COMMITTED
- 只能读取已提交数据,解决脏读问题(
Oracle
默认级别)
- 只能读取已提交数据,解决脏读问题(
ISOLATION_REPEATABLE_READ
- 是否读取其他事务提交修改后的数据,解决不可重复读问题(
MySQL
默认级别)
- 是否读取其他事务提交修改后的数据,解决不可重复读问题(
ISOLATION_SERIALIZABLE
- 是否读取其他事务提交添加后的数据,解决幻读问题
5.2.4 事务的隔离级别
REQUIRED
:如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。一般的选择(默认值)SUPPORTS
:支持当前事务,如果当前没有事务,就以非事务方式执行(没有事务)MANDATORY
:使用当前的事务,如果当前没有事务,就抛出异常REQUERS_NEW
:新建事务,如果当前在事务中,把当前事务挂起。NOT_SUPPORTED
:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。NEVER
:以非事务方式运行,如果当前存在事务,抛出异常。NESTED
:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行REQUIRED
类似的操作。
5.2.5 TransactionStatus接口
此接口提供的是事务具体的运行状态,TransacitonStatus
接口描述了某个时间点上事务对象的状态信息,包含有6个具体的操作:
- 刷新事务
void flush()
- 获取是否存在存储点(相当于回滚的断点,回滚时不是回滚到最初的开始,而是回滚到设置的存储点处)
boolean hasSavepoint()
- 获取事务是否完成
boolean isCompleted()
- 获取事务是否为新的事务
boolean isNewTransaction()
- 获取事务是否回滚
boolean isRollbackOnly()
- 设置事务回滚
void setRollbackOnly()
5.3 Spring中基于XML
的声明式事务控制配置步骤
配置事务管理器
1
2
3<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dateSource"></property>
</bean>配置事务的通知
此时我们需要导入事务的约束
tx
名称空间和约束,同时也需要aop
使用
tx:advice
标签配置事务通知- 属性:
id
:给事务通知起一个唯一标识transaction-manager
:给事务通知提供一个事务管理器引用
1
2
3<!-- 配置事务通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
</tx:advice>- 属性:
配置
AOP
中的通用切入点表达式1
2
3
4<aop:config>
<!-- 通用表达式配置 -->
<aop:pointcut id="pt1" expression="execution(* cn.lizhi.service.*.*(..))"/>
</aop:config>建立事务通知和切入点表达式的对应关系
1
2
3
4
5<aop:config>
<!-- 通用表达式配置 -->
<aop:pointcut id="pt1" expression="execution(* cn.lizhi.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"></aop:advisor>
</aop:config>配置事务的属性 –
tx:attributes
是在事务的通知
tx:advice
标签的内部tx:method
name
属性:表明业务层中哪个方法需要被事务控制。
配置事务的属性:
isolation
:用于指定事务的隔离级别。默认值是DEFAULT
,表示使用数据库的默认隔离级别。propagation
:用于指定事务的传播行为。默认值是REQUIRED
,表示一定会有事务,增删改的选择。查询方法可以选择SUPPORTS
。read-only
:用于指定事务是否只读。只有查询方法才能设置为true
。默认值是false
,表示读写。timeout
:用于指定事务的超时时间,默认值是-1,表示永不超时。如果指定了数值,以秒为单位。rollback-for
:用于指定一个异常。当产生该异常时,事务回滚,产生其他异常时,事务不回滚。没有默认值,表示任何异常都回滚。no-rollback-for
:用于指定一个异常,当产生异常时,事务不回滚,产生其他异常时事务回滚。没有默认值,表示任何异常都回滚。
1
2
3
4
5
6
7
8
9<!-- 配置事务通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 增删改事务的控制 -->
<tx:method name="transfer" propagation="REQUIRED" read-only="false"/>
<!-- 查询方法事务的控制 -->
<tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
</tx:attributes>
</tx:advice>需要命名规范,通过通配符配置方法名。
最终bean.xml
中的配置为:
1 |
|
5.4 Spring中基于注解的声明式事务控制配置步骤
配置事务管理器
1
2
3
4<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dateSource"></property>
</bean>开启
spring
对注解事务的支持1
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
在需要事务支持的地方使用
@Transaction
注解
以上中的注意事项有:因为是使用注解式配置,所以在dao
层,不能通过继承JdbcDaoSupport
的方式简化我们的代码,故需要在bean.xml
中对SpringJdbcTemplate
进行配置。
5.4.1 基于纯注解的声明式事务控制
通过配置类的方式实现。
SpringConfig
配置类1
2
3
4
5
6"cn.lizhi") (
.class, TransactionConfig.class}) ({JdbcConfig
@PropertySource("classpath:jdbcConfig.properties")
public class SpringConfig {
}JdbcConfig
配置类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
26public class JdbcConfig {
"${driver}") (
private String driver;
"${username}") (
private String username;
"${password}") (
private String password;
"${url}") (
private String url;
"template") (
public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
"dataSource") (
public DataSource getDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setUrl(url);
return dataSource;
}
}TransactionConfig
配置类1
2
3
4
5
6
7
8
9public class TransactionConfig {
"transactionManager") (
public DataSourceTransactionManager getTransactionManager(DataSource dataSource) {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}
5.5 Spring编程式事务控制
因为我们的事务管理都是由Spring
进行控制,所以我们都需要进行事务管理器的配置。
1 | <!-- 配置事务管理器 --> |
当有了事务管理器之后,Spring
同样为我们提供了事务模板,供我们具体使用事务控制的相关方法(相当于代理对象,对业务层方法进行增强):
1 | <!-- 事务模板对象 --> |
查看具体的TransactionTemplate
源码,其中一段为:
1 |
|
在上面的异常代码块中,可以看出,当执行不通过时,执行this.rollbackOnException(status, var5)
,进行事务回滚;当执行通过时,try
代码块中执行事务状态result = action.doInTransaction(status)
,最后执行this.transactionManager.commit(status)
,进行事务的提交。
具体使用:
模仿TransactionTemplate
中的execute
方法,在业务层中需要事务控制的方法中,执行该方法,该方法中的参数是一个接口,需要我们自己实现,其内容就填写我们需要控制的业务具体代码块。
1 | public void transfer(final String startName, final String endName, final Float money) { |
由于编程式的事务控制,又增加了代码的冗余,违背了AOP
的初心,所以实际中很少使用这种事务控制方式。
六、Spring整合JavaWeb
整合步骤:
导包 —— Spring相关的Jar包
写配置
- 将所有组件加入容器中,并能正确获取
@Controller
:Servlet
层;但是不能标注在Servlet
层,因为这个对象是Tomcat创建的,而非Spring
容器进行创建的。@Service
:业务逻辑层@Repository
:dao层@Component
:其他组件
- 将所有组件加入容器中,并能正确获取
每个组件之间自动装配
配置出声明式事务
- 配置数据源。配置数据源之前,可抽离出
JDBC
配置文件。 JDBCTemplate
操作数据库。给其注入数据源。- 配置事务管理器 – 让其控制住数据源
- 注解方式
- XML配置方式
- 配置数据源。配置数据源之前,可抽离出
IOC容器创建和销毁都要在合适的时机完成;
1
2
3
4
5
6项目启动:{
IOC创建完成
}
项目销毁:{
IOC销毁
}通过监听器完成这项工作,监听器是Tomcat中的,因此,配置在web.xml中。采用Spring提供的监听器(ContextLoaderListener);容器位置,即为Spring配置文件。
这个监听器创建好的IOC容器在ContextLoader —— 这个属性就是IoC容器
1
private WebApplicationContext context;
通过静态方法能获取——
getCurrentWebApplicationContext