前言
我所在的团队主营是供应链业务方向,但是降本增效也得帮销售域的同事写东西。因为公司在用飞书,所以飞书自建应用成了移动端小程序的首选。在写完第一个小程序的后端后,领导找到我让写一个脚手架,辅助同事开发。于是乎,素材来了,在写完SDK的SOP文档,我马不停蹄地更新了这篇文章。脚手架相对简单,我也是想着在后续开发过程中继续迭代的,主要是讲一下思路和SDK里面有意思的东西。明天会更新到我的小仓库里面,这几天光写文档了,忘了更新,可以关注一下,马上100Star了,gitee.com/cloudswzy/g...。
脚手架设计
背景
我介入的时候整个部门已经做了几个小应用了,我们团队刚开始做。不过因为架构组没有像传统WEB应用那样出一个统一的框架,所以领导让我去做一个脚手架共享给团队内部。如果是老读者的话,肯定知道博主之前就是做过类似的活,详见xxx。思考再三,决定还是使用模板项目+SDK的模式构建脚手架,接着就要考虑如何为飞书自建应用做特别的优化。
后端上架构逻辑维持和WEB应用相同的结构,即SpringBoot微服务+自建DevOps+公司基建(注册中心、网关、日志收集、监控等)。常规基建里只有登录认证这块需要重写,其他地方都可以复用。啰嗦一下背景,脚手架设计是在团队做第二个小应用之前提出的,当时给了我一天时间,真是蚌埠住了。在正式开启开发的时候,因为太赶了,飞书这边相关的一切都由我去写脚手架并提供,其他三个后端同事去赶业务部分的开发。最后当我花了三天设计并写完脚手架后,后端功能都写完了,绝活。
思路
思路上总体对历史的SDK做减法,大部分用不着的都给去掉,中间件能不用就不用,尤其是Redis、Seata和Elastic-Job之类需要外部依赖的。从一个简单基础的框架配置出发,需要什么能力
- 初始化默认参数和校验配置,比如服务编码、用到的中间件的必需配置之类的,做下判空或者格式校验,具体可以使用@PostConstruct和implements ApplicationContextInitializer之类的方式实现。
- 功能的拓展全局配置,比如jackson或者fastjson等日期或者数字的JSON格式化规范、HttpClient、RestTemplate和Feign之类的扩展改造(比如加Header)等等
- 规范类的配置,比如日志切面、全局异常监听、通用异常类、通用返回结果类等
- 基础组件的初始化和拓展,比如向注册中心、网关、监控等上报信息,给日志收集填充参数,单点登录的接入等等
针对飞书小应用,从业务角度可以在框架里添加和修改
- 登录认证授权鉴权按照飞书的规范重写
- 引入ORM框架Mybatis-Plus和配套的代码生成工具,并做一些自定义配置
- 封装飞书的方法,比如获取各类Token、消息通知等等
feishu-plus-sdk
亮点说明
- 提供比原始框架更友好的日志切面,出入参打印更细致,排除部分BUG
- 提供比原始框架更友好的全局异常监听,增加更多类型异常的监听处理
- 自带Mybatis-Plus最新版,并默认配置数据库方言和自定义扩展
- 自带完整的登录认证授权鉴权,配合模板项目无需二次开发,并默认提供获取当前用户和模拟登录功能
- 使用缓存并封装飞书大部分Token的获取,提供一键获取的方法
- 封装飞书的消息通知并保留其自带的日志打印、缓存等功能,非正式环境默认只发送给默认收件人
使用说明
异常使用com.xxx.framework.feishuplus.exception.BusinessException,确保异常被监听
转换工具使用com.xxx.framework.feishuplus.util.ModelConverterUtils,支持list转list,转page
飞书Token获取请使用com.xxx.framework.feishuplus.feishu.FeiShuPlusUtil
依赖解析
xml
<dependencies>
<!-- 配置参数说明-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>2.3.12.RELEASE</version>
<optional>true</optional>
</dependency>
<!-- httpclient工具,任选-->
<dependency>
<groupId>com.xxx.framework</groupId>
<artifactId>xxx-framework-http</artifactId>
<version>3.0.5-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
<scope>provided</scope>
</dependency>
<!-- springboot参数校验-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.3.12.RELEASE</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- 飞书sdk-->
<dependency>
<groupId>com.larksuite.oapi</groupId>
<artifactId>oapi-sdk</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.46</version>
</dependency>
</dependencies>
主要是引入了MP和飞书原版SDK这两个比较重要的组件,其他都是基础的,比如参数校验和JSON工具
部分核心文件讲解
scss
import com.lark.oapi.Client;
import com.lark.oapi.core.enums.BaseUrlEnum;
import com.lark.oapi.core.request.SelfBuiltTenantAccessTokenReq;
import com.lark.oapi.core.response.TenantAccessTokenResp;
import com.lark.oapi.service.contact.v3.model.BatchUserReq;
import com.lark.oapi.service.contact.v3.model.BatchUserResp;
import com.lark.oapi.service.contact.v3.model.User;
import com.lark.oapi.service.im.v1.enums.ReceiveIdTypeEnum;
import com.lark.oapi.service.im.v1.model.CreateMessageReq;
import com.lark.oapi.service.im.v1.model.CreateMessageReqBody;
import com.lark.oapi.service.im.v1.model.CreateMessageResp;
import com.xxx.framework.base.config.BaseEnvironmentConfigration;
import com.xxx.framework.feishuplus.exception.BusinessException;
import com.xxx.framework.feishuplus.pojo.MessageInDTO;
import com.xxx.framework.feishuplus.properties.CommonProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@Slf4j
@Component
@EnableConfigurationProperties({CommonProperties.class})
public class FeiShuNoticeUtil {
@Autowired
private CommonProperties commonProperties;
@Autowired
private BaseEnvironmentConfigration baseEnv;
private static Client client;
/**
* 延时生成客户端,全局配置Client
*/
public static Client getClient(CommonProperties commonProperties) {
if (client == null) {
client = Client.newBuilder(commonProperties.getFeishuAppId(), commonProperties.getFeishuAppSecret())
.openBaseUrl(BaseUrlEnum.FeiShu) // 设置域名,默认为飞书
.logReqAtDebug(true) // 在 debug 模式下会打印 http 请求和响应的 headers,body 等信息。
.build();
}
return client;
}
/**
* 发送消息
*/
public void sendMessage(MessageInDTO message) {
//先判断是否需要推送消息给真实用户
if (!message.getNoticeReal()) {
//如果不是生产环境,则使用默认配置
if (!"pro".equals(baseEnv.getCurrentEnv())) {
String noticeDefault = commonProperties.getNoticeDefault();
if (StringUtils.hasText(noticeDefault)) {
String[] split = noticeDefault.split(",");
message.setUserIdList(Arrays.asList(split));
} else {
message.setUserIdList(null);
}
}
}
if (message.getUserIdList() != null) {
CompletableFuture.runAsync(() -> {
for (String userId : message.getUserIdList()) {
CreateMessageReq req = CreateMessageReq.newBuilder()
.receiveIdType(StringUtils.hasText(message.getReceiveIdType()) ? message.getReceiveIdType()
: ReceiveIdTypeEnum.USER_ID.getValue())
.createMessageReqBody(CreateMessageReqBody.newBuilder()
.receiveId(userId)
.msgType(message.getMsgType())
.content(message.getContent())
.uuid(UUID.randomUUID().toString())
.build())
.build();
// 发起请求
CreateMessageResp resp = null;
try {
resp = getClient(commonProperties).im().message().create(req);
// 处理服务端错误
if (!resp.success()) {
log.warn("code:{},msg:{},reqId:{}", resp.getCode(), resp.getMsg(), resp.getRequestId());
}
} catch (Exception e) {
log.error("发送信息请求失败", e);
}
}
});
}
}
}
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @Description 业务包可配置参数
**/
@Data
@ConfigurationProperties(prefix = "xxx.business")
public class CommonProperties {
/**
* 飞书APPID
*/
private String feishuAppId;
/**
* 飞书APP密钥
*/
private String feishuAppSecret;
}
为什么要延时获取Client?
首先说说为什么要设置一个类静态变量来初始化,原因是避免重复创建和销毁带来的资源浪费。相同的创建思路,比如我们会在框架中定义一个Redis操作工具类Bean交给Spring管理,默认就是单例的,还有Es、Kafka之类的,虽然官方例子为了举例方便都是创建一个新的,但是我们作为框架的二开是要做封装的。
这里延时获取是因为我们的参数是在配置文件CommonProperties中配置的,我在配置中定义了飞书ID和密钥,这个配置是动态的,所以不能一开始获取,就得新建一个getClient的方法去获取,类似于单例模式的懒加载。除了资源消耗,还有个好处就是这个ApiClient默认是内存管理Token的,详见飞书SDK-Java版文档的client.disableTokenCache()参数。如果有开启的话,内部会存储Token,使用相同的ApiClient时会从内存中直接获取对应的Token,而不用再次调用接口获取
聊聊飞书SDK
截图对应方法,com.lark.oapi.core.request.ReqTranslator#getToken,上面说的如果开启内存管理Token的话,在每一次使用ApiClient请求的时候都会走这个前置逻辑。判断内存管理是否开启,如果开启的话就从GlobalTokenManager.getTokenManager()中获取,没开启的话就调用对应API获取,这实际上是一个易用性的优化,很细节。
因为种种原因登录模块写得很拉垮,前后端约定俗成导致前期问题多多,为了赶工,我没有用ApiClient写登录模块,而是自己写了一下。因为通过接口返回Token里面带了过期时间,所以理所当然地需要缓存一下。所以一个需求产生了,需要一个带过期时间的缓存框架,首先想到的是Redis,但是服务资源紧张,而且大部分自建应用的体量也用不着,都是单点部署,所以我想着用本地缓存处理。想了想,Caffeine和Guava都不支持设置时间,于是考虑自己写一个,但是我突然想到ApiClient提到了默认是内存管理,所以他应该有实现,翻开源码果然就发现了com.lark.oapi.core.cache.LocalCache。他提供了com.lark.oapi.core.cache.ICache接口用作扩展,默认是使用LocalCache。
这里挺有意思的,用了本地缓存经典的ConcurrentMap<String, Value> CACHE作为基座,内部类封装了真正的值value并绑定了一个过期时间,在get/set方法做一下扩展处理就完成了带过期时间的本地缓存的需求。
翻阅oapi-sdk-java源码的时候,真的感觉太细了,有的时候在写的时候能通过类名知道是干什么的,并且每个方法都有专属的枚举类,细啊,易用性拉满。
open.feishu.cn/?lang=zh-CN,飞书开发者后台这个做的很不错,旁边这个示例代码简直顶呱呱,有的地方没有也能通过SDK的源码找出来。但让我头大的是,文档太细了,细到了没时间看的程度,后面这个SDK也会慢慢迭代,跟着公司的项目走,慢慢补充。
模板项目文件说明
包结构简析
├─java
│ └─com
│ └─xxx
│ └─template
│ │ TemplateApplication.java--默认启用Feign、Async和事务
│ ├─api
│ │ │ ErpApi.java--Feign样例
│ │ └─faliback
│ │ ErpApiFaliBack.java--Feign样例
│ ├─constants
│ │ CommonConstant.java--常量样例
│ ├─controller
│ │ CommonController.java--默认提供飞书登录、获取当前用户、获取文件服务器TOKEN和模拟登录
│ │ ExcludeController.java--开放接口
│ ├─mapper
│ ├─pojo
│ │ ├─dto
│ │ │ └─api
│ │ │ ErpPostHeader.java
│ │ │ ErpPreHeader.java
│ │ └─vo
│ │ └─view
│ │ CurUserVO.java--返回给前端的用户包装类,需要修改
│ ├─service
│ │ │ FeiShuPlusService.java
│ │ └─impl
│ │ FeiShuPlusServiceImpl.java--基于飞书PLUS-SDK的基础代码
│ └─util
│ MybatisPlusCodeGenerator.java--代码生成工具
└─resources
│ application-dev.properties--数据库配置,需填充飞书相关配置
│ application-pro.properties--数据库配置,需填充飞书相关配置
│ application-test.properties--数据库配置,需填充飞书相关配置
│ application-uat.properties--数据库配置,需填充飞书相关配置
│ application.properties--提供基础配置,需填充文件服务器配置
└─mapper
TestXml.xml
写在最后
好久没更新了,距离上一篇正经的技术文双剑破万法-递归加反射完成接口数据修改,过了45天了。emmm,不能说懈怠了,哈哈,毕竟还是很充实和忙碌。攒了三篇文章,还有篇数据库问题排查的这周出,Flink的刚弄完还没来得及回顾和写文档,估计放在下月跟另一篇文章放一块。还有个事,哥哥们求内推,博主来挑战一波,5年Java经验在职,意向北京和四川,给我发私信喔,谢啦谢啦!!☆⌒(*^-゜)v。掘金文章都支持转载或者首发公众号,最近积极投稿中,部分已发布在个人公众号《神独自在的技术生活》,部分投稿给了其他公众号大佬,需要首发的可以私信我,欢迎欢迎!