策略模式的应用

前言

系统有一个需求就是采购员审批注册供应商的信息时,会生成一个供应商的账号,此时需要发送供应商的账号信息(账号、密码)到注册填写的邮箱中,通知供应商账号信息,当时很快就写好了一个工具类,用来发送普通的文本邮件信息。但是随着系统的迭代,后面又新增了一些需求,比如一些单据需要在供应商确认时,发送一条站内信到首页,这样采购员登录时就可以看到最新的单据信息,进行相应的处理;或者采购员创建一些单据时,需要发送站内信到首页,然后供应商登录系统时,可以看到最新单据信息并进行处理,因此,我在原有的工具类基础上,修改发送邮件信息的方法,加入了消息类型参数,并根据消息类型,调用相应的方法处理;过了一段时间,业务又找了过来,说当用户修改密码时,需要发送一个短信验证码,验证码输对了才给他修改,接着我又在工具类里面,加入了处理短信的发送逻辑。

伪代码如下:

@Component
public class NoticeSendUtils {
    // 省略其他配置

    /**
     * 发送消息
     *
     * @param params 参数
     * @param type 消息类型(0-邮件消息,1-站内信消息,2-短信消息)
     * @param content 消息内容
     */
    public void sendMessage(Object params, Integer type, String content) {
        if (type.equals(0)) {
            this.sendMailMessage(params, content);
        } else if (type.equals(1)) {
            this.sendStationMessage(params, content);
        } else {
            this.sendPhoneMessage(params, content);
        }
    }


    /**
     * 发送邮件消息
     * 
     * @param params
     * @param content
     */
    private void sendMailMessage(Object params, String content) {
        // 处理邮件消息
    }

    /**
     * 发送站内信消息
     * 
     * @param params
     * @param content
     */
    private void sendStationMessage(Object params, String content) {
        // 处理站内信消息
    }

    /**
     * 发送短信消息
     * 
     * @param params
     * @param content
     */
    private void sendPhoneMessage(Object params, String content) {
        // 处理短信消息
    }

存在问题

  • 当需要新增一种消息发送类型时,需要修改该工具类加上if-else逻辑,处理新的消息类型发送,这违背了开放封闭原则(软件实体应该对扩展开放,对修改封闭。这意味着当软件需要适应新的需求时,应该通过添加新的代码来扩展系统的行为,而不是修改已有的代码),新增一种消息类型,就要修改该类原有的方法
  • 调用者调用时,需要指定消息类型和内容,系统就会存在大量这样的调用代码,如果需要在发送消息的方法新增参数,那么所有调用者都需要改变新增参数,系统后期就会非常难维护
  • 没有对消息发送过程产生的异常进行处理,无法知晓消息有没有发送成功

因此,趁着最近没有什么需求,对消息发送功能采用策略模式进行了重构,由消息模板的类型决定调用相应的消息类型处理类处理消息发送,独立维护了一个消息中心模块,也提供页面管理功能,可对消息发送模板进行配置,并且存储了消息发送记录,这样可以知晓消息有没有发送成功,对原有的消息发送功能进行了解耦。

以下仅提供部分核心代码和相关表设计,关键的是其中的设计思想

使用

消息规则配置

主要配置发送方的邮箱配置和短信功能账号配置,系统采用了阿里云的短信服务,所以配置了阿里云的短信服务的账号和密码;在发送消息时,先查一下这里面的配置,比如发送邮箱消息,则查询规则类型为邮箱的信息,查询到了就调用相应的方法发送消息

如图所示

在这里插入图片描述

消息模板配置

主要配置消息模板,每个消息模板都有唯一的模板编码,一个消息模板可以有多个适用规则,比如一个模板有短信和站内信的适用规则,那么当调用者使用这个模板时,会同时发送一个站内信(首页待办消息展示)和一封邮件信息

列表页面如图所示

在这里插入图片描述

修改页面如图所示

  • 短信相关配置只有适用规则为短信才必填
  • PC-地址主要是为了站内信实现点击消息时,跳转到对应页面
  • 短信模板编码由阿里云短信服务提供
  • 调用者的参数字段名称需要和模板内容的${}表达式中的名称一致(使用了freemarker进行模板渲染)

在这里插入图片描述

消息发送记录

主要查看消息有没有发送成功

在这里插入图片描述

消息接收中心

主要显示站内信发送情况

在这里插入图片描述

设计

消息规则配置表

CREATE TABLE `msg_configuration` (
  `id` varchar(36) NOT NULL COMMENT '主键',
  `code` varchar(255) DEFAULT NULL COMMENT '编码',
  `ip` varchar(255) DEFAULT NULL COMMENT 'ip',
  `password` varchar(255) DEFAULT NULL COMMENT '密码',
  `port` varchar(50) DEFAULT NULL COMMENT '端口',
  `protocol` varchar(100) DEFAULT NULL COMMENT '协议名称',
  `type` int(11) DEFAULT NULL COMMENT '0邮箱 1短信',
  `username` varchar(255) DEFAULT NULL COMMENT '用户名',
  `enable` int(11) DEFAULT NULL COMMENT '0未启用 1启用',
  `create_date` datetime DEFAULT NULL COMMENT '创建时间',
  `creator` varchar(36) DEFAULT NULL COMMENT '创建人',
  `update_date` datetime DEFAULT NULL COMMENT '修改时间',
  `modifier` varchar(36) DEFAULT NULL COMMENT '最后修改人',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='消息规则配置表';

消息模板配置表

CREATE TABLE `msg_public_template` (
  `id` varchar(36) NOT NULL COMMENT '主键',
  `code` varchar(200) DEFAULT NULL COMMENT '模板编号',
  `sys_notice_content` mediumtext COMMENT '模板内容',
  `message_code` varchar(100) DEFAULT NULL COMMENT '短信编码',
  `message_type_code` varchar(200) DEFAULT NULL COMMENT '消息类型  1站内信 2邮件 3短信',
  `name` varchar(200) DEFAULT NULL COMMENT '模板名称',
  `notice_type_code` tinyint(2) DEFAULT NULL COMMENT '通知类型快码',
  `service_module_code` varchar(100) DEFAULT NULL COMMENT '业务模块快照编码',
  `template_type_code` tinyint(7) DEFAULT NULL COMMENT '模板类型快照编码',
  `title` varchar(200) DEFAULT NULL COMMENT '消息模板标题',
  `pc_url` varchar(255) DEFAULT NULL COMMENT 'PC-跳转地址',
  `business_obj_id` varchar(36) DEFAULT NULL COMMENT '业务对象',
  `notice_enabled_flag` tinyint(2) DEFAULT NULL COMMENT '通知是否启用(1.启用/0.不启用)',
  `create_date` datetime DEFAULT NULL COMMENT '创建时间',
  `creator` varchar(36) DEFAULT NULL COMMENT '创建人',
  `update_date` datetime DEFAULT NULL COMMENT '修改时间',
  `modifier` varchar(36) DEFAULT NULL COMMENT '最后修改人',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='消息模板配置表';

消息发送记录表

CREATE TABLE `msg_send_record` (
  `id` varchar(36) NOT NULL,
  `content` mediumtext COMMENT '发送内容',
  `msg_public_template_id` varchar(200) DEFAULT NULL COMMENT '消息模板Id',
  `read_flag` tinyint(2) DEFAULT NULL COMMENT '已读状态(1.已读/0.未读)',
  `receiver_name` varchar(100) DEFAULT NULL COMMENT '接收人姓名',
  `receiver_uid` varchar(36) DEFAULT NULL COMMENT '接收人主键',
  `send_time` datetime DEFAULT NULL COMMENT '发送时间',
  `send_type` tinyint(2) DEFAULT NULL COMMENT '通知渠道 1站内信 2邮件 3短信',
  `status` tinyint(2) DEFAULT NULL COMMENT '发送状态(1.发送中/2.发送成功/3.发送失败)',
  `title` varchar(200) DEFAULT NULL COMMENT '发送主题',
  `business_id` varchar(36) DEFAULT NULL COMMENT '业务id(跳转页面链接可以拼接相关id跳转)',
  `create_date` datetime DEFAULT NULL COMMENT '创建时间',
  `creator` varchar(36) DEFAULT NULL COMMENT '创建人',
  `update_date` datetime DEFAULT NULL COMMENT '修改时间',
  `modifier` varchar(36) DEFAULT NULL COMMENT '最后修改人',
  `error_msg` varchar(1000) DEFAULT NULL COMMENT '错误信息',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='消息发送记录表';

实现

如图所示,经过策略模式设计如下,后续有新的消息类型增加,只需要新增一个具体策略类实现相关发送逻辑即可,无需修改原有的代码,没有违背开放封闭原则

  • 消息发送类型的抽象策略类NoticeExchanger,规定了具体策略类必须重写的抽象方法match(是否支持当前消息类型发送)、exchanger(处理消息发送),以及自己实现的saveMessageRecord方法(保存消息发送记录)、parseMessage方法(解析模板内容和标题)
  • 具体策略类EmailNoticeExchanger,负责邮件消息的发送
  • 具体策略类StationNoticeExchanger,负责站内信的发送
  • 具体策略类SmsNoticeExchanger,负责短信消息的发送
  • 环境类NoticeServiceImpl,维护一个策略对象的引用集合,负责将消息发送请求委派给具体的策略对象执行
在这里插入图片描述
抽象策略类NoticeExchanger
public abstract class NoticeExchanger {

    @Resource
    private MsgSendRecordService msgSendRecordService;


    /**
     * 是否支持当前消息类型发送(true-支持,false-不支持)
	 
     * @param type 消息类型
     * @return
     */
    public abstract boolean match(String type);

    /**
     * 处理消息发送
	 *
     * @param map 相关参数
     * @return
     */
    public abstract boolean exchanger(Map<String, Object> map) throws Exception;


    /**
     * 使用Freemarker解析模板内容和标题
	 *
     * @param notice 相关参数
     * @return
     */
    public Map<String, Object> parseMessage(Map<String, Object> notice){
        MsgPublicTemplate msgPublicTemplate = notice.get("msgPublicTemplate");
        if(msgPublicTemplate==null){
            throw new CommonException(ExceptionDefinition.TEMPLATE_NOT_FOUND);
        }
        if(msgPublicTemplate.getNoticeEnabledFlag().intValue()==0){
            throw new CommonException(ExceptionDefinition.TEMPLATE_NOT_ENABLED);
        }
        //freemarker解析模板,填充模板内容
        //标题
        String title=msgPublicTemplate.getTitle();
        //内容
        String sysNoticeContent = msgPublicTemplate.getContent();
        Map<String, Object> params = notice.get("params");
        try {
            title= FreemarkerUtils.generateContent(params,title);
            sysNoticeContent=FreemarkerUtils.generateContent(params,sysNoticeContent);
        } catch (Exception e) {
            throw new CommonException(ExceptionDefinition.TRANSFORMATION_OF_THE_TEMPLATE);
        }
		Map<String, Object> result = new HashMap<>();
		result.put("title", title);
		result.put("sysNoticeContent", sysNoticeContent);
        return result;
    }

    /**
     * 保存消息发送记录
	 *
     * @param msgSendRecordDto 相关参数
     * @return
     */
    public void saveSendMessage(MsgSendRecordDto msgSendRecordDto){
        // ...参数校验
        String [] ids = msgSendRecordDto.getUserId().split(",");
        String [] names = msgSendRecordDto.getUserName().split(",");
        // ...参数填充
        //是否多个用户
        if (ids.length == 0) {
            msgSendRecord.setReceiverName(msgSendRecordDto.getUserName())
                    .setReceiverUid(msgSendRecordDto.getUserId());
            msgSendRecordService.save(msgSendRecord);
        }
        if (ids.length > 0) {
            List<MsgSendRecord> msgSendRecordList = Lists.newArrayList();
            for (int i = 0; i < ids.length; i++) {
                MsgSendRecord data = BeanUtils.copyProperties(msgSendRecordDto, MsgSendRecord.class);
                data.setReceiverUid(ids[i]);
                data.setReceiverName(names[i]);
                msgSendRecordList.add(data);
            }
            msgSendRecordService.saveBatch(msgSendRecordList);
        } 
    }
}
具体策略类EmailNoticeExchanger

发送邮件

@Component
public class EmailNoticeExchanger extends NoticeExchanger {
    private Logger logger = LoggerFactory.getLogger(EmailNoticeExchanger.class);
    @Autowired
    private ISendEmailService sendEmailService;
    @Autowired
    private MsgConfigurationMapper msgConfigurationMapper;

    /**
     * 是否支持邮件发送
     *
     * @param type 消息类型
     * @return
     */
    @Override
    public boolean match(String type) {
        if (!String.valueOf(SendTypeEnum.EMAIL.getItem()).equals(type)) {
            return false;
        }
        return true;
    }

     /**
     * 处理消息发送
	 *
     * @param map 相关参数
     * @return
     */
    @Override
    public boolean exchanger(Map<String, Object> map) throws Exception {
        EmailNotice notice = new EmailNotice();
        BeanUtils.populate(notice, map);
        String code = notice.getCode();
        Map<String, Object> params = notice.getParams();
		// 解析模板内容和标题
        Map<String, Object> objectMap = parseMessage(map);
		String title = objectMap.get("title") == null ? "" : objectMap.get("title").toString();
		String sysNoticeContent = objectMap.get("sysNoticeContent") == null ? "" : objectMap.get("sysNoticeContent").toString();
		try {
			// 查询邮箱配置
			MsgConfiguration msgConfiguration = 省略...
			if (msgConfiguration == null) {
				throw new CommonException(ExceptionDefinition.NO_LAUNCH_CONFIGURATION);
			}
			// 组装参数发送邮件
			EmailConfig emailConfig = new EmailConfig();
			emailConfig.setUsername(msgConfiguration.getUsername());
			emailConfig.setPassword(msgConfiguration.getPassword());
			emailConfig.setMailServerHost(msgConfiguration.getIp());
			emailConfig.setMailServerPort(msgConfiguration.getPort());
			emailConfig.setProtocol(msgConfiguration.getProtocol());
			emailConfig.setFromAddress(msgConfiguration.getUsername());

			MailData mailData = new MailData();
			mailData.setSubject(title);
			mailData.setContent(sysNoticeContent);
			mailData.setToAddresss(notice.getToAddress());
			mailData.setCcAddresss(notice.getCcAddress());
			//发送邮件
			sendEmailService.sendMail(mailData, emailConfig);
			// 省略组装参数...
			// 保存发送记录
			saveSendMessage(msgSendRecordDto);
			logger.info("send email success!");
			return true;
		} catch (Exception e) {
			logger.error(e.getMessage());
			// 省略组装参数...
			// 保存发送记录
			saveSendMessage(msgSendRecordDto);
			return false;
		}
        return false;
    }
}
具体策略类StationNoticeExchanger

发送站内信

@Component
public class StationNoticeExchanger extends NoticeExchanger {
    private Logger logger = LoggerFactory.getLogger(StationNoticeExchanger.class); 

    /**
     * 是否支持站内信发送
     *
     * @param type 消息类型
     * @return
     */
    @Override
    public boolean match(String type) {
        if (!String.valueOf(SendTypeEnum.STATION.getItem()).equals(type)) {
            return false;
        }
        return true;

    }

     /**
     * 处理消息发送
	 *
     * @param map 相关参数
     * @return
     */
    @Override
    public boolean exchanger(Map<String, Object> map) throws Exception {
        logger.info("=========== send station begin !========================");
        StationNotice notice = new StationNotice();
        BeanUtils.populate(notice, map);
		// 解析模板内容和标题
        Map<String, Object> objectMap = parseMessage(map);
		String title = objectMap.get("title") == null ? "" : objectMap.get("title").toString();
		String sysNoticeContent = objectMap.get("sysNoticeContent") == null ? "" : objectMap.get("sysNoticeContent").toString();
		// 发送站内信即保存发送记录即可,记录类型为站内信
		MsgSendRecordDto msgSendRecordDto = new MsgSendRecordDto();
		// 省略组装参数...
	    // 保存发送记录
	    saveSendMessage(msgSendRecordDto);
		logger.info("=================send station success!==========================");
        return true;
    }
}
具体策略类SmsNoticeExchanger

发送短信

@Component
public class SmsNoticeExchanger extends NoticeExchanger{
	
    private Logger logger = LoggerFactory.getLogger(SmsNoticeExchanger.class); 

    @Autowired
    private ISendSmsService sendSmsService;

    @Autowired
    private MsgConfigurationMapper msgConfigurationMapper;


    @Override
    public boolean match(String type) {
        if(!String.valueOf(SendTypeEnum.SMS.getItem()).equals(type)){
            return false;
        }
        return true;
    }

    @Override
    public boolean exchanger(Map<String, Object> map) {
        SmsNotice notice = new SmsNotice();
        BeanUtils.populate(notice, map);
        // 解析模板内容和标题,这里的模板内容和标题只在发送记录使用,短信的模板内容配置在了阿里云短信服务
        Map<String, Object> objectMap = parseMessage(map);
		String title = objectMap.get("title") == null ? "" : objectMap.get("title").toString();
		String sysNoticeContent = objectMap.get("sysNoticeContent") == null ? "" : objectMap.get("sysNoticeContent").toString();
		try {
			// 查询短信配置
			MsgConfiguration msgConfiguration = 省略...
			if(msgMailConfiguration == null){
				throw new CommonException(ExceptionDefinition.SEND_CHANNELS);
			}
			logger.info("send sms success begin !");
			//发送短信,填充阿里云用户名、密码、短信模板编码、参数等等,调用阿里云api发送短信
			sendSmsService.sendSms(notice, msgConfiguration);
			// 省略组装参数...
			// 保存发送记录
			saveSendMessage(msgSendRecordDto);
			logger.info("send sms success!");
		} catch (Exception e) {
			// 省略组装参数...
			// 保存发送记录
			saveSendMessage(msgSendRecordDto);
			logger.error(e.getMessage());
			throw new CommonException(ExceptionDefinition.SEND_SMS_EXCEPTIONS);
		}
        return true;
    }
}
消息类型枚举类
public enum SendTypeEnum {
    /**
     * 通知渠道类型
     */
    STATION(1,"站内信"),
    EMAIL(2,"邮件"),
    SMS(3,"短信");


    private int item;

    private String itemName;

    SendTypeEnum(int item, String itemName) {
        this.item = item;
        this.itemName = itemName;
    }

    public int getItem() {
        return item;
    }

    public void setItem(int item) {
        this.item = item;
    }

    public String getItemName() {
        return itemName;
    }

    public void setItemName(String itemName) {
        this.itemName = itemName;
    }

    public static String getItemName(int item){
        for (SendTypeEnum es : SendTypeEnum.values()){
            if(item == es.getItem()){
                return es.getItemName();
            }
        }
        return "";
    }
}
环境类NoticeServiceImpl

负责将消息发送请求委派给具体的策略对象执行

@Service
@Slf4j
public class NoticeServiceImpl implements NoticeService,ApplicationContextAware {
    
	// 保存所有的消息策略类
    private Collection<NoticeExchanger> exchangers;

    // 线程池,异步发送消息
    private ExecutorService executorService;
	
	
	@Resource
    private NoticeConvertUtils noticeConvertUtils;

    @Resource
    private MsgPublicTemplateMapper msgPublicTemplateMapper;

    public NoticeServiceImpl(){
		// 创建线程池
        Integer availableProcessors = Runtime.getRuntime().availableProcessors();
        Integer numOfThreads = availableProcessors * 2;
        executorService = new ThreadPoolExecutor(availableProcessors,numOfThreads,100, TimeUnit.SECONDS,new LinkedBlockingDeque<>());

    }
	
	/**
     * 当前Bean初始化之前会执行当前方法,获取所有的消息策略类
     */
	@Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		// 获取实现了NoticeExchanger接口的所有bean
        Map<String, NoticeExchanger> beansOfType = applicationContext.getBeansOfType(NoticeExchanger.class);
        this.exchangers=beansOfType.values();
    }
	
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void sendMessage(NoticeParamDto noticeParamDto) {
        Map<String, Object> notice = null;
        try {
			// 将参数转换成map
            notice = noticeConvertUtils.sendMessageIsParam(notice);
        } catch (Exception e) {
            log.error("消息发送失败,消息模板内容转换失败", e);
            throw new CommonException("消息发送失败,消息模板内容转换失败", 999);
        }
		
        if(notice.get("code") == null){
            throw new CustomException(CommonCode.NO_TEMPLATE);
        }
        QueryWrapper<MsgPublicTemplate> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("code", notice.get("code").toString());
        MsgPublicTemplate msgPublicTemplate  = msgPublicTemplateMapper.selectOne(queryWrapper);
        notice.put("msgPublicTemplate",msgPublicTemplate);
        if(msgPublicTemplate.getMessageTypeCode() == null){
            throw new CustomException(CommonCode.NO_TEMPLATE);
        }
		// 获取当前消息模板的类型,以逗号隔开,由所有的消息策略类去匹配类型,匹配成功则提交任务给线程池异步执行
        String[] array = msgPublicTemplate.getMessageTypeCode().split(",");
        for(int i = 0; i < array.length; i++){
            if(StringUtils.isNotBlank(array[i])){
                exchangers.forEach(item->{
                    if(item.match(array[i)){
                        //开启线程池处理
                        log.info("发送站内信任务提交");
                        executorService.submit(new NoticeTask(item,notice));
                        log.info("发送站内信任务提交完成");
                    }
                });
            }
        }
    }
}

noticeConvertUtils的sendMessageIsParam(notice)逻辑,主要将传递过来的参数转成Map对象

public Map<String, Object> sendMessageIsParam(NoticeParamDto notice) throws Exception {
        Map<String, Object> map = new HashMap<>();
        map = this.convertToMap(notice.getParams(), map);
        Map<String, Object> returnMap = new HashMap<>();
        returnMap.put("businessId", notice.getBusinessId());
        returnMap.put("code", notice.getSendMessageCode());
        returnMap.put("phones", notice.getPhones());
        returnMap.put("toAddress", notice.getToAddress());
        if (StringUtils.isEmpty(notice.getCcAddress())) {
            returnMap.put("ccAddress", notice.getCcAddress());
        }
        returnMap.put("userId", notice.getUserId());
        returnMap.put("userName", notice.getUserName());
        returnMap.put("params", map);
        return returnMap;
    }
参数类NoticeParamDto
@Data
@Accessors(chain = true)
public class NoticeParamDto {

    /**
     * 消息id
     */
    private String id;

    /**
     * 业务单据id
     */
    private String businessId;

    /**
     * 消息模板编码
     */
    private String sendMessageCode;

    /**
     * 接收人手机号
     */
    private String phones;

    /**
     * 接收人邮箱,多个以英文逗号分割
     */
    private String toAddress;

    /**
     * 抄送人邮箱
     */
    private String ccAddress;

    /**
     * 接收人账号
     */
    private String userId;

    /**
     * 接收人姓名
     */
    private String userName;

    /**
     * 传递参数,字段名称需和模板内容、标题一样,否则解析模板内容、标题失败
     */
    private Object params;
}
任务类NoticeTask
@Slf4j
public class NoticeTask implements Callable<Boolean>{
    private NoticeExchanger noticeExchanger;
    private Map<String, Object> notice;

    public NoticeTask(NoticeExchanger noticeExchanger, Map<String, Object> notice){
        this.noticeExchanger=noticeExchanger;
        this.notice=notice;
    }
    @Override
    public Boolean call() throws Exception {
        log.info("============发送消息任务开始=============");
        return noticeExchanger.exchanger(notice);
    }
}

至此,核心代码已经介绍完成。由于这是一个独立的服务,所以我写了一个接口来调用NoticeServiceImpl的发送消息方法,然后再写一个Feign接口提供给其他服务使用,调用者调用时只需要传递消息模板编码、接收人消息等参数即可,无需在原来的代码上写上大量的内容拼接参数处理,实现了解耦,后续也更好维护。

当后续需要新增消息发送类型,比如要发送微信公众号消息,接着扩展即可,新增一个微信公众号消息发送的策略类和枚举类型,写对应逻辑即可,其他不需要变化,这样就非常灵活,也变得易扩展、易维护了。

当然这里还可以优化,比如我上面代码用了大量的map操作,有时候这些参数看着一头雾水,应该封装成实体类;再比如,我这里采用的是feign远程调用,对于调用者来说还是同步调用,需要等待其发送消息完成,有一定的性能消耗,后续可以采用消息队列进行优化,调用者将参数发送到消息队列就返回客户端提升用户体验,然后环境类监听主题消费即可。当然引入消息队列,还得考虑其中的常见问题(消息丢失、消息重复消费等等)。

好了,今天就讲这么多了!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/778579.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

华为机试HJ34图片整理

华为机试HJ34图片整理 题目&#xff1a; 想法&#xff1a; 将输入的字符串中每个字符都转为ASCII码&#xff0c;再通过快速排序进行排序并输出 input_str input() input_list [int(ord(l)) for l in input_str]def partition(arr, low, high):i low - 1pivot arr[high]f…

基于深度学习LightWeight的人体姿态检测跌倒系统源码

一. LightWeight概述 light weight openpose是openpose的简化版本&#xff0c;使用了openpose的大体流程。 Light weight openpose和openpose的区别是&#xff1a; a 前者使用的是Mobilenet V1&#xff08;到conv5_5&#xff09;&#xff0c;后者使用的是Vgg19&#xff08;前10…

啥?你没听过SpringBoot的FatJar?

写在最前面&#xff1a; SpringBoot是目前企业里最流行的框架之一&#xff0c;SpringBoot的部署方式多数采用jar包形式。通常&#xff0c;我们使用java -jar便可以直接运行jar文件。普通的jar只包含当前 jar的信息&#xff0c;当内部依赖第三方jar时&#xff0c;直接运行则会报…

数字化精益生产系统--MRP 需求管理系统

MRP&#xff08;Material Requirements Planning&#xff0c;物料需求计划&#xff09;需求管理系统是一种在制造业中广泛应用的计划工具&#xff0c;旨在通过分析和计划企业生产和库存需求&#xff0c;优化资源利用&#xff0c;提高生产效率。以下是对MRP需求管理系统的功能设…

[FreeRTOS 功能应用] 事件组 功能应用

文章目录 一、基础知识点二、代码讲解三、结果演示四、代码下载 一、基础知识点 [FreeRTOS 基础知识] 事件组 概念 [FreeRTOS 内部实现] 事件组 本实验是基于STM32F103开发移植FreeRTOS实时操作系统&#xff0c;事件组实战操作。(当task1和task2同时完成&#xff0c;才执行ta…

Python爬虫教程第1篇-基础知识

文章目录 什么是爬虫爬虫的工作原理用途搜索引擎爬虫Robots协议HTTP的请求过程URL的含义HTTP常见请求头爬虫常用的技术 什么是爬虫 信息的交互是通过web网页、或者移动端等不同的客户端端形式进行交互&#xff0c;这个过程是一个人与网路正常的交互行为。而爬虫可以用来模拟人…

easyx图形库

目录 1、绘制简单的图形化窗口 2、设置窗口属性 2.1 颜色设置 2.2 刷新 3、基本绘图函数 3.1 绘制直线 3.2 绘制圆 3.3 绘制矩形 4、贴图 4.1 原样贴图 4.1.1 IMAGE变量去表示图片 4.1.2 加载图片 4.1.3 显示图片 4.2 透明贴图 4.2.1 认识素材 4.3 png贴图 5…

使用块的网络 VGG

一、AlexNet与VGG 1、深度学习追求更深更大&#xff0c;使用VGG将卷积层组合为块 2、VGG块&#xff1a;3*3卷积&#xff08;pad1&#xff0c;n层&#xff0c;m通道&#xff09;、2*2最大池化层 二、VGG架构 1、多个VGG块后接全连接层 2、不同次数的重复块得到不同的架构&a…

go语言day10 接口interface 类型断言 type关键字

接口&#xff1a; 空接口类型&#xff1a; 要实现一个接口&#xff0c;就要实现该接口中的所有方法。因为空接口中没有方法&#xff0c;所以自然所有类型都实现了空接口。那么就可以使用空接口类型变量去接受所有类型对象。 类比java&#xff0c;有点像Object类型的概念&#x…

使用Docker、Docker-compose部署单机版达梦数据库(DM8)

安装前准备 Linux Centos7安装&#xff1a;https://blog.csdn.net/andyLyysh/article/details/127248551?spm1001.2014.3001.5502 Docker、Docker-compose安装&#xff1a;https://blog.csdn.net/andyLyysh/article/details/126738190?spm1001.2014.3001.5502 下载DM8镜像 …

数据按月分表

当数据量过大&#xff0c;从数据层面可以按月分表&#xff0c;报表查询时可以根据&#xff0c;查询时间来计算查询的年月&#xff0c;查询对应的表 1、按月分表&#xff1a; 存储过程SP_BRANCH_TABLE_TEST 以下存储过程分表&#xff0c;加了索引可以方便后续查询 USE [DASHBOAR…

三分钟内了解卷轴模式

在数字化时代的浪潮中&#xff0c;卷轴商业模式巧妙地将积分体系、互动任务、社交裂变、虚拟经济体系以及个性化成长路径等多元要素融为一体。 积分体系&#xff1a;激发参与动力的源泉 卷轴商业模式的核心在于其精心构建的积分系统。新用户踏入平台&#xff0c;即获赠一笔启…

Windows上Docker的安装与初体验

Docker Desktop下载地址 国内下载地址 一、基本使用 1. 运行官方体验镜像 docker run -d -p 80:80 docker/getting-started执行成功 停止体验服务 docker stop docker/getting-started删除体验镜像 docker rmi docker/getting-started2. 修改docker镜像的存储位置 3. …

vofa+:一款超级好用的可视化串口调试软件

目录 一、软件配置 1、先配置好usart1串口 2、重定向printf: 3&#xff0c;勾选魔术棒中的LIB 二、vofa的使用 1、RawData模式 2、FireWater 一、软件配置 1、先配置好usart1串口 2、重定向printf: 在 stm32f4xx_hal.c中添加&#xff1a; #include <stdio.h> e…

【nvm管理nodejs版本,切换node指定版本】

nvm管理nodejs版本 nvm管理nodejs版本主要功能使用 nvm nvm管理nodejs版本 nvm&#xff08;Node Version Manager&#xff09;顾名思义node版本管理器&#xff0c;无须去node管网下载很多node安装程序;用于管理多个 Node.js 版本的工具。它允许你在同一台机器上同时安装和管理…

文件上传(本地、OSS)

什么是文件上传&#xff1a;将文件上传到服务器。 文件上传-本地存储 前端 <template> <div><!-- 上传文件需要设置表单的提交方式为post&#xff0c;并设置enctype属性、表单项的type属性设置为file --><form action"http://localhost:8080/wedu/…

使用Python绘制和弦图

使用Python绘制和弦图 和弦图效果代码 和弦图 和弦图用于展示数据的多对多关系&#xff0c;适合用于社交网络、交通流量等领域的分析。 效果 代码 import pandas as pd import holoviews as hv from holoviews import opts hv.extension(bokeh)# 示例数据 data [(A, B, 2),…

价格预言机的使用总结(一):Chainlink篇

文章首发于公众号&#xff1a;Keegan小钢 前言 价格预言机已经成为了 DeFi 中不可获取的基础设施&#xff0c;很多 DeFi 应用都需要从价格预言机来获取稳定可信的价格数据&#xff0c;包括借贷协议 Compound、AAVE、Liquity &#xff0c;也包括衍生品交易所 dYdX、PERP 等等。…

vb.netcad二开自学笔记1:万里长征第一步Hello CAD!

已入门的朋友请绕行&#xff01; 今天开启自学vb.net 开发autocad&#xff0c;网上相关资料太少了、太老了。花钱买课吧&#xff0c;穷&#xff01;又舍不得&#xff0c;咬牙从小白开始摸索自学吧&#xff0c;虽然注定是踏上了一条艰苦之路&#xff0c;顺便作个自学笔记备忘!积…

网络安全领域国标分类汇总大全V1.0版:共计425份标准文档,全部可免费下载

《网络安全法》、《数据安全法》、《个人信息保护法》落地实施需要大量国家标准的支撑&#xff0c;博主耗时三周时间&#xff0c;吐血整理了从1999年至今相关的所有涉及安全的国家标准&#xff0c;梳理出《网络安全领域国标分类汇总大全V1.0版》&#xff0c;共计 425 项现行标准…