Spring5 新特性_Java学习


1. Spring 5.x 整合 Log4j2

  • Spring5 已经移除 Log4jConfigListener,官方建议使用 Log4j2。

1.1 导入 jar 包

Log4j2 官网下载地址

Spring5 新特性_Spring_02

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 级别的效果

Spring5 新特性_Java学习_03

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");
    }
}
Spring5 新特性_Java学习_04

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 包

Spring5 新特性_Spring_05

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 包就可以了
Spring5 新特性_JavaEE_06

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 比较

Spring5 新特性_Spring_07
  • 两个框架都可以使用注解方式,都运行在 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

MonoFlux,这两个类实现接口 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 元素映射为新元素
Spring5 新特性_JavaEE_08
6.5.4.2 flapMap 元素映射为流
  • 把每个元素转换流,把转换之后多个流合并大的流
Spring5 新特性_Java学习_09

6.6 WebFlux 核心容器 —— Netty

SpringWebflux 基于 Reactor,默认使用容器是 Netty,Netty 是高性能的 NIO 框架,NIO 即同步非阻塞。

6.6.1 BIO:阻塞IO

只能同时处理一个连接,要管理多个并发客户端,需要为每个新的客户端 Socket 创建一个新的 Thread,线程模型如下图所示:

Spring5 新特性_Spring_10

这样就会存在以下问题:

  • 在任何时候都可能有大量的线程处于休眠状态,只是等待输入或者输出数据就绪,这可能算是一种资源浪费。
  • 需要为每个线程的调用栈都分配内存。
  • 即使 Java 虚拟机(JVM) 在物理上可以支持非常大数量的线程, 但是远在到达该极限之前, 上下文切换所带来的开销就会带来麻烦。

6.6.2 NIO:非阻塞

Spring5 新特性_Java学习_11

从该图可以看出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参数配置。

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 测试

Spring5 新特性_java_12

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() 方法进行测试

Spring5 新特性_java_13

  • 访问:http://localhost:49733/users
Spring5 新特性_Java学习_14

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();  //终止信号

    }
}

运行:

Spring5 新特性_Spring_15