gu.dtalk.cmd.BaseCmdManager Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of dtalk-cmd Show documentation
Show all versions of dtalk-cmd Show documentation
implement cmd/task manager
package gu.dtalk.cmd;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.primitives.Ints;
import gu.dtalk.Ack;
import gu.dtalk.DeviceInstruction;
import gu.dtalk.IAckAdapter;
import gu.dtalk.exception.AckTimtoutException;
import gu.simplemq.ISubscriber;
import gu.simplemq.Channel;
import gu.simplemq.IPublisher;
import gu.simplemq.IUnregistedListener;
import gu.simplemq.exceptions.SmqUnsubscribeException;
import static com.google.common.base.Preconditions.*;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import com.google.common.base.Strings;
/**
*
* 设备命令发送管理模块基类
* 线程安全
* 发送设备命令示例:
*
* String ackChannel = IFaceLogClient.applyAckChannel(myToken); // 向facelog服务申请命令响应通道
* long cmdSn = IFaceLogClient.applyCmdSn(myToken); // 向facelog服务申请命令序列号
* targetBuilder()
* .setCmdSn(cmdSn) // 设置命令序列号
* .setDeviceTarget(deviceId) // 指定目标设备ID
* .setAckChannel(ackChannel) // 设置命令响应通道
* .build()
* .reset(null); // 执行reset命令,立即执行
* // 如果同时设置命令响应处理对象,请调用 reset(Long ,IAckAdapter)
*
* 带{@code IAckAdapter}类型参数的方法为异步执行方法,需要应用项目提供{@code IAckAdapter}实例,
* {@code sync}后缀的设备命令方法为同步执行方法,
* @author guyadong
*
*/
public abstract class BaseCmdManager {
protected final ISubscriber subscriber;
protected final IPublisher publisher;
private Supplier cmdSnSupplier;
private Supplier ackChannelSupplier = Suppliers.ofInstance(null);
protected BaseCmdManager(IPublisher publisher,ISubscriber subscriber) {
this.publisher = checkNotNull(publisher,"publisher is null");
this.subscriber = checkNotNull(subscriber,"subscriber is null");
}
/**
* 执行数据发送
* @param cmd 设备命令
* @return 收到命令的客户端数目
*/
protected abstract long doSendCmd(DeviceInstruction cmd);
/**
* 用于处理超时等待的{@link Ack}对象
* 向{@link Ack}对象发送超时错误{@link Ack.Status#TIMEOUT}
* @author guyadong
*
* @param 设备命令响应返回数据类型
*/
private class TimeoutCleaner implements IUnregistedListener>{
@Override
public void apply(Channel> input) {
IAckAdapter adapter = (IAckAdapter)input.getAdapter();
try{
if(!adapter.isFinished()){
// 通知执行器命令超时
adapter.onSubscribe(new Ack().setStatus(Ack.Status.TIMEOUT));
}
}catch(SmqUnsubscribeException e){
}catch(RuntimeException e){
e.printStackTrace();
}
}
}
/**
* 用于同步执行设备命令的{@link IAckAdapter}实现
* @author guyadong
*
* @param 设备命令响应返回数据类型
*/
private class AdapterSync extends IAckAdapter.BaseAdapter{
final List> acks = Collections.synchronizedList(new LinkedList>());
final AtomicBoolean timeout = new AtomicBoolean(false);
@Override
protected void doOnTimeout() {
timeout.set(true);
}
@Override
protected void doOnSubscribe(Ack t) {
acks.add(t);
}
}
/**
* 设备命令参数构建工具类,用于设置命令目标,命令序列号,命令响应频道
* @author guyadong
*/
public class CmdBuilder{
List target;
boolean group;
/** 命令发送后是否自动清除TLS变量 */
boolean autoRemove = true;
private String ackChannel;
private Integer cmdSn;
private CmdBuilder(){
}
/**
* 调用{@link Supplier}实例获取当前设备命令需要的序列号和响应通道,
* 此方法每次调用获取的命令序列号都不同,所以不可以随意调用
* @return 当前对象
*/
CmdBuilder apply(){
if(cmdSn == null){
checkState(null != cmdSnSupplier,"cmdSnSupplier is uninitialized");
// 命令行序列号不可为空
cmdSn = checkNotNull(cmdSnSupplier.get(),"cmdSn is null");
}
if(Strings.isNullOrEmpty(ackChannel)){
ackChannel = ackChannelSupplier.get();
}
return this;
}
/**
* 指定目标ID(设备/设备组)列表,参见 {@link DeviceInstruction#setTarget(List, boolean)}
* @param target 目标列表
* @param group 目标是否为设备组
* @return 当前对象
*/
public CmdBuilder setTarget(List target,boolean group){
this.target = target;
this.group = group;
return this;
}
/**
* 指定设备目标为设备ID列表,参见 {@link DeviceInstruction#setTarget(List, boolean)}
* @param target 目标列表
* @return 当前对象
*/
public CmdBuilder setDeviceTarget(List target){
return setTarget(target,false);
}
/**
* 指定设备目标为设备ID列表,参见 {@link DeviceInstruction#setTarget(List, boolean)}
* @param target 目标列表
* @return 当前对象
*/
public CmdBuilder setDeviceTarget(int... target){
return setDeviceTarget(Ints.asList(target));
}
/**
* 指定设备目标为设备组ID列表,参见 {@link DeviceInstruction#setTarget(List, boolean)}
* @param target 目标列表
* @return 当前对象
*/
public CmdBuilder setDeviceGroupTarget(List target){
return setTarget(target,true);
}
/**
* 指定设备目标为设备组ID列表,参见 {@link DeviceInstruction#setTarget(List, boolean)}
* @param target 目标列表
* @return 当前对象
*/
public CmdBuilder setDeviceGroupTarget(int... target){
return setDeviceGroupTarget(Ints.asList(target));
}
/**
* @param autoRemove 为{@code true}时,完成设备命令发送后自动清除Thread Local Storage变量{@link CmdManager#TLS_BUILDER},
* 默认值为{@code true}
* @return 当前对象
*/
public CmdBuilder autoRemove(boolean autoRemove){
this.autoRemove = autoRemove;
return this;
}
/**
* @param ackChannel 要设置的 ackChannel
* @return 当前对象
*/
public CmdBuilder setAckChannel(String ackChannel) {
this.ackChannel = ackChannel;
return this;
}
public CmdBuilder setCmdSn(int cmdSn) {
this.cmdSn = cmdSn;
return this;
}
}
/**
* TLS变量,用于保存当前线程使用的 {@link CmdBuilder}对象
,
* TLS变量在多线程高并发环境如果不显式执行{@link ThreadLocal#remove()}有资源泄漏风险,
* 如果{@link CmdBuilder#autoRemove}为{@code true}(默认值),则调用设备命令方法发送完命令后会自动清除TLS变量,
* 否则需要调用 {@link #removeTlsTarget()}方法手动清除。
*/
private static final ThreadLocal TLS_BUILDER = new ThreadLocal();
public CmdBuilder targetBuilder() {
if(null == TLS_BUILDER.get()){
TLS_BUILDER.set(new CmdBuilder());
}
return TLS_BUILDER.get();
}
/**
* 清除TLS变量 {@link #TLS_BUILDER}
* @return 当前对象
* @see ThreadLocal#remove()
*/
public BaseCmdManager removeTlsTarget() {
TLS_BUILDER.remove();
return this;
}
/**
* 指定提供命令序列号的{@link Supplier}实例
* @param cmdSnSupplier {@link Supplier}实例
* @return 当前对象
*/
public BaseCmdManager setCmdSn(Supplier cmdSnSupplier) {
this.cmdSnSupplier = checkNotNull(cmdSnSupplier);
return this;
}
/**
* 指定命令响应通道,参见 {@link DeviceInstruction#setAckChannel(String)}
* @param ackChannel 响应频道名
* @return 当前对象
*/
public BaseCmdManager setAckChannel(String ackChannel){
return this.setAckChannel(Suppliers.ofInstance(checkNotNull(Strings.emptyToNull(ackChannel),"ackChannel is null or empty")));
}
/**
* 指定提供命令响应通道的{@link Supplier}实例
* @param ackChannelSupplier {@link Supplier}实例
* @return 当前对象
*/
public BaseCmdManager setAckChannel(Supplier ackChannelSupplier){
this.ackChannelSupplier = checkNotNull(ackChannelSupplier);
return this;
}
/**
* 发送设备命令
* 将设备命令数据封装为{@link DeviceInstruction}对象并执行数据发送,
* 发送前最后检查数据有效性
* @param cmdpath 设备命令名(全路径)
* @param params 设备命令参数对象, {@code 参数名(key)->参数值(value)映射},没有参数可为{@code null}
* @return 收到命令的客户端数目
*/
private long sendCmd(String cmdpath, Map params) {
checkArgument(!Strings.isNullOrEmpty(cmdpath),"cmdpath must not be null or empty");
CmdBuilder builder = targetBuilder();
try{
DeviceInstruction deviceInstruction = new DeviceInstruction()
.setCmdpath(cmdpath)
.setCmdSn(builder.cmdSn)
.setTarget(builder.target, builder.group)
.setAckChannel(builder.ackChannel)
.setParameters(params);
return doSendCmd(deviceInstruction);
}finally{
if(builder.autoRemove){
removeTlsTarget();
}
}
}
/**
* 发送设备命令
* 发送前申请命令序列号和响应频道
* @param cmdpath 设备命令名(dtalk全路径)
* @param params 设备命令参数对象, {@code 参数名(key)->参数值(value)映射},没有参数可为{@code null}
* @return 收到命令的客户端数目
*/
public int runCmd(String cmdpath, Map params) {
targetBuilder().apply();
return (int) sendCmd(cmdpath, params);
}
/**
* 设备命令(异步调用)
* 该方法会自动将命令响应通道名({@link #setAckChannel(String)})
* 关联命令处理对象({@code adapter})注册到REDIS订阅频道,当有收到设备命令响应时自动交由{@code adapter}处理
* 该方法要求必须指定命令响应通道,参见{@link #setAckChannel(String)},{@link #setAckChannel(Supplier)}
*
* @param cmdpath 设备命令名(dtalk全路径)
* @param params 设备命令参数对象, {@code 参数名(key)->参数值(value)映射},没有参数可为{@code null}
* @param adapter 命令响应处理对象,不可为{@code null}
*/
public void runCmd(String cmdpath, Map params, IAckAdapter