0%

Spring Framework

概述

什么是Spring框架

Spring 是于 2003 年兴起的一个轻量级的 Java 开发框架,它是为了解决企业应用开发 的复杂性而创建的。Spring 的核心是控制反转(IoC)和面向切面编程(AOP)。Spring 是可 以在 Java SE/EE 中使用的轻量级开源框架

​ Spring 的主要作用就是为代码“解耦”,降低代码间的耦合度。就是让对象和对象(模 块和模块)之间关系不是使用代码关联,而是通过配置来说明。即在 Spring 中说明对象(模 块)的关系。

Spring的优点

​ Spring 是一个框架,是一个半成品的软件。有 20 个模块组成。它是一个容器管理对象, 容器是装东西的,Spring 容器不装文本,数字。装的是对象。Spring 是存储对象的容器。

(1) 轻量

Spring 框架使用的 jar 都比较小,一般在 1M 以下或者几百 kb。Spring 核心功能的所需 的 jar 总共在 3M 左右。 Spring 框架运行占用的资源少,运行效率高。不依赖其他 jar

(2) 针对接口编程,解耦合

​ Spring 提供了 Ioc 控制反转,由容器管理对象,对象的依赖关系。原来在程序代码中的 对象创建方式,现在由容器完成。对象之间的依赖解耦合。

(3) AOP 编程的支持

​ 通过 Spring 提供的 AOP 功能,方便进行面向切面的编程,许多不容易用传统 OOP 实现 的功能可以通过 AOP 轻松应付 在 Spring 中,开发人员可以从繁杂的事务管理代码中解脱出来,通过声明式方式灵活地 进行事务的管理,提高开发效率和质量。

(4) 方便集成各种优秀框架

​ Spring 不排斥各种优秀的开源框架,相反 Spring 可以降低各种框架的使用难度,Spring 提供了对各种优秀框架(如 Struts,Hibernate、MyBatis)等的直接支持。简化框架的使用。 Spring 像插线板一样,其他框架是插头,可以容易的组合到一起。需要使用哪个框架,就把 这个插头放入插线板。不需要可以轻易的移除。

Spring 体系结构

​ Spring 由 20 多个模块组成,它们可以分为数据访问/集成(Data Access/Integration)、 Web、面向切面编程(AOP, Aspects)、提供JVM的代理(Instrumentation)、消息发送(Messaging)、 核心容器(Core Container)和测试(Test)。

IoC控制翻转

​ 控制反转(IoC,Inversion of Control),是一个概念,是一种思想。指将传统上由程序代码直接操控的对象调用权交给容器,通过容器来实现对象的装配和管理。控制反转就是对对象控制权的转移,从程序代码本身反转到了外部容器。通过容器实现对象的创建,属性赋值, 依赖的管理。

IoC 是一个概念,是一种思想,其实现方式多种多样。当前比较流行的实现方式是依赖 注入。应用广泛。

依赖classA 类中含有 classB 的实例,在 classA 中调用 classB 的方法完成功能,即 classAclassB 有依赖。

IoC的是实现:

  • 依赖注入:DI(Dependency Injection),程序代码不做定位查询,这些工作由容器自行完成。

依赖注入 DI 是指程序运行过程中,若需要调用另一个对象协助时,无须在代码中创建 被调用者,而是依赖于外部容器,由外部容器创建后传递给程序。 Spring 的依赖注入对调用者与被调用者几乎没有任何要求,完全支持对象之间依赖关系 的管理。

Spring 框架使用依赖注入(DI)实现 IoC。

Spring 容器是一个超级大工厂,负责创建、管理所有的 Java 对象,这些 Java 对象被称 为 Bean。Spring 容器管理着容器中 Bean 之间的依赖关系,Spring 使用“依赖注入”的方式 来管理 Bean 之间的依赖关系。使用 IoC 实现对象之间的解耦和。

Spring 第一个程序

1. 创建 Maven 项目

2. 引入 Maven 依赖 pom.xml

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

3. 定义接口和实体类

1
2
3
public interface SomeService {
void doSome();
}
1
2
3
4
5
6
7
8
9
public class SomeServiceImpl implements SomeService {
public SomeServiceImpl() {
System.out.println("SomeServiceImpl.SomeServiceImpl");
}
@Override
public void doSome() {
System.out.println("SomeServiceImpl.doSome");
}
}

4. 创建Spring配置文件

src/main/resources/ 目录现创建一个 xml 文件,文件名可以随意,但 Spring 建议的名 applicationContext.xml

spring 配置中需要加入约束文件才能正常使用,约束文件是 xsd 扩展名。

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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 声明bean,告诉spring要创建某个类的对象
id:对象的自定义名称, 唯一值
class: 类的全限定名称(不能是接口,因为spring是反射机制,必须使用类)
-->
<bean id="someService" class="com.bjpowernode.service.impl.SomeServiceImpl"/>
<bean id="someService1" class="com.bjpowernode.service.impl.SomeServiceImpl"/>
<bean id="mydate" class="java.util.Date" />
</beans>

<bean /> :用于定义一个实例对象。一个实例对应一个 bean 元素。

id:该属性是 Bean 实例的唯一标识,程序通过 id 属性访问 Bean,Bean 与 Bean 间的依 赖关系也是通过 id 属性关联的。

class :指定该 Bean 所属的类,注意这里只能是类,不能是接口。

5. 定义测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test02() {
//1. 指定spring配置文件的名称
String config = "beans.xml";
//2. 创建表示spring容器的对象, ApplicationContext;
//ApplicationContext就是表示spring容器, 通过容器对象获取对象了
//ClassPathXmlApplicationContext: 表示从类路径中加载spring配置文件
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
//执行上行代码时,配置文件中的java对象全部自动创建
//3. 从容器中获取某个对象, 调用对象的方法
SomeService service = (SomeService) ac.getBean("someService");
//4. 使用spring创建好的对象
service.doSome();
}

6. 使用 spring 创建非自定义类对象

spring 配置文件加入 java.util.Date 定义:

1
<bean id="myDate" class="java.util.Date" />

MyTest 测试类中: 调用 getBean(“myDate”); 获取日期类对象。

7. 容器接口和实现类

ApplicationContext 接口(容器)

ApplicationContext 用于加载 Spring 的配置文件,在程序中充当“容器”的角色。其实现类有两个。

配置文件在类路径下

若 Spring 配置文件存放在项目的类路径下,则使用 ClassPathXmlApplicationContext 实现 类进行加载

ApplicationContext 容器中对象的装配时机

ApplicationContext 容器,会在容器对象初始化时,将其中的所有对象一次性全部装配好。 以后代码中若要使用到这些对象,只需从内存中直接获取即可。执行效率较高。但占用内存。

基于 XML 的 DI

DI: 依赖注入

DI的实现有两种:

  1. 在spring的配置文件中,使用标签和属性完成,叫做基于XML的di实现
  2. 使用spring中的注解,完成属性赋值,叫做基于注解的di实现

di的语法分类:

  1. set注入(设置注入): spring调用类的set方法,在set方法中实现属性的赋值
    80%左右都是使用set注入
  2. 构造注入,spring调用类的有参数构造方法,创建对象。在构造方法中完成赋值

注入分类

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

set注入

简单类型

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

1
2
3
4
5
6
7
8
<bean id="myStudent" class="com.bjpowernode.ba01.Student">
<property name="name" value="李四"/>
<property name="age" value="20"/>
<property name="email" value = "99799@qq.com"/>
</bean>
<bean id="mydate" class="java.util.Date">
<property name="time" value="23423114234223" />
</bean>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Student {
private String name;
private int age;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

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

public void setAge(Integer age) {
this.age = age;
}
public void setEmail(String email) {
System.out.println("email = " + email);
}
}
引用类型

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

1
2
3
4
5
6
7
8
9
<bean id="myStudent" class="com.bjpowernode.ba02.Student">
<property name="name" value="李四"/>
<property name="age" value="20"/>
<property name="school" ref="mySchool"/>
</bean>
<bean id="mySchool" class="com.bjpowernode.ba02.School">
<property name="name" value="北京大学"/>
<property name="address" value="北京海淀区"/>
</bean>

构造注入

构造注入是指,在构造调用者实例的同时,完成被调用者的实例化。即,使用构造器设置依赖关系。

1
2
3
4
5
6
public Student(String myname, int myage, School mySchool) {
System.out.println("===========Studnet有参数构造方法=========");
this.name = myname;
this.age = myage;
this.school = mySchool;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<bean id="myStudent" class="com.bjpowernode.ba03.Student">
<constructor-arg name="myname" value="张三"/>
<constructor-arg name="myage" value="20"/>
<constructor-arg name="mySchool" ref ="myXuexiao"/>
</bean>
<bean id="myXuexiao" class="com.bjpowernode.ba03.School">
<property name="name" value="北京大学"/>
<property name="address" value="北京海淀区"/>
</bean>
<bean id="myStudent2" class="com.bjpowernode.ba03.Student">
<constructor-arg index="0" value="李四"/>
<constructor-arg index="1" value="26"/>
<constructor-arg index="2" ref="myXuexiao"/>
</bean>

构造注入 : spring调用类的有参数构造方法,在创建对象的同时,在构造方法中给属性赋值
使用 <constructor-arg> 标签
<constructor-arg> 标签: 一个<constructor-arg>表示构造方法中一个参数
<constructor-arg> 标签属性:
name: 表示构造方法的形参名
index: 表示构造方法参数的位置,参数从左往右是0, 1, 2的顺序
value: 构造方法的形参类型是简单类型的,使用value
ref: 构造方法的形参类型是引用类型,使用ref进行赋值

引用类型属性自动注入

对于引用类型属性的注入,也可不在配置文件中显示的注入。可以通过为标签 设置 autowire 属性值,为引用类型属性进行隐式自动注入(默认是不自动注入引用类型属性)。根据自动注入判断标准的不同,可以分为两种:

byName:根据名称自动注入

byType: 根据类型自动注入、

byName方式自动注入

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

1
2
3
4
5
6
7
8
9
<bean id="myStudent" class="com.bjpowernode.ba04.Student" autowire="byName">
<property name="name" value="李四"/>
<property name="age" value="20"/>
<!-- <property name="school" ref="mySchool"/>-->
</bean>
<bean id="school" class="com.bjpowernode.ba04.School">
<property name="name" value="清华大学"/>
<property name="address" value="北京海淀区"/>
</bean>
1
2
3
4
5
6
7
8
9
10
11
public class Student {
private String name;
private int age;
private School schooll;

public void setSchool(School schooll) { //和属性名无关 !!!
System.out.println("setSchool" + schooll);
this.schooll = schooll;
}
.......
}

byType 方式自动注入

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

1
2
3
4
5
6
7
8
<bean id="myStudent" class="com.bjpowernode.ba05.Student" autowire="byType">
<property name="name" value="张萨"/>
<property name="age" value="26"/>
</bean>
<bean id="primarySchool" class="com.bjpowernode.ba05.PrimarySchool">
<property name="name" value="北京小学"/>
<property name="address" value="北京大兴区"/>
</bean>

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

1
2
3
4
5
6
7
8
9
<!--    spring-total表示主配置文件:包含其他的配置文件的
主配置文件一般是不定义对象的
-->
<!-- 加载的是文件列表-->
<!-- <import resource="classpath:ba06/spring-school.xml"/>-->
<!-- <import resource="classpath:ba06/spring-student.xml"/>-->

<!-- 在包含关系的配置文件中, 可以使用通配符-->
<import resource="classpath:ba06/spring-*.xml"/>

基于注解的DI

使用注解的步骤:

  1. 加入Maven依赖 spring-context , 在你加入spring-context的同时, 已经间接加入spring-aop依赖,使用注解必须使用spring-aop依赖
  2. 在类中加入 spring 的注解(多个不同功能的注解)
  3. 在 spring 配置文件中,加入一个组件扫描器的标签,说明注解在项目中的位置
  4. 使用注解创建对象,创建容器ApplicationContext

举例:

1
2
3
4
5
6
7
8
9
10
11
/**
* @Component: 创建对象的,等同于<bean>功能
* 属性:value 就是对象的名称,也就是bean的id值
* value的值是唯一的,创建的对象在整个spring容器中就一个
* 等同于<bean id="myStudent" class="com.bjpowernode.ba01.Student"/>
*/
@Component(value = "myStudent")
public class Student {
..........
}

声明组件扫描器(component-scan), 组件就是java对象
base-package: 指定注解在你的项目中的包名
component-scan工作方式:spring会扫描遍历base-package指定的包,
扫描包中和子包中所有类,找到类中的注解,按照注解功能创建对象,或给组件赋值

1
<context:component-scan base-package="com.bjpowernode.ba01"/>

定义 Bean 的注解@Component

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

1
2
3
//@Component(value = "myStudent")
//@Component //默认对象名称为类名首字母小写
@Component("myStudent")

另外,Spring 还提供了 3 个创建对象的注解:

  • @Repository 用于对 DAO 实现类进行注解(持久层类)
  • @Service 用于对Service实现类进行注解(业务层类)
  • @Controller 用于对 Controller实现类进行注解 (用在控制器上)

这三个注解与@Component 都可以创建对象,但这三个注解还有其他的含义,@Service 创建业务层对象,业务层对象可以加入事务功能,@Controller 注解创建的对象可以作为处 理器接收用户的请求。

@Repository,@Service,@Controller 是对@Component 注解的细化,标注不同层的对 象。即持久层对象,业务层对象,控制层对象。

@Component 不指定 value 属性,bean 的 id 是类名的首字母小写。

简单类型属性注入@Value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component("myStudent")
public class Student {
/**
* @Value: 简单类型的属性赋值
* 位置:1. 在属性定义上面,无需set方法
* 2. 在set方法的上面
*/
@Value("张飞") //通过反射注入
private String name;
private Integer age;

public void setName(String name) {
System.out.println("HHH"); //不会执行
this.name = name;
}
@Value("30")
public void setAge(Integer age) {
System.out.println("setAge:" + age);
this.age = age;
}
}

byType 自动注入@Autowired

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

byName 自动注入@Autowired 与@Qualifier

需要在引用属性上联合使用注解@Autowired 与@Qualifier。@Qualifier 的 value 属性用于指定要匹配的 Bean 的 id 值。类中无需 set 方法,也可加到 set 方法上。

1
2
3
@Autowired
@Qualifier("mySchool")
private School school;

@Autowired 还有一个属性 required,默认值为 true,表示当匹配失败后,会终止程序运 行。若将其值设置为 false,则匹配失败,将被忽略,未匹配的属性值为 null。

1
2
3
@Autowired(required = false)
@Qualifier("mySchool1")
private School school;

JDK 注解@Resource 自动注入

Spring提供了对 jdk中@Resource注解的支持。@Resource 注解既可以按名称匹配Bean, 也可以按类型匹配 Bean。默认是按名称注入, 如何byName赋值失败,则使用byType, 使用该注解,要求 JDK 必须是 6 及以上版本。 @Resource 可在属性上,也可在 set 方法上

如果是jdk11以上版本,下pom.xml文件中添加

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

@Resource 注解指定其 name 属性,则 name 的值即为按照名称进行匹配的 Bean 的 id。

1
2
@Resource(name="mySchool")
private School school;

注解和XML的对比

注解的优点是:

  • 方便
  • 直观
  • 高效

弊端:以硬编码的方式写入Java代码中,修改是需要重新编译代码的

XML方式的优点:

  • 配置和代码是分离的
  • 在 xml 中做修改,无需编译代码,只需重启服务器将新的配置加载

xml的缺点:编写麻烦,效率低, 大型项目过于复杂

AOP面向切面编程

动态代理

复习JDK动态代理的实现步骤:

  1. 创建目标类,SomeServiceImpl目标类,给它的doSome, doOther增加 输出时间,事务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome() {
    System.out.println("执行业务方法doSome()");
    }
    @Override
    public void doOther() {
    System.out.println("执行业务方法doOther()");
    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    public class ServiceTools {
    public static void doLog() {
    System.out.println("非业务方法:方法的执行时间" + new Date());
    }
    public static void doTrans() {
    System.out.println("非业务方法: 方法结束,提交事务");
    }
    }
  2. 创建InvocationHandler接口的实现类,在这个类实现给目标方法增加功能

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    package com.bjpowernode.handler;

    import com.bjpowernode.util.ServiceTools;

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

    public class MyInvocationHandler implements InvocationHandler {
    private Object target;

    public MyInvocationHandler(Object target) {
    this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable {
    System.out.println("执行了MyInvocationHandler中的invoke()方法");
    System.out.println("method名称: " + method.getName());
    String methodName = method.getName();
    Object res = null;
    if("doSome".equals(methodName)) {
    ServiceTools.doLog(); //在目标方法执行之前,输出时间
    //通过代理对象执行方法时候,会调用invoke()
    //执行目标类的方法,通过Method类实现
    res = method.invoke(target, args); //SomeServiceImpl.doOther();

    ServiceTools.doTrans(); //在目标方法执行之后,提交事务
    } else {
    res = method.invoke(target, args);
    }

    return res;
    }
    }

  3. 使用jdk中 类Proxy, 创建代理对象。 实现创建对象的能力

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

import com.bjpowernode.handler.MyInvocationHandler;
import com.bjpowernode.service.SomeService;
import com.bjpowernode.service.impl.SomeServiceImpl;

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

public class MyApp {
public static void main(String[] args) {
// SomeService service = new SomeServiceImpl();
// service.doSome();
// System.out.println("======================");
// service.doOther();
//使用jdk的Proxy创建代理对象
// 1. 创建目标对象
SomeService target = new SomeServiceImpl();
// 2. 创建InvocationHandler对象
InvocationHandler handler = new MyInvocationHandler(target);
// 3. 使用Proxy创建代理
SomeService proxy = (SomeService) Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
handler);
// 4. 通过代理执行方法,会调用handler中的invoke()方法
proxy.doSome();
System.out.println("========================================");
proxy.doOther();
}
}

AOP概述

AOP(Aspect Orient Programming),面向切面编程。面向切面编程是从动态角度考虑程序运行过程。

AOP 底层,就是采用动态代理模式实现的。采用了两种代理:JDK 的动态代理,与 CGLIB 的动态代理

实现方式:

  • jdk动态代理:使用jdk中的Proxy, Method, InvocationHandler创建代理对象。

    • jdk动态代理要求目标类必须实现接口
  • cglib动态代理:第三方工具库,创建代理对象,原理是继承。通过继承目标类,创建子类,子类就是代理对象。要求目标类不能是final的,方法也不能是final的

动态代理的作用:

  1. 在目标类源代码不改变的情况下,增加功能
  2. 减少代码的重复
  3. 专注业务逻辑代码
  4. 解耦合,让你的业务功能和日志,事务非业务功能分离

AOP: 面向切面编程,基于动态代理的,使用jdk, cglib两种方式。 AOP就是动态代理的规范化,把动态代理的实现步骤,方式都定义好了。让开发人员用一种统一的方式,使用动态代理。

Aspect: 切面,给你的目标类增加的功能,就是切面。像上面用的日志,事务都是切面。

切面的特点:一般都是非业务方法,独立使用的。

怎么理解面向切面编程 ?

  • 需要在分析项目功能的同时,找出切面

  • 合理安排切面的执行时间(在目标方法前,还是目标方法后)

  • 合理安排切面执行的位置,在哪个类,哪个方法增加增强功能

AOP 编程术语

切面(Aspect)

表示增强的功能,就是一堆代码,完成某一个功能,非业务功能,常见的切面功能有日志,事务,统计信息,参数检查,权限验证。

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

连接点(JoinPoint)

连接业务方法和切面的位置,就是某类中的业务方法

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

切入点(Pointcut)

指多个连接点方法的集合。多个方法

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

目标对象(Target)

去给哪个类的方法增加功能,这个类就是目标对象

通知(Advice)

表示切面功能执行的时间

切入点定义切入的位置,通知定义切入的时间。

切面有三个关键的要素:

  • 切面的功能代码,切面干什么

  • 切面的执行位置,使用Pointcut表示切面执行的位置

  • 切面的执行时间,使用Advice表示时间,在目标方法执行之前,还是目标方法执行之后

AspectJ 对 AOP 的实现

对于 AOP 这种编程思想,很多框架都进行了实现。Spring 就是其中之一,可以完成面向切面编程。然而,AspectJ 也实现了 AOP 的功能,且其实现方式更为简捷,使用更为方便, 而且还支持注解式开发。所以,Spring 又将 AspectJ 的对于 AOP 的实现也引入到了自己的框 架中。 在 Spring 中使用 AOP 开发时,一般使用 AspectJ 的实现方式。

AspectJ 是一个优秀面向切面的框架,它扩展了 Java 语言,提供了强大的切面实现。

AspectJ框架实现aop有两种方式:

  1. 使用xml的的配置文件:配置全局事务
  2. 使用注解,一般在项目中使用注解实现aop功能,aspectJ有5个注解

AspectJ的通知类型

切面的执行时间,这个执行时间在规范中叫做Advice(通知,增强)

在 aspectJ框架中使用注解表示的。也可以使用xml配置文件中的标签

  • 前置通知 @Before
  • 后置通知 @AfterReturning
  • 环绕通知 @Around
  • 异常通知 @AfterThrowing
  • 最终通知 @After

AspectJ 的切入点表达式

表示切面执行的位置,使用的是切入点表达式。

AspectJ 定义了专门的表达式用于指定切入点。表达式的原型是:

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

modifiers-pattern : 访问权限类型

ret-type-pattern :返回值类型

declaring-type-pattern :包名类名

name-pattern(param-pattern)  :方法名(参数类型和参数个数)

throws-pattern : 抛出异常类型

? 表示可选的部分

以上表达式共 4 个部分。 execution(访问权限 方法返回值 方法声明(参数) 异常类型)

切入点表达式要匹配的对象就是目标方法的方法名。所以,execution 表达式中明显就 是方法的签名。注意,表达式中黑色文字表示可省略部分,各部分间用空格分开。在其中可 以使用以下符号:

举例

execution(public * *(..))

  • 指定切入点为:任意公共方法

execution(* set*(..))

  • 指定切入点为:任意一个以 “set” 开始的方法

execution(* com.xyz.service.*.*(..))

  • 指定切入点为:定义在service包里任意类的任意方法

execution(* com.xyz.service..*.*(..))

  • 指定切入点为:定义在 service 包或者子包里的任意类的任意方法。
  • “..”出现在类名中,后面必须跟 “*” ,表示包,子包下的所有类

execution(* *..service.*.*(..))

  • 指定所有包下的 service 子包下所有类(接口)中所有方法为切入点

AspectJ 的开发环境

使用aspectj实现aop的基本步骤:

  1. 新建Maven项目

  2. 加入依赖

    1. spring依赖

      1
      2
      3
      4
      5
      <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.2.5.RELEASE</version>
      </dependency>
    2. aspectj依赖

      1
      2
      3
      4
      5
      <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
      <version>5.2.5.RELEASE</version>
      </dependency>
    3. junit单元测试

      1
      2
      3
      4
      5
      6
      <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
      </dependency>
  3. 创建目标类,接口和它的实现类,要做的是给类中的方法增加功能

  4. 创建切面类:普通类

    1. 在类的上面加入 @Aspect

    2. 在类中定义方法,方法就是切面要执行的功能代码,

      在方法的上面加入aspectj的通知注解,例如@Before

      还需要指定切入点表达式execution()

  5. 创建spring的配置文件:声明对象,把对象交给容器统一管理

    1. 声明目标对象
    2. 声明切面类对象
    3. 声明aspectj框架中的自动代理生成器标签
      • 自动代理生成器:用来完成代理对象自动创建功能的
  6. 创建测试类,从spring容器中获取目标对象(实际上就是代理对象)

    通过代理执行方法,实现aop的功能增强

Aspect基于注解的AOP实现

实现步骤

定义业务接口与实现类

1
2
3
public interface SomeService {
void doSome(String name, Integer age);
}
1
2
3
4
5
6
7
public class SomeServiceImpl implements SomeService {
@Override
public void doSome(String name, Integer age) {
//给doSome方法增加一个功能,在doSome()执行之前,输出方法的执行时间
System.out.println("============目标方法doSome()========");
}
}

定义切面类

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
/**
* @Aspect : 是 aspectj 框架中的注解。
* 作用:表示当前类是切面类
* 切面类: 使用来给业务方法增加功能的类,在这个类中有切面的功能代码
* 位置:类定义的上面
*/
@Aspect
public class MyAspect {
/**
* 定义方法,实现切面功能的
* 方法的定义要求:
* 1. 公共方法
* 2. 方法没有返回值
* 3. 可以有参数,也可以没有参数
* 如果有参数,参数不是自定义的,有几个参数类型可以使用
*/
/**
* @Before: 前置通知注解
* 属性: value, 是切入点表达式,表示切面功能执行的位置。
* 位置:在方法的上面
* 特点: 1. 在目标方法之前先执行
* 2. 不会改变目标方法的执行结果
* 3. 不会影响目标方法的执行
*/
@Before(value="execution(public void com.bjpowernode.ba01.SomeServiceImpl.doSome(String, Integer))")
public void MyBefore() {
//切面执行的功能代码
System.out.println("前置通知,切面功能: 在目标方法之前输出执行时间: " + new Date());
}

}

声明目标对象,切面类对象

1
2
3
4
<!--    声明目标对象-->
<bean id="someService" class="com.bjpowernode.ba01.SomeServiceImpl"/>
<!-- 声明切面对象-->
<bean id="myAspect" class="com.bjpowernode.ba01.MyAspect"/>

注册 AspectJ 的自动代理

1
<aop:aspectj-autoproxy />

在定义好切面 Aspect 后,需要通知 Spring 容器,让容器生成“目标类+ 切面”的代理 对象。这个代理是由容器自动生成的。只需要在 Spring 配置文件中注册一个基于 aspectj 的 自动代理生成器,其就会自动扫描到@Aspect 注解,并按通知类型与切入点,将其织入,并 生成代理。

声明自动代理生成器:使用aspectj框架内部功能,创建目标对象的代理对象。
创建代理对象是在内存中实现的,修改目标对象的内存中的结构,创建为一个代理对象。
所以目标对象就是被修改后的代理对象
aspectj-autoproxy : 会把spring容器中的所有的目标对象,一次性都生成代理对象

<aop:aspectj-autoproxy/>的底层是由 AnnotationAwareAspectJAutoProxyCreator 实现的。 从其类名就可看出,是基于 AspectJ 的注解适配自动代理生成器。 其工作原理是,通过扫描找到@Aspect 定义的切面类,再由切面类根据切入点找到目标类的目标方法,再由通知类型找到切入的时间点。

测试类中使用目标对象的 id

1
2
3
4
5
6
7
8
9
@Test
public void test01() {
String config = "applicationContext.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
//从容器中获取目标对象
SomeService proxy = (SomeService) ctx.getBean("someService");
//通过代理对象执行方法,实现目标方法执行时,增强了功能
proxy.doSome("lisi", 20);
}

@Before 前置通知-方法有 JoinPoint 参数

在目标方法执行之前执行。被注解为前置通知的方法,可以包含一个 JoinPoint 类型参 数。该类型的对象本身就是切入点表达式。通过该参数,可获取切入点表达式、方法签名、 目标对象等。 不光前置通知的方法,可以包含一个 JoinPoint 类型参数,所有的通知方法均可包含该 参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 指定方法中的参数:JoinPoint
* 作用是:可以在通知方法中获取方法执行的信息,例如方法名称,方法的实参
* 如果你的切面功能中需要到方法的信息,就加入JointPoint
* 这个JointPoint参数是由框架赋予的,必须是第一个位置的参数
*/
@Before(value="execution(void *..SomeServiceImpl.doSome(String, Integer))")
public void MyBefore(JoinPoint jp) {
System.out.println("方法的签名(定义) = " + jp.getSignature());
System.out.println("方法的名称 = " + jp.getSignature().getName());
Object[] args = jp.getArgs();
for(Object arg : args) {
System.out.println("参数 = " + arg);
}
//切面执行的功能代码
System.out.println("2======前置通知,切面功能: 在目标方法之前输出执行时间: " + new Date());

}

@AfterReturning 后置通知-注解有 returning 属性

在目标方法执行之后执行。由于是目标方法之后执行,所以可以获取到目标方法的返回值。

该注解的 returning 属性就是用于指定接收方法返回值的变量名的。所以,被注解为后置通知的方法,除了可以包含 JoinPoint 参数外,还可以包含用于接收返回值的变量。该变 量最好为 Object 类型,因为目标方法的返回值可能是任何类型。

接口增加的方法

1
2
3
4
5
public interface SomeService {
void doSome(String name, Integer age);
String doOther(String name, Integer age);
Student doOther2(String name, Integer age);
}

实现方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SomeServiceImpl implements SomeService {
@Override
public void doSome(String name, Integer age) {
//给doSome方法增加一个功能,在doSome()执行之前,输出方法的执行时间
System.out.println("============目标方法doSome()========");
}
@Override
public String doOther(String name, Integer age) {
System.out.println("=========== 目标方法doOther() =============");
return "abcd";
}
@Override
public Student doOther2(String name, Integer age) {
Student student = new Student();
student.setName("lisi");
student.setAge(20);
return student;
}
}

定义切面

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
@Aspect
public class MyAspect {
/**
* 后置通知:方法是实现切面功能的
* 方法的定义要求:
* 1. public
* 2. void
* 3. 方法名称自定义
* 4. 方法是有参数的, 推荐使用Object, 参数名自定义
*/
/**
* @AfterReturning:后置通知
* 属性: 1. value: 切入点表达式
* 2. returning : 自定义变量,表示目标方法的返回值的
* 自定义变量名必须和通知方法的形参名一样
* 位置: 在方法定义的上面
* 特点:
* 1. 在目标方法之后执行
* 2. 能够获得到目标方法的返回值,可以根据这个返回值做不同的处理功能
* Object res = doOther();
* 3. 可以修改这个返回值
*
* 后置通知的执行:
* Object res = doOther();
* myAfterReturning(res);
*/
@AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))",
returning = "res")
public void myAfterReturning(Object res) {
// Object res : 是目标方法的返回值,根据返回值做你的切面的功能处理
System.out.println("后置通知:在目标方法之后执行的,获取的返回值是 : " + res);
if(res.equals("abcd")) {

} else {

}
if(res != null) {
res = "Hello Aspectj"; //不会影响返回值
}
}
@AfterReturning(value = "execution(* *..SomeServiceImpl.doOther2(..))",
returning = "res")
public void myAfterReturning2(Object res) {
// Object res : 是目标方法的返回值,根据返回值做你的切面的功能处理
System.out.println("后置通知:在目标方法之后执行的,获取的返回值是 : " + res);
Student student = (Student)res; //引用类型可以改变返回值
student.setName("牛可乐");
student.setAge(3);
}
}

@Around 环绕通知-增强方法有 ProceedingJoinPoint 参数

在目标方法执行之前之后执行。被注解为环绕增强的方法要有返回值,Object 类型。并 且方法可以包含一个 ProceedingJoinPoint 类型的参数。接口 ProceedingJoinPoint 其有一个 proceed()方法,用于执行目标方法。若目标方法有返回值,则该方法的返回值就是目标方法 的返回值。最后,环绕增强方法将其返回值返回。该增强方法实际是拦截了目标方法的执行。

接口增加方法:

1
2
3
public interface SomeService {
String doFirst(String name, Integer age);
}

接口方法的实现:

1
2
3
4
5
6
7
public class SomeServiceImpl implements SomeService {
@Override
public String doFirst(String name, Integer age) {
System.out.println("--------- 业务方法doFirst ------------");
return "doFirst";
}
}

定义切面:

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
@Aspect
public class MyAspect {
/**
* 环绕通知方法定义格式
* 1. public
* 2. 必须有一个返回值
* 3. 方法名称自定义
* 4. 方法有参数,固定的参数 ProceedingJoinPoint
*/
/**
* @Around : 环绕通知
* 属性: value 切入点表达式
* 位置: 切入点表达式的上方
* 特点:
* 1. 它是功能最强的通知
* 2. 在目标方法前和后都能增强功能
* 3. 控制目标方法是否被调用
* 4. 修改原来目标方法的执行结果。影响最后的调用结果
*
* 环绕通知,等同于jdk动态代理的,InvocationHandler接口
*
* 参数:ProceedingJoinPoint 就等同于 Method
* 作用:执行目标方法的
* 返回值: 目标方法的执行结果,可以被修改。
*
* 环绕通知:经常做事务,在目标方法之前开启事务,执行目标方法,在目标方法之后提交事务
*/
@Around(value = "execution(* * ..SomeServiceImpl.doFirst(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
//获取第一个参数的值
Object[] args = pjp.getArgs();
String name = "";
if(args != null && args.length >= 1) {
Object arg = args[0];
name = (String)arg;
}
//实现环绕通知的作用
Object result = null;
System.out.println("环绕通知:在目标方法之前,输出时间:" + new Date());
// 1. 目标方法的调用
if("zhangsan".equals(name)) {
result = pjp.proceed(); //method.invoke();
}
System.out.println("环绕通知:在目标方法之后,提交事务!");
// 2. 在目标方法前或后增加功能

//修改目标方法的执行结果,影响方法最后的调用结果
if(result != null) {
result = "Hello AspectJ AOP";
}
// 3. 返回目标方法的执行结果
return result;
}
}

@AfterThrowing 异常通知-注解中有 throwing 属性

在目标方法抛出异常后执行。该注解的 throwing 属性用于指定所发生的异常类对象。 当然,被注解为异常通知的方法可以包含一个参数 Throwable,参数名称为 throwing 指定的 名称,表示发生的异常对象。

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
@Aspect
public class MyAspect {
/**
* 异常通知方法定义格式
* 1. public
* 2. 没有返回值
* 3. 方法名称自定义
* 4. 方法有一个Exception, 如果还有是JoinPoint
*/
/**
* @AfterThrowing: 异常通知
* 属性:1. value 切入点表达式
* 2. throwing 自定义的变量,表示目标方法抛出的异常对象
* 变量名必须和方法的形参名一样
* 特点:
* 1. 在目标方法抛出异常时执行
* 2. 可以做异常监控程序,监控目标方法执行时是否有异常
* 如果有异常,可以发送短信、邮件进行通知
* 执行就是
* try {
* SomeService.doSecond();
* } catch(Exception e) {
* myAfterThrowing(e);
* }
*/
@AfterThrowing(value = "execution(* *..SomeServiceImpl.doSecond(..))",
throwing = "ex")
public void myAfterThrowing(Exception ex) {
System.out.println("异常通知:方法发生异常时,执行: " + ex.getMessage());
//发送邮件,短信,通知开发人员
}

}

@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
26
27
28
29
30
31
32
@Aspect
public class MyAspect {
/**
* 最终通知方法的定义格式
* 1. public
* 2. 没有返回值
* 3. 方法名称自定义
* 4. 方法没有参数,如果有就是 JoinPoint
*/
/**
* @After : 最终通知
* 属性:value 切入点表达式
* 位置: 在方法的上面
* 特点:
* 1. 总是会执行
* 2. 在目标方法执行之后执行的
*
* try {
* SomeServiceImpl.doThird(..);
* }catch (Exception e) {}
*
* }finally {
* myAfter();
* }
*/
@After(value = "execution(* *..SomeServiceImpl.doThird(..))")
public void myAfter() {
System.out.println("执行最终通知,总是会执行");
// 一般做资源清除工作的。
}

}

@Pointcut 定义切入点

当较多的通知增强方法使用相同的 execution 切入点表达式时,编写、维护均较为麻烦。 AspectJ 提供了@Pointcut 注解,用于定义 execution 切入点表达式。

其用法是,将@Pointcut 注解在一个方法之上,以后所有的 execution 的 value 属性值均可使用该方法名作为切入点。代表的就是@Pointcut 定义的切入点。这个使用@Pointcut 注解 的方法一般使用 private 的标识方法,即没有实际作用的方法。

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
@Aspect
public class MyAspect {
@After(value = "mypt()")
public void myAfter() {
System.out.println("执行最终通知,总是会执行");
// 一般做资源清除工作的。
}
@Before(value = "mypt()")
public void myBefore() {
System.out.println("前置通知,目标方法之前执行");
// 一般做资源清除工作的。
}
/**
* @Pointcut: 定义和管理切入点的, 如果你的项目中有多个切入点表达式是重复的,
* 可以复用的,可以使用 @Pointcut
* 属性:value 切入点表达式
* 位置: 在自定义方法的上面
* 特点:
* 当使用 @Pointcut定义在一个方法的上面,此时这个方法的名称就是切入点表达式的别名
* 其他的通知中,value属性就可以使用这个方法名称,代替切入点表达式了
*/
@Pointcut(value = "execution(* *..SomeServiceImpl.doThird(..))")
private void mypt() {

}

}

如果目标类没有接口,使用cglib动态代理,Spring框架会自动应用cglib

1
2
3
4
5
<!--
如果你期望目标类有接口,使用cglib代理
proxy-target-class="true": 告诉框架,使用cglib动态代理
-->
<aop:aspectj-autoproxy proxy-target-class="true" />

Spring 集成 MyBatis

将mybatis框架和spring集成到一起,像一个框架一样使用

用到的技术:ioc

为什么ioc:能够把mybatis和spring集成到一起,像一个框架,是因为ioc能创建对象。可以把mybatis中的对象交给spring统一创建,开发人员从spring中获取对象。开发人员就不用同时面对两个或多个框架了,就面对一个spring

mybatis使用步骤,对象

  1. 定义dao接口,StudentDao
  2. 定义mapper文件,StudentDao.xml
  3. 定义mybatis 的主配置文件
  4. 创建dao代理对象,StudentDao dao = SqlSession.getMapper(StudentDao.class)
  5. List<Student> students = dao.selectStudents();

要使用dao对象,需要使用getMapper() 方法

怎么能使用getMapper()方法,需要哪些条件

  1. 获取 SqlSession对象, 使用SqlSessionFactory的 openSession() 方法
  2. 创建SqlSessionFactory对象。 通过读取mybatis主配置文件,能创建SqlSessionFactory对象

需要有SqlSessionFactory对象,使用Factory能获取SqlSession, 有了SqlSession就能有dao, 目的就是获取dao对象

Factory创建需要读取主配置文件

主配置文件:

  1. 数据库信息
  2. mapper文件的位置

我们会使用独立的连接池类替换mybatis默认自己带的,把连接池类也交给spring创建


通过以上说明,我们需要spring创建以下对象

  1. 独立的连接池类的对象,使用阿里的druid连接池
  2. SqlSessionFactory对象
  3. 创建dao对象

需要学习的就是上面三个对象的创建语法,使用xml文件的bean标签

将 MyBatis 与 Spring 进行整合,主要解决的问题就是将 SqlSessionFactory 对象交由 Spring 来管理。所以,该整合,只需要将 SqlSessionFactory 的对象生成器 SqlSessionFactoryBean 注册在 Spring 容器中,再将其注入给 Dao 的实现类即可完成整合。 实现 Spring 与 MyBatis 的整合常用的方式:扫描的 Mapper 动态代理 Spring 像插线板一样,mybatis 框架是插头,可以容易的组合到一起。插线板 spring 插 上 mybatis,两个框架就是一个整体。

步骤:

  1. 新建Maven项目
  2. 加入maven依赖
    1. spring 依赖
    2. mybatis 依赖
    3. mysql驱动
    4. spring事务的依赖
    5. mybatis和spring集成的依赖:mybatis官方提供的,用来在spring项目中创建mybatis的SqlSessionFactory, dao对象的
  3. 创建实体类
  4. 创建dao接口mapper文件
  5. 创建mybatis主配置文件
  6. 创建Service接口实现类,属性是dao
  7. 创建spring配置文件:声明mybatis对象交给spring创建
    1. 数据源
    2. SqlSessionFactory
    3. Dao对象
    4. 声明自定义的service
  8. 创建测试类,获取Service对象,通过service调用dao完成数据库的访问

MySQL 创建数据库 springdb,新建表 Student

maven 依赖 pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.bjpowernode</groupId>
<artifactId>ch07-spring-mybatis</artifactId>
<version>1.0-SNAPSHOT</version>


<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>

<dependencies>
<!-- 单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!-- spring核心ioc-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<!-- 做spring事务用到的-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<!-- mybatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.1</version>
</dependency>
<!-- mybatis和spring集成的依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<!-- mysql驱动-->
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.9</version>
</dependency>
<!-- 阿里公司数据库连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>
</dependencies>

<build>
<!-- 目的是把src/main/java目录中的xml文件包含到输出结果中。输出到classes目录中-->
<resources>
<resource>
<directory>src/main/java</directory><!--所在的目录-->
<includes><!--包括目录下的.properties,.xml 文件都会扫描到-->
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
<!-- 指定jdk版本-->
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

定义实体类 Student

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
public class Student {
private Integer id;
private String name;
private String email;
private Integer age;
public Student() {
}
public Student(Integer id, String name, String email, Integer age) {
this.id = id;
this.name = name;
this.email = email;
this.age = age;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", email='" + email + '\'' +
", age=" + age +
'}';
}
}

定义 StudentDao 接口

1
2
3
4
public interface StudentDao {
int insertStudent(Student student);
List<Student> selectStudents();
}

定义映射文件 mapper

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bjpowernode.dao.StudentDao">
<insert id="insertStudent">
insert into student values(#{id}, #{name}, #{email}, #{age})
</insert>
<select id="selectStudents" resultType="com.bjpowernode.domain.Student">
select id, name, email, age from student order by id desc
</select>
</mapper>

定义 Service 接口和实现类

1
2
3
4
public interface StudentService {
int addStudent(Student student);
List<Student> queryStudents();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class StudentServiceImpl implements StudentService {
private StudentDao studentDao;
public void setStudentDao(StudentDao studentDao) {
this.studentDao = studentDao;
}
@Override
public int addStudent(Student student) {
int nums = studentDao.insertStudent(student);
return nums;
}
@Override
public List<Student> queryStudents() {
List<Student> students = studentDao.selectStudents();
return students;
}
}

定义 MyBatis 主配置文件

在 src 下定义 MyBatis 的主配置文件,命名为 mybatis.xml。

这里有两点需要注意

(1)主配置文件中不再需要数据源的配置了。因为数据源要交给 Spring 容器来管理了。

(2)这里对 mapper 映射文件的注册,使用<package/>标签,即只需给出 mapper 映射文件 所在的包即可。因为 mapper 的名称与 Dao 接口名相同,可以使用这种简单注册方式。这种 方式的好处是,若有多个映射文件,这里的配置也是不用改变的。当然,也可使用原来的<resource/>标签方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<!-- 设置别名-->
<typeAliases>
<!-- name:实体类所在的包名 -->
<package name="com.bjpowernode.domain"/>
</typeAliases>
<!-- sql mapper(sql映射文件)的位置 -->
<mappers>
<!-- name: 是包名,这个包中所有的mapper.xml一次都能加载 -->
<package name="com.bjpowernode.dao"/>
</mappers>
</configuration>

修改 Spring 配置文件

(1) 数据源的配置

使用 JDBC 模板,首先需要配置好数据源,数据源直接以 Bean 的形式配置在 Spring 配 置文件中。根据数据源的不同,其配置方式不同:

Druid 数据源 DruidDataSource

Druid 是阿里的开源数据库连接池。是 Java 语言中最好的数据库连接池。Druid 能够提供强大的监控和扩展功能。Druid 与其他数据库连接池的最大区别是提供数据库的

官网:https://github.com/alibaba/druid

使用地址:https://github.com/alibaba/druid/wiki/

配置连接池:

(2) 从属性文件读取数据库连接信息

为了便于维护,可以将数据库连接信息写入到属性文件中,使 Spring 配置文件从中读取数据。

属性文件名称自定义,但一般都是放在 src 下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!--    把数据库的配置信息,写在一个独立的文件,便于修改数据库的配置信息
spring知道jdbc.properties文件的位置
-->
<!-- <context:property-placeholder location="classpath:jdbc.properties"/>-->
<util:properties id="jdbc" local-override="true" location="classpath:jdbc.properties"/>
<!-- 声明数据源DataSource, 作用是连接数据库的 -->
<bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<!-- 使用set注入给DruidDataSource提供连接数据库信息 -->
<!-- 使用属性配置文件中的数据,语法${key} -->
<property name="url" value="#{jdbc['jdbc.url']}" />
<property name="username" value="#{jdbc['jdbc.username']}"/>
<property name="password" value="#{jdbc['jdbc.passwd']}"/>
<property name="maxActive" value="#{jdbc['jdbc.max']}"/>
</bean>

(3) 注册 SqlSessionFactoryBean

1
2
3
4
5
6
7
8
9
10
11
<!--    声明的是mybatis中提供的SqlSessionFactoryBean类,这个类内部创建SqlSessionFactory的 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- set注入,把数据库连接池付给了dataSource属性 -->
<property name="dataSource" ref="myDataSource" />
<!--
mybatis主配置文件的位置
configLocation属性是Resource类型的,读取配置文件的
它的赋值,使用value, 指定文件的路径,使用classpath:表示文件的位置
-->
<property name="configLocation" value="classpath:mybatis.xml"/>
</bean>

(4) 配置DAO类

定义 Mapper 扫描配置器 MapperScannerConfigurer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--
创建 dao 对象,使用SqlSession的getMapper(StudentDao.class)
MapperScannerConfigurer: 在内部调用getMapper(),生成每个dao接口的代理对象
-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 指定SqlSessionFactory对象的id -->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<!-- 指定包名,包名是dao接口所在的包名
MapperScannerConfigurer会扫描这个包中的所有接口,把每个接口都执行
一次getMapper()方法,得到每个接口的dao对象
创建好的dao对象放入spring的容器中, dao对象的默认名称是 接口名首字母小写
-->
<property name="basePackage" value="com.bjpowernode.dao"/>
</bean>

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testDaoInsert() {
String config = "applicationContext.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
StudentDao dao = (StudentDao) ctx.getBean("studentDao");
Student student = new Student();
student.setId(1013);
student.setName("周峰");
student.setEmail("kkscsa@qq.com");
student.setAge(20);
int nums = dao.insertStudent(student);
System.out.println("num = " + nums);
}

向 Service 注入接口名

1
2
3
4
<!--    声明service       -->
<bean id="studentService" class="com.bjpowernode.service.impl.StudentServiceImpl">
<property name="studentDao" ref="studentDao"/>
</bean>

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
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
<?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/util"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">
<!-- 把数据库的配置信息,写在一个独立的文件,便于修改数据库的配置信息
spring知道jdbc.properties文件的位置
-->
<!-- <context:property-placeholder location="classpath:jdbc.properties"/>-->
<util:properties id="jdbc" local-override="true" location="classpath:jdbc.properties"/>
<!-- 声明数据源DataSource, 作用是连接数据库的 -->
<bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<!-- 使用set注入给DruidDataSource提供连接数据库信息 -->
<!-- 使用属性配置文件中的数据,语法${key} -->
<property name="url" value="#{jdbc['jdbc.url']}" />
<property name="username" value="#{jdbc['jdbc.username']}"/>
<property name="password" value="#{jdbc['jdbc.passwd']}"/>
<property name="maxActive" value="#{jdbc['jdbc.max']}"/>
</bean>
<!-- 声明的是mybatis中提供的SqlSessionFactoryBean类,这个类内部创建SqlSessionFactory的 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- set注入,把数据库连接池付给了dataSource属性 -->
<property name="dataSource" ref="myDataSource" />
<!--
mybatis主配置文件的位置
configLocation属性是Resource类型的,读取配置文件的
它的赋值,使用value, 指定文件的路径,使用classpath:表示文件的位置
-->
<property name="configLocation" value="classpath:mybatis.xml"/>
</bean>

<!--
创建 dao 对象,使用SqlSession的getMapper(StudentDao.class)
MapperScannerConfigurer: 在内部调用getMapper(),生成每个dao接口的代理对象
-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 指定SqlSessionFactory对象的id -->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<!-- 指定包名,包名是dao接口所在的包名
MapperScannerConfigurer会扫描这个包中的所有接口,把每个接口都执行
一次getMapper()方法,得到每个接口的dao对象
创建好的dao对象放入spring的容器中, dao对象的默认名称是 接口名首字母小写
-->
<property name="basePackage" value="com.bjpowernode.dao"/>
</bean>
<!-- 声明service -->
<bean id="studentService" class="com.bjpowernode.service.impl.StudentServiceImpl">
<property name="studentDao" ref="studentDao"/>
</bean>

</beans>

Spring 事务

Spring 的事务管理

事务原本是数据库中的概念,在 Dao 层。但一般情况下,需要将事务提升到业务层, 即 Service 层。这样做是为了能够使用事务的特性来管理具体的业务。

在 Spring 中通常可以通过以下两种方式来实现对事务的管理:

(1)使用 Spring 的事务注解管理事务

(2)使用 AspectJ 的 AOP 配置管理事务

Spring 事务管理 API

Spring 的事务管理,主要用到两个事务相关的接口。

(1) 事务管理器接口

事务管理器是 PlatformTransactionManager 接口对象。其主要用于完成事务的提交、回滚,及获取事务的状态信息。

常用的两个实现类

PlatformTransactionManager 接口有两个常用的实现类:

  • DataSourceTransactionManager:使用 JDBC 或 MyBatis 进行数据库操作时使用。
  • HibernateTransactionManager:使用 Hibernate 进行持久化数据时使用。

Spring 的回滚方式

Spring 事务的默认回滚方式是:发生运行时异常和 error 时回滚,发生受查(编译)异常时 提交。不过,对于受查异常,程序员也可以手工设置其回滚方式。

回顾错误与异常

Throwable 类是 Java 语言中所有错误或异常的超类。只有当对象是此类(或其子类之一) 的实例时,才能通过 Java 虚拟机或者 Java 的 throw 语句抛出。

Error 是程序在运行过程中出现的无法处理的错误,比如 OutOfMemoryError、 ThreadDeath、NoSuchMethodError 等。当这些错误发生时,程序是无法处理(捕获或抛出) 的,JVM 一般会终止线程。

程序在编译和运行时出现的另一类错误称之为异常,它是 JVM 通知程序员的一种方式。 通过这种方式,让程序员知道已经或可能出现错误,要求程序员对其进行处理。

异常分为运行时异常与受查异常。

运行时异常,是 RuntimeException 类或其子类,即只有在运行时才出现的异常。如, NullPointerException、ArrayIndexOutOfBoundsException、IllegalArgumentException 等均属于 运行时异常。这些异常由 JVM 抛出,在编译时不要求必须处理(捕获或抛出)。但,只要代 码编写足够仔细,程序足够健壮,运行时异常是可以避免的。

受查异常,也叫编译时异常,即在代码编写时要求必须捕获或抛出的异常,若不处理, 则无法通过编译。如 SQLException,ClassNotFoundException,IOException 等都属于受查异常。

RuntimeException 及其子类以外的异常,均属于受查异常。当然,用户自定义的 Exception 的子类,即用户自定义的异常也属受查异常。程序员在定义异常时,只要未明确声明定义的 为 RuntimeException 的子类,那么定义的就是受查异常。

(2) 事务定义接口

事务定义接口 TransactionDefinition 中定义了事务描述相关的三类常量:事务隔离级别、 事务传播行为、事务默认超时时限,及对它们的操作。

定义了五个事务隔离级别常量

这些常量均是以 ISOLATION_开头。即形如 ISOLATION_XXX。

  • DEFAULT:采用 DB 默认的事务隔离级别。MySql 的默认为 REPEATABLE_READ; Oracle 默认为 READ_COMMITTED。
  • READ_UNCOMMITTED:读未提交。未解决任何并发问题。
  • READ_COMMITTED:读已提交。解决脏读,存在不可重复读与幻读。
  • REPEATABLE_READ:可重复读。解决脏读、不可重复读,存在幻读
  • SERIALIZABLE:串行化。不存在并发问题。

定义了七个事务传播行为常量

所谓事务传播行为是指,处于不同事务中的方法在相互调用时,执行期间事务的维护情况。如,A 事务中的方法 doSome()调用 B 事务中的方法 doOther(),在调用执行期间事务的 维护情况,就称为事务传播行为。事务传播行为是加在方法上的。

事务传播行为常量都是以 PROPAGATION_ 开头,形如 PROPAGATION_XXX。

PROPAGATION_REQUIRED

PROPAGATION_REQUIRES_NEW

PROPAGATION_SUPPORTS

PROPAGATION_MANDATORY

PROPAGATION_NESTED

PROPAGATION_NEVER

PROPAGATION_NOT_SUPPORTED

a、 PROPAGATION_REQUIRED:

指定的方法必须在事务内执行。若当前存在事务,就加入到当前事务中;若当前没有事 务,则创建一个新事务。这种传播行为是最常见的选择,也是 Spring 默认的事务传播行为。

如该传播行为加在 doOther()方法上。若 doSome()方法在调用 doOther()方法时就是在事 务内运行的,则 doOther()方法的执行也加入到该事务内执行。若 doSome()方法在调用 doOther()方法时没有在事务内执行,则 doOther()方法会创建一个事务,并在其中执行。

b、PROPAGATION_SUPPORTS

指定的方法支持当前事务,但若当前没有事务,也可以以非事务方式执行。

c、 PROPAGATION_REQUIRES_NEW

总是新建一个事务,若当前存在事务,就将当前事务挂起,直到新事务执行完毕。

定义了默认事务超时时限

常量 TIMEOUT_DEFAULT 定义了事务底层默认的超时时限,sql 语句的执行时长。

注意,事务的超时时限起作用的条件比较多,且超时的时间计算点较复杂。所以,该值一般就使用默认值即可。

程序举例环境搭建

Step0:创建数据库表

创建两个数据库表 sale , goods

sale 销售表

goods 商品表

Step1: maven 依赖 pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.bjpowernode</groupId>
<artifactId>ch08-spring-trans</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>

<dependencies>
<!-- 单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!-- spring核心ioc-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<!-- 做spring事务用到的-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<!-- mybatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.1</version>
</dependency>
<!-- mybatis和spring集成的依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<!-- mysql驱动-->
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.9</version>
</dependency>
<!-- 阿里公司数据库连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>
</dependencies>

<build>
<!-- 目的是把src/main/java目录中的xml文件包含到输出结果中。输出到classes目录中-->
<resources>
<resource>
<directory>src/main/java</directory><!--所在的目录-->
<includes><!--包括目录下的.properties,.xml 文件都会扫描到-->
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
<!-- 指定jdk版本-->
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

Step2:创建实体类

创建实体类 Sale 与 Goods

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
public class Sale {
private Integer id;
private Integer gid;
private Integer nums;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public Integer getGid() {
return gid;
}

public void setGid(Integer gid) {
this.gid = gid;
}

public Integer getNums() {
return nums;
}

public void setNums(Integer nums) {
this.nums = nums;
}
}

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
public class Goods {
private Integer id;
private String name;
private Integer amount;
private Float price;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

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

public Integer getAmount() {
return amount;
}

public void setAmount(Integer amount) {
this.amount = amount;
}

public Float getPrice() {
return price;
}

public void setPrice(Float price) {
this.price = price;
}
}

Step3:定义 dao 接口

1
2
3
4
public interface SaleDao {
//增加销售记录
int insertSale(Sale sale);
}
1
2
3
4
5
6
7
8
public interface GoodsDao {
//更新库存
// goods 表示本次用户购买的商品信息, id, 购买数量
int updateGoods(Goods goods);

//查询商品购买信息
Goods selectGoods(Integer id);
}

Step4:定义 dao 接口对应的 sql 映射文件

SaleDao.xml

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bjpowernode.dao.SaleDao">
<insert id="insertSale" >
insert into sale(gid, nums) values(#{gid}, #{nums})
</insert>
</mapper>

GoodsDao.xml

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bjpowernode.dao.GoodsDao">
<select id="selectGoods" resultType="com.bjpowernode.domain.Goods">
select id, name, amount, price from goods where id = #{gid}
</select>
<update id="updateGoods">
update set amount = amount - #{amount} where id = #{id}
</update>
</mapper>

Step5:定义异常类

定义 service 层可能会抛出的异常类 NotEnoughException

1
2
3
4
5
6
7
8
9
public class NotEnoughException extends RuntimeException {
public NotEnoughException() {
super();
}

public NotEnoughException(String message) {
super(message);
}
}

Step6:定义 Service 接口

1
2
3
4
public interface BuyGoodsService {
//购买商品的方法,goodsId:购买商品的编号,nums:购买的数量
void buy(Integer goodsId, Integer nums);
}

Step7:定义 service 的实现类

定义 service 层接口的实现类 BuyGoodsServiceImpl

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
public class BuyGoodsServiceImpl implements BuyGoodsService {
private SaleDao saleDao;
private GoodsDao goodsDao;

@Override
public void buy(Integer goodsId, Integer nums) {
System.out.println("=============== buy 方法的开始 ===================");
//记录销售信息,向sale表添加记录
Sale sale = new Sale();
sale.setGid(goodsId);
sale.setNums(nums);
saleDao.insertSale(sale);
//更新库存
Goods goods = goodsDao.selectGoods(goodsId);
if(goods == null) {
throw new NullPointerException("编号是:" + goodsId + " ,商品不存在");
} else if(goods.getAmount() < nums) {
//商品库存不足
throw new NotEnoughException("编号是: " + goodsId + " ,商品库存不足");
}
Goods buyGoods = new Goods();
buyGoods.setId(goodsId);
buyGoods.setAmount(nums);
goodsDao.updateGoods(buyGoods);
System.out.println("============= buy方法的完成 ===============");
}

public void setSaleDao(SaleDao saleDao) {
this.saleDao = saleDao;
}

public void setGoodsDao(GoodsDao goodsDao) {
this.goodsDao = goodsDao;
}
}

Step8:修改 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
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
<?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/util"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">
<!-- 把数据库的配置信息,写在一个独立的文件,便于修改数据库的配置信息
spring知道jdbc.properties文件的位置
-->
<!-- <context:property-placeholder location="classpath:jdbc.properties"/>-->
<util:properties id="jdbc" local-override="true" location="classpath:jdbc.properties"/>
<!-- 声明数据源DataSource, 作用是连接数据库的 -->
<bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<!-- 使用set注入给DruidDataSource提供连接数据库信息 -->
<!-- 使用属性配置文件中的数据,语法${key} -->
<property name="url" value="#{jdbc['jdbc.url']}" />
<property name="username" value="#{jdbc['jdbc.username']}"/>
<property name="password" value="#{jdbc['jdbc.passwd']}"/>
<property name="maxActive" value="#{jdbc['jdbc.max']}"/>
</bean>
<!-- 声明的是mybatis中提供的SqlSessionFactoryBean类,这个类内部创建SqlSessionFactory的 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- set注入,把数据库连接池付给了dataSource属性 -->
<property name="dataSource" ref="myDataSource" />
<!--
mybatis主配置文件的位置
configLocation属性是Resource类型的,读取配置文件的
它的赋值,使用value, 指定文件的路径,使用classpath:表示文件的位置
-->
<property name="configLocation" value="classpath:mybatis.xml"/>
</bean>

<!--
创建 dao 对象,使用SqlSession的getMapper(StudentDao.class)
MapperScannerConfigurer: 在内部调用getMapper(),生成每个dao接口的代理对象
-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 指定SqlSessionFactory对象的id -->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<!-- 指定包名,包名是dao接口所在的包名
MapperScannerConfigurer会扫描这个包中的所有接口,把每个接口都执行
一次getMapper()方法,得到每个接口的dao对象
创建好的dao对象放入spring的容器中, dao对象的默认名称是 接口名首字母小写
-->
<property name="basePackage" value="com.bjpowernode.dao"/>
</bean>
<!-- 声明service -->
<bean id="buyService" class="com.bjpowernode.service.impl.BuyGoodsServiceImpl">
<property name="goodsDao" ref="goodsDao"/>
<property name="saleDao" ref="saleDao"/>
</bean>

</beans>

Step9:定义测试类

定义测试类 MyTest。现在就可以在无事务代理的情况下运行了。

1
2
3
4
5
6
7
8
9
10
public class MyTest {
@Test
public void test01() {
String config = "applicationContext.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
//从容器中获取service
BuyGoodsService service = (BuyGoodsService) ctx.getBean("buyService");
service.buy(1001, 10);
}
}

使用 Spring 的事务注解管理事务

通过@Transactional 注解方式,可将事务织入到相应 public 方法中,实现事务管理。

@Transactional 的所有可选属性如下所示:

  • propagation:用于设置事务传播属性。该属性类型为 Propagation 枚举,默认值为Propagation.REQUIRED

  • isolation:用于设置事务的隔离级别。该属性类型为 Isolation 枚举,默认值为 Isolation.DEFAULT。

  • readOnly:用于设置该方法对数据库的操作是否是只读的。该属性为 boolean,默认值 为 false。

  • timeout:用于设置本操作与数据库连接的超时时限。单位为秒,类型为 int,默认值为 -1,即没有时限。

  • rollbackFor:指定需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若只有 一个异常类时,可以不使用数组。

  • rollbackForClassName:指定需要回滚的异常类类名。类型为 String[],默认值为空数组。 当然,若只有一个异常类时,可以不使用数组。

  • noRollbackFor:指定不需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若 只有一个异常类时,可以不使用数组。

  • noRollbackForClassName:指定不需要回滚的异常类类名。类型为 String[],默认值为空 数组。当然,若只有一个异常类时,可以不使用数组。

需要注意的是,@Transactional 若用在方法上,只能用于 public 方法上。对于其他非 public 方法,如果加上了注解@Transactional,虽然 Spring 不会报错,但不会将指定事务织入到该方法中。因为 Spring 会忽略掉所有非 public 方法上的@Transaction 注解。

若@Transaction 注解在类上,则表示该类上所有的方法均将在执行时织入事务。

实现注解的事务步骤

1. 声明事务管理器对象

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

2. 开启事务注解驱动,告诉spring框架,使用注解的方式管理事务

spring 使用 aop 机制,创建@Transactional 所在的类代理对象,给方法增加事务功能

spring 给业务方法增加事务:在你的业务方法执行之前,先开启事务,在业务方法之后提交或回滚事务,使用aop环绕通知

1
2
3
4
<!--    2. 开启事务注解驱动,告诉spring使用注解管理事务,创建代理对象
transaction-manager: 事务管理器对象id
-->
<tx:annotation-driven transaction-manager="TransactionManager"/>

3. 在你的方法上面加入@Trancational

1
2
3
4
5
6
7
8
9
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.DEFAULT,
readOnly = false,
rollbackFor = {
NullPointerException.class,
NotEnoughException.class
}
)

一般直接写@Transactional

rollbackFor: 表示发生指定的异常一定回滚

处理逻辑是:

1)Spring框架会首先检查方法抛出的异常是不是在rollbackFor的属性值中,如果异常在rollbackFor列表中,不管是什么类型的异常,一定回滚

2)如果你抛出的异常不在rollbackFor列表中,spring会判断异常是不是RuntimeException,如果是一定回滚

使用 AspectJ 的 AOP 配置管理事务

适合大型项目,有很多类,方法,需要大量配置事务,使用aspectj框架功能,在spring配置文件中声明类,方法需要的事务。这种方式业务方法和事务配置完全分离

使用 XML 配置事务代理的方式的不足是,每个目标类都需要配置事务代理。当目标类 较多,配置文件会变得非常臃肿。

实现步骤:都是在xml配置文件中实现

Step 1: maven 依赖 pom.xml

要使用的是aspectj框架,需要加入依赖

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

Step2: 在容器中添加事务管理器

声明事务管理器对象

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

Step3: 声明方法需要的事务类型(配置方法的事务属性【隔离级别,传播行为,超时】)

配置事务通知,为事务通知设置相关属性。用于指定要将事务以什么方式织入给哪些方法。

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
<!--    2. 声明业务方法它的事务属性(隔离级别,传播行为,超时时间)
id: 自定义名称,表示 <tx:advice> 和 </tx:advice> 之间的配置内容的
-->
<tx:advice id="myAdvice" transaction-manager="transactionManager">
<!-- <tx:attributes>: 配置事务属性-->
<tx:attributes>
<!-- 给具体的方法配置事务属性,method可以有多个,分别给不同的方法设置事务的属性
name: 方法名称
1) 完整的方法名称,不带有包和类
2) 方法可以使用通配符,*表示任意字符
propagation: 传播行为,枚举值
isolation: 隔离级别
rollback-for: 你指定的异常类名,全限定类名。发生异常一定回滚
-->
<tx:method name="buy" propagation="REQUIRED" isolation="DEFAULT"
rollback-for="java.lang.NullPointerException,
com.bjpowernode.execp.NotEnoughException"/>
<tx:method name="add*" propagation="REQUIRES_NEW" />
<!-- 指定修改方法-->
<tx:method name="modify*" />
<!-- 删除方法-->
<tx:method name="remove*" />
<!-- 查询方法,query, search, find -->
<tx:method name="*" propagation="SUPPORTS" read-only="true"/>
</tx:attributes>
</tx:advice>

Step4:配置aop: 指定哪些类要创建代理

配置增强器,指定将配置好的事务通知,织入给谁。

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--    配置aop-->
<aop:config>
<!-- 配置切入点表达式,指定哪些包中的类,要使用事务
id: 切入点表达式的名称,唯一值
expression: 切入点表达式,指定哪些类要使用事务, aspectj会创建代理对象
-->
<aop:pointcut id="servicePt" expression="execution(* *..service..*.*(..))"/>
<!-- 配置增强器:关联advice和pointcut
advice-ref: 通知,上面tx:advice那里的配置
pointcut-ref: 切入点表达式的id
-->
<aop:advisor advice-ref="myAdvice" pointcut-ref="servicePt" />
</aop:config>

Step5:修改测试类

1
2
3
4
5
6
7
8
9
@Test
public void test01() {
String config = "applicationContext.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
//从容器中获取service
BuyGoodsService service = (BuyGoodsService) ctx.getBean("buyService");
System.out.println("service是代理:" + service.getClass().getName());
service.buy(1001, 1000);
}

Spring 与 Web

使用 Spring 的监听器 ContextLoaderListener

对于 Web 应用来说,ServletContext 对象是唯一的,一个 Web 应用,只有一个 ServletContext 对象,该对象是在 Web 应用装载时初始化的。若将 Spring 容器的创建时机, 放在 ServletContext 初始化时,就可以保证 Spring 容器的创建只会执行一次,也就保证了 Spring 容器在整个应用中的唯一性。

当 Spring 容器创建好后,在整个应用的生命周期过程中,Spring 容器应该是随时可以被访问的。即,Spring 容器应具有全局性。而放入 ServletContext 对象的属性,就具有应用的 全局性。所以,将创建好的 Spring 容器,以属性的形式放入到 ServletContext 的空间中,就 保证了 Spring 容器的全局性。

上述的这些工作,已经被封装在了如下的 Spring 的 Jar 包的相关 API 中: spring-web-5.2.5.RELEASE

Step1: maven依赖pom.xml

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

Step2:注册监听器 ContextLoaderListener

若要在 ServletContext 初始化时创建Spring 容器 , 就需要使用监听器接口ServletContextListener 对 ServletContext 进行监听。在 web.xml 中注册该监听器。

1
2
3
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

Spring 为该监听器接口定义了一个实现类 ContextLoaderListener,完成了两个很重要的工作:创建容器对象,并将容器对象放入到了 ServletContext 的空间中。

Step3:指定 Spring 配置文件的位置

ContextLoaderListener 在对 Spring 容器进行创建时,需要加载 Spring 配置文件。其默认 的 Spring 配置文件位置与名称为:WEB-INF/applicationContext.xml。但,一般会将该配置文件放置于项目的 classpath 下,即 src 下,所以需要在 web.xml 中对 Spring 配置文件的位置及名称进行指定。

1
2
3
4
5
6
7
8
9
10
<!--    注册监听器ContextLoaderListener
监听器被创建对象后,会读取/WEB-INF/applicationContext.xml
为什么读取文件? 因为在监听器中要创建ApplicationContext对象,需要加载配置文件。
/WEB-INF/applicationContext.xml就是监听器默认读取spring配置文件的路径
可以修改文件的默认位置,使用context-param重新指定文件的位置
-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</context-param>

Step4:获取 Spring 容器对象

在 Servlet 中获取容器对象的常用方式有两种:

(1) 直接从 ServletContext 中获取

从对监听器 ContextLoaderListener 的源码分析可知,容器对象在 ServletContext 的中存放的 key 为 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE。所以,可以直接通过 ServletContext 的 getAttribute()方法,按照指定的 key 将容器对象获取到。

1
2
3
4
5
6
7
WebApplicationContext ctx = null;
String key = WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE;
Object attr = getServletContext().getAttribute(key);
if(attr != null) {
ctx = (WebApplicationContext) attr;
}
StudentService service = (StudentService)ctx.getBean("studentService");

(2) 通过 WebApplicationContextUtils 获取

工具类 WebApplicationContextUtils 有一个方法专门用于从 ServletContext 中获取 Spring 容器对象:getRequiredWebApplicationContext(ServletContext sc)

1
2
3
WebApplicationContext ctx = null;
ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext());
StudentService service = (StudentService)ctx.getBean("studentService");

完整servlet代码

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
public class RegisterServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String strId = request.getParameter("id");
String strName = request.getParameter("name");
String strEmail = request.getParameter("email");
String strAge = request.getParameter("age");
// String config = "spring.xml";
// ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
WebApplicationContext ctx = null;
//获取ServletContext中的容器对象,创建好的容器对象,拿来就用
// String key = WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE;
// Object attr = getServletContext().getAttribute(key);
// if(attr != null) {
// ctx = (WebApplicationContext) attr;
// }
//使用框架中的方法获取容器对象
ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext());
System.out.println("容器对象的信息 ========== " + ctx);

//获取Service
StudentService service = (StudentService)ctx.getBean("studentService");
Student student = new Student();
student.setId(Integer.parseInt(strId));
student.setName(strName);
student.setEmail(strEmail);
student.setAge(Integer.valueOf(strAge));
service.addStudent(student);
request.getRequestDispatcher("/result.jsp").forward(request, response);
}
}

求大佬赏个饭