2025-9-28学习笔记

目录

设备管理(续):

功能实现:

从物联网平台同步产品列表

查询所有产品列表

注册设备:

分页查询设备列表:

查询设备详细数据:

查看设备上报的数据(设备影子):

微实战:

修改设备:

删除设备:

提交代码:

同步和异步:

消息队列:

AMQP:

设备数据转发:

创建数据转发规则:

[Java SDK接入示例:](#Java SDK接入示例:)

SDK改造:

线程池相关配置:

接收设备端数据:


设备管理(续):

功能实现:

将device表导入到若依的代码生成功能当中,将生成的代码拷贝到idea当中,然后将DeviceController当中除了查询的方法,其他方法都删除掉:

DeviceController:

java 复制代码
package com.zzyl.nursing.controller;

import com.zzyl.common.core.controller.BaseController;
import com.zzyl.common.core.page.TableDataInfo;
import com.zzyl.nursing.domain.Device;
import com.zzyl.nursing.service.IDeviceService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * 设备Controller
 * 
 * @author Le1Yu
 * @date 2025-09-28
 */
@Api("设备管理")
@RestController
@RequestMapping("/nursing/device")
public class DeviceController extends BaseController
{
    @Autowired
    private IDeviceService deviceService;

    /**
     * 查询设备列表
     */
    @ApiOperation("查询设备列表")
    @PreAuthorize("@ss.hasPermi('nursing:device:list')")
    @GetMapping("/list")
    public TableDataInfo<List<Device>> list(@ApiParam("查询条件对象") Device device)
    {
        startPage();
        List<Device> list = deviceService.selectDeviceList(device);
        return getDataTable(list);
    }
}

效果展示:

从物联网平台同步产品列表

解决syncProductList接口报错问题:

DeviceController:

java 复制代码
@PostMapping("/syncProductList")
@ApiOperation(value = "从物联网平台同步产品列表")
public AjaxResult syncProductList() {
    deviceService.syncProductList();
    return success();
}

IDeviceService:

java 复制代码
/**
 * 从物联网平台同步产品列表
 */
void syncProductList();

DeviceServiceImpl:

java 复制代码
@Autowired
private RedisTemplate<String, String> redisTemplate;

@Autowired
private IoTDAClient client;

/**
 * 同步产品列表
 */
@Override
public void syncProductList() {
    // 请求参数
    ListProductsRequest listProductsRequest = new ListProductsRequest();
    // 设置条数
    listProductsRequest.setLimit(50);
    // 发起请求
    ListProductsResponse response = client.listProducts(listProductsRequest);
    if(response.getHttpStatusCode() != 200) {
        throw new BaseException("物联网接口 - 查询产品,同步失败");
    }
    // 存储到redis
    redisTemplate.opsForValue().set(CacheConstants.IOT_ALL_PRODUCT_LIST, JSONUtil.toJsonStr(response.getProducts()));

}

CacheConstants:

java 复制代码
    /**
     * iot所有产品列表缓存key
     */
    public static final String IOT_ALL_PRODUCT_LIST = "iot:all_product_list";

效果展示:

查询所有产品列表

DeviceController:

java 复制代码
@GetMapping("/allProduct")
    @ApiOperation(value = "查询所有产品列表")
    public R<List<ProductVo>> allProduct() {
        List<ProductVo> list = deviceService.allProduct();
        return R.ok(list);
    }

新建ProductVo:

java 复制代码
package com.zzyl.nursing.vo;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

/**
 * 产品信息响应模型
 *
 * @author itcast
 **/
@Data
@ApiModel("产品信息响应模型")
public class ProductVo {
    /**
     * 产品的ProductKey,物联网平台产品唯一标识
     */
    @ApiModelProperty("产品的ProductKey,物联网平台产品唯一标识")
    private String productId;

    /**
     * 产品名称
     */
    @ApiModelProperty("产品名称")
    private String name;
}

IDeviceService及其实现类:

java 复制代码
/**
 * 查询所有产品列表
 *
 * @return 产品列表
 */
List<ProductVo> allProduct();

/**
 * 查询所有产品列表
 *
 * @return
 */
@Override
public List<ProductVo> allProduct() {
    // 从redis中查询数据
    String jsonStr = redisTemplate.opsForValue().get(CacheConstants.IOT_ALL_PRODUCT_LIST);
    // 如果数据为空,则返回一个空集合
    if(StringUtils.isEmpty(jsonStr)){
        return Collections.emptyList();
    }
    // 解析数据,并返回
    return JSONUtil.toList(jsonStr, ProductVo.class);
}

效果展示:

注册设备:

DeviceController:

java 复制代码
@PostMapping("/register")
@ApiOperation(value = "注册设备")
public AjaxResult registerDevice(@RequestBody DeviceDto deviceDto) {
    deviceService.registerDevice(deviceDto);
    return success();
}

新增DeviceDto:

java 复制代码
package com.zzyl.nursing.dto;


import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel(value = "设备注册参数")
public class DeviceDto {

    private Long id;

    /** 备注 */
    private String remark;

    /**
     * 设备标识码,通常使用IMEI、MAC地址或Serial No作为node_id
     */
    @ApiModelProperty(value = "设备标识码", required = true)
    private String nodeId;

    @ApiModelProperty(value = "设备id")
    public String iotId;

    @ApiModelProperty(value = "产品的id")
    public String productKey;

    @ApiModelProperty(value = "产品名称")
    private String productName;

    @ApiModelProperty(value = "位置名称回显字段")
    private String deviceDescription;

    @ApiModelProperty(value = "位置类型 0 老人 1位置")
    Integer locationType;

    @ApiModelProperty(value = "绑定位置")
    Long bindingLocation;

    @ApiModelProperty(value = "设备名称")
    String deviceName;

    @ApiModelProperty(value = "物理位置类型 -1 老人 0楼层 1房间 2床位")
    Integer physicalLocationType;
}

IDeviceService:

java 复制代码
    /**
     * 注册设备
     * @param deviceDto
     */
    void registerDevice(DeviceDto deviceDto);

DeviceServiceImpl:

java 复制代码
/**
 * 注册设备
 * @param dto
 */
@Override
public void registerDevice(DeviceDto dto) {
    // 判断设备名称是否存在
    long count = count(Wrappers.<Device>lambdaQuery().eq(Device::getDeviceName, dto.getDeviceName()));
    if(count > 0) {
        throw new BaseException("设备名称已存在,请重新输入");
    }

    // 判断设备标识是否存在
    count = count(Wrappers.<Device>lambdaQuery().eq(Device::getNodeId, dto.getNodeId()));
    if(count > 0) {
        throw new BaseException("设备标识码已存在,请重新输入");
    }

    // 判断同一位置是否绑定了相同的产品
    count = count(Wrappers.<Device>lambdaQuery()
            .eq(Device::getProductKey, dto.getProductKey())
            .eq(Device::getBindingLocation, dto.getBindingLocation())
            .eq(Device::getLocationType, dto.getLocationType())
            .eq(dto.getPhysicalLocationType() != null, Device::getPhysicalLocationType, dto.getPhysicalLocationType()));
    if(count > 0) {
        throw new BaseException("该老人/位置已绑定该产品,请重新选择");
    }

    // 注册设备--->IoT平台
    AddDeviceRequest request = new AddDeviceRequest();
    AddDevice body = new AddDevice();
    body.withProductId(dto.getProductKey());
    body.withDeviceName(dto.getDeviceName());
    body.withNodeId(dto.getNodeId());

    // 秘钥设置
    AuthInfo authInfo = new AuthInfo();
    String secret = UUID.randomUUID().toString().replaceAll("-", "");
    authInfo.withSecret(secret);
    body.setAuthInfo(authInfo);
    request.setBody(body);

    AddDeviceResponse response;
    try {
        response = client.addDevice(request);
    } catch (Exception e) {
        e.printStackTrace();
        throw new BaseException("物联网接口 - 注册设备,调用失败");
    }
    // 本地保存设备
    // 属性拷贝
    Device device = BeanUtil.toBean(dto, Device.class);
    // 秘钥
    device.setSecret(secret);
    // 设备id、设备绑定状态
    device.setIotId(response.getDeviceId());
    save(device);
}

效果展示:

分页查询设备列表:

该接口的基本功能已经实现,现在我们做一个数据字典的优化:

在若依系统下找到系统管理-字典管理-新增,填写完信息后生成字典,然后点击字典类型进入到字典当中,开始新增字典标签及其键值

效果展示:

查询设备详细数据:

DeviceController:

java 复制代码
/**
 * 获取设备详细信息
 */
@GetMapping("/{iotId}")
@ApiOperation("获取设备详细信息")
public AjaxResult getInfo(@PathVariable("iotId") String iotId) {
    return success(deviceService.queryDeviceDetail(iotId));
}

DeviceService:

java 复制代码
/**
 * 查询设备详情
 * @param iotId
 * @return
 */
DeviceDetailVo queryDeviceDetail(String iotId);

新增DeviceDetailVo:

java 复制代码
package com.zzyl.nursing.vo;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.zzyl.common.annotation.Excel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * @author itcast
 */
@Data
@ApiModel("设备详情响应模型")
public class DeviceDetailVo {

    /**
     * 设备id
     */
    @ApiModelProperty(value = "设备id")
    private Long id;

    /**
     * 物联网设备id
     */
    @ApiModelProperty(value = "物联网设备id")
    private String iotId;

    /**
     * 设备名称
     */
    @ApiModelProperty(value = "设备名称")
    private String deviceName;

    /**
     * 设备标识码
     */
    @ApiModelProperty(value = "设备标识码")
    private String nodeId;

    /**
     * 设备秘钥
     */
    @ApiModelProperty(value = "设备秘钥")
    private String secret;

    /**
     * 产品id
     */
    @ApiModelProperty(value = "产品id")
    public String productKey;

    /**
     * 产品名称
     */
    @ApiModelProperty(value = "产品名称")
    public String productName;

    /**
     * 位置类型 0 随身设备 1固定设备
     */
    @ApiModelProperty(value = "位置类型 0 随身设备 1固定设备")
    private Integer locationType;

    /**
     * 绑定位置,如果是随身设备为老人id,如果是固定设备为位置的最后一级id
     */
    @ApiModelProperty(value = "绑定位置,如果是随身设备为老人id,如果是固定设备为位置的最后一级id")
    private Long bindingLocation;


    /**
     * 接入位置
     */
    @ApiModelProperty(value = "接入位置")
    private String remark;

    /**
     * 设备状态,ONLINE:设备在线,OFFLINE:设备离线,ABNORMAL:设备异常,INACTIVE:设备未激活,FROZEN:设备冻结
     */
    @ApiModelProperty(value = "设备状态,ONLINE:设备在线,OFFLINE:设备离线,ABNORMAL:设备异常,INACTIVE:设备未激活,FROZEN:设备冻结")
    private String deviceStatus;

    /**
     * 激活时间
     */
    @ApiModelProperty(value = "激活时间,格式:yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime activeTime;

    /**
     * 创建时间
     */
    @ApiModelProperty(value = "创建时间,格式:yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;

    /**
     * 创建人id
     */
    @ApiModelProperty(value = "创建人id")
    private Long createBy;

    /**
     * 创建人名称
     */
    @ApiModelProperty(value = "创建人名称")
    private String creator;

    /** 位置备注 */
    @ApiModelProperty(value = "位置备注")
    private String deviceDescription;
}

DeviceServiceImpl:

java 复制代码
@Override
public DeviceDetailVo queryDeviceDetail(String iotId) {
    // 查询本地设备数据
    Device device = getOne(Wrappers.<Device>lambdaQuery().eq(Device::getIotId, iotId));
    if(ObjectUtil.isEmpty(device)) {
        return null;
    }
    // 调用华为云接口查询设备详情
    ShowDeviceRequest request = new ShowDeviceRequest();
    request.setDeviceId(iotId);
    ShowDeviceResponse response;
    try {
        response = client.showDevice(request);
    } catch (Exception e) {
        throw new BaseException("物联网接口 - 查询设备详情,调用失败");
    }
    // 属性拷贝
    DeviceDetailVo deviceVo = BeanUtil.toBean(device, DeviceDetailVo.class);
    deviceVo.setDeviceStatus(response.getStatus());
    String activeTimeStr = response.getActiveTime();
    // 日期转换
    if(StringUtils.isNotEmpty(activeTimeStr)) {
        // 把字符串转换为LocalDateTime
        LocalDateTime activeTime = LocalDateTimeUtil.parse(activeTimeStr, DatePattern.UTC_MS_PATTERN);
        // 日期时区转换
        deviceVo.setActiveTime(DateTimeZoneConverter.utcToShanghai(activeTime));
    }

    return deviceVo;
}

新增工具类DateTimeZoneConverter:

java 复制代码
package com.zzyl.common.utils;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Optional;

/**
 * 日期时区转换工具类
 */
public class DateTimeZoneConverter {
    // 预定义常用时区
    public static final ZoneId UTC_ZONE = ZoneOffset.UTC;
    public static final ZoneId SHANGHAI_ZONE = ZoneId.of("Asia/Shanghai");
    
    // 私有构造防止实例化
    private DateTimeZoneConverter() {}

    /**
     * 转换LocalDateTime时区(明确时区上下文)
     * @param sourceTime 源时间(无时区信息)
     * @param sourceZone 源时间所在的时区
     * @param targetZone 目标时区
     * @return 转换后的LocalDateTime
     */
    public static LocalDateTime convert(LocalDateTime sourceTime, 
                                       ZoneId sourceZone,
                                       ZoneId targetZone) {
        return sourceTime.atZone(sourceZone)
                        .withZoneSameInstant(targetZone)
                        .toLocalDateTime();
    }

    /**
     * 安全转换方法(Optional包装)
     * @param sourceTime 可为null的源时间
     * @param sourceZone 源时区
     * @param targetZone 目标时区
     * @return Optional包装的转换结果
     */
    public static Optional<LocalDateTime> safeConvert(LocalDateTime sourceTime,
                                                     ZoneId sourceZone,
                                                     ZoneId targetZone) {
        return Optional.ofNullable(sourceTime)
                      .map(time -> convert(time, sourceZone, targetZone));
    }

    /**
     * UTC转上海时区的快捷方法
     * @param utcTime UTC时间的LocalDateTime
     * @return 上海时区本地时间
     */
    public static LocalDateTime utcToShanghai(LocalDateTime utcTime) {
        return convert(utcTime, UTC_ZONE, SHANGHAI_ZONE);
    }

    /**
     * 带格式解析的完整流程
     * @param timeStr 时间字符串
     * @param pattern 格式模式(需匹配timeStr)
     * @param sourceZone 字符串对应的时区
     * @param targetZone 目标时区
     * @return Optional包装的转换结果
     */
    public static Optional<LocalDateTime> parseAndConvert(String timeStr,
                                                         String pattern,
                                                         ZoneId sourceZone,
                                                         ZoneId targetZone) {
        try {
            LocalDateTime sourceTime = LocalDateTime.parse(timeStr, DateTimeFormatter.ofPattern(pattern));
            return Optional.of(convert(sourceTime, sourceZone, targetZone));
        } catch (Exception e) {
            return Optional.empty();
        }
    }
}

效果展示:

查看设备上报的数据(设备影子):

DeviceController:

java 复制代码
/**
 * 查询设备上报数据
 */
@GetMapping("/queryServiceProperties/{iotId}")
@ApiOperation("查询设备上报数据")
public AjaxResult queryServiceProperties(@PathVariable("iotId") String iotId) {
    AjaxResult ajaxResult = deviceService.queryServiceProperties(iotId);
    return ajaxResult;
}

IDeviceService及其实现类:

java 复制代码
/**
 * 查询设备上报数据
 * @param iotId
 * @return
 */
AjaxResult queryServiceProperties(String iotId);

@Override
public AjaxResult queryServiceProperties(String iotId) {

    ShowDeviceShadowRequest request = new ShowDeviceShadowRequest();
    request.setDeviceId(iotId);
    ShowDeviceShadowResponse response = client.showDeviceShadow(request);
    if(response.getHttpStatusCode() != 200) {
        throw new BaseException("物联网接口 - 查询设备影子,调用失败");
    }
    List<DeviceShadowData> shadow = response.getShadow();
    if(CollUtil.isEmpty(shadow)) {
        List<Object> emptyList = Collections.emptyList();
        return AjaxResult.success(emptyList);
    }
    // 获取上报数据的reported (参考返回的json数据)
    DeviceShadowProperties reported = shadow.get(0).getReported();
    // 把数据转换为JSONObject(map),方便处理
    JSONObject jsonObject = JSONUtil.parseObj(reported.getProperties());
    // 遍历数据,封装到list中
    List<Map<String,Object>>  list = new ArrayList<>();
    // 事件上报时间
    String eventTimeStr = reported.getEventTime();
    // 把字符串转换为LocalDateTime
    LocalDateTime eventTimeLocalDateTime = LocalDateTimeUtil.parse(eventTimeStr, "yyyyMMdd'T'HHmmss'Z'");
    // 时区转换
    LocalDateTime eventTime = DateTimeZoneConverter.utcToShanghai(eventTimeLocalDateTime);

    // k:属性标识,v:属性值
    jsonObject.forEach((k,v) -> {
        Map<String,Object> map = new HashMap<>();
        map.put("functionId", k);
        map.put("value", v);
        map.put("eventTime", eventTime);
        list.add(map);
    });

    // 数据返回
    return AjaxResult.success(list);
}

微实战:

修改设备:

DeviceController:

java 复制代码
    @PutMapping
    @ApiOperation("修改设备")
    public AjaxResult updateDevice(@RequestBody Device device){
        deviceService.updateDevice(device);
        return success();
    }

DeviceServiceImpl:

java 复制代码
    /**
     * 修改设备
     * 
     * @param device 设备
     * @return 结果
     */
    @Override
    public int updateDevice(Device device)
    {
        //- 修改IoTDA平台上的设备名称
        UpdateDeviceRequest request = new UpdateDeviceRequest();
        request.setDeviceId(device.getIotId());
        UpdateDevice body = new UpdateDevice();
        body.setDeviceName(device.getDeviceName());
        request.setBody(body);
        try {
            client.updateDevice(request);
        } catch (Exception e) {
            throw new BaseException("调用IoT平台修改设备失败");
        }
        //- 修改本地存储的设备信息
        boolean flag;
        try {
            flag = updateById(device);
        } catch (Exception e) {
            throw new BaseException("该老人/位置已绑定该类型的设备,请重新选择绑定位置");
        }
        return flag ? 1 : 0;
    }

删除设备:

DeviceController:

java 复制代码
    @DeleteMapping("/{iotId}")
    public AjaxResult deleteDevice(@PathVariable("iotId") String iotId){
        deviceService.deleteDeviceById(iotId);
        return success();
    }

DeviceServiceImpl:

java 复制代码
    /**
     * 删除设备信息
     * 
     * @param iotId 设备主键
     */
    @Override
    public void deleteDeviceById(String iotId)
    {
        //- 先从IoTDA平台删除设备
        DeleteDeviceRequest request = new DeleteDeviceRequest();
        request.setDeviceId(iotId);
        try {
            client.deleteDevice(request);
        } catch (Exception e) {
            throw new BaseException("调用IoT平台删除设备失败");
        }
        //- 再删除本地存储的设备
        remove(Wrappers.<Device>lambdaQuery().eq(Device::getIotId, iotId));
    }

提交代码:

同步和异步:

  • 同步(Background Synchronous)是指任务在后台进行处理,但其他任务需要等待任务完成后才能执行;同步请求下,任务1完成后才能执行任务2,任务2需要等待任务1的完成。这种顺序执行的方式称为同步。

  • 异步(Asynchronous)是指任务的提交和执行是相互独立的,任务的执行不会阻塞程序的继续执行;异步请求下,任务1和任务2可以并行执行,彼此之间相互独立,不需要等待对方的完成。这种并行执行的方式称为异步。

消息队列:

  • broker:负责接收、存储和分发消息的中间件组件,实现了发送者和接收者之间的解耦和异步通信

  • 生产者:负责将消息发送到消息队列中

  • 消费者:负责从消息队列中获取消息并进行处理

  • 队列:负责存储消息

  • topic:消息的分类

  • 生产者:设备负责将消息发送到IOT中(队列)

  • 每个产品可以绑定不同的topic来对消息进行分类,比如有手表topic、烟雾topic

  • IOT本身相当于是一个队列

  • 消费者可以从指定的topic中获取数据

  • 如果有多个消费者都要接收同一类消息,可以设置多个消费者,称为消费者组

AMQP:

咱们现在已经清楚了消息队列的基础概念,在IOT中是用AMQP来接收和处理消息的。

AMQP全称Advanced Message Queuing Protocol,是一种网络协议,用于在应用程序之间传递消息。它是一种开放标准的消息传递协议,可以在不同的系统之间实现可靠、安全、高效的消息传递。

AMQP协议的实现包括多种消息队列软件,例如RabbitMQ、Apache ActiveMQ、Apache Qpid等。这些软件提供了可靠、高效的消息传递服务,广泛应用于分布式系统、云计算、物联网等领域。

设备数据转发:

设备接入物联网平台后,可以通过自定义Topic或产品模型将数据上报给平台。在控制台上设置订阅后,平台会将包括设备生命周期变更、属性上报、消息上报及状态变更等信息,通过订阅推送的方式转发到您指定的服务器。

  • 订阅(AMQP): 用户可以通过控制台或调用API创建订阅,以获取设备的变更信息,如设备状态、数据上报等。订阅时需要指定具体的AMQP消息通道。

  • 推送: 一旦订阅成功,物联网平台会根据用户选择的数据类型,自动将相关变更推送到指定的AMQP消息队列中 。用户使用AMQP客户端(Apache Qpid)连接到IoT平台即可接收这些更新信息。

按照上述流程,咱们需要在IOT平台中创建转发规则,才能让AMQP客户端接收到数据

创建数据转发规则:

  1. 找到规则 ->数据转发 ->创建规则

  2. 创建规则,其中数据来源为:**设备属性;**配置完成后,点击右下角的 [创建规则]

  3. 添加转发目标,转发目标选择:AMQP推送消息队列

  4. 上述操作完成后,可以启动规则

Java SDK接入示例:

下载sdk代码:

下载地址:https://developer.obs.myhuaweicloud.com/manage/amqp/amqp-demo.zip

使用idea打开刚刚下载的amqp-demo中的amqp-client-examples代码目录,如下图:

修改连接参数:

AmqpConstants:

java 复制代码
package com.iot.amqp;


public interface AmqpConstants {
    /**
     * AMQP接入域名
     * 参考:https://support.huaweicloud.com/usermanual-iothub/iot_01_00100_2.html#section2
     */
    String HOST = "****.iot-amqps.cn-north-4.myhuaweicloud.com";   // eg: "****.iot-amqps.cn-north-4.myhuaweicloud.com";

    /**
     * AMQP接入端口
     * 参考:https://support.huaweicloud.com/usermanual-iothub/iot_01_00100_2.html#section2
     */
    int PORT = 5671;

    /**
     * 接入凭证键值
     * 参考:https://support.huaweicloud.com/usermanual-iothub/iot_01_00100_2.html#section3
     */
    String ACCESS_KEY = "*********";

    /**
     * 接入凭证密钥
     * 参考:https://support.huaweicloud.com/usermanual-iothub/iot_01_00100_2.html#section3
     */
    String ACCESS_CODE = "**************************";

    /**
     * 默认队列
     */
    String DEFAULT_QUEUE = "DefaultQueue";
}

放开AbstractAmqpExample中的打印消息的代码,如下图:

效果展示:

SDK改造:

SDK中提供的这个工具类,只是官网提供的参考代码,咱们需要将其改造后集成到自己的项目中,改造思路如下:

  • 导入对应的依赖

  • 所有的可变参数,如HOST、ACCESS_KEYACCESS_CODEDEFAULT_QUEUE统一在配置文件中维护

  • 在项目中根据项目需求配置线程池

  • 让Spring进行管理和监听,一旦有数据变化,马上消费,可以让这个类实现ApplicationRunner接口,重新run方法

zzyl-nursing-platform模块中导入依赖:

XML 复制代码
<!-- amqp 1.0 qpid client -->
<dependency>
    <groupId>org.apache.qpid</groupId>
    <artifactId>qpid-jms-client</artifactId>
    <version>0.61.0</version>
</dependency>

实现ApplicationRunner接口之后的AmqpClient:

java 复制代码
package com.zzyl.nursing.task;

import cn.hutool.core.text.CharSequenceUtil;
import com.zzyl.framework.config.properties.HuaWeiIotConfigProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.qpid.jms.*;
import org.apache.qpid.jms.message.JmsInboundMessageDispatch;
import org.apache.qpid.jms.transports.TransportOptions;
import org.apache.qpid.jms.transports.TransportSupport;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;

import javax.jms.*;
import java.net.InetAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * @author itcast
 */
@Slf4j
@Component
public class AmqpClient implements ApplicationRunner {

    @Autowired
    private HuaWeiIotConfigProperties huaWeiIotConfigProperties;

    // 业务处理异步线程池,线程池参数可以根据您的业务特点调整,或者您也可以用其他异步方式处理接收到的消息。
    @Autowired
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;

    // 控制台服务端订阅中消费组状态页客户端ID一栏将显示clientId参数。
    // 建议使用机器UUID、MAC地址、IP等唯一标识等作为clientId。便于您区分识别不同的客户端。
    private static String clientId;

    static {
        try {
            clientId = InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        start();
    }

    public void start() throws Exception {
        // 参数说明,请参见AMQP客户端接入说明文档。
        for (int i = 0; i < huaWeiIotConfigProperties.getConnectionCount(); i++) {
            // 创建amqp连接
            Connection connection = getConnection();

            // 加入监听者
            ((JmsConnection) connection).addConnectionListener(myJmsConnectionListener);
            // 创建会话。
            // Session.CLIENT_ACKNOWLEDGE: 收到消息后,需要手动调用message.acknowledge()。
            // Session.AUTO_ACKNOWLEDGE: SDK自动ACK(推荐)。
            Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
            connection.start();

            // 创建Receiver连接。
            MessageConsumer consumer = newConsumer(session, connection, huaWeiIotConfigProperties.getQueueName());
            consumer.setMessageListener(messageListener);
        }

        log.info("amqp  is started successfully, and will exit after server shutdown ");
    }

    /**
     * 创建amqp连接
     *
     * @return amqp连接
     */
    private Connection getConnection() throws Exception {
        String connectionUrl = generateConnectUrl();
        JmsConnectionFactory cf = new JmsConnectionFactory(connectionUrl);
        // 信任服务端
        TransportOptions to = new TransportOptions();
        to.setTrustAll(true);
        cf.setSslContext(TransportSupport.createJdkSslContext(to));
        String userName = "accessKey=" + huaWeiIotConfigProperties.getAccessKey();
        cf.setExtension(JmsConnectionExtensions.USERNAME_OVERRIDE.toString(), (connection, uri) -> {
            // IoTDA的userName组成格式如下:"accessKey=${accessKey}|timestamp=${timestamp}"
            String newUserName = userName;
            if (connection instanceof JmsConnection) {
                newUserName = ((JmsConnection) connection).getUsername();
            }
            return newUserName + "|timestamp=" + System.currentTimeMillis();
        });

        // 创建连接。
        return cf.createConnection(userName, huaWeiIotConfigProperties.getAccessCode());
    }

    /**
     * 生成amqp连接地址
     *
     * @return amqp连接地址
     */
    public String generateConnectUrl() {
        String uri = MessageFormat.format("{0}://{1}:{2}",
                (huaWeiIotConfigProperties.isUseSsl() ? "amqps" : "amqp"),
                huaWeiIotConfigProperties.getHost(),
                String.valueOf(huaWeiIotConfigProperties.getPort()));
        Map<String, String> uriOptions = new HashMap<>();
        uriOptions.put("amqp.vhost", huaWeiIotConfigProperties.getVhost());
        uriOptions.put("amqp.idleTimeout", String.valueOf(huaWeiIotConfigProperties.getIdleTimeout()));
        uriOptions.put("amqp.saslMechanisms", huaWeiIotConfigProperties.getSaslMechanisms());

        Map<String, String> jmsOptions = new HashMap<>();
        jmsOptions.put("jms.prefetchPolicy.queuePrefetch", String.valueOf(huaWeiIotConfigProperties.getQueuePrefetch()));
        if (CharSequenceUtil.isNotBlank(clientId)) {
            jmsOptions.put("jms.clientID", clientId);
        } else {
            jmsOptions.put("jms.clientID", UUID.randomUUID().toString());
        }
        jmsOptions.put("failover.reconnectDelay", String.valueOf(huaWeiIotConfigProperties.getReconnectDelay()));
        jmsOptions.put("failover.maxReconnectDelay", String.valueOf(huaWeiIotConfigProperties.getMaxReconnectDelay()));
        if (huaWeiIotConfigProperties.getMaxReconnectAttempts() > 0) {
            jmsOptions.put("failover.maxReconnectAttempts", String.valueOf(huaWeiIotConfigProperties.getMaxReconnectAttempts()));
        }
        if (huaWeiIotConfigProperties.getExtendedOptions() != null) {
            for (Map.Entry<String, String> option : huaWeiIotConfigProperties.getExtendedOptions().entrySet()) {
                if (option.getKey().startsWith("amqp.") || option.getKey().startsWith("transport.")) {
                    uriOptions.put(option.getKey(), option.getValue());
                } else {
                    jmsOptions.put(option.getKey(), option.getValue());
                }
            }
        }
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(uriOptions.entrySet().stream()
                .map(option -> MessageFormat.format("{0}={1}", option.getKey(), option.getValue()))
                .collect(Collectors.joining("&", "failover:(" + uri + "?", ")")));
        stringBuilder.append(jmsOptions.entrySet().stream()
                .map(option -> MessageFormat.format("{0}={1}", option.getKey(), option.getValue()))
                .collect(Collectors.joining("&", "?", "")));
        return stringBuilder.toString();
    }

    /**
     * 创建消费者
     *
     * @param session    session
     * @param connection amqp连接
     * @param queueName  队列名称
     * @return 消费者
     */
    public MessageConsumer newConsumer(Session session, Connection connection, String queueName) throws Exception {
        if (connection == null || !(connection instanceof JmsConnection) || ((JmsConnection) connection).isClosed()) {
            throw new Exception("create consumer failed,the connection is disconnected.");
        }

        return session.createConsumer(new JmsQueue(queueName));
    }

    private final MessageListener messageListener = message -> {
        try {
            // 异步处理收到的消息,确保onMessage函数里没有耗时逻辑
            threadPoolTaskExecutor.submit(() -> processMessage(message));
        } catch (Exception e) {
            log.error("submit task occurs exception ", e);
        }
    };

    /**
     * 在这里处理您收到消息后的具体业务逻辑。
     */
    private void processMessage(Message message) {
        String contentStr;
        try {
            contentStr = message.getBody(String.class);
            String topic = message.getStringProperty("topic");
            String messageId = message.getStringProperty("messageId");
            log.info("receive message,\n topic = {},\n messageId = {},\n content = {}", topic, messageId, contentStr);
        } catch (JMSException e) {
            throw new RuntimeException("服务器错误");
        }

    }

    private final JmsConnectionListener myJmsConnectionListener = new JmsConnectionListener() {
        /**
         * 连接成功建立。
         */
        @Override
        public void onConnectionEstablished(URI remoteURI) {
            log.info("onConnectionEstablished, remoteUri:{}", remoteURI);
        }

        /**
         * 尝试过最大重试次数之后,最终连接失败。
         */
        @Override
        public void onConnectionFailure(Throwable error) {
            log.error("onConnectionFailure, {}", error.getMessage());
        }

        /**
         * 连接中断。
         */
        @Override
        public void onConnectionInterrupted(URI remoteURI) {
            log.info("onConnectionInterrupted, remoteUri:{}", remoteURI);
        }

        /**
         * 连接中断后又自动重连上。
         */
        @Override
        public void onConnectionRestored(URI remoteURI) {
            log.info("onConnectionRestored, remoteUri:{}", remoteURI);
        }

        @Override
        public void onInboundMessage(JmsInboundMessageDispatch envelope) {
        }

        @Override
        public void onSessionClosed(Session session, Throwable cause) {
        }

        @Override
        public void onConsumerClosed(MessageConsumer consumer, Throwable cause) {
        }

        @Override
        public void onProducerClosed(MessageProducer producer, Throwable cause) {
        }
    };
}

效果展示:

线程池相关配置:

在目前若依的项目中已经提供了线程池的配置ThreadPoolTaskExecutor

类的位置:com.zzyl.framework.config.ThreadPoolConfig:

java 复制代码
package com.zzyl.framework.config;

import com.zzyl.common.utils.Threads;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 线程池配置
 *
 * @author ruoyi
 **/
@Configuration
public class ThreadPoolConfig
{
    // 核心线程池大小
    private int corePoolSize = 50;    //修改为Runtime.getRuntime().availableProcessors() * 2;

    // 最大可创建的线程数
    private int maxPoolSize = 200;

    // 队列最大长度
    private int queueCapacity = 1000;

    // 线程池维护线程所允许的空闲时间
    private int keepAliveSeconds = 300;

    @Bean(name = "threadPoolTaskExecutor")
    public ThreadPoolTaskExecutor threadPoolTaskExecutor()
    {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setMaxPoolSize(maxPoolSize);
        executor.setCorePoolSize(corePoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setKeepAliveSeconds(keepAliveSeconds);
        // 线程池对拒绝任务(无线程可用)的处理策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }

    /**
     * 执行周期性或定时任务
     */
    @Bean(name = "scheduledExecutorService")
    protected ScheduledExecutorService scheduledExecutorService()
    {
        return new ScheduledThreadPoolExecutor(corePoolSize,
                new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(),
                new ThreadPoolExecutor.CallerRunsPolicy())
        {
            @Override
            protected void afterExecute(Runnable r, Throwable t)
            {
                super.afterExecute(r, t);
                Threads.printException(r, t);
            }
        };
    }
}

线程池核心参数和原理

  • corePoolSize 核心线程数目

    • 到底多少合适?

      • 对于IO密集型的项目,一般设置核心线程数为:CPU核数 * 2

      • 对于计算密集型的项目,一般设置核心线程数为: CPU核数 + 1

  • maximumPoolSize 最大线程数目 = (核心线程+临时线程的最大数目)

  • keepAliveTime 生存时间 - 临时线程的生存时间,生存时间内没有新任务,此线程资源会释放

  • unit 时间单位 - 临时线程的生存时间单位,如秒、毫秒等

  • workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建临时线程执行任务

  • threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等

  • handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略

    • AbortPolicy:直接抛出异常,默认策略;

    • CallerRunsPolicy:用调用者所在的线程来执行任务;

    • DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;

    • DiscardPolicy:直接丢弃任务;

  • 执行流程(原理)

接收设备端数据:

使用若依的代码生成功能生成代码:

点击编辑修改表描述以及生成功能名称,完成后生成代码并拷贝到自己的项目当中;

最后修改DeviceData表,添加三个注解,方便使用构建者设计模式来构建对象,如下图:

未完待续,下一篇讲解具体的功能实现

相关推荐
C++chaofan2 小时前
项目中为AI添加对话记忆
java·数据结构·人工智能·redis·缓存·个人开发·caffeine
老华带你飞2 小时前
机电公司管理小程序|基于微信小程序的机电公司管理小程序设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·微信小程序·小程序·机电公司管理小程序
yuxb732 小时前
Ceph 分布式存储学习笔记(三):块存储和对象存储管理
笔记·ceph·学习
yuxb733 小时前
Ceph 分布式存储学习笔记(一):介绍、部署与集群配置(上)
笔记·ceph·学习
拾忆,想起3 小时前
AMQP协议深度解析:消息队列背后的通信魔法
java·开发语言·spring boot·后端·spring cloud
PH = 73 小时前
Spring Ai Alibaba开发指南
java·后端·spring
涛声依旧4 小时前
基于springBoot鲜花商城小程序
java·spring·微信小程序
LK_074 小时前
【Open3D】Open3D 可视化窗口交互控制说明
笔记·3d
汇能感知5 小时前
声光可调滤光器(AOTF):光谱相机的“电子调谐旋钮”
经验分享·笔记·科技