SpringBoot 实现动态切换数据源,这样做才更优雅!

实例讲解Spring boot动态切换数据源 在公司的系统里,由于数据量较大,所以配置了多个数据源,它会根据用户所在的地区去觉得查询那个一个数据库。这样就产生了动态切换数据源的场景。今天,就模拟一下在主库查询订单信息查询不到的时候,切换数据源去历史库里面查询。 阅读详情

👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料: 

8fd6375e84d95c1d8791c10b33ce26d6.gif

👉这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号等等功能:

  • Boot 地址:https://gitee.com/zhijiantianya/ruoyi-vue-pro

  • Cloud 地址:https://gitee.com/zhijiantianya/yudao-cloud

  • 视频教程:https://doc.iocoder.cn

来源:juejin.cn/post/
7261601725840179255

87ca66019dce32132b9c81d5f4364551.jpeg


最近在做业务需求时,需要从不同的数据库中获取数据然后写入到当前数据库中,因此涉及到切换数据源问题。本来想着使用Mybatis-plus中提供的动态数据源SpringBoot的starter:dynamic-datasource-spring-boot-starter来实现。

结果引入后发现由于之前项目环境问题导致无法使用。然后研究了下数据源切换代码,决定自己采用ThreadLocal+AbstractRoutingDataSource来模拟实现dynamic-datasource-spring-boot-starter中线程数据源切换。

1 简介

上述提到了ThreadLocal和AbstractRoutingDataSource,我们来对其进行简单介绍下。

ThreadLocal:想必大家必不会陌生,全称:thread local variable。主要是为解决多线程时由于并发而产生数据不一致问题。ThreadLocal为每个线程提供变量副本,确保每个线程在某一时间访问到的不是同一个对象,这样做到了隔离性,增加了内存,但大大减少了线程同步时的性能消耗,减少了线程并发控制的复杂程度。

  • ThreadLocal作用:在一个线程中共享,不同线程间隔离

  • ThreadLocal原理:ThreadLocal存入值时,会获取当前线程实例作为key,存入当前线程对象中的Map中。

AbstractRoutingDataSource:根据用户定义的规则选择当前的数据源,

作用:在执行查询之前,设置使用的数据源,实现动态路由的数据源,在每次数据库查询操作前执行它的抽象方法determineCurrentLookupKey(),决定使用哪个数据源。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

2 代码实现

程序环境:

SpringBoot2.4.8

Mybatis-plus3.2.0

Druid1.2.6

lombok1.18.20

commons-lang3 3.10

2.1 实现ThreadLocal

创建一个类用于实现ThreadLocal,主要是通过get,set,remove方法来获取、设置、删除当前线程对应的数据源。

/**
 * @author: jiangjs
 * @description:
 * @date: 2023/7/27 11:21
 **/
public class DataSourceContextHolder {
    //此类提供线程局部变量。这些变量不同于它们的正常对应关系是每个线程访问一个线程(通过get、set方法),有自己的独立初始化变量的副本。
    private static final ThreadLocal<String> DATASOURCE_HOLDER = new ThreadLocal<>();

    /**
     * 设置数据源
     * @param dataSourceName 数据源名称
     */
    public static void setDataSource(String dataSourceName){
        DATASOURCE_HOLDER.set(dataSourceName);
    }

    /**
     * 获取当前线程的数据源
     * @return 数据源名称
     */
    public static String getDataSource(){
        return DATASOURCE_HOLDER.get();
    }

    /**
     * 删除当前数据源
     */
    public static void removeDataSource(){
        DATASOURCE_HOLDER.remove();
    }

}

2.2 实现AbstractRoutingDataSource

定义一个动态数据源类实现AbstractRoutingDataSource,通过determineCurrentLookupKey方法与上述实现的ThreadLocal类中的get方法进行关联,实现动态切换数据源。

/**
 * @author: jiangjs
 * @description: 实现动态数据源,根据AbstractRoutingDataSource路由到不同数据源中
 * @date: 2023/7/27 11:18
 **/
public class DynamicDataSource extends AbstractRoutingDataSource {

    public DynamicDataSource(DataSource defaultDataSource,Map<Object, Object> targetDataSources){
        super.setDefaultTargetDataSource(defaultDataSource);
        super.setTargetDataSources(targetDataSources);
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}

上述代码中,还实现了一个动态数据源类的构造方法,主要是为了设置默认数据源,以及以Map保存的各种目标数据源。其中Map的key是设置的数据源名称,value则是对应的数据源(DataSource)。

2.3 配置数据库

application.yml中配置数据库信息:

#设置数据源
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      master:
        url: jdbc:mysql://xxxxxx:3306/test1?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
        username: root
        password: 123456
        driver-class-name: com.mysql.cj.jdbc.Driver
      slave:
        url: jdbc:mysql://xxxxx:3306/test2?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
        username: root
        password: 123456
        driver-class-name: com.mysql.cj.jdbc.Driver
      initial-size: 15
      min-idle: 15
      max-active: 200
      max-wait: 60000
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      validation-query: ""
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      pool-prepared-statements: false
      connection-properties: false
/**
 * @author: jiangjs
 * @description: 设置数据源
 * @date: 2023/7/27 11:34
 **/
@Configuration
public class DateSourceConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource masterDataSource(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.slave")
    public DataSource slaveDataSource(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "dynamicDataSource")
    @Primary
    public DynamicDataSource createDynamicDataSource(){
        Map<Object,Object> dataSourceMap = new HashMap<>();
        DataSource defaultDataSource = masterDataSource();
        dataSourceMap.put("master",defaultDataSource);
        dataSourceMap.put("slave",slaveDataSource());
        return new DynamicDataSource(defaultDataSource,dataSourceMap);
    }

}

通过配置类,将配置文件中的配置的数据库信息转换成datasource,并添加到DynamicDataSource中,同时通过@Bean将DynamicDataSource注入Spring中进行管理,后期在进行动态数据源添加时,会用到。

2.4 测试

在主从两个测试库中,分别添加一张表test_user,里面只有一个字段user_name。

create table test_user(
  user_name varchar(255) not null comment '用户名'
)

在主库添加信息:

insert into test_user (user_name) value ('master');

从库中添加信息:

insert into test_user (user_name) value ('slave');

我们创建一个getData的方法,参数就是需要查询数据的数据源名称。

@GetMapping("/getData.do/{datasourceName}")
public String getMasterData(@PathVariable("datasourceName") String datasourceName){
    DataSourceContextHolder.setDataSource(datasourceName);
    TestUser testUser = testUserMapper.selectOne(null);
    DataSourceContextHolder.removeDataSource();
    return testUser.getUserName();
}

其他的Mapper和实体类大家自行实现。

执行结果:

1、传递master时:

60f0329d6158af8c4ca2ce55ea50f52a.jpeg

2、传递slave时:

6121d1082ddec0d14127e6a41b4b3b3e.jpeg

通过执行结果,我们看到传递不同的数据源名称,查询对应的数据库是不一样的,返回结果也不一样。

在上述代码中,我们看到DataSourceContextHolder.setDataSource(datasourceName); 来设置了当前线程需要查询的数据库,通过DataSourceContextHolder.removeDataSource(); 来移除当前线程已设置的数据源。使用过Mybatis-plus动态数据源的小伙伴,应该还记得我们在使用切换数据源时会使用到DynamicDataSourceContextHolder.push(String ds); 和DynamicDataSourceContextHolder.poll(); 这两个方法,翻看源码我们会发现其实就是在使用ThreadLocal时使用了栈,这样的好处就是能使用多数据源嵌套,这里就不带大家实现了,有兴趣的小伙伴可以看看Mybatis-plus中动态数据源的源码。

注:启动程序时,小伙伴不要忘记将SpringBoot自动添加数据源进行排除哦,否则会报循环依赖问题。

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

2.5 优化调整

2.5.1 注解切换数据源

在上述中,虽然已经实现了动态切换数据源,但是我们会发现如果涉及到多个业务进行切换数据源的话,我们就需要在每一个实现类中添加这一段代码。

说到这有小伙伴应该就会想到使用注解来进行优化,接下来我们来实现一下。

2.5.1.1 定义注解

我们就用mybatis动态数据源切换的注解:DS,代码如下:

/**
 * @author: jiangjs
 * @description:
 * @date: 2023/7/27 14:39
 **/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DS {
    String value() default "master";
}
2.5.1.2 实现aop
@Aspect
@Component
@Slf4j
public class DSAspect {

    @Pointcut("@annotation(com.jiashn.dynamic_datasource.dynamic.aop.DS)")
    public void dynamicDataSource(){}

    @Around("dynamicDataSource()")
    public Object datasourceAround(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature)point.getSignature();
        Method method = signature.getMethod();
        DS ds = method.getAnnotation(DS.class);
        if (Objects.nonNull(ds)){
            DataSourceContextHolder.setDataSource(ds.value());
        }
        try {
            return point.proceed();
        } finally {
            DataSourceContextHolder.removeDataSource();
        }
    }
}

代码使用了@Around,通过ProceedingJoinPoint获取注解信息,拿到注解传递值,然后设置当前线程的数据源。对aop不了解的小伙伴可以自行google或百度。

2.5.1.3 测试

添加两个测试方法:

@GetMapping("/getMasterData.do")
public String getMasterData(){
    TestUser testUser = testUserMapper.selectOne(null);
    return testUser.getUserName();
}

@GetMapping("/getSlaveData.do")
@DS("slave")
public String getSlaveData(){
    TestUser testUser = testUserMapper.selectOne(null);
    return testUser.getUserName();
}

由于@DS中设置的默认值是:master,因此在调用主数据源时,可以不用进行添加。

执行结果:

1、调用getMasterData.do方法:

ed2dc97cc0a4dbc8ee28e8e4d90f34be.jpeg

2、调用getSlaveData.do方法:

e918cdc2129faa6f77a07df17d9fac9b.jpeg

通过执行结果,我们通过@DS也进行了数据源的切换,实现了Mybatis-plus动态切换数据源中的通过注解切换数据源的方式。

2.5.2 动态添加数据源

业务场景 :有时候我们的业务会要求我们从保存有其他数据源的数据库表中添加这些数据源,然后再根据不同的情况切换这些数据源。

因此我们需要改造下DynamicDataSource来实现动态加载数据源。

2.5.2.1 数据源实体
/**
 * @author: jiangjs
 * @description: 数据源实体
 * @date: 2023/7/27 15:55
 **/
@Data
@Accessors(chain = true)
public class DataSourceEntity {

    /**
     * 数据库地址
     */
    private String url;
    /**
     * 数据库用户名
     */
    private String userName;
    /**
     * 密码
     */
    private String passWord;
    /**
     * 数据库驱动
     */
    private String driverClassName;
    /**
     * 数据库key,即保存Map中的key
     */
    private String key;
}

实体中定义数据源的一般信息,同时定义一个key用于作为DynamicDataSource中Map中的key。

2.5.2.2 修改DynamicDataSource
/**
 * @author: jiangjs
 * @description: 实现动态数据源,根据AbstractRoutingDataSource路由到不同数据源中
 * @date: 2023/7/27 11:18
 **/
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {

    private final Map<Object,Object> targetDataSourceMap;

    public DynamicDataSource(DataSource defaultDataSource,Map<Object, Object> targetDataSources){
        super.setDefaultTargetDataSource(defaultDataSource);
        super.setTargetDataSources(targetDataSources);
        this.targetDataSourceMap = targetDataSources;
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }

    /**
     * 添加数据源信息
     * @param dataSources 数据源实体集合
     * @return 返回添加结果
     */
    public void createDataSource(List<DataSourceEntity> dataSources){
        try {
            if (CollectionUtils.isNotEmpty(dataSources)){
                for (DataSourceEntity ds : dataSources) {
                    //校验数据库是否可以连接
                    Class.forName(ds.getDriverClassName());
                    DriverManager.getConnection(ds.getUrl(),ds.getUserName(),ds.getPassWord());
                    //定义数据源
                    DruidDataSource dataSource = new DruidDataSource();
                    BeanUtils.copyProperties(ds,dataSource);
                    //申请连接时执行validationQuery检测连接是否有效,这里建议配置为TRUE,防止取到的连接不可用
                    dataSource.setTestOnBorrow(true);
                    //建议配置为true,不影响性能,并且保证安全性。
                    //申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
                    dataSource.setTestWhileIdle(true);
                    //用来检测连接是否有效的sql,要求是一个查询语句。
                    dataSource.setValidationQuery("select 1 ");
                    dataSource.init();
                    this.targetDataSourceMap.put(ds.getKey(),dataSource);
                }
                super.setTargetDataSources(this.targetDataSourceMap);
                // 将TargetDataSources中的连接信息放入resolvedDataSources管理
                super.afterPropertiesSet();
                return Boolean.TRUE;
            }
        }catch (ClassNotFoundException | SQLException e) {
            log.error("---程序报错---:{}", e.getMessage());
        }
        return Boolean.FALSE;
    }

    /**
     * 校验数据源是否存在
     * @param key 数据源保存的key
     * @return 返回结果,true:存在,false:不存在
     */
    public boolean existsDataSource(String key){
        return Objects.nonNull(this.targetDataSourceMap.get(key));
    }
}

在改造后的DynamicDataSource中,我们添加可以一个 private final Map<Object,Object> targetDataSourceMap,这个map会在添加数据源的配置文件时将创建的Map数据源信息通过DynamicDataSource构造方法进行初始赋值,即:DateSourceConfig类中的createDynamicDataSource()方法中。

同时我们在该类中添加了一个createDataSource方法,进行数据源的创建,并添加到map中,再通过super.setTargetDataSources(this.targetDataSourceMap) ;进行目标数据源的重新赋值。

2.5.2.3 动态添加数据源

上述代码已经实现了添加数据源的方法,那么我们来模拟通过从数据库表中添加数据源,然后我们通过调用加载数据源的方法将数据源添加进数据源Map中。

在主数据库中定义一个数据库表,用于保存数据库信息。

create table test_db_info(
    id int auto_increment primary key not null comment '主键Id',
    url varchar(255) not null comment '数据库URL',
    username varchar(255) not null comment '用户名',
    password varchar(255) not null comment '密码',
    driver_class_name varchar(255) not null comment '数据库驱动'
    name varchar(255) not null comment '数据库名称'
)

为了方便,我们将之前的从库录入到数据库中,修改数据库名称。

insert into test_db_info(url, username, password,driver_class_name, name)
value ('jdbc:mysql://xxxxx:3306/test2?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false',
       'root','123456','com.mysql.cj.jdbc.Driver','add_slave')

数据库表对应的实体、mapper,小伙伴们自行添加。

启动SpringBoot时添加数据源:

/**
 * @author: jiangjs
 * @description:
 * @date: 2023/7/27 16:56
 **/
@Component
public class LoadDataSourceRunner implements CommandLineRunner {
    @Resource
    private DynamicDataSource dynamicDataSource;
    @Resource
    private TestDbInfoMapper testDbInfoMapper;
    @Override
    public void run(String... args) throws Exception {
        List<TestDbInfo> testDbInfos = testDbInfoMapper.selectList(null);
        if (CollectionUtils.isNotEmpty(testDbInfos)) {
            List<DataSourceEntity> ds = new ArrayList<>();
            for (TestDbInfo testDbInfo : testDbInfos) {
                DataSourceEntity sourceEntity = new DataSourceEntity();
                BeanUtils.copyProperties(testDbInfo,sourceEntity);
                sourceEntity.setKey(testDbInfo.getName());
                ds.add(sourceEntity);
            }
            dynamicDataSource.createDataSource(ds);
        }
    }
}

经过上述SpringBoot启动后,已经将数据库表中的数据添加到动态数据源中,我们调用之前的测试方法,将数据源名称作为参数传入看看执行结果。

2.5.2.4 测试
ec47f8349619232c63841bf74ed1127d.jpeg

通过测试我们发现数据库表中的数据库被动态加入了数据源中,小伙伴可以愉快地随意添加数据源了。

好了,今天就跟大家唠叨到这,希望我的叨叨让大家对于动态切换数据源的方式能够有更深地理解。

业精于勤荒于嬉,动起来吧,小伙伴们。

github:github.com/lovejiashn/… [1]


欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,“长按”或“扫描”下方二维码噢:

30739da5b945925449f3482560f87ccf.png

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

a407339a3d2b052259a8b7acbd372357.png

4e3d1b7c6c1fc701619aa9acd09cf0e9.pngf5b87a05c5baaeb807323f148894f246.pngbd71d1cfeda720b5ccb11f8a312c834a.pngbd580b6971672c30b6d323e7b8e67061.png

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
JAVA配置多数据源并动态切换 参考: https://my.oschina.net/simpleton/blog/916108 http://wenqy.com/2017/12/24/spring-boot%E5%AD%A6%E4%B9%A0%E7%B3%BB%E5%88%97%E4%B9%8Batomikos%E5%A4%9A%E6%95%B0%E6%8D%AE%E6%BA%90%E5%92%8C%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A15.html 需求描述 近期被分配了一个需. 阅读详情

相关推荐

springboot mybatis使用动态数据源的例子

springboot mybatis 使用动态数据源的配置 包含mybatis 的 使用以及email的配置使用 可以自己设置使用不同环境下的文件配置

validation query

public static void mySQLConfigPlugin(Plugins me) { // C3p0Plugin dbplugin = createC3p0Plugin(); DruidPlugin dbplugin = createDruidPlugin(); /** * hsqldb - "select 1 from INFORMATION_SCHEMA.SYSTEM_

hety119的专栏 2094

使用Android GPS api 实现一个轨迹记录的小功能

引入 市场上的跑步软件已经是非常多了,如咕咚和悦跑圈还有其他,很多大厂也在涉足这个方面。而最简单的运动跑步,最近几年也因简单,不限场地,运动效果出众备受大众喜欢。 我的这篇文章的内容很多也是从网上学习其他的博客内容,外加利用android自身的提供的api实现的,希望能对有这方面需求的朋友有所帮助。 介绍 先上两张图,界面比较丑,不过一些数据基本都有了,这个图片是临时要用截取的,所以轨迹线很短,之前

尝试着去了解代码 1万+

SpringBoot轻松实现多数据源切换

项目需要实现在多个数据源之间读写数据,例如在 A 数据源和 B 数据源读取数据,然后在 C 数据源写入数据 或者 部分业务数据从 A 数据源中读取、部分从B数据源中读取诸如此类需求。本文将简单模拟在SpringBoot项目中实现不同数据源之间读取数据。

观止study 3434

SAP FICO 银企直联实战:从配置到异常处理的全流程解析

本文详细解析了SAP FICO银企直联的实战配置与异常处理全流程,涵盖银行主数据搭建、支付类配置、银行接口适配等关键步骤。通过实际案例分享,帮助用户掌握实时付款、余额监控和自动对账等核心功能,提升企业财务效率与安全性。特别针对常见问题如凭证重复付款、前置机UUID冲突等提供了实用解决方案。

weixin_29215815的博客 663

从论文到生产:iSLIP优先级匹配算法在SDN交换机中的20年演进史

本文回顾了iSLIP优先级匹配算法在SDN交换机中的20年演进历程,从斯坦福大学的原始论文到现代芯片实现。通过独特的指针滑动和迭代循环机制,iSLIP算法显著提升了交换机的吞吐量和公平性,解决了传统FIFO调度的队头阻塞问题。文章详细探讨了算法在硬件实现中的工程挑战、开源实践及未来AI时代的智能调度方向。

weixin_29230649的博客 331

java 动态数据源切换,Java+Spring+MyBatis实现多数据源的动态切换

在实际的项目开发过程中我们经常会遇到一个项目需要使用多个数据源的情况,而多数据源又可分为固定多数据源和动态多数据源两种情况。固定多数据源是指在项目中需要使用多个数据源,但数据源的个数是确定的,不会改变,如我们的项目需要使用订单库和商品库这两个数据源,项目中所有的业务逻辑都只需要操作这两个库。动态多数据源是指在项目需要使用多数据源,但是数据源的个数不确定,可能会随着项目的需要动态的新增或删除数据源。...

weixin_42501329的博客 617

服务器被矿工入侵记录

一次服务器被矿工入驻的处理记录

王瑞的技术专栏 1333

Spring Boot实现动态切换数据源,这样做才更优雅!

上述提到了ThreadLocal和AbstractRoutingDataSource,我们来对其进行简单介绍下。:想必大家必不会陌生,全称:thread local variable。主要是为解决多线程时由于并发而产生数据不一致问题。ThreadLocal为每个线程提供变量副本,确保每个线程在某一时间访问到的不是同一个对象,这样做到了隔离性,增加了内存,但大大减少了线程同步时的性能消耗,减少了线程并发控制的复杂程度。ThreadLocal作用:在一个线程中共享,不同线程间隔离。

sjkflw121150的博客 735

【IT小趣闻】免费的DeepSeek是如何赚钱的?

导语:目前很多的通用人工智能也就是所谓的AGI例如文心一言、豆包、通义千问和最近大火的DeepSeek都是免费给普通用户使用的,对开源商业模式不太熟悉的普通人可不太了解这免费背后的商业套路,下面我们就一起来揭秘。开源模式能降低用户信任成本、加速技术普及,而商业化服务则针对企业级需求。DeepSeek 可通过组合上述策略,在服务开发者社区的同时,满足企业客户的深度需求,实现可持续发展。你也看出来了,上边是DeepSeek生成的,讲的还是挺明白的。

SunMy的博客 1767

深信服aCloud虚拟存储VS3.0实战:从数据分片到仲裁机制的全流程解析

本文深度解析了深信服aCloud虚拟存储VS3.0的核心架构与生产级实践。从数据分片、条带化写入、多副本机制到智能缓存分层,详细阐述了其如何实现高性能与高可靠性的平衡。同时,深入剖析了动态均衡、平滑扩容以及故障恢复与仲裁机制,为运维人员和技术决策者提供了在超融合基础架构(HCI)中部署和管理虚拟存储的实战指南。

net55的博客 588

SpringBoot实现动态切换数据源,这样做才更优雅!

????这是一个或许对你有用的社群????一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料:《项目实战(视频)》:从书中学,往事中“练”《互联网高频面试题》:面朝简历学习,春暖花开《架构 x 系统设计》:摧枯拉朽,掌控面试高频场景题《精进 Java 学习指南》:系统学习,互联网主流技术栈《必读 Java 源码专栏》:知其然,知其所以然????这是一个或许...

weixin_44421461的博客 188

【软件测试】人工智能(AI)在软件测试领域的应用

AI在软件测试领域的应用使测试过程更加自动化、高效和智能。它可以帮助测试团队减轻负担,更快地识别和修复问题,最终提高软件的质量和可靠性。随着AI技术的不断进步,未来AI在软件测试领域的应用将更加广泛和深入。

weixin_46453070的博客 4385
上一篇: 项目终于用上了 xxl-job!
下一篇: 一个收入被严重低估的方向,96w稳了!!
公众号-芋道源码
博客等级 码龄10年 1631粉丝 377原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值