Spring Boot框架中如何优雅的注入实体Bean

2020/09/23 Spring Boot 浏览

Spring Boot框架中,注入实体Bean是几乎每一个Java程序员都能遇到的事情,因为Spring Boot采用约定优于配置的策略,去除了原来在Spring MVC中通过Xml进行注入的方式,全部通过Java Configuration的编码方式进行实体Bean的注入,

因此我们在开发中,对于外部组件、自己封装的业务SDK等等都需要开发者自行将实体Bean注入到Spring的容器中,然后通过注解在Spring的框架中方便的进行使用

那么,在Spring Boot框架中,我们在注入实体Bean时,如何优雅的进行注入呢?或者我们在注入实体Bean的同时,我们应该注意什么?

常规注入

常规注入很简单,通过使用@Bean注解即可完成简单的实体Bean注入,如下示例:

@Configuration
public class AdminKernelConfig {
    @Bean
    public DynamicWechatRoute dynamicWechatRoute(){
        return new DynamicWechatRoute();
    }
}

在常规注入时,假如我们要注入的Bean是通过构造函数来创建的,此时主要有2中方式进行构造

通过@Autowired注解引入外部依赖Bean,然后传递进行构造,如下代码:

@Configuration
public class AdminKernelConfig {
    @Autowired 
    Environment environment;
    
    @Bean
    public DynamicWechatRoute dynamicWechatRoute(){
        return new DynamicWechatRoute(environment);
    }
}

另外也可以通过参数传递直接引用,代码如下:

@Configuration
public class AdminKernelConfig {
    
    @Bean
    public DynamicWechatRoute dynamicWechatRoute(Environment environment){
        return new DynamicWechatRoute(environment);
    }
}

配置注入

很多时候我们创建的实体类都是需要通过外部传参进行构造的,通过基础类型参数或者封装的实体Property类进行构造,一般外部参数是通过写在Spring Boot的配置中

通过@Value注解引入外部变量进行实体Bean构造,如下:

@Configuration
public class AdminKernelConfig {
    
    @Value("${signKey}")
    String signKey;
    
    @Bean
    public DynamicWechatRoute dynamicWechatRoute(){
        return new DynamicWechatRoute(signKey);
    }
}

上面这种是很常规简单的做法,我们构造的实体类只需要一个基础String类型即可完成构造

但通常情况下,外部参数通常都很多,这种情况我们通常会单独写一个配置属性类进行封装,然后在实体类中通过该配置属性类进行参数构造,通过@EnableConfigurationProperties@ConfigurationProperties(prefix = "your.prefix")这两个注解配合使用实现效果

@ConfigurationProperties注解作用于我们的配置属性类上,配置一个前缀属性即可,例如:

@ConfigurationProperties(prefix = "test")
public class TestProperties {

    private String accessKeyId;

    private String accessKeySecret;
    //getter & setter

}

配上属性前缀test,此时我们可以在application.yml的配置文件中进行配置,代码如下:

test:
    accessKeyId: abc
    accessKeySecret: cdeeeeeeeeeeeee

配置好后,我们在我们的JavaConfiguration配置类即可进行引用注入,如下:

@Configuration
@EnableConfigurationProperties(TestProperties.class)
public class AdminKernelConfig {
    
    @Bean
    public DynamicWechatRoute dynamicWechatRoute(TestProperties testProperties){
        return new DynamicWechatRoute(testProperties);
    }
    
}

这种方式的好处是避免我们在Config类中定义大量的注解@Value对属性进行引用,造成代码结构上混乱。

条件注入

条件注入作为Spring框架提供给开发者的高级特性而存在,开发者希望能针对某些特定的条件满足的情况下,才注入Bean到Spring的容器中,这种特性提供了很好的可扩展性。

针对条件注入,Spring提供了@Conditional注解来解决这个问题.先来看@Conditional注解的源码:

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

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

}

@Condtional注解提供了一个属性value,该属性声明了一个Condition的class,Condition是Spring提供的接口

源码:

public interface Condition {

	/**
	 * Determine if the condition matches.
	 * @param context the condition context
	 * @param metadata metadata of the {@link org.springframework.core.type.AnnotationMetadata class}
	 * or {@link org.springframework.core.type.MethodMetadata method} being checked
	 * @return {@code true} if the condition matches and the component can be registered,
	 * or {@code false} to veto the annotated component's registration
	 */
	boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

从源码可以得知,@Conditional注解可以作用于类、方法,只要提供的Condition全部满足的情况下,才会将实体Bean注入到所有的容器中.

如果@Conditional作用于拥有@Configuration注解的类上,那么该类下的所有Bean的创建注入都需要满足@Conditional注解的条件才可以注入

如果@Conditional作用于方法上,那么该方法需要注入Bean时,只有满足了条件的情况下才会注入.

Spring Boot为我们提供了很多默认的Condition实现类,通过默认提供的Condition基本可以满足我们的日常需求,如果不满足,开发者可自定义Condtion的实现开发自己的装载Bean需求。

接下来,先看Spring Boot为我们提供的默认Condition实现,包路径:org.springframework.boot.autoconfigure.condition

常用应用程序使用注解,主要包含以下:

注解 说明
@ConditionalOnProperty 根据特定的属性进行条件注入
@ConditionalOnExpression 根据SPEL表达式组合复杂情况,满足的情况下条件注入
@ConditionalOnBean 根据容器中存在外部某个实体Bean的情况下条件注入
@ConditionalOnMissingBean 容器中不存在某个实体Bean的情况下条件注入
@ConditionalOnResource 资源文件存在的情况下载进行条件注入

常用注解

以下关于@Conditional注解的是Spring Boot提供给开发者可以在应用程序中使用的注解。

@ConditionalOnProperty

@ConditionalOnProperty注解是Spring Boot框架中最常用的条件注解,它允许根据特定的环境属性有条件的进行Bean注入.

示例代码如下:

@Configuration
@ConditionalOnProperty(value="knife4j.enabled", havingValue = "true",matchIfMissing = true)
public class Knife4jModule {
 //more..
}

在上面的代码示例中,仅当knife4j.enabled的属性为true时,才会加载Knife4jModule这个配置模块,如果开发者根本没有配置这个属性,由于我们将matchIfMissing定义为true,因此程序启动时仍将加载该模块。

@ConditionalOnExpression

如果我们需要基于多个属性的条件进行组合才能创建Bean,那么我们可以使用@ConditionalOnExpression注解

示例代码如下:

@Configuration
@ConditionalOnExpression(value="${knife4j.enabled:true} and ${knife4j.basic.enabled:true}")
public class Knife4jModule {
 //more..
}

通过Spring提供的SPEL表达式组合多个表达式的复杂情况,仅到表达式中满足条件时,才会加载Knife4jModule这个配置模块

@ConditionalOnBean

通常情况下,我们希望只有在某一个Bean可用的情况下,我们在加载配置注入我们的实体Bean

示例代码如下:

@Configuration
@ConditionalOnBean(SwaggerModule.class)
public class Knife4jModule {
 //more..
}

在加载Knife4jModule之前,我们需要SwaggerModel的实体Bean在Spring的容器中存在可用时,才加载该配置

@ConditionalOnMissingBean

@ConditionalOnMissingBean@ConditionalOnBean意思正好相反,只有在Spring的容器中不存在该实体Bean时才进行条件注入

示例代码如下:

@Configuration
public class OnMissingBeanModule {

  @Bean
  @ConditionalOnMissingBean
  public DataSource dataSource() {
    return new InMemoryDataSource();
  }
}

一般该注解作用于实体Bean本身,从上面的示例中,只有在Spring容器中不存在DataSource的实例Bean时,才进行加载条件注入Bean.

@ConditionalOnResource

根据某些资源的情况下载加载Bean的情况,可以使用@ConditionalOnResource注解

示例代码如下:

@Configuration
@ConditionalOnResource(resources = "/logback.xml")
public class LogbackModule {
  //...
}

LogbackModule模块仅当logback.xml资源文件在当前环境中存在的情况下才加载.

通过这种方式,我们可以根据找到自己模块的配置后才进行实体Bean的创建.

不常用注解

虽然Spring Boot提供了很多默认的@Conditional的注解扩展实现,但是并不是所有的扩展实现都是提供给开发者来使用的,有些则是提供给框架内部进行使用的.

@ConditionOnClass

仅当某个类在类路径上时才加载Bean

@Configuration
@ConditionalOnClass(name = "this.clazz.does.not.Exist")
public class OnClassModule {
	//  ...
}

@ConditionalOnMissingClass

仅当某个类不在类路径上时才加载Bean

@Configuration
@ConditionalOnMissingClass(value = "this.clazz.does.not.Exist")
public class OnMissingClassModule {
  //...
}

@ConditionalOnJndi

仅当通过JNDI可以使用某些资源时才加载Bean

@Configuration
@ConditionalOnJndi("java:comp/env/foo")
public class OnJndiModule {
  //...
}

@ConditionalOnJava

仅当在java某个版本时才加载Bean

@Configuration
@ConditionalOnJava(JavaVersion.EIGHT)
public class OnJavaModule {
  //...
}

自定义

通过上面的不常用注解,我们其实可以发现,针对各种条件下才能对Bean进行注入的实在太多,这种情况下,当我们的程序需要在某种情况下才能注入Bean时,Spring肯定不能满足,此时就需要自定义条件注入Condition

简单的自定义实现

目前假设有需求,我们在创建某个实体Bean时,需要根据配置文件的某一个String属性进行对比,只有在Bean上给定的目标值和配置文件中给定的属性值相等的情况下才注入该Bean

通过上面的需求,我们首先需要定义Condition接口的实现,代码如下

public class ConditionOnKeyApply implements Condition {
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        Map<String, Object> multiValueMap=annotatedTypeMetadata.getAnnotationAttributes(ConditionOnKey.class.getName());
        //获取property
        String propertyValue=Objects.toString(multiValueMap.get("property"),"");
        //获取目标值
        String targetValue=Objects.toString(multiValueMap.get("targetValue"),"");
        if (StrUtil.isNotBlank(propertyValue)&&StrUtil.isNotBlank(targetValue)){
            //都不为空的情况下
            Environment environment=conditionContext.getEnvironment();
            //从配置环境中获取值
            String sourceValue=environment.getProperty(propertyValue);
            System.out.println("环境值:"+sourceValue+",目标值:"+targetValue);
            // 进行比对
            return StrUtil.equalsIgnoreCase(sourceValue,targetValue);
        }
        return false;
    }
}

定义我们自定义的注解@ConditionOnKey,代码如下:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
@Documented
//指定Conditional的实现类
@Conditional(value = ConditionOnKeyApply.class)
public @interface ConditionOnKey {
    /**
     * 获取某个属性的env值
     * @return
     */
    String property();

    /**
     * 目标值
     * @return
     */
    String targetValue() default "true";
}

在JavaConfiguration中进行注入

@Configuration
public class ConditionKeyConfig {


    @Bean
    @ConditionOnKey(property = "key",targetValue = "test")
    public ConditionKeyModel conditionKeyModel(){
        return new ConditionKeyModel();
    }
}

从注入的代码中,如果我们在application.yml配置文件中配置一个属性为key,值为test的情况下,ConditionKeyModel这个实体会注入Spring容器中,否则不会进行注入.

key: test

外部导入

通常我们在使用第三方技术组件时,只需要简单的在Spring Boot的启动类上加入@Enablexxx等这类注解,既可以帮我们快速集成第三方的技术能力。

这种方式我们在自己封装时也可以使用,通常@Enablexx注解使用的是@Import注解来导入一个java configuration的配置文件类进行实现

看一个Swagger的示例,一般我们在使用swagger的时候通常使用@EnableSwagger2来使用,如下代码:

@Configuration
@EnableSwagger2
public class SwaggerConfiguration {
    
}

@EnableSwagger2的注解源码如下:

@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({Swagger2DocumentationConfiguration.class})
public @interface EnableSwagger2 {
    
}

注解上除了标注该注解的作用目标以及Retention,还使用了@Import注解将Swagger2DocumentationConfiguration类进行了导入,来看源码:

@Configuration
@Import({ SpringfoxWebMvcConfiguration.class, SwaggerCommonConfiguration.class })
@ComponentScan(basePackages = {
    "springfox.documentation.swagger2.mappers"
})
@ConditionalOnWebApplication
public class Swagger2DocumentationConfiguration {
  @Bean
  public JacksonModuleRegistrar swagger2Module() {
    return new Swagger2JacksonModule();
  }

  @Bean
  public HandlerMapping swagger2ControllerMapping(
      Environment environment,
      DocumentationCache documentationCache,
      ServiceModelToSwagger2Mapper mapper,
      JsonSerializer jsonSerializer) {
    return new PropertySourcedRequestMappingHandlerMapping(
        environment,
        new Swagger2Controller(environment, documentationCache, mapper, jsonSerializer));
  }
}

源码中是一个Configuration类,然后通过上面我们说的最简单的常规注入了2个实体Bean

一般这种方式我们可以在封装自己的组件时进行使用,通过提供一个@Enable系列的注解,方便外部人员使用和记忆.

站内搜索

    Table of Contents