前言:
在我另一篇笔记中已经记载了如何用nginx + vsftp搭建图片服务器(请参考nginx + vsftp搭建图片服务器),并且用vsftp的客户端工具filezilla测试过已经可用。但是在开发中应该是把用户在前端页面提交的图片保存到图片服务器中,接下来就来实现这个功能。
需求:
用户在页面中上传一张图片,把图片保存到图片服务器,把图片的url保存到user表中,复制user表中的图片url在浏览器中可访问到用户上传的图片。
功能实现:
一、数据库设计:
图片发自简书App
二、项目设计:
为了快速开发,本案例使用springboot + mybatis实现。项目结构如下:
图片发自简书App
1、添加依赖:
pom.xml:
<!-- 文件上传 --> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.4</version> </dependency> <dependency> <groupId>commons-net</groupId> <artifactId>commons-net</artifactId> <version>3.3</version> </dependency> <!-- 时间操作组件 --> <dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> <version>2.5</version> </dependency><!-- mybaties --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <!-- mysql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
2、配置:
application.properties:
注意,下面的basepath配置的是文件上传的根路径,/home/ftpuser/images
,图片都传到这个目录或其子目录下,baseUrl是访问图片时的基础Url,因为在搭建图片服务器时我们设置了访问根目录是/home/ftpuser
,所以访问的基础url就是192.168.xx.xxx/images
#配置数据库连接信息spring.datasource.url=jdbc:mysql:///db_demo?useUnicode=true&characterEncoding=utf8 spring.datasource.username=#spring.datasource.password=#spring.datasource.driver-class-name=com.mysql.jdbc.Driver#Mybatis扫描接口对应的xml文件mybatis.mapper-locations=classpath:mappers/*.xml#起别名。可省略写mybatis的xml中的resultType的全路径 mybatis.type-aliases-package=com.zhu.pojo#配置文件上传器spring.http.multipart.maxFileSize=100Mb spring.http.multipart.maxRequestSize=100Mb#ftp相关配置FTP.ADDRESS=192.168.xx.xxx FTP.PORT=21 FTP.USERNAME=ftpuser FTP.PASSWORD=ftpuser FTP.BASEPATH=/home/ftpuser/images#图片服务器相关配置IMAGE.BASE.URL=http://192.168.xx.xxx/images#视图解析器spring.mvc.view.prefix=/pages/ spring.mvc.view.suffix=.html
3、实体类:
省略set和get方法。
public class User { private Integer uid; private String username; private String password; private String picture; }
4、dao层:
UserMapper.java
public interface UserMapper { int insert(User record); }
UserMapper.xml
<insert id="insert" parameterType="com.zhu.pojo.User"> insert into user (uid, username, password, picture) values (#{uid,jdbcType=INTEGER}, #{username,jdbcType=CHAR}, #{password,jdbcType=CHAR}, #{picture,jdbcType=CHAR}) </insert>
5、service层:
service层只是简单的调用dao层,保存user对象。
@Servicepublic class UserServiceImpl implements UserService{ @Autowired private UserMapper userMapper; @Override public void insertUser(User user) { userMapper.insert(user); } }
6、utils:
①、FtpUtils.java:
我们知道filezilla是vsftp客户端工具,输入ip、端口、vsftp用户的用户名和密码就可以连接上服务。那么在Java中,我们就new一个客户端,除了需要传入以上四个值外,还需要基础目录、文件存放路径和文件io流。还有一点特别注意,一定要加上ftp.enterLocalPassiveMode()
设置被动模式,否则的话会出现图片传到服务器上去了,但是大小一直是0。这个方法的意思就是每次数据连接之前,ftp client告诉ftp server开通一个端口来传输数据。为什么要这样做呢,因为ftp server可能每次开启不同的端口来传输数据,但是在linux上或者其他服务器上面,由于安全限制,可能某些端口没有开启,所以就出现阻塞。
public class FtpUtil { /** * Description: 向FTP服务器上传文件 * @param host FTP服务器ip * @param port FTP服务器端口 * @param username FTP登录账号 * @param password FTP登录密码 * @param basePath FTP服务器基础目录,/home/ftpuser/images * @param filePath FTP服务器文件存放路径。例如分日期存放:/2018/05/28。文件的路径为basePath+filePath * @param filename 上传到FTP服务器上的文件名 * @param input 输入流 * @return 成功返回true,否则返回false */ public static boolean uploadFile(String host, int port, String username, String password, String basePath, String filePath, String filename, InputStream input) { boolean result = false; FTPClient ftp = new FTPClient(); try { int reply; ftp.connect(host, port);// 连接FTP服务器 // 如果采用默认端口,可以使用ftp.connect(host)的方式直接连接FTP服务器 ftp.login(username, password);// 登录 reply = ftp.getReplyCode(); if (!FTPReply.isPositiveCompletion(reply)) { ftp.disconnect(); return result; } //切换到上传目录 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); } } } } //设置为被动模式 ftp.enterLocalPassiveMode(); //设置上传文件的类型为二进制类型 ftp.setFileType(FTP.BINARY_FILE_TYPE); //上传文件 if (!ftp.storeFile(filename, input)) { return result; } input.close(); ftp.logout(); result = true; } catch (IOException e) { e.printStackTrace(); } finally { if (ftp.isConnected()) { try { ftp.disconnect(); } catch (IOException ioe) { } } } return result; } }
②、IDUtils.java:
这个工具类是用来生成随机的文件名。因为用户上传的时候,可能文件名是一致的,比如A用户上传了名为a.jpg的图片,B用户也上传了名为a.jpg的图片的话就会上传失败,会提示文件已存在。所以需要生成一个随机的文件名保证不重名。
public class IDUtils { /** * 生成随机图片名 */ public static String genImageName() { //取当前时间的长整形值包含毫秒 long millis = System.currentTimeMillis(); //long millis = System.nanoTime(); //加上三位随机数 Random random = new Random(); int end3 = random.nextInt(999); //如果不足三位前面补0 String str = millis + String.format("%03d", end3); return str; } }
7、controller:
在springmvc中,前端页面提交的图片信息会自动封装在MultipartFile对象中,在这个controller中的通过MultipartFile对象获取图片本来的文件名,然后截取后缀,用工具类生成新的文件名,再把后缀拼接上,然后通过@Value注解获取application.properties中配置的ftp相关的配置的值,调用ftp工具类进行图片的上传,调用service把用户信息保存到数据库。
@Controllerpublic class UserContrller { @Value("${FTP.ADDRESS}") private String host; // 端口 @Value("${FTP.PORT}") private int port; // ftp用户名 @Value("${FTP.USERNAME}") private String userName; // ftp用户密码 @Value("${FTP.PASSWORD}") private String passWord; // 文件在服务器端保存的主目录 @Value("${FTP.BASEPATH}") private String basePath; // 访问图片时的基础url @Value("${IMAGE.BASE.URL}") private String baseUrl; @Autowired private UserService userService; @RequestMapping("/pic/upload") @ResponseBody public String pictureUpload( @RequestParam("pic") MultipartFile uploadFile, @RequestParam("username")String username, @RequestParam("password") String password) { try { //1、给上传的图片生成新的文件名 //1.1获取原始文件名 String oldName = uploadFile.getOriginalFilename(); //1.2使用IDUtils工具类生成新的文件名,新文件名 = newName + 文件后缀 String newName = IDUtils.genImageName(); newName = newName + oldName.substring(oldName.lastIndexOf(".")); //1.3生成文件在服务器端存储的子目录 String filePath = new DateTime().toString("/yyyy/MM/dd"); //2、把前端输入信息,包括图片的url保存到数据库 User user = new User(); user.setUsername(username); user.setPassword(password); user.setPicture(baseUrl + filePath + "/" + newName); userService.insertUser(user); //3、把图片上传到图片服务器 //3.1获取上传的io流 InputStream input = uploadFile.getInputStream(); //3.2调用FtpUtil工具类进行上传 boolean result = FtpUtil.uploadFile(host, port, userName, passWord, basePath, filePath, newName, input); if(result) { return "success"; }else { return "false"; } } catch (IOException e) { return "false"; } } }
8、index.html:
这个页面只有一点需要注意,那就是form中需要加上enctype="multipart/form-data"
,不然controller中通过MultipartFile获取不到图片信息。
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Insert title here</title></head><body> <form action="/pic/upload" enctype="multipart/form-data" method="post"> <input type="text" name="username"><br> <input type="password" name="password"><br> <input type="file" name="pic"><br> <input type="submit" value="提交"> </form></body></html>
9、测试:
①:先用filezilla连上vsftp,看看上传之前的目录结构:
图片发自简书App
②:运行项目,上传文件:
图片发自简书App
图片发自简书App
③:返回了success,再到filezilla中刷新一下,看看图片是否成功上传到服务器:
图片发自简书App
④:已经上传成功了。再看看数据表中的信息:
图片发自简书App
⑤:再复制数据表中保存的图片url到浏览器中。看看能否访问到图片:
图片发自简书App
成功访问到了刚才上传的图片!
总结:
1、过程梳理:
先搭建起项目,在html页面中通过<input type="file">
上传文件,在controller中通过MultipartFile对象接收图片信息,然后获取原文件名,调用IDUtis工具类生成新的文件名,调用joda-time时间组件获取当前时间作为图片在服务器端保存的目录,然后用@Value读取在application.properties中的配置信息,拼接出图片的url,调用service保存到数据库中。最后调用ftp工具类,new了一个ftp的客户端,传入相关参数,把图片上传到图片服务器。
2、避坑说明:
在上面已经说过了,在ftp工具类中,一定要加上ftp.enterLocalPassiveMode()
设置被动模式,不然上传到服务器的就是空文件,大小一直是0字节。
作者:贪挽懒月
链接:https://www.jianshu.com/p/67fc9148f95a