老系統改造增加初始化,自動化數據源配置
- 一、前言
- 二、改造描述
- 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
重新配置即可
四、結語
此方法使用于老項目,并且不會做升級的老項目。示例代碼不一定規范,各位可按照邏輯做調整。頁面美化,可自行根據需要調整