# Spring5 新特性
# 1. Spring 5.x 整合 Log4j2
- Spring5 已经移除 Log4jConfigListener,官方建议使用 Log4j2。
# 1.1 导入 jar 包
Log4j2 官网下载地址 (opens new window)
# 1.2 创建 log4j2.xml 配置文件并进行配置
- 文件名必须为:log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!--Configuration 后面的 status 用于设置 log4j2 自身内部的信息输出,可以不设置, 当设置成 trace 时,可以看到 log4j2 内部各种详细输出-->
<configuration status="DEBUG">
<!--先定义所有的 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>
# 1.3 测试一下 DEBUG 级别的效果
# 1.4 手动进行日志输出
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Hedon Wang
* @create 2020-09-16 17:17
*/
public class UserLog {
private static final Logger log = LoggerFactory.getLogger(UserLog.class);
public static void main(String[] args) {
log.info("Hello log4j2");
log.warn("Warning log4j2");
}
}
# 2. Spring 5.x 支持 @Nullable 注解
@Nullable 注解可以使用在方法上面,属性上面,参数上面,表示方法返回可以为空,属性值可以为空,参数值可以为空。
# 3. Spring 5.x 支持函数式风格(Lambda表达式)
- 指定 bean 名称
@Test
public void testGenericApplicationContext(){
//1. 创建 GenericApplicationContext 对象
GenericApplicationContext context = new GenericApplicationContext();
//2-1 清空context
context.refresh();
//2-2 注册 bean
context.registerBean("account1", Account.class,() -> new Account());
//3. 获取 bean
Account account1 = context.getBean("account1", Account.class);
System.out.println(account1);
}
不指定 bean 名称
这里不指定名称的话不是默认为“类名首字母小写”,而是需要写全限定类名来获取。
@Test
public void testGenericApplicationContextWithoutName(){
//1. 创建 GenericApplicationContext 对象
GenericApplicationContext context = new GenericApplicationContext();
//2-1 清空context
context.refresh();
//2-2 注册 bean
context.registerBean(Account.class,() -> new Account());
//3. 获取 bean
Account account1 = context.getBean("com.hedon.bean.Account", Account.class);
System.out.println(account1);
}
# 4. Spring5.x 整合 JUnit4
# 4.1 需要引入 spring-test 的 jar 包
# 4.2 使用示例
- @RunWith:指定单元测试框架
- @ContextConfiguration:加载配置文件
@RunWith(SpringJUnit4ClassRunner.class) //指定单元测试框架
@ContextConfiguration("classpath:bean.xml") //加载配置文件
public class Junit4Test {
@Autowired
private AccountService accountService;
@Test
public void test(){
accountService.print();
}
}
# 5. Spring 5.x 支持整合 JUnit5
# 5.1 引入 Junit5 的 jar 包
import org.junit.jupiter.api.Test;
//然后等它报错,根据提示导入 jar 包就可以了
# 5.2 使用示例
//@ExtendWith(SpringExtension.class)
//@ContextConfiguration("classpath:bean.xml")
@SpringJUnitConfig(locations = "classpath:bean.xml") //上面两个等价于这一个
public class Junit5Test {
@Autowired
private AccountService accountService;
@Test
public void test(){
accountService.print();
}
}
# 6. Spring WebFlux
# 6.1 WebFlux 简介
WebFlux 是 Spring5 添加新的模块,用于 web 开发的,功能和 SpringMVC 类似、Webflux 是当前一种比较流行的响应式编程框架。
传统 web 框架,比如 SpringMVC,这些基于 Servlet 容器,WebFlux 是一种异步非阻塞的框架,异步非阻塞的框架在 Servlet3.1 以后才支持,核心是基于 Reactor 的相关 API 实现的。
# 6.2 异步非阻塞
# 6.2.1 异步&同步
异步和同步针对被调用者,被调用者受到请求之后,做完请求任务之后才给出反馈就是同步,受到请求之后马上给出反馈然后再去做事情就是异步。
# 6.2.2 阻塞&非阻塞
阻塞和非阻塞针对调用者,调用者发出请求后,只能傻傻等待直到请求得到回应,那就是阻塞,调用者发出请求后可以先干别的事,那就是非阻塞。
# 6.3 WebFlux 特点
# 6.3.1 非阻塞式
在有限资源下,提高系统吞吐量和伸缩性,以 Reactor 为基础实现响应式编程。
# 6.3.2 函数式编程
Spring5 框架基于 java8,WebFlux 使用 Java8 函数式编程方式实现路由请求。
# 6.4 与 Spring MVC 比较
- 两个框架都可以使用注解方式,都运行在 Tomet 等容器中。
- SpringMVC 采用命令式编程,WebFlux 采用异步响应式编程。
- 跨服务调用(网关)经常用 WebFlux。
# 6.5 响应式编程
# 6.5.1 介绍
响应式编程是一种面向数据流和变化传播的编程范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。
电子表格程序就是响应式编程的一个例子。单元格可以包含字面值或类似"=B1+C1"的公式,而包含公式的单元格的值会依据其他单元格的值的变化而变化。
# 6.5.2 Java8 及之前版本
- 提供的观察者模式两个类 Observer 和 Observable
public class ObserverDemo extends Observable {
public static void main(String[] args) {
ObserverDemo observerDemo = new ObserverDemo();
//添加观察者
observerDemo.addObserver((o,arg)->{
System.out.println("发生变化");
});
//添加观察者
observerDemo.addObserver(new Observer() {
@Override
public void update(Observable o, Object arg) {
System.out.println("收到被观察者的通知,准备发生改变");
}
});
//发生变化
observerDemo.setChanged();
//通知
observerDemo.notifyObservers();
}
}
- 输出
收到被观察者的通知,准备发生改变
发生变化
Process finished with exit code 0
# 6.5.3 响应式编程(Reactor 实现)
- 响应式编程操作中,都需要满足 Reactive 规范,Reactor 就满足了。
# 6.5.3.1 核心类 Mono 和 Flux
Mono 和 Flux,这两个类实现接口 Publisher,提供丰富操作符。
Flux 对象实现发布者,返回 N 个元素;
Mono 实现发布者,返回 0 或者 1 个元素。
# 6.5.3.2 三种信号
Flux 和 Mono 都是数据流的发布者,使用 Flux 和 Mono 都可以发出三种数据信号: 元素值,错误信号,完成信号。
错误信号和完成信号都代表终止信号,不能共存的。终止信号用于告诉订阅者数据流结束了,错误信号终止数据流同时把错误信息传递给订阅者。
如果没有发送任何元素值,而是直接发送错误或者完成信号,表示是空数据流。
如果没有错误信号,没有完成信号,表示是无限数据流。
# 6.5.3.3 代码实现
引入 jar 包坐标
<dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-core</artifactId> <version>3.3.6.RELEASE</version> </dependency>
编写程序代码
public class TestReactor { public static void main(String[] args) { /** * juts 方法直接申明 */ //Flux可以传多个元素 Flux.just(1,2,3,4); //Moon传1个或0个元素 Mono.just(1); /** * 其他方法 */ //数组形式的数据流 Integer[] array = {1,2,3,4}; Flux.fromArray(array); //集合形式的数据流 List<Integer> list = Arrays.asList(array); Flux.fromIterable(list); //Stream流 Stream<Integer> stream = list.stream(); Flux.fromStream(stream); } }
==注意==:调用 just 或者其他方法只是声明数据流,数据流并没有发出,只有进行订阅之后才会触发数据流,不订阅什么都不会发生的。
上述程序直接运行,没有东西:
15:45:29.462 [main] DEBUG reactor.util.Loggers$LoggerFactory - Using Slf4j logging framework
Process finished with exit code 0
订阅一波:
//Flux可以传多个元素
Flux.just(1,2,3,4).subscribe(System.out::println);
//Moon传1个或0个元素
Mono.just(1).subscribe(System.out::println);
运行程序:
15:46:23.974 [main] DEBUG reactor.util.Loggers$LoggerFactory - Using Slf4j logging framework
1
2
3
4
1
Process finished with exit code 0
# 6.5.4 操作符
对数据流进行一道道操作,成为操作符,比如工厂流水线。
# 6.5.4.1 map 元素映射为新元素
# 6.5.4.2 flapMap 元素映射为流
- 把每个元素转换流,把转换之后多个流合并大的流
# 6.6 WebFlux 核心容器 —— Netty
SpringWebflux 基于 Reactor,默认使用容器是 Netty,Netty 是高性能的 NIO 框架,NIO 即同步非阻塞。
# 6.6.1 BIO:阻塞IO
只能同时处理一个连接,要管理多个并发客户端,需要为每个新的客户端 Socket 创建一个新的 Thread,线程模型如下图所示:
这样就会存在以下问题:
- 在任何时候都可能有大量的线程处于休眠状态,只是等待输入或者输出数据就绪,这可能算是一种资源浪费。
- 需要为每个线程的调用栈都分配内存。
- 即使 Java 虚拟机(JVM) 在物理上可以支持非常大数量的线程, 但是远在到达该极限之前, 上下文切换所带来的开销就会带来麻烦。
# 6.6.2 NIO:非阻塞
从该图可以看出Selector 是Java 的非阻塞 I/O 实现的关键。它使用了事件通知 API 以确定在一组非阻塞套接字中有哪些已经就绪能够进行 I/O 相关的操作。因为可以在任何的时间检查任意的读操作或者写操作的完成状态。该种模型下,一个单一的线程便可以处理多个并发的连接。
与BIO相比,该模型有以下特点:
- 使用较少的线程便可以处理许多连接,因此也减少了内存管理和上下文切换所带来开销。
- 当没有 I/O 操作需要处理的时候,线程也可以被用于其他任务。
虽然 Java 的NIO在性能上比 BIO 已经相当的优秀,但是要做到如此正确和安全并不容易。特别是,在高负载下可靠和高效地处理和调度 I/O 操作是一项繁琐而且容易出错的任务,于是就有了 Netty。
# 6.6.3 Netty
Netty 对 NIO 的 API 进行了封装,通过以下手段让性能又得到了一定程度的提升:
- 使用多路复用技术,提高处理连接的并发性。
- 零拷贝。
- Netty 的接收和发送数据采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝。
- Netty 提供了组合 Buffer 对象,可以聚合多个 ByteBuffer 对象进行一次操作。
- Netty 的文件传输采用了 transferTo 方法,它可以直接将文件缓冲区的数据发送到目标 Channel,避免了传统通过循环 write 方式导致的内存拷贝问题。
- 内存池:为了减少堆外直接内存的分配和回收产生的资源损耗问题,Netty 提供了基于内存池的缓冲区重用机制。
- 使用主从 Reactor 多线程模型,提高并发性。
- 采用了串行无锁化设计,在 IO 线程内部进行串行操作,避免多线程竞争导致的性能下降。
- 默认使用 Protobuf 的序列化框架。
- 灵活的 TCP 参数配置。
参考1:https://blog.csdn.net/eric_sunah/article/details/80424344
参考2:https://www.infoq.cn/article/netty-high-performance/#anch111813
# 6.7 WebFlux 核心控制器
SpringWebflux 核心控制器是 DispatchHandler,该控制器负责请求的处理。它实现了接口 WebHandler,WebHandler 中只有一个方法 handler(),它在 DispatchHandler 中的实现如下::
public Mono<Void> handle(ServerWebExchange exchange) { //ServerWebExchange:存放http请求响应的信息
//如果 handlerMappings 为空,则返回一个 createNotFoundError 的错误
return this.handlerMappings == null ? this.createNotFoundError() :
//如果不为空,则根据请求地址获取对应的 mapping
Flux.fromIterable(this.handlerMappings).concatMap((mapping) -> {
return mapping.getHandler(exchange);
}).next().switchIfEmpty(this.createNotFoundError()).flatMap((handler) -> {
//调用具体的业务方法
return this.invokeHandler(exchange, handler);
}).flatMap((result) -> {
//返回处理结果
return this.handleResult(exchange, result);
});
}
DispatchHandler 有 3 个属性:
public class DispatcherHandler implements WebHandler, ApplicationContextAware {
@Nullable
private List<HandlerMapping> handlerMappings;
@Nullable
private List<HandlerAdapter> handlerAdapters;
@Nullable
private List<HandlerResultHandler> resultHandlers;
- HandlerMapping:请求查询到处理的方法
- HandlerAdapter:真正负责请求处理
- HandlerResultHandler:响应结果处理
# 6.8 WebFlux 实现函数式的两个重要接口
- RouterFunction:路由处理
- HandlerFunction:处理函数
# 6.9 WebFlux 基于注解编程模型
# 6.9.1 创建 SpringBoot 项目,引入 WebFlux 依赖
将:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
换成:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
# 6.9.2 配置启动端口号
server.port=8081
# 6.9.3 创建包和相关类
实体类 User
public class User { private String name; private String gender; private Integer age; //getter、setter //有参、无参构造 //toString() }
创建接口 UserService,定义操作方法
public interface UserService { //根据id查询用户 Mono<User> getUserById(Integer id); //查询所有用户 Flux<User> getAllUsers(); //添加用户 Mono<Void> saveUserInfo(Mono<User> userMono); }
创建接口实现类 UserServiceImpl,实现具体操作:
@Service public class UserServiceImpl implements UserService { //创建 map 集合存储数据 private final Map<Integer, User> users = new HashMap<>(); //初始化 public UserServiceImpl() { this.users.put(1, new User("lucy", "男", 20)); this.users.put(2, new User("mary", "女", 30)); this.users.put(3, new User("jack", "女", 50)); } //根据ID查询User @Override public Mono<User> getUserById(Integer id) { return Mono.justOrEmpty(this.users.get(id)); } //查询所有User @Override public Flux<User> getAllUsers() { return Flux.fromIterable(this.users.values()); } //保存 User @Override public Mono<Void> saveUserInfo(Mono<User> userMono) { return userMono.doOnNext(user -> { //向 map 中放值 int id = users.size()+1; users.put(id,user); }).thenEmpty(Mono.empty()); //操作后将 Mono 中的值清空(即发送终止信号) } }
# 6.9.4 创建控制器
@RestController
public class UserController {
//注入 userService
@Autowired
private UserService userService;
//id 查询
@GetMapping("/user/{id}")
public Mono<User> getUserById(@PathVariable int id) {
return userService.getUserById(id);
}
//查询所有
@GetMapping("/user")
public Flux<User> getUsers() {
return userService.getAllUsers();
}
//添加
@PostMapping("/saveUser")
public Mono<Void> saveUser(@RequestBody User user) {
Mono<User> userMono = Mono.just(user);
return userService.saveUserInfo(userMono);
}
}
# 6.9.5 测试
# 6.9.6 比较 SpringMVC
- SpringMVC 方式实现,同步阻塞的方式,基于 SpringMVC+Servlet+Tomcat。
- SpringWebFlux 方式实现,异步非阻塞方式,基于 SpringWebFlux+Reactor+Netty。
# 6.10 WebFlux 基于函数式编程模型
# 6.10.1 注意点
- 在使用函数式编程模型操作时候,需要自己初始化服务器。
- 基于函数式编程模型时候,有两个核心接口:RouterFunction(实现路由功能,请求转发给对应的 handler)和 HandlerFunction(处理请求生成响应的函数)。核心任务是定义两个函数式接口的实现并且启动需要的服务器。
- SpringWebFlux 请求和响应不再是 ServletRequest 和 ServletResponse,而是 ServerRequest 和 ServerResponse。
# 6.10.2 复制基于注解编程模型的工程,保留实体类和服务层
# 6.10.3 创建 Handler(编写业务具体实现方法)
public class UserHandler {
private final UserService userService;
public UserHandler(UserService userService) {
this.userService = userService;
}
//根据 id 查询
public Mono<ServerResponse> getUserById(ServerRequest serverRequest){
//1. 获取 id 值
Integer id = Integer.valueOf(serverRequest.pathVariable("id"));
//2. 空值处理
Mono<ServerResponse> notFound = ServerResponse.notFound().build();
//3. 调用 service 方法得到数据
Mono<User> userMono = this.userService.getUserById(id);
//4. 把 userMono 进行转换返回 => 使用 Reactor 操作符 flatMap
return userMono.flatMap(user ->
ServerResponse
.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(fromObject(user))
.switchIfEmpty(notFound));
}
//查询所有
public Mono<ServerResponse> getAllUsers(ServerRequest serverRequest){
//调用 service 得到结果
Flux<User> users = this.userService.getAllUsers();
return ServerResponse
.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(users,User.class);
}
//添加
public Mono<ServerResponse> addUser(ServerRequest serverRequest){
//得到 user 对象
Mono<User> userMono = serverRequest.bodyToMono(User.class);
//添加
return ServerResponse
.ok()
.build(this.userService.saveUserInfo(userMono));
}
}
# 6.10.4 初始化服务器,编写 Router
public class Server {
/**
* ① 创建 Router 路由
*/
public RouterFunction<ServerResponse> routingFunction() {
//1. 创建 hanler 对象
UserService userService = new UserServiceImpl();
UserHandler handler = new UserHandler(userService);
//2. 设置路由
return RouterFunctions.route(
/**
* 请求的地址
* 接收的参数
* 调用的方法
*/
GET("/user/{id}")
.and(accept(APPLICATION_JSON)), handler::getUserById)
.andRoute(
GET("/users")
.and(accept(APPLICATION_JSON)), handler::getAllUsers
);
}
/**
* ② 创建服务器完成适配
*/
public void createReactorServer() {
//1. 路由和 handler 适配
RouterFunction<ServerResponse> route = routingFunction();
HttpHandler httpHandler = toHttpHandler(route);
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler);
//2. 创建服务器
HttpServer httpServer = HttpServer.create();
httpServer.handle(adapter).bindNow();
}
/**
* ③ 最终调用
*/
public static void main(String[] args) throws Exception {
Server server = new Server();
server.createReactorServer(); //创建服务器
System.out.println("enter to exit"); //按回车键退出
System.in.read();
}
}
# 6.10.5 运行 main() 方法进行测试
- 访问:http://localhost:49733/users
# 6.10.6 使用 WebClient 调用上面的接口
public class Client {
public static void main(String[] args) {
//调用服务器地址
WebClient client = WebClient.create("http://localhost:49733");
//1. 根据 id 查询
String id = "1";
User userResult = client.get().uri("/user/{id}", id)
.accept(MediaType.APPLICATION_JSON)
.retrieve() //检索(储存的信息)
.bodyToMono(User.class) //Mono返回1个或0个元素
.block(); //终止信号
//输出查询到的User
System.out.println(userResult);
//2. 查询所有
Flux<User> userFlux = client.get().uri("/users")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToFlux(User.class); //Flux返回多个元素
//输出
userFlux.map(user -> user.getName()) //map即前面说的把一个元素映射为另外一个元素
.buffer()
.doOnNext(System.out::println)
.blockFirst(); //终止信号
}
}
运行: