Опубликован: 18.03.2010 | Уровень: специалист | Доступ: платный
Лекция 4:

Технология Enterprise Java Beans. Часть 2

Компоненты, управляемые сообщениями

Компоненты, управляемые сообщениями (Message-Driven Beans) - это серверные компоненты без состояния, способные обрабатывать асинхронные сообщения протокола JMS (Java Messaging Service).Помимо этого компоненты поддерживают механизм транзакций (о нем дальше). Компонент отвечает за обработку сообщений, а его контейнер заботится об автоматическом управлении всем окружением компонента, включающим в себя транзакции, безопасность, ресурсы, совместный доступ и подтверждения получения сообщений.

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

Механизм транзакций в EJB

Что такое транзакция?

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

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

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

Транзакции обладают следующими свойствами, называемыми по-английски ACID:

  • Атомарность (Atomicity).Транзакция должна быть выполнена целиком или не выполнена совсем. Для успешного выполнения транзакции все составляющие ее действия должны завершиться благополучно. В таком случае произведенные изменения фиксируются (commit).Если же в одном из действий возникает ошибка, транзакция отменяется и все возвращается в исходное состояние (rollback),как будто транзакция и не начиналась. Например, покупатель не согласится уйти без своей покупки после того, как заплатил за нее деньги: либо он получит свою покупку, либо добьется возврата денег. Остановка в промежуточном состоянии невозможна (если человек в здравом уме).
  • Согласованность (Consistency).Речь идет о согласованности (непротиворечивости) данных в хранилище, над которым производится транзакция. Транзакция должна переводить данные из одного согласованного состояния в другое согласованное. Например, после того, как покупатель ушел с покупкой, информация об этом должна попасть в систему складского учета. Если же товар все еще будет числиться на складе, то возникнет несогласованность
  • Изолированность (Isolation).Транзакция должна выполняться изолированно от других транзакций или процессов. Во время выполнения транзакции данные не могут изменяться извне, другой частью системы. Например, в чек, который выбивает кассир, не должны попасть товары покупателя, которого в тот же момент обслуживает соседний кассир.
  • Устойчивость (Durability).Устойчивость подразумевает, что по завершении транзакции все изменения данных записываются в хранилище. Таким образом, даже сбой в системе не приведет к потере данных, и работа системы может быть возобновлена из того состояния, когда успешно завершилась последняя транзакция.

Транзакции в EJB

Одним из главных достоинств EJB является поддержка декларативного управления транзакциями. Без такой возможности пришлось бы смешивать бизнес-логику с громоздкой обработкой всех возможных ошибок и с поддержкой отката данных к исходному состоянию (а что если при откате произойдут ошибки?), а это негативно сказалось бы на читаемости и поддерживаемости кода.

Декларативное управление транзакциями позволяет обеспечить выполнение ACID -свойств путем маркирования бизнес-методов аннотацией @javax.ejb.TransactionAttribute или редактирования XML -дескриптора развертывания, то есть без изменений бизнес-логики. Всю сложную работу по отмене транзакции при возникновении ошибки возьмет на себя EJB- контейнер.

Обычно каждый бизнес-метод EJB является транзакцией. Это означает, что все действия, выполняемые в рамках этого метода, включая вызовы других EJB,являются одной транзакцией и обладают ACID -свойствами. В случае возникновения исключения (потомка RuntimeException или произвольного исключения, помеченного как @javax.ejb.ApplicationException (rollback=true) ) транзакция отменяется. Более тонкое управление транзакциями, вплоть до запрета транзакций, осуществляется путем задания параметров аннотации @javax.ejb. TransactionAttribute:

  • NOT_SUPPORTED. При вызове метода с таким атрибутом текущая транзакция приостанавливается. Метод исполняется вне транзакции. При выходе из метода транзакция возобновляется.
  • SUPPORTS. Такой атрибут означает, что метод может вызываться как в контексте транзакции, так и вне его. Если вызов метода произошел в рамках транзакции, то метод исполняется как ее часть. При вызове метода вне транзакции новая транзакция не инициируется.
  • REQUIRED. Атрибут говорит о том, что метод может исполняться только в рамках транзакции. Это может быть либо уже существующая транзакция, из которой был вызван этот метод, либо новая транзакция, автоматически инициируемая для этого метода при вызове его извне транзакции. Все методы EJB по умолчанию имеют именно этот атрибут.
  • REQUIRES_NEW. Данный атрибут означает, что при вызове метода всегда инициируется новая транзакция, вне зависимости от того, вызван он из уже существующей транзакции или нет. Если метод был вызван в контексте транзакции, на время работы метода та транзакция приостанавливается.
  • MANDATORY. Атрибут показывает, что метод может вызываться только в контекте существующей транзакции. В противном случае будет выброшено исключение javax.ejb.EJBTransactionRequiredException.
  • NEVER. Такой атрибут говорит о том, что метод не может быть вызван в рамках транзакции. Вызов метода из транзакции приведет к исключению javax.ejb.EJBException. Успешный вызов метода возможен только извне транзакции. Еще более гибкое управление транзакциями возможно при помощи объекта UserTransaction, однако этот способ увеличивает вероятность "сделать что-нибудь не так" и сводит на нет достоинства EJB по декларативному управлению транзакциями.

Пример: перевод денег с одного счета на другой

Классическим примером транзакции является перевод денег с одного банковского счета на другой. Эта операция состоит из двух действий: снятие денег с первого счета и добавление денег на второй счет. Нетрудно заметить, что ошибка в одном из этих действий приведет к нарушению согласованности системы, то есть к потере денег или к появлению денег из ниоткуда. Различные неожиданности возможны и в случае, когда один счет одновременно участвует в двух переводах. Решение проблемы известно: необходимо использовать транзакции с их ACID -свойствами.

В данном примере мы разработаем сеансовый компонент без состояния (Stateless session bean),который осуществляет перевод денег с одного банковского счета на другой и делает запись об этом в протокол. Вот удаленный интерфейс этого EJB:

package ru.ifmo.javaee.bank;
import java.util.List;
import javax.ejb.Remote;
@Remote
public interface Bank  
{
public int createAccount();
public void removeAccount(int id);
public List<BankAccount> listAccounts();
public void deposit(int id,   int sum);
public void withdraw(int id,   int sum);
public void transfer(int srcId,   int dstId,   int sum);
}

Демонстрировать транзакции мы будем на примере метода transfer(). Остальное играет вспомогательную роль. Рассмотрим реализацию EJB:

package ru.ifmo.javaee.bank;
import java.util.List;
import javax.ejb.Stateless; 
import javax.ejb.TransactionAttribute; 
import javax.ejb.TransactionAttributeType; 
import javax.persistence.EntityManager; 
import javax.persistence.PersistenceContext; import javax.persistence.Query;
@Stateless
public class BankBean implements Bank  
{
@PersistenceContext 
private EntityManager em;
public int createAccount()   
{
BankAccount account = new BankAccount(); 
em.persist(account);
return account.getId();
}
public void removeAccount(int id)   
{
BankAccount account = em.find(BankAccount.class, id); em.remove(account);
}

public List<BankAccount> listAccounts()   
{
Query query = em.createQuery("FROM BankAccount"); 
return query.getResultList();
}

public void deposit(int id,   int sum)   
{
BankAccount account = em.find(BankAccount.class, id); account.deposit(sum);
}

public void withdraw(int id,   int sum)   
{
BankAccount account = em.find(BankAccount.class, id);
account.withdraw(sum);
}

@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) 

public void transfer(int srcId, int dstId, int sum)   
{
withdraw(srcId,   sum);
deposit(dstId,   sum);
em.persist(new TransferRecord(srcId,  dstId, sum)); 
if   (Math.random()   > 0.5)   
{
throw new RuntimeException("fake exception");
}
}
}

Атрибут REQUIRES_NEW обеспечивает исполнение каждого вызова метода transfer() в контексте отдельной транзакции.

Транзакция состоит из снятия денег с первого счета, добавления на второй счет и создания записи о произведенном переводе. Ошибка в любом из этих действий приведет к генерации исключения и откату транзакции.

Для простоты ошибка генерируется искусственно с вероятностью 50% (см. последние три строки метода transfer () ). В среднем половина транзакций должна завершаться удачно, оставшиеся должны отменяться.

Приведем исходные коды используемых здесь вспомогательных классов.

package ru.ifmo.javaee.bank;
import java.io.Serializable;
import javax.persistence.Entity; 
import javax.persistence.GeneratedValue; 
import javax.persistence.Id; 
import javax.persistence.Table;

@Entity
@Table(name="account")
public class BankAccount implements Serializable  
{
private static final long serialVersionUID = 8754634761874208747L;
private int id; 
private int balance;
public BankAccount()   
{   }

public BankAccount(int balance)   
{ 
this.balance = balance;
}
@Id
@GeneratedValue 
public int getId()   
{ 
return id;
}

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



public int getBalance()   
{ return balance;

}

public void setBalance(int balance)   
{ 
if (balance >= 0)  
	{
	this.balance = balance; 
	}  
else  
	{
	throw new IllegalArgumentException("Negative balance is not
	allowed");
	}
}

public void deposit(int sum)   
{ 
if (sum < 0)  
	{
	throw new IllegalArgumentException("Negative sum is not allowed");
	}

balance += sum;
}

public void withdraw(int sum)   
{ 
if (sum < 0)  
	{
	throw new IllegalArgumentException("Negative sum is not allowed");
	}

if   (sum > balance)   
	{
	throw new IllegalArgumentException("Withdrawn sum greater than balance");
	}
balance -= sum;
}

public String toString()   
{
return "Account #" + id + " with balance $" + balance;
}
}

package ru.ifmo.javaee.bank;
import javax.persistence.Column; 
import javax.persistence.Entity; 
import javax.persistence.GeneratedValue; 
import javax.persistence.Id; 
import javax.persistence.Table;

@Entity
@Table(name="transfer_log") 
public class TransferRecord 
{
private int id; 
private int srcId; 
private int dstId; 
private int sum;

public TransferRecord(int srcId,   int dstId,   int sum)   
{ 
this.srcId = srcId; 
this.dstId = dstId; 
this.sum = sum;
}



@Id
@GeneratedValue 
public int getId()   
{ 
return id;
}

public void setId(int id)   
{ 
this.id = id;
}
public int getSrcId()   
{ 
return srcId;
}

public void setSrcId(int srcId)   
{
this.srcId = srcId;
}

public int getDstId()   
{ 
return dstId;
}

public void setDstId(int dstId)   
{ 
this.dstId = dstId;
}
@Column(name="transfer sum") 
public long getSum()   
{
return sum;
}
public void setSum(int sum)   
{ 
this.sum = sum;
}
}

Дескриптор развертывания для JBoss:

<?xml version="1.0" encoding="UTF-8"?> <jboss>
<enterprise-beans> <session>
<ejb-name>BankBean</ejb-name> 
<jndi-name>Bank/Remote</jndi-name> 
</session> </enterprise-beans> 
</jboss>

Все готово к тестированию. Тестировать работу транзакций будем при помощи простой программы-клиента.

package ru.ifmo.javaee.bank;
import java.util.Hashtable; import java.util.List;
import javax.naming.Context; 
import javax.naming.InitialContext; import javax.naming.NamingException;

public class BankClient  
{
public static void main(String[]   args)   
{ if   (args.length == 0)   
	{
	System.out.println("Usage: BankClient <command> [arguments]"); return;
	}
Hashtable<String,   String> 
env = new Hashtable<String,   String>();

env.put(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory");

env.put(Context.PROVIDER_URL, "localhost:1099");
try 
	{
	InitialContext ctx = new InitialContext(env); 
	Bank bank = (Bank) ctx.lookup("Bank/Remote"); ctx.close();
	if   (args[0].equals("createAccount"))   
		{
		int balance = Integer.parseInt(args[1]); 
		int id = bank.createAccount(); 
		bank.deposit(id,  balance);
		System.out.println("Created account #" + id +
		
		" with $" + balance);
		}  
	else if   (args[0].equals("removeAccount"))   
		{ 
		int id = Integer.parseInt(args[1]); bank.removeAccount(id);
		System.out.println("Removed account #" + id); 
		}  
	else if   (args[0].equals("listAccounts"))   
		{
		System.out.println("Listing all accounts..."); 
		List<BankAccount> accounts = bank.listAccounts(); 
		for (BankAccount account : accounts)   
			{ System.out.println(account);
			}
		}  
	else if   (args[0].equals("transfer"))   
		{
		int srcId = Integer.parseInt(args[1]); 
		int dstId = Integer.parseInt(args[2]); 
		int amount = Integer.parseInt(args[3]); System.out.println("Transferring $" + amount +
		"  from #" + srcId + " to #" + dstId + "...");
		try 
			{
			bank.transfer(srcId,  dstId,   amount); 
			}   
		catch   (RuntimeException e)   
			{
			System.out.println("Caught exception:   " + e.getMessage());
			System.out.println("Transaction rolled back");
			}
		}  
	else  
		{
		System.out.println("Expecting one of: createAccount, removeAccount, listAccounts, transfer");
		}
	}   
catch   (NamingException e)   { e.printStackTrace();
}
}
}

В зависимости от переданных в командной строке параметров программа выполняет разные действия:

  • createAccount <сумма> - создание счета с заданной суммой, будет выведен номер нового счета;
  • removeAccount <номер> - удаление счета с данным номером;
  • listAccounts - вывод списка счетов с номерами и суммами;
  • transfer <номер1> <номер2> <сумма> - перевод заданной суммы с первого счета на второй.
Антон Зубеков
Антон Зубеков

Здравствуйте, подскажите пожалуйста где можно достать материалы по курсу Кросс-платформенные и многозвенные технологии, о которых говориться, к примеру, в Лекции 2. Пример "Служба мгновенных сообщений"

Ярославй Грива
Ярославй Грива
Россия, г. Санкт-Петербург
Ольга Малых
Ольга Малых
Россия, Казань, Университет управления "ТИСБИ"