Pull to refresh

Spring Framework без XML… совсем!

Reading time15 min
Views53K
Original author: Tomek Nurkiewicz
В свете нынешней эпохи определения всего и вся аннотациями предлагаю вам статью о Spring Framework и возможностях аннотирования проектов. Прим. перев.
В начале был EJB 2.1, с его огромным количеством XML-файлов везде где только можно. Не будет особым преувеличением, если сказать, что на одну строку кода для бизнес-логики нужно было написать по крайней мере 10 строк кода от фреймворка и две страницы XML. Локальные и удалённые интерфейсы, ручной JNDI-lookup, многоуровневые try-catch, проверки на RemoteException… enterprise, в-общем. Даже инструменты соответствующие были для автоматической генерации всей этой «кухни».

Потом пара ребят создали Spring framework. После кастов непонятным PortableRemoteObject.narrow() это стало глотком свежего воздуха. Прошло время (кстати, помнит кто-то, сколько лет назад был последний major-релиз JRE?) и Sun осознали урок. EJB 3.0 был даже проще Spring, XML-free, с аннотациями, dependency injection. 3.1 стал ещё одним огромным шагом в сторону упрощения. По логике, EJB сейчас можно рассматривать как часть того, что предлагает Spring, и я весьма удивлён, почему нет реализации EJB в plain Spring (ой, погодите...), учитывая его поддержку из-коробки JPA 1.0/2.0, JSR-250, JSR-330, JAX-WS/RS и прочего. Spring framework сегодня воспринимается как медленный, тяжёлый и сложный для поддержки фреймворк, в-основном из-за XML-дескрипторов. В-общем, Spring в противостоянии JEE-фреймворков как мальчик для битья.

Я не люблю политику и не защищаю любимый фреймворк написанием длинных сочинений. Вместо этого я возьму простое, но не тривиальное Spring-приложение и перепишу его без XML. Совсем без XML, без единой строчки.
Для статьи я написал очень простое веб-приложение на Spring (обычная версия в ветке xml, конечная в master) с JDBC, JMS и JMX, просто чтобы не упрощать задачу донельзя. Каждое изменение будет отражено в отдельном коммите в репозиторий, и, шаг за шагом, я буду избавляться от XML до тех пор, пока его не останется совсем. Начнём.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:amq="http://activemq.apache.org/schema/core"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
            http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
             http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core/activemq-core-5.4.2.xsd
             http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:mbean-export />

    <bean id="fooService" class="com.blogspot.nurkiewicz.FooService">
        <property name="jmsOperations" ref="jmsTemplate" />
    </bean>

    <bean id="fooRequestProcessor" class="com.blogspot.nurkiewicz.FooRequestProcessor">
        <property name="fooRepository" ref="fooRepository" />
    </bean>

    <bean id="fooRepository" class="com.blogspot.nurkiewicz.FooRepository" init-method="init">
        <property name="jdbcOperations" ref="jdbcTemplate" />
    </bean>


    <!-- JDBC -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="org.h2.Driver" />
        <property name="url" value="jdbc:h2:~/workspace/h2/spring-noxmal;DB_CLOSE_ON_EXIT=FALSE;TRACE_LEVEL_FILE=4;AUTO_SERVER=TRUE" />
        <property name="username" value="sa" />
        <property name="password" value="" />
    </bean>

    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <constructor-arg ref="dataSource" />
    </bean>

    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <constructor-arg ref="dataSource" />
    </bean>

    <tx:annotation-driven />


    <!-- JMS -->
    <bean id="jmsConnectionFactory" class="org.apache.activemq.pool.PooledConnectionFactory">
        <constructor-arg>
            <bean class="org.apache.activemq.ActiveMQConnectionFactory">
                <property name="brokerURL" value="tcp://localhost:61616" />
            </bean>
        </constructor-arg>
    </bean>

    <amq:queue id="requestsQueue" physicalName="requests" />

    <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
        <constructor-arg ref="jmsConnectionFactory" />
        <property name="defaultDestination" ref="requestsQueue" />
    </bean>

    <bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
        <property name="connectionFactory" ref="jmsConnectionFactory" />
        <property name="destination" ref="requestsQueue" />
        <property name="sessionTransacted" value="true"/>
        <property name="concurrentConsumers" value="5"/>
        <property name="messageListener">
            <bean class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
                <constructor-arg ref="fooRequestProcessor" />
                <property name="defaultListenerMethod" value="process"/>
            </bean>
        </property>
    </bean>

</beans>

Пара user-бинов, JDBS с транзакциями и JMS, отправка и принятие. Подробности не особо важны – один из бинов через JMX, посылает JMS-сообщение, сообщение принимается и ложится (persisted) в базу.

Самый распространённый и устоявшийся подход к уменьшению XML-слоя в Spring заключается в использовании аннотаций Service и Resource вместе с <context:component-scan/> для user-бинов (изменения):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:amq="http://activemq.apache.org/schema/core"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
            http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
             http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core/activemq-core-5.4.2.xsd
             http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:mbean-export />

    <context:component-scan base-package="com.blogspot.nurkiewicz"/>

    <!-- JDBC -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="org.h2.Driver" />
        <property name="url" value="jdbc:h2:~/workspace/h2/spring-noxmal;DB_CLOSE_ON_EXIT=FALSE;TRACE_LEVEL_FILE=4;AUTO_SERVER=TRUE" />
        <property name="username" value="sa" />
        <property name="password" value="" />
    </bean>

    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <constructor-arg ref="dataSource" />
    </bean>

    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <constructor-arg ref="dataSource" />
    </bean>

    <tx:annotation-driven />


    <!-- JMS -->
    <bean id="jmsConnectionFactory" class="org.apache.activemq.pool.PooledConnectionFactory">
        <constructor-arg>
            <bean class="org.apache.activemq.ActiveMQConnectionFactory">
                <property name="brokerURL" value="tcp://localhost:61616" />
            </bean>
        </constructor-arg>
    </bean>

    <amq:queue id="requestsQueue" physicalName="requests" />

    <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
        <constructor-arg ref="jmsConnectionFactory" />
        <property name="defaultDestination" ref="requestsQueue" />
    </bean>

    <bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
        <property name="connectionFactory" ref="jmsConnectionFactory" />
        <property name="destination" ref="requestsQueue" />
        <property name="sessionTransacted" value="true"/>
        <property name="concurrentConsumers" value="5"/>
        <property name="messageListener">
            <bean class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
                <constructor-arg ref="fooRequestProcessor" />
                <property name="defaultListenerMethod" value="process"/>
            </bean>
        </property>
    </bean>

</beans>

Минус 10 строчек XML, ничего особенного… А user-бины?
@Service
public class FooRepository {

    @Resource
    private JdbcOperations jdbcOperations;

    @PostConstruct
    public void init() {
        log.info("Database server time is: {}", jdbcOperations.queryForObject("SELECT CURRENT_TIMESTAMP", Date.class));
    }
    
    //...
    
}

Сеттеры и init-методы заменены на аннотации. Ну и что дальше? Большинство людей-кто-любит-аннотации останавливаются на достигнутом, но, как вы видите, осталось много XML. Вопрос теперь в том, как использовать аннотации в случае с third-party вещами типа connection pool-ов и др.?
Тут-то и начинается веселье. Для начала мы должны избавиться от data source XML и заменить всё это на… (изменения)
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ContextConfiguration {

    @Bean
    public DataSource dataSource() {
        final BasicDataSource ds = new BasicDataSource();
        ds.setDriverClassName("org.h2.Driver");
        ds.setUrl("jdbc:h2:~/workspace/h2/spring-noxmal;DB_CLOSE_ON_EXIT=FALSE;TRACE_LEVEL_FILE=4;AUTO_SERVER=TRUE");
        ds.setUsername("sa");
        return ds;
    }

}

@Configuration, Bean, dataSource(), что это...?!? Но работает как раз, как вы и подумали: Spring находит класс ContextConfiguration и все методы, аннотированные как Bean. Каждый такой метод эквивалентен XML-евскому <bean...> (здесь даже Scope, @DependsOn и Lazy), поэтому мы можем убрать XML-объявление бина dataSource. Хотя, вообще-то, можно избавиться и от JdbcTemplate и менеджера транзакций (изменения):
@Bean
public JdbcOperations jdbcOperations() {
    return new JdbcTemplate(dataSource());
}

@Bean
public PlatformTransactionManager transactionManager() {
    return new DataSourceTransactionManager(dataSource());
}

Обратите внимание на то, как легко можно заинжектить data source-бин в другие бины. У вас есть метод, создающий data source, и у вас есть два метода, которым этот data source нужен (JdbcTemplate и менеджер транзакций). Легче не бывает, и, возможно, ваша подружка бы так и реализовала dependency injection (эм, Guice?)

Одно должно обеспокоить вас сейчас… Если вызывать dataSource() дважды, не значит ли это, что создаётся два независимых экземпляра DataSource? Это совсем не то, чего бы нам хотелось. Это меня озадачивало, но в очередной раз Spring оказался умным зверьком. Не найдя аннотации Scope, он начинает считать, что data source должен быть singleton-ом и, применив немного CGLIB-proxying-магии на методе dataSource(), ограничивает количество вызовов одним. Ну, или если поконкретнее, то вызывать-то можно много раз, но возвратиться один и тот же бин. Круто!
В итоге XML-конфигурация сокращена до:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:amq="http://activemq.apache.org/schema/core"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
            http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
             http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core/activemq-core-5.4.2.xsd
             http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:mbean-export />

    <context:component-scan base-package="com.blogspot.nurkiewicz"/>

    <!-- JDBC -->
    <tx:annotation-driven />

    <!-- JMS -->
    <bean id="jmsConnectionFactory" class="org.apache.activemq.pool.PooledConnectionFactory">
        <constructor-arg>
            <bean class="org.apache.activemq.ActiveMQConnectionFactory">
                <property name="brokerURL" value="tcp://localhost:61616" />
            </bean>
        </constructor-arg>
    </bean>

    <amq:queue id="requestsQueue" physicalName="requests" />

    <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
        <constructor-arg ref="jmsConnectionFactory" />
        <property name="defaultDestination" ref="requestsQueue" />
    </bean>

    <bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
        <property name="connectionFactory" ref="jmsConnectionFactory" />
        <property name="destination" ref="requestsQueue" />
        <property name="sessionTransacted" value="true"/>
        <property name="concurrentConsumers" value="5"/>
        <property name="messageListener">
            <bean class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
                <constructor-arg ref="fooRequestProcessor" />
                <property name="defaultListenerMethod" value="process"/>
            </bean>
        </property>
    </bean>

</beans>


Теперь можно остановиться и подумать, как переписать оставшиеся в конфигурации бины. Это не стоит того, всё прозрачно (изменения).
@Bean
public ConnectionFactory jmsConnectionFactory() {
    final ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory();
    factory.setBrokerURL("tcp://localhost:61616");
    return new PooledConnectionFactory(factory);
}

@Bean
public Queue requestsQueue() {
    return new ActiveMQQueue("requests");
}

@Bean
public JmsOperations jmsOperations() {
    final JmsTemplate jmsTemplate = new JmsTemplate(jmsConnectionFactory());
    jmsTemplate.setDefaultDestination(requestsQueue());
    return jmsTemplate;
}

Объявление DefaultMessageListenerContainer содержит anonymous inner бины, использующиеся один раз внутри родительского бина. Private-метод в порядке (изменения):
@Bean
public AbstractJmsListeningContainer jmsContainer() {
    final DefaultMessageListenerContainer container = new DefaultMessageListenerContainer();
    container.setConnectionFactory(jmsConnectionFactory());
    container.setDestination(requestsQueue());
    container.setSessionTransacted(true);
    container.setConcurrentConsumers(5);
    container.setMessageListener(messageListenerAdapter());
    return container;
}

private MessageListenerAdapter messageListenerAdapter() {
    final MessageListenerAdapter adapter = new MessageListenerAdapter(fooRequestProcessor);
    adapter.setDefaultListenerMethod("process");
    return adapter;
}


Нечего особо комментировать из-за того, что plain Java-конфигурация в Spring тривиальна и незатейлива – код говорит сам за себя. На тот случай, если вы запутались, мы пришли вот к этому::

<?xml version=«1.0» encoding=«UTF-8»?>
<beans xmlns=«www.springframework.org/schema/beans»
xmlns:xsi=«www.w3.org/2001/XMLSchema-instance»
xmlns:tx=«www.springframework.org/schema/tx»
xmlns:context=«www.springframework.org/schema/context»
xsi:schemaLocation=«www.springframework.org/schema/beans www.springframework.org/schema/beans/spring-beans-3.0.xsd
www.springframework.org/schema/tx www.springframework.org/schema/tx/spring-tx-2.5.xsd
www.springframework.org/schema/context www.springframework.org/schema/context/spring-context.xsd»>

<context:mbean-export />

<context:component-scan base-package=«com.blogspot.nurkiewicz»/>

<tx:annotation-driven />



Честно говоря, все это не было сильно сложно, но вот от последних нескольких строк XML было реально сложно избавиться. Поверьте – вы не хотите делать то, что я делал, чтобы сладить с этими красивыми маленькими namespace-powered объявлениями. Но после пары минут, пары неудачных опытов и кучи просмотренного Spring-кода я наконец убрал объявления JMX (изменения) и транзакций (изменения). Выглядит невинно и я доволен, что вам не пришлось копаться в Spring-коде, чтобы самим дойти до этого:
@Bean
public AnnotationMBeanExporter annotationMBeanExporter() {
    return new AnnotationMBeanExporter();
}

@Bean
public TransactionAttributeSource annotationTransactionAttributeSource() {
    return new AnnotationTransactionAttributeSource();
}

@Bean
public TransactionInterceptor transactionInterceptor() {
    return new TransactionInterceptor(transactionManager(), annotationTransactionAttributeSource());
}

Вот так. Осталось убрать XML-инструкции для Spring относительно местонахождения бинов и сниппета web.xml:
<context:component-scan base-package="com.blogspot.nurkiewicz"/>

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

</web-app>

Другого способа нормально запустить Spring в web-среде нет, таки надо указать web-контейнеру, что нужен Spring. Знаю, знаю, я обещал, что совсем не будет XML, ну соврал, и что теперь… простите, ОК? Делов-то… Ага, шучу.  Давайте избавимся и от этого. Пара секунд! Ну… хотя зависит от того, как быстро вы скачаете новейший Tomcat 7 или другой поддерживающий JSR 315 (известный как Servlet 3.0) web-контейнер…

Web-фрагменты – технология, позволяющая деликатно интегрировать разные web-фреймворки в сервлет-контейнеры. Если у вас есть опыт работы с фреймворками, то вы знаете, что все они нуждаются в описании специального сервлета, фильтра или listener-а в web.xml. В большинстве случаев это единственный раз, когда возникает servlet dependency и Spring не исключение. Суть web-фрагментов в том, что они пытаются освободить разработчиков от этого. Servlet 3.0-контейнеры должны просканировать все JAR-ы в /WEB-INF/lib и, если какой-нибудь из JAR содержит web-fragment.xml в /META-INF, то он будет зарегистрирован в web.xml.

Поняли, куда веду? Что, если мы создадим маленький JAR с маленьким web-фрагментом только ради того, чтобы запустить Spring без XML? WAR-структура:
.
|-- META-INF
`-- WEB-INF
    |-- classes
    |   |-- com
    |   |   `-- blogspot
    |   |       `-- nurkiewicz
    |   |           |-- ContextConfiguration.class
    |   |           |-- FooRepository.class
    |   |           |-- FooRequestProcessor.class
    |   |           |-- FooService$1.class
    |   |           `-- FooService.class
    |   `-- logback.xml
    |-- lib
    |   |-- spring-web-3.0.5.RELEASE.jar
    |   |-- spring-web-fragment-0.0.1-SNAPSHOT.jar
    |   |   `-- META-INF
    |   |       |-- MANIFEST.MF
    |   |       |-- web-fragment-context.xml
    |   |       `-- web-fragment.xml
    |   `-- spring-beans-3.0.5.RELEASE.jar
    `-- web.xml

Единственное назначение spring-web-fragment-*.jar в предоставлении web-fragment.xml контейнеру:
<web-fragment xmlns="http://java.sun.com/xml/ns/javaee"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-fragment_3_0.xsd"
              version="3.0">

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:/META-INF/web-fragment-context.xml</param-value>
    </context-param>

</web-fragment>

Один новый элемент — web-fragment-context.xml Spring context file. Мы не можем использовать то, что идёт по умолчанию (/WEB-INF/applicationContext.xml), потому что этого файла больше нет (!). Но наш маленький JAR выглядит как если бы он был лучшим местом для этого:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
		   http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
		   http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
       ">

    <context:component-scan base-package="." />

</beans>

Объявление пакета в виде "." вызывает настороженность. Это очень печально, но я пробовал обойти требование в объявлении хотя бы одного пакета. Это требование, возможно, и обосновано (думаю, сканирование CLASSPATH занимает определенное время), но я не мог просто указать мой пакет, иначе я должен был бы менять это объявление для каждого нового проекта. Но это идёт вразрез с главным преимуществом подхода с web-фрагментами – при создании JAR-а с двумя маленькими XML-файлами его можно использовать для всех проектов. Всё, что надо, это включить его в список библиотек вашего WAR-а и начать аннотировать POJO как Service (и/или использовать @Configuration).

Если это будет доступно среди того, что предлагает из-коробки Spring (если нравится идея, голосуйте), то новички смогут насладиться путешествием в Spring сразу же как добавят Spring в pom.xml. По факту pom.xml можно писать на различных языках, как и logback.xml. Эй, без XML! Вы уверены? Вам больше нравится XML или Java? Или, может быть, Groovy? Пожалуйста, молчите. Spring предоставляет вам возможность быть настолько простым и незатейливым в использовании насколько вы желаете. Без того, чтобы быть упрощённым настолько, что будет обрезана функциональность.
Tags:
Hubs:
+29
Comments92

Articles

Change theme settings