问题描述:项目需要对接上级平台协议, 有一个协议需要我们上传人脸打卡设备,以及每次人脸打卡设备的上下线的状态发生变化的时候都需要推送给上级平台;
原先的设计方式:
因为考勤机的在线情况查询并不完全由我们来控制,有一部分是把设备交给另一个第三方, 他们可以去获取设备的在线状态而我们需要做的是调取此第三方设计的接口,这里是一个定时任务:
//这段代码是在属于某个协议的
private Map<String, Boolean> getDeviceStatus(Collection<String> deviceKeys){
if(CollectionUtils.isEmpty(deviceKeys)){
throw new BaseException("empty argument of deviceKeys");
}
String requestUrl = prefix + getDeviceStatus;
Map<String, Boolean> resultMap = new HashMap<>();
for(String deviceKey : (deviceKeys instanceof Set) ? deviceKeys : new HashSet<>(deviceKeys)){
HashMap<String, String> params = new HashMap<>();
params.put("deviceKey", deviceKey);
String response = HttpClientUtil.doGet(requestUrl, params, getHeader());
if(JSON.isValid(response)){
JSONObject responseJson = JSONObject.parseObject(response, JSONObject.class);
if(responseJson.getInteger("code").equals(0)){
resultMap.put(deviceKey, "在线".equals(responseJson.getString("msg")));
}else{
log.error("error occur {}", responseJson.getString("msg"));
}
}
}
return resultMap;
}
还有的一部分就是我自己可以调用设备厂商的接口用来查询设备的在线状态的,与其他平台无关,
这里也是一个定时任务用来获取设备的在线状态
//这里是我调用设备厂商的协议进行获取设备状态
public Map<String, Boolean> batchSearchDeviceOnlineStatus(Collection<String> deviceKeys) throws SdkException{
if(CollectionUtils.isEmpty(deviceKeys)){
return Collections.emptyMap();
}
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("source", U_FACE_DEVICE_TYPE);
requestBody.put(
"deviceNos",
String.join(",", ((deviceKeys instanceof Set) ? deviceKeys : new HashSet<>(deviceKeys)))
);
try{
String resp = HttpClientUtil.doPost4Json(
BASE_URL + DEVICE_ONLINE_STATUS,
requestBody,
getRequestHeader()
);
if(StringUtils.hasLength(resp) && JSON.isValid(resp)){
JSONObject jsonObject = JSONObject.parseObject(resp, JSONObject.class);
log.info("device status result: {}", jsonObject);
JSONArray data = jsonObject.getJSONArray("data");
List<OnlineStatus> statusList = data.toJavaList(OnlineStatus.class);
return statusList.stream().collect(Collectors.toMap(
OnlineStatus::getDeviceNo,
OnlineStatus::isOnline
));
}
} catch (IOException ignored){}
return Collections.emptyMap();
}
@Data
private static class OnlineStatus {
String deviceNo;
boolean online;
}
于是我就想如果再来一个协议也是需要我调用第三方协议中的接口进行获取数据,那么还得再增加一个定时任务, 基于此, 我想着是否可以重新设计一下
重新设计:
①首先定义一个接口用来规范不同的协议返回同样的是否在线结果
/**
* 该接口需要所有的可以获取设备状态的类要实现接口
*/
public interface DeviceStatusSupplier {
/**
* @param deviceKeys 设备序列号
* @return Map<设备序列号, 是否在线>
*/
Map<String, Boolean> fetchDeviceStatus(Collection<String> deviceKeys);
}
②让可以获取设备状态的类实现这个接口
//该协议类不是SpringBean
public class AnYouYunPlatformStrategyImpl extends PersonnelManagementProtocolAdapter
implements PersonnelManagementProtocol, DeviceStatusSupplier {
@Override
public Map<String, Boolean> fetchDeviceStatus(Collection<String> deviceKeys) {
//这里调用的是刚刚协议中已经实现了的方法
return this.getDeviceStatus(deviceKeys);
}
}
@Component
@Slf4j
public class RecognitionDeviceUtil implements DeviceStatusSupplier{
@Override
public Map<String, Boolean> fetchDeviceStatus(Collection<String> deviceKeys) {
//这里调用的是通过设备厂商提供的接口获取设备在线状态
return this.batchSearchDeviceOnlineStatus(deviceKeys);
}
}
说明:由于多个项目可能会使用到一个协议(但是他们的对接参数不一致, 比如说经典的 key, secret,requestHost等),这些东西要在构造方法中进行初始化, 因此没有把它声明为SpringBean
③接下来再创建一个类用来引用所有DeviceStatusSupplier接口的实现类
@Component
@Slf4j
@EnableScheduling
public class RecognitionDeviceStatusHolder {
private final List<DeviceStatusSupplier> supplierList; //所有DeviceStatusSupplier实现类
// 通过SpringIOC自动注入的DeviceStatusSupplier的实现
@Autowired
public RecognitionDeviceStatusHolder(DeviceStatusSupplier... suppliers){
this.supplierList = new LinkedList<>(Arrays.asList(suppliers));
}
//没有被spring管理的实现,需要手动调用此方法将自己注册进来
public void registerImpl(DeviceStatusSupplier impl){
this.supplierList.add(impl);
}
}
④拥有可以获取设备状态的实现类, 接下来就可以通过定时任务去获取设备的在线状态了
private static final DateTimeFormatter DATE_TIME_FORMATTER = DTFUtils.DATE_TIME_FORMATTER;
@Autowired
private DeviceMapper deviceMapper;
@Scheduled(fixedRate = 120000, initialDelay = 10 * 1000) // 每2分钟执行一次
public void checkDeviceStatus() {
log.info("查询设备在线状态定时任务开始执行, 开启时间: {}", LocalDateTime.now().format(DATE_TIME_FORMATTER));
// 获取设备列表
List<Device> devices = deviceMapper.selectList(null);
// 检查设备列表是否为空
if (!CollectionUtils.isEmpty(devices)) {
// 获取设备状态
Map<String, Boolean> updatedDevices = getUpdatedDeviceStatus(devices);
log.info("本次需要进行更新设备状态的数据 updatedDevices: {}, 缓存中的设备在线状态deviceStatusCache: {}", updatedDevices, deviceStatusCache);
if (!CollectionUtils.isEmpty(updatedDevices)){
// 更新设备状态到数据库
deviceMapper.batchUpdateOnlineStatus(updatedDevices);
// 推送状态变化
notifyStatusChange(updatedDevices);
}
}
}
private final Map<String, Boolean> deviceStatusCache = new ConcurrentHashMap<>(); //缓存所有设备的当前在线状态
private Map<String, Boolean> getUpdatedDeviceStatus(List<Device> devices) {
Set<String> deviceKeys = devices.stream().map(Device::getDeviceKey).collect(Collectors.toSet());
Map<String, Boolean> forUpdateDevices = new HashMap<>();
for (DeviceStatusSupplier supplier : supplierList) {
try {
// 获取每个实现类的返回结果
Map<String, Boolean> deviceStatus = supplier.fetchDeviceStatus(deviceKeys);
if (!CollectionUtils.isEmpty(deviceStatus)){
//如果当前缓存设备状态信息的容器为空, 则直接全部插入
if (CollectionUtils.isEmpty(deviceStatusCache)){
deviceStatusCache.putAll(deviceStatus);
forUpdateDevices.putAll(deviceStatus);
}
//否则 遍历新的状态集合, 如果有对应的状态和上一次不一致的 或者有新的key出现 则进行更新
else {
for (Map.Entry<String, Boolean> entry : deviceStatus.entrySet()){
if (!deviceStatusCache.containsKey(entry.getKey()) || !deviceStatusCache.get(entry.getKey()).equals(entry.getValue())) {
forUpdateDevices.put(entry.getKey(), entry.getValue());
deviceStatusCache.put(entry.getKey(), entry.getValue());
}
}
}
}
} catch (Exception ex) {
log.error("Error fetching device status", ex);
}
}
return forUpdateDevices;
}
<update id="batchUpdateOnlineStatus" parameterType="java.util.Map">
update device
set online_status =
case device_key
<foreach collection="map.entrySet()" index="key" item="value" separator=" ">
when #{key} then IF(#{value}, 1, 0)
</foreach>
end
where device_key in
<foreach collection="map.entrySet()" index="key" separator="," open="(" close=")">
#{key}
</foreach>
</update>
⑤在每次获取状态发生改变的设备集合后,需要将该数据传递出去
//第一种实现, 在类中创建一个集合用来保存所有的消费者, 在获取到有新的设备状态时挨个通知每个消费者进行消费
private final List<Consumer<Map<String, Boolean>>> statusListeners = new CopyOnWriteArrayList<>(); //保存所有的监听者
public void registerObserver(Consumer<Map<String, Boolean>> consumer){
this.statusListeners.add(consumer);
}
private void notifyStatusChange(Map<String, Boolean> updatedDevices) {
if (!CollectionUtils.isEmpty(statusListeners)) {
statusListeners.forEach(consumer -> consumer.accept(updatedDevices));
}
}
//第二种实现, 使用消息中间件, 定义一个公共的队列,将产生的数据推送的队列中, 由消息监听方提供具体的实现方法
其实我更想使用第二种的方式来实现,因为那样可能会更灵活多样
⑥注册,已经在IOC容器中的DeviceStatusSupplier类型已经自动被注入了, 但是非SpringBean还需要自己手动注入一下
生产者注入:
public AnYouYunPlatformStrategyImpl(AnYouYunParam param) {
this.prefix = param.getHost();
this.token = param.getToken();
deviceStatusHolder.registerImpl(this);
}
private RecognitionDeviceStatusHolder deviceStatusHolder = SpringUtil.getBean(RecognitionDeviceStatusHolder.class);
消费者注入:
public TCPlatformStrategyImpl() {
deviceStatusHolder.registerObserver(statusMap -> statusMap.keySet().forEach(this::uploadRecognizedDevice));
}
private static final RecognitionDeviceStatusHolder deviceStatusHolder = SpringUtil.getBean(RecognitionDeviceStatusHolder.class);
到这里就完成了代码的更新了,我也不知道为什么我会想到用这种方法修改代码,早上突然就想到定义多个定时任务不是个事, 至于这里用到了什么设计模式,我自己可能也说不出个一二三,如果哪里写错的话,还请大家原谅并指出