课程内容介绍
视频地址:https://www.bilibili.com/video/BV1Vf4y127N5
Spring 概念
IoC 容器
IoC 底层原理
IoC 接口(BeanFactory)
IoC 操作 Bean 管理(基于xml)
IoC 操作 Bean 管理(基于注解)
AOP
JdbcTemplate
事务管理
Spring 5 新特性
Spring 框架概述
Spring 是轻量级的开源的 JavaEE 框架
Spring 可以解决企业应用开发的复杂性
两个核心部分:IoC、AOP
- IoC:Inversion of Control,控制反转。把创建对象过程交给 Spring 进行管理
- AOP:Aspect Oriented Programming,面向切面编程。不修改源代码进行功能增强
Spring 特点
- 方便解耦,简化开发
- AOP 编程支持
- 方便程序测试
- 方便和其他框架进行整合
- 方便进行事务操作
- 降低 API 开发难度
本课程选取 Spring 5.x
IoC
概念和原理
1、什么是 IoC ?
控制反转,把对象创建和对象之间的调用过程,交给 Spring 进行管理
使用 IoC 目的:降低耦合度
入门案例就是 IoC 实现
2、IoC 底层原理
xml 解析、工厂模式、反射
接口
1、IoC 思想基于 IoC 容器完成,IoC 容器底层就是对象工厂
2、Spring 提供 IoC 容器实现两种方式(两个接口):
(1) BeanFactory
:IoC 容器基本实现,是 Spring 内部使用接口,一般不提供开发人员使用
加载配置文件时不会创建对象,在获取(使用)对象时才创建
(2) ApplicationContext
: BeanFactory
接口的子接口,提供更多更强大的功能,一般由开发人员使用
加载配置文件时就会将配置的对象创建
3、 ApplicationContext
接口有实现类
IoC 操作 Bean 管理
1、什么是 Bean 管理
Bean 管理指的是两个操作:Spring 创建对象,Spring 注入属性
2、Bean 管理操作有两种方式
基于 xml 配置文件方式实现
基于注解方式实现
基于 xml 方式
基于 xml 方式创建对象
1 | <bean id="alice" class="com.yin.spring5.bean.User"></bean> |
在 Spring 配置文件中,使用 bean 标签,标签中添加对应属性,就可以实现对象创建
bean 标签有很多属性,常用属性:
id
:唯一标识class
:全类名
创建对象的时候,默认执行无参构造器
基于 xml 方式注入属性
DI:Dependency Injection,依赖注入,是 IoC 的一种实现方式
第一种注入方式:set 方法注入
1
2
3
4
5<!--set 方法注入-->
<bean id="alice" class="com.yin.spring5.bean.User">
<property name="name" value="Alice"/>
<property name="age" value="20"/>
</bean>第二种注入方式:有参构造器注入
1
2
3
4
5<!--有参构造器注入-->
<bean id="bob" class="com.yin.spring5.bean.User">
<constructor-arg name="name" value="Bob"/>
<constructor-arg name="age" value="20"/>
</bean>p 名称空间注入(了解即可)
添加约束:
1
xmlns:p="http://www.springframework.org/schema/p"
1
2
3<!--p命名空间-->
<bean id="cindy" class="com.yin.spring5.bean.User" p:name="Cindy" p:age="18">
</bean>c 名称空间注入(了解即可)
添加约束:
1
xmlns:c="http://www.springframework.org/schema/c"
1
2
3<!--c命名空间-->
<bean id="david" class="com.yin.spring5.bean.User" c:name="David" c:age="18">
</bean>
xml 注入其他类型
字面量
注入
null
1
2
3
4
5
6
7<!--注入null-->
<bean id="eva" class="com.yin.spring5.bean.User">
<property name="name" value="Eva"/>
<property name="age">
<null/>
</property>
</bean>属性值包含特殊字符
将特殊字符进行转义
1
2
3
4
5
6<!--属性值包含特殊字符-->
<bean id="special" class="com.yin.spring5.bean.User">
<!--将特殊字符进行转义-->
<property name="name" value="<<TRUMP>>"/>
<property name="age" value="18"/>
</bean>输出:
User{name='<<TRUMP>>', age=18}
使用 CDATA
1
2
3
4
5
6
7
8<!--属性值包含特殊字符-->
<bean id="special" class="com.yin.spring5.bean.User">
<!--使用 CDATA-->
<property name="name">
<value><![CDATA[<<BIDEN>>]]></value>
</property>
<property name="age" value="18"/>
</bean>输出:
User{name='<<BIDEN>>', age=18}
外部 bean
1
2
3
4
5public class UserDao {
public void delete() {
System.out.println("UserDao#delete()...");
}
}1
2
3
4
5
6
7
8
9
10
11
12public class UserService {
UserDao dao;
public void setDao(UserDao dao) {
this.dao = dao;
}
public void deleteUser() {
System.out.println("UserService#deleteUser()...");
dao.delete();
}
}1
2
3
4
5
6<bean id="userDao" class="com.yin.spring5.dao.UserDao">
</bean>
<bean id="userService" class="com.yin.spring5.service.UserService">
<!--注入外部 bean-->
<property name="dao" ref="userDao"/>
</bean>内部 bean
1
2
3
4public class Department {
private String departName;
// getter setter toString 等方法在此省略
}1
2
3
4
5
6public class Employee {
private String empName;
private String email;
private Department dept;
// getter setter toString 等方法在此省略
}1
2
3
4
5
6
7
8
9
10<bean id="employee" class="com.yin.spring5.bean.Employee">
<property name="empName" value="杰克"/>
<property name="email" value="jack@jack.com"/>
<!--内部 bean-->
<property name="dept">
<bean class="com.yin.spring5.bean.Department">
<property name="departName" value="开发部"/>
</bean>
</property>
</bean>输出:
Employee{empName='杰克', email='jack@jack.com', dept=Department{departName='开发部'}}
级联赋值
写法一:
1
2
3
4
5
6
7
8<bean id="employee2" class="com.yin.spring5.bean.Employee">
<property name="empName" value="Pony"/>
<property name="email" value="pony@pony.com"/>
<property name="dept" ref="department"/>
</bean>
<bean id="department" class="com.yin.spring5.bean.Department">
<property name="departName" value="运维部"/>
</bean>输出:
Employee{empName='Pony', email='pony@pony.com', dept=Department{departName='运维部'}}
写法二:
1
2
3
4
5
6
7
8
9
10<bean id="employee2" class="com.yin.spring5.bean.Employee">
<property name="empName" value="Pony"/>
<property name="email" value="pony@pony.com"/>
<property name="dept" ref="department"/>
<!--注意这里-->
<property name="dept.departName" value="开发部"/>
</bean>
<bean id="department" class="com.yin.spring5.bean.Department">
<property name="departName" value="运维部"/>
</bean>输出:
Employee{empName='Pony', email='pony@pony.com', dept=Department{departName='开发部'}}
xml 注入集合属性
注入数组类型属性
1
2
3
4
5
6
7
8<!--数组类型注入-->
<property name="array">
<array>
<value>array1</value>
<value>array2</value>
<value>array3</value>
</array>
</property>注入 List 集合类型属性
1
2
3
4
5
6
7
8<!--List 类型注入-->
<property name="list">
<list>
<value>list1</value>
<value>list2</value>
<value>list3</value>
</list>
</property>注入 Map 集合类型属性
1
2
3
4
5
6
7
8<!--Map 类型注入-->
<property name="map">
<map>
<entry key="key1" value="value1"/>
<entry key="key2" value="value2"/>
<entry key="key3" value="value3"/>
</map>
</property>注入 Set 集合类型属性
1
2
3
4
5
6
7
8<!--Set 类型注入-->
<property name="set">
<set>
<value>set1</value>
<value>set2</value>
<value>set3</value>
</set>
</property>输出:
1
Student{stuName='Pony', array=[array1, array2, array3], list=[list1, list2, list3], map={key1=value1, key2=value2, key3=value3}, set=[set1, set2, set3]}
在集合里面设置对象类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<bean id="student" class="com.yin.spring5.bean.Student">
<!--省略部分代码,只看有关的-->
<!--在集合里面设置对象类型-->
<property name="courses">
<list>
<ref bean="course1"/>
<ref bean="course2"/>
</list>
</property>
</bean>
<bean id="course1" class="com.yin.spring5.bean.Course">
<property name="courseName" value="数据结构"/>
<property name="credit" value="5"/>
</bean>
<bean id="course2" class="com.yin.spring5.bean.Course">
<property name="courseName" value="操作系统"/>
<property name="credit" value="4"/>
</bean>输出:
1
[Course{courseName='数据结构', credit=5}, Course{courseName='操作系统', credit=4}]
把集合注入部分提取出来
a. 在配置文件中引入
util
名称空间1
2
3
4
5
6
7
8
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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 http://www.springframework.org/schema/util/spring-util.xsd">
</beans>b. 使用
util
标签完成 List 集合注入提取1
2
3
4public class Book {
private List<String> list;
// getter setter toString 等方法在此省略
}1
2
3
4
5
6
7
8
9
10<bean id="book" class="com.yin.spring5.bean.Book">
<property name="list" ref="bookList"/>
</bean>
<!--List 集合注入提取-->
<util:list id="bookList">
<value>Java编程思想</value>
<value>算法导论</value>
<value>Effective Java</value>
</util:list>输出:
1
[Java编程思想, 算法导论, Effective Java]
FactoryBean
普通 bean:配置文件中定义的 bean 类型就是返回类型
工厂 bean:配置文件中定义的 bean 类型可以和返回类型不一样
- 创建类,实现
FactoryBean
接口 - 实现接口的方法
1 | /** |
1 | <bean id="myFactoryBean" class="com.yin.spring5.bean.MyFactoryBean"> |
1 |
|
测试:
1 | Course{courseName='数据结构', credit=5} |
Bean 作用域
在 Spring 中,设置创建 bean 实例是单实例还是多实例(默认单实例)
1 |
|
输出:
1 | com.yin.spring5.bean.Book@433defed |
如何设置单实例/多实例?bean 标签中 scope
属性设置作用域
scope
属性值:
singleton
:单实例prototype
:多实例
将 scope 属性值设置为 `` 后,再进行上面测试:
1 | <bean id="book" class="com.yin.spring5.bean.Book" scope="prototype"> |
输出:
1 | com.yin.spring5.bean.Book@548a24a |
singleton
和 prototype
的区别:
singleton
是单实例,prototype
是多实例singleton
在 Spring 加载配置文件时就会创建单实例对象prototype
在获取对象时才会进行创建
Bean 生命周期
生命周期:从对象创建到销毁的过程
bean 生命周期:
- 通过构造器创建 bean 实例(无参构造)
- 为 bean 注入属性和对其他 bean 的引用(set 方法)
- 调用 bean 的初始化方法(需要进行配置初始化方法)
- 获取并使用 bean
- 当容器关闭时,调用 bean 的销毁方法(需要进行配置销毁方法)
代码验证:
1 | public class User { |
1 | <bean id="user" class="com.yin.spring5.bean.User" |
1 |
|
输出:
1 | User 无参构造器执行。。。 |
其实完整来讲,bean 的声明周期有 7 步。在初始化方法执行前后,分别有 postProcessBeforeInitialization
和 postProcessAfterInitialization
方法执行。
在上述代码的基础上,再添加以下内容:
1 | public class MyBeanPost implements BeanPostProcessor { |
1 | <bean id="myBeanPost" class="com.yin.spring5.bean.MyBeanPost"></bean> |
再次进行测试,输出如下:
1 | User 无参构造器执行。。。 |
综上,bean 生命周期完整表述如下:
- 通过构造器创建 bean 实例(无参构造)
- 为 bean 注入属性和对其他 bean 的引用(set 方法)
- 把 bean 实例传递给初始化前的后置处理
postProcessBeforeInitialization
- 调用 bean 的初始化方法(需要进行配置初始化方法)
- 把 bean 实例传递给初始化后的后置处理
postProcessAfterInitialization
- 获取并使用 bean
- 当容器关闭时,调用 bean 的销毁方法(需要进行配置销毁方法)
xml 自动装配
什么是自动装配?
根据指定装配规则(属性名称或属性类型),Spring 自动将匹配的属性值进行注入
bean 标签中的 autowire
属性可以配置自动装配,autowire
常用属性值:
byName
:根据名称装配(注入 bean 的 id 要与被注入 bean 的 set 方法一致)byType
:根据类型装配(有多个同类型的 bean 时,此属性值无法使用)
代码演示:
1 | public class Department { |
1 | public class Employee { |
1 | <bean id="dept" class="com.yin.spring5.autowire.Department"> |
输出:
1 | Employee{empName='Pony', dept=Department{deptName='开发部'}} |
若将 Employee
类中默认生成的 setDept
方法名修改为 setDept2
,则输出变为:
1 | Employee{empName='Pony', dept=Department{deptName='测试部'}} |
由此证实:**byName
的自动注入是根据 set 方法判断的,而不是类中的属性名**。
外部属性文件
以数据库连接池为例:
1 | <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> |
将数据库配置抽取到 properties 文件
1 | jdbc.driver=com.mysql.cj.jdbc.Driver |
则 Spring 配置文件改写如下,注意需要引入 context 名称空间:
1 |
|
基于注解方式
创建 bean
Spring 创建 bean 的四个注解:@Component
,@Controller
,@Service
,@Repository
。
1 | <!--开启组件扫描,将会扫描 base-package 及其子包下所有组件--> |
排除某些组件,以排除 @Controller
组件扫描为例:
1 | <!--开启组件扫描--> |
context:component-scan
标签部分属性:
base-package
:要扫描组件的基础包,默认将会扫描 base-package 及其子包下所有组件use-default-filters
:是否使用默认的扫描规则
context:component-scan
标签的子标签:
context:exclude-filter
:指定排除规则context:include-filter
:指定包含规则
属性注入
@Autowired
:根据属性类型自动装配@Qualifier
:根据属性名称注入@Resource
:可以根据类型注入,可以根据名称注入(这是 javax 包下的注解,已移除)@Value
:注入普通类型属性
AOP
AOP,Aspect Oriented Programming,面向切面编程
利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
底层原理
AOP 底层使用动态代理。有两种情况:
有接口,使用 JDK 动态代理
创建接口实现类代理对象,增强类的方法
没有接口,使用 CGLIB 动态代理
创建子类的代理对象,增强类的方法
JDK 动态代理
使用 JDK 动态代理,使用 java.lang.reflect.Proxy
类里面的方法创建代理对象
代码演示:
创建接口,定义方法
1
2
3
4
5public interface UserDao {
int add(int a, int b);
String update(String id);
}创建接口实现类,实现方法
1
2
3
4
5
6
7
8
9
10
11
12
13public class UserDaoImpl implements UserDao {
public int add(int a, int b) {
System.out.println("add 方法执行");
return a + b;
}
public String update(String id) {
System.out.println("update 方法执行");
return id;
}
}使用
java.lang.reflect.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
32
33
34public class JdkProxy {
public static void main(String[] args) {
// 创建接口实现类代理对象
Class[] interfaces = {UserDao.class};
UserDao userDao = new UserDaoImpl(); // 被代理对象
UserDao proxyInstance =
(UserDao) Proxy.newProxyInstance(JdkProxy.class.getClassLoader(),
interfaces, new UserDaoProxy(userDao));
System.out.println(proxyInstance.add(2, 3));
System.out.println(proxyInstance.update("abc"));
}
}
// 创建代理对象代码
class UserDaoProxy implements InvocationHandler {
private Object obj;
// 把被代理对象传递过来
public UserDaoProxy(Object obj) {
this.obj = obj;
}
// 增强的逻辑
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 方法之前
System.out.println(method.getName() + " 方法之前执行,参数:" + Arrays.toString(args));
// 被增强的方法执行
Object returnValue = method.invoke(obj, args);
// 方法之后
System.out.println(method.getName() + " 方法之后执行");
return returnValue;
}
}
输出:
1 | add 方法之前执行,参数:[2, 3] |
AOP 术语
- 连接点:类中可以被增强的方法称为连接点
- 切入点:实际被真正增强的方法称为切入点
- 通知(增强):实际增强的逻辑部分称为通知(增强)
- 前置通知
- 后置通知
- 环绕通知
- 异常通知
- 最终通知
- 切面:把通知应用到切入点的过程
AOP 操作
Spring 框架一般基于 AspectJ 实现 AOP 操作
AspectJ 不是 Spring 组成部分,而是独立的 AOP 框架,但是一般把 AspectJ 和 Spring 框架一起使用,进行 AOP 操作
Spring 基于 AspectJ 实现 AOP 操作有两种方式:xml 配置文件方式、注解方式。
引入依赖
1 | <dependencies> |
切入点表达式
切入点表达式作用:指明对哪个类的哪个方法进行增强
语法结构:
1 | execution([权限修饰符][返回类型][全类名][方法名]([参数列表])) |
例 1:对 com.yin.spring5.UserDao#add
方法进行增强
1 | execution(* com.yin.spring5.UserDao.add(..)) // 权限修饰符省略 |
例 2:对 com.yin.spring5.UserDao
类中所有方法增强
1 | execution(* com.yin.spring5.UserDao.*(..)) // 权限修饰符省略 |
例 2:对 com.yin.spring5
包下所有类中所有方法增强
1 | execution(* com.yin.spring5.*.*(..)) // 权限修饰符省略 |
基于注解
创建类,在类中定义方法
1
2
3
4
5public class User {
public void add() {
System.out.println("User.add 方法执行");
}
}创建增强类,编写增强逻辑
在增强类中,创建方法,让不同的方法代表不同的通知
1
2
3
4
5
6
7public class UserProxy {
// 前置通知
public void before() {
System.out.println("UserProxy.before 方法执行");
}
......
}进行通知的配置
配置文件开启注解扫描
1
<context:component-scan base-package="com.yin.spring5.anno"/>
使用注解创建 User 和 UserProxy 对象
1
2
public class User {...}1
2
public class UserProxy {...}在增强类上面添加注解
@Aspect
1
2
3
public class UserProxy {...}配置文件开启生成代理对象
1
2<!--开启 Aspect 生成代理对象,要引入aop名称空间-->
<aop:aspectj-autoproxy/>
配置不同类型的通知
在增强类的里面,在作为通知方法上面添加通知类型注解,使用切入点表达式配置
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 UserProxy {
// 前置通知
public void before() {
System.out.println("UserProxy.before 方法执行");
}
// 最终通知
public void after() {
System.out.println("UserProxy.after 方法执行");
}
// 后置通知(返回通知)
public void afterReturning() {
System.out.println("UserProxy.afterReturning 方法执行");
}
// 异常通知
public void afterThrowing() {
System.out.println("UserProxy.afterThrowing 方法执行");
}
// 环绕通知
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("环绕之前。。。");
// 被增强的方法执行
proceedingJoinPoint.proceed();
System.out.println("环绕之后。。。");
}
}
测试:
1 |
|
输出:
1 | 环绕之前。。。 |
抽取相同的切入点
1 |
|
有多个增强类对同一个方法进行增强,设置增强类优先级
在增强类上添加注解 @Order(value = )
,数字越小,优先级越高
完全使用注解
创建配置类,不需要创建 xml 配置文件
1 |
|
基于 xml 配置文件
JdbcTemplate
什么是 JdbcTemplate ?
Spring 对 JDBC 进行封装,使用 JdbcTemplate 方便实现对数据库操作
环境配置
引入依赖
1 | <dependencies> |
配置数据库连接池
1 | <!--引入外部数据库配置文件--> |
配置 JdbcTemplate
1 | <!--JdbcTemplate--> |
编写类文件
编写 Book 实体类,创建对应数据表
1 | public class Book { |
环境一览
1 |
|
1 | public interface BookDao { |
1 |
|
1 |
|
操作数据库
增
1 |
|
改
1 |
|
删
1 |
|
查
某个值
1 |
|
对象
1 |
|
集合
1 |
|
批量操作
批量添加
1 |
|
1 |
|
批量修改
1 |
|
1 |
|
批量删除
1 |
|
1 |
|
事务
概念
什么是事务?
事务是数据库操作的最基本单元,逻辑上的一组操作,要么都成功,如果有一个操作失败,所有操作都失败
典型场景:银行转账
事务四个特性 ACID
原子性 Atomicity
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都成功,要么都不成功
一致性 Consistency
事务必须使数据库从一个一致性状态变换到另一个一致性状态
隔离性 Isolation
事务的隔离性是指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰
持久性 Durability
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响
环境搭建
引入依赖
1 | <dependencies> |
创建数据库表
1 | use `jdbc_template`; |
编写实体类、DAO、Service 等
1 | public class Account { |
1 | public interface AccountDao { |
1 |
|
1 | public class AccountService { |
上面的代码,如果正常执行是没有问题的,但是如果代码执行过程中出现异常,会有问题
比如:
1 | public void transferAccount(int deductId, int addId, int money) { |
为避免诸如上述问题,就需要事务。
事务操作流程:
- 开启事务
- 业务操作
- 若无异常,提交事务
- 出现异常,回滚事务
Spring 事务管理
事务一般添加到 Service 层
Spring 进行事务管理有两种方式:编程式事务管理 和 声明式事务管理。一般都使用声明式事务管理。
声明式事务管理有两种方式:基于注解,基于 xml 配置文件。
Spring 进行声明式事务管理,底层使用 AOP 原理。
Spring 事务管理 API
提供
PlatformTransactionManager
接口,代表事务管理器,这个接口针对不同框架提供不同的实现类
基于注解的声明式事务管理
配置事务管理器
1 | <!--配置事务管理器--> |
开启事务注解
1 | <!--开启事务注解--> |
上面讲到,事务一般添加到 Service 层
在 Service 类上(或者类中的方法上)添加事务注解 @Transactional
事务注解 @Transactional
既可以添加到类上,也可以添加到方法上
- 如果添加到类上,则该类中的所有方法都会添加事务
- 如果添加到方法上,则只会给该方法添加事务
1 |
|
事务注解 @Transactional
部分参数:
propagation
:事务传播类型(默认REQUIRED
)多事务方法直接进行调用,这个过程中事务是如何进行管理的
isolation
:事务隔离级别事务特性之一。不考虑隔离性会产生很多问题,三个读问题:脏读、不可重复读、幻读(虚读)。
- 脏读:一个未提交事务读取到另一个未提交事务的数据
- 不可重复读:一个未提交事务读取到另一个提交事务修改数据
- 幻读:一个未提交事务读取到另一个提交事务添加数据
timeout
:事务超时时间(秒,默认 -1 即永不超时)事务需要在指定时间内进行提交,如果不提交就会回滚
readOnly
:事务是否只读(默认false
)rollbackFor
:指明哪些异常类型进行事务回滚noRollbackFor
:指明哪些异常类型不进行事务回滚
事务方法:对数据库表数据进行变化的操作
事务的传播行为可以由传播类型指定,Spring 定义了 7 种传播类型
传播类型 | 描述 |
---|---|
REQUIRED | 如果有事务在运行,当前的方法就在这个事务内运行;否则,就启动一个新的事务,并在自己的事务内运行 |
REQUIRES_NEW | 当前的方法必须启动新事务,并在它自己的事务内运行;如果有事务正在运行,应该将它挂起 |
SUPPORTS | 如果有事务在运行,当前的方法就在这个事务内运行,否则它可以不运行在事务中 |
NOT_SUPPORTED | 当前的方法不应该运行在事务中,如果有运行的事务,将它挂起 |
MANDATORY | 当前的方法必须运行在事务内部,如果没有正在运行的事务,就抛出异常 |
NEVER | 当前的方法不应该运行在事务中,如果有运行的事务,就抛出异常 |
NESTED | 如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行;否则,就启动一个新的事务,并在它自己的事务内运行 |
事务隔离级别
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ_UNCOMMITTED(读未提交) | 有 | 有 | 有 |
READ_COMMITTED(读已提交) | 无 | 有 | 有 |
REPEATABLE_READ(可重复读,MySQL默认) | 无 | 无 | 有 |
SERIALIZABLE(串行化) | 无 | 无 | 无 |
基于 XML 的声明式事务管理
第一步,配置事务管理器;第二步,配置通知;第三步,配置切入点和切面
1 | <!--基于 XML 的事务--> |
完全注解
创建配置类,代替 xml 配置文件
1 | // 配置类 |
Spring 5 新功能
Spring 5 基于 Java 8,运行时兼容 Java 9,移除了不建议使用的类和方法。
Spring 5 自带了通用的日志封装
Spring 5 移除了
Log4jConfigListener
,官方建议使用 Log4j2Spring 5 核心注解支持
@Nullable
注解@Nullable
注解可以用在方法上、属性上、参数上,表示方法返回值、属性、参数可以为空支持函数式风格 GenericApplicationContext / AnnotationConfigApplicationContext
1
2
3
4
5
6
7
8
9
10
public void test4() {
GenericApplicationContext context = new GenericApplicationContext();
context.refresh();
// 注册 bean
context.registerBean("acct", Account.class, Account::new);
// 获取注册的对象
Account acct = context.getBean("acct", Account.class);
System.out.println(acct);
}Spring 5 支持整合 JUnit 5
Spring 5 整合 Log4j2
- 引入依赖
- 创建 log4j2.xml 配置文件(文件名固定)
Spring WebFlux
Spring WebFlux 介绍
Spring WebFlux 是 Spring 5 新增的模块,用于 Web 开发,功能与 Spring MVC 类似,使用当前比较流行的响应式编程
使用传统 Web 框架,比如 Spring MVC,这些基于 Servlet 容器。WebFlux 是一种异步非阻塞式的框架,异步非阻塞的框架在 Servlet 3.1 以后才支持,核心是基于 Reactor 的相关 API 实现的。
什么是异步非阻塞?
同步与异步
异步和同步针对调用者。调用者发送请求,如果等着对方回应之后才去左其他事就是同步;如果发送请求后不等着对方回应就去做其他事就是异步。
阻塞与非阻塞
阻塞和非阻塞针对被调用者。被调用者收到请求之后,做完请求任务之后才给出反馈就是阻塞,收到请求之后马上给出反馈然后再去做事情就是非阻塞。
WebFlux 特点
- 非阻塞式:在有限资源下,提高系统吞吐量和伸缩性,以 Reactor 为基础实现响应式编程
- 函数式编程:使用 Java 8 函数式编程方式实现路由请求
对比 Spring MVC
- 两种方式都可以使用注解方式,都运行在 Tomcat 等容器中
- SpringMVC 采用命令式编程,WebFlux 采用异步响应式编程
响应式编程
什么是响应式编程?
响应式编程是一种面向数据流和变化传播的编程范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。
例如,在命令式编程环境中,a=b+c
表示将表达式的结果赋给 a,而之后改变 b 或 c 的值不会影响 a。但在响应式编程中,a 的值会随着 b 或 c 的更新而更新。
电子表格程序就是响应式编程的一个例子。单元格可以包含字面值或类似 “=B1+C1” 的公式,而包含公式的单元格的值会依据其他单元格的值的变化而变化。
Java 8 及之前版本
提供的观察者模式两个类:Observer 接口,Observable 类(两者从 Java 9 开始被遗弃)
响应式编程(Reactor 实现)
- 响应式编程操作中,Reactor 是满足 Reactive 规范框架
- Reactor 有两个核心类,Mono 和 Flux,这两个类实现 Publisher 接口,提供丰富操作符。Flux 对象实现发布者,返回 N 个元素;Mono 实现发布者,返回 0 或 1 个元素
- Flux 和 Mono 都是数据流的发布者,使用 Flux 和 Mono 都可以发出三种数据信号:元素值、错误信号、完成信号。错误信号和完成信号都表示终止信号,终止信号用于告诉订阅者数据流结束了,错误信号终止数据流同时把错误信息传递给订阅者
- 三种信号特点:
- 错误信号和完成信号都是终止信号,二者不能共存
- 如果没有发送任何元素值,而是直接发送终止信号,表示是空数据流
- 如果没有终止信号,表示是无限数据流
代码演示 Flux 和 Mono
引入依赖
1 | <!-- https://mvnrepository.com/artifact/io.projectreactor/reactor-core --> |
调用 just 方法或者其他方法只是声明数据流,数据流并没有发出,只有进行订阅之后才会触发数据流,不订阅什么都不会发生。
1 | // just 方法直接声明 |
操作符
对数据流进行一道道操作,称为操作符,比如工厂流水线
map:将元素映射为新元素
flatMap:将元素映射为流。把每个元素转换流,把转换之后多个流合并为大的流
WebFlux执行流程和核心 API
Spring WebFlux 基于 Reactor,默认使用容器是 Netty,Netty 是高性能 NIO(Non-blocking IO,非阻塞IO) 框架。(与 NIO 相对的是 BIO,即 Blocking IO)
BIO 的通信方式:
NIO 的通信方式:
Spring WebFlux 执行过程与 Spring MVC 相似
Spring WebFlux 核心控制器 DispatchHandler
,实现接口 WebHandler
。
DispatcherHandler.handle
方法:
在 Spring WebFlux 中,DispatcherHandler 负责请求的处理
以下三个都是接口:
HandlerMapping
:请求查询到处理的方法。Interface to be implemented by objects that define a mapping between requests and handler objects.
由定义请求和处理程序对象之间的映射关系的对象实现的接口。
HandlerAdapter
:真正负责请求处理。Contract that decouples the {@link DispatcherHandler} from the details of invoking a handler and makes it possible to support any handler type.
使 DispatcherHandler 与调用处理程序的详细信息分离的契约,并且可以支持任何处理程序类型。
HandlerResultHandler
:响应结果处理。Process the {@link HandlerResult}, usually returned by an {@link HandlerAdapter}.
处理 HandlerResult,通常由 HandlerAdapter 返回。
Spring WebFlux 实现函数式编程,两个接口:
RouterFunction
:Represents a function that routes to a {@linkplain HandlerFunction handler function}.HandlerFunction
:Represents a function that handles a {@linkplain ServerRequest request}.
基于注解编程模型
使用注解方式,与之前 Spring MVC 使用相似,只需要引入相关依赖,Spring Boot 自动配置相关运行容器,默认情况下使用 Netty 服务器。
引入依赖
1 | <dependency> |
编写代码
1 | public interface UserService { |
1 |
|
1 |
|
- Spring MVC 方式,同步阻塞的方式,基于 Spring MVC + Servlet + Tomcat;
- Spring WebFlux 方式,异步非阻塞的方式,基于 Spring WebFlux + Reactor + Netty。
基于函数式编程模型
- 使用函数式编程模型,需要自己初始化服务器
- 两个核心接口:RouterFunction(实现路由功能,请求转发给对应的 handler)和 HandlerFunction(处理请求生成响应的函数)。核心任务定义两个函数式接口的实现并且启动需要的服务器。
- Spring WebFlux 的请求和响应不再是 ServletRequest 和 ServletResponse,而是 ServerRequest 和 ServerResponse。
把基于注解的复制一份,删去 controller 部分
创建 Handler
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
32public class UserHandler {
private final UserService userService;
public UserHandler(UserService userService) {
this.userService = userService;
}
public Mono<ServerResponse> getUserById(ServerRequest request) {
// 获取 id 值
int id = Integer.parseInt(request.pathVariable("id"));
// 空值处理
Mono<ServerResponse> notFound = ServerResponse.notFound().build();
// 调用 service 得到数据
Mono<User> userMono = userService.getUserById(id);
// 把 userMono 进行转换返回,使用 Reactor 操作符 flatMap
return userMono.flatMap(user -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(user)))
.switchIfEmpty(notFound);
}
public Mono<ServerResponse> getAllUsers() {
Flux<User> allUsers = userService.getAllUsers();
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON)
.body(allUsers, User.class);
}
public Mono<ServerResponse> saveUser(ServerRequest request) {
Mono<User> userMono = request.bodyToMono(User.class);
return ServerResponse.ok().build(userService.saveUser(userMono));
}
}初始化服务器,编写 Router
- 创建路由的方法
- 创建服务器完成适配
- 最终调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31public class Server {
public static void main(String[] args) throws IOException {
Server server = new Server();
server.createReactorServer();
System.out.println("enter to exit");
System.in.read();
}
// 1. 创建 Router 路由
public RouterFunction<ServerResponse> routingFunction() {
// 创建 handler 对象
UserService userService = new UserServiceImpl();
UserHandler userHandler = new UserHandler(userService);
// 设置路由
return RouterFunctions.route(GET("/user/{id}").and(accept(MediaType.APPLICATION_JSON)), userHandler::getUserById)
.andRoute(GET("/users").and(accept(MediaType.APPLICATION_JSON)),
userHandler::getAllUsers);
}
// 2. 创建服务器完成适配
public void createReactorServer() {
// 路由和 handler 适配
RouterFunction<ServerResponse> route = routingFunction();
HttpHandler httpHandler = toHttpHandler(route);
ReactorHttpHandlerAdapter handlerAdapter =
new ReactorHttpHandlerAdapter(httpHandler);
// 创建服务器
HttpServer httpServer = HttpServer.create();
httpServer.handle(handlerAdapter).bindNow();
}
}使用 WebClient 调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class Client {
public static void main(String[] args) {
WebClient webClient = WebClient.create("http://localhost:8235");
User user = webClient.get().uri("/user/{id}", 1)
.accept(MediaType.APPLICATION_JSON).retrieve().bodyToMono(User.class)
.block();
System.out.println(user);
Flux<User> users = webClient.get().uri("/users")
.accept(MediaType.APPLICATION_JSON).retrieve().bodyToFlux(User.class);
users.map(User::toString).buffer().doOnNext(System.out::println).blockFirst();
}
}
课程总结
Spring 框架概述
轻量级开源 JavaEE 框架,核心 IoC 和 AOP
IoC 容器
- IoC 底层原理(工厂、反射等)
- IoC 接口(BeanFactory)
- IoC 操作 Bean 管理(基于 xml)
- IoC 操作 Bean 管理(基于注解)
AOP
- AOP 底层原理:动态代理,有接口(JDK 动态代理),没有接口(CGLIB 动态代理)
- 术语:切入点、增强(通知)、切面
- 基于 AspectJ 实现 AOP 操作
JdbcTemplate
- 使用 JdbcTemplate 实现 CRUD 操作
- 使用 JdbcTemplate 实现批量操作
事务管理
- 事务概念
- 重要概念(传播行为、隔离级别)
- 基于注解实现声明式事务管理
- 完全注解方式
Spring 5 新特性
- 整合日志框架
@Nullable
注解- 函数式注册对象
- 整合 JUnit 5 单元测试框架
- Spring WebFlux