美文网首页
聊聊如何利用spring实现服务隔离

聊聊如何利用spring实现服务隔离

作者: linyb极客之路 | 来源:发表于2024-05-20 09:24 被阅读0次

    前言

    假设我们有个场景,我们需要实现服务之间的数据隔离、配置隔离、依赖的spring bean之间隔离。大家会有什么实现思路?今天给大家介绍spring-cloud-context里面有个NamedContextFactory可以达到上面的效果

    NamedContextFactory简介

    NamedContextFactory可以实现子容器,通过它创建子容器,然后通过NamedContextFactory.Specification可以定制子容器会用到的bean。

    所以为什么通过NamedContextFactory可以达到数据隔离、配置隔离、依赖的spring bean之间隔离,本质就是利用NamedContextFactory为不同的服务,创建出不同的子容器,子容器之间彼此不共享,从而达到隔离的效果

    下面通过一个示例来讲解

    示例

    注: 示例就模拟一个用户注册成功后发送华为云短信,下单成功后发送阿里云短信为例子

    1、模拟定义短信接口

    public interface SmsService {
    
        void send(String phone, String content);
    }
    
    

    2、模拟定义相应短信实现类

    public class DefaultSmsService implements SmsService {
        @Override
        public void send(String phone, String content) {
            System.out.printf("send to %s content %s used default sms%n", phone, content);
        }
    }
    
    
    public class AliyunSmsService implements SmsService {
        @Override
        public void send(String phone, String content) {
            System.out.printf("send to %s content %s used aliyun sms%n", phone, content);
        }
    }
    
    public class HuaWeiSmsService implements SmsService {
        @Override
        public void send(String phone, String content) {
            System.out.printf("send to %s content %s used huawei sms%n", phone, content);
        }
    }
    
    

    3、自定义短信默认配置类

    @Configuration
    public class DefaultSmsClientConfiguration {
    
    
        @Bean
        @ConditionalOnMissingBean
        public SmsService smsService(){
            return new DefaultSmsService();
        }
    
    }
    

    4、定制短信需要的子容器NamedContextFactory.Specification

    public class SmsClientSpecification implements NamedContextFactory.Specification{
        private String name;
    
        private Class<?>[] configuration;
    
        public SmsClientSpecification() {
        }
    
        public SmsClientSpecification(String name, Class<?>[] configuration) {
            this.name = name;
            this.configuration = configuration;
        }
    
        @Override
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        @Override
        public Class<?>[] getConfiguration() {
            return configuration;
        }
    
        public void setConfiguration(Class<?>[] configuration) {
            this.configuration = configuration;
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            SmsClientSpecification that = (SmsClientSpecification) o;
            return Arrays.equals(configuration, that.configuration)
                    && Objects.equals(name, that.name);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(configuration, name);
        }
    
        @Override
        public String toString() {
            return new StringBuilder("SmsSpecification{").append("name='")
                    .append(name).append("', ").append("configuration=")
                    .append(Arrays.toString(configuration)).append("}").toString();
        }
    }
    
    

    属性讲解

    name: 子容器的名称(示例中我们会把用户服务名和订单服务名当成子容器名称)

    configuration: name子容器需要的configuration

    NamedContextFactory.Specification的作用是当创建子容器时,如果容器的name匹配了Specification的name,则会加载 Specification对应Configuration类,并将Configuration类里面标注@Bean的返回值注入到子容器中

    5、为不同的服务创建不同的SmsClientSpecification并注入到spring容器中

    @Configuration
    @Import(SmsClientConfigurationRegistrar.class)
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface SmsClient {
    
        /**
         * Synonym for name (the name of the client).
         *
         * @see #name()
         * @return name of the Sms client
         */
        String value() default "";
    
        /**
         * The name of the sms client, uniquely identifying a set of client resources,
         * @return name of the Sms client
         */
        String name() default "";
    
        /**
         * A custom <code>@Configuration</code> for the sms client. Can contain override
         * <code>@Bean</code> definition for the pieces that make up the client
         */
        Class<?>[] configuration() default {};
    }
    
    
    @Configuration
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.TYPE })
    @Documented
    @Import(SmsClientConfigurationRegistrar.class)
    public @interface SmsClients {
    
        SmsClient[] value() default {};
    
        Class<?>[] defaultConfiguration() default {};
    
    }
    
    

    注: 利用import机制,将SmsClientSpecification注入到spring容器

    public class SmsClientConfigurationRegistrar implements ImportBeanDefinitionRegistrar {
    
        @Override
        public void registerBeanDefinitions(AnnotationMetadata metadata,
                BeanDefinitionRegistry registry) {
            Map<String, Object> attrs = metadata
                    .getAnnotationAttributes(SmsClients.class.getName(), true);
            if (attrs != null && attrs.containsKey("value")) {
                AnnotationAttributes[] clients = (AnnotationAttributes[]) attrs.get("value");
                for (AnnotationAttributes client : clients) {
                    registerClientConfiguration(registry, getClientName(client),
                            client.get("configuration"));
                }
            }
            if (attrs != null && attrs.containsKey("defaultConfiguration")) {
                String name;
                if (metadata.hasEnclosingClass()) {
                    name = "default." + metadata.getEnclosingClassName();
                }
                else {
                    name = "default." + metadata.getClassName();
                }
                registerClientConfiguration(registry, name,
                        attrs.get("defaultConfiguration"));
            }
            Map<String, Object> client = metadata
                    .getAnnotationAttributes(SmsClient.class.getName(), true);
            String name = getClientName(client);
            if (name != null) {
                registerClientConfiguration(registry, name, client.get("configuration"));
            }
        }
    
        private String getClientName(Map<String, Object> client) {
            if (client == null) {
                return null;
            }
            String value = (String) client.get("value");
            if (!StringUtils.hasText(value)) {
                value = (String) client.get("name");
            }
            if (StringUtils.hasText(value)) {
                return value;
            }
            throw new IllegalStateException(
                    "Either 'name' or 'value' must be provided in @SmsClient");
        }
    
        private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
                Object configuration) {
            BeanDefinitionBuilder builder = BeanDefinitionBuilder
                    .genericBeanDefinition(SmsClientSpecification.class);
            builder.addConstructorArgValue(name);
            builder.addConstructorArgValue(configuration);
            registry.registerBeanDefinition(name + ".SmsClientSpecification",
                    builder.getBeanDefinition());
        }
    
    }
    
    

    6、创建短信NameContextFactory

    public class SmsClientNameContextFactory extends NamedContextFactory<SmsClientSpecification> {
    
        public SmsClientNameContextFactory() {
            super(DefaultSmsClientConfiguration.class, "sms", "sms.client.name");
        }
    
        public SmsService getSmsService(String serviceName) {
            return getInstance(serviceName, SmsService.class);
        }
    }
    
    

    注: super三个参数讲解

    public NamedContextFactory(Class<?> defaultConfigType, String propertySourceName,
                String propertyName) {
            this.defaultConfigType = defaultConfigType;
            this.propertySourceName = propertySourceName;
            this.propertyName = propertyName;
        }
    

    defaultConfigType: 默认配置类,NamedContextFactory创建子容器时,默认就会加载该配置类,该配置类主要用来做兜底,当找不到容器为name的configuration,则会使用该配置类
    propertySourceName: 给propertySource取个名称
    propertyName: 子容器可以通过读取配置propertyName来获取容器名。当创建子容器时通常会提供子容器的容器name。子容器中的Environment会被写入一条配置,sms.client.name=容器name

    7、将SmsClientNameContextFactory注入到spring容器

       @Bean
        @ConditionalOnMissingBean
        public SmsClientNameContextFactory smsClientNameContextFactory(@Autowired(required = false) List<SmsClientSpecification> smsSpecifications){
            SmsClientNameContextFactory smsClientNameContextFactory = new SmsClientNameContextFactory();
            smsClientNameContextFactory.setConfigurations(smsSpecifications);
            return smsClientNameContextFactory;
        }
    

    8、创建不同的短信配置类

    public class AliyunSmsClientConfiguration {
    
        @ConditionalOnMissingBean
        @Bean
        public SmsService smsService() {
           return new AliyunSmsService();
        }
    }
    
    
    public class HuaWeiSmsClientConfiguration {
    
        @ConditionalOnMissingBean
        @Bean
        public SmsService smsService() {
           return new HuaWeiSmsService();
        }
    }
    
    

    注: 因为上述配置只需被子容器加载,因此不需要加 @Configuration

    9、为用户服务和订单服务指定NamedContextFactory.Specification

    
    @Configuration
    @SmsClients(value = {@SmsClient(name = OrderService.SERVICE_NAME, configuration = AliyunSmsClientConfiguration.class),
            @SmsClient(name = UserService.SERVICE_NAME, configuration = HuaWeiSmsClientConfiguration.class)})
    public class SmsClientAutoConfiguration {
    }
    
    

    10、测试

    模拟用户注册

    @Service
    @RequiredArgsConstructor
    public class UserService {
    
        private final ApplicationContext applicationContext;
    
        public static final String SERVICE_NAME = "userService";
    
        public void registerUser(String userName, String password,String mobile){
            System.out.println("注册用户"+userName+"成功");
            UserRegisterEvent event = new UserRegisterEvent(userName,password,mobile);
            applicationContext.publishEvent(event);
        }
    }
    
    
    @Component
    @RequiredArgsConstructor
    public class UserRegisterListener {
    
        private final SmsClientNameContextFactory smsClientNameContextFactory;
    
    
        @EventListener
        @Async
        public void listener(UserRegisterEvent event) {
            SmsService smsService = smsClientNameContextFactory.getSmsService(UserService.SERVICE_NAME);
            smsService.send(event.getMobile(), "恭喜您注册成功!初始密码为:"+event.getPassword()+",请尽快修改密码!");
        }
    }
    
    

    核心:

     SmsService smsService = smsClientNameContextFactory.getSmsService(UserService.SERVICE_NAME);
    

    和 @SmsClient(name = UserService.SERVICE_NAME)对应起来

    运行查看控制台

    50b5cb1df821d1b6cc46c56cffce32b1_6e37b104a2274a8699051c17ec845f54.png

    当服务名不匹配时,再观察控制台


    97588a4e1758b1f571be34be8184f1e8_8328fd6e2538cbb250df093ac7b6a8fa.png

    发现此时是走默认配置

    总结

    本文主要是聊下通过NamedContextFactory来实现服务隔离,核心点就是通过创建不同子容器进行隔离。这种方式在ribbon、openfeign、以及loadbalancer都有类似的实现,感兴趣朋友可以查阅其源码。不过这边有细节点需要注意,因为NamedContextFactory默认是懒加载创建子容器,所以可能第一次调用会比较慢。这也是ribbon第一次调用慢的原因

    demo链接

    https://github.com/lyb-geek/springboot-learning/tree/master/springboot-named-context-factory

    相关文章

      网友评论

          本文标题:聊聊如何利用spring实现服务隔离

          本文链接:https://www.haomeiwen.com/subject/ybnnfjtx.html