自定义Spring Boot Starter(官网文档解读)

摘要

本文将详细介绍自定义 Spring Boot Starter 的完整过程。要构建自定义 Starter,首先需掌握 Spring Boot 中 @Auto-configuration 以及相关注解的工作原理,同时了解 Spring Boot 提供的一系列条件注解。在具备这些知识基础后,再按照特定步骤完成自定义 Starter 的开发,最后对其进行测试。接下来,让我们基于官网文档,深入学习具体内容。

@AutoConfiguration

@AutoConfiguration 是 Spring Boot 3.0 引入的一个注解,用于标记自动配置类。自动配置类的主要作用是根据类路径中的依赖、配置属性等条件,自动为应用程序配置所需的 Bean。它替代了早期版本中的 @Configuration@EnableAutoConfiguration 组合,让自动配置类的定义更加清晰和简洁。

代码示例

import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;

@AutoConfiguration
public class MyAutoConfiguration {

    @Bean
    public MyService myService() {
        return new MyService();
    }
}

在这个示例中,MyAutoConfiguration 类被标记为自动配置类,其中定义了一个 MyService 的 Bean。当 Spring Boot 应用启动时,会自动扫描并加载这个自动配置类,创建 MyService 的实例。

Spring Boot 自动配置类的配置

加载

  • 我们需要在 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 配置文件中,加入我们的自动配置类,每个配置类占一行,从而让Spring Boot 加载自动配置类。
  • 可以使用 # 字符在该文件中添加注释。

  • 若自动配置类不是顶级类,其类名应使用 $ 分隔其与包含类,如 com.example.Outer$NestedAutoConfiguration

  • 自动配置类必须通过在 imports 文件中列出类名来加载。要确保它们定义在特定的包空间中,且永远不会成为组件扫描的目标。此外,自动配置类不应使用组件扫描来查找其他组件,而应使用特定的 @Import 注解。

顺序设置

  • @AutoConfiguration 注解属性:若配置需要按特定顺序应用,可使用 @AutoConfiguration 注解的 beforebeforeNameafterafterName 属性,或使用专门的 @AutoConfigureBefore@AutoConfigureAfter 注解。例如,若提供特定的 Web 配置,可能需要在 WebMvcAutoConfiguration 之后应用该配置类。

  • @AutoConfigureOrder 注解:若要对彼此无直接关联的某些自动配置类进行排序,可使用 @AutoConfigureOrder 注解。该注解与常规的 @Order 注解语义相同,但专门用于自动配置类。

  • 与标准的 @Configuration 类一样,自动配置类的应用顺序仅影响其定义 Bean 的顺序,而后续创建这些 Bean 的顺序不受影响,创建顺序由每个 Bean 的依赖关系和任何 @DependsOn 关系决定。

  • 带顺序的自动配置代码示例

    import org.springframework.boot.autoconfigure.AutoConfiguration;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    import org.springframework.context.annotation.Bean;

    // 第一个自动配置类,创建 ServiceOne
    @AutoConfiguration
    public class ServiceOneAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public ServiceOne serviceOne() {
    return new ServiceOne();
    }
    }

    // 第二个自动配置类,使用 @AutoConfiguration 的 after 属性,在 ServiceOneAutoConfiguration 之后配置
    @AutoConfiguration(after = ServiceOneAutoConfiguration.class)
    public class ServiceTwoAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public ServiceTwo serviceTwo() {
    return new ServiceTwo();
    }
    }

    // 第三个自动配置类,使用 @AutoConfigureOrder 注解进行排序
    import org.springframework.boot.autoconfigure.AutoConfigureOrder;
    import org.springframework.core.Ordered;

    @AutoConfigureOrder(Ordered.LOWEST_PRECEDENCE)
    @AutoConfiguration
    public class ServiceThreeAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public ServiceThree serviceThree() {
    return new ServiceThree();
    }
    }

弃用与替换

  • 在开发过程中,有时需要对自动配置类进行弃用并提供替代方案,例如更改自动配置类所在的包名。

  • 由于自动配置类可能会在 before/after 排序和排除项中被引用,因此需要额外创建一个文件来告知 Spring Boot 如何处理替换操作。具体做法是创建一个名为 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.replacements 的文件,在其中指明旧类和新类之间的映射关系。替换示例:

复制代码
person.wend.springbootlearnexample.config.MyAutoConfiguration=person.wend.springbootlearnexample.config.auto.MyAutoConfiguration
  • 同时,AutoConfiguration.imports 文件也需要更新,使其仅引用替换后的新类,以确保 Spring Boot 在进行自动配置时能正确加载最新的配置类。

条件注解

在进行自动配置时,SpringBoot 包含六类@Conditional注解。可通过对 @Configuration 类或单个 @Bean 方法添加这些注解,在自定义代码中复用。这些注解包括:

  1. 类条件注解(Class Conditions)

  2. Bean 条件注解(Bean Conditions)

  3. 属性条件注解(Property Conditions)

  4. 资源条件注解(Resource Conditions)

  5. Web 应用条件注解(Web Application Conditions)

  6. SpEL 表达式条件注解(SpEL Expression Conditions)

类条件注解

  • @ConditionalOnClass:@ConditionalOnClass 注解用于指定只有当类路径中存在指定的类时,被注解的配置类或 @Bean 方法才会生效。这在需要根据特定依赖是否存在来决定是否进行配置时非常有用。例如,当项目引入了某个库时,才加载与之相关的配置。

  • @ConditionalOnMissingClass:@ConditionalOnMissingClass 注解与 @ConditionalOnClass 注解相反,它用于指定只有当类路径中不存在指定的类时,被注解的配置类或 @Bean 方法才会生效。这在需要避免某些配置在特定类存在时生效的场景中非常有用。

  • 示例代码

    import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    // 模拟某个库中的类
    class SomeLibraryClass {}

    @Configuration
    @ConditionalOnClass(SomeLibraryClass.class)
    public class MyConfig {

      @Bean
      public MyService myService() {
          return new MyService();
      }
    

    }

    class MyService {}

  • @ConditionalOnClass@ConditionalOnMissingClass 注解通过检查类路径中特定类的存在与否,为 Spring Boot 应用的配置提供了灵活的条件加载机制,有助于根据不同的依赖环境进行动态配置。

Bean 条件注解

  • @ConditionalOnBean@ConditionalOnMissingBean 注解可根据特定 Bean 是否存在来决定是否包含某个 Bean。

  • 使用 value 属性按类型指定 Bean

  • 使用 name 属性按名称指定 Bean

  • search 属性可用于限制在搜索 Bean 时应考虑的 ApplicationContext 层次结构

  • 示例代码

    import org.springframework.boot.autoconfigure.AutoConfiguration;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    import org.springframework.context.annotation.Bean;

    @AutoConfiguration
    public class MyAutoConfiguration {

      @Bean
      @ConditionalOnMissingBean
      public SomeService someService() {
          return new SomeService();
      }
    

    }

  • 在上述示例中,如果 ApplicationContext 中尚未包含 SomeService 类型的 Bean,那么 someService Bean 将会被创建。

属性条件注解

  • @ConditionalOnProperty 注解可根据 Spring 环境属性来决定是否包含配置。使用 prefixname 属性指定要检查的属性。默认情况下,任何存在且不等于 false 的属性都会匹配。还可以使用 havingValuematchIfMissing 属性进行更高级的检查。若 name 属性中指定了多个名称,则所有属性都必须通过测试条件才会匹配。

  • 代码示例

    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    @Configuration
    public class PropertyConfig {

      @Bean
      @ConditionalOnProperty(prefix = "myapp", name = "enabled", havingValue = "true")
      public MyService myService() {
          return new MyService();
      }
    

    }

    class MyService {}

  • application.properties 中配置 myapp.enabled=true 时,myService Bean 才会被创建。

资源条件注解

  • @ConditionalOnResource 注解只有在特定资源存在时才会包含配置。可以使用 Spring 常用约定指定资源,如 file:/home/user/test.dat

    import org.springframework.boot.autoconfigure.condition.ConditionalOnResource;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    @Configuration
    public class ResourceConfig {

      @Bean
      @ConditionalOnResource(resources = "classpath:test.properties")
      public ResourceService resourceService() {
          return new ResourceService();
      }
    

    }

    class ResourceService {}

  • 当类路径下存在 test.properties 文件时,resourceService Bean 才会被创建。

Web 应用条件注解

  • @ConditionalOnWebApplication@ConditionalOnNotWebApplication 注解根据应用是否为 Web 应用来决定是否包含配置。基于 Servlet 的 Web 应用是指使用 Spring WebApplicationContext、定义了会话作用域或具有 ConfigurableWebEnvironment 的应用;响应式 Web 应用是指使用 ReactiveWebApplicationContext 或具有 ConfigurableReactiveWebEnvironment 的应用。

  • @ConditionalOnWarDeployment@ConditionalOnNotWarDeployment 注解根据应用是否为部署到 Servlet 容器的传统 WAR 应用来决定是否包含配置。对于使用嵌入式 Web 服务器运行的应用,此条件不匹配。

    import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    @Configuration
    public class WebAppConfig {

      @Bean
      @ConditionalOnWebApplication
      public WebService webService() {
          return new WebService();
      }
    

    }

    class WebService {}

  • 当应用是 Web 应用时,webService Bean 才会被创建。

SpEL 表达式条件注解

  • @ConditionalOnExpression 注解根据 SpEL 表达式的结果来决定是否包含配置。但在表达式中引用 Bean 会导致该 Bean 在上下文刷新处理的早期初始化,从而使该 Bean 无法进行后处理(如配置属性绑定),其状态可能不完整。

    import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    @Configuration
    public class SpELConfig {

      @Bean
      @ConditionalOnExpression("#{systemProperties['java.version'].startsWith('17')}")
      public Java17Service java17Service() {
          return new Java17Service();
      }
    

    }

    class Java17Service {}

  • 当 Java 版本以 17 开头时,java17Service Bean 才会被创建。

测试自动配置

自动配置受用户配置(如@Bean定义和Environment自定义)、条件评估(特定库是否存在)等因素影响。每个测试都应创建一个定义良好的ApplicationContextApplicationContextRunner是实现此目标的好方法。

ApplicationContextRunner

  • ApplicationContextRunner通常被定义为测试类的一个字段,用于收集基本的通用配置。以下示例确保MyServiceAutoConfiguration始终调用:

      private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
      	.withConfiguration(AutoConfigurations.of(MyServiceAutoConfiguration.class));
    
  • 若有多个自动配置,无需排序,调用顺序与运行应用时相同。

  • 每个测试可用运行器表示特定用例,如调用用户配置(UserConfiguration)并检查自动配置是否正确退出,@Test方法中使用withUserConfigurationrun回调上下文结合AssertJ进行断言。

    @Test
    void defaultServiceBacksOff() {
    this.contextRunner.withUserConfiguration(UserConfiguration.class).run((context) -> {
    assertThat(context).hasSingleBean(MyService.class);
    assertThat(context).getBean("myCustomService").isSameAs(context.getBean(MyService.class));
    });
    }

      @Configuration(proxyBeanMethods = false)
      static class UserConfiguration {
    
      	@Bean
      	MyService myCustomService() {
      		return new MyService("mine");
      	}
    
      }
    
  • 可轻松自定义Environment,如withPropertyValues方法设置属性值并进行断言。

    @Test
    void serviceNameCanBeConfigured() {
    this.contextRunner.withPropertyValues("user.name=test123").run((context) -> {
    assertThat(context).hasSingleBean(MyService.class);
    assertThat(context.getBean(MyService.class).getName()).isEqualTo("test123");
    });
    }

  • 运行器可用于显示ConditionEvaluationReport,通过ConditionEvaluationReportLoggingListener设置日志级别打印报告。

    class MyConditionEvaluationReportingTests {

      @Test
      void autoConfigTest() {
      	new ApplicationContextRunner()
      		.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
      		.run((context) -> {
      			// Test something...
      		});
      }
    

    }

模拟 Web 环境

若测试仅在 servlet 或反应式 Web 应用程序上下文中运行的自动配置,可分别使用WebApplicationContextRunnerReactiveWebApplicationContextRunner

  • WebApplicationContextRunner代码示例:

    import org.junit.jupiter.api.Test;
    import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
    import org.springframework.context.ApplicationContext;
    import static org.assertj.core.api.Assertions.assertThat;
    import reactor.test.StepVerifier;

    public class ReactiveAutoConfigurationTest {

      private final ReactiveWebApplicationContextRunner reactiveWebContextRunner = new ReactiveWebApplicationContextRunner()
             .withConfiguration(AutoConfigurations.of(MyReactiveAutoConfiguration.class));
    
      @Test
      void testReactiveAutoConfiguration() {
          reactiveWebContextRunner.run((ApplicationContext context) -> {
              // 断言上下文中存在 MyReactiveService 的 Bean
              assertThat(context).hasSingleBean(MyReactiveService.class);
              MyReactiveService myReactiveService = context.getBean(MyReactiveService.class);
              // 调用反应式服务类的方法并使用 StepVerifier 进行断言
              StepVerifier.create(myReactiveService.sayHello())
                     .expectNext("Hello from MyReactiveService in Reactive context")
                     .verifyComplete();
          });
      }
    

    }

  • ReactiveWebApplicationContextRunner代码示例:

    import org.junit.jupiter.api.Test;
    import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
    import org.springframework.context.ApplicationContext;
    import static org.assertj.core.api.Assertions.assertThat;
    import reactor.test.StepVerifier;

    public class ReactiveAutoConfigurationTest {

      private final ReactiveWebApplicationContextRunner reactiveWebContextRunner = new ReactiveWebApplicationContextRunner()
             .withConfiguration(AutoConfigurations.of(MyReactiveAutoConfiguration.class));
    
      @Test
      void testReactiveAutoConfiguration() {
          reactiveWebContextRunner.run((ApplicationContext context) -> {
              // 断言上下文中存在 MyReactiveService 的 Bean
              assertThat(context).hasSingleBean(MyReactiveService.class);
              MyReactiveService myReactiveService = context.getBean(MyReactiveService.class);
              // 调用反应式服务类的方法并使用 StepVerifier 进行断言
              StepVerifier.create(myReactiveService.sayHello())
                     .expectNext("Hello from MyReactiveService in Reactive context")
                     .verifyComplete();
          });
      }
    

    }

覆盖类路径

使用FilteredClassLoader可测试运行时特定类和 / 或包不存在时的情况,如通过withClassLoader方法设置类加载器并断言自动配置是否正确禁用。

    @Test
	void serviceIsIgnoredIfLibraryIsNotPresent() {
		this.contextRunner.withClassLoader(new FilteredClassLoader(MyService.class))
			.run((context) -> assertThat(context).doesNotHaveBean("myService"));
	}

创建自定义Starter的步骤

典型的 Spring Boot Starter 包含用于自动配置和定制特定技术(以 "acme" 为例)基础设施的代码。为实现易于扩展,会在专用命名空间中向环境暴露多个配置键,并且提供单一的 "starter" 依赖,方便用户快速上手。

  • acme:"acme" 是一个占位符,用来代表任意一种技术、框架、库或者服务。"acme" 代表开发者想要自动配置和定制其基础设施的目标技术。例如,这个技术可能是 Redis、MongoDB、RabbitMQ 等,"acme" 在这里可以理解成一个泛指的、可以被替换为具体技术的抽象概念。

自定义 Starter 的组成

  • 自动配置模块(autoconfigure module):包含针对 "acme" 的自动配置代码。

  • Starter 模块:依赖于自动配置模块,同时包含 "acme" 以及其他常用的额外依赖。简而言之,添加该 Starter 应能提供使用相应库所需的一切。

模块分离的考量

  • 分离的优势:将自动配置和 Starter 分成两个模块并非必要,但当 "acme" 存在多种变体、选项或可选功能时,进行分离是更好的选择。这样可以明确表明某些功能是可选的,并且能够定制一个对这些可选依赖有特定选择的 Starter。同时,其他人可以仅依赖自动配置模块,按照自己的需求定制 Starter。

  • 合并的情况:如果自动配置相对简单,且没有可选功能,将两个模块合并到 Starter 中也是可行的。

Starter 的命名规则

  • 官方Starter:Spring-Boot 官方的Starter 都统一以 spring-boot-starter-* 格式命名,其中*是特定类型的应用程序。此命名结构旨在帮助您在需要查找启动器时使用。许多 IDE 中的 Maven 集成允许您按名称搜索依赖项。例如,安装了适当的 Eclipse 或 Spring Tools 插件后,您可以按ctrl-spacePOM 编辑器并键入"spring-boot-starter"以获取完整列表。

  • 三方库或自建Starter:第三方启动器不应以 spring-boot开头,因为它是为官方 Spring Boot 工件保留的。通常正确的写法是,名为thirdpartyproject的第三方启动器项目,通常其定义的Starter应命名为thirdpartyproject-spring-boot-starter

配置键

命名空间使用规范

如果自定义的 Starter 提供配置键,要使用独特的命名空间,切勿将配置键置于 Spring Boot 已使用的命名空间(如 servermanagementspring 等)中。因为后续 Spring Boot 可能会修改这些命名空间,从而导致模块出现问题。一般来说,应使用自己拥有的命名空间作为配置键的前缀,例如 acme

  • 配置示例
复制代码
acme.my-project.person.first-name=John
acme.my-project.person.last-name=Smith
acme.my-project.person.version=1.0.0
配置键文档化
  • 使用 @ConfigurationProperties 注解类:为保证配置键有文档记录,需为 @ConfigurationProperties 注解类中的每个属性添加字段 Javadoc。

    @ConfigurationProperties("acme")
    public class AcmeProperties {

      /**
       * Whether to check the location of acme resources.
       */
      private boolean checkLocation = true;
    
      /**
       * Timeout for establishing a connection to the acme server.
       */
      private Duration loginTimeout = Duration.ofSeconds(3);
    
      // getters/setters ...
    
      public boolean isCheckLocation() {
      	return this.checkLocation;
      }
    
      public void setCheckLocation(boolean checkLocation) {
      	this.checkLocation = checkLocation;
      }
    
      public Duration getLoginTimeout() {
      	return this.loginTimeout;
      }
    
      public void setLoginTimeout(Duration loginTimeout) {
      	this.loginTimeout = loginTimeout;
      }
    

    }

  • 配置示例展示了 AcmeProperties 类,包含 checkLocationloginTimeout 等属性,并为每个属性添加了说明性的 Javadoc。

  • 记录类的处理方式:若使用 @ConfigurationProperties 注解记录类(record class),需通过类级别的 Javadoc 标签 @param 来提供记录组件的描述,因为记录类没有显式的实例字段来放置常规的字段级 Javadoc。

描述规范

为确保描述的一致性,需遵循以下规则:

  • 描述开头不要使用 "The" 或 "A"。

  • 对于布尔类型,描述以 "Whether" 或 "Enable" 开头。

  • 对于基于集合的类型,描述以 "Comma - separated list" 开头。

  • 使用 Duration 类型而非 long 类型,若默认单位不是毫秒,需描述默认单位,如 "If a duration suffix is not specified, seconds will be used"。

  • 除非默认值需在运行时确定,否则描述中不要提供默认值。

元数据生成

要触发元数据生成,以便 IDE 能为配置键提供辅助功能。可以查看生成的元数据文件 META - INF/spring-configuration-metadata.json,确保配置键有恰当的文档记录。在兼容的 IDE 中使用自定义的 Starter 也是验证元数据质量的好方法。

"autoconfigure" 模块

"autoconfigure" 模块包含了使用某个库所需的一切必要内容。它可能包含配置键定义(例如使用 @ConfigurationProperties 注解),以及可用于进一步自定义组件初始化方式的回调接口。

  • 依赖设置:应该将对该库的依赖标记为可选的。这样做可以更轻松地将 "autoconfigure" 模块包含到项目中。因为当依赖被标记为可选时,库本身不会被提供,默认情况下 Spring Boot 会跳过对相关自动配置的加载。

    • 在 Maven 项目中,通过在 pom.xml 文件里的 <dependency> 标签中添加 <optional>true</optional> 来将依赖标记为可选。

      <dependencies> <dependency> <groupId>com.example</groupId> <artifactId>your-library</artifactId> <version>1.0.0</version> <optional>true</optional> </dependency> </dependencies>
  • 自动配置元数据文件:Spring Boot 使用注解处理器将自动配置的条件收集到一个元数据文件(META - INF/spring-autoconfigure-metadata.properties)中。如果该文件存在,Spring Boot 会使用它来提前过滤掉不匹配的自动配置,从而提高应用的启动时间。

  • Maven 构建配置:在使用 Maven 进行项目构建时,需要将编译器插件(版本 3.12.0 或更高)配置为把 spring-boot-autoconfigure-processor 添加到注解处理器路径中。

    <project> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <annotationProcessorPaths> <path> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure-processor</artifactId> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </build> </project>

Starter 模块的本质与作用

Starter 本质上是一个空的 JAR 文件,其主要作用是提供使用相关库所需的必要依赖,可被视为关于启动使用该库所需依赖的一种推荐组合。

  • spring-boot-starter 示例:
  • Starter 依赖添加原则:

    • 避免主观臆断:当你开发一个 Spring Boot Starter 时,不要想当然地认为引入这个 Starter 的项目是什么样的情况。比如说,不能觉得所有用你这个 Starter 的项目都有某些特定的配置或者已经引入了某些其他库。如果这个 Starter 所自动配置的库在正常使用的时候还需要其他的 Starter 配合,那你就得把这些额外需要的 Starter 清楚地告诉别人。

    • 合理选择默认依赖:要是你要自动配置的库有很多可以选择的依赖,这时候选一套合适的默认依赖就挺难办的。你得保证 Starter 里只放那些在大家平时用这个库的时候一定会用到的依赖,那些可有可无的依赖就别放进去了。

  • Starter 依赖要求

    • Starter 模块必须直接或间接引用 Spring Boot 核心 Starter(spring-boot- starter)。若项目仅使用自定义 Starter 创建,由于核心 Starter 的存在,Spring Boot 的核心功能仍能正常使用。如果自定义 Starter 依赖于其他已包含核心 Starter 的 Starter,则无需再额外添加核心 Starter 依赖。

参考文献

Creating Your Own Auto-configuration :: Spring Boot

相关推荐
m0_7263658313 分钟前
某宝同款百度盘不限速后台系统源码_卡密系统_Java账号管理系统部署方案
java·开发语言
_nut_2 小时前
手撕跳表/数据结构
java·开发语言·数据结构
没明白白2 小时前
插入排序:一种简单而直观的排序算法
java·算法·排序算法
小猪咪piggy2 小时前
【数据结构】(12) 反射、枚举、lambda 表达式
java·开发语言·数据结构
web147862107232 小时前
数据库系统架构与DBMS功能探微:现代信息时代数据管理的关键
java·开发语言·数据库
wolf犭良2 小时前
21.《SpringBoot 异步编程@Async与CompletableFuture》
java·数据库·spring
程序员南飞2 小时前
算法-数据结构-图的构建(邻接矩阵表示)
java·数据结构·算法·职场和发展
舰长1152 小时前
安装可视化jar包部署平台JarManage
java·jar
天上掉下来个程小白3 小时前
登录-10.Filter-登录校验过滤器
spring boot·后端·spring·filter·登录校验
抹除不掉的轻狂丶3 小时前
JVM生产环境问题定位与解决实战(三):揭秘Java飞行记录器(JFR)的强大功能
java·开发语言·jvm