- 现文件上传功能
(1)、建立一个spring boot的工程
单击“Next”,进入工程命名对话框。
单击“Next”,进入工程依赖配置对话框。
选中Spring Web。单击“Next”,进入工程目录设置对话框。
根据自己实际情况将工程保存到某个目录下。单击“Finish”。
(2)、配置POM
手动添加下面的依赖。
<!-- 渲染前端页面 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 文件上传 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
thymeleaf是页面渲染框架,因为要写一个表单提交来上传文件。文件上传功能利用apache的commons-fileupload组件,因此要添加它已经它自身的依赖。commons-fileupload组件可以很方便的实现上传进度的监控。
(3)、实现上传功能
建立进度监听类SgFileUploaderListener。
@Component
public class SgFileUploaderListener implements ProgressListener {
private HttpSession session;
public SgFileUploaderListener() {
}
public void setSession(HttpSession session) {
this.session = session;
session.setAttribute("uploadPercent", 0);
}
public void update(long pBytesRead, long pContentLength, int pItems) {
int percent = (int)((double)pBytesRead * 100.0D / (double)pContentLength);
this.session.setAttribute("uploadPercent", percent);
}
}
可以看出,文件上传的进度是依靠session来保存和读取的。组件底层会根据上传进程自动调用update函数,而update函数将当前文件的进度值计算出来保存在uploadPercent这个session对象里。
实现含监听进度的自定义CommonsMultipartResolver类
public class SgFileUploaderMultipartResolver extends CommonsMultipartResolver {
@Autowired
private SgFileUploaderListener listener;
public SgFileUploaderMultipartResolver() {
}
protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
String encoding = this.determineEncoding(request);
FileUpload fileUpload = this.prepareFileUpload(encoding);
fileUpload.setProgressListener(this.listener);
this.listener.setSession(request.getSession());
try {
List fileItems = ((ServletFileUpload)fileUpload).parseRequest(request);
return this.parseFileItems(fileItems, encoding);
} catch (FileUploadException var5) {
throw new MultipartException("Failed to parse multipart servlet request", var5);
}
}
}
Spring boot自身的CommonsMultipartResolver就是用来处理文件上传的,它自身没有进度监听功能,因此我们派生一个子类,然后在parseRequest方法里假如自己的监听对象,从而实现文件上传进度的监视。
文件上传类SgFileUploader
这个类实现2个核心功能,一是处理文件上传,一是获取进度。
public Map<String, Object> upload(MultipartFile imgFile,
HttpServletRequest request,String fileType, String dir,
String fileName) throws IOException {
Map<String, Object> mapRet = new HashMap<String, Object>();
ServletContext context=request.getSession().getServletContext();
String pageCtx = filePath; // 保存上传文件的总路径
String relPath = context.getRealPath(filePath);
if (null == imgFile) {
mapRet.put("code", 1);
mapRet.put("message", "上传的文件无效.");
return mapRet;
}
StringBuilder tempPath = new StringBuilder(relPath);
tempPath.append(dir);
String saveDir = tempPath.toString();
File folder = new File(saveDir);
if(!folder.exists()) {
folder.mkdirs();
}
if(!folder.isDirectory() || !folder.canWrite()) {
mapRet.put("code", 2);
mapRet.put("message", "文件夹不存在或者没有读写权限.");
return mapRet;
}
String fl = imgFile.getOriginalFilename();
String fileExt = fl.substring(fl.lastIndexOf(".")+1);
if(!Arrays.<String>asList(extMap.get(fileType).split(",")).contains(fileExt)){
mapRet.put("error", 4);
mapRet.put("message", "不允许上传的文件格式:" + extMap.get(fileType));
return mapRet;
}
long maxSize = sizeMap.get(fileType);
if (imgFile.getSize() > maxSize) {
mapRet.put("code", 5);
String size = null;
if(maxSize < 1024) {
size = maxSize + "B";
}
if(maxSize > 1024 && maxSize < 1024 * 1024){
size = maxSize/1024 + "KB";
}
if(maxSize > 1024 * 1024){
size = maxSize/(1024 * 1024) + "MB";
}
mapRet.put("message", "文件尺寸太大: " + size);
return mapRet;
}
// 如果没有指定文件名,则按随机命名
if (fileName == null || fileName == "") {
UUID uuid = UUID.randomUUID();
fileName = uuid.toString();
fileName = fileName.replace("-", "");
}
tempPath.append("/").append(fileName).append(".").append(fileExt);
String savePath = tempPath.toString().replaceAll("\\\\",
"/");
// 读取上传数据流,并上传
InputStream src = imgFile.getInputStream();
OutputStream tarOs = null;
try {
File tarFile = new File(savePath);
tarOs = new FileOutputStream(tarFile);
byte[] buffer = new byte[4096];
int n = 0;
long nRead = 0;
long totalSize = imgFile.getSize();
DecimalFormat df = new DecimalFormat("######0.00");
while (-1 != (n = src.read(buffer))) {
nRead += n;
double dRate = (double)nRead / totalSize * 100;
String rate = df.format(dRate);
tarOs.write(buffer, 0, n);
}
} catch (IOException e) {
mapRet.put("code", 6);
mapRet.put("message", "Copy File is Fali, Because "+e);
} finally {
try {
if (null != src) {
src.close();
}
if (null != tarOs) {
tarOs.close();
}
} catch (IOException e) {
mapRet.put("code", 7);
mapRet.put("message", "Close Stream is Fail, Because "+e);
}
}
// 返回上传文件的完整路径
StringBuilder url = new StringBuilder(pageCtx);
url.append(dir).append("/").append(fileName).append(".").append(fileExt);
mapRet.put("code", 0); // 返回0表示没有错误
mapRet.put("url", url.toString());
return mapRet;
}
这里fileType参数指明了文件类型,不同的类型可以限制不同的上传文件大小。dir参数指明文件保存的子文件夹。
public String getProgress(HttpServletRequest request){
HttpSession se = request.getSession();
Object o = se.getAttribute("uploadPercent");
String rateRet = "";
if (o != null) {
rateRet = o.toString();
}
return rateRet;
}
获取进度就是读取uploadPercent这个session值。
(4)、测试上传功能
在templates目录下建立html测试页面upload.html,并建立一个controller来测试上传类的接口。
TestController类代码如下:
@Controller
@RequestMapping(value = "/main")
public class TestController {
@RequestMapping("/test")
public String test(){
return "upload";
}
@RequestMapping("/upload")
@ResponseBody
public Map<String, Object> upload(MultipartFile file, HttpServletRequest request,
String fileType, String dir) {
Map<String, Object> mapRet = new HashMap<String, Object>();
SgFileUploader sfu = new SgFileUploader();
try {
mapRet = sfu.upload(file, request, fileType, dir, "");
}
catch (Exception e){
mapRet.put("code", -1);
mapRet.put("message", "上传文件失败:" + e.toString());
e.printStackTrace();
}
return mapRet;
}
}
建立配置类SgUploaderMultipartConfig,告诉spring boot,处理表单交给我们自定义的SgFileUploaderMultipartResolver来处理。
@Configuration
public class SgUploaderMultipartConfig {
@Bean(name = "multipartResolver")
public MultipartResolver multipartResolver()
{
return new SgFileUploaderMultipartResolver();
}
}
为了限制总的上传文件的大小,在程序启动类SguploaderTestApplication添加配置函数。
@SpringBootApplication(scanBasePackages = "com")
public class SguploaderTestApplication {
public static void main(String[] args) {
SpringApplication.run(SguploaderTestApplication.class, args);
}
/**
* 文件上传配置
* @return
*/
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
//设置内存阀值,超过后写入临时文件
factory.setFileSizeThreshold(DataSize.parse("10240KB")); // 10M
//文件最大
factory.setMaxFileSize(DataSize.parse("102400KB")); //100MB
/// 设置总上传数据总大小
factory.setMaxRequestSize(DataSize.parse("1024000KB")); // 1G
return factory.createMultipartConfig();
}
}
注意,如果启动工程找不到某些类,写上scanBasePackages = "com",这样凡是com下的包都可以扫描到。
启动测试接口,测试文件上传。
2、将文件上传相关类导出到jar包
为了让别的项目能更加简洁方便的调用上传功能,现在将相关类导出到一个jar包,这里以idea为例。
先编译下工程。如果没有错误,则单击File->Project Structure。单击Artifacts,再单击三米高的+号,新建一个空的jar包。
然后取号要导出的包的名称,并按包的文件夹层次关系建立好相关目录。
目录层次要和包的目录层次一样,方法就是单击中间工具栏的第一个建立文件夹图标。如果目录层次不一样,导出去的jar包被别人调用时可能会出现找不到类的情况。
然后单击utils目录,再单击带下拉箭头的+号按钮,将经过编译的相关类文件(扩展名为class)都添加到相应目录。
Idea工程编译后会生成target目录,因此去target目录下找。
添加好后,如下图:
然后选中jar包文件,点“Create Mainifest…”创建jar包的部署文件。在弹出的对话框里选中一个文件夹,我这里还是选定的target文件夹下的utils文件夹,和class文件一起。
然后单击“Apply”,再单击”OK”。进入到电脑资源管理器刚才选定的目录,手动编辑MANIFEST.MF文件并保存。
Premain-Class就是包的路径。
然后通过菜单Bulid-》Build Artifacts…,弹出编译jar菜单窗口。
选中要打包的jar包名,选中子菜单Build进行打包。打包成功后,默认是在工程目录的out子目录下。
3、建立测试工程调用jar包
新建测试工程sg-file-uploader-test。和建立工程sg-file-uploader类似,也是基于spring boot创建,pom文件,配置类SgUploaderMultipartConfig均一样。
(1)、导入jar包
在工程的src目录下创建lib子目录。将上个工程导出的jar包拷贝进来。然后再pom文件里添加自己的jar包依赖。
<!-- 自己封装的jar包 -->
<dependency>
<groupId>com.wave12</groupId>
<artifactId>sguploader</artifactId>
<version>1.1.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/src/lib/sg-file-uploader-1.1.0.jar</systemPath>
</dependency>
(2)、测试调用jar包功能
效果和上面的工程一样。可以看到,在测试工程的src文件夹,已经看不到utils子包,即上传功能的代码不用呈现在工程代码里,因为它已经封装在一个jar包里,只需要在pom文件里添加好依赖即可。然后在需要调用jar包功能的地方,写好import语句,就可以调用其中的类和方法了。
本教程的完整代码,可以参考这里:
http://kbase.wave12.com/java/doc/detail?id=5d4d29df0d4f4dfa8558b33a9b11795a
封装jar包的目的就是使得现有工程的代码架构更加简洁,调用者也不会不小心改动到jar包里的代码,而调用不受到影响。在企业里,很多公司都会把一些重用度非常高的代码封装成jar包,在多个项目里调用。