如题,该插件支持项目启动时自动检查数据库是否需要执行SQL脚本。做该插件的原因是该客户有运维人员,并且是客户自己运营系统。我们是碰不到客户机器的,系统也是多租户,一个租户是一个数据仓库,人工执行肯定不行的。所以数据库连接启动时就需要执行升级SQL。网上有类似插件,但是不是复杂就是过于简单,想要SQL脚本支持Enjoy SQL模板语法,最好还是自己撸一个比较好用和方便定制。
分享内容我简单的整理剔除了一些业务代码,尽量保持简洁易读。废话不多说了,直接上🐴!
UpgradeSqlPlugin是JF插件:
package com.momathink.common.plugin.activerecord;
import cn.yunjiaowu.table.plugin.activerecord.directive.*;
import com.jfinal.kit.PathKit;
import com.jfinal.log.Log;
import com.jfinal.plugin.IPlugin;
import com.jfinal.plugin.activerecord.*;
import com.jfinal.template.Engine;
import com.jfinal.template.Template;
import java.io.File;
import java.util.*;
/**
* 项目启动时 数据库升级插件
* @author 杜福忠
*/
public class UpgradeSqlPlugin implements IPlugin {
private Log log = Log.getLog(UpgradeSqlPlugin.class);
private ActiveRecordPlugin arp;
private DbPro db;
private Config config;
private Engine engine;
private String tableName = "upgrade_sql_config_log";
private String folder = "/upgrade_sql";
private ArrayList<String> existing = null;
private boolean immediatelyStop = true;
public UpgradeSqlPlugin(IDataSourceProvider dataSource) {
this.arp = new ActiveRecordPlugin(getName(), dataSource);
}
@SuppressWarnings("unused")
public UpgradeSqlPlugin(IDataSourceProvider dataSource, int transactionLevel) {
this.arp = new ActiveRecordPlugin(getName(), dataSource, transactionLevel);
}
private static int count = 0;
protected synchronized static String getName() {
count ++;
return "UpgradeSqlPlugin_" + count;
}
@Override
public boolean stop() {
db = null;
config = null;
engine = null;
existing = null;
if (this.arp != null){
this.arp.stop();
this.arp = null;
}
SqlParaDirective.remove();
log = null;
return true;
}
@Override
public boolean start() {
Objects.requireNonNull(this.arp, "UpgradeSqlPlugin设计上只支持start一次");
this.arp.start();
try {
configDb();
configSql();
}catch (Exception e) {
log.error("升级数据库运行异常:", e);
return false;
}
//执行完升级SQL 立即注销 节约内存
if (this.immediatelyStop){
stop();
}
return true;
}
@SuppressWarnings("unused")
public UpgradeSqlPlugin setImmediatelyStop(boolean immediatelyStop) {
this.immediatelyStop = immediatelyStop;
return this;
}
private void configSql() {
File files = getFolderPath();
if (! files.isDirectory()){
return;
}
File[] listFiles = files.listFiles();
if (listFiles == null || listFiles.length == 0) {
return;
}
//名称排序
Arrays.sort(listFiles);
for (File file : listFiles) {
//名称统一小写,避免平台问题
String sqlName = file.getName().toLowerCase();
if (file.isFile() && sqlName.endsWith(".sql") && !existing.contains(sqlName)){
runSql(file, sqlName);
}
}
}
private File getFolderPath() {
return new File(PathKit.getRootClassPath() + folder);
}
private void runSql(File file, String sqlName) {
Template template = engine.getTemplate(file.getName());
log.info("数据库执行升级文件:" + sqlName);
long t = System.currentTimeMillis();
final String[] ret = {"ok", null};
db.tx(() -> {
try{
ret[1] = template.renderToString();
return true;
}catch (Exception e){
ret[0] = "fail";
ret[1] = e.getMessage();
return false;
}
});
saveRecord(sqlName, ret[0], t, ret[1]);
}
protected void saveRecord(String sqlName, String status, long t, String logStr) {
if (logStr != null) {
logStr = logStr.trim();
}
Record r = new Record();
r.set("sqlName", sqlName);
r.set("status", status);
r.set("runDate", new Date());
r.set("duration", System.currentTimeMillis() - t);
r.set("log", logStr);
db.save(tableName, r);
}
private void configDb() {
this.config = arp.getConfig();
Objects.requireNonNull(config, "ActiveRecordPlugin 需要先启动");
this.db = Db.use(config.getName());
SqlParaDirective.setDb(this.db);
configEngine();
try{
List<Record> list = db.find("SELECT sqlName FROM " + tableName + " WHERE status = 'ok'");
this.existing = new ArrayList<>(list.size());
for (Record r : list) {
this.existing.add(r.getStr("sqlName"));
}
}catch (ActiveRecordException e) {
//建表
initTable();
this.existing = new ArrayList<>(0);
}
}
protected void initTable() {
db.update("CREATE TABLE `" + tableName + "` ( " +//
" `id` int(11) NOT NULL AUTO_INCREMENT, " +//
" `sqlName` varchar(255) NOT NULL, " +//
" `status` varchar(10) NULL, " +//
" `runDate` datetime NULL, " +//
" `duration` int(11) NULL, " +//
" `log` longtext NULL, " +//
" PRIMARY KEY (`id`) " +//
") COMMENT='数据库升级记录'");
}
private void configEngine() {
this.engine = config.getSqlKit().getEngine();
engine.setBaseTemplatePath(folder);
engine.addDirective("try", TryDirective.class, true);
engine.addDirective("find", FindDirective.class, true);
engine.addDirective("findFirst", FindFirstDirective.class, true);
engine.addDirective("runSql", RunSqlDirective.class, true);
}
@SuppressWarnings("unused")
public void setTableName(String tableName) {
Objects.requireNonNull(tableName, "tableName 不能为null");
this.tableName = tableName;
}
public void setFolder(String folder) {
Objects.requireNonNull(folder, "folder 不能为null");
if (! folder.startsWith("/")) {
folder = "/" + folder;
}
this.folder = folder;
}
public DbPro getDb() {
return db;
}
}使用姿势:
1、注册插件:https://jfinal.com/doc/2-5
public void configPlugin(Plugins me) {
DruidPlugin dp = new DruidPlugin(jdbcUrl, userName, password);
me.add(dp);
//自动检查升级数据库
me.add(new UpgradeSqlPlugin(dp));
ActiveRecordPlugin arp = new ActiveRecordPlugin(dp);
arp.addMapping("user", User.class);
me.add(arp);
}数据库连接账户需要使用 有建表 修改表 等权限的账户,一般升级脚本里面就是要建表和改变表结构的。
如果担心业务数据账户安全 ,可以使用独立的DruidPlugin,和业务数据库账户分开,高权限的账户只做升级操作,升级完立即stop掉就可以了。
2、编写SQL脚本:
在配置文件目录 》建一个upgrade_sql目录,里面编写模板SQL文件即可,比如:

SQL文件除了支持 官方的#para 指令,还增加了几个常用指令:
#runSql、#find、#findFirst、#try
执行多行SQL: #runSql(受影响行数返回结果的变量名默认是result, true) 特殊场景:给true参数时代表运行异常时可继续执行 在此是SQL语句,多条用 ;\n 分号加回车符 隔开 #end 查询list集合的,一般用于需要先查出结果再根据数据进行修改的,比如迁移历史数据,之前是一对一,后面改成了一对多等等业务 #find(返回结果的变量名默认是list) 在此是SQL语句 #end 查一条数据的: #findFirst(返回结果的变量名默认是record) 在此是SQL语句 #end 以及吃住异常再处理的指令: #try(默认值exception可设置) 包裹代码或指令 #end #if(exception) #(exception.getMessage()) #end ----------------------------------------------------- 例子: #runSql() ALTER TABLE xxxx #end
项目启动升级后,数据库会自动建一个upgrade_sql_config_log记录升级情况。

业务系统做一个界面可查看升级记录就OK了。
PS:附加上面的Enjoy指令代码:
package com.momathink.common.plugin.activerecord.directive;
import com.jfinal.plugin.activerecord.SqlPara;
/**
* #find 指令 用于需要查询结果后代码处理的
* <p>
* 定义:
* #find(返回结果的变量名默认是list)
* 在此是SQL语句
* #end
* <p>
*/
public class FindDirective extends SqlParaDirective {
@Override
protected Object getData(SqlPara sqlPara) {
setDefaultName("list");
return db().find(sqlPara);
}
}package com.momathink.common.plugin.activerecord.directive;
import com.jfinal.plugin.activerecord.SqlPara;
/**
* #findFirst 指令 用于需要查询结果后代码处理的
* <p>
* 定义:
* #findFirst(返回结果的变量名默认是record)
* 在此是SQL语句
* #end
* <p>
*/
public class FindFirstDirective extends SqlParaDirective {
@Override
protected Object getData(SqlPara sqlPara) {
setDefaultName("record");
return db().findFirst(sqlPara);
}
}package com.momathink.common.plugin.activerecord.directive;
import com.jfinal.kit.StrKit;
import com.jfinal.log.Log;
import com.jfinal.plugin.activerecord.ActiveRecordException;
import com.jfinal.plugin.activerecord.SqlPara;
import com.jfinal.template.expr.ast.Const;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* #sqlPara 指令方便定义SqlPara <a href="https://www.jfinal.com/doc/5-13">SqlPara</a>
* <p>
* 定义:
* #runSql(受影响行数返回结果的变量名默认是result, true) 特殊场景:给true参数时代表运行异常时可继续执行
* 在此是SQL语句,多条用 ;\n 分号加回车符 隔开
* #end
* <p>
*/
public class RunSqlDirective extends SqlParaDirective {
private boolean isRuntimeException = false;
private void initIsRuntimeException() {
int k0 = 0;
if (exprArray == null || exprArray.length == k0) {
return;
}
if (exprArray[k0] instanceof Const) {
isRuntimeException = ((Const) exprArray[k0]).isTrue();
return;
}
int k1 = 1;
if (exprArray.length == k1) {
return;
}
if (exprArray[k1] instanceof Const) {
isRuntimeException = ((Const) exprArray[k1]).isTrue();
}
}
private static final Object[] NULL_PARA_ARRAY = new Object[0];
@Override
protected Object getData(SqlPara sqlPara) {
setDefaultName("result");
initIsRuntimeException();
String sql = sqlPara.getSql();
Object[] paras = sqlPara.getPara();
//单行SQL
if (!sql.contains(";")){
return update(sql, paras);
}
String[] sqlArr = sql.replaceAll(";\r\n", ";\n").split(";\n");
if (sqlArr.length == 1){
return update(sql, paras);
}
//多行SQL
List<Integer> ret = new ArrayList<>(sqlArr.length);
int from = 0;
int to;
Object[] para;
for (String s : sqlArr) {
if (StrKit.isBlank(s)) {
continue;
}
int count = count(s);
if (count == 0) {
para = NULL_PARA_ARRAY;
} else {
to = from + count;
para = Arrays.copyOfRange(paras, from, to);
from = to;
}
ret.add(update(s, para));
}
return ret;
}
private static int count(String s) {
int to = 0;
int length = s.length();
for (int i = 0; i < length; i++) {
if (s.charAt(i) == '?') {
to++;
}
}
return to;
}
private int update(String sql, Object[] para) {
if (StrKit.isBlank(sql)){
return 0;
}
try {
return db().update(sql, para);
}catch (ActiveRecordException e){
if (isRuntimeException){
write(sql);
write(" 执行失败:");
write(e.getMessage());
write(" 。跳过继续执行。\n");
}else {
throw e;
}
}
return 0;
}
}package com.momathink.common.plugin.activerecord.directive;
import com.jfinal.plugin.activerecord.DbPro;
import com.jfinal.plugin.activerecord.SqlPara;
import com.jfinal.template.Directive;
import com.jfinal.template.Env;
import com.jfinal.template.TemplateException;
import com.jfinal.template.expr.ast.Expr;
import com.jfinal.template.expr.ast.ExprList;
import com.jfinal.template.expr.ast.Id;
import com.jfinal.template.io.CharWriter;
import com.jfinal.template.io.FastStringWriter;
import com.jfinal.template.io.Writer;
import com.jfinal.template.stat.Scope;
import java.io.IOException;
public abstract class SqlParaDirective extends Directive {
protected Expr[] exprArray;
protected String name;
private Writer writer;
//SqlKit.SQL_PARA_KEY
protected static final String SQL_PARA_KEY = "_SQL_PARA_";
private static final ThreadLocal<DbPro> TL = new ThreadLocal<>();
@Override
public void setExprList(ExprList exprList) {
exprArray = exprList.getExprArray();
if (exprArray.length == 0) {
return;
}
if ((exprArray[0] instanceof Id)) {
this.name = ((Id) exprArray[0]).getId();
}
}
@Override
public void exec(Env env, Scope scope, Writer writer) {
this.writer = writer;
SqlPara sqlPara = new SqlPara();
//放入数据域中
scope.setLocal(SQL_PARA_KEY, sqlPara);
//渲染SQL
CharWriter charWriter = new CharWriter(64);
FastStringWriter fsw = new FastStringWriter();
charWriter.init(fsw);
try {
stat.exec(env, scope, charWriter);
} finally {
charWriter.close();
}
//移除sqlPara对象,避免污染Map
scope.removeLocal(SQL_PARA_KEY);
sqlPara.setSql(fsw.toString());
Object data = getData(sqlPara);
//放置返回结果
scope.set(name, data);
}
@Override
public boolean hasEnd() {
return true;
}
protected abstract Object getData(SqlPara sqlPara);
protected void setDefaultName(String name) {
if (this.name == null) {
this.name = name;
}
}
protected void write(String str) {
try {
writer.write(str, 0, str.length());
} catch (IOException e) {
throw new TemplateException(e.getMessage(), location, e);
}
}
public static DbPro db(){
return TL.get();
}
public static void setDb(DbPro db){
TL.set(db);
}
public static void remove(){
TL.remove();
}
}package com.momathink.common.plugin.activerecord.directive;
import com.jfinal.template.Directive;
import com.jfinal.template.Env;
import com.jfinal.template.expr.ast.Expr;
import com.jfinal.template.expr.ast.ExprList;
import com.jfinal.template.expr.ast.Id;
import com.jfinal.template.io.Writer;
import com.jfinal.template.stat.Scope;
/**
* #try(默认值exception可设置)
* 包裹代码
* #end
* #if(exception)
* #(exception.getMessage())
* #end
*/
public class TryDirective extends Directive {
protected String exceptionName = "exception";
protected String name;
@Override
public void setExprList(ExprList exprList) {
Expr[] exprArray = exprList.getExprArray();
if (exprArray.length == 0) {
return;
}
if ((exprArray[0] instanceof Id)) {
this.name = ((Id) exprArray[0]).getId();
}
}
@Override
public void exec(Env env, Scope scope, Writer writer) {
try {
stat.exec(env, scope, writer);
} catch (Exception e) {
setDefaultName(exceptionName);
scope.set(this.name, e);
}
}
@Override
public boolean hasEnd() {
return true;
}
protected void setDefaultName(String name) {
if (this.name == null) {
this.name = name;
}
}
}再附加一个 UpgradeSqlKit 工具类是我们SaaS系统在用的,这个与 UpgradeSqlPlugin 选一个用就可以了,大部分业务应该只有一两个数据源。
package com.momathink.common.plugin.activerecord;
import com.jfinal.kit.PathKit;
import com.jfinal.log.Log;
import com.jfinal.plugin.activerecord.ActiveRecordException;
import com.jfinal.plugin.activerecord.DbPro;
import com.jfinal.plugin.activerecord.Record;
import com.jfinal.plugin.activerecord.sql.SqlKit;
import com.jfinal.template.Engine;
import com.jfinal.template.Template;
import com.momathink.common.plugin.activerecord.directive.*;
import java.io.File;
import java.util.*;
/**
* 数据库升级工具<br/>
*
* 该工具与 UpgradeSqlPlugin 的区别:<br/>
*
* UpgradeSqlKit 适合大量数据源,重复利用创建的SQL模板。例:
* static UpgradeSqlKit usk = new UpgradeSqlKit();
* usk.run(db);
* 该DbPro对象可被高度定制,比如指向不同的数据源等业务<br/>
*
* UpgradeSqlPlugin 适合只有一两个数据源的时候,项目启动时当做插件配置后即可使用<br/>
*
* @author 杜福忠
*/
public class UpgradeSqlKit {
private static final Log log = Log.getLog(UpgradeSqlKit.class);
private Engine engine;
private String tableName = "upgrade_sql_config_log";
private String folder = "/upgrade_sql";
private final LinkedHashMap<String, Template> sqlTpl = new LinkedHashMap<>();
public UpgradeSqlKit() {
configEngine();
configSql();
}
public void run(DbPro db){
DbPro dbPro = SqlParaDirective.db();
SqlParaDirective.setDb(db);
try{
ArrayList<String> existing = existing(db);
for (Map.Entry<String, Template> en : sqlTpl.entrySet()) {
if (! existing.contains(en.getKey())){
run(db, en.getKey(), en.getValue());
}
}
}finally {
if (dbPro != null){
SqlParaDirective.setDb(dbPro);
}else {
SqlParaDirective.remove();
}
}
}
private void run(DbPro db, String sqlName, Template template) {
log.info("数据库执行升级文件:" + sqlName);
long t = System.currentTimeMillis();
final String[] ret = {"ok", null};
db.tx(() -> {
try{
ret[1] = template.renderToString();
return true;
}catch (Exception e){
ret[0] = "fail";
ret[1] = e.getMessage();
return false;
}
});
saveRecord(db, sqlName, ret[0], t, ret[1]);
}
protected void saveRecord(DbPro db, String sqlName, String status, long t, String logStr) {
if (logStr != null) {
logStr = logStr.trim();
}
Record r = new Record();
r.set("sqlName", sqlName);
r.set("status", status);
r.set("runDate", new Date());
r.set("duration", System.currentTimeMillis() - t);
r.set("log", logStr);
db.save(tableName, r);
}
private ArrayList<String> existing(DbPro db) {
ArrayList<String> existing;
try{
List<Record> list = db.find("SELECT sqlName FROM " + tableName + " WHERE state = 'ok'");
existing = new ArrayList<>(list.size());
for (Record r : list) {
existing.add(r.getStr("sqlName"));
}
}catch (ActiveRecordException e) {
//建表
initTable(db);
existing = new ArrayList<>(0);
}
return existing;
}
protected void initTable(DbPro db) {
db.update("CREATE TABLE `" + tableName + "` ( " +//
" `id` int(11) NOT NULL AUTO_INCREMENT, " +//
" `sqlName` varchar(255) NOT NULL, " +//
" `status` varchar(10) NULL, " +//
" `runDate` datetime NULL, " +//
" `duration` int(11) NULL, " +//
" `log` longtext NULL, " +//
" PRIMARY KEY (`id`) " +//
")");
}
private void configSql() {
File files = getFolderPath();
if (! files.isDirectory()){
return;
}
File[] listFiles = files.listFiles();
if (listFiles == null || listFiles.length == 0) {
return;
}
//名称排序
Arrays.sort(listFiles);
for (File file : listFiles) {
//名称统一小写,避免平台问题
String sqlName = file.getName().toLowerCase();
if (file.isFile() && sqlName.endsWith(".sql")){
sqlTpl.put(sqlName, engine.getTemplate(file.getName()));
}
}
}
private void configEngine() {
SqlKit sqlKit = new SqlKit("UpgradeSqlKit");
this.engine = sqlKit.getEngine();
engine.setBaseTemplatePath(folder);
engine.addDirective("try", TryDirective.class, true);
engine.addDirective("find", FindDirective.class, true);
engine.addDirective("findFirst", FindFirstDirective.class, true);
engine.addDirective("runSql", RunSqlDirective.class, true);
}
public void setTableName(String tableName) {
Objects.requireNonNull(tableName, "tableName 不能为null");
this.tableName = tableName;
}
public void setFolder(String folder) {
Objects.requireNonNull(folder, "folder 不能为null");
if (! folder.startsWith("/")) {
folder = "/" + folder;
}
this.folder = folder;
}
private File getFolderPath() {
return new File(PathKit.getRootClassPath() + folder);
}
}有社友反馈分享代码集成到项目有困难,我把该代码加入到了 官网的 JFinal demo for maven 5.0.0 项目中了,可以方便参考和复制下载:
https://gitee.com/bean80/jfinal-upgrade-sql-plugin
好了,分享完毕,有参考价值就点个赞呗~ 有特殊业务自行改造吧~