如何开发自己的spring boot starter

原创
2019/02/25 21:13
阅读数 1.4K

官方对Spring Boot的介绍是这么说的:

Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can "just run".

We take an opinionated view of the Spring platform and third-party libraries so you can get started with minimum fuss. Most Spring Boot applications need very little Spring configuration.

Features

  • Create stand-alone Spring applications

  • Embed Tomcat, Jetty or Undertow directly (no need to deploy WAR files)

  • Provide opinionated 'starter' dependencies to simplify your build configuration

  • Automatically configure Spring and 3rd party libraries whenever possible

  • Provide production-ready features such as metrics, health checks and externalized configuration

  • Absolutely no code generation and no requirement for XML configuration

大概意思就是Spring Boot集成一些第三方库,基于"约定优于配置 "的原则,简化了各种配置并通过starter引入,同时提供了一些检查功能,使用户可以快速开发并部署Spring应用。这些Spring本身和第三方库提供的功能,都是由“AutoConfigure”相关的功能实现的,可以说Spring Boot=SpringFramework+AutoConfigure。Spring已经默认提供了大部分常用库的支持,我们仍然可以自己开发starter,让用户可以更快更简单的引入其他第三方或自己开发的功能。

Quick start

创建依赖模块

创建一个maven项目:spring-boot-starter-foo-api,模拟一个第三方库

 <?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>spring-boot-custom-starter</artifactId>
         <groupId>org.dfg.demo</groupId>
         <version>1.0-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 ​
     <artifactId>spring-boot-starter-foo-api</artifactId>
 </project>

创建一个类,模拟库API

 public class Account {
     private String number;
     //getter、setter
     public String toString() {
         return String.format("Account:{number=%s}", number);
     }
 }

创建autoconfigue模块

创建一个maven项目:spring-boot-starter-foo-autoconfigue,用于配置依赖并加入Spring

 <?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>spring-boot-custom-starter</artifactId>
         <groupId>org.dfg.demo</groupId>
         <version>1.0-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 ​
     <artifactId>spring-boot-starter-foo-autoconfigue</artifactId>
 ​
     <dependencies>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-autoconfigure</artifactId>
         </dependency>
         <dependency>
             <groupId>org.dfg.demo</groupId>
             <artifactId>spring-boot-starter-foo-api</artifactId>
             <version>1.0-SNAPSHOT</version>
         </dependency>
     </dependencies>
 </project>

创建配置类

 @Configuration
 public class FooAutoConfiguration {
     @Bean(name = "account")
     @ConditionalOnMissingBean
     public Account getAccount() {
         System.out.println("create account");
         return new Account("011");
     }
 }

到这一步就是通常情况下Spring基友注解API配置的用法,但是Spring Boot Starter的目标是做到无配置、自动发现、自动配置。这就需要下一步,在resources目录新建文件:META-INF\spring.factories,并输入:

 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
 org.dfg.demo.springboot.FooAutoConfiguration

创建starter模块

创建一个maven项目:spring-boot-starter-foo-starter,作为starter依赖

 <?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>spring-boot-custom-starter</artifactId>
         <groupId>org.dfg.demo</groupId>
         <version>1.0-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 ​
     <artifactId>spring-boot-starter-foo-starter</artifactId>
 ​
     <dependencies>
         <dependency>
             <groupId>org.dfg.demo</groupId>
             <artifactId>spring-boot-starter-foo-autoconfigue</artifactId>
             <version>1.0-SNAPSHOT</version>
         </dependency>
     </dependencies>
 </project>

在resources目录下创建文件:META-INF\spring.provides,并输入:

 provides: spring-boot-starter-foo-autoconfigue,spring-boot-starter-foo-api

创建应用模块

创建一个maven项目:spring-boot-starter-foo-example,模拟需求依赖的应用

 <?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>spring-boot-custom-starter</artifactId>
         <groupId>org.dfg.demo</groupId>
         <version>1.0-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
     <artifactId>spring-boot-starter-foo-example</artifactId>
 ​
     <dependencies>
         <dependency>
             <groupId>org.dfg.demo</groupId>
             <artifactId>spring-boot-starter-foo-starter</artifactId>
             <version>1.0-SNAPSHOT</version>
         </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter</artifactId>
         </dependency>
     </dependencies>
 </project>

创建测试类:

 @SpringBootApplication
 public class AccountApplication {
 }
 @RunWith(SpringJUnit4ClassRunner.class)
 @SpringBootTest(classes = AccountApplication.class)
 public class AccountTest {
     @Autowired
     ApplicationContext context;
 ​
     @Test
     public void accountTest() throws Exception {
         Assert.assertTrue(context.containsBean("account"));
         Account account = context.getBean(Account.class);
         System.out.println(account);
         Assert.assertEquals(account.getNumber(), "011");
     }
 }

执行单元测试,程序打印:

 create account
 Account:{number=011}

可见,应用只需引入starter依赖,无需任何其他操作,就能使用其提供的功能,做到了简单无配置。

自定义配置

零配置固然方便,但是实际应用中必然需要自定义配置,Spring提供了允许自定义配置的功能。

在spring-boot-starter-foo-api模块中加入一个类模拟API:

 public class User {
 ​
     private String name;
     private int age;
 ​
     public User(String name, int age) {
         this.name = name;
         this.age = age;
     }
     //getter、setter
     @Override
     public String toString() {
         return String.format("User:{name=%s, age=%s}", name, age);
     }
 }

在spring-boot-starter-foo-autoconfigue模块中加入配置参数映射类:

 @ConfigurationProperties(prefix = "foo")
 public class FooProperties implements InitializingBean {
 ​
     private User config;
 ​
     public User getConfig() {
         return config;
     }
 ​
     public void setConfig(User config) {
         this.config = config;
     }
 ​
     @Override
     public void afterPropertiesSet() {
         if (config == null) {
             config = new User();
             config.name = "tony";
             config.age = 18;
         }
     }
 ​
     public static class User {
         private String name;
         private int age;
 ​
         public String getName() {
             return name;
         }
 ​
         public void setName(String name) {
             this.name = name;
         }
 ​
         public int getAge() {
             return age;
         }
 ​
         public void setAge(int age) {
             this.age = age;
         }
     }
 }

修改自动配置类:

 @Configuration
 @EnableConfigurationProperties(FooProperties.class)
 public class FooAutoConfiguration {
     @Bean(name = "user")
     @ConditionalOnMissingBean
     public User getUserByProperty(FooProperties properties) {
         System.out.println("create user");
         String name = properties.getConfig().getName();
         int age = properties.getConfig().getAge();
         User user = new User(name, age);
         return user;
     }
 }

在spring-boot-starter-foo-example模块中的resources目录下创建配置文件application.yaml:

 foo:
   config:
     name: abc
     age: 9

创建测试类:

 @SpringBootApplication
 public class AccountApplication {
 }
 @RunWith(SpringJUnit4ClassRunner.class)
 @SpringBootTest(classes = {AccountApplication.class})
 public class UserTest {
     @Autowired
     ApplicationContext context;
 ​
     @Test
     public void userTest() throws Exception {
         Assert.assertTrue(context.containsBean("user"));
         User user = context.getBean(User.class);
         System.out.println(user);
         Assert.assertEquals(user.getName(), "abc");
         Assert.assertEquals(user.getAge(), 9);
     }
 }

执行单元测试,程序打印:

 create user
 User:{name=abc, age=9}

至此一个引入依赖即可使用,并支持配置的starter就完成了。

相关注解

  • @Configuration,表示这是一个配置类,Spring容器会在启动时从这个类获取bean。

  • @EnableConfigurationProperties,启用@ConfigurationProperties,可以直接指定一个。

  • @ConfigurationProperties,将外部属性绑定到类,并在定义Bean的方法中使用。

  • @ConditionalOnClass/@ConditionalOnMissingClass,组件(配置类或者创建bean的方法)只有在环境变量里存在/不存在时才注册。

  • @ConditionalOnBean/@ConditionalOnMissingBean,只有当指定bean存在/不存在时满足匹配,当放在创建bean方法上时,默认取这个方法返回类型。由于只能取到已创建的bean,所以需要下一个注解配合。

  • @AutoConfigureBefore/@AutoConfigureAfter,指定的EnableAutoConfiguration必须在指定的自动配置类之前/之后执行。

  • @ConditionalOnSingleCandidate,同@ConditionalOnBean,但是要求只有一个

关于spring.factories

AutoConfiguration类唯一一次被用到就是在META-INF\spring.factories文件里,spring就是通过这个文件找到要加载的配置类的。依赖于spring本身的加载机制,类似于SPI(Service Provider Interface)。通过SpringFactoriesLoader这个类读取配置文件,其中key是接口或抽象类的全限定名,value是用逗号分隔的实现类名。

 package org.springframework.core.io.support;
 ​
 public final class SpringFactoriesLoader {
 ​
     public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
 ​
     private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
         ...
         try {
             Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
             result = new LinkedMultiValueMap<>();
             while (urls.hasMoreElements()) {
                 URL url = urls.nextElement();
                 UrlResource resource = new UrlResource(url);
                 Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                 for (Map.Entry<?, ?> entry : properties.entrySet()) {
                     String factoryClassName = ((String) entry.getKey()).trim();
                     for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
                         result.add(factoryClassName, factoryName.trim());
                     }
                 }
             }
             cache.put(classLoader, result);
             return result;
         } catch (IOException ex) {
             throw new IllegalArgumentException("Unable to load factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);
         }
     }
 }

当Spring启动后,会执行ConfigurableApplicationContext.refresh()初始化容器,其中invokeBeanFactoryPostProcessors()会执行所有BeanFactoryPostProcessor,其中有个ConfigurationClassPostProcessor用于处理@Configuration类。其中processConfigBeanDefinitions()创建ConfigurationClassParser解析配置类,直到ConfigurationClassParser.processImports()中查找配置类所有。由于@SpringBootApplication有注解@EnableAutoConfiguration,而@EnableAutoConfiguration又有注解@Import(AutoConfigurationImportSelector.class),会注册一个DeferredImportSelector处理器。然后执行这些处理器,在DeferredImportSelectorGrouping.getImports()中调用AutoConfigurationImportSelector.process()。

 public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
 ​
     protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,         AnnotationMetadata annotationMetadata) {
         if (!isEnabled(annotationMetadata)) {
             return EMPTY_ENTRY;
         }
         AnnotationAttributes attributes = getAttributes(annotationMetadata);
         List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
         //处理过滤
         ...
         return new AutoConfigurationEntry(configurations, exclusions);
     }
     
     protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,  AnnotationAttributes attributes) {
         // 查找EnableAutoConfiguration.class对应的Configuration类名
         List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
         return configurations;
     }
     
     protected Class<?> getSpringFactoriesLoaderFactoryClass() {
         return EnableAutoConfiguration.class;
     }
     
     private static class AutoConfigurationGroup implements DeferredImportSelector.Group, BeanClassLoaderAware, BeanFactoryAware, ResourceLoaderAware {
         @Override
         public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
             // 查找要自动配置的配置类
             AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
                     .getAutoConfigurationEntry(getAutoConfigurationMetadata(), annotationMetadata);
             this.autoConfigurationEntries.add(autoConfigurationEntry);
             for (String importClassName : autoConfigurationEntry.getConfigurations()) {
                 this.entries.putIfAbsent(importClassName, annotationMetadata);
             }
         }
         ...
     }
 }

ConfigurationClassParser通过迭代查找所有的配置类,并最终registerBeanDefinition()注册为BeanDefinition()。

关于starter模块

关于spring-boot-starter-foo-starter这个模块,里面只提供META-INF\spring.provides一个文件,而spring并未读取这个文件。事实上这个文件和这个模块并不是必需的,官方的说法是这个文件的用处是帮助STS(和支持这个功能的插件)提供内容自动完成建议。

https://github.com/spring-projects/spring-boot/issues/1926

所以实际应用中,可以直接引用spring-boot-starter-foo-autoconfigue这个模块。


相关代码:

https://github.com/dingfugui/spring-notes/tree/master/spring-boot-custom-starter

 

展开阅读全文
加载中

作者的其它热门文章

打赏
2
1 收藏
分享
打赏
0 评论
1 收藏
2
分享
返回顶部
顶部