IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> SpringCloud Alibaba + SpringBoot Admin + 钉钉通知 -> 正文阅读

[Java知识库]SpringCloud Alibaba + SpringBoot Admin + 钉钉通知

作者:token annotation punctuation

======== 创建SpringBoot Admin服务端

1、POM依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!-- 服务监控管理 -->
    <packaging>jar</packaging>
    <groupId>com.za.edu</groupId>
    <version>0.0.1-SNAPSHOT</version>
    <name>zaedu-server-manager</name>
    <artifactId>zaedu-server-manager</artifactId>

    <!-- SpringBoot版本 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.3.RELEASE</version>
        <relativePath/>
    </parent>

    <dependencies>
        <!-- =========================== Nacos =========================== -->
        <!-- Nacos服务发现检测 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>2.2.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>2.2.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.nacos</groupId>
            <artifactId>nacos-spring-context</artifactId>
            <version>0.3.4</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-openfeign-core</artifactId>
            <version>2.2.3.RELEASE</version>
        </dependency>


        <!-- =========================== Admin服务监控管理 =========================== -->
        <dependency>
            <groupId>de.codecentric</groupId>
            <artifactId>spring-boot-admin-starter-server</artifactId>
            <version>2.2.4</version>
        </dependency>

        <dependency>
            <groupId>de.codecentric</groupId>
            <artifactId>spring-boot-admin-server-ui</artifactId>
            <version>2.2.4</version>·
        </dependency>

        <!-- 安全认证 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--Web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>


        <!-- SSH运维工具 -->
        <dependency>
            <groupId>ch.ethz.ganymed</groupId>
            <artifactId>ganymed-ssh2</artifactId>
            <version>262</version>
        </dependency>

        <!-- 集群环境 跨服务请求工具 -->
        <dependency>
            <groupId>com.netflix.feign</groupId>
            <artifactId>feign-jackson</artifactId>
            <version>8.18.0</version>
        </dependency>

        <dependency>
            <groupId>com.jcraft</groupId>
            <artifactId>jsch</artifactId>
            <version>0.1.55</version>
        </dependency>


        <!-- 日志 引入log4j2依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
        <!-- 去除log4j2依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <exclusions><!-- 去掉springboot默认配置 -->
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!--工具类 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>


        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>5.1.4</version>
            <scope>compile</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.13</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
            <version>4.4</version>
            <scope>compile</scope>
        </dependency>

        <dependency>
            <groupId>com.aliyun.openservices</groupId>
            <artifactId>ons-client</artifactId>
            <version>1.8.7.1.Final</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>9.0.39</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.2.11.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.11.RELEASE</version>
        </dependency>


    </dependencies>

    <profiles>
        <!-- 开发环境 -->
        <profile>
            <id>dev</id>
            <properties>
                <group>DEV_GROUP</group>                           <!-- Nacos配置环境分组 -->
                <namespace>public</namespace>                      <!-- 命名空间 -->
                <server-addr>XX.XXX.XX.XXX:8848</server-addr>      <!-- Nacos IP地址 -->
                <activatedProperties>dev</activatedProperties>     <!-- 项目环境 -->
            </properties>
            <activation>
                <activeByDefault>false</activeByDefault>
            </activation>
        </profile>

        <!-- 测试环境 -->
        <profile>
            <id>test</id>
            <properties>
                <group>TEST_GROUP_DEBUG</group>                    <!-- Nacos 环境分组 -->
                <namespace>public</namespace>                      <!-- Nacos 命名空间 -->
                <server-addr>XX.XXX.XX.XXX:8848</server-addr>      <!-- Nacos IP地址 -->
                <activatedProperties>test</activatedProperties>    <!-- 项目环境 -->
            </properties>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
        </profile>

        <!-- 生产环境 -->
        <profile>
            <id>prod</id>
            <properties>
                <group>PROD_GROUP</group>                          <!-- Nacos 环境分组 -->
                <namespace>public</namespace>                      <!-- Nacos 命名空间 -->
                <server-addr>XX.XXX.XX.XXX:8848</server-addr>      <!-- Nacos IP地址 -->
                <activatedProperties>prod</activatedProperties>    <!-- 项目环境 -->
            </properties>
            <activation>
                <activeByDefault>false</activeByDefault>
            </activation>
        </profile>
    </profiles>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <includeSystemScope>true</includeSystemScope>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

2、启动类加上@EnableAdminServer

import de.codecentric.boot.admin.server.config.EnableAdminServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
 * @author: Owen
 * @date: 2020/12/22
 * @description:服务监控管理
 */
@EnableAdminServer
@EnableFeignClients
@SpringBootApplication
@EnableDiscoveryClient
public class ServerManagerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServerManagerApplication.class, args);
    }
}

3、SecuritySecureConfig安全认证

package com.za.edu.config;
import de.codecentric.boot.admin.server.config.AdminServerProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

/**
 * @Author: Owen
 * @Date: 2022/6/22
 * @Description:安全认证适配器
 */
@Configuration
public class SecuritySecureConfig extends WebSecurityConfigurerAdapter {

    private final String adminContextPath;

    public SecuritySecureConfig(AdminServerProperties adminServerProperties) {
        this.adminContextPath = adminServerProperties.getContextPath();
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 登录成功处理类
        SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
        successHandler.setTargetUrlParameter("redirectTo");
        successHandler.setDefaultTargetUrl(adminContextPath + "/");

        http.authorizeRequests()
                //静态文件允许访问
                .antMatchers(adminContextPath + "/assets/**").permitAll()
                //登录页面允许访问
                .antMatchers(adminContextPath + "/login","/css/**","/js/**","/image/*").permitAll()
                //其他所有请求需要登录
                .anyRequest().authenticated()
                .and()
                //登录页面配置,用于替换security默认页面
                .formLogin().loginPage(adminContextPath + "/login").successHandler(successHandler).and()
                //登出页面配置,用于替换security默认页面
                .logout().logoutUrl(adminContextPath + "/logout").and()
                .httpBasic().and()
                .csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .ignoringAntMatchers(
                        "/instances",
                        "/actuator/**");

    }

}

4、CorsConfig跨域配置

package com.za.edu.config;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

/**
 *@Author Owen
 *@Date 2020/8/17
 *@Description跨域配置
 */
@Configuration
public class CorsConfig {

    @Bean
    public FilterRegistrationBean corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("OPTIONS");
        config.addAllowedMethod("HEAD");
        config.addAllowedMethod("GET");
        config.addAllowedMethod("PUT");
        config.addAllowedMethod("POST");
        config.addAllowedMethod("DELETE");
        config.addAllowedMethod("PATCH");
        source.registerCorsConfiguration("/**", config);
        final FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
        bean.setOrder(0);
        return bean;
    }
}

5、application.yml配置文件

server:
  port: 9088
  # SpringBoot Admin页面URL前缀
  servlet:
    context-path: /admin

spring:
  application:
    name: zaedu-server-manager
  #当前服务配置环境
  profiles:
    active: @activatedProperties@

  # SpringBoot Admin security安全认证账号
  security:
    user:
      name: admin
      password: admin

  #================== 服务注册与发现 ==================
  cloud:
    nacos:
      discovery:
        group: @group@
        namespace: @namespace@
        server-addr: @server-addr@
         # Nacos (开启\关闭) 服务发现
         #       enabled: false
         # Nacos (开启\关闭) 自动注册服务
        register-enabled: true
        
       #================== SpringBoot Admin配置 ==================
        metadata:
          management:
            context-path: ${server.servlet.context-path}/actuator

        # SpringBoot Admin用户账号
          user:
            name: admin
            password: admin



  #================== SpringBoot Admin ==================
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always
    #SpringBoot Admin 日志文件目录
    logfile:
      #例:F:/文件夹/项目目录/trunk/zaedu-cloud/logs/:
      external-file: ${user.dir}/logs/${spring.application.name}/info.log
      enabled: true

logging:
  config: classpath:log4j2.xml
  #打印项目业务相关的sql日志
  level:
    com:
      #禁止打印c.a.n.client.config.impl.ClientWorker : get changedGroupKeys:[] 日志
      alibaba:
        nacos:
          client:
            config:
              impl: WARN

6、log4j2.xml日志配置

<?xml version="1.0" encoding="UTF-8"?>
<!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出-->
<!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数-->
<configuration monitorInterval="5">
    <!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
    <!--从小到大依次是:trace、debug、info、warn、error
        由于我们使用的是 slf4j 接口包,该接口包中只提供了未标有删除线的日志级别的输出。(官方注释) -->

    <!--变量配置-->
    <Properties>
        <!-- 格式化输出:%date表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符-->
        <!-- %logger{36} 表示 Logger 名字最长36个字符 -->
        <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight{%-5level}{FATAL=Bright red, ERROR=Bright red, WARN=Bright yellow, INFO=Bright Cyan , DEBUG=Bright blue, TRACE=Bright Magenta } %highlight{[%t]}{FATAL=Bright red, ERROR=Bright yellow, WARN=Bright white, INFO=Bright Magenta, DEBUG=Bright blue, TRACE=Bright Magenta} %logger: %highlight{%msg%n}{FATAL=Bright red, ERROR=Bright red, WARN=Bright yellow, INFO=Bright Cyan , DEBUG=Bright blue, TRACE=Bright Magenta}"/>
        <!-- 定义日志存储的路径 -->
        <property name="FILE_PATH" value="logs/zaedu-server-manager"/>
        <property name="FILE_NAME" value="zaedu-server-manager"/>
    </Properties>

    <appenders>

        <console name="Console" target="${SYSTEM_OUT}">
            <!--输出日志的格式-->
            <PatternLayout pattern="${LOG_PATTERN}" disableAnsi="false" noConsoleNoAnsi="false"/>
            <!--控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <!--onMatch="ACCEPT" 表示匹配该级别及以上
                onMatch="DENY" 表示不匹配该级别及以上
                onMatch="NEUTRAL" 表示该级别及以上的,由下一个filter处理,如果当前是最后一个,则表示匹配该级别及以上
                onMismatch="ACCEPT" 表示匹配该级别以下
                onMismatch="NEUTRAL" 表示该级别及以下的,由下一个filter处理,如果当前是最后一个,则不匹配该级别以下的
                onMismatch="DENY" 表示不匹配该级别以下的-->
            <ThresholdFilter level="debug" onMatch="ACCEPT"/>
        </console>

        <!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append(是否追加)属性决定,适合临时测试用-->
        <!--        <File name="Filelog" fileName="${FILE_PATH}/test.log" append="false">-->
        <!--             <PatternLayout pattern="${LOG_PATTERN}" disableAnsi="false" noConsoleNoAnsi="false"/>-->
        <!--        </File>-->

        <!-- 这个会打印出所有的info及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
        <RollingFile name="RollingFileInfo" fileName="${FILE_PATH}/info.log"
                     filePattern="${FILE_PATH}/${FILE_NAME}-INFO-%d{yyyy-MM-dd}_%i.log.gz">
            <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="${LOG_PATTERN}" disableAnsi="false" noConsoleNoAnsi="false"/>
            <Policies>
                <!--
                此参数需要与filePattern结合使用,规定了触发rollover的频率,默认值为1。
                假设interval为4,若filePattern的date/time pattern的最小时间粒度为小时(如yyyy-MM-dd HH),
                则每4小时触发一次rollover;若filePattern的date/time pattern的最小时间粒度为分钟(如yyyy-MM-dd HH-mm),
                则每4分钟触发一次rollover。
                 -->
                <TimeBasedTriggeringPolicy interval="1"/>
                <!-- 按文件大小触发-->
                <SizeBasedTriggeringPolicy size="500MB"/>
            </Policies>
            <!-- 通过DeleteAction可以删除任何文件,而不仅仅像DefaultRolloverStrategy那样,删除最旧的文件,所以使用的时候需要谨慎!-->
            <DefaultRolloverStrategy>
                <Delete basePath="${FILE_PATH}" maxDepth="2">
                    <IfFileName glob="zaedu-server-manager-*.log.gz" />
                    <IfLastModified age="30d" />
                </Delete>
            </DefaultRolloverStrategy>
        </RollingFile>

        <!-- 这个会打印出所有的warn及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
        <RollingFile name="RollingFileWarn" fileName="${FILE_PATH}/warn.log"
                     filePattern="${FILE_PATH}/${FILE_NAME}-WARN-%d{yyyy-MM-dd}_%i.log.gz">
            <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="${LOG_PATTERN}" disableAnsi="false" noConsoleNoAnsi="false"/>
            <Policies>
                <!--
               此参数需要与filePattern结合使用,规定了触发rollover的频率,默认值为1。
               假设interval为4,若filePattern的date/time pattern的最小时间粒度为小时(如yyyy-MM-dd HH),
               则每4小时触发一次rollover;若filePattern的date/time pattern的最小时间粒度为分钟(如yyyy-MM-dd HH-mm),
               则每4分钟触发一次rollover。
                -->
                <TimeBasedTriggeringPolicy interval="1"/>
                <!-- 按文件大小触发-->
                <SizeBasedTriggeringPolicy size="500MB"/>
            </Policies>
            <!-- 通过DeleteAction可以删除任何文件,而不仅仅像DefaultRolloverStrategy那样,删除最旧的文件,所以使用的时候需要谨慎!-->
            <DefaultRolloverStrategy>
                <Delete basePath="${FILE_PATH}" maxDepth="2">
                    <IfFileName glob="zaedu-server-manager-*.log.gz" />
                    <IfLastModified age="30d" />
                </Delete>
            </DefaultRolloverStrategy>
        </RollingFile>

        <!-- 这个会打印出所有的error及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
        <RollingFile name="RollingFileError" fileName="${FILE_PATH}/error.log"
                     filePattern="${FILE_PATH}/${FILE_NAME}-ERROR-%d{yyyy-MM-dd}_%i.log.gz">
            <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="${LOG_PATTERN}" disableAnsi="false" noConsoleNoAnsi="false"/>
            <Policies>
                <!--
               此参数需要与filePattern结合使用,规定了触发rollover的频率,默认值为1。
               假设interval为4,若filePattern的date/time pattern的最小时间粒度为小时(如yyyy-MM-dd HH),
               则每4小时触发一次rollover;若filePattern的date/time pattern的最小时间粒度为分钟(如yyyy-MM-dd HH-mm),
               则每4分钟触发一次rollover。
                -->
                <TimeBasedTriggeringPolicy interval="1"/>
                <!-- 按文件大小触发-->
                <SizeBasedTriggeringPolicy size="500MB"/>
            </Policies>
            <!-- 通过DeleteAction可以删除任何文件,而不仅仅像DefaultRolloverStrategy那样,删除最旧的文件,所以使用的时候需要谨慎!-->
            <DefaultRolloverStrategy>
                <Delete basePath="${FILE_PATH}" maxDepth="2">
                    <IfFileName glob="zaedu-server-manager-*.log.gz" />
                    <IfLastModified age="30d" />
                </Delete>
            </DefaultRolloverStrategy>
        </RollingFile>

    </appenders>

    <!--Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。-->
    <!--然后定义loggers,只有定义了logger并引入的appender,appender才会生效-->
    <loggers>

        <!--若是additivity设为false,则 子Logger 只会在自己的appender里输出,而不会在 父Logger 的appender里输出。-->
        <Logger name="org.springframework" level="info" additivity="false">
            <AppenderRef ref="Console"/>
        </Logger>
        <logger name="corg.apache.ibatis" level="debug">
            <appender-ref ref="Console"/>
            <appender-ref ref="RollingFileInfo"/>
        </logger>

        <root level="info">
            <appender-ref ref="Console"/>
            <appender-ref ref="RollingFileInfo"/>
            <appender-ref ref="RollingFileWarn"/>
            <appender-ref ref="RollingFileError"/>
        </root>
    </loggers>

</configuration>

7、登录Admin页面

在这里插入图片描述
查看当前服务
在这里插入图片描述
查看详情
在这里插入图片描述
查看日志
在这里插入图片描述

======== 创建 (zaedu-inform)客户端 并整合钉钉消息通知

1、POM依赖

 <!-- 钉钉消息推送 -->
    <dependency>
        <groupId>com.aliyun</groupId>
        <artifactId>alibaba-dingtalk-service-sdk</artifactId>
        <version>1.0.1</version>
        <exclusions>
            <exclusion>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    
    
     <!-- ============ Admin客户端 必须引入以下依赖 ============ -->
        <!--SpringBoot Admin 客户端-->
    <dependency>
        <groupId>de.codecentric</groupId>
        <artifactId>spring-boot-admin-starter-client</artifactId>
    </dependency>
    
    <!--SpringBoot Admin 健康检查监测 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
   

2、配置文件application.yml

server:
  port: 9029
spring:
  application:
  #消息通知服务
    name: zaedu-inform
  #配置的环境
  profiles:
    active: test

#================== SpringBoot Admin 配置参数 ==================
management:
  health:
    redis:
      enabled: false #关闭redis健康检查
    sentinel:
      enabled: false #关闭sentinel健康检查
    ldap:
      enabled: false #关闭ldap健康检查
  #暴露端点
  endpoints:
    web:
      exposure:
        include: '*'
    #关闭过滤敏感信息
    health:
      sensitive: false
  endpoint:
    health:
      show-details: always  #显示详细信息
    #SpringBoot Admin 日志文件目录
    logfile:
      #例:F:/文件夹/项目目录/trunk/zaedu-cloud/logs/:
      external-file: ${user.dir}/logs/${spring.application.name}/info.log
      enabled: true
      
#================== 日志配置 ==================
logging:
  config: classpath:log4j2.xml
  #打印项目业务相关的sql日志
  level:
    com:
      za:
        edu:
          mapper: debug
          dao: debug
      #禁止打印c.a.n.client.config.impl.ClientWorker : get changedGroupKeys:[] 日志
      alibaba:
        nacos:
          client:
            config:
              impl: WARN
              
#================== 钉钉消息通知  ==================
# 对接详情 查看钉钉官方文档:https://open.dingtalk.com/document/group/custom-robot-access

dingTalk:
  #签名
  sign: XXXXXXX
  #消息服务地址
  serverUrl: https://XXXXXXX?access_token=XXXXXXX
  

              


对接详情 查看钉钉官方文档: https://open.dingtalk.com/document/group/custom-robot-access

3、定义消息体DingTalkMessage

package com.za.edu.bean;
import com.dingtalk.api.request.OapiRobotSendRequest;
import lombok.Data;
import java.util.List;

/**
* @Author: Owen
* @Date: 2022/6/30
* @Description:钉钉消息
*/
@Data
public class DingTalkMessage {
    //消息标题
    private String title;
    //密钥
    private String secret;
    //消息内容
    private String content;
    //消息类型
    private String msgType;
    //webhook服务地址
    private String webhook;
    //是否推送所有人
    private Boolean isAtAll;

    //指定对象
    private List<String> mobileList;

    //================= 链接消息 =================
    //链接消息 图片地址
    private String picUrl;
    //链接消息 跳转url地址
    private String messageUrl;

    //================= 卡片模板 图文带链接 =================
    //被@人的手机号。
    private List<String> atMobiles;
    //被@人的用户userid。
    private List<String> atUserIds;
    //批量链接
    private List<OapiRobotSendRequest.Links> links;

    //================= 按钮选择框消息 =================
    private  String btnOrientation;                //0:按钮竖直排列 1:按钮横向排列
    private  List<OapiRobotSendRequest.Btns> btns; //按钮链接
    public  DingTalkMessage(){
    }


}

4、定义MessageModeEnums消息模板

package com.za.edu.enums;


import lombok.extern.slf4j.Slf4j;

/**
 * @author: Owen
 * @date: 2021/6/8
 * @description:钉钉消息模板
 */
@Slf4j
public enum MessageModeEnums {
    //消息关键字(钉钉配置的自定义关键词,并且消息中必须包含该(自定义关键词) 才能发送成功!!!!)
    TITLE_SERVER_ALARM("服务进程异常通知","消息关键字(服务进程异常警报)"),
    TITLE_SERVER_STATUS("服务状态变更通知","消息关键字(服务状态变更通知)"),

    //消息类型
    MODE_TEXT("text","文本消息"),
    MODE_LINK("link","链接消息"),
    MODE_MARKDOWN ("markdown","图文链接消息"),
    MODE_FEED_CARD ("feedCard","卡片图文样式消息"),
    MODE_ACTION_CARD ( "actionCard","事件选择框样式消息");

    MessageModeEnums(String type, String explain) {
        this.type = type;
        this.explain = explain;
    }

    private String type;         //消息类型
    private String explain;      //操作详情


    public String getType() {
        return type;
    }

    public String getExplain() {
        return explain;
    }

    /**
     * @author: Owen
     * @date: 2021/6/8
     * @description:查看根据命令拿详情
     */
    public static String getValue(String type) {
        String result = null;
        try {
            for (MessageModeEnums enums : MessageModeEnums.values()) {
                //是否符合当前设备型号
                if (enums.getType().equals(type)) {
                    result = enums.getExplain();
                }
            }
        } catch (Exception e) {
            log.error("Message event get explain faild: " + e.getMessage());
        }
        return result;
    }
}

5、钉钉消息推送工具类DingTalkUtils

package com.za.edu.utils;

import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.OapiRobotSendRequest;
import com.dingtalk.api.response.OapiRobotSendResponse;
import com.za.edu.bean.DingTalkMessage;
import com.za.edu.enums.MessageModeEnums;
import com.za.edu.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLEncoder;

/**
 * @Author: Owen
 * @Date: 2022/6/30
 * @Description:钉钉消息推送
 */
@Slf4j
public class DingTalkUtils {


    /**
     * @Author: Owen
     * @Date: 2022/6/30
     * @Description:推送钉钉消息
     */
    public static OapiRobotSendResponse sendMessage(DingTalkMessage message) {
        //发起API请求
        OapiRobotSendRequest request = null;
        //响应结果
        OapiRobotSendResponse response = null;
        try {
            //校验消息类型
            ExceptionUtil.isBlank(message.getMsgType(), "Message type not null!");
            //校验消息内容
            ExceptionUtil.isBlank(message.getContent(), "Message content not null!");
            //获取 消息客户端
            DingTalkClient client = getClient();
            ExceptionUtil.isNull(client, "Message client not null!");
            //文本消息
            if (MessageModeEnums.MODE_TEXT.getType().equals(message.getMsgType())) {
                //构建文本消息
                request = buildTextMessage(message.getContent(), message.getIsAtAll());
            } else if (MessageModeEnums.MODE_LINK.getType().equals(message.getMsgType())) {
                //构建链接消息
                request = buildLinkMessage(message);
            } else if (MessageModeEnums.MODE_MARKDOWN.getType().equals(message.getMsgType())) {
                //构建图文样式消息
                request = buildMarkdownMessage(message);
            } else if (MessageModeEnums.MODE_FEED_CARD.getType().equals(message.getMsgType())) {
                //构建 卡片样式(主图)+(子参数)多条链接跳转
                request = buildFeedCardMessage(message);
            }
            else if (MessageModeEnums.MODE_ACTION_CARD.getType().equals(message.getMsgType())) {
                //选择框模型,跳转链接消息
                request = buildActionCardMessage(message);
            }
            //未知消息类型
            else {
                throw new BusinessException("Unknown message type:[" + message.getMsgType() + "]!");
            }
            ExceptionUtil.isNull(request,"build request faild !");
            //推送钉钉消息
            response = client.execute(request);
            log.info("DingTalk send message response:" + response);
        } catch (Exception e) {
            e.printStackTrace();
            log.error("DingTalk send message faild:" + e.getMessage());
        }
        return response;
    }

    /**
     * @Author: Owen
     * @Date: 2022/6/30
     * @Description:构建文本消息
     */
    public static OapiRobotSendRequest buildTextMessage(String content, Boolean isAtAll) {
        try {
            ExceptionUtil.isNull(isAtAll, "params:[isAtAll] not null !");
            ExceptionUtil.isBlank(content, "params:[content] not null !");

            //发起API请求
            OapiRobotSendRequest request = new OapiRobotSendRequest();
            OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
            //构建文本消息模型
            OapiRobotSendRequest.Text text = new OapiRobotSendRequest.Text();
            text.setContent(content);

            //设置文本消息模型
            at.setIsAtAll(isAtAll);//是否@所有人
            request.setAt(at);
            request.setMsgtype(MessageModeEnums.MODE_TEXT.getType());//消息类型
            request.setText(text);
            return request;
        } catch (Exception e) {
            log.error("Build text message faild: " + e.getMessage());
            return null;
        }
    }

    /**
     * @Author: Owen
     * @Date: 2022/6/30
     * @Description:构建链接消息
     */
    public static OapiRobotSendRequest buildLinkMessage(DingTalkMessage message) {
        try {
            ExceptionUtil.isBlank(message.getTitle(), "params:[title] not null !");
            ExceptionUtil.isBlank(message.getPicUrl(), "params:[picUrl] not null !");
            ExceptionUtil.isNull(message.getIsAtAll(), "params:[isAtAll] not null !");
            ExceptionUtil.isBlank(message.getContent(), "params:[content] not null !");
            ExceptionUtil.isBlank(message.getMessageUrl(), "params:[messageUrl] not null !");

            //发起API请求
            OapiRobotSendRequest request = new OapiRobotSendRequest();
            OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
            //构建文本链接消息
            OapiRobotSendRequest.Link link = new OapiRobotSendRequest.Link();
            link.setTitle(message.getTitle());//消息标题
            link.setText(message.getContent());//消息内容
            link.setPicUrl(message.getPicUrl());//链接图片
            link.setMessageUrl(message.getMessageUrl());//链接跳转地址

            //设置文本消息模型
            at.setIsAtAll(message.getIsAtAll());//是否@所有人
            request.setAt(at);
            request.setMsgtype(MessageModeEnums.MODE_LINK.getType());
            request.setLink(link);
            return request;
        } catch (Exception e) {
            log.error("Build link message faild: " + e.getMessage());
            return null;
        }
    }

    /**
     * @Author: Owen
     * @Date: 2022/6/30
     * @Description:构建图文样式消息 /* 数据格式 例:
     * {"msgtype": "markdown",
     * "markdown": {"title":"标题","text": "内容1 \n > 内容2\n > ![screenshot](https://图片地址.png)\n > ###### 内容2 (https://跳转链接地址) \n"},
     * "at": {"atMobiles": ["用户手机号1","用户手机号2"],"atUserIds": ["用户1 ID","用户2 ID"],"isAtAll": false}
     * }
     */
    public static OapiRobotSendRequest buildMarkdownMessage(DingTalkMessage message) {

        try {
            ExceptionUtil.isBlank(message.getTitle(), "params:[title] not null !");
            ExceptionUtil.isNull(message.getIsAtAll(), "params:[isAtAll] not null !");
            ExceptionUtil.isBlank(message.getContent(), "params:[content] not null !");

            //发起API请求
            OapiRobotSendRequest request = new OapiRobotSendRequest();
            OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
            //构建图文链接消息
            OapiRobotSendRequest.Markdown markdown = new OapiRobotSendRequest.Markdown();
            markdown.setTitle(message.getTitle());//消息标题 title
            markdown.setText(message.getContent());//消息内容 text
            //设置文本消息模型
            at.setIsAtAll(message.getIsAtAll());//是否@所有人
            request.setAt(at);
            request.setMsgtype(MessageModeEnums.MODE_MARKDOWN.getType());
            request.setMarkdown(markdown);
            return request;
        } catch (Exception e) {
            log.error("Build link message faild: " + e.getMessage());
            return null;
        }
    }

            /* 数据格式 例:
            {
                "msgtype":"feedCard",
                    "feedCard": {
                 //批量子链接
                "links": [
                {
                        "title": "标题1",
                        "messageURL": "图片1地址",
                        "picURL": "内容1"
                },
                {
                        "title": "标题2",
                        "messageURL": "图片2地址",
                        "picURL": "内容2"
                }
              ]
             }
            }*/
    /**
     * @Author: Owen
     * @Date: 2022/6/30
     * @Description:卡片样式(主图)+(子参数)多条链接跳转
     */
    public static OapiRobotSendRequest buildFeedCardMessage(DingTalkMessage message) {
        try {
            ExceptionUtil.isEmpty(message.getLinks(), "params:[links] not null !");
            ExceptionUtil.isNull(message.getIsAtAll(), "params:[isAtAll] not null !");

            //发起API请求
            OapiRobotSendRequest request = new OapiRobotSendRequest();
            OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
            //构建卡片图文模型 嵌入链接消息
            OapiRobotSendRequest.Feedcard feedcard = new OapiRobotSendRequest.Feedcard();
            //批量 子链接消息 参数格式:(title:标题)、(picURL:图片Url)、(messageURL:跳转Url)
            feedcard.setLinks(message.getLinks());

            //设置文本消息模型
            at.setIsAtAll(message.getIsAtAll());//是否@所有人
            request.setAt(at);
            request.setMsgtype(MessageModeEnums.MODE_FEED_CARD.getType());
            request.setFeedCard(feedcard);
            return request;
        } catch (Exception e) {
            log.error("Build link message faild: " + e.getMessage());
            return null;
        }
    }


    /*       例:{
                "msgtype": "actionCard",
                "actionCard": {
                    "title": "标题",
                    "text": "![screenshot](https://图片地址.png)
                    //换行符
                    \n\n
                    #### 内容1 \n\n 内容2",
                    "btnOrientation": "0",
                    "btns": [
                        {
                            "title": "是",
                            "actionURL": "https://事件地址1/"
                        },
                        {
                            "title": "否",
                            "actionURL": "https://事件地址2/"
                        }
                    ]
                }
            }*/
    /**
     * @Author: Owen
     * @Date: 2022/6/30
     * @Description:卡片样式(主图)+(子参数)多条链接跳转
     */
    public static OapiRobotSendRequest buildActionCardMessage(DingTalkMessage message) {
        try {
            ExceptionUtil.isBlank(message.getTitle(), "params:[title] not null !");
            ExceptionUtil.isNull(message.getIsAtAll(), "params:[isAtAll] not null !");
            ExceptionUtil.isBlank(message.getContent(), "params:[content] not null !");

            //发起API请求
            OapiRobotSendRequest request = new OapiRobotSendRequest();
            OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
            //构建批量卡片图文模型 嵌入链接消息
            OapiRobotSendRequest.Actioncard actioncard = new OapiRobotSendRequest.Actioncard();
            actioncard.setTitle(message.getTitle());//主图 信息标题
            actioncard.setText(message.getContent());//内容 text

            //批量按钮框,按钮参数格式 (title:标题)、(actionURL:跳转Url)
            actioncard.setBtns(message.getBtns());

            //设置文本消息模型
            at.setIsAtAll(message.getIsAtAll()); //是否@所有人
            request.setAt(at);
            request.setMsgtype(MessageModeEnums.MODE_ACTION_CARD.getType());
            request.setActionCard(actioncard);
            return request;
        } catch (Exception e) {
            log.error("Build link message faild: " + e.getMessage());
            return null;
        }
    }


    /**
     * @author: Owen
     * @date: 2020/12/4
     * @description:钉钉消息推送工具(单例模式)
     */
    private static DingTalkClient getClient() {
        String sign;          //钉钉签名密钥
        String serverUrl;     //钉钉消息服务地址
        DingTalkClient client;//钉钉消息客户端

        //============ 根据项目环境 获取配置文件参数 ============
        try {
            //签名
            sign = ApplicationContextHolder.getApplicationProperty("dingTalk.sign");
            ExceptionUtil.isBlank(sign, "Params: [sign] not null!");

            //服务地址
            serverUrl = ApplicationContextHolder.getApplicationProperty("dingTalk.serverUrl");
            ExceptionUtil.isBlank(serverUrl, "Params: [serverUrl] not null!");

            //创建钉钉 消息客户端
            client = new DefaultDingTalkClient(serverUrl);
            ExceptionUtil.isNull(client, "Create dingTalk client faild!");
        } catch (Exception e) {
            throw new BusinessException("Init instance faild: " + e.getMessage());
        }

        //============ 构建钉钉客户端 ============
        try {
            //时间戳
            Long timestamp = System.currentTimeMillis();
            //时间戳拼接密钥
            String stringToSign = timestamp + "\n" + sign;
            //HmacSHA256加密算法
            Mac mac = Mac.getInstance("HmacSHA256");
            //初始化SecretKey
            mac.init(new SecretKeySpec(sign.getBytes("UTF-8"), "HmacSHA256"));
            //构建 密钥字节数据
            byte[] signData = mac.doFinal(stringToSign.getBytes("UTF-8"));
            //密钥加密
            String signEncoder = URLEncoder.encode(new String(Base64.encodeBase64(signData)), "UTF-8");
            //创建客户端
            client = new DefaultDingTalkClient(serverUrl + "&timestamp=" + timestamp + "&sign=" + signEncoder);
        } catch (Exception e) {
            log.error("Create dingTalk client faild: " + e.getMessage());
            return null;
        }
        //当前若已加载过 钉钉客户端实例 则直接复用
        return client;
    }


}

6、消息推送接口DingTalkMessageController

package com.za.edu.controller;
import com.dingtalk.api.response.OapiRobotSendResponse;
import com.za.edu.bean.DingTalkMessage;
import com.za.edu.returns.ApiResult;
import com.za.edu.utils.DingTalkUtils;
import com.za.edu.utils.ExceptionUtil;
import org.springframework.web.bind.annotation.*;
import java.util.Objects;

/**
 * @Author Owen
 * @Description钉钉消息通知
 * @Date 2020/10/12
 */
@RestController
@RequestMapping("/dingTalk/message")
public class DingTalkMessageController {


    /**
     * @Author Owen
     * @Description推送钉钉通知消息
     * @Date 2020/10/12
     */
    @PostMapping(value = "/pushInform")
    public ApiResult pushInform(@RequestBody DingTalkMessage message) {
        ExceptionUtil.isNull(message, "Message params not null!");
        OapiRobotSendResponse result = DingTalkUtils.sendMessage(message);
        return ApiResult.successWithObject(Objects.isNull(result) ? null : result.getCode());
    }


}

7、启动第一个客户端(通知项目zaedu-inform)

在这里插入图片描述

======== Admin服务端 整合项目服务 监听事件

1、TaluohuiNotifier 服务监听通知类

package com.za.edu.event;
import com.za.edu.bean.DingTalkMessage;
import com.za.edu.bean.ServerInfo;
import com.za.edu.client.InformServiceClient;
import com.za.edu.utils.ApplicationContextHolder;
import com.za.edu.utils.NacosUtils;
import de.codecentric.boot.admin.server.domain.entities.Instance;
import de.codecentric.boot.admin.server.domain.entities.InstanceRepository;
import de.codecentric.boot.admin.server.domain.events.InstanceEvent;
import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent;
import de.codecentric.boot.admin.server.notify.AbstractEventNotifier;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @Author: Owen
 * @Date: 2022/6/27
 * @Description:Admin自定义监控报警 服务监控在监听到服务无法访问等问题时,
 * 实现自定义的报警功能 例:(钉钉、邮箱、公众号)等
 */
@Slf4j
@Component
@SuppressWarnings("all")
public class TaluohuiNotifier extends AbstractEventNotifier {
    //自定义Nacos工具类(用于获取服务详情)
    @Resource
    private NacosUtils nacosUtils;
    @Resource
    private InformServiceClient informServiceClient;

    protected TaluohuiNotifier(InstanceRepository repository) {
        super(repository);
    }

    /**
     * @Author: Owen
     * @Date: 2022/6/27
     * @Description:实现对服务事件的通知
     */
    @Override
    protected Mono<Void> doNotify(InstanceEvent event, Instance instance) {
        //监听服务状态
        return Mono.fromRunnable(() -> {
            try {
                //项目服务状态变更
                if (event instanceof InstanceStatusChangedEvent) {
                    log.info("\nServer status event: "
                            + "Name: [" + instance.getRegistration().getName()
                            + "], Status: [" + ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus()
                            + "], Instance: [" + event.getInstance() + "]");
           
                    //上线
                    if ("UP".contains(((InstanceStatusChangedEvent) event).getStatusInfo().getStatus())) {
                        //钉钉通知
                        DingTalkMessage message = new DingTalkMessage();
                        message.setIsAtAll(false);  //是否@所有人
                        message.setMsgType("text"); //文本消息
                        //获取环境
                        ApplicationContext context = ApplicationContextHolder.getApplicationContext();
                        String active = context.getEnvironment().getActiveProfiles()[0];//服务配置环境
                        StringBuffer body = new StringBuffer
                                ("        《服务状态通知》        ");
                        body.append("\n服务环境: " + active);
                        body.append("\n服务状态: 上线! ");
                        body.append("\n服务名称: " + instance.getRegistration().getName());
                        body.append("\n服务注册IP: " + instance.getRegistration().getServiceUrl());
                        body.append("\nNacos分组: " + nacosUtils.getGroupName());
                        message.setContent(body.toString());
                        //推送钉钉消息
                        informServiceClient.pushInform(message);
                    }
                    //下线
                    else if ("OFFLINE".contains(((InstanceStatusChangedEvent) event).getStatusInfo().getStatus())) {
                      
                        //钉钉通知
                        DingTalkMessage message = new DingTalkMessage();
                        message.setIsAtAll(false);  //是否@所有人
                        message.setMsgType("text"); //文本消息
                        //获取环境
                        ApplicationContext context = ApplicationContextHolder.getApplicationContext();
                        String active = context.getEnvironment().getActiveProfiles()[0];//服务配置环境
                        StringBuffer body = new StringBuffer
                                ("        《服务状态通知》        ");
                        body.append("\n服务环境: " + active);
                        body.append("\n服务状态: 离线!");
                        body.append("\n服务名称: " + instance.getRegistration().getName());
                        body.append("\n服务注册IP: " + instance.getRegistration().getServiceUrl());
                        body.append("\nNacos分组: " + nacosUtils.getGroupName());
                        message.setContent(body.toString());
                        //推送钉钉消息
                        informServiceClient.pushInform(message);
                    }
                }
                //项目服务详情
                else {
                    log.info("\nServer info: "
                            + "Name: [" + instance.getRegistration().getName()
                            + "], Type: [" + event.getType()
                            + "], Instance: [" + event.getInstance() + "]");
                }
            } catch (Exception e) {
                log.error("Runnable server exception: " + e.getMessage());
            }
        });
    }
}

2、跨服务调用钉钉通知

package com.za.edu.client;
import com.za.edu.bean.DingTalkMessage;
import com.za.edu.returns.ApiResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
* @Author: Owen
* @Date: 2022/6/30
* @Description:通知业务
*/
@FeignClient("zaedu-inform")
public interface InformServiceClient {
    /**
     * @author: Owen
     * @date: 2021/1/6
     * @description:钉钉消息
     */
    @RequestMapping(value = "/dingTalk/message/pushInform", method = RequestMethod.POST, produces = {"application/json"}, consumes = {"application/excel+json"})
    ApiResult pushInform(@RequestBody DingTalkMessage message);

}


3、启动第二个客户端(zaedu-algorithm)

参考第一个客户端 引入 客户端pom依赖 以及 配置参数即可
在这里插入图片描述

在这里插入图片描述

4、获取服务列表 接口详情

通过访问 http://192.168.0.27:9088/admin/applications可以 获取当前服务详情列表
在这里插入图片描述

5、服务端触发 客户端服务 状态变更事件**

在这里插入图片描述

6、推送 钉钉自定义机器人 消息通知**

在这里插入图片描述

7、推送 钉钉自定义机器人 下线事件通知**

服务下线则触发下线通知
在这里插入图片描述
在这里插入图片描述

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-07-05 23:25:27  更:2022-07-05 23:25:39 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 15:36:34-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码