0、基本介绍

Spring是对业务层的操作,同时可以整合Mybatis框架和Spring MVC框架。下图是MVC结构:

耦合:简单理解为程序间的依赖关系

  • 类之间的依赖
  • 方法间的依赖

解耦:降低程序间的依赖关系

实际开发中编译期不依赖,运行时才依赖。

解耦的思路:

第一步:使用反射来创建对象,而避免使用new关键字。

第二步:通过读取配置文件来获取要创建的对象全限定类名。

一个创建Bean对象的工厂。

Bean:含有可重用组件的含义。

JavaBean:用Java语言编写的可重用组件。

  • JavaBean > 实体类
  • JavaBean就是创建servicedao对象的。

第一个:需要一个配置文件来配置我们的servicedao配置的内容:唯一标识=全限定类名(key=value)

第二个:通过读取配置文件中配置的内容,反射创建对象。

配置文件可以是xml,也可以是properties

使用步骤:

  1. 创建Properties对象,读取配置文件。
  2. 通过类加载器读取流。

以上两步通过static静态代码块加载。

加载文件代码块:

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
/**
* 模拟工厂进行解耦,一个创建Bean对象的工厂
*/
public class BeanFactory {

//定义一个Properties对象
private static Properties props;

//定义一个Map,用于存放我们要创建的对象。我们把它称之为容器
private static Map<String,Object> beans;

//使用静态代码块为Properties对象赋值
static {
try {
//实例化对象
props = new Properties();
//获取properties文件的流对象
InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
props.load(in);
//实例化容器
beans = new HashMap<String,Object>();
//取出配置文件中所有的Key
Enumeration keys = props.keys();
//遍历枚举 -- 用来获取全限定类名的唯一标识符(key)
while (keys.hasMoreElements()){
//取出每个Key
String key = keys.nextElement().toString();
//根据key获取value -- 全限定类名
String beanPath = props.getProperty(key);
//反射创建对象
Object value = Class.forName(beanPath).newInstance();
//把key和value存入容器中
beans.put(key,value);
}
}catch(Exception e){
throw new ExceptionInInitializerError("初始化properties失败!");
}
}

/**
* 根据bean的名称获取对象
* @param beanName
* @return
*/
public static Object getBean(String beanName){
return beans.get(beanName);
}

// private static Properties pro; //
// private static Map<String, Object> beans;// 定义一个map,用于存放我们要创建的对象,将之称之为容器。
//
// static {
// try {
// pro = new Properties();
// InputStream is = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties"); // 配置文件加载进内存
// pro.load(is);
// beans = new HashMap<String, Object>();
// // 取出配置文件中的所有的key
// Enumeration keys = pro.keys();
// // 将全限定类名和反射创建的对象组成key-value,存放到集合中,这样我们对同一个类对象,就是只是从bean的集合中获取,始终获取的都是同一个对象。
// while (keys.hasMoreElements()) {
// String key = keys.nextElement().toString();
// String beanPath = pro.getProperty(key);
// Object value = Class.forName(beanPath).newInstance();
// beans.put(key, value);
// }
// } catch (IOException e) {
// e.printStackTrace();
// } catch (IllegalAccessException e) {
// e.printStackTrace();
// } catch (InstantiationException e) {
// e.printStackTrace();
// } catch (ClassNotFoundException e) {
// e.printStackTrace();
// }
//
// }
//
// /**
// * 根据bean的名称获取对象
// *
// * @param beanName
// * @return
// */
// public static Object getBean(String beanName) {
// Object bean = null; // 创建一个对象引用
// bean = beans.get(beanName);
// return bean;
// }
//
}

单例对象:从始至终只有一个对象。

  • 只被创建一次,从而类中的成员也就只会初始化一次。

多例对象:对象被创建多次,执行效率没有单例对象高。

目的:我们需要创建单例对象,只会初始化一次。

在创建工厂时,我们创建一个容器,将配置文件中的所有对象都提前创建好,存入到容器中,这样我们每次获取对象时,都是从这个容器中获取对象,保证我们始终获取的都是同一个对象。这里的容器就可以是Map集合。

  1. BeanFactory中,以容器装载对象。 – 当获取对象时,是从容器中获取对象
  2. 获取对象,使用工厂获取对象,避免了new关键字创建出的对象,并由于工厂中,对象存储在容器中,保证了这里的对象的单例的。

为了降低Java开发的复杂性,Spring采取了以下4种关键策略:

  • 基于POJO的轻量级和最小侵入性编程
  • 通过依赖注入和面向接口实现松耦合
  • 基于切面和惯例进行声明式编程
  • 通过切面和模板减少样板式代码

Spring与很多框架不同的一个关键点在于:很多框继承通过强迫应用继承它们的类或实现它们的接口从而导致应用与框架绑死。而Spring不会强迫你实现Spring规范的接口或接口或继承Spring规范的类。

一、IOC – 控制反转

new的自主控制权交给了工厂。工厂再通过全限定类名决定得到获取到的对象。此时类无法再确定所获得到对象是否是自己所需要的(降低了耦合)。

使用步骤一:

  1. 创建配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <?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">

    <!-- 将对象的创建交给spring来管理 -->
    <bean id="accountService" class="cn.lihzi.service.impl.AccountServiceImpl"></bean>
    <bean id="accountDao" class="cn.lihzi.dao.impl.AccountDaoImpl"></bean>
    </beans>
  2. 获取容器 – spring的核心容器,并根据id获取对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public 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的三个常用实现类:

  1. ClassPathXmlApplicationContext

    它可以加载类路径下的配置文件,要求配置文件必须在类路径下。不在的话,加载不了。

  2. FileSystemXmlApplicationContext

    它可以加载磁盘任意路径下的配置文件(必须有访问权限)

  3. AnnotationConfigApplicationContext

    它用于读取注解创建的容器

1.2 核心容器的两个接口引发出的问题

  1. ApplicationContext

    它在构建核心容器时,创建对象采取的策略是采用立即加载的方法。也就是说,只要一读取完配置文件马上就创建配置文件中配置的对象。

    试用情况:单例对象适用,更多采用此接口

  2. BeanFactory

    它在构建核心容器时,创建对象采取的策略是采用延迟加载的方式。也就是说,什么时候根据id获取对象,什么时候才是真正的创建对象。

    试用情况:多例对象适用。

1.3 Bean对象的细节

  1. 创建Bean的三种方式:
  • 第一种方式

使用默认构造函数创建。
spring的配置文件中使用bean标签,配以idclass属性之后,且没有其他属性和标签时。
采用的就是默认构造函数创建bean对象,此时如果类中没有默认构造函数,则对象无法创建。

1
2
<bean id="accountService" class="cn.lihzi.service.impl.AccountServiceImpl"></bean>
<bean id="accountDao" class="cn.lihzi.dao.impl.AccountDaoImpl"></bean>
  • 第二种方式

使用工厂中的普通方法创建对象(使用某个类中的方法创建对象,并存入spring容器)

1
2
<bean id="instanceFactory" class="cn.lihzi.factory.InstanceFactory"></bean>
<bean id="accountService" factory-bean="instanceFactory" factory-method="getAccountService"/>

通过factory-bean找到id得到其对象,再通过factory-method得到其方法对象。而其中的id="accountService"是对应到容器中的key

  • 第三种方式

使用工厂中的静态方法创建对象(使用某个类中的静态方法创建对象,并存入spring容器)。

1
<bean id="accountService" class="cn.lihzi.factory.StaticInstanceFactory" factory-method="getAccountService"/>

其中,第二种、第三种方式可以用来获取到jar包中的方法对象。

  1. Bean的作用范围

默认的作用范围是单例

bean标签的scope属性:

作用:用于指定bean的作用范围

取值:常用的就是单例的和多例的

  • singleto:单例的(默认值)
  • prototype:多例的
  • request:作用于web应用的请求范围
  • session:作用于web应用的会话范围
  • global-session:作用于集群环境的会话范围(全局会话范围),当不会集群环境时,它就是session
  1. bean对象的生命周期
  • 单例对象

    出生:当容器创建时对象出生

    活着:只要容器还在,对象一直活着

    死亡:容器销毁,对象消亡

    总结:单例对象的生命周期和容器相同

  • 多例对象

    出生:当我们使用对象时spring框架为我们创建

    活着:对象只要是在使用过程中就一直活着

    死亡:当对象长时间不用,且没有别的对象引用时,由Java的垃圾回收器回收

bean配置文件中分别是,init-methoddestory-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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 构造函数注入:
使用的标签:constructor-arg
标签出现的位置:bean标签的内部
标签中的属性
type:用于指定要注入的数据的数据类型,该数据类型也是构造函数中某个或某些参数的类型(当构造函数中同时含有多个相同的数据类型时,就无法分别)
index:用于指定要注入的数据给构造函数中指定索引位置的参数赋值。索引的位置是从0开始
name:用于指定给构造函数中指定名称的参数赋值。(最常用,也是最直接的方式)
=============以上三个用于指定给构造函数中哪个参数赋值================
value:用于提供基本类型和String类型的数据
ref:用于指定其他的bean类型数据。它指的就是在spring的Ioc核心容器中出现过的bean对象。
特点:
在获取bean对象时,注入数据是必须的操作,否则对象无法创建成功。
缺点:
改变了bean对象的实例化方法,使我们在创建对象时,如果用不到这些数据,也必须提供。
-->

类的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AccountServiceImpl implements AccountService {

private String name;
private Integer age;
private Date birthday;

public AccountServiceImpl(String name, Integer age, Date birthday) {
this.name = name;
this.age = age;
this.birthday = birthday;
}

public void save() {
System.out.println("service方法执行.."+name+":"+age+":"+birthday);
}
}

配置文件:

1
2
3
4
5
6
7
<bean id="accountService" class="cn.lihzi.service.impl.AccountServiceImpl">
<constructor-arg name="name" value="Tom"></constructor-arg>
<constructor-arg name="age" value="18"></constructor-arg>
<constructor-arg name="birthday" ref="now"></constructor-arg>
</bean>
<!-- 以下是配置一个日期对象 反射方式创建Date的对象,再由id指定赋值-->
<bean id="now" class="java.util.Date"></bean>

1.4.2 set方法注入 – 适用于存在空参的构造方法(更常用的set方法注入)

涉及的标签:property

出现的位置:bean标签的内部

标签的属性

  • name:用于指定注入时所调用的set方法名称
  • value:用于提供基本类型和String类型的数据
  • ref:用于指定其他的bean类型数据。它指的就是在springIoc核心容器中出现的bean对象。

优势:

  • 创建对象时没有明确的限制,可以直接使用默认构造函数

弊端:

  • 如果有某个成员必须有值,则获取对象是有可能set方法没有执行。

配置文件:

1
2
3
4
5
6
    <!-- set -->
<bean id="accountService1" class="cn.lihzi.service.impl.AccountServiceImpl1">
<property name="age" value="18"></property>
<property name="name" value="Tom"></property>
<!-- <property name="birthday" ref="now"></property>-->
</bean>

1.4.3 复杂类型的注入/集合类型的注入

用于给List结构集合注入的标签:listarrayset

用于给Map结构集合注入的标签:mapprops

结构相同,标签可以互换。

实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class AccountServiceImpl2 implements AccountService {

private Map<String, String> map;
private List<String> list;
private Set<String> set;
private Properties properties;
private String[] str;

/*
省略了getter和setter方法
*/

public void save() {

System.out.println("str:"+Arrays.toString(str));
System.out.println("map:"+map);
System.out.println("list:"+list);
System.out.println("set:"+set);
System.out.println("properties:"+properties);
}
}

Bean配置文件:

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
  <!-- 复杂对象的封装使用 -->

<bean id="accountService2" class="cn.lihzi.service.impl.AccountServiceImpl2">
<property name="list">
<list>
<value>a</value>
<value>b</value>
<value>c</value>
</list>
</property>
<property name="str">
<list>
<value>a</value>
<value>b</value>
<value>c</value>
</list>
</property>
<property name="set">
<list>
<value>a</value>
<value>b</value>
<value>c</value>
</list>
</property>
<property name="map">
<map>
<entry key="name">
<value>Tom</value>
</entry>
<entry key="age" value="18"></entry>
</map>
</property>

<property name="properties">
<!-- <map>-->
<!-- <entry key="name">-->
<!-- <value>Tom</value>-->
<!-- </entry>-->
<!-- <entry key="age" value="18"></entry>-->
<!-- -->
<!-- </map>-->
<props>
<prop key="name">Tom</prop>
<prop key="age">18</prop>
</props>
</property>
</bean>

二、基于注解的方式 – IOC

xml起始的配置

1
2
3
<bean id="accountService" class="cn.lihzi.service.impl.AccountServiceImpl" scope="" init-method="" destory-method="">
<property name="" value="" ref=""></property>
</bean>

注解类别:

  1. 用于创建对象的(创建的对象存放至Spring容器中)

    • 其作用就和在XML配置文件中编写一个<bean>标签实现的功能是一样的

    • @Component

  2. 作用:用于把当前类对象写入spring容器中。

  3. 属性:

    - `value`:用于指定`bean`的`id`。当我们不写时,它的默认值是当前类名,且首字母小写。
  • @Controller:一般用于表现层

  • @Service:一般用在业务层

  • @Respository:一般用于持久层

以上三个注解他们的作用和属性与Component是一模一样的。

他们三个是Spring框架为我们提供明确的三层使用的注解,使我们的三层对象更加清晰。

  1. 用于注入数据的(注入的对象是从Spring容器中获取)

    • 其作用就和在xml配置文件中的bean标签中写一个<property>标签的作用是一样的
    • @Autowired
      • 作用:自动按照类型注入。只要容器中有唯一的一个bean对象类型和要注入的变量类型匹配,就可以注入成功。
      • 出现位置:可以是变量上,也可以是方法上。
      • 细节:在使用注解注入时,set方法就不是必须的了。
      • 如果IOC容器中有多个类型匹配时:先找到同类别的,再根据变量名称进行注入。

    自动注入流程

    通过在Spring容器中符合数据类型的value,并将value值赋给该引用,当出现同类型的引用时,再根据变量名称进行赋值。

    • @Qualifier
      • 作用:在按照类中注入的基础之上再按照名称注入。它在给类成员注入时不能单独使用。但是在给方法参数注入时可以。(要和@Autowired配合使用)
      • 属性
        • value:用于指定注入beanid
    • @Resource
      • 作用:直接按照beanid注入。它可以独立使用。
      • 属性
        • name:用于指定beanid

    以上三个注入都只能注入其他bean类型的数据,而基本类型和String类型无法使用上述注解实现。

    另外集合只能通过xml来实现。

    • @Value
      • 作用:用于注入基本类型和String类型的数据。
      • 属性:
        • value:用于指定数据的值。它可以使用springSpEL(也就是springel表达式)
        • SpEL的写法:${表达式}。注意:其表达式写在哪里(JSPMybatisSpring…),就是从哪里获值。
  2. 用于改变作用范围的

    • 其作用就和在bean标签中使用scope属性实现的功能是一样的
    • @Scope
      • 作用:用于指定bean的作用范围
      • 属性:
        • value:指定范围的取值。常用取值:singletonprototype(默认为singleton)
  3. 生命周期相关

    • 其作用就和在bean标签中使用init-methoddestory-method的作用是一样的。
    • @PreDestroy
      • 作用:用于指定销毁方法
    • @PostConstruct
      • 作用:用于指定初始化方法

注意:需要告知spring在创建容器时要扫描的包、配置所需要的标签不是在bean的约束中,而是一个名称为context名称空间和约束中。其配置文件形式为:

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
http://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="cn.lihzi"></context:component-scan> <!-- 扫描这个包下的所有包及其子包 -->

</beans>

2.1 实例:简单的数据库增删改查

  1. XML方式

service层中,提供setter方法,共xml配置使用。

配置:

  • 业务层对象
  • 持久层对象
  • JDBC对象
  • 数据库连接池

注意数据源的单例、多例造成的线程混乱问题

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

<!-- 创建service对象,并将service对象中的变量注入数据dao -->
<bean id="accountService" class="cn.lizhi.service.impl.AccountServiceImpl">
<property name="dao" ref="accountDao"></property>
</bean>

<!-- 创建dao对象,并给变量注入数据,runner -->
<bean id="accountDao" class="cn.lizhi.dao.impl.AccountDaoImpl">
<!-- 注入runner -->
<property name="runner" ref="queryRunner"></property>
</bean>

<!-- 创建QueryRunner对象 并采用构造方法的方式,注入数据源(这里采用的是有参构造方法,其参数就是dataSource数据源,故进而再创建dataSource对象) -->
<!-- QueryRunner对象,是单例对象。为了让多个dao在调用这个对象时,互不干扰,故采取多例的方法 -->
<bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
<!-- 注入数据源,方便sql语句的复用,不用每次都传入数据 -->
<constructor-arg name="ds" ref="dataSource"></constructor-arg>
</bean>

<!-- 配置数据源dataSource,给dataSource对象的变量注入数据-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://url:3306/draft"></property>
<property name="user" value="username"></property>
<property name="password" value="password"></property>
</bean>
</beans>

测试方法:

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
public class AccountServiceTest {

// 1. 获取对象
private ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
// 2. 得到业务层对象
private AccountService service = ac.getBean("accountService", AccountService.class);

@Test
public void findAll() {
List<Account> accounts = service.findAll();
for (Account account : accounts) {
System.out.println(account);
}
}

@Test
public void findById() {
Account account = service.findById(1);
System.out.println(account);
}

@Test
public void insert() {
Account account = new Account();
account.setName("Tom");
account.setMoney(800.8f);
service.insert(account);
}
}
  1. 基于注解的IOC配置

注意一点:当用注解方式对变量进行注入时,setter方法就不是必要的了。

xml配置文件:

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

<context:component-scan base-package="cn.lizhi"></context:component-scan> <!-- 扫描这个包下的所有包及其子包 -->
<!-- service和dao的对象通过注解的方法创建以及注入-->
<bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
<constructor-arg name="ds" ref="dataSource"></constructor-arg>
</bean>
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://url/draft"></property>
<property name="user" value="username"></property>
<property name="password" value="password"></property>
</bean>
</beans>

2.2 使用注解不带有xml配置文件的使用(纯注解)

  1. 创建配置类 — config

    例如:SpringConfiguration:其作用同bean.xml相同

    spring中的新注解:

    1. @Configuration

      • 作用:指定当前类是一个配置类
    • 细节:当配置类作为AnnotationConfigApplicationContext对象创建的参数时,该注解可以不写。(原因是参数传递的是配置的Class对象,就能够读取到这个类)
  2. @ComponentScan

    • 作用:用于通过注解指定spring在创建容器时要扫描的包

    • 属性:

      • value:它和basePackages的作用是一样的,都是用于指定创建容器时要扫描的包。我们使用此注解就等同于在xml中配置了:

        <context:component-scan base-package="cn.lizhi"></context:component-scan>

  3. @Bean

    • 作用:用于把当前方法的返回值作为bean对象存入springioc容器中

      • 属性:

        • name:用于指定beanid。当不写时,默认值是当前方法的名称
      • 细节:

        当我们使用注解配置方法时,如果方法需要传递参数,Spring框架会去容器中查找有没有可用的对应类型的bean对象。查找的方式和Autowired注解的作用是一样的。

  4. @Import

    • 作用:用于导入其他的配置类
    • 属性
      • value:用于指定其他配置类的字节码。当我们使用import的注解之后,有Import注解的类就是父配置类,而导入的都是子配置类。

在主配置类下配置@import@import中的参数为value数组,内容填写子配置类。这样也可以不用在子配置类下配置@Configuration注解。

  1. @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
    @Configuration
    @ComponentScan("cn.lizhi")
    public class SpringConfiguration {

    @Bean("runner") // 用于将返回值存入容器中 -- 其中id设置为runner
    @Scope("prototype") // 将数据源设置成多例
    public QueryRunner getRunner(DataSource dataSource) { // 获取QueryRunner对象 -- 参数为dataSource,故进一步再得到dataSource对象,见下方
    return new QueryRunner(dataSource);
    }

    @Bean("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
    42
    public 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);

    @Test
    public void findAll() {
    List<Account> accounts = service.findAll();
    for (Account account : accounts) {
    System.out.println(account);
    }
    }

    @Test
    public void findById() {
    Account account = service.findById(1);
    System.out.println(account);
    }

    @Test
    public void insert() {
    Account account = new Account();
    account.setName("Tom");
    account.setMoney(800.8f);
    service.insert(account);
    }

    @Test
    public void update() {
    Account account = service.findById(4);
    account.setMoney(1000.0f);
    service.update(account,account.getId());
    }

    @Test
    public void delete() {
    service.delete(4);
    }
    }

    注意:此时接口的实现方法使用的是AnnotationConfigApplicationContext

  2. 抽取子配置类

    1. 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
      31
      public class JdbcConfig {

      @Value("${jdbc.driver}")
      private String driver;
      @Value("${jdbc.url}")
      private String url;
      @Value("${jdbc.user}")
      private String user;
      @Value("${jdbc.password}")
      private String password;

      @Bean("runner")
      @Scope("prototype") // 将数据源设置成多例
      public QueryRunner getRunner(DataSource dataSource) {
      return new QueryRunner(dataSource);
      }

      @Bean("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;
      }
      }
    2. SpringConfiguration - 父配置类

      1
      2
      3
      4
      5
      @Import(JdbcConfig.class)
      @ComponentScan("cn.lizhi")
      @PropertySource("classpath:jdbcConfig.properties")
      public class SpringConfiguration {
      }
    3. jdbcConfig.properties - 配置文件

      1
      2
      3
      4
      jdbc.driver=com.mysql.jdbc.Driver
      jdbc.url=jdbc:mysql://url:3306/draft
      jdbc.user=username
      jdbc.password=password

2.3 spring整合junit问题

整合原因:

  1. 应用程序的入口 – main 方法
  2. junit单元测试中,没有main方法也能执行
    • junit集成了一个main方法
    • 该方法就会判断当前测试类中哪些方法有@Test注解
    • junit就让有Test注解的方法执行
  3. junit中无法探测出是否存在spring框架
    • 在执行测试方法时,junit无法得知我们是否使用了spring框架
    • 因此也就不会为我们读取配置文件/配置类创建spring核心容器

综上:在执行测试方式时,没有IOC容器,就算谢了Autowired注解,也无法实现注入

Spring整合junit的配置

  1. 导入spring整合junitjar(坐标)

  2. 使用junit提供的一个注解把原有的main方法替换了,替换成spring提供的 @RunWith注解配置–SpringJUnit4ClassRunner.class

  3. 告知spring的运行器,springioc创建是基于xml还是基于注解的,并且说明位置。

    • @ContextConfiguration

      • locations:指定xml文件的位置,加上classpath关键字,表示在类路径下。

      • classes:指定注解所在地位置。å例如:@ContextConfiguration(classes=SpringConfiguration.class)

      当我们使用spring 5.x版本的时候,要求junitjar包必须是4.12以上。

三、AOP – 导读

AOP允许将遍布应用各处的功能分离出来形成可重用的组件。即通过AOP,可以使用各种功能层去包裹核心业务层。这些层以声明的方式灵活地应用到系统中,核心应用甚至根本不知道它们的存在,即将安全、事务和日志关注点与核心业务逻辑相分离。

3.1 案例-transfer(银行转账案例)

service接口中定义转账方法(其参数列表为转出用户,转入用户,转账金额),dao接口中定义根据用户名称查找用户的方法。

当在数据库数据进行更新时(转账过程中),如果出现异常,可能就会出现破坏数据库的一致性操作。故下面要进行对事物的控制。

事务控制在service层。

事务控制

以上的问题引发了一个思考,就是获取的connection应该全部由同一个connection进行控制,要成功就一起成功,如果失败就一起失败。

解决办法:需要使用ThreadLocal对象把Connection与当前线程绑定,从而使一个线程中只有一个能控制事务的对象。

事务控制应该都是在业务层。

注意:在web工程中,当tomcat服务器启动时,会初始化线程池,当我们对tomcat服务器进行访问时,便会从线程池中获取线程,同时当使用数据库连接池时,在获取连接以后,当我们对线程访问完毕以后,需要对线程进行归还,此时归还到线程池中的线程还在绑定着数据库的连接,所以在归还连接(线程)前(无论是线程或数据库连接),都需要将线程与数据库连接进行解绑,否则当我们再获取这个线程时,因为它绑定着数据库连接池中的那个连接,再使用时是无法使用的,因为这个数据连接已经被close(归还)了,需要我们重新获取连接并进行绑定。

通过创建service的代理对象的工厂解决事务上方法的耦合问题。即对service类中的方法进行增强(增强的内容就是加入事务的控制)。

事务解决的整个思路:

  1. 创建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();
    }
    }
    }
  2. 创建事务管理类,用于在业务层对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();
    }
    }
    }
  3. 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
    41
    public 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()用于获取连接。

  4. 创建工厂类-用于创建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
    46
    public 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,当我们使用它的方法时,我们其实是在使用其代理对象(通过BeanFactorygetAccountService方法创建出的对象)中增强后的方法。其中,创建工厂中普通方法的配置方法:

    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
    12
    public 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
    25
    public 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类中关于事务管理的方法名进行修改,那么在业务层中相应调用事务的方法名也都要修改。

    如果,采用动态代理的方式:

    1. 我们关于事务管理的代码只需要写一次(工厂类中的代理对象)。
    2. TransactionManage类中事务管理相关的方法名修改后,只需要在代理对象中对增强的方法进行修改即可。
  5. 最后依赖注入的对象是代理对象

    1
    2
    3
    @Autowired
    @Qualifier("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相关术语介绍及使用介绍

  1. Joinpoint(连接点)

    所谓连接点是指那些被拦截的点。在Spring中,这些点指的是方法,因为Spring只支持方法类型的连接点。即:在业务逻辑中的全部方法。

  2. Pointcut(切入点)

    所谓切入点是指我们要对哪些Joinpoint进行拦截的定义。即:在业务逻辑中,那些被增强的方法。所以,有些方法是连接点,但不是切入点,因为没有被增强。

    所以得出:所有的切入点都是连接点,但并不一定所有的连接点都是切入点。(只有被增强的连接点,才是切入点)

  3. Advice(通知/增强)

    所谓通知,是指拦截到Joinpoint之后所要做的事情就是通知。(即,想要增加的功能,事先定义好,然后在想要用的地方添加通知即可)

    通知的类型:

    • 前置通知:在method.invoke()之前执行的方法
    • 后置通知:在method.invoke()之后执行的方法
    • 异常通知:catch中的代码
    • 最终通知:finally中的代码
    • 环绕通知:整个invoke(public Object invoke..)方法在执行就是环绕通知,即在环绕通知中有明确的切入点方法调用。 – 最强大的一个通知
  4. Introduction(引介)

    引介是一种特殊的通知在不修改类代码的前提下,Introduction可以在运行期为类动态地添加一些方法或Field

  5. Target(目标对象)

    代理的目标对象。(被代理对象)

  6. Weaving(织入)

    是指把增强应用到目标对象来创建新的代理对象的过程。

    Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。

  7. Proxy(代理)

    一个类被AOP织入增强后,就产生一个结果代理类。

  8. Aspect(切面)

    是切入点和通知(引介)的结合。

Spring框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类型,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。

将公共代码作为通知(即把通知Bean交给Spring来管理),由AOP配置织入到切入点。

Logger类作为通知:

1
2
3
4
5
6
7
8
9
10
11
/**
* 模拟记录日志的工具类,里面提供了公共的代码
*/
public class Logger {
/**
* 用于打印日志,计划让其在切入点方法执行之前执行(这里切入点方法就是业务层方法)
*/
public void printLog() {
System.out.println("记录了日志...");
}
}

Logger类为例,aop相关配置步骤:

  1. 配置SpringIOC,把service对象配置进来。

  2. spring中基于XMLaop配置步骤:

    1. 把通知Bean交给spring来管理

    2. 使用aop:config标签表明开始AOP的配置

    3. 使用aop:aspect标签表明配置切面

      • id属性:是给切面提供一个唯一的标识
      • ref属性:是指定通知类beanId
    4. aop:aspect标签的内部使用对应标签来配置通知的类型

      Logger类中的pringLog方法是在切入点方法执行之前,所以是前置通知

      aop:before 表示配置前置通知

      • method属性:用于指定Logger类中哪个方法是前置通知。
      • pointcut属性:用于指定切入点表达式,该表达式的含义是对业务层中哪些方法增强

      切入点表达式的写法:

      • 关键字:execution(表达式)

      • 表达式:

        访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表)

根据以上说明,见下方配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 配置Spring的IOC,把Service对象配置进来 -->
<bean id="accountService" class="cn.lizhi.service.impl.AccountServiceImpl"></bean>
<!-- 配置Logger类(通知) -->
<bean id="logger" class="cn.lizhi.utlis.Logger"></bean>
<!-- 配置AOP -->
<aop:config>
<!-- 配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置通知的类型,并且建立通知方法和切入点方法的关联-->
<aop:before method="printLog" pointcut="execution(public void cn.lizhi.service.impl.AccountServiceImpl.saveAccount())"></aop:before>
</aop:aspect>
</aop:config>
</beans>
  1. 切入点表达式的写法

    1. 全通配写法

      * *..*.*(..)

      原因:

      • 访问修饰符可以省略

        void cn.lizhi.service.impl.AccountServiceImpl.saveAccount()

      • 返回值可以改成任意类型的返回值

        * cn.lizhi.service.impl.AccountServiceImpl.saveAccount()

      • 包名可以使用通配符,表示任意包。但是有几级包,就需要几个*.

        * *.*.*.*.AccountServiceImpl.saveAccount()

      • 包名可以使用..表示当前包及其子包

        * *..AccountServiceImpl.saveAccount()

      • 类名和方法名都可以使用*来实现通配

        * *..*.*()

      • 参数列表:

        1. 可以直接写数据类型:

          • 基本类型直接写名称 例如:int
          • 引用类型写包名.类名的方式 例如:java.lang.String
        2. 可以使用通配符表示任意类型,但是必须有参数

        3. 可以使用..表示有无参数均可,有参数可以是任意类型

          * *..*.*(..)

      实际开发中切入点表达式的通常写法:

      • 切到业务层实现类下的所有方法

        * cn.lizhi.service.impl.*.*(..)

      总结:以上是正则表达式的应用,正则表达式中*代表0次或无限次扩展;.代表任何单个字符。

4.1.2 四种常用通知类型

  • 前置通知:在切入点方法执行之前执行,类比动态代理中的method.invoke执行之前的开启事务方法 – before
  • 后置通知:在切入点方法正常执行之后执行。它和异常通知永远只能执行一个,类比事务中的commitafter-returning
  • 异常通知:在切入点方法执行产生异常之后执行。它和后置通知永远只能执行一个,类比catch代码块中的rollback。– after-throwing
  • 最终通知:无论切入点方法是否正常执行,它都会在其后面执行,类比finally代码块。 – after

最终配置文件:

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
<?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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 配置Spring的IOC,把Service对象配置进来 -->
<bean id="accountService" class="cn.lizhi.service.impl.AccountServiceImpl"></bean>
<!-- 配置Logger类(通知) -->
<bean id="logger" class="cn.lizhi.utlis.Logger"></bean>
<!-- 配置AOP -->
<aop:config>
<aop:pointcut id="pt1" expression="execution(* cn.lizhi.service.impl.*.*(..))"/>
<!-- 配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置通知的类型,并且建立通知方法和切入点方法的关联-->
<aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>
<aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>
</aop:aspect>
</aop:config>
</beans>

其中这个切入点表达式的配置

1
<aop:pointcut id="pt1" expression="execution(* cn.lizhi.service.impl.*.*(..))"/>

id属性用于指定表达式的唯一标识。

expression属性用于指定表达式内容。

同时,此标签写在aop:aspect标签内部只能当前切面使用。

当它写在aop:aspect外面时,此时就变成了所有切面可用。

步骤:

  1. 把需要切入的对象,声明一个bean
  2. <aop:aspect>元素中引用该bean,为了进一步定义切面
  3. 声明<aop:before>等标签,即声明前置通知、后置通知
  4. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Object aroundPrintLog(ProceedingJoinPoint pjp) {
Object obj = null;
try {
System.out.println("Logger类中aroundPrintLog方法开始记录日志了...前置"); // 前置通知
Object[] args = pjp.getArgs(); // 得到方法执行所需要的参数
obj = pjp.proceed(args);// 明确调用业务层方法(切入点方法)
System.out.println("Logger类中aroundPrintLog方法开始记录日志了...后置"); // 后置通知
return obj;
} catch (Throwable throwable) {
System.out.println("Logger类中aroundPrintLog方法开始记录日志了...异常"); // 异常通知
throw new RuntimeException(throwable);
}finally {
System.out.println("Logger类中aroundPrintLog方法开始记录日志了...结束"); // 结束通知
}

}

从上面可以看出,环绕通知是Spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。

4.2.2 基于注解的AOP配置

  1. 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>
  2. 在通知类中,开启注解

    • @Before
    • @AfterReturning
    • @AfterThrowing
    • @After
    • @Around

    另外需要额外在通知类中,建立通知方法和切入点的关联,以上的通知注解中的参数就写关联方法的函数名:

    1
    2
    3
    4
    5
    6
    7
    @Pointcut("execution(* cn.lizhi.service.impl.*.*(..))")
    private void pt1() {}

    @Before("pt1()")
    public void beforePrintLog() {
    System.out.println("beforePrintLog记录了日志...");
    }

    最后需要在配置文件中,开启Spring对注解AOP的支持:

    1
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

    或者采用纯注解的方式(定义配置类):

    1
    2
    3
    4
    5
    @Configuration
    @ComponentScan(basePackages="cn.lizhi")
    @EnableAspectJAutoProxy
    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
2
3
4
5
6
7
8
9
10
// 准备数据源,spring的内置数据源
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver"); // 注入数据库驱动
ds.setUrl("jdbc:mysql://url:3306/draft"); // 连接地址
ds.setUsername("root"); // 用户名称
ds.setPassword("1234"); // 密码
// 1.创建JdbcTemplate对象
JdbcTemplate template = new JdbcTemplate();
// 注入数据源
template.setDataSource(ds);

通过配置文件,利用IOC思想简化以上操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?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">
<!-- 创建JdbcTemplate对象 -->
<bean id="template" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dateSource"></property>
</bean>

<!-- dataSource数据源 -->
<bean id="dateSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://url/draft"></property>
<property name="username" value="username"></property>
<property name="password" value="password"></property>
</bean>
</beans>

代码实现:

1
2
3
4
5
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");// 获取容器
JdbcTemplate template = ac.getBean("template", JdbcTemplate.class);// 获取template对象
template.execute("insert into account (name,money) values('Lisa',900)");// 执行操作
}

5.1.2 JdbcTemplate的使用 – CRUD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
JdbcTemplate template = ac.getBean("template", JdbcTemplate.class);
// template.execute("insert into account (name,money) values('Lisa',900)");
// 1.保存操作
template.update("insert into account(name,money) values(?,?)", "Ben", 800f);
// 2.更新操作
template.update("update account set name=?,money=? where id=?", "Gu", 900f, 5);
// 3.删除操作
template.update("delete from account where id=?", 6);
// 4.查询所有
List<Account> accounts = template.query("select * from account", new BeanPropertyRowMapper<Account>(Account.class));
// 5.查询单个
Account account = template.queryForObject("select * from account where id = ?", new BeanPropertyRowMapper<Account>(Account.class), 10);
}

5.1.3 JdbcDaoSupport的使用

可以用于抽取dao层中的重复代码块。

例如:

dao层的接口:

1
2
3
4
5
6
7
8
public interface AccountDao {

//更新数据
void updateAccount(Account account);

//查询所有
List<Account> findAll();
}

实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class AccountDaoImpl implements AccountDao {

private JdbcTemplate template = null;

public void setTemplate(JdbcTemplate template) {
this.template = template;
}

public void updateAccount(Account account) {
template.update("update account set name=?,money=? where id = ?", account.getName(), account.getMoney(), account.getId());
}

public List<Account> findAll() {
List<Account> lists = null;
try {
lists = template.query("select * from account", new BeanPropertyRowMapper<Account>(Account.class));
} catch (DataAccessException e) {
e.printStackTrace();
}
return lists;
}

}

其配置文件bean.xmlbean配置为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 <!--配置账户的持久层-->
<bean id="accountDao" class="cn.lizhi.dao.impl.AccountDaoImpl">
<property name="template" ref="template"></property>
</bean>

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

<!-- DataSource数据源 -->
<bean id="dateSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql:/url:3306/draft"></property>
<property name="username" value="username"></property>
<property name="password" value="password"></property>
</bean>

当我们有多个dao的实现方法时,那么这里的重复代码块:

1
2
3
4
private JdbcTemplate template = null;
public void setTemplate(JdbcTemplate template) {
this.template = template;
}

所以这里就引发了我们的一个思考,是否可以把这些重复的代码块进行抽取,作为一个单独的类,然后当我们的实现类去继承这个类,进而简化我们的代码。答案是可以的。

现在我们对这个类进行抽取:

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

private JdbcTemplate jdbcTemplate = null;

public JdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}

public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}

那么我们原有的实现类就需要继承这个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao {

public void updateAccount(Account account) {
super.getJdbcTemplate().update("update account set name=?,money=? where id = ?", account.getName(), account.getMoney(), account.getId());
}

public List<Account> findAll() {
List<Account> lists = null;
try {
lists = super.getJdbcTemplate().query("select * from account", new BeanPropertyRowMapper<Account>(Account.class));
} catch (DataAccessException e) {
e.printStackTrace();
}
return lists;
}
}

从上面的bean.xml配置文件中,我们看到需要单独对JdbcTemplate进行配置,我们是否可以将JdbcTemplateDataSource进行统一配置呢。我们将DataSource同样创建在JdbcDaoSupport类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class JdbcDaoSupport {

private JdbcTemplate jdbcTemplate = null;
private DataSource dataSource = null;

public JdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}

public void setDataSource(DataSource dataSource) {
if (jdbcTemplate == null) {
jdbcTemplate = createJdbcTemplate(dataSource);
}
}

private JdbcTemplate createJdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}

}

在这里我们创建了一个DataSource对象,并改写了其set方法。作用:如果我们直接传递template对象,那么template理所应当有值(有set方法);如果我们没有传递template对象,我们可以通过DataSource来获取template对象,由于我们改写了其set方法,我们同样可以使template有值(创建了有参构造函数的template,并向其传递了dataSource)。

此时bean.xml配置文件中bean的配置为:

1
2
3
4
5
6
7
8
9
10
11
<!--配置账户的持久层-->
<bean id="accountDao" class="cn.lizhi.dao.impl.AccountDaoImpl">
<property name="dataSource" ref="dateSource"></property>
</bean>

<bean id="dateSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://url:3306/draft"></property>
<property name="username" value="username"></property>
<property name="password" value="password"></property>
</bean>

从上面的bean.xml文件也可以看出,在创建accountDao时,我们是向其注入了dataSource,所以进而就直接触发其set方法(因为accountDaoImpl继承了JdbcDaoSupport),判断template是否有值,如果没有值,就调用其父类中的createJdbcTemplate方法,其中传递的参数dataSource是来自于bean.xml配置文件中的id=dataSourcebean依赖注入。

以上的操作其实Spring已经帮我们实现了,不需要我们自己手动构建(也算是自己练习了一下源码)。截取其部分源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public abstract class JdbcDaoSupport extends DaoSupport {
@Nullable
private JdbcTemplate jdbcTemplate;

public JdbcDaoSupport() {
}

public final void setDataSource(DataSource dataSource) {
if (this.jdbcTemplate == null || dataSource != this.jdbcTemplate.getDataSource()) {
this.jdbcTemplate = this.createJdbcTemplate(dataSource);
this.initTemplateConfig();
}

}

protected JdbcTemplate createJdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}

@Nullable
public final JdbcTemplate getJdbcTemplate() {
return this.jdbcTemplate;
}
}

是不是同样的存在createJdbcTemplate(DataSource dataSource)getJdbcTemplate()方法。从上面的源码中也可以看出Spring在使用set依赖注入时,可以不用定义其变量,只要定义其set属性方法即可。

最后,以上这种继承的方法适用于xml配置文件的方法,不适用于注解开发的方式(因为无法在jar包中的JdbcTemplate上加注解,注入我们的数据类型)。

5.2 Spring中事务控制的API

5.2.1 Spring事务控制

  1. JavaEE体系进行分层开发,事务处理处于业务层,Spring提供了分层设计业务层的事务处理解决方案。
  2. Spring框架为我们提供了一组事务控制的接口
  3. Spring的事务控制都是基于AOP的,它既可以使用编程的方式实现,也可以使用配置的方法实现。

依赖导入:

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>

5.2.2 Spring中事务控制的API介绍

  1. PlatformTransactionManager

    1
    2
    3
    4
    5
    6
    7
    public interface PlatformTransactionManager {
    TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;

    void commit(TransactionStatus var1) throws TransactionException;

    void rollback(TransactionStatus var1) throws TransactionException;
    }

    此接口存在commitrollback方法,即为通知bean

    PlatformTransactionManager接口提供事务操作的方法,包含有3个具体的操作:

  • 获取事务状态信息

    • TransactionStatus getTransaction(@Nullable TransactionDefinition var1)

      • 提交事务
        • void commit(TransactionStatus var1)
      • 回滚事务
        • void rollback(TransactionStatus var1)

      我们需要使用的是其实现类:

      1. org.springframework.jdbc.datasource.DataSourceTransactionManager

        使用Spring JDBCiBatis进行持久化数据时使用

      2. org.springframework.orm.hibernate5.HibernateTransactionManager

        使用Hibernate版本进行持久化数据时使用

  1. 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. 配置事务管理器

    1
    2
    3
    <bean id="transactionManager" 		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dateSource"></property>
    </bean>
  2. 配置事务的通知

    此时我们需要导入事务的约束 tx名称空间和约束,同时也需要aop

    使用tx:advice标签配置事务通知

    • 属性:
      • id:给事务通知起一个唯一标识
      • transaction-manager:给事务通知提供一个事务管理器引用
    1
    2
    3
    <!-- 配置事务通知 -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
    </tx:advice>
  3. 配置AOP中的通用切入点表达式

    1
    2
    3
    4
    <aop:config>
    <!-- 通用表达式配置 -->
    <aop:pointcut id="pt1" expression="execution(* cn.lizhi.service.*.*(..))"/>
    </aop:config>
  4. 建立事务通知和切入点表达式的对应关系

    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>
  5. 配置事务的属性 – 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
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
<?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"
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">
<!--配置账户的持久层-->
<bean id="accountDao" class="cn.lizhi.dao.impl.AccountDaoImpl">
<property name="dataSource" ref="dateSource"></property>
</bean>

<!-- 配置账户的业务层 -->
<bean id="accountService" class="cn.lizhi.service.impl.AccountServiceImpl">
<property name="dao" ref="accountDao"/>
</bean>

<!-- 配置数据源 -->
<bean id="dateSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://url:3306/draft"></property>
<property name="username" value="username"></property>
<property name="password" value="password"></property>
</bean>

<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dateSource"></property>
</bean>
<!-- 配置事务通知 -->
<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>
<!-- AOP配置 -->
<aop:config>
<!-- 通用表达式配置 -->
<aop:pointcut id="pt1" expression="execution(* cn.lizhi.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"></aop:advisor>
</aop:config>
</beans>

5.4 Spring中基于注解的声明式事务控制配置步骤

  1. 配置事务管理器

    1
    2
    3
    4
    <!-- 配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dateSource"></property>
    </bean>
  2. 开启spring对注解事务的支持

    1
    <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
  3. 在需要事务支持的地方使用@Transaction注解

以上中的注意事项有:因为是使用注解式配置,所以在dao层,不能通过继承JdbcDaoSupport的方式简化我们的代码,故需要在bean.xml中对SpringJdbcTemplate进行配置。

5.4.1 基于纯注解的声明式事务控制

通过配置类的方式实现。

  1. SpringConfig配置类

    1
    2
    3
    4
    5
    6
    @ComponentScan("cn.lizhi")
    @Import({JdbcConfig.class, TransactionConfig.class})
    @PropertySource("classpath:jdbcConfig.properties")
    @EnableTransactionManagement
    public class SpringConfig {
    }
  2. 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
    public class JdbcConfig {

    @Value("${driver}")
    private String driver;
    @Value("${username}")
    private String username;
    @Value("${password}")
    private String password;
    @Value("${url}")
    private String url;

    @Bean("template")
    public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
    return new JdbcTemplate(dataSource);
    }

    @Bean("dataSource")
    public DataSource getDataSource() {
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    dataSource.setDriverClassName(driver);
    dataSource.setUsername(username);
    dataSource.setPassword(password);
    dataSource.setUrl(url);
    return dataSource;
    }
    }
  3. TransactionConfig配置类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class TransactionConfig {

    @Bean("transactionManager")
    public DataSourceTransactionManager getTransactionManager(DataSource dataSource) {
    DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
    transactionManager.setDataSource(dataSource);
    return transactionManager;
    }
    }

5.5 Spring编程式事务控制

因为我们的事务管理都是由Spring进行控制,所以我们都需要进行事务管理器的配置。

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

当有了事务管理器之后,Spring同样为我们提供了事务模板,供我们具体使用事务控制的相关方法(相当于代理对象,对业务层方法进行增强):

1
2
3
4
<!-- 事务模板对象  -->
<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager"/>
</bean>

查看具体的TransactionTemplate源码,其中一段为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Nullable
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");
if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
return ((CallbackPreferringPlatformTransactionManager)this.transactionManager).execute(this, action);
} else {
TransactionStatus status = this.transactionManager.getTransaction(this);
Object result;
try {
result = action.doInTransaction(status);
} catch (Error | RuntimeException var5) {
this.rollbackOnException(status, var5);
throw var5;
} catch (Throwable var6) {
this.rollbackOnException(status, var6);
throw new UndeclaredThrowableException(var6, "TransactionCallback threw undeclared checked exception");
}

this.transactionManager.commit(status);
return result;
}
}

在上面的异常代码块中,可以看出,当执行不通过时,执行this.rollbackOnException(status, var5),进行事务回滚;当执行通过时,try代码块中执行事务状态result = action.doInTransaction(status),最后执行this.transactionManager.commit(status),进行事务的提交。

具体使用:

模仿TransactionTemplate中的execute方法,在业务层中需要事务控制的方法中,执行该方法,该方法中的参数是一个接口,需要我们自己实现,其内容就填写我们需要控制的业务具体代码块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    public void transfer(final String startName, final String endName, final Float money) {

template.execute(new TransactionCallback<Object>() {
public Object doInTransaction(TransactionStatus transactionStatus) {
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()); // 会获取连接
return null;
}
});

}

由于编程式的事务控制,又增加了代码的冗余,违背了AOP的初心,所以实际中很少使用这种事务控制方式。

六、Spring整合JavaWeb

整合步骤:

  1. 导包 —— Spring相关的Jar包

  2. 写配置

    • 将所有组件加入容器中,并能正确获取
      • @ControllerServlet层;但是不能标注在Servlet层,因为这个对象是Tomcat创建的,而非Spring容器进行创建的。
      • @Service:业务逻辑层
      • @Repository:dao层
      • @Component:其他组件
  3. 每个组件之间自动装配

  4. 配置出声明式事务

    • 配置数据源。配置数据源之前,可抽离出JDBC配置文件。
    • JDBCTemplate操作数据库。给其注入数据源。
    • 配置事务管理器 – 让其控制住数据源
      • 注解方式
      • XML配置方式
  5. IOC容器创建和销毁都要在合适的时机完成;

    1
    2
    3
    4
    5
    6
    项目启动:{
    IOC创建完成
    }
    项目销毁:{
    IOC销毁
    }

    通过监听器完成这项工作,监听器是Tomcat中的,因此,配置在web.xml中。采用Spring提供的监听器(ContextLoaderListener);容器位置,即为Spring配置文件。

    这个监听器创建好的IOC容器在ContextLoader —— 这个属性就是IoC容器

    1
    private WebApplicationContext context;

    通过静态方法能获取——getCurrentWebApplicationContext


Comment