SpringBoot使用用户输入的自定义数据源启动【附源码】

一、项目背景

不知道小伙伴们有没有遇到过这样的需求,就是一个项目启动时不知道数据源,需要项目无数据源启动后,用户在画面自定义录入数据源信息,然后项目再初始化数据库链接,初始化管理员用户。最后项目进入正常使用。

正常情况下,应该不会遇到这种需求吧,我们都是把数据库链接放在配置文件,然后启动项目,简简单单,轻轻松松。但是当整个项目交给用户使用时,谁使用都不知道情况下,算了,只能让他们自己输入数据源了。。。

本文就是针对这个问题,简单的介绍一下我实现的思路吧,本文只放核心代码,源码放在文章最后了。

二、涉及到技术栈

由于前端技术受限(画页面影响我输出的速度),这里就不画页面了,通过接口方式来展示。

  • Spring Boot version: 2.7.12
  • Mysql version: 8.0.29
  • Mybatis-plus version: 3.3.2
  • Mybatis动态数据源

demo中引入的依赖

		<!-- 引入web相关 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--使用Mysql数据库-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.29</version>
        </dependency>

        <!-- mybatis-plus的依赖 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.2</version>
        </dependency>

        <!--动态数据源-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>3.3.2</version>
        </dependency>

        <!--Lombok管理Getter/Setter/log等-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
            <version>1.18.24</version>
        </dependency>

三、功能实现

本文只是为了演示实现思想,源码只是一个实现的小demo,具体使用还是需要结合自己项目。

实现思想

  1. 首先,项目启动后不加载数据源
  2. 然后通过拦截器检验,是否连接数据库
  3. 如果没有连接数据库,则去配置文件的地址找配置文件,如果存在,则加载数据库配置
  4. 如果不存在数据库文件,则抛出异常,让用户输入数据库链接
  5. 用户输入数据库链接后,进行校验连接
  6. 如果连接通过,生成配置文件,保存在指定目录,供后续重启加载
  7. 同时,装载Hikari连接池,利用Mybatis plus动态切换数据源的功能,将此连接池切为master,至此,数据库启动成功。

数据库表

CREATE TABLE `maple_user`
(
    `id`        BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
    `user_name` VARCHAR(64) NOT NULL COMMENT '登录账号',
    `password`  VARCHAR(64) NOT NULL COMMENT '登录密码',
    PRIMARY KEY (`id`) USING BTREE
) COMMENT='用户信息' COLLATE='utf8_general_ci' ENGINE=InnoDB;

创建项目

这个很简单,就不多说了(说多了就是废话😂)

image-20230525143334803

配置文件

这里配置文件简单配置了,mybatis-plus的配置和文件存储的路径(init.config)

server:
  port: 8080

mybatis-plus:
  mapper-locations: classpath*:mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

init:
  config:
    filePath: /srv/apps/config/
    dbFileName: db.properfies
    userFileName: user.properfies

调用接口

这里提供了四个接口,分别是

  • 初始化数据库配置
  • 校验数据库是否链接
  • 重置数据库配置,并断开链接
  • 获取用户信息,如果不存在,初始化

接口简单的controller贴上,非核心实现就不贴了,需要的朋友可以去看源码

package com.maple.inputdb.controller;

import com.maple.inputdb.bean.InitModel;
import com.maple.inputdb.config.InitDataConfig;
import com.maple.inputdb.entity.MapleUser;
import com.maple.inputdb.service.IMapleUserService;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author 笑小枫 <https://xiaoxiaofeng.com/>
 * @date 2023/3/9
 */
@RestController
@RequestMapping("/init")
@AllArgsConstructor
public class InitController {

    private final InitDataConfig initDataConfig;

    private final IMapleUserService userService;

    /**
     * 初始化数据库
     *
     * @param initModel 数据库配置
     */
    @PostMapping("/initData")
    public void initData(@RequestBody InitModel initModel) {
        initDataConfig.initData(initModel);
    }

    /**
     * 校验数据库是否链接
     *
     * @return 配置完成
     */
    @PostMapping("/check")
    public String check() {
        return "系统配置完成";
    }

    /**
     * 重置连接数据
     */
    @PostMapping("/resetData")
    public void resetData() {
        initDataConfig.deleteFile();
    }

    /**
     * 获取用户信息,如果不存在,初始化
     *
     * @param userName 用户账号
     * @return 用户信息
     */
    @PostMapping("/getUser")
    public MapleUser getUser(String userName) {
        return userService.getUser(userName);
    }
}

核心工具类

  • 数据库是否链接全部变量,使用单例模式,初始化是否连接数据库的状态,放全局变量
package com.maple.inputdb.config;

/**
 * 数据库是否链接全部变量
 *
 * @author 笑小枫 <https://xiaoxiaofeng.com/>
 * @date 2023/3/23
 */
public class DbStatusSingleton {
    
    /**
     * false:未连接数据库  true:已连接数据库
     */
    private boolean dbStatus = false;

    private static final DbStatusSingleton DB_STATUS_SINGLETON = new DbStatusSingleton();

    private DbStatusSingleton() {
    }

    public static DbStatusSingleton getInstance() {
        return DB_STATUS_SINGLETON;
    }

    public boolean getDbStatus() {
        return dbStatus;
    }

    public void setDbStatus(boolean dbStatus) {
        this.dbStatus = dbStatus;
    }

}
  • 创建拦截器,请求进来之前先判断是否初始化配置,如果没有则报错,这里可以指定错误码,前端可以统一拦截错误码,然后跳转初始化配置页面。注意:需要在启动类上添加@ServletComponentScan注解
package com.maple.inputdb.filter;

import com.maple.inputdb.config.DbStatusSingleton;
import com.maple.inputdb.config.DynamicDatasourceConfig;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;

/**
 * @author 笑小枫 <https://xiaoxiaofeng.com/>
 * @date 2023/3/23
 */
@WebFilter(filterName = "dbFilter", urlPatterns = "/*")
@Order(1)
@Slf4j
@AllArgsConstructor
public class DbFilter implements Filter {

    private final DynamicDatasourceConfig datasourceConfig;

    private final List<String> excludedUrlList;

    @Override
    public void init(FilterConfig filterConfig) {
        excludedUrlList.addAll(Collections.singletonList(
                "/init/initData"
        ));
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        String url = ((HttpServletRequest) request).getRequestURI();
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        boolean isMatch = false;
        for (String excludedUrl : excludedUrlList) {
            if (Pattern.matches(excludedUrl.replace("*", ".*"), url)) {
                isMatch = true;
                break;
            }
        }
        if (isMatch) {
            chain.doFilter(request, response);
        } else {
            boolean isOk = DbStatusSingleton.getInstance().getDbStatus() || datasourceConfig.checkDataSource();
            if (isOk) {
                chain.doFilter(request, response);
            } else {
                log.error("初始化系统失败,请先进行系统配置");
                writeRsp(httpServletResponse);
            }
        }
    }

    private void writeRsp(HttpServletResponse response) {
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        response.setHeader("content-type", "application/json;charset=UTF-8");
        try {
            response.getWriter().println("初始化系统失败,请先进行系统配置");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • 校验数据库配置,储存初始化配置
package com.maple.inputdb.config;

import com.maple.inputdb.bean.InitModel;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

/**
 * @author 笑小枫 <https://xiaoxiaofeng.com/>
 * @date 2023/3/10
 */
@Slf4j
@Component
@AllArgsConstructor
public class InitDataConfig {

    private final DynamicDatasourceConfig dynamicDatasourceConfig;

    private final InitConfigProperties initConfigProperties;

    public void initData(InitModel initModel) {

        if (DbStatusSingleton.getInstance().getDbStatus()
                || Boolean.TRUE.equals(dynamicDatasourceConfig.checkDataSource())) {
            throw new RuntimeException("数据已完成初始化,请勿重复操作");
        }

        // 检查数据库连接是否正确
        checkConnection(initModel);

        if (!new File(initConfigProperties.getInitFilePath() + initConfigProperties.getInitUserName()).exists()) {
            File file = createFile(initConfigProperties.getInitFilePath(), initConfigProperties.getInitUserName());
            try (FileWriter out = new FileWriter(file);
                 BufferedWriter bw = new BufferedWriter(out)) {
                bw.write("userName=" + initModel.getSysUserName());
                bw.newLine();
                bw.write("password=" + initModel.getSysPassword());
                bw.flush();
            } catch (IOException e) {
                log.info("写入管理员信息文件失败", e);
                throw new RuntimeException("写入管理员信息文件失败,请重试");
            }
        }

        if (!new File(initConfigProperties.getInitFilePath() + initConfigProperties.getInitDbName()).exists()) {
            File file = createFile(initConfigProperties.getInitFilePath(), initConfigProperties.getInitDbName());
            try (FileWriter out = new FileWriter(file);
                 BufferedWriter bw = new BufferedWriter(out)) {

                bw.write(String.format("jdbcUrl=jdbc:mysql://%s:%s/%s?autoReconnect=true&autoReconnectForPools=true&useUnicode=true&characterEncoding=UTF-8",
                        initModel.getDatabaseHost(), initModel.getDatabasePort(), initModel.getDatabaseName()));
                bw.newLine();
                bw.write("username=" + initModel.getUser());
                bw.newLine();
                bw.write("password=" + initModel.getPassword());
                bw.newLine();
                bw.write("driverClassName=com.mysql.cj.jdbc.Driver");
                bw.flush();

            } catch (IOException e) {
                log.info("写入数据库文件失败", e);
                throw new RuntimeException("写入数据库文件失败,请重试");
            }
        }

        boolean isOk = dynamicDatasourceConfig.checkDataSource();
        if (!isOk) {
            throw new RuntimeException("初始化数据库信息失败,请检查配置是否正确");
        }
    }

    /**
     * 检查数据库连接是否正确
     *
     * @param initModel 连接信息
     */
    private void checkConnection(InitModel initModel) {
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
            Connection conn = DriverManager.getConnection(String.format("jdbc:mysql://%s:%s/%s",
                    initModel.getDatabaseHost(), initModel.getDatabasePort(), initModel.getDatabaseName()), initModel.getUser(), initModel.getPassword());
            log.info("校验数据库连接成功,开始进行数据库配置" + conn.getCatalog());
            conn.close();
        } catch (SQLException | ClassNotFoundException e) {
            log.info("校验数据库连接失败", e);
            throw new RuntimeException("初始化数据库信息失败,请检查配置是否正确:" + e.getMessage());
        }
    }

    private File createFile(String path, String fileName) {
        File pathFile = new File(path);
        if (pathFile.mkdirs()) {
            log.info(path + " is not exist, this is auto created.");
        }
        File file = new File(path + File.separator + fileName);
        try {
            if (!file.createNewFile()) {
                throw new RuntimeException(String.format("创建%s文件失败,请重试", fileName));
            }
        } catch (IOException e) {
            log.error(String.format("创建%s文件失败", fileName), e);
            throw new RuntimeException(String.format("创建%s文件失败,请重试", fileName));
        }
        return file;
    }

    public void deleteFile() {
        File sqlFile = new File(initConfigProperties.getInitFilePath() + initConfigProperties.getInitDbName());
        if (sqlFile.exists()) {
            log.info(sqlFile.getName() + " --- delete sql file result:" + sqlFile.delete());
        }

        File userFile = new File(initConfigProperties.getInitFilePath() + initConfigProperties.getInitUserName());
        if (userFile.exists()) {
            log.info(userFile.getName() + " --- delete user file result:" + userFile.delete());
        }

        dynamicDatasourceConfig.stopDataSource();

        // 数据初始化状态设为false
        DbStatusSingleton.getInstance().setDbStatus(false);
        log.info("初始化数据重置完成");
    }
}
  • 使用Mybatis plus动态数据源功能,进行切换数据源,完成数据库连接启动和关闭功能
package com.maple.inputdb.config;

import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.sql.Connection;
import java.util.Properties;

/**
 * @author 笑小枫 <https://xiaoxiaofeng.com/>
 * @date 2023/3/9
 */
@Slf4j
@Component
public class DynamicDatasourceConfig {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private InitConfigProperties initConfigProperties;

    public Boolean checkDataSource() {
        try {
            Connection connection = dataSource.getConnection();
            connection.close();
            DbStatusSingleton.getInstance().setDbStatus(true);
            return true;
        } catch (Exception e) {
            log.info("获取数据库连接失败,即将重新连接数据库...");
            return addDataSource();
        }
    }

    public Boolean addDataSource() {
        File file = new File(initConfigProperties.getInitFilePath() + initConfigProperties.getInitDbName());
        if (!file.exists()) {
            log.error("连接数据库失败:没有找到db.properties文件");
            return false;
        }
        try (InputStream rs = new FileInputStream(file)) {
            Properties properties = new Properties();
            properties.load(rs);
            HikariConfig config = new HikariConfig(properties);
            config.setPassword(config.getPassword());
            DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
            ds.addDataSource("master", new HikariDataSource(config));
            ds.setPrimary("master");
            DbStatusSingleton.getInstance().setDbStatus(true);
            log.info("连接数据库成功");
            return true;
        } catch (Exception e) {
            log.error("连接数据库失败:" + e);
            return false;
        }
    }

    /**
     * 关闭数据库连接
     */
    public void stopDataSource() {
        try {
            Connection connection = dataSource.getConnection();
            connection.close();
        } catch (Exception e) {
            log.info("数据库连接已关闭,无需重复关闭...");
            return;
        }
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        HikariDataSource hds = (HikariDataSource) ds.getDataSource("master");
        try {
            if (hds.isRunning()) {
                hds.close();
                log.info("数据库连接已关闭");
            }
            ds.setPrimary("null");
            ds.removeDataSource("master");
        } catch (Exception e) {
            log.error("关闭数据库连接失败:", e);
            e.printStackTrace();
        }
    }
}
  • 初始化数据的Model类
package com.maple.inputdb.bean;

import lombok.Data;

/**
 * @author 笑小枫 <https://xiaoxiaofeng.com/>
 * @date 2023/3/10
 */
@Data
public class InitModel {
    /**
     * 数据库相关字段
     */
    private String databaseHost;
    private String databasePort;
    private String databaseName;
    private String user;
    private String password;


    /**
     * 初始化用户
     */
    private String sysUserName;
    private String sysPassword;

}
  • 初始化数据的配置文件类
package com.maple.inputdb.config;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

/**
 * @author 笑小枫 <https://xiaoxiaofeng.com/>
 * @date 2023/3/10
 */
@Data
@Configuration
public class InitConfigProperties {

    @Value("${init.config.filePath}")
    private String initFilePath;

    @Value("${init.config.dbFileName}")
    private String initDbName;

    @Value("${init.config.userFileName}")
    private String initUserName;

}

还有一些getUser接口牵扯到的文件crud类,这里就不一一去贴了。

四、功能测试

  • check连接:首先启动项目,调用/init/check接口,这是还没有初始化数据库配置,在拦截器拦截校验时报错了,如下图所示:

image-20230526111351407

  • 初始化连接:调用/init/initData接口初始化数据
{
  "sysUserName": "admin",
  "sysPassword": "123456",
  "databaseHost": "127.0.0.1",
  "databasePort": "3306",
  "databaseName": "maple",
  "user": "root",
  "password": "123456"
}

初始化时,会先校验是否初始化过配置,如果没有才会进行,保存配置,并连接数据库,如下图所示:

image-20230526111702732

这是看我们存放配置的文件路径里面已经出现了我们的文件。

image-20230526112003126

打开db.properfies文件,可以看到下面内容,后续项目重新启动,会先来此目录判断文件是否存在,如果存在,则会自动加载文件内容,去连接数据库。image-20230526112028170

  • check连接:可以看到此时数据库已经连接成功

image-20230526111853516

  • 重置数据库:调用/init/resetData接口,这里会报一个错,可以忽略,想了解详情的,可以去看源码,然后会无了一个大语…

image-20230526112301509

  • 再次check连接:可以看到,在拦截器拦截校验时,数据库已经断开,配置文件也已经删除,需要重新配置了

image-20230526112454084
最后附上现有业务的两张初始化的页面吧,就不贴代码了~

初始化页面:

image-20230606101627994

初始化数据库画面:

image-20230606101437988

初始化管理员账号信息画面:

image-20230606101518567

五、功能总结

本文主要利用Mybatis Plus的动态切换数据源的功能,间接实现了无数据源启动,用户自定义数据源的功能。只是一种实现思路,肯定还会有更优的实现方案,暂时还没有找到,如找到,会继续出文介绍。

配合本文的还有数据库版本管理,连接数据库后,可以初始化数据库表结构,然后再初始化管理员信息,后续迭代升级时,sql变更,在项目启动时自动加载,维护数据库表版本,可以去看后续的文章,通过flywaydb实现。

本文到此就结束了,如果帮助到你了,帮忙点个赞👍

本文源码:https://github.com/hack-feng/maple-product/tree/main/maple-input-db

SpringBoot使用flywaydb实现数据库版本管理【附源码】

我是笑小枫,全网皆可搜的【笑小枫】

  • 17
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论
资源介绍:基于Spring Boot开发的桥牌计分系统 本次为大家呈现的是一个基于Spring Boot框架开发的桥牌计分系统,这是一个专为桥牌爱好者设计的强大工具,不仅能够帮助玩家快速、准确地记录比赛成绩,还提供了丰富的数据分析功能,帮助玩家深入了解自己的牌技水平,优化比赛策略。 该系统采用了Spring Boot作为后端开发框架,保证了系统的稳定性和可扩展性。前端界面设计简洁明了,易于操作,即使是不熟悉计算机技术的桥牌爱好者也能轻松上手。同时,系统支持多用户并发使用,可以满足大型桥牌赛事的需求。 在功能方面,系统提供了完整的桥牌计分流程,包括比赛设置、成绩录入、成绩查询等功能。用户可以根据比赛规则自定义计分方式,系统会自动计算得分并生成详细的成绩报表。此外,系统还提供了数据可视化功能,用户可以通过图表直观地了解比赛成绩的分布情况,发现潜在的问题和优势。 值得一提的是,该系统还具有良好的二次开发定制性。开发者可以根据具体需求对系统进行扩展和优化,添加新的功能模块或调整现有功能。这使得该系统不仅适用于桥牌比赛计分,还可以作为其他类似比赛的计分工具,具有广泛的应用前景。 总的来说,这个基于Spring Boot开发的桥牌计分系统是一个功能强大、易于操作、可定制性高的工具,对于桥牌爱好者来说是一个非常实用的助手。无论是用于日常练习还是大型赛事,它都能为玩家提供准确、高效的计分服务,助力玩家在桥牌世界中取得更好的成绩。
SpringBoot2的启动流程是通过@SpringBootApplication注解自动化配置来实现的。该注解包含了多个注解的组合,其中包括@ComponentScan、@EnableAutoConfiguration和@Configuration等。通过这些注解,Spring Boot会自动扫描并加载配置类,并根据自动配置规则来配置应用程序。 具体而言,当应用程序启动时,Spring Boot会创建一个Spring应用程序上下文。在创建上下文的过程中,会先加载主配置类(通常是包含main方法的类),然后根据@ComponentScan注解扫描指定包下的所有组件。 接下来,Spring Boot会根据@EnableAutoConfiguration注解自动配置应用程序。这个注解会根据classpath和条件匹配的规则,加载配置类,并将它们注册到应用程序上下文中。这些配置使用了@Configuration注解,会声明一些Bean,并根据条件来决定是否生效。 最后,Spring Boot启动应用程序,并执行相应的事件处理器。这些事件处理器可以通过自定义ApplicationListener来实现。在应用程序运行期间,Spring Boot会触发不同的事件,并调用相应的事件处理器。 参考文献: 引用:SpringBoot2 | @SpringBootApplication注解 自动化配置流程源码分析(三) [2] 引用:SpringBoot2 | SpringBoot监听器源码分析 | 自定义ApplicationListener(六) 引用:该系列主要还是Spring的核心源码,不过目前Springboot大行其道,所以就从Springboot开始分析。最新版本是Springboot2.0.4,Spring5,所以新特性本系列后面也会着重分析。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

笑小枫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值