Springboot Property Binder

用API获取property 的值在springboot 1.x版本中,通常从Environment对象读取

  //判断是否包含键值
  boolean containsProperty(String key);
  
  //获取属性值,如果获取不到返回null
  String getProperty(String key);
  
  //获取属性值,如果获取不到返回缺省值
  String getProperty(String key, String defaultValue);
  
  //获取属性对象;其转换和Converter有关,会根据sourceType和targetType查找转换器
  <T> T getProperty(String key, Class<T> targetType);

但是这样的方式功能有限,还需要自己进行字符串到对象的转换。

Binder

Springboot 2.x新引入的类,负责处理对象与多个ConfigurationPropertySource(属性)之间的绑定,比Environment类好用很多,可以非常方便地进行类型转换,以及提供回调方法介入绑定的各个阶段进行深度定制。

 //绑定对象
  MailPropertiesC propertiesC = Binder.get(environment) //首先要绑定配置器
      //再将属性绑定到对象上
      .bind( "binder.sample.test", Bindable.of(MailPropertiesC.class) ).get(); //再获取实例
      
  //绑定Map
  Map<String,Object> propMap = Binder.get(environment)
      .bind( "fish.jdbc.datasource",Bindable.mapOf(String.class, Object.class) ).get();
      
  //绑定List
  List<String> list = Binder.get(environment)
      .bind( "binder.sample.list",Bindable.listOf(String.class) ).get();
      
  //转换以及默认值
  String datestr = (String) Binder.get(environment)
      .bind( "binder.sample.date",Bindable.of(String.class) )
          //映射为大写
          .map(String::toUpperCase)
          //默认值
          .orElse("not date string");
           
  //绑定过程回调函数,高度定制
  LocalDate str = Binder.get(environment)
      .bind("binder.sample.date", Bindable.of(LocalDate.class), new BindHandler() {
   
        @Override
        public <T> Bindable<T> onStart(ConfigurationPropertyName name, 
              Bindable<T> target, BindContext context) {
          log.info("绑定开始{}",name);
          return target;
        }
        
        @Override
        public Object onSuccess(ConfigurationPropertyName name, Bindable<?> target, 
              BindContext context, Object result) {
          log.info("绑定成功{}",target.getValue());
          return result;
        }
   
        @Override
        public Object onFailure(ConfigurationPropertyName name, Bindable<?> target, 
              BindContext context, Exception error) throws Exception {
          log.info("绑定失败{}",name);
          return "没有找到匹配的属性";
        }
   
        @Override
        public void onFinish(ConfigurationPropertyName name, Bindable<?> target, 
              BindContext context, Object result) throws Exception {
          log.info("绑定结束{}",name);
        }
      }).get();

那么我们看看Binder 是怎么工作的。org.springframework.boot.context.properties.bind.Binder

首先看和Environment的绑定。

    public static Binder get(Environment environment) {
        return get(environment, null);
    }

    /**
     * Create a new {@link Binder} instance from the specified environment.
     * @param environment the environment source (must have attached
     * {@link ConfigurationPropertySources})
     * @param defaultBindHandler the default bind handler to use if none is specified when
     * binding
     * @return a {@link Binder} instance
     * @since 2.2.0
     */
    public static Binder get(Environment environment, BindHandler defaultBindHandler) {
        Iterable<ConfigurationPropertySource> sources = ConfigurationPropertySources.get(environment);
        PropertySourcesPlaceholdersResolver placeholdersResolver = new PropertySourcesPlaceholdersResolver(environment);
        return new Binder(sources, placeholdersResolver, null, null, defaultBindHandler);
    }

之后是构造函数, 这时候初始化了一个类型转换Service conversionService, 它的实例 ApplicationConversionService.getSharedInstance();

    public Binder(Iterable<ConfigurationPropertySource> sources, PlaceholdersResolver placeholdersResolver,
            ConversionService conversionService, Consumer<PropertyEditorRegistry> propertyEditorInitializer,
            BindHandler defaultBindHandler, BindConstructorProvider constructorProvider) {
        Assert.notNull(sources, "Sources must not be null");
        this.sources = sources;
        this.placeholdersResolver = (placeholdersResolver != null) ? placeholdersResolver : PlaceholdersResolver.NONE;
        this.conversionService = (conversionService != null) ? conversionService
                : ApplicationConversionService.getSharedInstance();
        this.propertyEditorInitializer = propertyEditorInitializer;
        this.defaultBindHandler = (defaultBindHandler != null) ? defaultBindHandler : BindHandler.DEFAULT;
        if (constructorProvider == null) {
            constructorProvider = BindConstructorProvider.DEFAULT;
        }
        ValueObjectBinder valueObjectBinder = new ValueObjectBinder(constructorProvider);
        JavaBeanBinder javaBeanBinder = JavaBeanBinder.INSTANCE;
        this.dataObjectBinders = Collections.unmodifiableList(Arrays.asList(valueObjectBinder, javaBeanBinder));
    }

请注意, ApplicationConversionService.getSharedInstance(); 不是在这个时候才被初始化的。而是SpringApplication 在启动的时候configureEnvironment

    protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
        if (this.addConversionService) {
            ConversionService conversionService = ApplicationConversionService.getSharedInstance();
            environment.setConversionService((ConfigurableConversionService) conversionService);
        }
        configurePropertySources(environment, args);
        configureProfiles(environment, args);
    }

在本文后面会介绍如何加一个converter service。 在Context 下面在bind 方法中构造了Context 对象,

        Context() {
            this.converter = BindConverter.get(Binder.this.conversionService, Binder.this.propertyEditorInitializer);
        }


    private <T> T bind(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler, boolean create) {
        Assert.notNull(name, "Name must not be null");
        Assert.notNull(target, "Target must not be null");
        handler = (handler != null) ? handler : this.defaultBindHandler;
        Context context = new Context();
        return bind(name, target, handler, context, false, create);
    }

BindConverter 是重要的类型转换接口,它里面封装了一个CompositeConversionService 是TypeConverterConversionService,ApplicationConversionService 的组合。


    private BindConverter(ConversionService conversionService,
            Consumer<PropertyEditorRegistry> propertyEditorInitializer) {
        Assert.notNull(conversionService, "ConversionService must not be null");
        List<ConversionService> conversionServices = getConversionServices(conversionService,
                propertyEditorInitializer);
        this.conversionService = new CompositeConversionService(conversionServices);
    }

    private List<ConversionService> getConversionServices(ConversionService conversionService,
            Consumer<PropertyEditorRegistry> propertyEditorInitializer) {
        List<ConversionService> services = new ArrayList<>();
        services.add(new TypeConverterConversionService(propertyEditorInitializer));
        services.add(conversionService);
        if (!(conversionService instanceof ApplicationConversionService)) {
            services.add(ApplicationConversionService.getSharedInstance());
        }
        return services;
    }

之后就寻找PropertyResource, 然后进行转换了。



    private <T> T bind(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler, Context context,
            boolean allowRecursiveBinding, boolean create) {
        try {
            Bindable<T> replacementTarget = handler.onStart(name, target, context);
            if (replacementTarget == null) {
                return handleBindResult(name, target, handler, context, null, create);
            }
            target = replacementTarget;
            Object bound = bindObject(name, target, handler, context, allowRecursiveBinding);
            return handleBindResult(name, target, handler, context, bound, create);
        }
        catch (Exception ex) {
            return handleBindError(name, target, handler, context, ex);
        }
    }

    private <T> T handleBindResult(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler,
            Context context, Object result, boolean create) throws Exception {
        if (result != null) {
            result = handler.onSuccess(name, target, context, result);
            result = context.getConverter().convert(result, target);
        }
        if (result == null && create) {
            result = create(target, context);
            result = handler.onCreate(name, target, context, result);
            result = context.getConverter().convert(result, target);
            Assert.state(result != null, () -> "Unable to create instance for " + target.getType());
        }
        handler.onFinish(name, target, context, result);
        return context.getConverter().convert(result, target);
    }

从上面代码可以看出,一个bind 基本由BindHandler来执行, 得出result由BindConverter 来转换类型。

Add a Customization ConverterService

如何在解析Property 时候来支持我们一个新的类型,比如给一个字符串,自动构建一个URL。从上文看到,我们可以从这段代码入手

    protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
        if (this.addConversionService) {
            ConversionService conversionService = ApplicationConversionService.getSharedInstance();
            environment.setConversionService((ConfigurableConversionService) conversionService);
        }
        configurePropertySources(environment, args);
        configureProfiles(environment, args);
    }

我们应可以对environment里的conversionService,做个代理就可以实现增加一个conversion service了。


public class URLConfigurableConversionService implements ConfigurableConversionService {

    private final ConfigurableConversionService m_delegate;

    public RaptorIOConfigurableConversionService(ConfigurableConversionService delegate) {
        m_delegate = delegate;
    }

    @Override
    public <S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter) {
        m_delegate.addConverter(sourceType, targetType, converter);
    }

    @Override
    public void addConverter(Converter<?, ?> converter) {
        m_delegate.addConverter(converter);
    }

    @Override
    public void addConverter(GenericConverter converter) {
        m_delegate.addConverter(converter);
    }

    @Override
    public void addConverterFactory(ConverterFactory<?, ?> converterFactory) {
        m_delegate.addConverterFactory(converterFactory);
    }

    @Override
    public boolean canConvert(Class<?> sourceType, Class<?> targetType) {
        return m_delegate.canConvert(sourceType, targetType);
    }

    @Override
    public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) {
        return m_delegate.canConvert(sourceType, targetType);
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T convert(Object source, Class<T> targetType) {
        //only try to convert to URL in case of file existence case
        if (targetType.equals(URL.class) && source instanceof String) {
            try {
                File file = new File((String) source); //NOSONAR
                if (file.exists()) {
                    return (T) file.toURI().toURL();
                }
            } catch (Exception e) { //NOSONAR
                //ignore it
            }
        }
        return m_delegate.convert(source, targetType);
    }

    @Override
    public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
        return m_delegate.convert(source, sourceType, targetType);
    }

    @Override
    public void removeConvertible(Class<?> sourceType, Class<?> targetType) {
        m_delegate.removeConvertible(sourceType, targetType);
    }
}

实现一个自己的conversion service ,用environment 的conversion service作为代理, 在onApplicationEvent 时设置进去。

   public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
       try {
           ConfigurableEnvironment env = event.getEnvironment()
           //setup raptorio file to URL conversion service
           //to support "URL configFileUrl = getProperty(String key, URL.class)"
           URLConfigurableConversionService conversionService = new URLConfigurableConversionService(env.getConversionService());
           env.setConversionService(conversionService);
       } catch (Exception e) {
           logger.error("Failed to init environment variables.", e);
       }
   }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容