Перейти к основному содержимому

Spring Bean и EJB — сравнение функций

· 12 мин. чтения

1. Обзор

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

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

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

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

2.1. Корпоративные Java-бины

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

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

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

2.2. Весна

В то время как 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 Component .

4.1. Пример одноэлементного компонента 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 из контейнера.
  • count2 берется из количества точек , на котором остался синглтон, и в сумме дает 20
  • secondCounter сохраняет имя, которое мы установили для 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.foreach.ejbspringcomparison.spring")
public class ApplicationConfig {}

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

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

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

@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 Component с прототипом 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, мы можем использовать тот же тест, который описан для EJB. Единственная разница снова в том, как мы получаем bean-компонент из контейнера:

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

6. EJB без сохранения состояния! = Что угодно в 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;

Основная идея EJB-компонентов без сохранения состояния заключается в повышении производительности за счет создания пула экземпляров похожих bean-компонентов.

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

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

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

7.1. Пример MDB

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

@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);
}
}
}

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

  • Место назначенияТип как Очередь

  • myQueue в качестве имени очереди назначения , которую прослушивает наш bean-компонент .

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

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

@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());
}

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

7.2. Пример Spring JMS

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

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

@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;
}
}

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

@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");
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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