老系统改造增加初始化,自动化数据源配置(tomcat+jsp+springmvc)
老系统改造增加初始化,自动化数据源配置
- 一、前言
- 二、改造描述
- 1、环境说明
- 2、实现步骤简要思考
- 三、开始改造
- 1、准备sql初始化文件
- 2、启动时自动读取jdbc文件,创建数据源,如未配置,需要一个默认的临时数据源
- 2.1去掉sping mvc原本配置的固定dataSource,改为动态dataSource
- 2.2 代码类,这里是示例,我就不管规范了,放到一起
- 2.2.1 DynamicDataSourceConfig.java
- 2.2.2 JdbcConfig.java
- 2.2.3 ProxyDataSource.java
- 3. 编辑jdbc配置,保存配置,根据配置创建新的数据源,并销毁就的数据源,同时改变新数据源的SqlSessionFactory
- 3.1放开登录拦截
- 3.1.1 登录拦截器添加代码
- 3.2 控制器实现,和前端页面代码
- 3.2.1 SetupController.java
- 3.2.2 DatabaseInitService.java
- 3.2.3 setup.jsp
- 3.2.4 init-db.jsp
- 3.2.5 message.jsp
- 4、正常访问系统
- 四、结语
一、前言
在技术飞速迭代的当下,许多老旧项目因未及时升级,陷入了部署困境。这类系统往往缺乏标准化配置,依赖特殊运行环境,加之团队中运维角色的缺失,每次上线部署都得劳烦开发人员远程操作。繁琐的手动配置、环境依赖冲突、版本兼容问题层出不穷,不仅占用开发精力,还常因操作失误导致部署失败,拖慢业务推进节奏。为此,亟需一套极简的部署方案
—— 无需专业技术背景,让普通员工通过点击几次鼠标,就能完成系统的安装与上线,彻底摆脱对开发人员的依赖,化解老系统部署的效率瓶颈。
二、改造描述
1、环境说明
springmvc
+ mybatis plus
+ jsp
+ maven
的 javaweb
项目
2、实现步骤简要思考
- 准备初始化sql结构文件 定义绕过权限的接口,访问初始化配置页面
- 启动时自动读取jdbc文件,创建数据源,如未配置,需要一个默认的临时数据源
- 编辑jdbc配置,保存配置,根据配置创建新的数据源,并销毁就的数据源,同时改变新数据源的SqlSessionFactory,执行初始化脚本
- 正常访问系统
三、开始改造
1、准备sql初始化文件
不涉及代码,不过多介绍,把准备好的sql脚本放到资源文件下,新建一个sql目录,注意后续增量sql继续增加后面,按照增量时间改名。后续初始化的时候,会按照时间顺序执行初始化脚本
注意:1、调整maven的pom.xml文件,让maven能把sql脚本文件编译到class里面去,不然后面执行,找不到文件
2、启动时自动读取jdbc文件,创建数据源,如未配置,需要一个默认的临时数据源
2.1去掉sping mvc原本配置的固定dataSource,改为动态dataSource
注意:需要添加包扫描,保证动态dataSource那部分bean能被正常扫描
2.2 代码类,这里是示例,我就不管规范了,放到一起
2.2.1 DynamicDataSourceConfig.java
package com.luozc.config.jdbc;import com.alibaba.druid.pool.DruidDataSource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope; // 添加缺失的Scope导入
import javax.sql.DataSource;
import java.sql.SQLException;@Configuration
public class DynamicDataSourceConfig {@Autowiredprivate JdbcConfig jdbcConfig;@Bean(name = "dataSource")@Scope("prototype")public DataSource dataSource() throws SQLException {System.out.println("[动态数据源] 创建新数据源...");jdbcConfig.loadConfig();String driver = jdbcConfig.getDriverClassName();String url = jdbcConfig.getUrl();String username = jdbcConfig.getUsername();String password = jdbcConfig.getPassword();System.out.println("[动态数据源] 使用以下配置创建数据源:");System.out.println("driver: " + driver);System.out.println("url: " + url);System.out.println("username: " + username);System.out.println("password: " + (password != null ? "******" : "null"));if (driver == null || url == null) {System.err.println("[动态数据源] 配置为空,返回代理数据源");return new ProxyDataSource(jdbcConfig);}DruidDataSource dataSource = new DruidDataSource();dataSource.setDriverClassName(driver);dataSource.setUrl(url);dataSource.setUsername(username);dataSource.setPassword(password);// 验证数据库连接try {// 获取并关闭连接,验证连接有效性dataSource.getConnection().close();System.out.println("[动态数据源] 新数据源已成功创建并验证。");} catch (SQLException e) {System.err.println("[动态数据源] 数据库连接验证失败: " + e.getMessage());// 记录异常堆栈信息e.printStackTrace();throw e;}return dataSource;}
}
2.2.2 JdbcConfig.java
package com.luozc.config.jdbc;import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import java.io.*;
import java.nio.file.Files;
import java.util.Properties;@Component
public class JdbcConfig {private static final String JDBC_CONFIG_FILE = "jdbc.properties";private String driverClassName; // 默认驱动private String url; // 默认URLprivate String username;private String password;private boolean initialized = false;public void init() {System.out.println("JdbcConfig loaded:");System.out.println("driverClassName: " + driverClassName);System.out.println("url: " + url);System.out.println("username: " + username);}// 加载配置public void loadConfig() {Properties props = new Properties();String userDir = System.getProperty("user.dir");File file = new File(userDir + File.separator + JDBC_CONFIG_FILE);if(file.exists()){try (InputStream is = Files.newInputStream(file.toPath())) {props.load(is);driverClassName = props.getProperty("jdbc.driverClassName");url = props.getProperty("jdbc.url");username = props.getProperty("jdbc.username");password = props.getProperty("jdbc.password");initialized = Boolean.parseBoolean(props.getProperty("jdbc.initialized", "false"));} catch (IOException e) {e.printStackTrace();}}}// 验证配置是否有效public boolean isValid() {return driverClassName != null && !driverClassName.isEmpty() &&url != null && !url.isEmpty() &&username != null && !username.isEmpty();}// 保存配置到属性文件public void saveConfig(boolean persist) {Properties props = new Properties();String userDir = System.getProperty("user.dir");props.setProperty("jdbc.driverClassName", driverClassName);props.setProperty("jdbc.url", url);props.setProperty("jdbc.username", username);props.setProperty("jdbc.password", password);initialized = persist;props.setProperty("jdbc.initialized", String.valueOf(persist));File file = new File(userDir + File.separator + JDBC_CONFIG_FILE);try (OutputStream os = new FileOutputStream(file)) {props.store(os, "Sys Configuration");} catch (IOException e) {e.printStackTrace();}}// Getters and Setterspublic String getDriverClassName() { return driverClassName; }public void setDriverClassName(String driverClassName) { this.driverClassName = driverClassName; }public String getUrl() { return url; }public void setUrl(String url) { this.url = url; }public String getUsername() { return username; }public void setUsername(String username) { this.username = username; }public String getPassword() { return password; }public void setPassword(String password) { this.password = password; }public boolean isInitialized() { return initialized; }public void setInitialized(boolean initialized) { this.initialized = initialized; }
}
2.2.3 ProxyDataSource.java
package com.luozc.config.jdbc;import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.stereotype.Component;import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;/*** 代理数据源,在数据库配置完成前处理请求*/
@Component
public class ProxyDataSource implements DataSource {private final JdbcConfig jdbcConfig;private DataSource fallbackDataSource; // 新增备用数据源public ProxyDataSource(JdbcConfig jdbcConfig) {this.jdbcConfig = jdbcConfig;this.fallbackDataSource = createFallbackDataSource();}private DataSource createFallbackDataSource() {DruidDataSource ds = new DruidDataSource();ds.setDriverClassName("org.h2.Driver");ds.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1");ds.setUsername("sa");ds.setPassword("");ds.setInitialSize(1);ds.setMinIdle(1);ds.setMaxActive(5);return ds;}@Overridepublic Connection getConnection() throws SQLException {if (!jdbcConfig.isInitialized()) {return fallbackDataSource.getConnection();}throw new UnsupportedOperationException("代理数据源不支持直接获取连接");}@Overridepublic Connection getConnection(String username, String password) throws SQLException {return getConnection();}// 实现其他 DataSource 方法...@Override public <T> T unwrap(Class<T> iface) throws SQLException { return null; }@Override public boolean isWrapperFor(Class<?> iface) throws SQLException { return false; }@Override public PrintWriter getLogWriter() throws SQLException { return null; }@Override public void setLogWriter(PrintWriter out) throws SQLException {}@Override public void setLoginTimeout(int seconds) throws SQLException {}@Override public int getLoginTimeout() throws SQLException { return 0; }@Override public Logger getParentLogger() throws SQLFeatureNotSupportedException { return null; }}
注意:这里的备用数据源,需要引入包
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><version>1.4.200</version></dependency>
3. 编辑jdbc配置,保存配置,根据配置创建新的数据源,并销毁就的数据源,同时改变新数据源的SqlSessionFactory
3.1放开登录拦截
代码如下
3.1.1 登录拦截器添加代码
// 获取JdbcConfig BeanServletContext context = request.getServletContext();JdbcConfig jdbcConfig = (JdbcConfig) context.getAttribute("jdbcConfig");// 如果配置为空,先加载配置if (jdbcConfig == null) {jdbcConfig = new JdbcConfig();jdbcConfig.loadConfig();if(jdbcConfig.isValid()){context.setAttribute("jdbcConfig", jdbcConfig);}}// 排除配置相关的URLString requestURI = request.getRequestURI();if (requestURI.contains("/setup") || requestURI.contains("/init-db")|| requestURI.startsWith("/resources")) {return true;}// 如果未初始化或配置无效,重定向到配置页面if (!jdbcConfig.isInitialized() || !jdbcConfig.isValid()) {response.sendRedirect(request.getContextPath() + "/setup");return false;}
3.2 控制器实现,和前端页面代码
3.2.1 SetupController.java
package com.luozc.config.jdbc;import com.alibaba.druid.pool.DruidDataSource;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.transaction.TransactionFactory;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;import javax.sql.DataSource;
import java.io.IOException;
import java.sql.Connection;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;@Controller
public class SetupController {@Autowiredprivate JdbcConfig jdbcConfig;@Autowiredprivate DatabaseInitService initService;@Autowiredprivate ApplicationContext applicationContext; // 注入上下文@Autowiredprivate SqlSessionFactory sqlSessionFactory; // 新增注入@GetMapping("/setup")public String showSetupForm(Model model) {return "setup"; // 返回配置页面}@PostMapping("/setup")public String saveConfig(@RequestParam("driverClassName") String driverClassName,@RequestParam("url") String url,@RequestParam("username") String username,@RequestParam("password") String password,@RequestParam(name="sfInit",required = false) String sfInit,Model model) {// 保存数据库配置jdbcConfig.setDriverClassName(driverClassName);jdbcConfig.setUrl(url);jdbcConfig.setUsername(username);jdbcConfig.setPassword(password);if(StringUtils.isNotBlank(sfInit)){jdbcConfig.saveConfig(false);return "redirect:/init-db";}else{jdbcConfig.saveConfig(true);// 可选:手动刷新数据源try {// 获取当前数据源并尝试关闭(如果是DruidDataSource)DataSource currentDataSource = (DataSource) applicationContext.getBean("dataSource");if (currentDataSource instanceof DruidDataSource) {((DruidDataSource) currentDataSource).close();System.out.println("[数据源切换] 旧数据源已关闭。");}// 获取Bean工厂并移除旧的数据源定义DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();if (beanFactory.containsBeanDefinition("dataSource")) {beanFactory.removeBeanDefinition("dataSource");}// 获取新的数据源实例DataSource newDataSource = (DataSource) applicationContext.getBean("dataSource");// 正确刷新 MyBatis 的 SqlSessionFactory,确保 TransactionFactory 不为空TransactionFactory transactionFactory = sqlSessionFactory.getConfiguration().getEnvironment().getTransactionFactory();if (transactionFactory == null) {transactionFactory = new JdbcTransactionFactory(); // 使用默认事务工厂}sqlSessionFactory.getConfiguration().setEnvironment(new org.apache.ibatis.mapping.Environment("default", transactionFactory, newDataSource));System.out.println("[MyBatis] 环境已刷新,使用新数据源。");// 验证新数据源连接try (Connection conn = newDataSource.getConnection()) {System.out.println("[数据源切换] 新数据源验证成功,当前数据库: " + conn.getCatalog());}System.out.println("[数据源切换] 新数据源已激活。");} catch (Exception e) {e.printStackTrace();System.err.println("[数据源切换] 切换失败: " + e.getMessage());}return "redirect:/"; // 已初始化,重定向到首页}}@GetMapping("/init-db")public String showInitPage(Model model) {if (jdbcConfig.isInitialized()) {return "redirect:/"; // 已初始化,重定向到首页}return "init-db"; // 返回初始化页面}@PostMapping("/init-db")public String initDatabase(Model model) {try {initService.initDatabase();// 可选:手动刷新数据源try {// 获取当前数据源并尝试关闭(如果是DruidDataSource)DataSource currentDataSource = (DataSource) applicationContext.getBean("dataSource");if (currentDataSource instanceof DruidDataSource) {((DruidDataSource) currentDataSource).close();System.out.println("[数据源切换] 旧数据源已关闭。");}// 获取Bean工厂并移除旧的数据源定义DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();if (beanFactory.containsBeanDefinition("dataSource")) {beanFactory.removeBeanDefinition("dataSource");}// 获取新的数据源实例DataSource newDataSource = (DataSource) applicationContext.getBean("dataSource");// 正确刷新 MyBatis 的 SqlSessionFactory,确保 TransactionFactory 不为空TransactionFactory transactionFactory = sqlSessionFactory.getConfiguration().getEnvironment().getTransactionFactory();if (transactionFactory == null) {transactionFactory = new JdbcTransactionFactory(); // 使用默认事务工厂}sqlSessionFactory.getConfiguration().setEnvironment(new org.apache.ibatis.mapping.Environment("default", transactionFactory, newDataSource));System.out.println("[MyBatis] 环境已刷新,使用新数据源。");// 验证新数据源连接try (Connection conn = newDataSource.getConnection()) {System.out.println("[数据源切换] 新数据源验证成功,当前数据库: " + conn.getCatalog());}System.out.println("[数据源切换] 新数据源已激活。");} catch (Exception e) {e.printStackTrace();System.err.println("[数据源切换] 切换失败: " + e.getMessage());}model.addAttribute("message", "数据库初始化成功!请重启应用服务器。");} catch (Exception e) {e.printStackTrace();model.addAttribute("error", "初始化失败: " + e.getMessage());return "init-db";}return "message"; // 返回成功消息页面}
}
3.2.2 DatabaseInitService.java
package com.luozc.config.jdbc;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.stereotype.Service;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;@Service
public class DatabaseInitService {@Autowiredprivate JdbcConfig jdbcConfig;public void initDatabase() throws Exception {// 加载数据库驱动Class.forName(jdbcConfig.getDriverClassName());// 建立数据库连接try (Connection conn = DriverManager.getConnection(jdbcConfig.getUrl(),jdbcConfig.getUsername(),jdbcConfig.getPassword())) {// 执行初始化SQL脚本executeSqlScript(conn, "/sql/init.sql");List<String> sortedSqlFiles = getSortedSqlFiles();for (int i = 0; i < sortedSqlFiles.size(); i++) {// 执行初始化SQL脚本executeSqlScript(conn, "/sql/"+sortedSqlFiles.get(i));}// 标记数据库已初始化jdbcConfig.saveConfig(true);}}private void executeSqlScript(Connection conn, String sqlResourcePath) throws IOException, SQLException {InputStream is = getClass().getResourceAsStream(sqlResourcePath);if (is == null) {throw new IOException("SQL脚本文件未找到: " + sqlResourcePath);}try (BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {StringBuilder sqlStatement = new StringBuilder();String line;while ((line = reader.readLine()) != null) {// 跳过注释if (line.startsWith("--") || line.trim().isEmpty()) {continue;}sqlStatement.append(line);// 如果是完整的SQL语句if (line.trim().endsWith(";")) {String sql = sqlStatement.toString().replace(";", "");try (Statement stmt = conn.createStatement()) {stmt.execute(sql);}sqlStatement.setLength(0); // 清空}}}}// 修改getSortedSqlFiles方法,使用Spring的ResourceUtils读取资源文件public List<String> getSortedSqlFiles() {List<String> sqlFiles = new ArrayList<>();try {// 使用Spring的ResourceUtils获取资源目录ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();Resource[] resources = resolver.getResources("classpath:sql/*.sql");// 提取文件名并添加到列表for (Resource resource : resources) {if (resource.isReadable()) {String filename = resource.getFilename();if (filename != null && filename.endsWith(".sql")&&!filename.contains("init")) {sqlFiles.add(filename);}}}// 按文件名中的数字排序sqlFiles.sort((f1, f2) -> {// 提取文件名中的数字部分int num1 = extractNumber(f1);int num2 = extractNumber(f2);return Integer.compare(num1, num2);});} catch (Exception e) {e.printStackTrace();}return sqlFiles;}// 从文件名中提取数字部分private int extractNumber(String filename) {// 去掉.sql扩展名String name = filename.substring(0, filename.lastIndexOf("."));// 提取数字后缀int i = name.length() - 1;while (i >= 0 && Character.isDigit(name.charAt(i))) {i--;}// 如果文件名以数字结尾,则返回对应的数字,否则返回0if (i < name.length() - 1) {return Integer.parseInt(name.substring(i + 1));} else {return 0; // 对于没有数字后缀的文件,默认为0}}
}
jsp代码如下,注意放的目录为你配置spring mvc的上下文目录
3.2.3 setup.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head><title>数据库配置</title><style>body { font-family: Arial, sans-serif; }.container { max-width: 800px; margin: 0 auto; padding: 10px; }.form-group { margin-bottom: 15px; }label { display: block; margin-bottom: 5px; }input { width: 100%; padding: 8px; box-sizing: border-box; }button { padding: 10px 20px; background-color: #4CAF50; color: white; border: none; }</style>
</head>
<body>
<div class="container"><h2>创建数据库用户sql示例:</h2><form ><div class="form-group"><label >查询表空间地址:</label><div style="color: red;">SELECT T.TABLESPACE_NAME,D.FILE_NAME,D.AUTOEXTENSIBLE,D.BYTES,D.MAXBYTES,D.STATUS FROM DBA_TABLESPACES T, DBA_DATA_FILES DWHERE T.TABLESPACE_NAME = D.TABLESPACE_NAME ORDER BY TABLESPACE_NAME, FILE_NAME;</div></div><div class="form-group"><label >创建永久表空间,初始大小500MB,自动扩展每次50MB,最大2GB:</label><div style="color: red;">CREATE TABLESPACE xxx DATAFILE '/usr/local/oracle/oradata/orcl/xxx.dbf'SIZE 500M AUTOEXTEND ON NEXT 50M MAXSIZE 2G EXTENT MANAGEMENT LOCAL SEGMENT SPACE MANAGEMENT AUTO;</div></div><div class="form-group"><label >创建用户并分配表空间:</label><div style="color: red;">CREATE USER xxx IDENTIFIED BY xxx DEFAULT TABLESPACE xxx ;</div></div><div class="form-group"><label >允许用户无限制使用表空间:</label><div style="color: red;">ALTER USER xxx QUOTA UNLIMITED ON xxx;</div></div><div class="form-group"><label >基础权限(登录、建表):</label><div style="color: red;">GRANT CREATE SESSION, CREATE TABLE TO xxx;</div></div><div class="form-group"><label >高级权限(建视图、序列、存储过程):</label><div style="color: red;">GRANT CREATE VIEW, CREATE SEQUENCE TO xxx;</div></div></form><h2>数据库配置</h2><form action="setup" method="post"><div class="form-group"><label for="sfInit">是否初始化:</label><input type="checkbox" id="sfInit" name="sfInit" value="true" style="width: auto"></div><div class="form-group"><label for="driverClassName">driverClassName:</label><input type="text" id="driverClassName" name="driverClassName" required></div><div class="form-group"><label for="url">JDBC URL:</label><input type="text" id="url" name="url" required></div><div class="form-group"><label for="username">用户名:</label><input type="text" id="username" name="username" required></div><div class="form-group"><label for="password">密码:</label><input type="password" id="password" name="password" required></div><button type="submit">保存配置</button></form>
</div>
</body>
</html>
3.2.4 init-db.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head><title>初始化数据库</title><style>body { font-family: Arial, sans-serif; }.container { max-width: 500px; margin: 0 auto; padding: 20px; }.error { color: red; }</style>
</head>
<body>
<div class="container"><h2>初始化数据库</h2><p>检测到新的数据库配置,需要初始化数据库。</p><% if (request.getAttribute("error") != null) { %><p class="error"><%= request.getAttribute("error") %></p><% } %><form action="init-db" method="post"><button type="submit">开始初始化</button></form>
</div>
</body>
</html>
3.2.5 message.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head><title>操作结果</title><style>body { font-family: Arial, sans-serif; }.container { max-width: 500px; margin: 0 auto; padding: 20px; }</style>
</head>
<body>
<div class="container"><h2>操作结果</h2><p><%= request.getAttribute("message") %></p><p>点击下面的链接返回首页:</p><a href="<%= request.getContextPath() %>/">首页</a>
</div>
</body>
</html>
4、正常访问系统
至此系统正常访问
注意:此系统可随时切换数据源,直接访问地址 /setup
重新配置即可
四、结语
此方法使用于老项目,并且不会做升级的老项目。示例代码不一定规范,各位可按照逻辑做调整。页面美化,可自行根据需要调整