在開(kāi)發(fā)JavaWeb應(yīng)用程序的過(guò)程中可能經(jīng)常需要使用到SQL語(yǔ)句來(lái)訪問(wèn)數(shù)據(jù)庫(kù)。為了屏蔽SQL注入帶來(lái)的危險(xiǎn),在Java中通常使用PreparedStatement,使用預(yù)編譯的SQL語(yǔ)句。預(yù)編譯的SQL語(yǔ)句是那些包含?的語(yǔ)句,使用PreparedStatement可以讓數(shù)據(jù)庫(kù)預(yù)先編譯這些SQL模板,只有調(diào)用的時(shí)候套用必須的參數(shù)即可。
SQL文件的存放位置
那么在JavaWeb項(xiàng)目中預(yù)編譯的SQL語(yǔ)句到底放在那里呢?
放在Java代碼里肯定是不好的,為什么,有兩點(diǎn),第一,SQL語(yǔ)句放在Java代碼里
太難看可,有不好的味道(參看Refactor),第二,每次SQL語(yǔ)句變更(可能經(jīng)常發(fā)生)
都需要編譯這些Java代碼,比較煩。
那么SQL語(yǔ)句到底放在那里呢?根據(jù)這么多年的開(kāi)發(fā)經(jīng)驗(yàn)SQL文通常可以放在classes目錄
下的文件中,存放SQL語(yǔ)句的文件有三種類(lèi)型:properties文件,xml文件和txt文件。
在詳細(xì)討論文件格式之前,我們先討論以下如何在Java類(lèi)中得到這些文件的引用,以便從中得到需要的SQL語(yǔ)句。
使用Class的getResourceAsStream方法可以獲得對(duì)文件引用的InputStream。例如:文件目錄結(jié)構(gòu):
src
com
jpleasure
dao
SomeDao.java
SomeDao.properties
SomeDao.xml
SomeDao.sql
// properties
public class SomeDao{
public InputStream getInputStream() {
this.getClass().getClassLoader().
getResourceAsStream("com/jpleasure/dao/SomeDao.properties");
}
}
// xml
public class SomeDao{
public InputStream getInputStream() {
this.getClass().getClassLoader().
getResourceAsStream("com/jpleasure/dao/SomeDao.properties");
}
}
// txt(.sql)
public class SomeDao{
public InputStream getInputStream() {
this.getClass().getClassLoader().
getResourceAsStream("com/jpleasure/dao/SomeDao.properties");
}
}
Propertis文件
Properties 文件是Java支持的標(biāo)準(zhǔn)的屬性文件(相當(dāng)于Windows對(duì)ini文件的支持)。
Properties 文件的格式為:
# 注釋
# 定義key,并且與key對(duì)應(yīng)的值為value
key = value
可以使用java.util.Properties類(lèi)來(lái)包裝properties文件,使用如下
Properties props = new Properties();
try {
props.load(this.getInputStream());
} catch(IOException ioex) {
// 文件不存在,或者格式問(wèn)題等
}
String value = props.getString("key")
注意上述格式中value不能換行,要想換行必須使用轉(zhuǎn)義字符“\”。
在存儲(chǔ)SQL的時(shí)候我們使用如下的格式,例如:
# 某業(yè)務(wù),某操作SQL
xxx_0001 = select * from dual
# 某業(yè)務(wù),其他操作SQL
xxx_0002 = \
select \
* \
from \
dual
使用Properties文件的優(yōu)點(diǎn)是:Java內(nèi)置支持布需要手寫(xiě)文件解析代碼,另外使用也非常簡(jiǎn)單。
缺點(diǎn)是:SQL語(yǔ)句不能直接編寫(xiě),需要追加轉(zhuǎn)義字符"\",無(wú)法將這樣的SQL語(yǔ)句直接拷貝到數(shù)據(jù)庫(kù)客戶(hù)端中運(yùn)行。
XML文件
使用XML文件保存SQL比較常用的格式為:
<sqls>
<sql id=”xxx_0001”>
Select
*
from
dual <!-- 文件注釋 -->
</sql>
<sql id=”xxx_0001”>
…
</sql>
</sqls>
使用XML格式的文件保存SQL需要自己寫(xiě)文件解析代碼,由于Java對(duì)XML提供了內(nèi)置的支持,并且第三方的開(kāi)源庫(kù)也很多,并且非常容易使用所以從xml文件中解析SQL語(yǔ)句也沒(méi)有什么困難。以下以jdom為例講解如何解析上述的XML格式的SQL文件。
protected Map analysis() {
Map map = new HashMap();
DOMBuilder builder = new DOMBuilder();
Document doc = null;
try {
// 解析XML文件
doc = builder.build(this.getInputStream());
// 獲得根節(jié)點(diǎn): <sqls>
Element element = doc.getRootElement();
// 獲得所有根節(jié)點(diǎn)的子節(jié)點(diǎn): <sql>節(jié)點(diǎn)列表
List sqlNodeList = element.getChildren("sql");
for (int i = 0; i < sqlNodeList.size(); i++) {
Element sqlNode = (Element) sqlNodeList.get(i);
// 獲得SQL語(yǔ)句ID
String id = sqlNode.getAttribute("id").getValue();
// 獲得SQL語(yǔ)句內(nèi)容
String sql = sqlNode.getTextTrim();
map.put(id, sql);
}
} catch (JDOMException e) {
e.printStackTrace();
}
return map;
}
使用XML文件格式保存SQL語(yǔ)句據(jù)的時(shí)候需要注意,SQL語(yǔ)句中的大于(>)小于(<)和XML文件的格式?jīng)_突,有兩種解決方法,
第一:使用全角的大于(>)小于(<)號(hào)。
第二:使用<![CDATA[ ]]>來(lái)包圍所有的內(nèi)容。
另外,通常情況下(不使用<![CDATA[ ]]>的時(shí)候)SQL的注釋只能使用XML的注釋格式(<!-- -->)。
使用XML格式的文件的有點(diǎn):SQL語(yǔ)句可以正常書(shū)寫(xiě),文件解析相對(duì)簡(jiǎn)單。
缺點(diǎn):大于號(hào),小于號(hào)的沖突;無(wú)法添加SQL注釋?zhuān)ㄍǔV荒苁褂?/span>XML格式的注釋?zhuān)?/span>
TXT文件(以.SQL為后綴)
我們先說(shuō)一下TXT文件(.sql文件)的格式
------------------------------------
--@ SQL-1
------------------------------------
SELECT
SYSDATE
FROM
DUAL(SQL)
------------------------------------
--@ SQL-2
-- 某某用途的SQL文
------------------------------------
SELECT
SYSDATE, ROWID -- 某某字段
FROM
DUAL(SQL) -- 某某表
使用txt格式的文件,非常的簡(jiǎn)單,和一般的寫(xiě)SQL語(yǔ)句一樣,可以使用任何的SQL標(biāo)準(zhǔn)語(yǔ)法。只是有一個(gè)地方需要注意,就是每個(gè)SQL語(yǔ)句的頭注釋的地方加上一行特殊的內(nèi)容用來(lái)標(biāo)記SQL語(yǔ)句的ID:
--@ SQL-1
這樣的文件解析比較困難,但是也不是不能做,解析代碼如下:
protected Map analysis() {
Map sqlMap = new HashMap();
String id = null;
StringBuffer sql = new StringBuffer();
if (this.getInputStream() != null) {
BufferedReader sqlFileReader = null;
try {
sqlFileReader = new BufferedReader(
new InputStreamReader(this.getInputStream()));
String currentLine = null;
// 逐行讀取文件內(nèi)容
while ((currentLine = sqlFileReader.readLine()) != null) {
// 發(fā)現(xiàn)新的SQL語(yǔ)句,將已經(jīng)發(fā)現(xiàn)的SQL語(yǔ)句放在SQL容器中
if (currentLine.startsWith("--@")) {
if (id != null) {
sqlMap.put(id, sql.toString());
id = null;
sql = new StringBuffer();
}
id = currentLine.substring("--@".length()).trim();
} else if (currentLine.startsWith("--")) {
// 不讀取注視行
continue;
} else {
// 非注釋?zhuān)?/span>ID行
// 去掉SQL語(yǔ)句行的末尾注視
if (currentLine.length() > 0) {
int commentsIndex = currentLine.indexOf("--");
if (commentsIndex > 0) {
currentLine = currentLine.substring(0,
commentsIndex);
}
// 將換行符替換為空格
sql.append(currentLine + " ");
} else {
// 將換行符替換為空格
sql.append(" ");
}
}
currentLine = null;
}
if (id != null) {
sqlMap.put(id, sql.toString());
}
} catch (IOException ioex) {
ioex.printStackTrace();
} finally {
if(sqlFileReader != null) {
try {
sqlFileReader.close();
} catch (IOException e) {
}
}
}
}
return sqlMap;
}
使用TXT格式的文件的有點(diǎn):SQL語(yǔ)句書(shū)寫(xiě)非常方便,可以拷貝出來(lái)直接運(yùn)行,可以使用標(biāo)準(zhǔn)的SQL注釋格式。
缺點(diǎn):每個(gè)SQL語(yǔ)句必須添加特殊的ID標(biāo)志(相對(duì)與Properties和xml來(lái)說(shuō)可能也不算是缺點(diǎn)),解析困難,需要較強(qiáng)的文本接卸的能力。
通用SQL文件讀取庫(kù)
如何設(shè)計(jì)一個(gè)同時(shí)支持三種文件格式的SQL文件讀取庫(kù)?

首先我們抽象出一個(gè)SqlManager的接口,它提供通過(guò)Key獲得對(duì)應(yīng)SQL語(yǔ)句的操作。代碼為:
public interface SqlManager {
/**
* 獲取指定Key的SQL語(yǔ)句
*
* @param key
* SQL語(yǔ)句的Key值
* @return SQL語(yǔ)句內(nèi)容
* @throws SqlMgntException
* 無(wú)法取得SQL語(yǔ)句時(shí)拋出此異常。
*/
public String getSql(String key) throws SqlMgntException;
}
然后使用一個(gè)抽象基類(lèi),來(lái)定義通用的屬性和操作,代碼為:
public abstract class AbstractSqlManager implements SqlManager {
/* SQL文件輸入流 */
private final InputStream is;
/**
* SQL文件輸入流獲取方法。
*
* @return SQL文件輸入流
*/
public InputStream getInputStream() {
return is;
}
/**
* 默認(rèn)構(gòu)造方法
*
* @param is
* SQL文件輸入流
*/
public AbstractSqlManager(InputStream is) {
this.is = is;
}
/* SQL語(yǔ)句容器 */
private Map sqlContainer = null;
/* 同步KEY */
private final Object syncKey = new Object();
/**
* 通過(guò)SQL語(yǔ)句ID獲取SQL語(yǔ)句內(nèi)容。
*
* @param key
* SQL語(yǔ)句ID
* @return SQL語(yǔ)句
*/
public String getSql(final String key) throws SqlMgntException {
synchronized (syncKey) {
if (sqlContainer == null) {
this.sqlContainer = initContainer();
}
}
return (String) sqlContainer.get(key);
}
/**
* 抽象的模板方法,用來(lái)解析不同類(lèi)型的SQL文件。
* @return 配對(duì)的SQL內(nèi)容
*/
protected abstract Map initContainer();
}
注意其中的抽象方法:
protected abstract Map initContainer();
通過(guò)這個(gè)抽象方法把SQL文件的解析交給了具體的實(shí)例(模式參看:模板方法)
三個(gè)具體的SqlManager類(lèi)我們不必具體說(shuō)了,都是對(duì)上述抽線的解析方法的實(shí)現(xiàn)。
工廠類(lèi)可以根據(jù)對(duì)應(yīng)文件的后綴明創(chuàng)建對(duì)應(yīng)的SqlManager。代碼為:
public class SqlManagerFactory {
/* XMl文件類(lèi)型 */
public static final String TYPE_XML = ".xml";
/* SQL文件類(lèi)型 */
public static final String TYPE_SQL = ".sql";
/* 屬性文件類(lèi)型 */
public static final String TYPE_PROP = ".properties";
// begin ma.zhao@dl.cn 2006/03/09
// add singleton map for SqlManager
private static Map sqlManagerContainer = new HashMap();
// end ma.zhao@dl.cn 2006/03/09
/**
* 創(chuàng)建對(duì)應(yīng)的SqlManager實(shí)現(xiàn)類(lèi)
*
* @param filePath
* SQL文件相對(duì)路徑和文件名
* @return
*/
public static SqlManager createSqlManager(String filePath) throws SqlMgntException {
SqlManager manager = null;
synchronized (sqlManagerContainer) {
manager = (SqlManager) sqlManagerContainer.get(filePath);
// 如果SqlManager不存在,則根據(jù)文件初始化SqlManager,
// 并且將它放在SqlManager容器中
if (manager == null) {
InputStream is = SqlManagerFactory.class.getClassLoader()
.getResourceAsStream(filePath);
if(is == null) {
throw new SqlMgntException(
"Sql File Not Fount! Input file name path is:" + filePath);
}
if (filePath.endsWith(TYPE_XML)
|| filePath.endsWith(TYPE_XML.toUpperCase())) {
manager = new SqlManagerXmlImpl(is);
} else if (filePath.endsWith(TYPE_SQL)
|| filePath.endsWith(TYPE_SQL.toUpperCase())) {
manager = new SqlManagerSqlImpl(is);
} else if (filePath.endsWith(TYPE_PROP)
|| filePath.endsWith(TYPE_PROP.toUpperCase())) {
manager = new SqlManagerPropImpl(is);
} else {
throw new SqlMgntException(
"Sql File Type Not Support, Input file path is:" + filePath);
}
//
if (manager != null) {
sqlManagerContainer.put(filePath, manager);
}
}
}
return manager;
}
}
注意上述代碼可以控制每個(gè)對(duì)應(yīng)的SQL文件始終只有一個(gè)對(duì)應(yīng)的SqlManager,不會(huì)對(duì)同一個(gè)文件創(chuàng)建多個(gè)SqlManager類(lèi)。
所有上述過(guò)程中的異常都以SqlMgntException的方式向上拋出,代碼為:
public class SqlMgntException extends Exception {
/**
*
*/
private static final long serialVersionUID = 1L;
public SqlMgntException() {
super();
// TODO Auto-generated constructor stub
}
public SqlMgntException(String message, Throwable cause) {
super(message, cause);
// TODO Auto-generated constructor stub
}
public SqlMgntException(String message) {
super(message);
// TODO Auto-generated constructor stub
}
public SqlMgntException(Throwable cause) {
super(cause);
// TODO Auto-generated constructor stub
}
}
優(yōu)化的方向,
1 初始話加載所有的SQL語(yǔ)句
程序運(yùn)行之初,將所有的SQL語(yǔ)句裝載在內(nèi)存中。
ExtJS教程-
Hibernate教程-
Struts2 教程-
Lucene教程