Spring MVC-AngularJS-File Upload-org.Apacz.commons.fileupload.FileUploadException
Mam jako serwer aplikację webową Java Spring MVC. I aplikacji opartej na AngularJS jako klienta.
W AngularJS muszę wgrać plik i wysłać na serwer.
Oto mój html
<form ng-submit="uploadFile()" class="form-horizontal" enctype="multipart/form-data">
<input type="file" name="file" ng-model="document.fileInput" id="file" onchange="angular.element(this).scope().setTitle(this)" />
<input type="text" class="col-sm-4" ng-model="document.title" id="title" />
<button class="btn btn-primary" type="submit">
Submit
</button>
</form>
Oto mój UploadController.js
'use strict';
var mainApp=angular.module('mainApp', ['ngCookies']);
mainApp.controller('FileUploadController', function($scope, $http) {
$scope.document = {};
$scope.setTitle = function(fileInput) {
var file=fileInput.value;
var filename = file.replace(/^.*[\\\/]/, '');
var title = filename.substr(0, filename.lastIndexOf('.'));
$("#title").val(title);
$("#title").focus();
$scope.document.title=title;
};
$scope.uploadFile=function(){
var formData=new FormData();
formData.append("file",file.files[0]);
$http({
method: 'POST',
url: '/serverApp/rest/newDocument',
headers: { 'Content-Type': 'multipart/form-data'},
data: formData
})
.success(function(data, status) {
alert("Success ... " + status);
})
.error(function(data, status) {
alert("Error ... " + status);
});
};
});
Idzie na serwer. Oto mój DocumentUploadController.java
@Controller
public class DocumentUploadController {
@RequestMapping(value="/newDocument", headers = "'Content-Type': 'multipart/form-data'", method = RequestMethod.POST)
public void UploadFile(MultipartHttpServletRequest request, HttpServletResponse response) {
Iterator<String> itr=request.getFileNames();
MultipartFile file=request.getFile(itr.next());
String fileName=file.getOriginalFilename();
System.out.println(fileName);
}
}
Kiedy uruchamiam to dostaję następujący wyjątek
org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request; nested exception is org.apache.commons.fileupload.FileUploadException: the request was rejected because no multipart boundary was found] with root cause
org.apache.commons.fileupload.FileUploadException: the request was rejected because no multipart boundary was found
at org.apache.commons.fileupload.FileUploadBase$FileItemIteratorImpl.<init>(FileUploadBase.java:954)
at org.apache.commons.fileupload.FileUploadBase.getItemIterator(FileUploadBase.java:331)
at org.apache.commons.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:351)
at org.apache.commons.fileupload.servlet.ServletFileUpload.parseRequest(ServletFileUpload.java:126)
at org.springframework.web.multipart.commons.CommonsMultipartResolver.parseRequest(CommonsMultipartResolver.java:156)
at org.springframework.web.multipart.commons.CommonsMultipartResolver.resolveMultipart(CommonsMultipartResolver.java:139)
at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1047)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:892)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:856)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:920)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:827)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:647)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:801)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:728)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:305)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:222)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:123)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:99)
at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:953)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:408)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1023)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:589)
at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:310)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:744)
W moim applicationkontekst.xml , I wspominali
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="100000" />
</bean>
Używam
spring - 3.2.1.RELAESE
commons-fileupload - 1.2.2
commons-io - 2.4
Jak to rozwiązać?
Byłoby świetnie, gdyby ktoś mi powiedział, Jak wysłać plik i inne formdata z angularJS i dostać go na serwer.
UPDATE 1
@Michał: widzę to tylko w konsoli, po kliknięciu submit.POST http://localhost:9000/serverApp/rest/newDocument 500 (Internal Server Error) angular.js:9499
(anonymous function) angular.js:9499
sendReq angular.js:9333
$http angular.js:9124
$scope.uploadFile invoice.js:113
(anonymous function) angular.js:6541
(anonymous function) angular.js:13256
Scope.$eval angular.js:8218
Scope.$apply angular.js:8298
(anonymous function) angular.js:13255
jQuery.event.dispatch jquery.js:3074
elemData.handle
Mój serwer działa na innym porcie 8080. Jestem uisng yeoman, grunt i bower. Taki cienki gruntfile.js wspomniałem o porcie serwera. Więc idzie na serwer i uruchamia to and throws the exception
UPDATE 2
Granica nie jest ustawiona
Request URL:http://localhost:9000/serverApp/rest/newDocument
Request Method:POST
Status Code:500 Internal Server Error
Request Headers view source
Accept:application/json, text/plain, */*
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-US,en;q=0.8
Connection:keep-alive
Content-Length:792
Content-Type:multipart/form-data
Cookie:ace.settings=%7B%22sidebar-collapsed%22%3A-1%7D; isLoggedIn=true; loggedUser=%7B%22name%22%3A%22admin%22%2C%22password%22%3A%22admin23%22%7D
Host:localhost:9000
Origin:http://localhost:9000
Referer:http://localhost:9000/
User-Agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36
X-Requested-With:XMLHttpRequest
Request Payload
------WebKitFormBoundaryCWaRAlfQoZEBGofY
Content-Disposition: form-data; name="file"; filename="csv.csv"
Content-Type: text/csv
------WebKitFormBoundaryCWaRAlfQoZEBGofY--
Response Headers view source
connection:close
content-length:5007
content-type:text/html;charset=utf-8
date:Thu, 09 Jan 2014 11:46:53 GMT
server:Apache-Coyote/1.1
4 answers
Napotkałem ten sam problem i napotkałem ten sam problem nawet po aktualizacji transformRequest. 'Niektóre jak, granica nagłówka nie wydaje się być ustawiona poprawnie.
Po http://uncorkedstudios.com/blog/multipartformdata-file-upload-with-angularjs , problem został rozwiązany. / Align = "left" / ...
Ustawiając 'Content-Type': undefined, przeglądarka ustawia Content-Type na multipart/form-data dla nas i wypełnia poprawną granicę. Ręcznie ustawienie 'Content-Type': multipart / form-data nie wypełni parametru boundary żądania.
Nie wiem, czy to komuś pomoże, ale może ułatwi ludziom oglądającym ten post... Przynajmniej to utrudnia.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-02-28 08:43:12
Wprowadzenie
Miałem ten sam problem i znalazłem kompletne rozwiązanie, aby wysłać zarówno json jak i plik ze strony bazującej na angular do metody Spring MVC.
Głównym problemem jest $http, który nie wysyła odpowiedniego nagłówka Content-type (wyjaśnię dlaczego).
The theory about multipart / form-data
Aby wysłać zarówno json jak i file musimy wysłać multipart / form-data, co oznacza "wysyłamy różne elementy w ciele oddzielone specjalnym separatorem". This special separator nazywa się "boundary" , który jest ciągiem znaków, który nie występuje w żadnym z elementów, które mają być wysłane.
Serwer musi wiedzieć, która granica jest używana, więc musi być wskazana w nagłówku Content-type (Content-Type multipart / form-data; boundary = $the_boundary_used).
Więc... potrzebne są dwie rzeczy:
- w nagłówku --> wskaż multipart / form-data i która granica jest używana (tutaj jest miejsce, w którym $http nie działa)
- w ciele --> oddzielenie każdego parametru żądania granicą
Przykład dobrej prośby:
Header
Content-Type multipart/form-data; boundary=---------------------------129291770317552
Który mówi serwerowi " wysyłam wiadomość wieloczęściową z następnym separatorem (boundary): ---------------------------129291770317552
Ciało
-----------------------------129291770317552 Content-Disposition: form-data; name="clientInfo"
{ "name": "Johny", "surname":"Cash"}
-----------------------------129291770317552
Content-Disposition: form-data; name="file"; filename="yourFile.pdf"
Content-Type: application/pdf
%PDF-1.4
%õäöü
-----------------------------129291770317552 --
Gdzie wysyłamy 2 argumenty," clientInfo "i" file " oddzielone granicą.
Problem
Jeśli żądanie jest wysyłane z $http, granica nie jest wysyłana w nagłówku (punkt 1), więc Spring nie jest w stanie przetworzyć danych (nie wie, jak podzielić "części" żądania).
Inny problem polega na tym, że granica jest znana tylko przez FormData... ale FormData nie ma żadnych accesorów, więc nie można wiedzieć, która granica jest używana!!!
Rozwiązanie
Zamiast używać $http w js powinieneś użyć standardowego XMLHttpRequest, coś w stylu:
//create form data to send via POST
var formData=new FormData();
console.log('loading json info');
formData.append('infoClient',angular.toJson(client,true));
// !!! when calling formData.append the boundary is auto generated!!!
// but... there is no way to know which boundary is being used !!!
console.log('loading file);
var file= ...; // you should load the fileDomElement[0].files[0]
formData.append('file',file);
//create the ajax request (traditional way)
var request = new XMLHttpRequest();
request.open('POST', uploadUrl);
request.send(formData);
Wtedy w Twojej wiosennej metodzie możesz mieć coś w stylu:
@RequestMapping(value = "/", method = RequestMethod.POST)
public @ResponseBody Object newClient(
@RequestParam(value = "infoClient") String infoClientString,
@RequestParam(value = "file") MultipartFile file) {
// parse the json string into a valid DTO
ClientDTO infoClient = gson.fromJson(infoClientString, ClientDTO.class);
//call the proper service method
this.clientService.newClient(infoClient,file);
return null;
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-02-03 16:07:43
Odpowiedź Carlosa Verdesa nie zadziałała z moim $ http interceptor, który dodaje nagłówki autoryzacji i tak dalej. Postanowiłem więc dodać do jego rozwiązania i stworzyć moje używając $http.
Clientside Angular (1.3.15)
Mój formularz (używając składni controllerAs ) zakłada plik i prosty obiekt zawierający informacje potrzebne do wysłania na serwer. W tym przypadku używam prostej nazwy i typ String właściwość.
<form>
<input type="text" ng-model="myController.myObject.name" />
<select class="form-control input-sm" ng-model="myController.myObject.type"
ng-options="type as type for type in myController.types"></select>
<input class="input-file" file-model="myController.file" type="file">
</form>
Pierwszy krok było stworzenie dyrektywy, która wiąże mój plik z zakresem wyznaczonego kontrolera (w tym przypadku myController), abym mógł uzyskać do niego dostęp. Powiązanie go bezpośrednio z Modelem w kontrolerze nie będzie działać, ponieważ plik input type = file nie jest wbudowaną funkcją.
.directive('fileModel', ['$parse', function ($parse) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
var model = $parse(attrs.fileModel);
var modelSetter = model.assign;
element.bind('change', function(){
scope.$apply(function(){
modelSetter(scope, element[0].files[0]);
});
});
}
};
}]);
Po Drugie stworzyłem fabrykę o nazwie myObject za pomocą metody instancji create , która pozwala mi przekształcić dane po wywołaniu create na serwerze. Ta metoda dodaje wszystko do obiektu FormData i konwertuje go za pomocą metoda transformRequest (angular.tożsamość ). Ważne jest, aby ustawić nagłówek na undefined . (Starsze wersje kątowe mogą wymagać ustawienia czegoś więcej niż undefined). Pozwoli to na Automatyczne ustawienie znacznika multidata/boundary (Zobacz post Carlosa).
myObject.prototype.create = function(myObject, file) {
var formData = new FormData();
formData.append('refTemplateDTO', angular.toJson(myObject));
formData.append('file', file);
return $http.post(url, formData, {
transformRequest: angular.identity,
headers: {'Content-Type': undefined }
});
}
Po stronie klienta pozostaje jedynie utworzenie instancji nowego myObject w myController i wywołanie metody create w funkcji create kontrolera po przesłaniu formularza.
this.myObject = new myObject();
this.create = function() {
//Some pre handling/verification
this.myObject.create(this.myObject, this.file).then(
//Do some post success/error handling
);
}.bind(this);
Serverside Spring (4.0)
Na Restcontrolle mogę teraz po prostu wykonać następujące czynności: (zakładając, że mamy POJO MyObject)
@RequestMapping(method = RequestMethod.POST)
@Secured({ "ROLE_ADMIN" }) //This is why I needed my $httpInterceptor
public void create(MyObject myObject, MultipartFile file) {
//delegation to the correct service
}
UWAGA, nie używam requestparameters, ale pozwalam spring zrobić konwersję JSON do POJO/DTO. Upewnij się, że masz MultiPartResolver bean również skonfigurowany poprawnie i dodany do pom.xml. (I Jackson-Mapper w razie potrzeby)
Wiosna-kontekst.xml
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="268435456" /> <!-- 256 megs -->
</bean>
Pom.xml
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>${commons-fileupload.version}</version>
</dependency>
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-06-16 12:06:38
Możesz spróbować tego
.js
$scope.uploadFile=function(){
var formData=new FormData();
formData.append("file",file.files[0]);
$http.post('/serverApp/rest/newDocument', formData, {
transformRequest: function(data, headersGetterFunction) {
return data;
},
headers: { 'Content-Type': undefined }
}).success(function(data, status) {
alert("Success ... " + status);
}).error(function(data, status) {
alert("Error ... " + status);
});
.java
@Controller
public class DocumentUploadController {
@RequestMapping(value="/newDocument", method = RequestMethod.POST)
public @ResponseBody void UploadFile(@RequestParam(value="file", required=true) MultipartFile file) {
String fileName=file.getOriginalFilename();
System.out.println(fileName);
}
}
To na podstawie https://github.com/murygin/rest-document-archive
Jest dobry przykład przesyłania plików https://murygin.wordpress.com/2014/10/13/rest-web-service-file-uploads-spring-boot/
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-11-27 13:58:57