Spring Boot 集成 TDengine 时序数据库完整实战
摘要:本文从零开始详细介绍如何在 Spring Boot 项目中集成 TDengine 时序数据库,包含完整的依赖配置、自定义注解实现、工具类代码、实体定义和可运行 Demo,看完就能直接集成到自己的项目中。
一、什么是 TDengine
TDengine 是一款高性能、分布式、时序数据库,专为物联网、工业互联网、金融监控等场景设计。主要特性包括:
- 高性能写入:单表每秒百万级数据写入
- 高压缩比:时序数据专用压缩算法,存储空间节省 90%+
- 超级表设计:一次定义数据结构,自动为每个设备创建子表
- 标签查询:支持按设备类型、位置等维度快速聚合
- SQL 语法:类 SQL 语法,学习成本低
二、快速开始
2.1 环境要求
| 组件 | 版本 |
|---|---|
| JDK | 1.8+ |
| Spring Boot | 2.5.x+ |
| TDengine | 2.6.x+ |
| Maven | 3.6+ |
2.2 TDengine 安装
Linux 环境
bash
wget https://www.taosdata.com/assets-download/taos/TD-server-2.6.0-Linux-x64-2.0.15.0.tar.gz
tar -xzf TD-server-2.6.0-Linux-x64-2.0.15.0.tar.gz
cd TD-server-2.6.0-Linux-x64-2.0.15.0
sudo ./install.sh
Docker 环境(推荐)
bash
docker run -d --name tdengine -p 6030:6030 tdengine/tdengine:2.6.0
验证安装
bash
taos
三、项目集成步骤
3.1 创建 Spring Boot 项目
使用 Spring Initializr 或手动创建 Maven 项目。
3.2 添加 Maven 依赖
在 pom.xml 中添加以下依赖:
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>tdengine-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.12</version>
</parent>
<properties>
<java.version>1.8</java.version>
<taos.version>2.0.0</taos.version>
</properties>
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot JDBC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- TDengine JDBC 驱动 (仅支持 JNI 版本) -->
<dependency>
<groupId>com.taosdata.jdbc</groupId>
<artifactId>taos-jdbcdriver</artifactId>
<version>${taos.version}</version>
</dependency>
<!-- HikariCP 连接池 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<!-- Hutool 工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.22</version>
</dependency>
<!-- Lombok (可选) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.3 配置文件
src/main/resources/application.yml
yaml
server:
port: 8080
spring:
application:
name: tdengine-demo
# TDengine 数据源配置
taos:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.taosdata.jdbc.TSDBDriver
url: jdbc:TAOS://localhost:6030/iot
username: root
password: taosdata
# 连接池配置
minimum-idle: 5
maximum-pool-size: 20
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
# SQL 执行日志 (开发环境开启)
logging:
level:
com.taosdata.jdbc: debug
com.example.taos: trace
四、自定义注解实现
4.1 @STable - 超级表注解
src/main/java/com/example/taos/annotation/STable.java
java
package com.example.taos.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 超级表注解
* 用于标记 TDengine 的超级表和数据表映射关系
*
* 使用示例:
* @STable(
* value = "meters", // 超级表名
* table = "t_${deviceId}", // 子表名模板
* using = true // 启用自动创建子表
* )
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface STable {
/**
* 超级表表名
* @return 超级表名称,如果不使用超级表则返回空字符串
*/
String value() default "";
/**
* 数据表表名
* 支持使用 ${} 占位符动态生成表名
* 示例:数据表名 = "t_${deviceId}" → 实际表名:t_device001, t_device002
* @return 数据表名称模板
*/
String table();
/**
* 是否自动创建数据表
* true: 如果数据表不存在,插入时自动创建
* false: 数据表必须预先创建好
* @return 是否启用自动创建
*/
boolean using() default false;
}
4.2 @IotTableId - 时间戳字段注解
src/main/java/com/example/taos/annotation/IotTableId.java
java
package com.example.taos.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 时间戳字段注解
* 用于标记 TDengine 表的主键时间字段
*
* 使用要求:
* - 字段类型必须是 Date、Long 或 Timestamp
* - 每个实体类必须有且仅有一个 @IotTableId 字段
*
* 使用示例:
* @IotTableId
* private Date ts;
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface IotTableId {
/**
* 字段名称 (默认使用实体字段名)
* @return 表字段名
*/
String value() default "";
}
4.3 @IotField - 普通数据字段注解
src/main/java/com/example/taos/annotation/IotField.java
java
package com.example.taos.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.sql.Types;
/**
* 普通数据字段注解
* 用于标记 TDengine 表的数据列
*
* 使用示例:
* @IotField("voltage")
* private Double voltage;
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface IotField {
/**
* 表字段名 (默认使用实体字段名)
* @return 字段名
*/
String value() default "";
/**
* 表字段类型 (默认使用实体字段类型)
* @see java.sql.Types
* @return 字段类型
*/
int type() default Types.NULL;
/**
* 保留小数位数 (针对浮点数)
* -1 表示不处理
* @return 小数位数
*/
int scale() default -1;
}
4.4 @IotTag - 标签字段注解
src/main/java/com/example/taos/annotation/IotTag.java
java
package com.example.taos.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.sql.Types;
/**
* 标签字段注解
* 用于标记 TDengine 超级表的 TAG 列
* TAG 是 TDengine 的特色,用于设备维度查询
*
* 使用示例:
* @IotTag("device_id")
* private String deviceId;
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface IotTag {
/**
* 标签名称 (默认使用实体字段名)
* @return 标签名
*/
String value() default "";
/**
* 标签字段类型 (默认使用实体字段类型)
* @see java.sql.Types
* @return 字段类型
*/
int type() default Types.NULL;
}
五、核心工具类实现
5.1 字段元数据类
src/main/java/com/example/taos/sql/FieldMeta.java
java
package com.example.taos.sql;
import java.lang.reflect.Field;
/**
* 字段元数据
* 封装实体类字段的映射信息
*/
public class FieldMeta {
/** 字段名 */
private String fieldName;
/** 字段类型 (Java 类型) */
private String fieldType;
/** 数据库字段名 */
private String columnName;
/** 数据库字段类型 */
private int columnType;
/** 反射字段对象 */
private Field field;
/** 小数位数 */
private int scale = -1;
public FieldMeta() {
}
public FieldMeta(Field field) {
this.field = field;
this.fieldName = field.getName();
this.fieldType = field.getType().getSimpleName();
this.columnName = field.getName();
field.setAccessible(true);
}
// Getter 和 Setter
public String getFieldName() {
return fieldName;
}
public void setFieldName(String fieldName) {
this.fieldName = fieldName;
}
public String getFieldType() {
return fieldType;
}
public void setFieldType(String fieldType) {
this.fieldType = fieldType;
}
public String getColumnName() {
return columnName;
}
public void setColumnName(String columnName) {
this.columnName = columnName;
}
public int getColumnType() {
return columnType;
}
public void setColumnType(int columnType) {
this.columnType = columnType;
}
public Field getField() {
return field;
}
public void setField(Field field) {
this.field = field;
}
public int getScale() {
return scale;
}
public void setScale(int scale) {
this.scale = scale;
}
/**
* 获取字段值
*/
public Object getFieldValue(Object entity) throws IllegalAccessException {
return field.get(entity);
}
}
5.2 表元数据类
src/main/java/com/example/taos/sql/TaosTableMeta.java
java
package com.example.taos.sql;
import java.util.ArrayList;
import java.util.List;
/**
* TDengine 表元数据
* 封装超级表、数据表、字段的完整映射信息
*/
public class TaosTableMeta {
/** 超级表名 */
private String stableName;
/** 数据表名模板 */
private String tableNameTemplate;
/** 是否自动创建数据表 */
private boolean using;
/** 时间戳字段 */
private FieldMeta timeField;
/** 数据字段列表 */
private List<FieldMeta> dataFields = new ArrayList<>();
/** 标签字段列表 */
private List<FieldMeta> tagFields = new ArrayList<>();
// Getter 和 Setter
public String getStableName() {
return stableName;
}
public void setStableName(String stableName) {
this.stableName = stableName;
}
public String getTableNameTemplate() {
return tableNameTemplate;
}
public void setTableNameTemplate(String tableNameTemplate) {
this(tableNameTemplate);
}
public void setTableNameTemplate(String tableNameTemplate) {
this.tableNameTemplate = tableNameTemplate;
}
public boolean isUsing() {
return using;
}
public void setUsing(boolean using) {
this.using = using;
}
public FieldMeta getTimeField() {
return timeField;
}
public void setTimeField(FieldMeta timeField) {
this.timeField = timeField;
}
public List<FieldMeta> getDataFields() {
return dataFields;
}
public void setDataFields(List<FieldMeta> dataFields) {
this.dataFields = dataFields;
}
public List<FieldMeta> getTagFields() {
return tagFields;
}
public void setTagFields(List<FieldMeta> tagFields) {
this.tagFields = tagFields;
}
/**
* 生成 INSERT SQL 语句
*/
public String generateInsertSql() {
StringBuilder sb = new StringBuilder();
sb.append("(");
// 时间戳字段
sb.append(timeField.getColumnName());
// 数据字段
for (FieldMeta field : dataFields) {
sb.append(", ").append(field.getColumnName());
}
sb.append(")");
return sb.toString();
}
/**
* 生成参数占位符 SQL
*/
public String generateParamSql() {
StringBuilder sb = new StringBuilder();
sb.append("(");
sb.append("?");
int totalFields = 1 + dataFields.size();
for (int i = 1; i < totalFields; i++) {
sb.append(", ?");
}
sb.append(")");
return sb.toString();
}
/**
* 生成超级表 USING 子句
*/
public String generateUsingClause() {
if (stableName == null || stableName.isEmpty()) {
return null;
}
StringBuilder sb = new StringBuilder();
sb.append("USING ").append(stableName);
if (!tagFields.isEmpty()) {
sb.append(" (");
for (int i = 0; i < tagFields.size(); i++) {
if (i > 0) sb.append(", ");
sb.append(tagFields.get(i).getColumnName());
}
sb.append(")");
}
return sb.toString();
}
/**
* 生成 TAG 值列表
*/
public String generateTagValues() {
if (tagFields.isEmpty()) {
return null;
}
StringBuilder sb = new StringBuilder();
sb.append("(");
for (int i = 0; i < tagFields.size(); i++) {
if (i > 0) sb.append(", ");
sb.append("?");
}
sb.append(")");
return sb.toString();
}
/**
* 获取总字段数
*/
public int getTotalFields() {
return 1 + dataFields.size(); // 时间戳 + 数据字段
}
}
5.3 SQL 执行上下文
src/main/java/com/example/taos/sql/SqlExecContext.java
java
package com.example.taos.sql;
/**
* SQL 执行上下文
* 封装要执行的 SQL 语句和参数
*/
public class SqlExecContext {
/** SQL 语句 */
private String sql;
/** 参数值数组 */
private Object[] params;
public SqlExecContext(String sql, Object[] params) {
this.sql = sql;
this.params = params;
}
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
}
public Object[] getParams() {
return params;
}
public void setParams(Object[] params) {
this.params = params;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("SQL: ").append(sql).append("\n");
sb.append("Params: ");
if (params != null) {
for (Object param : params) {
sb.append(param).append(", ");
}
}
return sb.toString();
}
}
5.4 元数据解析器
src/main/java/com/example/taos/sql/TaosMetaExtractor.java
java
package com.example.taos.annotation;
import com.example.taos.sql.FieldMeta;
import com.example.taos.sql.TaosTableMeta;
import java.lang.reflect.Field;
import java.sql.Date;
import java.sql.Types;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* TDengine 元数据解析器
* 从实体类注解中提取表结构信息
*/
public class TaosMetaExtractor {
/** 元数据缓存 */
private static final Map<Class<?>, TaosTableMeta> META_CACHE = new ConcurrentHashMap<>();
/**
* 解析实体类的表元数据
*/
public static TaosTableMeta parse(Class<?> entityClass) {
return META_CACHE.computeIfAbsent(entityClass, TaosMetaExtractor::doParse);
}
/**
* 清空缓存
*/
public static void clearCache() {
META_CACHE.clear();
}
private static TaosTableMeta doParse(Class<?> entityClass) {
STable sTable = entityClass.getAnnotation(STable.class);
if (sTable == null) {
throw new IllegalArgumentException(
"实体类必须使用 @STable 注解:" + entityClass.getName()
);
}
TaosTableMeta meta = new TaosTableMeta();
meta.setStableName(sTable.value());
meta.setTableNameTemplate(sTable.table());
meta.setUsing(sTable.using());
// 解析字段
FieldMeta timeField = null;
for (Field field : entityClass.getDeclaredFields()) {
field.setAccessible(true);
if (field.isAnnotationPresent(IotTableId.class)) {
// 时间戳字段
if (timeField != null) {
throw new IllegalArgumentException(
"实体类只能有一个 @IotTableId 字段:" + entityClass.getName()
);
}
timeField = parseField(field, Types.TIMESTAMP);
// 验证类型
Class<?> fieldType = field.getType();
if (!Date.class.isAssignableFrom(fieldType)
&& !Long.class.isAssignableFrom(fieldType)
&& field.getType() != long.class) {
throw new IllegalArgumentException(
"@IotTableId 字段必须是 Date 或 Long 类型:" + field.getName()
);
}
} else if (field.isAnnotationPresent(IotField.class)) {
// 普通数据字段
IotField annotation = field.getAnnotation(IotField.class);
FieldMeta fieldMeta = parseField(field, annotation.type());
// 自定义字段名
if (!annotation.value().isEmpty()) {
fieldMeta.setColumnName(annotation.value());
}
// 小数位数
if (annotation.scale() >= 0) {
fieldMeta.setScale(annotation.scale());
}
meta.getDataFields().add(fieldMeta);
} else if (field.isAnnotationPresent(IotTag.class)) {
// 标签字段
IotTag annotation = field.getAnnotation(IotTag.class);
FieldMeta fieldMeta = parseField(field, annotation.type());
// 自定义标签名
if (!annotation.value().isEmpty()) {
fieldMeta.setColumnName(annotation.value());
}
meta.getTagFields().add(fieldMeta);
}
}
if (timeField == null) {
throw new IllegalArgumentException(
"实体类必须有且仅有一个 @IotTableId 字段:" + entityClass.getName()
);
}
meta.setTimeField(timeField);
return meta;
}
private static FieldMeta parseField(Field field, int columnType) {
FieldMeta meta = new FieldMeta(field);
// 根据 Java 类型推断数据库类型
if (columnType == Types.NULL) {
columnType = inferSqlType(field.getType());
}
meta.setColumnType(columnType);
return meta;
}
/**
* 根据 Java 类型推断 SQL 类型
*/
private static int inferSqlType(Class<?> type) {
if (type == String.class) {
return Types.VARCHAR;
} else if (type == Integer.class || type == int.class) {
return Types.INTEGER;
} else if (type == Long.class || type == long.class) {
return Types.BIGINT;
} else if (type == Double.class || type == double.class) {
return Types.DOUBLE;
} else if (type == Float.class || type == float.class) {
return Types.FLOAT;
} else if (type == Boolean.class || type == boolean.class) {
return Types.BOOLEAN;
} else if (java.util.Date.class.isAssignableFrom(type)
|| java.sql.Date.class.isAssignableFrom(type)
|| java.sql.Timestamp.class.isAssignableFrom(type)) {
return Types.TIMESTAMP;
} else {
return Types.VARCHAR;
}
}
}
5.5 动态表名解析器
src/main/java/com/example/taos/sql/TableNameResolver.java
java
package com.example.taos.sql;
import java.lang.reflect.Field;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 动态表名解析器
* 解析表名模板中的 ${fieldName} 占位符
*/
public class TableNameResolver {
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\$\\{([^}]+)\\}");
/**
* 解析表名
* 示例:tableTemplate = "t_${deviceId}", entity.deviceId = "device001"
* 返回:"t_device001"
*/
public static String resolve(String tableTemplate, Object entity) {
if (tableTemplate == null || tableTemplate.isEmpty()) {
throw new IllegalArgumentException("表名模板不能为空");
}
Matcher matcher = PLACEHOLDER_PATTERN.matcher(tableTemplate);
String tableName = tableTemplate;
while (matcher.find()) {
String fieldName = matcher.group(1);
String fieldValue = getFieldValue(entity, fieldName);
if (fieldValue == null) {
throw new IllegalArgumentException(
"解析表名失败,字段 [" + fieldName + "] 的值为 null"
);
}
tableName = tableName.replace("${" + fieldName + "}", fieldValue);
}
return tableName;
}
/**
* 获取字段值
*/
private static String getFieldValue(Object entity, String fieldName) {
try {
Field field = entity.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
Object value = field.get(entity);
return value != null ? value.toString() : null;
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new IllegalArgumentException(
"无法获取字段值:" + fieldName, e
);
}
}
}
六、数据访问层实现
6.1 数据源配置类
src/main/java/com/example/taos/config/TaosConfig.java
java
package com.example.taos.config;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
/**
* TDengine 数据源配置
*/
@Configuration
public class TaosConfig {
/**
* 创建 TDengine 数据源
*/
@Bean("taosDataSource")
@ConfigurationProperties(prefix = "taos.datasource")
public DataSource taosDataSource() {
return new HikariDataSource();
}
}
6.2 TDengine 操作接口
src/main/java/com/example/taos/repository/TaosRepository.java
java
package com.example.taos.repository;
import java.util.List;
/**
* TDengine 数据访问接口
*/
public interface TaosRepository {
/**
* 插入单条数据
* @param entity 实体对象
* @return 影响的行数
*/
int insert(Object entity);
/**
* 批量插入数据
* @param entities 实体对象列表
* @return 影响的行数
*/
int batchInsert(List<Object> entities);
/**
* 分批批量插入数据
* @param entities 实体对象列表
* @param batchSize 每批数量
* @return 影响的行数
*/
int batchInsert(List<Object> entities, int batchSize);
}
6.3 TDengine 操作实现类
src/main/java/com/example/taos/repository/TaosRepositoryImpl.java
java
package com.example.taos.repository;
import com.example.taos.annotation.IotTableId;
import com.example.taos.annotation.STable;
import com.example.taos.sql.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Repository;
import javax.sql.DataSource;
import java.sql.*;
import java.util.*;
/**
* TDengine 数据访问实现类
*/
@Repository
public class TaosRepositoryImpl implements TaosRepository {
private static final Logger logger = LoggerFactory.getLogger(TaosRepositoryImpl.class);
private final DataSource dataSource;
public TaosRepositoryImpl(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public int insert(Object entity) {
try {
SqlExecContext context = buildInsertContext(entity);
return executeUpdate(context);
} catch (Exception e) {
logger.error("插入数据失败", e);
throw new RuntimeException("插入数据失败", e);
}
}
@Override
public int batchInsert(List<Object> entities) {
if (entities == null || entities.isEmpty()) {
return 0;
}
try {
return executeBatch(entities);
} catch (Exception e) {
logger.error("批量插入数据失败", e);
throw new RuntimeException("批量插入数据失败", e);
}
}
@Override
public int batchInsert(List<Object> entities, int batchSize) {
if (entities == null || entities.isEmpty()) {
return 0;
}
int totalInserted = 0;
int totalSize = entities.size();
int batchCount = (totalSize + batchSize - 1) / batchSize;
for (int i = 0; i < batchCount; i++) {
int fromIndex = i * batchSize;
int toIndex = Math.min(fromIndex + batchSize, totalSize);
List<Object> batch = entities.subList(fromIndex, toIndex);
totalInserted += batchInsert(batch);
}
return totalInserted;
}
/**
* 构建 INSERT SQL 上下文
*/
private SqlExecContext buildInsertContext(Object entity) throws Exception {
Class<?> entityClass = entity.getClass();
// 解析元数据
TaosTableMeta meta = TaosMetaExtractor.parse(entityClass);
// 解析表名 (支持动态表名)
String tableName = TableNameResolver.resolve(
meta.getTableNameTemplate(), entity
);
// 构建 SQL
StringBuilder sql = new StringBuilder();
sql.append("INSERT INTO ").append(tableName);
// 如果使用超级表自动创建
if (meta.isUsing() && meta.getStableName() != null && !meta.getStableName().isEmpty()) {
String usingClause = meta.generateUsingClause();
if (usingClause != null) {
sql.append(" ").append(usingClause);
}
String tagValues = meta.generateTagValues();
if (tagValues != null) {
sql.append(" tags ").append(tagValues);
}
}
// 数据字段
sql.append(" ").append(meta.generateInsertSql());
sql.append(" VALUES ");
sql.append(meta.generateParamSql());
// 构建参数
List<Object> params = new ArrayList<>();
// 时间戳参数
FieldMeta timeField = meta.getTimeField();
Object timeValue = timeField.getFieldValue(entity);
if (timeValue == null) {
// 自动生成时间戳
timeValue = new java.util.Date();
}
params.add(convertTimestamp(timeValue));
// 数据字段参数
for (FieldMeta field : meta.getDataFields()) {
params.add(field.getFieldValue(entity));
}
// TAG 字段参数 (如果有 USING 子句)
if (meta.isUsing() && !meta.getTagFields().isEmpty()) {
for (FieldMeta tagField : meta.getTagFields()) {
params.add(tagField.getFieldValue(entity));
}
}
return new SqlExecContext(sql.toString(), params.toArray());
}
/**
* 转换时间戳
*/
private Object convertTimestamp(Object timeValue) {
if (timeValue instanceof java.util.Date) {
return new Timestamp(((java.util.Date) timeValue).getTime());
} else if (timeValue instanceof Long) {
return new Timestamp((Long) timeValue);
} else {
return timeValue;
}
}
/**
* 批量执行
*/
private int executeBatch(List<Object> entities) throws Exception {
if (entities.isEmpty()) {
return 0;
}
Class<?> entityClass = entities.get(0).getClass();
TaosTableMeta meta = TaosMetaExtractor.parse(entityClass);
// 按表名分组 (不同设备的数据表可能不同)
Map<String, List<Object>> groupedEntities = new HashMap<>();
for (Object entity : entities) {
String tableName = TableNameResolver.resolve(
meta.getTableNameTemplate(), entity
);
groupedEntities.computeIfAbsent(tableName, k -> new ArrayList<>()).add(entity);
}
// 按表名分别执行批量插入
int totalInserted = 0;
for (Map.Entry<String, List<Object>> entry : groupedEntities.entrySet()) {
String tableName = entry.getKey();
List<Object> tableEntities = entry.getValue();
// 构建批量 SQL
StringBuilder sql = new StringBuilder();
sql.append("INSERT INTO ").append(tableName);
// USING 子句 (只取第一个实体的 TAG 值)
if (meta.isUsing() && meta.getStableName() != null && !meta.getStableName().isEmpty()) {
String usingClause = meta.generateUsingClause();
if (usingClause != null) {
sql.append(" ").append(usingClause);
}
Object firstEntity = tableEntities.get(0);
String tagValues = buildTagValues(firstEntity, meta);
if (tagValues != null) {
sql.append(" tags ").append(tagValues);
}
}
sql.append(" ").append(meta.generateInsertSql());
sql.append(" VALUES ");
// 多行 VALUES
List<Object> allParams = new ArrayList<>();
for (int i = 0; i < tableEntities.size(); i++) {
if (i > 0) sql.append(", ");
sql.append(meta.generateParamSql());
Object entity = tableEntities.get(i);
List<Object> params = buildParams(entity, meta);
allParams.addAll(params);
}
SqlExecContext context = new SqlExecContext(sql.toString(), allParams.toArray());
totalInserted += executeUpdate(context);
}
return totalInserted;
}
/**
* 构建 TAG 值字符串
*/
private String buildTagValues(Object entity, TaosTableMeta meta) {
if (meta.getTagFields().isEmpty()) {
return null;
}
StringBuilder sb = new StringBuilder("(");
for (int i = 0; i < meta.getTagFields().size(); i++) {
if (i > 0) sb.append(", ");
try {
Object value = meta.getTagFields().get(i).getFieldValue(entity);
if (value instanceof String) {
sb.append("'").append(value).append("'");
} else {
sb.append(value);
}
} catch (IllegalAccessException e) {
throw new RuntimeException("获取 TAG 字段值失败", e);
}
}
sb.append(")");
return sb.toString();
}
/**
* 构建参数列表
*/
private List<Object> buildParams(Object entity, TaosTableMeta meta) throws IllegalAccessException {
List<Object> params = new ArrayList<>();
// 时间戳
FieldMeta timeField = meta.getTimeField();
Object timeValue = timeField.getFieldValue(entity);
if (timeValue == null) {
timeValue = new java.util.Date();
}
params.add(convertTimestamp(timeValue));
// 数据字段
for (FieldMeta field : meta.getDataFields()) {
params.add(field.getFieldValue(entity));
}
return params;
}
/**
* 执行 SQL 更新
*/
private int executeUpdate(SqlExecContext context) throws SQLException {
long startTime = System.currentTimeMillis();
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(context.getSql())) {
// 设置参数
Object[] params = context.getParams();
for (int i = 0; i < params.length; i++) {
stmt.setObject(i + 1, params[i]);
}
int rows = stmt.executeUpdate();
if (logger.isTraceEnabled()) {
logger.trace("TDengine 执行成功 ({}ms): {} 行",
System.currentTimeMillis() - startTime, rows);
}
return rows;
}
}
}
七、业务服务层
src/main/java/com/example/taos/service/DeviceDataService.java
java
package com.example.taos.service;
import com.example.taos.entity.DeviceData;
import com.example.taos.repository.TaosRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 设备数据服务类
*/
@Service
public class DeviceDataService {
private static final Logger logger = LoggerFactory.getLogger(DeviceDataService.class);
@Autowired
private TaosRepository taosRepository;
/**
* 保存单条设备数据 (异步)
*/
@Async
public void saveData(DeviceData data) {
data.setTs(new Date());
taosRepository.insert(data);
logger.debug("保存设备数据:deviceId={}, voltage={}",
data.getDeviceId(), data.getVoltage());
}
/**
* 批量保存设备数据 (异步)
*/
@Async
public void batchSave(List<DeviceData> dataList) {
for (DeviceData data : dataList) {
data.setTs(new Date());
}
int rows = taosRepository.batchInsert(dataList);
logger.info("批量保存设备数据:{} 条,成功 {} 行", dataList.size(), rows);
}
/**
* 分批保存设备数据 (适合大数据量)
*/
@Async
public void batchSaveWithSize(List<DeviceData> dataList, int batchSize) {
for (DeviceData data : dataList) {
data.setTs(new Date());
}
int rows = taosRepository.batchInsert(dataList, batchSize);
logger.info("分批保存设备数据:{} 条 (每批 {} 条),成功 {} 行",
dataList.size(), batchSize, rows);
}
/**
* 模拟设备数据采集
*/
public DeviceData simulateData(String deviceId, String location, String deviceType) {
DeviceData data = new DeviceData(deviceId);
data.setLocation(location);
data.setDeviceType(deviceType);
// 模拟传感器数据
data.setVoltage(220.0 + Math.random() * 10);
data.setCurrent(5.0 + Math.random() * 2);
data.setActivePower(1000.0 + Math.random() * 100);
data.setReactivePower(50.0 + Math.random() * 10);
data.setPowerFactor(0.95 + Math.random() * 0.05);
return data;
}
/**
* 批量模拟数据
*/
public List<DeviceData> simulateBatchData(int count, String deviceId,
String location, String deviceType) {
List<DeviceData> dataList = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
dataList.add(simulateData(deviceId, location, deviceType));
}
return dataList;
}
}
八、控制器层
src/main/java/com/example/taos/controller/DeviceDataController.java
java
package com.example.taos.controller;
import com.example.taos.entity.DeviceData;
import com.example.taos.service.DeviceDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 设备数据控制器
*/
@RestController
@RequestMapping("/api/device")
@CrossOrigin(origins = "*")
public class DeviceDataController {
@Autowired
private DeviceDataService deviceDataService;
/**
* 上报单条设备数据
*/
@PostMapping("/report")
public Map<String, Object> reportData(@RequestBody DeviceData data) {
Map<String, Object> result = new HashMap<>();
try {
deviceDataService.saveData(data);
result.put("code", 200);
result.put("message", "数据上报成功");
} catch (Exception e) {
result.put("code", 500);
result.put("message", "数据上报失败:" + e.getMessage());
e.printStackTrace();
}
return result;
}
/**
* 批量上报设备数据
*/
@PostMapping("/batch-report")
public Map<String, Object> batchReport(@RequestBody List<DeviceData> dataList) {
Map<String, Object> result = new HashMap<>();
try {
deviceDataService.batchSave(dataList);
result.put("code", 200);
result.put("message", "批量上报成功,共 " + dataList.size() + " 条");
} catch (Exception e) {
result.put("code", 500);
result.put("message", "批量上报失败:" + e.getMessage());
e.printStackTrace();
}
return result;
}
/**
* 模拟设备数据生成
*/
@GetMapping("/simulate")
public Map<String, Object> simulate(
@RequestParam(defaultValue = "device001") String deviceId,
@RequestParam(defaultValue = "10") int count
) {
Map<String, Object> result = new HashMap<>();
try {
List<DeviceData> dataList = deviceDataService.simulateBatchData(
count, deviceId, "北京机房", "智能电表"
);
deviceDataService.batchSaveWithSize(dataList, 100);
result.put("code", 200);
result.put("message", "模拟生成 " + count + " 条数据成功");
result.put("data", dataList);
} catch (Exception e) {
result.put("code", 500);
result.put("message", "模拟失败:" + e.getMessage());
e.printStackTrace();
}
return result;
}
/**
* 健康检查接口
*/
@GetMapping("/health")
public Map<String, Object> health() {
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("message", "服务正常运行");
return result;
}
}
九、实体类定义
src/main/java/com/example/taos/entity/DeviceData.java
java
package com.example.taos.entity;
import com.example.taos.annotation.IotTableId;
import com.example.taos.annotation.STable;
import com.example.taos.annotation.IotField;
import com.example.taos.annotation.IotTag;
import java.util.Date;
/**
* 智能电表数据采集实体
*
* 数据库映射:
* - 超级表:meters
* - 子表:t_device001, t_device002, ...
*/
@STable(
value = "meters", // 超级表名
table = "t_${deviceId}", // 子表名模板 (自动替换 ${deviceId})
using = true // 启用自动创建子表
)
public class DeviceData {
// ========== 时间戳字段 (必填) ==========
@IotTableId
private Date ts;
// ========== 普通数据字段 ==========
/** 电压 (V) */
@IotField("voltage")
private Double voltage;
/** 电流 (A) */
@IotField("current")
private Double current;
/** 有功功率 (W) */
@IotField("active_power")
private Double activePower;
/** 无功功率 (var) */
@IotField("reactive_power")
private Double reactivePower;
/** 功率因数 */
@IotField("power_factor")
private Double powerFactor;
// ========== 标签字段 (用于设备维度查询) ==========
/** 设备编号 */
@IotTag("device_id")
private String deviceId;
/** 安装位置 */
@IotTag("location")
private String location;
/** 设备类型 */
@IotTag("device_type")
private String deviceType;
// 默认构造函数
public DeviceData() {
this.ts = new Date();
}
// 带设备编号的构造函数
public DeviceData(String deviceId) {
this();
this.deviceId = deviceId;
}
// Getter 和 Setter
public Date getTs() {
return ts;
}
public void setTs(Date ts) {
this.ts = ts;
}
public Double getVoltage() {
return voltage;
}
public void setVoltage(Double voltage) {
this.voltage = voltage;
}
public Double getCurrent() {
return current;
}
public void setCurrent(Double current) {
this.current = current;
}
public Double getActivePower() {
return activePower;
}
public void setActivePower(Double activePower) {
this.activePower = activePower;
}
public Double getReactivePower() {
return reactivePower;
}
public void setReactivePower(Double reactivePower) {
this.reactivePower = reactivePower;
}
public Double getPowerFactor() {
return powerFactor;
}
public void setPowerFactor(Double powerFactor) {
this.powerFactor = powerFactor;
}
public String getDeviceId() {
return deviceId;
}
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public String getDeviceType() {
return deviceType;
}
public void setDeviceType(String deviceType) {
this.deviceType = deviceType;
}
@Override
public String toString() {
return "DeviceData{" +
"ts=" + ts +
", deviceId='" + deviceId + '\'' +
", voltage=" + voltage +
", current=" + current +
", activePower=" + activePower +
", reactivePower=" + reactivePower +
", powerFactor=" + powerFactor +
'}';
}
}
十、启动类
src/main/java/com/example/taos/TaosDemoApplication.java
java
package com.example.taos;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
/**
* TDengine Demo 启动类
*/
@SpringBootApplication
@EnableAsync // 启用异步处理
public class TaosDemoApplication {
public static void main(String[] args) {
SpringApplication.run(TaosDemoApplication.class, args);
System.out.println("=========================================");
System.out.println(" TDengine Demo 启动成功!");
System.out.println(" API 地址:http://localhost:8080/api/device");
System.out.println(" 健康检查:http://localhost:8080/api/device/health");
System.out.println("=========================================");
}
}
十一、初始化数据库
11.1 创建数据库和超级表
使用 taos shell 或客户端工具执行:
sql
-- 创建数据库
CREATE DATABASE IF NOT EXISTS iot;
-- 使用数据库
USE iot;
-- 创建超级表
CREATE STABLE IF NOT EXISTS meters (
ts TIMESTAMP,
voltage DOUBLE,
current DOUBLE,
active_power DOUBLE,
reactive_power DOUBLE,
power_factor DOUBLE
) TAGS (
device_id VARCHAR(50),
location VARCHAR(50),
device_type VARCHAR(50)
);
-- 查看超级表结构
DESCRIBE meters;
-- 查看数据库
SHOW DATABASES;
11.2 验证连接
bash
# 进入 taos shell
taos
# 执行查询
USE iot;
SELECT * FROM meters LIMIT 10;
十二、测试验证
12.1 启动项目
bash
mvn spring-boot:run
看到以下输出表示启动成功:
=========================================
TDengine Demo 启动成功!
API 地址:http://localhost:8080/api/device
健康检查:http://localhost:8080/api/device/health
=========================================
12.2 使用 cURL 测试
健康检查
bash
curl http://localhost:8080/api/device/health
单条数据上报
bash
curl -X POST http://localhost:8080/api/device/report \
-H "Content-Type: application/json" \
-d '{
"deviceId": "device001",
"location": "北京机房",
"deviceType": "智能电表",
"voltage": 220.5,
"current": 10.2,
"activePower": 2200.5,
"reactivePower": 100.3,
"powerFactor": 0.95
}'
批量数据上报
bash
curl -X POST http://localhost:8080/api/device/batch-report \
-H "Content-Type: application/json" \
-d '[
{
"deviceId": "device001",
"location": "北京机房",
"deviceType": "智能电表",
"voltage": 220.5,
"current": 10.2,
"activePower": 2200.5,
"reactivePower": 100.3,
"powerFactor": 0.95
},
{
"deviceId": "device002",
"location": "上海机房",
"deviceType": "智能电表",
"voltage": 219.8,
"current": 9.8,
"activePower": 2150.0,
"reactivePower": 98.5,
"powerFactor": 0.94
}
]'
模拟数据生成
bash
curl "http://localhost:8080/api/device/simulate?deviceId=device003&count=100"
12.3 在 TDengine 中查询
sql
-- 使用数据库
USE iot;
-- 查询所有数据
SELECT * FROM meters;
-- 查询特定设备的数据
SELECT * FROM t_device001;
-- 按设备统计
SELECT device_id, COUNT(*), AVG(voltage), AVG(current)
FROM meters
GROUP BY device_id;
-- 时间范围查询
SELECT * FROM meters
WHERE ts >= '2024-01-01 00:00:00' AND ts <= NOW();
-- 查看所有子表
SHOW TABLES;
十三、性能优化建议
13.1 批量写入
java
// ✅ 推荐:批量写入,每批 1000-5000 条
service.batchSaveWithSize(dataList, 1000);
// ❌ 避免:单条循环写入
for (DeviceData data : dataList) {
service.saveData(data); // 性能差
}
13.2 异步处理
java
@Service
public class DeviceDataService {
@Async
public void saveData(DeviceData data) {
// 异步写入,不阻塞主线程
taosRepository.insert(data);
}
}
配置异步线程池:
java
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("taos-async-");
executor.initialize();
return executor;
}
}
13.3 连接池优化
yaml
taos:
datasource:
# 最小连接数
minimum-idle: 5
# 最大连接数
maximum-pool-size: 20
# 连接超时时间 (ms)
connection-timeout: 30000
# 空闲连接超时时间 (ms)
idle-timeout: 600000
# 连接最大生命周期 (ms)
max-lifetime: 1800000
13.4 表设计优化
- 合理设计 Tag: 将用于查询过滤的字段设为 Tag
- 子表数量控制: 建议单超级表下子表数量不超过 10 万
- 数据保留策略: 配置合适的数据保留时长
- 分区策略: 按时间自动分区
十四、常见问题
Q1: 连接失败 "Unable to load JNI libraries"
原因: TDengine JDBC 驱动依赖本地 JNI 库
解决方案:
Linux:
bash
# 安装 TDengine 客户端
yum install -y TDengine-client
# 或设置 LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/usr/local/taos/driver:$LD_LIBRARY_PATH
Docker:
bash
# 确保容器已安装 TDengine 客户端
docker exec -it tdengine taos
Q2: 中文乱码
解决方案: 在 JDBC URL 中添加字符集参数
yaml
taos:
datasource:
url: jdbc:TAOS://localhost:6030/iot?charset=UTF-8
Q3: 批量插入性能低
优化建议:
- 增加批次大小(1000-5000 条/批)
- 使用异步写入
- 增加连接池大小
- 检查网络延迟
Q4: 表不存在错误
解决方案:
- 确保
@STable(using = true)已设置 - 检查表名模板
${fieldName}对应字段是否有值 - 手动创建超级表
十五、项目目录结构
tdengine-demo/
├── src/
│ └── main/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── taos/
│ │ ├── TaosDemoApplication.java # 启动类
│ │ ├── annotation/ # 自定义注解
│ │ │ ├── STable.java
│ │ │ ├── IotTableId.java
│ │ │ ├── IotField.java
│ │ │ └── IotTag.java
│ │ ├── config/ # 配置类
│ │ │ └── TaosConfig.java
│ │ ├── controller/ # 控制器
│ │ │ └── DeviceDataController.java
│ │ ├── entity/ # 实体类
│ │ │ └── DeviceData.java
│ │ ├── repository/ # 数据访问层
│ │ │ ├── TaosRepository.java
│ │ │ └── TaosRepositoryImpl.java
│ │ ├── service/ # 服务层
│ │ │ └── DeviceDataService.java
│ │ └── sql/ # SQL 工具类
│ │ ├── FieldMeta.java
│ │ ├── TaosTableMeta.java
│ │ ├── SqlExecContext.java
│ │ ├── TaosMetaExtractor.java
│ │ └── TableNameResolver.java
│ └── resources/
│ └── application.yml # 配置文件
├── pom.xml # Maven 配置
└── README.md # 项目说明
十六、总结
本文从零开始介绍如何在 Spring Boot 项目中集成 TDengine 时序数据库,包含:
- ✅ 完整的依赖配置 - Maven 依赖和配置文件
- ✅ 自定义注解实现 - 4 个核心注解的完整代码
- ✅ 工具类实现 - 元数据解析、SQL 生成、表名解析
- ✅ 数据访问层 - Repository 接口和实现
- ✅ 完整业务代码 - Service 和 Controller
- ✅ 可运行 Demo - 复制即可使用
- ✅ 性能优化 - 批量写入、异步处理、连接池配置
- ✅ 常见问题 - 连接、乱码、性能等问题解决方案
TDengine 作为国产开源时序数据库,在 IoT 场景下表现出色。通过超级表 + 子表的设计,可以轻松应对海量设备数据采集场景。