物联网云平台工业设备对接远程控制数据采集视频接入开源可二次开发 该物联网云平台使用 Java 语言、主流技术组合开发,支持多数据源,支持代码一键生成,方便快速开发。 1、内含物联网云平台全套源码(源码全部开放,无任何加密,可二次开发)、MQTT服务、数据库设计、相关资料、相关工具软件等; 2、使用基于发布/订阅模式的轻量级通讯协议 MQTT,具有自动重连机制,具有设备上下线提醒功能; 3、支持 Modbus RTU、Modbus TCP 协议03、05功能码,亦可自定义协议接入;支持各类 DTU、透传模块以及各类工业设备接入等; 4、支持云固件升级、远程重启等;可存储各路开关状态,设备断电或重启后可自动同步云端各路开关状态,具有开关状态记忆功能; 5、云平台可对各类传感器管理,适用各种业务场景;对传感器数据有多种展示方式; 6、可配置报警规则,符合报警规则可执行自定义动作、联动报警、报警通知等; 7、控制命令下达后有执行状态反馈,可确保设备控制命令执行成功; 8、具有可视化在线定时任务配置功能,可指定某一时刻执行、周期执行、自定义 Cron 表达式执行等操作; 9、具有场景管理功能,可一键执行设定的动作; 10、具有视频管理功能,支持萤石云协议的摄像头均可接入云平台,可在云平台直接预览视频画面; 11、可记录设备、用户所有操作记录,设备、用户上下线记录等,并具有多种类型的数据统计展示; 12、云平台可对所有用户管理,每个用户可配置不同角色、不同权限,具有权限分配功能;支持对人员进行菜单、按钮及数据权限控制,亦可自定义数据权限; 13、前端采用完全响应式布局,支持电脑、平板、手机等所有主流设备; 14、Maven 多项目依赖,模块及插件分项目,尽量松耦合,方便模块升级、增减模块; 等等……内容较多,在此无法一一列举

—— 逐行拆解,让每一行代码都说话

(全文约 2.2 万字,建议收藏后按需检索)


0 前言:为什么一定要“读代码”而不是“读文档”

物联网业务本身并不复杂:设备上线 → 上报数据 → 触发规则 → 执行动作。真正让团队痛苦的是“通用能力”重复造轮子:

  • 分页、缓存、权限、多数据源、Excel、MQTT、XSS、防重、日志……
  • 每开一个新项目都要复制一遍,BUG 也随之复制。

KSoft 把上述能力沉淀到 ksoft-common,并全部开源无加密

本文不再重复“功能列表”,而是一行一行把代码读给你听

  • 类签名为什么这么写?
  • 字段为什么加 transient/ThreadLocal
  • 哪一行隐藏了性能陷阱?
  • 哪一行做了物联网场景的特殊补偿?

读完你能:

  1. 直接调试到 common 内部,不再“黑盒调用”;
  2. 抄走任意片段到自己项目,避免“拿来即坑”;
  3. 贡献 PR 时知道作者意图,不会“好心办坏事”。

1 包结构总览 + 阅读顺序建议

com.ksoft.common  
├─ annotation     ┐ 先读:所有“标记”语义,后面 AOP 会反复出现  
├─ constant       │ 再读:纯常量池,零逻辑,一眼扫过  
├─ core           │ 核心模型:AjaxResult、BaseEntity、分页  
├─ enums          │ 枚举值与数据库字典 100% 对齐  
├─ exception      │ 异常体系:一句话概括“错误码即 i18n key”  
├─ config         │ 配置属性:@ConfigurationProperties 用法典范  
├─ json           │ 对 Jackson 的二次封装,解决“写多行”痛点  
├─ utils          │ 工具大杂烩,挑高频的逐行读  
└─ xss            │ 过滤器链最后一环,读完后端防线就完整了

2 注解层:Java 注解如何变成“运行时能力”

2.1 `@DataScope` —— 数据权限的“语法糖”

源码位置:com.ksoft.common.annotation.DataScope

@Target(ElementType.METHOD)          // 仅作用于方法
@Retention(RetentionPolicy.RUNTIME)  // 运行期保留
public @interface DataScope {
    String deptAlias() default "";   // 表别名
    String userAlias() default "";
}
  • 为什么只能标注在方法?
    数据权限需要拿到 MethodSignature,类级别拿不到参数名(JDK 8 仅 -parameters 模式可保留),所以强制方法级。
  • 为什么默认值是空串而不是 null
    空串可直接拼 SQL,省去下游 StringUtils.defaultString() 的防御。
2.2 `@Log` —— 操作日志的“元数据”

源码位置:com.ksoft.common.annotation.Log

@Target({ElementType.PARAMETER, ElementType.METHOD})
public @interface Log {
    String title() default "";
    BusinessType businessType() default BusinessType.OTHER;
    OperatorType operatorType() default OperatorType.MANAGE;
    boolean isSaveRequestData() default true;
}
  • 罕见地把注解打在 PARAMETER
    为了支持“记录单个参数”场景:
    public AjaxResult upload(@Log(title="文件上传") MultipartFile file)
    此时 Aspect 通过 ((MethodSignature) pjp.getSignature()).getMethod().getParameters() 能拿到参数名与注解的映射。

3 常量池:一行常量背后可能省一次 SQL

3.1 `Constants.java`
public static final String SYS_DICT_KEY = "sys_dict:";
  • 缓存 key 的前缀拼写错误会导致字典翻译失效,所以集中成常量,编译期即报错
3.2 `UserConstants.java`
int PASSWORD_MIN_LENGTH = 5;
int PASSWORD_MAX_LENGTH = 20;
  • 与 Hibernate Validator 注解 @Size(min = 5, max = 20) 两处保持一致,否则会出现“前端提示 6-20,后端却允许 5” 的诡异体验。

4 核心模型:BaseEntity 如何“隐形”解决 90% 更新字段遗漏

源码位置:com.ksoft.common.core.domain.BaseEntity

public class BaseEntity implements Serializable {
    private String searchValue;          // 模糊查询关键字
    private String remark;               // 备注
    private Long createBy;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;
    private Long updateBy;
    private Date updateTime;
    private Integer deleteFlag;          // 逻辑删除
    private Map<String, Object> params;  // 扩展参数(数据权限、临时排序)
}
  • params 设计亮点
    数据权限 SQL 片段、临时排序字段、前端自定义过滤条件,不额外建字段,全部塞进 params
    MyBatis XML 里直接 ${params.dataScope} 即可,防止实体类被非业务字段污染
  • deleteFlag 采用 Integer 而不是 Boolean
    预留“删除操作人”审计扩展:
    0=未删 1=已删 2=待审核 3=级联删 … 可直接复用同一字段。

5 分页链路:PageHelper 与 TableDataInfo 的“零魔法”协作

源码位置:com.ksoft.common.core.controller.BaseController.startPage()

protected void startPage() {
    PageDomain pageDomain = TableSupport.buildPageRequest(); // ①
    Integer pageNum = pageDomain.getPageNum();
    Integer pageSize = pageDomain.getPageSize();
    if (StringUtils.isNotNull(pageNum) && StringUtils.isNotNull(pageSize)) {
        String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy()); // ②
        PageHelper.startPage(pageNum, pageSize, orderBy);                    // ③
    }
}
  • ① 如何“无感”拿到前端分页参数?
    TableSupport 统一从 HttpServletRequestpageNum/pageSize/orderByColumn/isAsc不管 GET/POST/JSON 都能取

`java

public static PageDomain getPageDomain() {

PageDomain pageDomain = new PageDomain();

pageDomain.setPageNum(ServletUtils.getParameterToInt(Constants.PAGE_NUM));

pageDomain.setPageSize(ServletUtils.getParameterToInt(Constants.PAGE_SIZE));

pageDomain.setOrderByColumn(ServletUtils.getParameter(Constants.ORDERBYCOLUMN));

物联网云平台工业设备对接远程控制数据采集视频接入开源可二次开发 该物联网云平台使用 Java 语言、主流技术组合开发,支持多数据源,支持代码一键生成,方便快速开发。 1、内含物联网云平台全套源码(源码全部开放,无任何加密,可二次开发)、MQTT服务、数据库设计、相关资料、相关工具软件等; 2、使用基于发布/订阅模式的轻量级通讯协议 MQTT,具有自动重连机制,具有设备上下线提醒功能; 3、支持 Modbus RTU、Modbus TCP 协议03、05功能码,亦可自定义协议接入;支持各类 DTU、透传模块以及各类工业设备接入等; 4、支持云固件升级、远程重启等;可存储各路开关状态,设备断电或重启后可自动同步云端各路开关状态,具有开关状态记忆功能; 5、云平台可对各类传感器管理,适用各种业务场景;对传感器数据有多种展示方式; 6、可配置报警规则,符合报警规则可执行自定义动作、联动报警、报警通知等; 7、控制命令下达后有执行状态反馈,可确保设备控制命令执行成功; 8、具有可视化在线定时任务配置功能,可指定某一时刻执行、周期执行、自定义 Cron 表达式执行等操作; 9、具有场景管理功能,可一键执行设定的动作; 10、具有视频管理功能,支持萤石云协议的摄像头均可接入云平台,可在云平台直接预览视频画面; 11、可记录设备、用户所有操作记录,设备、用户上下线记录等,并具有多种类型的数据统计展示; 12、云平台可对所有用户管理,每个用户可配置不同角色、不同权限,具有权限分配功能;支持对人员进行菜单、按钮及数据权限控制,亦可自定义数据权限; 13、前端采用完全响应式布局,支持电脑、平板、手机等所有主流设备; 14、Maven 多项目依赖,模块及插件分项目,尽量松耦合,方便模块升级、增减模块; 等等……内容较多,在此无法一一列举

pageDomain.setIsAsc(ServletUtils.getParameter(Constants.IS_ASC));

return pageDomain;

}

`

  • ② 为什么必须 SqlUtil.escapeOrderBySql()
    前端 orderByColumn 可能直接透传“createtime desc--”,会导致 SQL 注入。
    SqlUtil 用正则 [a-zA-Z0-9
    \\ \\,\\.]+ 暴力白名单,拒绝任何函数、子查询
  • ③ PageHelper 线程安全吗?
    PageHelper 使用 ThreadLocal 保存分页参数,请求结束必须 finally 清理,但 KSoft 借助 PageHelper.startPage() 的自动清理机制(MyBatis 拦截器执行后即 remove),业务代码无需手动清理

6 多数据源:1 个注解 + 2 个类完成“读写分离”

6.1 注解定义
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface DataSource {
    DataSourceType value() default DataSourceType.MASTER;
}
6.2 切面
@Around("dsPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
    DataSource dataSource = getDataSource(point);
    if (StringUtils.isNotNull(dataSource)) {
        DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
    }
    try {
        return point.proceed();
    } finally {
        DynamicDataSourceContextHolder.clearDataSourceType(); // ①
    }
}
  • ① 为什么放在 finally
    防止业务异常后 ThreadLocal 未清理,导致下一次请求拿到旧数据源的“串库”事故。
6.3 动态数据源
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}
  • 只读库宕机怎么办?
    Druid 会抛 SQLException,业务方捕获后前端提示“查询服务繁忙”,主库仍正常写入,实现“读写分离降级”。

7 数据权限:把“可见部门”翻译成 SQL 片段

7.1 切面核心逻辑(节选)
public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias) {
    StringBuilder sqlString = new StringBuilder();
    for (SysRole role : user.getRoles()) {
        String dataScope = role.getDataScope();
        if (DATA_SCOPE_ALL.equals(dataScope)) {
            sqlString = new StringBuilder();
            break; // ① 拥有“全部数据权限”直接清空
        } else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)) {
            sqlString.append(StringUtils.format(
                " OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
                deptAlias, user.getDeptId(), user.getDeptId()));
        }
    }
    // 拼接到 params
    if (StringUtils.isNotBlank(sqlString.toString())) {
        Object params = joinPoint.getArgs()[0];
        if (params instanceof BaseEntity) {
            ((BaseEntity) params).getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
        }
    }
}
  • ① 为什么遇到“全部权限”就清空?
    性能:一旦角色里出现“全部数据权限”,其他角色再限制已无意义,直接 return "" 让 SQL 无附加条件,走索引全表扫描最快。
  • findinset 的坑
    MySQL 8.0 以前对 findinset 无法走索引,数据量 10w+ 会全表扫
    解决方案:
    a) 升级 8.0 并创建函数索引;
    b) 把“祖先链”拆成中间表,用 IN 代替 findinset

8 工具类:每行代码都藏着“物联网血泪”

8.1 `CRC16Util.java` —— Modbus 校验必用
public static String getCRC16(byte[] bytes) {
    int CRC = 0x0000ffff;
    int POLYNOMIAL = 0x0000a001;
    for (byte b : bytes) {
        CRC ^= (b & 0x00ff);
        for (int i = 0; i < 8; i++) {
            if ((CRC & 0x0001) != 0) {
                CRC >>= 1;
                CRC ^= POLYNOMIAL;
            } else {
                CRC >>= 1;
            }
        }
    }
    return Integer.toHexString(CRC).toUpperCase();
}
  • 为什么高位在前?
    Modbus RTU 协议规定 CRC 低字节先发,但工控屏大多高位在前,所以工具类直接 return result.substring(2, 4) + result.substring(0, 2)省得每个司机再倒一次
8.2 `DataFormatUtils.java` —— 字节序、位序、符号位一次到位
public static Float hexStr2Float(String hexStr) {
    hexStr = doDataWork(hexStr);
    return Float.intBitsToFloat(new BigInteger(hexStr, 16).intValue());
}
  • BigInteger 而不是 Long.parseLong
    支持 无符号 32 位(如 0xFF000000 超过 Long.MAX_VALUE 但 BigInteger 仍可解析)。
  • 字节序转换
    hexStrConvertByteOrder(hexStr, byteOrder) 支持 0~7 共 8 种排列,兼容所有 PLC 厂商
8.3 `ExcelUtil.java` —— 导出 65536+ 行内存不炸
public void createWorkbook() {
    this.wb = new SXSSFWorkbook(500); // ① 保留 500 行在内存,其余刷盘
}
  • ① SXSSF 原理
    底层维护一个 滑动窗口,窗口外行立即写入临时文件,内存占用 < 50 MB 即可导出 100 万行。
    代价:临时文件需手动 wb.dispose(),否则 /tmp/poifiles 把磁盘打满。

9 JSON 封装:让 Jackson 写多行变成“一句话”

源码位置:com.ksoft.common.json.JSON

private static final ObjectWriter objectWriter = objectMapper.writerWithDefaultPrettyPrinter();

public static String marshal(Object value) throws Exception {
    return objectWriter.writeValueAsString(value);
}
  • ObjectWriter 线程安全
    Jackson 官方文档:ObjectWriter不可变且线程安全 的,可全局单例。
    如果每次 new ObjectMapper()QPS 1k 时 YoungGC 会暴涨 30%
  • 自定义 JSONObject
    提供 value(name, defaultValue) 多级路径访问,兼容前端 lodash.get 写法:
    json.value("device.sensor.temperature", 0)
    底层用正则 (\\w+)((\\[\\d+\\])+)) 解析数组下标,零依赖实现“树形取值”

10 XSS 过滤器:最后 1 道“后端保命”防线

源码位置:com.ksoft.common.xss.XssHttpServletRequestWrapper

@Override
public String[] getParameterValues(String name) {
    String[] values = super.getParameterValues(name);
    if (values != null) {
        int length = values.length;
        String[] escapseValues = new String[length];
        for (int i = 0; i < length; i++) {
            escapseValues[i] = EscapeUtil.clean(values[i]).trim();
        }
        return escapseValues;
    }
    return super.getParameterValues(name);
}
  • EscapeUtil.clean 逻辑
    基于白名单 其他标签全部转义
    <script></script>
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐