首页 > 代码库 > JMeter 上传文件时,如何参数化 Content-Disposition 的 filename?
JMeter 上传文件时,如何参数化 Content-Disposition 的 filename?
问题描述
文件上传时,用户定义 Content-Disposition 是失效的。笔者在写压力测试脚本的时候,有个上传页面,服务器是根据用户传过来的 Content-Disposition 里的 filename 值来定义保存文件的文件名的。但是测试人员不可能为每一次请求都准备一个不同的文件(这个工作量海了去了),所以 JMeter 传给服务器的 Content-Disposition 里的 filename 必须是随机而不重复的。
有人问,用户真实上传时,浏览器传给服务器的 filename 也是上传文件名吗?不是的,js 这样修改的 filename:
uploader.onBeforeUploadItem = function (item) { //修改名字 var timeStamp = new Date().getTime(); var fileName = item.file.name; item.file.name = timeStamp + fileName.substr(fileName.lastIndexOf(‘.‘)); var day = $filter(‘date‘)(new Date(), ‘yyyyMMdd‘); item.url = [item.url, "batchImport", item.importType, day, session.userId].join("/"); };
笔者尝试了多种办法,试图修改服务器接收到的 filename 值,结果都失败了。笔者尝试的办法有:
1. 添加 HTTP 参数
如图所示,我们期待服务器接收到的 filename 值是 00004000.xls,而不是 00000000.xls。
结果服务器接收到的是 00000000.xls。服务器返回给客户端的存储路径为证:/batchImport/merAdd/20141128/1/00000000.xls。查看本次 HTTP 请求,可以看到以下信息:
POST http://serverIP/upload/batchImport/merAdd/20141128/1
POST data:
--DoZtX5jrOIxJTocysPzYJ1WVqtoagXMQHHqho4i
Content-Disposition: form-data; name="Content-Disposition"
Content-Type: text/plain; charset=US-ASCII
Content-Transfer-Encoding: 8bit
form-data; name="14170058206940.xls"; filename="00004000.xls"
--DoZtX5jrOIxJTocysPzYJ1WVqtoagXMQHHqho4i
Content-Disposition: form-data; name="file"; filename="00000000.xls"
Content-Type: application/vnd.ms-excel
Content-Transfer-Encoding: binary
<actual file content, not shown here>
--DoZtX5jrOIxJTocysPzYJ1WVqtoagXMQHHqho4i--
Cookie Data:
$Version=0; JSESSIONID=AC79777AEFE5AFC690623FCCB09E5DD5; $Path=/
Request Headers:
Connection: keep-alive
Content-Length: 34786
Content-Type: multipart/form-data; boundary=DoZtX5jrOIxJTocysPzYJ1WVqtoagXMQHHqho4i
Host: serverIP
User-Agent: Apache-HttpClient/4.2.6 (java 1.5)
看来 JMeter 把我们的 Content-Disposition 参数名字都丢了。
2. 添加 HTTP 信息头管理器
如图所示,我们期待服务器接收到的 filename 值是 40004000.xls,而不是 00000000.xls。
然后我们发次请求,然后查看本次 HTTP 请求,可以看到以下信息:
POST http://serverIP/upload/batchImport/merAdd/20141128/1
POST data:
--BNKvCNweqwpTJToYINcDn6JJfzjazBE550a-
Content-Disposition: form-data; name="file"; filename="00000000.xls"
Content-Type: application/vnd.ms-excel
Content-Transfer-Encoding: binary
<actual file content, not shown here>
--BNKvCNweqwpTJToYINcDn6JJfzjazBE550a---
Cookie Data:
$Version=0; JSESSIONID=49AB53310FB7241B5544B4E747A58F80; $Path=/
Request Headers:
Connection: keep-alive
Content-Disposition: form-data; name="file"; filename="40004000.xls"
Content-Length: 34535
Content-Type: multipart/form-data; boundary=BNKvCNweqwpTJToYINcDn6JJfzjazBE550a-
Host: serverIP
User-Agent: Apache-HttpClient/4.2.6 (java 1.5)
这次 JMeter 没有把我们的 Content-Disposition 弄丢,它出现在了 Request Headers 里边。但是服务器貌似读取的是 POST data 中 Content-Disposition 里的那个 filename。有服务器返回给客户端的存储路径为证:/batchImport/merAdd/20141128/1/00000000.xls。
3. 使用 BeanShell
如图所示,我们期待服务器接收到的 filename 值是 40004004.xls,而不是 00000000.xls。我们怀着期待的心情再次向服务器发起请求。请求如下:
POST http://serverIP/upload/batchImport/merAdd/20141128/1
POST data:
--LWS2eUVPPPuDxcfT7dS4RpqQJe2uP_0lAme6Qx2Q
Content-Disposition: form-data; name="file"; filename="00000000.xls"
Content-Type: application/vnd.ms-excel
Content-Transfer-Encoding: binary
<actual file content, not shown here>
--LWS2eUVPPPuDxcfT7dS4RpqQJe2uP_0lAme6Qx2Q--
Cookie Data:
$Version=0; JSESSIONID=81514F48024CE0B4CB53DB0CBC283C11; $Path=/
Request Headers:
Connection: keep-alive
Content-Length: 34543
Content-Type: multipart/form-data; boundary=LWS2eUVPPPuDxcfT7dS4RpqQJe2uP_0lAme6Qx2Q
Host: serverIP
User-Agent: Apache-HttpClient/4.2.6 (java 1.5)
结果是不管是前置 BeanShell,还是 BeanShell 监听器,显然对于我们的需求无能为力。
向万能的谷歌求助,得到的结果基本都是 It‘s impossible。
最后笔者怀着郁闷的心情去找这个项目的责任人,试图说服他,服务器不应该以客户端传来的 filename 对保存文件进行命名,应该有自己的一套随机生成文件名的规则,得到的答复却是:NO。
万般无奈之下,笔者只好去看 JMeter 的源代码了。好嘛,JMeter 2.12 的源代码(src 目录下的纯 *.java 文件)足足有 6.75 MB。而且还是用 Ant 代码管理的,黑压压的看着森人。哎,不爽也得看,没办法啊,谁让咱要吃性能测试这碗饭呢,工作总是要继续的吧。
硬着头皮看了一下午,结果很不幸,发现 JMeter 是把 Content-Disposition 里的 filename 写死的,它压根儿就没想留给用户对 filename 进行参数化途径!
比如 org.apache.jmeter.protocol.http.sampler.PostWriter 的 writeStartFileMultipart 方法是这样写死的:
/** * Write the start of a file multipart, up to the point where the * actual file content should be written */ private void writeStartFileMultipart(OutputStream out, String filename, String nameField, String mimetype) throws IOException { write(out, "Content-Disposition: form-data; name=\""); // $NON-NLS-1$ write(out, nameField); write(out, "\"; filename=\"");// $NON-NLS-1$ write(out, new File(filename).getName()); writeln(out, "\""); // $NON-NLS-1$ writeln(out, "Content-Type: " + mimetype); // $NON-NLS-1$ writeln(out, "Content-Transfer-Encoding: binary"); // $NON-NLS-1$ out.write(CRLF); }
虽然很气愤,但觉着总算没来错地方,继续看源码。
查看 PostWriter 的单元测试代码 org.apache.jmeter.protocol.http.sampler.PostWriterTest,在测试 sendPostData 方法里有以下语句:
postWriter.setHeaders(connection, sampler);
postWriter.sendPostData(connection, sampler);
也就是说先写头,再写 post 包体。org.apache.jmeter.protocol.http.sampler.HTTPJavaImpl 的 sample 方法也印证了这个:
try { conn = setupConnection(url, method, res); // Attempt the connection: savedConn = conn; conn.connect(); break; } catch (BindException e) { if (retry >= MAX_CONN_RETRIES) { log.error("Can‘t connect after "+retry+" retries, "+e); throw e; } log.debug("Bind exception, try again"); if (conn!=null) { savedConn = null; // we don‘t want interrupt to try disconnection again conn.disconnect(); } setUseKeepAlive(false); continue; // try again } catch (IOException e) { log.debug("Connection failed, giving up"); throw e; } } if (retry > MAX_CONN_RETRIES) { // This should never happen, but... throw new BindException(); } // Nice, we‘ve got a connection. Finish sending the request: if (method.equals(HTTPConstants.POST)) { String postBody = sendPostData(conn); res.setQueryString(postBody); }
conn = setupConnection(url, method, res); 建立连接的时候就将头写好了(参加下边的 setupConnection 方法),后边的 String postBody = sendPostData(conn); 才开始发送文件等包体。
为什么 JMeter 设置 HTTP 信息头里不管用呢?
看看 org.apache.jmeter.protocol.http.sampler.HTTPJavaImpl 的 setupConnection 方法:
protected HttpURLConnection setupConnection(URL u, String method, HTTPSampleResult res) throws IOException { SSLManager sslmgr = null; if (HTTPConstants.PROTOCOL_HTTPS.equalsIgnoreCase(u.getProtocol())) { try { sslmgr=SSLManager.getInstance(); // N.B. this needs to be done before opening the connection } catch (Exception e) { log.warn("Problem creating the SSLManager: ", e); } } final HttpURLConnection conn; final String proxyHost = getProxyHost(); final int proxyPort = getProxyPortInt(); if (proxyHost.length() > 0 && proxyPort > 0){ Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); //TODO - how to define proxy authentication for a single connection? // It‘s not clear if this is possible // String user = getProxyUser(); // if (user.length() > 0){ // Authenticator auth = new ProxyAuthenticator(user, getProxyPass()); // } conn = (HttpURLConnection) u.openConnection(proxy); } else { conn = (HttpURLConnection) u.openConnection(); } // Update follow redirects setting just for this connection conn.setInstanceFollowRedirects(getAutoRedirects()); int cto = getConnectTimeout(); if (cto > 0){ conn.setConnectTimeout(cto); } int rto = getResponseTimeout(); if (rto > 0){ conn.setReadTimeout(rto); } if (HTTPConstants.PROTOCOL_HTTPS.equalsIgnoreCase(u.getProtocol())) { try { if (null != sslmgr){ sslmgr.setContext(conn); // N.B. must be done after opening connection } } catch (Exception e) { log.warn("Problem setting the SSLManager for the connection: ", e); } } // a well-bahaved browser is supposed to send ‘Connection: close‘ // with the last request to an HTTP server. Instead, most browsers // leave it to the server to close the connection after their // timeout period. Leave it to the JMeter user to decide. if (getUseKeepAlive()) { conn.setRequestProperty(HTTPConstants.HEADER_CONNECTION, HTTPConstants.KEEP_ALIVE); } else { conn.setRequestProperty(HTTPConstants.HEADER_CONNECTION, HTTPConstants.CONNECTION_CLOSE); } conn.setRequestMethod(method); setConnectionHeaders(conn, u, getHeaderManager(), getCacheManager()); String cookies = setConnectionCookie(conn, u, getCookieManager()); setConnectionAuthorization(conn, u, getAuthManager()); if (method.equals(HTTPConstants.POST)) { setPostHeaders(conn); } else if (method.equals(HTTPConstants.PUT)) { setPutHeaders(conn); } if (res != null) { res.setRequestHeaders(getConnectionHeaders(conn)); res.setCookies(cookies); } return conn; }
这个方法里建立了一个 http 连接,并且在返回连接之前,先把用户 HTTP 信息头管理器里的内容写进连接(setConnectionHeaders(conn, u, getHeaderManager(), getCacheManager()); 句),然后调用 PostWriter 的写头方法(就是 setPostHeaders(conn); 句)。这也解释了本文上边的两个 Content-Disposition 的问题。
也就是说 Content-Disposition 头写了两次!很不幸的是,HTTP 并没对 Content-Disposition 做重复性校验!更不幸的是,即便是 HTTP 会对 Content-Disposition 做重复性校验,我们的头信息管理器里自定义的也不会起效,上边代码已经说明了,JMeter 会先写头信息管理器里的属性,然后再调用 PostWriter 进行 Content-Disposition 写入,后者会对前者进行覆盖!
这简直是糟透了。这应该是 JMeter 的一个 bug,或者说做的不够好的地方,因为它把我们自定义 Content-Disposition 这条路堵死了。
解决方案
HTTP 请求 - Implementation 选择的是 Java 的解决办法
自己动手,丰衣足食。既然 JMeter 把这条路堵死了,那么我们可以去把这条路打开 —— 只需调整下 PostWriter 的源代码的 writeStartFileMultipart 即可:/** * Write the start of a file multipart, up to the point where the * actual file content should be written */ private void writeStartFileMultipart(OutputStream out, String filename, String nameField, String mimetype) throws IOException { write(out, "Content-Disposition: form-data; name=\""); // $NON-NLS-1$ write(out, nameField); write(out, "\"; filename=\"");// $NON-NLS-1$ write(out, nameField); writeln(out, "\""); // $NON-NLS-1$ writeln(out, "Content-Type: " + mimetype); // $NON-NLS-1$ writeln(out, "Content-Transfer-Encoding: binary"); // $NON-NLS-1$ out.write(CRLF); }
其实只改了一句,就是把原来的 write(out, new File(filename).getName()); 改为 write(out, nameField);
然后将 JMeter 安装目录下的 lib/ext 目录中的 ApacheJMeter_http.jar 解压缩,将我们修改编译好的 PostWriter.class 把原来的翻盖掉,重新打包(jar -cvf ApacheJMeter_http.jar *),把原有的 ApacheJMeter_http.jar 删掉,使用新打包的。
上边是采样器 JVM 默认 HTTP 请求的解决办法(也就是你的采样器 - HTTP 请求 - Implementation 选择的是 Java)。如果我们 HTTP 请求选中的是 HttpClient4,或者 HttpClient3.1 呢(如果你没选,JMeter 默认是 HttpClient4,笔者就是使用的默认的,也就是说没选 Implementation)?以下是 HttpClient4 的解决办法。
HTTP 请求 - Implementation 选择的是 HttpClient4 的解决办法
查看 org.apache.jmeter.protocol.http.sampler.HTTPHC4Impl 的 sendPostData 方法,找到以下句:
ViewableFileBody[] fileBodies = new ViewableFileBody[files.length]; for (int i=0; i < files.length; i++) { HTTPFileArg file = files[i]; fileBodies[i] = new ViewableFileBody(new File(file.getPath()), file.getMimeType()); multiPart.addPart(file.getParamName(),fileBodies[i]); } post.setEntity(multiPart); if (multiPart.isRepeatable()){ ByteArrayOutputStream bos = new ByteArrayOutputStream(); for(ViewableFileBody fileBody : fileBodies){ fileBody.hideFileData = http://www.mamicode.com/true;>
可以看出,文件信息就在 postedBody.append(new String(bos.toByteArray(), 句写入 post 体(读者感兴趣的话可以去断点跟踪,或者打 log 验证),它写入的就是这个 ViewableFileBody 对象。找到 ViewableFileBody 类,其源码为:// Helper class so we can generate request data without dumping entire file contents private static class ViewableFileBody extends FileBody { private boolean hideFileData; public ViewableFileBody(File file, String mimeType) { super(file, mimeType); hideFileData = http://www.mamicode.com/false;>
可以看出它继承自 org.apache.http.entity.mime.content.FileBody,FileBody 有 getFilename 方法,查看其源码:public String getFilename() { return this.file.getName(); }
好了,就从这里入手了。修改 org.apache.jmeter.protocol.http.sampler.HTTPHC4Impl 的内部类 ViewableFileBody 如下:// Helper class so we can generate request data without dumping entire file contents private static class ViewableFileBody extends FileBody { private boolean hideFileData; public ViewableFileBody(File file, String mimeType) { super(file, mimeType); hideFileData = http://www.mamicode.com/false;>
OK,编译 - 覆盖 - 打包,然后把 JMeter 安装目录下的 lib/ext 下的原有的 ApacheJMeter_http.jar 删掉,使用新打包的。重新执行测试,截取的 HTTP 请求如下:
POST http://serverIP/upload/batchImport/merAdd/20141128/1
POST data:
--QpmvliwpJdOaJSQGKd-Ux3tR_7HnPX3s1K8KA
Content-Disposition: form-data; name="file"; filename="1417182984171.xls"
Content-Type: application/vnd.ms-excel
Content-Transfer-Encoding: binary
<actual file content, not shown here>
--QpmvliwpJdOaJSQGKd-Ux3tR_7HnPX3s1K8KA--
Cookie Data:
$Version=0; JSESSIONID=56E8E454EA4F1378AAE45DD0A89A9FE5; $Path=/
Request Headers:
Connection: keep-alive
Content-Length: 34542
Content-Type: multipart/form-data; boundary=QpmvliwpJdOaJSQGKd-Ux3tR_7HnPX3s1K8KA
Host: serverIP
User-Agent: Apache-HttpClient/4.2.6 (java 1.5)
可以看到,filename 终于不再是 00000000.xls 了,服务器返回的存储路径是 /batchImport/merAdd/20141128/1/1417182984171.xls。成功了。
备注:笔者下载的 JMeter Binaries 和 Source 的版本都是 2.12。HTTP 请求 - Implementation 选择的是 HttpClient3.1 的解决办法
嗯,对,没错,就是 org.apache.jmeter.protocol.http.sampler.HTTPHC3Impl。这个留给聪明的读者去实现吧:)JMeter 上传文件时,如何参数化 Content-Disposition 的 filename?
声明:以上内容来自用户投稿及互联网公开渠道收集整理发布,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任,若内容有误或涉及侵权可进行投诉: 投诉/举报 工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。