30 Nov 2013

"Правильные" прототипы. Путь Ninja.

Весь код можно посмотреть и скачать на GitHub

Основная идея Ninja framework это сразу приступать к работе, а не сидеть и настраивать проект. Часто прототипы это сделанные на скорую руку поделки для демонстрации и чаще всего сами прототипы не развиваются дальше, а пишется все по новой. Ну или если прототип изначально делался “правильно” и в нем все настроено (cache, i18n, миграция баз, конфиги приложения) он скорее всего будет дорабатываться в дальнейшем, но на него потребуется больше времени для разработки. На такую разработку обычно нет времени и все лепится на скорую руку, жестко забивают настройки приложения в код. Ninja как раз помогает пройти этап настройки и сразу приступить к разработке. Хотя я сам не люблю всякие комбайны, которые ограничивают тебя в выборе, но в данном случае, лично мне понравилось.

Документация
Документации там немного и как говорится - все по делу. Со всей докой можно за день ознакомится.

Лучше всего посмотреть на практике это.
Создаем Maven проект из архетипа mvn archetype:generate -DarchetypeGroupId=org.ninjaframework -DarchetypeArtifactId=ninja-servlet-jpa-blog-archetype
Не обязательно с него, просто полезно посмотреть как и что работает. я по удалял все сущности не нужные, их там пару штук.
Ну вот собственно и все, можно начинать разрабатывать) Мы получили кучу всяких зависимостей для разработки, например: jetty, freemarker, sl4j, ehcache, guice, flyway, h2 base, hibernate и др.

Для запуска приложения можно воспользоватся классом с main методом (**ninja.standalone.NinjaJetty**) ну или варку собирать.

Из приятного. Есть несколько оберток, которыми удобно пользоваться, заинжектим их в контроллер. Logger не является оберткой, но он тоже есть, настроен и им можно пользоваться.

@Inject
private Logger logger;

@Inject
private NinjaProperties ninjaProperties;

@Inject
private NinjaCache ninjaCache;

@Inject
private Messages messages;

Results (Result)

Удобная штука для веб приложений. Это билдер ответов. Собственно, что в нем есть.
У этого объекта есть несколько классов для создания Result. Они там разные, например:
Results.json() или Results.html().
Разобраться в нем просто, все понятно по названиям методов. В объект Result можно добавлять свои объекты, которые будут использоватmся в шаблонах (для генерации ответа).

Results.html().render("nameObj", object);

Шаблон можно указать самому
Results.html()
           .render("nameObj", object)
           .template("path/template.ftl.html")

или положить и назвать его в соответствии с соглашениями. Например в views/ApplicationController/index.ftl.html. Этот шаблон будет использоваться для метода index из контроллера ApplicationController.

Cache

Докумнетация
Для работы с кешом используется обертка NinjaCache. По-умолчанию используется реализация Ehcache, но так же можно использовать memcached. Для этого в конфиг (*application.conf*) надо прописать пару параметров:

cache.implementation=ninja.cache.CacheMemcachedImpl
memcached.host=127.0.0.1:11211
// user and password are optional
memcached.user=USER          
memcached.password=PASSWORD
java
Единственно memcached (в отличие от Ehcache) требует, чтобы объекты хранимые в кеше имплементили интерфейс Serializable. Посмотрим, как работать с оберткой. Тут все просто. Добавим в контроллер метод для теста.
...

private static String CACHE_KEY = "test_cache";

...

public Result testCache() {
        String test = (String) ninjaCache.get(CACHE_KEY);
        if(test == null){
           test = "new_object";
            ninjaCache.set(CACHE_KEY, test, "1d");
        }else{
            test += "_from_cache";
        }
        return Results.html();

Так же нужно прописать в Routes связь на тот метод:
@Override
    public void init(Router router) {  
     ...
     router.GET().route("/cache").with(ApplicationController.class, "testCache"); 
}

И шаблон для этой страницы (*testCache.ftl.html*).
<#import "../layout/defaultLayout.ftl.html" as layout> 
<@layout.myLayout "Test cache">    

    ${cacheObj}

</@layout.myLayout>

при изменения в шаблонах изменения сами подхватятся, просто перегрузить страницу достаточно

Собственно при обращении к http://localhost:8080/cache сначала создастся новый объект и он сохранится в кэше, при последующих запросах, он будет браться из кэша. Так же в методе добавления объекта в кэш, можно указать время хранения (по-умолчанию 1 месяц). Если объект уже там есть с таким ключом, то он заменится.

Configuration

Документация
Все свойства приложения лежат в файле application.conf. Есть стандартные свойства ninja и можно добавлять свои.
Добавим в application.conf строку

test.prop=Hello, world!

В контроллере возьмем это свойство и добавим в ответ
...
.render("prop", ninjaProperties.get("test.prop"))
java
В шаблоне index.ftl.html, просто выведем его
...
${prop}
...

открываем http://localhost:8080 и видим содеhжимое свойства.
Так же в конфиг файле можно указать для какого окружения это значение свойства. Если в конфиг файл добавить свойство test.prop с префиксом
...
%prod.test.prop=Hello, prod!
...

и запустить приложение с ключом -Dninja.mode=prod, то на странице отобразится именно это значение. Но если кто-то любит держать настройки прода отдельно или нужно предоставить возможность эксплуатации менять их, то можно запускать приложение с ключом -Dninja.external.configuration=conf/production.conf. Все свойства с теми же ключами в приложении будут заменены на значения из этого файла.

i18n

Документация
Еще одна полезная штука, о которой следует позаботится в начале пути.
Добавим тестовое сообщение в messages.properties.

...
test=Тест
...

В шаблоне к нему можно обратится так:
...
${i18n("test")}
...

В приложении:
...
messages.get("test", Optional.of("en")).get()
...

Если файла для такой локали не найдено, то берется та, что по умолчанию.

Read more
08 Nov 2013

Хранение и шаринг сессий между приложениями

Идея статьи появилась вместе с задачей поставленной. В принципе была задача попробовать реализовать это и посмотреть, чем это будет сделать удобнее. В качестве языка был выбран Groovy, а точнее фреймворк Grails. Но конечная реализация будет такая же как и для Java, просто на Grails сделать это быстрее.

Задача звучала так: сделать прототип архитектуры на связке Grails + Jetty, web сессии должны шарится между всеми запущенными нодами. Хранение и шаринг только сессий.

Terracotta

Первым смотрел именно ее. После пары дней серфинга по их сайту остался негативный осадок :) Ну правда, очень все не структурировано и не понятно, создается ощущение, что они пытаются спрятать правду. Подробно описывать, что получилось не буду, так как этот вариант не подошел. Необходимо было, чтобы на разных серверах крутился сервер Terracota, и это сделать в принципе можно, проблема в том, что лишь одна нода является активной (**coordinate active Terracotta server**), все остальные лишь репликами, который в случае загибания выбирают нового вождя и слушаются его. При этом, на этих же серверах стояли бы экземпляры приложения и задумывалось, что каждый экземпляр будет использовать именно тот сервер Terracota, который установлен на той же машине. Но так не получилось, потому что можно подключится лишь к активному серверу. С помощью Terracota, это можно сделать, для этого необходимо в конфиге группы зеркал, можно посмотреть тут раздел Scaling the Terracotta Server Arraу. Реализовать это в старых версиях нельзя, но в продуктах BigMemory это есть. Лицензию они на почту присылают после скачивания.

Hazelcast

После неудачи с терракотой начал пробовать этот продукт, версия 3.1. Собственно оказалось все очень просто. Далее опишу несколько шагов для создания проекта. В принципе все просто и IDE не понадобится, все делалось из консоли.

Создание Grails приложения

Grails используется версии 2.3.1.Тут все просто.

grails create-app grails-hazelcast

После инициализации приложение нужно поправить скрипт для сборки приложения (я пользуюсь vim, вы редактируйте чем угодно):
cd grails-hazelcast
vim grails-app/conf/BuildConfig.groovy

Для начала удаляем плагин для томката и добавляем для jetty. Так же добавляем зависимость для Hazelcast:
dependencies {
        ...
        compile "com.hazelcast:hazelcast-all:3.1"
        ...
    }

plugins {
       ...
        build ":jetty:2.0.3"
       ...
}

Добавим наш пакет в конфиг для логгера:
vim grails-app/conf/Config.groovy

log4j = {
   ...
    info   'grails.hazelcast'
   ...
}

Также необходимо установить шаблоны в него, так как нам понадобится изменить web.xml
grails install-templates
vim src/templates/war/web.xml

что нужно прописывать описано в документации на сайте, раздел Http Session Clustering with HazelcastWM (смотрите нужную версию документации).
таким образом добавляем описанную там структуру в наш web.xml, единственно что изменил, это параметр указывающий конфигурационный файл config-location. И параметр map-name указывает имя коллекции, куда будут сохранятся сессии.

<filter>
    <filter-name>hazelcast-filter</filter-name>
        <filter-class>com.hazelcast.web.WebFilter</filter-class>
     
        <init-param>
            <param-name>map-name</param-name>
            <param-value>my-sessions</param-value>
        </init-param>
     
        <init-param>
            <param-name>sticky-session</param-name>
            <param-value>true</param-value>
        </init-param>

        <init-param>
            <param-name>cookie-name</param-name>
            <param-value>hazelcast.sessionId</param-value>
	</init-param>


    
        <init-param>
          <param-name>cookie-secure</param-name>
          <param-value>false</param-value>
	</init-param>
 
        <init-param>
          <param-name>cookie-http-only</param-name>
          <param-value>false</param-value>
        </init-param>
       
       <init-param>
            <param-name>debug</param-name>
            <param-value>true</param-value>
        </init-param>
 
        <init-param>
          <param-name>config-location</param-name>
          <param-value>/WEB-INF/hazelcast.xml</param-value>
        </init-param>
 
        <init-param>
          <param-name>instance-name</param-name>
          <param-value>default</param-value>
       </init-param>

        <init-param>
        <param-name>use-client</param-name>
        <param-value>false</param-value>
       </init-param>

       <init-param>
         <param-name>shutdown-on-destroy</param-name>
         <param-value>true</param-value>
       </init-param>
</filter>
<filter-mapping>
    <filter-name>hazelcast-filter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>FORWARD</dispatcher>
    <dispatcher>INCLUDE</dispatcher>
    <dispatcher>REQUEST</dispatcher>
</filter-mapping>

<listener>
    <listener-class>com.hazelcast.web.SessionListener</listener-class>
</listener>

Теперь собственно нужно создать этот конфиг Hazelcast (откуда брал не помню, или из доки или из примеров):

vim web-app/WEB-INF/hazelcast.xml

<hazelcast xsi:schemaLocation="http://www.hazelcast.com/schema/config
    http://www.hazelcast.com/schema/config/hazelcast-config-3.1.xsd"
    xmlns="http://www.hazelcast.com/schema/config"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

    <group>
        <name>dev</name>
        <password>dev-pass</password>
    </group>
    <network>
        <port auto-increment="true">5701</port>
        <join>
            <multicast enabled="true">
                <multicast-group>224.2.2.3</multicast-group>
                <multicast-port>54327</multicast-port>
            </multicast>
            <tcp-ip enabled="false">
                <interface>127.0.0.1</interface>
            </tcp-ip>
            <aws enabled="false">
                <access-key>my-access-key</access-key>
                <secret-key>my-secret-key</secret-key>
                <region>us-west-1</region>
                <security-group-name>hazelcast-sg</security-group-name>
                <tag-key>type</tag-key>
                <tag-value>hz-nodes</tag-value>
            </aws>
        </join>
        <interfaces enabled="false">
            <interface>10.56.10.*</interface>
        </interfaces>
        <ssl enabled="false" />
        <socket-interceptor enabled="false" />
        <symmetric-encryption enabled="false">
            <algorithm>PBEWithMD5AndDES</algorithm>
            <salt>thesalt</salt>
            <password>thepass</password>
            <iteration-count>19</iteration-count>
        </symmetric-encryption>
        <asymmetric-encryption enabled="false">
            <algorithm>RSA/NONE/PKCS1PADDING</algorithm>
            <keyPassword>thekeypass</keyPassword>
            <keyAlias>local</keyAlias>
            <storeType>JKS</storeType>
            <storePassword>thestorepass</storePassword>
            <storePath>keystore</storePath>
        </asymmetric-encryption>
    </network>
    <partition-group enabled="false"/>
    <management-center enabled="false" update-interval="3" >http://127.0.0.1:8080/mancenter</management-center>
    <executor-service>
        <core-pool-size>16</core-pool-size>
        <max-pool-size>64</max-pool-size>
        <keep-alive-seconds>60</keep-alive-seconds>
    </executor-service>
    <queue name="default">
        <max-size-per-jvm>0</max-size-per-jvm>
        <backing-map-ref>default</backing-map-ref>
    </queue>
    <map name="default">
        <backup-count>1</backup-count>
        <time-to-live-seconds>0</time-to-live-seconds>
        <max-idle-seconds>0</max-idle-seconds>
        <eviction-policy>NONE</eviction-policy>
        <eviction-percentage>25</eviction-percentage>
        <merge-policy>hz.ADD_NEW_ENTRY</merge-policy>
    </map>
    <properties>
        <property name="hazelcast.logging.type">log4j</property>
    </properties>
</hazelcast>

Менялись параметры <interface>10.56.10.*</interface> тут указать свою сеть. И еще возможно следует отметить параметр management-center, там указывается ссылка на консоль для мониторинга. Проблема правда, что в бесплатной версии она работает с максимум двумя экземплярами. Скачать можно тут, в папке bin есть скрипт для запуска.

Теперь создадим контроллер для тестирования:

grails create-controller TestSessions
vim grails-app/controllers/grails/hazelcast/TestSessionsController.groovy
package grails.hazelcast

import com.hazelcast.client.*
import com.hazelcast.config.*
import com.hazelcast.core.*
import groovy.util.logging.*

@Log
class TestSessionsController {

    def index() {
        session.setAttribute("testAttr","testVal");
        Config cfg = new Config();
        HazelcastInstance hz = Hazelcast.newHazelcastInstance(cfg);
        IMap map = hz.getMap("my-sessions");
        log.info "SIZE========"+map.size();
    }
}

Собственно все, при каждом обращении он будет создавать новый экземпляр Hazecast (который будет взаимодействовать с уже созданными, если таковые имеются в указанной в конфиге сети). В сессию кладем аттрибут, чтобы там что либо лежало, так как сохраняются в мапу, только сериализуемые объекты.
Собираем варку и кладем ее в jetty (или в несколько экземпляров jetty, так же возможно на разные машины):

grails war

Кому лень все делать, ссылка на GitHub
Запускаем и радуемся.

Read more