Кросс-платформенные и многозвенные технологии

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

Разбить на страницы
Показывать лекцию целиком

Объектные компоненты

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

Объектные компоненты представляют записи в базе данных. При изменении объектных компонентов происходят изменения в базе данных. В предыдущих примерах уже рассматривалась возможность внесения изменения в базу данных непосредственно через сеансовые компоненты, но это было не очень удобно - приходилось, к примеру, для создания заказа в базе данных выполнять несколько запросов SQL.При использовании объектных компонентов можно было бы просто использовать метод Order.createOrder (List commoditiesList). Компонент Order представляет приложению полный доступ к информации о заказах, а также возможность управлять ими -создавать, модифицировать и удалять. При помощи объектных компонентов разработку можно упростить и сделать более рентабельной.

Когда объектный компонент создается, в базу данных должна быть вставлена новая запись и создан экземпляр компонента, связанный с этими данными. По мере того, как компонент изменяется, его состояние меняется, и изменения должны быть синхронизированы с данными в базе данных. За процесс координации данных, представляющий экземпляр компонента, с базой данных, отвечает служба Persistence.

Существует два основных типа объектных компонентов, различающихся реализацией механизма постоянства: компоненты с постоянством, управляемым контейнером (container-managed persistence или CMP),и компоненты поддерживающие постоянство самостоятельно (bean-managedpersistence или BMP).

В случае с CMP,контейнер знает, как постоянство экземпляра и поля отношений с другими компонентами отображаются в базе данных, и автоматически вставляет, модифицирует и удаляет данные. В случае BMP,вся работа выполняется разработчиком самостоятельно. Ему необходимо написать код манипуляции базой данных. Контейнер EJB сообщает экземпляру компонента, когда безопасно вставлять, модифицировать и удалять его данные из базы данных. Экземпляр выполняет всю работу по обеспечению своего постоянства самостоятельно.

Начиная со спецификации EJB 2.0, CMP подверглось очень сильным изменениям. В результате CMP теперь больше не является обратно совместимым с предыдущими спецификациями. Поэтому довольно часто используют сокращение CMP 2.0.

В CMP 2.0 контейнер объектных компонентов автоматически управляет их состоянием. Контейнер заботится о регистрации компонентов в транзакциях и поддерживает их состояние в базе данных. Разработчик компонентов описывает атрибуты и связи компонентов при помощи виртуальных (virtual) полей постоянства и отношений. Разработчик не определяет их явно. Вместо этого в классе создаются абстрактные методы доступа ( set и get ).

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

При создании объектного компонента (BMP или CMP),контейнер EJB выполняет следующую последовательность действий:

  • Контейнер вызывает метод Class.newInstance() у класса объектного компонента. До этого момента объектный компонент существовал только как набор файлов (класса компонента, интерфейсов и дескриптора развертывания). У компонента вызывается метод setEntityContext (EntityContext). После этого созданный компонент помещается в пул объектных компонентов. Обычно контейнер хранит несколько экземпляров компонента в пуле. Они помещаются туда при запуске контейнера. В дальнейшем, в зависимости от оьстоятельств, контейнер может увеличивать или уменьшать количество экземпляров компонентов в пуле. Компонент, находящийся в пуле, способен выполнять запросы ejbFind<суффикс> и ejbSelect<суффикс> (рассмотрим далее).
  • При вызове методов ejbCreate() а затем метода ejbPostCreate() у домашнего интерфейса, или же когда контейнер активирует компонент посредством метода ejbActivate(), компонент переходит в состояние готовности. После завершения метода ejbCreate() создается первичный ключ - идентификатор созданного объекта, а затем уже вызывается метод ejbPostCreate(). В состоянии готовности компонент способен обслуживать запросы от клиента. Когда клиент вызывает метод удаленного интерфейса getName(), то этот метод вызывается посредством RMI у компонента на сервере. В состоянии готовности могут также вызываться методы ejbLoad() и ejbStore(). Они вызываются в любом порядке -порядок определяется производителем компонента. Некоторые производители вызывают эти методы перед каждым выполнением прикладного метода.
  • Компонент может переходить обратно из состояния готовности в пул в случае пассивизации (посредством метода ejbPassivate() ). Эта операция может выполняться для более эффективного управления ресурсами. Также компонент может переходить обратно при вызове метода ejbRemove() - в этом случае все данные о компоненте удаляются из базы данных.
  • Когда контейнер решит уменьшить количество экземпляров в пуле, он удаляет некоторые из них. Теперь сборщик мусора может их окончательно удалить из памяти. После удаления из пула, у компонента вызывается метод unsetEntityContext().
  • Пример "Космические корабли"

    Компонент

    Разработаем объектный компонент BMP. Он будет использоваться для хранения информации о космических кораблях. Параметры космических кораблей компонент будет получать из базы данных, посредством его set и get методов клиентская программа или другие компоненты смогут изменять эти параметры. Компонент будет называться WidgetBean.

    Создание таблицы в базе данных

    (рис 4.1) Инфологическая модель базы данных космических кораблей

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

    create or replace package WIDGET_ID_PKG as LAST_ID integer;
    function GET_LAST_ID return integer; end WIDGET_ID_PKG; 
    /
    create or replace package body WIDGET ID PKG as function GET_LAST_ID return integer is 
    begin
    return LAST_ID; 
    end GET_LAST_ID;
    end WIDGET_ID_PKG;
    
    /
    
    drop table WIDGETS;
    create table WIDGETS   (
    ID integer not null, 
    DESCRIPTION varchar(50) not null, 
    PRICE float not null, 
    primary key(ID)
    );
    drop sequence WIDGETS_SEQ;
    create sequence WIDGETS_SEQ 
    start with 1 
    increment by 1
    nomaxvalue;
    
    create trigger WIDGETS_TRIGGER before insert on WIDGETS 
    for each row 
    begin
    select WIDGETS_SEQ.nextval into WIDGET_ID_PKG.LAST_ID from dual;
    select WIDGET_ID_PKG.LAST_ID into  :new.id from dual;
    end;
    /
    insert into WIDGETS(DESCRIPTION, PRICE)
    values('Buran',   1000);
    insert into WIDGETS(DESCRIPTION, PRICE)
    values('Shuttle',   2000);
    insert into WIDGETS(DESCRIPTION, PRICE)
    values('Space Ship One',   3000);

    Удаленный интерфейс

    Вначале создадим для компонента удаленный интерфейс, который будет состоять только из get и set методов. Эти методы будут соответствовать полям в только что созданной таблице WIDGETS.

    public interface WidgetRemote extends EJBObject  
    {
    public Integer getId()   throws RemoteException;
    public double getPrice()   throws RemoteException;
    public String getDescription()   throws RemoteException;
    public void setPrice(double price)   throws RemoteException;
    public void setDescription(String description)   throws RemoteException;
    }

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

    Домашний интерфейс

    Домашний интерфейс компонента WidgetBean несколько отличается от аналогичных домашних интерфейсов сеансовых компонентов. В нем помимо create методов определены find методы, которые позволяют осуществлять поиск компонентов по определенным параметрам. К примеру, можно искать все космические корабли с ценою не выше заданной.

    Для компонента WidgetBean определен create метод, который создает новый экземпляр компонента с заданным описанием и ценой. В качестве find методов определены два метода, которые обычно присутствуют у всех домашних интерфейсов объектных компонентов -

  • public Collection findAll() - возвращает коллекцию из всех компонентов, определенных на данный момент в базе данных. В качестве объектов коллекции выступают удаленные интерфейсы компонента.
  • public WidgetRemote findByPrimaryKey(Integer widgetId) -возвращает удаленный интерфейс компонента с заданным первичным ключом. В случае, если компонента с таким первичным ключом не существует, генерируется исключение javax.ejb.FinderException.
  • public interface WidgetHome extends EJBHome  
    {
    public WidgetRemote create(String description,  double price)
    throws RemoteException,   CreateException;
    public WidgetRemote findByPrimaryKey(Integer widgetId)
    throws FinderException,   RemoteException;
    public Collection findAll()   throws FinderException,   RemoteException;
    }

    Класс компонента

    В классе компонента придется вручную реализовывать все заявленные выше методы, а также методы ejbLoad (), ejbStore (), ejbRemove (), ejbActivate () и ejbPassivate().

    Класс объектного компонента должен имплементировать (implement) интерфейс EntityBean.

    public class WidgetBean implements EntityBean

    Начнем с самого простого - set и get методов. Их реализация приводится ниже. Могло бы показаться несколько странным, что в этих методах отсутствуют какие-либо механизмы взаимодействия с базой данных. Все взаимодействия с базой данных по поддержанию постоянства компонента описываются в методах ejbLoad() и ejbStore().

    private Integer widgetId;
    private String description;
    private double price;
    private EntityContext context;
    public Integer getId()   
    {
    return widgetId;
    }
    public String getDescription()   
    {
    return description;
    }
    public double getPrice()   
    { 
    return price;
    }
    
    public void setDescription(String description)   
    { 
    this.description = description;
    }
    public void setPrice(double price)   
    { 
    this. price = price;
    }

    В методах ejbLoad() и ejbStore() вызываются private методы компонента loadRow() и storeRow(). Метод loadRow () по widgetId загружает из базы данных цену и описание, а метод storeRow() выполняет сохранение этих полей в базе данных. Работа с базой данных аналогична работе с базой данных из примера с интернет магазином. В начате метода ejbLoad() выполняется следующая инструкция -

    widgetId = (Integer) context.getPrimaryKey();

    Метод loadRow() использует widgetId для того, чтобы загрузить запись из базы данных.

    Но в некоторых случаях widgetId может быть недоступно (эксперты говорят, что для некоторых производителей контейнеров ejb это случается, если был сделан запрос поиска по первичному ключу посредством метода Integer ejbFindByPrimaryKey (Integer) (см. далее), и компонент в этот момент был активизирован). Для этого вызывается метод Object getPrimaryKey () класса EntityContext. Через контекст первичный ключ доступен всегда. Возможно это обуславливается особенностями некоторых контейнеров EJB.Так или иначе, эксперты гарантируют, что если получать первичный ключ в методе ejbLoad() и методе ejbActivate(), то проблем возникнуть не должно. Что ж, доверимся их мнению.

    public void ejbLoad()   
    {
    widgetId =  (Integer)   context.getPrimaryKey();
    try 
    	{
    	loadRow();
    	}   
    catch   (Exception ex)   
    	{
    	throw new EJBException("ejbLoad:   " + ex.getMessage());
    	}
    }
    
    public void ejbStore()   
    {
    try 
    	{
    	storeRow();
    	}   
    catch   (Exception ex)   
    	{
    	throw new EJBException("ejbLoad:   " + ex.getMessage());
    	}
    }
    private void loadRow()   throws SQLException  
    	{ 
    	Connection con = null; 
    	PreparedStatement prepStmt = null;
    	try 
    		{
    		con = dataSource.getConnection(); 
    		String selectStatement ="select DESCRIPTION, PRICE " +
    		"from WIDGETS where ID = ?  ";
    		prepStmt = con.prepareStatement(selectStatement); 
    		prepStmt.setInt(1,  widgetId); 
    		ResultSet rs = prepStmt.executeQuery();
    		if   (rs.next())   
    			{
    			this.description = rs.getString(1); 
    			this.price = rs.getDouble(2); 
    			prepStmt.close();
    			}
    		else  
    			{
    			prepStmt.close();
    			throw new NoSuchEntityException("Row for widgetId " + widgetId + " not found in database.");
    			}
    		con.close();
    		}   
    	catch   (Exception e)   
    		{
    		} 
    	finally 
    		{
    		if   (prepStmt   != null)   
    			{
    			try 
    			{
    			prepStmt.close(); 
    			}   
    			catch   (Exception e)
    			{
    			}
    			}
    		if   (con   != null)   
    			{
    			try 
    			{
    			con.close();
    			}   
    			catch   (Exception e)
    			{
    			}
    			}
    		}
    	}
    
    private void storeRow()   throws SQLException  
    	{ 
    	Connection con = null; 
    	PreparedStatement prepStmt = null;
    	try 
    		{
    		con = dataSource.getConnection(); 
    		String updateStatement = "update WIDGETS set DESCRIPTION =   ? , 
    			" + "PRICE = ? " + "where ID = ?";
    		prepStmt = con.prepareStatement(updateStatement);
    		prepStmt.setString(1,  description); 
    		prepStmt.setDouble(2,  price); 
    		prepStmt.setInt(3,  widgetId);
    		int rowCount = prepStmt.executeUpdate();
    		prepStmt.close();
    		con.close();
    		
    		if   (rowCount == 0)   
    			{
    			throw new EJBException("Storing row for widgetId " + widgetId + " failed.");
    			}
    		}   
    	catch   (Exception e)   
    		{
    		} 
    	finally 
    		{
    		if (prepStmt != null) 
    			{
    			try 
    			{
    			prepStmt.close(); 
    			}   
    			catch   (Exception e)   
    			{
    			}
    		}
    		if   (con   != null)   
    			{
    			try 
    			{
    			con.close();
    			}   
    			catch   (Exception e)   
    			{
    			}
    			}
    		}
    	}

    Теперь перейдем к create методу компонента WidgetBean.В нем выполняется создание записи в базе данных, соответствующей компоненту. Делается это посредством метода Integer insertRow(String description, double price). Он создает новую запись в таблице базы данных, а потом возвращает ID созданной записи через запрос к WIDGET_ID_PKG. Метод ejbCreate() возвращает первичный ключ созданного экземпляра компонента, но клиентское приложение при вызове метода create домашнего интерфейса получит уже не первичный ключ, а удаленный интерфейс созданного компонента (см. WidgetRemote ). Метод ejbPostCreate () не выполняет никаких действий. При работе с BMP он вообще не используется. В нем иногда возникает необходимость при работе с CMP.

    public Integer ejbCreate(String description,  double price)   throws
    CreateException  
    {
    try 
    	{
    	widgetId = insertRow(description, price);
    	}   
    catch   (Exception ex)   
    	{
    	throw new EJBException("ejbCreate:   " + ex.getMessage());
    	}
    
    this.description = description;
    this.price = price;
    return widgetId;
    }
    
    public void ejbPostCreate(String description,  double price)   
    {   }
    
    private Integer insertRow(String description,  double price)   throws SQLException  
    {
    Connection con = null; 
    PreparedStatement prepStmt = null;
    try 
    	{
    	con = dataSource.getConnection();
    	String insertStatement ="insert into WIDGETS(DESCRIPTION,   PRICE)   
    		values  " + "  (?, ?)";
    	
    	prepStmt = con.prepareStatement(insertStatement);
    	prepStmt.setString(1,  description); 
    	prepStmt.setDouble(2,  price); 
    	prepStmt.executeUpdate();
    	prepStmt.close();
    	prepStmt = con.prepareStatement("select " + WIDGET_ID_PKG.GET_LAST_ID from dual");
    	ResultSet r = prepStmt.executeQuery(); 
    	int id = -1;
    	if (r.next()) 
    		{
    		id = r.getInt(1);
    		}
    	
    	if (id == -1) 
    		{
    		throw new SQLException("Error getting last index");
    		}
    	
    	con.close();
    	return new Integer(id);
    	}   
    catch   (Exception e)   
    	{
    	} 
    finally 
    	{
    	if   (prepStmt   != null)   
    		{
    		try 
    		{
    		prepStmt.close(); 
    		}   
    		catch   (Exception e)
    		{
    		}
    		}
    	if   (con   != null)   
    		{
    		try 
    		{
    		con.close();
    		}   
    		catch   (Exception e)
    		{
    		}
    		}
    	}
    
    return null;
    }

    При помощи метода ejbRemove() выполняется удаление экземпляра компонента из базы данных. Из него вызывается метод deleteRow (Integer id), который удаляет запись в базе данных с соответствующим первичным ключом.

    public void ejbRemove()   
    {
    try 
    	{
    	deleteRow(widgetId); 
    	}   
    catch   (Exception ex)   
    	{
    	throw new EJBException("ejbRemove:   " + ex.getMessage());
    	}
    private void deleteRow(Integer widgetId)   throws SQLException  
    	{
    	Connection con = null; 
    	PreparedStatement prepStmt = null;
    	try 
    		{
    		con = dataSource.getConnection(); 
    		String deleteStatement ="delete from WIDGETS where ID = ?  "; 
    		prepStmt = con.prepareStatement(deleteStatement);
    		prepStmt.setInt(1,  widgetId); 
    		prepStmt.executeUpdate(); 
    		prepStmt.close(); 
    		con.close();
    		}   
    	catch   (Exception e)   
    		{
    		} 
    	finally 
    		{
    		if (prepStmt != null) 
    			{ 
    			try 
    			{
    			prepStmt.close(); 
    			}   
    			catch   (Exception e)   
    			{
    			}
    			}
    		}
    		if   (con  != null)   
    			{
    			try 
    			{
    			con.close();
    			}   
    			catch   (Exception e)   
    			{
    			}
    			}
    		}

    Реализация методов поиска элементов аналогична реализации предыдущих методов, за исключением что возвращается либо первичный ключ ( Integer ejbFindByPrimaryKey(Integer primaryKey) ) либо коллекция из первичных ключей ( Collection ejbFindAll() ), как и в случае с create методами.

    public Integer ejbFindByPrimaryKey(Integer primaryKey)   throws FinderException  
    {
    boolean result;
    try 
    	{
    	result = selectByPrimaryKey(primaryKey);
    	}   
    catch   (Exception ex)   
    	{
    	throw new EJBException("ejbFindByPrimaryKey:   " + ex.getMessage());
    	}
    if   (result)   
    	{
    	return primaryKey; 
    	}  
    else  
    	{
    	throw new ObjectNotFoundException("Row for id " + primaryKey + " not found.");
    	}
    }
    
    public Collection ejbFindAll() throws FinderException  
    {
    try 
    	{
    	Collection col = getAllElements();
    	return col;
    	}   
    catch   (SQLException e)   
    	{
    	throw new ObjectNotFoundException("Error retriving all" + "the elements in the database.");
    	}
    }
    private boolean selectByPrimaryKey(Integer primaryKey)   throws SQLException  	
    {
    Connection con = null;
    PreparedStatement prepStmt = null;
    boolean result = false;
    try 
    	{
    	con = dataSource.getConnection(); String selectStatement =
    	"select ID from WIDGETS where ID = ? ";
    	prepStmt = con.prepareStatement(selectStatement); 
    	prepStmt.setInt(1,  primaryKey);
    	ResultSet rs = prepStmt.executeQuery();
    	result = rs.next();
    	prepStmt.close();
    	con.close();
    	return result;
    	}   
    catch   (Exception e)   
    	{ 
    	return result;
    	} 
    finally 
    	{
    	if   (prepStmt   != null)   
    		{
    		try 
    		{
    		prepStmt.close(); 
    		}   
    		catch   (Exception e)
    		{
    		}
    		}
    	if   (con != null)   
    		{
    		try 
    		{
    		con.close();
    		}   
    		catch (Exception e)
    		{
    		}
    		}
    	}
    }
    private Collection getAllElements() throws SQLException  
    { 
    Connection con = null;
    PreparedStatement prepStmt = null; 
    ArrayList list = new ArrayList();
    try 
    	{
    	con = dataSource.getConnection(); String selectStmt = "select ID " + "from WIDGETS";
    	prepStmt = con.prepareStatement(selectStmt);
    	ResultSet rs = prepStmt.executeQuery();
    	while   (rs.next())   
    		{
    		int id = rs.getInt(1); 
    		list.add(new Integer(id));
    		}
    	return list;
    	}   
    catch   (Exception e)   
    	{
    	} 
    finally 
    	{
    	if (prepStmt != null) 
    		{ 
    		try 
    		{
    		prepStmt.close(); 
    		}   
    		catch   (Exception e)
    		{
    		}
    		}
    	if   (con   != null)   
    		{
    		try 
    		{
    		con.close();
    		}   
    		catch   (Exception e)
    		{
    		}
    		}
    	}
    return null;
    }

    Осталось рассмотреть последний методы, связанные с активизацией, пассивизацией и получением контекста объектных компонентов. В методе void setEntityContext (EntityContext context) сохраняется переданный контекст объектных компонентов, а также организуется соединение с базой данных. Стоит отметить, что в методе void ejbActivate () также вызывается метод Object EntityContext.getPrimaryKey (), который восстанавливает значение первичного ключа widgetId. В методе ejbPassivate () эксперты рекомендуют обнулять первичный ключ.

    public void setEntityContext(EntityContext entityContext)   
    { 
    this.context = entityContext;
    try 
    	{
    	InitialContext ctx = new InitialContext(); 
    	if   (dataSource == null)
    	dataSource =   (DataSource)
    	ctx.lookup("java:/OracleDS"); 
    	System.out.println("Data source aquired"); 
    	}   
    catch   (NamingException ex)   
    	{
    	throw new EJBException(ex);
    	}
    }
    
    public void unsetEntityContext()   
    { this.context = null;
    }
    public void ejbActivate()   
    {
    widgetId = (Integer)context.getPrimaryKey();
    }
    public void ejbPassivate()   
    { widgetId = null;
    }

    Десприптор развертывания

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

  • В элементе <persistence-type> описывается тип постоянства - значение "Bean" означает, что постоянством управляет компонент, а значение "Container" - то, что пространством управляет контейнер.
  • В элементе <prim-key-class> определен класс первичного ключа. В данном случае это класс Integer. Первичный ключ компонента может состоять из нескольких полей, в этом случае в качестве первичного ключа должен выступать определенный пользователем сериализуемый класс, с полями, соответствующими полям первичного ключа в базе данных.
  • Элемент <reentrant> определяет, разрешены ли повторые вхождения в методы экземпляра компонента. То есть если вдруг пришли два вызова методов компонента на один и тот же экземпляр компонента одновременно. Если значение "true", то они оба одноврменно будут выполнять соответствующие методы компонента, если же значение "false", то тогда одному вызову придется подождать.
  • <?xml version="1.0" encoding="UTF-8"?>
    
    <ejb-jar id="ejb-jar ID" version="2.1"
    xmlns = "http://java.sun.com/xml/ns/j2ee" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/ejb-jar_2_1.xsd">
    
    <display-name>SpaceShipBean</display-name>
    
    <enterprise-beans>
    
    <entity>
    <description>
    The bean demonstrating space ships </description>
    <ejb-name>WidgetBean</ejb-name> 
    <home>spaceShip.WidgetHome</home> 
    <remote>spaceShip.WidgetRemote</remote> 
    <ejb-class>spaceShip.WidgetBean</ejb-class> 
    <persistence-type>Bean</persistence-type> 
    <prim-key-class>java.lang.Integer</prim-key-class> 
    <reentrant>false</reentrant> </entity> 
    </enterprise-beans> </ejb-jar>

    Клиентское приложение

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

    (рис 4.2) Внешний вид приложения

    При запуске в приложение загружаются посредством вызова метода Collection findAll () домашнего интерфейса все имеющиеся в наличии корабли (Рис. 4.2). Получение коллекции из всех элементов, а также операции с удаленными интерфейсами компонента WidgetBean выделены жирным шрифтом.

    Collection col = home.findAll();model = new DefaultTableModel();
    model.addColumn("ID");
    model.addColumn("Description");
    model.addColumn("Price");
    model.addColumn("Delete");
    // Обрабатываем список из всех кораблей в базе данных
    for   (Iterator iter = col.iterator();  iter.hasNext();)   {
    WidgetRemote remote =  (WidgetRemote)   iter.next();
    model.addRow(new Object []{remote.getId(),
    remote.getDescription(),
    remote.getPrice(),   "Delete?
    (рис 4.3) Добавление нового корабля

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

    int result = JOptionPane.showOptionDialog
    	(table, new JScrollPane(panel), "Add new widget", 
    		JOptionPane.OK_CANCEL_OPTION,   JOptionPane.PLAIN_MESSAGE, null,  null,  null);
    
    if (result == JOptionPane.OK_OPTION) 
    { 
    try 
    	{
    	double price = Double.parseDouble(priceField.getText()); 
    	String description = descrField.getText(); 
    	home.create(description, price);
    	loadElements();
    	table.invalidate(); 
    	}   
    catch   (NumberFormatException ex)   
    	{
    	ex.printStackTrace(); 
    	}   
    catch   (RemoteException ex)   
    	{
    	ex.printStackTrace(); 
    	}   
    catch   (CreateException ex)   
    	{
    	ex.printStackTrace();
    	}
    }
    (рис 4.4) Станция "Мир" появилась на складе

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

    (рис 4.5) Новый корабль в базе данных

    Посредством запроса через SQL*PLUS,проверим, что корабль появился в базе данных (Рис. 4.5).

    (рис 4.6) Поддержим отечественного производителя! Русские названия тоже работают

    Если ввести русское название для корабля, то все тоже должно работать (Рис. 4.6).

    (рис 4.7) Удалим ненужный корабль

    При выделении ячейки столбца "Delete" появляется кнопка "Yes". Нажав ее можно удалить корабль. В Удаление производится посредством метода remove (Object primaryKey) домашнего интерфейса компонента.

    Integer id =   (Integer)   table.getModel().getValueAt(row,   0);
    if   (id.intValue()   > 0)   
    {
    try 
    	{
    	home.remove(id);
    	loadElements();
    	table.invalidate();
    	}   
    catch   (RemoteException e)   
    	{
    	e.printStackTrace();
    	}   
    catch   (RemoveException e)   
    	{
    	e.printStackTrace();
    	}
    }
    (рис 4.8) Изменение цены корабля

    При выделении ячейки столбца "Description" или "Price" появляется кнопка "Edit". Нажав ее можно редактировать описание или цену корабля (Рис. 4.8). Поиск экземпляра компонента по его первичному ключу и изменение поля в этом компоненте выделено жирным шрифтом.

    int result = JOptionPane.showOptionDialog(table,
    new JScrollPane(textField),   (String)table.getColumnName(column),
    JOptionPane.OK_CANCEL_OPTION,   JOptionPane.PLAIN_MESSAGE,
    null,  null,  null);
    if (result == JOptionPane.OK_OPTION)   
    {
    Integer id =   (Integer)   table.getModel().getValueAt(row,   0);
    if   (id.intValue()   > 0)   
    	{
    	try 
    		{
    		WidgetRemote remote =   (WidgetRemote) home.findByPrimaryKey(id);
    		remote.setDescription(textField.getText());
    		table.setValueAt(textField.getText(),   row,   column);
    		table.invalidate(); 
    		}   
    	catch   (RemoteException e)   
    		{
    		e.printStackTrace(); 
    		}   
    	catch   (FinderException e)   
    		{
    		e.printStackTrace();
    		}
    	}
    }

    Код клиентского приложения состоит из двух файлов - ButtonTableCellEditor.java и WidgetEditClient.java. Класс ButtonTableCellEditor из одноименного файла предоставляет возможность для создания редактируемых при помощи кнопки ячеек таблицы. С его помощью можно редактировать поля и удалять экземпляры компонентов из базы данных.

    public abstract class ButtonTableCellEditor implements TableCellEditor, ActionListener  
    {
    private TableCellEditor editor;
    private JButton customEditorButton;
    protected JTable table; 
    protected int row,   column;
    public ButtonTableCellEditor(TableCellEditor editor,   String buttonName)
    	{
    	this.editor = editor;
    	customEditorButton = new JButton(buttonName);
    	customEditorButton.addActionListener(this);
    	customEditorButton.setFocusable(false);
    	customEditorButton.setFocusPainted(false);
    	customEditorButton.setMargin(new Insets(0,   0,   0,   0));
    	}
    public Component getTableCellEditorComponent(JTable table,  Object value, boolean isSelected,   int row,   int column)   
    	{
    	JPanel panel = new JPanel(new BorderLayout());
    	panel.add(editor.getTableCellEditorComponent(table,  value, isSelected,   row,   column));
    	panel.add(customEditorButton,   BorderLayout.EAST);
    	this.table = table;
    	this.row = row;
    	this.column = column;
    	return panel;
    	}
    
    public Object getCellEditorValue()
    	{
    	return editor.getCellEditorValue();
    	}
    
    public boolean isCellEditable(EventObject anEvent)
    	{
    	return editor.isCellEditable(anEvent);
    	}
    public boolean shouldSelectCell(EventObject anEvent)
    	{ 
    	return editor.shouldSelectCell(anEvent);
    	}
    
    public boolean stopCellEditing()
    	{
    	return editor.stopCellEditing();
    	}
    public void cancelCellEditing()
    	{ 
    	editor.cancelCellEditing();
    	}
    
    public void addCellEditorListener(CellEditorListener l)
    	{ 
    	editor.addCellEditorListener(l);
    	}
    
    public void removeCellEditorListener(CellEditorListener l)
    	{
    	editor.removeCellEditorListener(l);
    	}
    public final void actionPerformed(ActionEvent e)
    	{ 
    	editor.cancelCellEditing(); 
    	editCell(table,   row,   column);
    	}
    
    protected abstract void editCell(JTable table,   int row,   int column);
    }

    Класс WidgetEditClient наследуется (extends) от JFrame. То как в нем организовано взаимодействие с EJB уже было рассмотрено ранее. Сейчас просто будет приведен код этого класса.

    public class WidgetEditClient extends JFrame implements ActionListener  
    {
    private static final long serialVersionUID = -7066592068515570150L;
    private DefaultTableModel model;
    private JTable table = null;
    private Context jndiContext;
    private WidgetHome home;
    private JButton addButton;
    public WidgetEditClient()   
    	{
    	setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    	// Obtaining the beans  from the database
    	try 
    		{
    		jndiContext = createJBossContext();
    		Object ref = jndiContext.lookup("WidgetBean");
    		home =   (WidgetHome)
    		PortableRemoteObject.narrow(ref,  WidgetHome.class); 
    		}   
    	catch(NamingException e)   
    		{ e.printStackTrace();
    		}
    	
    	loadElements();
    	
    	JScrollPane scroll = new JScrollPane(table);
    	scroll.setBorder(BorderFactory.createEmptyBorder()); 
    	getContentPane().setLayout(new BorderLayout());
    	
    	getContentPane().add(BorderLayout.CENTER,  scroll); 
    	getContentPane().add(BorderLayout.SOUTH,   addButton = new
    	
    	JButton("Add"));
    	addButton.addActionListener(this); 
    	pack();
    	}
    	
    public void actionPerformed(ActionEvent e)   
    	{ 
    	JPanel panel = new JPanel(); 
    	panel.setLayout(new GridBagLayout());
    	GridBagConstraints gc = new GridBagConstraints();
    	gc.insets = new Insets(5,   5,   5,   5); 
    	gc.weightx = 1.0; 
    	gc.weighty = 0.0;
    	gc.fill = GridBagConstraints.BOTH;
    	gc.gridx = 0; 
    	gc.gridy = 0; 
    	gc.gridwidth = 1; 
    	gc.gridheight = 1;
    	panel.add(new JLabel("Price:"),   gc); 
    	gc.gridy = 1;
    	
    	panel.add(new JLabel("Description:"),   gc);
    	gc.gridx = 1; 
    	gc.gridy = 0;
    	
    	JTextField priceField = new JTextField(); panel.add(priceField,   gc);
    	gc.gridy = 1;
    	JTextField descrField = new JTextField(); 
    	panel.add(descrField,   gc);
    	
    	int result = JOptionPane.showOptionDialog
    		(table, new JScrollPane(panel), "Add new widget", 
    			JOptionPane.OK_CANCEL_OPTION, 
    			JOptionPane.PLAIN_MESSAGE, null,  null,  null);
    	
    	if (result == JOptionPane.OK_OPTION)   
    		{
    		try 
    			{
    			double price = Double.parseDouble(priceField.getText()); 
    			String description = descrField.getText(); 
    			home.create(description,  price);
    			loadElements();
    			table.invalidate(); 
    			}   
    		catch   (NumberFormatException ex)   
    			{ 
    			ex.printStackTrace();
    			} 
    		catch (RemoteException ex) 
    			{ 
    			ex.printStackTrace();
    			} 
    		catch (CreateException ex) 
    			{ 
    			ex.printStackTrace();
    			}
    		}
    	}
    }
    
    private class DeleteCellEditor extends ButtonTableCellEditor  
    { 
    public DeleteCellEditor(TableCellEditor editor)  
    	{ 
    	super(editor,   "Yes");
    	}
    
    protected void editCell(JTable table,   int row,   int column)   
    	{
    	Integer id =  (Integer)   table.getModel().getValueAt(row,   0);
    	if   (id.intValue()   > 0)   
    		{
    		try 
    			{
    			home.remove(id);
    			loadElements();
    			table.invalidate();
    			}   
    		catch   (RemoteException e)   
    			{
    			e.printStackTrace();
    			}   
    		catch   (RemoveException e)   
    			{
    			e.printStackTrace();
    			}
    		}
    	}
    }
    
    private class DescriptionCellEditor extends ButtonTableCellEditor  
    {
    public DescriptionCellEditor(TableCellEditor editor)   
    	{
    	super   (editor,   "Edit");
    	}
    
    protected void editCell(JTable table,   int row,   int column)   
    	{
    	JTextField textField = new JTextField();
    	Object value = table.getValueAt(row,   column);
    	if(value   != null)
    		{
    		textField.setText((String)   value);
    		textField.setCaretPosition(0);
    		}
    	int result = JOptionPane.showOptionDialog(table, new JScrollPane(textField), 
    		(String)table.getColumnName(column),
    	
    	JOptionPane.OK_CANCEL_OPTION,
    	
    	JOptionPane.PLAIN_MESSAGE, null, null, null);
    	
    	if (result == JOptionPane.OK_OPTION) 
    		{
    		Integer id =   (Integer)
    		table.getModel().getValueAt(row,   0);
    		if (id.intValue() > 0) 
    			{
    			try 
    			{
    			WidgetRemote remote =   (WidgetRemote) home.findByPrimaryKey(id);
    			remote.setDescription(textField.getText());
    			table.setValueAt(textField.getText(),   row, column);
    			table.invalidate(); 
    			}   
    			catch   (RemoteException e)   
    			{
    			e.printStackTrace(); 
    			}   
    			catch   (FinderException e)   
    			{
    			e.printStackTrace();
    			}
    			}
    		}
    	}
    }
    
    private class PriceCellEditor extends ButtonTableCellEditor  
    { 
    public PriceCellEditor(TableCellEditor editor)  
    	{ 
    	super(editor,   "Edit");
    	}
    
    protected void editCell(JTable table,   int row,   int column)   
    	{
    	JTextField textField = new JTextField(); 
    	Object value = table.getValueAt(row,   column);
    	if(value   != null)
    		{
    		textField.setText(((Double)   value).toString()); 
    		textField.setCaretPosition(0);
    		}
    	
    	int result = JOptionPane.showOptionDialog(table, new JScrollPane(textField), 
    		(String)table.getColumnName(column),
    	JOptionPane.OK_CANCEL_OPTION,
    	JOptionPane.PLAIN_MESSAGE, null, null, null);
    	
    	if(result == JOptionPane.OK_OPTION) 
    		{
    		Integer id =   (Integer)
    		table.getModel().getValueAt(row,   0);
    		if (id.intValue() > 0) 
    			{
    			try 
    				{
    				WidgetRemote remote =   (WidgetRemote) home.findByPrimaryKey(id);
    				double price = Double.
    				parseDouble(textField.getText());
    				remote.setPrice(price);
    				
    				table.setValueAt(price,   row,   column);
    				table.invalidate(); 
    				}   
    			catch   (RemoteException e)   
    				{
    				e.printStackTrace(); 
    				}   
    			catch   (FinderException e)   
    				{
    				e.printStackTrace(); 
    				}   
    			catch   (NumberFormatException e)   
    				{
    				e.printStackTrace();
    				}
    			}
    		}
    	}
    
    private void loadElements()   
    	{
    	try 
    		{
    		Collection col = home.findAll();
    		model = new DefaultTableModel();
    		model.addColumn("ID");
    		model.addColumn("Description");
    		model.addColumn("Price");
    		model.addColumn("Delete");
    		for   (Iterator iter = col.iterator();  iter.hasNext();)   
    			{
    			WidgetRemote remote =   (WidgetRemote)   iter.next();
    			model.addRow(new Object   []{remote.getId(), remote.getDescription(),
    			remote.getPrice(),   "Delete?"});
    			}
    		if (table == null) 
    			{
    			table = new JTable(model); 
    			}  
    		else  
    			{
    			table.setModel(model);
    			}
    		JTextField textField = new JTextField();
    		textField.setBorder(BorderFactory.createEmptyBorder()); 
    		DefaultCellEditor editor = new DefaultCellEditor(textField); editor.setClickCountToStart(l);
    		
    		table.getColumn(table.getColumnName(1)).setCellEditor(
    		new DescriptionCellEditor(editor));
    		table.getColumn(table.getColumnName(2)).setCellEditor(
    		new PriceCellEditor(editor));
    		table.getColumn(table.getColumnName(3)).setCellEditor(
    		new DeleteCellEditor(editor)); 
    		}   
    	catch(RemoteException e)   
    		{
    		e.printStackTrace(); 
    		}   
    	catch(FinderException e)   
    		{ 
    		e.printStackTrace();
    		}
    	}
    public static void main(String args[])   
    	{
    	WidgetEditClient client = new WidgetEditClient();
    	client.setLocation(200,   200); 
    	client.setSize(600,   400); 
    	client.setVisible(true);
    	}
    
    public static Context createJBossContext()   throws NamingException  
    	{ 
    	Properties p = new Properties(); 
    	p.put("java.naming.factory.initial", 	"org.jnp.interfaces.NamingContextFactory"); 
    	p.put("java.naming.provider.url", "jnp://127.0.0.1:1099"); 
    	p.put("java.naming.factory.url.pkgs", "org.jboss.naming:org.jnp.interfaces"); 
    	Context jndiContext = new InitialContext(p); 
    	return jndiContext;
    	}
    }

    Вывод

    Как видно из приведенного кода компонента WidgetBean,разработка BMP компонентов дело рутинное и довольно долгое. Использование CMP сильно упрощает процесс разработки объектных компонентов. В следующем примере будет рассмотрено использование CMP в следующем примере.

    Пример "Модернизированный интернет магазин"

    Общее описание

    Предыдущий пример с интернет магазином обладал рядом недостатков. В этом примере мы постараемся их исправить при помощи использования объектных компонент CMP.

    Создание проекта

    В этом примере создание компонент EJB будет тесно связано со встроенными функциями Eclipse-WTP,поэтому создадим сразу EJB проект с названием, например, ShoppingBasketObjectBeans.

    (рис 4.9) Подключаем XDoclet к проекту

    При создании проекта подключаем XDoclet (Рис. 4.9).

    Работа с XDoclet

    Для того чтобы сделать работу с объектными компонентами CMP легче, следует использовать средство XDoclet. XDoclet разрабатывался для поддержки атрибутно-ориентированной парадигмы программирования в различных технологиях платформы Java.Суть работы с XDoclet сводится к тому, что в код добавляются так называемые атрибуты. А затем встроенный генератор XDoclet по этим атрибутам создает либо автоматически сгенерированный код, либо различные вспомогательные файлы (xml,разнообразные файлы конфигураций и проч.). Атрибуты в XDoclet имеют точно такой же вид, как и javadoc комментарии. К примеру, XDoclet атрибут, описывающий объектный компонент Commodities выглядит следующим образом:

    <!-- begin-xdoclet-definition -->
    *	@ejb.bean name="Commodities"
    * jndi-name="Commodities"
    * type="CMP"
    * primkey-field="id"
    * schema="CommoditiesSCHEMA"
    * cmp-version="2.x" 
    */

    Затем XDoclet по этому атрибуту сгенерирует файл ejb.xml, в котором он сразу пропишет все нужные xml-теги,соответствующие описанным в атрибуте параметрам. Это очень удобно иметь сразу перед глазами все параметры компонента EJB,а не заглядывать каждый раз в файл ejb.xml и синхронизировать его с классом компонента.

    Полный список возможностей XDoclet доступен на сайте http://xdoclet. sourceforge.net/. Для примера нам понадобится поддержка EJB и JBoss в XDoclet.

    Обычно XDoclet устанавливается по умолчанию с Eclipse-WTP.Но если вам попалась урезанная версия Eclipse,или же вы хотите установить другую версию XDoclet,то следует щелкнуть правой кнопкой на EJB -проекте и выбрать раздел Properties.

    (рис 4.10) Выбор пути с библиотеками XDoclet

    В разделе XDoclet следует выбрать версию XDoclet (на данный моент поддерживаются три -1.2.1, 1.2.2 и 1.2.3) и указать путь к библиотекам XDoclet.Сами библиотеки можно скачать с сайта http://xdoclet.sourceforge.net/.

    Помимо указания пути к библиотекам XDoclet,нужно настроить XDoclet соответствующим образом, для того, чтобы он корректно генерировал файлы описаний для JBoss.В разделе XDoclet отключаем пункт "Use global xdoclet preferences". Раскрываем раздел XDoclet слева и там будет два подраздела - EJBDoclet и WEBDoclet.Выбираем подраздел EJBDoclet.

    (рис 4.11) Включаем поддержку JBoss

    И теперь включаем поддержку JBoss для нашего проекта (Рис. 4.11).

    Создание таблиц в базе данных

    (рис 4.12) Инфологическая модель базы данных

    Инфологическая модель базы данных не изменилась по сравнению с предыдущим примером (Рис. 4.12). Только названия таблиц переименованы, чтобы не конфликтовать с названиями таблиц из предыдущего примера. Однако SQL-код,создающий таблицы в базе данных претерпел некоторые изменения -

  • Во-первых, все поля, кроме первичного ключа перестали быть read-only. Это делает работу с объектными компонентами CMP гораздо более гибкой.
  • Во-вторых, теперь больше не нужно создавать пакеты (Package) для хранения только что созданного первичного ключа. Эту работу берет на себя контейнер EJB (в данном случае JBoss).
  • В-третьих, триггеры тоже больше не нужны. Теперь контейнер EJB сам генерирует автоматически первичный ключ по данной последовательности (в случае Oracle,для других баз данных используются другие механизмы).
  • Поэтому теперь SQL -код выглядит следующим образом

    drop table COMMODITY_ORDER_ENTITY;
    
    
    drop table COMMODITIES_LIST_ENTITY;
    
    
    drop table ORDERS_ENTITY;
    create table COMMODITIES_LIST_ENTITY (
    ID integer not null,
    NAME varchar(20)   not null,
    DESCRIPTION varchar(50)   not null,
    PRICE float not null,
    IMAGEPATH varchar(30),
    primary key(ID)
    );
    drop sequence COMMODITIES_ENTITY_SEQ;
    create sequence COMMODITIES_ENTITY_SEQ 
    start with 1 
    increment by 1 
    nomaxvalue;
    create table ORDERS_ENTITY
    
    ID integer not null,
    DATE_CREATED DATE not null,
    USERNAME varchar(20) not null, primary key(ID)
    );
    drop sequence ORDERS_ENTITY_SEQ;
    create sequence ORDERS_ENTITY_SEQ 
    start with 1 
    increment by 1 
    nomaxvalue;
    
    create table COMMODITY_ORDER_ENTITY
    (
    ID integer not null, ORDER_ID integer,
    COMMODITY_ID integer, 
    QUANTITY integer not null, 
    primary key(ID)
    );
    
    alter table COMMODITY_ORDER_ENTITY
    add constraint FK_ORDER_ENTITY 
    foreign key (ORDER_ID) 
    references ORDERS_ENTITY(ID);
    alter table COMMODITY_ORDER_ENTITY
    add constraint FK_COMMODITIES_ENTITY
    foreign key(COMMODITY_ID)
    references COMMODITIES_LIST_ENTITY(ID);
    drop sequence COMMODITY_ORDER_ENTITY_SEQ;
    create sequence COMMODITY_ORDER_ENTITY_SEQ
    start with 1 increment by 1 nomaxvalue;
    insert into COMMODITIES_LIST_ENTITY(ID, NAME, DESCRIPTION, PRICE, IMAGEPATH)
    values(COMMODITIES_ENTITY_SEQ.nextval,   'Kent 1',   
    	'Light cigarettes',   1.99, 'img/kent1.jpg');
    insert into COMMODITIES_LIST_ENTITY(ID, NAME, DESCRIPTION, PRICE, IMAGEPATH)
    values(COMMODITIES_ENTITY_SEQ.nextval,   'Kent 4',   
    	'Light cigarettes',   1.99, 'img/kent4.jpg');
    insert into COMMODITIES_LIST_ENTITY(ID, NAME, DESCRIPTION, PRICE, IMAGEPATH)
    values(COMMODITIES_ENTITY_SEQ.nextval,   'Kent 8',   
    	'Light cigarettes',   1.99, 'img/kent8.jpg');

    Компоненты

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

    Компонент CommoditiesBean

    Этот компонент описывает единицу товара на складе и соответствует таблице COMMODITIES_LIST_ENTITY. В Eclipse-WTP встроена возможность автоматического создания объектных компонент CMP по таблицам в базе данных. Сейчас с ее помощью будет создан компонент CommoditiesBean.

    (рис 4.13) Создаем новый компонент

    Щелкаем правой кнопкой на проекте и выбираем раздел New -> XDoclet Enterprise JavaBean.

    (рис 4.14) Объектный компонент CMP

    В открывшемся меню выбираем "Container Manager Entity Bean" (Рис. 4.14).

    (рис 4.15) Описание компонента

    Вводим описание компонента (Рис. 4.15) - пакет (Javapackage),в котором будут располагаться классы компонента ( shoppingBasketObject ), его имя (CommoditiesBean),класс, от которого наследуется компонент ( java.lang.Object ).

    (рис 4.16) Параметры компонента

    В параметрах компонента можно поменять имя компонента, идентификатор абстрактной схемы постоянства (необходима для языка запросов к объектным компонентам EJB-QL -будет рассмотрен далее), описание. В качестве CMP также следует оставить версию 2.x (версия 1.x оставлена для обратной совместимости с EJB 1.1).В параметре "usecase" выбираем "Import attributes from table" - это значит, что в дальнейшем объектный компонент будет создан непосредственно по таблице в базе данных.

    (рис 4.17) Создание подключения к базе данных

    После этого нам будет предложено выбрать подключение к базе данных, в которой находится необходимая таблица. В моем случае такое подключение уже было создано (Рис. 4.17). Внизу выводятся параметры этого подключения. Для того, чтобы создать новое подключение, необходимо нажать кнопку "New".

    (рис 4.18) Создание нового подключения

    В окне создания нового подключения необходимо ввести все параметры этого подключения - название базы данных ( EJBDB, в примерах), драйвер подключения

    ( oracle.jdbc.OracleDriver ) - для базы данных Oracle,расположение библиотеки с классами драйвера ( ojdbc14.jar - внутри установочного каталога Oracle,или там, где вы его положите) и URL подключения к базе данных в формате jdbc:oracle:thin:@адрес подключения:порт:название базы данных. Слева необходимо выбрать тип базы данных (Oracle 9i) и логин и пароль для подключения. После этого кнопкой Test Connection можно проверить факт того, что все настройки были введены корректно.

    (рис 4.19) Подключение удалось

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

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

    (рис 4.20) Создание компонента по таблице

    Теперь можно выбрать необходимую таблицу в базе данных

    ( BORIS.COMMODITIES_LIST_ENTITY - таково ее полное имя в Oracle, BORIS - это пользователь, которому она принадлежит). Осталось только немного подредактировать параметры. Eclipse-WTP по умолчанию ,почему-то, сопоставляет целочисленным полям из базы данных тип java.math.BigDecimal. Заменяем его на java.lang.Integer, а тип JDBC - с DECIMAL на INTEGER. Также тип FLOAT из базы данных стал строковым. Заменяем его на java.lang.Double, а тип JDBC - на DOUBLE. Теперь с типами стало все в порядке, переходим к последнему этапу создания объектного компонента CommoditiesBean.

    (рис 4.21) Выбор интерфейсов

    В последнем окне можно к интерфейсу javax.ejb.EntityBean добавить еще какие либо интерфейсы, которые будут заимплементированы классом компонента (Рис. 4.21). В этом случае это не нужно. Нажимаем Finish. После этого Eclipse-WTP сгенерирует нам класс компонента shoppingBasketObject.CommoditiesBean со всеми необходимыми атрибутами XDoclet.

    XDoclet по этим атрибутам сгенерирует удаленные, домашние и локальные интерфейсы, различные вспомогательные классы, а также файлы ejb.xml, jboss.xml и jbosscmp-jdbc.xml (если вы не забыли включить поддержку JBoss в настройках XDoclet).Теперь для работы и изменения созданного компонента CommoditiesBean,достаточно редактировать только файл CommoditiesBean.java. Все остальные файлы будут заново генерироваться XDoclet каждый раз при сохранении этого файла.

    (рис 4.22) Созданные файлы

    Все сгенерированные XDoclet файлы на Рис. 4.22.

    Класс компонента CommoditiesBean

    Рассмотрим сгенерированный класс компонента CommoitiesBean.В нем определены атрибуты XDoclet.

    /**
    * <!-- begin-xdoclet-definition --> 
    *@ejb.bean name="CommoditiesList"
    * jndi-name="CommoditiesList"
    * type="CMP"
    * primkey-field="id"
    
    * schema="CommoditiesListSCHEMA"
    * cmp-ersion="2.x" 
    * @ejb.persistence
    * table-ame="BORIS.COMMODITIES_LIST_ENTITY" 
    
    * @ejb.finder
    * query="SELECT OBJECT(a)   FROM CommoditiesListSCHEMA as a"
    * signature="java.util.Collection findAll()" 
    *
    * @ejb.pk class="java.lang.Integer"
    *
    *
    * <!-- end-xdoclet-definition -->
    * @generated
    **/

    Первый атрибут - это атрибут @ejb.bean. В нем описываются базовые характеристики объектного компонента:

  • Параметр name - имя компонента,
  • Параметр jndi-name - jndi -имя, по которому компонент будет доступен,
  • Параметр type - тип (CMP),
  • Параметр primkey-field - поле, используемое в качестве первичного ключа ( id ),
  • Параметр schema - название абстрактной схемы постоянства (необходимо для EJB-QL).
  • И параметр cmp-version - версия CMP, в нашем случае это "2.x". С помощью этого параметра можно задать CMP 1.x - для совместимости со старыми EJB -приложениями.
  • (рис 4.23) XDoclet отработал

    Здесь можно, к примеру, изменить jndi -имя компонента на "SomeStrangeName". После того, как XDoclet отработает (Рис. 4.23), файл jboss.xml примет следующий вид:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE jboss PUBLIC "-//JBoss//DTD JBOSS 3.2//EN" "http://www.jboss.org/j2ee/dtd/jboss 3 2.dtd">
    <jboss>
    <enterprise-beans>
    <entity>
    <ejb-name>Commodities</ejb-name>
    <jndi-name>SomeStrangeBean</jndi-name> 
    <local-jndi-name>CommoditiesLocal</local-jndi-name> 
    <method-attributes> </method-attributes> 
    </entity> 
    </enterprise-beans>
    <resource-managers>
    </resource-managers>
    </jboss>

    Вообще в файле jboss.xml можно задать локальные и удаленные jndi -имена для компонентов. Помимо этого есть раздел <resource-managers>. Его рассмотрение лежит вне пределов данного методического пособия. При помощи него можно расширять возможности JBoss - к примеру, добавить поддержку JavaMail.Клиентское приложение может получить доступ к этому сервису через JNDI и отправлять сообщения по электронной почте. В разделе <method-attributes> можно определить некоторые дополнительные атрибуты для методов данного компонента. Например, такой параметр как <transaction-timeout> - максимальное время выполнения транзакции (по умолчанию все методы компонентов EJB являются транзакциями - подробнее про это в следующем разделе). Следующий атрибут - это атрибут @ejb.persistence. В нем задается имя таблицы, которой соответствует объектный компонент.

    Далее идет атрибут @ejb. finder. В этом атрибуте можно задавать параметры поиска одного или нескольких экземпляров компонент при помощи SQL -подобного языка запросов EJB-QL.В данном случае сгенерирован стандартный запрос по выбору всех экземпляров компонента в базе данных (см. аналогичный запрос из предыдущего примера с BMP).Рассмотрим этот запрос - "SELECT OBJECT(a) FROM CommoditiesSCHEMA as a". Он состоит из оператора SELECT, за которым следует выражение OBJECT(a). Это значит, что выбирается непосредственно сам экземпляр компонента (при желании можно выбирать и поля компонента и даже вызывать агрегирующие функции типа суммы, среднего или максимума из наборов полей). Завершается оператор ключевым словом FROM за которым следует имя схемы постоянства (если помните, ей давалось имя при создании компонента).

    Абстрактная схема постоянства - это как совокупность всех экземпляров одного компонента. Если приводить аналогию с SQL, то экземпляр компонента - это строка в таблице, а абстрактная схема - вся таблица целиком.

    Метод findByPrimaryKey(Object pk) создается для объектного компонента CMP по умолчанию. Его не надо нигде определять.

    И последним идет атрибут @ejb.pk. В нем задается класс первичного ключа. Компонент почти готов для работы, однако осталось сделать несколько последних штрихов. Во-первых, в базе данных первичный ключ генерируется автоматически. Это нужно объяснить контейнеру JBoss.Есть несколько способом сделать это, самый простой - это добавить два новых атрибута:

  • @jboss.entity-command name = "oracle-sequence". Говорит JBoss о том, что первичный ключ генерируется при помощи последовательности Oracle.
  • @jboss.entity-command-attribute name = "sequence" value = "COMMODITIES_ENTITY_SEQ". Тут указываем имя последовательности, при помощи которой генерируется новый первичный ключ.
  • Ну и напоследок нужно добавить местоположение базы данных, в которой находится таблица. Файл, описывающий доступ к базе данных ( DataSource ) уже был создан и использовался в примере с обменом валют и в примере с BMP.Атрибут @jboss.persistence позволяет указать соответствующий доступ к базе данных для компонента. Полностью атрибут с параметрами выглядит следующим образом:

    @jboss.persistence
    datasource="java:/OracleDS"
    datasource-mapping="Oracle9i"
    table-name="BORIS.COMMODITIES_LIST_ENTITY"
    create-table="false"
    remove-table="false"
    alter-table="false"
  • Параметр datasource - это jndi -имя источника данных (см. получение DataSource в примере с BMP ).
  • Параметр datasource-mapping - это идентификатор типа базы данных. В данном случае - Oracle 9i.В соответствии с этим идентификатором, JBoss,будет формировать запросы к базе данных. Для каждой базы данных эти настройки определены в файле server\default\conf\standardjbosscmp-jdbc.xml установочного каталога JBoss.Менять в этом файле ничего не стоит, а посмотреть можно.
  • Параметр table-name - имя таблицы, которой соответствует компонент. JBoss (как и все другие контейнеры EJB) использует свои собственные настройки для описания соответствия компонентов таблицам из базы данных (файл jbosscmp-jdbc.xml ). Формат этого файла таков, что имя таблицы нужно продублировать.
  • Параметры create-table, remove-table и alter-table - определяют, может ли компонент создавать, удалять или изменять таблицы. Практической пользы от этих параметров для объектного компонента CMP немного. Скорее всего, он может использоваться при задании аналогичного атрибута для сеансовых компонент.
  • С заголовком класса компонента Commodities разобрались. Теперь перейдем непосредственно к коду этого класса. Вначале идет create метод без параметров. Для него определен атрибут @ejb.create-method, который должен определяться для всех create методов компонента. У этого атрибута один параметр - view-type. Он определяет, к какому интерфейсу (удаленному или локальному) принадлежит данный метод. Если нам понадобится создать новый экземпляр компонента, то разумно задать для него непосредственно в create методе сразу все необходимые параметры.

    /**
    * @ejb.create-method view-type="remote"
    
    */
    public Integer ejbCreate()
    throws CreateException  {
    return null;
    }
    
    public void ejbPostCreate()   {
    }

    Модернизируем create метод следующим образом:

    /**
    *  @ejb.create-method view-type="remote" */
    public Integer ejbCreate(String name,   
    	String description,  
    	double price, 
    	String imagePath) 
    	throws CreateException  {
    setName(name);
    setDescription(description);
    setPrice(price);
    setImagepath(imagePath);
    return null;
    }
    public void ejbPostCreate(String name, String description,  double price, String imagePath)   {
    }

    Теперь для того, чтобы создать компонент Commodities,необходимо передать ему все имя, описание, цену и путь до изображения единицы товара, описываемой этим компонентом. Осталось рассмотреть set и get методы, при помощи которых выполняется получение и изменение полей компонента. Сделаем это на примере поля name. Для этого поля, как и для всех JBoss сгенерировал при создании компонента уже все необходимые атрибуты.

    /**
    * 
    * 
    * <!-- begin-user-doc -->
    * CMP Field name 
    *
    * Returns the name
    * @return the name 
    *
    * <!-- end-user-doc -- > 
    *
    * <!-- begin-xdoclet-definition --> 
    *
    * @ejb.persistent-field
    * @ejb.persistence
    
    * column-name="NAME"
    * jdbc-type="VARCHAR"
    * sql-type="VARCHAR2"
    * read-only="false"
    *
    *
    * @ejb.interface-method *
    * <!-- end-xdoclet-definition -- >
    * @generated 
    */
    
    public abstract java.lang.String getName();
    /**
    * <!-- begin-user-doc -- >
    * Sets the name 
    *
    * @param java.lang.String the new name value
    * <!-- end-user-doc -- > 
    *
    * <!-- begin-xdoclet-definition -- >
    * @ejb.interface-method
    * <!-- end-xdoclet-definition -- >
    * @generated
    * /
    
    public abstract void setName(java.lang.String name);

    Как у set метода, так и у get метода определен атрибут @ejb.interface-method.

    Тестовый клиент для компонента CommoditiesBean

    Проверим работоспособность компонента при помощи простого тестового клиента. Вначале он получает все товары в магазине через метод findAll(), потом создает новый товар в магазине, удаляет его и, наконец, выполняет поиск по первичному ключу товара.

    public class ShoppingBasketOrderClient  
    {
    public static void main(String args[])   
    	{
    	try 
    		{
    		Context jndiContext = createJBossContext(); 
    		Object ref = jndiContext.lookup("Commodities");
    		CommoditiesHome commoditiesHome =   
    			(CommoditiesHome) PortableRemoteObject.narrow(ref, CommoditiesHome.class);
    		Collection col = commoditiesHome.findAll();
    		for   (Iterator iter = col.iterator();  iter.hasNext();)   
    			{ 
    			Commodities comm =   (Commodities)   iter.next();
    			System.out.println(comm.getName()   + "  " +
    			comm.getPrice());
    			}
    		
    		Commodities commCr = commoditiesHome.create("Marijuana",
    		"Finest American Blend",   6.50,  null); 
    		System.out.println("Created -  " + commCr.getName()   + "  " +
    		commCr.getPrice()); commCr.remove();
    		
    		Commodities commFound = commoditiesHome.findByPrimaryKey(new Integer(1));
    		System.out.println("Found - " + commFound.getName() + "  
    			" + commFound.getPrice());
    		}   
    	catch   (NamingException e)   
    		{
    		e.printStackTrace(); 
    		}   
    	catch   (FinderException e)   
    		{
    		e.printStackTrace(); 
    		}   
    	catch   (RemoteException e)   
    		{
    		e.printStackTrace(); 
    		}   
    	catch   (CreateException e)   
    		{
    		e.printStackTrace(); 
    		}   
    	catch   (RemoveException e)
    		{
    		e.printStackTrace();
    		}
    	}
    
    public static Context createJBossContext()   throws NamingException  
    	{ 
    	Properties p = new Properties(); 
    	p.put("java.naming.factory.initial", "org.jnp.interfaces.NamingContextFactory"); 
    	p.put("java.naming.provider.url", "jnp://127.0.0.1:1099"); 
    	p.put("java.naming.factory.url.pkgs", "org.jboss.naming:org.jnp.interfaces"); 
    	Context jndiContext = new InitialContext(p); 
    	return jndiContext;
    	}
    }
    (рис 4.24) Вывод тестового клиента

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

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

    Компоненты, управляемые сообщениями (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> <сумма> - перевод заданной суммы с первого счета на второй.
  • Безопасность в EJB

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

  • Аутентификация (Authentication).Аутентификация - это процесс подтверждения личности пользователя, пытающегося получить доступ в систему. Чаще всего подтверждение личности представляет собой ввод пароля, хотя возможны и более экзотические варианты, например, сканирование отпечатков пальцев или сетчатки глаза.
  • Авторизация (Authorization).Авторизация заключается в определении того, какие действия может и не может совершать каждый пользователь, вошедший в систему. Например, одним пользователям может быть дан доступ на запись в базу данных, другие же могут только читать из нее данные.
  • Конфиденциальность (Confidentiality).Требуется защитить данные, передаваемые по каналам связи между системой и пользователями, от перехвата и фальсификации злоумышленниками. Обычно для этого используется шифрование, например, SSL.
  • Авторы спецификации EJB сосредоточились на решении задачи авторизации, оставив остальные задачи разработчикам EJB -контейнеров.

    Обычно аутентификация происходит на уровне JNDI,при создании контекста. Когда пользователь подключается к EJB системе, с ним ассоциируется определенный идентификатор безопасности ( security identity ). Этот идентификатор неявно передается при каждом вызове методов EJB.На основе идентификатора безопасности контейнер EJB решает, имеет ли пользователь право на совершение запрошенной операции. В EJB реализована авторизация на основе ролей. Каждому подключенному к системе пользователю присваивается роль, например, "служащий", "менеджер", "администратор". В то же время, каждому методу EJB приписывается набор ролей пользователей, имеющих право на вызов этого метода. В момент вызова метода пользователем производится проверка того, что роль пользователя является одной из допустимых ролей вызываемого метода. Допустимые роли определяются а ннотацией @javax.annotation.security.RolesAllowed, принимающей в качестве параметра массив строк - названий ролей.

    Аннотация @javax.annotation.security.PermitAll позволяет открыть доступ к методу пользователям, играющим любые роли.

    Аннотация @javax.annotation.security.RunAs задает роль, от имени которой метод EJB будет вызывать другие методы. По умолчанию метод работает в роли вызвавшего его пользователя.

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

    В этом примере мы добавим защиту от несанкционированного доступа к написанному в предыдущем разделе EJB,реализующему операции с банковскими счетами. Дадим доступ к методам BankBean работникам банка, запретив его всем остальным. Соответствующую роль назовем BankEmployee. Желаемого эффекта можно добиться, добавив следующую аннотацию к объявлению класса BankBean:

    @Stateless
    @RolesAllowed("BankEmployee")
    public class BankBean implements Bank  {
    ...

    Также необходимы дополнительные настройки контейнера EJB. Мы рассмотрим настройки контейнера JBoss.

    Во-первых, необходимо обновить дескриптор развертывания приложения:

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

    Security domain - это база данных пользователей. Приложения могут пользоваться общей базой, а могут иметь свои собственные базы пользователей. Мы используем security domain, который мы назвали SecureBank.

    Во-вторых, требуется описать используемый security domain в конфигурационных файлах JBoss.Для это добавим следующие строки в файл ${jboss_home}/server/default/conf/login-config.xml:

    <application-policy name="SecureBank">
    <authentication>
    <login-module
    code="org.jboss.security.auth.spi.UsersRolesLoginModule"
    flag="required">
    <module-option name="usersProperties">
    props/bank-users.properties
    </module-option>
    <module-option name="rolesProperties">
    props/bank-roles.properties
    </module-option> </login-module> </authentication> </application-policy>

    Этот способ настройки пользователей использует два текстовых файла. Первый из них, props/bank-users.properties, задает пользователей и их пароли:

    vladykin=asdfgh

    Второй, props/bank-roles.properties, задает роли пользователей:

    vladykin=BankEmployee

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

    Hashtable<String, String> env = new Hashtable<String,   String>();
    env.put(Context.INITIAL_CONTEXT_FACTORY, "org.jboss.security.jndi.JndiLoginInitialContextFactory");
    env.put(Context.PROVIDER_URL, "localhost:1099"); 
    env.put(Context.SECURITY_PRINCIPAL, args[3]);
    env.put(Context.SECURITY_CREDENTIALS, args[4]);

    За это отвечают параметры SECURITY_PRINCIPAL и SECURITY_CREDENTIALS. Их значения извлекаются из последних параметров, переданных программе в командной строке.

    Страницы:

    Объектные компоненты

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

    Объектные компоненты представляют записи в базе данных. При изменении объектных компонентов происходят изменения в базе данных. В предыдущих примерах уже рассматривалась возможность внесения изменения в базу данных непосредственно через сеансовые компоненты, но это было не очень удобно - приходилось, к примеру, для создания заказа в базе данных выполнять несколько запросов SQL.При использовании объектных компонентов можно было бы просто использовать метод Order.createOrder (List commoditiesList). Компонент Order представляет приложению полный доступ к информации о заказах, а также возможность управлять ими -создавать, модифицировать и удалять. При помощи объектных компонентов разработку можно упростить и сделать более рентабельной.

    Когда объектный компонент создается, в базу данных должна быть вставлена новая запись и создан экземпляр компонента, связанный с этими данными. По мере того, как компонент изменяется, его состояние меняется, и изменения должны быть синхронизированы с данными в базе данных. За процесс координации данных, представляющий экземпляр компонента, с базой данных, отвечает служба Persistence.

    Существует два основных типа объектных компонентов, различающихся реализацией механизма постоянства: компоненты с постоянством, управляемым контейнером (container-managed persistence или CMP),и компоненты поддерживающие постоянство самостоятельно (bean-managedpersistence или BMP).

    В случае с CMP,контейнер знает, как постоянство экземпляра и поля отношений с другими компонентами отображаются в базе данных, и автоматически вставляет, модифицирует и удаляет данные. В случае BMP,вся работа выполняется разработчиком самостоятельно. Ему необходимо написать код манипуляции базой данных. Контейнер EJB сообщает экземпляру компонента, когда безопасно вставлять, модифицировать и удалять его данные из базы данных. Экземпляр выполняет всю работу по обеспечению своего постоянства самостоятельно.

    Начиная со спецификации EJB 2.0, CMP подверглось очень сильным изменениям. В результате CMP теперь больше не является обратно совместимым с предыдущими спецификациями. Поэтому довольно часто используют сокращение CMP 2.0.

    В CMP 2.0 контейнер объектных компонентов автоматически управляет их состоянием. Контейнер заботится о регистрации компонентов в транзакциях и поддерживает их состояние в базе данных. Разработчик компонентов описывает атрибуты и связи компонентов при помощи виртуальных (virtual) полей постоянства и отношений. Разработчик не определяет их явно. Вместо этого в классе создаются абстрактные методы доступа ( set и get ).

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

    При создании объектного компонента (BMP или CMP),контейнер EJB выполняет следующую последовательность действий:

  • Контейнер вызывает метод Class.newInstance() у класса объектного компонента. До этого момента объектный компонент существовал только как набор файлов (класса компонента, интерфейсов и дескриптора развертывания). У компонента вызывается метод setEntityContext (EntityContext). После этого созданный компонент помещается в пул объектных компонентов. Обычно контейнер хранит несколько экземпляров компонента в пуле. Они помещаются туда при запуске контейнера. В дальнейшем, в зависимости от оьстоятельств, контейнер может увеличивать или уменьшать количество экземпляров компонентов в пуле. Компонент, находящийся в пуле, способен выполнять запросы ejbFind<суффикс> и ejbSelect<суффикс> (рассмотрим далее).
  • При вызове методов ejbCreate() а затем метода ejbPostCreate() у домашнего интерфейса, или же когда контейнер активирует компонент посредством метода ejbActivate(), компонент переходит в состояние готовности. После завершения метода ejbCreate() создается первичный ключ - идентификатор созданного объекта, а затем уже вызывается метод ejbPostCreate(). В состоянии готовности компонент способен обслуживать запросы от клиента. Когда клиент вызывает метод удаленного интерфейса getName(), то этот метод вызывается посредством RMI у компонента на сервере. В состоянии готовности могут также вызываться методы ejbLoad() и ejbStore(). Они вызываются в любом порядке -порядок определяется производителем компонента. Некоторые производители вызывают эти методы перед каждым выполнением прикладного метода.
  • Компонент может переходить обратно из состояния готовности в пул в случае пассивизации (посредством метода ejbPassivate() ). Эта операция может выполняться для более эффективного управления ресурсами. Также компонент может переходить обратно при вызове метода ejbRemove() - в этом случае все данные о компоненте удаляются из базы данных.
  • Когда контейнер решит уменьшить количество экземпляров в пуле, он удаляет некоторые из них. Теперь сборщик мусора может их окончательно удалить из памяти. После удаления из пула, у компонента вызывается метод unsetEntityContext().
  • Пример "Космические корабли"

    Компонент

    Разработаем объектный компонент BMP. Он будет использоваться для хранения информации о космических кораблях. Параметры космических кораблей компонент будет получать из базы данных, посредством его set и get методов клиентская программа или другие компоненты смогут изменять эти параметры. Компонент будет называться WidgetBean.

    Создание таблицы в базе данных

    (рис 4.1) Инфологическая модель базы данных космических кораблей

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

    create or replace package WIDGET_ID_PKG as LAST_ID integer;
    function GET_LAST_ID return integer; end WIDGET_ID_PKG; 
    /
    create or replace package body WIDGET ID PKG as function GET_LAST_ID return integer is 
    begin
    return LAST_ID; 
    end GET_LAST_ID;
    end WIDGET_ID_PKG;
    
    /
    
    drop table WIDGETS;
    create table WIDGETS   (
    ID integer not null, 
    DESCRIPTION varchar(50) not null, 
    PRICE float not null, 
    primary key(ID)
    );
    drop sequence WIDGETS_SEQ;
    create sequence WIDGETS_SEQ 
    start with 1 
    increment by 1
    nomaxvalue;
    
    create trigger WIDGETS_TRIGGER before insert on WIDGETS 
    for each row 
    begin
    select WIDGETS_SEQ.nextval into WIDGET_ID_PKG.LAST_ID from dual;
    select WIDGET_ID_PKG.LAST_ID into  :new.id from dual;
    end;
    /
    insert into WIDGETS(DESCRIPTION, PRICE)
    values('Buran',   1000);
    insert into WIDGETS(DESCRIPTION, PRICE)
    values('Shuttle',   2000);
    insert into WIDGETS(DESCRIPTION, PRICE)
    values('Space Ship One',   3000);

    Удаленный интерфейс

    Вначале создадим для компонента удаленный интерфейс, который будет состоять только из get и set методов. Эти методы будут соответствовать полям в только что созданной таблице WIDGETS.

    public interface WidgetRemote extends EJBObject  
    {
    public Integer getId()   throws RemoteException;
    public double getPrice()   throws RemoteException;
    public String getDescription()   throws RemoteException;
    public void setPrice(double price)   throws RemoteException;
    public void setDescription(String description)   throws RemoteException;
    }

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

    Домашний интерфейс

    Домашний интерфейс компонента WidgetBean несколько отличается от аналогичных домашних интерфейсов сеансовых компонентов. В нем помимо create методов определены find методы, которые позволяют осуществлять поиск компонентов по определенным параметрам. К примеру, можно искать все космические корабли с ценою не выше заданной.

    Для компонента WidgetBean определен create метод, который создает новый экземпляр компонента с заданным описанием и ценой. В качестве find методов определены два метода, которые обычно присутствуют у всех домашних интерфейсов объектных компонентов -

  • public Collection findAll() - возвращает коллекцию из всех компонентов, определенных на данный момент в базе данных. В качестве объектов коллекции выступают удаленные интерфейсы компонента.
  • public WidgetRemote findByPrimaryKey(Integer widgetId) -возвращает удаленный интерфейс компонента с заданным первичным ключом. В случае, если компонента с таким первичным ключом не существует, генерируется исключение javax.ejb.FinderException.
  • public interface WidgetHome extends EJBHome  
    {
    public WidgetRemote create(String description,  double price)
    throws RemoteException,   CreateException;
    public WidgetRemote findByPrimaryKey(Integer widgetId)
    throws FinderException,   RemoteException;
    public Collection findAll()   throws FinderException,   RemoteException;
    }

    Класс компонента

    В классе компонента придется вручную реализовывать все заявленные выше методы, а также методы ejbLoad (), ejbStore (), ejbRemove (), ejbActivate () и ejbPassivate().

    Класс объектного компонента должен имплементировать (implement) интерфейс EntityBean.

    public class WidgetBean implements EntityBean

    Начнем с самого простого - set и get методов. Их реализация приводится ниже. Могло бы показаться несколько странным, что в этих методах отсутствуют какие-либо механизмы взаимодействия с базой данных. Все взаимодействия с базой данных по поддержанию постоянства компонента описываются в методах ejbLoad() и ejbStore().

    private Integer widgetId;
    private String description;
    private double price;
    private EntityContext context;
    public Integer getId()   
    {
    return widgetId;
    }
    public String getDescription()   
    {
    return description;
    }
    public double getPrice()   
    { 
    return price;
    }
    
    public void setDescription(String description)   
    { 
    this.description = description;
    }
    public void setPrice(double price)   
    { 
    this. price = price;
    }

    В методах ejbLoad() и ejbStore() вызываются private методы компонента loadRow() и storeRow(). Метод loadRow () по widgetId загружает из базы данных цену и описание, а метод storeRow() выполняет сохранение этих полей в базе данных. Работа с базой данных аналогична работе с базой данных из примера с интернет магазином. В начате метода ejbLoad() выполняется следующая инструкция -

    widgetId = (Integer) context.getPrimaryKey();

    Метод loadRow() использует widgetId для того, чтобы загрузить запись из базы данных.

    Но в некоторых случаях widgetId может быть недоступно (эксперты говорят, что для некоторых производителей контейнеров ejb это случается, если был сделан запрос поиска по первичному ключу посредством метода Integer ejbFindByPrimaryKey (Integer) (см. далее), и компонент в этот момент был активизирован). Для этого вызывается метод Object getPrimaryKey () класса EntityContext. Через контекст первичный ключ доступен всегда. Возможно это обуславливается особенностями некоторых контейнеров EJB.Так или иначе, эксперты гарантируют, что если получать первичный ключ в методе ejbLoad() и методе ejbActivate(), то проблем возникнуть не должно. Что ж, доверимся их мнению.

    public void ejbLoad()   
    {
    widgetId =  (Integer)   context.getPrimaryKey();
    try 
    	{
    	loadRow();
    	}   
    catch   (Exception ex)   
    	{
    	throw new EJBException("ejbLoad:   " + ex.getMessage());
    	}
    }
    
    public void ejbStore()   
    {
    try 
    	{
    	storeRow();
    	}   
    catch   (Exception ex)   
    	{
    	throw new EJBException("ejbLoad:   " + ex.getMessage());
    	}
    }
    private void loadRow()   throws SQLException  
    	{ 
    	Connection con = null; 
    	PreparedStatement prepStmt = null;
    	try 
    		{
    		con = dataSource.getConnection(); 
    		String selectStatement ="select DESCRIPTION, PRICE " +
    		"from WIDGETS where ID = ?  ";
    		prepStmt = con.prepareStatement(selectStatement); 
    		prepStmt.setInt(1,  widgetId); 
    		ResultSet rs = prepStmt.executeQuery();
    		if   (rs.next())   
    			{
    			this.description = rs.getString(1); 
    			this.price = rs.getDouble(2); 
    			prepStmt.close();
    			}
    		else  
    			{
    			prepStmt.close();
    			throw new NoSuchEntityException("Row for widgetId " + widgetId + " not found in database.");
    			}
    		con.close();
    		}   
    	catch   (Exception e)   
    		{
    		} 
    	finally 
    		{
    		if   (prepStmt   != null)   
    			{
    			try 
    			{
    			prepStmt.close(); 
    			}   
    			catch   (Exception e)
    			{
    			}
    			}
    		if   (con   != null)   
    			{
    			try 
    			{
    			con.close();
    			}   
    			catch   (Exception e)
    			{
    			}
    			}
    		}
    	}
    
    private void storeRow()   throws SQLException  
    	{ 
    	Connection con = null; 
    	PreparedStatement prepStmt = null;
    	try 
    		{
    		con = dataSource.getConnection(); 
    		String updateStatement = "update WIDGETS set DESCRIPTION =   ? , 
    			" + "PRICE = ? " + "where ID = ?";
    		prepStmt = con.prepareStatement(updateStatement);
    		prepStmt.setString(1,  description); 
    		prepStmt.setDouble(2,  price); 
    		prepStmt.setInt(3,  widgetId);
    		int rowCount = prepStmt.executeUpdate();
    		prepStmt.close();
    		con.close();
    		
    		if   (rowCount == 0)   
    			{
    			throw new EJBException("Storing row for widgetId " + widgetId + " failed.");
    			}
    		}   
    	catch   (Exception e)   
    		{
    		} 
    	finally 
    		{
    		if (prepStmt != null) 
    			{
    			try 
    			{
    			prepStmt.close(); 
    			}   
    			catch   (Exception e)   
    			{
    			}
    		}
    		if   (con   != null)   
    			{
    			try 
    			{
    			con.close();
    			}   
    			catch   (Exception e)   
    			{
    			}
    			}
    		}
    	}

    Теперь перейдем к create методу компонента WidgetBean.В нем выполняется создание записи в базе данных, соответствующей компоненту. Делается это посредством метода Integer insertRow(String description, double price). Он создает новую запись в таблице базы данных, а потом возвращает ID созданной записи через запрос к WIDGET_ID_PKG. Метод ejbCreate() возвращает первичный ключ созданного экземпляра компонента, но клиентское приложение при вызове метода create домашнего интерфейса получит уже не первичный ключ, а удаленный интерфейс созданного компонента (см. WidgetRemote ). Метод ejbPostCreate () не выполняет никаких действий. При работе с BMP он вообще не используется. В нем иногда возникает необходимость при работе с CMP.

    public Integer ejbCreate(String description,  double price)   throws
    CreateException  
    {
    try 
    	{
    	widgetId = insertRow(description, price);
    	}   
    catch   (Exception ex)   
    	{
    	throw new EJBException("ejbCreate:   " + ex.getMessage());
    	}
    
    this.description = description;
    this.price = price;
    return widgetId;
    }
    
    public void ejbPostCreate(String description,  double price)   
    {   }
    
    private Integer insertRow(String description,  double price)   throws SQLException  
    {
    Connection con = null; 
    PreparedStatement prepStmt = null;
    try 
    	{
    	con = dataSource.getConnection();
    	String insertStatement ="insert into WIDGETS(DESCRIPTION,   PRICE)   
    		values  " + "  (?, ?)";
    	
    	prepStmt = con.prepareStatement(insertStatement);
    	prepStmt.setString(1,  description); 
    	prepStmt.setDouble(2,  price); 
    	prepStmt.executeUpdate();
    	prepStmt.close();
    	prepStmt = con.prepareStatement("select " + WIDGET_ID_PKG.GET_LAST_ID from dual");
    	ResultSet r = prepStmt.executeQuery(); 
    	int id = -1;
    	if (r.next()) 
    		{
    		id = r.getInt(1);
    		}
    	
    	if (id == -1) 
    		{
    		throw new SQLException("Error getting last index");
    		}
    	
    	con.close();
    	return new Integer(id);
    	}   
    catch   (Exception e)   
    	{
    	} 
    finally 
    	{
    	if   (prepStmt   != null)   
    		{
    		try 
    		{
    		prepStmt.close(); 
    		}   
    		catch   (Exception e)
    		{
    		}
    		}
    	if   (con   != null)   
    		{
    		try 
    		{
    		con.close();
    		}   
    		catch   (Exception e)
    		{
    		}
    		}
    	}
    
    return null;
    }

    При помощи метода ejbRemove() выполняется удаление экземпляра компонента из базы данных. Из него вызывается метод deleteRow (Integer id), который удаляет запись в базе данных с соответствующим первичным ключом.

    public void ejbRemove()   
    {
    try 
    	{
    	deleteRow(widgetId); 
    	}   
    catch   (Exception ex)   
    	{
    	throw new EJBException("ejbRemove:   " + ex.getMessage());
    	}
    private void deleteRow(Integer widgetId)   throws SQLException  
    	{
    	Connection con = null; 
    	PreparedStatement prepStmt = null;
    	try 
    		{
    		con = dataSource.getConnection(); 
    		String deleteStatement ="delete from WIDGETS where ID = ?  "; 
    		prepStmt = con.prepareStatement(deleteStatement);
    		prepStmt.setInt(1,  widgetId); 
    		prepStmt.executeUpdate(); 
    		prepStmt.close(); 
    		con.close();
    		}   
    	catch   (Exception e)   
    		{
    		} 
    	finally 
    		{
    		if (prepStmt != null) 
    			{ 
    			try 
    			{
    			prepStmt.close(); 
    			}   
    			catch   (Exception e)   
    			{
    			}
    			}
    		}
    		if   (con  != null)   
    			{
    			try 
    			{
    			con.close();
    			}   
    			catch   (Exception e)   
    			{
    			}
    			}
    		}

    Реализация методов поиска элементов аналогична реализации предыдущих методов, за исключением что возвращается либо первичный ключ ( Integer ejbFindByPrimaryKey(Integer primaryKey) ) либо коллекция из первичных ключей ( Collection ejbFindAll() ), как и в случае с create методами.

    public Integer ejbFindByPrimaryKey(Integer primaryKey)   throws FinderException  
    {
    boolean result;
    try 
    	{
    	result = selectByPrimaryKey(primaryKey);
    	}   
    catch   (Exception ex)   
    	{
    	throw new EJBException("ejbFindByPrimaryKey:   " + ex.getMessage());
    	}
    if   (result)   
    	{
    	return primaryKey; 
    	}  
    else  
    	{
    	throw new ObjectNotFoundException("Row for id " + primaryKey + " not found.");
    	}
    }
    
    public Collection ejbFindAll() throws FinderException  
    {
    try 
    	{
    	Collection col = getAllElements();
    	return col;
    	}   
    catch   (SQLException e)   
    	{
    	throw new ObjectNotFoundException("Error retriving all" + "the elements in the database.");
    	}
    }
    private boolean selectByPrimaryKey(Integer primaryKey)   throws SQLException  	
    {
    Connection con = null;
    PreparedStatement prepStmt = null;
    boolean result = false;
    try 
    	{
    	con = dataSource.getConnection(); String selectStatement =
    	"select ID from WIDGETS where ID = ? ";
    	prepStmt = con.prepareStatement(selectStatement); 
    	prepStmt.setInt(1,  primaryKey);
    	ResultSet rs = prepStmt.executeQuery();
    	result = rs.next();
    	prepStmt.close();
    	con.close();
    	return result;
    	}   
    catch   (Exception e)   
    	{ 
    	return result;
    	} 
    finally 
    	{
    	if   (prepStmt   != null)   
    		{
    		try 
    		{
    		prepStmt.close(); 
    		}   
    		catch   (Exception e)
    		{
    		}
    		}
    	if   (con != null)   
    		{
    		try 
    		{
    		con.close();
    		}   
    		catch (Exception e)
    		{
    		}
    		}
    	}
    }
    private Collection getAllElements() throws SQLException  
    { 
    Connection con = null;
    PreparedStatement prepStmt = null; 
    ArrayList list = new ArrayList();
    try 
    	{
    	con = dataSource.getConnection(); String selectStmt = "select ID " + "from WIDGETS";
    	prepStmt = con.prepareStatement(selectStmt);
    	ResultSet rs = prepStmt.executeQuery();
    	while   (rs.next())   
    		{
    		int id = rs.getInt(1); 
    		list.add(new Integer(id));
    		}
    	return list;
    	}   
    catch   (Exception e)   
    	{
    	} 
    finally 
    	{
    	if (prepStmt != null) 
    		{ 
    		try 
    		{
    		prepStmt.close(); 
    		}   
    		catch   (Exception e)
    		{
    		}
    		}
    	if   (con   != null)   
    		{
    		try 
    		{
    		con.close();
    		}   
    		catch   (Exception e)
    		{
    		}
    		}
    	}
    return null;
    }

    Осталось рассмотреть последний методы, связанные с активизацией, пассивизацией и получением контекста объектных компонентов. В методе void setEntityContext (EntityContext context) сохраняется переданный контекст объектных компонентов, а также организуется соединение с базой данных. Стоит отметить, что в методе void ejbActivate () также вызывается метод Object EntityContext.getPrimaryKey (), который восстанавливает значение первичного ключа widgetId. В методе ejbPassivate () эксперты рекомендуют обнулять первичный ключ.

    public void setEntityContext(EntityContext entityContext)   
    { 
    this.context = entityContext;
    try 
    	{
    	InitialContext ctx = new InitialContext(); 
    	if   (dataSource == null)
    	dataSource =   (DataSource)
    	ctx.lookup("java:/OracleDS"); 
    	System.out.println("Data source aquired"); 
    	}   
    catch   (NamingException ex)   
    	{
    	throw new EJBException(ex);
    	}
    }
    
    public void unsetEntityContext()   
    { this.context = null;
    }
    public void ejbActivate()   
    {
    widgetId = (Integer)context.getPrimaryKey();
    }
    public void ejbPassivate()   
    { widgetId = null;
    }

    Десприптор развертывания

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

  • В элементе <persistence-type> описывается тип постоянства - значение "Bean" означает, что постоянством управляет компонент, а значение "Container" - то, что пространством управляет контейнер.
  • В элементе <prim-key-class> определен класс первичного ключа. В данном случае это класс Integer. Первичный ключ компонента может состоять из нескольких полей, в этом случае в качестве первичного ключа должен выступать определенный пользователем сериализуемый класс, с полями, соответствующими полям первичного ключа в базе данных.
  • Элемент <reentrant> определяет, разрешены ли повторые вхождения в методы экземпляра компонента. То есть если вдруг пришли два вызова методов компонента на один и тот же экземпляр компонента одновременно. Если значение "true", то они оба одноврменно будут выполнять соответствующие методы компонента, если же значение "false", то тогда одному вызову придется подождать.
  • <?xml version="1.0" encoding="UTF-8"?>
    
    <ejb-jar id="ejb-jar ID" version="2.1"
    xmlns = "http://java.sun.com/xml/ns/j2ee" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/ejb-jar_2_1.xsd">
    
    <display-name>SpaceShipBean</display-name>
    
    <enterprise-beans>
    
    <entity>
    <description>
    The bean demonstrating space ships </description>
    <ejb-name>WidgetBean</ejb-name> 
    <home>spaceShip.WidgetHome</home> 
    <remote>spaceShip.WidgetRemote</remote> 
    <ejb-class>spaceShip.WidgetBean</ejb-class> 
    <persistence-type>Bean</persistence-type> 
    <prim-key-class>java.lang.Integer</prim-key-class> 
    <reentrant>false</reentrant> </entity> 
    </enterprise-beans> </ejb-jar>

    Клиентское приложение

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

    (рис 4.2) Внешний вид приложения

    При запуске в приложение загружаются посредством вызова метода Collection findAll () домашнего интерфейса все имеющиеся в наличии корабли (Рис. 4.2). Получение коллекции из всех элементов, а также операции с удаленными интерфейсами компонента WidgetBean выделены жирным шрифтом.

    Collection col = home.findAll();model = new DefaultTableModel();
    model.addColumn("ID");
    model.addColumn("Description");
    model.addColumn("Price");
    model.addColumn("Delete");
    // Обрабатываем список из всех кораблей в базе данных
    for   (Iterator iter = col.iterator();  iter.hasNext();)   {
    WidgetRemote remote =  (WidgetRemote)   iter.next();
    model.addRow(new Object []{remote.getId(),
    remote.getDescription(),
    remote.getPrice(),   "Delete?
    (рис 4.3) Добавление нового корабля

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

    int result = JOptionPane.showOptionDialog
    	(table, new JScrollPane(panel), "Add new widget", 
    		JOptionPane.OK_CANCEL_OPTION,   JOptionPane.PLAIN_MESSAGE, null,  null,  null);
    
    if (result == JOptionPane.OK_OPTION) 
    { 
    try 
    	{
    	double price = Double.parseDouble(priceField.getText()); 
    	String description = descrField.getText(); 
    	home.create(description, price);
    	loadElements();
    	table.invalidate(); 
    	}   
    catch   (NumberFormatException ex)   
    	{
    	ex.printStackTrace(); 
    	}   
    catch   (RemoteException ex)   
    	{
    	ex.printStackTrace(); 
    	}   
    catch   (CreateException ex)   
    	{
    	ex.printStackTrace();
    	}
    }
    (рис 4.4) Станция "Мир" появилась на складе

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

    (рис 4.5) Новый корабль в базе данных

    Посредством запроса через SQL*PLUS,проверим, что корабль появился в базе данных (Рис. 4.5).

    (рис 4.6) Поддержим отечественного производителя! Русские названия тоже работают

    Если ввести русское название для корабля, то все тоже должно работать (Рис. 4.6).

    (рис 4.7) Удалим ненужный корабль

    При выделении ячейки столбца "Delete" появляется кнопка "Yes". Нажав ее можно удалить корабль. В Удаление производится посредством метода remove (Object primaryKey) домашнего интерфейса компонента.

    Integer id =   (Integer)   table.getModel().getValueAt(row,   0);
    if   (id.intValue()   > 0)   
    {
    try 
    	{
    	home.remove(id);
    	loadElements();
    	table.invalidate();
    	}   
    catch   (RemoteException e)   
    	{
    	e.printStackTrace();
    	}   
    catch   (RemoveException e)   
    	{
    	e.printStackTrace();
    	}
    }
    (рис 4.8) Изменение цены корабля

    При выделении ячейки столбца "Description" или "Price" появляется кнопка "Edit". Нажав ее можно редактировать описание или цену корабля (Рис. 4.8). Поиск экземпляра компонента по его первичному ключу и изменение поля в этом компоненте выделено жирным шрифтом.

    int result = JOptionPane.showOptionDialog(table,
    new JScrollPane(textField),   (String)table.getColumnName(column),
    JOptionPane.OK_CANCEL_OPTION,   JOptionPane.PLAIN_MESSAGE,
    null,  null,  null);
    if (result == JOptionPane.OK_OPTION)   
    {
    Integer id =   (Integer)   table.getModel().getValueAt(row,   0);
    if   (id.intValue()   > 0)   
    	{
    	try 
    		{
    		WidgetRemote remote =   (WidgetRemote) home.findByPrimaryKey(id);
    		remote.setDescription(textField.getText());
    		table.setValueAt(textField.getText(),   row,   column);
    		table.invalidate(); 
    		}   
    	catch   (RemoteException e)   
    		{
    		e.printStackTrace(); 
    		}   
    	catch   (FinderException e)   
    		{
    		e.printStackTrace();
    		}
    	}
    }

    Код клиентского приложения состоит из двух файлов - ButtonTableCellEditor.java и WidgetEditClient.java. Класс ButtonTableCellEditor из одноименного файла предоставляет возможность для создания редактируемых при помощи кнопки ячеек таблицы. С его помощью можно редактировать поля и удалять экземпляры компонентов из базы данных.

    public abstract class ButtonTableCellEditor implements TableCellEditor, ActionListener  
    {
    private TableCellEditor editor;
    private JButton customEditorButton;
    protected JTable table; 
    protected int row,   column;
    public ButtonTableCellEditor(TableCellEditor editor,   String buttonName)
    	{
    	this.editor = editor;
    	customEditorButton = new JButton(buttonName);
    	customEditorButton.addActionListener(this);
    	customEditorButton.setFocusable(false);
    	customEditorButton.setFocusPainted(false);
    	customEditorButton.setMargin(new Insets(0,   0,   0,   0));
    	}
    public Component getTableCellEditorComponent(JTable table,  Object value, boolean isSelected,   int row,   int column)   
    	{
    	JPanel panel = new JPanel(new BorderLayout());
    	panel.add(editor.getTableCellEditorComponent(table,  value, isSelected,   row,   column));
    	panel.add(customEditorButton,   BorderLayout.EAST);
    	this.table = table;
    	this.row = row;
    	this.column = column;
    	return panel;
    	}
    
    public Object getCellEditorValue()
    	{
    	return editor.getCellEditorValue();
    	}
    
    public boolean isCellEditable(EventObject anEvent)
    	{
    	return editor.isCellEditable(anEvent);
    	}
    public boolean shouldSelectCell(EventObject anEvent)
    	{ 
    	return editor.shouldSelectCell(anEvent);
    	}
    
    public boolean stopCellEditing()
    	{
    	return editor.stopCellEditing();
    	}
    public void cancelCellEditing()
    	{ 
    	editor.cancelCellEditing();
    	}
    
    public void addCellEditorListener(CellEditorListener l)
    	{ 
    	editor.addCellEditorListener(l);
    	}
    
    public void removeCellEditorListener(CellEditorListener l)
    	{
    	editor.removeCellEditorListener(l);
    	}
    public final void actionPerformed(ActionEvent e)
    	{ 
    	editor.cancelCellEditing(); 
    	editCell(table,   row,   column);
    	}
    
    protected abstract void editCell(JTable table,   int row,   int column);
    }

    Класс WidgetEditClient наследуется (extends) от JFrame. То как в нем организовано взаимодействие с EJB уже было рассмотрено ранее. Сейчас просто будет приведен код этого класса.

    public class WidgetEditClient extends JFrame implements ActionListener  
    {
    private static final long serialVersionUID = -7066592068515570150L;
    private DefaultTableModel model;
    private JTable table = null;
    private Context jndiContext;
    private WidgetHome home;
    private JButton addButton;
    public WidgetEditClient()   
    	{
    	setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    	// Obtaining the beans  from the database
    	try 
    		{
    		jndiContext = createJBossContext();
    		Object ref = jndiContext.lookup("WidgetBean");
    		home =   (WidgetHome)
    		PortableRemoteObject.narrow(ref,  WidgetHome.class); 
    		}   
    	catch(NamingException e)   
    		{ e.printStackTrace();
    		}
    	
    	loadElements();
    	
    	JScrollPane scroll = new JScrollPane(table);
    	scroll.setBorder(BorderFactory.createEmptyBorder()); 
    	getContentPane().setLayout(new BorderLayout());
    	
    	getContentPane().add(BorderLayout.CENTER,  scroll); 
    	getContentPane().add(BorderLayout.SOUTH,   addButton = new
    	
    	JButton("Add"));
    	addButton.addActionListener(this); 
    	pack();
    	}
    	
    public void actionPerformed(ActionEvent e)   
    	{ 
    	JPanel panel = new JPanel(); 
    	panel.setLayout(new GridBagLayout());
    	GridBagConstraints gc = new GridBagConstraints();
    	gc.insets = new Insets(5,   5,   5,   5); 
    	gc.weightx = 1.0; 
    	gc.weighty = 0.0;
    	gc.fill = GridBagConstraints.BOTH;
    	gc.gridx = 0; 
    	gc.gridy = 0; 
    	gc.gridwidth = 1; 
    	gc.gridheight = 1;
    	panel.add(new JLabel("Price:"),   gc); 
    	gc.gridy = 1;
    	
    	panel.add(new JLabel("Description:"),   gc);
    	gc.gridx = 1; 
    	gc.gridy = 0;
    	
    	JTextField priceField = new JTextField(); panel.add(priceField,   gc);
    	gc.gridy = 1;
    	JTextField descrField = new JTextField(); 
    	panel.add(descrField,   gc);
    	
    	int result = JOptionPane.showOptionDialog
    		(table, new JScrollPane(panel), "Add new widget", 
    			JOptionPane.OK_CANCEL_OPTION, 
    			JOptionPane.PLAIN_MESSAGE, null,  null,  null);
    	
    	if (result == JOptionPane.OK_OPTION)   
    		{
    		try 
    			{
    			double price = Double.parseDouble(priceField.getText()); 
    			String description = descrField.getText(); 
    			home.create(description,  price);
    			loadElements();
    			table.invalidate(); 
    			}   
    		catch   (NumberFormatException ex)   
    			{ 
    			ex.printStackTrace();
    			} 
    		catch (RemoteException ex) 
    			{ 
    			ex.printStackTrace();
    			} 
    		catch (CreateException ex) 
    			{ 
    			ex.printStackTrace();
    			}
    		}
    	}
    }
    
    private class DeleteCellEditor extends ButtonTableCellEditor  
    { 
    public DeleteCellEditor(TableCellEditor editor)  
    	{ 
    	super(editor,   "Yes");
    	}
    
    protected void editCell(JTable table,   int row,   int column)   
    	{
    	Integer id =  (Integer)   table.getModel().getValueAt(row,   0);
    	if   (id.intValue()   > 0)   
    		{
    		try 
    			{
    			home.remove(id);
    			loadElements();
    			table.invalidate();
    			}   
    		catch   (RemoteException e)   
    			{
    			e.printStackTrace();
    			}   
    		catch   (RemoveException e)   
    			{
    			e.printStackTrace();
    			}
    		}
    	}
    }
    
    private class DescriptionCellEditor extends ButtonTableCellEditor  
    {
    public DescriptionCellEditor(TableCellEditor editor)   
    	{
    	super   (editor,   "Edit");
    	}
    
    protected void editCell(JTable table,   int row,   int column)   
    	{
    	JTextField textField = new JTextField();
    	Object value = table.getValueAt(row,   column);
    	if(value   != null)
    		{
    		textField.setText((String)   value);
    		textField.setCaretPosition(0);
    		}
    	int result = JOptionPane.showOptionDialog(table, new JScrollPane(textField), 
    		(String)table.getColumnName(column),
    	
    	JOptionPane.OK_CANCEL_OPTION,
    	
    	JOptionPane.PLAIN_MESSAGE, null, null, null);
    	
    	if (result == JOptionPane.OK_OPTION) 
    		{
    		Integer id =   (Integer)
    		table.getModel().getValueAt(row,   0);
    		if (id.intValue() > 0) 
    			{
    			try 
    			{
    			WidgetRemote remote =   (WidgetRemote) home.findByPrimaryKey(id);
    			remote.setDescription(textField.getText());
    			table.setValueAt(textField.getText(),   row, column);
    			table.invalidate(); 
    			}   
    			catch   (RemoteException e)   
    			{
    			e.printStackTrace(); 
    			}   
    			catch   (FinderException e)   
    			{
    			e.printStackTrace();
    			}
    			}
    		}
    	}
    }
    
    private class PriceCellEditor extends ButtonTableCellEditor  
    { 
    public PriceCellEditor(TableCellEditor editor)  
    	{ 
    	super(editor,   "Edit");
    	}
    
    protected void editCell(JTable table,   int row,   int column)   
    	{
    	JTextField textField = new JTextField(); 
    	Object value = table.getValueAt(row,   column);
    	if(value   != null)
    		{
    		textField.setText(((Double)   value).toString()); 
    		textField.setCaretPosition(0);
    		}
    	
    	int result = JOptionPane.showOptionDialog(table, new JScrollPane(textField), 
    		(String)table.getColumnName(column),
    	JOptionPane.OK_CANCEL_OPTION,
    	JOptionPane.PLAIN_MESSAGE, null, null, null);
    	
    	if(result == JOptionPane.OK_OPTION) 
    		{
    		Integer id =   (Integer)
    		table.getModel().getValueAt(row,   0);
    		if (id.intValue() > 0) 
    			{
    			try 
    				{
    				WidgetRemote remote =   (WidgetRemote) home.findByPrimaryKey(id);
    				double price = Double.
    				parseDouble(textField.getText());
    				remote.setPrice(price);
    				
    				table.setValueAt(price,   row,   column);
    				table.invalidate(); 
    				}   
    			catch   (RemoteException e)   
    				{
    				e.printStackTrace(); 
    				}   
    			catch   (FinderException e)   
    				{
    				e.printStackTrace(); 
    				}   
    			catch   (NumberFormatException e)   
    				{
    				e.printStackTrace();
    				}
    			}
    		}
    	}
    
    private void loadElements()   
    	{
    	try 
    		{
    		Collection col = home.findAll();
    		model = new DefaultTableModel();
    		model.addColumn("ID");
    		model.addColumn("Description");
    		model.addColumn("Price");
    		model.addColumn("Delete");
    		for   (Iterator iter = col.iterator();  iter.hasNext();)   
    			{
    			WidgetRemote remote =   (WidgetRemote)   iter.next();
    			model.addRow(new Object   []{remote.getId(), remote.getDescription(),
    			remote.getPrice(),   "Delete?"});
    			}
    		if (table == null) 
    			{
    			table = new JTable(model); 
    			}  
    		else  
    			{
    			table.setModel(model);
    			}
    		JTextField textField = new JTextField();
    		textField.setBorder(BorderFactory.createEmptyBorder()); 
    		DefaultCellEditor editor = new DefaultCellEditor(textField); editor.setClickCountToStart(l);
    		
    		table.getColumn(table.getColumnName(1)).setCellEditor(
    		new DescriptionCellEditor(editor));
    		table.getColumn(table.getColumnName(2)).setCellEditor(
    		new PriceCellEditor(editor));
    		table.getColumn(table.getColumnName(3)).setCellEditor(
    		new DeleteCellEditor(editor)); 
    		}   
    	catch(RemoteException e)   
    		{
    		e.printStackTrace(); 
    		}   
    	catch(FinderException e)   
    		{ 
    		e.printStackTrace();
    		}
    	}
    public static void main(String args[])   
    	{
    	WidgetEditClient client = new WidgetEditClient();
    	client.setLocation(200,   200); 
    	client.setSize(600,   400); 
    	client.setVisible(true);
    	}
    
    public static Context createJBossContext()   throws NamingException  
    	{ 
    	Properties p = new Properties(); 
    	p.put("java.naming.factory.initial", 	"org.jnp.interfaces.NamingContextFactory"); 
    	p.put("java.naming.provider.url", "jnp://127.0.0.1:1099"); 
    	p.put("java.naming.factory.url.pkgs", "org.jboss.naming:org.jnp.interfaces"); 
    	Context jndiContext = new InitialContext(p); 
    	return jndiContext;
    	}
    }

    Вывод

    Как видно из приведенного кода компонента WidgetBean,разработка BMP компонентов дело рутинное и довольно долгое. Использование CMP сильно упрощает процесс разработки объектных компонентов. В следующем примере будет рассмотрено использование CMP в следующем примере.

    Пример "Модернизированный интернет магазин"

    Общее описание

    Предыдущий пример с интернет магазином обладал рядом недостатков. В этом примере мы постараемся их исправить при помощи использования объектных компонент CMP.

    Создание проекта

    В этом примере создание компонент EJB будет тесно связано со встроенными функциями Eclipse-WTP,поэтому создадим сразу EJB проект с названием, например, ShoppingBasketObjectBeans.

    (рис 4.9) Подключаем XDoclet к проекту

    При создании проекта подключаем XDoclet (Рис. 4.9).

    Работа с XDoclet

    Для того чтобы сделать работу с объектными компонентами CMP легче, следует использовать средство XDoclet. XDoclet разрабатывался для поддержки атрибутно-ориентированной парадигмы программирования в различных технологиях платформы Java.Суть работы с XDoclet сводится к тому, что в код добавляются так называемые атрибуты. А затем встроенный генератор XDoclet по этим атрибутам создает либо автоматически сгенерированный код, либо различные вспомогательные файлы (xml,разнообразные файлы конфигураций и проч.). Атрибуты в XDoclet имеют точно такой же вид, как и javadoc комментарии. К примеру, XDoclet атрибут, описывающий объектный компонент Commodities выглядит следующим образом:

    <!-- begin-xdoclet-definition -->
    *	@ejb.bean name="Commodities"
    * jndi-name="Commodities"
    * type="CMP"
    * primkey-field="id"
    * schema="CommoditiesSCHEMA"
    * cmp-version="2.x" 
    */

    Затем XDoclet по этому атрибуту сгенерирует файл ejb.xml, в котором он сразу пропишет все нужные xml-теги,соответствующие описанным в атрибуте параметрам. Это очень удобно иметь сразу перед глазами все параметры компонента EJB,а не заглядывать каждый раз в файл ejb.xml и синхронизировать его с классом компонента.

    Полный список возможностей XDoclet доступен на сайте http://xdoclet. sourceforge.net/. Для примера нам понадобится поддержка EJB и JBoss в XDoclet.

    Обычно XDoclet устанавливается по умолчанию с Eclipse-WTP.Но если вам попалась урезанная версия Eclipse,или же вы хотите установить другую версию XDoclet,то следует щелкнуть правой кнопкой на EJB -проекте и выбрать раздел Properties.

    (рис 4.10) Выбор пути с библиотеками XDoclet

    В разделе XDoclet следует выбрать версию XDoclet (на данный моент поддерживаются три -1.2.1, 1.2.2 и 1.2.3) и указать путь к библиотекам XDoclet.Сами библиотеки можно скачать с сайта http://xdoclet.sourceforge.net/.

    Помимо указания пути к библиотекам XDoclet,нужно настроить XDoclet соответствующим образом, для того, чтобы он корректно генерировал файлы описаний для JBoss.В разделе XDoclet отключаем пункт "Use global xdoclet preferences". Раскрываем раздел XDoclet слева и там будет два подраздела - EJBDoclet и WEBDoclet.Выбираем подраздел EJBDoclet.

    (рис 4.11) Включаем поддержку JBoss

    И теперь включаем поддержку JBoss для нашего проекта (Рис. 4.11).

    Создание таблиц в базе данных

    (рис 4.12) Инфологическая модель базы данных

    Инфологическая модель базы данных не изменилась по сравнению с предыдущим примером (Рис. 4.12). Только названия таблиц переименованы, чтобы не конфликтовать с названиями таблиц из предыдущего примера. Однако SQL-код,создающий таблицы в базе данных претерпел некоторые изменения -

  • Во-первых, все поля, кроме первичного ключа перестали быть read-only. Это делает работу с объектными компонентами CMP гораздо более гибкой.
  • Во-вторых, теперь больше не нужно создавать пакеты (Package) для хранения только что созданного первичного ключа. Эту работу берет на себя контейнер EJB (в данном случае JBoss).
  • В-третьих, триггеры тоже больше не нужны. Теперь контейнер EJB сам генерирует автоматически первичный ключ по данной последовательности (в случае Oracle,для других баз данных используются другие механизмы).
  • Поэтому теперь SQL -код выглядит следующим образом

    drop table COMMODITY_ORDER_ENTITY;
    
    
    drop table COMMODITIES_LIST_ENTITY;
    
    
    drop table ORDERS_ENTITY;
    create table COMMODITIES_LIST_ENTITY (
    ID integer not null,
    NAME varchar(20)   not null,
    DESCRIPTION varchar(50)   not null,
    PRICE float not null,
    IMAGEPATH varchar(30),
    primary key(ID)
    );
    drop sequence COMMODITIES_ENTITY_SEQ;
    create sequence COMMODITIES_ENTITY_SEQ 
    start with 1 
    increment by 1 
    nomaxvalue;
    create table ORDERS_ENTITY
    
    ID integer not null,
    DATE_CREATED DATE not null,
    USERNAME varchar(20) not null, primary key(ID)
    );
    drop sequence ORDERS_ENTITY_SEQ;
    create sequence ORDERS_ENTITY_SEQ 
    start with 1 
    increment by 1 
    nomaxvalue;
    
    create table COMMODITY_ORDER_ENTITY
    (
    ID integer not null, ORDER_ID integer,
    COMMODITY_ID integer, 
    QUANTITY integer not null, 
    primary key(ID)
    );
    
    alter table COMMODITY_ORDER_ENTITY
    add constraint FK_ORDER_ENTITY 
    foreign key (ORDER_ID) 
    references ORDERS_ENTITY(ID);
    alter table COMMODITY_ORDER_ENTITY
    add constraint FK_COMMODITIES_ENTITY
    foreign key(COMMODITY_ID)
    references COMMODITIES_LIST_ENTITY(ID);
    drop sequence COMMODITY_ORDER_ENTITY_SEQ;
    create sequence COMMODITY_ORDER_ENTITY_SEQ
    start with 1 increment by 1 nomaxvalue;
    insert into COMMODITIES_LIST_ENTITY(ID, NAME, DESCRIPTION, PRICE, IMAGEPATH)
    values(COMMODITIES_ENTITY_SEQ.nextval,   'Kent 1',   
    	'Light cigarettes',   1.99, 'img/kent1.jpg');
    insert into COMMODITIES_LIST_ENTITY(ID, NAME, DESCRIPTION, PRICE, IMAGEPATH)
    values(COMMODITIES_ENTITY_SEQ.nextval,   'Kent 4',   
    	'Light cigarettes',   1.99, 'img/kent4.jpg');
    insert into COMMODITIES_LIST_ENTITY(ID, NAME, DESCRIPTION, PRICE, IMAGEPATH)
    values(COMMODITIES_ENTITY_SEQ.nextval,   'Kent 8',   
    	'Light cigarettes',   1.99, 'img/kent8.jpg');

    Компоненты

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

    Компонент CommoditiesBean

    Этот компонент описывает единицу товара на складе и соответствует таблице COMMODITIES_LIST_ENTITY. В Eclipse-WTP встроена возможность автоматического создания объектных компонент CMP по таблицам в базе данных. Сейчас с ее помощью будет создан компонент CommoditiesBean.

    (рис 4.13) Создаем новый компонент

    Щелкаем правой кнопкой на проекте и выбираем раздел New -> XDoclet Enterprise JavaBean.

    (рис 4.14) Объектный компонент CMP

    В открывшемся меню выбираем "Container Manager Entity Bean" (Рис. 4.14).

    (рис 4.15) Описание компонента

    Вводим описание компонента (Рис. 4.15) - пакет (Javapackage),в котором будут располагаться классы компонента ( shoppingBasketObject ), его имя (CommoditiesBean),класс, от которого наследуется компонент ( java.lang.Object ).

    (рис 4.16) Параметры компонента

    В параметрах компонента можно поменять имя компонента, идентификатор абстрактной схемы постоянства (необходима для языка запросов к объектным компонентам EJB-QL -будет рассмотрен далее), описание. В качестве CMP также следует оставить версию 2.x (версия 1.x оставлена для обратной совместимости с EJB 1.1).В параметре "usecase" выбираем "Import attributes from table" - это значит, что в дальнейшем объектный компонент будет создан непосредственно по таблице в базе данных.

    (рис 4.17) Создание подключения к базе данных

    После этого нам будет предложено выбрать подключение к базе данных, в которой находится необходимая таблица. В моем случае такое подключение уже было создано (Рис. 4.17). Внизу выводятся параметры этого подключения. Для того, чтобы создать новое подключение, необходимо нажать кнопку "New".

    (рис 4.18) Создание нового подключения

    В окне создания нового подключения необходимо ввести все параметры этого подключения - название базы данных ( EJBDB, в примерах), драйвер подключения

    ( oracle.jdbc.OracleDriver ) - для базы данных Oracle,расположение библиотеки с классами драйвера ( ojdbc14.jar - внутри установочного каталога Oracle,или там, где вы его положите) и URL подключения к базе данных в формате jdbc:oracle:thin:@адрес подключения:порт:название базы данных. Слева необходимо выбрать тип базы данных (Oracle 9i) и логин и пароль для подключения. После этого кнопкой Test Connection можно проверить факт того, что все настройки были введены корректно.

    (рис 4.19) Подключение удалось

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

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

    (рис 4.20) Создание компонента по таблице

    Теперь можно выбрать необходимую таблицу в базе данных

    ( BORIS.COMMODITIES_LIST_ENTITY - таково ее полное имя в Oracle, BORIS - это пользователь, которому она принадлежит). Осталось только немного подредактировать параметры. Eclipse-WTP по умолчанию ,почему-то, сопоставляет целочисленным полям из базы данных тип java.math.BigDecimal. Заменяем его на java.lang.Integer, а тип JDBC - с DECIMAL на INTEGER. Также тип FLOAT из базы данных стал строковым. Заменяем его на java.lang.Double, а тип JDBC - на DOUBLE. Теперь с типами стало все в порядке, переходим к последнему этапу создания объектного компонента CommoditiesBean.

    (рис 4.21) Выбор интерфейсов

    В последнем окне можно к интерфейсу javax.ejb.EntityBean добавить еще какие либо интерфейсы, которые будут заимплементированы классом компонента (Рис. 4.21). В этом случае это не нужно. Нажимаем Finish. После этого Eclipse-WTP сгенерирует нам класс компонента shoppingBasketObject.CommoditiesBean со всеми необходимыми атрибутами XDoclet.

    XDoclet по этим атрибутам сгенерирует удаленные, домашние и локальные интерфейсы, различные вспомогательные классы, а также файлы ejb.xml, jboss.xml и jbosscmp-jdbc.xml (если вы не забыли включить поддержку JBoss в настройках XDoclet).Теперь для работы и изменения созданного компонента CommoditiesBean,достаточно редактировать только файл CommoditiesBean.java. Все остальные файлы будут заново генерироваться XDoclet каждый раз при сохранении этого файла.

    (рис 4.22) Созданные файлы

    Все сгенерированные XDoclet файлы на Рис. 4.22.

    Класс компонента CommoditiesBean

    Рассмотрим сгенерированный класс компонента CommoitiesBean.В нем определены атрибуты XDoclet.

    /**
    * <!-- begin-xdoclet-definition --> 
    *@ejb.bean name="CommoditiesList"
    * jndi-name="CommoditiesList"
    * type="CMP"
    * primkey-field="id"
    
    * schema="CommoditiesListSCHEMA"
    * cmp-ersion="2.x" 
    * @ejb.persistence
    * table-ame="BORIS.COMMODITIES_LIST_ENTITY" 
    
    * @ejb.finder
    * query="SELECT OBJECT(a)   FROM CommoditiesListSCHEMA as a"
    * signature="java.util.Collection findAll()" 
    *
    * @ejb.pk class="java.lang.Integer"
    *
    *
    * <!-- end-xdoclet-definition -->
    * @generated
    **/

    Первый атрибут - это атрибут @ejb.bean. В нем описываются базовые характеристики объектного компонента:

  • Параметр name - имя компонента,
  • Параметр jndi-name - jndi -имя, по которому компонент будет доступен,
  • Параметр type - тип (CMP),
  • Параметр primkey-field - поле, используемое в качестве первичного ключа ( id ),
  • Параметр schema - название абстрактной схемы постоянства (необходимо для EJB-QL).
  • И параметр cmp-version - версия CMP, в нашем случае это "2.x". С помощью этого параметра можно задать CMP 1.x - для совместимости со старыми EJB -приложениями.
  • (рис 4.23) XDoclet отработал

    Здесь можно, к примеру, изменить jndi -имя компонента на "SomeStrangeName". После того, как XDoclet отработает (Рис. 4.23), файл jboss.xml примет следующий вид:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE jboss PUBLIC "-//JBoss//DTD JBOSS 3.2//EN" "http://www.jboss.org/j2ee/dtd/jboss 3 2.dtd">
    <jboss>
    <enterprise-beans>
    <entity>
    <ejb-name>Commodities</ejb-name>
    <jndi-name>SomeStrangeBean</jndi-name> 
    <local-jndi-name>CommoditiesLocal</local-jndi-name> 
    <method-attributes> </method-attributes> 
    </entity> 
    </enterprise-beans>
    <resource-managers>
    </resource-managers>
    </jboss>

    Вообще в файле jboss.xml можно задать локальные и удаленные jndi -имена для компонентов. Помимо этого есть раздел <resource-managers>. Его рассмотрение лежит вне пределов данного методического пособия. При помощи него можно расширять возможности JBoss - к примеру, добавить поддержку JavaMail.Клиентское приложение может получить доступ к этому сервису через JNDI и отправлять сообщения по электронной почте. В разделе <method-attributes> можно определить некоторые дополнительные атрибуты для методов данного компонента. Например, такой параметр как <transaction-timeout> - максимальное время выполнения транзакции (по умолчанию все методы компонентов EJB являются транзакциями - подробнее про это в следующем разделе). Следующий атрибут - это атрибут @ejb.persistence. В нем задается имя таблицы, которой соответствует объектный компонент.

    Далее идет атрибут @ejb. finder. В этом атрибуте можно задавать параметры поиска одного или нескольких экземпляров компонент при помощи SQL -подобного языка запросов EJB-QL.В данном случае сгенерирован стандартный запрос по выбору всех экземпляров компонента в базе данных (см. аналогичный запрос из предыдущего примера с BMP).Рассмотрим этот запрос - "SELECT OBJECT(a) FROM CommoditiesSCHEMA as a". Он состоит из оператора SELECT, за которым следует выражение OBJECT(a). Это значит, что выбирается непосредственно сам экземпляр компонента (при желании можно выбирать и поля компонента и даже вызывать агрегирующие функции типа суммы, среднего или максимума из наборов полей). Завершается оператор ключевым словом FROM за которым следует имя схемы постоянства (если помните, ей давалось имя при создании компонента).

    Абстрактная схема постоянства - это как совокупность всех экземпляров одного компонента. Если приводить аналогию с SQL, то экземпляр компонента - это строка в таблице, а абстрактная схема - вся таблица целиком.

    Метод findByPrimaryKey(Object pk) создается для объектного компонента CMP по умолчанию. Его не надо нигде определять.

    И последним идет атрибут @ejb.pk. В нем задается класс первичного ключа. Компонент почти готов для работы, однако осталось сделать несколько последних штрихов. Во-первых, в базе данных первичный ключ генерируется автоматически. Это нужно объяснить контейнеру JBoss.Есть несколько способом сделать это, самый простой - это добавить два новых атрибута:

  • @jboss.entity-command name = "oracle-sequence". Говорит JBoss о том, что первичный ключ генерируется при помощи последовательности Oracle.
  • @jboss.entity-command-attribute name = "sequence" value = "COMMODITIES_ENTITY_SEQ". Тут указываем имя последовательности, при помощи которой генерируется новый первичный ключ.
  • Ну и напоследок нужно добавить местоположение базы данных, в которой находится таблица. Файл, описывающий доступ к базе данных ( DataSource ) уже был создан и использовался в примере с обменом валют и в примере с BMP.Атрибут @jboss.persistence позволяет указать соответствующий доступ к базе данных для компонента. Полностью атрибут с параметрами выглядит следующим образом:

    @jboss.persistence
    datasource="java:/OracleDS"
    datasource-mapping="Oracle9i"
    table-name="BORIS.COMMODITIES_LIST_ENTITY"
    create-table="false"
    remove-table="false"
    alter-table="false"
  • Параметр datasource - это jndi -имя источника данных (см. получение DataSource в примере с BMP ).
  • Параметр datasource-mapping - это идентификатор типа базы данных. В данном случае - Oracle 9i.В соответствии с этим идентификатором, JBoss,будет формировать запросы к базе данных. Для каждой базы данных эти настройки определены в файле server\default\conf\standardjbosscmp-jdbc.xml установочного каталога JBoss.Менять в этом файле ничего не стоит, а посмотреть можно.
  • Параметр table-name - имя таблицы, которой соответствует компонент. JBoss (как и все другие контейнеры EJB) использует свои собственные настройки для описания соответствия компонентов таблицам из базы данных (файл jbosscmp-jdbc.xml ). Формат этого файла таков, что имя таблицы нужно продублировать.
  • Параметры create-table, remove-table и alter-table - определяют, может ли компонент создавать, удалять или изменять таблицы. Практической пользы от этих параметров для объектного компонента CMP немного. Скорее всего, он может использоваться при задании аналогичного атрибута для сеансовых компонент.
  • С заголовком класса компонента Commodities разобрались. Теперь перейдем непосредственно к коду этого класса. Вначале идет create метод без параметров. Для него определен атрибут @ejb.create-method, который должен определяться для всех create методов компонента. У этого атрибута один параметр - view-type. Он определяет, к какому интерфейсу (удаленному или локальному) принадлежит данный метод. Если нам понадобится создать новый экземпляр компонента, то разумно задать для него непосредственно в create методе сразу все необходимые параметры.

    /**
    * @ejb.create-method view-type="remote"
    
    */
    public Integer ejbCreate()
    throws CreateException  {
    return null;
    }
    
    public void ejbPostCreate()   {
    }

    Модернизируем create метод следующим образом:

    /**
    *  @ejb.create-method view-type="remote" */
    public Integer ejbCreate(String name,   
    	String description,  
    	double price, 
    	String imagePath) 
    	throws CreateException  {
    setName(name);
    setDescription(description);
    setPrice(price);
    setImagepath(imagePath);
    return null;
    }
    public void ejbPostCreate(String name, String description,  double price, String imagePath)   {
    }

    Теперь для того, чтобы создать компонент Commodities,необходимо передать ему все имя, описание, цену и путь до изображения единицы товара, описываемой этим компонентом. Осталось рассмотреть set и get методы, при помощи которых выполняется получение и изменение полей компонента. Сделаем это на примере поля name. Для этого поля, как и для всех JBoss сгенерировал при создании компонента уже все необходимые атрибуты.

    /**
    * 
    * 
    * <!-- begin-user-doc -->
    * CMP Field name 
    *
    * Returns the name
    * @return the name 
    *
    * <!-- end-user-doc -- > 
    *
    * <!-- begin-xdoclet-definition --> 
    *
    * @ejb.persistent-field
    * @ejb.persistence
    
    * column-name="NAME"
    * jdbc-type="VARCHAR"
    * sql-type="VARCHAR2"
    * read-only="false"
    *
    *
    * @ejb.interface-method *
    * <!-- end-xdoclet-definition -- >
    * @generated 
    */
    
    public abstract java.lang.String getName();
    /**
    * <!-- begin-user-doc -- >
    * Sets the name 
    *
    * @param java.lang.String the new name value
    * <!-- end-user-doc -- > 
    *
    * <!-- begin-xdoclet-definition -- >
    * @ejb.interface-method
    * <!-- end-xdoclet-definition -- >
    * @generated
    * /
    
    public abstract void setName(java.lang.String name);

    Как у set метода, так и у get метода определен атрибут @ejb.interface-method.

    Тестовый клиент для компонента CommoditiesBean

    Проверим работоспособность компонента при помощи простого тестового клиента. Вначале он получает все товары в магазине через метод findAll(), потом создает новый товар в магазине, удаляет его и, наконец, выполняет поиск по первичному ключу товара.

    public class ShoppingBasketOrderClient  
    {
    public static void main(String args[])   
    	{
    	try 
    		{
    		Context jndiContext = createJBossContext(); 
    		Object ref = jndiContext.lookup("Commodities");
    		CommoditiesHome commoditiesHome =   
    			(CommoditiesHome) PortableRemoteObject.narrow(ref, CommoditiesHome.class);
    		Collection col = commoditiesHome.findAll();
    		for   (Iterator iter = col.iterator();  iter.hasNext();)   
    			{ 
    			Commodities comm =   (Commodities)   iter.next();
    			System.out.println(comm.getName()   + "  " +
    			comm.getPrice());
    			}
    		
    		Commodities commCr = commoditiesHome.create("Marijuana",
    		"Finest American Blend",   6.50,  null); 
    		System.out.println("Created -  " + commCr.getName()   + "  " +
    		commCr.getPrice()); commCr.remove();
    		
    		Commodities commFound = commoditiesHome.findByPrimaryKey(new Integer(1));
    		System.out.println("Found - " + commFound.getName() + "  
    			" + commFound.getPrice());
    		}   
    	catch   (NamingException e)   
    		{
    		e.printStackTrace(); 
    		}   
    	catch   (FinderException e)   
    		{
    		e.printStackTrace(); 
    		}   
    	catch   (RemoteException e)   
    		{
    		e.printStackTrace(); 
    		}   
    	catch   (CreateException e)   
    		{
    		e.printStackTrace(); 
    		}   
    	catch   (RemoveException e)
    		{
    		e.printStackTrace();
    		}
    	}
    
    public static Context createJBossContext()   throws NamingException  
    	{ 
    	Properties p = new Properties(); 
    	p.put("java.naming.factory.initial", "org.jnp.interfaces.NamingContextFactory"); 
    	p.put("java.naming.provider.url", "jnp://127.0.0.1:1099"); 
    	p.put("java.naming.factory.url.pkgs", "org.jboss.naming:org.jnp.interfaces"); 
    	Context jndiContext = new InitialContext(p); 
    	return jndiContext;
    	}
    }
    (рис 4.24) Вывод тестового клиента

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

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

    Компоненты, управляемые сообщениями (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> <сумма> - перевод заданной суммы с первого счета на второй.
  • Безопасность в EJB

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

  • Аутентификация (Authentication).Аутентификация - это процесс подтверждения личности пользователя, пытающегося получить доступ в систему. Чаще всего подтверждение личности представляет собой ввод пароля, хотя возможны и более экзотические варианты, например, сканирование отпечатков пальцев или сетчатки глаза.
  • Авторизация (Authorization).Авторизация заключается в определении того, какие действия может и не может совершать каждый пользователь, вошедший в систему. Например, одним пользователям может быть дан доступ на запись в базу данных, другие же могут только читать из нее данные.
  • Конфиденциальность (Confidentiality).Требуется защитить данные, передаваемые по каналам связи между системой и пользователями, от перехвата и фальсификации злоумышленниками. Обычно для этого используется шифрование, например, SSL.
  • Авторы спецификации EJB сосредоточились на решении задачи авторизации, оставив остальные задачи разработчикам EJB -контейнеров.

    Обычно аутентификация происходит на уровне JNDI,при создании контекста. Когда пользователь подключается к EJB системе, с ним ассоциируется определенный идентификатор безопасности ( security identity ). Этот идентификатор неявно передается при каждом вызове методов EJB.На основе идентификатора безопасности контейнер EJB решает, имеет ли пользователь право на совершение запрошенной операции. В EJB реализована авторизация на основе ролей. Каждому подключенному к системе пользователю присваивается роль, например, "служащий", "менеджер", "администратор". В то же время, каждому методу EJB приписывается набор ролей пользователей, имеющих право на вызов этого метода. В момент вызова метода пользователем производится проверка того, что роль пользователя является одной из допустимых ролей вызываемого метода. Допустимые роли определяются а ннотацией @javax.annotation.security.RolesAllowed, принимающей в качестве параметра массив строк - названий ролей.

    Аннотация @javax.annotation.security.PermitAll позволяет открыть доступ к методу пользователям, играющим любые роли.

    Аннотация @javax.annotation.security.RunAs задает роль, от имени которой метод EJB будет вызывать другие методы. По умолчанию метод работает в роли вызвавшего его пользователя.

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

    В этом примере мы добавим защиту от несанкционированного доступа к написанному в предыдущем разделе EJB,реализующему операции с банковскими счетами. Дадим доступ к методам BankBean работникам банка, запретив его всем остальным. Соответствующую роль назовем BankEmployee. Желаемого эффекта можно добиться, добавив следующую аннотацию к объявлению класса BankBean:

    @Stateless
    @RolesAllowed("BankEmployee")
    public class BankBean implements Bank  {
    ...

    Также необходимы дополнительные настройки контейнера EJB. Мы рассмотрим настройки контейнера JBoss.

    Во-первых, необходимо обновить дескриптор развертывания приложения:

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

    Security domain - это база данных пользователей. Приложения могут пользоваться общей базой, а могут иметь свои собственные базы пользователей. Мы используем security domain, который мы назвали SecureBank.

    Во-вторых, требуется описать используемый security domain в конфигурационных файлах JBoss.Для это добавим следующие строки в файл ${jboss_home}/server/default/conf/login-config.xml:

    <application-policy name="SecureBank">
    <authentication>
    <login-module
    code="org.jboss.security.auth.spi.UsersRolesLoginModule"
    flag="required">
    <module-option name="usersProperties">
    props/bank-users.properties
    </module-option>
    <module-option name="rolesProperties">
    props/bank-roles.properties
    </module-option> </login-module> </authentication> </application-policy>

    Этот способ настройки пользователей использует два текстовых файла. Первый из них, props/bank-users.properties, задает пользователей и их пароли:

    vladykin=asdfgh

    Второй, props/bank-roles.properties, задает роли пользователей:

    vladykin=BankEmployee

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

    Hashtable<String, String> env = new Hashtable<String,   String>();
    env.put(Context.INITIAL_CONTEXT_FACTORY, "org.jboss.security.jndi.JndiLoginInitialContextFactory");
    env.put(Context.PROVIDER_URL, "localhost:1099"); 
    env.put(Context.SECURITY_PRINCIPAL, args[3]);
    env.put(Context.SECURITY_CREDENTIALS, args[4]);

    За это отвечают параметры SECURITY_PRINCIPAL и SECURITY_CREDENTIALS. Их значения извлекаются из последних параметров, переданных программе в командной строке.

    Вернуться к учебному плану