Java SPI解读:揭秘服务提供接口的设计与应用

一、什么是SPI?

在 Java 编程中,SPI(Service Provider Interface)是实现可插拔式应用的一种机制。它就像是应用程序的魔法盒,让你可以随时添加新的功能实现,而不需搞得一团糟。通过SPI,我们可以在运行时动态加载具体的服务实现,这意味着你的应用程序可以像变戏法一样,轻松地变身成不同的形态。

SPI的关键特性就是可插拔性动态加载。这意味着你可以随心所欲地向应用程序添加新功能,而不会破坏原有的代码。这两个关键点也是开发人员常常提及的,让我们来解开这些概念的迷雾,首先要从 API 开始说起。

1.1、API

API是应用程序接口(Application Programming Interface)的缩写,它是开发者编写定义的一组接口和与之对应的众多实现类,外部用户可以根据需要选择具体的实现方式。

看到接口是不是想到了SPI中提到的可插拔性?没错,这确实与面向接口编程有关。举个例子:

import java.util.List;
import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        // 面向接口编程
        List<String> myList = new ArrayList<>();
        myList.add("Hello");
        myList.add("World");
        for (String str : myList) {
            System.out.println(str);
        }
    }
}

在这个例子中,我们创建了一个 ArrayList 集合,但是引用却指向了 List 接口。为什么要这样做呢?这是为了让程序更具扩展性。假设 myList 变量后续需要使用不同的集合类,我们只需修改新的集合实现类即可,而不需要修改后续其他代码。比如:

import java.util.List;
import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        // 面向接口编程
        List<String> myList = new ArrayList<>();
		// 切换集合
        myList = new LinkedList<>();
        
        myList.add("Hello");
        myList.add("World");
        for (String str : myList) {
            System.out.println(str);
        }
    }
}

看起来比较优雅,说它是可插拔也算合适。但这里实现了动态加载吗?请注意,这里说的动态加载并不是Java的多态:

  • 动态加载(Dynamic Loading): 是指在程序运行时根据需要加载类或模块的过程。这通常发生在运行时,而不是在编译时确定。动态加载可以使程序更加灵活,可以根据需要加载不同的类,以及在程序运行期间根据条件决定加载哪些类。

  • 多态(Polymorphism): 是面向对象编程的一个重要概念,它允许不同的子类对象以自己的方式来实现父类的方法。多态性是通过方法的重写和动态绑定来实现的。

我们上面的代码展示了多态,但并没有实现动态加载,因为用户仍然需要手动更改具体实现类。

这种由 JDK 提供的接口和各种实现类,被称为 API;这种方式实现了可插拔性,但并没有实现动态加载,结构如下图:

在这里插入图片描述

了解了 API 的概念,接下来我们讨论SPI

1.2、SPI

  1. SPI(Service Provider Interface)是在JDK 6版本后引入的一项新特性。它通过接口、约定和动态加载的方式,实现了模块之间无需更改代码便可无缝衔接的功能。其中,最典型的例子是JDBC。JDK提供了数据库连接的接口,各个数据库厂商根据这个接口实现了不同的逻辑。开发人员在使用时,只需要依赖对应的数据库驱动即可。他们编写的代码仍然是基于JDK提供的接口,而无需更改代码形式。这种设计使得应用程序更加灵活、可扩展,并且降低了对特定数据库的依赖。如下:
// 使用的是jdk提供的接口, 在Java 6及以上版本,符合SPI的驱动包不再需要显式调用Class.forName()来加载驱动程序
import java.sql.*;

public class SimpleJDBCExample {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mydatabase";
        String user = "username";
        String password = "password";

        try (Connection connection = DriverManager.getConnection(url, user, password);
             Statement statement = connection.createStatement();
             ResultSet resultSet = statement.executeQuery("SELECT * FROM mytable")) {

            while (resultSet.next()) {
                int id = resultSet.getInt("id");
                String name = resultSet.getString("name");
                System.out.println("ID: " + id + ", Name: " + name);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

  1. SPI主要通过三个步骤来实现各应用无缝衔接的功能:

    • 接口:应用需要提供一个抽象接口,供其他模块进行实现,例如在JDK中的Connection接口。
    • 约定:其他模块需要在其jar包的META-INF/services/目录下创建一个以服务接口命名的文件,文件中列出了实现该接口的具体类的全限定名称。
    • 动态加载:JDK 6引入了ServiceLoader类,它的主要功能是检测所有jar包里的META-INF/services目录下的文件,并创建对应的实现类。
  2. 以上理论太过抽象,来个示例:

    3.1. 定义一个Connection接口

    public interface Connection {
        String getName();
    }
    

    3.2. 创建MysqlConnection实现类

    public class MysqlConnection implements Connection {
        @Override
        public String getName() {
            return "MysqlConnection";
        }
    }
    

    3.3. 创建OracleConnection实现类

    public class OracleConnection implements Connection {
        @Override
        public String getName() {
            return "OracleConnection";
        }
    }
    

    3.4. 新建META-INF/services目录并创建org.apache.spi.example.jdbc.Connection文件

    resources
        └─META-INF
            └─services
                └─org.apache.spi.example.jdbc.Connection
    

    3.5. 将实现类全限定名写入org.apache.spi.example.jdbc.Connection文件中,如下:

    org.apache.spi.example.jdbc.MysqlConnection
    org.apache.spi.example.jdbc.OracleConnection
    

    3.6. 编写测试类

    public class Example {
        @Test
        public void example01() {
            ServiceLoader<Connection> serviceLoader = ServiceLoader.load(Connection.class);
            for (Connection search : serviceLoader) {
                System.out.println(search.getName());
            }
        }
    }
    

    结果打印:

    MysqlConnection
    OracleConnection
    
  3. 至于为什么将配置文件放在META-INF/services下面,原因在于ServiceLoader代码中固定了文件扫描路径(约定),如下:

    private static final String PREFIX = "META-INF/services/"
    
  4. 以上便是SPI的基本使用,通过约定配置、面向接口编程以及ServiceLoader实现了不同模块的可插拔性,设计结构如下图:

在这里插入图片描述

二、对比其他实现

看到这里有的读者可能会有疑问,SPI是否可以通过反射或Spring的自动装配来实现上述需求?答案是肯定的。SPI的动态加载实际上就是使用了反射来实现。但值得注意的是,SPI本身是一种设计思想,它通过接口、约定和动态加载来实现模块之间的解耦和扩展性。

在JDK 6及以上版本中,SPI提供了一种默认实现,但开发人员完全可以根据自己的定制化需求,按照公司内部的约定来定义配置文件。

为了更好地理解SPI的设计思想,下面将对比几种常见的实现方式。

3.1、反射

反射是一种在运行时动态获取类的信息并调用其方法或创建其实例的机制。然而,光靠反射是无法实现SPI的设计思想的,因为反射本身缺乏约定性;在使用反射时,首先需要确定要加载的范围,即包名,示例如下:

import java.lang.reflect.*;

public class ReflectionExample {
    public static void main(String[] args) {
        String packageName = "java.util"; // 指定的包名
        String interfaceName = "List"; // 指定的接口名

        try {
            Class<?>[] classes = Package.getPackage(packageName).getClasses();
            for (Class<?> clazz : classes) {
                if (clazz.getInterfaces().length > 0 && clazz.getInterfaces()[0].getSimpleName().equals(interfaceName)) {
                    System.out.println("Found class implementing interface " + interfaceName + ": " + clazz.getSimpleName());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

虽然上面的示例展示了如何在指定包名下查找实现了特定接口的类,但这种方式存在一定局限性。对于自身应用来说,还相对好处理,因为我们自己开发的包名是确定的。但对于其他实现同一接口的第三方jar包,我们无法事先知道其包名,因此无法直接使用这种方式来加载其他jar包的实现类。

此外,与自行通过反射实现相比,JDK 6提供的ServiceLoader类更为便捷和高效。它是SPI设计思想的默认实现方式,能够自动加载指定接口的所有实现类。ServiceLoader会搜索所有jar包下是否存在META-INF/services目录,并读取这些目录下的配置文件来获取实现类的信息。这意味着即使这些实现类分布在应用程序所依赖的不同jar包中,ServiceLoader也能够加载它们。这使得使用JDK提供的ServiceLoader类更加简便,避免了重复造轮子的问题,提高了开发效率。

3.2、Spring-IOC

Spring的IOC(Inversion of Control)和SPI(Service Provider Interface)是两种不同的设计思想,它们在目的和应用场景上有所不同:

  1. IOC(Inversion of Control):

    • IOC是Spring框架的核心概念之一,它指的是控制反转,即将对象的创建和依赖关系的维护交给Spring容器管理。

    • 在IOC中,开发者将对象的创建和依赖关系的维护交给Spring容器,通过配置文件或注解来声明Bean的依赖关系,而不需要手动创建和管理对象。

    • 其作用范围是在其自身开发应用application类@ComponentScan扫描范围内,如下:

      import org.springframework.context.annotation.ComponentScan;
      import org.springframework.context.annotation.Configuration;
      
      @Configuration
      @ComponentScan(value = {"tech.qifu.jinke.yushu.dam", "tech.qifu.jinke.yushu.dis"})
      public class YushuDamAppAutoConfiguration {
      }
      
  2. SPI(Service Provider Interface):

    • SPI是一种Java设计模式,它通过接口、约定和动态加载来实现模块之间的解耦和扩展性。
    • 在SPI中,应用接口由平台或框架定义,不同的模块或厂商可以根据接口实现自己的逻辑,然后通过约定的方式将实现类注册到框架中。
    • 其作用范围在应用及应用所依赖所有jar包范围内,搜索所有jar包下是否存在META-INF/services目录,并读取这些目录下的配置文件来获取实现类的信息。

因此,IOC和SPI虽然都是用于降低程序的耦合度,但它们的实现方式和应用场景是不同的。IOC主要用于管理对象的创建和依赖关系,而SPI主要用于实现多模块之间的解耦和扩展性。在Spring框架中,IOC和SPI常常结合使用,以实现更灵活、可扩展的应用架构。

此外,使用IOC必须要引入Spring相关依赖,并且引入了一定的运行时开销。

3.3、spring.factories

spring.factories是Spring框架中的一种特殊配置文件,用于自动化配置和加载Spring应用中的扩展点。

在Spring Boot应用中,spring.factories文件通常位于META-INF/spring.factories路径下。这个文件使用标准的Java properties格式,其中包含了各种Spring应用中需要自动化加载的配置信息,如下图:

在这里插入图片描述

开发者可以在spring.factories文件中注册各种扩展点,例如自定义的EnableAutoConfigurationBeanFactoryPostProcessorApplicationListener等。这些扩展点可以是自己编写的类,也可以是第三方库提供的。

Spring框架在启动时会自动扫描所有jar包中META-INF/spring.factories文件中定义的扩展点,从而实现自动化配置和加载。这使得Spring应用的开发和管理更加简便,可以方便地集成各种第三方库和自定义功能。

从设计思想上看和SPI机制很像,只是约定文件从META-INF/services/变为META-INF/spring.factories,且spring将所有自定义扩展整合到一个配置文件中,故该方式又被称为:Spring的SPI机制。

四、实操

以下示例均可在 GitHub#spi-examples 仓库上找到。

4.1、JDBC示例

  1. jdbc是SPI的典型应用,这里我们简单模拟一下,这里使用maven工具创建三个model模块,结构如下:

    spi-examples
    	└─spi-jdbc
    	└─spi-mysql-connector
    	└─spi-oracle-connector
    
  2. spi-jdbc模块中创建:Connection接口及DriverManager类,代码如下:

    public interface Connection {
        String getName();
    }
    
    public class DriverManager {
    
        public static void getConnection() {
            ServiceLoader<Connection> connectionLoader = ServiceLoader.load(Connection.class);
            for (Connection connection : connectionLoader) {
                System.out.println(connection.getName());
            }
        }
    }
    
  3. spi-mysql-connector模块pom.xml 依赖 spi-jdbc模块,并创建MysqlConnection实现类,代码如下:

    import org.apache.spi.employ.jdbc.Connection;
    
    public class MysqlConnection implements Connection {
        @Override
        public String getName() {
            return "MysqlConnection";
        }
    }
    
  4. spi-mysql-connector模块resources文件下创建META-INF/services/org.apache.spi.employ.jdbc.Connection文件:

    org.apache.spi.realize.jdbc.mysql.MysqlConnection
    
  5. 接着在spi-oracle-connector模块pom.xml 依赖 spi-jdbc模块,并创建OracleConnection实现类,代码如下:

    import org.apache.spi.employ.jdbc.Connection;
    
    public class OracleConnection implements Connection {
        @Override
        public String getName() {
            return "OracleConnection";
        }
    }
    
  6. spi-oracle-connector模块resources文件下创建META-INF/services/org.apache.spi.employ.jdbc.Connection文件:

    org.apache.spi.realize.jdbc.oracle.OracleConnection
    
  7. 以上准备工作完成,接下来我们模拟用户使用,创建一个spi-user模块,pom中依赖spi-mysql-connector模块,如下:

    <artifactId>spi-user</artifactId>
    
    <dependencies>
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>spi-mysql-connector</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
    
  8. spi-user中编写测试类,如下:

    import org.apache.spi.employ.jdbc.DriverManager;
    import org.junit.Test;
    
    public class Example {
        @Test
        public void example() {
            DriverManager.getConnection();
        }
    }
    
  9. 此时打印结果为:MysqlConnection,证明已经成功注入MysqlConnection

  10. 接下来我们更改spi-user的pom依赖为spi-mysql-connector,如下:

    <artifactId>spi-user</artifactId>
    
    <dependencies>
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>spi-oracle-connector</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
    
  11. 此时运行测试类Example,结果打印为:OracleConnection,证明已经成功注入OracleConnection

注意:此时spi-user模块的代码没有任何修改,却可以根据不同的依赖包灵活的使用不同的连接器,这便是SPI的可插拔性及动态加载特性!

4.2、SPI实现IOC

在上述 JDBC 示例中,我们展示了跨 JAR 包之间通过该SPI动态注入的示例。然而,SPI 也可以在应用自身的 JAR 包中发挥作用。在大数据领域,特别是大数据开发的程序,除了服务应用外,通常不会使用 Spring 相关的依赖。但是,Spring 的 IOC(控制反转)功能确实非常便利。在这种情况下,我们可以通过 SPI 来实现控制反转,从而提高程序的可读性和可扩展性。示例如下:

  1. 新增spi-self模块,创建IdentityService接口,如下:

    public interface Identity {
        String getIdentity();
    }
    
    public interface Service extends Identity {
        void execute();
    }
    

    Identity 接口用于区分多个 Service 实现类,类似于 Spring 中的 Bean ID。

  2. 创建CommodityServiceImplOrderServiceImpl实现类,如下:

    import org.apache.spi.self.service.Service;
    
    public class OrderServiceImpl implements Service {
        @Override
        public void execute() {
            System.out.println("OrderServiceImpl");
        }
    
        @Override
        public String getIdentity() {
            return "OrderServiceImpl";
        }
    }
    
    import org.apache.spi.self.service.Service;
    
    public class CommodityServiceImpl implements Service {
        @Override
        public void execute() {
            System.out.println("CommodityServiceImpl");
        }
    
        @Override
        public String getIdentity() {
            return "CommodityServiceImpl";
        }
    }
    
  3. resources文件下创建META-INF/services/org.apache.spi.self.service.Service文件,内容如下:

    org.apache.spi.self.service.impl.CommodityServiceImpl
    org.apache.spi.self.service.impl.OrderServiceImpl
    
  4. 创建PluginDiscovery功能类,通过SPI获取实现类并对外提供两个获取接口函数,代码如下:

    import org.apache.spi.self.service.Service;
    
    import java.util.ServiceLoader;
    
    public class PluginDiscovery {
    
        /** 按类型匹配, 取默认第一个 */
        public static Service discoveryService() {
            ServiceLoader<Service> serviceLoader = ServiceLoader.load(Service.class);
            return serviceLoader.iterator().next();
        }
    
        /** 按类型及ID匹配 */
        public static Service discoveryService(String id) {
            ServiceLoader<Service> serviceLoader = ServiceLoader.load(Service.class);
            for (Service service : serviceLoader) {
                if (service.getIdentity().equalsIgnoreCase(id)) {
                    return service;
                }
            }
            throw new RuntimeException(String.format("not find Id:%s Service Class", id));
        }
    
    }
    
  5. 创建测试类Example,代码如下:

    import org.apache.spi.self.service.Service;
    import org.junit.Test;
    
    public class Example {
        @Test
        public void example01() {
            Service service = PluginDiscovery.discoveryService();
            service.execute();
        }
    
        @Test
        public void example02() {
            Service service = PluginDiscovery.discoveryService("OrderServiceImpl");
            service.execute();
        }
    }
    
  6. 最终结果正确打印,我们成功通过类型和 ID 匹配到了相应的实现类。这一切都是通过 SPI 间接实现了类似于 Spring IOC 的功能,使得代码更加简洁、灵活,增强了程序的可维护性和可扩展性。

4.3、注解实现SPI

以上两个示例均需要手动编写 META-INF/services 配置文件,这种人工操作不仅耗时,还会增加出错的风险。因此,我们需要一个可以自动生成 SPI 配置文件并自动写入实现类的工具来简化这个过程。幸运的是,谷歌提供了一个名为 auto-service-annotations 的包,可以帮助我们实现这一需求。示例如下:

  1. 新增spi-self-auto模块,pom依赖如下:

    <artifactId>spi-self-auto</artifactId>
    
        <properties>
            <auto-service.version>1.1.1</auto-service.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>com.google.auto.service</groupId>
                <artifactId>auto-service-annotations</artifactId>
                <version>${auto-service.version}</version>
            </dependency>
            <dependency>
                <groupId>com.google.auto.service</groupId>
                <artifactId>auto-service</artifactId>
                <version>${auto-service.version}</version>
            </dependency>
        </dependencies>
    
  2. IdentityService 接口以及 PluginDiscovery 类与之前示例中的 spi-self 模块保持一致。不过,我们重新编写了 CommodityServiceImplOrderServiceImpl 实现类,并在它们上面添加了 @AutoService 注解,代码如下:

    import com.google.auto.service.AutoService;
    import org.apache.spi.auto.service.Service;
    
    @AutoService(Service.class)
    public class OrderServiceImpl implements Service {
        @Override
        public void execute() {
            System.out.println("OrderServiceImpl");
        }
    
        @Override
        public String getIdentity() {
            return "OrderServiceImpl";
        }
    }
    
    import com.google.auto.service.AutoService;
    import org.apache.spi.auto.service.Service;
    
    @AutoService(Service.class)
    public class CommodityServiceImpl implements Service {
        @Override
        public void execute() {
            System.out.println("CommodityServiceImpl");
        }
    
        @Override
        public String getIdentity() {
            return "CommodityServiceImpl";
        }
    }
    
  3. 创建测试类Example,代码如下:

    import org.apache.spi.auto.service.Service;
    import org.junit.Test;
    
    public class Example {
        @Test
        public void example01() {
            Service service = PluginDiscovery.discoveryService();
            service.execute();
        }
    
        @Test
        public void example02() {
            Service service = PluginDiscovery.discoveryService("OrderServiceImpl");
            service.execute();
        }
    }
    
  4. 最终结果正确打印。在这个示例中,我们没有手动编写 META-INF/services 配置文件,而是通过 @AutoService 注解,在编译期间自动生成了配置文件,并将其存放在 target/classes 目录下,结构如下:

    target
       └─classes
    	  └─META-INF
    	      └─services
    	          └─org.apache.spi.auto.service.Service
    
  5. 而在package/install 操作时则会自动将该配置类打入jar包中,这样的结构使得SPI的使用更加便捷,减少了手动维护的工作,同时确保了代码的可读性和可维护性。

五、总结

总的来说 SPI(Service Provider Interface)是一种用于实现组件化和插件化的 Java 标准。通过 SPI,开发者可以定义服务接口,并允许外部实现这些接口,然后在运行时动态加载并使用这些实现。以下是 SPI 的主要特点和总结:

  1. 灵活性和可扩展性: SPI 允许系统在运行时动态地加载并使用外部实现,从而增加了系统的灵活性和可扩展性。系统可以根据需求动态选择和加载合适的实现,而无需在代码中显式指定。
  2. 松耦合: SPI 通过接口和实现类的分离,实现了组件之间的松耦合。组件之间只通过接口进行通信,而不直接依赖具体的实现,使得组件更易于替换和升级。
  3. 自动发现机制: SPI 提供了自动发现机制,使得系统可以自动扫描并加载符合条件的实现类。开发者只需在实现类上添加特定的注解或者遵循约定,就可以实现自动注册和加载。
  4. 标准化: SPI 是 Java 标准库提供的一种机制,因此具有良好的兼容性和稳定性。开发者可以借助 SPI 实现应用程序的插件化,而无需依赖第三方框架或库。
  5. 易用性: SPI 的使用相对简单,只需要定义接口、实现接口并添加特定的注解或配置即可实现插件的加载和使用,无需复杂的配置和编码。

总的来说,SPI 是一种强大的机制,可以帮助开发者实现组件化和插件化,提高系统的灵活性、可扩展性和可维护性,是 Java 开发中常用的设计模式之一。

六、相关资料

  • 文章代码示例
  • SPI官网介绍
  • Difference between SPI and API?

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

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

相关文章

汇春科技之MDT10F684

目录 第一、时钟 第二&#xff0c;定时器Timer0 第三&#xff0c;pwm 汇春官网&#xff1a;汇春科技 (yspringtech.com) 汇春是麦肯的原厂&#xff0c;以下是两个论坛&#xff0c;其中都有关于麦肯单片机的学习论坛&#xff0c;可以参考学习&#xff0c;第一个叫英锐恩&…

康耐视visionpro-CogAcqFifoTool工具详细说明

CogAcqFifoTool操作说明&#xff1a; ① 打开工具栏&#xff0c;双击或点击鼠标拖拽 添加CogAcqFifoTool ②.从图片采集设备/图像采集卡列表里选择对应的相机&#xff0c;视频格式选择图像格式。 Mono表示黑白图像&#xff0c;RGB表示彩色相机。点击初始化取相初始化相机。 ③…

【OJ】动归练习三

个人主页 &#xff1a; zxctscl 如有转载请先通知 题目 1. LCR166. 珠宝的最高价值1.1 分析1.2 代码 2. 931.下降路径最小和2.1 分析2.2 代码 3. 64.最小路径和3.1 分析3.2 代码 1. LCR166. 珠宝的最高价值 1.1 分析 状态表示 以[i][j]位置为结尾&#xff0c;表示到达[i][j]位置…

AI大模型智能大气科学探索之:ChatGPT在大气科学领域建模、数据分析、可视化与资源评估中的高效应用及论文写作

深度探讨人工智能在大气科学中的应用&#xff0c;特别是如何结合最新AI模型与Python技术处理和分析气候数据。课程介绍包括GPT-4等先进AI工具&#xff0c;旨在帮助大家掌握这些工具的功能及应用范围。内容覆盖使用GPT处理数据、生成论文摘要、文献综述、技术方法分析等实战案例…

HN 热帖|难以想象,20 年前代码版本管理是如何做的

本文源自 Hacker News 热帖&#xff0c;原文 Twenty Years Is Nothing&#xff0c;作者 Adrian Kosmaczewski。 在之前的文章中&#xff0c;我们曾称英语在我们的行业中如此普遍&#xff0c;以至于没有人质疑其使用。同样&#xff0c;Git 也是如此。很难想象仅仅二十年前&#…

掌握数字化运维方法,构建数字化运维体系

文章目录 &#x1f4cb; 前言&#x1f3af; 什么是数字化转型&#x1f3af; 数字化运维发展变化&#x1f3af; 数字化转型书籍推荐&#x1f9e9; 主要内容&#x1f9e9; 适合读者 &#x1f525; 参与方式 &#x1f4cb; 前言 数字化转型已经成为大势所趋&#xff0c;各行各业正…

Leetcode1997. 访问完所有房间的第一天

Every day a Leetcode 题目来源&#xff1a;1997. 访问完所有房间的第一天 解法1&#xff1a;动态规划 状态转移&#xff1a; 代码&#xff1a; /** lc appleetcode.cn id1997 langcpp** [1997] 访问完所有房间的第一天*/// lc codestart class Solution { private:const in…

探索定制化创新,定制你的Jetson Linux驱动开发之旅!

Jetson驱动定制开发 Jetson linux驱动定制开发 在数字创新的浪潮中&#xff0c;Jetson系列为我们带来了无限的可能性。然而&#xff0c;要想真正发挥这种潜力&#xff0c;我们需要更多的自由和个性化。现在&#xff0c;通过定制化的Jetson Linux驱动开发&#xff0c;你可以实…

MYSQL8最新安装教程 ! ! !

MYSQL8最新安装教程 安装配置MySql一、下载MySql进入官网&#xff1a;https://dev.mysql.com 二、新建文件夹管理Mysql系列文件三、配置my.ini文件四、执行数据库初始化命令五、基础配置六、配置系统环境变量 可能会遇到无法启动MYSQL服务的问题:一、尝试删除MySQL服务&#xf…

揭秘:为何单点登陆方案(SSO)已无法满足现代企业的身份管理需求,统一身份中台才是未来

在信息化建设的浪潮中&#xff0c;企业面临着越来越多的应用系统管理和用户身份认证问题。许多企业最初可能认为&#xff0c;单点登录&#xff08;SSO&#xff09;系统就是他们所需要的解决方案&#xff0c;用以简化用户在多个系统间的登录过程。然而&#xff0c;随着业务的发展…

正大国际:黄金投资稳定与保值的避险之选

黄金作为备受投资者追捧的贵金属&#xff0c;在金融市场中扮演着重要的角色。黄金作为一种避险资产具有稳定性和保值特性&#xff0c;能够在市场动荡时提供投资者的资金保护&#xff0c; 正大召煮4/26/12 xiaoccsw 避险需求:当股票市场、货币市场或其他资产类别表现不佳时&a…

电脑关机速度很慢怎么解决?

给电脑关机,总是要很久才完全关闭。这是因为计算机运行了太长时间,并且打开的程序太多,则关闭时间超过十秒钟,这是正常的现象。还有就是计算机升级或补丁程序更新也将导致计算机缓慢关闭。此时,建议耐心等待关闭完成。还有可能是系统故障了。接下来分享电脑关机速度很慢怎…

高中数学:零点综合题型(拔高)

一、零点与交点 关键原则 1、数形结合 2、方程思想 例题1 解题思路 1、函数转化成方程 2、零点问题转化成交点问题 3、数形结合 4、对数运算法则&#xff08;函数值的和 转化成 x的积&#xff09; 二、分段函数零点 关键原则 1、分段函数分段看 2、数形结合 3、零点转交点…

springboot多模块

这里springboot使用idea中的 Spring Initializr 来快速创建。 一、demo 1、创建父项目 首先使用 Spring Initializr 来快速创建好一个父Maven工程。然后删除无关的文件&#xff0c;只需保留pom.xml 文件。 &#xff08;1&#xff09;new Project -> spring initializr快…

Java设计模式之装饰器模式

装饰器模式是一种结构型设计模式&#xff0c;它允许动态地将责任附加到对象上。装饰器模式是通过创建一个包装对象&#xff0c;也就是装饰器&#xff0c;来包裹真实对象&#xff0c;从而实现对真实对象的功能增强。装饰器模式可以在不修改原有对象的情况下&#xff0c;动态地添…

李宏毅深度强化学习导论——策略梯度

引言 这是李宏毅老师深度强化学习视频的学习笔记&#xff0c;主要介绍策略梯度的概念&#xff0c;在上篇文章的末尾从交叉熵开始引入策略梯度。 如何控制你的智能体 上篇文章末尾我们提到了两个问题&#xff1a; 如何定义这些分数 A A A&#xff0c;即定义奖励机制&#xff…

Nmap扫描工具流量特征

如果Nmap扫描探测服务过程中如有HTTP协议&#xff0c;则User-Agent也会有明显的特征。例如会出现nmap关键字。 但是User-Agent可以被修改&#xff0c;如果被修改的话&#xff0c;我们可以通过告警的次数进行判断&#xff08;同一源IP&#xff09;&#xff0c;看是否是大量的扫描…

产品经理与产品原型

1. 前言 互联网产品经理在向技术部门递交产品策划方案时,除了详尽的需求阐述,一份清晰易懂的产品原型设计方案同样不可或缺。一份出色的原型设计,不仅能促进前期的深入讨论,更能让美工和开发人员更直观地理解产品特性,进而优化工作流程,减少不必要的时间消耗,提升整体工…

主流公链 - BCH BSV BTG

为什么出现分叉 BTC是自由的&#xff0c;BTC社区也是自由的&#xff0c;自然而然的会出现不同观点的群体 1. 比特币现金&#xff08;Bitcoin Cash&#xff0c;BCH&#xff09; 分叉日期&#xff1a; 2017年8月1日主要目的&#xff1a; 提高比特币的交易吞吐量和降低交易费用技术…
最新文章