Skip to content

自定义缓存规则节点 ​

正文与代码来自原稿。对齐 TB 自带缓存体系,增加 ruleChainCache,规则节点内读写,避免反复查库。

1、Caffeine / Redis 缓存实现 ​

RuleChainCaffeineCache ​

java
package org.thingsboard.server.cache;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.CacheConstants;

@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("RuleChainCache")
public class RuleChainCaffeineCache extends CaffeineTbTransactionalCache<String, String> {

    public RuleChainCaffeineCache(CacheManager cacheManager) {
        super(cacheManager, CacheConstants.RULE_CHAIN_CACHE);
    }
}

RuleChainRedisCache ​

java
package org.thingsboard.server.cache;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.CacheConstants;

@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("RuleChainCache")
public class RuleChainRedisCache extends RedisTbTransactionalCache<String, String> {

    public RuleChainRedisCache(TBRedisCacheConfiguration configuration,
                               CacheSpecsMap cacheSpecsMap,
                               RedisConnectionFactory connectionFactory) {
        super(CacheConstants.RULE_CHAIN_CACHE, cacheSpecsMap, connectionFactory,
                configuration, new TbFSTRedisSerializer<>());
    }
}

2、CacheConstants ​

java
public static final String RULE_CHAIN_CACHE = "ruleChainCache";

3、thingsboard.yml ​

yaml
cache:
  type: "${CACHE_TYPE:caffeine}"
  maximumPoolSize: "${CACHE_MAXIMUM_POOL_SIZE:16}"
  attributes:
    enabled: "${CACHE_ATTRIBUTES_ENABLED:true}"
  specs:
    ruleChainCache:
      timeToLiveInMinutes: "${CACHE_SPECS_RULE_CHAIN_CACHE_TTL:1440}"
      maxSize: "${CACHE_SPECS_RULE_CHAIN_CACHE_MAX_SIZE:10000}"

使用 Redis 时注意 maxmemory-policy(如 allkeys-lru)等官方要求。

4、自定义节点 TbCacheNode ​

通过 SpringContextUtil 取对应 Bean,对消息或元数据做 SET / GET。

java
package org.thingsboard.rule.engine.custom.cache;

import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.*;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.cache.RuleChainCaffeineCache;
import org.thingsboard.server.cache.RuleChainRedisCache;
import org.thingsboard.server.cache.TbTransactionalCache;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.util.SpringContextUtil;

import java.util.Map;
import java.util.concurrent.ExecutionException;

import static org.thingsboard.common.util.DonAsynchron.withCallback;

@RuleNode(
        type = ComponentType.ACTION,
        name = "use cache",
        configClazz = TbCacheNodeConfiguration.class,
        nodeDescription = "Use cache",
        nodeDetails = "Use cache",
        uiResources = {"static/rulenode/rulenode-core-config.js"},
        icon = "add_circle")
@Slf4j
public class TbCacheNode implements TbNode {

    private TbCacheNodeConfiguration config;
    private TbTransactionalCache<String, String> tbTransactionalCache;

    @Override
    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
        this.config = TbNodeUtils.convert(configuration, TbCacheNodeConfiguration.class);
        if ("caffeine".equals(config.getCacheType())) {
            this.tbTransactionalCache = SpringContextUtil.getBean(RuleChainCaffeineCache.class);
        } else if ("redis".equals(config.getCacheType())) {
            this.tbTransactionalCache = SpringContextUtil.getBean(RuleChainRedisCache.class);
        } else {
            throw new RuntimeException("Cache type error");
        }
    }

    @Override
    public void onMsg(TbContext ctx, TbMsg msg)
            throws ExecutionException, InterruptedException, TbNodeException {
        try {
            withCallback(operateCache(msg),
                    res -> ctx.tellSuccess(msg),
                    t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
        } catch (Exception e) {
            e.printStackTrace();
            ctx.tellFailure(msg, e);
        }
    }

    private ListenableFuture<? extends Object> operateCache(TbMsg msg) {
        if ("SET".equals(config.getMethod())) {
            if (ObjectUtils.isEmpty(config.getCacheKey())
                    || ObjectUtils.isEmpty(config.getCacheValueKey())) {
                throw new RuntimeException("parameter error");
            }
            if ("msg".equals(config.getMsgMethod())) {
                JsonNode jsonNode = JacksonUtil.toJsonNode(msg.getData());
                String key = jsonNode.get(config.getCacheKey()).asText();
                String value = jsonNode.get(config.getCacheValueKey()).asText();
                tbTransactionalCache.put(key, value);
            } else if ("metadata".equals(config.getMsgMethod())) {
                Map<String, String> data = msg.getMetaData().getData();
                String key = data.get(config.getCacheKey());
                String value = data.get(config.getCacheValueKey());
                tbTransactionalCache.put(key, value);
            } else {
                throw new RuntimeException("method only msg or metadata");
            }
            return Futures.immediateFuture(true);
        } else if ("GET".equals(config.getMethod())) {
            if (ObjectUtils.isEmpty(config.getCacheKey())
                    || ObjectUtils.isEmpty(config.getCacheValueKey())) {
                throw new RuntimeException("parameter error");
            }
            if ("msg".equals(config.getMsgMethod())) {
                JsonNode jsonNode = JacksonUtil.toJsonNode(msg.getData());
                String key = jsonNode.get(config.getCacheKey()).asText();
                Object value = tbTransactionalCache.get(key).get();
                msg.getMetaData().putValue(config.getCacheValueKey(), (String) value);
                return Futures.immediateFuture(true);
            } else if ("metadata".equals(config.getMsgMethod())) {
                Map<String, String> data = msg.getMetaData().getData();
                String key = data.get(config.getCacheKey());
                Object value = tbTransactionalCache.get(key).get();
                msg.getMetaData().putValue(config.getCacheValueKey(), (String) value);
                return Futures.immediateFuture(true);
            } else {
                throw new RuntimeException("method only msg or metadata");
            }
        } else {
            throw new RuntimeException("method only SET or GET");
        }
    }
}

TbCacheNodeConfiguration ​

java
package org.thingsboard.rule.engine.custom.cache;

import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;

@Data
public class TbCacheNodeConfiguration implements NodeConfiguration<TbCacheNodeConfiguration> {

    private String cacheType;      // caffeine、redis
    private String msgMethod;      // msg 或 metadata
    private String method;         // SET、GET
    private String cacheKey;       // 用作缓存 key 的字段名
    private String cacheValueKey;  // SET 时取 value 的字段;GET 时写入 metadata 的键

    @Override
    public TbCacheNodeConfiguration defaultConfiguration() {
        TbCacheNodeConfiguration configuration = new TbCacheNodeConfiguration();
        configuration.setCacheType("caffeine");
        configuration.setMsgMethod("metadata");
        configuration.setMethod("SET");
        configuration.setCacheKey("");
        configuration.setCacheValueKey("");
        return configuration;
    }
}

辅

SpringContextUtil 需自行提供或从 ApplicationContext 注入;GET 后建议 TbMsg.transformMsg 再向下传递,避免原地改元数据在部分版本不生效。

5、小结 ​

先注册缓存 specs,再在规则节点里 SET/GET,适合跨消息共享中间状态(计数、会话标记等)。