文件上传
大多数的 Web 应用都不可避免的,会涉及到文件上传。文件上传,不过是一种适配 HTTP 输入流的方式。
为此,Nutz.Mvc 内置了一个专门处理文件上传的适配器 org.nutz.mvc.upload.UploadAdaptor
这个适配器专门解析形式为
<form target="hideWin" enctype="multipart/form-data" method="post">
<input type="file" name="photo">
...
的 HTML 提交表单
这个适配器的工作细节是这样的:
- 它一次将 HTTP 输入流中所有的文件读入,保存在临时文件目录里
- 表单项目会保存在内存里
- 在上传的过程中,它会向当前 Session 中设置一个对象: org.nutz.mvc.upload.UploadInfo
- 属性名为 "org.nutz.mvc.upload.UploadInfo"
- 通过静态函数 Uploads.getInfo(HttpServletRequest req) 可以很方便的获取当前会话的 UploadInfo
- 不断的读入输入流的过程,会记录在 UploadInfo 里面。
- UploadInfo 字段 sum 是当前 HTTP 请求的 ContentLength,表示 HTTP 输入流总长度为多少字节
- UploadInfo 字段 current 是当前会话已经上传了多少字节
- 如果使用ajax进行文件上传,请使用webuploader等js插件.
如何使用
如果你读过 适配器 一节,我想你已经知道怎么使用文件上传了,这里还需要多一点说明
通过 @AdaptBy 声明
...
@AdaptBy(type = UploadAdaptor.class, args = { "${app.root}/WEB-INF/tmp" })
public void uploadPhoto(@Param("id") int id, @Param("photo") File f){
...
Nutz.Mvc 会为你的入口函数创建一个上传适配器,上传适配器的会将临时文件存放在
WEB-INF/tmp 目录下。 请为不同的入口方法分配不同的临时文件夹路径
请为不同的入口方法分配不同的临时文件路径*哦
app.root代表web项目的根路径, 也就是servletContext.getRealPath("/")
所返回的值
更细腻的控制
更多的时候,你想控制
- 上传文件的临时文件数量
- 可以上传的单个文件最大尺寸
- 文件的类型
- 空文件跳过不保存
那么你需要把上传适配器交由 Ioc 容器来管理,首先你的入口函数需要这么写:
...
@AdaptBy(type = UploadAdaptor.class, args = { "ioc:myUpload" })
public void uploadPhoto(@Param("id") int id, @Param("photo") File f){
...
请注意这里的 "ioc:myUpload",Nutz.Mvc 发现你的参数为此形式时,它会从 Ioc 容器里获取你的
上传适配器的实例(名字为myUpload),而不是直接 new 出来, 而且必须为singleton=false。
然后你需要在你的 Ioc 配置文件里 (以 Json 配置为例子)配置你的 UploadAdaptor, 你需要增加
下面这三个对象:
...
tmpFilePool : {
type : 'org.nutz.filepool.NutFilePool',
// 临时文件最大个数为 1000 个
args : [ "~/nutz/blog/upload/tmps", 1000 ]
},
uploadFileContext : {
type : 'org.nutz.mvc.upload.UploadingContext',
singleton : false,
args : [ { refer : 'tmpFilePool' } ],
fields : {
// 是否忽略空文件, 默认为 false
ignoreNull : true,
// 单个文件最大尺寸(大约的值,单位为字节,即 1048576 为 1M)
maxFileSize : 1048576,
// 正则表达式匹配可以支持的文件名
nameFilter : '^(.+[.])(gif|jpg|png)
这样,在配置文件中,你就可以随心所欲的进行控制了。而且这么写仅仅多写了不到 20 行的配置文件,
到也没有特别麻烦,对吗?(相关详细规则描述:http_adaptor.man#通过_Ioc_容器获得适配器)
特别注意 有的同学使用文件上传遇到了
诡异的问题
如果你要用 Ioc 来管理 UploadAdaptor,你必须为不同的入口函数分配不同的实例。
当然这些实例可以引用同样的 uploadFileContext
和 tmpFilePool
。
否则我们不能保证适配的结果正确。
看到这里,有些同学会问了,为什么你用注解的方式可以这么写:
@AdaptBy(type = UploadAdaptor.class, args = { "${app.root}/WEB-INF/tmp" })
'`{app.root}' 用起来多方便啊,那么在 Ioc 容器来,我的临时目录还想放在这里,有没有
什么办法啊?
办法有。但是因为 Ioc 容器原来设计的是同 Mvc 的 Web 容器环境不相干的,所以要想绕过了这点
限制,会稍微的有一点麻烦。
首先,因为 Ioc 容器设计的良好扩展性,所有实现了 'org.nutz.ioc.Ioc2' 这个接口的 Ioc
容器(当然,Nutz 自带的 Ioc 容器 - NutzIoc,很好的实现了 Ioc2 这个接口)都可以被 Nutz.Mvc
注入一种新的获取值的方式(详情请参看 with_ioc.man#在容器对象里获得_ServletContext),
我想你只需要实现一个简单的 Java 对象即可:
package com.abc.myapp;
import javax.servlet.ServletContext;
public class MyUtils {
private ServletContext sc;
public String getPath(String path) {
return sc.getRealPath(path);
}
}
然后,你可以在 Ioc 容器的配置文件里,增一个对象,并修改 'tmpFilePool' 对象的配置信息:
utils : {
type : 'com.abc.myapp.MyUtils',
fields : {
sc : {app:'$servlet'} // 将 ServletContext 对象注入 MyUtils
}
},
tmpFilePool : {
type : 'org.nutz.filepool.NutFilePool',
args : [ {java:'$utils.getPath("/WEB-INF/tmp")'}, 1000 ] // 调用 MyUtils.getPath 函数
},
在入口函数的参数里获得上传的文件
因为 FORM 表单里的 input type=file 项都已经被 UploadAdaptor 解析并存入服务器的临时目录
(你声明的那个目录,比如上例是 /WEB-INF/tmp),你在入口函数里可以直接获取:
@AdaptBy(type = UploadAdaptor.class, args = { "ioc:myUpload" })
public void uploadPhoto( @Param("photo") File f){
...
你的参数 f 将获取到表单项中:
<form target="hideWin" enctype="multipart/form-data" method="post">
<input type="file" name="photo">
...
中的文件内容。你直接打开一个 InputStream 就能从这个 File 中读取客户端上传的文件内容了。
但是,如果你也想知道这个文件在客户端原本的名字,怎么办呢?你可以:
@AdaptBy(type = UploadAdaptor.class, args = { "ioc:myUpload" })
public void uploadPhoto( @Param("photo") TempFile tf){
File f = tf.getFile(); // 这个是保存的临时文件
FieldMeta meta = tf.getMeta(); // 这个原本的文件信息
String oldName = meta.getFileLocalName(); // 这个时原本的文件名称
// TODO do what you wanna to do here ...
}
一次上传多个同名文件
相信很多同学都会有这样的用法,就是,希望一次上传的表单为:
<form target="hideWin" enctype="multipart/form-data" method="post">
<input type="file" name="photo">
<input type="file" name="photo">
<input type="file" name="photo">
...
看,一个上传表单中带有多个同名文件,那么怎么办呢? 你在入口函数里可以这么声明:
@AdaptBy(type = UploadAdaptor.class, args = { "ioc:myUpload" })
public void uploadPhoto( @Param("photo") TempFile[] tfs){
...
}
是的,它们会被变成一个数组。顺序同 <form>
中的顺序。
但是这里,有一点限制
- 你只能用 TempFile`[`]
也就是说,如果你用了 File[]
将会出错。
可用的参数类型
- TempFile 及 TempFile
[
]
- File 推荐用TempFile
- Map 用于得到全部参数
AJAX 方式上传文件
现在的浏览器配合 jQuery 的话是很方便上传文件的,例子如下,省略 html 代码
<script>
$(document).ready(function () {
$("#upload").click(function () {
var formData = new FormData();
formData.append('file', $('#file')[0].files[0]);
$.ajax({
url: "/upload",
method: 'POST',
data: formData,
processData: false,
contentType: false,
success: function (data) {
console.log("success");
}
});
});
});
</script>
@At("/upload")
@AdaptBy(type = UploadAdaptor.class, args = {"${app.root}/WEB-INF/tmp"})
public void uploadPhoto(@Param("file") File f) {
System.out.println(f.getName());
}
其实这就是在画面生成一个 form 对象,然后用 AJAX POST 方式来提交这个 form。是不是一下让你想到远古时期用 js 提交 form 表单的做法呢。
推荐的js插件
- webuploader 百毒出品,体验还是不错的
- jQuery-Ajax-Upload 老牌ajax上传插件
直接上传到云
以七牛云存储为例, 使用其js-sdk及java-sdk
其中,java-sdk用于提供uptoken的入口方法
@Ok("json:full")
@At
public NutMap uptoken() {
String accessKey = conf.get("qiniu.accessKey");
String secretKey = conf.get("qiniu.secretKey");
String bucketName = conf.get("qiniu.bucketName");
Auth auth = Auth.create(accessKey, secretKey);
//StringMap policy = new StringMap(); // 可选,详情请查阅七牛的文档,建议限定上传路径
StringMap policy = null;
int timeout = 600;
String token = auth.uploadToken(bucketName, null, timeout, policy);
return new NutMap("uptoken", token);
}
供参考的js代码
var uploader = Qiniu.uploader({
runtimes : 'html5,flash,html4', // 上传模式,依次退化
browse_button : 'pickfiles', // 上传选择的点选按钮,必需
uptoken_url : base + '/upload/uptoken', // Ajax请求uptoken的Url,强烈建议设置(服务端提供)
get_new_uptoken : true, // 设置上传文件的时候是否每次都重新获取新的uptoken
max_file_size : '128mb', // 最大文件体积限制
max_retries : 3, // 上传失败最大重试次数
// dragdrop: true, // 开启可拖曳上传
// drop_element: 'container', // 拖曳上传区域元素的ID,拖曳文件或文件夹后可触发上传
chunk_size : '4mb', // 分块上传时,每块的体积
auto_start : true, // 选择文件后自动上传,若关闭需要自己绑定事件触发上传
unique_names : false,
domain : dw_domain,
init : {
'FileUploaded' : function(up, file, info) {
$.ajax({
url : base + "/upload/add",
data : info,
type : "POST",
success : function() {
layer.alert("上传成功");
}
});
}
}
});
}
},
myUpload : {
type : 'org.nutz.mvc.upload.UploadAdaptor',
singleton : false,
args : [ { refer : 'uploadFileContext' } ]
}
...
这样,在配置文件中,你就可以随心所欲的进行控制了。而且这么写仅仅多写了不到 20 行的配置文件,
到也没有特别麻烦,对吗?(相关详细规则描述:http_adaptor.man#通过_Ioc_容器获得适配器)
特别注意 有的同学使用文件上传遇到了
诡异的问题
如果你要用 Ioc 来管理 UploadAdaptor,你必须为不同的入口函数分配不同的实例。
当然这些实例可以引用同样的 uploadFileContext
和 tmpFilePool
。
否则我们不能保证适配的结果正确。
看到这里,有些同学会问了,为什么你用注解的方式可以这么写:
'`{app.root}' 用起来多方便啊,那么在 Ioc 容器来,我的临时目录还想放在这里,有没有
什么办法啊?
办法有。但是因为 Ioc 容器原来设计的是同 Mvc 的 Web 容器环境不相干的,所以要想绕过了这点
限制,会稍微的有一点麻烦。
首先,因为 Ioc 容器设计的良好扩展性,所有实现了 'org.nutz.ioc.Ioc2' 这个接口的 Ioc
容器(当然,Nutz 自带的 Ioc 容器 - NutzIoc,很好的实现了 Ioc2 这个接口)都可以被 Nutz.Mvc
注入一种新的获取值的方式(详情请参看 with_ioc.man#在容器对象里获得_ServletContext),
我想你只需要实现一个简单的 Java 对象即可:
然后,你可以在 Ioc 容器的配置文件里,增一个对象,并修改 'tmpFilePool' 对象的配置信息:
在入口函数的参数里获得上传的文件
因为 FORM 表单里的 input type=file 项都已经被 UploadAdaptor 解析并存入服务器的临时目录
(你声明的那个目录,比如上例是 /WEB-INF/tmp),你在入口函数里可以直接获取:
你的参数 f 将获取到表单项中:
中的文件内容。你直接打开一个 InputStream 就能从这个 File 中读取客户端上传的文件内容了。
但是,如果你也想知道这个文件在客户端原本的名字,怎么办呢?你可以:
一次上传多个同名文件
相信很多同学都会有这样的用法,就是,希望一次上传的表单为:
看,一个上传表单中带有多个同名文件,那么怎么办呢? 你在入口函数里可以这么声明:
是的,它们会被变成一个数组。顺序同 <form>
中的顺序。
但是这里,有一点限制
- 你只能用 TempFile`[`]
也就是说,如果你用了 File[]
将会出错。
可用的参数类型
- TempFile 及 TempFile
[
] - File 推荐用TempFile
- Map 用于得到全部参数
AJAX 方式上传文件
现在的浏览器配合 jQuery 的话是很方便上传文件的,例子如下,省略 html 代码
其实这就是在画面生成一个 form 对象,然后用 AJAX POST 方式来提交这个 form。是不是一下让你想到远古时期用 js 提交 form 表单的做法呢。
推荐的js插件
- webuploader 百毒出品,体验还是不错的
- jQuery-Ajax-Upload 老牌ajax上传插件
直接上传到云
以七牛云存储为例, 使用其js-sdk及java-sdk
其中,java-sdk用于提供uptoken的入口方法
供参考的js代码