• shardingsphere做了读写分离做了主从配置脱敏无效分析


    软件版本信息

    <dependency>
        <groupId>org.apache.shardingspheregroupId>
        <artifactId>sharding-jdbc-spring-boot-starterartifactId>
        <version>4.1.1version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    问题描述

    整合SpringBoot框架的时候,做了主从读写分离,脱敏配置一直无效。其实我们项目是先使用了读写分离,还使用了Mybatis-plus,所以影响因素很多的。发现问题后,自己先搞了一个简单的demo,去掉Mybatis-plus和读写分离,发现脱敏是有用的。加上读写分离就失效了。所以应该是读写分离脱敏配置失效导致的。最后分析源码,找原因。

    原因分析

    通过源码分析,知道shardingsphere在注入配置信息的核心类是org.apache.shardingsphere.shardingjdbc.spring.boot.SpringBootConfiguration
    代码如下:

    @Configuration
    @ComponentScan("org.apache.shardingsphere.spring.boot.converter")
    @EnableConfigurationProperties({
            SpringBootShardingRuleConfigurationProperties.class,
            SpringBootMasterSlaveRuleConfigurationProperties.class, SpringBootEncryptRuleConfigurationProperties.class,
            SpringBootPropertiesConfigurationProperties.class, SpringBootShadowRuleConfigurationProperties.class})
    @ConditionalOnProperty(prefix = "spring.shardingsphere", name = "enabled", havingValue = "true", matchIfMissing = true)
    @AutoConfigureBefore(DataSourceAutoConfiguration.class)
    @RequiredArgsConstructor
    public class SpringBootConfiguration implements EnvironmentAware {
        
        private final SpringBootShardingRuleConfigurationProperties shardingRule;
        
        private final SpringBootMasterSlaveRuleConfigurationProperties masterSlaveRule;
        
        private final SpringBootEncryptRuleConfigurationProperties encryptRule;
        
        private final SpringBootShadowRuleConfigurationProperties shadowRule;
        
        private final SpringBootPropertiesConfigurationProperties props;
        
        private final Map<String, DataSource> dataSourceMap = new LinkedHashMap<>();
        
        private final String jndiName = "jndi-name";
        
        /**
         * Get sharding data source bean.
         *
         * @return data source bean
         * @throws SQLException SQL exception
         */
        @Bean
        @Conditional(ShardingRuleCondition.class)
        public DataSource shardingDataSource() throws SQLException {
            return ShardingDataSourceFactory.createDataSource(dataSourceMap, new ShardingRuleConfigurationYamlSwapper().swap(shardingRule), props.getProps());
        }
        
        /**
         * Get master-slave data source bean.
         *
         * @return data source bean
         * @throws SQLException SQL exception
         */
        @Bean
        @Conditional(MasterSlaveRuleCondition.class)
        public DataSource masterSlaveDataSource() throws SQLException {
            return MasterSlaveDataSourceFactory.createDataSource(dataSourceMap, new MasterSlaveRuleConfigurationYamlSwapper().swap(masterSlaveRule), props.getProps());
        }
        
        /**
         * Get encrypt data source bean.
         *
         * @return data source bean
         * @throws SQLException SQL exception
         */
        @Bean
        @Conditional(EncryptRuleCondition.class)
        public DataSource encryptDataSource() throws SQLException {
            return EncryptDataSourceFactory.createDataSource(dataSourceMap.values().iterator().next(), new EncryptRuleConfigurationYamlSwapper().swap(encryptRule), props.getProps());
        }
        
        /**
         * Get shadow data source bean.
         *
         * @return data source bean
         * @throws SQLException SQL exception
         */
        @Bean
        @Conditional(ShadowRuleCondition.class)
        public DataSource shadowDataSource() throws SQLException {
            return ShadowDataSourceFactory.createDataSource(dataSourceMap, new ShadowRuleConfigurationYamlSwapper().swap(shadowRule), props.getProps());
        }
    }    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    从源码中会发现shardingsphere会根据配置不同注入不同的DataSource ,其中DataSource有如下几种:

    1. ShardingDataSource:分片的数据源;
    2. MasterSlaveDataSource:主从读写分离的数据源;
    3. EncryptDataSource:数据脱敏的数据源;
    4. ShadowDataSource:影子表数据源;

    接下来咱们分析一下数据源的具体创建过程,因为本文只涉及到主从读写分离和数据脱敏相关,所以只分析这两个数据源的创建过程,其他两个各位大佬有兴趣自己去分析。

    ShardingDataSource创建过程

    其实创建数据源的过程的代码挺简单的,直接贴源码吧。

    package org.apache.shardingsphere.shardingjdbc.api;
    
    import lombok.AccessLevel;
    import lombok.NoArgsConstructor;
    import org.apache.shardingsphere.api.config.masterslave.MasterSlaveRuleConfiguration;
    import org.apache.shardingsphere.core.rule.MasterSlaveRule;
    import org.apache.shardingsphere.shardingjdbc.jdbc.core.datasource.MasterSlaveDataSource;
    
    import javax.sql.DataSource;
    import java.sql.SQLException;
    import java.util.Map;
    import java.util.Properties;
    
    /**
     * Master-slave data source factory.
     */
    @NoArgsConstructor(access = AccessLevel.PRIVATE)
    public final class MasterSlaveDataSourceFactory {
        
        /**
         * Create master-slave data source.
         *
         * @param dataSourceMap data source map
         * @param masterSlaveRuleConfig master-slave rule configuration
         * @param props props
         * @return master-slave data source
         * @throws SQLException SQL exception
         */
        public static DataSource createDataSource(final Map<String, DataSource> dataSourceMap, final MasterSlaveRuleConfiguration masterSlaveRuleConfig, final Properties props) throws SQLException {
            return new MasterSlaveDataSource(dataSourceMap, new MasterSlaveRule(masterSlaveRuleConfig), props);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    /*
     * Licensed to the Apache Software Foundation (ASF) under one or more
     * contributor license agreements.  See the NOTICE file distributed with
     * this work for additional information regarding copyright ownership.
     * The ASF licenses this file to You under the Apache License, Version 2.0
     * (the "License"); you may not use this file except in compliance with
     * the License.  You may obtain a copy of the License at
     *
     *     http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    
    package org.apache.shardingsphere.shardingjdbc.jdbc.core.datasource;
    
    import lombok.Getter;
    import org.apache.shardingsphere.core.rule.MasterSlaveRule;
    import org.apache.shardingsphere.shardingjdbc.jdbc.adapter.AbstractDataSourceAdapter;
    import org.apache.shardingsphere.shardingjdbc.jdbc.core.connection.MasterSlaveConnection;
    import org.apache.shardingsphere.shardingjdbc.jdbc.core.context.MasterSlaveRuntimeContext;
    import org.apache.shardingsphere.spi.NewInstanceServiceLoader;
    import org.apache.shardingsphere.underlying.route.decorator.RouteDecorator;
    
    import javax.sql.DataSource;
    import java.sql.SQLException;
    import java.util.Map;
    import java.util.Properties;
    
    /**
     * Master-slave data source.
     */
    @Getter
    public class MasterSlaveDataSource extends AbstractDataSourceAdapter {
        
        private final MasterSlaveRuntimeContext runtimeContext;
        
        static {
            NewInstanceServiceLoader.register(RouteDecorator.class);
        }
        
        public MasterSlaveDataSource(final Map<String, DataSource> dataSourceMap, final MasterSlaveRule masterSlaveRule, final Properties props) throws SQLException {
            super(dataSourceMap);
            runtimeContext = new MasterSlaveRuntimeContext(dataSourceMap, masterSlaveRule, props, getDatabaseType());
        }
        
        @Override
        public final MasterSlaveConnection getConnection() {
            return new MasterSlaveConnection(getDataSourceMap(), runtimeContext);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    创建过程非常简单,从创建过程会发现,核心的属性有如下几个:

    1. dataSourceMap:主从数据源集合;
    2. masterSlaveRule:主从规则配置;
    3. props:额外的属性配置;

    到这里会发现,主从数据源创建过程根本不会涉及到数据脱敏的配置信息。我又特意看了一下创建Connection和PreparedStatement也一样根本没有涉及到脱敏相关的配置,自然在使用了主从的时候就脱敏配置可能会失效,这里为啥是可能呢?后文再说。

    EncryptDataSource创建过程

    创建过程也是挺简单的,直接贴代码吧

    @Getter
    public class EncryptDataSource extends AbstractDataSourceAdapter {
        
        private final EncryptRuntimeContext runtimeContext;
        
        static {
            NewInstanceServiceLoader.register(SQLRewriteContextDecorator.class);
            NewInstanceServiceLoader.register(ResultProcessEngine.class);
        }
        
        public EncryptDataSource(final DataSource dataSource, final EncryptRule encryptRule, final Properties props) throws SQLException {
            super(dataSource);
            runtimeContext = new EncryptRuntimeContext(dataSource, encryptRule, props, getDatabaseType());
        }
        
        @Override
        public final EncryptConnection getConnection() throws SQLException {
            return new EncryptConnection(getDataSource().getConnection(), runtimeContext);
        }
        
        /**
         * Get data source.
         *
         * @return data source
         */
        public DataSource getDataSource() {
            return getDataSourceMap().values().iterator().next();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    这里就不一样了,会发现有脱敏相关配置的关联,即:encryptRule 信息,所以配置单数据源的时候,脱敏配置是可以生效的。

    小扩展

    第一:遗留一个问题,那做分片的时候,脱敏有效吗?看官大佬,自己去看源码哈。

    第二:解答一下,之前说配置了主从分离可能会导致脱敏失效;为什么说可能呢?回到最开始的那个源码类就知道答案了。因为shardingsphere创建数据源的过程中是有条件的,大家可以去看看条件。所以当配置读写分离和脱敏配置的时候,会同时注入两个数据源。所以可能会导致代码中引用错数据源DataSource对象,所以可能会失效。在这里还需要注意一个仔细就是:创建脱敏数据源对象的时候,shardingsphere是取的dataSourceMap的首个对象,是不是觉得代码在走钢丝,配置多个数据源的时候,可能出现莫名其妙的问题。这里只是我的见解哈,因为整个Java生态框架整合的时候,每个框架的具体仔细我不能保证都非常清楚,所以说可能。只是觉得作为框架,应该考虑更多一些,要么抛异常,不能想当然取第一个,这种做法就是我们在工作中做需求的时候的我认为。
    数据源创建条件截图如下:
    在这里插入图片描述
    这两个类的实现代码如下:

    public final class MasterSlaveRuleCondition extends SpringBootCondition {
        
        private static final String MASTER_SLAVE_NAME = "spring.shardingsphere.masterslave.name";
        
        @Override
        public ConditionOutcome getMatchOutcome(final ConditionContext conditionContext, final AnnotatedTypeMetadata annotatedTypeMetadata) {
            return conditionContext.getEnvironment().containsProperty(MASTER_SLAVE_NAME)
                ? ConditionOutcome.match() : ConditionOutcome.noMatch("Can't find ShardingSphere master-slave rule configuration in environment.");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    public final class EncryptRuleCondition extends SpringBootCondition {
        
        private static final String ENCRYPT_ENCRYPTORS_PREFIX = "spring.shardingsphere.encrypt.encryptors";
        
        private static final String ENCRYPT_TABLES_PREFIX = "spring.shardingsphere.encrypt.tables";
        
        @Override
        public ConditionOutcome getMatchOutcome(final ConditionContext conditionContext, final AnnotatedTypeMetadata annotatedTypeMetadata) {
            boolean isEncrypt = PropertyUtil.containPropertyPrefix(conditionContext.getEnvironment(), ENCRYPT_ENCRYPTORS_PREFIX) 
                    && PropertyUtil.containPropertyPrefix(conditionContext.getEnvironment(), ENCRYPT_TABLES_PREFIX);
            return isEncrypt ? ConditionOutcome.match() : ConditionOutcome.noMatch("Can't find ShardingSphere encrypt rule configuration in environment.");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    通过条件源码会发现,条件重合是会发生的。

    解决方案

    知道原因了,就好解决了。初步的思路,在创建MasterSlaveDataSource对象的时候,给每个实际的数据源再套一层EncryptDataSource数据源,就应该可以解决问题。到这里就可以借助Spring给我们扩展点了,我使用的是对象创建的前置方法扩展点,代码如下:

    @EnableConfigurationProperties(SpringBootCustomEncryptRuleConfigurationProperties.class)
    @Configuration
    @RequiredArgsConstructor
    public class MasterSlaveEncryptDataSourceConfig implements BeanPostProcessor {
    
        private final SpringBootCustomEncryptRuleConfigurationProperties encryptRule;
    
        private final SpringBootPropertiesConfigurationProperties props;
    
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            if (bean instanceof MasterSlaveDataSource dataSource) {
                Map<String, DataSource> dataSourceMap = dataSource.getDataSourceMap();
                Map<String, DataSource> encryptDataSourceMap = new HashMap<>();
                dataSourceMap.forEach((name, ds) -> {
                    try {
                        encryptDataSourceMap.put(name, EncryptDataSourceFactory.createDataSource(ds, new EncryptRuleConfigurationYamlSwapper().swap(encryptRule), props.getProps()));
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                });
                dataSource.getDataSourceMap().putAll(encryptDataSourceMap);
                return dataSource;
            } else {
                return bean;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    @ConfigurationProperties(prefix = "spring.shardingsphere.custom-encrypt")
    public class SpringBootCustomEncryptRuleConfigurationProperties extends YamlEncryptRuleConfiguration {
    
    }
    
    • 1
    • 2
    • 3
    • 4

    有人可能会好奇哦,为啥需要后面这个类呢,仔细思考一下就知道了,如果使用了shardingsphere自带的脱敏配置类SpringBootEncryptRuleConfigurationProperties那就会创建一个EncryptDataSource数据源对象了,之前说过,注入多个数据源对象,可能会有副作用,所以自己搞一个脱敏配置类,配置信息和自带分离,这样shardingsphere就不会注入自带的EncryptDataSource数据源对象了。

    最后自己验证结果是没有问题的。demo下载地址:https://download.csdn.net/download/QQ70945934/86543032

    总结

    从源码中,可以学习到如何如下:

    1. 学习了@Conditional注解一种用法和使用场景;
    2. 使用了工厂设计模式,可以借鉴使用场景和思路;
    3. 使用了装饰模式,使用场景加强一个类的功能;原生DataSource ===> EncryptDataSource ===> MasterSlaveDataSource
    4. 使用Spring框架的高级扩展点,去完成我们实际的开发任务。
  • 相关阅读:
    帮助文档Api
    实验六—基本数据管理(三)
    Linux中Ctrl+C和Ctrl+Z的区别_实战讲解(超详细)
    SQL学习十九、使用游标
    计算机毕业设计SSMdjango学生学习评价与分析系统【附源码数据库】
    JDK21来了!附重要更新说明
    A - Turn the Rectangles
    C语言源代码系列-管理系统之会员计费系统
    Golang vs Rust 为后端选择哪种语言?
    AM@连续函数相关概念和运算性质
  • 原文地址:https://blog.csdn.net/QQ70945934/article/details/126910290