在项目中将应用服务器和文件服务器分开,能够增加项目的可维护性。本例分别使用FTP协议和SFTP协议在Windows Server 2008和Linux系统实现对文件的上传、下载和删除操作。
服务接口如下:
public interface FileManageDao { /** * 上传文件 * @param inputStream:文件输入流 * @param fileName:上传文件名称 */ public void upload(InputStream inputStream,String directory,String fileName) ; /** * 下载文件 * @param outputStream:文件输出流 * @param fileName:下载文件名称 */ public void download(OutputStream outputStream,String directory,String fileName) ; /** * 获取文件的输入流 * @param directory * @param fileName * @return */ public InputStream getAttachmentFile(String directory,String fileName); /** * 根据文档全名删除文件 * @param fileName */ public void delete(String directory,String fileName) ; /** * 判断文件是否存在 * @param directory * @param fileName * @return */ public boolean isExist(String directory,String fileName) ; }
服务接口模板类:
public abstract class FileManageDaoTemplate implements FileManageDao { //主机ip public String host="" ; //端口号,默认为22 public int port = 22 ; //服务器用户名,默认为root public String userName="root" ; //服务器密码 public String password ; //服务器上传地址 public String targetBaseLocation = "chrhc" ; //服务器连接超时时间(ms),默认60000 public int timeout = 60000; public void setTargetBaseLocation(String targetBaseLocation) { if(!StringUtils.hasLength(targetBaseLocation)){ return ; } targetBaseLocation = StringUtils.trimLeadingCharacter(targetBaseLocation, '/') ; targetBaseLocation = StringUtils.trimTrailingCharacter(targetBaseLocation, '/') ; this.targetBaseLocation = targetBaseLocation ; } ....set and get method..... }
下面在不同的系统分别采用不同的方式实现:
1.Windows 2008 Server下FTP协议
在Windows下选择使用FTP协议的原因是FTP是Windows自带的服务,只需要通过配置开启FTP服务即可,不需要第三方的安装包。Windows 2008下FTP配置见附件。
使用第三方免费安装包freeftpd也可以实现Windows系统下的FTP、SSH和SFTP服务,但是需要在软件界面中做相应的配置。
JAR包:使用Apache的FTP开源组件commons-net,添加如下Maven依赖:
<dependency> <groupId>commons-net</groupId> <artifactId>commons-net</artifactId> <version>3.3</version> </dependency>
文件上传模板类实现:
public class FileManageDaoImplByFTP extends FileManageDaoTemplate { /** * 获取ftp客户端连接 * @return * @throws SocketException * @throws IOException */ public FTPClient getFtpClient(String directory) throws SocketException, IOException{ FTPClient ftpClient = new FTPClient(); ftpClient.connect(host, port); ftpClient.setConnectTimeout(timeout); boolean loginFlag = ftpClient.login(userName, password) ; if(!loginFlag){ throw new SocketException("ftp login error ,please check setting .") ; } //设置文件上传目录 if(StringUtils.hasLength(directory.trim())){ directory = targetBaseLocation+"/"+StringUtils.trimLeadingCharacter(directory, '/'); }else{ directory = targetBaseLocation ; } //设置上传目录 boolean flag = ftpClient.changeWorkingDirectory(directory) ; //如果目录不存在,则创建目录 if(!flag){ String path = null ; for (String folderName : directory.split("/")) { if(path==null){ path = folderName ; }else{ path += "/"+folderName ; } ftpClient.makeDirectory(path); } flag = ftpClient.changeWorkingDirectory(path) ; } ftpClient.setBufferSize(1024); ftpClient.setControlEncoding("utf-8"); //设置文件类型(二进制) ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE); //设置ftp上传模式为client向server传 ftpClient.enterLocalPassiveMode(); return ftpClient ; } @Override public void upload(InputStream inputStream,String directory, String fileName) { FTPClient ftpClient = null ; try { ftpClient = getFtpClient(directory); //文件名称使用数字+英文,使用汉字会有乱码 boolean flag = ftpClient.storeFile(new String(fileName.getBytes("utf-8"),"iso-8859-1"), inputStream); if(!flag){ throw new Exception("上传文件 "+fileName+" 失败。") ; } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); }finally{ //关闭连接 IOUtils.closeQuietly(inputStream); close(ftpClient); } } @Override public void download(OutputStream outputStream,String directory, String fileName){ FTPClient ftpClient = null ; try { ftpClient = getFtpClient(directory); boolean fileIsExist = isExist(ftpClient, fileName) ; if(fileIsExist){ boolean flag = ftpClient.retrieveFile(fileName, outputStream) ; if(!flag){ throw new Exception("下载文件 "+fileName+" 失败。") ; } }else{ throw new Exception("FTP服务器文件 "+fileName+" 不存在.......") ; } } catch (Exception e) { // TODO Auto-generated catch block System.out.println(e.getMessage()); }finally{ close(ftpClient); } } @Override public void delete(String directory, String fileName) { FTPClient ftpClient = null ; try { ftpClient = getFtpClient(directory); boolean flag = ftpClient.deleteFile(new String(fileName.getBytes("utf-8"),"iso-8859-1")); if(!flag){ throw new Exception("删除文件 "+fileName+" 失败。") ; } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); }finally{ //关闭连接 close(ftpClient); } } public InputStream getAttachmentFile(String directory,String fileName){ FTPClient ftpClient = null ; InputStream inputStream = null; try { ftpClient = getFtpClient(directory); boolean fileIsExist = isExist(ftpClient, fileName) ; if(fileIsExist){ inputStream = ftpClient.retrieveFileStream(fileName); }else{ throw new Exception("FTP服务器文件 "+fileName+" 不存在.......") ; } } catch (Exception e) { // TODO Auto-generated catch block System.out.println(e.getMessage()); }finally{ //关闭连接 close(ftpClient); } return inputStream ; } public void close(FTPClient ftpClient){ if(ftpClient!=null){ try { ftpClient.disconnect(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } /** * 判断ftpClient当前所在目录下,名称为fileName的文件是否存在 * @param ftpClient * @param fileName * @return */ public boolean isExist(FTPClient ftpClient,final String fileName) { boolean flag = false ; try { FTPFile[] ftpFiles = ftpClient.listFiles(null, new FTPFileFilter() { @Override public boolean accept(FTPFile file) { if(file.getName().equals(fileName)){ return true ; } return false ; } }) ; if(ftpFiles!=null&&ftpFiles.length>0){ flag = true ; } } catch (Exception e) { System.out.println("-------query ftp file exist fail......"); } return flag ; } @Override public boolean isExist(String directory, final String fileName) { boolean flag = false ; FTPClient ftpClient = null ; try { ftpClient = getFtpClient(directory) ; FTPFile[] ftpFiles = ftpClient.listFiles(null, new FTPFileFilter() { @Override public boolean accept(FTPFile file) { if(file.getName().equals(fileName)){ return true ; } return false ; } }) ; if(ftpFiles!=null&&ftpFiles.length>0){ flag = true ; } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); }finally{ close(ftpClient); } return flag ; } }注:使用FTP上传文件时,在有的机器上会停留在storeFile方法上不动,没有反应。解决:需要设置FTP数据连接模式为被动模式,即在获取FTP连接后设置 ftpclient.enterLocalPassiveMode()。原因是: FTP协议有两种工作方式:PORT方式和PASV方式,中文意思为主动式和被动式。 PORT(主动)方式的连接过程是:客户端向服务器的FTP端口(默认是21)发送连接请 求,服务器接受连接,建立一条命令链路。当需要传送数据时,客户端在命令链路上用PORT 命令告诉服务器:“我打开了XXXX端口,你过来连接我”。于是服务器从20端口向客户端的 XXXX端口发送连接请求,建立一条数据链路来传送数据。 PASV(被动)方式的连接过程是:客户端向服务器的FTP端口(默认是21)发送连接请 求,服务器接受连接,建立一条命令链路。当需要传送数据时,服务器在命令链路上用PASV 命令告诉客户端:“我打开了XXXX端口,你过来连接我”。于是客户端向服务器的XXXX端口 发送连接请求,建立一条数据链路来传送数据。被动模式不需要关注客户端配置情况,只需要关注FTP服务器即可。FTP数据连接模式默认为本地主动模式,停留在storeFile方法上有可能是因为本地机器的某个端口被占用,所以没法传数据。 2.Linux下SFtp协议
在Linux使用vsftp安装和配置文件服务器,经测试和使用比较稳定。Linux下配置vsftp参考:http://jingyan.baidu.com/article/adc815133476bdf723bf7393.html
在Linux系统下使用SFTP协议传输文件,相比较FTP,SFTP协议对传输的文件进行了加密,使传输过程更加安全,但是由于使用了加密,传输速度要比FTP的传输速度慢。
JAR包:使用JSCH(Java Sercure Channel),JSCH是一个纯粹的用java实现SSH功能的java library。Maven依赖如下:
<dependency> <groupId>com.jcraft</groupId> <artifactId>jsch</artifactId> <version>0.1.51</version> </dependency>文件上传模板类实现:
public class FileManageDaoImplBySFTP extends FileManageDaoTemplate { /** * 获取连接通道 * @return * @throws JSchException */ public ChannelSftp getChannel() throws JSchException { JSch jsch = new JSch(); // 创建JSch对象 Session session = jsch.getSession(userName, host, port); // 根据用户名,主机ip,端口获取一个Session对象 if (password != null) { session.setPassword(password); // 设置密码 } Properties config = new Properties(); config.put("StrictHostKeyChecking", "no"); session.setConfig(config); // 为Session对象设置properties session.setTimeout(timeout); // 设置timeout时间 session.connect(); // 通过Session建立链接 Channel channel = session.openChannel("sftp"); // 打开SFTP通道 channel.connect(); // 建立SFTP通道的连接 return (ChannelSftp) channel; } /** * 关闭连接通道 * @throws Exception */ public void closeChannel(Channel channel) { if(channel==null){ return ; } channel.disconnect(); try { //关闭会话 if (channel.getSession() != null) { channel.getSession().disconnect(); } } catch (JSchException e) { e.printStackTrace(); } } /** * 上传文件到文件服务器 * @param inputStream * @param fileName * @throws JSchException */ @Override public void upload(InputStream inputStream,String directory,String fileName) { ChannelSftp channelSftp = null; try { channelSftp = getChannel(); channelSftp.put(inputStream, targetBaseLocation+directory+fileName); channelSftp.quit(); } catch (Exception e) { e.printStackTrace(); }finally{ closeChannel(channelSftp); } } /** * 从文件服务器上下载文件 */ @Override public void download(OutputStream outputStream,String directory,String fileName){ ChannelSftp channelSftp = null; try { channelSftp = getChannel(); channelSftp.get(targetBaseLocation+directory+fileName, outputStream, null,ChannelSftp.OVERWRITE , 0); } catch (Exception e) { //TODO Auto-generated catch block e.printStackTrace(); }finally{ closeChannel(channelSftp); } } public InputStream getAttachmentFile(String directory,String fileName){ return null ; } @Override public void delete(String directory,String fileName) { ChannelSftp channelSftp = null; try { channelSftp = getChannel(); channelSftp.rm(targetBaseLocation+directory+fileName); } catch (Exception e) { //TODO Auto-generated catch block e.printStackTrace(); }finally{ closeChannel(channelSftp); } } @Override public boolean isExist(String directory, String fileName) { // TODO Auto-generated method stub return false; } }
注:项目框架是Spring,DAO层的类都是使用单例模式,考虑到多线程,在DAO类中不要包括和连接相关的属性,以免一个线程在传输文件过程连接被另一个线程关闭。
Spring配置文件中配置模板类后,根据文件服务器的系统类型选择不同的传输方式。
<!-- 文件服务器模板方法 --> <bean id="fileManageDaoTemplate" class="com.dao.FileManageDaoTemplate" abstract="true"> <property name="host" value="ip"></property> <property name="port" value="port"></property> <property name="userName" value="administrator"></property> <property name="password" value="password"></property> <property name="targetLocation" value="/directory/"></property> <property name="timeout" value="60000"></property> </bean> <!-- linux系统使用sftp保存文件 --> <!-- <bean class="com.dao.FileManageDaoImplBySFTP" parent="fileManageDaoTemplate"> --> <!-- </bean> --> <bean class="com.dao.FileManageDaoImplByFTP" parent="fileManageDaoTemplate"></bean>
显示FTP服务器上的图片文件,方案如下:
1.FTP服务上的图片不能直接通过ftp链接显示,在Controller层的一个方法中根据文件目录和文件名,下载下文件后,再将文件回传回去。简单实现如下:
@RequestMapping(value = "/showImage/{directory}/{filename}.{suffix}") public void downloadImage( @PathVariable("directory") String directory, @PathVariable("filename") String filename, @PathVariable("suffix") String suffix, HttpServletResponse response) throws Exception { response.setContentType("image/jpeg"); attachmentFileService.get(response.getOutputStream(),directory+"/" , filename+"."+suffix) ; }
2.在ftp服务器的目录下搭建一个tomcat服务器,使用ftp上传时,将图片文件放到tomcat服务器下。前台通过访问ftp服务器下的tomcat服务器地址显示图片。