什么?!你要用 Java 写 BFF???

说起写 BFF,很多人的第一反应是用 node 写啊,但要真正落地,就不得不考虑实现成本,比如技术选型、passport 鉴权、接入 nacos、日志管理、服务监控等等,哪个基建做不到位都会影响到服务稳定性。

这至少需要一个小技术团队长期迭代维护,一般中小厂很可能无法单独抽出人力做这个,但 BFF 的收益又是显而易见的。

BFF 也是服务端,因此公司的后端基建我们可以直接拿来用,其成本就是需要前端再学习一门后端语言呗,程序员最拿手不就是学习能力嘛,废话不多说开干!

什么?!你要用 Java 写 BFF???

这里会分几个方面介绍搭建 BFF 服务需要考虑哪些:

  1. 配置 IDE
  2. 创建项目
  3. 实现接口
  4. 打包构建
  5. 日志管理
  6. 服务监控及报警

配置 IDE

第一步先安装 java 环境,这里要注意公司项目所使用 jdk 的版本号不一定是最新版本,要保持一致。

第二步就是下载 IDE 了,我一开始使用 vscode,但是缺少一些插件,后来还是改回了 IDEA → 点击这里下载

使用 IDEA 打开项目后需要设置项目使用的 jdk 的版本

什么?!你要用 Java 写 BFF???

这里推荐一个 java 非常好用的插件:Lombok 详细介绍可以参考这篇文章 → 点击查看

什么?!你要用 Java 写 BFF???

其中用的比较多的是 @Setter@Getter@Slf4j

// Student.java

// 只需要在你的类或者字段上加上这两个注解,Lombok 就会自动为你生成 `getter` 和 `setter` 方法
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class Student {
  private String name;
  private int age;
}
// Service.java

import lombok.extern.slf4j.Slf4j;
import Student;

// 使用 SLF4J 注解后,可以直接在类中使用 log 记录日志
@Slf4j
public class Service {
  public setStudent {
    Student student = new Student();
    
    // 使用 getter 方法获取 name 属性
    student.getName();
    
    // 使用 setter 方法获取 age 属性
    student.setAge(20);
  }
  
  public void doSomething() {
    // 直接使用 log 记录日志
    log.info("This is an information message.");
    log.error("This is an error message.");
  }
}

创建项目

这里主要介绍下我们项目的目录结构,仅供参考

|-- bff

    |-- bff-api                                             入口层
    |   |-- xxx.bff.api
    |   |   |-- fallback                                     接口报错处理
    |   |   |-- request                                      请求定义
    |   |   |-- response                                     响应定义
    |   |   |-- feignApi                                     接口定义    
    
    
    |-- bff-core                                           逻辑层
    |   |-- xxx.bff
    |   |   |-- constant                                     常量
    |   |   |-- controller                                   接口类实现
    |   |   |-- service                                      服务层
    |   |   |-- utils                                        工具
    |   |-- resources                                        环境变量配置
    
    
    |-- logs        					   日志
    
    |-- pom.xml                                            包版本配置,类似 package.json
    
    |-- build.sh                                           打包脚本
    
    |-- Dockerfile                                         docker 配置

其中:

  1. bff-api 中定义 api 的接口路径、入参和响应的数据类型、接口报错后的自定义处理等
  2. bff-core 中定义接口具体逻辑,controller 层连接 apiservice,并且可以通过中间件实现登录鉴权、灰度处理等功能

实现接口

(ps: 下面的示例代码仅供参考,落地还是要看实际项目)

1. 定义接口

// feignApi

package bff.api;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "bff-fe-api", url = "默认路径", decode404 = true, fallback = BffFeignApiFallback.class)
@RequestMapping(path = "/bff")
public interface BffFeignApi {
    @RequestMapping(method = RequestMethod.GET, path = "/test")
    public BaseResponse<ResTest> test(@RequestParam(value = "id") Long id);
}

2. 定义接口返回类型

// response

package bff.api.response;

import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
public class ResTest {
    private Integer id;
    private Long type;
}

3. 定义接口入参类型

package bff.api.request;

import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
public class ReqTest {
    private Integer id;
}

4. 接口报错逻辑处理

// fallback

package bff.api.fallback;

import bff.api.BffFeignApi;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class BffFeignApiFallback implements BffFeignApi {
    private static final int _500 = 500;
    @Value("${k8s.serverName:${spring.application.name}}")
    private String applicationName;

    public BaseResponse<String> test(Long id) {
        log.warn("test fallback");
        return new BaseResponse<String>(_500, "test Fallback", null);
    }
}

5. Controller 层实现 API 接口并调用对应的 Service

// controller

package bff.controller;

import bff.api.BffFeignApi;
import bff.api.response.ResTest;
import bff.service.FeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class FeController implements BffFeignApi {
	@Autowired
	private FeService feService;

	@Override
	public BaseResponse<String> test(Long id) {
		String str = "Fe" + id;
		return new BaseResponse<ResTest>(feService.test(str));
	}
}

6. 定义 Service 接口类型

// service

package bff.service;

import bff.api.response.ResTest;

public interface FeService {
    ResTest test();
}

7. 实现 Service 接口

// serviceImpl

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import bff.api.response.ResTest;

@Slf4j
@Service
public class FeServiceImpl implements FeService {
  public CenterGetCoreInfoRes centerGetCoreInfo() {
    ResTest resTest = new ResTest();

    resTest.setId(1);
    resTest.setType(1);

    return resTest;
  }
}

RPC 调用

随着后端接口原子化越来越盛行,BFF 在很多场景中被用于聚合后端接口,因此 BFF 并发调用多个后端接口就变成了必要条件,在 node 中可以使用 promise.allSettled 并发调用,但是 Java 语言是没有事件循环机制的,需要使用到多线程实现接口并行调用,这里我使用了 ThreadPoolExecutor 创建一个线程池,通过 submit 方法提交任务,通过 get 方法获取结果数据,统一返回给前端。

这里详细介绍下 ThreadPoolExecutor 的使用方式

new ThreadPoolExecutor(
    /*
     * corePoolSize: 指定了线程池中的线程数量
     * 它的数量决定了添加的任务是开辟新的线程去执行,
     * 还是放到workQueue任务队列中去;
     */
    10,
    /*
     * maximumPoolSize: 指定了线程池中的最大线程数量
     * 这个参数会根据你使用的workQueue任务队列的类型,
     * 决定线程池会开辟的最大线程数量
     */
    32,
    /*
     * keepAliveTime: 当线程池中的空闲线程数量超过corePoolSize时,
     * 多余的线程会在多长时间内被销毁
     */
    60L,
    // unit:keepAliveTime的单位
    TimeUnit.SECONDS,
    /*
     * workQueue:任务队列,被添加到线程池中,但尚未被执行的任务;
     * 它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种;
     * ArrayBlockingQueue 是有界任务队列,设置为 1 即队列长度为 1,任务多于 1 就会创建线程
     * 如果线程数超过 maximumPoolSize,则会走到拒绝策略中
     */
    new ArrayBlockingQueue<>(1, true),
    /*
     * handler:拒绝策略,当任务太多来不及处理时,如何拒绝任务。
     * CallerRunsPolicy 策略:如果线程池的线程数量达到上限,
     * 该策略会把任务队列中的任务放在调用者线程当中执行
     */
    new ThreadPoolExecutor.CallerRunsPolicy()
)

这里核心需要考虑两个方面:

  1. 如何确定核心线程数和最大线程数,这个要依据接口实际QPS调用量和机器CPU配置,并不是越高越好,我用了三倍真实QPS调用量进行了压测,设置了 10 个常驻线程 + 32 的最大线程,接口耗时正常,CPU 占有率稳定没有突刺。
  2. 设置任务队列,我的思路是线程数尽量设置的多一些,尽可能不使用任务队列,因为一旦开启任务队列,并行请求就会降级为串行,用户体验就会下降,这是我不希望看到的。

什么?!你要用 Java 写 BFF???

在开启线程池前,两次 RPC 请求是串行执行的,一个接口响应后才发送下一个请求

什么?!你要用 Java 写 BFF???

可以看到,开启线程池后,有两个线程并行发送了请求。

实际落地效果:接口耗时从 1s 优化到了 300ms

打包构建、日志管理、服务监控、报警监控

这些和后端的 Java 项目是一样的,就简单说下每个方面的核心思路,能力上和后端项目做拉齐

  1. 打包构建:这个比较简单,配置一下 Dockerfile 就 ok
  2. 日志管理:这里区分了 access.logrpc.logwarn.logerror.log,格式上进行统一,方便后期做可视化的日志检索平台
  3. 服务监控:这里主要监控线上机器的运行情况、接口耗时情况等
  4. 报警监控:报警分成两种,一种是统一报警,比如接口 500、超时等情况;另一种是业务报错,比如调用外部 RPC 失败/超时的情况,都需要进行监控,有问题第一时间能够感知。

总结

搭建一个 BFF 服务本质上也是搭建一个简单的后端服务,麻雀虽小五脏俱全,要从项目、构建、监控等各个基建层着手建设,缺一不可。

最后再想聊下技术选型,正如标题所说「BFF 为啥要用 Java 做?作为一个前端 node 不该是首选嘛?」

我个人认为,这点还是要从公司技术底层的实际情况来看,我们前期也对 node 进行了大量的技术调研。因为技术上无论是 node 还是 Java 都没有技术壁垒,只是我们对 node 方面的基建线上目前还是缺省状态,而 Java 的基建都是现成的可以直接拿来用,因此从日常维护成本和人力投入成本上看,Java 是我们现阶段的最优解。

原文链接:https://juejin.cn/post/7355324594221711400 作者:重威

(0)
上一篇 2024年4月9日 上午10:12
下一篇 2024年4月9日 上午10:23

相关推荐

发表回复

登录后才能评论