Все слышали о порталах, которые объединяют одно веб-приложение в одно большое. Программное обеспечение портала работает как гибридные приложения — контент из нескольких источников собирается в одном сервисе, в основном, отображается на одной веб-странице. Программное обеспечение портала также позволяет изменять пользовательские настройки, такие как язык или тема, для всех отдельных веб-приложений (независимых модулей), встроенных в программное обеспечение портала. Кроме того, ожидается единый вход в систему (SSO), и он также должен работать. Это означает, что единый вход в систему позволяет пользователю получить доступ ко всем встроенным веб-приложениям. Было бы интересно узнать, существует ли в мире JEE простое и легкое решение для разработки модульных приложений JSF 2, для автоматического сбора и представления их в одном портальном веб-приложении. Конечно, существуют OSGi и сложные портальные мосты, обеспечивающие поддержку разработки портлетов, совместимых с JSR-168 или JSR-286. Но, к счастью, JSF 2 уже предоставляет простую возможность для этого «под капотом». С меньшими усилиями мы можем создать программное обеспечение, подобное порталу. Все, что нам нужно, это JSF 2 и CDI — де-факто стандартная структура DI в мире Java.
Тема этого поста не нова. Вы можете найти некоторые обсуждения и идеи в Интернете. Я бы упомянул здесь только две ссылки. Первая — статья «Как: модульные приложения Java EE с CDI и PrettyFaces» в блоге ocpsoft. Второе «Модульные веб-приложения с JSF2» было представлено в вики JBoss. Идея состоит в том, чтобы создать файлы JAR, содержащие отдельные веб-приложения, и снабдить их основным файлом WAR. Файл WAR объединяет JAR-файлы в процессе сборки, например, через зависимости Maven. Это означает, что файлы JAR находятся в WAR под WEB-INF / lib /. Файлы XHTML в JAR-файлах находятся ниже / META-INF / resources / и будут автоматически загружаться JSF 2. Они доступны для JSF, как если бы они находились в папке / webapp / resources /. Например, вы можете включить лицевые стороны из JAR с довольно распространенным интерфейсом: include. Это работает как шарм. Чтобы иметь возможность получать общую информацию о каждом модуле JSF во время выполнения, нам также нужен пустой CDI’s beans.xml в файлах JARs. Они расположены как обычно ниже
Папка META-INF.
Теперь давайте начнем с кодирования. Но сначала давайте определимся со структурой проекта. Вы можете найти полный реализованный пример на GitHub . Это всего лишь доказательство концепции легкой реализации, подобной порталу JSF 2, с демонстрационными веб-приложениями (написанными с использованием JSF 2.2). Есть 5 подпроектов:
- jsftoolkit-jar Базовая структура, предоставляющая интерфейсы и утилиты для модульных приложений JSF.
- modA-jar Первое веб-приложение (модуль A), которое зависит от jsftoolkit-jar.
- modB-jar Второе веб-приложение (модуль B), которое зависит от jsftoolkit-jar.
- portal-jar Java часть программного обеспечения, подобного порталу. Это также зависит от jsftoolkit-jar.
- portal-war Веб-часть программного обеспечения, похожего на портал. Он объединяет все артефакты и является развертываемой WAR.
Базовая структура (jsftoolkit-jar) имеет интерфейсы, которые должны быть реализованы каждым модулем. Наиболее важными являются
|
01
02
03
04
05
06
07
08
09
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
/** * Interface for modular JSF applications. This interface should be implemented by every module (JSF app.) * to allow a seamless integration into a "portal" software. */public interface ModuleDescription { /** * Provides a human readable name of the module. * * @return String name of the module */ String getName(); /** * Provides a description of the module. * * @return String description */ String getDescription(); /** * Provides a module specific prefix. This is a folder below the context where all web pages and * resources are located. * * @return String prefix */ String getPrefix(); /** * Provides a name for a logo image, e.g. "images/logo.png" (used in h:graphicImage). * * @return String logo name */ String getLogoName(); /** * Provides a start (home) URL to be navigated for the module. * * @return String URL */ String getUrl();}/** * Any JSF app. implementing this interface can participate in an unified message handling * when all keys and messages are merged to a map and available via "msgs" EL, e.g. as #{msgs['mykey']}. */public interface MessagesProvider { /** * Returns all mesages (key, text) to the module this interface is implemented for. * * @param locale current Locale or null * @return Map with message keys and message text. */ Map<String, String> getMessages(Locale locale);} |
Возможные реализации для модуля A выглядят следующим образом:
|
01
02
03
04
05
06
07
08
09
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
/** * Module specific implementation of the {@link ModuleDescription}. */@ApplicationScoped@Namedpublic class ModADescription implements ModuleDescription, Serializable { @Inject private MessagesProxy msgs; @Override public String getName() { return msgs.get("a.modName"); } @Override public String getDescription() { return msgs.get("a.modDesc"); } @Override public String getPrefix() { return "moda"; } @Override public String getLogoName() { return "images/logo.png"; } @Override public String getUrl() { return "/moda/views/hello.jsf"; }}/** * Module specific implementation of the {@link MessagesProvider}. */@ApplicationScoped@Namedpublic class ModAMessages implements MessagesProvider, Serializable { @Override public Map<String, String> getMessages(Locale locale) { return MessageUtils.getMessages(locale, "modA"); }} |
Префикс этого модуля — мода. Это означает, что веб-страницы и ресурсы находятся в папке META-INF / resources / moda /. Это позволяет избежать коллизий путей (идентичных путей) во всех отдельных веб-приложениях. Служебный класс MessageUtils (из jsftoolkit-jar) здесь не представлен. Я покажу только класс MessagesProxy. Класс MessagesProxy — это компонент в области приложения, предоставляющий доступ ко всем доступным сообщениям в модульном веб-приложении JSF. Его можно использовать как в Java, так и в XHTML, поскольку он реализует интерфейс Map. Все найденные доступные реализации интерфейса MessagesProvider внедряются CDI автоматически во время выполнения. Мы используем экземпляр <MessagesProvider>.
|
01
02
03
04
05
06
07
08
09
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
|
@ApplicationScoped@Named(value = "msgs")public class MessagesProxy implements Map<String, String>, Serializable { @Inject private UserSettingsData userSettingsData; @Any @Inject private Instance<MessagesProvider> messagesProviders; /** all cached locale specific messages */ private Map<Locale, Map<String, String>> msgs = new ConcurrentHashMap<Locale, Map<String, String>>(); @Override public String get(Object key) { if (key == null) { return null; } Locale locale = userSettingsData.getLocale(); Map<String, String> messages = msgs.get(locale); if (messages == null) { // no messages to current locale are available yet messages = new HashMap<String, String>(); msgs.put(locale, messages); // load messages from JSF impl. first messages.putAll(MessageUtils.getMessages(locale, MessageUtils.FACES_MESSAGES)); // load messages from providers in JARs for (MessagesProvider messagesProvider : messagesProviders) { messages.putAll(messagesProvider.getMessages(locale)); } } return messages.get(key); } public String getText(String key) { return this.get(key); } public String getText(String key, Object... params) { String text = this.get(key); if ((text != null) && (params != null)) { text = MessageFormat.format(text, params); } return text; } public FacesMessage getMessage(FacesMessage.Severity severity, String key, Object... params) { String summary = this.get(key); String detail = this.get(key + "_detail"); if ((summary != null) && (params != null)) { summary = MessageFormat.format(summary, params); } if ((detail != null) && (params != null)) { detail = MessageFormat.format(detail, params); } if (summary != null) { return new FacesMessage(severity, summary, ((detail != null) ? detail : StringUtils.EMPTY)); } return new FacesMessage(severity, "???" + key + "???", ((detail != null) ? detail : StringUtils.EMPTY)); } ///////////////////////////////////////////////////////// // java.util.Map interface ///////////////////////////////////////////////////////// public int size() { throw new UnsupportedOperationException(); } // other methods ...} |
Что ж. Но где подобраны экземпляры ModuleDescription? Логика в портале-банке. Я использую тот же механизм с инстансом CDI. CDI выяснит все доступные реализации ModuleDescription для нас.
|
01
02
03
04
05
06
07
08
09
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
|
/** * Collects all available JSF modules. */@ApplicationScoped@Namedpublic class PortalModulesFinder implements ModulesFinder { @Any @Inject private Instance<ModuleDescription> moduleDescriptions; @Inject private MessagesProxy msgs; private List<FluidGridItem> modules; @Override public List<FluidGridItem> getModules() { if (modules != null) { return modules; } modules = new ArrayList<FluidGridItem>(); for (ModuleDescription moduleDescription : moduleDescriptions) { modules.add(new FluidGridItem(moduleDescription)); } // sort modules by names alphabetically Collections.sort(modules, ModuleDescriptionComparator.getInstance()); return modules; }} |
Теперь мы можем создавать динамические плитки в пользовательском интерфейсе, которые представляют точки входа в соответствующие веб-модули.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
<pe:fluidGrid id="fluidGrid" value="#{portalModulesFinder.modules}" var="modDesc" fitWidth="true" hasImages="true"> <pe:fluidGridItem styleClass="ui-widget-header"> <h:panelGrid columns="2" styleClass="modGridEntry" columnClasses="modLogo,modTxt"> <p:commandLink process="@this" action="#{navigationContext.goToPortlet(modDesc)}"> <h:graphicImage library="#{modDesc.prefix}" name="#{modDesc.logoName}"/> </p:commandLink> <h:panelGroup> <p:commandLink process="@this" action="#{navigationContext.goToPortlet(modDesc)}"> <h:outputText value="#{modDesc.name}" styleClass="linkToPortlet"/> </p:commandLink> <p/> <h:outputText value="#{modDesc.description}"/> </h:panelGroup> </h:panelGrid> </pe:fluidGridItem></pe:fluidGrid> |
Плитки были созданы компонентом pe: fluidGrid из PrimeFaces Extensions . Они отзывчивы, то есть они перестраиваются при изменении размера окна браузера. На следующем рисунке показано, как выглядит веб-приложение портала после запуска. Он показывает два модульных демонстрационных приложения, найденных в пути к классам. Каждое модульное веб-приложение отображается в виде плитки, содержащей логотип, название и краткое описание. Логотипы и названия кликабельны. Щелчок перенаправляет на соответствующее веб-приложение.
Как видите, на главной странице портала вы можете переключать текущий язык и тему. На втором рисунке показано, что происходит, если пользователь нажимает на модуль A. Показано веб-приложение для модуля A. Вы можете видеть кнопку «Назад к порталу», поэтому возможна обратная навигация на домашнюю страницу портала.
Два примечания в конце:
- Каждый модуль можно запускать как отдельное веб-приложение. Мы можем проверить во время выполнения (опять же с помощью CDI), находится ли модуль в «портале» или нет, и использовать разные главные шаблоны. Подсказка здесь .
- Я не реализовал экран входа в систему, но с единой регистрацией проблем не возникает, потому что у нас только одно большое приложение (одна WAR) Все доставлено туда.

