当前位置: 首页 > news >正文

我用Mybatis的方式封装了OLAP查询!

背景

相信做数据平台的朋友对OLAP并不陌生,主流的OLAP引擎有Clickhouse,Impala,Starrocks…以及公司二开的OLAP平台,本次要说的OLAP属于最后一种。
最近在做一个BI项目,业务背景很简单,就是一个数据展示平台。后端是SpringBoot + Mybatis 。 其中有一个比较特殊的是,我们不直接连接数据库,而是向OLAP平台传一个SQL,然后以HTTP请求的形式,从OLAP获得查询的结果。
由于Mybatis不支持配置HTTP形式数据源,我们这边后端同学的做法是,假装是数据库查询,实际用到的地方通过SqlSessionFactory获取执行SQL,然后将其封装在HTTP请求里。 对OLAP返回的Content 解析KeyValues的JSON,最终获得结果。

这种实现方式有一个问题就是, 我们使用Dao + XML的目的只是为了一段SQL,并不能直观的知道一个DAO里面的方法在什么地方使用到了。(因为SqlSessionFacatory获取SQL需要的是DAO名称和Method名称,所以以前是通过包路径获取)

Before

Service类里面的使用就是这种形式:

public DemoServiceImpl implements DemoService{@Autowired    OlapQueryUtils olapQueryUtils;// OlapQueryUtils是负责HTTP请求的工具类public Map<String,Object> getOlapData(RequestParam param){Map<String,Object> result = new HashMap<>();JSONArray json = olapQueryUtils.query("com.xx.xx.DemoDao.selectList", param);// 解析json成自己List<T>List<T> list = JSONUtils.parse(json, List<T>.class);result.put(Constants.DATA, list );return result;}
}

这段代码的问题有两个:

  • com.xx.xx.DemoDao.selectList 是HardCode,如果这个类被移动或者重命名,这段代码会报错
  • 返回的数据都要从JSONArray开始解析,JSON转换操作充斥所有Service。

Dao文件

public interface DemoDao{String selectList(RequestParam param); // no usage
}

这段简短的Dao代码,同样也有问题:

  • 这个Dao代码的方法签名没有意义,至少返回类型没有意义,因为都是HTTP统一的JSONArray;
  • 而且更致命的一点是no usage. IDE无法识别出来,容易被误删。

After

先不说怎么去实现,怎么去解决问题,看一下封装之后的代码片段。
Service:

public DemoServiceImpl implements DemoService{@AutowiredDemoDao demoDao;public Map<String,Object> getOlapData(RequestParam param){Map<String,Object> result = new HashMap<>();result.put(Constants.DATA, demoDao.selectList(param) );return result;}
}

Dao

@OlapMapper
public interface DemoDao{List<T> selectList(RequestParam param); // 1 usage
}

How

这里的原理很简单,就是模仿Mybatis用动态代理技术把DemoDao的动态bean注册到Spring。
Spring动态代理有三个关键步骤:

  • Registry: 注册bean,让DemoDao可以按需被注入到Service中
  • Factory: bean工厂,生产bean
  • Proxy: 动态代理,提供接口方法实际实现。

Registry

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.data.util.AnnotatedTypeScanner;public class OlapDaoRegistry implements BeanDefinitionRegistryPostProcessor, ResourceLoaderAware, ApplicationContextAware {private ApplicationContext applicationContext;private ResourcePatternResolver resourcePatternResolver;private CachingMetadataReaderFactory metadataReaderFactory;private ResourceLoader resourceLoader;@Overridepublic void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {Set<Class<?>> sets = getOlapMappers();for (Class<?> bean : sets) {BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(bean);GenericBeanDefinition beanDefinition = (GenericBeanDefinition) builder.getRawBeanDefinition();beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(bean);// 使用我们定义出来OlapFactory来注册beanbeanDefinition.setBeanClass(OlapDaoFactory.class);beanDefinition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE);registry.registerBeanDefinition(bean.getSimpleName(), beanDefinition);}}// 注册带@olapMapper的DAO文件@SneakyThrowsprivate Set<Class<?>> getOlapMappers() {AnnotatedTypeScanner scanner = new AnnotatedTypeScanner(OlapMapper.class);return scanner.findTypes("com.xx.xx");}@Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {}@Overridepublic void setResourceLoader(ResourceLoader resourceLoader) {this.resourcePatternResolver = new PathMatchingResourcePatternResolver();this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);this.resourceLoader = resourceLoader;}@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.applicationContext = applicationContext;}}

Factory

import org.springframework.beans.factory.FactoryBean;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;public class OlapDaoFactory<T> implements FactoryBean<T> {private final Class<T> clazz;public OlapDaoFactory(Class<T> clazz) {this.clazz = clazz;}@Override@SuppressWarnings({Constant.Suppress.UNCHECKED})public T getObject() {// 使用我们定义的OlapServiceProxy来代理需要提供的BeanInvocationHandler invocationHandler = new OlapServiceProxy<>(clazz);return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, invocationHandler);}@Overridepublic Class<?> getObjectType() {return clazz;}
}

Proxy

// 跟Mybatis一样支持数据源的动态切换,以Clickhouse和Starrocks两种为例// 这里通过moduleName来查看是否支持数据源,你也可以去掉这个设计// 因为缓存可以大幅度提高OLAP select的效率,这里引入了缓存的设计import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;@Slf4j
@RequiredArgsConstructor
public class OlapServiceProxy<T> implements InvocationHandler {private final Class<T> clazz;private String getDaoPrefix() {return clazz.getName() + ".";}private String getRedisKeyPre() {String daoPrefix = getDaoPrefix();daoPrefix = daoPrefix.replace("com.xx.", "");if (!daoPrefix.startsWith("appName.")) {daoPrefix = "appName." + daoPrefix;}return daoPrefix.replace("\\.", ":");}private static void preCheck(String module) {if (!module.contains("-")) {throw new UnsupportedOperationException("模块名应该包含'-'");}}private String getMethodName(String methodName) {return getDaoPrefix() + methodName;}private JSONArray queryCkWithCache(Object request, String method, String module) {preCheck(module);CkModelUtils ckModelUtils = SpringReflectUtils.getBean(CkModelUtils.class);return ckModelUtils.getCacheOrOlapArrayResultData(request, getMethodName(method), getRedisKeyPre() + module, Map.class, module);}private JSONArray queryCk(Object request, String method, String module) {preCheck(module);CkModelUtils ckModelUtils = SpringReflectUtils.getBean(CkModelUtils.class);return ckModelUtils.getDataFromOlap(request, getMethodName(method));}private JSONArray querySrWithCache(Object request, String method, String module) {preCheck(module);SrModelUtils srModelUtils = SpringReflectUtils.getBean(SrModelUtils.class);return srModelUtils.getCacheOrOlapArrayResultData(request, getDaoPrefix(), method, getRedisKeyPre() + module, Map.class, module);}private JSONArray querySr(Object request, String method, String module) {preCheck(module);SrModelUtils srModelUtils = SpringReflectUtils.getBean(SrModelUtils.class);return srModelUtils.getModelData(request, getDaoPrefix(), method, module);}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// fail fastif (Object.class.equals(method.getDeclaringClass())) {log.info("invoke equals method ");return method.invoke(this, args);}Datasource datasource = getDatasource(method);Object request = wrapParam(method, args);JSONArray data = queryFromOlap(method, request, datasource);return processReturnData(method, data);}/*** 从Olap查询获取JSONArray返回数据* @param method 被代理的方法* @param request 请求对象* @param datasource 数据源, 目前可选: CK,SR* @return olap返回的keyValues JSONArray*/private JSONArray queryFromOlap(Method method, Object request, Datasource datasource) {String module = "通用-动态代理";if (method.isAnnotationPresent(Module.class)) {module = method.getAnnotation(Module.class).value();}boolean isCache = this.clazz.isAnnotationPresent(Cache.class) || method.isAnnotationPresent(Cache.class);if (isCache) {if (datasource.equals(Datasource.CK)) {return queryCkWithCache(request, method.getName(), module);} else {return querySrWithCache(request, method.getName(), module);}} else {if (datasource.equals(Datasource.CK)) {return queryCk(request, method.getName(), module);} else {return querySr(request, method.getName(), module);}}}/*** 返回值处理* @param method 被代理的方法, 用来获取返回值类型* @param data olap查询到的JSONArray* @return 根据方法签名返回值,返回转换后的数据*/private @Nullable Object processReturnData(Method method, JSONArray data) {Class<?> returnType = method.getReturnType();// JSONArray直接返回if (returnType.getName().equals(JSONArray.class.getName())) {return data;}// 数组和列表-> SelectMany 就返回多行if (returnType.isArray() || Collection.class.isAssignableFrom(returnType)) {return data.toJavaObject(method.getGenericReturnType());} else {// 返回一行直接取第一个转成对象if (CollectionUtils.isEmpty(data)) return null;if (isNativeType(returnType)) {JSONObject jsonObject = data.getJSONObject(0);String key = jsonObject.keySet().iterator().next();return jsonObject.getObject(key, returnType);}return data.getObject(0, returnType);}}// 数据源: 默认CK -> 类注解覆盖 -> 方法注解覆盖private Datasource getDatasource(Method method) {Datasource datasource = Datasource.CK;if (this.clazz.isAnnotationPresent(DS.class)) {datasource = this.clazz.getAnnotation(DS.class).value();}if (method.isAnnotationPresent(DS.class)) {datasource = method.getAnnotation(DS.class).value();}return datasource;}private Object wrapParam(Method method, Object[] args) {if (args == null || args.length == 0) return null;if (args.length > 1) {Map<String, Object> paramMap = new HashMap<>();Annotation[][] annotations = method.getParameterAnnotations();for (int i = 0; i < args.length; i++) {Object arg = args[i];String key =Arrays.stream(annotations[i]).filter(x -> x instanceof Param).findFirst().map(x -> ((Param) x).value()).orElseThrow(UnsupportedOperationException::new);paramMap.put(key, arg);}return paramMap;} else {return args[0];}}/*** 判断是不是直接类型*/private boolean isNativeType(Class<?> clazz) {String clazzName = clazz.getName();Class<?>[] nativeClasses = {String.class, Integer.class, Boolean.class, Double.class, Long.class, Float.class, Short.class};return Arrays.stream(nativeClasses).anyMatch(x -> clazzName.equals(x.getName()));}
}

自定义注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Module {String value();
}@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DS {Datasource value();
}/*** OlapMapper注解* <p>*      - 用在整个Dao文件上表示所有的方法均走缓存* <p>*      - 用在某个具体方法上面修改该方法的缓存配置*/
@Documented
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Cache {}@Component
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OlapMapper {
}@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Param {String value() ;
}

后记

这篇代码量比较大,就是说这个是一个用得着的时候可以直接抄的博客,一切是为了代码的可维护性!

相关文章:

我用Mybatis的方式封装了OLAP查询!

背景 相信做数据平台的朋友对OLAP并不陌生&#xff0c;主流的OLAP引擎有Clickhouse&#xff0c;Impala&#xff0c;Starrocks…以及公司二开的OLAP平台&#xff0c;本次要说的OLAP属于最后一种。 最近在做一个BI项目&#xff0c;业务背景很简单&#xff0c;就是一个数据展示平…...

golang rune类型解析,与byte,string对比,以及应用

Golang中的rune类型是一个32位的整数类型(int32)&#xff0c;它是用来表示Unicode码点的。rune类型的值可以是任何合法的Unicode码点&#xff0c;它通常用来处理字符串中的单个字符。 在Golang中&#xff0c;字符常量使用单引号来表示&#xff0c;例如 a。使用单引号表示的字符…...

重学java 51.Collections集合工具类、泛型

"我已不在地坛&#xff0c;地坛在我" —— 《想念地坛》 24.5.28 一、Collections集合工具类 1.概述:集合工具类 2.特点: a.构造私有 b.方法都是静态的 3.使用:类名直接调用 4.方法: static <T> boolean addAll(collection<? super T>c,T... el…...

多语言印度红绿灯系统源码带三级分销代理功能

前端为2套UI&#xff0c;一套是html写的&#xff0c;一套是编译后的前端 后台功能很完善&#xff0c;带预设、首充返佣、三级分销机制、代理功能。 东西很简单&#xff0c;首页就是红绿灯的下注页面&#xff0c;玩法虽然单一&#xff0c;好在不残缺可以正常跑。...

HTML拆分与共享方式——多HTML组合技术

作者:私语茶馆 1.应用场景 如果是一个产品级的Web项目,往往非常多的页面部分是重复的(为保持风格一致),每个HTML页面将这些重复部分重新写一次,既带来极大的工作量,也造成后续修改不便。 因此会考虑到将一个HTML的不同部分拆分为多个HTML页面,利用类似Include方式包含…...

K8s集群之 存储卷 PV PVC

目录 默写 1 如何将pod创建在指定的Node节点上 2 污点的种类(在node上设置) 一 挂载存储​​​​​​​ 1 emptyDir存储卷 2 hostPath存储卷 ①在 node01 节点上创建挂载目录 ② 在 node02 节点上创建挂载目录 ③ 创建 Pod 资源 ④ 在master上检测一下&#xff1a;…...

“腾讯云 AI 代码助手”体验

一、“腾讯云 AI 代码助手”体验 1、注册账号并进行实名认证 2、进入开发环境 3、体验javascript简单函数 代码如下&#xff1a; //请写一个两个日期计算的函数 function dateDiff(date1, date2) {return date2.getTime() - date1.getTime(); } var date1 new Date("2…...

Django入门全攻略:从零搭建你的第一个Web项目

系列文章目录 努力ing Django入门全攻略&#xff1a;从零搭建你的第一个Web项目努力ing… 文章目录 系列文章目录前言一、Django1.0 框架介绍1.1 Django安装1.2 Django项目创建1.3 目录介绍 二、子应用2.1 子应用创建2.2 目录结构2.3 子应用注册2.4 子应用视图逻辑2.4.1 编写视…...

AI大模型日报#0529:杨红霞创业入局“端侧模型”、Ilya左膀右臂被Claude团队挖走

导读&#xff1a;AI大模型日报&#xff0c;爬虫LLM自动生成&#xff0c;一文览尽每日AI大模型要点资讯&#xff01;目前采用“文心一言”&#xff08;ERNIE 4.0&#xff09;、“零一万物”&#xff08;Yi-34B&#xff09;生成了今日要点以及每条资讯的摘要。欢迎阅读&#xff0…...

达梦数据库

达梦数据库 达梦Docker部署 达梦Docker部署 1、下载链接 https://pan.baidu.com/s/1RI3Lg0ppRhCgUsThjWV6zQ?pwdjc62 2、docker启动命令 docker run -d -p 5236:5236 \ --restartalways \ --name dm8 \ -e LD_LIBRARY_PATH/app/dm8/bin \ -e LENGTH_IN_CHAR1 \ -e CASE_SENS…...

什么是Axios

2024年5月23日&#xff0c;周四上午 Axios 是一个基于Promise的HTTP客户端&#xff0c;用于浏览器和node.js环境。它提供了一个简单易用的API来发送HTTP请求&#xff0c;并支持Promise API&#xff0c;这使得异步请求变得容易处理。 Axios的一些主要特点包括&#xff1a; Pro…...

React 其他 Hooks

其他 Hooks useRef 可用于获取 DOM 元素 const ScrollRef useRef(null)ScrollRef.current useContext &#xff08;先回顾一下之前的 Context 知识&#xff0c;借用之前 ppt 和源码&#xff09; Hooks 中使用 useContext 来获取 context 的值 // 父组件创建 contextexpor…...

echarts配置记录,一些已经废弃的写法

1、normal&#xff0c;4.0以后无需将样式写在normal中了 改前&#xff1a; 改后&#xff1a; DEPRECATED: normal hierarchy in labelLine has been removed since 4.0. All style properties are configured in labelLine directly now. 2、axisLabel中的文字样式无需使用te…...

电量计量芯片HLW8110的前端电路设计与误差分析校正.pdf 下载

电量计量芯片HLW8110的前端电路设计与误差分析校正.pdf 下载地址&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1vlCtC3LGFMzYpSUUDY-tEg 提取码&#xff1a;8110...

Redis实践记录与总结

最近生产环境缓存数据库数据过大&#xff08;如何搭建单服务redis缓存数据库&#xff1f;以及可视化工具Another Redis Desktop Manager使用&#xff09;&#xff0c;导致在对数据库做rdb快照备份时消耗内存过大&#xff0c;缓存数据库宕机一小时。基础运维通过增加虚拟机内存暂…...

持续总结中!2024年面试必问 20 道 Rocket MQ面试题(三)

上一篇地址&#xff1a;持续总结中&#xff01;2024年面试必问 20 道 Rocket MQ面试题&#xff08;二&#xff09;-CSDN博客 五、什么是生产者&#xff08;Producer&#xff09;和消费者&#xff08;Consumer&#xff09;在RocketMQ中&#xff1f; RocketMQ是一个高性能、高吞…...

Android 自定义Adapter关键函数getView性能最优使用

文章目录 1、自定义Adapter关键函数getView()标准写法2、布局文件list_item_user.xml3、解释3、示例使用4、结果5、进一步优化和扩展5.1. **优化性能&#xff1a;ViewHolder模式**5.2. **处理多种类型的视图**5.3. **使用RecyclerView.Adapter** 6、RecyclerView使用示例7、结果…...

Linux服务上MySQL的启动、重启和关闭

Linux服务上MySQL的启动、重启和关闭 MySQL是一种广泛使用的开源关系型数据库管理系统&#xff0c;常用于各种规模的应用程序中。在Linux服务器上管理MySQL服务是一个基本的运维任务。本文将详细介绍如何在Linux系统上启动、重启和关闭MySQL服务&#xff0c;涵盖不同Linux发行…...

ctfshow web入门 嵌入式 bash cpp pwn

kali转bash shell方法 方便我们本地 bash脚本教程 下面这个代码是bash脚本 #!/bin/bashOIFS"$IFS"IFS"," //表示逗号为字段分隔符set $QUERY_STRING //将参数传入数组Args($QUERY_STRING)IFS"$OIFS" //恢复原始IFS值if [ "$…...

【ONE·Git || 基本用法入门】

总言 主要内容&#xff1a;主要介绍Git中常用的指令。   PS&#xff1a;多人协作与企业开发模型使用&#xff0c;此部分内容不作博文总结。             文章目录 总言1、初识Git1.1、版本控制器1.2、git安装 2、基本操作2.1、Git本地仓库2.1.1、创建Git本地仓库&…...

工业安全零事故的智能守护者:一体化AI智能安防平台

前言&#xff1a; 通过AI视觉技术&#xff0c;为船厂提供全面的安全监控解决方案&#xff0c;涵盖交通违规检测、起重机轨道安全、非法入侵检测、盗窃防范、安全规范执行监控等多个方面&#xff0c;能够实现对应负责人反馈机制&#xff0c;并最终实现数据的统计报表。提升船厂…...

多场景 OkHttpClient 管理器 - Android 网络通信解决方案

下面是一个完整的 Android 实现&#xff0c;展示如何创建和管理多个 OkHttpClient 实例&#xff0c;分别用于长连接、普通 HTTP 请求和文件下载场景。 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas…...

大型活动交通拥堵治理的视觉算法应用

大型活动下智慧交通的视觉分析应用 一、背景与挑战 大型活动&#xff08;如演唱会、马拉松赛事、高考中考等&#xff09;期间&#xff0c;城市交通面临瞬时人流车流激增、传统摄像头模糊、交通拥堵识别滞后等问题。以演唱会为例&#xff0c;暖城商圈曾因观众集中离场导致周边…...

使用分级同态加密防御梯度泄漏

抽象 联邦学习 &#xff08;FL&#xff09; 支持跨分布式客户端进行协作模型训练&#xff0c;而无需共享原始数据&#xff0c;这使其成为在互联和自动驾驶汽车 &#xff08;CAV&#xff09; 等领域保护隐私的机器学习的一种很有前途的方法。然而&#xff0c;最近的研究表明&…...

【第二十一章 SDIO接口(SDIO)】

第二十一章 SDIO接口 目录 第二十一章 SDIO接口(SDIO) 1 SDIO 主要功能 2 SDIO 总线拓扑 3 SDIO 功能描述 3.1 SDIO 适配器 3.2 SDIOAHB 接口 4 卡功能描述 4.1 卡识别模式 4.2 卡复位 4.3 操作电压范围确认 4.4 卡识别过程 4.5 写数据块 4.6 读数据块 4.7 数据流…...

Android Bitmap治理全解析:从加载优化到泄漏防控的全生命周期管理

引言 Bitmap&#xff08;位图&#xff09;是Android应用内存占用的“头号杀手”。一张1080P&#xff08;1920x1080&#xff09;的图片以ARGB_8888格式加载时&#xff0c;内存占用高达8MB&#xff08;192010804字节&#xff09;。据统计&#xff0c;超过60%的应用OOM崩溃与Bitm…...

GruntJS-前端自动化任务运行器从入门到实战

Grunt 完全指南&#xff1a;从入门到实战 一、Grunt 是什么&#xff1f; Grunt是一个基于 Node.js 的前端自动化任务运行器&#xff0c;主要用于自动化执行项目开发中重复性高的任务&#xff0c;例如文件压缩、代码编译、语法检查、单元测试、文件合并等。通过配置简洁的任务…...

云原生安全实战:API网关Kong的鉴权与限流详解

&#x1f525;「炎码工坊」技术弹药已装填&#xff01; 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 一、基础概念 1. API网关&#xff08;API Gateway&#xff09; API网关是微服务架构中的核心组件&#xff0c;负责统一管理所有API的流量入口。它像一座…...

省略号和可变参数模板

本文主要介绍如何展开可变参数的参数包 1.C语言的va_list展开可变参数 #include <iostream> #include <cstdarg>void printNumbers(int count, ...) {// 声明va_list类型的变量va_list args;// 使用va_start将可变参数写入变量argsva_start(args, count);for (in…...

通过MicroSip配置自己的freeswitch服务器进行调试记录

之前用docker安装的freeswitch的&#xff0c;启动是正常的&#xff0c; 但用下面的Microsip连接不上 主要原因有可能一下几个 1、通过下面命令可以看 [rootlocalhost default]# docker exec -it freeswitch fs_cli -x "sofia status profile internal"Name …...