项目地址和示例代码: https://github.com/lvyahui8/spring-boot-data-aggregatorjava
背景
接口开发是后端开发中最多见的场景, 多是RESTFul接口, 也多是RPC接口. 接口开发每每是从各处捞出数据, 而后组装成结果, 特别是那些偏业务的接口.git
例如, 我如今须要实现一个接口, 拉取用户基础信息
+用户的博客列表
+用户的粉丝数据
的整合数据, 假设已经有以下三个接口可使用, 分别用来获取 用户基础信息
,用户博客列表
, 用户的粉丝数据
.程序员
用户基础信息github
@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; } }
用户博客列表spring
@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的首页, 看着复杂, 实际也只有一个接口, 一次性拉下全部数据, 客户端开发也简单.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
后期计划
后续笔者将继续完善异常处理, 超时逻辑, 解决命名冲突的问题, 并进一步提升插件的易用性, 高可用性, 扩展性