在开发JavaWeb应用程序的过程中可能经常需要使用到SQL语句来访问数据库。为了屏蔽SQL注入带来的危险,在Java中通常使用PreparedStatement,使用预编译的SQL语句。预编译的SQL语句是那些包含?的语句,使用PreparedStatement可以让数据库预先编译这些SQL模板,只有调用的时候套用必须的参数即可。
SQL文件的存放位置
那么在JavaWeb项目中预编译的SQL语句到底放在那里呢?
放在Java代码里肯定是不好的,为什么,有两点,第一,SQL语句放在Java代码里
太难看可,有不好的味道(参看Refactor),第二,每次SQL语句变更(可能经常发生)
都需要编译这些Java代码,比较烦。
那么SQL语句到底放在那里呢?根据这么多年的开发经验SQL文通常可以放在classes目录
下的文件中,存放SQL语句的文件有三种类型:properties文件,xml文件和txt文件。
在详细讨论文件格式之前,我们先讨论以下如何在Java类中得到这些文件的引用,以便从中得到需要的SQL语句。
使用Class的getResourceAsStream方法可以获得对文件引用的InputStream。例如:文件目录结构:
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支持的标准的属性文件(相当于Windows对ini文件的支持)。
Properties 文件的格式为:
# 注释
# 定义key,并且与key对应的值为value
key = value
可以使用java.util.Properties类来包装properties文件,使用如下
Properties props = new Properties();
try {
props.load(this.getInputStream());
} catch(IOException ioex) {
// 文件不存在,或者格式问题等
}
String value = props.getString("key")
注意上述格式中value不能换行,要想换行必须使用转义字符“\”。
在存储SQL的时候我们使用如下的格式,例如:
# 某业务,某操作SQL
xxx_0001 = select * from dual
# 某业务,其他操作SQL
xxx_0002 = \
select \
* \
from \
dual
使用Properties文件的优点是:Java内置支持布需要手写文件解析代码,另外使用也非常简单。
缺点是:SQL语句不能直接编写,需要追加转义字符"\",无法将这样的SQL语句直接拷贝到数据库客户端中运行。
XML文件
使用XML文件保存SQL比较常用的格式为:
<sqls>
<sql id=”xxx_0001”>
Select
*
from
dual <!-- 文件注释 -->
</sql>
<sql id=”xxx_0001”>
…
</sql>
</sqls>
使用XML格式的文件保存SQL需要自己写文件解析代码,由于Java对XML提供了内置的支持,并且第三方的开源库也很多,并且非常容易使用所以从xml文件中解析SQL语句也没有什么困难。以下以jdom为例讲解如何解析上述的XML格式的SQL文件。
protected Map analysis() {
Map map = new HashMap();
DOMBuilder builder = new DOMBuilder();
Document doc = null;
try {
// 解析XML文件
doc = builder.build(this.getInputStream());
// 获得根节点: <sqls>
Element element = doc.getRootElement();
// 获得所有根节点的子节点: <sql>节点列表
List sqlNodeList = element.getChildren("sql");
for (int i = 0; i < sqlNodeList.size(); i++) {
Element sqlNode = (Element) sqlNodeList.get(i);
// 获得SQL语句ID
String id = sqlNode.getAttribute("id").getValue();
// 获得SQL语句内容
String sql = sqlNode.getTextTrim();
map.put(id, sql);
}
} catch (JDOMException e) {
e.printStackTrace();
}
return map;
}
使用XML文件格式保存SQL语句据的时候需要注意,SQL语句中的大于(>)小于(<)和XML文件的格式冲突,有两种解决方法,
第一:使用全角的大于(>)小于(<)号。
第二:使用<![CDATA[ ]]>来包围所有的内容。
另外,通常情况下(不使用<![CDATA[ ]]>的时候)SQL的注释只能使用XML的注释格式(<!-- -->)。
使用XML格式的文件的有点:SQL语句可以正常书写,文件解析相对简单。
缺点:大于号,小于号的冲突;无法添加SQL注释(通常只能使用XML格式的注释)
TXT文件(以.SQL为后缀)
我们先说一下TXT文件(.sql文件)的格式
------------------------------------
--@ SQL-1
------------------------------------
SELECT
SYSDATE
FROM
DUAL(SQL)
------------------------------------
--@ SQL-2
-- 某某用途的SQL文
------------------------------------
SELECT
SYSDATE, ROWID -- 某某字段
FROM
DUAL(SQL) -- 某某表
使用txt格式的文件,非常的简单,和一般的写SQL语句一样,可以使用任何的SQL标准语法。只是有一个地方需要注意,就是每个SQL语句的头注释的地方加上一行特殊的内容用来标记SQL语句的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;
// 逐行读取文件内容
while ((currentLine = sqlFileReader.readLine()) != null) {
// 发现新的SQL语句,将已经发现的SQL语句放在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 {
// 非注释,ID行
// 去掉SQL语句行的末尾注视
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格式的文件的有点:SQL语句书写非常方便,可以拷贝出来直接运行,可以使用标准的SQL注释格式。
缺点:每个SQL语句必须添加特殊的ID标志(相对与Properties和xml来说可能也不算是缺点),解析困难,需要较强的文本接卸的能力。
通用SQL文件读取库
如何设计一个同时支持三种文件格式的SQL文件读取库?
首先我们抽象出一个SqlManager的接口,它提供通过Key获得对应SQL语句的操作。代码为:
public interface SqlManager {
/**
* 获取指定Key的SQL语句
*
* @param key
* SQL语句的Key值
* @return SQL语句内容
* @throws SqlMgntException
* 无法取得SQL语句时抛出此异常。
*/
public String getSql(String key) throws SqlMgntException;
}
然后使用一个抽象基类,来定义通用的属性和操作,代码为:
public abstract class AbstractSqlManager implements SqlManager {
/* SQL文件输入流 */
private final InputStream is;
/**
* SQL文件输入流获取方法。
*
* @return SQL文件输入流
*/
public InputStream getInputStream() {
return is;
}
/**
* 默认构造方法
*
* @param is
* SQL文件输入流
*/
public AbstractSqlManager(InputStream is) {
this.is = is;
}
/* SQL语句容器 */
private Map sqlContainer = null;
/* 同步KEY */
private final Object syncKey = new Object();
/**
* 通过SQL语句ID获取SQL语句内容。
*
* @param key
* SQL语句ID
* @return SQL语句
*/
public String getSql(final String key) throws SqlMgntException {
synchronized (syncKey) {
if (sqlContainer == null) {
this.sqlContainer = initContainer();
}
}
return (String) sqlContainer.get(key);
}
/**
* 抽象的模板方法,用来解析不同类型的SQL文件。
* @return 配对的SQL内容
*/
protected abstract Map initContainer();
}
注意其中的抽象方法:
protected abstract Map initContainer();
通过这个抽象方法把SQL文件的解析交给了具体的实例(模式参看:模板方法)
三个具体的SqlManager类我们不必具体说了,都是对上述抽线的解析方法的实现。
工厂类可以根据对应文件的后缀明创建对应的SqlManager。代码为:
public class SqlManagerFactory {
/* XMl文件类型 */
public static final String TYPE_XML = ".xml";
/* SQL文件类型 */
public static final String TYPE_SQL = ".sql";
/* 属性文件类型 */
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
/**
* 创建对应的SqlManager实现类
*
* @param filePath
* SQL文件相对路径和文件名
* @return
*/
public static SqlManager createSqlManager(String filePath) throws SqlMgntException {
SqlManager manager = null;
synchronized (sqlManagerContainer) {
manager = (SqlManager) sqlManagerContainer.get(filePath);
// 如果SqlManager不存在,则根据文件初始化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;
}
}
注意上述代码可以控制每个对应的SQL文件始终只有一个对应的SqlManager,不会对同一个文件创建多个SqlManager类。
所有上述过程中的异常都以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
}
}
优化的方向,
1 初始话加载所有的SQL语句
程序运行之初,将所有的SQL语句装载在内存中。
ExtJS教程-
Hibernate教程-
Struts2 教程-
Lucene教程