Spring5 新功能
新功能概述
-
整个 Spring5 框架的代码基于Java8 实现,运行时兼容 JDK9,许多不建议使用的类和方法在代码库中已经被删除了
-
Spring 5 框架自带了通用的日志封装
-
Spring 5 框架的核心容器支持
@Nullable
注解(@Nullable 注解可以使用在方法、属性、参数上)添加在方法上:表示该方法返回值可以为空
添加在属性上:表示该属性值可以为空
添加在参数上:表示该参数值可以为空
-
Spring5 核心容器支持函数式风格
GenericApplicationContext
/AnnotationConfigApplicationContext
-
Spring5 框架支持整合 JUnit5 单元测试框架
-
Spring5 框架提供了一个新的模块:
WebFlux
Spring5 整合日志框架
- Spring5 已经移除了 Log4jConfigListener,官方建议使用 Log4j2
- 如果想要使用 Log4j,那么就需要将 Spring 的版本降到 4 或 4 之前的版本
Ps: 下面演示的代码还是使用的事务管理时编写的代码
实现步骤
回顾:之前的代码架构
第一步:引入相关 jar 包
第二步:创建 log4j2.xml 配置文件(该配置文件的名字的固定的)
<?xml version="1.0" encoding="UTF-8"?>
<!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!--Configuration后面的status用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,可以看到log4j2内部各种详细输出(越往右边的级别打印的就越多)-->
<configuration status="INFO">
<!--先定义所有的appender-->
<appenders>
<!--输出日志信息到控制台-->
<console name="Console" target="SYSTEM_OUT">
<!--控制日志输出的格式-->
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</console>
</appenders>
<!--然后定义logger,只有定义了logger并引入的appender,appender才会生效-->
<loggers>
<!--root:用于指定项目的根日志,如果没有单独指定Logger,则会使用root作为默认的日志输出-->
<root level="info">
<appender-ref ref="Console"/>
</root>
</loggers>
</configuration>
注意:该文件的名称只能是 log4j2.xml,不能随意取名
第三步:测试效果
package com.laoyang.spring.test;
import com.laoyang.spring.config.TxConfig;
import com.laoyang.spring.service.UserService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.transaction.PlatformTransactionManager;
public class UserTest {
@Test
public void testTransferAccounts3() {
ApplicationContext context = new AnnotationConfigApplicationContext(TxConfig.class);
UserService userService = context.getBean("userService", UserService.class);
userService.transferAccounts();
}
}
Spring5 自带的日志功能
package com.laoyang.spring.test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogTest {
private static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);
public static void main(String[] args) {
LOGGER.info("hello");
LOGGER.warn("world");
}
}
导的包一定要是 slf4j 下的,不要导错了!!!
Spring 5 框架的核心容器支持 @Nullable 注解
- @Nullable 注解可以使用在方法、属性、参数上
- 添加在方法上:表示该方法返回值可以为空
- 添加在属性上:表示该属性值可以为空
- 添加在参数上:表示该参数值可以为空
package com.laoyang.spring.test;
import org.junit.Test;
import org.junit.rules.TestName;
import org.springframework.lang.Nullable;
/**
* @ClassName NullableTest
* @Description: @Nullable 注解的使用
* @Author Laoyang
* @Date 2021/12/22 11:25
*/
public class NullableTest {
@Nullable
private String name;
@Nullable
public String getName() {
return name;
}
public void getNames(@Nullable String a) {
System.out.println(a);
}
}
Spring5 核心容器支持函数式风格 GenericApplicationContext/AnnotationConfigApplicationContext
package com.laoyang.spring.test;
import com.laoyang.spring.config.TxConfig;
import com.laoyang.spring.service.UserService;
import org.junit.Test;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
/**
* @ClassName FunctionTest
* @Description: Spring5 核心容器支持函数式风格
* @Author Laoyang
* @Date 2021/12/22 11:51
*/
public class FunctionTest {
public static void main(String[] args) {
// 这种创建对象的方式是靠我们自己手动 new 出来的,Spring 是不能直接进行管理的
FunctionTest functionTest = new FunctionTest();
// 如果想要 Spring 进行管理,则需要进行注册
}
}
GenericApplicationContext
package com.laoyang.spring.test;
import com.laoyang.spring.config.TxConfig;
import com.laoyang.spring.service.UserService;
import org.junit.Test;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
/**
* @ClassName FunctionTest
* @Description: Spring5 核心容器支持函数式风格
* @Author Laoyang
* @Date 2021/12/22 11:51
*/
public class FunctionTest {
/**
* 使用函数式风格创建对象,交给 Spring 进行管理
*/
@Test
public void testGenericApplicationContext() {
// 1、创建 GenericApplicationContext 对象
GenericApplicationContext context = new GenericApplicationContext();
// 2、调用 context 的方法进行对象注册
context.refresh();
// context.registerBean(FunctionTest.class, () -> new FunctionTest());
context.registerBean("function", FunctionTest.class, () -> new FunctionTest());
/*
3、获取 Spring 注册的对象
这里的对象和之前的写法有一点不同,之前默认的对象名是类名首字母小写,但是这里的不同
这里有两种方式可以使用:
第一种:写类的全路径
第二种:在注册对象的时候手动设置对象名(在 context.registerBean() 方法中设置)
*/
// 第一种写法:类的全路径
// FunctionTest functionTest = (FunctionTest) context.getBean("com.laoyang.spring.test.FunctionTest");
// 第二种写法:写我们注册对象时设置的名称
FunctionTest functionTest = (FunctionTest) context.getBean("function");
System.out.println(functionTest);
}
}
AnnotationConfigApplicationContext
- 这种方式和上面那种差不多
package com.laoyang.spring.test;
import com.laoyang.spring.config.TxConfig;
import com.laoyang.spring.service.UserService;
import org.junit.Test;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
/**
* @ClassName FunctionTest
* @Description: Spring5 核心容器支持函数式风格
* @Author Laoyang
* @Date 2021/12/22 11:51
*/
public class FunctionTest {
@Test
public void testAnnotationConfigApplicationContext() {
// 1、创建 AnnotationConfigApplicationContext 对象
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
// 2、调用 context 的方法进行对象注册
context.refresh();
// context.registerBean(FunctionTest.class, () -> new FunctionTest());
context.registerBean("function", FunctionTest.class, () -> new FunctionTest());
/*
3、获取 Spring 注册的对象
这里的对象和之前的写法有一点不同,之前默认的对象名是类名首字母小写,但是这里的不同
这里有两种方式可以使用:
第一种:写类的全路径
第二种:在注册对象的时候手动设置对象名(在 context.registerBean() 方法中设置)
*/
// 第一种写法:类的全路径
// FunctionTest functionTest = (FunctionTest) context.getBean("com.laoyang.spring.test.FunctionTest");
// 第二种写法:写我们注册对象时设置的名称
FunctionTest functionTest = (FunctionTest) context.getBean("function");
System.out.println(functionTest);
}
}
整合 JUnit5 单元测试框架
整合 JUnit4
第一步:引入 Spring 相关针对测试的 jar 包
第二步:创建测试类,使用注解方式完成
package com.laoyang.spring.test;
import com.laoyang.spring.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @ClassName JUnit4Test
* @Description: 整合 JUnit4
* @Author Laoyang
* @Date 2021/12/22 16:12
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:bean1.xml")
public class JUnit4Test {
/*
@RunWith():指定 JUnit 版本
@ContextConfiguration():加载配置文件
*/
// 注入 service
@Autowired
private UserService userService;
@Test
public void testJUnit4() {
userService.transferAccounts();
}
}
测试前还需要将配置类中的 @Configuration 注解注释掉,不然会导致有 bean 不唯一,然后报错
整合 JUnit5
第一步:引入 JUnit5 相关 jar 包
第二步:创建测试类,使用注解完成
package com.laoyang.spring.test;
import com.laoyang.spring.service.UserService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* @ClassName JUnit5Test
* @Description: 整合 JUnit5
* @Author Laoyang
* @Date 2021/12/22 16:23
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:bean1.xml")
public class JUnit5Test {
/*
@ExtendWith():使用该注解引用 SpringExtension
> SpringExtension: 是JUnit多个可拓展API的一个实现,提供了对现存Spring TestContext Framework的支持
@ContextConfiguration():加载配置文件
*/
// 注入 service
@Autowired
private UserService userService;
@Test
public void testJUnit4() {
userService.transferAccounts();
}
}
可以使用一个复合注解替代类上的两个注解:@SpringJunitConfig
package com.laoyang.spring.test;
import com.laoyang.spring.service.UserService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
/**
* @ClassName JUnit5Test
* @Description: 整合 JUnit5
* @Author Laoyang
* @Date 2021/12/22 16:23
*/
//@ExtendWith(SpringExtension.class)
//@ContextConfiguration("classpath:bean1.xml")
@SpringJUnitConfig(locations = "classpath:bean1.xml")
public class JUnit5Test {
/*
@ExtendWith():使用该注解引用 SpringExtension
> SpringExtension: 是JUnit多个可拓展API的一个实现,提供了对现存Spring TestContext Framework的支持
@ContextConfiguration():加载配置文件
@SpringJUnitConfig(locations = "classpath:bean1.xml"):复合注解
*/
// 注入 service
@Autowired
private UserService userService;
@Test
public void testJUnit4() {
userService.transferAccounts();
}
}
SpringWebflux
说明
-
官方文档:https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html
-
SpringWebflux 是 Spring5 中添加的模块,用于 Web 开发,功能和 SpringMVC 类似
-
传统的 Web 框架(比如:SpringMVC)是基于 Servlet 容器实现的;而 Webflux 是一种异步非阻塞的框架,异步非阻塞的框架在 Servlet3.1 以后才会支持使用(Servlet3.1之前的版本不能使用 ),核心就是基于 Reactor 相关 API 实现的。
-
异步非阻塞:
同步和异步针对的是调用者
,比如调用者发送请求,如果等着对方回应之后才去做其他事情就是同步;如果发送请求之后不等对方回应就去做其它事情就是异步。非阻塞和阻塞针对的是被调用者
,如果被调用者收到请求之后,并做完请求任务之后才给出反馈,那么就是阻塞;如果收到请求之后马上给出反馈然后再去做其它事情,那么就是非阻塞。
Webflux 特点
- 异步非阻塞:好处:在有限的资源下提高系统的吞吐量和伸缩性,以 Reacror 为基础实现响应式编程
- 函数式编程:Spring5 框架基于 Java8,Webflux 可以使用 Java8 函数式编程方式实现路由请求
Webflux 和 SpringMVC 的区别
- 两个框架都可以使用注解方式进行操作,都可以运行在 Tomcat 等容器中
- SpringMVC 采用的是命令式编程(一行一行代码执行),而 Webflux 采用的是异步响应式编程
- 关于什么时候使用哪个框架,大家可查看官方文档 1.1.4,也可看下图了解一下(图片是官方截取的)
响应式编程
说明
-
响应式编程:响应式编程是一种面向数据流和变化传播的编程范式,这意味着可以在编程语言中很方便的表达静态或动态的数据流,而相关的计算模型会自动变化的值通过数据流进行传播。
以电子表格为例,单元格可以包含字面值或类似 ”B1+C1“ 的公式,而包含公式的单元格的值会依据其它单元格的值的变化而变化
Java8 及其之前版本的实现方式
-
Java8 及其之前版本中提供了观察者模式的两个类:
Observer
和Observable
-
Java9 及其之后版本使用
Flow
类取代了 Java8 及之前版本的那两个类 -
观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
比如军营中的哨兵,哨兵在军营的各个方位,然后一旦发现情况则告知主帅,然后由主帅发布命令(进攻、防守等)
主帅是一,哨兵是多
创建 SpringBoot 工程(因为 SpringBoot 使用起来比较方便)
选择 Spring Initializr 之后就下一步,然后设置对应的包路径,然后一直下一步就好了
编写对应代码进行测试,查看对应的效果
package com.laoyang.spring5_demo7.reactor8;
import java.util.Observable;
/**
* @ClassName ObserverDemo
* @Description: 观察者模式演示
* @Author Laoyang
* @Date 2021/12/22 20:58
*/
public class ObserverDemo extends Observable {
public static void main(String[] args) {
ObserverDemo observer = new ObserverDemo();
// 添加观察者
observer.addObserver((o, arg) -> {
System.out.println("发生了变化");
});
observer.addObserver((o, arg) -> {
System.out.println("收到被观察者的通知,准备改变");
});
// 将此 Observable 对象标记为已更改
observer.setChanged();
// 如果该 Observable 对象发生了变化,则通知所有观察者
observer.notifyObservers();
}
}
Reactor 实现响应式编程
-
在响应式编程的操作中,都需要满足 Reactive 规范,Reactor 就是满足这种规范的
-
在 Reactor 中有两个核心类:Mono 和 Flux,这两个类都实现了 Publisher 接口,都提供了丰富的操作符
Mono 对象实现发布者,返回 0 或 1 个元素
Flux 对象实现发布者,返回 n 个元素
-
Mono 和 Flux 都是数据流的发布者,使用 Mono 和 Flux 都可以发出三种数据信号:元素值、错误信号、完成信号
错误信号和完成信号都表示终止信号,终止信号用于告诉订阅者数据流结束了
错误信号终止数据流时会把错误信息传递给订阅者
代码演示(Flux 和 Mono)
第一步:引入Reactor 相关依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.laoyang</groupId>
<artifactId>spring5_demo7</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring5_demo7</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Reactor 相关依赖 -->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.4.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
第二步:代码演示
package com.laoyang.spring5_demo7.reactor8;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
/**
* @ClassName ReactorDemo
* @Description: Flux 和 Mono 的演示
* @Author Laoyang
* @Date 2021/12/23 15:58
*/
public class ReactorDemo {
/**
* 说明:
* 调用 just() 方法或者其它方法只是声明了数据流,并没有发出,
* 只有进行订阅之后才会触发数据流,不订阅就什么都不会发生。
*
* 订阅方法:subscribe()
*/
public static void main(String[] args) {
// just() 方法可以直接声明元素
// Flux.just(1, 2, 3);
// Mono.just(1);
Flux.just(1, 2, 3).subscribe(System.out::print);
Mono.just(1).subscribe(System.out::print);
// 其它方法
// 数组元素
Integer[] arr = {11, 22, 33};
Flux.fromArray(arr);
// 集合元素
List<Integer> list = Arrays.asList(arr);
Flux.fromIterable(list);
// Stream 流元素
Stream stream = list.stream();
Flux.fromStream(stream);
}
}
因为 Mono 和 Flux 都实现了 Publisher 接口,所以它们使用的方法基本也是一样的,所以后面就没有演示 Mono 的调用了
三种信号的特点
- 错误信号和完成信号都是终止信号,不能同时存在
- 如果没有发送任何元素值,而是直接发送错误信号或完成信号,那么就表示是一个空数据流
- 如果没有错误信号,也没有完成信号,那么就表示是一个无限数据流
操作符
-
对数据流进行一道道操作,就称为操作符(比如工厂中的流水线)
-
常用操作符:
map
:将元素映射为新元素flatMap
:将元素映射为流(把每个元素转换为流,在转换之后多个流合并为一个大的流)
Webflux 执行流程和核心 API
-
Webflux 基于 Reactor 实现,默认容器是 Netty,Netty 是高性能、异步非阻塞的框架
-
SpringWebflux 执行过程和 SpringMVC 相似
SpringWebflux 核心控制器
DispatchHandler
,实现WebHandler
接口WebHandler 接口中有一个方法:
handle()
-
SpringWebflux 中的 DispatcherHandler 负责请求的处理,SpringWebflux 中还有三个组件:
HandlerMapping
:请求查询的处理方法HandlerAdapter
:适配器,真正负责请求处理HandlerResultHandler
:响应结果的处理 -
SpringWebflux 实现函数式编程的两个接口:
RouterFunction(路由处理)
和HandlerFunction(处理函数)
SpringWebflux(基于注解编程模型实现)
说明
-
SpringWebflux 实现方式有两种:注解编程模型和函数式编程模式
-
注解方式和 SpringMVC 类似,只需要把相关依赖配置到项目中
因为使用的是 SpringBoot 工程,而 SpringBoot 会自动配置相关运行容器,默认情况下就是使用 Netty 服务器
具体实现
第一步:创建 SprtingBoot 工程,引入相关依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- SpringBoot2.0.* 及以上版本使用的就是 Sping5 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.laoyang</groupId>
<artifactId>spring</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- 这里改成 spring-boot-starter-webflux -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
第二步:在 application.properties
文件中配置启动的端口号
server.port=8081
第三步:创建包(entity、service、controller)和相关类
用户实体类:
package com.laoyang.spring.entity;
public class User {
private String name;
private String sex;
private Integer age;
public User() {
}
public User(String name, String sex, Integer age) {
this.name = name;
this.sex = sex;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
用户操作接口:
package com.laoyang.spring.service;
import com.laoyang.spring.entity.User;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface UserService {
/**
* 根据id查询用户
*/
Mono<User> findUserById(Integer id);
/**
* 查询所有用户
*/
Flux<User> findAllUser();
/**
* 添加用户
*/
Mono<Void> saveUser(Mono<User> user);
}
接口实现类:
package com.laoyang.spring.service.impl;
import com.laoyang.spring.entity.User;
import com.laoyang.spring.service.UserService;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
@Service
public class UserServiceImpl implements UserService {
// 创建 map 集合存储数据(因为这里不操作数据库,所以就使用 map 来代替数据库)
private final Map<Integer, User> userMap = new HashMap<>();
/**
* 初始化map
*/
public UserServiceImpl() {
this.userMap.put(1, new User("Tom", "男", 18));
this.userMap.put(2, new User("Jerry", "男", 72));
this.userMap.put(3, new User("lucy", "女", 36));
}
/**
* 根据id查询用户
*/
@Override
public Mono<User> findUserById(Integer id) {
return Mono.justOrEmpty(this.userMap.get(id));
}
/**
* 查询所有用户
*/
@Override
public Flux<User> findAllUser() {
return Flux.fromIterable(this.userMap.values());
}
/**
* 新增用户
*/
@Override
public Mono<Void> saveUser(Mono<User> user) {
// doOnNext() 方法相当于把 user 进行遍历,拿到里面的值,然后在把里面的值存到 userMap 中
return user.doOnNext(person -> {
// 向 map 对象中存值,为了 key 值不重复,可以使用当前 map 长度作为 key
int id = this.userMap.size() + 1;
this.userMap.put(id, person);
// 终止信号
}).thenEmpty(Mono.empty());
}
}
第四步:创建 Controller
package com.laoyang.spring.controller;
import com.laoyang.spring.entity.User;
import com.laoyang.spring.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
public class UserController {
// 注入 service
@Autowired
private UserService userService;
/**
* 根据id查询用户
*/
@GetMapping("/user/{id}")
public Mono<User> getUserId(@PathVariable Integer id) {
return userService.findUserById(id);
}
/**
* 查询所有用户
*/
@GetMapping("/user/all")
public Flux<User> getUsers() {
return userService.findAllUser();
}
/**
* 新增用户
*/
@PostMapping("/user/save")
public Mono<Void> saveUser(@RequestBody User user) {
Mono<User> mono = Mono.just(user);
return userService.saveUser(mono);
}
}
第五步:使用 PostMan 进行测试
emmmm 新增用户那个接口不太好查看测试之后的效果,因为是使用的 map 存储数据,大家可以替换成数据库数据,然后在数据库中查看数据,或者新增之后试着判断一下是否成功,然后返回一个提示。
说明:
- SpringMVC 是使用同步阻塞的方式实现的,基于 SpringMVC + Servlet + Tomcat
- SpringWebflux 是使用异步非阻塞的方式实现的,基于 SpringWebflux + Reactor + Netty
SpringWebflux(基于函数式编程模型实现)
说明
- 在使用函数式编程模型操作的时候,需要自己初始化服务器
- 基于函数式编程模型操作的时候,有两个核心接口:
RouterFunction(实现路由功能,请求转发给对应的 handler)
和HandlerFunction(处理请求生成响应的函数)
,核心任务就是定义两个函数式接口的实现并启动需要的服务器。 - SpringWebflux 请求和响应不再是 ServletRequest 和 ServletResponse 了,而是
ServerRequest
和ServerResponse
具体实现
第一步:把注解编程模型的工程复制一份,然后把 controller 删除(controller 包和里面的类都删掉)
第二步:创建 Handler(具体实现方法)
package com.laoyang.spring.handler;
import com.laoyang.spring.entity.User;
import com.laoyang.spring.service.UserService;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public class UserHandler {
private final UserService userService;
public UserHandler(UserService userService) {
this.userService = userService;
}
/**
* 根据id查询用户
*/
public Mono<ServerResponse> getUserById(ServerRequest request) {
/*
1、获取id
pathVariable():获取接口路径中的值
*/
Integer id = Integer.valueOf(request.pathVariable("id"));
// 为了保证代码质量,最好加上空值处理
Mono<ServerResponse> notFound = ServerResponse.notFound().build();
// 2、调用 service 中的方法得到对应数据
Mono<User> user = this.userService.findUserById(id);
/*
3、将 user 进行转换
使用 Reactor 操作符(flatMap)将 user 转换成流
contentType:设置数据类型
switchIfEmpty:判断数据是否为空,如果是空的,则返回 notFound 对象
*/
return user.flatMap(
person -> ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(person)
).switchIfEmpty(notFound);
}
/**
* 查询所有用户
*/
public Mono<ServerResponse> getAllUser() {
// 调用 servier 中的方法得到对应数据
Flux<User> users = this.userService.findAllUser();
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(users, User.class);
}
/**
* 新增用户
*/
public Mono<ServerResponse> saveUser(ServerRequest request) {
// 得到 User 对象
Mono<User> mono = request.bodyToMono(User.class);
return ServerResponse.ok().build(this.userService.saveUser(mono));
}
}
第三步:初始化服务器,编写 Router
package com.laoyang.spring;
import com.laoyang.spring.handler.UserHandler;
import com.laoyang.spring.service.UserService;
import com.laoyang.spring.service.impl.UserServiceImpl;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
public class Server {
/**
* 1、创建 Router 路由
*/
public RouterFunction<ServerResponse> routerFunction() {
// 创建 handler 对象
UserService userService = new UserServiceImpl();
UserHandler handler = new UserHandler(userService);
// 设置路由
return RouterFunctions.route(
/*
GET("/user/{id}"):设置请求路径
accept(APPLICATION_JSON):接收的数据格式
handler::getUserById:调用的方法
*/
GET("/user/{id}").and(accept(APPLICATION_JSON)),handler::getUserById)
.andRoute(
GET("/all").and(accept(APPLICATION_JSON)),handler::getAllUser)
.andRoute(
POST("/save").and(accept(APPLICATION_JSON)),handler::saveUser
);
}
}
后面测试发现 /user/{id} 和 /user/all 这些路径前面的一样,就会导致我测试 all 的时候也走 {id} 路径,然后就会报错,所以后面两个接口就不写 /user 了
第四步:创建服务器,完成适配
package com.laoyang.spring;
import com.laoyang.spring.handler.UserHandler;
import com.laoyang.spring.service.UserService;
import com.laoyang.spring.service.impl.UserServiceImpl;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.netty.http.server.HttpServer;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RouterFunctions.toHttpHandler;
public class Server {
/**
* 1、创建 Router 路由
*/
public RouterFunction<ServerResponse> routerFunction() {
// 创建 handler 对象
UserService userService = new UserServiceImpl();
UserHandler handler = new UserHandler(userService);
// 设置路由
return RouterFunctions.route(
/*
GET("/user/{id}"):设置请求路径
accept(APPLICATION_JSON):接收的数据格式
handler::getUserById:调用的方法
*/
GET("/user/{id}").and(accept(APPLICATION_JSON)),handler::getUserById)
.andRoute(
GET("/all").and(accept(APPLICATION_JSON)),handler::getAllUser)
.andRoute(
POST("/save").and(accept(APPLICATION_JSON)),handler::saveUser
);
}
/**
* 2、创建服务器完成适配
*/
public void createReatorServer() {
// 路由和 handler 适配
RouterFunction<ServerResponse> router = routerFunction();
HttpHandler httpHandler = toHttpHandler(router);
// 适配器,将路由和 handler 进行适配
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler);
// 创建服务器
HttpServer httpServer = HttpServer.create();
httpServer.handle(adapter).bindNow();
}
}
第五步:调用方法启动项目,然后使用 PostMan 进行测试查看对应效果
package com.laoyang.spring;
import com.laoyang.spring.handler.UserHandler;
import com.laoyang.spring.service.UserService;
import com.laoyang.spring.service.impl.UserServiceImpl;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.netty.http.server.HttpServer;
import java.io.IOException;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RouterFunctions.toHttpHandler;
public class Server {
public static void main(String[] args) throws IOException {
Server server = new Server();
server.createReatorServer();
System.out.println("enter to exit!");
System.in.read();
}
/**
* 1、创建 Router 路由
*/
public RouterFunction<ServerResponse> routerFunction() {
// 创建 handler 对象
UserService userService = new UserServiceImpl();
UserHandler handler = new UserHandler(userService);
// 设置路由
return RouterFunctions.route(
/*
GET("/user/{id}"):设置请求路径
accept(APPLICATION_JSON):接收的数据格式
handler::getUserById:调用的方法
*/
GET("/user/{id}").and(accept(APPLICATION_JSON)),handler::getUserById)
.andRoute(
GET("/all").and(accept(APPLICATION_JSON)),handler::getAllUser)
.andRoute(
POST("/save").and(accept(APPLICATION_JSON)),handler::saveUser
);
}
/**
* 2、创建服务器完成适配
*/
public void createReatorServer() {
// 路由和 handler 适配
RouterFunction<ServerResponse> router = routerFunction();
HttpHandler httpHandler = toHttpHandler(router);
// 适配器,将路由和 handler 进行适配
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler);
// 创建服务器
HttpServer httpServer = HttpServer.create();
httpServer.handle(adapter).bindNow();
}
}
注意:因为我们没有设置端口号,所以每次启动都会有一个随机的端口号,我们需要使用这个随机的端口号进行测试
效果如下
使用 WebClinet 进行调用
package com.laoyang.spring;
import com.laoyang.spring.entity.User;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import java.util.List;
/**
* @ClassName Client
* @Description:使用 WebClinet 进行调用
* @Author Laoyang
* @Date 2021/12/23 22:05
*/
public class Client {
public static void main(String[] args) {
// 调用服务器地址
WebClient webClient = WebClient.create("http://localhost:2888");
// 根据 id 查询
String id = "1";
/*
get():接口请求
uri("/user/", id):请求路径和参数
accept(MediaType.APPLICATION_JSON):接收的数据格式
retrieve():初始化
bodyToMono:转换为指定对象
*/
User user = webClient.get().uri("/user/{id}", id).
accept(MediaType.APPLICATION_JSON).retrieve().bodyToMono(User.class).block();
System.out.println(user.getName());
// 查询所有用户
Flux<User> result = webClient.get().uri("/all").accept(MediaType.APPLICATION_JSON).retrieve().bodyToFlux(User.class);
result.map(stu -> stu.getName()).buffer().doOnNext(System.out::println).blockFirst();
}
}
先启动 Server 类中的 main 方法,然后将随机生成的端口号设置好,在启动这个 main 方法
效果如下: