SpringBoot监控服务器信息以及SpringBoot自带Health Indicator

SpringBoot监控服务器内存信息简介

/**
* 定时任务监控服务器内存信息
*/
@Component
public class OsMonitorScheduleTask implements Runnable {
 
    @Resource
    private OsMonitorMapper osMonitorMapper;
 
    @Resource
    private SystemUserMapper systemUserMapper;
 
    private final FeedbackData feedbackData;
 
    private static final Logger log = LoggerFactory.getLogger(ValidCheckTask.class);
 
    @Autowired
    public OsMonitorScheduleTask(FeedbackData feedbackData) {
        this.feedbackData = feedbackData;
    }
 
    @Override
    public void run() {
        try {
            doBusiness();
        } catch (Exception e) {
            log.error("It has an error when get monitor info. message:{}", e.getMessage());
        }
    }

    /**
     * 具体业务处理
     */
    private void doBusiness() {
 
        // 重新加载配置
        ConfigProperties.loadConfig(FileUtils.getWebRootPath());
        int WARNING_THRESHOLD = Integer.parseInt(ConfigProperties.getKey(ConfigList.BASIC, "ALARM_THRESHOLD"));
        int sentType = Integer.parseInt(ConfigProperties.getKey(ConfigList.BASIC, "ALARM_SEND_TYPE"));
 
        OsMonitorInfoBean monitorInfo = OsMonitorUtils.GetMonitorInfo();
        monitorInfo.setThreshold(WARNING_THRESHOLD);
        monitorInfo.setRecordTime(new java.util.Date());
        double cpuRatio = Double.parseDouble(monitorInfo.getCpuRatio());
        if (cpuRatio < WARNING_THRESHOLD) {
            monitorInfo.setWarning(0);
            osMonitorMapper.osMonitorLogInsert(monitorInfo);
            return;
        }
monitorInfo.setWarning(1);
 
        OsMonitorInfoBean lastAlarm = osMonitorMapper.getLatestAlarmedMonitorInfo();
        long alarmInterval = Long.parseLong(ConfigProperties.getKey(ConfigList.BASIC, "ALARM_INTERVAL_MINUTE"));
        if (lastAlarm != null && ((new java.util.Date().getTime() - lastAlarm.getRecordTime().getTime()) <= (alarmInterval - 2) * 60 * 1000)) {
            monitorInfo.setAlarmType(0);
            osMonitorMapper.osMonitorLogInsert(monitorInfo);
            return; // 存在上一次报警但不超过 alarmInterval 分钟, 不报警
        }
 
        List<SystemUserBean> adminUsers = systemUserMapper.getSystemUserByName("superUser");
 
        // 短信通知管理员
        if (sentType == 0) {
            monitorInfo.setAlarmType(0);
            osMonitorMapper.osMonitorLogInsert(monitorInfo);
            log.error("突破报警阈值,但不发送报警信息");
        } else if (sentType == 1) {  // 发短信
            monitorInfo.setAlarmType(1);
            monitorInfo.setAlarmAddr(adminUsers.get(0).getPhone());
            osMonitorMapper.osMonitorLogInsert(monitorInfo);
            SendSmsMessage(WARNING_THRESHOLD, cpuRatio, adminUsers);
        } else if (sentType == 2) {  // 发邮箱
            monitorInfo.setAlarmType(2);
            monitorInfo.setAlarmAddr(adminUsers.get(0).getEmail());
            osMonitorMapper.osMonitorLogInsert(monitorInfo);
            SendMailMessage(WARNING_THRESHOLD, cpuRatio, adminUsers);
        } else if (sentType == 3) {  // 短信、邮箱同时发
            monitorInfo.setAlarmType(3);
            monitorInfo.setAlarmAddr(adminUsers.get(0).getPhone() + ";" + adminUsers.get(0).getEmail());
            osMonitorMapper.osMonitorLogInsert(monitorInfo);
            SendSmsMessage(WARNING_THRESHOLD, cpuRatio, adminUsers);
            SendMailMessage(WARNING_THRESHOLD, cpuRatio, adminUsers);
        }
    }
 
    // 发送短信
    private void SendSmsMessage(int WARNING_THRESHOLD, double cpuRatio, List<SystemUserBean> adminUsers) {
        if (adminUsers == null || adminUsers.size() == 0
                || adminUsers.get(0).getPhone() == null
                || adminUsers.get(0).getPhone().equals("")) {
            return;
        }
        FeedBack.addFeedbackMessage(message);
    }
 
    // 发送邮箱
    private void SendMailMessage(int WARNING_THRESHOLD, double cpuRatio, List<SystemUserBean> adminUsers) {
        if (adminUsers == null || adminUsers.size() == 0
                || adminUsers.get(0).getEmail() == null
                || adminUsers.get(0).getEmail().equals("")) {
            return;
        }
        FeedBackMessageBean message = new FeedBackMessageBean();
        String mailContact = adminUsers.get(0).getEmail();
        message.setContact(mailContact);
        message.setMsgtype(FeedbackDataType.MAIL_TYPE);
        message.setSubject("后台CPU报警");
        message.setContent(String.format("后台CPU使用率已达 %.2f%%,超过阈值 %2d%%,请关注。", cpuRatio, WARNING_THRESHOLD));
        feedbackData.addFeedbackMessage(message);
    }
}

FeedBack

@Component
public class FeedbackData {
 
    private static ConcurrentLinkedQueue<FeedBackMessageBean> queue = new ConcurrentLinkedQueue<FeedBackMessageBean>();
 
    private final RBlockingQueue<FeedBackMessageBean> rQueue;
 
    @Autowired
    public FeedbackData(RBlockingQueueService rBlockingQueueService) {
        rQueue = rBlockingQueueService.getBlockingQueue("FeedBackMessage");
    }
 
    /**
     * 添加信息到队列等待发送
     *
     * @param messageBean FeedBackMessageBean
     */
    public void addFeedbackMessage(FeedBackMessageBean messageBean) {
        if (messageBean != null) {
            //queue.add(messageBean);
            rQueue.add(messageBean);
        }
    }
 
    /**
     * 获取队列任务信息
     *
     * @return FeedBackMessageBean
     */
    public FeedBackMessageBean getFeedbackMessage() {
        if (!rQueue.isEmpty()) {
            return rQueue.poll();
        }
        return null
    }
}

SendFeedBack

public class SendFeedbackTask implements Runnable {
 
    private static final Logger logger = LoggerFactory.getLogger(SendFeedbackTask.class);
 
    private final FeedbackData feedbackData;
 
    /**
     * 线程是否存活
     */
    private boolean alive = false;
 
    /**
     * 消息处理线程
     */
    private Thread t = null;
 
    @Autowired
    public SendFeedbackTask(FeedbackData feedbackData) {
        this.feedbackData = feedbackData;
    }
 
    /**
     * 关闭线程
     */
    public void stop() {
        alive = false;
    }
 
    /**
     * 启动线程
     */
    public void start() {
        alive = true;
        t = new Thread(this);
        t.setName(this.getClass().getSimpleName());
        t.start();
    }

    @Override
    public void run() {
        do {
            try {
                if (!doBusiness()) {
                    try {
                        Thread.sleep(100);
                    } catch (Exception e) {
                        logger.error("It has an error when thread sleep.");
                    }
                }
            } catch (Exception e) {
                try {
                    Thread.sleep(100);
                } catch (Exception ee) {
                    logger.error("It has an error when thread sleep.");
                }
 
                logger.debug("It has an error when receive message. {}", e.getLocalizedMessage());
            }
        } while (alive);
    }
private boolean doBusiness() {
        FeedBackMessageBean message = feedbackData.getFeedbackMessage();
 
        if (message != null) {
 
            if (message.getMsgtype() == FeedbackDataType.SMS_TYPE) {
                SmsServerBean smsBean = (SmsServerBean) ConfigData.getConfigData(FieldConstants.SMS_SERVER_TYPE);
                if (smsBean == null || StringUtils.isEmpty(smsBean.getAccessKeyID())
                        || StringUtils.isEmpty(smsBean.getAccessKeySecret())
                        || StringUtils.isEmpty(smsBean.getSignName())
                        || StringUtils.isEmpty(smsBean.getFeedBackTemplateCode())
                        || StringUtils.isEmpty(smsBean.getAuthTemplateCode())) {
                    logger.error("It has an error when get the sms template.");
                    return false;
                }
 
                if (!StringUtils.isEmpty(message.getContent()) && !StringUtils.isEmpty(message.getContact())) {
         if (message.getSmstype() == FeedbackDataType.SMS_QUESTION_TYPE) {
                        String content = "{'question':'" + message.getContent() + "'}";
                        SMSUtils.sendSMSByAliyun(smsBean, message.getContact(), smsBean.getFeedBackTemplateCode(),
                                content);
                    }
 
                    if (message.getSmstype() == FeedbackDataType.SMS_AUTH_TYPE) {
                        String content = "{'reason':'" + message.getContent() + "'}";
                        SMSUtils.sendSMSByAliyun(smsBean, message.getContact(), smsBean.getAuthTemplateCode(), content);
                    }
 
                    if (!StringUtils.isEmpty(message.getContent()) && !StringUtils.isEmpty(message.getContact())) {
                        if (message.getSmstype() == FeedbackDataType.SMS_ALARM_TYPE) {
                            String content = "{'param':'" + message.getContent() + "'}";
                        SMSUtils.sendSMSByAliyun(smsBean, message.getContact(), smsBean.getAlarmTemplateCode(), content);
                        }
                    }
                    return true;
                }
            }
 
            if (message.getMsgtype() == FeedbackDataType.MAIL_TYPE) {
 
                MailServerBean mailServerBean = (MailServerBean) ConfigData
                        .getConfigData(FieldConstants.MAIL_SERVER_TYPE);
                if (mailServerBean == null || StringUtils.isEmpty(mailServerBean.getHost())
                        || StringUtils.isEmpty(mailServerBean.getPort())
                        || StringUtils.isEmpty(mailServerBean.getMail())
                        || StringUtils.isEmpty(mailServerBean.getPassword())) {
                    logger.error("It has an error when get the mail template.");
                    return false;
                }
MailMessage mailInfo = new MailMessage();
                mailInfo.setMailServerHost(mailServerBean.getHost());
                mailInfo.setMailServerPort(mailServerBean.getPort());
                mailInfo.setValidate(true);
                mailInfo.setUserName(mailServerBean.getMail());
                mailInfo.setPassword(mailServerBean.getPassword());
                mailInfo.setFromAddress(mailServerBean.getFromaddress());
 
                String mailBody = message.getContent();
 
                boolean useHtmlTemplate = message.getUseHtmlTemplate();
                if (!useHtmlTemplate) {  // 没有使用模板时需要替换
                    mailBody = mailBody.replaceAll(" ", "&nbsp;");
                    mailBody = mailBody.replaceAll("\n", "<br/>");
                }
 
                mailInfo.setToAddress(message.getContact());
                mailInfo.setSubject(message.getSubject());
                mailInfo.setContent(mailBody);
                if (MailSender.sendHtmlMail(mailInfo)) {
                    logger.info("success to send mail. receiver={}", message.getContact());
                } else {
                    logger.info("failed to send mail. receiver={}", message.getContact());
                }
            }
        }
        return false;
    }
}

邮件内容主题

    // 发送邮件的服务器的IP和端口
	private String mailServerHost;
	private String mailServerPort = "25";
	// 邮件发送者的地址
	private String fromAddress;
	// 邮件接收者的地址
	private String toAddress;
	// 登陆邮件发送服务器的用户名和密码
	private String userName;
	private String password;
	// 是否需要身份验证
	private boolean validate = false;
	// 邮件主题
	private String subject;
	// 邮件的文本内容
	private String content;
	// 邮件附件的文件名
	private String[] attachFileNames;
 
	// 重传次数 最多3次
	private Integer reSentTimes = 0;

    /**
	 * 获得邮件会话属性
	 */
	public Properties getProperties() {
		Properties p = new Properties();
 
		MailSSLSocketFactory sf = null;
		try {
			sf = new MailSSLSocketFactory();
		} catch (GeneralSecurityException e1) {
			e1.printStackTrace();
			log.error("getProperties get an exception:" + e1.getLocalizedMessage());
			return p;
		}
		sf.setTrustAllHosts(true);
 
		p.put("mail.smtp.host", this.mailServerHost);
		p.put("mail.smtp.port", this.mailServerPort);
		if ("ON".equalsIgnoreCase(ConfigProperties.getKey(ConfigList.BASIC, "SMTP_TLS"))) {
			p.put("mail.smtp.starttls.enable", "true");
		} else {
			p.put("mail.smtp.starttls.enable", "false");
		}
 
		p.put("mail.smtp.connectiontimeout", "30000");
		p.put("mail.smtp.timeout", "30000");
		p.put("mail.smtp.auth", validate ? "true" : "false");
 
		p.put("mail.pop3.socketFactory", sf);
		p.put("mail.pop3.socketFactory.fallback", "false");
		p.put("mail.pop3.port", "995");
		p.put("mail.pop3.socketFactory.port", "995");
        p.put("mail.pop3.disabletop", "true");
		p.put("mail.pop3.ssl.enable", "true");
		p.put("mail.pop3.useStartTLS", "true");
 
		return p;
	}

系统内存信息

    /**
     * 操作系统名
     */
    private String osName;
 
    /**
     * cpu使用率.
     */
    private String cpuRatio;
 
    /**
     * 系统总内存
     */
    private String osTotalMemory;
 
    /**
     * 系统剩余内存.
     */
    private String osFreeMemory;
 
    /**
     * 最大可获取内存.
     */
    private String maxReachableMemory;
 
    /**
     * 已分配的内存的剩余量
     */
    private String allocatedFreeMemory;
 
    /**
     * 已分配的内存的使用量
     */
    private String allocatedUsedMemory;
 
    /**
     * 最大可使用内存
     */
    private String maxUsableMemory;
 
    /**
     * 线程总数
     */
    private Integer appThread;
 
    /**
     * 记录时间
     */
    private Date recordTime;
 
    /**
     * 报警阈值
     */
    private Integer threshold;
 
    /**
     * 是否报警
     */
    private Integer warning;
 
    /**
     * 报警类型 0:不报警,1:短信,2:邮箱,3:短信+邮箱
     */
    private Integer alarmType;
 
    /**
     * 报警地址(短信/邮箱)
     */
    private String alarmAddr;

获取系统信息

/**
*
* 获取系统信息
*/
public class getSystemInfoUtils{

    /**
     * 获取 OS Name
     */
    private static String GetOsName() {
        return System.getProperty("os.name");
    }
 
    /**
     * 获取 JVM 参数
     */
    private static void GetJvmMemoryInfo(OsMonitorInfoBean infoBean) {
        double mb = 1024 * 1024 * 1.0;
        // jvm
        double totalMemory = Runtime.getRuntime().totalMemory() / mb;
        double freeMemory = Runtime.getRuntime().freeMemory() / mb;
        double maxMemory = Runtime.getRuntime().maxMemory() / mb;
 
        DecimalFormat df = new DecimalFormat("#.##M");
        // MonitorInfo
        infoBean.setMaxReachableMemory(df.format(maxMemory - totalMemory));
        infoBean.setAllocatedFreeMemory(df.format(freeMemory));
        infoBean.setAllocatedUsedMemory(df.format(totalMemory - freeMemory));
        infoBean.setMaxUsableMemory(df.format(maxMemory));
    }

    /**
     * 得到类所在磁盘
     */
    public static String getClassLocaledDisk() {
        Class<OsMonitorUtils> thisClass = OsMonitorUtils.class;
        try {
            String strClassName = thisClass.getName();
            String strPackageName = "";
            if (thisClass.getPackage() != null) {
                strPackageName = thisClass.getPackage().getName();
            }
            String strClassFileName = "";
            if (!"".equals(strPackageName)) {
                strClassFileName = strClassName.substring(strPackageName.length() + 1);
            } else {
                strClassFileName = strClassName;
            }
            URL url = null;
            url = thisClass.getResource(strClassFileName + ".class");
            String strURL = url.toString();
            strURL = strURL.substring(strURL.indexOf('/') + 1);
            //返回当前类的路径,并且处理路径中的空格,因为在路径中出现的空格如果不处理的话,
            //在访问时就会从空格处断开,那么也就取不到完整的信息了,这个问题在web开发中尤其要注意
            return strURL.substring(0, strURL.indexOf('/'));
        } catch (Exception ex) {
            ex.printStackTrace();
            throw ex;
        }
    }

    /**
     * 获取App运行中的线程数
     */
    private static int GetAppProcessingNum() {
        try {
            //获取所有运行中的线程
            Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
            return map.size();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return -1;
    }

    public static final SystemInfo si = new SystemInfo();
 
    public static final CentralProcessor cpu = si.getHardware().getProcessor();
 
    public static final Object oshiLock = new Object();
 
    static {
        cpu.getSystemCpuLoadTicks();   // 预加载一下,第一次调用很慢
    }
 
    /**
     * 获取 Oshi CPU 综合使用率
     */
    public static String GetOshiCpuRadio(CentralProcessor processor) {
        long[] oldTicks = processor.getSystemCpuLoadTicks();
        // 睡眠1s
        try {
            TimeUnit.MILLISECONDS.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        double totalCpu = processor.getSystemCpuLoadBetweenTicks(oldTicks);
        DecimalFormat df = new DecimalFormat("#.##");
        return df.format(totalCpu * 100D);
    }
    // ***************使用第三方框架OSHI获取系统信息************ //
    /**
     * 获取 Oshi 内存
     */
    public static GlobalMemory GetOshiMemory(SystemInfo si) {
        return si.getHardware().getMemory();
    }
 
    /**
     * 获取 Oshi 总内存
     */
    public static String GetOshiTotalMemory(GlobalMemory memory) {
        Double total = (double) memory.getTotal() / 1024 / 1024;
        DecimalFormat df = new DecimalFormat("#.##M");
        return df.format(total);
    }
 
    /**
     * 获取 Oshi 可用内存
     */
    public static String GetOshiFreeMemory(GlobalMemory memory) {
        Double free = (double) memory.getAvailable() / 1024 / 1024;
        DecimalFormat df = new DecimalFormat("#.##M");
        return df.format(free);
    }
    /**
     * 获取 Oshi 内存占比
     */
    public static String GetOshiMemoryPerc(SystemInfo si) {
        GlobalMemory memory = si.getHardware().getMemory();
        double total = (double) memory.getTotal();
        double used = total - (double) memory.getAvailable();
        double perc = used / total;
        DecimalFormat df = new DecimalFormat("#0");
        return df.format(perc * 100D);
    }
 
    /**
     * 获取 Oshi 磁盘使用占比
     */
    public static String GetOshiDiskPerc(SystemInfo si) {
        FileSystem fileSystem = si.getOperatingSystem().getFileSystem();
        List<OSFileStore> fileStores = fileSystem.getFileStores();
        double usable = 0;
        double total = 0;
        double perc = 0;
        int diskCount = 0;
        for (OSFileStore store : fileStores) {
            logger.info("fileStore name: {}", store.getName());
            usable += (double) store.getUsableSpace();
            total += (double) store.getTotalSpace();
            perc += (total - usable) / total * 100D;
            diskCount++;
        }
        DecimalFormat df = new DecimalFormat("#0");
        if (diskCount != 0) {
            return df.format(perc / diskCount);
        } else {
            return df.format(0);
        }
    }
 
     获取监控信息 ///
    ///
 
 
    // 获取监控信息
    public static OsMonitorInfoBean GetMonitorInfo() {
        // return GetSigarMonitorInfo();
        return GetOshiMonitorInfo();
    }

    public static OsMonitorInfoBean GetOshiMonitorInfo() {
        synchronized (oshiLock) {
            OsMonitorInfoBean infoBean = new OsMonitorInfoBean();
            infoBean.setOsName(GetOsName());
            infoBean.setCpuRatio(GetOshiCpuRadio(cpu));
            GlobalMemory memory = GetOshiMemory(si);
            infoBean.setOsTotalMemory(GetOshiTotalMemory(memory));
            infoBean.setOsFreeMemory(GetOshiFreeMemory(memory));
            GetJvmMemoryInfo(infoBean);
            infoBean.setAppThread(GetAppProcessingNum());
            return infoBean;
        }
    }

    /**
     * CPU的用户使用量、系统使用剩余量、总的剩余量、总的使用占用量等(单位:100%)
     */
    public static String getCpuPerc() {
        return getOshiCpuPerc();
    }
 
    /**
     * 内存资源信息
     */
    public static String getPhysicalMemory() {
        return getOshiMemoryPerc();
    }
 
    /**
     * 资源信息(主要是硬盘) 平均占比
     * 取硬盘已有的分区及其详细信息
     * 通过 sigar.getFileSystemList()来获得FileSystem列表对象,然后对其进行编历
     */
    public static String getFileSystemInfo() {
        return getOshiFileSystemInfo();
    }

    private static final SystemInfo si = OsMonitorUtils.si;
 
    private static final CentralProcessor cpu = OsMonitorUtils.cpu;
 
    private static final Object oshiLock = OsMonitorUtils.oshiLock;
 
    /**
     * CPU使用占比
     */
    public static String getOshiCpuPerc() {
        synchronized (oshiLock) {
            return OsMonitorUtils.GetOshiCpuRadio(cpu);
        }
    }
 
    /**
     * 内存使用占比
     */
    public static String getOshiMemoryPerc() {
        synchronized (oshiLock) {
            return OsMonitorUtils.GetOshiMemoryPerc(si);
        }
    }
 
    /**
     * 磁盘使用占比
     */
    public static String getOshiFileSystemInfo() {
        synchronized (oshiLock) {
            return OsMonitorUtils.GetOshiDiskPerc(si);
        }
    }

    // 获取操作平台信息
    public static String getOsPrefix() {
        String arch = System.getProperty("os.arch").toLowerCase();
        final String name = System.getProperty("os.name");
        String osPrefix;
        switch (Platform.getOSType()) {
            case Platform.WINDOWS: {
                if ("i386".equals(arch))
                    arch = "x86";
                osPrefix = "win32-" + arch;
            }
            break;
            case Platform.LINUX: {
                if ("x86".equals(arch)) {
                    arch = "i386";
                } else if ("x86_64".equals(arch)) {
                    arch = "amd64";
                }
                osPrefix = "linux-" + arch;
            }
            break;
            case Platform.MAC: {
                //mac系统的os.arch都是ppc(老版本的mac是powerpc,已经基本不用)看不出系统位数,使用下面的参数表示
                arch = System.getProperty("sun.arch.data.model");
                osPrefix = "mac-" + arch;
            }
            break;
            default: {
                osPrefix = name.toLowerCase();
                if ("x86".equals(arch)) {
                    arch = "i386";
                }
                if ("x86_64".equals(arch)) {
                    arch = "amd64";
                }
                int space = osPrefix.indexOf(" ");
                if (space != -1) {
                    osPrefix = osPrefix.substring(0, space);
                }
                osPrefix += "-" + arch;
            }
            break;
 
        }
 
        return osPrefix;
    }

    public static String getOsName() {
        String osName = "";
        String osPrefix = getOsPrefix();
        if (osPrefix.toLowerCase().startsWith("win32-x86")
                || osPrefix.toLowerCase().startsWith("win32-amd64")) {
            osName = "win";
        } else if (osPrefix.toLowerCase().startsWith("linux-i386")
                || osPrefix.toLowerCase().startsWith("linux-amd64")) {
            osName = "linux";
        } else if (osPrefix.toLowerCase().startsWith("mac-64")
                || osPrefix.toLowerCase().startsWith("mac-32")) {
            osName = "mac";
        }
 
        return osName;
    }

}

SpringBoot自带的Health Indicator 

使用

引入依赖

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-actuator</artifactId>
		</dependency>

		<dependency>
			<groupId>io.micrometer</groupId>
			<artifactId>micrometer-registry-prometheus</artifactId>
		</dependency>

配置

management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always

info.app.author=DigitalSonic
info.app.encoding=@project.build.sourceEncoding@

定义自己的Indicator实现HealthIndicator接口

@Component
public class MyIndicator implements HealthIndicator {
    @Autowired
    private CoffeeService coffeeService;

    @Override
    public Health health() {
        // 用于判断的指标
        long count = coffeeService.getCoffeeCount();
        Health health;
        if (count > 0) {
            health = Health.up()
                    .withDetail("count", count)
                    .withDetail("message", "We have enough coffee.")
                    .build();
        } else {
            health = Health.down()
                    .withDetail("count", 0)
                    .withDetail("message", "We are out of coffee.")
                    .build();
        }
        return health;
    }
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值