转自:juejin.im/post/5d064b90e51d45777540fda7

背景

接口开发是后端开发中最常见的场景, 可能是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依赖自动注入的思想, 那么我们可不可以通过注解的方式, 自动注入依赖, 自动并行调用接口呢? 答案是肯定的.

首先, 我们先定义一个聚合接口 (当然也可以不定义聚合类, 所有代码写在原Service类中同样可以)

@Component
public class UserAggregate {
    @DataProvider("userFullData")
    public User userFullData(@DataConsumer("user") User user,
                             @DataConsumer("posts") List<Post> posts,
                             @DataConsumer("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("user")
    @Override
    public User get(@InvokeParameter("userId") Long id) {
@Service
public class PostServiceImpl implements PostService {
    @DataProvider("posts")
    @Override
    public List<Post> getPosts(@InvokeParameter("userId") Long userId) {
@Service
public class FollowServiceImpl implements FollowService {
    @DataProvider("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.2</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

SpringBoot 接口并行高效聚合的更多相关文章

  1. SpringBoot接口服务处理Whitelabel Error Page(转)

    To switch it off you can set server.error.whitelabel.enabled=false http://stackoverflow.com/question ...

  2. 2、SpringBoot接口Http协议开发实战8节课(1-6)

    1.SpringBoot2.xHTTP请求配置讲解 简介:SpringBoot2.xHTTP请求注解讲解和简化注解配置技巧 1.@RestController and @RequestMapping是 ...

  3. delphi中接口的委托和聚合

    Delphi的TRegistry注册表类 方法详解 Delphi的接口编程入门 delphi中接口的委托和聚合 2009-09-27 10:44:44|  分类: 默认分类 |  标签: |举报 |字 ...

  4. Springboot接口简单实现生成MySQL插入语句

    Springboot接口简单实现调用接口生成MySQL插入语句 在实际测试中,有这样一个需求场景,比如:在性能压力测试中,可能需要我们事先插入数据库中一些相关联的数据. 我们在实际测试中,遇到问题,需 ...

  5. SpringBoot接口服务处理Whitelabel Error Page

    转载请注明来源:http://blog.csdn.net/loongshawn/article/details/50915979 <SpringBoot接口服务处理Whitelabel Erro ...

  6. Jmeter实时监控+SpringBoot接口性能实战

    性能测试 Jmeter实时监控+SpringBoot接口性能实战 自动化 SpringBoot Java Jmeter实时监控+SpringBoot接口性能实战 一.实验目的及实验环境 1.1.实验目 ...

  7. springboot接口:CommandLineRunner

    springBoot接口:CommandLineRunner 一.作用: 在使用SpringBoot构建项目时,我们通常有一些预先数据的加载.那么SpringBoot提供了一个简单的方式来实现–Com ...

  8. SpringBoot接口 - 如何优雅的对接口返回内容统一封装?

    在以SpringBoot开发Restful接口时,统一返回方便前端进行开发和封装,以及出现时给出响应编码和信息.@pdai SpringBoot接口 - 如何优雅的对接口返回内容统一封装? RESTf ...

  9. SpringBoot接口 - 如何优雅的对参数进行校验?

    在以SpringBoot开发Restful接口时, 对于接口的查询参数后台也是要进行校验的,同时还需要给出校验的返回信息放到上文我们统一封装的结构中.那么如何优雅的进行参数的统一校验呢? @pdai ...

随机推荐

  1. 解决 vue-cli构建项目自动打开浏览器问题

    1.打开项目下的config/index.js 2.找到module.exports的 dev下的 autoOpenBrowser ,将 false 改成 true 3.控制台输入:npm run d ...

  2. 2018-2-13-win10-uwp-读写csv-

    title author date CreateTime categories win10 uwp 读写csv lindexi 2018-2-13 17:23:3 +0800 2018-2-13 17 ...

  3. 使用Varnish加速Web

    通过配置Varnish缓存服务器,实现如下目标: - 使用Varnish加速后端Web服务 - 代理服务器可以将远程的Web服务器页面缓存在本地 - 远程Web服务器对客户端用户是透明的 - 利用缓存 ...

  4. 23飞机大战__pygame 快速入门

      1. 使用 pygame 创建图形窗口 小节目标 游戏的初始化和退出 理解游戏中的坐标系 创建游戏主窗口 简单的游戏循环 可以将图片素材 绘制 到 游戏的窗口 上, 开发游戏之前需要先知道 如何建 ...

  5. Fiddler过滤css、js、图片等静态文件

    REGEX:(?insx)/[^\?/]*\.(css|ico|jpg|png|gif|bmp|wav)(\?.*)?$ REGEX:(?insx)/[^\?/]*\.(action|do)(\?.* ...

  6. 如何对Web服务器进行飓风级防御

    万一你的网站被DDOS飓风式攻击,你会怎么办?笔者以经验告诉你,这里的每一层都起着保护网站的作用.如果您宁愿拥有如何在单台服务器上维持大量流量的版本,则可以: 专用硬件服务器,无共享主机 千兆上行链路 ...

  7. VUE的系统指令

    1. -text原样渲染,渲染文本 2.-html   HTML渲染页面 举例: <!doctype html> <html lang="en"> < ...

  8. 使用Fabric在tomcat中部署应用的问题总结

    关闭tomcat时 A.为什么调用shutdown时,报错连接拒绝 结论——很可能是因为tomcat没启动或没完全启动:而这个时候调用shutdown就会出现此类报错 解决方法:time.sleep ...

  9. 对props的研究

    Vue.component('my-component', { props: { // 基础的类型检查 (`null` 匹配任何类型) propA: Number, // 多个可能的类型 propB: ...

  10. OC + RAC (八) 查看信号状态和跳过信号

    -(void)_test9{ /// RACCommand又叫命令 是用来收发数据的 监听按钮点击,网络请求.... RACCommand * command = [[RACCommand alloc ...