xiwenAndlejian / my-blog

Java基础学习练习题
1 stars 0 forks source link

Rpc 实现笔记(四)自定义 Spring Boot Starter #24

Open xiwenAndlejian opened 5 years ago

xiwenAndlejian commented 5 years ago

参考内容:

如果你曾经维护过需要编写xml文件才能启动的Spring工程,并且也使用过不需要xml也能正常运行的spring-boot-starter,你也许会和我一样感叹而又疑惑:为什么spring-boot-starter能省去花费我们大量时间维护的xml和一些配置文件?它是怎么实现的?

答:是膜法,是我加了膜法!,其实不是,简化配置的效果必定是有原因的。

学习编程这么久,我认为编程中一个现象必定有它出现的原因,即有因才有果。只是大部分时候我们没有能力理解,或者不愿意深入探索出现这个现象的根本原因。

编写一个 Spring Boot Starter

源码

我们一边编写一个新的Spring Boot Starter,一边学习为什么它能简化我们的配置。

假设目前我们有一个公共服务,这个服务被多个工程所使用,并且需要一定基础配置项才能启用。为了简化使用方的流程,我们将这个服务单独独立出来,为它制作一个spring-boot-starter

构建 maven 工程

pom文件:

<?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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.dekuofa</groupId>
    <!-- ⚠️此处命名未符合 Spring 规范 -->
    <!-- ✅符合规范的命名应当是:demo-spring-boot-starter -->
    <artifactId>spring-boot-starter-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
        <springboot.version>2.1.1.RELEASE</springboot.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.4</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
            <version>${springboot.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <version>${springboot.version}</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>${springboot.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.3</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

迁移(创建)Service

@Getter
@Setter
public class DemoService {

    private DemoProperties properties;

    public DemoService(DemoProperties properties) {
        this.properties = properties;
    }

    public String doSomething() {
        return "doSomething with properties:" + properties.toString();
    }

}

⚠️:这里没有使用@Service注解,因此没有提供无参构造函数。若使用@Service,则需要提供无参的构造函数。

配置类:DemoProperties

@Data
@Primary
@ConfigurationProperties(prefix = "demo")
public class DemoProperties {

    public static final String DEFAULT_HOST = "localhost";
    public static final int    DEFAULT_PORT = 8000;

    private String host;
    private int    port;

    public DemoProperties() {
        this.host = DEFAULT_HOST;
        this.port = DEFAULT_PORT;
    }
}

@Primary:当存在多个可能类时,优先加载有此注解的类

@ConfigurationProperties:读取配置项并绑定在类对应属性上。其中prefix表示属性名前缀,因此对应配置文件的属性为:demo.hostdemo.port

自动配置类

@Configuration
@EnableConfigurationProperties(DemoProperties.class)
@ConditionalOnClass(DemoService.class)
public class DemoAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(DemoService.class)
    public DemoService demoService(DemoProperties properties) {
        return new DemoService(properties);
    }
}

@ConditionalOnClass:当指定类在类路径上时才匹配。

@ConditionalOnMissingBean:只有在BeanFactory中尚未包含指定的类或名称时才匹配。

@EnableConfigurationProperties:启动对带有@ConfigurationProerties注解的bean支持。

spring.factories

最后我们需要在路径:src/main/resouces下创建文件夹META-INF,并在其中创建文件spring.factories

spring.factories 文件中应当列出使用EnableAutoConfiguration的配置类。

⚠️Auto-configurations must be loaded that way only. Make sure that they are defined in a specific package space and that, in particular, they are never the target of component scanning.

自动配置类必须且只能通过这种方式被加载。确保它们被定义在特定的包空间中,它们永远都不是组件扫描的目标。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.dekuofa.autoconfigure.DemoAutoConfiguration

这样一个Spring Boot Starter就制作完成了。接下来我们对它进行测试,校验自动配置是否生效。

xiwenAndlejian commented 5 years ago

单元测试

public class AutoConfigTest {

    private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
            .withConfiguration(AutoConfigurations.of(DemoAutoConfiguration.class));

    @Test
    public void testDefaultConfig() {
        this.contextRunner.withUserConfiguration(DemoProperties.class)
                .run((context) -> {
                    // 容器中只存在一个 DemoService 类对应的 bean
                    assertThat(context).hasSingleBean(DemoService.class);
                    // isSameAs 与 == 等价,表示同一个对象
                    assertThat(context.getBean(DemoProperties.class)).isSameAs(
                            context.getBean(DemoService.class).getProperties());

                    DemoProperties properties = context.getBean(DemoProperties.class);
                    assertEquals(DemoProperties.DEFAULT_HOST, properties.getHost());
                    assertEquals(DemoProperties.DEFAULT_PORT, properties.getPort());
                });
    }

    @Test
    public void testCustomizeEnvironment() {
        this.contextRunner
                .withPropertyValues("demo.host=customize")
                .withPropertyValues("demo.port=9000")
                .run((context) -> {
            assertThat(context).hasSingleBean(DemoService.class);
            assertThat(context.getBean(DemoProperties.class).getPort()).isEqualTo(9000);
            assertThat(context.getBean(DemoProperties.class).getHost()).isEqualTo("customize");
        });
    }

    @Test
    public void testIfNotPresent() {
        // 当 DemoService 未被使用时,自动配置应当被禁用
        this.contextRunner.withClassLoader(new FilteredClassLoader(DemoService.class))
                .run((context) -> assertThat(context).doesNotHaveBean("demoService"));
    }
}

实际使用代码

@Autowired
private DemoService demoService;

public void callDoSomething() {
    System.out.println(demoService.doSomething());
}

注意,使用前应当把spring-boot-starter-demo的依赖加入当前工程pom文件中。

<dependency>
    <groupId>com.dekuofa</groupId>
    <artifactId>spring-boot-starter-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>