第16章 Spring数据访问之扩展篇
本章内容
- 活用模板方法模式及Callback
- 数据访问中的多数据源
- Spring 3.0展望
16.1 活用模板方法模式及Callback
纵观第13章到第15章的内容,我们会发现不管是Spring对JDBC API的抽象,还是对Hibernate、iBATIS等ORM的集成,全部都采用了一种理念或者处理方式,那就是模板方法模式与相应的Callback接口相结合。
那么,为什么要在这里使用模板方法与Callback相结合的问题处理方式呢?最基本的原因是:不管是JDBC还是Hibemate或者其他ORM实现,在资源管理上有一个共性,那就是需要在资源使用之后可以安全地释放这些资源 。与Bitter Java所提出的理念相同, 为了确保尽可能地将资源的获取和资源的释放操作放在一起,Spring在数据访问层处理资源的问题上,采用了模板方法模式 。
这样,以一种统一而集中的方式来处理资源的获取和释放,避免了将这种容易出现问题的操作分散到代码中的各个地方,进而也就避免了由此产生的“资源泄漏”一类比较严重的问题。
推而广之,我们可以以相同的模式来处理类似的问题,而也会发现,这样的处理与我们之前的处理或者封装方式是如此的不同,如此的简洁明了。
16.1.1 FTPClientTemplate
之前我们说过,通常的FX系统会从相应的新闻提供商那里定期获取外汇交易相关新闻。最常见的方式就是,通过FTP协议到指定的FTP服务器去定期下载相应的新闻文件,所以,FX系统的应用程序需要提供相应的实现类来进行FTP操作。而程序中的FTP操作应该是比较通用的,无非就是上传下载文件。
为了程序能有一个良好的结构,我们通常会将这些FTP操作逻辑封装为一个工具类。而下面我们将看到的,就是两种截然不同的工具类实现方式。
我们不需要为最为底层的FTP操作“重新发明轮子”,Jakarta Commons Net类库提供了基本的FTP支持,不过,直接使用CommonsNet的API就与直接使用JDBC API一样让人尴尬,下方代码清单的代码演示了直接使用Commons Net API进行FTP操作的一般情况。
boolean error = false;
try {
int reply;
ftp.connect("ftp.foobar.com");
System.out.println("Connected to " + server + ".");
System.out.print(ftp.getReplyString());
// After connection attempt. you should check the reply code to verify success
reply = ftp.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
ftp.disconnect();
System.err.println("FTP server refused connection.");
System.exit(1);
}
... // transfer files
ftp.logout();
} catch(IOException e) {
error = true;
e.printStackTrace();
} finnaly {
if (ftp.isConnected()) {
try {
ftp.disconnect();
} catch (IOException ioe) {
// do nothing
}
}
System.exit(error ? 1 : 0);
}
我得承认,FTPClient类提供的这段代码只是一段示例,不能在实际的生产环境下使用,所以,我们尝试对其进行封装。
对于使用FTPClient类实现的FTP操作来说,无非就是登录FTP服务器,传输文件,然后退出服务器三步。下方代码清单所示的FTP操作工具类是最为常见的实现方式。
class Phase1FtpUtility {
public boolean login(...) {
//登录代码
}
public void doTransfer(...) {
//文件传输逻辑实现.
}
public boolean logout() {
//退出登录
}
}
相对于示例中的代码来说,通过PhaselFtpUtility类的封装,现在进行FTP操作看起来要简洁多了。不过,这样的封装方式并没有起到多少实际效果。
Phase1FtpUtility对FTPClientAPI的封装力度不够,与直接使用FTPClient的API相比,调用方只是少写几行代码而已,如下所示:
Phase1Ftputility ftpUtility = ...;
if (ftpUtility.login(...)) {
ftpUtility.doTransfer(...);
}
ftpUtility.logout();
而且,这样的代码把资源的管理留给了每处调用Phase1FtpUtility进行FTP操作的调用代码。与数据库连接一样,你要怎样保证相应的资源在每处都能成功地获得释放呢?就现有的API封装方式,我们只能加强开发人员的约束力来达到正确使用API的目的了。
通常,Phase1FtpUtility的doTransfer(..)
方法用来实现具体的FTP操作逻辑,但现在的phase1FtpUtility只能提供固定的FTP操作逻辑。如果其他调用方需要不同的FTP操作,那么,或许就得子类化Phase1FtpUtility并覆写doTransfer(..)
方法了。不过,这样好像偏离了我们要将phaselFtpUtility作为单一工具类来使用的初衷。
鉴于这些限制,我们需要另一种FTPClientAPI的封装方式。而你也看出来了,就如Phase1FtpUtility所展示的那样,所有的使用FTPClient进行FTP操作的步骤几乎是一样的,唯一的不同就是每次进行FTP操作的细节。在经过JdbcTemplate、HibernateTemplate以及SqlMapClientTemplate等熏陶之后,自然而然就应该想到,我们可以对FTPClientAPI进行同样的处理,从而下方代码清单所给出的FTPClientTemplate就是我们的最终需要。
public class FTPClientTemplate {
private static final Log logger = LogFactory.getLog(FTPClientTemplate.class);
private FTPClientConfig ftpClientConfig; //可选属性
private String server; //必须指定
private String username; //必须指定
private String password; //必须指定
private int port = 21; //必须指定
public FTPClientTemplate(String host, String username, String password) {
this.server = host;
this.username = username;
this.password = password;
}
public void execute(FTPClientCallback callback) throws IOException {
FTPClient ftp = new FTPClient();
try {
if (this.getFtpClientConfig() != null) {
ftp.configure(this.getFtpClientConfigl));
}
ftp.connect(server, getPort());
// 检查到服务器的连接是否正确
int reply = ftp.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
throw new IOException("failed to connect to the FTP Server:" + server);
}
// 登录
boolean isLoginSuc = ftp.login(this.getUsername(), this.getPassword());
if (!isLoginSuc) {
throw newIOException("wrong username or password,please try to login again.");
}
// 通过回调方法实现特定的FTP操作
callback.processFTPRequest(ftp);
// 退出登录
ftp.logout();
} finally {
if (ftp.isConnected()) {
ftp.disconnect();
}
}
}
public String[] listFileNames(final String remoteDir, final String fileNamePattern) throws IOException {
final List<String[]> container = new ArrayList<String[]>();
execute(new FTPClientCallback() {
public void processFTPRequest(FTPClient ftp) throws IOException {
ftp.enterLocalPassiveMode();
changeWorkingDir(ftp, remoteDir);
if (logger.isDebugEnabled())
logger.đebug("working dir:" + ftp.printWorkingDirectory());
}
});
return container.get(0);
}
protected void changeWorkingDir(FTPClient ftp, String remoteDir) throws IOException {
Validate.notEmpty(remoteDir);
ftp.changeWorkingDirectory(remoteDir);
}
//setter和getter方法定义
}
我们通过execute(FTPClientCallback)
方法对整个的基于FTPClient的API使用流程进行了封装,而将我们真正关心的每次具体的FTP操作交给了FTPClientCallback,该接口定义如下所示:
public interface FTPClientCallback {
public void processFTPRequest(FTPClientftpClient) throws IOException;
}
现在要做的,就是根据每次FTP操作请求细节提供相应的FTPClientCallback实现,然后交给FTPClientTemplate执行,如下所示:
FTPClientTemplate ftpTemplate = new FTPClientTemplate(host, user, pwd);
FTPClientCallback callback = new FTPClientCallback() {
public void processFTPRequest(FTPClientftpClient) throws IOException {
...// 特定的FTP操作实现逻辑
}
};
ftpTemplate.excute(callback);
FTPClientTemplate一旦构建完成,其他任何调用方都可以共享使用它,调用方每次提供自己的FTPClientCallback实现即可。
现在的FTPClientTemplate看起来可能过于单薄。某些常用的FTP操作,如文件上传、文件下载、文件列表读取等,我们可以在FTPClientTemplate内直接提供,而没有必要让调用方每次实现几乎相同的代码。
listFileNames(remoteDir,fileNamePattern)
方法就是这样的方法实现,无非就是提供了相应的FTPClientCallback实现,然后最终委托execute()
方法执行而已。
作为工具类,我们可以直接将这些常用的FTP操作方法定义到FTPClientTemplate中。如果愿意,也可以设计一个FTPOperation之类的接口,里面定义一系列的FTP操作方法,然后让FTPClientTemplate来实现该接口。
16.1.2 HttpClientTemplate
现在基于REST方式的Web服务好像比原来SOAP的方式更加受人欢迎一些,世界上许多券商或者银行通常也会以REST的方式发送一些外汇牌价之类的信息,甚至,FX系统的某些外汇新闻提供商也通过HTTP协议采用类似于REST的方式来发送新闻。那么,与基于FTP协议的信息交换类似,对于这种方式的信息交换,我们也需要在应用程序中采用适当的API进行处理。
Apache Commons HttpClient是一个提供HTTP协议支持的Java类库,许多应用包括稍后我们将提到的Spring Remoting都是采用该类库实现的。我们同样可以使用该类库进行基于HTTP协议的信息交换,或者说得更“时髦”一点儿,进行REST方式的Web服务开发。
如果你是初次接触HttpClient,那么应该先看一下HtpClient网站提供的Tutorial文档,里面给出了类似下方代码清单给出的使用代码示例。
public class HttpClientTutorial {
private static String url = "http://www.apache.org/";
public static void main(String[] args) {
// Create an instance of HttpClient.
HttpClient client = new HttpClient();
// Create a method instance.
GetMethod method = new GetMethod(url);
// Provide custom retry handler is necessary
method.getParams().setParameter(HttpMethodParams, RETRY_HANDLER,
new DefaultHttpMethodRetryHandler(3, false));
try {
// Execute the method.
int statusCode = client.executeMethod(method);
if (statusCode != HttpStacus.SC_OK) {
System.err.println("Method failed: " + methoa.getStatusLine());
}
// Read the response body.
byte[] responseBody = method.getResponseBody();
// Deal with the response.
// Use caution: ensure correct character encoding and is not binary data
System.out.println(new String(responseBody));
} catch (HttpException e) {
System.err.println("Fatal protocol violation: " + e.getMessage());
e.printStackTrace();
} catch (IOException e) {
System.err.println("Fatal transport error: " + e.getMessage());
e.printStackTrace();
} finally {
// Release the connection.
method.releaseConnection();
}
}
}
又是获取资源、操作、释放资源、处理异常等,而且这样的代码无法用于生产环境也是肯定的了,那该怎么处理,我想你已经心里有数了吧?余下的部分,还是留给你来实现吧。