Skip to main content

Spring Boot Auto Configuration

Hung NguyenOctober 2, 2024About 16 minspring-bootjavaspring-boot

Spring Boot Auto Configuration

Bài viết này cung cấp ví dụ mã nguồn hoàn chỉnh, có thể xem tại https://github.com/YunaiV/SpringBoot-Labs trong thư mục lab-47.

Nội dung gốc không dễ thực hiện, hãy ủng hộ bằng cách nhấn Star nhé, cùng nhau tiến lên nào!

1. Tổng quan

Lời khuyên: Bởi vì bài viết này chia sẻ về nguyên lý của cấu hình tự động Spring Boot, nên yêu cầu người đọc có kinh nghiệm sử dụng Spring Boot. Nếu bạn chưa từng sử dụng Spring Boot, đừng lo lắng, hãy đọc trước hai chương của bài Spring Boot SpringMVC để cảm nhận sự hấp dẫn của Spring Boot.

Cấu hình tự động trong Spring Boot, như tên gọi, là mong muốn có thể tự động cấu hình để giải phóng chúng ta khỏi sự phiền phức của việc cấu hình thủ công. Để thực hiện cấu hình tự động, nó cần phải giải quyết ba câu hỏi:

Hãy cùng xem ví dụ dưới đây để trả lời ba câu hỏi trên. Khi chúng ta thêm phụ thuộc spring-boot-starter-web, nó sẽ tạo ra một Tomcat nhúng ở cổng 8080, đồng thời cho phép tùy chỉnh cổng thông qua cấu hình server.port trong tệp cấu hình application.yaml. Vậy câu trả lời cho ba câu hỏi là:

Lưu ý: Để dễ hiểu, các câu trả lời dưới đây chỉ mang tính tạm thời và chưa hoàn toàn chính xác.

Giờ hãy cùng can đảm xem qua lớp EmbeddedWebServerFactoryCustomizerAutoConfiguration, chịu trách nhiệm tạo cấu hình cho các máy chủ web nhúng như Tomcat, Jetty. Mã nguồn như sau:

@Configuration // <1.1>  
@ConditionalOnWebApplication // <2.1>  
@EnableConfigurationProperties(ServerProperties.class) // <3.1>  
public class EmbeddedWebServerFactoryCustomizerAutoConfiguration {  
  
    /**  
     * Cấu hình bên trong nếu Tomcat được sử dụng.  
     */  
    @Configuration // <1.2>  
    @ConditionalOnClass({ Tomcat.class, UpgradeProtocol.class })  
    public static class TomcatWebServerFactoryCustomizerConfiguration {  
  
        @Bean  
        public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(  
                Environment environment, ServerProperties serverProperties) {  
            // <3.2>  
            return new TomcatWebServerFactoryCustomizer(environment, serverProperties);  
        }  
  
    }  
  
    /**  
     * Cấu hình bên trong nếu Jetty được sử dụng.  
     */  
    @Configuration // <1.3>  
    @ConditionalOnClass({ Server.class, Loader.class, WebAppContext.class })  
    public static class JettyWebServerFactoryCustomizerConfiguration {  
  
        @Bean  
        public JettyWebServerFactoryCustomizer jettyWebServerFactoryCustomizer(  
                Environment environment, ServerProperties serverProperties) {  
             // <3.3>  
            return new JettyWebServerFactoryCustomizer(environment, serverProperties);  
        }  
  
    }  
  
    /**  
     * Cấu hình bên trong nếu Undertow được sử dụng.  
     */  
    // ... Bỏ qua mã nguồn của UndertowWebServerFactoryCustomizerConfiguration  
  
    /**  
     * Cấu hình bên trong nếu Netty được sử dụng.  
     */  
    // ... Bỏ qua mã nguồn của NettyWebServerFactoryCustomizerConfiguration  
  
}

Trước khi xem mã nguồn, chúng ta hãy cùng tìm hiểu một chút về Spring JavaConfig. Từ Spring 3.0, Spring cung cấp phương thức JavaConfig, cho phép chúng ta sử dụng mã Java để tạo Spring Bean. Ví dụ mã nguồn:

@Configuration  
public class DemoConfiguration {  
  
    @Bean  
    public Object object() {  
        return new Object();  
    }  
  
}

OK, giờ chúng ta quay lại với mã nguồn EmbeddedWebServerFactoryCustomizerAutoConfiguration, chúng ta sẽ chia thành ba phần để giải thích, vừa hay giải quyết ba câu hỏi ở trên:

① Lớp cấu hình

Tại vị trí <1.1>, thêm chú thích @Configuration vào lớp, khai báo đây là một lớp cấu hình. Vì mục đích của nó là cấu hình tự động, nên tên lớp kết thúc bằng AutoConfiguration.

Tại vị trí <1.2><1.3>, lần lượt là các lớp cấu hình được sử dụng để khởi tạo các Bean liên quan đến Tomcat và Jetty.

Vậy, chúng ta có kết luận một: thông qua các lớp cấu hình được chú thích bằng @Configuration, có thể giải quyết vấn đề "Tạo ra những Bean nào".

Thực tế, dự án spring-boot-autoconfigure của Spring Boot cung cấp rất nhiều lớp cấu hình tự động cho các framework khác nhau, chúng ta sẽ đi sâu hơn vào mục "2. Lớp cấu hình tự động" sau.

② Chú thích điều kiện

Tại vị trí <2>, thêm chú thích điều kiện @ConditionalOnWebApplication vào lớp, đây là một chú thích điều kiện, chỉ ra rằng lớp cấu hình hiện tại chỉ có hiệu lực khi dự án hiện tại là một dự án Web. Trong các dự án Spring Boot, loại dự án được chia thành dự án Web (sử dụng SpringMVC hoặc WebFlux) và dự án không phải Web. Nhờ vậy, chúng ta dễ hiểu tại sao lớp cấu hình EmbeddedWebServerFactoryCustomizerAutoConfiguration yêu cầu loại dự án phải là Web, chỉ có dự án Web mới cần tạo các máy chủ Web nhúng.

Tại vị trí <2.1><2.2>, thêm chú thích điều kiện @ConditionalOnClass vào lớp, đây là một chú thích điều kiện, chỉ ra rằng lớp cấu hình hiện tại chỉ có hiệu lực khi dự án hiện tại có các lớp được chỉ định.

Vậy, chúng ta có kết luận hai: thông qua các chú thích điều kiện, có thể giải quyết vấn đề "Điều kiện nào được thỏa mãn?".

Thực tế, gói condition của Spring Boot cung cấp rất nhiều chú thích điều kiện, chúng ta sẽ đi sâu hơn vào mục "2. Chú thích điều kiện" sau.

③ Thuộc tính cấu hình

Tại vị trí <3.1>, sử dụng chú thích @EnableConfigurationProperties để kích hoạt lớp ServerProperties lớp thuộc tính cấu hình. Trong Spring Boot, đã định nghĩa chú thích @ConfigurationProperties để khai báo lớp thuộc tính cấu hình, cho phép gán các cấu hình có tiền tố cụ thể vào các trường trong lớp đó. Ví dụ, mã nguồn của ServerProperties như sau:

@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties
        implements EmbeddedServletContainerCustomizer, EnvironmentAware, Ordered {

    /**
     * Cổng HTTP của Server.
     */
    private Integer port;

    /**
     * Đường dẫn ngữ cảnh của ứng dụng.
     */
    private String contextPath;
      
    // ... Bỏ qua các thuộc tính khác
      
}

Tại vị trí <3.2><3.3>, khi tạo các đối tượng TomcatWebServerFactoryCustomizerJettyWebServerFactoryCustomizer, đều truyền vào ServerProperties, làm cơ sở cho việc tạo các máy chủ Web với các cấu hình tương ứng. Nói cách khác, chúng ta có thể tùy chỉnh cấu hình máy chủ Web bằng cách thay đổi các cấu hình trong tệp cấu hình.

Vậy, chúng ta có kết luận ba: thông qua các thuộc tính cấu hình, có thể giải quyết vấn đề "Thuộc tính của các Bean được tạo là gì?".


🐶 Tới đây, chúng ta đã hiểu khá rõ cách Spring Boot giải quyết ba vấn đề nêu trên, nhưng vẫn chưa thể hiện được cách thực hiện cấu hình tự động. Ví dụ, khi chúng ta thêm các phụ thuộc như spring-boot-starter-web, Spring Boot biết cách quét các lớp cấu hình nào. Tiếp tục hành trình của chúng ta, hãy tiếp tục phân tích kỹ hơn.

2. Lớp cấu hình tự động

Trong dự án spring-boot-autoconfigure của Spring Boot, có rất nhiều cấu hình tự động cho các framework khác nhau, như hình dưới đây:

Khi chúng ta khởi động ứng dụng Spring Boot thông qua phương thức SpringApplication#run(Class<?> primarySource, String... args), có một thành phần rất quan trọng là lớp SpringFactoriesLoader. Lớp này sẽ đọc tệp spring.factories trong thư mục META-INF để lấy danh sách các lớp cấu hình tự động của từng framework.

Chúng ta hãy xem một ví dụ về tệp spring.factories trong dự án spring-boot-autoconfigure của Spring Boot:

Như vậy, các lớp cấu hình có chú thích @Configuration sẽ nâng cấp thành các lớp cấu hình tự động. Khi Spring Boot lấy được các lớp cấu hình tự động này, nó sẽ tự động tạo ra các Bean tương ứng, hoàn thành chức năng cấu hình tự động.

Chú thích: Có một chủ đề khá thú vị liên quan mà bạn có thể tìm hiểu thêm. Thực tế, chúng ta có thể hiểu tệp spring.factories như một cơ chế SPI (Service Provider Interface) của Spring Boot. Nếu bạn quan tâm, có thể tham khảo các bài viết sau:

Thực tế, cấu hình tự động chỉ là một trong những điểm mở rộng của Spring Boot dựa trên spring.factories, với điểm mở rộng EnableAutoConfiguration. Từ hình trên, chúng ta cũng có thể thấy các điểm mở rộng khác như:

  • ApplicationContextInitializer
  • ApplicationListener
  • AutoConfigurationImportListener
  • AutoConfigurationImportFilter
  • FailureAnalyzer
  • TemplateAvailabilityProvider

Vì dự án spring-boot-autoconfigure cung cấp cấu hình tự động cho các framework phổ biến, nên các framework khác cần tự triển khai cấu hình tự động cho mình. Ví dụ, Dubbo cung cấp cấu hình tự động thông qua dự án dubbo-spring-boot-project, như hình dưới đây:

3. Chú thích điều kiện

Chú thích điều kiện không phải là tính năng riêng của Spring Boot, mà đã được giới thiệu trong phiên bản Spring 3.1 để đăng ký các Bean khác nhau tùy theo môi trường. Khi đó, chú thích @Profile đã ra đời. Dưới đây là một ví dụ:

@Configuration  
public class DataSourceConfiguration {  
  
    @Bean  
    @Profile("DEV")  
    public DataSource devDataSource() {  
        // ... MySQL đơn lẻ
    }  
  
    @Bean  
    @Profile("PROD")  
    public DataSource prodDataSource() {  
        // ... MySQL cụm  
    }  
      
}

Trong phiên bản Spring 4, chú thích @Conditional đã được giới thiệu, cho phép khai báo các điều kiện khi cấu hình lớp hoặc phương thức Bean. Ví dụ:

@Configuration  
public class TestConfiguration {  
  
    @Bean  
    @Conditional(XXXCondition.class)  
    public Object xxxObject() {  
        return new Object();  
    }  
}

Rõ ràng, chú thích @Conditional của Spring 4 không thuận tiện, yêu cầu chúng ta phải tự mở rộng. Vì vậy, Spring Boot đã cải tiến và cung cấp một số chú thích điều kiện phổ biến như sau:

4. Thuộc tính cấu hình

Spring Boot đọc các tệp cấu hình như application.yaml hay application.properties để tạo các Bean với thuộc tính tùy chỉnh. Thậm chí, có thể kết hợp với chú thích @ConditionalOnProperty để ngăn chặn việc tạo Bean.

Thực ra phần này không có nhiều nội dung để chia sẻ, bạn có thể đọc thêm bài viết Giới thiệu về tệp cấu hình Spring Boot để hiểu rõ hơn.

5. Starter tích hợp

Khi sử dụng Spring Boot, chúng ta không trực tiếp thêm phụ thuộc spring-boot-autoconfigure, mà thay vào đó sử dụng các phụ thuộc Starter tích hợp sẵn trong Spring Boot. Ví dụ, khi muốn sử dụng SpringMVC, chúng ta thêm phụ thuộc spring-boot-starter-web. Tại sao lại như vậy?

Vì các lớp cấu hình tự động của Spring Boot thường có chú thích điều kiện @ConditionalOnClass, xác định rằng nếu trong dự án có lớp chỉ định, thì mới tạo Bean tương ứng. Và để có các lớp này, chúng ta cần thêm phụ thuộc vào framework tương ứng.

Do đó, khi thêm phụ thuộc spring-boot-starter-web, nó sẽ tự động thêm các phụ thuộc cần thiết, đảm bảo các lớp cấu hình tự động có thể hoạt động và tạo Bean tương ứng. Như hình dưới đây:

Spring Boot tích hợp rất nhiều Starter, giúp chúng ta dễ dàng thêm các framework khác nhau và cấu hình tự động. Như hình dưới đây:

6. Tự tạo Starter

Trong một số trường hợp, chúng ta cần tự tạo Starter để đạt được mục tiêu tự động cấu hình. Ví dụ:

Dưới đây, chúng ta sẽ cùng tạo một Starter tùy chỉnh, giúp tự động cấu hình một máy chủ HttpServer tích hợp sẵn của Java HttpServer. Cấu trúc dự án cuối cùng sẽ như hình sau:

Dự án cuối cùng
Dự án cuối cùng

Trước khi bắt đầu, chúng ta cần tìm hiểu về quy tắc đặt tên của Spring Boot Starter để làm việc trở nên chuyên nghiệp hơn. Quy tắc đặt tên như sau:

Tình huốngQuy tắc đặt tênVí dụ
Starter tích hợp sẵn của Spring Bootspring-boot-starter-{framework}spring-boot-starter-web
Starter tùy chỉnh của framework{framework}-spring-boot-startermybatis-spring-boot-starter
Starter tùy chỉnh của công ty{company}-spring-boot-starter-{framework}Hiện chưa có, đây chỉ là ý tưởng của tôi

6.1 Dự án yunai-server-spring-boot-starter

Chúng ta sẽ tạo một dự án yunai-server-spring-boot-starter, giúp tự động cấu hình một máy chủ HttpServer tích hợp sẵn trong Java. Vì dự án khá đơn giản nên chúng ta sẽ không tách ra thành hai dự án như spring-boot-autoconfigurespring-boot-starter-{framework}.

6.1.1 Thêm phụ thuộc

Trong tệp pom.xml, thêm các phụ thuộc cần thiết.

<?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">  
    <parent>  
        <artifactId>lab-47</artifactId>  
        <groupId>cn.iocoder.springboot.labs</groupId>  
        <version>1.0-SNAPSHOT</version>  
    </parent>  
    <modelVersion>4.0.0</modelVersion>  
  
    <artifactId>yunai-server-spring-boot-starter</artifactId>  
  
    <dependencies>  
        <!-- Thêm thư viện Spring Boot Starter cơ bản -->  
        <dependency>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-starter</artifactId>  
            <version>2.2.2.RELEASE</version>  
        </dependency>  
    </dependencies>  
</project>

6.1.2 Lớp YunaiServerProperties

Trong gói cn.iocoder.springboot.lab47.yunaiserver.autoconfigure, tạo lớp YunaiServerProperties để đọc các thuộc tính cấu hình có tiền tố yunai.server. Mã nguồn:

@ConfigurationProperties(prefix = "yunai.server")  
public class YunaiServerProperties {  
  
    /**  
     * Cổng mặc định  
     */  
    private static final Integer DEFAULT_PORT = 8000;  
  
    /**  
     * Cổng  
     */  
    private Integer port = DEFAULT_PORT;  
  
    public static Integer getDefaultPort() {  
        return DEFAULT_PORT;  
    }  
  
    public Integer getPort() {  
        return port;  
    }  
  
    public YunaiServerProperties setPort(Integer port) {  
        this.port = port;  
        return this;  
    }  
}

6.1.3 Lớp YunaiServerAutoConfiguration

Trong gói cn.iocoder.springboot.lab47.yunaiserver.autoconfigure, tạo lớp YunaiServerAutoConfiguration để tự động cấu hình máy chủ HttpServer khi lớp com.sun.net.httpserver.HttpServer có sẵn trong dự án. Mã nguồn:

@Configuration // Khai báo lớp cấu hình  
@EnableConfigurationProperties(YunaiServerProperties.class) // Kích hoạt lớp cấu hình YunaiServerProperties  
public class YunaiServerAutoConfiguration {  
  
    private Logger logger = LoggerFactory.getLogger(YunaiServerAutoConfiguration.class);  
  
    @Bean // Khai báo tạo Bean  
    @ConditionalOnClass(HttpServer.class) // Điều kiện có lớp com.sun.net.httpserver.HttpServer. Lớp này có sẵn trong JDK nên điều kiện này luôn đúng.  
    public HttpServer httpServer(YunaiServerProperties serverProperties) throws IOException {  
        // Tạo đối tượng HttpServer và khởi động  
        HttpServer server = HttpServer.create(new InetSocketAddress(serverProperties.getPort()), 0);  
        server.start();  
        logger.info("[httpServer][Khởi động thành công trên cổng: {}]", serverProperties.getPort());  
  
        // Trả về đối tượng server  
        return server;  
    }  
}

6.1.4 Tệp spring.factories

Trong thư mục resources, tạo thư mục META-INF, sau đó tạo tệp spring.factories trong thư mục này và thêm lớp cấu hình tự động YunaiServerAutoConfiguration. Nội dung như sau:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.iocoder.springboot.lab47.yunaiserver.autoconfigure.YunaiServerAutoConfiguration

Đến đây, chúng ta đã hoàn thành một Starter tùy chỉnh. Tiếp theo, chúng ta sẽ thêm vào dự án 6.2 lab-47-demo để thử nghiệm.

6.2 Dự án lab-47-demo

Tạo dự án lab-47-demo và đưa vào Starter tùy chỉnh của chúng ta.

6.2.1 Nhập phụ thuộc

Trong tệp pom.xml, nhập các phụ thuộc liên quan.

<?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">  
    <parent>  
        <artifactId>lab-47</artifactId>  
        <groupId>cn.iocoder.springboot.labs</groupId>  
        <version>1.0-SNAPSHOT</version>  
    </parent>  
    <modelVersion>4.0.0</modelVersion>  
  
    <artifactId>lab-47-demo</artifactId>  
  
    <dependencies>  
        <!-- Nhập Starter tùy chỉnh -->  
        <dependency>  
            <groupId>cn.iocoder.springboot.labs</groupId>  
            <artifactId>yunai-server-spring-boot-starter</artifactId>  
            <version>1.0-SNAPSHOT</version>  
        </dependency>  
    </dependencies>  
</project>

6.2.2 Tệp cấu hình

Trong thư mục resource, tạo tệp application.yaml để thiết lập thuộc tính yunai.server.port nhằm tùy chỉnh cổng HttpServer. Cấu hình như sau:

yunai:  
  server:  
    port: 8888 # Cổng HttpServer tùy chỉnh

6.2.3 DemoApplication

Tạo lớp DemoApplication.java, chỉ cần cấu hình chú thích @SpringBootApplication. Mã như sau:

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

6.2.4 Kiểm tra đơn giản

Thực hiện phương thức DemoApplication#main(String[] args) để khởi động ứng dụng Spring Boot. Log in sẽ hiển thị như sau:

2020-02-02 13:03:12.156  INFO 76469 --- [           main] c.i.s.lab47.demo.DemoApplication         : Starting DemoApplication on MacBook-Pro-8 with PID 76469 (/Users/yunai/Java/SpringBoot-Labs/lab-47/lab-47-demo/target/classes started by yunai in /Users/yunai/Java/SpringBoot-Labs)  
2020-02-02 13:03:12.158  INFO 76469 --- [           main] c.i.s.lab47.demo.DemoApplication         : No active profile set, falling back to default profiles: default  
2020-02-02 13:03:12.873  INFO 76469 --- [           main] c.i.s.l.y.a.YunaiServerAutoConfiguration : [httpServer][Khởi động máy chủ thành công, cổng là:8888]  
2020-02-02 13:03:12.927  INFO 76469 --- [           main] c.i.s.lab47.demo.DemoApplication         : Started DemoApplication in 1.053 seconds (JVM running for 1.47)

Lúc này, khi chúng ta sử dụng trình duyệt truy cập vào địa chỉ http://127.0.0.1:8888/, kết quả trả về là 404 Not Found vì chúng ta chưa cung cấp Handler tương ứng cho HttpServer.

666. Tổng kết

Đến đây, chúng ta đã hoàn thành việc học về nguyên lý tự động cấu hình của Spring Boot.

Trong quá trình hiểu nguyên lý tự động cấu hình của Spring Boot, chúng ta sẽ nhận thấy rằng, dù là lớp cấu hình hay chú thích điều kiện, thực tế Spring đã cung cấp sẵn. Thậm chí, SpringFactoriesLoader cũng là một sản phẩm của Spring. Vì vậy, Spring Boot được xây dựng trên nền tảng của Spring, thực hiện một cơ chế khởi động Boot.

Một trong những cốt lõi của Spring là IOC, chịu trách nhiệm quản lý vòng đời của Bean. Còn Spring Boot thì quản lý vòng đời của ứng dụng Java.