SpringBoot 自动装配原理 & 自定义一个 starter

目录

  • [1、pom.xml 文件](#1、pom.xml 文件)
    • [1.1、parent 模块](#1.1、parent 模块)
      • 1.1.1、资源文件
        • [1.1.1.1、`resources` 标签说明](#1.1.1.1、resources 标签说明)
        • [1.1.1.2、从 Maven 视角:资源处理全流程​](#1.1.1.2、从 Maven 视角:资源处理全流程)
      • 1.1.2、插件
    • [1.2、dependencies 模块](#1.2、dependencies 模块)
  • 2、启动器
  • 3、主程序
    • [3.1、`@SpringBootApplication` 注解](#3.1、@SpringBootApplication 注解)
    • [3.2、`@SpringBootConfiguration` 注解](#3.2、@SpringBootConfiguration 注解)
      • [3.2.1、`@Configuration` 和 `@Component` 区别](#3.2.1、@Configuration@Component 区别)
    • [3.3、`@EnableAutoConfiguration` 注解](#3.3、@EnableAutoConfiguration 注解)
      • [3.3.1、`@AutoConfigurationPackage` 注解](#3.3.1、@AutoConfigurationPackage 注解)
        • [3.3.1.1、`AutoConfigurationPackages.Registrar#registerBeanDefinitions()` 方法:](#registerBeanDefinitions()` 方法:)
      • [3.3.2、`@Import` 注解](#3.3.2、@Import 注解)
      • 3.3.3、`AutoConfigurationImportSelector`
    • 3.4、自动装配原理
  • [4、自定义一个 starter](#4、自定义一个 starter)
    • 4.1、基础版
      • [4.1.1、创建一个 SpringBoot 工程](#4.1.1、创建一个 SpringBoot 工程)
      • [4.1.2、新建一个 Properteis 类](#4.1.2、新建一个 Properteis 类)
      • [4.1.3、新建一个自动配置类,关联 Properties 类](#4.1.3、新建一个自动配置类,关联 Properties 类)
      • 4.1.4、添加一个对外服务接口
      • 4.1.5、注册自动配置类
      • [4.1.6、`mvn install`](#4.1.6、mvn install)
      • [4.1.7、引用 starter](#4.1.7、引用 starter)
        • [4.1.7.1、pom.xml 中引入 starter](#4.1.7.1、pom.xml 中引入 starter)
        • [4.1.7.2、使用 starter](#4.1.7.2、使用 starter)
    • 4.2、引入条件装配
      • [4.2.1、修改配置类 `ReadingAutoConfiguration`](#4.2.1、修改配置类 ReadingAutoConfiguration)
      • [4.2.2、修改使用 starter](#4.2.2、修改使用 starter)
    • [4.3、重写 starter 的默认配置](#4.3、重写 starter 的默认配置)
    • [4.4、重写 starter 默认实现](#4.4、重写 starter 默认实现)
    • [4.5、实现一个自己的 `@EnableXXX`](#4.5、实现一个自己的 @EnableXXX)
      • [4.5.1、在 starter 中添加一个 selector](#4.5.1、在 starter 中添加一个 selector)
      • [4.5.2、在 starter 中删除 `resources/META-INF/spring.factories` 文件](#4.5.2、在 starter 中删除 resources/META-INF/spring.factories 文件)
      • [4.5.3、在 starter 中添加一个注解 `@EnableReading`](#4.5.3、在 starter 中添加一个注解 @EnableReading)
      • 4.5.4、在当前项目中做出修改

1、pom.xml 文件

创建一个 SpringBoot 工程,查看 pom.xml 文件,当前工程是依赖 parent 模块。如下图:

1.1、parent 模块

parent 模块主要加载配置文件【yml】和插件。如下图:

parent 模块又依赖 dependencies 模块

1.1.1、资源文件

src/main/resources 目录下的 yml/yaml/properties 文件进行占位符替换【第一个 resource:启用过滤】,其它文件不进行处理【第二个 resource:防止其它文件误被修改】,最终都会打包

1.1.1.1、resources 标签说明

Maven 处理 <resources> 时采用​​顺序合并策略 ​​,每个 <resource> 块定义不同的过滤规则:

  • 第一个资源块(精准过滤)
    • 处理范围:仅 application*.yml/yaml/properties
    • 操作:开启过滤(filtering=true
    • 目标:动态替换这些配置文件中的 @variable@ 占位符
  • 第二个资源块(全量保护)
    • 处理范围:排除上述配置文件后的​​其他所有资源​
    • 操作:默认关闭过滤(filtering=false
    • 目标:原样复制静态资源(如图片、HTML、XML 等)

处理流程如下图:

1.1.1.2、从 Maven 视角:资源处理全流程​

如下图:

关键结论:

  1. 所有资源文件都会被打包​​,无论是否经过过滤
  2. 过滤(filtering)只是对文件内容的处理,与是否打包无关
  3. 两个资源块共同作用确保:
    • 配置文件:动态内容替换
    • 其他资源:保持原始状态

1.1.2、插件

1.2、dependencies 模块

dependencies 模块主要管理了各种依赖的版本。所以,在 SpringBoot 工程中导入依赖时,不需要指定版本,已有默认的版本。如下图:

2、启动器

启动器:SpringBoot 启动的场景。如:spring-boot-starter-web,会帮我们自动导入 web 环境的所有的依赖:

xml 复制代码
<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-web</artifactId>
 </dependency>

SpringBoot 将所有的功能场景变成一个个启动器。当我们需要使用什么功能时,就只需要导入对应的启动器即可。

3、主程序

java 复制代码
@SpringBootApplication
public class MybootApplication {

    public static void main(String[] args) {
        SpringApplication.run(MybootApplication.class, args);
    }

}
  1. @SpringBootApplication:标注这个类是一个 SpringBoot 应用
  2. SpringApplication.run() 方法启动 SpringBoot 应用

3.1、@SpringBootApplication 注解

@SpringBootApplication 注解是一个复合注解

java 复制代码
@SpringBootConfiguration
@EnableAutoConfiguration
public @interface SpringBootApplication {
	//...
}

3.2、@SpringBootConfiguration 注解

java 复制代码
// 保证了 bean 的唯一性。@Component 无法保证
@Configuration
public @interface SpringBootConfiguration {
	// 默认使用 CGLIB 代理该类
    @AliasFor(annotation = Configuration.class)
    boolean proxyBeanMethods() default true;
}

@SpringBootConfiguration 注解相当于 @Configuration 注解

3.2.1、@Configuration@Component 区别

  • @Component 在 Spring 中是代表 LITE 模式的配置注解,这种模式下的注解不会被 Spring 所代理,就是一个标准类,如果在这个类中有 @Bean 标注的方法,那么方法间的相互调用,其实就是普通 Java 类的方法的调用。

  • @Configuration 在 Spring 中是代表 FULL 模式的配置注解,这种模式下的类会被 Spring 所代理,那么在这个类中的 @Bean 方法的相互调用,就相当于调用了代理方法,那么在代理方法中会判断,是否调用 getBean 方法还是 invokeSuper 方法,这里就是这两个注解的最根本的区别。@Configuration 中所有带 @Bean 注解的方法都会被动态代理,且该方法返回的都是同一个实例。且 @Configuration(proxyBeanMethods = false)@Component 作用一样

如下:

①:使用 @Configuration 注解:

java 复制代码
@Configuration
//@Component
public class UserConfig {

    @Bean
    public User user() {
        System.out.println("user..........");
        return new User();
    }

    @Bean
    public Teacher teacher() {
        // 通过代理调用, 返回同一实例
        return new Teacher(user());
    }

}

②:测试

java 复制代码
@GetMapping("/testAuto")
public void testAuto() {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(UserConfig.class);
    User configB1 = ctx.getBean(User.class);
    User configB2 = ctx.getBean(UserConfig.class).user();
    // 输出 true
    System.out.println(configB1 == configB2);
	
	ApplicationContext ctx = new AnnotationConfigApplicationContext(UserComponent.class);
    User component1 = ctx.getBean(User.class);
    User component2 = ctx.getBean(UserConfig.class).user();
    // 输出 false
    System.out.println(configB1 == configB2);
}

@Component 类中,若需要依赖其他 @Bean,应通过参数注入而非直接调用方法:

java 复制代码
@Component
public class UserConfig {

    @Bean
    public User user() {
        System.out.println("user..........");
        return new User();
    }

    /*@Bean
    public Teacher teacher() {
        // 通过代理调用, 返回同一实例
        return new Teacher(user());
    }*/

    @Bean
    public Teacher teacher(User user) {
        return new Teacher(user);
    }

}

3.3、@EnableAutoConfiguration 注解

java 复制代码
@AutoConfigurationPackage
// 导入参数类到 IOC 容器中
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
	//...
}

3.3.1、@AutoConfigurationPackage 注解

java 复制代码
// 保存扫描路径,提供给 spring-data-jpa 查询【@Entity】
@Import({AutoConfigurationPackages.Registrar.class})
public @interface AutoConfigurationPackage {
    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};
}
3.3.1.1、AutoConfigurationPackages.Registrar#registerBeanDefinitions() 方法:
java 复制代码
public abstract class AutoConfigurationPackages {

    public static void register(BeanDefinitionRegistry registry, String... packageNames) {
        if (registry.containsBeanDefinition(BEAN)) {
            BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN);
            ConstructorArgumentValues constructorArguments = beanDefinition.getConstructorArgumentValues();
            constructorArguments.addIndexedArgumentValue(0, addBasePackages(constructorArguments, packageNames));
        } else {
            GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
            beanDefinition.setBeanClass(BasePackages.class);
            beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, packageNames);
            beanDefinition.setRole(2);
            registry.registerBeanDefinition(BEAN, beanDefinition);
        }

    }

    private static String[] addBasePackages(ConstructorArgumentValues constructorArguments, String[] packageNames) {
        String[] existing = (String[])((String[])constructorArguments.getIndexedArgumentValue(0, String[].class).getValue());
        Set<String> merged = new LinkedHashSet();
        merged.addAll(Arrays.asList(existing));
        merged.addAll(Arrays.asList(packageNames));
        return StringUtils.toStringArray(merged);
    }

    
    static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
        Registrar() {
        }

        public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
            AutoConfigurationPackages.register(registry, (new PackageImport(metadata)).getPackageName());
        }
    }
}

3.3.2、@Import 注解

@Import 注解:SpringBoot 自动装配的核心注解。它有三种用法:

  1. 参数如果是普通类:将该类实例化并交给 IOC 容器管理
  2. 参数如果是 ImportBeanDefinitionRegistrar 的实现类,则支持手工注册 Bean
  3. 参数如果是 ImportSelector 的实现类,注册 selectImports() 方法返回的数组【类全路径】到 IOC 容器 【批量注册】

3.3.3、AutoConfigurationImportSelector

selectImports() 方法:查询导入的组件

java 复制代码
public String[] selectImports(AnnotationMetadata annotationMetadata) {
    if (!this.isEnabled(annotationMetadata)) {
        return NO_IMPORTS;
    } else {
        AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
        return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
    }
}

getAutoConfigurationEntry() 方法:

java 复制代码
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
    if (!this.isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    } else {
    	// 获取注解中的属性
        AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
        // 获取候选的配置
        List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
        configurations = this.removeDuplicates(configurations);
        Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
        this.checkExcludedClasses(configurations, exclusions);
        configurations.removeAll(exclusions);
        configurations = this.getConfigurationClassFilter().filter(configurations);
        this.fireAutoConfigurationImportEvents(configurations, exclusions);
        return new AutoConfigurationEntry(configurations, exclusions);
    }
}

getCandidateConfigurations() 方法:获取候选的配置

java 复制代码
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
    Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
    return configurations;
}

SpringFactoriesLoader#loadFactoryNames() 方法:

java 复制代码
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
    ClassLoader classLoaderToUse = classLoader;
    if (classLoader == null) {
        classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
    }

    String factoryTypeName = factoryType.getName();
    return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}

loadSpringFactories() 方法:

java 复制代码
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
    Map<String, List<String>> result = (Map)cache.get(classLoader);
    if (result != null) {
        return result;
    } else {
        Map<String, List<String>> result = new HashMap();

        try {
            Enumeration<URL> urls = classLoader.getResources("META-INF/spring.factories");

            while(urls.hasMoreElements()) {
                URL url = (URL)urls.nextElement();
                UrlResource resource = new UrlResource(url);
                Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                Iterator var6 = properties.entrySet().iterator();

                while(var6.hasNext()) {
                    Map.Entry<?, ?> entry = (Map.Entry)var6.next();
                    String factoryTypeName = ((String)entry.getKey()).trim();
                    String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                    String[] var10 = factoryImplementationNames;
                    int var11 = factoryImplementationNames.length;

                    for(int var12 = 0; var12 < var11; ++var12) {
                        String factoryImplementationName = var10[var12];
                        ((List)result.computeIfAbsent(factoryTypeName, (key) -> {
                            return new ArrayList();
                        })).add(factoryImplementationName.trim());
                    }
                }
            }

            result.replaceAll((factoryType, implementations) -> {
                return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
            });
            cache.put(classLoader, result);
            return result;
        } catch (IOException var14) {
            throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
        }
    }
}

加载 autoconfigure 包下的 META-INF/spring.factories 自动配置类到 IOC 容器中去:

3.4、自动装配原理

SpringBoot 启动的时候会通过 @EnableAutoConfiguration 注解找到 META-INF/spring.factories 配置文件中的所有自动配置类,并对其进行加载【但不一定生效,要判断条件是否成立】。只要导入了对应的 starter,就有对应的启动器,自动配置类就会生效,然后就配置成功。

  • 这些自动配置类都是以 AutoConfiguration 结尾来命名的,它实际上就是一个 Java 配置类形式的 Spring 容器配置类,它能通过以 Properties 结尾命名的类中取得在全局配置文件中配置的属性如:server.port
  • XxxxProperties 类是通过 @ConfigurationProperties注解与全局配置文件中对应的属性进行绑定的。

AutoConfigurationImportSelector#selectImports() 方法通过 SpringLoaderFactories#loadFactories() 方法扫描所有具有 META-INFO/spring.factories 文件的 jar 包(spring-boot-autoconfigure.jar 就有)。这个spring.factories 文件也是一组一组的 key=value 的形式,其中一个 key 是 EnableAutoConfiguration 类的全类名,而它的 value 是一个 xxxxAutoConfiguration 的类名的列表,这些类名以逗号分隔。在SpringApplication.run(...) 的内部就会执行 selectImports()方法,找到所有 JavaConfig 自动配置类的全限定名对应的 class,然后将所有自动配置类加载到 Spring 容器中

4、自定义一个 starter

4.1、基础版

结构图如下:

4.1.1、创建一个 SpringBoot 工程

创建一个 SpringBoot 工程:read-spring-boot-starter【官方推荐命名:××-spring-boot-starter】,其 pom.xml 如下:

xml 复制代码
<?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>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.14</version>
        <relativePath/>
    </parent>

    <groupId>com.zzc</groupId>
    <artifactId>read-spring-boot-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>read-spring-boot-starter</name>
    <description>read-spring-boot-starter</description>
    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.7.6</spring-boot.version>
    </properties>

    <dependencies>
        <!--自动配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.9</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

4.1.2、新建一个 Properteis 类

java 复制代码
@Data
@ConfigurationProperties(prefix = "reading")
public class ReadingProperties {

    // 类型
    private String type = "txt";
}

4.1.3、新建一个自动配置类,关联 Properties 类

java 复制代码
@Configuration
@EnableConfigurationProperties(ReadingProperties.class)
public class ReadingAutoConfiguration {

    @Autowired
    private ReadingProperties readingProperties;

 	// 若当前IOC容器中没有ReadingService时,提供一个默认的ReadingService实现
    @Bean
    @ConditionalOnMissingBean(ReadingService.class)
    public ReadingService readingService() {
        return new ReadingServiceImpl(readingProperties);
    }

}

4.1.4、添加一个对外服务接口

java 复制代码
@Slf4j
@NoArgsConstructor
@AllArgsConstructor
public class ReadingServiceImpl implements ReadingService {

    private ReadingProperties readingProperties;

    @Override
    public void reading() {
        log.info("start reading..., type is {}", readingProperties.getType());
    }
}

4.1.5、注册自动配置类

添加文件 resources/META-INF/spring.factories,其内容为:

properties 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.zzc.read.auto.ReadingAutoConfiguration

key 为 EnableAutoConfiguration 类的全限定名,value 为配置类的全限定名

4.1.6、mvn install

执行 mvn install 命令进行打包

4.1.7、引用 starter

4.1.7.1、pom.xml 中引入 starter
xml 复制代码
<dependency>
    <groupId>com.zzc</groupId>
    <artifactId>read-spring-boot-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
4.1.7.2、使用 starter
java 复制代码
@Slf4j
@RestController
public class TestController {

    @Autowired
    private ReadingService readingService;

    @GetMapping("/testAuto")
    public void testAuto() {
        readingService.reading();
    }
}

结果如下:

4.2、引入条件装配

4.2.1、修改配置类 ReadingAutoConfiguration

修改配置类 ReadingAutoConfiguration

java 复制代码
@Configuration
@EnableConfigurationProperties(ReadingProperties.class)
// 当存在reading.enable属性,且其值为true时,才初始化该Bean
@ConditionalOnProperty(name = "reading.enable", havingValue = "true")
public class ReadingAutoConfiguration {

    @Autowired
    private ReadingProperties readingProperties;

 	// 若当前IOC容器中没有ReadingService时,提供一个默认的ReadingService实现
    @Bean
    @ConditionalOnMissingBean(ReadingService.class)
    public ReadingService readingService() {
        return new ReadingServiceImpl(readingProperties);
    }

}

再重新 mvn install

4.2.2、修改使用 starter

yml 文件添加配置:

yml 复制代码
reading:
  enable: true
java 复制代码
@RestController
public class TestController {

    @Autowired(required = false)
    private ReadingService readingService;

    @GetMapping("/testAuto")
    public void testAuto() {
        readingService.reading();
    }
}

4.3、重写 starter 的默认配置

之前创建 starter 时,reading.type 默认配置为 reading.type=txt,想重写默认配置也是很简单的,只需要在当前项目 application.yml 中稍作添加:

yml 复制代码
reading:
  enable: true
  type: json

再次调用,结果如下:

4.4、重写 starter 默认实现

如果觉得 starter 默认的 ReadingService 实现不够好,那么也可以自定义 ReadingService 实现。因为 starter 构造 ReadingService 实现那里加上了 @ConditionalOnMissingBean(ReadingService.class) 。所以,只需要在当前工程自行实现 ReadingService 接口,并将其注册到 SpringIOC 中,则 starter 中默认 ReadingService 实现将不会被初始化。

添加类 MyReadingServiceImpl

java 复制代码
@Slf4j
@Service
public class MyReadingServiceImpl implements ReadingService {

    @Autowired
    private ReadingProperties readingProperties;

    @Override
    public void reading() {
        log.info("My reading start reading..., type is {}", readingProperties.getType());
    }
}

重启项目后,结果如下:

4.5、实现一个自己的 @EnableXXX

application.yml 配置 reading.enable=true 这样的方式让自动装配生效,其实不够优雅。那么我们也可以像别的 starter 那样提供 @EnableXXX@EnableScheduling@EnableAsync@EnableCaching...)注解,然后在 SpringApplication 启动类加上 @EnableXXX,让我们的自动装配生效

4.5.1、在 starter 中添加一个 selector

创建一个 ReadingSelector,用来替换之前的 ReadingAutoConfiguration

java 复制代码
@Configuration
@EnableConfigurationProperties(ReadingProperties.class)
public class ReadingSelector {

    @Autowired
    private ReadingProperties readingProperties;

    @Bean
    @ConditionalOnMissingBean(ReadingService.class)
    public ReadingService readingService() {
        return new ReadingServiceImpl(readingProperties);
    }

}

4.5.2、在 starter 中删除 resources/META-INF/spring.factories 文件

4.5.3、在 starter 中添加一个注解 @EnableReading

java 复制代码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
// @Import: 往Spring IOC中导入一个类
@Import(ReadingSelector.class)
public @interface EnableReading {

}

再重新 mvn install

4.5.4、在当前项目中做出修改

①:修改 yml 文件:

yml 复制代码
reading:
  type: json

②:修改主启动类,添加注解 @EnableReading

java 复制代码
@EnableReading
@SpringBootApplication
public class TestApplication {

    public static void main(String[] args) {
        SpringApplication.run(TestApplication.class, args);
    }

}

③:删除类 MyReadingServiceImpl

④:测试接口

java 复制代码
@RestController
public class TestController {

    @Autowired
    private ReadingService readingService;

    @GetMapping("/testAuto")
    public void testAuto() {
        readingService.reading();
    }
}

结果如下:

相关推荐
devlei1 小时前
从源码泄露看AI Agent未来:深度对比Claude Code原生实现与OpenClaw开源方案
android·前端·后端
pshdhx_albert2 小时前
AI agent实现打字机效果
java·http·ai编程
沉鱼.442 小时前
第十二届题目
java·前端·算法
努力的小郑3 小时前
Canal 不难,难的是用好:从接入到治理
后端·mysql·性能优化
赫瑞3 小时前
数据结构中的排列组合 —— Java实现
java·开发语言·数据结构
Victor3563 小时前
MongoDB(87)如何使用GridFS?
后端
Victor3563 小时前
MongoDB(88)如何进行数据迁移?
后端
小红的布丁4 小时前
单线程 Redis 的高性能之道
redis·后端
GetcharZp4 小时前
Go 语言只能写后端?这款 2D 游戏引擎刷新你的认知!
后端
周末也要写八哥4 小时前
多进程和多线程的特点和区别
java·开发语言·jvm