文件执行和生成"/>
基于JFinal的evens的SQL文件执行和生成
A.上传SQL文件执行。
1.上传文件
public void uploadSQL(){UploadFile upfile = getFile();File file = upfile.getFile();String uploadFilePath = UploadFileUtil.saveFile2(file);EventKit.post(new UploadSQLEvent(uploadFilePath));// 后面就是开始执行这个SQL文件Map<String, String> jsonMap = new HashMap<String, String>();jsonMap.put("status", "success");jsonMap.put("message", "上传成功,已经开始执行SQL文件,请查阅相关表");renderJson(jsonMap);}
2.发送事件,开始执行
@Listener(order = 8, enableAsync = true)
public class UploadSQLListener implements ApplicationListener<UploadSQLEvent>{private static final Logger logger = LoggerFactory.getLogger(UploadSQLListener.class);@Overridepublic void onApplicationEvent(UploadSQLEvent event){String fileName = (String)event.getSource();logger.info("--------fileName=" + fileName);Db.execute(new ICallback(){@Overridepublic Object call(Connection conn) throws SQLException{SQLHelper.executeSQLFile(conn, fileName);return null;}});}}
B.下载SQL文件
1.向数据库保存一条下载记录,并发送下载事件
public void dumpSQL(){final String tableName = getPara("tableName");String path = JFinal.me().getServletContext().getRealPath("/");if(!path.endsWith(File.separator)){path += File.separator;}String fileName = tableName + "-(" + DateKit.strFormat.format(new Date()) + ").sql";EventKit.post(new DumpSQLEvent(new DumpSQLBean(tableName, path + fileName)));Record record = new Record();record.set(COLUMN_TASK_ID, Utils.createRandom());record.set(COLUMN_PATH, path);record.set(COLUMN_FILE_NAME, fileName);record.set(COLUMN_FILE_TIME, new Date());Db.save(DUMP_TABLE, record);ClientJsonObject cjo = new ClientJsonObject();cjo.resultCode = ClientApiConstant.ResultCode.SUCCESS_CODE;cjo.message = ClientApiConstant.Msg.SUCCESS;cjo.obj = tableName + " 表的导出任务已经开始,请下载 " + fileName;renderJson(JSON.toJSONString(cjo, true));}
2.开始下载
@Listener(order = 7, enableAsync = true)
public class DumpSQLListener implements ApplicationListener<DumpSQLEvent>{private static final Logger logger = LoggerFactory.getLogger(DumpSQLListener.class);@Overridepublic void onApplicationEvent(DumpSQLEvent event){DumpSQLBean bean = (DumpSQLBean)event.getSource();logger.info("tableName=" + bean.tableName + "--------fileName=" + bean.fileName);Db.execute(new ICallback(){@Overridepublic Object call(Connection conn) throws SQLException{SQLHelper.createDumpSQLFile(conn, bean.tableName, bean.fileName);return null;}});}}
3.等待下载完成,实际下载SQL文件,首先刷新下载列表
public void sqlRefresh(){ClientJsonObject cjo = new ClientJsonObject();List<Record> records = Db.find("select * from " + DUMP_TABLE);List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();for(Record record : records){Date date = record.getDate(COLUMN_FILE_TIME);record.set(COLUMN_FILE_TIME, DateKit.sdfLong.format(date));record.remove(COLUMN_PATH);list.add(record.getColumns());}cjo.resultCode = ClientApiConstant.ResultCode.SUCCESS_CODE;cjo.message = ClientApiConstant.Msg.SUCCESS;cjo.obj = list;renderJson(JSON.toJSONString(cjo, true));}
4.下载并删除下载记录
public void download(){int id = getParaToInt("id");Record record = Db.findById(DUMP_TABLE, id);renderFile(new File(record.getStr(COLUMN_PATH) + record.getStr(COLUMN_FILE_NAME)));}public void sqlClean(){ClientJsonObject cjo = new ClientJsonObject();// 1.删除磁盘上的临时文件File file = null;List<Record> records = Db.find("select * from " + DUMP_TABLE);for(Record record : records){String path = record.getStr(COLUMN_PATH);String fileName = record.getStr(COLUMN_FILE_NAME);file = new File(path + fileName);if(null != file && file.exists()){file.delete();}}// 2.删除数据库记录Db.update("delete from " + DUMP_TABLE);cjo.resultCode = ClientApiConstant.ResultCode.SUCCESS_CODE;cjo.message = ClientApiConstant.Msg.SUCCESS;cjo.obj = "删除成功";renderJson(JSON.toJSONString(cjo, true));}
C.常量定义和表结构
1.常量定义
private static final String DUMP_TABLE = "dumpsql";private static final String COLUMN_ID = "id";private static final String COLUMN_PATH = "path";private static final String COLUMN_FILE_NAME = "filename";private static final String COLUMN_FILE_TIME = "time";private static final String COLUMN_TASK_ID = "taskid";
2.表结构
CREATE TABLE `dumpsql` (`id` int(11) NOT NULL AUTO_INCREMENT,`taskid` varchar(100) NOT NULL COMMENT '任务Id,导出任务',`path` varchar(100) NOT NULL COMMENT '绝对路径',`filename` varchar(100) NOT NULL COMMENT '文件名',`time` datetime DEFAULT NULL COMMENT '时间',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
D.核心SQLHelper用于生成和执行SQL文件
package cn.esstx.cq.server.util;import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStream;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Date;import com.jfinal.plugin.activerecord.Db;
import com.jfinal.plugin.activerecord.Record;import cn.esstx.cq.server.util.scriptrunner.ScriptRunner;public class SQLHelper{/*** * @Title: createDumpSQLFile* @Description:根据表名生成创建表的结构和数据的文件* @param conn,数据库连接* @param tableName,表名* @param fileName,绝对路径*/public static void createDumpSQLFile(Connection conn, String tableName, String fileName){try{OutputStream os = new FileOutputStream(fileName);/* 注释信息 */createComment(conn, tableName, os);/* 创建表的SQL */createCreateSQL(conn, tableName, os);/* 生成数据的SQL */createInsertSQL(conn, tableName, os);os.close();}catch(Exception e){e.printStackTrace();}}/*** @Title: createComment* @Description:创建注释信息* @param conn* @param tableName* @param os*/private static void createComment(Connection conn, String tableName, OutputStream os){try{String catalog = conn.getCatalog();DatabaseMetaData metaData = conn.getMetaData();StringBuffer buffer = new StringBuffer();buffer.append("/*\nYYH ").append(metaData.getDatabaseProductName()).append(" Data Transfer\n").append("Target Server Type : ").append(metaData.getDatabaseProductName()).append("\n\nSource Server Version : ").append(metaData.getDatabaseProductVersion()).append("\nSource Database : ").append(catalog).append("\n\nDate: ").append(DateKit.sdfLong.format(new Date())).append("\n*/\n\n");os.write(buffer.toString().getBytes());os.flush();}catch(Exception e){e.printStackTrace();}}/*** @Title: createCreateSQL* @Description:创建创建表的SQL语句* @param conn* @param tableName* @param os* @throws IOException*/public static void createCreateSQL(Connection conn, String tableName, OutputStream os) throws IOException{os.write("SET FOREIGN_KEY_CHECKS=0;\n\n".getBytes());os.write(("DROP TABLE IF EXISTS `" + tableName + "`;\n").getBytes());os.write(("-- ----------------------------\n-- Table structure for `" + tableName+ "`\n-- ----------------------------\n").getBytes());try{Record record = Db.findFirst("show create table " + tableName);os.write(record.getStr("Create Table").getBytes());}catch(Exception e){e.printStackTrace();}os.write(";\n\n".getBytes());os.flush();}/*** @Title: createInsertSQL* @Description:创建Insert SQL语句* @param conn* @param tableName* @param os* @throws SQLException* @throws IOException*/public static void createInsertSQL(Connection conn, String tableName, OutputStream os)throws SQLException, IOException{os.write(("-- ----------------------------\n-- Records of " + tableName + "\n-- ----------------------------\n").getBytes());int pageSize = 50;long count = 0;ResultSet set = query(conn, "select count(*) from " + tableName);if(set.next()){count = set.getLong(1);}// System.out.println("count=" + count);int totalPage = (int)(count / pageSize);if(count % pageSize != 0){totalPage++;}// System.out.println("totalPage=" + totalPage);for(int i = 0; i < totalPage; i++){handleOnePage(conn, tableName, os, i + 1, pageSize);}}/*** @Title: handleOnePage* @Description:处理一页* @param conn* @param tableName* @param os* @param pageNumber* @param pageSize* @throws SQLException* @throws IOException*/public static void handleOnePage(Connection conn, String tableName, OutputStream os, int pageNumber, int pageSize)throws SQLException, IOException{int offset = pageSize * (pageNumber - 1);ResultSet set = query(conn, "select * from " + tableName + " limit " + offset + ", " + pageSize);ResultSetMetaData metaData = set.getMetaData();int count = metaData.getColumnCount();// List<String> labels = getLabels(metaData);StringBuffer insertSQL = null;String insert = "INSERT INTO `" + tableName + "` VALUES (";while(set.next()){insertSQL = new StringBuffer(insert);for(int i = 0; i < count; i++){Object obj = set.getObject(i + 1);int type = metaData.getColumnType(i + 1);appendAColumn(insertSQL, obj, type);}String sql = insertSQL.substring(0, insertSQL.length() - 2);// 去掉最后的逗号和空格sql = sql + ");\n";// System.out.println(sql);//// 这个语句可以保存到文件中,在你的数据库中运行这个文件即可。os.write(sql.getBytes());os.flush();insertSQL.setLength(0);}set.close();}/*** @Title: appendAColumn* @Description:根据类型增加一列到StringBuffer* @param insertSQL* @param obj* @param type*/public static void appendAColumn(StringBuffer insertSQL, Object obj, int type){if(null == obj){insertSQL.append("null").append(", ");} else{if(isChar(type)){// 根据列的类型看是否需要添加''String objString = String.valueOf(obj);insertSQL.append("'" + handleQuote(objString) + "'").append(", ");// 还要进一步处理有\n,""的情况} else{insertSQL.append(obj).append(", ");}}}/*** @Title: handleQuote* @Description:处理字符串中的"" 、\n* @param string* @return String*/private static String handleQuote(String src){String dest = src.replaceAll("\n", "\\\\n");// 把换行变成\ndest = dest.replaceAll("\"", "\\\\\"");// 把"换成\"return dest;}private static boolean isChar(int type){switch(type){case Types.CHAR:case Types.DATE:case Types.LONGNVARCHAR:case Types.LONGVARCHAR:case Types.NCHAR:case Types.NVARCHAR:case Types.SQLXML:case Types.VARCHAR:case Types.TIMESTAMP:case Types.TIME:return true;default:return false;}}public static ResultSet query(Connection connection, String sql, Object... args){ResultSet rs = null;try{PreparedStatement ps = connection.prepareStatement(sql);for(int i = 0; i < args.length; i++)ps.setObject(i + 1, args[i]);rs = ps.executeQuery();}catch(Exception e){e.printStackTrace();}return rs;}/*** @Title: executeSQLFile* @Description:执行上传的SQL文件* @param conn* @param fileName*/public static void executeSQLFile(Connection conn, String fileName){try{ScriptRunner runner = new ScriptRunner(conn, true, true);// runner.setErrorLogWriter(new PrintWriter(System.out));// 输出到标准流里runner.setLogWriter(null);// 不输出日志runner.runScript(new FileReader(fileName));conn.close();}catch(Exception e){e.printStackTrace();}}
}
E.执行sql语句的scriptrunner
package cn.esstx.cq.server.util.scriptrunner;import java.io.IOException;
import java.io.LineNumberReader;
import java.io.PrintWriter;
import java.io.Reader;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;/*** Tool to run database scripts*/
public class ScriptRunner{// private static final Log log = LogFactory.getLog(ScriptRunner.class);private Connection connection;private String driver;private String url;private String username;private String password;private boolean stopOnError;private boolean autoCommit;private PrintWriter logWriter = new PrintWriter(System.out);private PrintWriter errorLogWriter = new PrintWriter(System.err);/*** Default constructor*/public ScriptRunner(Connection connection, boolean autoCommit, boolean stopOnError){this.connection = connection;this.autoCommit = autoCommit;this.stopOnError = stopOnError;}public ScriptRunner(String driver, String url, String username, String password, boolean autoCommit,boolean stopOnError){this.driver = driver;this.url = url;this.username = username;this.password = password;this.autoCommit = autoCommit;this.stopOnError = stopOnError;}/*** Setter for logWriter property** @param logWriter* - the new value of the logWriter property*/public void setLogWriter(PrintWriter logWriter){this.logWriter = logWriter;}/*** Setter for errorLogWriter property** @param errorLogWriter* - the new value of the errorLogWriter property*/public void setErrorLogWriter(PrintWriter errorLogWriter){this.errorLogWriter = errorLogWriter;}/*** Runs an SQL script (read in using the Reader parameter)** @param reader* - the source of the script*/public void runScript(Reader reader) throws IOException, SQLException{try{if(connection == null){DriverManager.registerDriver((Driver)Resources.classForName(driver).newInstance());Connection conn = DriverManager.getConnection(url, username, password);try{if(conn.getAutoCommit() != autoCommit){conn.setAutoCommit(autoCommit);}runScript(conn, reader);}finally{conn.close();}} else{boolean originalAutoCommit = connection.getAutoCommit();try{if(originalAutoCommit != this.autoCommit){connection.setAutoCommit(this.autoCommit);}runScript(connection, reader);}finally{connection.setAutoCommit(originalAutoCommit);}}}catch(IOException e){throw e;}catch(SQLException e){throw e;}catch(Exception e){throw new NestedRuntimeException("Error running script. Cause: " + e, e);}}/*** Runs an SQL script (read in using the Reader parameter) using the* connection passed in** @param conn* - the connection to use for the script* @param reader* - the source of the script* @throws SQLException* if any SQL errors occur* @throws IOException* if there is an error reading from the Reader*/private void runScript(Connection conn, Reader reader) throws IOException, SQLException{StringBuffer command = null;try{LineNumberReader lineReader = new LineNumberReader(reader);String line = null;while((line = lineReader.readLine()) != null){if(command == null){command = new StringBuffer();}String trimmedLine = line.trim();if(trimmedLine.startsWith("--")){println(trimmedLine);} else if(trimmedLine.length() < 1 || trimmedLine.startsWith("//")){// Do nothing} else if(trimmedLine.length() < 1 || trimmedLine.startsWith("--")){// Do nothing} else if(trimmedLine.endsWith(";")){command.append(line.substring(0, line.lastIndexOf(";")));command.append(" ");Statement statement = conn.createStatement();println(command);boolean hasResults = false;if(stopOnError){hasResults = statement.execute(command.toString());} else{try{statement.execute(command.toString());}catch(SQLException e){e.fillInStackTrace();printlnError("Error executing: " + command);printlnError(e);}}if(autoCommit && !conn.getAutoCommit()){connmit();}ResultSet rs = statement.getResultSet();if(hasResults && rs != null){ResultSetMetaData md = rs.getMetaData();int cols = md.getColumnCount();for(int i = 0; i < cols; i++){String name = md.getColumnName(i);print(name + "\t");}println("");while(rs.next()){for(int i = 0; i < cols; i++){String value = rs.getString(i);print(value + "\t");}println("");}}command = null;try{statement.close();}catch(Exception e){// Ignore to workaround a bug in Jakarta DBCP}Thread.yield();} else{command.append(line);command.append(" ");}}if(!autoCommit){connmit();}}catch(SQLException e){e.fillInStackTrace();printlnError("Error executing: " + command);printlnError(e);throw e;}catch(IOException e){e.fillInStackTrace();printlnError("Error executing: " + command);printlnError(e);throw e;}finally{conn.rollback();flush();}}private void print(Object o){if(logWriter != null){System.out.print(o);}}private void println(Object o){if(logWriter != null){logWriter.println(o);}}private void printlnError(Object o){if(errorLogWriter != null){errorLogWriter.println(o);}}private void flush(){if(logWriter != null){logWriter.flush();}if(errorLogWriter != null){errorLogWriter.flush();}}}
package cn.esstx.cq.server.util.scriptrunner;import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.URL;
import java.URLConnection;
import java.util.Properties;/*** A class to simplify access to resources through the classloader.*/
public class Resources extends Object{private static ClassLoader defaultClassLoader;private Resources(){}/*** Returns the default classloader (may be null).** @return The default classloader*/public static ClassLoader getDefaultClassLoader(){return defaultClassLoader;}/*** Sets the default classloader** @param defaultClassLoader* - the new default ClassLoader*/public static void setDefaultClassLoader(ClassLoader defaultClassLoader){Resources.defaultClassLoader = defaultClassLoader;}/*** Returns the URL of the resource on the classpath** @param resource* The resource to find* @return The resource* @throws IOException* If the resource cannot be found or read*/public static URL getResourceURL(String resource) throws IOException{return getResourceURL(getClassLoader(), resource);}/*** Returns the URL of the resource on the classpath** @param loader* The classloader used to load the resource* @param resource* The resource to find* @return The resource* @throws IOException* If the resource cannot be found or read*/public static URL getResourceURL(ClassLoader loader, String resource) throws IOException{URL url = null;if(loader != null)url = loader.getResource(resource);if(url == null)url = ClassLoader.getSystemResource(resource);if(url == null)throw new IOException("Could not find resource " + resource);return url;}/*** Returns a resource on the classpath as a Stream object** @param resource* The resource to find* @return The resource* @throws IOException* If the resource cannot be found or read*/public static InputStream getResourceAsStream(String resource) throws IOException{return getResourceAsStream(getClassLoader(), resource);}/*** Returns a resource on the classpath as a Stream object** @param loader* The classloader used to load the resource* @param resource* The resource to find* @return The resource* @throws IOException* If the resource cannot be found or read*/public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException{InputStream in = null;if(loader != null)in = loader.getResourceAsStream(resource);if(in == null)in = ClassLoader.getSystemResourceAsStream(resource);if(in == null)throw new IOException("Could not find resource " + resource);return in;}/*** Returns a resource on the classpath as a Properties object** @param resource* The resource to find* @return The resource* @throws IOException* If the resource cannot be found or read*/public static Properties getResourceAsProperties(String resource) throws IOException{Properties props = new Properties();InputStream in = null;String propfile = resource;in = getResourceAsStream(propfile);props.load(in);in.close();return props;}/*** Returns a resource on the classpath as a Properties object** @param loader* The classloader used to load the resource* @param resource* The resource to find* @return The resource* @throws IOException* If the resource cannot be found or read*/public static Properties getResourceAsProperties(ClassLoader loader, String resource) throws IOException{Properties props = new Properties();InputStream in = null;String propfile = resource;in = getResourceAsStream(loader, propfile);props.load(in);in.close();return props;}/*** Returns a resource on the classpath as a Reader object** @param resource* The resource to find* @return The resource* @throws IOException* If the resource cannot be found or read*/public static Reader getResourceAsReader(String resource) throws IOException{return new InputStreamReader(getResourceAsStream(resource));}/*** Returns a resource on the classpath as a Reader object** @param loader* The classloader used to load the resource* @param resource* The resource to find* @return The resource* @throws IOException* If the resource cannot be found or read*/public static Reader getResourceAsReader(ClassLoader loader, String resource) throws IOException{return new InputStreamReader(getResourceAsStream(loader, resource));}/*** Returns a resource on the classpath as a File object** @param resource* The resource to find* @return The resource* @throws IOException* If the resource cannot be found or read*/public static File getResourceAsFile(String resource) throws IOException{return new File(getResourceURL(resource).getFile());}/*** Returns a resource on the classpath as a File object** @param loader* - the classloader used to load the resource* @param resource* - the resource to find* @return The resource* @throws IOException* If the resource cannot be found or read*/public static File getResourceAsFile(ClassLoader loader, String resource) throws IOException{return new File(getResourceURL(loader, resource).getFile());}/*** Gets a URL as an input stream** @param urlString* - the URL to get* @return An input stream with the data from the URL* @throws IOException* If the resource cannot be found or read*/public static InputStream getUrlAsStream(String urlString) throws IOException{URL url = new URL(urlString);URLConnection conn = url.openConnection();return conn.getInputStream();}/*** Gets a URL as a Reader** @param urlString* - the URL to get* @return A Reader with the data from the URL* @throws IOException* If the resource cannot be found or read*/public static Reader getUrlAsReader(String urlString) throws IOException{return new InputStreamReader(getUrlAsStream(urlString));}/*** Gets a URL as a Properties object** @param urlString* - the URL to get* @return A Properties object with the data from the URL* @throws IOException* If the resource cannot be found or read*/public static Properties getUrlAsProperties(String urlString) throws IOException{Properties props = new Properties();InputStream in = null;String propfile = urlString;in = getUrlAsStream(propfile);props.load(in);in.close();return props;}/*** Loads a class** @param className* - the class to load* @return The loaded class* @throws ClassNotFoundException* If the class cannot be found (duh!)*/public static Class classForName(String className) throws ClassNotFoundException{Class clazz = null;try{clazz = getClassLoader().loadClass(className);}catch(Exception e){// Ignore. Failsafe below.}if(clazz == null){clazz = Class.forName(className);}return clazz;}/*** Creates an instance of a class** @param className* - the class to create* @return An instance of the class* @throws ClassNotFoundException* If the class cannot be found (duh!)* @throws InstantiationException* If the class cannot be instantiaed* @throws IllegalAccessException* If the class is not public, or other access problems arise*/public static Object instantiate(String className)throws ClassNotFoundException, InstantiationException, IllegalAccessException{return instantiate(classForName(className));}/*** Creates an instance of a class** @param clazz* - the class to create* @return An instance of the class* @throws InstantiationException* If the class cannot be instantiaed* @throws IllegalAccessException* If the class is not public, or other access problems arise*/public static Object instantiate(Class clazz) throws InstantiationException, IllegalAccessException{return clazz.newInstance();}private static ClassLoader getClassLoader(){if(defaultClassLoader != null){return defaultClassLoader;} else{return Thread.currentThread().getContextClassLoader();}}}
package cn.esstx.cq.server.util.scriptrunner;/*** Nexted exception implementation. Thanks Claus.*/public class NestedRuntimeException extends RuntimeException{// @Fields serialVersionUID :private static final long serialVersionUID = 1L;private static final String CAUSED_BY = "\nCaused by: ";private Throwable cause = null;/*** Constructor*/public NestedRuntimeException(){}/*** Constructor** @param msg* error message*/public NestedRuntimeException(String msg){super(msg);}/*** Constructor** @param cause* the nested exception (caused by)*/public NestedRuntimeException(Throwable cause){super();this.cause = cause;}/*** Constructor** @param msg* error message* @param cause* the nested exception (caused by)*/public NestedRuntimeException(String msg, Throwable cause){super(msg);this.cause = cause;}/*** Gets the causing exception, if any.** @return The cause of the exception*/public Throwable getCause(){return cause;}/*** Converts the exception to a string representation** @return The string representation of the exception*/public String toString(){if(cause == null){return super.toString();} else{return super.toString() + CAUSED_BY + cause.toString();}}/*** Sends a stack trace to System.err (including the root cause, if any)*/public void printStackTrace(){super.printStackTrace();if(cause != null){System.err.println(CAUSED_BY);cause.printStackTrace();}}/*** Sends a stack trace to the PrintStream passed in (including the root* cause, if any)** @param ps* - the PrintStream to send the output to*/public void printStackTrace(java.io.PrintStream ps){super.printStackTrace(ps);if(cause != null){ps.println(CAUSED_BY);cause.printStackTrace(ps);}}/*** Sends a stack trace to the PrintWriter passed in (including the root* cause, if any)** @param pw* - the PrintWriter to send the output to*/public void printStackTrace(java.io.PrintWriter pw){super.printStackTrace(pw);if(cause != null){pw.println(CAUSED_BY);cause.printStackTrace(pw);}}}
鸣谢:
scriptrunner来自于mybatis
connection来自JFinal
events结构来自JFinal-events
生成SQL语句的借鉴了JFinal的分页做法
更多推荐
基于JFinal的evens的SQL文件执行和生成
发布评论