Spring WebClient实战示例

 更新时间:2022年01月12日 09:55:18   作者:程猿薇茑  
本文主要介绍了Spring WebClient实战示例,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

WebClient实战

本文代码地址https://github.com/bigbirditedu/webclient

Spring Webflux 是 Spring Framework 5.0 的新特性,是随着当下流行的 Reactive Programming 而诞生的高性能框架。传统的 Web 应用框架,比如我们所熟知的 Struts2,Spring MVC 等都是基于 Servlet API 和 Servlet 容器之上运行的,本质上都是阻塞式的。Servlet 直到 3.1 版本之后才对异步非阻塞进行了支持。而 WebFlux天生就是一个典型的异步非阻塞框架,其核心是基于 Reactor 相关 API 实现的。相比传统的 Web 框架,WebFlux 可以运行在例如 Netty、Undertow 以及 Servlet 3.1 容器之上,其运行环境比传统 Web 框架更具灵活性。

WebFlux 的主要优势有:

  • 非阻塞性:WebFlux 提供了一种比 Servlet 3.1 更完美的异步非阻塞解决方案。非阻塞的方式可以使用较少的线程以及硬件资源来处理更多的并发。
  • 函数式编程:函数式编程是 Java 8 重要的特性,WebFlux 完美支持。

webclient的HTTP API请参考:https://github.com/bigbirditedu/webclient

服务端性能对比

比较的是Spring MVC 与 Spring WebFlux 作为HTTP 应用框架谁的性能更好。

Spring WebFlux

先看看Spring WebFlux

引入pom依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

编写http接口

@RestController
@RequestMapping("/webflux")
public class WebFluxController {

    public static AtomicLong COUNT = new AtomicLong(0);

    @GetMapping("/hello/{latency}")
    public Mono<String> hello(@PathVariable long latency) {
        System.out.println("Start:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")));
        System.out.println("Page count:" + COUNT.incrementAndGet());
        Mono<String> res = Mono.just("welcome to Spring Webflux").delayElement(Duration.ofSeconds(latency));//阻塞latency秒,模拟处理耗时
        System.out.println("End:  " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")));
        return res;
    }
}

启动服务器

可以看到webflux 默认选择Netty作为服务器

在这里插入图片描述

使用JMeter进行压测:File->新建测试计划->添加用户线程组->在线程组上添加一个取样器,选择Http Request

配置Http请求,并在HTTP Request上添加监听器;这里不做复杂的压测分析,选择结果树和聚合报告即可

在这里插入图片描述

设置http请求超时时间

在这里插入图片描述

设置并发用户数,60秒内全部启起来;

不断调整进行测试;每次开始前先Clear All清理一下旧数据,再点save保存一下,再点Start开始

在这里插入图片描述

1000用户,99线大约24毫秒的延迟

在这里插入图片描述

2000用户,99线大约59毫秒的延迟

在这里插入图片描述

3000用户,99线大约89毫秒的延迟

在这里插入图片描述

4000用户

webflux到4000并发用户时还是很稳

在这里插入图片描述

Spring MVC

再来看看SpringMVC的性能

引入pom文件

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

编写http接口

@RestController
@RequestMapping("/springmvc")
public class SpringMvcController {

    public static AtomicLong COUNT = new AtomicLong(0);

    @GetMapping("/hello/{latency}")
    public String hello(@PathVariable long latency) {
        System.out.println("Start:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")));
        System.out.println("Page count:" + COUNT.incrementAndGet());
        try {
            //阻塞latency秒,模拟处理耗时
            TimeUnit.SECONDS.sleep(latency);
        } catch (InterruptedException e) {
            e.printStackTrace();
            return "Exception during thread sleep";
        }
        System.out.println("End:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")));
        return "welcome to Spring MVC";
    }
}

启动服务器。可以看到SpringMVC默认选择Tomcat作为服务器

在这里插入图片描述

设置请求路径

在这里插入图片描述

100用户

在这里插入图片描述

200用户

在这里插入图片描述

300用户

从300用户开始,响应时间就开始增加

在这里插入图片描述

400用户

在这里插入图片描述

500用户

在这里插入图片描述

550用户

本例中,传统Web技术(Tomcat+SpringMVC)在处理550用户并发时,就开始有超时失败的

在这里插入图片描述

600用户

在处理600用户并发时,失败率就已经很高;用户并发数更高时几乎都会处理不过来,接近100%的请求超时。

在这里插入图片描述

1000用户

在这里插入图片描述

2000用户

在这里插入图片描述

3000用户

在这里插入图片描述

4000用户

在这里插入图片描述

客户端性能比较

我们来比较一下HTTP客户端的性能。

先建一个单独的基于Springboot的Http Server工程提供标准的http接口供客户端调用。

/**
 * Http服务提供方接口;模拟一个基准的HTTP Server接口
 */
@RestController
public class HttpServerController {

    @RequestMapping("product")
    public Product getAllProduct(String type, HttpServletRequest request, HttpServletResponse response) throws InterruptedException {
        long start = System.currentTimeMillis();
        System.out.println("Start:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")));

        //输出请求头
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String head = headerNames.nextElement();
            System.out.println(head + ":" + request.getHeader(head));
        }

        System.out.println("cookies=" + request.getCookies());

        Product product = new Product(type + "A", "1", 56.67);
        Thread.sleep(1000);

        //设置响应头和cookie
        response.addHeader("X-appId", "android01");
        response.addCookie(new Cookie("sid", "1000101111"));
        System.out.println("End:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")));
        System.out.println("cost:" + (System.currentTimeMillis() - start) + product);

        return product;
    }

    @RequestMapping("products")
    public List<Product> getAllProducts(String type) throws InterruptedException {
        long start = System.currentTimeMillis();
        List<Product> products = new ArrayList<>();
        products.add(new Product(type + "A", "1", 56.67));
        products.add(new Product(type + "B", "2", 66.66));
        products.add(new Product(type + "C", "3", 88.88));
        Thread.sleep(1000);
        System.out.println("cost:" + (System.currentTimeMillis() - start) + products);
        return products;
    }

    @RequestMapping("product/{pid}")
    public Product getProductById(@PathVariable String pid, @RequestParam String name, @RequestParam double price) throws InterruptedException {
        long start = System.currentTimeMillis();
        Product product = new Product(name, pid, price);
        Thread.sleep(1000);
        System.out.println("cost:" + (System.currentTimeMillis() - start) + product);
        return product;
    }

    @RequestMapping("postProduct")
    public Product postProduct(@RequestParam String id, @RequestParam String name, @RequestParam double price) throws InterruptedException {
        long start = System.currentTimeMillis();
        Product product = new Product(name, id, price);
        Thread.sleep(1000);
        System.out.println("cost:" + (System.currentTimeMillis() - start) + product);
        return product;
    }

    @RequestMapping("postProduct2")
    public Product postProduct(@RequestBody Product product) throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread.sleep(1000);
        System.out.println("cost:" + (System.currentTimeMillis() - start) + product);
        return product;
    }

    @RequestMapping("uploadFile")
    public String uploadFile(MultipartFile file, int age) throws InterruptedException {
        long start = System.currentTimeMillis();
        System.out.println("age=" + age);
        String filePath = "";
        try {
            String filename = file.getOriginalFilename();
            //String extension = FilenameUtils.getExtension(file.getOriginalFilename());
            String dir = "D:\\files";
            filePath = dir + File.separator + filename;
            System.out.println(filePath);
            if (!Files.exists(Paths.get(dir))) {
                new File(dir).mkdirs();
            }
            file.transferTo(Paths.get(filePath));
        } catch (IOException e) {
            e.printStackTrace();
        }
        Thread.sleep(1000);
        System.out.println("cost:" + (System.currentTimeMillis() - start));
        return filePath;
    }
}

Tip

其它客户端代码请访问:https://github.com/bigbirditedu/webclient

webclient

和测试服务端时单独依赖不同的服务器相比,这次同时引入两个依赖。

   <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
   </dependency>

   <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
   </dependency>

引入starter-web是为了启动Tomcat服务器,测试时统一使用Tomcat服务器跑http客户端应用程序;

引入starter-webflux是为了单独使用webclient api,而不是为了使用Netty作为Http服务器;

500用户(超时时间设置6秒)

在这里插入图片描述

1000用户(超时时间设置6秒)

在这里插入图片描述

1100用户(超时时间设置6秒)

可以看到已经开始有响应超时的了

在这里插入图片描述

1200用户(超时时间设置10秒)

在这里插入图片描述

resttemplate(不带连接池)

500用户(超时时间设置6秒)

在这里插入图片描述

1000用户并发(超时时间设置6秒)

在这里插入图片描述

1100用户并发(超时时间设置6秒)

在这里插入图片描述

1200用户(超时时间设置10秒),有少量响应超时

在这里插入图片描述

resttemplate(带连接池)

        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.6</version>
        </dependency>

500用户(超时时间设置6秒)

在这里插入图片描述

1000用户(超时时间设置6秒)

在这里插入图片描述

1100用户(超时时间设置6秒)

和 不带连接池相比,错误率减少

在这里插入图片描述

1200用户(超时时间设置10秒),效果比不带连接池的resttemplate好点,但是响应耗时普遍还是比带连接池的webclient高

在这里插入图片描述

综合来看,是否使用http连接池对于单个接口影响有限,池的效果不明显;在多http地址、多接口路由时连接池的效果可能更好。

webclient连接池

默认情况下,WebClient使用连接池运行。池的默认设置是最大500个连接和最大1000个等待请求。如果超过此配置,就会抛异常。

reactor.netty.internal.shaded.reactor.pool.PoolAcquirePendingLimitException: Pending acquire queue has reached its maximum size of 1000

报错日志显示已经达到了默认的挂起队列长度限制1000,因此我们可以自定义线程池配置,以获得更高的性能。

关于Reactor Netty连接池请参考Netty官方和Spring官方的文档:

https://projectreactor.io/docs/netty/snapshot/reference/index.html#_connection_pool_2

https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-client-builder-reactor-resources

1000用户(超时时间设置6秒)

在这里插入图片描述

1100用户(超时时间设置6秒)

带连接池的效果好些,没有出现失败的

在这里插入图片描述

1200用户(超时时间设置10秒),响应延迟比默认配置的webclient好些

在这里插入图片描述

webclient阻塞方式获取结果;不自定义webclient线程池配置,2000用户(JMeter不配置超时时间)

在这里插入图片描述

webclient+CompletableFuture方式获取结果;不自定义webclient线程池配置,2000用户(JMeter不配置超时时间)

在这里插入图片描述

虽然测试效果几乎没有差别,但是我们要清楚地知道调用block方法是会引发实时阻塞的,会一定程度上增加对CPU的消耗;

实际开发中通常是为了使用异步特性才用webclient,如果用block方式就白瞎了webclient了,还不如直接用restTemplate。

2000用户性能比较

pooled webclient

在这里插入图片描述

rest

在这里插入图片描述

pooled rest

在这里插入图片描述

3000用户性能比较

pooled webclient

在这里插入图片描述

rest

在这里插入图片描述

pooled rest

在这里插入图片描述

webclient 的HTTP API

WebClient 作为一个 HTTP 客户端工具,其提供了标准 HTTP 请求方式,支持 Get、Post、Put、Delete、Head 等方法,可以作为替代 resttemplate 的一个强有力的工具。

API演示代码地址:https://github.com/bigbirditedu/webclient

小结

使用webClient在等待远程响应的同时不会阻塞本地正在执行的线程 ;本地线程处理完一个请求紧接着可以处理下一个,能够提高系统的吞吐量;而restTemplate 这种方式是阻塞的,会一直占用当前线程资源,直到http返回响应。如果等待的请求发生了堆积,应用程序将创建大量线程,直至耗尽线程池所有可用线程,甚至出现OOM。另外频繁的CPU上下文切换,也会导致性能下降。

但是作为上述两种方式的调用方(消费者)而言,其最终获得http响应结果的耗时并未减少。比如文章案例中,通过浏览器访问后端的的两个接口(SpringMVC、SpringWebFlux)时,返回数据的耗时相同。即最终获取(消费)数据的地方还会等待。

使用webclient替代restTemplate的好处是可以异步等待http响应,使得线程不需要阻塞;单位时间内有限资源下支持更高的并发量。但是建议webclient和webflux配合使用,使整个流程全异步化;如果单独使用webclient,笔者实测,和resttemplate差别不大!欢迎留言指教!

到此这篇关于Spring WebClient实战示例的文章就介绍到这了,更多相关Spring WebClient 内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • java获取中文拼音首字母的实例

    java获取中文拼音首字母的实例

    下面小编就为大家带来一篇java获取中文拼音首字母的实例。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-09-09
  • Spring框架中Bean的三种配置和实例化方法总结

    Spring框架中Bean的三种配置和实例化方法总结

    在Spring框架中,Bean的配置和实例化是很重要的基础内容,掌握各种配置方式,才能灵活管理Bean对象,本文将全面介绍Bean的别名配置、作用范围配置,以及构造器实例化、工厂实例化等方式
    2023-10-10
  • Java虚拟机运行时栈的栈帧

    Java虚拟机运行时栈的栈帧

    本节将会介绍一下Java虚拟机栈中的栈帧,会对栈帧的组成部分(局部变量表、操作数栈、动态链接、方法出口)分别进行介绍,最后还会通过javap命令反解析编译后的.class文件,进行分析方法执行时的局部变量表、操作数栈等
    2021-09-09
  • 如何使用NSSM将jar包打成Windows服务

    如何使用NSSM将jar包打成Windows服务

    这篇文章主要介绍了如何使用NSSM将jar包打成Windows服务,本文通过图文并茂的形式给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧
    2024-03-03
  • SpringBoot获取客户端的IP地址的实现示例

    SpringBoot获取客户端的IP地址的实现示例

    在Web应用程序中,获取客户端的IP地址是一项非常常见的需求,本文主要介绍了SpringBoot获取客户端的IP地址的实现示例,具有一定的参考价值,感兴趣的可以了解一下
    2023-09-09
  • spring boot与ktor整合的实现方法

    spring boot与ktor整合的实现方法

    这篇文章主要给大家介绍了关于spring boot与ktor整合的实现方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-09-09
  • Java中的this和super实例浅析

    Java中的this和super实例浅析

    要说this和super就不得不说Java的封装和继承了。这篇文章主要介绍了Java中的this和super实例浅析,需要的朋友可以参考下
    2017-03-03
  • Java 二叉树遍历的常用方法

    Java 二叉树遍历的常用方法

    二叉树的遍历可以说是解决二叉树问题的基础。我们常用的遍历方式无外乎就四种 前序遍历、中序遍历、后续遍历、层次遍历 这四种。
    2021-05-05
  • Mybatis(ParameterType)传递多个不同类型的参数方式

    Mybatis(ParameterType)传递多个不同类型的参数方式

    这篇文章主要介绍了Mybatis(ParameterType)传递多个不同类型的参数方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-04-04
  • Java实现图片对比功能

    Java实现图片对比功能

    个人从来没有研究过图像学,也没看过什么论文或者相关文档,写这个完全是靠google和百度,自己写了个实验了下,测试用例也少,估计有大BUG的存在,所以看的人权当学习交流,切勿生产使用。
    2014-09-09

最新评论