Servlet. Загрузка файла на сервер (Upload)

 
 
 
Сообщения:833
В примере будут испльзоватся Servlet 2.5, для версии 2.4 тоже должно работать.

Часть 1. Подготовка формы
Для того, что бы отправить файл на сервер с помщью браузера. Первым делом нужно правильно сформировать форму для отправки запроса. Выглядеть она должна примерно так:
<html>
<head>
<title>Upload</title>
</head>
<body>
	<form action="http://localhost:8080/upload" method="post" enctype="multipart/form-data">
		<input name="description" type="text"><br>
		<input name="data" type="file"><br>
		<input type="submit"><br>
	</form>
</body>
</html>

Теперь подробно рассмотрим все аттрибуты тега form, все они являются обязательными, без них работать не будет.

action="http://localhost:8080/upload" - указывает адрес, куда будет направлен запрос, вместо http://localhost:8080/upload можно указать любой другой URL. Можно использовать относительные URLы.

method="post" - указывает кокого типа запрос будет сформирован.
По умолчанию аттрибут имеет значение get. Особенностью метода POST является, то что POST запрос может включать в себя произвольные данные. Эти данные обычно называют телом запроса(телом POST запроса). Файлы передаются как-раз в теле запроса.

enctype="multipart/form-data" - указывает браузеру каким образом должно формироваться тело запроса. По умолчанию имеет значение application/x-www-form-urlencoded. Когда в теле запроса передаются данные, они передаются не в сыром виде, так как они хранятся на диске или в памяти. Они предварительно кодируются. Атрибут enctype говорит каким способом нужно кодировать. Браузер добавляет к запросу заголовок Content-Type:enctype-value, где enctype-value это то, что указано в атрибуте enctype (в нашем случае multipart/form-data). Плюс этот заголовок может хранить ещё какие-то дополнительные данные. Сервер этот заголовок прочитает, и на основе его будет знать как раскодировать тело запроса. Или поймёт, что он не знает как это сделать. Именно такая ситуация в сервлетах до версии 2.5 включительно, в версии 3.0 ситуация изменилась. Сервер умеет раскодировать только application/x-www-form-urlencoded закодированные данные. Он понятия не имеет что делать multipart/form-data закодированными данными. И по этому приходится добавлять сторонние библиотеки которые берут эту задачу на себя.

Часть 2. Что делает сервер.
Можно пропустить и читать сразу следующую часть, здесь в основном теория для понимания процесса в целом.

Напишем, сервлет который будет этот запрос принимать.

package sevlet;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


public class Upload extends HttpServlet {
	protected void doPost(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {		
		
		String description = request.getParameter("description");
		String data = request.getParameter("data");
		
		response.getWriter().println("description="+description);
		response.getWriter().println("data="+data);
	}	
}
Сервлет должен прочитать параметры, и отправить их обратно в виде текста.
Развернём сервлет и отправим на него запрос. В поле description вписываем, что угодно, в поле data выбираем любой файл. В результате получим:
Quote:
description=null
data=null

То есть ничего хорошего не получим. Параметр description и тот не получим даже. Давайте пошагово разберём, что там произошло.
Для отправки запроса, я использовал браузер Mozila Firefox, предварительно установив плагин LiveHTTPheaders, который показывает запросы и ответы в том виде в котором они передаются браузером.

Шаг 1.
В поле description ввёл qwerty. В качестве data указал файл data.txt в котором был стих Пушкина парус. И нажал отправить запрос.
Вид запроса который я получил с помощью плагина:
Quote:
http://localhost:8080/upload

POST /upload HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 (.NET CLR 3.5.30729)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: ru,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: windows-1251,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Content-Type: multipart/form-data; boundary=---------------------------74482520013458
Content-Length: 650
-----------------------------74482520013458
Content-Disposition: form-data; name="description"

qwerty
-----------------------------74482520013458
Content-Disposition: form-data; name="data"; filename="data.txt"
Content-Type: text/plain

I"a`?o'n~
A'a*e"a*a*o` i"a`?o'n~ i^a"e`i'i^e^e`e'
A^ o`o'i`a`i'a* i`i^?y" a~i^e"o'a'i^i`!...
?o`i^ e`u`a*o` i^i' a^ n~o`?a`i'a* a"a`e"a*e^i^e'?
?o`i^ e^e`i'o'e" i^i' a^ e^?a`? ?i^a"i'i^i`?...

E`a~?a`?o` a^i^e"i'u^ - a^a*o`a*? n~a^e`u`a*o`,
E` i`a`?o`a` a~i'a*o`n~y" e` n~e^?u^i"e`o`...
O'a^u^, - i^i' n~?a`n~o`e`y" i'a* e`u`a*o`
E` i'a* i^o` n~?a`n~o`e`y" a'a*?e`o`!

I"i^a" i'e`i` n~o`?o'y" n~a^a*o`e"a*e' e"a`c,o'?e`,
I'a`a" i'e`i` e"o'? n~i^e"i'o"a` c,i^e"i^o`i^e'...
A` i^i', i`y"o`a*?i'u^e', i"?i^n~e`o` a'o'?e`,
E^a`e^ a'o'a"o`i^ a^ a'o'?y"o~ a*n~o`u" i"i^e^i^e'!
-----------------------------74482520013458--

Все вполне читаемо человеческим глазом, вместо паруса мы видим хрень потому, что это оно так русский текст закодировало.

Если не указать аттрибут enctype, или указать enctype=application/x-www-form-urlencoded, то этот же запрос будет выглядеть так. В этом случае браузер содержимое файла не трогает, а передает только имя фала. И видно, что тело запроса закодировано по другому(попроще).
Quote:

http://localhost:8080/upload

POST /upload HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 (.NET CLR 3.5.30729)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: ru,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: windows-1251,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 32
description=qwerty&data=data.txt


Шаг 2.
Браузер устанавливает соединение с сервером и начинает передавать
этот запрос. Сервер начинает читать его. Дочитывает все до строчки Content-Length:650 включительно. Потом смотрит заголовок Content-Type: multipart/form-data; boundary=---------------------------74482520013458. Видит там multipart/form-data и понимает, что не умеет такое орабатывать. Дальше он даже не дочитывает запрос до конца.

Шаг 3.
Довайте модернизируем наш сервлет, и дочитаем остаток запроса вручную.
package sevlet;

import java.io.IOException;
import java.io.InputStreamReader;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


public class Upload extends HttpServlet {
	protected void doPost(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {		
		
		String description = request.getParameter("description");
		String data = request.getParameter("data");
		
		response.getWriter().println("description="+description);
		response.getWriter().println("data="+data);
		
		response.getWriter().println("POST request body:");
		
		InputStreamReader reader = new InputStreamReader(request.getInputStream());
		int c;
		while ((c=reader.read())>=0) {
			response.getWriter().print((char)c);
		}
	}	
}

Этот сервлет отправит в ответ, тело запроса(ту часть, что не дочитал сервер) в том виде что получил, без расшифровки. Отправим аналогичный запрос на обновленный сервлет. В результате получим:
Quote:
description=null
data=null
POST request body:
-----------------------------204722362218538
Content-Disposition: form-data; name="description"

qwerty
-----------------------------204722362218538
Content-Disposition: form-data; name="data"; filename="data.txt"
Content-Type: text/plain

?????
?????? ????? ????????
? ?????? ???? ???????!...
??? ???? ?? ? ?????? ????????
??? ????? ?? ? ???? ???????...

?????? ????? - ????? ??????,
? ????? ?????? ? ???????...
???, - ?? ??????? ?? ????
? ?? ?? ??????? ?????!

??? ??? ????? ??????? ??????,
??? ??? ??? ?????? ???????...
? ??, ????????, ?????? ????,
??? ????? ? ????? ???? ?????!
-----------------------------204722362218538--


Вывод.
Все сторонние библиотеки, для закачки файла на сервер. Занимаются тем, что дочитывают тело запроса, раскодируют его и предоставляют данные в удобном виде.

Часть 3. Upload файла с помощью библиотеки FileUpload от Apache.
Первым делом нужно эту библиотеку скачать, скачать можно тут http://commons.apache.org/fileupload/, также она зависит от библиотеки IO ее тоже качаем http://commons.apache.org/io/. Закидываем их в WEB-INF/lib.
Теперь напишем сервлет, который будет принимать multipart/form-data запрос. И сохранять на сервере все файлы которые будут отправлены. Обычные параметры будет просто выводить в консоль.
package sevlet;

import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

public class Upload extends HttpServlet {	
	private Random random = new Random();
	
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		//проверяем является ли полученный запрос multipart/form-data
		boolean isMultipart = ServletFileUpload.isMultipartContent(request);
		if (!isMultipart) {
			response.sendError(HttpServletResponse.SC_BAD_REQUEST);
			return;
		}

		// Создаём класс фабрику 
		DiskFileItemFactory factory = new DiskFileItemFactory();

		// Максимальный буфера данных в байтах,
		// при его привышении данные начнут записываться на диск во временную директорию
		// устанавливаем один мегабайт
		factory.setSizeThreshold(1024*1024);
		
		// устанавливаем временную директорию
		File tempDir = (File)getServletContext().getAttribute("javax.servlet.context.tempdir");
		factory.setRepository(tempDir);

		//Создаём сам загрузчик
		ServletFileUpload upload = new ServletFileUpload(factory);
		
		//максимальный размер данных который разрешено загружать в байтах
		//по умолчанию -1, без ограничений. Устанавливаем 10 мегабайт. 
		upload.setSizeMax(1024 * 1024 * 10);

		try {
			List items = upload.parseRequest(request);
			Iterator iter = items.iterator();
			
			while (iter.hasNext()) {
			    FileItem item = (FileItem) iter.next();

			    if (item.isFormField()) {
			    	//если принимаемая часть данных является полем формы			    	
			        processFormField(item);
			    } else {
			    	//в противном случае рассматриваем как файл
			        processUploadedFile(item);
			    }
			}			
		} catch (Exception e) {
			e.printStackTrace();
			response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
			return;
		}		
	}
	
	/**
	 * Сохраняет файл на сервере, в папке upload.
	 * Сама папка должна быть уже создана. 
	 * 
	 * @param item
	 * @throws Exception
	 */
	private void processUploadedFile(FileItem item) throws Exception {
		File uploadetFile = null;
		//выбираем файлу имя пока не найдём свободное
		do{
			String path = getServletContext().getRealPath("/upload/"+random.nextInt() + item.getName());					
			uploadetFile = new File(path);		
		}while(uploadetFile.exists());
		
		//создаём файл
		uploadetFile.createNewFile();
		//записываем в него данные
		item.write(uploadetFile);
	}

	/**
	 * Выводит на консоль имя параметра и значение
	 * @param item
	 */
	private void processFormField(FileItem item) {
		System.out.println(item.getFieldName()+"="+item.getString());		
	}
}

Для того, чтобы пример работал, необходимо вручную создать папку upload в корне web - приложения.
Теперь, отправляем например выше упомянутый запрос, смотрим результат.

В добавок скажу, что сохранять файлы можно в любом месте, не обязательно в директории web приложения. Главное, что бы у процесса в котором запущен сервер были права на запись.

Пример полного приложения можно скачать тут.
 
 
Сообщения:82
Офигеть. Столько это искал...

А у меня будет свой форум с флудилкой и чатом...
 
 
Сообщения:3866
Спасибо, очень круто!
 
 
Сообщения:1259
А в чём разница с over9000 аналогичных примеров в интернете? Кроме того, что Пушкин к "Парусу" никакого отношения не имеет. Да, и у самого апача, помнится неплохие HowTo были.
 
 
Сообщения:797
Это пример для версии Servlets < 3.0. В версии 3.0 и выше можно использовать @MultipartConfig и request.getParts(). Пример:
http://www.codejava.net/java-ee/servlet/java-file-upload-example-with-servlet-30-api

Нет ничего проще, чем заблудиться в иллюзиях, нет ничего сложнее, чем освободиться от них.
Изменен:05 окт 2015 10:22
 
Модераторы:Нет
Сейчас эту тему просматривают:Нет