likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 21

背景

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

需求梳理

  1. 需要使用AOP进行全局响应数据拦截
  2. 需要实现一个CDN解析器接口,在需要替换CDN地址的VO对象中实现
  3. 源站/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";


}
  • 响应过程中需要拦截的对象
  1. Resp,封装返回给客户端统一的响应对象
  2. PageResult,封装分页查询返回给客户端统一的分页对象
  3. List集合,Java.util.List返回给客户端的集合对象
  4. CDNResolver对象,解析器对象,最终解析替换CDN地址的对象
  • 全局响应拦截处理
@Slf4j(topic = "TOPIC_COMMON")
@ControllerAdvice
public class GlobalResponseBodyAdviceAdapter implements ResponseBodyAdvice<Object> {


    /**
     * CDN配置
     */
    @Autowired
    private CDNNacosConfig cdnNacosConfig;

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> 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());
            }
        }

    }


}

总结

  1. 低代码,无侵入式实现替换
  2. VO对象返回层级不多,使用了递归,代码更为优雅可读,层级多切勿使用
  3. 对于相同属性名称的,可以直接在解析器接口再重写属性的GET方法,可再度减少代码量
转载自:https://juejin.cn/post/7268539701937979426
评论
请登录