«1. Обзор

За прошедшие годы экосистема Java сильно развилась и выросла. За это время Enterprise Java Beans и Spring стали двумя технологиями, которые не только конкурировали, но и симбиотически учились друг у друга.

В этом уроке мы рассмотрим их историю и различия. Конечно, мы увидим несколько примеров кода EJB и их эквивалентов в мире Spring.

2. Краткая история технологий

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

2.1. Enterprise Java Beans

Спецификация EJB является подмножеством спецификации Java EE (или J2EE, теперь известной как Jakarta EE). Его первая версия вышла в 1999 году, и это была одна из первых технологий, призванных упростить разработку серверных корпоративных приложений на Java.

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

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

2.2. Spring

В то время как EJB (и Java EE в целом) изо всех сил пытались удовлетворить сообщество Java, Spring Framework появился как глоток свежего воздуха. Его первый этапный выпуск вышел в 2004 году и предложил альтернативу модели EJB и ее тяжеловесным контейнерам.

Благодаря Spring корпоративные приложения Java теперь можно запускать в более легких контейнерах IOC. Кроме того, он также предлагал инверсию зависимостей, поддержку AOP и Hibernate среди множества других полезных функций. Благодаря огромной поддержке со стороны сообщества Java, Spring в настоящее время вырос в геометрической прогрессии, и его можно назвать полноценным фреймворком для приложений Java/JEE.

В своем последнем воплощении Spring 5.0 даже поддерживает модель реактивного программирования. Еще одно ответвление, Spring Boot, полностью меняет правила игры благодаря встроенным серверам и автоматическим настройкам.

3. Прелюдия к сравнению функций

Прежде чем перейти к сравнению функций с примерами кода, давайте установим несколько основ.

3.1. Основное различие между ними

Во-первых, фундаментальное и очевидное различие заключается в том, что EJB — это спецификация, тогда как Spring — это целая структура.

Спецификация реализована многими серверами приложений, такими как GlassFish, IBM WebSphere и JBoss/WildFly. Это означает, что нашего решения использовать модель EJB для разработки серверной части нашего приложения недостаточно. Нам также нужно выбрать, какой сервер приложений использовать.

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

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

3.2. Полезная информация

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

Чтобы лучше понять примеры, подумайте о том, чтобы сначала прочитать Java EE Session Beans, Message Driven Beans, Spring Bean и Spring Bean Annotations.

«Мы будем использовать OpenJB в качестве встроенного контейнера для запуска образцов EJB. Для запуска большинства примеров Spring будет достаточно его контейнера IOC; для Spring JMS нам понадобится встроенный брокер ApacheMQ.

Для тестирования всех наших примеров мы будем использовать JUnit.

4. Singleton EJB == Компонент Spring

Иногда нам нужен контейнер для создания только одного экземпляра компонента. Например, предположим, что нам нужен bean-компонент для подсчета количества посетителей нашего веб-приложения. Этот bean-компонент необходимо создать только один раз во время запуска приложения.

Давайте посмотрим, как этого добиться с помощью EJB Singleton Session и компонента Spring.

4.1. Пример Singleton EJB

Сначала нам понадобится интерфейс, чтобы указать, что наш EJB может обрабатываться удаленно:

@Remote
public interface CounterEJBRemote {    
    int count();
    String getName();
    void setName(String name);
}

Следующим шагом является определение класса реализации с аннотацией javax.ejb.Singleton, и альт! Наш синглтон готов:

@Singleton
public class CounterEJB implements CounterEJBRemote {
    private int count = 1;
    private String name;

    public int count() {
        return count++;
    }
    
    // getter and setter for name
}

Но прежде чем мы сможем протестировать синглтон (или любой другой образец кода EJB), нам нужно инициализировать ejbContainer и получить контекст:

@BeforeClass
public void initializeContext() throws NamingException {
    ejbContainer = EJBContainer.createEJBContainer();
    context = ejbContainer.getContext();
    context.bind("inject", this);
}

Теперь давайте посмотрим на тест :

@Test
public void givenSingletonBean_whenCounterInvoked_thenCountIsIncremented() throws NamingException {

    int count = 0;
    CounterEJBRemote firstCounter = (CounterEJBRemote) context.lookup("java:global/ejb-beans/CounterEJB");
    firstCounter.setName("first");
        
    for (int i = 0; i < 10; i++) {
        count = firstCounter.count();
    }
        
    assertEquals(10, count);
    assertEquals("first", firstCounter.getName());

    CounterEJBRemote secondCounter = (CounterEJBRemote) context.lookup("java:global/ejb-beans/CounterEJB");

    int count2 = 0;
    for (int i = 0; i < 10; i++) {
        count2 = secondCounter.count();
    }

    assertEquals(20, count2);
    assertEquals("first", secondCounter.getName());
}

Несколько вещей, которые следует отметить в приведенном выше примере:

    Мы используем поиск JNDI, чтобы получить counterEJB из контейнера. сохраняет имя, которое мы установили для firstCounter

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

4.2. Пример Singleton Spring Bean

Ту же функциональность можно получить, используя компоненты Spring.

Здесь нам не нужно реализовывать какой-либо интерфейс. Вместо этого мы добавим аннотацию @Component:

@Component
public class CounterBean {
    // same content as in the EJB
}

Фактически, компоненты Spring по умолчанию являются синглтонами.

Нам также нужно настроить Spring для сканирования компонентов:

@Configuration
@ComponentScan(basePackages = "com.baeldung.ejbspringcomparison.spring")
public class ApplicationConfig {}

Подобно тому, как мы инициализировали контекст EJB, теперь мы установим контекст Spring:

@BeforeClass
public static void init() {
    context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
}

Теперь давайте посмотрим на наш компонент в action:

@Test
public void whenCounterInvoked_thenCountIsIncremented() throws NamingException {    
    CounterBean firstCounter = context.getBean(CounterBean.class);
    firstCounter.setName("first");
    int count = 0;
    for (int i = 0; i < 10; i++) {
        count = firstCounter.count();
    }

    assertEquals(10, count);
    assertEquals("first", firstCounter.getName());

    CounterBean secondCounter = context.getBean(CounterBean.class);
    int count2 = 0;
    for (int i = 0; i < 10; i++) {
        count2 = secondCounter.count();
    }

    assertEquals(20, count2);
    assertEquals("first", secondCounter.getName());
}

Как мы видим, единственная разница в отношении EJB заключается в том, как мы получаем bean-компонент из контекста контейнера Spring вместо поиска JNDI.

5. Stateful EJB == Компонент Spring с прототипом Scope

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

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

5.1. Пример EJB с отслеживанием состояния

Подобно нашему примеру одноэлементного EJB, нам нужен интерфейс javax.ejb.Remote и его реализация. Только на этот раз он снабжен аннотацией javax.ejb.Stateful:

@Stateful
public class ShoppingCartEJB implements ShoppingCartEJBRemote {
    private String name;
    private List<String> shoppingCart;

    public void addItem(String item) {
        shoppingCart.add(item);
    }
    // constructor, getters and setters
}

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

@Test
public void givenStatefulBean_whenBathingCartWithThreeItemsAdded_thenItemsSizeIsThree()
  throws NamingException {
    ShoppingCartEJBRemote bathingCart = (ShoppingCartEJBRemote) context.lookup(
      "java:global/ejb-beans/ShoppingCartEJB");

    bathingCart.setName("bathingCart");
    bathingCart.addItem("soap");
    bathingCart.addItem("shampoo");
    bathingCart.addItem("oil");

    assertEquals(3, bathingCart.getItems().size());
    assertEquals("bathingCart", bathingCart.getName());
}

Теперь, чтобы продемонстрировать, что компонент действительно поддерживает состояние между экземплярами, давайте добавим в этот тест еще один shoppingCartEJB:

ShoppingCartEJBRemote fruitCart = 
  (ShoppingCartEJBRemote) context.lookup("java:global/ejb-beans/ShoppingCartEJB");

fruitCart.addItem("apples");
fruitCart.addItem("oranges");

assertEquals(2, fruitCart.getItems().size());
assertNull(fruitCart.getName());

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

5.2. Пример Spring Bean с сохранением состояния

Чтобы получить тот же эффект с помощью Spring, нам нужен компонент с областью действия прототипа:

@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ShoppingCartBean {
   // same contents as in the EJB
}

Вот и все, отличаются только аннотации — остальная часть кода остается прежней.

Чтобы протестировать наш Stateful bean-компонент, мы можем использовать тот же тест, который описан для EJB. Единственная разница снова заключается в том, как мы получаем bean-компонент из контейнера:

ShoppingCartBean bathingCart = context.getBean(ShoppingCartBean.class);

6. EJB без сохранения состояния != Anything in Spring

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

6.1. Пример EJB без сохранения состояния

«Для таких сценариев у EJB есть вариант без сохранения состояния. Контейнер поддерживает пул экземпляров bean-компонентов, и любой из них возвращается вызывающему методу.

То, как мы определяем его, такое же, как и другие типы EJB, с удаленным интерфейсом и реализацией с аннотацией javax.ejb.Stateless:

@Stateless
public class FinderEJB implements FinderEJBRemote {

    private Map<String, String> alphabet;

    public FinderEJB() {
        alphabet = new HashMap<String, String>();
        alphabet.put("A", "Apple");
        // add more values in map here
    }

    public String search(String keyword) {
        return alphabet.get(keyword);
    }
}

Давайте добавим еще один простой тест, чтобы увидеть это в действии: ~~ ~

@Test
public void givenStatelessBean_whenSearchForA_thenApple() throws NamingException {
    assertEquals("Apple", alphabetFinder.search("A"));        
}

В приведенном выше примере AlphaFinder вводится как поле в тестовый класс с использованием аннотации javax.ejb.EJB: похожие бобы.

@EJB
private FinderEJBRemote alphabetFinder;

Однако Spring не поддерживает эту философию и предлагает синглтоны только как не имеющие состояния.

7. Компоненты, управляемые сообщениями == Spring JMS

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

7.1. Пример MDB

Чтобы создать Enterprise Java Bean, управляемый сообщениями, нам нужно реализовать интерфейс javax.jms.MessageListener, определяющий его метод onMessage, и аннотировать класс как javax.ejb.MessageDriven:

Обратите внимание, что мы также предоставляем пару конфигураций для нашей MDB:

@MessageDriven(activationConfig = { 
  @ActivationConfigProperty(propertyName = "destination", propertyValue = "myQueue"), 
  @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue") 
})
public class RecieverMDB implements MessageListener {

    @Resource
    private ConnectionFactory connectionFactory;

    @Resource(name = "ackQueue")
    private Queue ackQueue;

    public void onMessage(Message message) {
        try {
            TextMessage textMessage = (TextMessage) message;
            String producerPing = textMessage.getText();

            if (producerPing.equals("marco")) {
                acknowledge("polo");
            }
        } catch (JMSException e) {
            throw new IllegalStateException(e);
        }
    }
}

destinationType as Queue myQueue в качестве имени очереди назначения, которую прослушивает наш компонент

    В этом примере наш получатель также выдает подтверждение, и в этом смысле сам по себе отправитель. Он отправляет сообщение в другую очередь с именем ackQueue.

Теперь давайте проверим это в действии:

Здесь мы отправили сообщение в myQueue, которое было получено нашим POJO с аннотацией @MessageDriven. Затем этот POJO отправил подтверждение, и наш тест получил ответ как MessageConsumer.

@Test
public void givenMDB_whenMessageSent_thenAcknowledgementReceived()
  throws InterruptedException, JMSException, NamingException {
    Connection connection = connectionFactory.createConnection();
    connection.start();
    Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
    MessageProducer producer = session.createProducer(myQueue);
    producer.send(session.createTextMessage("marco"));
    MessageConsumer response = session.createConsumer(ackQueue);

    assertEquals("polo", ((TextMessage) response.receive(1000)).getText());
}

7.2. Пример Spring JMS

Что ж, теперь пришло время сделать то же самое, используя Spring!

Во-первых, нам нужно добавить немного конфигурации для этой цели. Нам нужно аннотировать наш класс ApplicationConfig с помощью @EnableJms и добавить несколько bean-компонентов для настройки JmsListenerContainerFactory и JmsTemplate:

Затем нам нужен Producer — простой компонент Spring — который будет отправлять сообщения в myQueue и получить подтверждение от ackQueue:

@EnableJms
public class ApplicationConfig {

    @Bean
    public DefaultJmsListenerContainerFactory jmsListenerContainerFactory() {
        DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory());
        return factory;
    }

    @Bean
    public ConnectionFactory connectionFactory() {
        return new ActiveMQConnectionFactory("tcp://localhost:61616");
    }

    @Bean
    public JmsTemplate jmsTemplate() {
        JmsTemplate template = new JmsTemplate(connectionFactory());
        template.setConnectionFactory(connectionFactory());
        return template;
    }
}

Затем у нас есть компонент Receiver с методом, аннотированным как @JmsListener, для асинхронного получения сообщений от myQueue:

@Component
public class Producer {
    @Autowired
    private JmsTemplate jmsTemplate;

    public void sendMessageToDefaultDestination(final String message) {
        jmsTemplate.convertAndSend("myQueue", message);
    }

    public String receiveAck() {
        return (String) jmsTemplate.receiveAndConvert("ackQueue");
    }
}

Он также действует как отправитель для подтверждения сообщения получение в ackQueue.

@Component
public class Receiver {
    @Autowired
    private JmsTemplate jmsTemplate;

    @JmsListener(destination = "myQueue")
    public void receiveMessage(String msg) {
        sendAck();
    }

    private void sendAck() {
        jmsTemplate.convertAndSend("ackQueue", "polo");
    }
}

В соответствии с нашей практикой давайте проверим это с помощью теста:

В этом тесте мы отправили marco в myQueue и получили polo в качестве подтверждения от ackQueue, так же, как мы сделали с EJB.

@Test
public void givenJMSBean_whenMessageSent_thenAcknowledgementReceived() throws NamingException {
    Producer producer = context.getBean(Producer.class);
    producer.sendMessageToDefaultDestination("marco");

    assertEquals("polo", producer.receiveAck());
}

Здесь следует отметить, что Spring JMS может отправлять/получать сообщения как синхронно, так и асинхронно.

8. Заключение

В этом руководстве мы видели сравнение Spring и Enterprise Java Beans один на один. Мы разобрались в их истории и принципиальных отличиях.

Затем мы рассмотрели простые примеры, чтобы продемонстрировать сравнение Spring Beans и EJB. Излишне говорить, что это всего лишь поверхностное представление о том, на что способны технологии, и многое еще предстоит изучить.

Более того, это могут быть конкурирующие технологии, но это не значит, что они не могут сосуществовать. Мы можем легко интегрировать EJB в структуру Spring.

Как всегда, исходный код доступен на GitHub.

«