my.life.logging.Blog

Создание портлета для Liferay с помощью Wicket. Часть 4. Если используется Spring Framework.

Данное руководство детально описывает процесс создания портлета для Liferay портала cредствами фреймворка Wicket.

Все части руководства:

  1. Портлетный проект для Liferay портала
  2. Конфигурация Wicket-портлета, или web.xml, portlet.xml и т.д., и т.п.
  3. Разработка функциональности Wicket-портлета
  4. Если используется Spring Framework

Исходный код разрабатываемого примера доступен на GitHub.


“Весеннее” достижение

Если ты, уважаемый читатель, хорошо знаком с программистским фольклором и до сих пор наивно полагаешь, что Java – это “язык индустриальной эпохи”, и в его экосистеме не осталось такого места, которое не было бы загрязнено выбросами “фабрик”, то боюсь, конечно, тебя разочаровывать, но, по всей видимости, ты безнадежно отстал от жизни. Ибо с середины 2000х годов Java – это все-таки скорее и чаще Spring Framework, “Dependency Injection” и все, что с этим связано.

Одной из частей рассмативаемого нами в этом руководстве фреймворка Wicket является модуль, который занимается его интеграцией со Spring. Таким образом, если требуется применить IoC, вынести какую-либо конфигурацию в XML, настроить авторизацию с помощью Spring Security, воспользоваться AOP или, страшно сказать, задействовать в проекте мессаджинг с помощью JMS – Wicket не станет помехой для реализации всех этих вещей с помощью Spring Framework. На самом деле, Wicket просто выступит в качестве представления для всей остальной логики приложения, большая часть которой может быть написана как раз с использованием Spring.

Согласись, было бы неплохо, если бы всеми инструментами Spring мы могли бы воспользоваться и при разработке портлета на Wicket?

Собственно, в этой части руководства мы и рассмотрим, как можно разработать портлет, используя совместно Wicket и Spring Framework. Точнее, мы модифицируем Wicket-портлет, разработанный в предыдущих частях, добавив в него поддержку Spring. Назовем новую версию портлета “Весенним” достижением” – “Spring Achievement”.

Чтобы наш новый пример не показался уж слишком оторванным от жизни, давай хотя бы воспользуемся в портлете одной из основных частей Spring, а именно паттерном “Dependency Injection”. Я предлагаю прислушаться к никогда не стареющему правилу “Не набирать, а выбирать” и сделать так, чтобы человек, совершивший достижение, мог выбрать себе награду из заранее подготовленного списка наград. Список наград в нашем примере будет формироваться сервисом, который в свою очередь будет инъектиться на страницу с помощью Spring.

Причем здесь Spring MVC?

Сразу расставлю все точки над “и” (тем более, что в русском языке над “и” нет никаких точек вообще) и скажу, что портлет, который мы собираемся создать, будет всего-навсего лишь модифицированным Spring MVC портлетом. То есть сам по себе это будет Spring MVC портлет, реализуемый классом org.springframework.web.portlet.DispatcherPortlet, но в качестве представлений для разных режимов портлета будут использоваться написанные нами ранее классы Wicket. Для краткости в дальнейшем, заменив “View” в MVC на “Wicket”, будем называть наш портлет Spring MWC портлетом. Пусть это и не совсем точная и вообще придуманная мной пять минут назад формулировка, зато как свежо и изящно!

Одной из самых интересных особенностей портлетного Spring MVC приложения является, на мой взгляд, наличие отдельного локального WebApplicationContext у каждого из портлетов. Эта особенность будет перенята и нашим Spring MWC портлетом.

Таким образом, каждый экземпляр Spring MWC портлета будет иметь собственный WebApplicationContext, который унаследует также и все bean’ы, описанные в корневом контексте приложения. Помимо того, что любой bean из корневого контекста может быть переопределен в контексте портлета (нам впрочем этого делать не потребуется), в этом контексте можно также определить множество других bean’ов с локальной для портлета областью видимости.

Но вначале разберемся с корневым WebApplicationContext приложения…

Web.xml и корневой WebApplicationContext

Конфигурирование корневого контекста Spring, bean’ы которого имеют глобальную для всех портлетов область видимости, происходит достаточно стандартным образом:

web.xml
1
2
3
4
5
6
7
8
9
10
...
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
...

В файле /WEB-INF/applicationContext.xml, который, как несложно заметить, и используется для инициализации корневого WebApplicationContext, мы опишем всего два bean’а. Одним из них будет bean сервиса для получения списка наград – чтобы можно было пользоваться одним и тем же объектом сервиса во всех экземплярах портлета – но о нем мы поговорим позже, в соответствующем разделе. Сейчас же рассмотрим второй bean из корневого контекста – bean портлетного приложения:

applicationContext.xml
1
2
3
...
<bean id="achievementPortletApp" class="net.shafranov.portlets.achievement.AchievementPortletApp" />
...

Ссылку на bean приложения нужно указать в описании фильтра в web.xml. Для этого вместо параметра applicationClassName используем два других параметра: один – applicationFactoryClassName со значением “org.apache.wicket.spring.SpringWebApplicationFactory”, и второй – applicationBean, как раз и содержащий ссылку на bean приложения:

web.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
...
<filter>
    <filter-name>AchievementPortlet</filter-name>
    <filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
    <!-- Параметр applicationFactoryClassName -->
    <init-param>
        <param-name>applicationFactoryClassName</param-name>
        <param-value>org.apache.wicket.spring.SpringWebApplicationFactory</param-value>
    </init-param>
    <!-- Параметр applicationBean - ссылка на bean приложения -->
    <init-param>
        <param-name>applicationBean</param-name>
        <param-value>achievementPortletApp</param-value>
    </init-param>
    <!-- Остальные типовые параметры, как и в предыдущей версии портлета -->
    <init-param>
        <param-name>detectPortletContext</param-name>
        <param-value>true</param-value>
    </init-param>
    <init-param>
        <param-name>configuration</param-name>
        <param-value>deployment</param-value>
    </init-param>
</filter>

<filter-mapping>
    <filter-name>AchievementPortlet</filter-name>
    <url-pattern>/spring_achievement/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>INCLUDE</dispatcher>
    <dispatcher>FORWARD</dispatcher>
</filter-mapping>
...

Portlet.xml и контекст портлета

В файле portlet.xml вместо класса org.apache.wicket.protocol.http.portlet.WicketPortlet, как я и обещал, мы используем портлетный класс Spring MVC – org.springframework.web.portlet.DispatcherPortlet:

portlet.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
...
<portlet>
    <portlet-name>spring_achievement</portlet-name>
    <display-name>Spring Achievement</display-name>
    <portlet-class>org.springframework.web.portlet.DispatcherPortlet</portlet-class>
    <!-- Параметр для инициализации WebApplicationContext портлета -->
    <init-param>
        <name>contextConfigLocation</name>
        <value>/WEB-INF/achievementPortletContext.xml</value>
    </init-param>
    <!-- Остальные параметры инициализации, как и в предыдущей версии портлета -->
    <init-param>
        <name>wicketFilterPath</name>
        <value>/spring_achievement</value>
    </init-param>
    <init-param>
        <name>viewPage</name>
        <value>/spring_achievement/view</value>
    </init-param>
    <init-param>
        <name>editPage</name>
        <value>/spring_achievement/edit</value>
    </init-param>
    <init-param>
        <name>helpPage</name>
        <value>/spring_achievement/help</value>
    </init-param>
    <!-- ... 
        Разделы <expiration-cache />, <supports />, <supported-locale />, 
        <portlet-info />, <security-role-ref />, аналогичные разделам 
        из предыдущей версии портлета 
         ... -->
</portlet>
...

Единственный новый параметр инициализации здесь – параметр contextConfigLocation, в котором нужно указать путь до конфигурационного файла, используемого для инициализации локального WebApplicationContext портлета. Остальные параметры остаются такими же, как и в предыдущей не-Spring’овой версии портлета, так как суровая правда состоит в том, что DispatcherPortlet будет лишь “оберткой” для лежащего под ним WicketPortlet. “Оберткой”, впрочем, в крайней степени умелой, способной поддерживать не только корневой WebApplicationContext, но и локальный контекст приложения портлета.

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

  1. Если при инициализации портлета в качестве параметра инициализации мы указываем лишь contextConfigLocation, то каким образом мы сконфигурируем DispatcherPortlet, чтобы он в итоге передавал управление WicketPortlet?
  2. И зачем нам вообще может понадобиться WebApplicationContext портлета, если мы уже решили реализовать сервис наград в корневом контексте приложения?

Забавно, но ответ на оба этих вопроса в действительности одинаков: WebApplicationContext портлета, в числе всего прочего, и есть то место, где происходит настройка DispatcherPortlet.

Перечень всех bean’ов, которые используются для настройки DispatcherPortlet, можно найти в официальной документации по фреймворку Spring MVC. Мы используем всего два bean’а. Первый – с типом org.springframework.web.portlet.handler.PortletModeHandlerMapping – используется для установки контроллеров для обработки различных режимов портлета. В нашем случае этот bean для всех трех режимов – VIEW, EDIT и HELP – ссылается на один и тот же контроллер, в качестве которого выступает второй bean – achievementPortletController:

achievementPortletContext.xml
1
2
3
4
5
6
7
8
9
10
11
...
<bean class="org.springframework.web.portlet.handler.PortletModeHandlerMapping">
    <property name="portletModeMap">
        <map>
            <entry key="view" value-ref="achievementPortletController"></entry>
            <entry key="edit" value-ref="achievementPortletController"></entry>
            <entry key="help" value-ref="achievementPortletController"></entry>
        </map>
    </property>
</bean>
...

Bean achievementPortletController имеет тип org.springframework.web.portlet.mvc.PortletWrappingController и как раз передает управление WicketPortlet:

achievementPortletContext.xml
1
2
3
4
5
6
7
...
<bean id="achievementPortletController" class="org.springframework.web.portlet.mvc.PortletWrappingController">
    <property name="portletClass">
        <value>org.apache.wicket.protocol.http.portlet.WicketPortlet</value>
    </property>
</bean>
...

Сервис для получения списка наград

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

Вначале интерфейс:

IPrizeService.java
1
2
3
4
public interface IPrizeService {

    List<String> loadPrizes();
}

Далее, предположив, что мы реализовали каким-нибудь образом этот интерфейс в классе PrizeServiceImpl, передадим Spring управление жизненным циклом реализации. Объявляем bean сервиса в корневом контексте портлета:

applicationContext.xml
1
2
3
...
<bean class="net.shafranov.portlets.achievement.service.PrizeServiceImpl" />
...

Далее инъектим сервис на страницу редактирования с помощью привычной аннотации @SpringBean:

AchievementEditPage.java
1
2
3
4
5
6
7
public class AchievementEditPage extends LiferayPortletPage {

    @SpringBean
    private IPrizeService prizeService;

    ...
}

При этом не забываем, что, чтобы аннотация @SpringBean работала, нам нужно установить SpringComponentInjector в качестве слушателя инициализации Wicket-компонентов в классе приложения:

AchievementPortletApp.java
1
2
3
4
5
6
7
8
9
...
@Override
protected void init() {
    // инициализация Spring
    addComponentInstantiationListener(new SpringComponentInjector(this));

    // монтирование страниц и прочее ...
}
...

В конце концов, используем сервис в коде комбобокса для выбора награды:

AchievementEditPage.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
add(new DropDownChoice<String>("prize",
        new PortletPreferenceModel("prize", new StringResourceModel("defaultValue.prize", this, null)),
        new LoadableDetachableModel<List<String>>() {

            public List<String> load() {
                return prizeService.loadPrizes();
            }
        }) {

    protected boolean wantOnSelectionChangedNotifications() {
        return true;
    }
});
...

Результат

Думаю, нельзя не согласиться, что со Spring наш портлет стал еще чуточку удобнее:

Демонстрация использования сервиса получения списка наград

Осталось сказать только, что исходники портлета, модифицированного в сторону использования в нем Spring Framework, можно увидеть в общем с предыдущей версией портлета репозитории на Github.

Comments