Поискал на хабре схожие статьи, нашел только Morphia — легкий ORM для MongoDB, управляемый аннотациями, ничего по связке Spring Data + MongoDB не нашлось, в связи с этим решил написать пост из раздела «для самых маленьких» по настройке и использованию связки Spring + MongoDB.
Что будет из себя представлять само приложение:
Сделаем самый простой менеджер контактов, для того, чтобы попробовать на примере элементарные операции CRUD.Используемые библиотеки:
- Spring IoC, MVC, Data (Mongo)
- Mongo Driver
- Log4j через sl4j
- ну и немножко дополнительных, которые опишу уже в конфигурационном файле
Собирать проект будет Maven, а код, лично я, пишу в Intellij IDEA (думаю, это лучшая IDE для Java). Кстати говоря, о преимуществах этой среды в своё время рассказывал asolntsev в посте Почему IDEA лучше Eclipse.
Конфигурация проекта
Для начала опишем конфигурацию Maven:
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>war</packaging>
<groupId>habra</groupId>
<artifactId>habr</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- На момент написания статьи, версии библиотек являются последними -->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<version.jdk>1.6</version.jdk>
<version.spring>4.0.2.RELEASE</version.spring>
<version.spring.mongodb>1.4.0.RELEASE</version.spring.mongodb>
<version.jackson>1.9.13</version.jackson>
</properties>
<dependencies>
<!-- Все, что нужно для Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${version.spring}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${version.spring}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${version.spring}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${version.spring}</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
<version>${version.spring.mongodb}</version>
</dependency>
<!-- MongoDB драйвер -->
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
<version>2.11.4</version>
</dependency>
<!--
Jackson JSON Mapper
Тащу его всегда, когда нужно писать API на базе JSON объектов.
В нашем случае можете эту библиотеку не тянуть.
-->
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>${version.jackson}</version>
</dependency>
<!-- Servlet Api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- Логгирование -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.5</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.5</version>
</dependency>
<!-- TEST -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!--
Apache Commons, тяну пратически в каждый свой проект из-за их полезности.
Можете также их пропустить.
-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>2.4</version>
<configuration>
<webXml>src/main/webapp/WEB-INF/web.xml</webXml>
</configuration>
</plugin>
</plugins>
</build>
</project>
Теперь, когда все библиотеки описаны, можно приступить к описанию конфигурационных файлов Spring.
Для этого я обычно создаю папку «spring» в src/main/resources, некоторые хранят файлы конфигурации Spring в webapp/WEB-INF/*, но это уже кому как удобно. Скажу сразу, конфигурационных файла будет 2, по крайне мере я считаю, что это наиболее верный вариант описания конфигурации. 1-й файл будет включать конфигурацию по созданию бинов, подключение к БД, и пр., так сказать конфигурация контекста всего приложения. 2-й файл — это описание работы DispatcherServlet, в общем все, что связано уже с отображением страниц, и вообще c Spring MVC.
Начнем с описания контекста всего приложения:
src/main/resources/spring/applicationContext.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mongo="http://www.springframework.org/schema/data/mongo"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/data/mongo
http://www.springframework.org/schema/data/mongo/spring-mongo.xsd">
<!-- Включаем контекстные аннотации типа @Service, @Controller, @Repository... -->
<context:annotation-config/>
<!--
Указываем Springу пакет, в котором он будет искать классы,
помеченные аннотациями @Service, @Repository, и создавать их бины, но исключать он будет @Controller,
т.к. эти классы нам нужны будут в другом месте.
-->
<context:component-scan base-package="ru.habrahabr.sm">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!--
Загружает properties файл в конфигурацию Spring (т.е. сюда).
Переменные из файла можно будет использовать как ${mongo.host} (пример см. ниже)
-->
<context:property-placeholder location="classpath:database.properties"/>
<!-- Создаем бин 'mongo' -->
<mongo:mongo host="${mongo.host}" port="${mongo.port}"/>
<!--
Создаем бин 'mongoDbFactory'.
Если MongoDB не требует авторизации, то поля username, password можно убрать
-->
<mongo:db-factory
username="${mongo.username}"
password="${mongo.password}"
dbname="${mongo.db}"
mongo-ref="mongo"/>
<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
<constructor-arg name="mongoDbFactory" ref="mongoDbFactory"/>
</bean>
</beans>
Описание контекста Spring MVC:
src/main/resources/spring/dispatcherServlet.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- Включаем MVC аннотации -->
<mvc:annotation-driven/>
<!--
Использование MVC Resources
Проще говоря, все файлы из папки webapp/resources/ будут доступны по адресу: localhost/resources/
-->
<mvc:resources mapping="/resources/**" location="/resources/"/>
<!-- Указываем Spring MVC где искать классы-контроллеры -->
<context:component-scan base-package="ru.habrahabr.sm">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
</context:component-scan>
<!-- Указываем Spring MVC где будут лежать наши Viewшки, в данном случае это "/WEB-INF/pages/" -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/pages/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>
Отлично, Spring настроен, но если вы внимательно читали конфигурационные файлы, то заметили, что в applicationContext.xml мы тянули "classpath:database.properties". Это значит, что нужно создать файл «database.properties» со следующим содержанием:
src/main/resources/database.properties
mongo.host=localhost
mongo.port=27017
mongo.db=mydb
mongo.username=username
mongo.password=password
Разумеется, нужно заменить данные после знака '=' своими. Заметьте, что поля username, password необязательные, и если у вас не нужна авторизация для получения доступа к MongoDB, загляните в applicationContext.xml, там указано какие поля нужно убрать.
Сразу создадим конфигурационный файл для системы логгирования:
src/main/resources/log4j.properties
log4j.rootLogger=INFO, stdout
# Direct log messages to stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.conversionPattern=%d{dd.MM.yy HH:mm:ss} %5p - %m%n
log4j.appender.stdout.encoding=UTF-8
Наконец можно перейти к завершающему этапу конфигурирования Java Web приложения — описание web.xml:
src/main/webapp/WEB-INF/web.xml
<web-app version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<!-- Spring Application Context -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<!-- /Spring Application Context -->
<!-- Spring Dispatcher Servlet Context -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/dispatcherServlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- /Spring Dispatcher Servlet Context -->
<!-- Filters -->
<!-- Character Filter -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- /Character Filter -->
<!-- /Filters -->
</web-app>
На данном этапе у Вас уже должна быть следующая структура файлов в проекте:
В папку «src/webapp/resources/» можете класть любой статический контент (картинки, стили, js скрипты, и т.д.).
Написание кода
Наконец-то проект сконфигурирован, и можно приступить к написанию самого кода приложения. Начнем с описания модели и реализации слоя по работе с БД. Хочу отметить следующее: в MongoDB нет целочисленного AUTO_INCREMENT PK поля, а объекту по умолчанию присваивается ID такого вида: ObjectId(«5326b46f44ae9e6328b4566c»). Spring Data понимает этот ID как объект типа String. Проще говоря, если вам нужно сделать так, чтобы у вас ID объекта было целочисленным и авто увеличивающимся, то придется над этим поработать самостоятельно. Но на самом деле в этом нет ничего сложного и не стоит этого бояться, а я сейчас опишу как это делается! Если же Вас устраивает и длинный String в качестве ID объекта, то пропустите все классы (и соответственно их использование) в которых встречается слово «Sequence».Для начала создаем коллекцию в БД с именем sequences. И руками вносим туда объект (insert):
{
"_id" : "contacts",
"sequence" : 0
}
В этой коллекции мы будем хранить пару: «имя таблицы» — «последний ID», это значит, что для каждой таблицы(коллекции), в которой Вы хотите использовать нашу самописную AUTO_INCREMENT реализацию, вам нужно внести новую запись, как указано выше, только вместо «contacts» введите имя нужной Вам коллекции. Кстати, если еще не знаете какой инструмент(management-tool) использовать для MongoDB, то рекомендую Robomongo.Теперь создаем класс-обертку для этой коллекции:
Sequence.java
package ru.habrahabr.sm.model;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
/**
* Date: 26.03.2014
* Time: 15:38
*
* @author Ruslan Molchanov (ruslanys@gmail.com)
*/
@Document(collection = Sequence.COLLECTION_NAME)
public class Sequence {
public static final String COLLECTION_NAME = "sequences";
@Id
private String id;
private Long sequence;
public Sequence() {
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Long getSequence() {
return sequence;
}
public void setSequence(Long sequence) {
this.sequence = sequence;
}
}
Обратите внимание на запись "@Document(collection = Sequence.COLLECTION_NAME)" на самом деле можно было бы написать "@Document(collection = «sequences»)", но это уже скорее дело вкуса. Плюс есть одно явное достоинство у такого метода, дальше поймете какое.
Теперь опишем слой по работе с БД для класса-обертки "Sequence", скажу сразу, чтобы не растягивать статью, я не буду создавать интерфейсы для сервисов и DAO, буду пользоваться уже реализациями объектов (хотя это вроде не очень хорошо), надеюсь, что Вы понимаете о чем идет речь.
SequenceDao.java
package ru.habrahabr.sm.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.FindAndModifyOptions;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Repository;
import ru.habrahabr.sm.exceptions.SequenceException;
import ru.habrahabr.sm.model.Sequence;
/**
* Код взят с замечательного ресурса www.mkyong.com
* http://mkyong.com/mongodb/spring-data-mongodb-auto-sequence-id-example/
*/
@Repository
public class SequenceDao {
@Autowired private MongoOperations mongoOperations;
public Long getNextSequenceId(String key) {
// получаем объект Sequence по наименованию коллекции
Query query = new Query(Criteria.where("id").is(key));
// увеличиваем поле sequence на единицу
Update update = new Update();
update.inc("sequence", 1);
// указываем опцию, что нужно возвращать измененный объект
FindAndModifyOptions options = new FindAndModifyOptions();
options.returnNew(true);
// немного магии :)
Sequence sequence = mongoOperations.findAndModify(query, update, options, Sequence.class);
// if no sequence throws SequenceException
if(sequence == null) throw new SequenceException("Unable to get sequence for key: " + key);
return sequence.getSequence();
}
}
Теперь класс исключения, которое выбрасывается, если в нашей коллекции `sequences` не будет соответствующей записи в БД (о которой я писал выше):
SequenceException.java
package ru.habrahabr.sm.exceptions;
/**
* Date: 26.03.2014
* Time: 16:09
*
* @author Ruslan Molchanov (ruslanys@gmail.com)
*/
public class SequenceException extends RuntimeException {
public SequenceException(String message) {
super(message);
}
}
Теперь создаем в БД коллекцию `contacts`, и класс-обертку:
Contact.java
package ru.habrahabr.sm.model;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.io.Serializable;
/**
* Date: 26.03.2014
* Time: 15:29
*
* @author Ruslan Molchanov (ruslanys@gmail.com)
*/
@Document(collection = Contact.COLLECTION_NAME)
public class Contact implements Serializable {
public static final String COLLECTION_NAME = "contacts";
@Id
private Long id;
/* *******************************************************
Если вы хотите, чтобы ID объекта была автогенерируемая
строка (об этом я писал в посте), то опишите поле ID так:
@Id
private String id;
********************************************************* */
private String name;
private String number;
private String email;
public Contact() {
}
public Contact(String name, String number, String email) {
this.name = name;
this.number = number;
this.email = email;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
Теперь опишем DAO слой для нашего класса «Contact»:
ContactDao.java
package ru.habrahabr.sm.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Repository;
import ru.habrahabr.sm.model.Contact;
import java.util.List;
/**
* Date: 26.03.2014
* Time: 19:13
*
* @author Ruslan Molchanov (ruslanys@gmail.com)
*/
@Repository
public class ContactDao {
@Autowired private MongoOperations mongoOperations;
public void save(Contact contact) {
mongoOperations.save(contact);
}
public Contact get(Long id) {
return mongoOperations.findOne(Query.query(Criteria.where("id").is(id)), Contact.class);
}
public List<Contact> getAll() {
return mongoOperations.findAll(Contact.class);
}
public void remove(Long id) {
mongoOperations.remove(Query.query(Criteria.where("id").is(id)), Contact.class);
}
}
Хотелось бы отметить, что обновить запись в БД можно на уровне DAO следующим образом:
mongoOperations.updateFirst(query, update, Contact.class);
Разобраться с этим самостоятельно проще простого и не составит для Вас трудностей. Но хотелось бы отметить, что если вы выполните:mongoOperations.save(contact);
на объект, у которого ID уже выставлен, и существует в БД, то произойдет перезапись объекта в БД под указанным ID.Теперь опишем слой бизнес-логики для класса «Contact»:
ContactService.java
package ru.habrahabr.sm.services;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import ru.habrahabr.sm.dao.ContactDao;
import ru.habrahabr.sm.dao.SequenceDao;
import ru.habrahabr.sm.model.Contact;
import java.util.List;
/**
* Date: 26.03.2014
* Time: 20:09
*
* @author Ruslan Molchanov (ruslanys@gmail.com)
*/
@Service
public class ContactService {
@Autowired private SequenceDao sequenceDao;
@Autowired private ContactDao contactDao;
public void add(Contact contact) {
contact.setId(sequenceDao.getNextSequenceId(Contact.COLLECTION_NAME));
contactDao.save(contact);
}
public void update(Contact contact) {
contactDao.save(contact);
}
public Contact get(Long id) {
return contactDao.get(id);
}
public List<Contact> getAll() {
return contactDao.getAll();
}
public void remove(Long id) {
contactDao.remove(id);
}
}
Ну вот, собственно и все, все, что касается связки Spring + MongoDB мы проделали, т.е. теперь Вы можете пользоваться ORM Spring Data с уже подключенной к проекту MongoDB, но раз я обещал на примере справочника показать работу с БД, то осталось написать еще класс-контроллер:
MainController.java
package ru.habrahabr.sm.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import ru.habrahabr.sm.model.Contact;
import ru.habrahabr.sm.services.ContactService;
/**
* Date: 26.03.2014
* Time: 20:30
*
* @author Ruslan Molchanov (ruslanys@gmail.com)
*/
@Controller
public class MainController {
@Autowired private ContactService contactService;
@RequestMapping(value = "/", method = RequestMethod.GET)
public ModelAndView showAll() {
ModelAndView modelAndView = new ModelAndView("all");
modelAndView.addObject("contacts", contactService.getAll());
return modelAndView;
}
@RequestMapping(value = "/add", method = RequestMethod.GET)
public ModelAndView showAddForm() {
return new ModelAndView("add_form", "contact", new Contact());
}
@RequestMapping(value = "/add", method = RequestMethod.POST)
public String addContact(@ModelAttribute("contact") Contact contact) {
if(contact.getId() == null) contactService.add(contact);
else contactService.update(contact);
return "redirect:/";
}
@RequestMapping(value = "/edit", method = RequestMethod.GET)
public ModelAndView showEditForm(@RequestParam(required = true) Long id) {
return new ModelAndView("add_form", "contact", contactService.get(id));
}
@RequestMapping(value = "/delete", method = RequestMethod.GET)
public String deleteContact(@RequestParam(required = true) Long id) {
contactService.remove(id);
return "redirect:/";
}
}
Ну и вьюшки:
src/webapp/WEB-INF/pages/add_form.jsp
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Добавить контакт</title>
</head>
<body>
<form:form method="POST" action="/add" modelAttribute="contact">
<form:hidden path="id" />
<table>
<tr>
<td>Name:</td>
<td><form:input path="name" /></td>
</tr>
<tr>
<td>Number:</td>
<td><form:input path="number" /></td>
</tr>
<tr>
<td>E-mail:</td>
<td><form:input path="email" /></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" />
</td>
</tr>
</table>
</form:form>
</body>
</html>
Еще одна:
src/webapp/WEB-INF/all.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>Все контакты</title>
</head>
<body>
<table width="600px">
<tr>
<td><b>ID</b></td>
<td><b>Name</b></td>
<td><b>Number</b></td>
<td><b>E-mail</b></td>
<td><b>Action</b></td>
</tr>
<c:forEach var="contact" items="${contacts}">
<tr>
<td>${contact.id}</td>
<td>${contact.name}</td>
<td>${contact.number}</td>
<td>${contact.email}</td>
<td><a href="/edit?id=${contact.id}">Edit</a> | <a href="/delete?id=${contact.id}">Delete</a></td>
</tr>
</c:forEach>
<tr>
<td colspan="5">
<a href="/add">Добавить запись</a>
</td>
</tr>
</table>
</body>
</html>
Ну вот и все, собираем проект, запускаем, и вуаля:
Ссылка на исходники: github.com/ruslanys/sample-spring-mongodb
P.S. Буду счастлив, если кому-нибудь пригодится этот пост. Буду благодарен за подсказки и исправления.
UPD. По рекомендации товарищей с Хабра, занес все листинги под спойлер.