AppEngine →  Загрузка, хранение и извлечение изображений в Google App Engine

В данной статье хочу поведать сообществу о способе загрузки и хранения изображений в хранилище, которое предоставляет Google App Engine. Решение построено по классическому принципу, на простых JSP-страницах, хотя может быть с легкостью адаптировано применительно к GWT-коду. Для диспетчеризации запросов я решил использовать Spring Framework 3 и JDO для работы с хранилищем. Таким образом, попутно будет рассмотрен вопрос интеграции JDO-классов в Spring-приложения.
Поглядеть как работает приложение можно здесь
Попрошу не судить строго ибо это моя первая проба пера. Итак, поехали!

Изначально Google App Engine не предоставляет возможности для загрузки файлов непосредственно на жесткие диски АппСпота. Многие рекомендуют при разработке своих приложений выносить статическую графику на внешние сервера (например, на тот же Amazon S3). Не запрещена также загрузка статического контента непосредственно с самим приложением (например, в каталоге images проекта и т.д.). Но бывают иногда моменты, когда нужно непосредственно загружать содержимое графических файлов в хранилище.
Для этих целей в данный момент есть два способа, которые можно "легально" использовать в проектах на GAE:
  • Использовать BLOB-поле модели для хранения бинарного контента
  • Использовать BlobStore, доступность которого возможна только при включенном биллинге приложения
В описываемом решении применен первый подход.
Хочу сразу отметить, что все вопросы, связанные с созданием GAE-проекта и копированием в WEB-INF/lib библиотек я затрагивать не буду, речь, как говориться, о другом.

Модель для хранения информации о изображении

Для хранения информации о изображении я сделал небольшой класс модели, который содержит в себе blob-поле. Этот класс зааннотирован JDO-аннотациями, т.е. его можно сохранять в хранилище
@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class PictureBean {
	@PrimaryKey
	@Persistent(primaryKey = "true", valueStrategy = IdGeneratorStrategy.IDENTITY)
	private Long id;

	@Persistent
	private Blob imgContent;

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public Blob getImgContent() {
		return imgContent;
	}

	public void setImgContent(Blob imgContent) {
		this.imgContent = imgContent;
	}
}
Когда класс модели определен -- самое время описать конфигурацию Spring-контекста для работы с GAE DataStore посредством JDO:
<?xml version="1.0" encoding="utf-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
		xmlns:context="http://www.springframework.org/schema/context"
		xsi:schemaLocation="
		http://www.springframework.org/schema/beans
		http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
		http://www.springframework.org/schema/context
		http://www.springframework.org/schema/context/spring-context-2.5.xsd">

	<context:component-scan base-package="com.appspot.gshocklab" />

	<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" />
	<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" />

	<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
		<property name="prefix" value="/WEB-INF/pages/" />
		<property name="suffix" value=".jsp" />
	</bean>

	<!-- Google AppEngine JDO persistence support -->
	<bean id="persistenceManagerFactory" class="org.springframework.orm.jdo.LocalPersistenceManagerFactoryBean">
		<property name="jdoProperties">
			<props>
				<prop key="javax.jdo.PersistenceManagerFactoryClass">
					org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory
				</prop>
				<prop key="javax.jdo.option.ConnectionURL">appengine</prop>
				<prop key="javax.jdo.option.NontransactionalRead">true</prop>
				<prop key="javax.jdo.option.NontransactionalWrite">true</prop>
				<prop key="javax.jdo.option.RetainValues">true</prop>
				<prop key="datanucleus.appengine.autoCreateDatastoreTxns">true</prop>
			</props>
		</property>
	</bean>
</beans>
Объявленный бин persistenceManagerFactory будет использован в коде для "общения" с хранилищем.

Контроллер для загрузки изображения в Datastore

Контроллер, который обрабатывает запрос на загрузку изображения в хранилище выполнен как аннотированный Spring-контроллер
@Controller
public class ImageUploadController {
	@Autowired
	private PersistenceManagerFactory pmf;

	@RequestMapping(value = "/doUpload", method = RequestMethod.POST)
	public String doUploadAction(HttpServletRequest request, HttpServletResponse response) {
		ServletFileUpload uploadServlet = new ServletFileUpload();
		FileItemIterator iter;
		try {
			iter = uploadServlet.getItemIterator(request);
			if (iter.hasNext()) {
				FileItemStream fs = iter.next();
				InputStream is = fs.openStream();

				byte[] contentBytes = IOUtils.toByteArray(is);

				PersistenceManager pm = pmf.getPersistenceManager();
				PictureBean pictBean = new PictureBean();
				pictBean.setImgContent(new Blob(contentBytes));
				pm.makePersistent(pictBean);

				pm.close();
				IOUtils.closeQuietly(is);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}

		return "redirect:gallery";
	}
}
Аннотация @Controller говорит о том, что данный класс будет участвовать в обработке запросов от клиентской части, а метод, помеченный @RequestMapping(value = "/doUpload", method = RequestMethod.POST) будет заниматься обработкой POST-запроса /doUpload.
В тот момент, когда данный метод начинает выполняться, с помощью библиотеки Apache Commons Fileupload выполняется считывание входного потока байтов из объекта http-запроса. Далее считанный двоичный контент просто "перелаживается" в blob-поле нашей модели и делается попытка ее сохранения в хранилище
PersistenceManager pm = pmf.getPersistenceManager();
PictureBean pictBean = new PictureBean();
pictBean.setImgContent(new Blob(contentBytes));
pm.makePersistent(pictBean);
Да, кстати, доступ к хранилищу у нас осуществляется посредством проинициализированного PersistenceManagerFactory при запуске Spring-контекста и "внедренного" в класс контроллера с помощью аннотации @Autowired.

Извлечение и отображение сохраненного изображения

Соответствующим образом, для отображения сохраненного в blob-поле изображения его нужно зачитать из хранилища и "отдать" клиенту. Поскольку разработанное приложение является "академическим" то вычитывание и отображение выполняется в несколько подходов:
  1. Загрузка всех PistureBean-ов и получение их идентификаторов
    @RequestMapping(value = "/gallery", method = RequestMethod.GET)
    public String handleGalleryRequest(Model modelData) {
    	PersistenceManager pm = pmf.getPersistenceManager();
    	List<PictureBean> images = null;
    
    	try {
    		pm.getFetchPlan().setDetachmentOptions(FetchPlan.DETACH_UNLOAD_FIELDS);
    		Query q = pm.newQuery("select from " + PictureBean.class.getName());
    
    		try {
    			images = (List<PictureBean>) q.execute();
    		} catch (Exception ex) {
    			ex.printStackTrace();
    		} finally {
    			q.closeAll();
    		}
    		List<Long> imgIds = new ArrayList<Long>();
    		for (PictureBean b : (List<PictureBean>)pm.detachCopyAll(images)) {
    			imgIds.add(b.getId());
    		}
    
    		modelData.addAttribute("imgIds", imgIds);
    	} catch (Exception ex) {
    		ex.printStackTrace();
    	} finally {
    		pm.close();
    	}
    	return "gallery";
    }
    
  2. Отображение на JSP-странице тегов img с идентификаторами сохраненных в хранилище изображений:

    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <%@page isELIgnored="false" %>

    <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

    <h2>Gallery</h2>
    <hr />

    <c:forEach items="${imgIds}" var="imageId">
    <img src="showImage?imgId=${imageId}" alt="Image from Datastore" /><br/>
    </c:forEach>

  3. Отображение конкретного изображения:
    @RequestMapping(value = "/showImage", method = RequestMethod.GET)
    	public void showImageFromDatastore(@RequestParam("imgId") Long imageId, HttpServletResponse response) {
    		PersistenceManager pm = pmf.getPersistenceManager();
    
    		try {
    			PictureBean pict = (PictureBean) pm.getObjectById(PictureBean.class, imageId);
    			if (pict != null) {
    				response.getOutputStream().write(pict.getImgContent().getBytes());
    				response.getOutputStream().flush();
    			}
    		} catch (Exception e) {
    			e.printStackTrace();
    		} finally {
    			pm.close();
    		}
    	}
    
Последний метод извлекает экземпляр класса модели PistureBean, берет из него blob-контент и в потоке отдает клиенту при отображении HTML-страницы в браузере. Делается это простым "перекачиванием" массива байт в поток ответа сервлета. Поскольку на странице имеется несколько разных img-тегов, то подгрузка изображений из хранилища выполняется в нескольких потоках посредством метода @RequestMapping(value = "/showImage", method = RequestMethod.GET).

Вместо заключения

В данной заметке я сделал попытку рассказать о том, как можно хранить изображения в Google App Engine хранилище посредством использования BLOB-полей класса модели. Как видим, жить можно и описанный подход имеет право на существование. Но стоит предостеречь пользователей GAE от повсеместного применения такого метода -- если загружать большого объема файлы то может быстро закончиться лимит на использование хранилища. В таком случае считаю уместным включение биллинга для своего приложения и использовать BlobStore Services или же обратиться к внешним источникам хранения контента. В случае небольших CMS, интернет-магазинов etc. думаю, вполне можно upload-ить небольшие графические файлы в Datastore.
Многие моменты касательно организации проекта я заведомо опустил, чтобы не загромождать и так перегруженный кодом текст еще исходниками JSP-страниц. Также я оставил без внимания технику работы с GAE Datastore посредством JDO. Этому можно посвятить отдельную заметку, даже целую серию.

Спасибо. Буду рад ответить на все вопросы в рамках своей компетенции ;)

З.Ы. Надеюсь, материал не сильно перегружен исходниками

комментарии:

Vitaly Gashock 24.05.2010 21:38
Прошу прощения, но я почему-то не нашел как указать категорию AppEngine для данной заметки...
Enetri 25.05.2010 06:44
Все просто: это не категория, а название блога.
Vitaly Gashock 25.05.2010 07:32
Спасибо за подсказку ;) Изменил название блога
+1Vitaly Gashock 25.05.2010 07:33
Исходный код вместе с Eclipse-проектом и всеми библиотеками можно взять на странице проекта на Google Code

добавить комментарий: