3.2 Spring Boot自动配置原理

那么,有没有一种方案,可以把上面这些繁杂费时费力的重复性劳动“一键打包、开箱即用”?

接下来,我们就逐步展示Spring Boot是怎样通过自动配置和提供一系列开箱即用的启动器starter来封装上面的复杂性使其简单化的。

3.2.1 Java配置

在整个Spring Boot应用程序中,我们将看不到一个传统意义上的Spring XMI配置文件。其实,在Spring3.x和Spring4.x中就出现了大量简化XML配置的解决方案。例如:

❑组件扫描(Component Scan):Spring去自动发现应用上下文中创建的Bean。

❑自动装配(Autowired):Spring自动创建Bean之间的依赖。

❑通过JavaConfig方式实现Java代码配置Bean。

下面是一个使用Java Config方式配置Thymeleaf视图模板引擎的代码示例:

        @Configuration
        @ComponentScan(basePackages = { "com.easy.Spring Boot"})
        @EnableWebMvc // 启用WebMVC配置(关于WebMVC的自定义配置我们将在后面章节中介绍)
        public class WebMvcConfig extends WebMvcConfigurerAdapter
        {
            @Bean
            public TemplateResolver templateResolver()    {//配置模板解析器
                TemplateResolver templateResolver = new ServletContextTemplateResolver();
                templateResolver.setPrefix("/WEB-INF/views/");
                templateResolver.setSuffix(".html");
                templateResolver.setTemplateMode("HTML5");
                templateResolver.setCacheable(false);
                return templateResolver;
            }
            @Bean
            public SpringTemplateEngine templateEngine() {//配置模板引擎
                SpringTemplateEngine templateEngine = new SpringTemplateEngine();
                templateEngine.setTemplateResolver(templateResolver());
                return templateEngine;
            }
            @Bean
            public ThymeleafViewResolver viewResolver()  {//配置视图解析器
                ThymeleafViewResolver thymeleafViewResolver = new ThymeleafView
                    Resolver();
                thymeleafViewResolver.setTemplateEngine(templateEngine());
                thymeleafViewResolver.setCharacterEncoding("UTF-8");
                return thymeleafViewResolver;
            }
            @Override
            public void addResourceHandlers(ResourceHandlerRegistry registry)
                                                                  {//静态资源处理器配置
                      registry.addResourceHandler("/resources/**").addResourceLocations
                          ("/resources/");
                  }
                  …
                  @Bean(name = "messageSource")
                  public MessageSource configureMessageSource(){//消息源配置
                      ReloadableResourceBundleMessageSource messageSource = new Reloada
                          bleResourceBundleMessageSource();
                      messageSource.setBasename("classpath:messages");
                      messageSource.setCacheSeconds(5);
                      messageSource.setDefaultEncoding("UTF-8");
                      return messageSource;
                  }
              }

在WebMvcConfig.java配置类中,我们做了如下的配置:

❑将它标记为使用@Configuration注释的Spring配置类。

❑启用基于注释的Spring MVC配置,使用@EnableWebMvc。

❑通过注册TemplateResolver、SpringTemplateEngine、ThymeleafViewResolver Bean来配置Thymeleaf ViewResolver。

❑注册的ResourceHandlers Bean用来配置URI/resources/**静态资源的请求映射到/resources/目录下。

❑配置的MessageSource Bean从classpath路径下的ResourceBundle中的messages-{country-code}.properties消息配置文件中加载i18n消息。

这些样板化的Java配置代码比XML要更加简单些,同时易于管理。而Spring Boot则是引入了一系列的约定规则,将上面的样板化配置抽象内置到框架中去,用户连上面的Java配置代码也将省去。

3.2.2 条件化Bean

Spring Boot除了采用Java、Config方式实现“零XML”配置外,还大量采用了条件化Bean方式来实现自动化配置,本节就介绍这个内容。

1.条件注解@Conditional

假如你想一个或多个Bean只有在应用的路径下包含特定的库时才创建,那么使用这节我们所要介绍的@Conditional注解定义条件化的Bean就再适合不过了。

Spring4.0中引入了条件化配置特性。条件化配置通过条件注解@Conditional来标注。条件注解是根据特定的条件来选择Bean对象的创建。条件注解根据不同的条件来做出不同的事情(简单说就是if else逻辑)。在Spring中条件注解可以说是设计模式中状态模式的一种体现方式,同时也是面向对象编程中多态的应用部分。

常用的条件注解如表3-1所示。

表3-1 常用的条件注解

2.条件注解使用实例

下面我们通过实例来说明条件注解@Conditional的具体工作原理。

1)创建示例工程。

为了精简篇幅,这里只给出关键步骤。首先使用Spring Initializr创建一个Spring Boot工程,选择Web Starter依赖,配置项目名称和存放路径,配置Gradle环境,最后导入到IDEA中,完成工程的创建工作。

2)实现Condition接口。

下面我们来实现org.springframework.context.annotation.Condition接口,实现类是MagicCondition。

实现类的“条件”逻辑是:当application.properties配置文件中存在“magic”配置项,同时当值是true的时候:

        magic=true
        #magic=false

就表示条件匹配。

新建MagicCondition类,实现Condition接口。在IDEA中会自动提示我们实现其中的方法,如图3-1所示。

图3-1 IDEA会自动提示我们实现其中的方法

选择要实现的matches函数,如图3-2所示。

图3-2 选择要实现的matches函数

完整的实现代码如下:

        class MagicCondition : Condition {
            override fun matches(context: ConditionContext, metadata: AnnotatedType
                Metadata): Boolean {
                val env = context.getEnvironment()
                if (env.containsProperty("magic")) // 检查application.properties配置
                    文件中是否存在magic属性key
                {
                    val b = env["magic"]            //获取magic属性key的值
                    return b == "true"              //如果是true,返回true
                }
                return false                          // 返回false
            }
        }

实现这个Condition接口只需要实现matches方法。如果matches方法返回true就创建该Bean,如果返回false则不创建Bean。这就是否创建MagicService Bean的条件。

matches方法中的第1个参数类型ConditionContext是一个接口,它的定义如下:

        public interface ConditionContext {
            BeanDefinitionRegistry getRegistry();
            ConfigurableListableBeanFactory getBeanFactory();
            Environment getEnvironment();
            ResourceLoader getResourceLoader();
            ClassLoader getClassLoader();
        }

ConditionContext中的方法API说明如表3-2所示。

表3-2 ConditionContext中的方法API

matches方法中的第2个参数类型AnnotatedTypeMetadata,则能够让我们检查带有@Bean注解的方法上是否有其他注解。AnnotatedTypeMetadata接口的定义如下:

        public interface AnnotatedTypeMetadata {
            boolean isAnnotated(String annotationType);
            Map<String, Object> getAnnotationAttributes(String annotationType);
              Map<String, Object> getAnnotationAttributes(String annotationType, boolean
                  classValuesAsString);
              MultiValueMap<String, Object> getAllAnnotationAttributes(String annota
                  tionType);
              MultiValueMap<String, Object> getAllAnnotationAttributes(String annota
                  tionType, boolean classValuesAsString);
          }

使用isAnnotated()方法,能够判断带有@Bean注解的方法是不是还有其他特定的注解。使用另外的几个方法,我们能够检查@Bean注解的方法上,所标注的其他注解的属性。

例如Spring4使用@Conditional对多环境部署配置文件功能实现的ProfileCondition类的代码如下:

        class ProfileCondition implements Condition {
            @Override
            public boolean matches(ConditionContext context, AnnotatedTypeMetadata
                metadata) {
                MultiValueMap<String, Object> attrs = metadata.getAllAnnotationA
                    ttributes(Profile.class.getName());
                if (attrs ! = null) {
                    for (Object value : attrs.get("value")) {
                          if (context.getEnvironment().acceptsProfiles((String[])
                              value)) {
                          return true;
                          }
                    }
                    return false;
                }
                return true;
            }
        }

我们可以看到,ProfileCondition通过AnnotatedTypeMetadata得到了用于@Profile注解的所有属性:

        MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes
            (Profile.class.getName());

然后循环遍历attrs这个Map中的属性“value”的值(包含了Bean的profile名称),使用ConditionContext中的Environment来检查这个value进而决定使用哪个Profile处于激活状态。

3)条件配置类ConditionalConfig。

Spring4提供了一个通用的基于特定条件创建Bean的方式:@Conditional注解。编写条件配置类ConditionalConfig代码如下:

        @Configuration
        @ComponentScan(basePackages = ["com.easy.Spring Boot.demo_conditional_bean"])
        class ConditionalConfig {
            @Bean
            @Conditional(MagicCondition::class) //指定条件类
            fun magicService(): MagicServiceImpl {
                return MagicServiceImpl()
            }
        }

逻辑是当Spring容器中存在MagicCondition Bean,并满足MagicCondition类的条件时,去实例化magicService这个Bean。否则不注册这个Bean。

4)MagicServiceImpl逻辑实现。

MagicServiceImpl业务Bean的逻辑很简单,就是打印一个标识信息。实现代码如下:

        class MagicServiceImpl : MagicService {
            override fun info(): String {
                return "THIS IS MAGIC"           // 打印一个标识信息
            }
        }
        interface MagicService {
            fun info(): String
        }

5)测试MagicController。

我们使用一个HTTP接口来测试条件化Bean的注册结果:

        @RestController
        class MagicController {
            @GetMapping("magic")
            fun magic(): String {
                try {
                    val magicService = SpringContextUtil.getBean("magicService") as
                                      MagicService // 从Spring容器中获取magicService Bean
                    return magicService.info() //调用info()方法
                } catch (e: Exception) {
                    e.printStackTrace()
                }
                return "null"
            }
        }

其中SpringContextUtil实现代码如下:

        object SpringContextUtil {
            lateinit var applicationContext: ApplicationContext
            fun setGlobalApplicationContext(context: ApplicationContext) {
                applicationContext = context
            }
        fun getBean(beanId: String): Any {
            return applicationContext.getBean(beanId)
        }
    }

在Spring Boot启动入口类中,我们把Spring Boot应用的上下文对象放到Spring Context-Util中的这个applicationContext成员变量中:

        @Spring BootApplication
        class DemoConditionalBeanApplication
        fun main(args: Array<String>) {
            val context = runApplication<DemoConditionalBeanApplication>(*args)
            SpringContextUtil.setGlobalApplicationContext(context)
        }

完整的项目代码参考示例工程源代码。

提示

本小节的实例工程源码:https://github.com/KotlinSpringBoot/demo_conditional_bean

6)运行测试。

我们先来测试magic = true。在application.properties中配置:

        magic=true

重新启动应用程序,浏览器输入:http://127.0.0.1:8080/magic,输出“THIS IS MAGIC”。

再来测试magic = false. 在application.properties中配置:

        magic=false

重新启动应用程序,浏览器输入:http://127.0.0.1:8080/magic,输出:“null”。

这个时候,我们看到应用程序后台日志有如下输出:

        org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean
            named 'magicService' available ……
        com.easy.Spring Boot.demo_conditional_bean.controller.MagicController.magic
            (MagicController.kt:14)

表明magicService这个Bean没有注册到Spring容器中。条件化注册Bean验证OK。

3.2.3 组合注解

组合注解就是将现有的注解进行组合,生成一个新的注解。使用这个新的注解就相当于使用了该组合注解中所有的注解。这个特性还是蛮有用的,例如Spring Boot应用程序的入口类注解@Spring BootApplication就是典型的例子:

        @Target(ElementType.TYPE)
        @Retention(RetentionPolicy.RUNTIME)
        @Documented
        @Inherited
        @Spring BootConfiguration
        @EnableAutoConfiguration
        @ComponentScan(excludeFilters = {
        @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExclude
        Filter.class) })
        public @interface Spring BootApplication

早期版本的Spring Boot中,用户需要使用如下三个注解来标注应用入口main类:

❑@Configuration

❑@EnableAutoConfiguration

❑@ComponentScan

在Spring Boot1.2.0中只需用一个统一的注解@Spring BootApplication。