07 Nov 2013

Рецензия на JBehave

Резюме: в последнее время все большей популярностью начинают пользоваться BDD фреймворки для тестирования, однако не каждый из них так хорош как кажется.

О BDD тестах

Сначала поясню что такое BDD (Behaviour Driven Development) и какая идея стоит за BDD тестами. Так вот, требования можно записывать по-разному, и одной из форм является Given-When-Then стиль, например:

Given User is registered and logged in, when she posts an article, article count gets incremented.

Идея BDD тестов в том, чтоб использовать такое требование как тест сценарий. Таким образом если мы автоматизируем его проверку, то это одновременно станет как нашей документацией, так и достаточно читаемым тестом.

Как JBehave реализует BDD тесты

Сначала описывается сценарий в простом текстовом файле называемом Историей:

Scenario: When user write an article, her article count increments

Given user is registered
And user is logged in
When user posts an article
Then user article count is incremented

Каждый из этих шагов мапится на метод в так называемых Step’ах в Java классах:

@Given("user is registered")
public void userIsRegistered(){}

@Given("user is logged in")
public void userIsLoggedIn(){}

@When("user posts an article")
public void writeAnArticle() {}

@Then("user article count is incremented")
public void assertArticleCountIncremented(){}

Ну и в этих классах уже можно колбасить логику. Тут поддерживается передача параметров, переиспользование историй, композитные шаги, мета-фильтры для включения/выключения историй из прогона и пр. В общем фич не так уж мало.

Проблемы JBehave

Неожиданно, когда начинаешь сам писать тесты, сталкиваешься с двумя проблемами, о которых сначала и не думал.

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

Во-вторых, у сценария нет контекста! Ведь никто не подумал а как мы создадим пользователя в первом шаге, а затем будем им же логиниться во втором и писать статьи в третьем. А оказывается между шагами нужно хранить состояние, но JBehave (кстати как и Fitnesse) при всем количестве фич не поддерживает этого. И приходится вставлять очень болезненные костыли - хранить состояние каждого тест кейса в глобальном хранилище (мапа?). А ведь все не так просто - один и тот же шаг может переиспользоваться в разных сценариях, и предыдущие шаги в этих сценариях могут быть разными - они могут как создавать какое-то состояние, так и не делать этого. И как в нашем текущем шаге определить намеревался ли предыдущий метод сохранить данные или нет?

В общем это достаточно роковые недостатки из-за которых я разочаровался в JBehave. Особенно учитывая тот факт, что фреймворк в последнее время практически не развивается.

Альтернативы

Сначала стоит заметить что на голом TestNG тесты выходят намного компактней и приятней, хоть и об отчетах приходится думать “вручную”.

А еще есть с первого взгляда вкусные фреймворки, которые лишены перечисленных недостатков, такие как основанный на Groovy easyb:

scenario "null is pushed onto empty stack", {
  given "an empty stack",{
    stack = new Stack()
  }

  when "null is pushed", {
    pushnull = {
      stack.push(null)
    }
  }

  then "an exception should be thrown", {
    ensureThrows(RuntimeException){
      pushnull()
    }
  }

  and "then the stack should still be empty", {
    stack.empty.shouldBe true
  }
}

Снова Groovy-based фреймворк Spock:

def "HashMap accepts null key"() {
  setup:
  def map = new HashMap()
  
  when:
  map.put(null, "elem")
  
  then:
  notThrown(NullPointerException)
}

Сам я с этими фреймворками не работал и не знаю их недостатков, но как минимум у них есть test case scope.

Read more
07 Nov 2013

Рецензия на ZK Framework

Резюме: после некоторого использования ZK Framework он оказался хорошим решением для очень простых приложений, однако в реальной жизни проявились его неприятные особенности (здесь я приведу опыт 2012 года, к текущему времени могло что-то поменяться). В общем отрицательных впечатлений намного больше, рекомандовать этот фреймворк не могу.

Отрицательные стороны

Ужасное community, такое же как и код. Платную поддержку не использовал и сказать ничего о ней не могу, скажу о бесплатной. Форум у ZK присутствует, и даже ответы какие-то есть. Однако из тех вопросов, которые я там задавал (а их было штук 10) почти ни на один отвечено не было. Это не такая большая проблема если исходники на руках, однако в них такой г-код, что понять как что работает очень сложно. К тому же все построено на событиях и есть некий Event Bus, что еще более усложняет понимание внутреннего исходного кода.

Также мало кто использует этот фреймворк по сравнению с теми же JSF, Spring MVC, поэтому помощи обычно ждать не откуда.

MVVM sucks. MVVM (Model-View-ViewModel) - это подход, когда у страниц есть маппинг к определенному объекту и при обновлении состояния Вида обновляется и объект. Ну и наоборот. В демках выглядит все просто прекрасно (собсно поэтому и решился использовать этот MVVM), вот Вид:

<textbox value="@load(vm.person.name) @save(vm.person.name, before='save')"/>

Тот сохраняет значение поля в vm.person.name, оттуда же и берет значение для заполнения UI. Все круто пока не сталкиваешься с реальной жизнью, когда UI становится чуть сложней чем элементарный. Есть несколько проблем у этого подхода:

  • Он использует состояние в памяти. В отличие от MVC, который обычно просто сохраняет данные на каждый запрос в БД, MVVM как правило хранит все в памяти. От этого вы вскоре достаточно быстро начнете испытывать проблемы, потому как состояния хранить нужно будет все больше.

  • В сложных (да даже обычных) UI обычным binding’ом не обойтись - нужно какой-то pre/post processing, нужно обновлять 10 других мест. Да, это все возможно и здесь, только опять же, потому что изменять нужно не одно место (хранилище данных), а много (разные вьюхи, которые в памяти храняться, то же хранилище данных), это становится задачей несколько нетривиальной.

Более детальные негативные отзывы вы можете почитать в блогах .Net разработчиков (которые тоже в свое время пропагандировали этот шаблон).

На самом деле ZK поддерживает как MVVM, так и MVC с MVP, однако больше всего они счас занимаются именно MVVM и его PR’ят. Собственно не ведитесь на удочку.

ZK позволяет разрабатывать только простой UI. Вообще ZK себя зарекомендовал как фреймворк для админок. Потому что там особо не надо выпендриваться со сложным и красивым UI. Простые элементы - это конечно хорошо, но даже для админок иногда приходиться сделать что-то более сложное, чем combobox. В такие моменты приходится разбираться не только с JavaScript’ом, но и с тем как писать компоненты для ZK.

Дружелюбность SEO. ZK приложения как правило одностраничны и под это заточен фреймворк. Проблема в том, что такие приложения либо не индексируются вовсе, либо индексируются плохо. Особенно учитывая что HTML у ZK выходит до визга сложный. У Zkoss есть некоторые наработки в этом направлении, однако в их эффективность верится мало.

Положительные стороны

Никакой компиляции Java->JS. В отличие от GWT/Vaadin, ZK framework не генерирует никаких JS файлов из Java классов, что не замедляет разработку таких приложений.

Быстрый UI. Если посмотреть на тот же Vaadin, то создается впечатление заторможенности. Он достаточно медленно реагирует на мышь. А вот что мне понравилось в ZK - так это быстрое время отклика.

Read more
08 Aug 2013

Использование ThreadLocal переменных

Введение

Вы уже наверно знаете, что поля классов в java бывают статические и не статические. Любое поле класса без модификатора static принадлежит объекту данного класса и создается каждый раз когда создается новый экземпляр класса. Статические переменные(помеченные модификатором static) не принадлежат экземпляру класса и существует всегда в единственном экземпляре независимо от того, сколько экземпляров класса было создано. Появившийся в java 1.2 класс java.lang.ThreadLocalпо сути предоставляет нам ещё одну область жизни объектов, ThreadLocal предоставляет абстракцию над переменными локальными по отношению к потоку испольнения java.lang.Thread. ThreadLocal переменные отличаются от обычных переменных тем, что у каждого потока свой собственный, индивидуально инициализируемый экземпляр переменной, доступ к которой он получает через методы get() или set().

Я в своей практике встречался с четырьмя основными целями применения ThreadLocal переменных:
1. Упрощение API.
2. Cинтаксический сахар.
3. Кеширование непотокобезопасных(non thread safe) ресурсов.
4. Уменьшение области конкуренции между потоками(lock striping).

Упрощение API с помощью ThreadLocal.

Допустим Вы разрабатываете JEE веб приложение, после прохождения аутентификации на странице логина информация о пользователе запоминается в http сессии, и Вам в любой точке кода может понадобится информация о пользователе от которого пришел http запрос.
Наверняка Вы не захотите всю логику помещать в сервлеты и JSP, Вы выделети в приложение несколько слоев(бизнесс логика, доступ к данным и.т.д.), но после распределния ответсвенности по слоям у Вас может возникнуть проблема с тем, что не в каждой точке кода будет доступ к Http сессии, соответсвенно не везде можно будет узнать от какого пользователя пришел запрос.
Встает вопрос? а как проектировать свой API? Добавлять в каждый метод каждого класса дополнительный параметр представляющий данные пользователе?

До появления ThreadLocal это был единственный выход, с появлением же ThreadLocal мы можем привязать данные о пользователи к потоку обработки http запроса, и достать эту информацию в любом месте программы. Для этого нам понадобится зарегистрировать слушателя в контексте веб приложения который будет срабатывать на любой входящий http запрос:

Итак начнем с класса для реализации потокобезопсного контеста пользователя:

package ru.javatalks;

/**
 * @author Vermut
 *
 */
public class SecurityContextHolder {
	
	private static final ThreadLocal<User> threadLocalScope = new  ThreadLocal<>();
	
	public final static User getLoggedUser() {
		return threadLocalScope.get();
	}
	
	public final static void setLoggedUser(User user) {
		threadLocalScope.set(user);
	}

}

Как мы знаем сервера приложений могут выполнять тысячи запросов одновременно и на первый взгляд такой код не потоко безопасен. Ведь действительно мы запоминаем текущего пользователя в статической переменной, а статическая переменная одна на весь класс, и вроде бы как паралельные http запросы должны перетирать данные друг друга.
Но вся фишка ThreadLocal заключается в том что имея всего одну ThreadLocal переменную, мы можем иметь различное значение для каждого из потоков, то есть один поток никогда не прочтет, удалит или не перезатрет данные присвоенные другим потоком. Таким образом несмотря на разделяемую статическую переменную код выше потоко-безопасен.

Хранилище аутентификацционных данных написано, теперь нужно написать и сконфигурировать в web.xml слушателя входящих HTTP запросов, который бы при поступлении запроса присоеденяя данные о пользователе к потоку обработки а по завершении обработки запроса, очищал бы эту информацию.

package ru.javatalks;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
 * @author Vermut
 *
 */
public class AuthentificationPropagationListener implements ServletRequestListener {

	@Override
	public void requestInitialized(ServletRequestEvent event) {
		HttpServletRequest request = (HttpServletRequest) event.getServletRequest();
		HttpSession session = request.getSession(false);
		if (session == null) {
			return;
		}
		User user = (User) session.getAttribute("loged_user");
		SecurityContextHolder.setLoggedUser(user);
	}
	
	@Override
	public void requestDestroyed(ServletRequestEvent event) {
		SecurityContextHolder.setLoggedUser(null);
	}

}

Дело осталось за малым воспользоваться написаными функционалом из любой точки приложения в которой нет доста к http запросу или сессии:

package ru.javatalks;

/**
 * @author Vermut
 *
 */
public class OrderService {
	
	public void createOrder(Order order) {
		User user = SecurityContextHolder.getLoggedUser();
		checkEligibility(user, order.getSum());
		order.setUserId(user.getId())
		...
	}

}

Конечно же функционал реализованный выше редко когда придется писать самому, есть множество библиотек связанных с security в которых это уже реализовано, например spring-security, стоит только иметь в виду что все они будут точно также работать посредством ThreadLocal. Построение API вокруг ThreadLocal широко используется в java enterprise edition и используется не только для ассоциирования контекста безопасности с потоком, но и для других вещей как например транзакции, открытые JPA сессии. Так же хочу заметить что использование ThreadLocal в JEE окружении сопряжено с возникновением многих проблемам и без глубокого понимания платформы jee, многопоточности и механизма загрузки классов от использования ThreadLocal в JEE лучше отказаться. Проблемы порождаемые ThreadLocal переменными в JEE окружении описаны в конце статьи.

2. Cинтаксический сахар или программирование на языке.

ThreadLocal можно использовть для добавления синтаксического сахара в язык java и многие библиотеки этим пользуются, например mybatis:

private String selectPersonSql() { 
    BEGIN(); // Clears ThreadLocal variable 
    SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FULL_NAME"); 
    SELECT("P.LAST_NAME, P.CREATED_ON, P.UPDATED_ON"); 
    FROM("PERSON P"); 
    FROM("ACCOUNT A"); 
    INNER_JOIN("DEPARTMENT D on D.ID = P.DEPARTMENT_ID"); 
    INNER_JOIN("COMPANY C on D.COMPANY_ID = C.ID"); 
    WHERE("P.ID = A.ID"); 
    WHERE("P.FIRST_NAME like ?"); 
    OR(); 
    WHERE("P.LAST_NAME like ?"); 
    GROUP_BY("P.ID"); 
    HAVING("P.LAST_NAME like ?"); 
    OR(); 
    HAVING("P.FIRST_NAME like ?"); 
    ORDER_BY("P.ID"); 
    ORDER_BY("P.FULL_NAME"); 
    return SQL(); 
  }

Код получился легко читаемым, как видно функции BEGIN, SELECT и.т.д. не вызываются ни на одном объекте, то есть они статические и за счет статического импорта появившегося в java 5, вызов таких функций можно осуществлять без префикса класса. Потоко-безопасность достигается за счет того что каждый поток выполнения имеет собственный экземпляр билдера запросов. Конечно следует ожидать, что c появлением лямбд в java 8, использование ThreadLocal в качестве синтаксического сахара потеряет свою актуальность

Кеширование непотокобезопасных(non thread safe) ресурсов.

Однажды делая код ревью одного класса я обнаружил очень интересный баг многопоточности:

public class DateAdapter extends XmlAdapter<String, Date> {

    private static final DateFormat format = new SimpleDateFormat("dd.MM.yyyy");

    @Override
    public String marshal(Date value) throws Exception {
        return format.format(value);
    }

    @Override
    public Date unmarshal(String value) throws Exception {
        return isNullOrEmpty(value)? null: format.parse(value);  
    }

}

Класс использовался как кастомный адаптер для даты в JAXB. Вроде бы простой маленьки класс и в нём негде ошибится, однако есть одно но, класс java.text.SimpleDateFormat не является потоко безопасным, параллельные потоки должны либо синхронизировать доступ к инстансу объекта данного класса, либо отказаться от разделения одного инстанса SimpleDateFormat.
То есть просто создать один экземпляр формата и запомнить в статической переменной нельзя, иначе мы получим мусор на выходе если форматировать даты паралельно из нескольких потоков. Честно говоря в приложении рассчитаном на входящий поток данных 5 тысяч входящих документов в секунду, ни генерировать мусор создавая каждый раз новый экземпляр формата, ни тем более создавать бутылочное горлышко в виде synchronized блоков мне не хотелось, и поскольку стояло жесткое требование по максимуму отказаться от библиотек не входящих в j2se, то есть нельзя было использовать сторонние реализации форматеров то код выше превратился в следующее:
/**
 *
 * @author Vermut
 *
 */
public final class DateAdapter extends XmlAdapter<String, Date> {

    private static final ThreadLocal<DateFormat> THREAD_CACHE = new ThreadLocal<DateFormat> ();

    @Override
    public String marshal(Date value) throws Exception {
        return getFormat().format(value);
    }

    @Override
    public Date unmarshal(String value) throws Exception {
        return isNullOrEmpty(value)? null: getFormat().parse(value);
    }

    private static DateFormat getFormat() {
        DateFormat format = THREAD_CACHE.get();
        if (format == null) {
            format = new SimpleDateFormat("dd.MM.yyyy");
            THREAD_CACHE.set(format);
        }
        return format;
    }

}

Как видно обеспечено кеширование объектов DateFormat без синхронизации. В статье Java Best Practices – DateFormat in a Multithreading Environmentприведены результаты бенчмарков показывающих что такой подход позволяет увеличить производительность парсинга дат до 8 раз по сравнению с созданием каждый раз нового экземпляра формата.

Сужение области конкуренции между потоками(lock striping).

Lock striping техника представления сложного объекта, к которому осуществляется конкуретный доступ в виде отдельных маленьких частей, каждую часть такого объекта можно менять без блокировки целого объекта. Например техника lock striping применена в CuncurrentHashMap - вся коллекция разбита на регионы, и треды при модификации не конкурируют за всю коллекцию целиком, они конкурируют за её отдельные регионы, таким образом острота конкуренции снижается.

Прежде всего для этого параграфа хотелось бы сразу поместить disclaimer и cсылку на эту презентацию
java8 новинки в java.util.concurrent, в презентации авторитетные специалисты в области java оптимизации в квалификации которых не возникает ни каких сомнений, крайне не рекомендуют использовать ThreadLocal для сuncurrent оптимизаций.

Однако пока java8 ещё не зарелизена, а в продакшн энтерпрайз приложений java8 попадет вообще не скоро, то пример использования ThreadLocal я всё же опубликую.

И так представим высоконагруженное приложение с тысячами рабочих потоков. Потоки занимаются тем что обрабатывают пачки входящих документов и нам бы хотелось собирать некоторую статистику о работе приложения, а конкретно количество обработанных документов. Но при этом мы бы хотели чтобы сбор статистики обходился нам бесплатно, и не вносил бы лишних нагрузку в приложение.
Какие сть варианты решения:
Выполнять запрос SELECT COUNT(*) FROM TABLE в базу данных - на таких объемах не очень удачное решение.
Заводить AtomicLong - поможет на небольшом количестве потоков, но не на тысячах.

Итак применим ThreadLocal. Общее количество обработанных сервером документов можно представить как сумму обработанных документов каждым рабочим потоком. То есть к каждому треду можно поставить соответсвии число, и он будет увеличивать это число вообще без конкуренции с каким либо другим потоком, то есть как мы и хотели сбор статистики обходится нам бесплатно.

package ru.javatalks;

import java.util.ArrayList;
import java.util.List;

/**
 * @author Vermut
 *
 */
package ru.javatalks;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @author Vermut
 *
 */
public final class ThreadLocalAdder {
	
	private final ThreadLocal<SumContainer> threadLocalScope = new ThreadLocal<>();
	private final List<SumContainer> allThreadSums = new ArrayList<>();
	private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
	
	public void add(long value) {
		SumContainer threadLocalSum = threadLocalScope.get();
		if (threadLocalSum == null) {
			threadLocalSum = new SumContainer();
			threadLocalScope.set(threadLocalSum);
			/*
			 * Самое первое получение локального блокирует подсчет общей суммы,
			 *  но это критично только пока приложение не войдет в рабочий ритм, 
                          * в    разогнавшемся приложении уже каждый поток хоть раз да проинкрементил свой счетчик.
			 */
			readWriteLock.writeLock().lock();
			try {
				allThreadSums.add(threadLocalSum);
			} finally {
				readWriteLock.writeLock().unlock();
			}
		}
		threadLocalSum.value += value;
	}
	
	public long getSum() {
		long sum = 0L;
		/*
		 * Как видно подсчет суммы медленная операция, так как сумма не хранится в готовом для чтения виде,
		 * что в принципе вписывается в концепцию lock striping, и не является критичным моментом в данном конкретном случае,
		 * так как админы приложения могут и несколько дней не заглядывать в перфоманс монитор.
		 */ 
		readWriteLock.readLock().lock();
		try {
			for (SumContainer threadLocalSum : allThreadSums) {
				sum += threadLocalSum.value;
			}
		} finally {
			readWriteLock.readLock().unlock();
		}
		return sum;
	}
	
	private static class SumContainer {
		/*
		 * Поле специально не volatile и не атомик, потому что абсолютная точность не нужна,
		 * допустимо чтобы запрос на получение статиcтики не увидел последних инкрементов сделанных рабочим потоком.
		 * Таким образом мы добились чего хотели, сбор статистики сделан бесплатным, его стоимость равна чтению из
		 * обычного (несинхронизированного) хешмапа коим по сути является ThreadLocal
		 */
		public long value;
		
	}

}

Для сокращения кода и упрощения понимания концепции ThreadLocal, из кода удалена обработка ситуаций внезапной смерти потоков. По хорошему нужно обрабатывать ситуацию, когда ThreadLocal собирается сборщиком мусора потому, что поток которому она принадлежит более недостижим из корня, но чтобы не отвлекать внимание читателя в сторону PhantomReference обработка этих ситуаций из кода удалена, а аспекты взаимодействия ThreadLocal и сборки мусора вынесены в отдельный параграф.

А вот небольшой тест показывающий применение такого аккомулятора:

package ru.javatalks;

/**
 * @author Vermut
 *
 */
public class ThreadLocalAdderTest {
	
	private static final int OBSERVER_SLEEP_TIMEOUT = 1000;
	private static final long ITERATION_COUNT_PEER_THREAD = 1_000_000_000l;
	private static final int THREAD_COUNT = 10;
	private static final long INCREMENT_COUNT = 10;
	
	
	public static void main(String[] args) throws InterruptedException {
		ThreadLocalAdder threadLocalAdder = new ThreadLocalAdder();
		AdderThread[] threads = new AdderThread[THREAD_COUNT];
		for (int i = 0; i < THREAD_COUNT; i++) {
			threads[i] = new AdderThread(threadLocalAdder);
			threads[i].start();
		}
		int aliveThreadCount = THREAD_COUNT;
		long previousSum = 0l;
		long sum = threadLocalAdder.getSum();
		long sumNotChangedBeetweenIterationCount = 0l;
		long iterationCount = 0;
		while (aliveThreadCount > 0) {
			Thread.sleep(OBSERVER_SLEEP_TIMEOUT);
			iterationCount ++;
			aliveThreadCount = 0;
			for (AdderThread thread : threads) {
				if (thread.isAlive()) {
					aliveThreadCount ++;
				}
			}
			sum = threadLocalAdder.getSum();
			if (previousSum == sum) {
				sumNotChangedBeetweenIterationCount++;
			}
			System.out.println(iterationCount + " : " + sum + " : " +  sumNotChangedBeetweenIterationCount);
			previousSum = sum;
		}
		System.out.println(sum == ITERATION_COUNT_PEER_THREAD * THREAD_COUNT * INCREMENT_COUNT);
	}
	
	private static final class AdderThread extends Thread {
		
		private ThreadLocalAdder adder;

		public AdderThread(ThreadLocalAdder adder) {
			this.adder = adder;
		}
		
		@Override
		public void run() {
			for (long i = 0; i < ITERATION_COUNT_PEER_THREAD; i++) {
				adder.add(INCREMENT_COUNT);
			}
		}
	}

}

Read more
03 Jul 2013

Testing Worst Practices

Резюме: при достаточной популярности написания тестов (особенно модульных), появляются очень разнообразные “лучшие практики” в связи с этим. Однако похоже многие разработчики (даже очень опытные) все-таки не понимают как не нужно писать тесты. Поэтому поговорим о worst practices в тестировании. Все примеры в статье - из настоящих проектов.

Лирическое отступление

Интересен тот факт, что при всей популярности многочисленных книг и советов по написанию модульных тестов, авторы практически никогда не ссылаются на материалы из области QA, хотя казалось бы у кого нужно учиться тестированию. Может поэтому программисты не умеют писать тесты? Многие из нижеперечисленных советов опираются на то, как тест инженеры подходят к написанию тест кейсов.

Проблем с тестами может быть три: их трудно поддерживать, читать и писать. Каждая проблема может решаться по-своему, во многих случаях достаточно просто порефакторить тесты, сделать их более грамотными. Бывает этого недостаточно и нужно рефакторить сам код - не имея адекватного кода писать тесты достаточно проблематично. Разработчики же в погоне за “лучшими практиками” забывают, что главное - это простота и эффективность, поэтому хочу показать во что выливается слепое следование идеалам из интернетов.

Прежде чем начнем рассматривать конкретные случаи, - немного терминологии:

  • SUT - system under test, то бишь то, что мы тестируем. Бывает еще называют CUT (component under test). В случае модульных тестов это как правило тестируемый класс.

  • DOC - depended on objects, то бишь то, от чего зависит SUT. Например, класс может использовать другой класс делегируя часть функционала. Этот делегат и называют DOC’ом.

  • Тест - в данном контексте это тестовый метод, тест кейс. Не класс! Тестовый класс - это набор тестов.

Практика

Итак, не тестируйте несколько результатов. Если функция затрагивает несколько аспектов, то у разработчиков руки чешутся протестировать их все в одном тесте, к примеру:

@Test
    public void testTopicChanged() throws MailingFailedException {
        prepareEnabledProperty();
        topic.getSubscribers().add(user1);
        topic.getSubscribers().add(user2);
        topic.getSubscribers().add(currentUser);
        when(subscriptionService.getAllowedSubscribers(topic)).thenReturn(topic.getSubscribers());

        service.subscribedEntityChanged(topic);

        verify(mailService, times(2)).sendUpdatesOnSubscription(any(JCUser.class), eq(topic));
        verify(mailService).sendUpdatesOnSubscription(user1, topic);
        verify(mailService).sendUpdatesOnSubscription(user2, topic);
        assertEquals(topic.getSubscribers().size(), 3);
    }

В данном тесте проверяется, что подписанные пользователи получают письмо, а неподписанные его не получают. Недостаток такого подхода в том, что когда упадет тест будет не понятно что не работает - нужно лезть в сам тест и смотреть его код. Если же было бы два теста: subscribedUserShouldReceiveNotification & notSubscribedUserShouldNotBeNotified тогда при красном тесте ясно ЧТО не работает.

Плюс сам тест становится сложней. Много маленьких тестов с короткими сценариями намного лучше, чем мало, но с длинной простыней.

Не проверяйте вызовы методов. Ваша SUT должна вернуть результат. И только его (этот результат) нужно проверять. Как не надо:

...
when(userDao.getByUsername(username)).thenReturn(user);
...
boolean isAuthenticated = userService.loginUser(username, PASSWORD, false, httpRequest, httpResponse);
assertTrue(isAuthenticated);
verify(userDao).getByUsername(username);

В данном случае вызов userDao.getByUsername() является внутренним устройством SUT. Нам не надо знать об этом, нам важен результат. Дабы вообще было возможно протестировать SUT нам приходится лезть своими грязными руками в реализацию и определять поведение DOC’a - но это плохо. Что еще хуже - мы на этом не останавливаемся и проверяем, что взаимодействие с DOC’ом на самом деле произошло. Но зачем, является ли это результатом функции? Это личная жизнь SUT, не нужно в нее вмешиваться ибо при рефакторинге нам многое придется в тестах переписывать. Если хотите писать поддерживаемые тесты, избегайте проверок вызовов.

Исключение составляет тот случай, когда взаимодействие с DOC’ом собсно является результатом выполнения функции. К примеру, оповещение пользователей по мейлу о каком-то событии - это результат действий SUT. К сожалению без влезания во внутренности реализации SUT в таком случае не всегда можно обойтись.

Не усложняйте тестовые данные “на всякий случай”. Наглядней на примере:

@Test
    public void extractMentionedUserShouldReturnAllMentionedCyrillicUserInBBCodes() {
        String textWithUsersMentioning = "In this text we have 3 user mentioning: first [user]Иванов[/user]," +
                "second [user notified=true]Петров[/user]," +
                "third [user]Сидоров[/user]";

        MentionedUsers mentionedUsers = MentionedUsers.parse(textWithUsersMentioning);
        Set<String> extractedUserNames = mentionedUsers.extractAllMentionedUsers(textWithUsersMentioning);

        assertTrue(extractedUserNames.size() == 3, "Passed text should contain 3 user mentioning.");
        assertTrue(extractedUserNames.contains("Иванов"), "Иванов is mentioned, so he should be extracted.");
        assertTrue(extractedUserNames.contains("Петров"), "Петров is mentioned, so he should be extracted.");
        assertTrue(extractedUserNames.contains("Сидоров"), "Сидоров is mentioned, so he should be extracted.");
    }

Данный тест должен проверить, что метод может работать с русскими пользователями. Однако он проверяет сразу 3х, а не одного! Знаю о чем думал разработчик, и возможно подумали вы, - на всякий случай, авось проблема обнаружится когда пользователей несколько. Но задача тестов - предоставлять гарантии, а не тыкать пальцем в небо. Гарантии предоставляются конкретными тест кейсами, которые проверяют вполне конкретные случаи. Это раз. А два - кто сказал, что с одним пользователем не сломается? Если хотите проверить случай - добавьте тест специально для этого случая, но не мешайте все вместе - лишь переводите чужое и свое время.

Другое, о чем может подумать разработчик, когда пишет такое - потестирую-ка я и другие значения, может на другом значении упадет! Но эта логика тоже не верна, у QA есть несколько методик при написании тест кейсов. Одна из них - это разбиение тестовых данных на классы эквивалентности. Суть в том, что весь набор данных делится на группы, и мы считаем, что данные из одной и той же группы вряд ли приведут к ошибке. Пока у вас не будет четкого разбиения данных на группы, ваши тесты пишутся на авось. Не пишите лишние тесты для одного и того же класса эквивалентности.

Не используйте DataProvider’ы. Похожих результатов в JUnit можно достигнув с помощью @Parameterized и @Theory. Они позволяют задать какие-то тестовые данные в одном месте сразу кучей, а затем использовать один тестовый метод для работы с этими данными:

@DataProvider(name = "status-provider")
    public Object[][] statusData() {
        return new Object[][]{
                {PrivateMessageStatus.NEW, false},
                {PrivateMessageStatus.DRAFT, false},
                {PrivateMessageStatus.SENT, true},
                {PrivateMessageStatus.DELETED_FROM_INBOX, false},
                {PrivateMessageStatus.DELETED_FROM_OUTBOX, true}
        };
    }

Этот метод предоставляет данные и результат теста. А сам тест выглядит так:
@Test(dataProvider = "status-provider")
    public void testIsReplyAllowed(PrivateMessageStatus status, boolean expectedResult) {
        pm.setStatus(status);
        assertEquals(expectedResult, pm.isReplyAllowed());
    }

Почему так - ай-ай-ай:

  • Если упадет тест, то догадайся какой же случай все-таки свалился.

  • Дебажить такое тоже нелегко, тест-то всегда запускается со всеми параметрами, а что если нужен 3й?

  • Тест не показывает что же мы все-таки тестируем. Одно дело если тест называется userCanReplyToMessageInSentState - тут прям из названия понятна бизнес задача и что требуется от функции, а другое - это просто мапа Object-Boolean. Хотя, признаться, в этом случае все еще не так плохо.

Не мокайте то, что можно не мокать. Моканье сущностей, утилит - дело неблагодарное, не каждый вызов DOC’a нужно мокать. Самый яркий пример - моканье обычных сущностей (entities). Пример:

post = mock(Post.class);
when(post.getId()).thenReturn(POST_ID);
when(post.getUserCreated()).thenReturn(user);
when(post.getPostContent()).thenReturn(POST_CONTENT);
when(post.getTopic()).thenReturn(topic);

Почему бы не сделать: new Post(POST_CONTENT, topic).withId(POST_ID).withAuthor(user), ведь проще же, наглядней.

Избегайте использования полей в тестовых классах. Помните, что поля для класса глобальны и могут использоваться любым методом. Каждое добавленное поле в классе - существенное усложнение для чтения всех методов. Потому как для прочтения тестов часто не достаточно прочесть лишь метод, как правило используются разного рода моки, которые матрешкой собираются друг в друга. Чем меньше будет этого “шума”, тем проще будет читать каждый отдельный тест. И для новичков в вашем проекте намного проще будет писать свои тесты - для этого им не нужно будет читать 40 строк странных полей. Пример:

private static final String USER_NAME = "username";
    private static final String FIRST_NAME = "first name";
    private static final String LAST_NAME = "last name";
    private static final String EMAIL = "[email protected]";
    private static final String PASSWORD = "password";
    private static final String IMAGE_BYTE_ARRAY_IN_BASE_64_STRING = "it's dummy string";

    @Mock
    private UserService userService;
    @Mock
    private MessageSource messageSource;
    @Mock
    private ImageControllerUtils imageControllerUtils;
    private AvatarController avatarController;
    private byte[] validAvatar = new byte[] {-119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0,
            0, 0, 4, 0, 0, 0, 4, 1, 0, 0, 0, 0, -127, -118, -93, -45, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 1,
            -118, 0, 0, 1, -118, 1, 51, -105, 48, 88, 0, 0, 0, 32, 99, 72, 82, 77, 0, 0, 122, 37, 0, 0,
            -128, -125, 0, 0, -7, -1, 0, 0, -128, -23, 0, 0, 117, 48, 0, 0, -22, 96, 0, 0, 58, -104, 0, 0,
            23, 111, -110, 95, -59, 70, 0, 0, 0, 22, 73, 68, 65, 84, 120, -38, 98, -40, -49, -60, -64, -92,
            -64, -60, 0, 0, 0, 0, -1, -1, 3, 0, 5, -71, 0, -26, -35, -7, 32, 96, 0, 0, 0, 0, 73, 69, 78, 68,
            -82, 66, 96, -126
    };

Это не рекордсмен на моей практике, но уже такой перечень полей и констант должен отпугивать желающих его прочесть, а тем более, - дописать. Можно попытаться найти оправдания, мол, это позволяет избегать дублирования кода и уменьшить размеры самих тестов. Однако вы усложняете все(!) тесты для локального выигрыша. Если вам хочется подготовить данные или вы хотите избежать дублирования - используйте приватные методы, к примеру. Да, они тоже будут глобальны для класса, но они не являются обязательными для прочтения во всех тестах, для написания дополнительного теста вам не нужно перечитывать все методы.

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

Не используйте наследование. Разработчики любят изобретать. А еще любят писать красивый код. Жаль, что у них ни то, ни другое не получается. Тесты не исключение - частенько разработчики норовят написать очередной фреймворк для тестирования. Это случается когда у большого количества тестовых классов есть что-то общее. К примеру, у нас есть интеграционные тесты и все создают какой-то контекст и подготавливают какое-то окружение, а затем многие из них производят похожие действия для полной готовности SUT. И тут идея.. а не написать ли нам абстрактный тестовый класс, который будет это все делать, а мы его будем лишь наследовать в конкретных тестах. Даешь изобретателям по ушам! Проблема в том, что наследование привносит свою долю сложности в дизайн классов. Недаром от него в последнее время отказываются в пользу делегирования (гуглим delegation over inheritance). Так вот, имейте в виду, что для понимания вашего класса вам придется ити вверх по иерархии и читать предка. Вы ведь не знаете вот так наугад что в нем.

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

В общем был у меня опыт работы в проекте, где такая иерархия была доведена до абсурда. А все ведь намного проще - сделайте вы этот класс с общей логикой, да вызовайте разные его методы в @BeforeMethod. Да, в каждом тестовом классе это нужно делать, но это понятно, это явный вызов явного метода с нормальным названием.

Замечу, что я не противник всяких AbstractTransactionalTestNGSpringContextTests - во-первых, этот класс всем известен, не только тем, кто его писал. Во-вторых, хороший фреймворк - это фреймворк, в исходники которого не нужно заглядывать каждые 5 минут, в исходники этого класса вряд ли приходится заглядывать постоянно. Не у каждого рядового разработчика получается написать такой фреймворк.

Избегайте использования @Before. Не вообще, конечно, - создать объект SUT’a вполне там можно, и моки заинжектить тоже. Но случается что используют prerequisite методы слишком интенсивно. Пример:

@BeforeMethod
    public void init() throws NotFoundException {
        initMocks(this);

        Branch branch = new Branch("branch", "branch");
        branch.setId(BRANCH_ID);

        Topic topic = new Topic(user, "title");
        topic.setBranch(branch);
        topic.setId(TOPIC_ID);

        post = new Post(user, POST_CONTENT);
        post.setId(POST_ID);
        post.setTopic(topic);
        topic.getPosts().addAll(asList(post));

        when(postService.get(POST_ID)).thenReturn(post);
        when(topicFetchService.get(TOPIC_ID)).thenReturn(topic);
        when(breadcrumbBuilder.getForumBreadcrumb(topic)).thenReturn(new ArrayList<Breadcrumb>());

        controller = new PostController(
                postService, breadcrumbBuilder, topicFetchService, topicModificationService,
                bbCodeService, lastReadPostService, userService);
    }

Тут опять проблемы - во-первых, то же самое, что и с полями - нужно всегда читать этот код, он ведь выполняется для каждого теста. Во-вторых, часть этого кода является пререквизитами только некоторых тестов. В общем здесь усложнение опять ни к чему, снова на выручку приходят приватные методы.

Read more
19 May 2013

Ошибки, снижающие производительность и их устранение

Добрый день, уважаемый читатель.

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

1. Борьба с инвариантами
Итак, что же такое инвариант и почему с ними стоит бороться? Говоря сухим академическим языком, инвариантом называется логическое выражение, истинное перед началом выполнения цикла и после каждого прохода тела цикла. Другими словам, это условие (или величина), неизменное во время выполнения цикла.

Рассмотрим следующий пример:

String string = “некоторая строка”;
for (int i = 0; i < string.length(); i++){        //string.length() вызывается при каждом проходе цикла
        //делаем что-то
}

Это чрезвычайно распространённая ошибка, которая может существенно ударить по производительности, когда при проверке истинности выполняется сложное действие или когда количество проходов цикла очень велико. Причина в том, что метод length() никак не меняет строку, следовательно, его вызов при каждом проходе является бесполезным. Думаю, читатель уже догадался, как можно оптимизировать цикл всего одной строчкой и сделать наш код более красивым и правильным:
String string = “некоторая строка”;
int stringLenght = string.length();        //длинна строки вычисляется лишь однажды
for (int i = 0; i < stringLenght; i++){
        //делаем что-то
}

Сразу же отмечу, что если исходная строка всё же изменяется в теле цикла, то данный метод бесполезен.

2. Неявный инвариант
Рассмотрим другой пример, в котором мной была допущена ошибка, связанная с инвариантами. Ошибку я обнаружил случайно, поскольку в данном случае она не столь очевидна. Однажды я писал класс, один из методов которого сканировал большой текстовый файл в поисках строк, соответствующих определённому шаблону. Шаблон был неизменен для каждого файла, поэтому логично было бы сделать вот так:

public class Parser {
        private Pattern p;
    
        public Parser(String pattern){
                //создаём шаблон неизменный в течение всего поиска
                p = Pattern.compile(pattern);
        }

        public void parseStrings(){
                BufferedReader br;
                //инициализируем br
                String s;
                while((s = br.readLine()) != null){
                        if (checkString(s))
                                //...
                }
        }

        private boolean checkString(String s){
                return p.matcher(s).matches();
        }
}

Однако, по непонятной причине в силу неопытности я сделал так:
public class Parser{
        private String pattern;

        public Parser(String pattern){
                this.pattern = pattern;
        }

        public void parseStrings(){
                BufferedReader br;
                //...
                String s;
                while((s = br.readLine()) != null){
                        if (checkString(s))
                                //...
                }
        }

        //неэффективная реализация метода
        private boolean checkString(String s){
                //шаблон создаётся каждый раз, хотя в этом нет необходимости
                Pattern p = Pattern.compile(pattern);
                return p.matcher(s).matches();
        }
}

Как видите, в методе checkString() мной была допущена ошибка. Шаблон для проверки создавался при каждом вызове метода (а вызывался он при прохождении каждой строки), хотя необходимости в этом не было, поскольку шаблон неизменен в течение всего поиска.

3. Эффективное удаление из середины ArrayList.
Идея написать этот раздел родилась после прочтения поста хабражителя sphinks «Java собеседование. Коллекции». Отличная статья, которую я рекомендую всем как начинающим, там и более опытным разработчикам. Считаю, что каждый откроет для себя новые и интересные факты и станет более грамотным. Но вернёмся к нашим баранам спискам. Итак, меня заинтересовал вот этот фрагмент: «…удаление последнего элемента происходит за константное время. Недостатки ArrayList проявляются при вставке/удалении элемента в середине списка — это вызывает перезапись всех элементов размещённых «правее» в списке на одну позицию влево, кроме того, при удалении элементов размер массива не уменьшается, до явного вызова метода trimToSize().»
Вдумайтесь: если из списка произвольной длинны вам необходимо удалить n элементов, начиная с позиции m, то независимо от длинны списка, будут перебраны и смещены левее все элементы, начиная с позиции m+n. И это будет выполняться при каждом вызове remove(). Думаю, читатель уже прикинул, сколько это займёт времени.
sphinks также предлагает способ оптимизации удаления: «На самом деле все довольно просто и очевидно, когда знаешь, как происходит удаление одного элемента. Допустим нужно удалить n элементов с позиции m в списке. Вместо выполнения удаления одного элемента n раз (каждый раз смещая на 1 позицию элементы, стоящие «правее» в списке), нужно выполнить смещение всех элементов, стоящих «правее» n+m позиции на n элементов левее к началу списка. Таким образом, вместо выполнения n итераций перемещения элементов списка, все выполняется за 1 проход».
Я решил реализовать предложенный алгоритм и сравнить его быстродействие с простым вызовом remove().
Получился вот такой класс:

package collectionstudy;

import java.io.*;
import java.util.ArrayList;

public class Main {
    //позиция с которой удаляем
    private static int m = 0;
    //количество удаляемых элементов
    private static int n = 0;
    //количество элементов в списке
    private static final int size = 1000000;
    //основной список (для удаления вызовом remove() и его копия для удаления путём перезаписи)
    private static ArrayList<Integer> initList, copyList;
    
    public static void main(String[] args){
        
        initList = new ArrayList<>(size);
        for (int i = 0; i < size; i++)
            initList.add(i);
        System.out.println("Список из 1.000.000 элементов заполнен");
        
        copyList = new ArrayList<>(initList);
        System.out.println("Создана копия списка\n");
        
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        try{
            System.out.print("С какой позиции удаляем? > ");
            m = Integer.parseInt(br.readLine());
            System.out.print("Сколько удаляем? > ");
            n = Integer.parseInt(br.readLine());
        } catch(IOException e){
            System.err.println(e.toString());
        }
        System.out.println("\nВыполняем удаление вызовом remove()...");
        long start = System.currentTimeMillis();
        
        for (int i = m - 1; i < m + n - 1; i++)
            initList.remove(i);
        
        long finish = System.currentTimeMillis() - start;
        System.out.println("Время удаления с помощью вызова remove(): " + finish);
        System.out.println("Размер исходного списка после удаления: " + initList.size());
        
        System.out.println("\nВыполняем удаление путем перезаписи...\n");
        start = System.currentTimeMillis();
        
        removeEfficiently();
        
        finish = System.currentTimeMillis() - start;
        System.out.println("Время удаления путём смещения: " + finish);
        System.out.println("Размер копии списка:" + copyList.size());
    }
    
    private static void removeEfficiently(){
        /* если необходимо удалить все элементы, начиная с указанного,
         * то удаляем элементы с конца до m
         */
        if (m + n >= size){
            int i = size - 1;
            while (i != m - 1){
                copyList.remove(i);
                i--;
            }
        } else{
            //переменная k необходима для отсчёта сдвига начиная от места вставка m
            for (int i  = m + n, k = 0; i < size; i++, k++)
               copyList.set(m + k, copyList.get(i));
            
            /* удаляем ненужные элементы в конце списка
             * удаляется всегда последний элемент, так как время этого действия
             * фиксировано и не зависит от размера списка
             */
            int i = size - 1;
            while (i != size - n - 1){
                copyList.remove(i);
                i--;
            }
            //сокращаем длину списка путём удаления пустых ячеек
            copyList.trimToSize();
        }
    }
}

Сравним?
run:
Список из 1.000.000 элементов заполнен
Создана копия списка

С какой позиции удаляем? > 600000
Сколько удаляем? > 20000

Выполняем удаление вызовом remove()...
Время удаления с помощью вызова remove(): 22359
Размер исходного списка после удаления: 980000

Выполняем удаление путем перезаписи...

Время удаления путём смещения: 62
Размер копии списка:980000
СБОРКА УСПЕШНО ЗАВЕРШЕНА (общее время: 33 секунды)

Как говориться, почувствуйте разницу! :)
Используя метод removeEfficiently() можно без особого труда написать свою, более эффективную реализацию ArrayList.

На сегодня это всё, надеюсь, что тебе было интересно и твой код станет более правильным и производительным. Комментарии, замечания и пожелания принимаются как в комментариях, так и в личку.

Успехов тебе!

Read more
09 May 2013

Deployment Pipeline на практике

Резюме: в сети появляется много теоретического материала по теме Continuous Delivery & Deployment Pipeline’ов, однако практических реализаций в примерах пока не находил. Собственно здесь хочу поделиться примером того как мы реализовали Deployment Pipeline внутри нашего open source проекта JTalks. Все исходники открыты, поэтому вы сможете не только прочесть пояснение, но и своровать реализацию.

Если вы не понимаете что такое Deployment Pipeline и зачем он нужен, вот некоторые пункты, однако для полного понимания нужно прочесть до конца:

  • Deployment Pipeline - это набор практик для автоматизации развертывания приложений на разные окружения, включая production

  • Deployment Pipeline позволяет сделать релизы частыми и уменьшить риски провалов

  • Deployment Pipeline дает возможность ускорить работу разных команд (Dev, QA, DevOps) автоматизируя рутинную работу

Выглядеть это может к примеру так

Jenkins

Первое и центральное место - это собсно наш Continuous Integration сервер. Я рекомендую использовать именно Jenkins,
потому как он имеет сотни удобных плагинов, и половины которых вам не дадут никакие коммерческие инструменты типа
Team City, Bamboo. Дополнительный пункт в пользу Jenkins - он открытый и вы сами можете дописывать его плагины. Такая
возможность обычно приходит очень кстати в особо крупных проектах, где стандартные фичи не подходят и нужно много
писать своих решений.

Какие плагины Jenkins’a стоит рассматривать для реализации Deployment Pipeline’ов:

  • Build Pipeline Plugin - собсно. Этот плагин далеко не идеален как и большинство Jenkins плагинов, с некоторым количеством багов (которые вы можете исправить). Он предоставляет возможность визуализации вашего конвеера. Визуализация очень важна чтоб все члены команды включая будущих новичков быстро сообразили какие существуют окружения и как приложение эволюционирует. Также этот плагин контролирует возможность запускать те или иные планы. Для реализации настоящего Deployment Pipeline’a вам важно, чтоб ваши артефакты не переходили в следующие фазы до того как выполнились предыдущие. Например, ваше приложение не должно проходить на UAT или PREPROD окружения до того как прошли автоматизированные системные тесты.
  • Rebuild Plugin - например вы решили зарелизить ваше приложение, а уже в PROD окружении оказалось что что-то не так. На моей практике было такое, что приложение работало недопустимо медленно на реальных данных. Тогда у вас должна быть возможность откатить ваше изменение. Откат же не должен в идеале отличаться от обычного релиза, и если вам посчастливилось оказаться в такой ситуации - просто зарелизьте предыдущую версию вашего приложения! Rebuild Plugin позволяет перезапускать предыдущие билды с теми же параметрами.
    Конечно такая простая ситуация бывает не всегда - иногда вам нужно откатить изменения в базе данных. Тогда вам конечно еще придется восстанавливать бекапы. Сделайте так чтоб любое ваше окружение использовало все те же скрипты - тогда вы их оттестируете сотни раз еще на фазе разработки.
  • SCM Sync Configuration Plugin - это не относится напрямую к вашему конвееру, однако лучше версионировать изменения в вашем Continuous Integration сервере. Ибо чем дальше в лес, тем больше дров - CI в какой-то момент становится вашим самым важным и центральным инструментом со сложной конфигурацией. Этот плагин стоит использовать с большой осторожностью, потому как бывало что он просто отказывал. В последнее время мы начали задумываться о ручном бекапе.

Артефакты (Binaries)

Следующая остановка - хранилище ваших артефактов. Одна из главных особенностей настоящих Deployment Pipeline’ов - ваши артефакты собираются лишь один раз и используются на каждом окружении. Это важно по нескольким причинам:

  • Вы получаете повторяемость окружений. Нет даже такой возможности что на разные окружения попали разные исходники. Если же вы собираете каждый раз новые артефакты, то так утверждать уже не можете. Во-первых, потому что во время сборок могут использоваться какие-то параметры окружения, во-вторых сами инструменты сборки могут выпендриться. Например, Maven может использовать другую версию артефакта если вы используете version ranges: <version>[1.5-1.7)</version>.
    Другой пример - это сами Maven Repos (Nexus, к примеру), которые могут возвращать разные артефакты в зависимости от их настроек и описания pom.xml. Это может произойти если используются Repository Groups которые ссылаются на несколько репозиториев с одними и теми же артефактами.

  • Если на каком-либо окружении был найден баг, вы всегда можете загрузить этот же артефакт на другое окружение и проверить что к чему.

  • Другие команды (QA) могут сами откатывать версии и таким образом локализовывать версию где был введен баг.

  • Ваш CI работает быстрей т.к. не нужно делать дополнительных сборок.

Хранить артефакты можно где угодно, даже тот же Jenkins позволяет их сохранять у себя и использовать Copy Artifact Plugin для передачи артефактов из плана в план. Однако намного удобней работать со специальными инструментами, такими как Sonatype Nexus, Artifactory. Есть несколько причин для этого:

  • У них есть специальные возможности для работы с артефактрами, такие как поиск и индексирование. Можно создавать внутри них несколько репозиториев и ограничивать к некоторым из них доступ. Можно даже создать репозиторий только со стабильными артефактами, а все остальные хранить отдельно.

  • Вам все равно они понадобятся для работы с Maven, Gradle - чтоб загружать из них зависимости.

  • Хранение артефактов в Jenkins’e усложняет с ним работу, например, нужно задумываться как делать бекапы эффективней.

Итого, мы определились что хранить артефакты и их историю для нас важно. Как это делать правильно? Когда происходит коммит вы должны запустить обязательные тесты и по их прохождению собирать и заливать артефакты в ваше хранилище. Каждый артефакт должен иметь какой-то идентификатор, какую-то версию, чтоб по ней можно было выкачать артефакт на других окружениях.

Например, у нас в JTalks план сборки выглядит так:

#get rid of SNAPSHOT and add build number after version
VERSION=`mvn help:evaluate -Dexpression=project.version | grep -v "^\["| grep -v Download`
VERSION=${VERSION/%-SNAPSHOT/} #get rid of -SNAPSHOT if it's there
VERSION=$VERSION'.'$PIPELINE_NUMBER #add unique build number
mvn versions:set -DnewVersion=$version #update the versions in pom files onto unique ones
mvn clean package #run actual build and unit tests

У каждого плана в Jenkins есть специальные переменные, такие как BUILD_NUMBER (мы его переименовывали в PIPELINE_NUMBER) - такая переменная используется для уникальности, то бишь каждый артефакт получает соответствующий суффикс. Далее с этим суффиксом мы заливаем артефакт в Nexus в специальный репозиторий. Т.к. каждая сборка имеет свой уникальный номер-суффикс - мы этот номер передаем в другие планы нашего Deployment Pipeline’a, они отыскивают нужный артефакт и развертывают его на нужных окружениях.

Конфигурирование проектов

Для того, чтоб реализовать красивый и простой Deployment Pipeline, разработчики должны сделать некоторые усилия. Например, у вас ничего не получится если вы конфигурируете проект на этапе сборки. А такие есть (упаси!) - собирают по артефакту на каждое окружение, где разница лишь в конфигурации, а код - тот же. Чтоб все получилось вам нужно придерживаться одного правила: конфигурировать сборку нужно снаружи! Вы можете оставить некоторые окружения (включая общие локальные) внутри атрефактов и позволять переключаться между ними с помощью каких-то флагов: -Denvironment=UAT, однако также должна быть возможность задавать параметры снаружи. Это а) позволит разворачивать артефакт на любом окружении б) даст возможность хранить секретную конфигурацию (например, пароли от PROD базы) в отдельном репозитории без общего доступа.

Теперь о практике: в JTalks мы это реализовали расширив возможности Sping’ового PropertyPlaceholderConfigurer своим JndiAwarePropertyPlaceholderConfigurer. Идея в том, что сначала мы проверяем есть ли переменная в JNDI, и лишь потом, если не обнаружилось, смотрим в переменные среды и properties файлы внутри проекта. А определение какой-то опции может выглядеть так:

<bean class="org.jtalks.jcommune.model.utils.JndiAwarePropertyPlaceholderConfigurer">
  <property name="location" value="classpath:/org/jtalks/jcommune/model/datasource.properties"/>
</bean>
<bean ..>
  <property name="user" value="${JCOMMUNE_DB_USER:root}"/>
</bean>

Теперь как же задавать свойства снаружи? Наверно, у каждого AppServer’a есть JNDI, Tomcat не исключение. Создаем файл и кладем его в conf/Catalina/localhost/[app-namme].xml. А там пишем:
<?xml version='1.0' encoding='utf-8'?>
<Context>
  <WatchedResource>WEB-INF/web.xml</WatchedResource>
  <Environment name="JCOMMUNE_DB_USER" value="root" type="java.lang.String"/>
</Context>

Все, параметр задан в JNDI и если он там указан, то будет браться именно оттуда. Если же его там нет, то смотрим в env vars или properties файлы.

Инструмент сборки

Каждый проект конечно должен собираться специальным инструментом. Есть всякие Ant, Maven, Gradle. Здесь рассмотрим последние два. Maven к сожалению не вписывается в общую картину. Наша идея - иметь уникальный артефакт после каждого коммита, однако Maven прописывает свои версии в pom.xml и менять их после каждого коммита как-то не комильфо. Я такой вариант видел в крупных проектах когда использовались тематические ветки + валидация проходила на этапе прекоммита, при этом версия обновлялась только когда код попадал в develop ветку как это любят изображать Git фетишисты. Однако для мелких и средних проектов - это излишество, да и на крупных не факт что хороший вариант. Короче говоря мы выкрутились в JTalks тем как раз, что подменяли версию, заливали артефакт, но коммит изменений в pom файлы мы не делали.

Gradle же более современный и гибкий инструмент, который вы скорей всего сможете настроить как угодно. Но он использует как правило те же соглашения по версионированию что и Maven. Поэтому складывается впечатление, что инструменты сборки не должны влиять на уникальность версии артефактов. Возможно идея с подменой версии без коммита является нормальной, а не костылем как это мне казалось изначально.

Scripting Language

Для автоматизации релизов вам придется писать скрипты. Это может быть bash, python, ruby, groovy, вы вольны выбирать сами. Bash’a вам вряд ли хватит, а все остальные особо друг от друга не отличаются. Однако есть преимущества каждого из них:

  • Python установлен на большинстве Linux дистрибутивов, дополнительно не придется ставить никаких платформ.

  • Плюсы же Ruby в том, что затем нам могут понадобится инструменты для настройки окружений такие как Chef, которые используют Ruby. Вы даже можете собственно Pipeline реализовать на Chef. Однако все сервера должны быть с предустановленным soft’ом для этого.

  • Groovy удобен тем, что в качестве инструмента сборки можно использовать Gradle и тогда включить скрипты в сами исходники проекта.

Свои скрипты стоит пакетировать, например, Python имеет свой package manager - PIP. Это удобно потому как вы можете версионировать артефакты самих скриптов, да и установка будет намного более тривиальной.

Пример таких скриптов на Python вы можете посмотреть в наших открытых исходниках.

Виртуализация

Для того чтоб все окружения были еще более похожими и не возникало неожиданных ситуаций связанных с операционной системой либо железом, стоит задуматься о виртуализации ваших окружений, вплоть до виртуализации PROD’a. Это вам также даст прирост в производительности труда т.к. избавит от ручной настройки новых окружений. Дополнительным плюсом может быть то, что в определенный момент вы захотите использовать облачные решения типа Amazon EC2, тогда переход на них будет еще более прост.

Есть еще такой прекрасный инструмент как Vagrant, который сильно облегчает проблемы с повторяемыми окружениями. Вам лишь нужно описать какой образ использовать, какой софт будет установлен (это делается благодаря интеграции с Chef и Puppet) и ву а ля - готовое окружение в одну команду. Теперь даже самые малотехнические люди могут развернуть вашу систему без проблем. JTalks VM демонстрирует возможности Vagrant’a:

  • Устанавливаете Vagrant, качаете сами Vagrant скрипты из гит приведенного выше репозитория

  • vagrant up - и через некоторое время, когда все закачается и установится, вы получаете дистрибутив Ubuntu с установленными там deployment скриптами проекта, а также MySQL, Tomcat’ом.

  • Заходите на виртуалку vagrant ssh и запускаете один из проектов: jtalks deploy --environment vagrant --project jcommune --build 2280. Видите последнюю цифру? Она как раз и значит номер того артефакта, который мы разворачиваем. Еще раз ссылка на артефакты.

  • Ну и открываете браузер, вот вам полностью установленное в несколько кликов приложение: http://localhost:4444/jcommune

Read more
12 Mar 2013

Интеграционные тесты + Maven

Резюме: хоть Maven и является инструментом, стандартизирующим структуру проекта и его сборку, однако он с крахом провалил стандартизацию разного рода тестов. Ща разберем полеты.

Итак, почему же все-таки Maven не справляется со стандартизацией тестов, ведь:

  • Модульные тесты:

    • Выполняются на фазе test
    • Лежат в src/test каталоге
    • Названия тестовых классов содержат слово Test
  • Интеграционные тесты:

    • Выполнаются на фазе integration-test
    • Лежат в src/test
    • В их имени должны быть буквы IT обозначающие собсно IntegrationTests.

Проблем на самом деле с этим несколько:

  • Фаза integration-test выполняется после модульных тестов, это значит что отдельно их не запустить. Каждый раз когда мы запускаем интеграционные тесты, выполняются модульные. Однако обычно мы хотим запустить модульные тесты один раз, а затем отдельно запускать интеграционные. Выходит чтоб модульные тесты не выполнялись нам нужно пропускать их с помощью -DskipTests, затем окажется что интеграционные тесты тоже не запускаются потому что failsafe плагин использует под собой surefire, начнется геморрой с созданием профилей и в конце концов начнет казаться что все это слишком сложно. Кстати, почему нам важно запускать тесты раздельно:

  • Разработчики могут быстро получить “зеленый” фидбек и продолжить работать. Именно модульные тесты способны быстро дать базовый ответ.

  • Немодульные тесты зависят от окружения, а значит могут падать. Если упали такие тесты, то еще нужно разобраться из-за окружения ли, либо и вправду сломалась логика.

  • Интеграционные тесты медленные, в зависимости от длительности их можно запускать по каждому коммиту, раз в день и т.п. Они тоже могут дробиться на более мелкие группы тестов, например, основные Smoke тесты выполняются первыми, затем регрессия, затем приемочные, затем какие-нибудь нагрузочные и т.п. Их удобно разделять потому что мы точно будем знать что сломалось. А также мы можем запускать отдельно определенную группу тестов и не ждать 4 часа чтоб прошло все.

  • Мы можем хотеть запускать одни и те же системные несколько раз. Например, мы их прогнали по текущему коммиту, а затем еще по тому что был неделю назад для того чтоб увидеть разницу между результатами.

  • Для системных тестов часто нужно подготовить окружение прежде чем их запускать.

  • Стандартно failsafe использует каталог src/test, а нам редко когда нужно помещать интеграционные и модульные тесты и ресурсы в одни и те же пакеты.

  • Фаза integration-test запускается перед install каждый раз. Мы же не хотим запускать медленные интеграционные тесты каждый раз когда устанавливаем артефакто в локальный репозиторий.

  • К сожалению для большинства разработчиков существуют лишь модульные, интеграционные тесты и “то, что делают QA”. Однако на самом деле тесты разделяются как минимум на модульные, системные и компонентные в зависимости от масштабов. Также есть функциональные и нефункциональные тесты и т.п. Более подробно можно с видами тестирования ознакомиться в одноименной статье. Но что главное - мы можем захотеть разделить все эти тесты. Для одних нужно поднимать все приложение, для других - лишь часть, для третьих вообще одного класса хватит. Однако Maven их никак не различает и не разделяет, у него есть либо модульные, либо интеграционные.

В общем подумав немного можно прити к выводу, что раз стандартный механизм Maven настолько несовершенен и все равно не сможет поддерживать всего нужного, мы можем отойти от него. Вместо этого предлагаю использовать plain old surefire plugin. Да, этот плагин заточен на написание модульных тестов, однако они по факту ничем не будут отличаться от “немодульных” - те же JUnit/TestNG будут описывать всю их логику (хотя тут позволяется также использовать всякие BDD фреймворки навроде JBehave, однако не о них речь).

Так вот как же это будет выглядеть. Для каждого из видов тестирования мы будем создавать а) профиль б) каталог с исходниками и ресурсами. Конфигурироваться же Maven будет следующим образом:

<properties>
    <test.sourceDirectory>${project.basedir}/src/test/java</test.sourceDirectory>
    <test.resourceDirectory>${project.basedir}/src/test/resources</test.resourceDirectory>
  </properties>

  <build>
    <testSourceDirectory>${test.sourceDirectory}</testSourceDirectory>
    <testResources>
      <testResource>
        <directory>${test.resourceDirectory}</directory>
      </testResource>
    </testResources>
  </build>
  <profiles>
    <profile>
      <id>component-test</id>
      <properties>
        <test.sourceDirectory>${project.basedir}/src/component-test/java</test.sourceDirectory>
        <test.resourceDirectory>${project.basedir}/src/component-test/resources</test.resourceDirectory>
      </properties>
    </profile>
    <profile>
      <id>system-test</id>
      <properties>
        <test.sourceDirectory>${project.basedir}/src/system-test/java</test.sourceDirectory>
        <test.resourceDirectory>${project.basedir}/src/system-test/resources</test.resourceDirectory>
      </properties>
    </profile>
  </profiles>

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

  • Модульные: mvn test

  • Компонентные: mvn test -Pcomponent-test

  • Системные: mvn test -Psystem-test

Структура каталогов тогда такая:

src
 |_main
      |_java
      |_resources
 |_test
      |_java
      |_resources
 |_component-test
      |_java
      |_resources
 |_system-test
      |_java
      |_resources

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

Единственное в чем я слукавил: фаза интеграционных тестов делиться на несколько шагов и это позволяет нам сначала поднять окружение, а затем его остановить. Подход с surefire такого не даст, вам придется вручную запускать окружение с помощью явно указанных команд. Однако:

  • Если нам нужно полностью развернутое окружение, то это не всегда возможно автоматизировать без написания скриптов, что значит что в большинстве случаев все равно придется руками дергать их

  • Сложность в конфигурировании разного рода тестов перекрывает сложность запуска какого-нибудь tomcat:start

  • Т.к. обычно системные тесты выносят в отдельные модули или проекты, то там вполне можно использовать фазу integration-test как единственную, которая запускает тесты.

Но если вам правда хочется удобным образом автоматизировать развертывание окружения, возможно вам будет лучше сконфигурировать integration-test фазу для этого.

JUnit Categories, TestNG Groups

Последнее о чем стоит упомянуть - это JUnit категории и TestNG группы. Они позволяют с помощью аннотаций как-то помечать тесты. Однако если в TestNG это хоть как-то можно сделать удобно, то в JUnit это сделано через прямую кишку и вам придется конфигурить много профилей, у вас не выйдет задать exclude=IntegrationTest по умолчанию, а затем через командную строку активировать какие-то другие категории, для этого вам придется все равно создавать профили, а значит никакого удобства такой способ не принесет. Раз не выйдет это использовать с JUnit’ом, значит не имеет смысл стандартизировать такой подход.

Read more
23 Jan 2013

Как проводить собеседования?

Расскажу свою точку зрения о том как проводить собеседования на должность разработчика, в частности на Java разработчика.

В первую очередь расставим цели:

  • Подобрать разработчика в свою команду

  • Он должен помочь нам с проектом технически

  • С ним должно быть приятно работать

К сожалению большинство собеседующих забывают об этих целях и во многих случаях собеседование скатывается к:

  • Озадачить кандидата задав ему сложный вопрос

  • Показать как я крут

  • “Он решил эту задачу, берем!” или “Он не решил эту задачу, он не достоин”

Как же все-таки должно проходить собеседование:

  • Для начала вам нужно расслабить кандидата (сделайте ему массаж ;)). Человек, отвечающий на вопросы и человек, работающий в команде, - это два разных человека. Хорошего разработчика можно запутать и он будет плохо отвечать на вопросы, и наоборот - плохой разработчик может знать как все должно выглядеть в теории, но никогда не делать этого на практике. Вам нужно добраться до разработчика спрятавшегося на время собеседования где-то глубоко внутри, а не послушать ответы “студента”.
    Если в его резюме перечислены хобби, поспрашивайте о них. Если для вас важно, чтоб человек знал английский, начните с проверки английского, а там уже можно порасспрашивать и про увлечения, и про школу с дет садиком, таким образом убьете двух зайцев. Важно, чтоб человек с вами начал общаться как с коллегой, но и в кабак его при этом вести не стоит - всему есть меры.
  • Не используйте тесты. Никогда вам тесты не покажут ни квалификацию разработчика, ни того какой он командный игрок. Во-первых, тесты просто раздражают, они нудные. В итоге разработчик может уйти от вас с негативом, а это в свою очередь скажется на авторитете компании, он ведь обязательно поделится своими впечатлениями с друзьями. Во-вторых, вы думаете внешние (центральные) экзамены, которые школьники сдают для поступления в ВУЗ хоть как-то показывают уровень студентов? Э-э-э нет, уровень - это не знание ответа на вопрос, а понимание предмета.

  • Попросите кандидата показать свой код прежде чем приглашать на собеседование. У многих есть какие-то open source проекты, которыми им будет приятно похвастаться.

  • Дайте человеку пописать код (особенно если у него не было чем похвастаться перед собеседованием). Прям на собеседовании выделите ему машину и IDE, чтоб увидеть как этот человек пишет. Причем,

    • Условия должны быть максимально приближенные к реальным. Человек не должен писать в notepad, это глупо, он не будет писать так на работе.
    • Задание не должно быть сложным, любое сложное задание может занять дни, даже если вы считаете что оно короткое и его можно написать за полчаса, у кандидата может сложится другая картина. Например, вы можете предложить написать какую-то коллекцию. Никогда вам человек не напишет правда качественную, оптимизированную коллекцию, с документацией и тестами за полчаса. Если же вам нужны лишь очертания коллекции, тогда объясните четко кандидату что вы от него хотите. Имейте в виду, что вам важней то, как кандидат пишет в реальной жизни, а не то как он пишет “когда надо быстро-быстро ой-ой-ой”. Я предпочитаю давать задачи, типа, найти максимальное число. Она тривиальна, поэтому кандидат вполне может уложиться в полчаса. Также из очень классных заданий - написание immutable классов, это элементарно сделать и это важно в каждодневной разработке.
    • Кандидат должен четко понимать что от него хотят. Если вы хотите оптимизированное решение, объясните это кандидату. Если вам нужно близкое к реалиям решение, объясните это кандидату. Если вам важно просто понимание кандидата, объясн…
  • Старайтесь не спорить с кандидатом, ваша цель - определить его уровень, а не переспорить его.

  • Задавайте вопросы по-настоящему важные для вас. Если у вас в проекте используется технология А, Б, В, а вы для комплекта расспрашиваете о Э, Ю, Я которые и близко к вам не относятся, значит вы не преследуете свою изначальную цель - найти сотрудника в ваш проект.
    Бывает, что компания крупная и важно знать уровень разработчика в разных областях, мол, может другой проект возьмет. В таком случае сначала убедитесь, что он не подходит вам, ну и все равно знайте меру.

  • Вам нужен не ответ, а понимание предмета. Вы не должны останавливаться на том, что человек дал вам ответ на конкретно поставленный вопрос, вам нужно обязательно копнуть глубже, попросить привести разные примеры, как человек то или иное применял на практике и т.п. Просто ответ вас не интересует, вас интересует насколько глубоко человек разбирается в предмете. С одной стороны кандидат который разбирается в предмете может не знать определенного ответа но разбираться в общем; ну и как всегда есть те, кто просто знает ответы на вопросы.

  • Неправильный ответ на заковыристый вопрос не дает минусов кандидату. Если вы задаете очень тонкий вопрос, вас не должно смущать что человек этого не знает. Во-первых, какой прок от этого вопроса? Что он, определит уровень кандидата? Эти знания, возможно, в жизни ему никогда не пригодятся. Во-вторых, ну не наткнулся он на ту же статью, что вы прочли вчера, зато он может быть сильным в другой области, осваивать которую нужно полгода.

  • Не задавайте вопросы по алгоритмике и математике, если ваш проект в этом правда не нуждается. Это очень популярная тема на собеседованиях, однако как математика, так и алгоритмика нужна в очень узком спектре задач. Я встречал очень сильных в программировании людей, которые не разбирались в этом: математика была давно и забылась, а алгоритмика как не была нужна, так и не стала. Даже если периодически какие-то знания нужны в вашем проекте - насколько они критичны? Любые знания, которые можно быстро нагуглить - просто крошка по сравнению с тем, чему люди учатся годами.

  • Лучше нанять толкового junior’a, чем бестолкового senior’a. Очень большой процент разработчиков остается на одном и том же уровне всю жизнь. Их много, и они бесполезны. Намного лучше нанять свежего разработчика с мозгами, который начнет разбираться в предмете уже через месяц. К сожалению рецептов как найти таких шустряков у меня нет, только после месяца-двух работы с людьми можно о чем-то говорить.

  • Не нанимайте сложных людей. Даже если он правда очень сильный, но с ним будут проблемы в общении, - это того не стоит. Видел не одну ситуацию, когда по увольнению (уходу) таких сотрудников вся команда облегченно вздыхала.

  • Не будьте категоричными. Если кандидат не знает какого-то важного аспекта, возможно он ему может быстро обучиться. Старайтесь оценивать человека в общем, а не по конкретным вопросам.

  • Расскажите свое мнение кандидату. Это лично моя фича и в некоторых ситуациях она не приемлена, однако я предпочитаю делиться своим впечатлением с кандидатом. Чтоб человек развивался, он должен понимать что не так и куда нужно двигаться. Сделайте человеку полезно и расскажите где он был неправ или неточен.

Не смотря на все эти рецепты, они хоть и снизят риски, однако по-настоящему вы увидете человека в деле только уже работая с ним.

Read more
24 Dec 2012

Spring AOP. Transaction manager на коленке.

Сегодня хочу рассказть вам немного об аспектах. Парадигма Аспектно-ориентированного программирования зарадилась довольно давно и теоритическую информацию можно свободно найти на просторах интернета. Java поддерживает работу с аспектами используя расширение именуемое AspectJ.
В своем примере я буду использовать Spring AOP. Этот подпроект написан полностью на Java, полностью понимает синтаксис AspectJ и использует свои собственные механизмы для weaving. Weaving - это процесс связывания аспектов с объектами приложения. Spring выполняет связывание в runtime. Если класс реализует интерфейс, то спринг обернет его в прокси средствами JDK не используя AspectJ. Но бывают случаи когда стандартных средств не достаточно и тогда в дело вступает AspectJ. Это, например, относится с созданным hibernate entity или @Configurable бинам. Интересно, что если изучить скомпилированный байт код, то можно увидеть, что некоторые типы advice реализуются с помощью шаблона proxy.
Следующий пример показывает как можно использовать аспекты для управления Hibernate сессиями и тразакциями. Естественно, он является только наглядным примером показывающем аспекты в действии и не притендует на что-то большое.

@Component
@Aspect
public class TransactionManagerWithAspects {
    public static final Logger log = LoggerFactory.getLogger(TransactionManagerWithAspects.class);

    @Autowired
    private SessionFactory sessionFactory;

    @Pointcut("execution(* app.dao.impl.*.find*(..))") 
    public void findEntry() {}

    @Pointcut("execution(* app.dao.impl.*.save*(..))") 
    public void saveEntry() {}
    
    @Pointcut("execution(* app.dao.impl.*.delete*(..)))") 
    public void deleteEntry() {}

    @Around("findEntry()")
    public Object readOnlyTx(ProceedingJoinPoint pjp) {
        Session session = null;
        Object methodResult = null;
        try {
            session = sessionFactory.getCurrentSession();
            methodResult = process(session, pjp);
        }
        catch (Throwable t) {
            log.error("Fatal error during invoke " + createMethodInfo(pjp), t);
        }
        finally {
            closeSession(session);
        }

        return methodResult;
    }

    @Around("deleteEntry() || saveEntry()")
    public Object commitTx(ProceedingJoinPoint pjp) {
        Session session = null;
        Object methodResult = null;
        try {
            session = beginTx();
            methodResult = process(session, pjp);
        }
        catch (Throwable t) {
            log.error("Fatal error during invoke " + createMethodInfo(pjp));
        }
        finally {
            finishTx(session);
        }

        return methodResult;
    }

    private Object process(Session session, ProceedingJoinPoint pjp) throws Throwable {
        BaseDAO baseDao = (BaseDAO) pjp.getTarget();
        baseDao.setSession(session);
        return pjp.proceed();
    }

    private String createMethodInfo(ProceedingJoinPoint pjp) {
        return new StringBuilder().
                append("method '").
                append(pjp.getSignature().getDeclaringTypeName()).
                append(".").
                append(pjp.getSignature().getName()).
                append("' with args '").
                append(Arrays.toString(pjp.getArgs())).
                toString();
    }

    private void finishTx(Session session) {
        try {
            if (session != null) {
                session.getTransaction().commit();
            }
        }
        catch (Exception e) {
            log.error("Error during commit session. Transaction will be rolled back.", e);
            if (session != null && session.getTransaction() != null) {
                session.getTransaction().rollback();
            }
        }
        finally {
            closeSession(session);
        }
    }

    private void closeSession(final Session session) {
        if (session != null && session.isOpen()) {
            session.close();
        }
    }

    private Session beginTx() throws HibernateException {
        Session session = sessionFactory.getCurrentSession();
        session.getTransaction().begin();
        return session;
    }
}

В классе объявлено несколько pointcuts которые замаплены на методы для чтения, сохранения и удаления сущностей. После pointcut следуют advices, декларирующие экшены, которые будут вызываться в точках pointcut. BaseDAO реализует простой CRUD для всех сущностей. Методы на чтение подразумевается, что будут работать вне транзакций.
А теперь представьте, что будет, если сюда дописать обработку эксепшенов и pointcuts замапить не по имени метода, а на аннотацию @Transactional. Ничего не напоминает? :)

Read more
21 Dec 2012

Рекурсивный перебор каталогов + прокрутка JScrollPane

Добрый день, уважаемые посетители.

Это моя первая статья в данном разделе. В ней я хочу уделить внимание особенностям работы с файловой системой в Java, алгоритму рекурсивного перебора каталогов и файлов, а также механизму реализации автоматической прокрутки JScrollPane во время динамического выведения текста на JTextArea.

N.B. Код проекта можно скачать здесь:

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

Рассмотрим простейший пример с вычислением факториала, то есть произведения 1*2*3*4*…*n, где n - натуральное число. Можно взять быка за рога и забабахать вот такой цикл:

int result = 1;
if (n == 0 || n == 1){
    System.out.println(result);
} else {
    for (int i = 2; i <= n; i++){
        result *= i;
    }
}
System.out.println(result);

Как видим, использование циклов усложняет текст программы, повышает вероятность ошибок, требует дополнительных переменных (и памяти).

Вместо этого можно использовать рекурсию, и обойтись всего 7 строками:

int factorial(int n){
    if (n == 0){
        return 1;
    } else{
        return n * factorial(n-1);
    }
}

Известно, что самое лучшее решение - это простейшее решение. Действительно, цикл гораздо нагляднее и понятен интуитивно. Рекурсия же сложнее для понимания и восприятия. Однако, мы не ищем лёгких путей и стараемся написать красивый и правильный код, а значит должны использовать весь арсенал языка Java.

Давайте посмотрим, как можно использовать рекурсию для последовательного перебора всех файлов или каталогов на жёстком диске. Ниже я привожу коды классов, из которых состоит программа. Главный класс не представляет особого интереса, поскольку он отвечает лишь за создание главного окна.

Код главного класса:

package filewalker;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

/**
 * Главный класс программы
 * Здесь происходит только отрисовка окна приложения
 * @author rad1kal
*/
public class FileWalker{
    private static JFrame frame;
    static MainPanel panel;
    
    public static void main(String[] args) {
        Dimension ScreenSize = Toolkit.getDefaultToolkit().getScreenSize();
        int x = ScreenSize.width / 2 - 600;
        int y = ScreenSize.height / 2 - 300;
        panel = new MainPanel();
        frame = new JFrame("FileWalker: рекурсивный поиск каталогов/файлов");
        frame.setLocation(x, y);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setPreferredSize(new Dimension(1200, 600));
        frame.pack();
        frame.add(panel);
        frame.setVisible(true);
        
        //реализуем слушатель клавиатуры средствами ActionMap
        
        ActionMap am = frame.getRootPane().getActionMap();
        InputMap im = frame.getRootPane().getInputMap(
                JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "QUIT");
        am.put("QUIT", new AbstractAction(){
            @Override
            public void actionPerformed(ActionEvent e){
                frame.dispose();
                System.exit(0);
            }
        });
    }
}

Код главной панели, на которой находятся элементы управления. Здесь реализован пример использования менеджера компоновки GridBagLayout.

package filewalker;

import java.awt.*;
import java.awt.event.*;
import java.io.FileNotFoundException;
import javax.swing.*;

/**
 * Класс, описывающий главную панель
 * @author rad1kal
 * @version 1.0
 */
class MainPanel extends JPanel{
    private ScrollPane outputField;
    private JTextField directoryInputField;

    MainPanel(){
        super();
        setLayout(new GridBagLayout());
        GridBagConstraints c = new GridBagConstraints();
        
        JLabel label = new JLabel("Введите путь к корневому каталогу:");
        c.insets = new Insets(10,20,0,0);
        c.anchor = GridBagConstraints.WEST;
        add(label, c);
        
        directoryInputField = new JTextField();
        c.fill = GridBagConstraints.HORIZONTAL;
        c.anchor = GridBagConstraints.CENTER;
        c.gridy = 1;
        c.insets = new Insets(10, 20, 0, 20);
        add(directoryInputField, c);
        
        JButton button = new JButton("Вывести все подкаталоги/файлы");
        button.addActionListener(new MainPanel.ButtonListener());
        button.setFocusable(false);
        c.gridy = 2;
        c.insets = new Insets(10, 0, 10, 0);
        c.anchor = GridBagConstraints.CENTER;
        c.fill = 0;
        add(button, c);
        
        outputField = new ScrollPane();
        c.insets = new Insets(0, 5, 0, 5);
        c.gridy = 3;
        c.weightx = 1;
        c.weighty = 10;
        c.fill = GridBagConstraints.BOTH;
        add(outputField, c);
        
        label = new JLabel("Нажмите Esc для завершения работы.");
        c.anchor = GridBagConstraints.CENTER;
        c.insets = new Insets(10,20,10,0);
        c.gridy = 4;
        c.weighty = 0;
        add(label, c);
    }
    
    /**
     * Обработчик нажатия кнопки
     * @author rad1kal
     */
    private class ButtonListener implements ActionListener{
        @Override
        public void actionPerformed(ActionEvent e){
            outputField.clearAll();
            String str = directoryInputField.getText();
            Walker walker;
            try{
                walker = new Walker(str, outputField);
            } catch (FileNotFoundException ex){
                outputField.showWarningMessage();
                return;
            }
            Thread t = new Thread(walker);
            t.start();
        }
    }
}

Класс, реализующий основной функционал программы. На нём остановимся подробнее. Итак, мы хотим перебрать все каталоги/файлы, начиная с корневого. Зачем это нужно? Вот возможные варианты ответа:

  • мы хотим найти некоторые данные в файле, но не знаем в каком именно каталоге и файлы они находятся;

  • мы хотим считать информацию из всех/строго определённых файлов;

  • мы хотим выполнить некоторые действия с каждым/строго определённым файлом/каталогом и т.п.

В моём случае я разрабатываю приложение для поиска информации в большом массиве текстовых файлов. Многие пользователи отметят, что я занимаюсь велосипедостроением и трачу время зря, ведь есть TotalCommander с уже встроенным функционалом. Доля истины в этом есть, однако TotalCommander не всегда корректно обрабатывает *.docx и *xlsx файлы, а также прочие XML-документы. Также отмечу, что при минимальном усилии, данную программу можно приспособить для рекурсивного поиска ссылок в веб-страницах, тем более что тема уже поднималась на форуме.

Итак, вот код поисковика:

package filewalker;

import java.io.File;
import java.io.FileNotFoundException;
import javax.swing.SwingUtilities;

/**
 * Данный класс содержит методы для рекурсивного перебора каталогов,
 * начиная с корневого (указывается пользователем).
 * @author rad1kal
 * @version 2.0
 */
public class Walker implements Runnable{
    public File rootDirectory;
    private ScrollPane outputField;
    
    /**
     * Инициализирует поля RootDirectoryName. Класс бросает исключение
     * FileNotFoundException, когда введен неправильный путь к каталогу.
     * @param rootDirectoryPath путь к корневому каталогу.
     * @param outputField ссылка на панель вывода.
     */
    Walker(String rootDirectoryPath, ScrollPane outputField) throws FileNotFoundException{
        this.outputField = outputField;
        File file = new File(rootDirectoryPath);
        if (file.exists() && file.isDirectory())
            rootDirectory = file;
        else
            throw new FileNotFoundException();
    }
    
    /**
     * Запускает поиск в отдельном потоке.
     * Это необходимо для динамического вывода данных на панель.
     */
    @Override
    public void run(){
        scanDirectory(rootDirectory);
    }
    
    /**
     * Поиск всех каталогов и файлов в папке.
     * @param directory 
     */
    void scanDirectory(File directory){
        File[] files = directory.listFiles();
        if (files != null){
            for (File f : files){
                final String path = f.getAbsolutePath();
                //потокобезопасный вывод происходит здесь
                if (SwingUtilities.isEventDispatchThread()){
                    outputField.append(path);
                } else {
                    SwingUtilities.invokeLater(new Runnable(){
                        @Override
                        public void run(){
                            outputField.append(path);
                        }
                    });
                }
                //рекурсивный вызов для просмотра вложенных каталогов
                if (f.isDirectory() && !f.isHidden()){
                    scanDirectory(f);
                } 
            }
        }
    }
}

Думаю, здесь всё понятно. В конструкторе создается и проверяется ссылка на корневой каталог и если ссылка ошибочна, то далее в методе scanDirectory() получаем список всех файлов и подкаталогов, и для каждого подкаталога метод вызывается снова. Метод поиска запускается в отдельном потоке, что дает возможность реализовать анимированное прокручивание полосы прокрутки в JScrollPane. Обратите внимание на то, как реализовано потокобезопасное взаимодействие с компонентами Swing:
if (SwingUtilities.isEventDispatchThread()){
                    outputField.append(path);
                } else {
                    SwingUtilities.invokeLater(new Runnable(){
                        @Override
                        public void run(){
                            outputField.append(path);
                        }
                    });
                }

здесь метод append() вызывается из потока Event Dispatching Thread, назначением которого является обработка событий связанных с графическим интерфейсом. Добавляя строку на панель вывода мы создаем событие, требующее перерисовки интерфейса. Вызов Event Dispatching Thread позволяет синхронизировать получение строк и их вывод.
Класс, наследующий JScrollPane:
package filewalker;

import javax.swing.JScrollPane;
import javax.swing.JTextArea;

/**
 * Класс, реализующий JScrollPane и содержащий метод
 * для добавления строк и прокручивания полосы прокрутки ScrollBar
 * @author rad1kal
 * @version 1.0
 */
class ScrollPane extends JScrollPane{
    private static JTextArea jta = new JTextArea();
    private final String WARNING_MESSAGE =
            "Вы ввели неверный путь или он ссылается на регулярный файл.";
    
    ScrollPane(){
        super(jta);
        setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
    }
    
    /**
     * Переопределённый метод, в который добавлена прокрутка VerticalScrollBar.
     * @param s добавляемая строка.
     */
    void append(String s){
        jta.append(s.concat("\n"));
        //Прокручивает полосу прокрутки.
        getVerticalScrollBar().setValue(getVerticalScrollBar().getMaximum());
    }
    
    /**
     * Метод очищает панель вывода.
     */
    void clearAll(){
        jta.setText("");
    }
    
    /**
     * Метод выводит предупреждение о неверном пути к корневому каталогу.
     */
    void showWarningMessage(){
        jta.append(WARNING_MESSAGE);
    }
}

Спасибо за ваше внимание, все вопросы, пожелания и замечания пишите в личку. Прошу не судить строго, ведь это моя первая статья. :)

Read more