• Spring Boot 2.0 的配置绑定类Bindable居然如此强大


    1. 前言

    在开发Spring Boot应用时会用到根据条件来向Spring IoC容器注入Bean。比如配置文件存在了某个配置属性才注入Bean

    根据配置属性来动态注入Bean

    图中红色的部分是说,只有ali.pay.v1.app-id存在于Spring的环境配置中时这个@Configuration标记的类才能注入Spring IoC

    这里面的@ConditionalOnProperty就是条件注解系列的一种。它还有很多种来满足各种场景的条件注解:

    条件注解家族

    其实数量远不止截图中这几个,在Spring 家族的其它框架中也有实现。

    这里扯得有点远了,今天不是来讲这些条件控制注解的用法的,只是我发现了一个使用条件注解@ConditionalOnProperty无法解决的问题。

    条件注入参考往期:Spring Boot 2 实战:使用 @Condition 注解来根据条件注入 Bean

    2. 配置文件存在Map结构的场景

    下面是一段配置文件:

    app:
     v1:
      foo:
        name: felord.cn
        description: 码农小胖哥
      bar:
        name: ooxx.cn
        description: xxxxxx
    

    对应配置类:

    @Data
    @ConfigurationProperties("app")
    public class AppProperties {
        /**
         *  
         */
        private Map<String, V1> v1 = new HashMap<>();
    
        /**
         *  
         *
         * @author felord.cn
         * @since 1.0.0.RELEASE
         */
        @Data
        public static class V1 {
            /**
             * name
             */
            private String name;
            /**
             * description
             */
            private String description;
    
        }
    }
    

    特殊之处来了,yml配置里的 foobar其实是作为Map中的key来标识V1的,和其它配置参数不同,这个key用户可以随意定义一个String来标识,可能是foo,可能是bar,完全根据开发者的喜好进行主观定义。这个时候你想根据app.v1.*.name(暂时用通配符*)来进行@ConditionalOnProperty判断是行不通的,因为你不确定*的值,该怎么办呢?

    3. 解决方案

    这里我花了一天的时间去摸索,最开始我认为Spring提供通配符(app.v1.*.name)甚至是SpringEL表达式可以拿到,但是搞了半天无功而返。

    突然我想到之前看Spring Security OAuth2源码中有类似的逻辑。用过Spring Security OAuth2相关的都知道Spring Security OAuth2也要求用户自定义一个key来标识自己的OAuth2客户端。比如我用Gitee的:

    spring:
      security:
        oauth2:
          client:
            registration:
              gitee:
                client-id: xxxxxx
                client-secret: xxxxx
    

    这里的key就是gitee,当然这根据开发者心情决定,甚至你用zhangshan作为key都可以。

    Spring Security OAuth2 提供了相关的条件注入思路,下面是其条件注入判断的核心类:

    public class ClientsConfiguredCondition extends SpringBootCondition {
    
       private static final Bindable<Map<String, OAuth2ClientProperties.Registration>> STRING_REGISTRATION_MAP = Bindable
             .mapOf(String.class, OAuth2ClientProperties.Registration.class);
    
       @Override
       public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
          ConditionMessage.Builder message = ConditionMessage.forCondition("OAuth2 Clients Configured Condition");
          Map<String, OAuth2ClientProperties.Registration> registrations = getRegistrations(context.getEnvironment());
          if (!registrations.isEmpty()) {
             return ConditionOutcome.match(message.foundExactly("registered clients " + registrations.values().stream()               .map(OAuth2ClientProperties.Registration::getClientId).collect(Collectors.joining(", "))));
          }
          return ConditionOutcome.noMatch(message.notAvailable("registered clients"));
       }
    
       private Map<String, OAuth2ClientProperties.Registration> getRegistrations(Environment environment) {
          return Binder.get(environment).bind("spring.security.oauth2.client.registration", STRING_REGISTRATION_MAP)
                .orElse(Collections.emptyMap());
       }
    
    }
    

    显然OAuth2ClientProperties的结构和我们要验证的AppProperties结构是一样的。所以上面的逻辑是可以抄过来的,它可以将环境配置中的带有不确定key的配置绑定到我们的配置类AppProperties中。核心的绑定逻辑是这一段:

    Binder.get(environment).bind("spring.security.oauth2.client.registration", STRING_REGISTRATION_MAP)
    

    首先通过Bindable来声明一个可绑定的数据结构,这里调用了mapOf方法声明了一个Map的数据绑定结构。然后通过绑定的具体操作对象Binder从配置环境接口Environment中提取了spring.security.oauth2.client.registration开头的配置属性并注入到Map中去。既然我们能够获取到了Map,根据什么策略判断就完全掌握在我们手中了。

    Bindable为Spring Boot 2.0提供的数据绑定新特性,有兴趣可从spring.io获取更多信息。

    接下来不用我说了吧,照葫芦画瓢还有谁不会呢?配合@Conditional注解就能实现根据app.v1下参数的实际情况来动态的进行Bean注入。

    4. 总结

    今天利用Spring Boot 2.0的数据绑定特性解决了一个实际需求,花了不少时间。当我们解决问题陷入困境时,首先要去想想有没有类似场景以及对应的解决方案。这同样说明平时的积累很重要,很多粉丝的问题其实公众号都有讲过,所以处处留心解释学问。多多留意:码农小胖哥,共同学习,共同进步。

    关注公众号:Felordcn 获取更多资讯

    个人博客:https://felord.cn

  • 相关阅读:
    数据库学习笔记5---MySQL字符串函数、日期时间函数
    关于hibernate的AnnotationConfiguration的问题
    浅谈Java web 中request的setAttribute()用法
    JAVA常见面试题之Forward和Redirect的区别
    JSP页面中<%!%>与<%%>与<%=%>
    Servlet的生命周期
    JavaEE学习路线图
    java web项目WEB-INF与META-INF的作用
    iOS-申请邓白氏编码的超详细流程介绍
    从高版本JDK换成低版本JDK报错
  • 原文地址:https://www.cnblogs.com/felordcn/p/14270268.html
Copyright © 2020-2023  润新知