Plugin - 插件开发03_Spring Boot动态插件化与热加载

文章目录

  • Pre
  • 方案概览
  • 使用插件的好处
  • 流程
  • Code
    • Plugin 定义
    • Plugin 实现
    • Plugin 使用方
      • 动态加载插件
      • 类加载器
      • 注册与卸载插件
      • 配置文件
      • 启动类
      • 测试验证
  • 小结

在这里插入图片描述


Pre

插件 - 通过SPI方式实现插件管理

插件 - 一份配置,离插件机制只有一步之遥

插件 - 插件机制触手可及

Plugin - 插件开发01_SPI的基本使用

Plugin - 插件开发02_使用反射机制和自定义配置实现插件化开发

Plugin - 插件开发03_Spring Boot动态插件化与热加载

Plugin - 插件开发04_Spring Boot中的SPI机制与Spring Factories实现


方案概览

常用的插件化实现思路:

  1. spi机制
  2. spring内置扩展点
  3. spring aop技术
  4. springboot中的Factories机制
  5. 第三方插件包:spring-plugin
  6. java agent 技术

在这里插入图片描述


使用插件的好处

  • 模块解耦:插件机制能帮助系统模块间解耦,使得服务间的交互变得更加灵活。
  • 提升扩展性和开放性:通过插件机制,系统能够轻松扩展,支持新的功能或服务,无需大规模修改核心代码。
  • 方便第三方接入:第三方可以按照预定义的插件接口进行集成,不会对主系统造成过多侵入。

流程

定义接口
定义接口
定义接口
打包SDK
打包SDK
打包SDK
读取配置
指定实现类
Bean实例化
调用接口方法
返回结果
应用 A: 定义接口
应用 B: 实现接口并打包成SDK
应用 C: 实现接口并打包成SDK
应用 D: 实现接口并打包成SDK
应用 E: 引用SDK并动态加载实现类
配置文件: 配置项
启动时注册Bean/运行时注册Bean
实例化实现类
调用 接口方法

Code

在这里插入图片描述

要实现一个插件化系统,以便动态地引入外部插件。这些插件可能包括功能增强、第三方集成或业务逻辑的拓展。
定义插件接口。

实现插件机制的关键步骤

  • 插件实现类。
  • 通过Spring的 ImportBeanDefinitionRegistrar 动态注册插件。
    使用自定义类加载器加载外部JAR包中的类。
    在Spring应用中动态加载、更新和删除插件。

Plugin 定义

在这里插入图片描述

首先,我们需要定义一个插件接口,该接口为插件提供统一的方法。插件实现类将根据此接口进行开发。

package com.plugin;

public interface IPlugin {
    String customPluginMethod(String name);
}

接口 IPlugin 包含一个方法 customPluginMethod,插件类可以通过实现该接口来定义具体的插件行为。

<?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">
    <parent>
        <artifactId>SpringBootPluginTest</artifactId>
        <groupId>com.plugin</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>plugin-api</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

</project>

Plugin 实现

在这里插入图片描述
插件实现类实现了 IPlugin 接口,提供了插件的具体功能。

package com.plugin.impl;

import com.plugin.IPlugin;

public class MyPluginImpl implements IPlugin {

    @Override
    public String customPluginMethod(String name) {
        return "MyPluginImpl-customPluginMethod executed - " + name;
    }
}

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.plugin</groupId>
        <artifactId>SpringBootPluginTest</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <groupId>com.plugin</groupId>
    <artifactId>plugin-impl</artifactId>
    <name>plugin-impl</name>
    <description>插件实现类</description>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <dependency>
            <groupId>com.plugin</groupId>
            <artifactId>plugin-api</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <finalName>${project.artifactId}-${project.version}-jar-with-dependencies</finalName>
                    <appendAssemblyId>false</appendAssemblyId>
                    <attach>false</attach>
                    <archive>
                        <manifest>
                            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
                        </manifest>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

我们将接口实现打包为jar包, 放到业务使用方配置的目录下。


Plugin 使用方

动态加载插件

在Spring Boot中,我们可以通过自定义 ImportBeanDefinitionRegistrar 来实现插件的动态加载。在插件模块启动时,Spring Boot会自动加载并注册插件类

package com.plugin.config;

import com.plugin.utils.ClassLoaderHelper;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;

/**
 * 启动时注册bean
 */
@Slf4j
public class PluginImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {
    /**
     * jar的存放路径
     */
    private String targetUrl;
    /**
     * 插件类全路径
     */
    private String pluginClass;

    /**
     * 注册Bean定义到Spring容器中
     *
     * @param importingClassMetadata 导入类的元数据,通常用于获取注解信息等
     * @param registry Bean定义的注册表,用于注册Bean定义
     */
    @SneakyThrows
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        // 获取自定义类加载器,用于加载插件类
        ClassLoader classLoader = ClassLoaderHelper.getClassLoader(targetUrl);
        // 加载插件类
        Class<?> clazz = classLoader.loadClass(pluginClass);

        // 创建Bean定义构建器
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
        // 获取Bean定义
        BeanDefinition beanDefinition = builder.getBeanDefinition();
        // 在注册表中注册Bean定义
        registry.registerBeanDefinition(clazz.getName(), beanDefinition);
        // 日志记录注册成功信息
        log.info("plugin register bean [{}],Class [{}] success.", clazz.getName(), clazz);
    }

    /**
     * 设置环境属性
     *
     * @param environment 环境对象,用于获取环境属性
     */
    @Override
    public void setEnvironment(Environment environment) {
        // 从环境对象中获取目标URL属性
        this.targetUrl = environment.getProperty("targetUrl");
        // 从环境对象中获取插件类属性
        this.pluginClass = environment.getProperty("pluginClass");
    }
}

在 registerBeanDefinitions 方法中,使用自定义的类加载器 ClassLoaderHelper 动态加载插件类,并将其注册为Spring容器中的Bean。


类加载器

package com.plugin.utils;

import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

/**
 * 类加载器工具类
 */
@Slf4j
public class ClassLoaderHelper {

    /**
     * 根据给定的URL获取一个ClassLoader实例
     * 此方法旨在动态加载指定位置的类资源,通过反射手段确保URLClassLoader的addURL方法可访问
     *
     * @param url 类资源的URL地址,指示ClassLoader要加载的类的位置
     * @return URLClassLoader的实例,用于加载指定URL路径下的类文件如果无法创建或访问ClassLoader,则返回null
     */
    public static ClassLoader getClassLoader(String url) {
        try {
            // 获取URLClassLoader的addURL方法,该方法允许向URLClassLoader添加新的URL
            Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
            // 如果方法不可访问,则设置其为可访问,因为addURL方法是受保护的,需要这样做才能调用
            if (!method.isAccessible()) {
                method.setAccessible(true);
            }
            // 创建一个新的URLClassLoader实例,初始URL为空数组,使用ClassLoaderUtil类的ClassLoader作为父ClassLoader
            URLClassLoader classLoader = new URLClassLoader(new URL[]{}, ClassLoaderHelper.class.getClassLoader());
            // 在创建 URLClassLoader 时,指定当前系统的 ClassLoader 为父类加载器  ClassLoader.getSystemClassLoader() 这步比较关键,用于打通主程序与插件之间的 ClassLoader ,解决把插件注册进 IOC 时的各种 ClassNotFoundException 问题
            // URLClassLoader classLoader = new URLClassLoader(new URL[]{}, ClassLoader.getSystemClassLoader());

            // 调用addURL方法,将指定的URL添加到classLoader中,以便它可以加载该URL路径下的类
            method.invoke(classLoader, new URL(url));
            // 返回配置好的ClassLoader实例
            return classLoader;
        } catch (Exception e) {
            // 记录错误信息和异常堆栈,当无法通过反射访问或调用addURL方法时,会进入此块
            log.error("getClassLoader-error", e);
            // 返回null,表示未能成功创建和配置ClassLoader实例
            return null;
        }
    }

}

为了加载外部JAR包中的插件类,需要一个自定义的类加载器。ClassLoaderHelper 类负责通过反射机制调用 addURL 方法,动态加载指定URL路径下的JAR包。


注册与卸载插件

package com.plugin.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Component;

/**
 * Spring 工具类
 *
 */
@Slf4j
@Component
public class SpringHelper implements ApplicationContextAware {
    private DefaultListableBeanFactory defaultListableBeanFactory;

    private ApplicationContext applicationContext;

    /**
     * 设置ApplicationContext环境
     *
     * 当该类被Spring管理时,Spring会调用此方法将ApplicationContext注入
     * 通过重写此方法,我们可以自定义处理ApplicationContext的方式
     *
     * @param applicationContext Spring的上下文对象,包含所有的Bean定义和配置信息
     * @throws BeansException 如果在处理Bean时发生错误
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        // 将传入的ApplicationContext赋值给类的成员变量,以便后续使用
        this.applicationContext = applicationContext;
        // 将applicationContext转换为ConfigurableApplicationContext,以便获取BeanFactory
        ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext;
        // 获取bean工厂并转换为DefaultListableBeanFactory,以便进行更深层次的自定义配置
        this.defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory();
    }

    /**
     * 注册bean到spring容器中
     *
     * @param beanName 名称
     * @param clazz    class
     */
    public void registerBean(String beanName, Class<?> clazz) {
        // 通过BeanDefinitionBuilder创建bean定义
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
        // 注册bean
        defaultListableBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getRawBeanDefinition());
        log.info("register bean [{}],Class [{}] success.", beanName, clazz);
    }

    /**
     * 根据bean名称移除bean定义
     * 此方法检查指定的bean名称是否在BeanFactory中定义如果定义存在,则将其移除
     *
     * @param beanName 要移除的bean的名称
     */
    public void removeBean(String beanName) {
        // 检查BeanFactory中是否定义了指定名称的bean
        if(defaultListableBeanFactory.containsBeanDefinition(beanName)) {
            // 如果bean定义存在,则从BeanFactory中移除该定义
            defaultListableBeanFactory.removeBeanDefinition(beanName);
        }
        // 记录移除bean操作的日志信息
        log.info("remove bean [{}] success.", beanName);
    }

    /**
     * 根据bean的名称获取对应的bean实例
     * 此方法用于从Spring应用上下文中获取指定名称的bean,便于在需要的地方直接获取bean实例,避免了硬编码
     *
     * @param name bean的名称,用于唯一标识一个bean
     * @return Object 返回指定名称的bean实例,类型为Object,可以根据需要转换为具体的类型
     */
    public Object getBean(String name) {
        return applicationContext.getBean(name);
    }
}

插件一旦加载并注册到Spring IoC容器后,我们可以通过API来操作插件,比如执行插件中的方法,或者动态更新和卸载插件。

package com.plugin.controller;

import com.plugin.IPlugin;
import com.plugin.utils.ClassLoaderHelper;
import com.plugin.utils.SpringHelper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@Slf4j
@RestController
public class PluginTestController {


    @Autowired(required = false)
    private IPlugin IPlugin;

    @Resource
    private SpringHelper springHelper;

    /**
     * jar的地址
     */
    @Value("${targetUrl}")
    private String targetUrl;
    /**
     * 插件类全路径
     */
    @Value("${pluginClass}")
    private String pluginClass;

    @GetMapping("/test")
    public String test() {
        return IPlugin.customPluginMethod("test plugin");
    }

    /**
     * 运行时注册bean
     * 此方法用于在应用程序运行时动态加载并注册一个bean,
     * 然后根据这个bean执行相应操作或返回其信息
     */
    @GetMapping("/reload")
    public Object reload() throws ClassNotFoundException {
        // 使用自定义类加载器获取指定URL的类加载器
        ClassLoader classLoader = ClassLoaderHelper.getClassLoader(targetUrl);
        // 通过类加载器加载指定名称的类
        Class<?> clazz = classLoader.loadClass(pluginClass);
        // 在Spring上下文中注册这个类作为一个bean
        springHelper.registerBean(clazz.getName(), clazz);
        // 从Spring上下文中获取新注册的bean
        Object bean = springHelper.getBean(clazz.getName());
        // 检查bean是否实现了PluginInterface接口
        if (bean instanceof IPlugin) {
            // 如果实现了,将此接口赋值给当前的pluginInterface,并调用其sayHello方法
            IPlugin plugin = (IPlugin) bean;
            this.IPlugin = plugin;
            return plugin.customPluginMethod("test reload");
        } else {
            // 如果没有实现,获取并记录该bean实现的第一个接口的名称,并返回bean的字符串表示
            log.info(bean.getClass().getInterfaces()[0].getName());
            return bean.toString();
        }
    }

    /**
     * 移除bean
     * 该方法用于从Spring应用上下文中移除指定的bean
     * 它首先通过ClassLoader加载指定的类,然后使用springUtil工具类移除对应的bean
     * 最后,它尝试获取并打印被移除的bean的信息,如果bean仍然存在的话
     *
     * @return 返回被移除bean的类名
     * @throws ClassNotFoundException 如果指定的类不存在,则抛出此异常
     */
    @GetMapping("/remove")
    public Object remove() throws ClassNotFoundException {
        // 获取目标URL对应的ClassLoader
        ClassLoader classLoader = ClassLoaderHelper.getClassLoader(targetUrl);
        // 通过ClassLoader加载插件类
        Class<?> clazz = classLoader.loadClass(pluginClass);
        // 使用springUtil工具类移除加载的插件类bean
        springHelper.removeBean(clazz.getName());
        // 清空pluginInterface引用,表示插件接口已被移除
        this.IPlugin = null;
        // 尝试获取已被移除的插件类bean
        // Object bean = springHelper.getBean(clazz.getName());
        // 如果bean不为空,打印bean的信息
        //if (bean != null) {
        //    log.info(bean.toString());
        // }
        // 返回被移除bean的类名
        return clazz.getName() + " removed";
    }
}

配置文件

在 application.properties 中配置插件路径和插件类的全路径

spring.main.allow-bean-definition-overriding=true


targetUrl=file:/D:/plugin-extends/plugin-impl-0.0.1-SNAPSHOT-jar-with-dependencies.jar
pluginClass=com.plugin.impl.MyPluginImpl
 

启动类

我们在Spring Boot启动类中使用 @Import 注解加载插件配置

package com.plugin;

import com.plugin.config.PluginImportBeanDefinitionRegistrar;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;

@SpringBootApplication
@Import(PluginImportBeanDefinitionRegistrar.class)
public class SpringBootPluginTestApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootPluginTestApplication.class, args);
    }
}

测试验证

在这里插入图片描述

启动时动态加载jar:http://127.0.0.1:8080/test
运行时动态加载jar:http://127.0.0.1:8080/reload
运行时动态卸载jar: http://127.0.0.1:8080/remove


小结

通过自定义类加载器、ImportBeanDefinitionRegistrar 和动态Bean管理,我们能够在Spring Boot应用中实现灵活的插件机制。这样的插件化架构不仅能够提升系统的可扩展性,还能有效地支持插件的动态加载和卸载,为系统提供更好的功能扩展能力。

在这里插入图片描述

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

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

相关文章

从单体到微服务:如何借助 Spring Cloud 实现架构转型

一、Spring Cloud简介 Spring Cloud 是一套基于 Spring 框架的微服务架构解决方案&#xff0c;它提供了一系列的工具和组件&#xff0c;帮助开发者快速构建分布式系统&#xff0c;尤其是微服务架构。 Spring Cloud 提供了诸如服务发现、配置管理、负载均衡、断路器、消息总线…

Flink学习连载文章13--FlinkSQL高级部分

eventTime 测试数据如下&#xff1a; {"username":"zs","price":20,"event_time":"2023-07-17 10:10:10"} {"username":"zs","price":15,"event_time":"2023-07-17 10:10:3…

UnityShaderLab 实现程序化形状(一)

1.实现一个长宽可变的矩形&#xff1a; 代码&#xff1a; fixed4 frag (v2f i) : SV_Target{return saturate(length(saturate(abs(i.uv - 0.5)-0.13)))/0.03;} 2.实现一个半径可变的圆形&#xff1a; 代码&#xff1a; fixed4 frag (v2f i) : SV_Target{return (distance(a…

【C++】CUDA线程模型

文章目录 1. 线程模型概述2. 一维线程模型3. 多维线程模型3.1 多维线程模型概述3.2 多维线程模型定义3.3 多维线程模型中的索引 4. 网格和线程块的限制条件 1. 线程模型概述 在CUDA编程中&#xff0c;线程的组织形式是多维的&#xff0c;主要通过网格&#xff08;Grid&#xff…

【JAVA高级篇教学】第二篇:使用 Redisson 实现高效限流机制

在高并发系统中&#xff0c;限流是一项非常重要的技术手段&#xff0c;用于保护后端服务&#xff0c;防止因流量过大导致系统崩溃。本文将详细介绍如何使用 Redisson 提供的 RRateLimiter 实现分布式限流&#xff0c;以及其原理、使用场景和完整代码示例。 目录 一、什么是限流…

聊聊在应用层面实现内网穿透功能是否可行

前言 最近接手了供方开发的网关项目&#xff0c;交接文档里面有个内网穿透的功能&#xff0c;一下子就吸引的我的目光。实现这个内网穿透的背景是业务部门有些业务是部署在公网&#xff0c;这些公网的业务想访问内网的业务&#xff0c;但因为公网和内网没打通&#xff0c;导致…

TPC-H数据集使用说明

TPCH数据使用说明 表模式&#xff1a; TPCH官网链接&#xff1a;TPC-H Homepage 同学们可以自行下载TPCH-tools自行生成数据&#xff08;10GB&#xff09;&#xff0c;下面主要是以mysql为例说明TPC-H的使用方法。 供同学自行参考&#xff1a; windows &#xff1a;TPC-H测…

vue2+html2canvas+js PDF实现试卷导出和打印功能

1.首先安装 import html2canvas from html2canvas; import { jsPDF } from jspdf; 2.引入打印插件print.js import Print from "/assets/js/print"; Vue.use(Print) // 打印类属性、方法定义 /* eslint-disable */ const Print function (dom, options) {if (…

Simdroid-EC:液冷仿真新星,助力新能源汽车电机控制器高效散热

近年来&#xff0c;新能源电动车的销量呈现出快速增长的态势。据统计&#xff0c;2024 年1-10月中国新能源汽车销量达728万辆&#xff0c;同比增长37.8%。 电机控制器在新能源汽车中对于保障动力和安全性能扮演着至关重要的角色&#xff0c;其核心部件IGBT&#xff08;绝缘栅双…

静态路由与交换机配置实验

1.建立网络拓扑 添加2台计算机&#xff0c;标签名为PC0、PC1&#xff1b;添加2台二层交换机2960&#xff0c;标签名为S0、S1&#xff1b;添加2台路由器2811&#xff0c;标签名为R0、R1&#xff1b;交换机划分的VLAN及端口根据如下拓扑图&#xff0c;使用直通线、DCE串口线连接…

深度学习:MindSpore自动并行

随着模型规模的逐渐增大&#xff0c;需要的算力逐渐增强&#xff0c;但是算力需求增长速度远高于芯片算力增长速度。现在唯一的解决方案只有通过超大规模集群训练大模型。 大集群训练大模型的挑战 内存墙 200B参数量的模型&#xff0c;参数内存占用745GB内存&#xff0c;训练…

前端成长之路:HTML(2)

HTML中有两个非常重要的标签——表格和表单&#xff0c;在介绍之前需要先了解表格和表单的区别&#xff1a;表格是用于展示数据的&#xff1b;表单是用于提交数据的。本文主要介绍表格。 表格标签 表格主要是用于显示、展示数据的&#xff0c;并非是页面布局。它可以使本来难…

如何使用WinCC DataMonitor基于Web发布浏览Excel报表文档

本文介绍使用 WinCC DataMonitor 的 "Excel Workbooks" 功能&#xff0c;通过 Excel 表格显示 WinCC 项目的过程值、归档变量值和报警归档消息。并可以通过 Web 发布浏览访问数据 1&#xff0e;WinCC DataMonitor是什么 ? DataMonitor 是 SIMATIC WinCC 工厂智能中…

Facebook广告突然无消耗?原因解析与解决方案。

在Facebook广告投放中&#xff0c;广告突然无消耗是很多广告主都会遇到的难题。这种情况不仅浪费时间&#xff0c;还可能导致营销活动停滞&#xff0c;影响业务发展。那么&#xff0c;广告无消耗的原因是什么&#xff1f;又该如何解决呢&#xff1f; 一、Facebook广告无消耗的…

安卓调试环境搭建

前言 前段时间电脑重装了系统&#xff0c;最近准备调试一个apk&#xff0c;没想到装环境的过程并不顺利&#xff0c;很让人火大&#xff0c;于是记录一下。 反编译工具下载 下载apktool.bat和apktool.jar 官网地址&#xff1a;https://ibotpeaches.github.io/Apktool/install…

shell基础知识4----正则表达式

一、文本搜索工具——grep grep -参数 条件 文件名 其中参数有以下&#xff1a; -i 忽略大小写 -c 统计匹配的行数 -v 取反&#xff0c;不显示匹配的行 -w 匹配单词 -E 等价于 egrep &#xff0c;即启用扩展正则表达式 -n 显示行号 -rl 将指定目录内的文件打…

git branch -vv(显示本地分支与远程分支的最新状态和提交信息)(very verbose mode)

文章目录 字段说明下一步操作建议字段说明当前状态分析相关操作建议 -vv功能说明-vv 与单个 -v 的区别总结 出现如下状况&#xff0c;是因为我把本地的develop分支没有提交到gitlab上的develop分支。 而是把develop分支的内容提交到了gitlab上的master分支&#xff0c;这样是不…

树莓派4B android 系统添加led灯 Hal 层

本文内容需要用到我上一篇文章做的驱动&#xff0c;可以先看文章https://blog.csdn.net/ange_li/article/details/136759249 一、Hal 层的实现 1.Hal 层的实现一般放在 vendor 目录下&#xff0c;我们在 vendor 目录下创建如下的目录 aosp/vendor/arpi/hardware/interfaces/…

Apache DolphinScheduler 限制秒级别的定时调度

背景 Apache DolphinScheduler 定时任务配置采用的 7 位 Crontab 表达式&#xff0c;分别对应秒、分、时、月天、月、周天、年。 在团队日常开发工作中&#xff0c;工作流的定时调度一般不会细化到秒级别。但历史上出现过因配置的疏忽大意而产生故障时间&#xff0c;如应该配…

MTK Android12 开机向导

文章目录 需求-场景参考资料&#xff1a;博客资料官网参考资料&#xff1a;参考资料注意点 附件资料文件说明&#xff1a;推荐工具&#xff1a;配置定制的 声明叠加层 APK需求实现替换字符、删减开机向导界面、添加开机向导界面删除部分界面需求&#xff0c;官网说明如下更新部…