Flink读写Redis(二)-flink-redis-connector代码学习
源码结构

RedisSink
package org.apache.flink.streaming.connectors.redis;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.sink.RichSinkFunction;
import org.apache.flink.streaming.connectors.redis.common.config.FlinkJedisClusterConfig;
import org.apache.flink.streaming.connectors.redis.common.config.FlinkJedisConfigBase;
import org.apache.flink.streaming.connectors.redis.common.config.FlinkJedisPoolConfig;
import org.apache.flink.streaming.connectors.redis.common.config.FlinkJedisSentinelConfig;
import org.apache.flink.streaming.connectors.redis.common.container.RedisCommandsContainer;
import org.apache.flink.streaming.connectors.redis.common.container.RedisCommandsContainerBuilder;
import org.apache.flink.streaming.connectors.redis.common.mapper.RedisCommand;
import org.apache.flink.streaming.connectors.redis.common.mapper.RedisDataType;
import org.apache.flink.streaming.connectors.redis.common.mapper.RedisCommandDescription;
import org.apache.flink.streaming.connectors.redis.common.mapper.RedisMapper;
import org.apache.flink.util.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
/**
 * A sink that delivers data to a Redis channel using the Jedis client.
 * <p> The sink takes two arguments {@link FlinkJedisConfigBase} and {@link RedisMapper}.
 * <p> When {@link FlinkJedisPoolConfig} is passed as the first argument,
 * the sink will create connection using {@link redis.clients.jedis.JedisPool}. Please use this when
 * you want to connect to a single Redis server.
 * <p> When {@link FlinkJedisSentinelConfig} is passed as the first argument, the sink will create connection
 * using {@link redis.clients.jedis.JedisSentinelPool}. Please use this when you want to connect to Sentinel.
 * <p> Please use {@link FlinkJedisClusterConfig} as the first argument if you want to connect to
 * a Redis Cluster.
 *
 * <p>Example:
 *
 * <pre>
 *{@code
 *public static class RedisExampleMapper implements RedisMapper<Tuple2<String, String>> {
 *
 *	private RedisCommand redisCommand;
 *
 *	public RedisExampleMapper(RedisCommand redisCommand){
 *		this.redisCommand = redisCommand;
 *	}
 *	public RedisCommandDescription getCommandDescription() {
 *		return new RedisCommandDescription(redisCommand, REDIS_ADDITIONAL_KEY);
 *	}
 *	public String getKeyFromData(Tuple2<String, String> data) {
 *		return data.f0;
 *	}
 *	public String getValueFromData(Tuple2<String, String> data) {
 *		return data.f1;
 *	}
 *}
 *JedisPoolConfig jedisPoolConfig = new JedisPoolConfig.Builder()
 *    .setHost(REDIS_HOST).setPort(REDIS_PORT).build();
 *new RedisSink<String>(jedisPoolConfig, new RedisExampleMapper(RedisCommand.LPUSH));
 *}</pre>
 *
 * @param <IN> Type of the elements emitted by this sink
 */
public class RedisSink<IN> extends RichSinkFunction<IN> {
	private static final long serialVersionUID = 1L;
	private static final Logger LOG = LoggerFactory.getLogger(RedisSink.class);
	/**
	 * This additional key needed for {@link RedisDataType#HASH} and {@link RedisDataType#SORTED_SET}.
	 * Other {@link RedisDataType} works only with two variable i.e. name of the list and value to be added.
	 * But for {@link RedisDataType#HASH} and {@link RedisDataType#SORTED_SET} we need three variables.
	 * <p>For {@link RedisDataType#HASH} we need hash name, hash key and element.
	 * {@code additionalKey} used as hash name for {@link RedisDataType#HASH}
	 * <p>For {@link RedisDataType#SORTED_SET} we need set name, the element and it's score.
	 * {@code additionalKey} used as set name for {@link RedisDataType#SORTED_SET}
	 */
	private String additionalKey;
	private RedisMapper<IN> redisSinkMapper;
	private RedisCommand redisCommand;
	private FlinkJedisConfigBase flinkJedisConfigBase;
	private RedisCommandsContainer redisCommandsContainer;
	/**
	 * Creates a new {@link RedisSink} that connects to the Redis server.
	 *
	 * @param flinkJedisConfigBase The configuration of {@link FlinkJedisConfigBase}
	 * @param redisSinkMapper This is used to generate Redis command and key value from incoming elements.
	 */
	public RedisSink(FlinkJedisConfigBase flinkJedisConfigBase, RedisMapper<IN> redisSinkMapper) {
		Preconditions.checkNotNull(flinkJedisConfigBase, "Redis connection pool config should not be null");
		Preconditions.checkNotNull(redisSinkMapper, "Redis Mapper can not be null");
		Preconditions.checkNotNull(redisSinkMapper.getCommandDescription(), "Redis Mapper data type description can not be null");
		this.flinkJedisConfigBase = flinkJedisConfigBase;
		this.redisSinkMapper = redisSinkMapper;
		RedisCommandDescription redisCommandDescription = redisSinkMapper.getCommandDescription();
		this.redisCommand = redisCommandDescription.getCommand();
		this.additionalKey = redisCommandDescription.getAdditionalKey();
	}
	/**
	 * Called when new data arrives to the sink, and forwards it to Redis channel.
	 * Depending on the specified Redis data type (see {@link RedisDataType}),
	 * a different Redis command will be applied.
	 * Available commands are RPUSH, LPUSH, SADD, PUBLISH, SET, PFADD, HSET, ZADD.
	 *
	 * @param input The incoming data
	 */
	@Override
	public void invoke(IN input) throws Exception {
		String key = redisSinkMapper.getKeyFromData(input);
		String value = redisSinkMapper.getValueFromData(input);
		switch (redisCommand) {
			case RPUSH:
				this.redisCommandsContainer.rpush(key, value);
				break;
			case LPUSH:
				this.redisCommandsContainer.lpush(key, value);
				break;
			case SADD:
				this.redisCommandsContainer.sadd(key, value);
				break;
			case SET:
				this.redisCommandsContainer.set(key, value);
				break;
			case PFADD:
				this.redisCommandsContainer.pfadd(key, value);
				break;
			case PUBLISH:
				this.redisCommandsContainer.publish(key, value);
				break;
			case ZADD:
				this.redisCommandsContainer.zadd(this.additionalKey, value, key);
				break;
			case HSET:
				this.redisCommandsContainer.hset(this.additionalKey, key, value);
				break;
			default:
				throw new IllegalArgumentException("Cannot process such data type: " + redisCommand);
		}
	}
	/**
	 * Initializes the connection to Redis by either cluster or sentinels or single server.
	 *
	 * @throws IllegalArgumentException if jedisPoolConfig, jedisClusterConfig and jedisSentinelConfig are all null
     */
	@Override
	public void open(Configuration parameters) throws Exception {
		this.redisCommandsContainer = RedisCommandsContainerBuilder.build(this.flinkJedisConfigBase);
	}
	/**
	 * Closes commands container.
	 * @throws IOException if command container is unable to close.
	 */
	@Override
	public void close() throws IOException {
		if (redisCommandsContainer != null) {
			redisCommandsContainer.close();
		}
	}
}
RedisSink类继承了RichSinkFunction类,扩展了其中的open、invoke、close方法。open方法在sink打开时执行一次,在RedisSink中,其创建了一个RedisCommandsContainer对象,该对象其实是封装了对redis的操作,包含连接redis以及不同数据类型的写入操作;close方法中执行了RedisCommandsContainer对象的close方法,其实时关闭redis连接;每当数据流入时,就会调用invoke方法,该方法中根据不同的redis数据类型,调用RedisCommandsContainer对象的不同方法将数据写入redis。
FlinkJedisConfigBase
RedisSink的构造方法中需要传入一个FlinkJedisConfigBase对象,该对象主要是用来设置一些redis连接参数,比如IP、用户、密码、连接超时等信息,在后续创建RedisCommandsContainer对象时使用,FlinkJedisConfigBase是一个抽象类,具体的实现类有FlinkJedisPoolConfig、FlinkJedisSentinelConfig、FlinkJedisClusterConfig三种,分别对应了redis、redis哨兵、redis集群不同的连接模式,比较简单,就不贴代码了。
RedisMapper接口
RedisSink的构造方法中还需要RedisMapper实现对象,需要用户自定义类实现该接口,主要是用来定义数据如何映射成redis的key和value,另外是返回一个RedisDataTypeDescription对象,该对象其实是包含了操作的redis数据类型信息
package org.apache.flink.streaming.connectors.redis.common.mapper;
import org.apache.flink.api.common.functions.Function;
import java.io.Serializable;
/**
 * Function that creates the description how the input data should be mapped to redis type.
 *<p>Example:
 *<pre>{@code
 *private static class RedisTestMapper implements RedisMapper<Tuple2<String, String>> {
 *    public RedisDataTypeDescription getCommandDescription() {
 *        return new RedisDataTypeDescription(RedisCommand.PUBLISH);
 *    }
 *    public String getKeyFromData(Tuple2<String, String> data) {
 *        return data.f0;
 *    }
 *    public String getValueFromData(Tuple2<String, String> data) {
 *        return data.f1;
 *    }
 *}
 *}</pre>
 *
 * @param <T> The type of the element handled by this {@code RedisMapper}
 */
public interface RedisMapper<T> extends Function, Serializable {
	/**
	 * Returns descriptor which defines data type.
	 *
	 * @return data type descriptor
	 */
	RedisCommandDescription getCommandDescription();
	/**
	 * Extracts key from data.
	 *
	 * @param data source data
	 * @return key
	 */
	String getKeyFromData(T data);
	/**
	 * Extracts value from data.
	 *
	 * @param data source data
	 * @return value
	 */
	String getValueFromData(T data);
}
RedisCommandDescription、RedisDataType、RedisCommand
RedisCommandDescription、RedisDataType、RedisCommand三个类用了表示操作的redis数据类型,比较简单,不在描述,支持的数据类型在RedisDataType枚举中
package org.apache.flink.streaming.connectors.redis.common.mapper;
/**
 * All available data type for Redis.
 */
public enum RedisDataType {
	/**
	 * Strings are the most basic kind of Redis value. Redis Strings are binary safe,
	 * this means that a Redis string can contain any kind of data, for instance a JPEG image or a serialized Ruby object.
	 * A String value can be at max 512 Megabytes in length.
	 */
	STRING,
	/**
	 * Redis Hashes are maps between string fields and string values.
	 */
	HASH,
	/**
	 * Redis Lists are simply lists of strings, sorted by insertion order.
	 */
	LIST,
	/**
	 * Redis Sets are an unordered collection of Strings.
	 */
	SET,
	/**
	 * Redis Sorted Sets are, similarly to Redis Sets, non repeating collections of Strings.
	 * The difference is that every member of a Sorted Set is associated with score,
	 * that is used in order to take the sorted set ordered, from the smallest to the greatest score.
	 * While members are unique, scores may be repeated.
	 */
	SORTED_SET,
	/**
	 * HyperLogLog is a probabilistic data structure used in order to count unique things.
	 */
	HYPER_LOG_LOG,
	/**
	 * Redis implementation of publish and subscribe paradigm. Published messages are characterized into channels,
	 * without knowledge of what (if any) subscribers there may be.
	 * Subscribers express interest in one or more channels, and only receive messages
	 * that are of interest, without knowledge of what (if any) publishers there are.
	 */
	PUBSUB
}
RedisCommandsContainer
RedisCommandsContainer接口定义了redis操作的方法,具体的实现类有RedisContainer、RedisClusterContainer,其中RedisContainer是用在直连redis和redis哨兵模式中,而RedisClusterContainer是用在集群模式中,具体代码不贴了,里面主要是调用了Jedis的API。
package org.apache.flink.streaming.connectors.redis.common.container;
import java.io.IOException;
import java.io.Serializable;
/**
 * The container for all available Redis commands.
 */
public interface RedisCommandsContainer extends Serializable {
	/**
	 * Sets field in the hash stored at key to value.
	 * If key does not exist, a new key holding a hash is created.
	 * If field already exists in the hash, it is overwritten.
	 *
	 * @param key Hash name
	 * @param hashField Hash field
	 * @param value Hash value
	 */
	void hset(String key, String hashField, String value);
	/**
	 * Insert the specified value at the tail of the list stored at key.
	 * If key does not exist, it is created as empty list before performing the push operation.
	 *
	 * @param listName Name of the List
	 * @param value  Value to be added
	 */
	void rpush(String listName, String value);
	/**
	 * Insert the specified value at the head of the list stored at key.
	 * If key does not exist, it is created as empty list before performing the push operation.
	 *
	 * @param listName Name of the List
	 * @param value  Value to be added
	 */
	void lpush(String listName, String value);
	/**
	 * Add the specified member to the set stored at key.
	 * Specified members that are already a member of this set are ignored.
	 * If key does not exist, a new set is created before adding the specified members.
	 *
	 * @param setName Name of the Set
	 * @param value Value to be added
	 */
	void sadd(String setName, String value);
	/**
	 * Posts a message to the given channel.
	 *
	 * @param channelName Name of the channel to which data will be published
	 * @param message the message
	 */
	void publish(String channelName, String message);
	/**
	 * Set key to hold the string value. If key already holds a value, it is overwritten,
	 * regardless of its type. Any previous time to live associated with the key is
	 * discarded on successful SET operation.
	 *
	 * @param key the key name in which value to be set
	 * @param value the value
	 */
	void set(String key, String value);
	/**
	 * Adds all the element arguments to the HyperLogLog data structure
	 * stored at the variable name specified as first argument.
	 *
	 * @param key The name of the key
	 * @param element the element
	 */
	void pfadd(String key, String element);
	/**
	 * Adds the specified member with the specified scores to the sorted set stored at key.
	 *
	 * @param key The name of the Sorted Set
	 * @param score Score of the element
	 * @param element  element to be added
	 */
	void zadd(String key, String score, String element);
	/**
	 * Close the Jedis container.
	 *
	 * @throws IOException if the instance can not be closed properly
	 */
	void close() throws IOException;
}
RedisCommandsContainerBuilder
RedisCommandsContainerBuilder类是根据不同的FlinkJedisConfigBase实现类来创建不同的RedisCommandsContainer对象
package org.apache.flink.streaming.connectors.redis.common.container;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.apache.flink.streaming.connectors.redis.common.config.FlinkJedisClusterConfig;
import org.apache.flink.streaming.connectors.redis.common.config.FlinkJedisConfigBase;
import org.apache.flink.streaming.connectors.redis.common.config.FlinkJedisPoolConfig;
import org.apache.flink.streaming.connectors.redis.common.config.FlinkJedisSentinelConfig;
import org.apache.flink.util.Preconditions;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisSentinelPool;
/**
 * The builder for {@link RedisCommandsContainer}.
 */
public class RedisCommandsContainerBuilder {
	/**
	 * Initialize the {@link RedisCommandsContainer} based on the instance type.
	 * @param flinkJedisConfigBase configuration base
	 * @return @throws IllegalArgumentException if jedisPoolConfig, jedisClusterConfig and jedisSentinelConfig are all null
     */
	public static RedisCommandsContainer build(FlinkJedisConfigBase flinkJedisConfigBase){
		if(flinkJedisConfigBase instanceof FlinkJedisPoolConfig){
			FlinkJedisPoolConfig flinkJedisPoolConfig = (FlinkJedisPoolConfig) flinkJedisConfigBase;
			return RedisCommandsContainerBuilder.build(flinkJedisPoolConfig);
		} else if (flinkJedisConfigBase instanceof FlinkJedisClusterConfig) {
			FlinkJedisClusterConfig flinkJedisClusterConfig = (FlinkJedisClusterConfig) flinkJedisConfigBase;
			return RedisCommandsContainerBuilder.build(flinkJedisClusterConfig);
		} else if (flinkJedisConfigBase instanceof FlinkJedisSentinelConfig) {
			FlinkJedisSentinelConfig flinkJedisSentinelConfig = (FlinkJedisSentinelConfig) flinkJedisConfigBase;
			return RedisCommandsContainerBuilder.build(flinkJedisSentinelConfig);
		} else {
			throw new IllegalArgumentException("Jedis configuration not found");
		}
	}
	/**
	 * Builds container for single Redis environment.
	 *
	 * @param jedisPoolConfig configuration for JedisPool
	 * @return container for single Redis environment
	 * @throws NullPointerException if jedisPoolConfig is null
	 */
	public static RedisCommandsContainer build(FlinkJedisPoolConfig jedisPoolConfig) {
		Preconditions.checkNotNull(jedisPoolConfig, "Redis pool config should not be Null");
		GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
		genericObjectPoolConfig.setMaxIdle(jedisPoolConfig.getMaxIdle());
		genericObjectPoolConfig.setMaxTotal(jedisPoolConfig.getMaxTotal());
		genericObjectPoolConfig.setMinIdle(jedisPoolConfig.getMinIdle());
		JedisPool jedisPool = new JedisPool(genericObjectPoolConfig, jedisPoolConfig.getHost(),
			jedisPoolConfig.getPort(), jedisPoolConfig.getConnectionTimeout(), jedisPoolConfig.getPassword(),
			jedisPoolConfig.getDatabase());
		return new RedisContainer(jedisPool);
	}
	/**
	 * Builds container for Redis Cluster environment.
	 *
	 * @param jedisClusterConfig configuration for JedisCluster
	 * @return container for Redis Cluster environment
	 * @throws NullPointerException if jedisClusterConfig is null
	 */
	public static RedisCommandsContainer build(FlinkJedisClusterConfig jedisClusterConfig) {
		Preconditions.checkNotNull(jedisClusterConfig, "Redis cluster config should not be Null");
		GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
		genericObjectPoolConfig.setMaxIdle(jedisClusterConfig.getMaxIdle());
		genericObjectPoolConfig.setMaxTotal(jedisClusterConfig.getMaxTotal());
		genericObjectPoolConfig.setMinIdle(jedisClusterConfig.getMinIdle());
		JedisCluster jedisCluster = new JedisCluster(jedisClusterConfig.getNodes(), jedisClusterConfig.getConnectionTimeout(),
			jedisClusterConfig.getMaxRedirections(), genericObjectPoolConfig);
		return new RedisClusterContainer(jedisCluster);
	}
	/**
	 * Builds container for Redis Sentinel environment.
	 *
	 * @param jedisSentinelConfig configuration for JedisSentinel
	 * @return container for Redis sentinel environment
	 * @throws NullPointerException if jedisSentinelConfig is null
	 */
	public static RedisCommandsContainer build(FlinkJedisSentinelConfig jedisSentinelConfig) {
		Preconditions.checkNotNull(jedisSentinelConfig, "Redis sentinel config should not be Null");
		GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
		genericObjectPoolConfig.setMaxIdle(jedisSentinelConfig.getMaxIdle());
		genericObjectPoolConfig.setMaxTotal(jedisSentinelConfig.getMaxTotal());
		genericObjectPoolConfig.setMinIdle(jedisSentinelConfig.getMinIdle());
		JedisSentinelPool jedisSentinelPool = new JedisSentinelPool(jedisSentinelConfig.getMasterName(),
			jedisSentinelConfig.getSentinels(), genericObjectPoolConfig,
			jedisSentinelConfig.getConnectionTimeout(), jedisSentinelConfig.getSoTimeout(),
			jedisSentinelConfig.getPassword(), jedisSentinelConfig.getDatabase());
		return new RedisContainer(jedisSentinelPool);
	}
}
整体下来,flink-redis-connector源码比较简洁,可以作为自定义flink sink的入门学习。
Flink读写Redis(二)-flink-redis-connector代码学习的更多相关文章
- Redis(二)、Redis持久化RDB和AOF
		一.Redis两种持久化方式 对Redis而言,其数据是保存在内存中的,一旦机器宕机,内存中的数据会丢失,因此需要将数据异步持久化到硬盘中保存.这样,即使机器宕机,数据能从硬盘中恢复. 常见的数据持久 ... 
- Flink读写Kafka
		Flink 读写Kafka 在Flink中,我们分别用Source Connectors代表连接数据源的连接器,用Sink Connector代表连接数据输出的连接器.下面我们介绍一下Flink中用于 ... 
- 二、Redis基本操作——String(实战篇)
		小喵万万没想到,上一篇博客,居然已经被阅读600次了!!!让小喵感觉压力颇大.万一有写错的地方,岂不是会误导很多筒子们.所以,恳请大家,如果看到小喵的博客有什么不对的地方,请尽快指正!谢谢! 小喵的唠 ... 
- Flink读写Redis(三)-读取redis数据
		自定义flink的RedisSource,实现从redis中读取数据,这里借鉴了flink-connector-redis_2.11的实现逻辑,实现对redis读取的逻辑封装,flink-connec ... 
- Flink的DataSource三部曲之二:内置connector
		欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ... 
- [你必须知道的NOSQL系列]专题二:Redis快速入门
		一.前言 在前一篇博文介绍了MongoDB基本操作,本来打算这篇博文继续介绍MongoDB的相关内容的,例如索引,主从备份等内容的,但是发现这些内容都可以通过官方文档都可以看到,并且都非常详细,所以这 ... 
- Redis哨兵模式(sentinel)学习总结及部署记录(主从复制、读写分离、主从切换)
		Redis的集群方案大致有三种:1)redis cluster集群方案:2)master/slave主从方案:3)哨兵模式来进行主从替换以及故障恢复. 一.sentinel哨兵模式介绍Sentinel ... 
- Redis进阶实践之二十 Redis的配置文件使用详解
		一.引言 写完上一篇有关redis使用lua脚本的文章,就有意结束Redis这个系列的文章了,当然了,这里的结束只是我这个系列的结束,但是要学的东西还有很多.但是,好多天过去了,总是感觉好像还缺点什么 ... 
- Redis(二十一):Redis性能问题排查解决手册(转)
		性能相关的数据指标 通过Redis-cli命令行界面访问到Redis服务器,然后使用info命令获取所有与Redis服务相关的信息.通过这些信息来分析文章后面提到的一些性能指标. info命令输出的数 ... 
随机推荐
- HTML5 localStorageXSS漏洞
			localStorage基础 Window localStorage 属性 HTML5 提供了两种新的本地存储方案,sessionStorage和localStorage,统称WebStorage. ... 
- 这份java多线程笔记,你真得好好看看,我还没见过总结的这么全面的
			1.线程,进程和多线程 1.程序:指指令和数据的有序集合,其本身没有任何意义,是一个静态的概念 2.进程:指执行程序的一次执行过程,是一个动态的概念.是系统资源分配的单位(注意:很多多线程是模拟出来的 ... 
- 面试腾讯,字节跳动,华为90%会被问到的HashMap!你会了吗?
			简介 HashMap是平常使用的非常多的,内部结构是 数组+链表/红黑树 构成,很多时候都是多种数据结构组合. 我们先看一下HashMap的基本操作: new HashMap(n); 第一个知识点 ... 
- nginx学习http_auth_basic_module模块
			对2.html页面做授权操作 先进行账号密码的生成 使用 htpasswd -c /etc/nginx/auth_conf 用户名 输入2次密码 (如果没有htpasswd,可以使用yum - ... 
- linux(centos7.x)安装jdk
			一.下载与安装 下载地址:链接:https://pan.baidu.com/s/1g7MF1xqlOxWnLGf2shl3NA 提取码:epae 下载完成后将安装包上传到linxu环境中,并将其 ... 
- LeetCode周赛#208
			本周周赛的题面风格与以往不太一样,但不要被吓着,读懂题意跟着模拟,其实会发现并不会难到哪里去. 1599. 经营摩天轮的最大利润 #模拟 题目链接 题意 摩天轮\(4\)个座舱,每个座舱最多可容纳\( ... 
- [TroubleShootting]Zabbix数据采集出现断点的问题
			背景 最近发现公司的Zabbix监控大屏上的监控图经常出现数据断点的现象,主要集中在一些自定义的监控项数据上,如下图: 原因 查看Zabbix Server日志以及zabbix官方手册后,分析可能原因 ... 
- [oBIX包使用教程] 使用 Python 通过 oBIX 协议访问 Niagara 数据
			oBIX 全称是 Open Building Information Exchange,它是基于 RESTful Web Service 的接口的标准,用于构建控制系统.oBIX是在专为楼宇自动化设计 ... 
- 色相偏移 HueShift  ASE
			色相偏移可以改变颜色色调,unity ASE没有参考UE4写个,原理很简单,将颜色向量绕(1,1,1)旋转,就可以得到不同色调的颜色. https://zhuanlan.zhihu.com/p/677 ... 
- JS代码下载百度文库纯文本文档
			下载百度文库纯文本文档流程 1.按 F12 或 Ctrl+Shift+I打开后台(或点击右键,在点击检查)[建议使用谷歌浏览器] 2.切换到控制台,赋值粘贴以下js代码,回车后,浏览器将自动下载保存 ... 
