第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();
            }
        }
    }
    

又是获取资源、操作、释放资源、处理异常等,而且这样的代码无法用于生产环境也是肯定的了,那该怎么处理,我想你已经心里有数了吧?余下的部分,还是留给你来实现吧。