主要代码
- HomeActivity.java
代码如下:
package com.itau.jingdong.ui;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.TabActivity;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.RadioGroup;
import android.widget.RelativeLayout;
import android.widget.TabHost;
import android.widget.RadioGroup.OnCheckedChangeListener;
import com.itau.jingdong.AppManager;
import com.itau.jingdong.R;
import com.nostra13.universalimageloader.core.ImageLoader;
//主页面
public class HomeActivity extends TabActivity {
//打印日志标记
public static final String TAG = HomeActivity.class.getSimpleName();
//定义一个单选按钮组 用于切换底部选项栏
private RadioGroup mTabButtonGroup;
private TabHost mTabHost;//选项卡组件
//定义选项卡对应的类名
public static final String TAB_MAIN = “MAIN_ACTIVITY”;//主页
public static final String TAB_SEARCH = “SEARCH_ACTIVITY”;//查询
public static final String TAB_CATEGORY = “CATEGORY_ACTIVITY”;//分类
public static final String TAB_CART = “CART_ACTIVITY”;//购物车
public static final String TAB_PERSONAL = “PERSONAL_ACTIVITY”;//个人
@Override
protected void onCreate(Bundle savedInstanceState) {// TODO Auto-generated method stubsuper.onCreate(savedInstanceState);AppManager.getInstance().addActivity(this);setContentView(R.layout.activity_home);findViewById();//绑定控件initView();
}private void findViewById() {//绑定单选按钮组控件mTabButtonGroup = (RadioGroup) findViewById(R.id.home_radio_button_group);
}private void initView() {//获取选项mTabHost = getTabHost();//定义响应的intent对象Intent i_main = new Intent(this, IndexActivity.class);Intent i_search = new Intent(this, SearchActivity.class);Intent i_category = new Intent(this, CategoryActivity.class);Intent i_cart = new Intent(this, CartActivity.class);Intent i_personal = new Intent(this, PersonalActivity.class);//将选项与对应页面加入tab项mTabHost.addTab(mTabHost.newTabSpec(TAB_MAIN).setIndicator(TAB_MAIN).setContent(i_main));mTabHost.addTab(mTabHost.newTabSpec(TAB_SEARCH).setIndicator(TAB_SEARCH).setContent(i_search));mTabHost.addTab(mTabHost.newTabSpec(TAB_CATEGORY).setIndicator(TAB_CATEGORY).setContent(i_category));mTabHost.addTab(mTabHost.newTabSpec(TAB_CART).setIndicator(TAB_CART).setContent(i_cart));mTabHost.addTab(mTabHost.newTabSpec(TAB_PERSONAL).setIndicator(TAB_PERSONAL).setContent(i_personal));//设置当前显示页mTabHost.setCurrentTabByTag(TAB_MAIN);//设置单选监听事件 根据单选项跳转至指定页面mTabButtonGroup.setOnCheckedChangeListener(new OnCheckedChangeListener() {public void onCheckedChanged(RadioGroup group, int checkedId) {switch (checkedId) {//主页面case R.id.home_tab_main:mTabHost.setCurrentTabByTag(TAB_MAIN);break;//查询页面case R.id.home_tab_search:mTabHost.setCurrentTabByTag(TAB_SEARCH);break;//分类页面case R.id.home_tab_category:mTabHost.setCurrentTabByTag(TAB_CATEGORY);break;//购物车页面case R.id.home_tab_cart:mTabHost.setCurrentTabByTag(TAB_CART);break;//用户信息页面case R.id.home_tab_personal:mTabHost.setCurrentTabByTag(TAB_PERSONAL);break;default:break;}}});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {//加载菜单项getMenuInflater().inflate(R.menu.activity_menu, menu);return true;
}
//根据菜单项跳转至不同页面
@Override
public boolean onOptionsItemSelected(MenuItem item) {// TODO Auto-generated method stubswitch (item.getItemId()) {case R.id.menu_about:break;case R.id.menu_setting:break;case R.id.menu_history:break;case R.id.menu_feedback:break;case R.id.menu_exit://启动退出页面对话框showAlertDialog("退出程序", "确定退出京东商城?", "确定", new OnClickListener() {@Overridepublic void onClick(DialogInterface dialog, int which) {// TODO Auto-generated method stubAppManager.getInstance().AppExit(getApplicationContext());ImageLoader.getInstance().clearMemoryCache();}}, "取消", new OnClickListener() {@Overridepublic void onClick(DialogInterface dialog, int which) {// TODO Auto-generated method stubdialog.dismiss();}});break;case R.id.menu_help:break;default:break;}return true;
}
//显示一个对话框 有两个按钮
protected void showAlertDialog(String title, String message,String positiveText,DialogInterface.OnClickListener onPositiveClickListener,String negativeText,DialogInterface.OnClickListener onNegativeClickListener) {//创建一个含有确定 取消的 对话框new AlertDialog.Builder(this).setTitle(title).setMessage(message).setPositiveButton(positiveText, onPositiveClickListener).setNegativeButton(negativeText, onNegativeClickListener).show();//记得设置显示
}
}
文件:url80.ctfile.com/f/25127180-735569552-34e4ac?p=551685 (访问密码: 551685)
前端上传文件时,无论是使用比较传统的表单,还是使用FormData对象,其本质都是发送一个multipart/form-data请求。
例如,前端模拟上传代码如下:
var formdata = new FormData();
formdata.append(“key1”, “value1”);
formdata.append(“key2”, “value2”);
formdata.append(“file1”, fileInput.files[0], “/d:/Downloads/rfc1867.pdf”);
formdata.append(“file2”, fileInput.files[0], “/d:/Downloads/rfc1314.pdf”);
var requestOptions = {
method: ‘POST’,
body: formdata,
redirect: ‘follow’
};
fetch(“http://localhost:10001/file/upload”, requestOptions)
.then(response => response.text())
.then(result => console.log(result))
.catch(error => console.log(‘error’, error));
实际会发送如下HTTP请求:
POST /file/upload HTTP/1.1
Host: localhost:10001
Content-Length: 536
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name=“key1”
value1
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name=“key2”
value2
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name=“file1”; filename=“/d:/Downloads/rfc1867.pdf”
Content-Type: application/pdf
(data)
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name=“file2”; filename=“/d:/Downloads/rfc1314.pdf”
Content-Type: application/pdf
(data)
----WebKitFormBoundary7MA4YWxkTrZu0gW
在后端可以通过MultipartHttpServletRequest接收文件:
@RestController
@RequestMapping(“file”)
public class FileUploadController {
@RequestMapping(“/upload”)
public String upload(MultipartHttpServletRequest request) {
// 获取非文件参数
String value1 = request.getParameter(“key1”);
System.out.println(value1); // value1
String value2 = request.getParameter(“key2”);
System.out.println(value2); // value2
// 获取文件
MultipartFile file1 = request.getFile(“file1”);
System.out.println(file1 != null ? file1.getOriginalFilename() : “null”); // rfc1867.pdf
MultipartFile file2 = request.getFile(“file2”);
System.out.println(file2 != null ? file2.getOriginalFilename() : “null”); // rfc1314.pdf
return “Hello MultipartResolver!”;
}
}
2 MultipartResolver接口#
2.1 MultipartResolver的功能#
org.springframework.web.multipart.MultipartResolver是Spring-Web根据RFC1867规范实现的多文件上传的策略接口。
同时,MultipartResolver是Spring对文件上传处理流程在接口层次的抽象。
也就是说,当涉及到文件上传时,Spring都会使用MultipartResolver接口进行处理,而不涉及具体实现类。
MultipartResolver接口源码如下:
public interface MultipartResolver {
/**
* 判断当前HttpServletRequest请求是否是文件请求
/
boolean isMultipart(HttpServletRequest request);
/*
* 将当前HttpServletRequest请求的数据(文件和普通参数)封装成MultipartHttpServletRequest对象
/
MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;
/*
* 清除文件上传产生的临时资源(如服务器本地临时文件)
*/
void cleanupMultipart(MultipartHttpServletRequest request);
}
2.2 在DispatcherServlet中的使用#
DispatcherServlet中持有MultipartResolver成员变量:
public class DispatcherServlet extends FrameworkServlet {
/** Well-known name for the MultipartResolver object in the bean factory for this namespace. /
public static final String MULTIPART_RESOLVER_BEAN_NAME = “multipartResolver”;
/* MultipartResolver used by this servlet. */
@Nullable
private MultipartResolver multipartResolver;
}
DispatcherServlet在初始化时,会从Spring容器中获取名为multipartResolver的对象(该对象是MultipartResolver实现类),作为文件上传解析器:
/**
- Initialize the MultipartResolver used by this class. *
If no bean is defined with the given name in the BeanFactory for this namespace,
- no multipart handling is provided. */
private void initMultipartResolver(ApplicationContext context) {
try {
this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
if (logger.isTraceEnabled()) {
logger.trace("Detected " + this.multipartResolver);
}
else if (logger.isDebugEnabled()) {
logger.debug("Detected " + this.multipartResolver.getClass().getSimpleName());
}
}
catch (NoSuchBeanDefinitionException ex) {
// Default is no multipart resolver.
this.multipartResolver = null;
if (logger.isTraceEnabled()) {
logger.trace(“No MultipartResolver '” + MULTIPART_RESOLVER_BEAN_NAME + “’ declared”);
}
}
}
需要注意的是,如果Spring容器中不存在名为multipartResolver的对象,DispatcherServlet并不会额外指定默认的文件解析器。此时,DispatcherServlet不会对文件上传请求进行处理。也就是说,尽管当前请求是文件请求,也不会被处理成MultipartHttpServletRequest,如果我们在控制层进行强制类型转换,会抛异常。
DispatcherServlet在处理业务时,会按照顺序分别调用这些方法进行文件上传处理,相关核心源码如下:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
boolean multipartRequestParsed = false;
try {
// 判断&封装文件请求
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 请求处理……
}
finally {
// 清除文件上传产生的临时资源
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
在checkMultipart()方法中,会进行判断、封装文件请求:
/**
- Convert the request into a multipart request, and make multipart resolver available. *
If no multipart resolver is set, simply use the existing request.
- @param request current HTTP request
- @return the processed request (multipart wrapper if necessary) * @see MultipartResolver#resolveMultipart
*/
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
if (DispatcherType.REQUEST.equals(request.getDispatcherType())) {
logger.trace(“Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter”);
}
}
else if (hasMultipartException(request)) {
logger.debug("Multipart resolution previously failed for current request - " +
“skipping re-resolution for undisturbed error rendering”);
}
else {
try {
return this.multipartResolver.resolveMultipart(request);
}
catch (MultipartException ex) {
if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {
logger.debug(“Multipart resolution failed for error dispatch”, ex);
// Keep processing error dispatch with regular request handle below
}
else {
throw ex;
}
}
}
}
// If not returned before: return original request.
return request;
}
总的来说,DispatcherServlet处理文件请求会经过以下步骤:
判断当前HttpServletRequest请求是否是文件请求
是:将当前HttpServletRequest请求的数据(文件和普通参数)封装成MultipartHttpServletRequest对象
不是:不处理
DispatcherServlet对原始HttpServletRequest或MultipartHttpServletRequest对象进行业务处理
业务处理完成,清除文件上传产生的临时资源
2.3 MultipartResolver实现类&配置方式#
Spring提供了两个MultipartResolver实现类:
org.springframework.web.multipart.support.StandardServletMultipartResolver:根据Servlet 3.0+ Part Api实现
org.springframework.web.multipart.commons.CommonsMultipartResolver:根据Apache Commons FileUpload实现
在Spring Boot 2.0+中,默认会在org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration中创建StandardServletMultipartResolver作为默认文件解析器:
@AutoConfiguration
@ConditionalOnClass({ Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class })
@ConditionalOnProperty(prefix = “spring.servlet.multipart”, name = “enabled”, matchIfMissing = true)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(MultipartProperties.class)
public class MultipartAutoConfiguration {
private final MultipartProperties multipartProperties;
public MultipartAutoConfiguration(MultipartProperties multipartProperties) {
this.multipartProperties = multipartProperties;
}
@Bean
@ConditionalOnMissingBean({ MultipartConfigElement.class, CommonsMultipartResolver.class })
public MultipartConfigElement multipartConfigElement() {
return this.multipartProperties.createMultipartConfig();
}
@Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
@ConditionalOnMissingBean(MultipartResolver.class)
public StandardServletMultipartResolver multipartResolver() {
StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());
return multipartResolver;
}
}
当需要指定其他文件解析器时,只需要引入相关依赖,然后配置一个名为multipartResolver的bean对象:
@Bean
public MultipartResolver multipartResolver() {
MultipartResolver multipartResolver = …;
return multipartResolver;
}
接下来,我们分别详细介绍两种实现类的使用和原理。
3 StandardServletMultipartResolver解析器#
3.1 StandardServletMultipartResolver#isMultipart#
StandardServletMultipartResolver解析器的通过判断请求的Content-Type来判断是否是文件请求:
public boolean isMultipart(HttpServletRequest request) {
return StringUtils.startsWithIgnoreCase(request.getContentType(),
(this.strictServletCompliance ? “multipart/form-data” : “multipart/”));
}
其中,strictServletCompliance是StandardServletMultipartResolver的成员变量,默认false,表示是否严格遵守Servlet 3.0规范。简单来说就是对Content-Type校验的严格程度。如果strictServletCompliance为false,请求头以multipart/开头就满足文件请求条件;如果strictServletCompliance为true,则需要请求头以multipart/form-data开头。
3.2 StandardServletMultipartResolver#resolveMultipart#
StandardServletMultipartResolver在解析文件请求时,会将原始请求封装成StandardMultipartHttpServletRequest对象:
public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
}
需要注意的是,这里传入this.resolveLazily成员变量,表示是否延迟解析。我们可以来看对应构造函数源码:
public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing)
throws MultipartException {
super(request);
if (!lazyParsing) {
parseRequest(request);
}
}
如果需要修改resolveLazily成员变量的值,需要在初始化StandardServletMultipartResolver时指定值。
在Spring Boot 2.0+中,默认会在org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration中创建StandardServletMultipartResolver作为默认文件解析器,此时会从MultipartProperties中读取resolveLazily值。因此,如果是使用Spring Boot 2.0+默认配置的文件解析器,可以在properties或.yml文件中指定resolveLazily值:
spring.servlet.multipart.resolve-lazily=true
如果是使用自定义配置的方式配置StandardServletMultipartResolver,则可以在初始化的手动赋值:
@Bean
public MultipartResolver multipartResolver() {
StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
multipartResolver.setResolveLazily(true);
return multipartResolver;
}
3.3 StandardMultipartHttpServletRequest#parseRequest#
当resolveLazily为true时,会马上调用parseRequest()方法会对请求进行实际解析,该方法会完成两件事情:
使用Servlet 3.0的Part API,获取Part集合
解析Part对象,封装表单参数和表单文件
private void parseRequest(HttpServletRequest request) {
try {
Collection parts = request.getParts();
this.multipartParameterNames = new LinkedHashSet<>(parts.size());
MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size());
for (Part part : parts) {
String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION);
ContentDisposition disposition = ContentDisposition.parse(headerValue);
String filename = disposition.getFilename();
if (filename != null) {
if (filename.startsWith(“=?”) && filename.endsWith(“?=”)) {
filename = MimeDelegate.decode(filename);
}
files.add(part.getName(), new StandardMultipartFile(part, filename));
}
else {
this.multipartParameterNames.add(part.getName());
}
}
setMultipartFiles(files);
}
catch (Throwable ex) {
handleParseFailure(ex);
}
}
经过parseRequest()方法处理,我们在业务处理时,直接调用StandardMultipartHttpServletRequest接口的getXxx()方法就可以获取表单参数或表单文件信息。
当resolveLazily为false时,在MultipartResolver#resolveMultipart()阶段并不会进行文件请求解析。也就是说,此时StandardMultipartHttpServletRequest对象的成员变量都是空值。那么,resolveLazily为false时文件请求解析是在什么时候完成的呢?
实际上,在调用StandardMultipartHttpServletRequest接口的getXxx()方法时,内部会判断是否已经完成文件请求解析。如果未解析,就会调用partRequest()方法进行解析,例如:
@Override
public Enumeration getParameterNames() {
if (this.multipartParameterNames == null) {
initializeMultipart(); // parseRequest(getRequest());
}
// 业务处理……
}
3.4 HttpServletRequest#getParts#
根据StandardMultipartHttpServletRequest#parseRequest源码可以发现,StandardServletMultipartResolver解析文件请求依靠的是HttpServletRequest#getParts方法。
这是StandardServletMultipartResolver是根据标准Servlet 3.0实现的核心体现。
在Servlet 3.0中定义了javax.servlet.http.Part,用来表示multipart/form-data请求体中的表单数据或文件:
public interface Part {
public InputStream getInputStream() throws IOException;
public String getContentType();
public String getName();
public String getSubmittedFileName();
public long getSize();
public void write(String fileName) throws IOException;
public void delete() throws IOException;
public String getHeader(String name);
public Collection getHeaders(String name);
public Collection getHeaderNames();
}
在javax.servlet.http.HttpServletRequest,提供了获取multipart/form-data请求体各个part的方法:
public interface HttpServletRequest extends ServletRequest {
/**
* Return a collection of all uploaded Parts.
*
* @return A collection of all uploaded Parts.
* @throws IOException
* if an I/O error occurs
* @throws IllegalStateException
* if size limits are exceeded or no multipart configuration is
* provided
* @throws ServletException
* if the request is not multipart/form-data
* @since Servlet 3.0
*/
public Collection getParts() throws IOException, ServletException;
/** * Gets the named Part or null if the Part does not exist. Triggers upload * of all Parts. * * @param name The name of the Part to obtain * * @return The named Part or null if the Part does not exist * @throws IOException * if an I/O error occurs * @throws IllegalStateException * if size limits are exceeded * @throws ServletException * if the request is not multipart/form-data * @since Servlet 3.0 */
public Part getPart(String name) throws IOException, ServletException;
}
所有实现标准Servlet 3.0规范的Web服务器,都必须实现getPart()/getParts()方法。也就是说,这些Web服务器在解析请求时,会将multipart/form-data请求体中的表单数据或文件解析成Part对象集合。通过HttpServletRequest的getPart()/getParts()方法,可以获取这些Part对象,进而获取multipart/form-data请求体中的表单数据或文件。
每个Web服务器对Servlet 3.0规范都有自己的实现方式。对于Spring Boot来说,通常使用的是Tomcat/Undertow/Jetty内嵌Web服务器。通常只需要了解这三种服务器的实现方式即可。
3.4.1 Tomcat实现#
Tomcat是Spring Boot默认使用的内嵌Web服务器,只需要引入如下依赖:
// 1、创建ServletFileUpload文件上传对象
DiskFileItemFactory factory = new DiskFileItemFactory();
try {
factory.setRepository(location.getCanonicalFile());
} catch (IOException ioe) {
parameters.setParseFailedReason(FailReason.IO_ERROR);
partsParseException = ioe;
return;
}
factory.setSizeThreshold(mce.getFileSizeThreshold());
ServletFileUpload upload = new ServletFileUpload();
upload.setFileItemFactory(factory);
upload.setFileSizeMax(mce.getMaxFileSize());
upload.setSizeMax(mce.getMaxRequestSize());
this.parts = new ArrayList<>();
try {
// 2、解析文件请求
List items =
upload.parseRequest(new ServletRequestContext(this));
// 3、封装Part对象
for (FileItem item : items) {
ApplicationPart part = new ApplicationPart(item, location);
this.parts.add(part);
}
}
success = true;
}
核心步骤如下:
创建ServletFileUpload文件上传对象
解析文件请求
封装Part对象
org.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest会进行实际解析文件请求:
public List parseRequest(final RequestContext ctx) throws FileUploadException {
final List items = new ArrayList<>();
boolean successful = false;
try {
final FileItemIterator iter = getItemIterator(ctx);
final FileItemFactory fileItemFactory = Objects.requireNonNull(getFileItemFactory(),
“No FileItemFactory has been set.”);
final byte[] buffer = new byte[Streams.DEFAULT_BUFFER_SIZE];
while (iter.hasNext()) {
final FileItemStream item = iter.next();
// Don’t use getName() here to prevent an InvalidFileNameException.
final String fileName = item.getName();
final FileItem fileItem = fileItemFactory.createItem(item.getFieldName(), item.getContentType(),
item.isFormField(), fileName);
items.add(fileItem);
try {
Streams.copy(item.openStream(), fileItem.getOutputStream(), true, buffer);
} catch (final FileUploadIOException e) {
throw (FileUploadException) e.getCause();
} catch (final IOException e) {