• Spring Data JPA 之 多数据源配置


    18 生产环境多数据源的处理方法

    ⼯作中我们时常会遇到跨数据库操作的情况,这时候就需要配置多数据源,那么如何配置呢?常⽤的⽅式及其背后的原理⽀撑是什么呢?我们下⾯来了解⼀下

    18.1 第一种方式:@Configuration 配置方法

    这种⽅式的主要思路是,不同 Package 下⾯的实体和 Repository 采⽤不同的 Datasource。所以我们改造⼀下我们的 example ⽬录结构,来看看不同 Repositories 的数据源是怎么处理的。

    18.1.1 通过多个@Configuration 的配置方法

    第⼀步:规划 Entity 和 Repository 的⽬录结构,为了⽅便配置多数据源。

    com.zzn.master 创建实体类 MasterUser 和 MasterUserRepository

    @Entity
    @Data
    public class MasterUser {
    
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        protected Long id;
        private String name;
        private String email;
        private Integer age;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    public interface MasterUserRepository extends JpaRepository<MasterUser, Long> {
    }
    
    • 1
    • 2

    com.zzn.slave 创建实体类 SlaveUser 和 SlaveUserRepository

    @Entity
    @Data
    public class SlaveUser {
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        protected Long id;
        private String name;
        private String email;
        private Integer age;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    public interface SlaveUserRepository extends JpaRepository<SlaveUser, Long> {
    }
    
    • 1
    • 2

    我们把实体和 Repository 分别放到了 naster 和 slave 两个⽬录⾥⾯,这时我们假设 master 是 MySQL,User 表在 master 数据源⾥⾯,那么我们需要配置⼀个 Master DataSource 的 Configuration 类,并且在⾥⾯配置 DataSource、TransactionManager 和 EntityManager。

    第⼆步:配置 MasterDataSourceConfig 类。

    ⽬录结构调整完之后,接下来我们开始配置数据源,完整代码如下:

    @Configuration
    @EnableTransactionManagement // 开启事务
    @EnableJpaRepositories(  // 利⽤ EnableJpaRepositories 配置哪些包下⾯的 Repositories,采⽤哪个 EntityManagerFactory 和哪个 TransactionManagement
            basePackages = {"com.zzn.master"},// master 数据源的 repository 的包路径
            entityManagerFactoryRef = "masterEntityManagerFactory",// 改变 master 数据源的 EntityManagerFactory 的默认值,改为 masterEntityManagerFactory
            transactionManagerRef = "masterTransactionManager" // 改变 master 数据源的 TransactionManagement 的默认值,masterTransactionManager
    )
    public class MasterDataSourceConfig {
    
        /**
         * 指定 master 数据源的 dataSource 配置
         *
         * @return master 数据源配置
         */
        @Primary
        @Bean(name = "masterDataSourceProperties")
        @ConfigurationProperties("spring.datasource.master") // master 数据源的配置前缀采⽤ spring.datasource.master
        public DataSourceProperties dataSourceProperties() {
            return new DataSourceProperties();
        }
    
        /**
         * 可以选择不同的数据源,这⾥使⽤ HikariDataSource,创建数据源
         *
         * @param masterDataSourceProperties 数据源配置
         * @return master 数据源
         */
        @Primary
        @Bean(name = "masterDataSource")
        @ConfigurationProperties(prefix = "spring.datasource.hikari.master") //配置 master 数据源所⽤的 hikari 配置 key 的前缀
        public DataSource dataSource(@Qualifier("masterDataSourceProperties")
                                     DataSourceProperties masterDataSourceProperties) {
            HikariDataSource dataSource =
                    masterDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
            if (StringUtils.hasText(masterDataSourceProperties.getName())) {
                dataSource.setPoolName(masterDataSourceProperties.getName());
            }
            return dataSource;
        }
    
        /**
         * 配置 master 数据源的 entityManagerFactory 命名为 masterEntityManagerFactory,⽤来对实体进⾏⼀些操作
         *
         * @param builder          构建器
         * @param masterDataSource master 数据源
         * @return master 实体管理工厂
         */
        @Primary
        @Bean(name = "masterEntityManagerFactory")
        public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder,
                                                                           @Qualifier("masterDataSource") DataSource masterDataSource) {
            return builder.dataSource(masterDataSource)
                    // master 数据的实体所在的路径
                    .packages("com.zzn.master")
                    // persistenceUnit 的名字采⽤ master
                    .persistenceUnit("master")
                    .build();
        }
    
        /**
         * 配置 master 数据源的事务管理者,命名为 masterTransactionManager 依赖 masterEntityManagerFactory
         *
         * @param masterEntityManagerFactory master 实体管理工厂
         * @return master 事务管理者
         */
        @Primary
        @Bean(name = "masterTransactionManager")
        public PlatformTransactionManager transactionManager(@Qualifier("masterEntityManagerFactory") EntityManagerFactory masterEntityManagerFactory) {
            return new JpaTransactionManager(masterEntityManagerFactory);
        }
    }
    
    • 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

    到这⾥,master 数据源 我们就配置完了,下⾯再配置 slave 数据源。

    第三步:配置 SlaveDataSourceConfig 类,加载 slave 数据源

    @Configuration
    @EnableTransactionManagement // 开启事务
    @EnableJpaRepositories(  // 利⽤ EnableJpaRepositories 配置哪些包下⾯的 Repositories,采⽤哪个 EntityManagerFactory 和哪个 TransactionManagement
            basePackages = {"com.zzn.slave"},// slave 数据源的 repository 的包路径
            entityManagerFactoryRef = "slaveEntityManagerFactory",// 改变 slave 数据源的 EntityManagerFactory 的默认值,改为 slaveEntityManagerFactory
            transactionManagerRef = "slaveTransactionManager" // 改变 slave 数据源的 TransactionManagement 的默认值,slaveTransactionManager
    )
    public class SlaveDataSourceConfig {
    
    
        /**
         * 指定 slave 数据源的 dataSource 配置
         *
         * @return slave 数据源配置
         */
        @Bean(name = "slaveDataSourceProperties")
        @ConfigurationProperties("spring.datasource.slave") // slave 数据源的配置前缀采⽤ spring.datasource.slave
        public DataSourceProperties dataSourceProperties() {
            return new DataSourceProperties();
        }
    
        /**
         * 可以选择不同的数据源,这⾥使⽤ HikariDataSource,创建数据源
         *
         * @param slaveDataSourceProperties 数据源配置
         * @return slave 数据源
         */
        @Bean(name = "slaveDataSource")
        @ConfigurationProperties(prefix = "spring.datasource.hikari.slave") //配置 slave 数据源所⽤的 hikari 配置 key 的前缀
        public DataSource dataSource(@Qualifier("slaveDataSourceProperties")
                                     DataSourceProperties slaveDataSourceProperties) {
            HikariDataSource dataSource =
                    slaveDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
            if (StringUtils.hasText(slaveDataSourceProperties.getName())) {
                dataSource.setPoolName(slaveDataSourceProperties.getName());
            }
            return dataSource;
        }
    
        /**
         * 配置 slave 数据源的 entityManagerFactory 命名为 slaveEntityManagerFactory,⽤来对实体进⾏⼀些操作
         *
         * @param builder         构建器
         * @param slaveDataSource slave 数据源
         * @return slave 实体管理工厂
         */
        @Bean(name = "slaveEntityManagerFactory")
        public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder,
                                                                           @Qualifier("slaveDataSource") DataSource slaveDataSource) {
            return builder.dataSource(slaveDataSource)
                    // slave 数据的实体所在的路径
                    .packages("com.zzn.slave")
                    // persistenceUnit 的名字采⽤ slave
                    .persistenceUnit("slave")
                    .build();
        }
    
        /**
         * 配置 slave 数据源的事务管理者,命名为 slaveTransactionManager 依赖 slaveEntityManagerFactory
         *
         * @param slaveEntityManagerFactory slave 实体管理工厂
         * @return slave 事务管理者
         */
        @Bean(name = "slaveTransactionManager")
        public PlatformTransactionManager transactionManager(@Qualifier("slaveEntityManagerFactory") EntityManagerFactory slaveEntityManagerFactory) {
            return new JpaTransactionManager(slaveEntityManagerFactory);
        }
    
    }
    
    • 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

    这⼀步你需要注意,MasterDataSourceConfig 和 SlaveDataSourceConfig 不同的是,master ⾥⾯每个 @Bean 都是 @Primary,⽽ slave ⾥⾯不是的。

    第四步:通过 application.yml 配置两个数据源的值,代码如下:

    spring:
      datasource:
        # master 数据库采用 MySql 数据库
        master:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai&logger=Slf4JLogger&profileSQL=true
          username: root
          password: root
        # slave 数据库采用 h2
        slave:
          url: jdbc:h2:~/test
          username: sa
          password: sa
        hikari:
          master:
            pool-name: jpa-hikari-pool-master
            max-lifetime: 900000
            maximum-pool-size: 8
          slave:
            pool-name: jpa-hikari-pool-slave
            max-lifetime: 500000
            maximum-pool-size: 6
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    第五步:我们写个 Controller 测试⼀下。

    @RestController
    @RequestMapping("")
    @RequiredArgsConstructor
    public class UserController {
    
        private final MasterUserRepository masterUserRepository;
        private final SlaveUserRepository slaveUserRepository;
    
    
        /**
         * 操作 master
         */
        @PostMapping("/user/master")
        public MasterUser saveUser(@RequestBody MasterUser user) {
            return masterUserRepository.save(user);
        }
    
        /**
         * 操作 slave
         */
        @PostMapping("/user/slave")
        public SlaveUser saveUserInfo(@RequestBody SlaveUser user) {
            return slaveUserRepository.save(user);
        }
    
    
    }
    
    • 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

    第六步:直接启动我们的项⽬,测试⼀下。

    请看这⼀步的启动⽇志:

    在这里插入图片描述

    可以看到启动的是两个数据源,如果我们分别请求 Controller 写的两个⽅法的时候,也会分别插⼊到不同的数据源⾥⾯去。

    通过上⾯的六个步骤你应该知道了如何配置多数据源,那么它的原理基础是什么呢?我们看⼀下 Datasource 与 TransactionManager、EntityManagerFactory 的关系和职责分别是怎么样的。

    18.1.2 DataSource与Transaction-Manager、EntityManager-Factory的关系分析

    在这里插入图片描述

    1. HikariDataSource 负责实现 DataSource,交给 EntityManager 和 TransactionManager 使⽤;
    2. EntityManager 是利⽤ Datasouce 来操作数据库,⽽其实现类是 SessionImpl;
    3. EntityManagerFactory 是⽤来管理和⽣成 EntityManager 的,⽽ EntityManagerFactory 的实现类是 LocalContainerEntityManagerFactoryBean,通过实现 FactoryBean 接⼝实现,利⽤了 FactoryBean 的 Spring 中的 bean 管理机制,所以需要我们在 MasterDatasourceConfig ⾥⾯配置 LocalContainerEntityManagerFactoryBean 的 bean 的注⼊⽅式;
    4. JpaTransactionManager 是⽤来管理事务的,实现了 TransactionManager 并且通过 EntityFactory 和 Datasource 进⾏ db 操作,所以我们要在 DataSourceConfig ⾥⾯告诉 JpaTransactionManager ⽤的 TransactionManager 是 masterEntityManagerFactory。

    18.1.3 默认的 JpaBaseConfiguration 加载方式分析

    上⼀讲只简单说明了 DataSource 的配置,其实还可以通过 HibernateJpaConfiguration,找到⽗类 JpaBaseConfiguration 类,就可以看到多数据源的参考原型,如下图所示:

    在这里插入图片描述

    通过上⾯的代码,可以看到在单个数据源情况下的 EntityManagerFactory 和 TransactionManager 的加载⽅法,并且我们在多数据源的配置⾥⾯还加载了⼀个类:EntityManagerFactoryBuilder entityManagerFactoryBuilder,也正是从上⾯的⽅法加载进去的。

    18.2 第二种方式:利用 AbstractRoutingDataSource 配置

    18.2.1 利用 AbstractRoutingDataSource 的配置方法

    我们都知道 DataSource 的本质是获得数据库连接,⽽ AbstractRoutingDataSource 帮我们实现了动态获得数据源的可能性。下⾯还是通过⼀个例⼦看⼀下它是怎么使⽤的。

    第⼀步:定⼀个数据源的枚举类,⽤来标示数据源有哪些。

    public enum RoutingDataSourceEnum {
    
        MASTER,
        SLAVE;
        
        public static RoutingDataSourceEnum findByCode(String dbRouting) {
            return Arrays.stream(values())
                    .filter(e -> e.name().equals(dbRouting))
                    .findFirst()
                    // 没找到的情况下,默认返回 Master
                    .orElse(MASTER);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    第⼆步:新增 DataSourceRoutingHolder,⽤来存储当前线程需要采⽤的数据源。

    /**
     * 利⽤ ThreadLocal 来存储,当前的线程使⽤的数据
     */
    public class DataSourceRoutingHolder {
    
        private static final ThreadLocal<RoutingDataSourceEnum> THREAD_LOCAL = new ThreadLocal<>();
    
        public static void setDataSource(RoutingDataSourceEnum dataSourceEnum) {
            THREAD_LOCAL.set(dataSourceEnum);
        }
    
        public static RoutingDataSourceEnum getDataSource() {
            return THREAD_LOCAL.get();
        }
    
        public static void clearDataSource() {
            THREAD_LOCAL.remove();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    **第三步:自定义 RoutingDataSource,继承 AbstractRoutingDataSource **

    public class RoutingDataSource extends AbstractRoutingDataSource {
    
        @Override
        protected Object determineCurrentLookupKey() {
            return DataSourceRoutingHolder.getDataSource();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    第四步:配置 RoutingDataSourceConfig,⽤来指定哪些 Entity 和 Repository 采⽤动态数据源。

    @Configuration
    @EnableTransactionManagement
    @EnableJpaRepositories(
            // 数据源的 repository 的包路径,这⾥我们覆盖 master 和 slave 的包路径
            basePackages = {"com.zzn"},
            entityManagerFactoryRef = "routingEntityManagerFactory",
            transactionManagerRef = "routingTransactionManager"
    )
    public class RoutingDataSourceConfig {
    
        /**
         * 创建 RoutingDataSource,引⽤我们之前配置的 masterDataSource 和 slaveDataSource
         */
        @Bean(name = "routingDataSource")
        public DataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
                                     @Qualifier("slaveDataSource") DataSource slaveDataSource) {
            Map<Object, Object> dataSourceMap = Maps.newHashMap();
            dataSourceMap.put(RoutingDataSourceEnum.MASTER, masterDataSource);
            dataSourceMap.put(RoutingDataSourceEnum.SLAVE, slaveDataSource);
            RoutingDataSource routingDataSource = new RoutingDataSource();
            // 设置 RoutingDataSource 的默认数据源
            routingDataSource.setDefaultTargetDataSource(masterDataSource);
            // 设置 RoutingDataSource 的数据源列表
            routingDataSource.setTargetDataSources(dataSourceMap);
            return routingDataSource;
        }
    
        /**
         * 类似 master 和 slave 的配置,唯⼀不同的是,这⾥采⽤ routingDataSource
         *
         * @param builder
         * @param routingDataSource entityManager 依赖 routingDataSource
         * @return
         */
        @Bean(name = "routingEntityManagerFactory")
        public LocalContainerEntityManagerFactoryBean
        entityManagerFactory(EntityManagerFactoryBuilder builder,
                             @Qualifier("routingDataSource") DataSource routingDataSource) {
            // 数据 routing 的实体所在的路径,这⾥我们覆盖 master 和 slave 的路径
            return builder.dataSource(routingDataSource).packages("com.zzn")
                    // persistenceUnit 的名字采⽤ db-routing
                    .persistenceUnit("db-routing")
                    .build();
        }
    
        /**
         * 配置数据的事务管理者,命名为routingTransactionManager依赖 routingEntityManagerFactory
         *
         * @param routingEntityManagerFactory
         * @return
         */
        @Bean(name = "routingTransactionManager")
        public PlatformTransactionManager
        transactionManager(@Qualifier("routingEntityManagerFactory")
                           EntityManagerFactory routingEntityManagerFactory) {
            return new JpaTransactionManager(routingEntityManagerFactory);
        }
    }
    
    • 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

    第五步:自定义动态数据源的注解和拦截器

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD})
    public @interface DS {
    
        RoutingDataSourceEnum value() default RoutingDataSourceEnum.MASTER;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    @Aspect
    // 保证该 AOP 在 @Transactional 之前执行
    @Order(-10)
    @Component
    @Slf4j
    public class RoutingDataSourceAspect {
    
        @Before(value = "@annotation(source)")
        public void changeDataSource(JoinPoint point, DS source) {
            RoutingDataSourceEnum currentSource = source.value();
            log.info("Change DataSource To:[" + currentSource + "]");
            DataSourceRoutingHolder.setDataSource(currentSource);
        }
    
        @After(value = "@annotation(source)")
        public void restoreDataSource(JoinPoint point, DS source) {
            // 方法执行完毕之后,销毁当前数据源信息,进行垃圾回收。
            DataSourceRoutingHolder.clearDataSource();
            log.info("Clear Change DataSource...");
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    **第六步:使用注解切换数据源 **

    @PostMapping("/user/slave")
    @DS(RoutingDataSourceEnum.SLAVE)
    public SlaveUser saveUserInfo(@RequestBody SlaveUser user) {
        return slaveUserRepository.save(user);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    通过上⾯六个步骤,我们可以利⽤ AbstractRoutingDataSource 实现动态数据源,实际⼯作中可能要复杂,有的需要考虑多线程、线程安全等问题,你要多加注意。

    18.2.2 微服务下多数据源的思考

    通过上⾯的两种⽅式,我们分别可以实现同⼀个 application 应⽤的多数据源配置,那么有什么注意事项呢?我简单总结如下⼏点建议。

    1. 此种⽅式利⽤了当前线程事务不变的原理,所以要注意异步线程的处理⽅式;
    2. 此种⽅式利⽤了 DataSource 的原理,动态地返回不同的 db 连接,⼀般需要在开启事务之前使⽤,需要注意事务的⽣命周期;
    3. ⽐较适合读写操作分开的业务场景;
    4. 多数据的情况下,避免⼀个事务⾥⾯采⽤不同的数据源,这样会有意想不到的情况发⽣,⽐如死锁现象;
    5. 学会通过⽇志检查我们开启请求的⽅法和开启的数据源是否正确,可以通过 Debug 断点来观察数据源是否选择的正确。

    18.2.3 微服务下的实战建议

    在实际⼯作中,为了便捷省事,更多开发者喜欢配置多个数据源,但是我强烈建议不要在对⽤户直接提供的 API 服务上⾯配置多数据源,否则将出现令⼈措⼿不及的 Bug。

    如果你是做后台管理界⾯,供公司内部员⼯使⽤的,那么这种 API 可以为了⽅便⽽使⽤多数据源。

    微服务的⼤环境下,服务越⼩,内聚越⾼,低耦合服务越健壮,所以⼀般跨库之间⼀定是是通过 REST 的 API 协议,进⾏内部服务之间的调⽤,这是最稳妥的⽅式,原因有如下⼏点:

    1. REST 的 API 协议更容易监控,更容易实现事务的原⼦性;
    2. db 之间解耦,使业务领域代码职责更清晰,更容易各⾃处理各种问题;
    3. 只读和读写的 API 更容易分离和管理。

    18.3 本章小结

    到这⾥,这⼀讲的内容就结束了。多数据的配置是⼀个⽐较复杂的事情,在本讲中通过两种⽅式,⾃定义 entityManager 和 transactionManager,实现了多数据源的配置。

  • 相关阅读:
    【编程之路】面试必刷TOP101:双指针(87-94,Python实现)
    java并发编程1
    学生成绩管理系统(C语言有结构体实现)
    信息系统项目管理师-采购管理论文提纲
    刷题笔记25——图论课程表
    Java自学第5课:Java web开发环境概述,更换Eclipse版本
    设计模式之中介模式
    Windows 10 启用windows功能.NET Framework3.5 时 windows无法完成请求的更改 错误代码:0x80072F8F解决方案
    rpc入门笔记0x01
    皮皮APP语音派对策划师:千亿娱乐社交下的百万自由职业者
  • 原文地址:https://blog.csdn.net/qq_40161813/article/details/126328265