【从01 千万级直播项目实战[优雅] 上万接口实现全局替换响应数据

2023年 8月 18日 16.5k 0

背景

前期快速迭代接口,接口返回的数据基本都是VO层包装返回给客户端,那如果我要对这成千上万个接口的VO层做字段数据的修改应该怎么操作? 可以实现全局替换吗? 可以按需优雅替换吗? 本文围绕一个真实使用案例进行阐述,案例为我们使用的文件存储OSS,前期因为没有使用CDN, 数据库、客户端数据返回都直接是OSS的源站域名,现在需要将源站资源地址改成可以全局替换/按需替换CDN地址的方式。

需求梳理

  • 需要使用AOP进行全局响应数据拦截
  • 需要实现一个CDN解析器接口,在需要替换CDN地址的VO对象中实现
  • 源站/CDN地址支持动态更换
  • 实现

    • CDN解析器
    public interface CDNResolver {
    
        /**
         * 替换CDN地址(默认类型OSS)
         *
         * @param sourceDomain 源站域名
         * @param distDomain   目标域名
         */
        void replaceUrl(String sourceDomain, String distDomain);
    
        /**
         * 替换CDN地址
         *
         * @param cdnSourceType
         * @param sourceDomain
         * @param distDomain
         */
        default void replaceUrl(CDNSourceType cdnSourceType, String sourceDomain, String distDomain) {
        }
    
    }
    
    • CDN源站类型定义
    @AllArgsConstructor
    @Getter
    public enum CDNSourceType {
    
        /**
         * OSS
         */
        OSS(1),
        /**
         * H5页面
         */
        H5(2),
        /**
         * web项目
         */
        WEB(3),
        ;
        private final int sourceType;
    }
    
    • CDN替换工具类
    public class CDNUtil {
    
    
        /**
         * 替换OSS CDN域名
         *
         * @param url
         * @param sourceDomain
         * @param distDomain
         * @return
         */
        public static String replaceOSSDomain(String url, String sourceDomain, String distDomain) {
            return replaceDomain(url, false, CDNSourceType.OSS.getSourceType(), sourceDomain, distDomain);
        }
    
        /**
         * 替换OSS CDN域名
         *
         * @param url
         * @param replaceAll
         * @param sourceDomain
         * @param distDomain
         * @return
         */
        public static String replaceOSSDomain(String url, boolean replaceAll, String sourceDomain, String distDomain) {
            return replaceDomain(url, replaceAll, CDNSourceType.OSS.getSourceType(), sourceDomain, distDomain);
        }
    
    
        /**
         * 替换CDN域名
         *
         * @param url
         * @param replaceAll
         * @param cdnSourceType
         * @param sourceDomain
         * @param distDomain
         * @return
         */
        public static String replaceDomain(String url, boolean replaceAll, int cdnSourceType, String sourceDomain, String distDomain) {
            if (StringUtils.isAnyBlank(url, sourceDomain, distDomain)) {
                return url;
            }
    
            if (!url.contains(sourceDomain)) {
                return url;
            }
    
            if (cdnSourceType == CDNSourceType.OSS.getSourceType()) {
                return replaceAll ? url.replaceAll(sourceDomain, distDomain) :
                        url.replace(sourceDomain, distDomain);
            }
    
    
            return url;
        }
    }
    
    • VO层实现CDN解析器
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public class LiveRoomMicVo implements Serializable, CDNResolver {
    
        /**
         * 麦上用户ID 为null则代表麦上没人
         */
        private Long userId;
    
        /**
         * 麦位位置 (1-8)
         */
        private int position;
    
        /**
         * 麦上用户昵称
         */
        private String nickname;
    
        /**
         * 麦上用户头像
         */
        private String avatar;
    
        /**
         * 是否老板麦位 true:是 false:否
         */
        private boolean bossPosition;
    
        /**
         * 麦位状态 {@link  com.miyo.user.entity.room.enums.LiveRoomMicState}
         */
        private int state;
    
        /**
         * 实时上麦魅力值
         */
        private BigDecimal charmScore;
    
        /**
         * 用户佩戴的头像框
         */
        private String avatarFrameUrl;
    
        /**
         * 用户佩戴声波
         *
         * @return
         */
        private String soundWaveUrl;
        /**
         * 是否主麦位
         */
        private boolean primaryPosition;
    
        /**
         * 图标
         * @return
         */
        private String icon;
    
        @Override
        public void replaceUrl(String sourceDomain, String distDomain) {
            this.userAvatar = CDNUtil.replaceOSSDomain(this.userAvatar, sourceDomain, distDomain);
            this.roomSignIcon = CDNUtil.replaceOSSDomain(this.roomSignIcon, sourceDomain, distDomain);
            this.beautifulNumber = CDNUtil.replaceOSSDomain(this.beautifulNumber, sourceDomain, distDomain);
            this.hallLevel = CDNUtil.replaceOSSDomain(this.hallLevel, sourceDomain, distDomain);
        }
    }
    
    • CDN Nacos动态配置
    @Data
    @Component
    @ConfigurationProperties("cdn")
    @RefreshScope
    public class CDNNacosConfig implements Serializable {
    
    
        /**
         * OSS源站域名
         */
        private String OSS_DOMAIN = "xxx.oss-ap-southeast-1.aliyuncs.com";
    
        /**
         * CDN加速域名
         */
        private String OSS_CDN_DOMAIN = "cdn-xxx.xxx.com";
    
    
    }
    
    • 响应过程中需要拦截的对象
  • Resp,封装返回给客户端统一的响应对象
  • PageResult,封装分页查询返回给客户端统一的分页对象
  • List集合,Java.util.List返回给客户端的集合对象
  • CDNResolver对象,解析器对象,最终解析替换CDN地址的对象
    • 全局响应拦截处理
    @Slf4j(topic = "TOPIC_COMMON")
    @ControllerAdvice
    public class GlobalResponseBodyAdviceAdapter implements ResponseBodyAdvice {
    
    
        /**
         * CDN配置
         */
        @Autowired
        private CDNNacosConfig cdnNacosConfig;
    
        @Override
        public boolean supports(MethodParameter returnType, Class> converterType) {
            return true;
        }
    
        @Override
        public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                      Class> selectedConverterType,
                                      ServerHttpRequest request, ServerHttpResponse response) {
    
            long startTime = System.nanoTime();
            resolverObject(body);
            long endTime = System.nanoTime();
    
            double duration = (endTime - startTime) / 1_000_000.0;  // 转为毫秒
            log.info("替换CDN地址耗时:{}ms", duration);
            return body;
        }
    
        /**
         * 解析对象
         *
         * @param body
         */
        private void resolverObject(Object body) {
    
            if (body instanceof CDNResolver) {
                //调用替换CDN接口方法
                ((CDNResolver) body).replaceUrl(cdnNacosConfig.getOSS_DOMAIN(), cdnNacosConfig.getOSS_CDN_DOMAIN());
            } else if (body instanceof Resp) {
                Resp resp = (Resp) body;
                //递归解析
                resolverObject(resp.getData());
            } else if (body instanceof List) {
    
                List list = (List) body;
                for (Object object : list) {
                    //递归解析
                    resolverObject(object);
                }
    
            } else if (body instanceof PageResult) {
    
                PageResult pageResult = (PageResult) body;
    
                if (!CollectionUtils.isEmpty(pageResult.getRecord())) {
                    //递归解析
                    resolverObject(pageResult.getRecord());
                }
            }
    
        }
    
    
    }
    

    总结

  • 低代码,无侵入式实现替换
  • VO对象返回层级不多,使用了递归,代码更为优雅可读,层级多切勿使用
  • 对于相同属性名称的,可以直接在解析器接口再重写属性的GET方法,可再度减少代码量
  • 相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论