告别硬编码:Spring条件注解优雅应对多类场景

一、背景

在当今的软件开发中,服务接口通常需要对应多个实现类,以满足不同的需求和场景。举例来说,假设我们是一家2B公司,公司的产品具备对象存储服务的能力。然而,在不同的合作机构部署时,发现每家公司底层的对象存储服务都不相同,比如机构A使用阿里云,机构B使用AWS S3等。针对这种情况,公司应用底层需要支持多种云存储平台,如阿里云、AWS S3等。

又由于每种云存储平台都拥有独特的API和特性,因此在设计软件时必须考虑到系统的可扩展性。通常情况下,我们会编写一个对外开放的openAPI接口,而应用底层需要根据不同的需求选择合适的实现类。

在这种情况下,如何避免硬编码并以一种优雅的方式实现上述需求成为了本篇博客要讨论的问题。

以下示例均可在 gitHub#inject-condition 仓库上找到。

二、解决方案

由于应用需要对外提供服务,我们以业内常见的Spring Boot服务应用为前提进行讨论。

在这种情况下,常见的解决方案可分为两类:SPI 和 Spring条件注解。

  1. SPI(Service Provider Interface)
    • SPI 是一种标准的Java扩展机制,允许第三方实现提供服务的接口,并由应用程序在运行时动态加载。
    • 在Spring Boot应用中,我们可以定义一个服务接口,然后多个实现类分别实现这个接口。使用SPI机制,我们可以在配置文件中指定想要使用的实现类。
    • 优点:灵活性高,支持动态加载和配置。
    • 缺点:需要手动管理配置文件,并且在服务实现类数量较多时,容易出现配置混乱的问题。
  2. Spring条件注解
    • Spring提供了一系列的条件注解,如@ConditionalOnProperty@ConditionalOnClass等,用于根据应用程序的配置或环境条件来动态地选择加载或配置Bean。
    • 我们可以使用条件注解来根据应用程序的配置来选择合适的实现类。比如,可以根据配置文件中的属性来决定使用哪个实现类。
    • 优点:无需手动管理配置文件,能够根据配置自动选择合适的实现类。
    • 缺点:相比SPI,条件注解的动态加载能力稍逊,使用上稍显复杂,需要了解和掌握Spring的条件注解机制。

综上所述,针对Spring Boot服务应用中服务接口对应多个实现类的需求,我们可以选择SPI或Spring条件注解作为解决方案。

由于SPI已在另一篇博客中有详细讲解,本文将重点讲解Spring条件注解。更多关于SPI的内容可参考笔者的另一篇博客:Java SPI解读:揭秘服务提供接口的设计与应用

三、示例

3.1、场景模拟

  1. 在应用中新建一个ObjectStorageService存储接口,代码如下:
import java.io.File;public interface ObjectStorageService {/*** 上传文件到对象存储* @param file 文件* @param bucketName 存储桶名称* @param objectKey 对象键(文件名)* @return 文件在对象存储中的URL*/String uploadObject(File file, String bucketName, String objectKey);/*** 从对象存储下载文件* @param bucketName 存储桶名称* @param objectKey 对象键(文件名)* @return 文件*/File downloadObject(String bucketName, String objectKey);
}
  1. 接下来,我们创建了三个通过@Service注入的实现类。首先是默认实现类DefaultObjectStorageServiceImpl,其次是阿里云存储服务的实现类AliyunObjectStorageServiceImpl,最后是S3存储服务的实现类S3ObjectStorageServiceImpl。具体的代码实现:
@Slf4j
@Service
public class DefaultObjectStorageServiceImpl implements ObjectStorageService {@Overridepublic String uploadObject(File file, String bucketName, String objectKey) {// 默认实现上传逻辑return "Default implementation: Upload successful";}@Overridepublic File downloadObject(String bucketName, String objectKey) {// 默认实现下载逻辑return new File("default-file.txt");}
}
@Slf4j
@Service
public class AliyunObjectStorageServiceImpl implements ObjectStorageService {@Overridepublic String uploadObject(File file, String bucketName, String objectKey) {// 阿里云实现上传逻辑return "Aliyun implementation: Upload successful";}@Overridepublic File downloadObject(String bucketName, String objectKey) {// 阿里云实现下载逻辑return new File("aliyun-file.txt");}
}
@Slf4j
@Service
public class S3ObjectStorageServiceImpl implements ObjectStorageService {@Overridepublic String uploadObject(File file, String bucketName, String objectKey) {// S3实现上传逻辑return "S3 implementation: Upload successful";}@Overridepublic File downloadObject(String bucketName, String objectKey) {// S3实现下载逻辑return new File("s3-file.txt");}
}
  1. 最后再创建一个Controller类通过@Autowired注解注入ObjectStorageService,并对外开放接口,代码如下:
@Slf4j
@RestController
public class StorageController {@Autowiredprivate ObjectStorageService objectStorageService;@GetMapping("/example")public void example() {log.info("objectStorageService: {}", objectStorageService);}
}
  1. 此时运行应用报错信息如下:
***************************
APPLICATION FAILED TO START
***************************Description:Field objectStorageService in org.example.inject.web.controller.StorageController required a single bean, but 3 were found:- aliyunObjectStorageServiceImpl: defined in file [D:\IdeaProjects\inject-examples\inject-condition\target\classes\org\example\inject\web\service\impl\AliyunObjectStorageServiceImpl.class]- defaultObjectStorageServiceImpl: defined in file [D:\IdeaProjects\inject-examples\inject-condition\target\classes\org\example\inject\web\service\impl\DefaultObjectStorageServiceImpl.class]- s3ObjectStorageServiceImpl: defined in file [D:\IdeaProjects\inject-examples\inject-condition\target\classes\org\example\inject\web\service\impl\S3ObjectStorageServiceImpl.class]Action:Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed

错误提示StorageController需要一个objectStorageService bean,但是却找到了3个可用的bean:aliyunObjectStorageServiceImpldefaultObjectStorageServiceImpls3ObjectStorageServiceImpl。spring也提示了解决方案:

  1. 在其中一个实现类上添加@Primary注解,指示Spring优先选择这个bean。
  2. 修改StorageController以接受多个objectStorageService,或者使用@Qualifier注解指定要注入的特定bean。

3.2、@Qualifier解决方案

  1. @Autowired是Spring2.5 引入的注解,@Autowired 注解只根据类型进行注入,不会根据名称匹配。当类型无法辨别注入对象时,可以使用 @Qualifier@Primary 注解来修饰,修改后代码如下:
@Slf4j
@RestController
public class StorageController {@Autowired@Qualifier("aliyunObjectStorageServiceImpl")private ObjectStorageService objectStorageService;@GetMapping("/example")public void example() {log.info("objectStorageService: {}", objectStorageService);}
}

@Qualifier注解中的参数是BeanID,即@Service注解所注入的实现类的名称。

  1. 运行应用后一切正常,命令行输入: curl http://localhost:8080/example,日志打印:注入成功
objectStorageService: org.example.inject.condition.service.impl.AliyunObjectStorageServiceImpl@6f2aa58b
  1. 遗憾的是,@Qualifier注解并不支持变量赋值,只能通过硬编码的方式指定具体的实现类。下面是一个错误示例:
@Slf4j
@RestController
public class StorageController {@Value("${storage.provider}")private String storageProvider;@Autowired@Qualifier("${storageProvider}")private ObjectStorageService objectStorageService;@GetMapping("/example")public void example() {log.info("objectStorageService: {}", objectStorageService);}
}

虽然我们希望通过配置变量的方式来指定具体的实现类,但是由于@Qualifier注解的限制,这种方案并不可行,因此不推荐使用。

3.3、@Resource解决方案

  1. 在Spring Boot应用中,除了@Autowired,还可以使用@Resource来进行依赖注入,代码如下:
import javax.annotation.Resource;@Slf4j
@RestController
public class StorageController {//  @Autowired@Resourceprivate ObjectStorageService objectStorageService;@GetMapping("/example")public void example() {log.info("objectStorageService: {}", objectStorageService);}
}
  1. @Resource@Autowired区别在于:

    • @Resource 是 JDK 原生的注解,而 @Autowired 是 Spring 2.5 引入的注解。

    • @Resource 注解有两个属性:nametype。Spring 将 @Resource 注解的 name 属性解析为 bean 的名称,而 type 属性则解析为 bean 的类型。因此,如果使用 name 属性,则采用 byName 的自动注入策略;如果使用 type 属性,则采用 byType 的自动注入策略。如果既不指定 name 也不指定 type 属性,则将通过反射机制使用 byName 自动注入策略。

    • @Autowired 注解只根据类型进行注入,不会根据名称匹配。当类型无法辨别注入对象时,可以使用 @Qualifier@Primary 注解来修饰。

  2. 所以我们可以通过@Resource注解指定name属性从而实现指定实现类注入,代码如下:

@Slf4j
@RestController
public class StorageController {//  @Autowired@Resource(name = "aliyunObjectStorageServiceImpl")private ObjectStorageService objectStorageService;@GetMapping("/example")public void example() {log.info("objectStorageService: {}", objectStorageService);}
}
  1. 运行应用后一切正常,命令行输入: curl http://localhost:8080/example,日志打印:注入成功
objectStorageService: org.example.inject.condition.service.impl.AliyunObjectStorageServiceImpl@6f2aa58b
  1. 遗憾的是,@Resource注解也不支持变量赋值,只能通过硬编码的方式指定具体的实现类,因此不推荐使用。

3.4、@Primary解决方案

  1. @Primary 是一个 Spring 框架中的注解,用于解决多个 Bean 实例同一类型的自动装配问题。当一个接口或者类有多个实现时,Spring 在自动装配时可能会出现歧义,不知道选择哪个 Bean 注入。这时候,可以使用 @Primary 注解来指定首选的 Bean,这样在自动装配时就会选择这个首选的 Bean。

  2. DefaultObjectStorageServiceImpl设置为首选实现类,代码如下:

import org.springframework.context.annotation.Primary;@Slf4j
@Service
@Primary
public class DefaultObjectStorageServiceImpl implements ObjectStorageService {@Overridepublic String uploadObject(File file, String bucketName, String objectKey) {// 默认实现上传逻辑return "Default implementation: Upload successful";}@Overridepublic File downloadObject(String bucketName, String objectKey) {// 默认实现下载逻辑return new File("default-file.txt");}
}
  1. StorageController控制层恢复为最初形态,代码如下:
@Slf4j
@RestController
public class StorageController {@Autowiredprivate ObjectStorageService objectStorageService;@GetMapping("/example")public void example() {log.info("objectStorageService: {}", objectStorageService);}
}
  1. 运行应用,命令行输入: curl http://localhost:8080/example,日志打印:默认实现类注入成功

    objectStorageService: org.example.inject.condition.service.impl.DefaultObjectStorageServiceImpl@633df06
    
  2. 遗憾的是,@Primary注解也是只能通过硬编码的方式指定具体的实现类,因此不推荐使用。

3.5、@Conditional解决方案[推荐]

  1. @Conditional 注解是 Spring 框架提供的一种条件化装配的机制,它可以根据特定的条件来决定是否创建一个 Bean 实例。通过 @Conditional 注解,可以在 Spring 容器启动时根据一些条件来动态地确定是否创建某个 Bean,从而实现更灵活的 Bean 装配。

  2. 在 Spring 中,有一系列内置的条件注解,例如:

    • @ConditionalOnClass:当类路径中存在指定的类时,才创建该 Bean。
    • @ConditionalOnMissingClass:当类路径中不存在指定的类时,才创建该 Bean。
    • @ConditionalOnBean:当容器中存在指定的 Bean 时,才创建该 Bean。
    • @ConditionalOnMissingBean:当容器中不存在指定的 Bean 时,才创建该 Bean。
    • @ConditionalOnProperty:当指定的配置属性满足一定条件时,才创建该 Bean。
    • @ConditionalOnExpression:当指定的 SpEL 表达式为 true 时,才创建该 Bean。
  3. 我们希望达到的效果是通过application.propertiesapplication.yml配置文件的一个配置项就可以指定具体实现类,而非通过硬编码的形式来实现,所以我们将使用@ConditionalOnProperty配置属性条件注解实现。其余注解可参考:官网介绍

  4. 先看下@ConditionalOnProperty注解的几个入参介绍:

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Documented
    @Conditional({OnPropertyCondition.class})
    public @interface ConditionalOnProperty {/*** 配置文件中 key 的前缀,可与 value 或 name 组合使用。*/String prefix() default "";/*** 与 value 作用相同,但不能与 value 同时使用。*/String[] name() default {};/*** 与 value 或 name 组合使用,只有当 value 或 name 对应的值与 havingValue 的值相同时,注入生效。*/String havingValue() default "";/*** 当该属性为 true 时,配置文件中缺少对应的 value 或 name 的属性值,也会注入成功。*/boolean matchIfMissing() default false;
    }
    
  5. 接下来定义配置key,在application.propertiesapplication.yml配置文件新增如下内容:

    storage.provider=aliyun
    
  6. 在各个实现类中新增@ConditionalOnProperty注解,代码如下:

    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;@Slf4j
    @Service
    //@Primary
    @ConditionalOnProperty(prefix = "storage", name = "provider", havingValue = "default", matchIfMissing = true)
    public class DefaultObjectStorageServiceImpl implements ObjectStorageService {// 省略
    }@Slf4j
    @Service
    @ConditionalOnProperty(prefix = "storage", name = "provider", havingValue = "aliyun")
    public class AliyunObjectStorageServiceImpl implements ObjectStorageService {// 省略
    }@Slf4j
    @Service
    @ConditionalOnProperty(prefix = "storage", name = "provider", havingValue = "s3")
    public class S3ObjectStorageServiceImpl implements ObjectStorageService {// 省略
    }
    
  7. 运行应用,命令行输入: curl http://localhost:8080/example,日志打印:

    objectStorageService: org.example.inject.condition.service.impl.AliyunObjectStorageServiceImpl@3b46e282
    
  8. 如果在 application.propertiesapplication.yml 配置文件中没有配置 storage.provider 属性,则会注入 DefaultObjectStorageServiceImpl 实现类。这是因为 DefaultObjectStorageServiceImpl 实现类的 matchIfMissing = true 属性已经指定了。

  9. 上述注解的实现方式是配置在每个实现类中,这种方式过于分散。为了让开发人员更清晰地了解应用的注入关系,我们应该通过 @Configuration 整合所有实现类的配置。以下是新增的 WebConfiguration 配置类的代码:

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;/*** 自动装配类*/
    @Configuration
    public class WebConfiguration {@Bean@ConditionalOnProperty(prefix = "storage", name = "provider", havingValue = "default", matchIfMissing = true)public ObjectStorageService defaultObjectStorageServiceImpl() {return new DefaultObjectStorageServiceImpl();}@Bean@ConditionalOnProperty(prefix = "storage", name = "provider", havingValue = "aliyun")public ObjectStorageService aliyunObjectStorageServiceImpl() {return new AliyunObjectStorageServiceImpl();}@Bean@ConditionalOnProperty(prefix = "storage", name = "provider", havingValue = "s3")public ObjectStorageService s3ObjectStorageServiceImpl() {return new S3ObjectStorageServiceImpl();}
    }
    

    再将各个实现类中的@Service,@ConditionalOnProperty注解去掉,更改后代码如下:

    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;@Slf4j
    public class DefaultObjectStorageServiceImpl implements ObjectStorageService {// 省略
    }@Slf4j
    public class AliyunObjectStorageServiceImpl implements ObjectStorageService {// 省略
    }@Slf4j
    public class S3ObjectStorageServiceImpl implements ObjectStorageService {// 省略
    }
    

    运行应用,命令行输入: curl http://localhost:8080/example,日志打印:

    objectStorageService: org.example.inject.condition.service.impl.AliyunObjectStorageServiceImpl@3b46e282
    
  10. 通过 @ConditionalOnProperty 注解和 WebConfiguration 统一装配类,我们基本实现了可配置化注入实现类的方案,初步实现了我们的目标。

3.6、自定义@Conditional解决方案[强烈推荐]

在上面的示例中,我们是通过在配置文件中定义属性来决定实现类,这需要在配置文件中定义一份属性,并在各个 @ConditionalOnProperty 注解中配置 prefixname 属性。以前面的示例为例,就需要进行4次配置。然而,这种方式容易出错,特别是当服务有多个接口需要配置多个实现类时,需要配置更多的属性,增加了配置的复杂性和出错的可能性,如下图所示:

在这里插入图片描述

根据上图中的三个接口,需要配置三个配置项以及7次 @ConditionalOnProperty 注解;因此,我们需要采用一种简化的方式来减少配置,只需要在配置文件中配置一次即可,而无需更改@ConditionalOnProperty 注解。

  1. 要满足上述需求,首先需要重点关注配置文件中的属性。以上面的对象存储的情景举例,一个重要的配置项是storage.provider=aliyun。为了更通用地解决所有接口的配置需求,建议统一将配置项命名为接口的全限定名。这种做法不仅能够确保配置项的唯一性,同时也让人一目了然,清晰明了。以上面对象存储场景为例,修改后的配置如下所示:

    org.example.inject.condition.service.ObjectStorageService=aliyun
    
  2. 其次希望简化@ConditionalOnProperty注解的编写,不再需要指定prefix = "storage", name = "provider"等属性。而是根据注解所在位置自动分析当前返回值类的全限定名称,然后直接从配置文件中读取相应的配置项。示例如下:

    @Bean
    @ConditionalOnProperty(name = ObjectStorageService.class, matchIfMissing = true)
    public ObjectStorageService defaultObjectStorageServiceImpl() {return new DefaultObjectStorageServiceImpl();
    }@Bean
    @ConditionalOnProperty(name = ObjectStorageService.class, havingValue = "aliyun")
    public ObjectStorageService aliyunObjectStorageServiceImpl() {return new AliyunObjectStorageServiceImpl();
    }@Bean
    @ConditionalOnProperty(name = ObjectStorageService.class, havingValue = "s3")
    public ObjectStorageService s3ObjectStorageServiceImpl() {return new S3ObjectStorageServiceImpl();
    }
    

    可以观察到,除了需要配置havingValue属性外,其他配置项无需手动设置,使得配置变得十分简洁。

  3. 注意,目前Spring并未提供类似的能力来实现我们需要的条件判断,因此我们需要自定义条件注解。幸运的是,Spring 提供了条件接口,让我们可以自行创建自定义的条件类来实现所需的条件判断逻辑。首先,我们创建一个自定义条件类,它继承Condition接口,并编写自定义的条件判断逻辑。代码如下:

    import org.springframework.context.annotation.Condition;
    import org.springframework.context.annotation.ConditionContext;
    import org.springframework.core.type.AnnotatedTypeMetadata;
    import org.springframework.util.StringUtils;/*** 自定义的条件判断类,用于根据指定类名的配置值判断是否应用某个配置。*/
    public class ConditionalOnClassNameCustom implements Condition {/*** 判断是否满足条件。** @param context  条件上下文* @param metadata 注解元数据* @return 如果满足条件,则返回true;否则返回false*/@Overridepublic boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {// 获取ConditionalOnClassName注解的属性值Class<?>[] annotationValues = (Class<?>[]) metadata.getAnnotationAttributes(ConditionalOnClassName.class.getName()).get("name");String annotationClassName = annotationValues[0].getName(); // 获取类的全限定名String havingValue = (String) metadata.getAnnotationAttributes(ConditionalOnClassName.class.getName()).get("havingValue");boolean matchIfMissing = (boolean) metadata.getAnnotationAttributes(ConditionalOnClassName.class.getName()).get("matchIfMissing");// 获取配置项对应的配置值String propertyValue = context.getEnvironment().getProperty(annotationClassName);// 检查配置值是否符合预期if (StringUtils.hasText(propertyValue)) {return havingValue.equals(propertyValue);} else {return matchIfMissing;}}
    }
    
  4. 借助这个条件判断逻辑,我们接下来设计一个全新的条件配置注解:ConditionalOnClassName,它将使用前述的ConditionalOnClassNameCustom实现类。具体代码如下:

    import org.springframework.context.annotation.Conditional;import java.lang.annotation.*;/*** 定义一个自定义条件注解,用于根据指定类名的配置值判断是否应用某个配置。*/
    @Target({ ElementType.TYPE, ElementType.METHOD }) // 注解可以应用于类和方法
    @Retention(RetentionPolicy.RUNTIME) // 注解会在运行时保留
    @Documented // 注解会被包含在javadoc中
    @Conditional(ConditionalOnClassNameCustom.class) // 该注解条件受到 ConditionalOnClassNameCustom 类的限制
    public @interface ConditionalOnClassName {Class<?>[] value() default {}; // 作为 value 属性的别名,用于更简洁地指定需要检查的类Class<?>[] name(); // 需要检查的类的全限定名数组String havingValue() default "default"; // 期望的配置值,默认为 "default"boolean matchIfMissing() default false; // 如果配置值缺失是否匹配,默认为 false
    }
    
  5. 完成了上述准备工作后,接下来是验证新创建的注解。我们需要修改WebConfiguration配置类。代码如下:

    /*** 自动装配类*/
    @Configuration
    public class WebConfiguration {@Bean@ConditionalOnClassName(name = ObjectStorageService.class, matchIfMissing = true)public ObjectStorageService defaultObjectStorageServiceImpl() {return new DefaultObjectStorageServiceImpl();}@Bean@ConditionalOnClassName(name = ObjectStorageService.class, havingValue = "aliyun")public ObjectStorageService aliyunObjectStorageServiceImpl() {return new AliyunObjectStorageServiceImpl();}@Bean@ConditionalOnClassName(name = ObjectStorageService.class, havingValue = "s3")public ObjectStorageService s3ObjectStorageServiceImpl() {return new S3ObjectStorageServiceImpl();}
    }
    
  6. 接下来定义配置key,在application.propertiesapplication.yml配置文件新增如下内容:

    org.example.inject.condition.service.ObjectStorageService=aliyun
    
  7. 运行应用,命令行输入: curl http://localhost:8080/example,日志打印:

    objectStorageService: org.example.inject.condition.service.impl.AliyunObjectStorageServiceImpl@4cf4e0a
    

在这个示例中,我们利用自定义条件注解简化了@ConditionalOnProperty注解的配置,同时统一了配置文件属性命名,实现了一次配置多处使用。这种优化提高了配置的简洁性和可维护性,同时减少了配置的复杂度和错误可能性。

四、总结

本文通过自定义条件注解,简化了@ConditionalOnProperty注解的配置,同时统一了配置文件属性命名。这一优化方案提高了系统的可维护性和稳定性。以往的配置模式需要在不同的类或方法上重复配置属性的前缀和名称,容易出错且繁琐。通过优化后的方案,只需在配置文件中一次性配置,即可在多处重复使用,简化了配置过程。这种优化提高了开发效率,降低了配置错误的风险,尤其适用于大型项目。

总的来说,通过自定义条件注解来简化配置,统一配置文件属性命名,是一种非常实用的优化方案。它不仅提高了系统的可维护性和稳定性,还能够提升开发效率,减少配置错误的可能性,是服务开发中值得推广的实践之一。

五、相关资料

  • Java SPI解读:揭秘服务提供接口的设计与应用
  • Spring条件注解官网介绍
  • 产品SDK化转型:标准化与机构个性化定制解决方案

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ABB 任务 模块 程序

1&#xff0c;任务由模块组成 &#xff0c; 2&#xff0c;模块分为程序模块和系统模块 3&#xff0c;可以通过新建程序模块和删除程序模块 4.可以在程序模块中构建程序 5&#xff0c;系统模块不能够被删除 6&#xff0c;main 程序主要体现在自动运行中

【Unity AR开发插件】四、制作热更数据-AR图片识别场景

专栏 本专栏将介绍如何使用这个支持热更的AR开发插件&#xff0c;快速地开发AR应用。 链接&#xff1a; Unity开发AR系列 插件简介 通过热更技术实现动态地加载AR场景&#xff0c;简化了AR开发流程&#xff0c;让用户可更多地关注Unity场景内容的制作。 “EnvInstaller…”支…

鸿蒙开发配置官方地图

一共需要配置 p12 p7b cer csr 四个文件 p12文件配置 注意创建文件名必须是.p12 到AGC创建项目 AppGallery Connect 添加自己的项目名称 我没有开启 暂时不需要 看个人需求 下载刚创建的cer证书 回到我的项目中 点击刚创建的项目 点击这里 四个文件齐全了 "metadata&qu…

Python爬取B站视频:封装一下

&#x1f4da;博客主页&#xff1a;knighthood2001 ✨公众号&#xff1a;认知up吧 &#xff08;目前正在带领大家一起提升认知&#xff0c;感兴趣可以来围观一下&#xff09; &#x1f383;知识星球&#xff1a;【认知up吧|成长|副业】介绍 ❤️如遇文章付费&#xff0c;可先看…

跨境电商赛道,云手机到底能不能化繁为简?

当下国内电商背景&#xff1a; 从零售额的数据来看&#xff1a;随着互联网的普及和消费者购物习惯的改变&#xff0c;国内电商市场规模持续扩大。据相关数据显示&#xff0c;网络消费亮点纷呈&#xff0c;一季度全国网上零售额达到了3.3万亿元&#xff0c;同比增长12.4%。这表…

本地centos7+docker+ollama+gpu部署

1、一台有 NVIDIA GPU 驱动的机器 2、Docker CE安装 # 删除旧版本的 Docker&#xff08;如果存在&#xff09; sudo yum remove -y docker docker-common docker-selinux docker-engine # 安装必要的软件包&#xff1a; sudo yum install -y yum-utils device-mapper-persiste…

gpt-4o继续迭代考场安排程序 一键生成考场清单

接上两篇gpt-4o考场安排-CSDN博客&#xff0c;考场分层次安排&#xff0c;最终exe版-CSDN博客 当然你也可以只看这一篇。 今天又添加了以下功能&#xff0c;程序见后。 1、自动分页&#xff0c;每个考场打印一页 2、添加了打印试场单页眉 3、添加了页脚 第X页&#xff0c;…

基于 Java 的浏览器——JxBrowser使用分享

软件介绍 JxBrowser 是一个基于 Java 的浏览器&#xff0c;它使用 Chromium 引擎来提供高性能的网页渲染和丰富的功能。它支持多种 GUI 框架&#xff0c;如 Swing、JavaFX 和 SWT&#xff0c;使得在 Java 应用程序中嵌入浏览器组件变得简单。 JxBrowser 是一个适用于多种用途…

一维前缀和[模版]

题目链接 题目: 分析: 因为要求数组中连续区间的和, 可以使用前缀和算法注意:下标是从1开始算起的, 真正下标0的位置是0第一步: 预处理出来一个前缀和数组dp dp[i] 表示: 表示[1,i] 区间所有元素的和dp[i] dp[i-1] arr[i]例如示例一中: dp数组为{1,3,7}第二步: 使用前缀数…

CSS基础(第二天)

Emmet语法 快速生成HTML结构语法 1. 生成标签 直接输入标签名 按tab键即可 比如 div 然后tab 键&#xff0c; 就可以生成 <div></div> 2. 如果想要生成多个相同标签 加上 * 就可以了 比如 div*3 就可以快速生成3个div 3. 如果有父子级关系的标签&#xff0c;可以…

Spark介绍

Spark简介 Spark,是一种通用的大数据计算框架,正如传统大数据技术Hadoop的MapReduce、Hive引擎,以及Storm流式实时计算引擎等. Spark是加州大学伯克利分校AMP实验室(Algorithms Machines and People Lab)开发的通用内存并行计算框架,用于构建大型的、低延迟的数据分析应用程序…

网站流量统计分析

网站流量统计分析&#xff1a;洞悉用户行为的关键 在当今数字化时代&#xff0c;网站流量统计分析已经成为了企业成功的关键因素之一。通过深入了解用户的行为和偏好&#xff0c;企业可以更好地调整其营销策略、优化用户体验以及提高转化率。本文将探讨网站流量统计分析的重要性…

13.js对象

定义 一种复杂数据类型&#xff0c;是无序的&#xff08;不保留键的插入顺序&#xff09;&#xff0c;以键值对&#xff08;{key:value})形式存放的数据集合 对象的创建 &#xff08;1&#xff09;字面量创建 var 对象名{ } &#xff08;2&#xff09;内部构造函数创建 v…

模板编译之入口分析

Vue 是一个渐进式 JavaScript 框架&#xff0c;提供了简单易用的模板语法&#xff0c;帮助开发者以声明式的方式构建用户界面。Vue 的模板编译原理是其核心之一&#xff0c;它将模板字符串编译成渲染函数&#xff0c;并在运行时高效地更新 DOM。本文将深入探讨 Vue 模板编译的原…

Logstash笔记

目录​​​​​​​ 一、简介 二、单个输入和输出插件 三、多个输入和输出插件 四、pipeline结构 五、队列和数据弹性 六、内存队列 七、持久化队列 八、死信队列 (DLQ) 九、输入插件 1)、beats 2)、dead_letter_queue 3)、elasticsearch 4)、file 5)、redis 十、…

【研发日记】嵌入式处理器技能解锁(一)——多任务异步执行调度的三种方法

文章目录 前言 Timer中断调度 Event中断调度 StateFlow调度 分析和应用 总结 参考资料 前言 近期在一些嵌入式系统开发项目中&#xff0c;在使用嵌入式处理器时&#xff0c;遇到了挺多费时费力的事情。所以利用晚上和周末时间&#xff0c;在这些方面深入研究了一下&…

2024年学浪视频怎么下载到手机相册

随着2024年的到来&#xff0c;学浪平台继续为广大学习者提供优质的在线教育资源。然而&#xff0c;如何将这些宝贵的视频内容下载到手机相册&#xff0c;方便随时离线观看呢&#xff1f;无论您是想在旅途中学习&#xff0c;还是希望在没有网络的情况下复习课程&#xff0c;本文…

selenium安装出错

selenium安装步骤&#xff08;法1&#xff09;&#xff1a; 安装失败法1 第一次实验&#xff0c;失败 又试了一次&#xff0c;失败 安装法2-失败&#xff1a; ERROR: Could not install packages due to an EnvironmentError: [WinError 5] 拒绝访问。: c:\\programdata\\a…

YoloV9实战与改进——专栏目录

摘要 &#x1f525;&#x1f680;本专栏教你如何嗨翻Yolov9&#xff01;&#x1f680;&#x1f525; &#x1f680;炸裂升级&#xff1a;嗨&#xff0c;小伙伴们&#xff01;这里有一波Yolov9的升级大招&#xff0c;带你领略最新论文的精华&#xff01;&#x1f4a5; 什么注意…