Spring Boot高效数据聚合之道深入讲解

背景

接口开发是后端开发中最常见的场景, 可能是RESTFul接口, 也可能是RPC接口. 接口开发往往是从各处捞出数据, 然后组装成结果, 特别是那些偏业务的接口.

例如, 我现在需要实现一个接口, 拉取用户基础信息+用户的博客列表+用户的粉丝数据的整合数据, 假设已经有如下三个接口可以使用, 分别用来获取 用户基础信息 ,用户博客列表, 用户的粉丝数据.

用户基础信息

@Service
public class UserServiceImpl implements UserService {
 @Override
 public User get(Long id) {
 try {Thread.sleep(1000L);} catch (InterruptedException e) {}
 /* mock a user*/
 User user = new User();
 user.setId(id);
 user.setEmail("lvyahui8@gmail.com");
 user.setUsername("lvyahui8");
 return user;
 }
}

用户博客列表

@Service
public class PostServiceImpl implements PostService {
 @Override
 public List<Post> getPosts(Long userId) {
 try { Thread.sleep(1000L); } catch (InterruptedException e) {}
 Post post = new Post();
 post.setTitle("spring data aggregate example");
 post.setContent("No active profile set, falling back to default profiles");
 return Collections.singletonList(post);
 }
}

用户的粉丝数据

@Service
public class FollowServiceImpl implements FollowService {
 @Override
 public List<User> getFollowers(Long userId) {
 try { Thread.sleep(1000L); } catch (InterruptedException e) {}
 int size = 10;
 List<User> users = new ArrayList<>(size);
 for(int i = 0 ; i < size; i++) {
  User user = new User();
  user.setUsername("name"+i);
  user.setEmail("email"+i+"@fox.com");
  user.setId((long) i);
  users.add(user);
 };
 return users;
 }
}

注意, 每一个方法都sleep了1s以模拟业务耗时.

我们需要再封装一个接口, 来拼装以上三个接口的数据.

PS: 这样的场景实际在工作中很常见, 而且往往我们需要拼凑的数据, 是要走网络请求调到第三方去的. 另外可能有人会想, 为何不分成3个请求? 实际为了客户端网络性能考虑, 往往会在一次网络请求中, 尽可能多的传输数据, 当然前提是这个数据不能太大, 否则传输的耗时会影响渲染. 许多APP的首页, 看着复杂, 实际也只有一个接口, 一次性拉下所有数据, 客户端开发也简单.

串行实现

编写性能优良的接口不仅是每一位后端程序员的技术追求, 也是业务的基本诉求. 一般情况下, 为了保证更好的性能, 往往需要编写更复杂的代码实现.

但凡人皆有惰性, 因此, 往往我们会像下面这样编写串行调用的代码

@Component
public class UserQueryFacade {
 @Autowired
 private FollowService followService;
 @Autowired
 private PostService postService;
 @Autowired
 private UserService userService;

 public User getUserData(Long userId) {
  User user = userService.get(userId);
  user.setPosts(postService.getPosts(userId));
  user.setFollowers(followService.getFollowers(userId));
  return user;
 }
}

很明显, 上面的代码, 效率低下, 起码要3s才能拿到结果, 且一旦用到某个接口的数据, 便需要注入相应的service, 复用麻烦.

并行实现

有追求的程序员可能立马会考虑到, 这几项数据之间并无强依赖性, 完全可以并行获取嘛, 通过异步线程+CountDownLatch+Future实现, 就像下面这样.

@Component
public class UserQueryFacade {
 @Autowired
 private FollowService followService;
 @Autowired
 private PostService postService;
 @Autowired
 private UserService userService;

 public User getUserDataByParallel(Long userId) throws InterruptedException, ExecutionException {
  ExecutorService executorService = Executors.newFixedThreadPool(3);
  CountDownLatch countDownLatch = new CountDownLatch(3);
  Future<User> userFuture = executorService.submit(() -> {
   try{
    return userService.get(userId);
   }finally {
    countDownLatch.countDown();
   }
  });
  Future<List<Post>> postsFuture = executorService.submit(() -> {
   try{
    return postService.getPosts(userId);
   }finally {
    countDownLatch.countDown();
   }
  });
  Future<List<User>> followersFuture = executorService.submit(() -> {
   try{
    return followService.getFollowers(userId);
   }finally {
    countDownLatch.countDown();
   }
  });
  countDownLatch.await();
  User user = userFuture.get();
  user.setFollowers(followersFuture.get());
  user.setPosts(postsFuture.get());
  return user;
 }
}

上面的代码, 将串行调用改为并行调用, 在有限并发级别下, 能极大提高性能. 但很明显, 它过于复杂, 如果每个接口都为了并行执行都写这样一段代码, 简直是噩梦.

优雅的注解实现

熟悉java的都知道, java有一种非常便利的特性 ~~ 注解. 简直是黑魔法. 往往只需要给类或者方法上添加一些注解, 便可以实现非常复杂的功能.

有了注解, 再结合Spring依赖自动注入的思想, 那么我们可不可以通过注解的方式, 自动注入依赖, 自动并行调用接口呢? 答案是肯定的.

首先, 我们先定义一个聚合接口

@Component
public class UserAggregate {
 @DataProvider(id="userFullData")
 public User userFullData(@DataConsumer(id = "user") User user,
        @DataConsumer(id = "posts") List<Post> posts,
        @DataConsumer(id = "followers") List<User> followers) {
  user.setFollowers(followers);
  user.setPosts(posts);
  return user;
 }
}

其中

  • @DataProvider 表示这个方法是一个数据提供者, 数据Id为 userFullData
  • @DataConsumer 表示这个方法的参数, 需要消费数据, 数据Id为 user ,posts, followers.

当然, 原来的3个原子服务 用户基础信息 ,用户博客列表, 用户的粉丝数据, 也分别需要添加一些注解

@Service
public class UserServiceImpl implements UserService {
 @DataProvider(id = "user")
 @Override
 public User get(@InvokeParameter("userId") Long id) {
@Service
public class PostServiceImpl implements PostService {
 @DataProvider(id = "posts")
 @Override
 public List<Post> getPosts(@InvokeParameter("userId") Long userId) {
@Service
public class FollowServiceImpl implements FollowService {
 @DataProvider(id = "followers")
 @Override
 public List<User> getFollowers(@InvokeParameter("userId") Long userId) {

其中

  • @DataProvider 与前面的含义相同, 表示这个方法是一个数据提供者
  • @InvokeParameter 表示方法执行时, 需要手动传入的参数

这里注意 @InvokeParameter 和 @DataConsumer的区别, 前者需要用户在最上层调用时手动传参; 而后者, 是由框架自动分析依赖, 并异步调用取得结果之后注入的.

最后, 仅仅只需要调用一个统一的门面(Facade)接口, 传递数据Id, Invoke Parameters,以及返回值类型. 剩下的并行处理, 依赖分析和注入, 完全由框架自动处理.

@Component
public class UserQueryFacade {
 @Autowired
 private DataBeanAggregateQueryFacade dataBeanAggregateQueryFacade;

 public User getUserFinal(Long userId) throws InterruptedException,
    IllegalAccessException, InvocationTargetException {
  return dataBeanAggregateQueryFacade.get("userFullData",
    Collections.singletonMap("userId", userId), User.class);
 }
}

如何用在你的项目中

上面的功能, 笔者已经封装为一个spring boot starter, 并发布到maven中央仓库.

只需在你的项目引入依赖.

<dependency>
 <groupId>io.github.lvyahui8</groupId>
 <artifactId>spring-boot-data-aggregator-example</artifactId>
 <version>1.0.1</version>
</dependency>

并在 application.properties 文件中声明注解的扫描路径.

# 替换成你需要扫描注解的包
io.github.lvyahui8.spring.base-packages=io.github.lvyahui8.spring.example

之后, 就可以使用如下注解和 Spring Bean 实现聚合查询

  • @DataProvider
  • @DataConsumer
  • @InvokeParameter
  • Spring Bean DataBeanAggregateQueryFacade

注意, @DataConsumer 和 @InvokeParameter 可以混合使用, 可以用在同一个方法的不同参数上. 且方法的所有参数必须有其中一个注解, 不能有没有注解的参数.

项目地址和上述示例代码: https://github.com/lvyahui8/spring-boot-data-aggregator

后期计划

后续笔者将继续完善异常处理, 超时逻辑, 解决命名冲突的问题, 并进一步提高插件的易用性, 高可用性, 扩展性

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

(0)

相关推荐

  • Spring Boot高效数据聚合之道深入讲解

    背景 接口开发是后端开发中最常见的场景, 可能是RESTFul接口, 也可能是RPC接口. 接口开发往往是从各处捞出数据, 然后组装成结果, 特别是那些偏业务的接口. 例如, 我现在需要实现一个接口, 拉取用户基础信息+用户的博客列表+用户的粉丝数据的整合数据, 假设已经有如下三个接口可以使用, 分别用来获取 用户基础信息 ,用户博客列表, 用户的粉丝数据. 用户基础信息 @Service public class UserServiceImpl implements UserService {

  • Spring boot @RequestBody数据传递过程详解

    这篇文章主要介绍了Spring boot @RequestBody数据传递过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 @RequestBody需要接的参数是一个string化的json @RequestBody,要读取的数据在请求体里,所以要发post请求,还要将Content-Type设置为application/json java的api 参数为JSONObject,获取到的参数处理 @PostMapping("/combine

  • spring Boot查询数据分页显示的方法实例

    springBoot所有依赖和配置文件都写好的情况下 1.dao接口的实现方法 package com.cy.pj.sys.dao; import java.util.List; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import com.cy.pj.sys.pojo.SysLog; @Mapper public interface SysLogDao {

  • Spring Boot实现数据访问计数器方案详解

    目录 1.数据访问计数器 2.代码实现 2.1.方案说明 2.2.代码 2.3.调用 1.数据访问计数器   在Spring Boot项目中,有时需要数据访问计数器.大致有下列三种情形: 1)纯计数:如登录的密码错误计数,超过门限N次,则表示计数器满,此时可进行下一步处理,如锁定该账户. 2)时间滑动窗口:设窗口宽度为T,如果窗口中尾帧时间与首帧时间差大于T,则表示计数器满.   例如使用redis缓存时,使用key查询redis中数据,如果有此key数据,则返回对象数据:如无此key数据,则查

  • spring boot 导出数据到excel的操作步骤(demo)

    目录 问题来源: 实现步骤: 1.添加maven依赖 2.编写excel工具类 3.编写controller,service,serviceImpl,dao,entity 3.1entity 3.2dao 3.3service 3.4serviceImpl 3.5controller 4.测试 问题来源: 前一段时间公司的项目有个导出数据的需求,要求能够实现全部导出也可以多选批量导出(虽然不是我负责的,我自己研究了研究),我们的项目是xboot前后端分离系统,后端的核心为SpringBoot 2

  • spring boot自定义log4j2日志文件的实例讲解

    背景:因为从 spring boot 1.4开始的版本就要用log4j2 了,支持的格式有json和xml两种格式,此次实践主要使用的是xml的格式定义日志说明. spring boot 1.5.8.RELEASE 引入log4j2的开发步骤如下: 1.首先把spring-boot-starter-web以及spring-boot-starter包下面的spring-boot-starter-logging排除,然后引入spring-boot-starter-log4j2包. <dependen

  • Spring boot + mybatis + orcale实现步骤实例代码讲解

    接着上次的实现, 添加 mybatis 查询 orcale 数据库 第一步: 新建几个必须的包, 结果如下 第二步: 在service包下新建personService.java 根据名字查person方法接口 package com.example.first.service; import com.example.first.entity.Person; public interface personService { Person queryPersonByName(String name

  • Spring Boot Jar 包部署脚本的实例讲解

    当使用 Spring Boot 进行开发时,它可以将项目打包成 Jar 包进行部署,但是我们的部署环境一般都是 Linux,每次手写脚本很烦人,所以我提供了一个简单的脚本,用于部署 Spring Boot 生成的 Jar 包. 脚本内容很简单,就是通过端口 kill 掉旧的进程,并将新的 Jar 包部署上去. #!/bin/bash port=8090 jar_name="上传到服务器上的 Jar 包名称" deployed_jar="服务器上部署的 Jar 包名称"

  • spring boot udp或者tcp接收数据的实例详解

    下面用的是 springboot内置integration依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-integration</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot<

  • spring boot容器启动流程

    一.前言 spring cloud大行其道的当下,如果不了解基本原理那么是很纠结的(看见的都是 约定大于配置 ,但是原理呢?为什么要这么做?).spring cloud是基于spring boot快速搭建的,今天咱们就看看spring boot容器启动流程.(本文不讲解如何快速启动spring boot,那些直接官方看即可, 官网文档飞机票 ) 二.容器启动 spring boot一般是 指定容器启动main方法,然后以命令行方式启动Jar包 ,如下图: @SpringBootApplicati

随机推荐