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

背景

接口开发是后端开发中最常见的场景, 可能是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-starter</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

后期计划

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

Spring Boot 高效数据聚合之道的更多相关文章

  1. Spring Boot的数据访问:CrudRepository接口的使用

    示例 使用CrudRepository接口访问数据 创建一个新的Maven项目,命名为crudrepositorytest.按照Maven项目的规范,在src/main/下新建一个名为resource ...

  2. Spring Boot的数据访问 之Spring Boot + jpa的demo

    1. 快速地创建一个项目,pom中选择如下 <?xml version="1.0" encoding="UTF-8"?> <project x ...

  3. Spring Boot与数据

    SpringBoot 着眼于JavaEE! 不仅仅局限于 Mybatis .JDBC. Spring Data JPA Spring Data 项目的目的是为了简化构建基于 Spring 框架应用的数 ...

  4. Spring Boot (31) 数据验证

    曾经参数的验证是这样的: public String test(User user){ if(user == null){ throw new NullPointerException("u ...

  5. (8)Spring Boot 与数据访问

    文章目录 简介 整合基本的JDBC与数据源 整合 druid 数据源 整合 mybatis 简介 对于数据访问层,无论是 SQL 还是 NOSQL ,Spring Boot 默认都采用整合 Sprin ...

  6. Spring Boot框架 - 数据访问 - 整合Mybatis

    一.新建Spring Boot项目 注意:创建的时候勾选Mybatis依赖,pom文件如下 <dependency> <groupId>org.mybatis.spring.b ...

  7. Spring Boot实现数据访问计数器

    1.数据访问计数器   在Spring Boot项目中,有时需要数据访问计数器.大致有下列三种情形: 1)纯计数:如登录的密码错误计数,超过门限N次,则表示计数器满,此时可进行下一步处理,如锁定该账户 ...

  8. Spring Boot框架 - 数据访问 - JDBC&自动配置

    一.新建Spring Boot 工程 特殊勾选数据库相关两个依赖 Mysql Driver — 数据库驱动 Spring Data JDBC 二.配置文件application.properties ...

  9. Windows版:Nginx部署React项目并访问Spring Boot后台数据

    一, 打包react项目 1,在工作空间目录下create-react-app test-arrange 创建项目test-arrange 2,在新建的项目中写好请求与页面 3,打包, 在项目目录下 ...

随机推荐

  1. 奥森图标和CSS特殊字体使用方法

    作为第一篇博文,写这个 我快要被气炸,好吧,废话不说了 昨天在项目中发现有很多这些Awesome图标 也在网上找了下Font Awesome下载后这些文件,现在的版本是4.2,Font Awesome ...

  2. Python问题解决记录

    Python如何进行中文注释:网址 解决Python UnicodeEncodeError: 'ascii' codec can't encode: 网址1.网址2.网址3 Python 字符串转换为 ...

  3. LUM使用常规命令

    查看日志 tail -f /var/log/messages LUM使用常规命令再列出一下######------ 软件操作方法:* 关闭LUM及所有组件:lu-stop* 启动LUM及所有组件:lu ...

  4. bzoj 3038: 上帝造题的七分钟2 线段树||hdu 4027

    3038: 上帝造题的七分钟2 Time Limit: 3 Sec  Memory Limit: 128 MBSubmit: 1066  Solved: 476[Submit][Status][Dis ...

  5. HDU 4034 Graph:反向floyd

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=4034 题意: 有一个有向图,n个节点.给出两两节点之间的最短路长度,问你原图至少有多少条边. 如果无解 ...

  6. php.ini中的session配置说明

    下面介绍能让session运行的必要配置步骤 手动配置PHP运行环境时,最容易遗忘的一项是服务器端session文件的存储目录配置工作,打开php.ini文件,搜索Session,找到session. ...

  7. Array对象(一)

    Array是JavaScript中的一个事先定义好的对象(也可以称作一个类),可以直接使用. 创建Array对象: var array = new Array(); 创建指定元素个数的Array对象: ...

  8. 英语发音规则---ai字母组合发音

    英语发音规则---ai字母组合发音 一.总结 一句话总结:字母组合ai在音词中一般发字母a的音/eɪ/,通常出现在闭音节中.这里要注意的是单词中air字母组合与ai字母组合发音的区别,air发/eə/ ...

  9. 分享知识-快乐自己:HTTP 响应码

    状态码 含义 100 客户端应当继续发送请求.这个临时响应是用来通知客户端它的部分请求已经被服务器接收,且仍未被拒绝.客户端应当继续发送请求的剩余部分,或者如果请求已经完成,忽略这个响应.服务器必须在 ...

  10. 分享知识-快乐自己:Nginx概述及如何使用

    概述: 什么是 Nginx? Nginx (engine x) 是一款轻量级的 Web 服务器 .反向代理服务器及电子邮件(IMAP/POP3)代理服务器. 什么是反向代理? 反向代理(Reverse ...