课程内容介绍
- 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 方式创建对象
<bean id="alice" class="com.yin.spring5.bean.User"></bean>
- 在 Spring 配置文件中,使用 bean 标签,标签中添加对应属性,就可以实现对象创建
- bean 标签有很多属性,常用属性:
-
id
:唯一标识 -
class
:全类名
- 创建对象的时候,默认执行无参构造器
基于 xml 方式注入属性
DI:Dependency Injection,依赖注入,是 IoC 的一种实现方式
- 第一种注入方式:set 方法注入
<!--set 方法注入-->
<bean id="alice" class="com.yin.spring5.bean.User">
<property name="name" value="Alice"/>
<property name="age" value="20"/>
</bean>
- 第二种注入方式:有参构造器注入
<!--有参构造器注入-->
<bean id="bob" class="com.yin.spring5.bean.User">
<constructor-arg name="name" value="Bob"/>
<constructor-arg name="age" value="20"/>
</bean>
- p 名称空间注入(了解即可)
添加约束:
xmlns:p="http://www.springframework.org/schema/p"
<!--p命名空间-->
<bean id="cindy" class="com.yin.spring5.bean.User" p:name="Cindy" p:age="18">
</bean>
- c 名称空间注入(了解即可)
添加约束:
xmlns:c="http://www.springframework.org/schema/c"
<!--c命名空间-->
<bean id="david" class="com.yin.spring5.bean.User" c:name="David" c:age="18">
</bean>
xml 注入其他类型
- 字面量
- 注入
null
<!--注入null-->
<bean id="eva" class="com.yin.spring5.bean.User">
<property name="name" value="Eva"/>
<property name="age">
<null/>
</property>
</bean>
- 属性值包含特殊字符
- 将特殊字符进行转义
<!--属性值包含特殊字符-->
<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
<!--属性值包含特殊字符-->
<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
public class UserDao {
public void delete() {
System.out.println("UserDao#delete()...");
}
}
public class UserService {
UserDao dao;
public void setDao(UserDao dao) {
this.dao = dao;
}
public void deleteUser() {
System.out.println("UserService#deleteUser()...");
dao.delete();
}
}
<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
public class Department {
private String departName;
// getter setter toString 等方法在此省略
}
public class Employee {
private String empName;
private String email;
private Department dept;
// getter setter toString 等方法在此省略
}
<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='开发部'}}
- 级联赋值
写法一:
<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='运维部'}}
写法二:
<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 注入集合属性
- 注入数组类型属性
<!--数组类型注入-->
<property name="array">
<array>
<value>array1</value>
<value>array2</value>
<value>array3</value>
</array>
</property>
- 注入 List 集合类型属性
<!--List 类型注入-->
<property name="list">
<list>
<value>list1</value>
<value>list2</value>
<value>list3</value>
</list>
</property>
- 注入 Map 集合类型属性
<!--Map 类型注入-->
<property name="map">
<map>
<entry key="key1" value="value1"/>
<entry key="key2" value="value2"/>
<entry key="key3" value="value3"/>
</map>
</property>
- 注入 Set 集合类型属性
<!--Set 类型注入-->
<property name="set">
<set>
<value>set1</value>
<value>set2</value>
<value>set3</value>
</set>
</property>
输出:
Student{stuName='Pony', array=[array1, array2, array3], list=[list1, list2, list3], map={key1=value1, key2=value2, key3=value3}, set=[set1, set2, set3]}
- 在集合里面设置对象类型
<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>
输出:
[Course{courseName='数据结构', credit=5}, Course{courseName='操作系统', credit=4}]
- 把集合注入部分提取出来
a. 在配置文件中引入util
名称空间
<?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: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 集合注入提取
public class Book {
private List<String> list;
// getter setter toString 等方法在此省略
}
<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>
输出:
[Java编程思想, 算法导论, Effective Java]
FactoryBean
普通 bean:配置文件中定义的 bean 类型就是返回类型
工厂 bean:配置文件中定义的 bean 类型可以和返回类型不一样
- 创建类,实现
FactoryBean
接口 - 实现接口的方法
/**
* MyFactoryBean 工厂Bean
*/
public class MyFactoryBean implements FactoryBean<Course> {
@Override
public Course getObject() throws Exception {
return new Course();
}
@Override
public Class<?> getObjectType() {
return null;
}
@Override
public boolean isSingleton() {
return false;
}
}
<bean id="myFactoryBean" class="com.yin.spring5.bean.MyFactoryBean">
</bean>
@Test
public void test3() {
ApplicationContext ioc =
new ClassPathXmlApplicationContext("spring3.xml");
Course course = ioc.getBean("myFactoryBean", Course.class);
course.setCourseName("数据结构");
course.setCredit(5);
System.out.println(course);
}
测试:
Course{courseName='数据结构', credit=5}
Bean 作用域
在 Spring 中,设置创建 bean 实例是单实例还是多实例(默认单实例)
@Test
public void test4() {
ApplicationContext ioc =
new ClassPathXmlApplicationContext("spring2.xml");
Book book1 = ioc.getBean("book", Book.class);
Book book2 = ioc.getBean("book", Book.class);
// 这里打印对象地址,需要把其 toString() 方法注释掉
System.out.println(book1);
System.out.println(book2);
System.out.println(book1==book2);
}
输出:
com.yin.spring5.bean.Book@433defed
com.yin.spring5.bean.Book@433defed
true
如何设置单实例/多实例?bean 标签中 scope
属性设置作用域
scope
属性值:
-
singleton
:单实例 -
prototype
:多实例
将 scope 属性值设置为 `` 后,再进行上面测试:
<bean id="book" class="com.yin.spring5.bean.Book" scope="prototype">
输出:
com.yin.spring5.bean.Book@548a24a
com.yin.spring5.bean.Book@433defed
false
singleton
和 prototype
的区别:
singleton
是单实例,prototype
是多实例singleton
在 Spring 加载配置文件时就会创建单实例对象prototype
在获取对象时才会进行创建
Bean 生命周期
生命周期:从对象创建到销毁的过程
bean 生命周期:
- 通过构造器创建 bean 实例(无参构造)
- 为 bean 注入属性和对其他 bean 的引用(set 方法)
- 调用 bean 的初始化方法(需要进行配置初始化方法)
- 获取并使用 bean
- 当容器关闭时,调用 bean 的销毁方法(需要进行配置销毁方法)
代码验证:
public class User {
private String name;
private Integer age;
private Character gender;
public User() {
System.out.println("User 无参构造器执行。。。");
}
public String getName() {
return name;
}
public void setName(String name) {
System.out.println("User.setName 执行。。。");
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
System.out.println("User.setAge 执行。。。");
this.age = age;
}
public Character getGender() {
return gender;
}
public void setGender(Character gender) {
System.out.println("User.setGender 执行。。。");
this.gender = gender;
}
public void init() {
System.out.println("自定义 User.init 执行。。。");
}
public void destroy() {
System.out.println("自定义 User.destroy 执行。。。");
}
}
<bean id="user" class="com.yin.spring5.bean.User"
init-method="init" destroy-method="destroy">
<property name="name" value="Alice"/>
<property name="gender" value="F"/>
<property name="age" value="20"/>
</bean>
@Test
public void test5() {
ClassPathXmlApplicationContext ioc =
new ClassPathXmlApplicationContext("spring4.xml");
User user = ioc.getBean("user", User.class);
System.out.println(user);
// 关闭容器
ioc.close();
}
输出:
User 无参构造器执行。。。
User.setName 执行。。。
User.setGender 执行。。。
User.setAge 执行。。。
自定义 User.init 执行。。。
com.yin.spring5.bean.User@16eb3ea3
自定义 User.destroy 执行。。。
其实完整来讲,bean 的声明周期有 7 步。在初始化方法执行前后,分别有 postProcessBeforeInitialization
和 postProcessAfterInitialization
方法执行。
在上述代码的基础上,再添加以下内容:
public class MyBeanPost implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean,
String beanName) throws BeansException {
System.out.println(" postProcessBeforeInitialization 执行。。。");
return null;
}
@Override
public Object postProcessAfterInitialization(Object bean,
String beanName) throws BeansException {
System.out.println(" postProcessAfterInitialization 执行。。。");
return null;
}
}
<bean id="myBeanPost" class="com.yin.spring5.bean.MyBeanPost"></bean>
再次进行测试,输出如下:
User 无参构造器执行。。。
User.setName 执行。。。
User.setGender 执行。。。
User.setAge 执行。。。
postProcessBeforeInitialization 执行。。。
自定义 User.init 执行。。。
postProcessAfterInitialization 执行。。。
com.yin.spring5.bean.User@33bc72d1
自定义 User.destroy 执行。。。
综上,bean 生命周期完整表述如下:
- 通过构造器创建 bean 实例(无参构造)
- 为 bean 注入属性和对其他 bean 的引用(set 方法)
- 把 bean 实例传递给初始化前的后置处理
postProcessBeforeInitialization
- 调用 bean 的初始化方法(需要进行配置初始化方法)
- 把 bean 实例传递给初始化后的后置处理
postProcessAfterInitialization
- 获取并使用 bean
- 当容器关闭时,调用 bean 的销毁方法(需要进行配置销毁方法)
xml 自动装配
什么是自动装配?
根据指定装配规则(属性名称或属性类型),Spring 自动将匹配的属性值进行注入
bean 标签中的 autowire
属性可以配置自动装配,autowire
常用属性值:
-
byName
:根据名称装配(注入 bean 的 id 要与被注入 bean 的 set 方法一致) -
byType
:根据类型装配(有多个同类型的 bean 时,此属性值无法使用)
代码演示:
public class Department {
// 此处略去 get set toString 等方法
private String deptName;
}
public class Employee {
// 此处略去 get set toString 等方法
private String empName;
private Department dept;
}
<bean id="dept" class="com.yin.spring5.autowire.Department">
<property name="deptName" value="开发部"/>
</bean>
<bean id="dept2" class="com.yin.spring5.autowire.Department">
<property name="deptName" value="测试部"/>
</bean>
<bean id="emp" class="com.yin.spring5.autowire.Employee" autowire="byName">
<property name="empName" value="Pony"/>
</bean>
输出:
Employee{empName='Pony', dept=Department{deptName='开发部'}}
若将 Employee
类中默认生成的 setDept
方法名修改为 setDept2
,则输出变为:
Employee{empName='Pony', dept=Department{deptName='测试部'}}
由此证实:byName
的自动注入是根据 set 方法判断的,而不是类中的属性名。
外部属性文件
以数据库连接池为例:
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/jdbc_template?serverTimezone=Asia/Shanghai"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
将数据库配置抽取到 properties 文件
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/jdbc_template?serverTimezone=Asia/Shanghai
jdbc.username=root
jdbc.password=123456
则 Spring 配置文件改写如下,注意需要引入 context 名称空间:
<?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">
<!--引入外部 properties 文件-->
<context:property-placeholder location="classpath:db-config.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<!--取出 properties 文件中的值-->
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
</beans>
基于注解方式
创建 bean
Spring 创建 bean 的四个注解:@Component
,@Controller
,@Service
,@Repository
。
<!--开启组件扫描,将会扫描 base-package 及其子包下所有组件-->
<context:component-scan base-package="com.yin.spring5"/>
排除某些组件,以排除 @Controller
组件扫描为例:
<!--开启组件扫描-->
<context:component-scan base-package="com.yin.spring5">
<!--排除 Controller 组件扫描-->
<context:exclude-filter type="annotation"
expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
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
类里面的方法创建代理对象
代码演示:
- 创建接口,定义方法
public interface UserDao {
int add(int a, int b);
String update(String id);
}
- 创建接口实现类,实现方法
public class UserDaoImpl implements UserDao {
@Override
public int add(int a, int b) {
System.out.println("add 方法执行");
return a + b;
}
@Override
public String update(String id) {
System.out.println("update 方法执行");
return id;
}
}
- 使用
java.lang.reflect.Proxy
类创建接口代理对象
public 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;
}
// 增强的逻辑
@Override
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;
}
}
输出:
add 方法之前执行,参数:[2, 3]
add 方法执行
add 方法之后执行
5
update 方法之前执行,参数:[abc]
update 方法执行
update 方法之后执行
abc
AOP 术语
- 连接点:类中可以被增强的方法称为连接点
- 切入点:实际被真正增强的方法称为切入点
- 通知(增强):实际增强的逻辑部分称为通知(增强)
- 前置通知
- 后置通知
- 环绕通知
- 异常通知
- 最终通知
- 切面:把通知应用到切入点的过程
AOP 操作
Spring 框架一般基于 AspectJ 实现 AOP 操作
AspectJ 不是 Spring 组成部分,而是独立的 AOP 框架,但是一般把 AspectJ 和 Spring 框架一起使用,进行 AOP 操作
Spring 基于 AspectJ 实现 AOP 操作有两种方式:xml 配置文件方式、注解方式。
引入依赖
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
切入点表达式
切入点表达式作用:指明对哪个类的哪个方法进行增强
语法结构:
execution([权限修饰符][返回类型][全类名][方法名]([参数列表]))
例 1:对 com.yin.spring5.UserDao#add
方法进行增强
execution(* com.yin.spring5.UserDao.add(..)) // 权限修饰符省略
例 2:对 com.yin.spring5.UserDao
类中所有方法增强
execution(* com.yin.spring5.UserDao.*(..)) // 权限修饰符省略
例 2:对 com.yin.spring5
包下所有类中所有方法增强
execution(* com.yin.spring5.*.*(..)) // 权限修饰符省略
基于注解
- 创建类,在类中定义方法
public class User {
public void add() {
System.out.println("User.add 方法执行");
}
}
- 创建增强类,编写增强逻辑
在增强类中,创建方法,让不同的方法代表不同的通知
public class UserProxy {
// 前置通知
public void before() {
System.out.println("UserProxy.before 方法执行");
}
......
}
- 进行通知的配置
- 配置文件开启注解扫描
<context:component-scan base-package="com.yin.spring5.anno"/>
- 使用注解创建 User 和 UserProxy 对象
@Component
public class User {...}
@Component
public class UserProxy {...}
- 在增强类上面添加注解
@Aspect
@Component
@Aspect
public class UserProxy {...}
- 配置文件开启生成代理对象
<!--开启 Aspect 生成代理对象,要引入aop名称空间-->
<aop:aspectj-autoproxy/>
- 配置不同类型的通知
在增强类的里面,在作为通知方法上面添加通知类型注解,使用切入点表达式配置
@Component
@Aspect
public class UserProxy {
// 前置通知
@Before("execution(* com.yin.spring5.anno.User.add(..))")
public void before() {
System.out.println("UserProxy.before 方法执行");
}
// 最终通知
@After("execution(* com.yin.spring5.anno.User.add(..))")
public void after() {
System.out.println("UserProxy.after 方法执行");
}
// 后置通知(返回通知)
@AfterReturning("execution(* com.yin.spring5.anno.User.add(..))")
public void afterReturning() {
System.out.println("UserProxy.afterReturning 方法执行");
}
// 异常通知
@AfterThrowing("execution(* com.yin.spring5.anno.User.add(..))")
public void afterThrowing() {
System.out.println("UserProxy.afterThrowing 方法执行");
}
// 环绕通知
@Around("execution(* com.yin.spring5.anno.User.add(..))")
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("环绕之前。。。");
// 被增强的方法执行
proceedingJoinPoint.proceed();
System.out.println("环绕之后。。。");
}
}
测试:
@Test
public void test1() {
ApplicationContext context = new ClassPathXmlApplicationContext("spring1.xml");
User user = context.getBean("user", User.class);
user.add();
}
输出:
环绕之前。。。
UserProxy.before 方法执行
User.add 方法执行
UserProxy.afterReturning 方法执行
UserProxy.after 方法执行
环绕之后。。。
抽取相同的切入点
@Component
@Aspect
public class UserProxy {
// 抽取相同的切入点
@Pointcut("execution(* com.yin.spring5.anno.User.add(..))")
public void pointDemo() {
}
// 前置通知
@Before("pointDemo()")
public void before() {
System.out.println("UserProxy.before 方法执行");
}
...
}
有多个增强类对同一个方法进行增强,设置增强类优先级
在增强类上添加注解 @Order(value = )
,数字越小,优先级越高
完全使用注解
创建配置类,不需要创建 xml 配置文件
@Configuration
@ComponentScan(basePackages = "com.yin.spring5.anno")
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AopConfig {
}
基于 xml 配置文件
JdbcTemplate
什么是 JdbcTemplate ?
Spring 对 JDBC 进行封装,使用 JdbcTemplate 方便实现对数据库操作
环境配置
引入依赖
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.22</version>
</dependency>
</dependencies>
配置数据库连接池
<!--引入外部数据库配置文件-->
<context:property-placeholder location="classpath:db-config.properties"/>
<!--Druid 数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClass}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
配置 JdbcTemplate
<!--JdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--注入数据源-->
<property name="dataSource" ref="dataSource"/>
</bean>
编写类文件
编写 Book 实体类,创建对应数据表
public class Book {
private Integer id;
private String title;
private String author;
private BigDecimal price;
// 此处省略 getter setter toString 等方法
}
环境一览
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<!--注意:此处文件头等信息略去-->
<!--组件扫描-->
<context:component-scan base-package="com.yin.spring5"/>
<!--引入外部数据库配置文件-->
<context:property-placeholder location="classpath:db-config.properties"/>
<!--Druid 数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClass}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!--JdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--注入数据源-->
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
public interface BookDao {
}
@Repository
public class BookDaoImpl implements BookDao {
JdbcTemplate jdbcTemplate;
@Autowired
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}
@Service
public class BookService {
BookDao bookDao;
@Autowired
public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}
}
操作数据库
增
@Override
public int insert(Book book) {
String sql = "INSERT INTO book(title,author,price) VALUES(?,?,?)";
return jdbcTemplate.update(sql, book.getTitle(), book.getAuthor(), book.getPrice());
}
改
@Override
public int update(Book book) {
String sql = "UPDATE book SET title=?, author=?, price=? WHERE id=?";
return jdbcTemplate.update(sql, book.getTitle(), book.getAuthor(),
book.getPrice(), book.getId());
}
删
@Override
public int delete(Integer id) {
String sql = "DELETE FROM book WHERE id=?";
return jdbcTemplate.update(sql, id);
}
查
某个值
@Override
public int recordCount() {
String sql = "SELECT COUNT(*) FROM book";
return jdbcTemplate.queryForObject(sql, Integer.TYPE);
}
对象
@Override
public Book getOneById(Integer id) {
String sql = "SELECT id, title, author, price FROM book WHERE id=?";
return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Book.class), id);
}
集合
@Override
public List<Book> getAll() {
String sql = "SELECT * FROM book";
return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Book.class));
}
批量操作
批量添加
@Override
public int[] batchInsert(List<Object[]> bookList) {
String sql = "INSERT INTO book(title,author,price) VALUES(?,?,?)";
return jdbcTemplate.batchUpdate(sql, bookList);
}
@Test
public void testBatchAdd() {
ApplicationContext context = new ClassPathXmlApplicationContext("spring1.xml");
BookService bookService = context.getBean("bookService", BookService.class);
List<Object[]> bookList = new ArrayList<>();
// 按占位符顺序写参数
Object[] book1 = {"城南旧事", "林海音", 29.00};
Object[] book2 = {"追风筝的人", "忘了", 29.00};
Object[] book3 = {"小王子", "安东尼", 19.00};
bookList.add(book1);
bookList.add(book2);
bookList.add(book3);
bookService.batchAddBook(bookList);
}
批量修改
@Override
public int[] batchUpdate(List<Object[]> bookList) {
String sql = "UPDATE book SET title=?, author=?, price=? WHERE id=?";
return jdbcTemplate.batchUpdate(sql, bookList);
}
@Test
public void testBatchUpdate() {
ApplicationContext context = new ClassPathXmlApplicationContext("spring1.xml");
BookService bookService = context.getBean("bookService", BookService.class);
List<Object[]> bookList = new ArrayList<>();
// 按占位符顺序写参数
Object[] book1 = {"城南旧事", "林海音(台湾)", 26.00, 24};
Object[] book2 = {"追风筝的人", "还是没想起来", 29.00, 25};
Object[] book3 = {"小王子", "安东尼奥", 20.00, 26};
bookList.add(book1);
bookList.add(book2);
bookList.add(book3);
bookService.batchUpdateBook(bookList);
}
批量删除
@Override
public int[] batchDelete(List<Object[]> idList) {
String sql = "DELETE FROM book WHERE id=?";
return jdbcTemplate.batchUpdate(sql, idList);
}
@Test
public void testBatchDelete() {
ApplicationContext context = new ClassPathXmlApplicationContext("spring1.xml");
BookService bookService = context.getBean("bookService", BookService.class);
List<Object[]> idList = new ArrayList<>();
// 按占位符顺序写参数
Object[] id1 = {19};
Object[] id2 = {20};
Object[] id3 = {21};
Object[] id4 = {25};
idList.add(id1);
idList.add(id2);
idList.add(id3);
idList.add(id4);
bookService.batchDeleteBook(idList);
}
事务
概念
- 什么是事务?
事务是数据库操作的最基本单元,逻辑上的一组操作,要么都成功,如果有一个操作失败,所有操作都失败
典型场景:银行转账 - 事务四个特性 ACID
- 原子性 Atomicity
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都成功,要么都不成功 - 一致性 Consistency
事务必须使数据库从一个一致性状态变换到另一个一致性状态 - 隔离性 Isolation
事务的隔离性是指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰 - 持久性 Durability
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响
环境搭建
引入依赖
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-orm -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.22</version>
</dependency>
</dependencies>
创建数据库表
use `jdbc_template`;
create table `account` (
`id` int primary key auto_increment comment 'id',
`name` varchar(50) comment '姓名',
`balance` int comment '余额'
);
insert into `account`(`name`, `balance`)
VALUES ('亚索', 1000),
('永恩', 1000);
编写实体类、DAO、Service 等
public class Account {
private Integer id;
private String name;
private Integer balance;
// 此处省略 getter setter toString 等方法
}
public interface AccountDao {
/**
* 增加金额
*
* @param id 用户id
* @param money 金额数目
*/
void addMoney(int id, int money);
/**
* 扣除金额
*
* @param id 用户id
* @param money 金额数目
*/
void deductMoney(int id, int money);
}
@Repository
public class AccountDaoImpl implements AccountDao {
final JdbcTemplate jdbcTemplate;
@Autowired
public AccountDaoImpl(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public void addMoney(int id, int money) {
String sql = "UPDATE account SET balance=balance+? WHERE id=?";
jdbcTemplate.update(sql, money, id);
}
@Override
public void deductMoney(int id, int money) {
String sql = "UPDATE account SET balance=balance-? WHERE id=?";
jdbcTemplate.update(sql, money, id);
}
}
public class AccountService {
final AccountDao accountDao;
@Autowired
public AccountService(AccountDao accountDao) {
this.accountDao = accountDao;
}
/**
* 转账业务
*
* @param deductId 扣除金额方id
* @param addId 增加金额方id
* @param money 转账金额数目
*/
public void transferAccount(int deductId, int addId, int money) {
accountDao.deductMoney(deductId, money);
accountDao.addMoney(addId, money);
}
}
上面的代码,如果正常执行是没有问题的,但是如果代码执行过程中出现异常,会有问题
比如:
public void transferAccount(int deductId, int addId, int money) {
accountDao.deductMoney(deductId, money);
// 模拟异常
int i = 10 / 0;
accountDao.addMoney(addId, money);
}
为避免诸如上述问题,就需要事务。
事务操作流程:
- 开启事务
- 业务操作
- 若无异常,提交事务
- 出现异常,回滚事务
Spring 事务管理
- 事务一般添加到 Service 层
- Spring 进行事务管理有两种方式:编程式事务管理 和 声明式事务管理。一般都使用声明式事务管理。
- 声明式事务管理有两种方式:基于注解,基于 xml 配置文件。
- Spring 进行声明式事务管理,底层使用 AOP 原理。
- Spring 事务管理 API
提供PlatformTransactionManager
接口,代表事务管理器,这个接口针对不同框架提供不同的实现类
基于注解的声明式事务管理
配置事务管理器
<!--配置事务管理器-->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据源-->
<property name="dataSource" ref="dataSource"/>
</bean>
开启事务注解
<!--开启事务注解-->
<tx:annotation-driven transaction-manager="transactionManager"/>
上面讲到,事务一般添加到 Service 层
在 Service 类上(或者类中的方法上)添加事务注解 @Transactional
事务注解 @Transactional
既可以添加到类上,也可以添加到方法上
- 如果添加到类上,则该类中的所有方法都会添加事务
- 如果添加到方法上,则只会给该方法添加事务
@Service
@Transactional
public class AccountService {...}
事务注解 @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 的声明式事务管理
第一步,配置事务管理器;第二步,配置通知;第三步,配置切入点和切面
<!--基于 XML 的事务-->
<!--1.配置事务管理器-->
<bean id="transactionManager2"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据源-->
<property name="dataSource" ref="dataSource2"/>
</bean>
<!--2.配置通知-->
<tx:advice id="txAdvice">
<!--配置事务参数-->
<tx:attributes>
<!--指定哪种规则的方法上添加事务-->
<tx:method name="transferAccount" propagation="REQUIRED"
isolation="REPEATABLE_READ"/>
</tx:attributes>
</tx:advice>
<!--3.配置切入点和切面-->
<aop:config>
<!--配置切入点-->
<aop:pointcut id="myPointCut"
expression="execution(* com.yin.spring5.service.AccountService.*(..))"/>
<!--配置切面-->
<aop:advisor advice-ref="txAdvice" pointcut-ref="myPointCut"/>
</aop:config>
完全注解
创建配置类,代替 xml 配置文件
@Configuration // 配置类
@ComponentScan(basePackages = "com.yin.spring5") // 组件扫描
@EnableTransactionManagement // 开启事务
public class TxConfig {
@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/jdbc_template" +
"?serverTimezone=Asia/Shanghai");
dataSource.setUsername("root");
dataSource.setPassword("123456");
return dataSource;
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
@Bean
public DataSourceTransactionManager transactionManager(DataSource dataSource) {
DataSourceTransactionManager transactionManager =
new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}
Spring 5 新功能
- Spring 5 基于 Java 8,运行时兼容 Java 9,移除了不建议使用的类和方法。
- Spring 5 自带了通用的日志封装
Spring 5 移除了Log4jConfigListener
,官方建议使用 Log4j2 - Spring 5 核心注解支持
@Nullable
注解@Nullable
注解可以用在方法上、属性上、参数上,表示方法返回值、属性、参数可以为空 - 支持函数式风格 GenericApplicationContext / AnnotationConfigApplicationContext
@Test
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
引入依赖
<!-- https://mvnrepository.com/artifact/io.projectreactor/reactor-core -->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.4.0</version>
</dependency>
调用 just 方法或者其他方法只是声明数据流,数据流并没有发出,只有进行订阅之后才会触发数据流,不订阅什么都不会发生。
// just 方法直接声明
Flux.just(1, 2, 3, 4).subscribe(System.out::println);
Mono.just(5).subscribe(System.out::println);
// 其他的方法
Integer[] array = {1,2,3,4};
Flux.fromArray(array);
List<Integer> list = Arrays.asList(array);
Flux.fromIterable(list);
Stream<Integer> stream = list.stream();
Flux.fromStream(stream);
操作符
对数据流进行一道道操作,称为操作符,比如工厂流水线
- 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 服务器。
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
编写代码
public interface UserService {
/**
* 根据 id 查询 User
*
* @param id 要查询的 id
* @return 查询的结果
*/
Mono<User> getUserById(int id);
/**
* 查询所有 User
*
* @return 查询的结果
*/
Flux<User> getAllUsers();
/**
* 添加 User
*
* @param user 要添加的 User
* @return 。
*/
Mono<Void> saveUser(Mono<User> user);
}
@Service
public class UserServiceImpl implements UserService {
// 为了方便,这里使用 Map 代替数据库存储数据
private final Map<Integer, User> map = new HashMap<>();
public UserServiceImpl() {
this.map.put(1, new User(1, "Alice", 'F', 18));
this.map.put(2, new User(2, "Bella", 'F', 20));
this.map.put(3, new User(3, "Cindy", 'F', 18));
this.map.put(4, new User(4, "Diana", 'F', 20));
this.map.put(5, new User(5, "Emily", 'F', 18));
}
@Override
public Mono<User> getUserById(int id) {
return Mono.justOrEmpty(this.map.get(id));
}
@Override
public Flux<User> getAllUsers() {
return Flux.fromIterable(this.map.values());
}
@Override
public Mono<Void> saveUser(Mono<User> userMono) {
return userMono.doOnNext(user -> map.put(user.getId(), user))
.thenEmpty(Mono.empty());
}
}
@RestController
public class UserController {
UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
@GetMapping("/user/{id}")
public Mono<User> getUserById(@PathVariable("id") int id) {
return userService.getUserById(id);
}
@GetMapping("/users")
public Flux<User> getAllUsers() {
return userService.getAllUsers();
}
@PostMapping("/user")
public Mono<Void> saveUser(User user) {
Mono<User> userMono = Mono.just(user);
return userService.saveUser(userMono);
}
}
- Spring MVC 方式,同步阻塞的方式,基于 Spring MVC + Servlet + Tomcat;
- Spring WebFlux 方式,异步非阻塞的方式,基于 Spring WebFlux + Reactor + Netty。
基于函数式编程模型
- 使用函数式编程模型,需要自己初始化服务器
- 两个核心接口:RouterFunction(实现路由功能,请求转发给对应的 handler)和 HandlerFunction(处理请求生成响应的函数)。核心任务定义两个函数式接口的实现并且启动需要的服务器。
- Spring WebFlux 的请求和响应不再是 ServletRequest 和 ServletResponse,而是 ServerRequest 和 ServerResponse。
- 把基于注解的复制一份,删去 controller 部分
- 创建 Handler
public 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
- 创建路由的方法
- 创建服务器完成适配
- 最终调用
public 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 调用
public 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