3、Spring Boot的一些原理

经过前面2节,我们大概知道了如果去使用Spring Boot,现在我们就Spring Boot的原理做个介绍,这节也会有Spring 的补充讲解

该节主要会讲解Spring Boot的自动装配、@Enable的工作原理、@Import的工作原理,以及@EnableAutoConfiguration的原理

3.1、Condition接口的用法

Spring Boot应该是“约定优于配置”的最佳实践者了,比如说我们加入了spring-data-jpa的jar包,那么Spring Boot就会默认给我们创建jap的相关链接(虽然很多时间这种默认的并不好,但是可以通过配置文件来配置我们想要的)。那么有时候我们需要按照我们自己的方案来装配对应的Bean,那这时候应该如何去做呢?

3.1.1、Condition的基本用法

我们先来看下面的示例

a、建立一个接口以及2个实现类

package com.dragon.condition;
public interface EncodingConvert {
}

package com.dragon.condition;
public class UTF8EncodingConvert implements EncodingConvert {
}

package com.dragon.condition;
public class GBKEncodingConvert implements EncodingConvert{
}

b、创建一个配置文件

package com.dragon.condition;

import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;

@SpringBootConfiguration
public class EncodingConvertConfiguration {

    @Bean
    public EncodingConvert createUTF8EncodingConvert(){
        return new UTF8EncodingConvert();
    }

    @Bean
    public EncodingConvert createGBKEncodingConvert(){
        return new GBKEncodingConvert();
    }
}

在上面的配置文件中,我们创建了上一步的2个类的bean。

c、启动类:

@SpringBootApplication
public class SpringBootConfigApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext application = SpringApplication.run(SpringBootConfigApplication.class, args);
        System.out.println(application.getBeansOfType(EncodingConvert.class));      
        application.close();
    }
}

运行该启动类,会打印如下:
这里写图片描述

不意外,我们可以看到2个类都加载进来了。那么我们如何才能够按照自己的意愿去装配Bean呢?

在spring-context中提供了一个org.springframework.context.annotation.Condition的接口,这个接口的作用就是按照条件来进行装配的。该接口中只有一个方法:

boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);

我们可以实现这个接口,然后根据返回值来达到时候装配的目的(返回true就装配,否则不装配)。

同样spring-context还提供了@Conditional这个注解

package org.springframework.context.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {

    /**
     * All {@link Condition}s that must {@linkplain Condition#matches match}
     * in order for the component to be registered.
     */
    Class<? extends Condition>[] value();

}

该注解包含一个value的属性。

@Conditional注解和Condition一般是配合起来使用的。只有接口的实现类返回true才装配,否则不装配。

我们来改造上面的示例。

在上面的代码基础上,我们添加2个实现了Condition的类。

public class GBKCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String encoding = System.getProperty("file.encoding");
        if(encoding != null){
            return encoding.equalsIgnoreCase("gbk");
        }
        return false;
    }
}

public class UTFCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String encoding = System.getProperty("file.encoding");
        if(encoding != null){
            return encoding.equalsIgnoreCase("utf-8");
        }
        return false;
    }
}

然后我们修改EncodingConvertConfiguration类的内容

@SpringBootConfiguration
public class EncodingConvertConfiguration {

    @Bean
    @Conditional(UTFCondition.class)
    public EncodingConvert createUTF8EncodingConvert(){
        return new UTF8EncodingConvert();
    }

    @Bean
    @Conditional(GBKCondition.class)
    public EncodingConvert createGBKEncodingConvert(){
        return new GBKEncodingConvert();
    }
}

主要是在每个Bean上面添加了@Conditional的注解。然后我们修改启动类:

@SpringBootApplication
public class SpringBootConfigApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext application = SpringApplication.run(SpringBootConfigApplication.class, args);
        System.out.println(System.getProperty("file.encoding"));
        System.out.println(application.getBeansOfType(EncodingConvert.class));
        application.close();
    }
}

我们增加了file.encoding这个环境变量的打印。我们来看打印结果
这里写图片描述

我们可以看到当前file.encoding的值,以及加载的类,这时候就没有加载GBK的类了.
而如果我们修改环境变量为:
这里写图片描述
然后再运行,结果如下
这里写图片描述
这个时候,就是装配的GBK的了。

上面的就是Condition的基本示例。

@Conditional除了使用在方法上面,还可以使用在类上面,如果是使用在类上面,就表示该类下面的所有方法都起作用。

从上面的@Conditional里面的value属性我们知道,这是一个数组,也就说可以有多个class的值。如果value是多个类的数组,那么表示这多个Condition都返回true了,那么才装配。

上面演示了@Condition注解和Condition接口的基本用法,结合这2个类就可以实现我们的按需加载。这个组合是Spring Boot实现自动加载的基本原因之一。

3.1.2、Spring Boot的自动加载

Spring Boot为我们提供了大量有用的利于自动装配的注解,这些注解位于spring-boot-autoconfiguration包下:

这里写图片描述

一些解释:
- ConditionalOnBean 当context有bean的时候才装配。与之相反的是ConditionalOnMissingBean
- ConditionalOnClass 当classpath有类的时候才装配。
- ConditionalOnExpression 当使用SpEL表达式为true的时候才装配
- ConditionalOnJava 根据当前JVM的版本号进行装配。这个版本号有个范围,范围是大于等于或者小于。比如说大于等于某个版本才进行装配
- ConditionalOnNotWebApplication 当不是web环境的才进行装配
- ConditionalOnProperty 当某个配置文件存在,或者等于某个值的时候才装配。比如

@ConditionalOnClass(DruidDataSource.class)
@ConditionalOnProperty(
            name = "spring.datasource.type", 
            havingValue = "com.alibaba.druid.pool.DruidDataSource", 
            matchIfMissing = true)
   matchIfMissing就表示当spring.datasource.type没有的时候,也会生效。记住是没有该属性。

我们打开任意一个ConditionalOn*的源代码都可以看到@Condition注解和Condition接口的影子。

当我们发现Spring Boots提供的Condition不满足条件的时候,我们可以3.1小节的Condition接口和@Condition注解来实现满足我们自己业务需要的场景。做法就是声明一个Condition接口的类,在Bean上配合@Condition注解来达到动态装配的效果。

3.2、@Import的用法

在一些复杂应用里面,一般会有多个配置文件,比如在一些传统工程里面:
这里写图片描述
我们发现上面有很多的import的标签,这就是导入其他的配置文件。import标签对应的annotation就是@Import。

我们先来看一个简单的示例

先声明2个普通的类:

public class Role {
}
public class User {
}

由于这2个类没有添加任何的注解,所以是不能被容器托管的。这时候如果运行下面的代码:

@ComponentScan
public class SpringBootEnableApplication2 {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringBootEnableApplication2.class, args);
        System.out.println(context.getBean("user"));
        System.out.println(context.getBean("role"));
        context.close();
    }
}

肯定会报下面的错:

org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'user' available

要想获取user这个bean,可以在User类上面加@Component注解来使的User类纳入到容器里面。

3.2.1、@Import导入类

除了使用上面的方式,我们还可以通过@Import来导入类,从而将该类纳入到Spring容器中:

@ComponentScan
@Import({User.class,Role.class})
public class SpringBootEnableApplication2 {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringBootEnableApplication2.class, args);
        System.out.println(context.getBean(User.class));
        System.out.println(context.getBean(Role.class));
        context.close();
    }
}

再运行这个启动类,可以看到下面的打印结果:
这里写图片描述

可以看到已经可以获取Bean了。这里的Bean需要使用context.getBean(User.class)方式获取,而不能使用context.getBean(“user”)这种方式获取。

3.2.2、@Import导入配置类

除了导入具体的类外,还可以导入配置类,比如有下面的配置类:

public class MyConfiguration {
    @Bean
    public Runnable createRunnable(){
        return () -> {};
    }

    @Bean
    public Runnable createRunnable2(){
        return () -> {};
    }
}

(虽然该类没有使用@Configuration注解)
然后修改我们的启动类:

@ComponentScan
@Import(MyConfiguration.class)
public class SpringBootEnableApplication2 {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringBootEnableApplication2.class, args);
        System.out.println(context.getBeansOfType(Runnable.class));
        context.close();
    }
}

我们来看打印效果
这里写图片描述
可以看到这里打印了MyConfiguration里面定义的类。当然我们也可以在MyConfiguration使用@Configuration注解来标志这是一个配置类。如果这样做了,那么在启用类上就不需要@Import了。

总结:@Import用来导入一个或多个类(会被spring容器托管),或者配置类(配置类里面的bean都会被spring容器托管)。

3.2.3、@Import导入ImportSelector

我们来看看ImportSelecctor接口:

package org.springframework.context.annotation;
import org.springframework.core.type.AnnotationMetadata;
public interface ImportSelector {
    String[] selectImports(AnnotationMetadata importingClassMetadata);
}

该接口的作用就是将selectImports返回的类都装配到Spring中去。入参是AnnotationMetadata,就是说可以根据业务逻辑来动态的返回类。我们来看下面的代码:

public class MyImportSelector implements ImportSelector {
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.dragon.User"};
    }
}

@ComponentScan
@Import(MyImportSelector.class)
public class SpringBootEnableApplication2 {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringBootEnableApplication2.class, args);
        System.out.println(context.getBeansOfType(User.class));
        System.out.println(context.getBeansOfType(Role.class));
        context.close();
    }
}

然后运行SpringBootEnableApplication2,我们会发现打印如下
这里写图片描述

我们发现User这个Bean打印了,但是Role这个Bean确是没有的。也就是说Spring容器只装置了User这个Bean。

如果我们把MyImportSelector改为下面:

public class MyImportSelector implements ImportSelector {
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.dragon.User", Role.class.getName()};
    }
}

那么2个Bean都是可以拿到的。

ImportSelector接口中,selectImports方法的返回值,必须是一个class(全称),该class会被spring容器所托管起来。

selectImports方法还可以获取到注解的详细信息,然后根据信息去动态的返回需要被spring容器管理的bean。比如说我们增加一个EnableLog的自定义注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MyImportSelector.class)
public @interface EnableLog {
    String name();
}

然后修改MyImportSelector:

/**
 * selectImports方法的返回值,必须是一个class(全称),该class会被spring容器所托管起来
 */
public class MyImportSelector implements ImportSelector {
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(EnableLog.class.getName());
        Object value=annotationAttributes.get("name");
        if(value.equals("my springboot")){
            //这里可以做些逻辑处理返回一些Bean。
System.out.println("===> 一些逻辑处理");
        }

        return new String[]{"com.dragon.User", Role.class.getName()};
    }
}

修改后的启动类:

@ComponentScan
@EnableLog(name = "my springboot")
public class SpringBootEnableApplication2 {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringBootEnableApplication2.class, args);
        System.out.println(context.getBeansOfType(User.class));
        System.out.println(context.getBeansOfType(Role.class));

        context.close();
    }
}

运行启动类,打印如下
这里写图片描述

从而达到了动态加载bean的效果。

3.2.4、ImportBeanDefinitionRegistrar

除了ImportSelector接口,还有个接口ImportBeanDefinitionRegistrar也是需要关注的。该接口的声明如下:

package org.springframework.context.annotation;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.core.type.AnnotationMetadata;
public interface ImportBeanDefinitionRegistrar {
    public void registerBeanDefinitions(
            AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry);
}

该接口的方法没有返回值。但是该方法有个BeanDefinitionRegistry的参数,BeanDefinitionRegistry的作用就是往容器里面注入Bean。

ImportSelector和ImportBeanDefinitionRegistrar的作用效果是一样的。只不过Spring通过ImportSelector的返回值来进行注入。而ImportBeanDefinitionRegistrar是让我们自己注入。

看下面的代码:

/**
 * registerBeanDefinitions方法的参数有一个BeanDefinitionRegistry,BeanDefinitionRegistry可以用来往spring容器中注入bean
 * 如此,我们就可以在registerBeanDefinitions方法里面动态的注入bean
 */
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        BeanDefinitionBuilder bdb = BeanDefinitionBuilder.rootBeanDefinition(User.class);
        registry.registerBeanDefinition("user", bdb.getBeanDefinition());

        BeanDefinitionBuilder bdb2 = BeanDefinitionBuilder.rootBeanDefinition(Role.class);
        registry.registerBeanDefinition("role", bdb2.getBeanDefinition());

        BeanDefinitionBuilder bdb3 = BeanDefinitionBuilder.rootBeanDefinition(MyConfiguration.class);
        registry.registerBeanDefinition(MyConfiguration.class.getName(), bdb3.getBeanDefinition());
    }
}

这样,我们就在容器中”注入”了3个Bean。因为MyImportBeanDefinitionRegistrar并没有加上诸如@Component注解。所以,还需要有其他的步骤:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MyImportBeanDefinitionRegistrar.class)
public @interface EnableLog {
    String name();
}

我们只需要修改上一节的EnableLog自定义注解,然后倒入上面的MyImportBeanDefinitionRegistrar。再次运行SpringBootEnableApplication2类就可以达到理想的效果。

理解了上面的原理后,我们再来看看第2小节里面的@EnableConfigurationProperties的源码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesImportSelector.class)
public @interface EnableConfigurationProperties {
    Class<?>[] value() default {};
}

该源码导入了EnableConfigurationPropertiesImportSelector这个类。然后我们看看这个类的源码:

class EnableConfigurationPropertiesImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata metadata) {
        MultiValueMap<String, Object> attributes = metadata.getAllAnnotationAttributes(
                EnableConfigurationProperties.class.getName(), false);
        Object[] type = attributes == null ? null
                : (Object[]) attributes.getFirst("value");
        if (type == null || type.length == 0) {
            return new String[] {
            ConfigurationPropertiesBindingPostProcessorRegistrar.class
                            .getName() };
        }
        return new String[] { ConfigurationPropertiesBeanRegistrar.class.getName(),
                ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() };
    }
//省略部分代码
}

这个类其实也是将一些类装载到Spring里面。从而达到了主动装配的效果。

3.2、@Enable系列使用原理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值