Свой блог я начал вести в 2010 году на сервисе блогов от Google, Blogger, и за 6 лет опубликовал около 180 постов. Должен сказать, среди блогохостингов лучше варианта не найти – полный доступ к редактированию шаблона, HTML-редактор, много настроек, интеграция с другими сервисами Гугла, большое количество полезных виджетов для блога. Но за это время я закончил школу, поступил в универ, устроился на работу, немножко научился программировать и подумал: зачем держать блог на готовой платформе, если я могу написать собственный движок?

Какие преимущества дает свой движок в сравнении с готовым от Google или ЖЖ? Это контроль над форматом хранения и обработки данных, возможность неограниченного расширения функционала. Например, для вывода списка заголовков постов с сортировкой по тегам (страница Содержание в моем старом блоге) используется скрипт на Javascript, который загружает целиком RSS блога и парсит из него заголовки, что является довольно тяжелой операцией. В своем движке я могу сформировать содержание на сервере и отдать браузеру готовый HTML.

В общем, это первый пост в разработанном мной блоге, добро пожаловать! Посты из старого блога вместе с комментариями перенесены сюда. Дальше в этом посте я расскажу о разработке и переезде.

Итак, я вооружился привычным по работе набором – Java, Spring Framework, Postgresql и Maven, и разработал себе бложик. Почему не на любимом C#/ASP.NET? Уж больно непросто найти дешевый/бесплатный хостинг с поддержкой этого фреймворка. Не так давно мне удалось раздобыть бесплатный хостинг для своего тестового проекта, предъявив студенческий билет, но вскоре после этого .masterhost прикрыл халяву, и тарифа для студентов у них больше нет, как и моего сайтика. В общем, найти Linux-хостинг для Java-приложения куда проще. Я воспользовался PaaS от Openshift, они бесплатно предоставляют 3 виртуальные машины с 1 Гб места, что для меня вполне достаточно.

Итак, блог представляет собой Web MVC приложение на фреймворке Spring Boot с дизайном на Bootstrap. Для шаблонов страниц был выбран движок Thymeleaf, который производит печальное впечатление после великолепного Razor, использующегося в ASP.NET MVC. Если последний позволяет писать на C# прямо в HTML коде, Thymeleaf предлагает свой довольно ограниченный язык выражений с использованием кастомных HTML-атрибутов. Это заставляет производить всю подготовку данных для отображения в коде контроллеров, что может быть не всегда удобным. Зато очень понравился модуль Spring Data JPA, избавляющий от необходимости реализовывать базовую работу с сущностями в базе данных. Это немного скрасило печаль от отсутствия альтернативы LINQ to Entities в Java. Также в конце поста немного расскажу о кэшировании в Spring. Больше о разработке написать особо нечего, кроме того, что у Spring'а хорошие гайды и документация. Исходный код блога доступен на Github.

Отдельной интересной задачей был перенос постов со старого блога. Blogger позволяет выгрузить бэкап шаблона, настроек и данных для блога в формате Atom (XML). В файле бэкапа каждая сущность (пост или настройка) находится в теге <entry></entry>:

<entry>
	<id>tag:blogger.com,1999:blog-5399870426561470322.post-5428309052430702807</id>
	<published>2012-04-06T04:48:00.001+04:00</published>
	<updated>2012-04-06T04:48:20.357+04:00</updated>
	<app:control xmlns:app='http://purl.org/atom/app#'>
		<app:draft>yes</app:draft>
	</app:control>
	<category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/blogger/2008/kind#post'/>
	<category scheme='http://www.blogger.com/atom/ns#' term='ярлык'/>
	<title type='text'>Название поста</title>
	<content type='html'>Содержание поста</content>
	<link rel='edit' type='application/atom+xml' href='#'/>
	<link rel='self' type='application/atom+xml' href='#'/>
	<author>
		<name>Ivan Lopatin</name>
		<uri>https://plus.google.com/111810495307545793719</uri>
		<email>noreply@blogger.com</email>
		<gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//lh3.googleusercontent.com/-PU2QEyFVKu8/AAAAAAAAAAI/AAAAAAAAFWY/S8x814Iw6hs/s32-c/photo.jpg'/>
	</author>
</entry>
Здесь на пост указывает вложенный тег <category></category> с атрибутом term='http://schemas.google.com/blogger/2008/kind#post'. Название и текст поста находятся в тегах <title></title и <content></content> соответственно. Вместе с постами я также переносил и их ярлыки, они находятся в тегах типа <category scheme='http://www.blogger.com/atom/ns#' term='ярлык'/>. Также важно отфильтровать черновики постов, у них присутствует тег с содержанием <app:draft>yes</app:draft>. Это все данные, которые мне были нужны, для их получения я написал программу на C#, которая разобрала XML и подложила посты в базу данных блога напрямую, ибо в C# есть язык запросов LINQ to XML, с которым парсинг XML не кажется таким невеселым занятием:
XDocument doc = XDocument.Load("E:\\blog-02-13-2016.xml");
XNamespace ns = "http://www.w3.org/2005/Atom";
XNamespace nsApp = "http://purl.org/atom/app#";
string postKind = "http://schemas.google.com/blogger/2008/kind#post";
var posts = (from e
             in doc.Descendants(ns + "entry")
             where e.Descendants(ns + "category")
             .Any(c => c.Attribute("term").Value == postKind)
             && e.Element(nsApp + "control") == null
             select e).ToList();
posts.Reverse();
var conn = new NpgsqlConnection("server=localhost;port=5432;username=postgres;password=password;database=blog");
conn.Open();
var insertPostsQuery = new StringBuilder();
var insertPostTagsQuery = new StringBuilder();
var tags = new List<string>();
for (int i = 0; i < posts.Count(); i++)
{
    var post = posts[i];
    string title = post.Element(ns + "title").Value;
    string body = post.Element(ns + "content").Value.Replace("'", "''");
    string dateTime = DateTime.Parse(post.Element(ns + "published").Value).ToString();
    insertPostsQuery.Append($"insert into posts (post_id, body, date, title) values ({i + 1}, \'{body}\', \'{dateTime}\', \'{title}\');");
    var postTags = (from t
                    in post.Descendants(ns + "category").Attributes("term")
                    where t.Value != postKind
                    select t.Value).ToList();
    postTags.ForEach(t =>
    {
        tags.Add(t);
        insertPostTagsQuery.Append($"insert into tag_post (post_id, tag_name) values ({i + 1}, \'{t}\');");
    });
}
(new NpgsqlCommand(insertPostsQuery.ToString(), conn)).ExecuteNonQuery();
var insertTagsQuery = new StringBuilder();
tags.Distinct().ToList().ForEach(t => insertTagsQuery.Append($"insert into tags (tag_name) values (\'{t}\');"));
(new NpgsqlCommand(insertTagsQuery.ToString(), conn)).ExecuteNonQuery();
(new NpgsqlCommand(insertPostTagsQuery.ToString(), conn)).ExecuteNonQuery();
conn.Close();

Помимо нового движка, блог также сменил и адрес, переехал с основного домена johnspade.ru на поддомен blog.johnspade.ru. Вследствие этого внешние ссылки, в первую очередь с паблика блога ВКонтакте, стали нерабочими. Немного изменив свою программу для разбора бэкапа блога, я составил список ссылок на посты и поиском по названиям подобрал соответствующие им идентификаторы записей в базе данных нового блога. Получился список соответствий вида { "/2014/02/cat.html": 170, "/2013/10/unknown-book.html": 169, ... }. На домене johnspade.ru развернул приложение, которое переадресовывает пользователей на посты в новом блоге в соответствии с таблицей.

Также в рамках переезда я решил сделать у себя аналоги виджетов из боковой колонки старого блога, в первую очередь список ярлыков. Хотелось по возможности обойтись без программирования на Javascript, поэтому реализовал его на Thymeleaf. Размер шрифта каждого ярлыка в списке зависит от количества постов с этим ярлыком. Сначала нужно задать границы размера шрифта, чтобы текст не был неприлично большим или маленьким, я взял значения от 80% до 160% базового размера. Это значит, что размер будет изменяться в интервале 80%. Далее нужно вычислить коэффициент, на который будет умножаться количество постов для каждого ярлыка, то есть разделить 80% на максимальное количество постов. Таким образом, название ярлыка с самым большим количеством постов всегда будет иметь размер 160%, а ярлык с одним постом – ~80%. Код виджета выглядит так:

<div class="sidebar-widget" th:if="${!#lists.isEmpty(tags)}"
     th:with="coef = ${80.0 / tags[0].posts.size()}">
    <h4>Ярлыки</h4>
    <span th:each="tag : ${tags}" th:if="${!#lists.isEmpty(tag.posts)}"
          th:style="'font-size: ' + ${#numbers.formatDecimal(80 + tag.posts.size() * coef, 0, 0)} + '%;'">
        <a th:href="@{/posts(tag = ${tag.name})}"><span th:text="${tag.name}" /></a>&nbsp;
        <span th:text="|(${tag.posts.size()}) |" />
    </span>
</div>

Виджет для архива по месяцам реализовать без Javascript не получилось, так как тег HTML5 <details /> для раскрывающегося списка пока что поддерживает только браузер Chrome. Для этого я использовал дополнение Bootstrap Tree View, которое поддерживает стили Bootstrap, а Thymeleaf позволил сформировать объект с данными для него в Java-коде на сервере и передать его прямо в JS-скрипт, очень удобно:

<script th:inline="javascript">
    /*<![CDATA[*/
    var data = /*[[${tree}]]*/ null;
    $('#tree').treeview({data: data, enableLinks: true, showBorder: false, showIcon: false, showTags: true});
    /*]]>*/
</script>

Теперь о кэшировании. Так как посты в блоге обновляются сравнительно нечасто, а данных хранится сравнительно немного, приложению совершенно незачем по каждому запросу лезть в базу данных, поэтому результат каждого обращения к БД кэшируется и потом отдается пользователям уже из памяти. В качестве отсечки для кэширования используется дата последнего сохранения данных (создание/редактирование/удаление поста). Кэширование в Spring настраивается очень просто – кэшируемые методы размечаются аннотацией @Cacheable. Результат вызова метода сохраняется в кэш и при последующих вызовах метода с такими же значениями параметров Spring сразу вернет результат без выполнения тела метода. Для этого генерируется ключ кэша по значениям параметров. Чтобы не вставлять дату последнего сохранения данных во все методы как неиспользуемый параметр, я переопределил генератор ключа кэширования, добавив отсечку в нем:

package ru.johnspade.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.cache.interceptor.SimpleKeyGenerator;

import java.lang.reflect.Method;

public class CacheKeyGenerator implements KeyGenerator {

	@Autowired
	private CacheService cacheService;

	@Override
	public Object generate(Object target, Method method, Object... params) {
		return SimpleKeyGenerator.generateKey(params, cacheService.getLastSaveTimestamp());
	}

}

На этом все, чтобы следить за новыми постами в этом блоге, предлагаю подписаться на страницу ВКонтакте или RSS-ленту.

Поиск