Статьи

Подтверждение концепции: распределенное многоуровневое приложение JSF 2.0

Цель этой статьи — представить другой подход к обработке распределенных многоуровневых приложений JSF. Попробуйте предложенные шаги для создания надежных распределенных приложений, которые мгновенно обслуживают ответы, независимо от сложности запроса клиента.

Компоненты этой статьи:

1. Определение общих требований распределенных многоуровневых приложений Java

2. Архитектурные решения Java для удовлетворения требований: классические решения против предложенного доказательства концепции

3. Доказательство концепции распределенного многоуровневого приложения JSF2.0 (реализация и преимущества)

Необходимы инструменты разработки:

  • IDE (я использовал NetBeans)
  • Java SE 1.6 или выше
  • Maven 2.0 или выше
  • для локального тестирования кода у вас должен быть установлен сервер приложений. Я использовал Oracle Glassfish 4.0, но вы можете выбрать любой другой сервер.
  • в качестве СУБД я использовал Oracle 11g Enterprise Edition (я использовал два разных экземпляра базы данных, установленных локально)

1. Определение общих требований распределенных многоуровневых приложений Java

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

  • Решение больших проблем без обращения к более крупным компьютерным системам может быть выполнено путем параллельного вычисления задач, разбив процесс на более мелкие части. Вместо сложных аппаратных систем вы можете использовать более мелкие, более дешевые и доступные для поиска аппаратные компоненты.
  • Большие наборы данных трудно переместить, но их проще контролировать и администрировать там, где они хранятся. Пользователи полагаются на удаленные серверы данных для предоставления необходимой информации.
  • В случае необходимости в механизмах обработки ошибок системы могут использовать избыточные компоненты приложения обработки на нескольких сетевых компьютерах. Таким образом, если процесс машины или агента выходит из строя, задача, которую машина выполняла, все еще может выполняться.

Разработка Java-распределенных приложений требует следующего:

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

2. Архитектурные решения Java для удовлетворения требований: классическое решение против предложенного доказательства концепции

Платформа Java EE использует модель распределенных многоуровневых приложений для корпоративных приложений. Логистика приложения делится на компоненты в зависимости от функции, а компоненты приложения, составляющие приложение Java EE, устанавливаются на различных компьютерах в зависимости от уровня в многоуровневой среде Java EE, к которой принадлежит компонент приложения.

Классическое решение архитектуры Java для распределенного многоуровневого приложения аналогично следующей схеме:

  •  Компоненты клиентского уровня, которые работают на клиентском компьютере. Клиент приложения работает на клиентском компьютере и предоставляет пользователям возможность обрабатывать задачи, для которых требуется более богатый пользовательский интерфейс, чем может обеспечить язык разметки.
  •  Компоненты веб-уровня, работающие на сервере Java EE. Веб-компоненты Java EE — это сервлеты или веб-страницы, созданные с использованием технологии Java Server Faces и / или технологии JSP (страницы JSP).
  • Компоненты бизнес-уровня, работающие на сервере Java EE. Бизнес-код, который представляет собой логику, которая решает или удовлетворяет потребности конкретного бизнес-домена, такого как банковское дело, розничная торговля или финансы, обрабатывается корпоративными компонентами, работающими либо на бизнес-уровне, либо на веб-уровне.
  • Корпоративная информационная система (EIS) — более поздняя версия программного обеспечения, работающего на сервере EIS. Уровень информационной системы предприятия обрабатывает программное обеспечение EIS и включает в себя системы инфраструктуры предприятия, такие как планирование ресурсов предприятия (ERP), обработка транзакций мэйнфреймов, системы баз данных и другие устаревшие информационные системы. Например, компонентам приложения Java EE может потребоваться доступ к корпоративным информационным системам для подключения к базе данных.

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

Как обсуждалось ранее, эта реализация также имеет:

  • Компоненты клиентского уровня, которые работают на клиентском компьютере.
  • Компоненты веб-уровня (стороны сервера Java), которые работают на сервере Java EE.
  • Компоненты бизнес-уровня (объекты персистентности Java, сессионные компоненты, управляемые сообщениями компоненты), которые работают на сервере Java EE.
  • Корпоративная информационная система (EIS) — более раннее программное обеспечение, которое работает на сервере EIS

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

3. Доказательство концепции распределенного многоуровневого приложения JSF2.0 (реализация и преимущества)

Давайте представим следующий сценарий: у нас есть приложение интернет-магазина; В приложении есть таблицы, относящиеся к деятельности из Румынии, в базе данных SHOP_RO , а деятельность из Бельгии хранится в SHOP_BE . Общие действия хранятся в базе данных SHOP_RO (таблицы типа PRODUCTS, PAYMENT, ADDRESSES являются общими).

Чтобы упростить взаимодействие с базами данных SHOP_BE и SHOP_RO, мы будем использовать частные ссылки. Ссылка базы данных  является объектом схемы в одной базе данных , которая позволяет осуществлять доступ к объектам в другой базе данных (в нашем случае другой база данных также является система база данных Oracle). Ссылка на базу данных является однонаправленной и облегчает связь между системами баз данных; ссылки на базу данных могут использоваться для передачи данных между приложениями.  

Перед выполнением команд sql для создания ссылок на базы данных мы должны создать необходимые службы баз данных для каждой из наших баз данных. Например, служба db_shop_ro будет создана с помощью приложения Net Configuration Assistant .

Нажмите Далее (Продолжить).

Продолжайте нажимать «Далее» (Continuare) и тестировать службу, используя пользователя базы данных. В моем случае пользователь AMMBRA существует в обеих базах данных, и я буду использовать его для тестирования службы.

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

--execute this on database SHOP_BE  
CREATE PUBLIC DATABASE LINK "SHOP_RO"
  CONNECT TO "AMMBRA" IDENTIFIED BY 'Ammbra2013'
  Using 'db_shop_ro';

 --for testing 
   SELECT USER FROM DUAL@shop_ro;



--execute this on database SHOP_RO  
  CREATE PUBLIC DATABASE LINK "SHOP_BE"
   CONNECT TO "AMMBRA" IDENTIFIED BY 'Ammbra2013'
   USING 'db_shop_be';
--for testing purpose 
   SELECT USER FROM DUAL@shop_be;

База данных SHOP_RO будет служить источником сохранения для нашего приложения на основе JSF. Наши прикладные уровни будут следующими:

Давайте рассмотрим, как приложение может работать с двумя заданными базами данных; для входа в систему нам потребуется проверить данные пользователя по двум таблицам USER_ACCOUNT_BE и USER_ACCOUNT_RO, которые существуют в связанных базах данных. Чтобы обеспечить прозрачность данных для пользователей, мы создадим следующее представление (которое обеспечивает общую прозрачность локализованных данных):

CREATE OR REPLACE VIEW AMMBRA.User_Account_All
AS
SELECT * FROM Ammbra.User_Account_Ro
UNION ALL
SELECT * FROM Ammbra.User_Account_Be@shop_be;

Когда пользователь войдет в наше приложение, сущность UserAccountAll предложит доступ к ранее созданному представлению USER_ACCOUNT_ALL. 

@Entity
@Table(name = "USER_ACCOUNT_ALL", catalog = "", schema = "AMMBRA")
@XmlRootElement
@NamedQueries({
    @NamedQuery(name = "UserAccountAll.findAll", query = "SELECT c FROM UserAccountAll c"),
    @NamedQuery(name = "UserAccountAll.findByAlias", query = "SELECT c FROM UserAccountAll c WHERE c.alias = :alias"),
    @NamedQuery(name = "UserAccountAll.findByPassword", query = "SELECT c FROM UserAccountAll c WHERE c.password = :password"),
    @NamedQuery(name = "UserAccountAll.findByName", query = "SELECT c FROM UserAccountAll c WHERE c.NAME = :NAME"),
    @NamedQuery(name = "UserAccountAll.findBySurname", query = "SELECT c FROM UserAccountAll c WHERE c.surname = :surname"),
    @NamedQuery(name = "UserAccountAll.findByDateOfBirth", query = "SELECT c FROM UserAccountAll c WHERE c.dateOfBirth = :dateOfBirth"),
    @NamedQuery(name = "UserAccountAll.findByCnp", query = "SELECT c FROM UserAccountAll c WHERE c.cnp = :cnp"),
    @NamedQuery(name = "UserAccountAll.findByEmail", query = "SELECT c FROM UserAccountAll c WHERE c.email = :email"),
    @NamedQuery(name = "UserAccountAll.findByPhone", query = "SELECT c FROM UserAccountAll c WHERE c.phone = :phone"),
    @NamedQuery(name = "UserAccountAll.findByCountry", query = "SELECT c FROM UserAccountAll c WHERE c.country = :country")})
public class UserAccountAll implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @Size(max = 25)
    @Column(name = "ALIAS", length = 25)
    private String alias;
    @Size(max = 25)
    @Column(name = "PASSWORD", length = 25)
    private String password;
    @Size(max = 100)
    @Column(name = "NAME", length = 100)
    private String name;
    @Size(max = 100)
    @Column(name = "SURNAME", length = 100)
    private String surname;
    @Column(name = "DATE_OF_BIRTH")
    @Temporal(TemporalType.TIMESTAMP)
    private Date dateOfBirth;
    @Size(max = 13)
    @Column(name = "CNP", length = 13)
    private String cnp;
    // @Pattern(regexp="[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", message="Invalid email")//if the field contains email address consider using this annotation to enforce field validation
    @Size(max = 100)
    @Column(name = "EMAIL", length = 100)
    private String email;
    @Size(max = 30)
    @Column(name = "PHONE", length = 30)
    private String phone;
    @Size(max = 25)
    @Column(name = "COUNTRY", length = 25)
    private String country;

    public UserAccountAll() {
    }

    public UserAccountAll(String alias) {
        this.alias = alias;
    }

    public String getAlias() {
        return alias;
    }

    public void setAlias(String alias) {
        this.alias = alias;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getName() {
        return name;
    }

    public void setNamr(String name) {
        this.name = name;
    }

    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }

    public Date getDateOfBirth() {
        return dateOfBirth;
    }

    public void setDateOfBirth(Date dateOfBirth) {
        this.dateOfBirth = dateOfBirth;
    }

    public String getCnp() {
        return cnp;
    }

    public void setCnp(String cnp) {
        this.cnp = cnp;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getCountry() {
        return country;
    }

    public void setCountry(String country) {
        this.country = country;
    }

    @Override
    public int hashCode() {
        int hash = 0;
        hash += (alias != null ? alias.hashCode() : 0);
        return hash;
    }

    @Override
    public boolean equals(Object object) {
        // TODO: Warning - this method won't work in the case the id fields are not set
        if (!(object instanceof UserAccountAll)) {
            return false;
        }
        UserAccountAll other = (UserAccountAll) object;
        if ((this.alias == null && other.alias != null) || (this.alias != null && !this.alias.equals(other.alias))) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "com.tutorial.ammbrashop.UserAccountAll[ alias=" + alias + " ]";
    }
    
}

Объект будет управляться классом UserAccountAllFacade

@Stateless
public class UserAccountAllFacade extends AbstractFacade< UserAccountAll> {
    @PersistenceContext(unitName = "com.tutorial_AmmbraShop_war_1.0-SNAPSHOTPU")
    private EntityManager em;

    @Override
    protected EntityManager getEntityManager() {
        return em;
    }

    public UserAccountAllFacade() {
        super(UserAccountAll.class);
    }
    
}

Приведенный выше класс расширяет поведение класса AbstractFacade:

public abstract class AbstractFacade<T> {

    private Class<T> entityClass;

    public AbstractFacade(Class<T> entityClass) {
        this.entityClass = entityClass;
    }

    protected abstract EntityManager getEntityManager();

    public void create(T entity) {
        getEntityManager().persist(entity);
    }

    public void edit(T entity) {
        getEntityManager().merge(entity);
    }

    public void remove(T entity) {
        getEntityManager().remove(getEntityManager().merge(entity));
    }

    public T find(Object id) {
        return getEntityManager().find(entityClass, id);
    }

    @SuppressWarnings("unchecked")
    public T findOne(String query, Object... params) {
        return (T) getNamedQueryWith(query, params).getSingleResult();
    }

    public List<T> findAll() {
        javax.persistence.criteria.CriteriaQuery cq = getEntityManager().getCriteriaBuilder().createQuery();
        cq.select(cq.from(entityClass));
        return getEntityManager().createQuery(cq).getResultList();
    }

    public List<T> findRange(int[] range) {
        javax.persistence.criteria.CriteriaQuery cq = getEntityManager().getCriteriaBuilder().createQuery();
        cq.select(cq.from(entityClass));
        javax.persistence.Query q = getEntityManager().createQuery(cq);
        q.setMaxResults(range[1] - range[0] + 1);
        q.setFirstResult(range[0]);
        return q.getResultList();
    }

    public int count() {
        javax.persistence.criteria.CriteriaQuery cq = getEntityManager().getCriteriaBuilder().createQuery();
        javax.persistence.criteria.Root<T> rt = cq.from(entityClass);
        cq.select(getEntityManager().getCriteriaBuilder().count(rt));
        javax.persistence.Query q = getEntityManager().createQuery(cq);
        return ((Long) q.getSingleResult()).intValue();
    }

}

Класс UserAccountAllFacade внедряется внутри класса LoginController:

@ManagedBean(name="loginController")
@SessionScoped
public class LoginController implements Serializable {

    private static final long serialVersionUID = 7765876811740798583L;
    @EJB
    private com.tutorial.ammbrashop.login.control.UserAccountAllFacade ejbFacade;

    private String alias;
    private String password;
    private UserAccountAll user;
    
    private boolean loggedIn;


    /**
     * Login operation.
     *
     * @return
     */
    public String doLogin() {
        
        user = ejbFacade.find(alias);
        // Successful login
        if (user != null && user.getPassword().equals(password)) {
            loggedIn = true;
            return "index";
        } else if (user != null && !user.getPassword().equals(password)) {
            // Set login ERROR
            FacesMessage msg = new FacesMessage("password is invalid. login not allowed", "ERROR MSG");
            msg.setSeverity(FacesMessage.SEVERITY_ERROR);
            FacesContext.getCurrentInstance().addMessage(null, msg);
        } else {
            // Set login ERROR
            FacesMessage msg = new FacesMessage("Invalid alias! ", "ERROR MSG");
            msg.setSeverity(FacesMessage.SEVERITY_ERROR);
            FacesContext.getCurrentInstance().addMessage(null, msg);
        }
        // To to login page
        return "login";

    }


    // ------------------------------
    // Getters & Setters 
    public String getAlias() {
        return alias;
    }

    public void setAlias(String alias) {
        this.alias = alias;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String Password) {
        this.password = password;
    }
    public UserAccountAll getUser() {
        return user;
    }

    public void setUser(UserAccountAll user) {
        this.user = user;
    }
    public boolean isLoggedIn() {
        return loggedIn;
    }

    public void setLoggedIn(boolean loggedIn) {
        this.loggedIn = loggedIn;
    }


}

Как будут обрабатываться операции вставки / обновления / удаления?

Когда должна быть выполнена операция вставки / обновления / удаления, мы не можем использовать представление. Давайте рассмотрим следующий сценарий: мы должны вставить нового пользователя, для которого в качестве страны учетной записи установлено значение Бельгия. Чтобы вставить его в таблицу SHOP_BE.USER_ACCOUNT_BE, мы должны выполнить запрос:

Insert into USER_ACCOUNT_BE@SHOP_BE (ALIAS,PASSWORD,NAME,SURNAME,DATE_OF_BIRTH,CNP,EMAIL,PHONE, COUNTRY) values ('anamih','mar10mih','Mihalceanu','Ana-Maria',to_date('15-AUG-86','DD-MON-RR'),'28608156889432','[email protected]','0744666311', 'Belgium'); 		

Код Java класса UserAccountAllFacade должен быть изменен путем переопределения метода create:

@Override
    public void create(UserAccountAll entity) {
        //super.create(entity); 

        if (entity.getCountry().equals("Romania")) {
            Query q = em.createNativeQuery("Insert into USER_ACCOUNT_RO (ALIAS,PASSWORD,NAME,SURNAME,DATE_OF_BIRTH,CNP,EMAIL,PHONE, COUNTRY) values ("
                    + entity.getAlias() + ",'" + entity.getPassword() + "','" + entity.getName() + "','" + entity.getSurname() + "','" + entity.getDateOfBirth() + "','" + entity.getCnp() + "','" + entity.getEmail()+ "','" + entity.getPhone()+ "','" + entity.getCountry() + "')");
            q.executeUpdate();
        } else {
            Query q = em.createNativeQuery("Insert into USER_ACCOUNT_BE@SHOP_BE (ALIAS,PASSWORD,NAME,SURNAME,DATE_OF_BIRTH,CNP,EMAIL,PHONE, COUNTRY) values ("
                    + entity.getAlias() + ",'" + entity.getPassword() + "','" + entity.getName() + "','" + entity.getSurname() + "','" + entity.getDateOfBirth() + "','" + entity.getCnp() + "','" + entity.getEmail()+ "','" + entity.getPhone()+ "','" + entity.getCountry() + "')");
            q.executeUpdate();
        }
    }

Какие преимущества вводит подход, как представлено?

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

Разработка кода приложения, как в представленном выше цикле, дает следующие преимущества:

  • данные распределяются между базами данных, а запросы распределяются между двумя (или более) базами данных; рабочая нагрузка базы данных сбалансирована между двумя базами данных.
  • фреймворки не создают никаких накладных расходов, потому что используются основные технологии Java
  • многопоточность
  • протоколы связи между базами данных являются гибкими; на основе существующих служб баз данных может быть создано большое количество ссылок на базы данных.
  • безопасность достигается с помощью методов проверки JSF (библиотека тегов JSF2.0), а также проверки контроллера действий.

Узнайте больше от: