SpringBoot教程(二十五) | SpringBoot配置多个数据源

SpringBoot教程(二十五) | SpringBoot配置多个数据源

  • 前言
  • 方式一:使用dynamic-datasource-spring-boot-starter
  • 方式二:使用AbstractRoutingDataSource
    • [1. 创建数据源枚举类](#1. 创建数据源枚举类)
    • [2. 创建数据源上下文持有者](#2. 创建数据源上下文持有者)
    • [3. 创建自定义的 AbstractRoutingDataSource](#3. 创建自定义的 AbstractRoutingDataSource)
    • [4. 配置yml文件](#4. 配置yml文件)
    • [5. 配置数据源](#5. 配置数据源)
      • [HikariCP 版本](#HikariCP 版本)
      • [Druid 版本](#Druid 版本)
    • [6. 自定义多数据源切换注解](#6. 自定义多数据源切换注解)
    • [7. 使用 AOP 或拦截器设置数据源键](#7. 使用 AOP 或拦截器设置数据源键)
    • [8. 在服务层 测试使用](#8. 在服务层 测试使用)
    • 总结
  • 方式三:分包方式
    • [1. 添加依赖](#1. 添加依赖)
    • [2. application.yml 配置文件](#2. application.yml 配置文件)
    • [3. DataSourceConfig1.java 配置类](#3. DataSourceConfig1.java 配置类)
    • [4. DataSourceConfig2.java 配置类](#4. DataSourceConfig2.java 配置类)
    • [5. Mapper接口和XML文件](#5. Mapper接口和XML文件)
    • [6. 使用Mapper接口的服务类](#6. 使用Mapper接口的服务类)
    • 总结

前言

SpringBoot配置多数据源指的是在一个Spring Boot项目中配置和连接到多个数据库实例的能力。

这种架构允许应用程序根据不同的业务需求、数据类型或性能要求,与多个独立的数据库环境交互。

在实现上,每个数据源都有自己的连接池、事务管理和数据访问对象。

方式一:使用dynamic-datasource-spring-boot-starter

引入maven依赖

Spring Boot 3 区别其他版本,使用的是dynamic-datasource-spring-boot3-starter ,

其他版本的 Spring Boot 使用 dynamic-datasource-spring-boot-starter , 除了依赖区别,其他配置和使用方式新老版本无差别。

Spring Boot 3

xml 复制代码
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
    <version>4.2.0</version>
</dependency>

Spring 1.5.x 及 Spring 2.x.x

xml 复制代码
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
  <version>4.2.0</version>
</dependency>

配置数据源

yaml 复制代码
spring:
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为 master
      strict: false # 设置严格模式,当数据源找不到时,是否抛出异常,默认为false不抛出
      datasource:
        master: # 主库
          type: com.alibaba.druid.pool.DruidDataSource
          driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
          url: jdbc:mysql://www.youlai.tech:3306/youlai_boot?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true
          username: youlai
          password: 123456
        slave: # 从库
          type: com.alibaba.druid.pool.DruidDataSource
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/youlai_boot?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true
          username: root
          password: 123456

动态切换数据源实战

注解切换数据源

@DS注解是 dynamic-datasource-spring-boot-starter提供的

@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解。

注解 结果
没有@DS 默认数据源
@DS("dsName") dsName可以为组名也可以为具体某个库的名称
java 复制代码
/**
 * 主库查询
 */
@DS("master")
@Select("select * from sys_user where id = #{userId}")
SysUser getUserFromMaster(Long userId);

/**
 * 从库查询
 */
@DS("slave")
@Select("select * from sys_user where id = #{userId}")
SysUser getUserFromSlave(Long userId);

单元测试类

java 复制代码
package com.youlai.system.mapper;

import com.youlai.system.model.entity.SysUser;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
@Slf4j
class SysUserMapperTest {

    @Autowired
    private SysUserMapper userMapper;

    private final Long userId = 1L;

    /**
     * 测试注解方式切换数据源
     */
    @Test
    void testSwitchDataSourceByAnnotation() {
        SysUser masterUser = userMapper.getUserFromMaster(userId);
        log.info("用户ID:{} 主库姓名:{}", userId, masterUser.getNickname());
        SysUser slaveUser = userMapper.getUserFromSlave(userId);
        log.info("用户ID:{} 从库姓名:{}", userId, slaveUser.getNickname());
    }
}

测试结果

方式二:使用AbstractRoutingDataSource

AbstractRoutingDataSource 是 Spring 框架中用于实现多数据源路由的一个抽象类。

它允许你在运行时根据某种键(通常是线程本地变量)动态地选择数据源。

1. 创建数据源枚举类

java 复制代码
/**
 * 数据源
 */
public enum DataSourceType {
    /**
     * 数据源1
     * */
    DB1,

    /**
     * 数据源2
     * */
    DB2
}

2. 创建数据源上下文持有者

首先,你需要一个类来持有当前线程的数据源键(DataSource Type)。这通常是通过 ThreadLocal 实现的。

java 复制代码
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 数据源切换处理
 */
public class DynamicDataSourceContextHolder {
    public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);

    /**
     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
     * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * 设置数据源的变量
     */
    public static void setDataSourceType(String dsType) {
        log.info("切换到{}数据源", dsType);
        CONTEXT_HOLDER.set(dsType);
    }

    /**
     * 获得数据源的变量
     */
    public static String getDataSourceType() {
        return CONTEXT_HOLDER.get();
    }

    /**
     * 清空数据源变量
     */
    public static void clearDataSourceType() {
        CONTEXT_HOLDER.remove();
    }
}

3. 创建自定义的 AbstractRoutingDataSource

接下来,你需要扩展 AbstractRoutingDataSource 并重写 determineCurrentLookupKey 方法来返回当前线程的数据源键。

java 复制代码
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;



/**
 * Spring的AbstractRoutingDataSource抽象类,实现动态数据源(他的作用就是动态切换数据源)
 * AbstractRoutingDataSource中的抽象方法determineCurrentLookupKey是实现数据源的route的核心,
 * 这里对该方法进行Override。【上下文DynamicDataSourceContextHolder为一线程安全的ThreadLocal】
 */
public class DynamicDataSource extends AbstractRoutingDataSource {


    /**
     * 取得当前使用哪个数据源
     * @return dbTypeEnum
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}

4. 配置yml文件

yaml 复制代码
spring:
  datasource:
    # 配置第一个数据源
    db1:
      url: jdbc:mysql://localhost:3306/db1
      username: user1
      password: pass1
      driver-class-name: com.mysql.cj.jdbc.Driver
    
    # 配置第二个数据源
    db2:
      url: jdbc:mysql://localhost:3306/db2
      username: user2
      password: pass2
      driver-class-name: com.mysql.cj.jdbc.Driver

5. 配置数据源

在你的配置类中,你需要配置多个数据源,并将它们添加到 multipleDataSource 中。

HikariCP 版本

java 复制代码
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.example.springbootfull.quartztest.datasource.DynamicDataSource;
import com.example.springbootfull.quartztest.enums.DataSourceType;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.quartz.QuartzDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * 多数据源配置
 */
@Configuration
public class DataSourceConfig {

    /**
     * 创建第一个数据源
     *
     * @return dataSource
     */
    @Bean(name = "dataSource1")
    @ConfigurationProperties(prefix = "spring.datasource.db1")
    public DataSource dataSource1() {
        return DataSourceBuilder.create().build();
    }

    /**
     * 创建第二个数据源
     *
     * @return dataSource
     */
    @Bean(name = "dataSource2")
    @ConfigurationProperties(prefix = "spring.datasource.db2")
    public DataSource dataSource2() {
        return DataSourceBuilder.create().build();
    }

    /**
     * 动态数据源配置
     * 多个相同类型的 Bean,那么就会出现歧义,需要使用@Qualifier
     * @Qualifier 是按名称来查找和注入 Bean 的
     * @return dataSource
     */
    @Primary
    @Bean("multipleDataSource")
    public DataSource multipleDataSource(@Qualifier("dataSource1") DataSource db1,
                                         @Qualifier("dataSource2") DataSource db2) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> dataSources = new HashMap<>();
        dataSources.put(DataSourceType.DB1, db1);
        dataSources.put(DataSourceType.DB2, db2);
        dynamicDataSource.setTargetDataSources(dataSources);
        //默认数据源
        dynamicDataSource.setDefaultTargetDataSource(db1);
        return dynamicDataSource;
    }

    @Bean("sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("multipleDataSource") DataSource multipleDataSource) throws Exception {
        // 导入mybatissqlsession配置
        MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
        // 指明数据源
        sessionFactory.setDataSource(multipleDataSource);
        // 设置mapper.xml的位置路径
        Resource[] resources = new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/**/*.xml");
        sessionFactory.setMapperLocations(resources);
        //指明实体扫描(多个package用逗号或者分号分隔)
        //sessionFactory.setTypeAliasesPackage("com.szylt.projects.project.entity");
        // 导入mybatis配置
        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setJdbcTypeForNull(JdbcType.NULL);
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setCacheEnabled(false);
        sessionFactory.setConfiguration(configuration);
        return sessionFactory.getObject();
    }


    //数据源事务配置
    @Bean
    public PlatformTransactionManager transactionManager(DataSource multipleDataSource) {
        return new DataSourceTransactionManager(multipleDataSource);
    }

}

Druid 版本

需要先引入 Druid 依赖,这里使用的是 Druid 官方的 Starter

bash 复制代码
<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>druid-spring-boot-starter</artifactId>
   <version>1.2.8</version>
</dependency>

然后,在properties配置文件中 为每个数据源配置DruidDataSour数据库连接池

bash 复制代码
spring.datasource.db1.type=com.alibaba.druid.pool.DruidDataSour
spring.datasource.db2.type=com.alibaba.druid.pool.DruidDataSour

接着把一下DataSourceConfig 类里面的 按下面的操作,就好了

  • DataSource 对象 换成 DruidDataSource 对象
  • DataSourceBuilder 对象 换成 DruidDataSourceBuilder 对象

6. 自定义多数据源切换注解

java 复制代码
import com.example.springbootfull.quartztest.enums.DataSourceType;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义多数据源切换注解
 * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准
 */
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource
{
    /**
     * 切换数据源名称
     * 默认为DB1
     */
    public DataSourceType value() default DataSourceType.DB1;
}

7. 使用 AOP 或拦截器设置数据源键

为了在使用时能够动态地切换数据源,你需要在执行数据库操作之前设置数据源键。

这通常是通过 AOP(面向切面编程)或拦截器来实现的。

需要引入aop依赖

xml 复制代码
<!--spring切面aop依赖-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

以下是一个使用 AOP 的示例:

以下这个位置,记得 替换成你项目中 自定义注解 DataSource 所在的具体位置

@Pointcut("@annotation(com.example.xx.xx.DataSource) || @within(com.example.xx.xx.DataSource)")

java 复制代码
import java.util.Objects;

import com.example.springbootfull.quartztest.annotation.DataSource;
import com.example.springbootfull.quartztest.datasource.DynamicDataSourceContextHolder;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;


/**
 * 多数据源处理
 *
 */
@Aspect
@Order(1)
@Component
public class DataSourceAspect {
    protected Logger logger = LoggerFactory.getLogger(getClass());

    //此处替换成你项目中 自定义注解 DataSource 所在的具体位置
    @Pointcut("@annotation(com.example.xx.xx.DataSource)"
            + "|| @within(com.example.xx.xx.DataSource)")
    public void dsPointCut() {

    }

    @Around("dsPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        DataSource dataSource = getDataSource(point);
        if (dataSource != null) {
            DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
        }
        try {
            return point.proceed();
        } finally {
            // 销毁数据源 在执行方法之后
            DynamicDataSourceContextHolder.clearDataSourceType();
        }
    }

    /**
     * 获取需要切换的数据源
     */
    public DataSource getDataSource(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
        if (Objects.nonNull(dataSource)) {
            return dataSource;
        }

        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
    }
}

8. 在服务层 测试使用

最后,在你的服务层方法上使用 @DataSource 注解来指定要使用的数据源。

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class MyService {

    @Autowired
    private MyMapper myMapper; // 假设MyMapper是一个Mapper接口

    // 使用第一个数据源
    @DataSource(value = DataSourceType.DB1)
    @Transactional
    public void useDb1() {
        // 执行数据库操作
        myMapper.someMethod();
    }

    // 使用第二个数据源
    @DataSource(value = DataSourceType.DB2)
    @Transactional
    public void useDb2() {
        // 执行数据库操作
        myMapper.anotherMethod();
    }
}

总结

通过上述步骤,你已经成功地在 Spring Boot 项目中配置了基于 AbstractRoutingDataSource 的多数据源支持。

当你调用服务层的方法时,AOP 拦截器会根据 @DataSource 注解的值设置当前线程的数据源键,从而动态地选择数据源。

方式三:分包方式

  1. 配置多个数据源 :在Spring Boot的配置文件中(如application.propertiesapplication.yml),配置多个数据源的信息,包括URL、用户名、密码等。
  2. 创建数据源配置类 :为每个数据源创建一个配置类,使用@Configuration注解标注,并在类中定义数据源(DataSource)、事务管理器(TransactionManager)以及SqlSessionFactory等Bean。
  3. 指定Mapper扫描路径 :使用@MapperScan注解指定每个数据源对应的Mapper扫描路径,以便Spring Boot能够正确地加载和注册Mapper接口。

1. 添加依赖

首先,在pom.xml文件中添加必要的依赖,包括Spring Boot的Web启动器、MyBatis的Spring Boot启动器、数据库驱动等。例如:

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>版本号</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <!-- 其他依赖 -->
</dependencies>

2. application.yml 配置文件

yaml 复制代码
# Spring Boot应用程序的配置文件
spring:
  datasource:
    # 第一个数据源的配置
    db1:
      jdbc-url: jdbc:mysql://localhost:3306/test1?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
      # 数据库用户名
      username: root
      # 数据库密码
      password: 123456
      # 数据库驱动类名
      driver-class-name: com.mysql.cj.jdbc.Driver
    # 第二个数据源的配置
    db2:
      jdbc-url: jdbc:mysql://localhost:3306/test2?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
      username: root
      password: 123456
      driver-class-name: com.mysql.cj.jdbc.Driver

3. DataSourceConfig1.java 配置类

java 复制代码
// 使用@Configuration注解表明这是一个配置类
@Configuration
// 使用@MapperScan注解指定Mapper接口所在的包,并指定SqlSessionFactory的引用
@MapperScan(basePackages = "com.example.mapper.db1", sqlSessionFactoryRef = "db1SqlSessionFactory")
public class DataSourceConfig1 {

    // 使用@Primary注解表明这是默认的数据源(如果有多个数据源时)
    // 使用@Bean注解表明这是一个Spring管理的bean,name属性指定bean的名称
    // 使用@ConfigurationProperties注解读取application.yml中的配置
    @Primary
    @Bean(name = "db1DataSource")
    @ConfigurationProperties(prefix = "spring.datasource.db1")
    public DataSource getDb1DataSource() {
        // 创建并返回数据源对象
        return DataSourceBuilder.create().build();
    }

    // 创建一个SqlSessionFactory对象,用于MyBatis的SQL会话管理
    @Primary
    @Bean(name = "db1SqlSessionFactory")
    public SqlSessionFactory db1SqlSessionFactory(@Qualifier("db1DataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
        // 设置数据源
        sessionFactoryBean.setDataSource(dataSource);
        // 设置Mapper XML文件的位置
        sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/db1/*.xml"));
        // 返回SqlSessionFactory对象
        return sessionFactoryBean.getObject();
    }
}

4. DataSourceConfig2.java 配置类

这个类与DataSourceConfig1.java类似,但它是为第二个数据源配置的。

java 复制代码
// 使用@Configuration注解表明这是一个配置类
@Configuration
// 使用@MapperScan注解指定第二个数据源对应的Mapper接口所在的包
@MapperScan(basePackages = "com.example.mapper.db2", sqlSessionFactoryRef = "db2SqlSessionFactory")
public class DataSourceConfig2 {

    // 创建一个名为db2DataSource的数据源bean
    @Bean(name = "db2DataSource")
    @ConfigurationProperties(prefix = "spring.datasource.db2")
    public DataSource getDb2DataSource() {
        // 创建并返回数据源对象
        return DataSourceBuilder.create().build();
    }

    // 创建一个名为db2SqlSessionFactory的SqlSessionFactory对象
    @Bean(name = "db2SqlSessionFactory")
    public SqlSessionFactory db2SqlSessionFactory(@Qualifier("db2DataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
        // 设置数据源
        sessionFactoryBean.setDataSource(dataSource);
        // 设置第二个数据源对应的Mapper XML文件的位置
        sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/db2/*.xml"));
        // 返回SqlSessionFactory对象
        return sessionFactoryBean.getObject();
    }
}

5. Mapper接口和XML文件

com.example.mapper.db1com.example.mapper.db2包下分别创建Mapper接口,这些接口定义了操作数据库的方法。

同时,在resources/mapper/db1resources/mapper/db2目录下创建对应的XML文件,这些文件包含了SQL语句和映射规则。

6. 使用Mapper接口的服务类

java 复制代码
// 使用@Service注解表明这是一个服务类
@Service
public class MyService {

    // 注入第一个数据源的Mapper接口
    @Autowired
    private Db1Mapper db1Mapper;

    // 注入第二个数据源的Mapper接口
    @Autowired
    private Db2Mapper db2Mapper;

    // 使用第一个数据源的方法
    public void useDb1() {
        // 调用db1Mapper的方法执行数据库操作
    }

    // 使用第二个数据源的方法
    public void useDb2() {
        // 调用db2Mapper的方法执行数据库操作
    }
}

总结

通过上述配置,您可以在Spring Boot项目中配置多个数据源,并通过不同的Mapper接口来操作这些数据源。

每个数据源都有自己的配置类、数据源对象、SqlSessionFactory对象和Mapper接口。

这样,您就可以在同一个项目中方便地访问多个数据库了。

参考文章

【1】Spring Boot 3 整合 Mybatis-Plus 动态数据源实现多数据源切换

相关推荐
西洼工作室5 分钟前
【java 正则表达式 笔记】
java·笔记·正则表达式
40岁的系统架构师7 分钟前
1 JVM JDK JRE之间的区别以及使用字节码的好处
java·jvm·python
皓木.7 分钟前
(自用)配置文件优先级、SpringBoot原理、Maven私服
java·spring boot·后端
舞者H10 分钟前
启动异常:Caused by: java.lang.IllegalStateException: Failed to introspect Class
java
代码中の快捷键11 分钟前
java开发面试有2年经验
java·开发语言·面试
壹佰大多14 分钟前
【spring-cloud-gateway总结】
java·spring·gateway
CodeChampion15 分钟前
60.基于SSM的个人网站的设计与实现(项目 + 论文)
java·vue.js·mysql·spring·elementui·node.js·mybatis
谢家小布柔17 分钟前
Java 中的字符串
java·开发语言
码老白17 分钟前
【老白学 Java】HashSet 应用 - 卡拉 OK(五)
java·开发语言
i7i8i9com18 分钟前
java 1.8+springboot文件上传+vue3+ts+antdv
java·spring boot·后端