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

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注解是 Spring Boot 框架中最常用的条件注解, 它允许根据特定的环境属性有条件的进行 Bean 注入.

示例代码如下:

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

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

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

示例代码如下:

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

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

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

示例代码如下:

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

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

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

示例代码如下:

@Configuration
public class OnMissingBeanModule {

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

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

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

示例代码如下:

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

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

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

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

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

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

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

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

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

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

仅当在 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 容器中, 否则不会进行注入.

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

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

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

key: test

@EnableSwagger2的注解源码如下:

@Configuration
@EnableSwagger2
public class SwaggerConfiguration {
    
}

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

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

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值