七、Spring5新功能
整个 Spring5 框架的代码基于 Java8,运行时兼容 JDK9,许多不建议使用的类和方法已从代码库中删除
7.1 整合日志框架
Spring 5.0 框架自带了通用的日志封装
- Spring5 已经移除 Log4jConfigListener,官方建议使用 Log4j2
7.1.1 Spring5 框架整合 Log4j2
1、导入jar包
<!--maven依赖-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.17.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.17.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.17.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.0-alpha6</version>
</dependency>
<!--下面这个依赖在slf4j的1.8版本后需要加上,否则会报no SLF4J provider were found,具体原因可查看报错时给出的链接进行查看-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>2.0.0-alpha6</version>
</dependency>
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="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 才会生效-->
<!--root:用于指定项目的根日志,如果没有单独指定 Logger,则会使用 root 作为默认的日志输出-->
<loggers>
<root level="info">
<appender-ref ref="Console"/>
</root>
</loggers>
</configuration>
3、测试代码(手动打印日志)
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class UserLog {
private static final Logger logger = LogManager.getLogger(UserLog.class);
public static void main(String[] args) {
logger.info("hello log4j2 info...");
logger.warn("hello log4j2 warn...");
}
}
在测试代码时因看视频中的Logger和LogFactory是从org.slf4j包中导入的,所以我在测试时也是导入的这个包,但是当我运行时发现没有任何日志信息打印出来,此时我就去搜查了一下,原来视频中的那种写法是log4j的写法,其实用于log4j2是错误的,我也不清楚为什么视频中就能测试出来,于是我看了下面这篇博客后导入正确的包才成功解决了这个问题https://blog.csdn.net/zqg4919/article/details/78321580
7.2 支持@Nullable注解
@Nullable
注解可以使用在方法上面,属性上面,参数上面,表示方法返回可以为空,属性值可以为空,参数值可以为空
public class NullableTest {
//注解使用在属性上面,属性值可以为空
@Nullable
private String name;
//注解用在方法上面,方法返回值可以为空
@Nullable
public void testNullable(@Nullable String parameter){//注解使用在方法参数里面,方法参数可以为空
}
}
7.3 函数式风格
Spring5 核心容器支持函数式风格 GenericApplicationContext
我们知道,在Spring框架中,对象的创建都有Spring容器自己完成,但是当我们想使用new
关键字自己创建对象时Spring容器是得不到这个对象的
举例:(为了方便实体类使用上述的NullableTest类)
NullableTest nullableTest = new NullableTest();
//当我们像上方那样创建好对象后,如果我们想通过getBean方法获取到这个对象或将它注入到其他地方,那么Spring容器是不能识别到这个对象从而进行这些行为的
//此时我们就需要用到Spring5的新功能
@Test
public void testGenericApplicationContext() {
//1 创建 GenericApplicationContext 对象
GenericApplicationContext context = new GenericApplicationContext();
//2 调用 context 的方法对象注册
context.refresh();
//registerBean方法中需要传入——创建的对象名,创建的对象类型,创建的对象——作为参数,其中对象名可以省略
//context.registerBean(NullableTest.class,() -> new NullableTest());//这种是省略对象名的写法
context.registerBean("nullableTest",NullableTest.class,() -> new NullableTest());//这种是不省略对象名的写法
//3 获取在 spring 注册的对象
//NullableTest nullableTest = (NullableTest) context.getBean("com.spring5.NullableTest");//省略对象名注册对象的方式
NullableTest nullableTest = (NullableTest) context.getBean("nullableTest");//不省略对象名注册对象的方式
//上述两种方式均能将我们自己创建的对象注册到Spring容器中
System.out.println(nullableTest);//com.spring5.NullableTest@78452606
}
7.4 整合JUnit5单元测试框架
7.4.1 JUnit4
1、引入Spring测试相关依赖或导入jar包
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.16</version>
</dependency>
2、编写测试类
//创建测试类,使用注解方式完成
@RunWith(SpringJUnit4ClassRunner.class) //单元测试框架
//@ContextConfiguration("classpath:bean.xml") //读取配置文件的的方式,这儿可以自己看看这个注解的具体源码就知道了
@ContextConfiguration(classes = {SpringConfig.class}) //读取配置类的方式
public class TestSpring5 {
@Autowired
private NullableTest nullableTest; //自动装载
@Test
public void testJUnit(){
System.out.println(nullableTest);//com.spring5.NullableTest@5aac4250
//当我们需要用到nullableTest对象时它会帮我们自动创建并装载,此时我们就不需要再通过ClassPathXmlApplicationContext类或AnnotationConfigApplicationContext类来获取对象了
}
}
从这里也就解决了我在前面提到的疑惑“为什么每次测试我们仍需要通过getBean方法来获取service对象”,原来是测试类没有获取到配置文件或配置类的配置并且测试类也没有没有注入到Spring容器中,就不能获取到配置从而就不能自动创建对象了
7.4.2 JUnit5
1、引入JUnit5相关依赖或导入jar包
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.16</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
2、编写测试类
//创建测试类,使用注解方式完成
//方式几乎和JUnit4一样,只不过需要注意导入的包必须要是junit-jupiter-api
@ExtendWith(SpringExtension.class)
//@ContextConfiguration("classpath:bean.xml")
@ContextConfiguration(classes = {SpringConfig.class})
public class TestJUnit5 {
@Autowired
private NullableTest nullableTest;
@Test
public void testJUnit5(){
System.out.println(nullableTest);//com.spring5.NullableTest@6ffab045
}
}
另外,在JUnit5中,测试类上方的两个注解可以用一个复合注解代替,即@SpringJUnitConfig(locations = "classpath:bean1.xml")
或@SpringJUnitConfig(classes = {SpringConfig.class})
7.5 Webflux
7.5.1 基本概念
Webflux是 Spring5 添加新的模块,用于 web 开发的,功能和 SpringMVC 类似的,Webflux 使用当前一种比较流行的响应式编程出现的框架。
目前使用的传统 web 框架,比如 SpringMVC,这些都是基于 Servlet 容器。
Webflux 是一种异步非阻塞的框架,异步非阻塞的框架在 Servlet3.1 以后才支持,核心是基于 Reactor 的相关 API 实现的。
什么是异步非阻塞?
- 异步和同步
- 非阻塞和阻塞
- 上面两种术语针对对象不一样
- 异步和同步针对调用者,调用者发送请求,如果等着对方回应之后才去做其他事情就是同步,如果发送请求之后不用等对方回应就去做其他事情就是异步(调用者需要等到一个请求被响应后才能发出下一个请求就是同步,调用者发出一个请求后还可以发送其他请求就是异步)
- 阻塞和非阻塞针对被调用者,被调用者受到请求之后,做完请求任务之后才给出反馈就是阻塞,受到请求之后马上给出反馈然后再去做事情就是非阻塞(被调用者处理完请求后再响应是阻塞,被调用者响应后再处理请求是非阻塞)
Webflux的特点:
- 非阻塞式:在有限资源下,提高系统吞吐量和伸缩性,以 Reactor 为基础实现响应式编程
- 函数式编程:Spring5 框架基于 java8,Webflux 使用 Java8 函数式编程方式实现路由请求
Spring WebFlux和Spring MVC的异同:
- 两个框架都可以使用注解方式,都运行在 Tomcat 等容器中
- SpringMVC 采用命令式编程,Webflux 采用异步响应式编程
7.5.2 响应式编程
什么是响应式编程?
响应式编程是一种面向数据流和变化传播的编程范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。比如,Excel的电子表格就是响应式编程的一个例子,单元格可以包含字面值或类似"=B1+C1"的公式,而包含公式的单元格的值会依据其他单元格的值的变化而变化。
1.Java实现响应式编程
在JDK8 及之前的版本中提供了观察者模式的两个类 Observer 和 Observable
其中,观察者类需要实现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((o, arg) -> {
System.out.println("收到观察者通知");
});
observerDemo.addObserver((o, arg) -> {
System.out.println("收到观察者通知1");
});
//若没有下方使数据改变并且通知观察者的代码,则不会输出任何语句
observerDemo.setChanged();//使数据发生改变
observerDemo.notifyObservers();//通知观察者
//收到被观察者通知1
//收到被观察者通知
//数据发生了变化
//从输出顺序可以看出,观察者被通知的顺序是就近原则,可能是这三个观察者对象被存放到了类似栈的数据结构中(个人认为)
}
}
在JDK9中,Observer和Observable就被Flow类替代了,在Flow类中,通过内部接口Publisher中的subscribe方法实现观察者模式
public class ObserverDemo extends Observable {
public static void main(String[] args) {
//subscriber——订阅者/观察者,Publisher——发布者/被观察者
//在这里面subscriber应该是充当观察者,当观察到有数据变化publisher则做出相应处理方式
Flow.Publisher<String> publisher = subscriber -> {
subscriber.onNext("1");
subscriber.onNext("2");
subscriber.onError(new RuntimeException("出错"));
}
}
}
想要弄懂这里需要了解什么是观察者模式?
观察者模式其实也是发布/订阅模式,这是一种对象间一对多(一是被观察者,多是观察者)的依赖关系,每当一个对象状态发生改变时,与其依赖的对象都会得到通知并自动更新。
2.Reactor实现响应式编程
Reactor官网文档:https://projectreactor.io/docs/core/release/reference/
响应式编程操作中,需要满足 Reactive 规范,而Reactor就是满足 Reactive 规范的一种框架
Reactor 有两个核心类,Mono
和 Flux
,这两个类实现接口 Publisher,提供丰富的操作符。Flux 对象实现发布者,返回 N 个元素;而Mono 实现发布者,返回 0 或者 1 个元素
Flux 和 Mono 都是数据流的发布者,使用 Flux 和 Mono 都可以发出三种数据信号:元素值,错误信号,完成信号(错误信号和完成信号都代表终止信号,终止信号用于告诉订阅者数据流结束了,错误信号终止数据流同时把错误信息传递给订阅者)
代码演示
<!--导入maven依赖-->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.4.14</version>
</dependency>
//测试代码
public class ReactorTest {
public static void main(String[] args) {
//just 方法直接声明数据流
Flux.just(1,2,3,4);
Mono.just(1);
//其他的方法
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); //流形式
}
}
//调用 just 或者其他方法只是声明数据流,数据流并没有发出,只有进行订阅之后才会触发数据流,不订阅什么都不会发生的
//只有像这样调用subscribe方法订阅后才会触发数据流
Flux.just(1,2,3,4).subscribe(System.out::println);
Mono.just(1).subscribe(System.out::println);
三种信号特点
- 错误信号和完成信号都是终止信号,不能共存的
- 如果没有发送任何元素值,而是直接发送错误或者完成信号,表示是空数据流
- 如果没有错误信号,没有完成信号,表示是无限数据流
3.操作符
操作符是对数据流进行一道道操作,最终得到我们想要的数据流,比如工厂流水线,其中对产品进行加工的一道道工序就是相当于是操作符
常见的操作符:
-
map :将元素映射为新元素
-
flatMap:将元素映射为流,把每个元素转换流,把转换之后的多个流合并大的流
7.5.3 执行流程和核心API
SpringWebflux 基于 Reactor,默认使用容器是 Netty,Netty 是高性能的 NIO 框架,异步非阻塞的框架
1.NIO框架
2.SpringWebflux 执行过程和 SpringMVC 相似
SpringWebflux 核心控制器 DispatcherHandler
类,该类实现接口 WebHandler
,实现了接口 WebHandler 中的一个方法 handle
该方法在DispatcherHandler类中的具体实现:(需要创建SpringBoot项目导入spring-boot-starter-webflux
依赖才能查看)
WebHandler接口不同实现类的不同功能:
3.SpringWebflux 里面 DispatcherHandler,负责请求的处理
- HandlerMapping:请求查询到处理的方法
- HandlerAdapter:真正负责请求处理
- HandlerResultHandler:响应结果处理
4.SpringWebflux 实现函数式编程,两个接口:RouterFunction(路由处理)和 HandlerFunction(处理函数)
7.5.4 注解编程模型
SpringWebflux 实现方式有两种:注解编程模型和函数式编程模型
使用注解编程模型的方式,和 SpringMVC 使用相似,只需要把相关依赖配置到项目中,SpringBoot 自动配置相关运行容器,默认情况下使用 Netty 服务器
首先,通过代码实现注解编程模型
1.创建一个SpringBoot项目
在新建项目中选择新建Spring Initializr项目
完成项目创建
2.导入依赖
在pom文件中将spring-boot-starter-web
依赖更改为spring-boot-starter-webflux
3.在配置文件中配置服务器端口号
4.创建好相关包和类
//实体类
public class User {
private String name;
private String gender;
private Integer age;
public User(String name, String gender, Integer age) {
this.name = name;
this.gender = gender;
this.age = age;
}
//省略setter,getter和toString方法
}
//service
@Service
public class UserServiceImpl implements UserService {
//由于为了方便不连接数据库,所以这里设置一个Map来存放数据
private final Map<Integer,User> users = new HashMap<>();
//在构造函数中对数据进行初始化
public UserServiceImpl(){
this.users.put(1,new User("Eric","男",23));
this.users.put(2,new User("Jack","男",26));
this.users.put(3,new User("Lucy","女",20));
}
//根据id查询用户
@Override
public Mono<User> getUserById(Integer id) {
return Mono.justOrEmpty(this.users.get(id)); //justOrEmpty方法:创建一个新的 Mono ,如果非 null 则发出指定的项目,否则只发出 完成
}
//查询所有用户
@Override
public Flux<User> getAllUser() {
return Flux.fromIterable(this.users.values());
//fromXxx方法
//fromArray(从数组)、fromIterable(从迭代器)、fromStream(从 Java Stream 流) 的方式来创建 Flux
}
//添加新用户
@Override
public Mono<Void> addUser(Mono<User> user) {
return user.doOnNext(person -> { //doOnNext方法:添加行为时触发 Mono 成功发出数据
//向集合添加新数据
int id = users.size() + 1;
users.put(id,person);
}).thenEmpty(Mono.empty());//生成一个空的有限流,这儿用来终止数据流
}
}
//controller
@RestController
public class UserController {
@Autowired
private UserService userService;
//id 查询
@GetMapping("/user/{id}")
public Mono<User> getUserById(@PathVariable int id) {
return userService.getUserById(id);
}
//查询所有
@GetMapping("/users")
public Flux<User> getUsers() {
return userService.getAllUser();
}
//添加
@PostMapping("/addUser")
public Mono<Void> saveUser(@RequestBody User user) {
Mono<User> userMono = Mono.just(user);//just方法的作用:将数据作为流发出并结束这段数据流
return userService.addUser(userMono);
}
}
//若以上方法不清楚功能,可以返回到7.5.2节的第二部分中点击链接查看Reactor官网文档
5.启动项目
启动项目可以在控制台发现SpringBoot项目使用的是Netty容器,并且使用的端口是配置的8081端口
然后在浏览器访问数据,数据在浏览器中成功显示则代表成功
注解编程模型实现的说明:
- SpringMVC 方式实现——是同步阻塞的方式,基于
SpringMVC+Servlet+Tomcat
- SpringWebflux 方式实现——异步非阻塞方式,基于
SpringWebflux+Reactor+Netty
7.5.5 函数式编程模型
在使用函数式编程模型操作时候,需要自己初始化服务器
基于函数式编程模型时候,有两个核心接口:RouterFunction
(实现路由功能,请求转发给对应的 handler)和 HandlerFunction
(处理请求,生成响应的函数)。核心任务就是定义这两个函数式接口的实现类并且启动需要的服务器
SpringWebflux 请求和响应不再是 ServletRequest 和 ServletResponse ,而是ServerRequest
和 ServerResponse
函数式编程各个部分之间的联系:
1.构建项目
在注解编程模型的基础上删除controller层,保留service和entity
2.创建Handler
public class UserHandler {
private final UserService userService;
public UserHandler(UserService userService) {
this.userService = userService;
}
//根据 id 查询
public Mono<ServerResponse> getUserById(ServerRequest request) {
//获取 id 值
int userId = Integer.parseInt(request.pathVariable("id"));
//空值处理
Mono<ServerResponse> notFound = ServerResponse.notFound().build();//没有找到对应数据就创建一个空数据
//调用 service 方法得到数据
Mono<User> userMono = this.userService.getUserById(userId);
//把 userMono 进行转换返回
//使用 Reactor 操作符 flatMap
return userMono.flatMap(person -> ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(fromObject(person))).switchIfEmpty(notFound);
//如果userMono不为空,服务响应成功,并将userMono转换成json数据格式并流化处理,将其作为响应返回;如果userMono为空,则返回刚刚创建的空数据
//从方法名可以看出:
//contentType方法是设置数据的格式类型,body方法是设置响应体的数据,switchIfEmpty方法是判断我们要传的数据是否为空
}
//查询所有
public Mono<ServerResponse> getAllUsers() {
//调用 service 得到结果
Flux<User> users = this.userService.getAllUser();
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(users,User.class);
}
//添加
public Mono<ServerResponse> saveUser(ServerRequest request) {
//得到 user 对象
Mono<User> userMono = request.bodyToMono(User.class);
return ServerResponse.ok().build(this.userService.addUser(userMono));
}
}
3.初始化服务器,编写router
public class Server {
//1 创建 Router 路由
public RouterFunction<ServerResponse> routingFunction() {
//创建 hanler 对象
UserService userService = new UserServiceImpl();
UserHandler handler = new UserHandler(userService);
//设置路由
return RouterFunctions.route(GET("/users/{id}").and(accept(APPLICATION_JSON)),handler::getUserById)
.andRoute(GET("/users").and(accept(APPLICATION_JSON)),handler::getAllUsers);
//设置 请求的方式、接收数据类型、处理请求的具体方法
//被路由的方法中都需要有ServerRequest参数,否则这里会无法解析
}
//2 创建服务器完成适配
public void createReactorServer() {
//路由和 handler 适配
RouterFunction<ServerResponse> route = routingFunction();//创建路由对象
HttpHandler httpHandler = toHttpHandler(route);
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler);
//创建服务器
HttpServer httpServer = HttpServer.create();
httpServer.handle(adapter).bindNow();
}
}
4.调用
public static void main(String[] args) throws Exception{
Server server = new Server();
server.createReactorServer();
System.out.println("enter to exit");
System.in.read();
}
运行main方法,在控制台可以看到开启的服务器及端口
在浏览器中访问路径,得到数据
在控制台还可以看见请求响应的具体信息:
5.使用WebClient调用
public class Client {
public static void main(String[] args) {
//调用服务器地址
WebClient webClient = WebClient.create("http://127.0.0.1:55642");
//根据 id 查询
String id = "1";
User user = webClient.get().uri("/users/{id}", id).accept(MediaType.APPLICATION_JSON).retrieve().bodyToMono(User.class).block();
System.out.println(user);//User{name='Eric', gender='男', age=23}
//查询所有
Flux<User> results = webClient.get().uri("/users").accept(MediaType.APPLICATION_JSON).retrieve().bodyToFlux(User.class);
results.map(stu -> stu.getName()).buffer().doOnNext(System.out::println).blockFirst();//[Eric, Jack, Lucy]
}
}