【小家Spring】高性能关键技术之---体验Spring MVC的异步模式(ResponseBodyEmitter、SseEmitter、StreamingResponseBody) 高级使用篇

简介: 【小家Spring】高性能关键技术之---体验Spring MVC的异步模式(ResponseBodyEmitter、SseEmitter、StreamingResponseBody) 高级使用篇

前言


上篇博文:【小家Spring】高性能关键技术之—体验Spring MVC的异步模式(Callable、WebAsyncTask、DeferredResult) 基础使用篇

介绍了Spring MVC异步模式的基本使用,相信小伙伴们基本的使用都能运用自如了。


那么本篇文章主要介绍一下异步模式的高级使用(ResponseBodyEmitter、SseEmitter、StreamingResponseBody)

DeferredResult高级使用


上篇博文介绍的它的基本使用,那么本文主要结合一些特殊的使用场景,来介绍下它的高级使用,让能更深刻的理解DeferredResult的强大之处。


它的优点也是非常明显的,能够实现两个完全不相干的线程间的通信。处理的时候请注意图中标记的线程安全问题~~~

image.png


实现长轮询服务端推送消息(long polling)


简单科普双向通信的方式


在WebSocket协议之前(它是2011年发布的),有三种实现双向通信的方式:轮询(polling)、长轮询(long-polling)和iframe流(streaming)。


  • 轮询(polling):这个不解释了。优点是实现简单粗暴,后台处理简单。缺点也是大大的,耗流量、耗CPU。。。
  • 长轮询(long-polling):长轮询是对轮询的改进版。客户端发送HTTP给服务器之后,看有没有新消息,如果没有新消息,就一直等待(而不是一直去请求了)。当有新消息的时候,才会返回给客户端。 优点是对轮询做了优化,时效性也较好。缺点是:保持连接会消耗资源; 服务器没有返回有效数据,程序超时~~~
  • iframe流(streaming):是在页面中插入一个隐藏的iframe,利用其src属性在服务器和客户端之间创建一条长连接,服务器向iframe传输数据(通常是HTML,内有负责插入信息的javascript),来实时更新页面。(个人觉得还不如长轮询呢。。。)
  • WebSocket:WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。它将TCP的Socket(套接字)应用在了webpage上。 它的有点一大把:支持双向通信,实时性更强;可发送二进制文件;非常节省流量。 但也是有缺点的:浏览器支持程度不一致,不支持断开重连 (其实是最推荐的~~~)


之前看apollo配置中心的实现原理,apollo的发布配置推送变更消息就是用DeferredResult实现的。它的大概实现步骤如下:


  1. apollo客户端会像服务端发送长轮询http请求,超时时间60秒
  2. 当超时后返回客户端一个304 httpstatus,表明配置没有变更,客户端继续这个步骤重复发起请求
  3. 当有发布配置的时候,服务端会调用DeferredResult.setResult返回200状态码。客户端收到响应结果后,会发起请求获取变更后的配置信息(注意这里是另外一个请求哦~)。


为了演示,简单的按照此方式,写一个Demo:

@Configuration
@EnableWebMvc
public class AppConfig implements WebMvcConfigurer {
    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        // 超时时间设置为60s
        configurer.setDefaultTimeout(TimeUnit.SECONDS.toMillis(60));
    }
}


服务端简单代码模拟如下:


@Slf4j
@RestController
public class ApolloController {
    // 值为List,因为监视同一个名称空间的长轮询可能有N个(毕竟可能有多个客户端用同一份配置嘛)
    private Map<String, List<DeferredResult<String>>> watchRequests = new ConcurrentHashMap<>();
    @GetMapping(value = "/all/watchrequests")
    public Object getWatchRequests() {
        return watchRequests;
    }
    // 模拟长轮询:apollo客户端来监听配置文件的变更~  可以指定namespace 监视指定的NameSpace
    @GetMapping(value = "/watch/{namespace}")
    public DeferredResult<String> watch(@PathVariable("namespace") String namespace) {
        log.info("Request received,namespace is" + namespace + ",当前时间:" + System.currentTimeMillis());
        DeferredResult<String> deferredResult = new DeferredResult<>();
        //当deferredResult完成时(不论是超时还是异常还是正常完成),都应该移除watchRequests中相应的watch key
        deferredResult.onCompletion(() -> {
            log.info("onCompletion,移除对namespace:" + namespace + "的监视~");
            List<DeferredResult<String>> list = watchRequests.get(namespace);
            list.remove(deferredResult);
            if (list.isEmpty()) {
                watchRequests.remove(namespace);
            }
        });
        List<DeferredResult<String>> list = watchRequests.computeIfAbsent(namespace, (k) -> new ArrayList<>());
        list.add(deferredResult);
        return deferredResult;
    }
    //模拟发布namespace配置:修改配置
    @GetMapping(value = "/publish/{namespace}")
    public void publishConfig(@PathVariable("namespace") String namespace) {
        //do Something for update config
        if (watchRequests.containsKey(namespace)) {
            List<DeferredResult<String>> deferredResults = watchRequests.get(namespace);
            //通知所有watch这个namespace变更的长轮训配置变更结果
            for (DeferredResult<String> deferredResult : deferredResults) {
                deferredResult.setResult(namespace + " changed,时间为" + System.currentTimeMillis());
            }
        }
    }
}


apollo处理超时时候会抛出一个异常AsyncRequestTimeoutException,因此我们全局处理一下就成:


@Slf4j
@ControllerAdvice
class GlobalControllerExceptionHandler {
    @ResponseStatus(HttpStatus.NOT_MODIFIED)//返回304状态码  效果同HttpServletResponse#sendError(int) 但这样更优雅
    @ResponseBody
    @ExceptionHandler(AsyncRequestTimeoutException.class) //捕获特定异常
    public void handleAsyncRequestTimeoutException(AsyncRequestTimeoutException e) {
        System.out.println("handleAsyncRequestTimeoutException");
    }
}


用Ajax模拟Client端的伪代码如下:


    //长轮询:一直去监听指定namespace的配置文件
        function watchConfig(){
          $.ajax({
            url:"http://localhost:8080/demo_war/watch/classroomconfig",
            method:"get",
            success:function(response,status){
          if(status == 304){
            watchConfig(); //超时,没有更改,那就继续去监听
          }else if(status == 200){
            getNewConfig(); //监听到更改后,立马去获取最新的配置文件内容回来做事
            ...
            watchConfig(); // 昨晚事后又去监听着
          }
            }
          });
        }
    // 调用去监听获取配置文件的函数
        watchConfig();


这样子我们就基本模拟了一个长轮询的案例~


长轮询的应用场景也是很多的,比如我们现在要实现这样一个功能:浏览器要实时展示服务端计算出来的数据。(这个用普通轮询就会有延迟且浪费资源,但是用这种类似长连接的方案就很合适)


ResponseBodyEmitter和SseEmitter


Callback和DeferredResult用于设置单个结果,如果有多个结果需要set返回给客户端时,可以使用SseEmitter以及ResponseBodyEmitter,each object is written with a compatible HttpMessageConverter。返回值可以直接写他们本身,也可以放在ResponseEntity里面


它俩都是Spring4.2之后提供的类。由ResponseBodyEmitterReturnValueHandler负责处理。 这个和Spring5提供的webFlux技术已经很像了,后续讲到的时候还会提到他们~~~~

Emitter:发射器


它们的使用方式几乎同:DeferredResult,这里我只把官方的例子拿出来你就懂了

image.png

SseEmitter是ResponseBodyEmitter的子类,它提供Server-Sent Events(Sse).服务器事件发送是”HTTP Streaming”的另一个变种技术.只是从服务器发送的事件按照W3C Server-Sent Events规范来的(推荐使用) 它的使用方式上,完全同上


Server-Sent Events这个规范能够来用于它们的预期使用目的:就是从server发送events到clients(服务器推).在Spring MVC中可以很容易的实现.仅仅需要返回一个SseEmitter类型的值.


向这种场景在在线游戏、在线协作、金融领域等等都有很好的应用。当然,如果你对稳定性什么的要求都非常高,官方也推荐最好是使用WebSocket来实现~


ResponseBodyEmitter允许通过HttpMessageConverter把发送的events写到对象到response中.这可能是最常见的情况。例如写JSON数据

可是有时候它被用来绕开message转换直接写入到response的OutputStream。例如文件下载.这样可以通过返回StreamingResponseBody类型的值做到.

StreamingResponseBody (很方便的文件下载)


它用于直接将结果写出到Response的OutputStream中; 如文件下载等

image.png


接口源码非常简单:


@FunctionalInterface
public interface StreamingResponseBody {
  void writeTo(OutputStream outputStream) throws IOException;
}


异步优化


Spring内部默认不使用线程池处理的(通过源码分析后面我们是能看到的),为了提高处理的效率,我们可以自己优化,建议自己在配置里注入一个线程池供给使用,参考如下:

  // 提供一个mvc里专用的线程池。。。  这是全局的方式~~~~
    @Bean
    public ThreadPoolTaskExecutor mvcTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setQueueCapacity(100);
        executor.setMaxPoolSize(25);
        return executor;
    }
// 最优解决方案不是像上面一样配置通用的,而是配置一个单独的专用的,如下~~~~
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
  // 配置异步支持~~~~
    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
      // 设置一个用于异步执行的执行器~~~AsyncTaskExecutor
        configurer.setTaskExecutor(mvcTaskExecutor());
        configurer.setDefaultTimeout(60000L);
    }
}

总结


总的来说,Spring MVC提供的便捷的异步支持,能够大大的提高Tomcat容器等的性能。同时也给我们的应用提供了更多的便利。这也为Spring5以后的Reactive编程模型提供了有利的支持和保障。

Spring是一个易学难精的技术,想要把各种技术融汇贯通,还有后续更扎实的深挖~

相关文章
|
15天前
|
存储 前端开发 测试技术
MVC、MVP、MVVM 模式
MVC、MVP 和 MVVM 是三种常见的软件架构模式,用于分离用户界面和业务逻辑。MVC(Model-View-Controller)通过模型、视图和控制器分离数据、界面和控制逻辑;MVP(Model-View-Presenter)将控制逻辑移到 Presenter 中,减少视图的负担;MVVM(Model-View-ViewModel)通过数据绑定机制进一步解耦视图和模型,提高代码的可维护性和测试性。
|
1月前
|
存储 Java API
简单两步,Spring Boot 写死的定时任务也能动态设置:技术干货分享
【10月更文挑战第4天】在Spring Boot开发中,定时任务通常通过@Scheduled注解来实现,这种方式简单直接,但存在一个显著的限制:任务的执行时间或频率在编译时就已经确定,无法在运行时动态调整。然而,在实际工作中,我们往往需要根据业务需求或外部条件的变化来动态调整定时任务的执行计划。本文将分享一个简单两步的解决方案,让你的Spring Boot应用中的定时任务也能动态设置,从而满足更灵活的业务需求。
68 4
|
1月前
|
前端开发 Java 应用服务中间件
【Spring】Spring MVC的项目准备和连接建立
【Spring】Spring MVC的项目准备和连接建立
52 2
|
2月前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
|
1月前
|
XML 前端开发 Java
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
本文阐述了Spring、Spring Boot和Spring MVC的关系与区别,指出Spring是一个轻量级、一站式、模块化的应用程序开发框架,Spring MVC是Spring的一个子框架,专注于Web应用和网络接口开发,而Spring Boot则是对Spring的封装,用于简化Spring应用的开发。
97 0
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
|
2月前
|
设计模式 开发框架 前端开发
MVC 模式在 C# 中的应用
MVC(Model-View-Controller)模式是广泛应用于Web应用程序开发的设计模式,将应用分为模型(存储数据及逻辑)、视图(展示数据给用户)和控制器(处理用户输入并控制模型与视图交互)三部分,有助于管理复杂应用并提高代码可读性和维护性。在C#中,ASP.NET MVC框架常用于构建基于MVC模式的Web应用,通过定义模型、控制器和视图,实现结构清晰且易维护的应用程序。
47 2
|
2月前
|
存储 缓存 Java
在Spring Boot中使用缓存的技术解析
通过利用Spring Boot中的缓存支持,开发者可以轻松地实现高效和可扩展的缓存策略,进而提升应用的性能和用户体验。Spring Boot的声明式缓存抽象和对多种缓存技术的支持,使得集成和使用缓存变得前所未有的简单。无论是在开发新应用还是优化现有应用,合理地使用缓存都是提高性能的有效手段。
37 1
|
2月前
|
Java Spring 容器
Spring使用异步注解@Async正确姿势
Spring使用异步注解@Async正确姿势,异步任务,spring boot
|
1月前
|
自然语言处理 JavaScript Java
Spring 实现 3 种异步流式接口,干掉接口超时烦恼
本文介绍了处理耗时接口的几种异步流式技术,包括 `ResponseBodyEmitter`、`SseEmitter` 和 `StreamingResponseBody`。这些工具可在执行耗时操作时不断向客户端响应处理结果,提升用户体验和系统性能。`ResponseBodyEmitter` 适用于动态生成内容场景,如文件上传进度;`SseEmitter` 用于实时消息推送,如状态更新;`StreamingResponseBody` 则适合大数据量传输,避免内存溢出。文中提供了具体示例和 GitHub 地址,帮助读者更好地理解和应用这些技术。
144 0
|
1月前
|
前端开发 Java
【案例+源码】详解MVC框架模式及其应用
【案例+源码】详解MVC框架模式及其应用
40 0
下一篇
无影云桌面