• Spring生态研习【五】:Springboot中bean的条件注入


    在springboot中,开发的确变的简单了很多,但是,开发者现在希望开发傻瓜式的方便搞定项目中的各种奇怪的需求最好了,不用烧脑,本来程序猿的生活就是枯燥的,不要再给自己添加更多的烦恼。

    今天,就为了方便这点,介绍下,如何解决在开发过程中,一些场景下,为了实现一个配置模块中,基于开关量或者选择配置项,实现不同功能,例如,在一个session共享模块当中,解决session是基于header传递还是基于cookie传递这两种应用场景,有些应用中希望基于header传递sessionId,但是有些应用中希望基于cookie传递sessionId,然后,session共享模块,是一个非常基础的组件,差不多是一个开箱即用的功能块。所以呢,最好能配置好,然后只需要基于配置文件中的某个选项,就能实现运行在不同的工作模式下。这个能否做到呢?真的只需要改一下配置中的开关量就能实现吗?

    能否实现,这里卖个关子,先不说,介绍完了本篇博文后,细心的读者一定知道答案,或者说一定能明白能否做,怎么做!

    第一大点:先介绍一下springboot中能够支持的或者说封装好的常用的条件注入的注解

    1 @ConditionalOnBean

    1.1 基本使用案例

    @Component
    @ConditionalOnBean(name="aBean")
    public class BBean {
      private final ABean aBean;
      public BBean(ABean aBean) {
          // ...
      }
    }

    1.2 使用说明

    只有当beang的名称为aBean存在的时候,才会注入BBean。

    2 @ConditionalOnMissingBean

    2.1 基本案例

    @Bean
    @ConditionalOnMissingBean(name = "notExistsBean")
    public BeanToCreate createOneBean() {
        return new BeanToCreate("notExistsBean");
    }

    2.2 使用说明

    只有当bean名称为notExistsBean不存在的时候,BeanToCreate类型的bean才会被创建,和@ConditionalOnBean的使用方式相反

    3 @ConditionalOnClass

    3.1 基本使用案例

    @Bean
    @ConditionalOnClass(DependedClz.class)
    public InjectIfClzExists injectIfClzExists() {
        return new InjectIfClzExists("dependedClz");
    }

    3.2 使用说明

    只有当Class为DependedClz.class存在的时候,才会注入类型为InjectIfClzExists的bean,使用上和@ConditionalOnBean有些类似。

    4 @ConditionalOnMissingClass

    4.1 使用案例

    @Bean
    @ConditionalOnMissingClass("com.shihuc.bean.clz.DependedClz")
    public InjectIfClzNotExists injectIfClzNotExists() {
        return new InjectIfClzNotExists("com.shihuc.bean.clz.DependedClz");
    }

    4.2 使用说明

    只有当类com.shihuc.bean.clz.DependedClz不存在的时候,才会注入类型为InjectIfClzNotExists的bean。

    5 @ConditionalOnProperty

    5.1 基本使用案例

    springboot的项目中配置文件application.properties文件中有如下配置:

    #.....
    section.condition_field=noti
    section.condition_property=test
    #...
    @Bean
    @ConditionalOnProperty("section.condition_field")
    public PropertyExistBean propertyExistBean() {
        return new PropertyExistBean("section.condition_field");
    }

    5.2 使用说明

    主要是根据配置文件中的参数,来决定是否需要创建这个bean,这样就给了我们一个根据配置来控制Bean的选择的手段了,这个非常的好用。因为application.properties文件中存在section.condition_field这个属性,所以,PropertyExistBean这个bean会被创建出来。

    5.3 扩展用法

    5.3.1 注解定义

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.TYPE, ElementType.METHOD })
    @Documented
    @Conditional(OnPropertyCondition.class)
    public @interface ConditionalOnProperty {
    
        /**
         * Alias for {@link #name()}.
         * @return the names
         * 注意,这个value和name不能同时使用
         */
        String[] value() default {};
    
        /**
         * A prefix that should be applied to each property. The prefix automatically ends
         * with a dot if not specified.
         * @return the prefix
         */
        String prefix() default "";
    
        /**
         * The name of the properties to test. If a prefix has been defined, it is applied to
         * compute the full key of each property. For instance if the prefix is
         * {@code app.config} and one value is {@code my-value}, the full key would be
         * {@code app.config.my-value}
         * <p>
         * Use the dashed notation to specify each property, that is all lower case with a "-"
         * to separate words (e.g. {@code my-long-property}).
         * @return the names
         */
        String[] name() default {};
    
        /**
         * The string representation of the expected value for the properties. If not
         * specified, the property must <strong>not</strong> be equal to {@code false}.
         * @return the expected value
         */
        String havingValue() default "";
    
        /**
         * Specify if the condition should match if the property is not set. Defaults to
         * {@code false}.
         * @return if should match if the property is missing
         */
        boolean matchIfMissing() default false;
    }

    当我想实现配置文件中存在属性aaa.bbb且其属性的值为ccc时,才注入bean实例DDDD(名为dddd)。

    @Bean("dddd")
    @ConditionalOnProperty(value="aaa.bbbb", havingValue="ccc")
    public DDDD propertyExistBean() {
        return new DDDD("aaa.bbb");
    }

    6 @ConditionalOnExpression

    6.1 使用案例

    配置文件application.properties中存在下面的配置内容:

    conditional.flag=true

    java对应代码:

    @Bean
    @ConditionalOnExpression("#{'true'.equals(environment['conditional.flag'])}")
    public ExpressTrueBean expressTrueBean() {
        return new ExpressTrueBean("express true");
    }

    6.2 使用说明

    相比较前面的Bean,Class是否存在,配置参数property是否存在或者有某个值而言,这个依赖SPEL表达式的,使用起来就功能显得更加强大了;其主要就是执行Spel表达式,根据返回的true/false来判断是否满足条件。

    第二大点: spring基于Condition接口和@Conditional注解进行注入bean

    这个相当于是条件注入bean的根源解决方案,上述其他几个ConditionalOnXXXX的注解,都是这个Conditional注解的具体场景的定制版,假如没有能够满足自己的应用场景的,或者说要自己实现一个比较特殊的条件注入呢,例如多个条件同时成立之类,怎么办呢,那就需要通过实现Condition接口然后基于@Conditional注解进行使用了。

    1 @Conditional注解定义

    //此注解可以标注在类和方法上
    @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,且入参是一个Condition的Class的数组。

    2 Condition是什么?

    @FunctionalInterface
    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);
    }

    3. 使用案例

    假设属性配置文件中,有两个环境参数,一个是温度temp,一个是湿度humi,只有当温度高于30度,且湿度大于50个点时,启用Linux,当温度小于30度且湿度小于50个点时,启用Windows,这个只是为了说明在一个@Conditional里面将多个条件满足该如何实现,还有其他的业务场景,可以参照这个案例。

    3.1 配置文件参数

    #温度数据,摄氏温度
    conditional.prop.temp=29
    #湿度数据,百分比,这里不带百分号,相当于扩大100倍,使用的时候除以100
    conditional.prop.humi=51

    3.2 定义bean

    有一个HeWoBean的接口,以及两个实现类HelloBean和WorldBean。

    /**
     * @Author: chengsh05
     * @Date: 2019/8/29 16:17
     */
    public interface HeWoBean {
    
        public String toString();
    }
    /**
     * @Author: chengsh05
     * @Date: 2019/8/29 15:52
     */
    public class HelloBean implements HeWoBean {
    
        public String getHhh() {
            return hhh;
        }
    
        public void setHhh(String hhh) {
            this.hhh = hhh;
        }
    
        public String getEee() {
            return eee;
        }
    
        public void setEee(String eee) {
            this.eee = eee;
        }
    
        String hhh;
    
        String eee;
    
        public HelloBean(String hh, String ee) {
            this.hhh = hh;
            this.eee = ee;
        }
    
        @Override
        public String toString() {
            return this.hhh + ", " + this.eee;
        }
    }
    /**
     * @Author: chengsh05
     * @Date: 2019/8/29 15:54
     */
    public class WorldBean implements HeWoBean {
        public String getWww() {
            return www;
        }
    
        public void setWww(String www) {
            this.www = www;
        }
    
        public String getOoo() {
            return ooo;
        }
    
        public void setOoo(String ooo) {
            this.ooo = ooo;
        }
    
        String www;
        String ooo;
    
        public WorldBean(String ww, String oo) {
            this.www = ww;
            this.ooo = oo;
        }
    
        @Override
        public String toString() {
            return this.www + ", " + this.ooo;
        }
    }

    3. condition接口实现类及@Conditional应用

    /**
     * @Author: chengsh05
     * @Date: 2019/8/29 9:08
     * @Description: 配置信息中,温度和湿度条件满足的时候,即当温度temp大于30度,湿度大于50%,启用Linux
     */
    public class LinuxTime implements Condition {
    
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            String tempStr = context.getEnvironment().getProperty("conditional.prop.temp");
            float temp = Float.valueOf(tempStr);
            String humiStr = context.getEnvironment().getProperty("conditional.prop.humi");
            float humi = Float.valueOf(humiStr);
            if(temp > 30 && humi > 60){
                return true;
            }
            return false;
        }
    }
    /**
     * @Author: chengsh05
     * @Date: 2019/8/29 9:07
     * @Description: 配置信息中,温度和湿度条件满足的时候,即当温度temp小于30度,湿度小于50%,启用windows
     */
    public class WindowsTime implements Condition {
    
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            String tempStr = context.getEnvironment().getProperty("conditional.prop.temp");
            float temp = Float.valueOf(tempStr);
            String humiStr = context.getEnvironment().getProperty("conditional.prop.humi");
            float humi = Float.valueOf(humiStr);
            if(temp < 30 && humi < 60){
                return true;
            }
            return false;
        }
    }
    /**
     * @Author: chengsh05
     * @Date: 2019/8/29 15:50
     */
    @Configuration
    public class MyConditional {
    
        @Bean("mybean")
        @Conditional(LinuxTime.class)
        public HelloBean createHello() {
            return new HelloBean("hello", "Linux");
        }
    
        @Bean("mybean")
        @Conditional(WindowsTime.class)
        public WorldBean createWorld() {
            return new WorldBean("world", "Windows");
        }
    }

    4.应用验证

    /**
     * @Author: chengsh05
     * @Date: 2019/8/29 16:03
     */
    @Controller
    @RequestMapping("/condition")
    public class ConditionalController {
    
        @Autowired
        @Qualifier("mybean")
        private HeWoBean myBean;
    
        @RequestMapping("/check")
        @ResponseBody
        public void check() {
            System.out.println("///||||\\ ==> " + myBean.toString());
        }
    }

    分析来看,LinuxTime因为没有满足温度temp和湿度humi的条件(即在配置文件中的参数),所以,LinuxTime这个bean在MyConditional这个配置类中是不会被创建出来的,即最终HeHoBean这个就只有WorldBean被注入到spring容器了。打印的日志,也证实了这个。

    ///||||\ ==> world, Windows

    总结:

    1. 基于springboot内置的条件注解,开发一些应用,基于某种条件进行bean的注入还是很方便的,基本可以解决大部分常见场景需求。

    2. 基于内置的条件注入注解的组合使用,可以实现多条件约束的bean的注入需求,只有多个条件注入条件都成立时,对应的bean才会被注入到spring的容器。

    3. 内置注解不管单独用还是组合使用,都不能搞定你的应用需求,那么可以选择实现condition接口,基于@Conditional注解来自己完成条件注入的需求了。

    到这里,看官们,你是否有结论了,关于前面提到的,session共享模块,基于配置参数开关量,灵活切换模块工作在header模式还是cookie模式?答案是可以的,至于如何实现,结合我这里的介绍,是能得到答案的。

  • 相关阅读:
    每日一题(文化课)一题---------19/7/30
    XXXXX,这个域名
    CF1037E. Trips
    bzoj3124: [Sdoi2013]直径 树形dp two points
    luogu P3952 时间复杂度 模拟
    AGC016D
    CF444E. DZY Loves Planting
    模拟赛
    HDU 3949 XOR [线性基|高斯消元]
    博客搬家
  • 原文地址:https://www.cnblogs.com/shihuc/p/11433062.html
Copyright © 2020-2023  润新知