注意:本文只适合小文本文件的上传下载,因为post请求是有大小限制的。默认大小是2m,虽然具体数值可以调节,但不适合做大文件的传输
最近公司有这么个需求:以后所有的项目开发中需要使用ftp服务器的地方都不能直接操作ftp服务器,而是通过调用一个统一的接口去操作文件的上传下载等功能。
其实就是针对ftp服务器封装一个项目,所有的针对ftp的操作都通过这个项目来实现。
技术点:接口调用必然存在各种参数、错误信息、成功信息等。那么我们的文件要通过什么形式放到参数中在网络传递呢?
文件可以转换成字节流在网络中传输没毛病,但是转换成二进制流之后放到我们封装的对象中再次序列化在网络中传输就有问题了。
所以关键是将我们的文件以何种数据结构封装在我们的参数对象中在网络里传输。答案是 字节数组;
目录结构:
入参对象: Vsftpd
package com.ch.vsftpd.common.pojo; /** * @Auther: 011336 * @Date: 2019/4/24 11:04 */ public class Vsftpd { /** * 请求类型[必填] * upload: 上传 文件已存在上传失败 * replace:上传 文件已存在则覆盖 * download: 下载 * display: 查看文件列表 * delete: 删除文件 */ private String optionType; /** * 请求者在接口管理系统中维护的项目编码[必填] */ private String projectCode; /** * 上传/下载 文件名[非必填] */ private String fileName; /** * 上传/下载 文件的字节数组[非必填] */ private byte[] byteArry; ... }
返回对象: VsftpResult
package com.ch.vsftpd.common.pojo; import java.util.List; /** * @Auther: 011336 * @Date: 2019/4/24 11:03 */ public class VsftpResult { private boolean status; private byte[] byteArry; private String[] fileNames; private List<ErrorInfo> errors; }
错误信息: ErrorInfo
package com.ch.vsftpd.common.pojo; /** * @Auther: 011336 * @Date: 2019/3/28 11:32 */ public class ErrorInfo { private String errorCode; private String errorMsg; public ErrorInfo() { } public ErrorInfo(String errorCode) { this.errorCode = errorCode; } public ErrorInfo(String errorCode, String errorMsg) { this.errorCode = errorCode; this.errorMsg = errorMsg; } }
对外提供接口: FtpController
/** * ftp服务,对外提供统一接口地址,通过控制传递的参数实现 上传、覆盖上传、获取文件列表和下载4个功能 * 具体参数参考【Vsftpd.java, FtpConstants】 * @before AuthorityInterceptor.java 拦截器做权限校验 * @Auther: 011336 * @Date: 2019/4/24 11:21 */ @Controller @RequestMapping("/vsftpdService") public class ftpController { private static Logger LOGGER = LoggerFactory.getLogger(ftpController.class); @ResponseBody @RequestMapping(path = "/vsftpd", method = RequestMethod.POST) public VsftpResult getAuthInfo(@RequestBody Vsftpd vsftpd){ LOGGER.info("ftpController.getAuthInfo start"); IFtpUploadStrategy strategy = null; List<ErrorInfo> errors = new ArrayList<>(); VsftpResult result = new VsftpResult(); //第一步校验参数是否合法 if (StringUtils.isEmpty(vsftpd.getOptionType())) { ErrorInfo errorInfo = new ErrorInfo("PARAMETER.FAIL","调用参数[type]不能为空!"); errors.add(errorInfo); } if (StringUtils.isEmpty(vsftpd.getProjectCode())) { ErrorInfo errorInfo = new ErrorInfo("PARAMETER.FAIL","参数[projectCode]不能为空!"); errors.add(errorInfo); } //根据请求类型使用不同策略 if(FtpConstants.UP_LOAD.equals(vsftpd.getOptionType())){ strategy = new FtpUploadStrategy(); }else if(FtpConstants.REPLACE.equals(vsftpd.getOptionType())){ strategy = new FtpUploadStrategy(); }else if(FtpConstants.DOWAN_LOAD.equals(vsftpd.getOptionType())){ strategy = new FtpDownLoadStrategy(); }else if(FtpConstants.DISPLAY.equals(vsftpd.getOptionType())){ strategy = new FtpDisplayStrategy(); }else if(FtpConstants.DELETE.equals(vsftpd.getOptionType())){ strategy = new FtpDeleteStrategy(); }else { ErrorInfo errorInfo = new ErrorInfo("PARAMETER.FAIL","调用参数[type]错误!"); errors.add(errorInfo); } if (CollectionUtils.isEmpty(errors)) { result = strategy.vsftpMethod(vsftpd); }else{ result.setStatus(false); result.setErrors(errors); } return result; } }
四种策略:
文件上传: FtpUploadStrategy

/** * 文件上传 * 通过参数vsftpd.getOptionType()分为普通上传 和 覆盖上传两种 * 第一种若文件已存在则返回错误信息提示文件已存在 * 第二种则直接覆盖 * @Auther: 011336 * @Date: 2019/4/24 10:59 */ public class FtpUploadStrategy implements IFtpUploadStrategy { private static Logger LOGGER = LoggerFactory.getLogger(FtpUploadStrategy.class); @Override public VsftpResult vsftpMethod(Vsftpd vsftpd){ LOGGER.info("FtpUploadStrategy.vsftpMethod start"); VsftpResult result = new VsftpResult(); List<ErrorInfo> errors = new ArrayList<>(); if (StringUtils.isEmpty(vsftpd.getFileName())) { ErrorInfo errorInfo = new ErrorInfo("PARAMETER.FAIL","参数[fileName]不能为空!"); errors.add(errorInfo); } if (vsftpd.getByteArry()==null) { ErrorInfo errorInfo = new ErrorInfo("PARAMETER.FAIL","参数[byteArry]不能为空!"); errors.add(errorInfo); } //当不强制上传的时候 文件若已存在则上传失败 boolean flag = false; try { if(FtpConstants.UP_LOAD.equals(vsftpd.getOptionType())) { //判断文件是否存在 boolean b = FtpUtil.fileExist(vsftpd.getProjectCode(), vsftpd.getFileName()); if (b) { ErrorInfo errorInfo = new ErrorInfo("PARAMETER.FAIL", "文件[" + vsftpd.getFileName() + "]已存在!"); errors.add(errorInfo); } } //上传文件(文件若存在则覆盖) if(CollectionUtils.isEmpty(errors)){ flag = FtpUtil.uploadFile(vsftpd.getFileName(),vsftpd.getByteArry(),vsftpd.getOptionType()); } } catch (Exception e) { e.printStackTrace(); ErrorInfo errorInfo = new ErrorInfo("FTP.ERROR","下载失败!服务端异常!"); errors.add(errorInfo); } if(!flag){ ErrorInfo errorInfo = new ErrorInfo("FTP.ERROR","上传失败!系统异常!"); errors.add(errorInfo); } if(CollectionUtils.isEmpty(errors)){ result.setStatus(true); }else{ result.setStatus(false); result.setErrors(errors); } LOGGER.info("FtpUploadStrategy.vsftpMethod end"); return result; } }
文件下载: FtpDownLoadStrategy

/** * 文件下载 * 若文件不存在 返回文件不存在的错误信息 * @Auther: 011336 * @Date: 2019/4/24 10:59 */ public class FtpDownLoadStrategy implements IFtpUploadStrategy { private static Logger LOGGER = LoggerFactory.getLogger(FtpDownLoadStrategy.class); @Override public VsftpResult vsftpMethod(Vsftpd vsftpd){ LOGGER.info("FtpDownLoadStrategy.vsftpMethod start"); VsftpResult result = new VsftpResult(); List<ErrorInfo> errors = new ArrayList<>(); byte[] arryArry = null; if (StringUtils.isEmpty(vsftpd.getFileName())) { ErrorInfo errorInfo = new ErrorInfo("PARAMETER.FAIL","参数[fileName]不能为空!"); errors.add(errorInfo); } //判断文件是否存在 try { boolean b = FtpUtil.fileExist(vsftpd.getProjectCode(),vsftpd.getFileName()); if (!b){ ErrorInfo errorInfo = new ErrorInfo("PARAMETER.FAIL","文件[fileName]不存在!"); errors.add(errorInfo); } //下载 if(CollectionUtils.isEmpty(errors)){ arryArry = FtpUtil.downloadFile(vsftpd.getProjectCode(),vsftpd.getFileName()); } } catch (Exception e) { e.printStackTrace(); ErrorInfo errorInfo = new ErrorInfo("FTP.ERROR","下载失败!服务端异常!"); errors.add(errorInfo); } if(arryArry == null){ ErrorInfo errorInfo = new ErrorInfo("FTP.ERROR","下载失败!系统异常!"); errors.add(errorInfo); } if(CollectionUtils.isEmpty(errors)){ result.setStatus(true); result.setByteArry(arryArry); }else{ result.setStatus(false); result.setErrors(errors); } LOGGER.info("FtpDownLoadStrategy.vsftpMethod end"); return result; } }
文件删除: FtpDeleteStrategy

/** * 文件删除 * 文件若不存在则返回删除成功 * @Auther: 011336 * @Date: 2019/4/24 10:59 */ public class FtpDeleteStrategy implements IFtpUploadStrategy { private static Logger LOGGER = LoggerFactory.getLogger(FtpDeleteStrategy.class); @Override public VsftpResult vsftpMethod(Vsftpd vsftpd){ LOGGER.info("FtpDeleteStrategy.vsftpMethod start"); VsftpResult result = new VsftpResult(); List<ErrorInfo> errors = new ArrayList<>(); if (StringUtils.isEmpty(vsftpd.getFileName())) { ErrorInfo errorInfo = new ErrorInfo("PARAMETER.FAIL","参数[fileName]不能为空!"); errors.add(errorInfo); } //删除文件 try { if(CollectionUtils.isEmpty(errors)){ FtpUtil.deleteFile(vsftpd.getProjectCode(),vsftpd.getFileName()); } } catch (Exception e) { e.printStackTrace(); ErrorInfo errorInfo = new ErrorInfo("FTP.ERROR","删除失败!服务端异常!"); errors.add(errorInfo); } if(CollectionUtils.isEmpty(errors)){ result.setStatus(true); }else{ result.setStatus(false); result.setErrors(errors); } LOGGER.info("FtpDeleteStrategy.vsftpMethod end"); return result; } }
获取文件列表:FtpDisplayStrategy

/** * 获取文件列表 * @Auther: 011336 * @Date: 2019/4/24 10:59 */ public class FtpDisplayStrategy implements IFtpUploadStrategy { private static Logger LOGGER = LoggerFactory.getLogger(FtpDisplayStrategy.class); @Override public VsftpResult vsftpMethod(Vsftpd vsftpd){ LOGGER.info("FtpDisplayStrategy.vsftpMethod start"); VsftpResult result = new VsftpResult(); List<ErrorInfo> errors = new ArrayList<>(); String[] fileNames = new String[0]; try { fileNames = FtpUtil.displayFile(vsftpd.getProjectCode()); } catch (Exception e) { e.printStackTrace(); ErrorInfo errorInfo = new ErrorInfo("FTP.ERROR","获取文件列表失败!服务端异常!"); errors.add(errorInfo); } if(CollectionUtils.isEmpty(errors)){ result.setStatus(true); result.setFileNames(fileNames); }else{ result.setStatus(false); result.setErrors(errors); } LOGGER.info("FtpDisplayStrategy.vsftpMethod end"); return result; } }
提供一个策略接口:

/** * @Auther: 011336 * @Date: 2019/4/24 11:21 */ public interface IFtpStrategy { /** * 统一处理用户请求 * @param vsftpd * @return */ VsftpResult vsftpMethod(Vsftpd vsftpd); }
最主要是我们的 FtpUtil 工具类
/** * @author 王未011336 * @date 2018/11/04 * ftp服务器文件上传下载 */ public class FtpUtil { private static Logger LOGGER = LoggerFactory.getLogger(FtpUtil.class); private static String LOCAL_CHARSET = "GBK"; private static String SERVER_CHARSET = "ISO-8859-1"; private static String host; private static String port; private static String username; private static String password; private static String basePath; private static String filePath; private static String localPath; /** *读取配置文件信息 * @return */ public static void getPropertity(){ Properties properties = new Properties(); ClassLoader load = FtpUtil.class.getClassLoader(); InputStream is = load.getResourceAsStream("conf/vsftpd.properties"); try { properties.load(is); host=properties.getProperty("vsftpd.ip"); port=properties.getProperty("vsftpd.port"); username=properties.getProperty("vsftpd.user"); password=properties.getProperty("vsftpd.pwd"); //服务器端 基路径 basePath=properties.getProperty("vsftpd.remote.base.path"); //服务器端 文件路径 filePath=properties.getProperty("vsftpd.remote.file.path"); //本地 下载到本地的目录 localPath=properties.getProperty("vsftpd.local.file.path"); } catch (IOException e) { e.printStackTrace(); } } /** * 上传重载 * @param filename 上传到服务器端口重命名 * @param buffer byte[] 文件流 * @return */ public static boolean uploadFile(String filename, byte[] buffer, String optionType) throws Exception{ getPropertity(); return uploadFile( host, port, username, password, basePath, filePath, filename, buffer, optionType); } /** * 获取文件列表 * @param filePath * @return * @throws Exception */ public static String[] displayFile(String filePath) throws Exception{ getPropertity(); return displayFile(host, port, username, password, basePath, filePath); } /** * 删除文件 * @param filePath * @return */ public static boolean deleteFile(String filePath, String fileName) throws Exception{ getPropertity(); return deleteFile(host, port, username, password, basePath, filePath, fileName); } /** * 判断文件是否存在 * @param filePath * @return * @throws Exception */ public static boolean fileExist(String filePath,String filename) throws Exception{ if(StringUtils.isEmpty(filename)){ return false; } getPropertity(); String[] names = displayFile(filePath); for (String name : names) { if(filename.equals(name)){ return true; } } return false; } /** *下载重载 * @param filePath 要下载的文件所在服务器的相对路径 * @param fileName 要下载的文件名 * @return */ public static byte[] downloadFile(String filePath,String fileName) throws Exception{ getPropertity(); return downloadFile( host, port, username, password, basePath, filePath, fileName); } /** * Description: 向FTP服务器上传文件 * @param host FTP服务器hostname * @param port FTP服务器端口 * @param username FTP登录账号 * @param password FTP登录密码 * @param basePath FTP服务器基础目录 * @param filePath FTP服务器文件存放路径。例如分日期存放:/2015/01/01。文件的路径为basePath+filePath * @param fileName 上传到FTP服务器上的文件名 * @return 成功返回true,否则返回false */ public static boolean uploadFile(String host, String port, String username, String password, String basePath, String filePath, String fileName, byte[] buffer, String optionType) throws Exception{ FTPClient ftp = new FTPClient(); try { fileName = new String(fileName.getBytes(LOCAL_CHARSET)); boolean result = connectFtp(ftp, host, port, username, password, basePath, filePath); if(!result){ return result; } //为了加大上传文件速度,将InputStream转成BufferInputStream , InputStream input InputStream inputStream = new ByteArrayInputStream(buffer); //加大缓存区 ftp.setBufferSize(1024*1024); //设置上传文件的类型为二进制类型 ftp.setFileType(FTP.BINARY_FILE_TYPE); if(FtpConstants.REPLACE.equals(optionType)){ ftp.deleteFile(fileName); } //上传文件 if (!ftp.storeFile(fileName, inputStream)) { return false; } inputStream.close(); ftp.logout(); } catch (IOException e) { LOGGER.info(e.getMessage()); e.printStackTrace(); throw e; } finally { if (ftp.isConnected()) { try { ftp.disconnect(); } catch (IOException ioe) { } } } return true; } /** * Description: 从FTP服务器下载文件 * @param host FTP服务器hostname * @param port FTP服务器端口 * @param username FTP登录账号 * @param password FTP登录密码 * @param basePath FTP服务器上的相对路径 * @param fileName 要下载的文件名 * @return */ public static byte[] downloadFile(String host, String port, String username, String password, String basePath, String filePath, String fileName) throws Exception{ FTPClient ftp = new FTPClient(); try { fileName = new String(fileName.getBytes(LOCAL_CHARSET)); boolean result = connectFtp(ftp, host, port, username, password, basePath, filePath); if(!result){ return null; } FTPFile[] fs = ftp.listFiles(); boolean flag = true; for (FTPFile ff : fs) { if (ff.getName().equals(fileName)) { InputStream input = ftp.retrieveFileStream(ff.getName()); BufferedInputStream in = new BufferedInputStream(input); ByteArrayOutputStream outStream = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len = -1; while((len = in.read(buffer)) != -1){ outStream.write(buffer, 0, len); } outStream.close(); in.close(); byte[] arryArry = outStream.toByteArray(); return arryArry; } } if(flag) { LOGGER.info("服务器端文件不存在..."); return null; } ftp.logout(); } catch (IOException e) { LOGGER.info(e.getMessage()); e.printStackTrace(); throw e; } finally { if (ftp.isConnected()) { try { ftp.disconnect(); } catch (IOException ioe) { } } } return null; } /** * 获取服务器文件名列表 * @param host * @param port * @param username * @param password * @param basePath * @param filePath * @return * @throws Exception */ public static String[] displayFile(String host, String port, String username, String password, String basePath, String filePath) throws Exception{ FTPClient ftp = new FTPClient(); try { boolean result = connectFtp(ftp, host, port, username, password, basePath, filePath); if(!result){ return null; } String[] names = ftp.listNames(); ftp.logout(); return names; } catch (IOException e) { LOGGER.info(e.getMessage()); e.printStackTrace(); throw e; } finally { if (ftp.isConnected()) { try { ftp.disconnect(); } catch (IOException ioe) { } } } } /** * 删除文件 * @param host * @param port * @param username * @param password * @param basePath * @param filePath * @return */ public static boolean deleteFile(String host, String port, String username, String password, String basePath, String filePath,String fileName) throws Exception{ FTPClient ftp = new FTPClient(); boolean b = false; try { boolean result = connectFtp(ftp, host, port, username, password, basePath, filePath); if(!result){ return b; } b = ftp.deleteFile(fileName); ftp.logout(); } catch (IOException e) { LOGGER.info(e.getMessage()); e.printStackTrace(); throw e; } finally { if (ftp.isConnected()) { try { ftp.disconnect(); } catch (IOException ioe) { } } } return b; } /** * 连接ftp服务器并切换到目的目录 * 调用此方法需手动关闭ftp连接 * @param ftp * @param host * @param port * @param username * @param password * @param basePath * @param filePath * @return */ private static boolean connectFtp( FTPClient ftp,String host, String port, String username, String password, String basePath, String filePath) throws Exception{ boolean result = false; try { int portNum = Integer.parseInt(port); int reply; // 连接FTP服务器 ftp.connect(host, portNum); // 如果采用默认端口,可以使用ftp.connect(host)的方式直接连接FTP服务器 ftp.login(username, password); reply = ftp.getReplyCode(); if (!FTPReply.isPositiveCompletion(reply)) { ftp.disconnect(); return result; } // 开启服务器对UTF-8的支持,如果服务器支持就用UTF-8编码,否则就使用本地编码(GBK). if (FTPReply.isPositiveCompletion(ftp.sendCommand("OPTS UTF8", "ON"))) { LOCAL_CHARSET = "UTF-8"; } ftp.setControlEncoding(LOCAL_CHARSET); //切换到上传目录 if (!ftp.changeWorkingDirectory(basePath+filePath)) { //如果目录不存在创建目录 String[] dirs = filePath.split("/"); String tempPath = basePath; for (String dir : dirs) { if (null == dir || "".equals(dir)) { continue; } tempPath += "/" + dir; if (!ftp.changeWorkingDirectory(tempPath)) { if (!ftp.makeDirectory(tempPath)) { return result; } else { ftp.changeWorkingDirectory(tempPath); } } } } result = true; } catch (IOException e) { LOGGER.info(e.getMessage()); e.printStackTrace(); throw e; } return result; } }
拦截器权限校验这边涉及到太多业务,读者可止步于此,但我还是要贴一下代码
配置拦截器: FilterManager

/** * @Auther: 011336 * @Date: 2019/3/28 19:17 */ @Configuration public class FilterManager implements WebMvcConfigurer { @Autowired private AuthorityInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry){ registry.addInterceptor(loginInterceptor) .addPathPatterns("/**"); } }
拦截器具体实现: AuthorityInterceptor

/** * 拦截器做权限验证 * 权限信息接SI接口管理系统,调用者需提供白名单用户名,和白名单密码(MD5加密) * 本服务根据调用者在SI中的项目编码projectCode为调用者创建独立目录地址 * requestUri 接口地址 * onlineInterfaceCode 接口线上编码 * 从SI同步而来的白名单信息包含此接口的线上接口编码onlineInterfaceCode,本项目维护了onlineInterfaceCode与requestUri之间的对应关系 * 目的:使系统兼容提供多个接口的情况 * @Auther: 011336 * @Date: 2019/4/28 19:11 */ @Component public class AuthorityInterceptor implements HandlerInterceptor { private static Logger LOGGER = LoggerFactory.getLogger(AuthorityInterceptor.class); @Autowired private IWhiteListService whiteListService; /** * 拦截器做权限验证 * @param request * @param response * @param handler * @return * @author 011336 * @date 2019/04/24 * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{ String whiteListUsername = request.getHeader("whiteListUsername"); String whiteListPassword = request.getHeader("whiteListPassword"); String projectCode = request.getHeader("projectCode"); if(StringUtils.isNotEmpty(projectCode)){ Optional<String> userName = Optional.ofNullable(whiteListUsername); Optional<String> passWord = Optional.ofNullable(whiteListPassword); String requestUri = request.getRequestURI(); Map<String,Object> condition = new HashMap<>(); condition.put("requestUri",requestUri); condition.put("projectCode",projectCode); WhiteList whiteList = whiteListService.query(condition); Optional<WhiteList> whiteOptional = Optional.ofNullable(whiteList); WhiteList white = whiteOptional.orElse(new WhiteList()); if(userName.orElse(UUID.randomUUID().toString()).equals(white.getWhiteListUsername()) && passWord.orElse(UUID.randomUUID().toString()).equals(white.getWhiteListPassword())) { LOGGER.info("["+projectCode+"]权限认证成功!"); return true; } } //重置response response.reset(); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json;charset=UTF-8"); PrintWriter pw = response.getWriter(); VsftpResult result = new VsftpResult(); result.setStatus(false); ErrorInfo errorInfo = new ErrorInfo("AUTHORITY.FAIL", "权限验证失败!"); List<ErrorInfo> errors = new ArrayList<>(); errors.add(errorInfo); result.setErrors(errors); pw.write(JsonUtils.objectToJson(result)); pw.flush(); pw.close(); LOGGER.info("["+projectCode+"]权限认证失败!"+ JsonUtils.objectToJson(result)); return false; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,ModelAndView model) throws Exception{ } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,Exception ex) throws Exception{ } }
现这样吧...心情不好