Термины «модульность» и «архитектура микросервисов» в наши дни появляются довольно часто в контексте построения масштабируемых, надежных распределенных систем. Сама платформа Java, как известно, является слабой в отношении модульности ( Java 9 собирается решить эту проблему путем предоставления проекта Jigsaw ), давая возможность появиться таким фреймворкам, как OSGi и JBoss Modules .
Когда я впервые услышал об OSGi в 2007 году, я был действительно взволнован всеми этими преимуществами, которые могут принести Java-приложения, будучи построенными на его основе. Но очень быстро вместо волнения возникло разочарование: нет поддержки инструментов, очень ограниченный набор совместимых библиотек и платформ, довольно нестабильно и трудно устранять неполадки во время выполнения. Ясно, что он не был готов к использованию средним Java-разработчиком, и поэтому мне пришлось положить его на полку. С годами OSGi повзрослела и получила широкую поддержку сообщества.
Любопытный читатель может спросить: каковы преимущества использования модулей и OSGi в частности? Чтобы назвать только несколько проблем, это помогает решить:
- явное (и версионное) управление зависимостями: модули объявляют, что им нужно (и, опционально, диапазоны версий)
- небольшая площадь: модули не упакованы со всеми зависимостями
- простота выпуска: модули могут разрабатываться и выпускаться независимо
- горячее повторное развертывание: отдельные модули могут быть повторно развернуты без влияния на другие
В сегодняшнем посте мы рассмотрим современное состояние построения модульных Java-приложений с использованием OSGi . Оставляя в стороне обсуждения, насколько хорош или плох OSGi , мы собираемся создать пример приложения, состоящего из следующих модулей:
- модуль доступа к данным
- модуль бизнес-услуг
- REST сервисный модуль
Apache OpenJPA 2.3.0 / JPA 2.0 для доступа к данным (к сожалению, JPA 2.1 еще не поддерживается реализацией OSGi по нашему выбору), Apache CXF 3.0.1 / JAX-RS 2.0 для уровня REST являются двумя основными строительными блоками приложения. Я обнаружил, что блог Кристиана Шнайдера , Liquid Reality , является бесценным источником информации об OSGi (а также о многих других темах).
В мире OSGi модули называются пакетами . Пакеты проявляют свои зависимости (пакеты импорта) и пакеты, которые они выставляют (пакеты экспорта), чтобы другие пакеты могли использовать их. Apache Maven также поддерживает эту модель упаковки. Пакеты управляются средой выполнения OSGi или контейнером, который в нашем случае будет Apache Karaf 3.0.1 (на самом деле, это единственное, что нам нужно скачать и распаковать).
Позвольте мне прекратить говорить и лучше показать код. Мы собираемся начать сверху ( REST ) и пройти весь путь вниз (доступ к данным), так как было бы легче следовать. Наш PeopleRestService является типичным примером реализации сервиса JAX-RS 2.0 :
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
84
85
|
package com.example.jaxrs; import java.util.Collection; import javax.ws.rs.DELETE; import javax.ws.rs.DefaultValue; import javax.ws.rs.FormParam; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import com.example.data.model.Person; import com.example.services.PeopleService; @Path ( "/people" ) public class PeopleRestService { private PeopleService peopleService; @Produces ( { MediaType.APPLICATION_JSON } ) @GET public Collection< Person > getPeople( @QueryParam ( "page" ) @DefaultValue ( "1" ) final int page ) { return peopleService.getPeople( page, 5 ); } @Produces ( { MediaType.APPLICATION_JSON } ) @Path ( "/{email}" ) @GET public Person getPerson( @PathParam ( "email" ) final String email ) { return peopleService.getByEmail( email ); } @Produces ( { MediaType.APPLICATION_JSON } ) @POST public Response addPerson( @Context final UriInfo uriInfo, @FormParam ( "email" ) final String email, @FormParam ( "firstName" ) final String firstName, @FormParam ( "lastName" ) final String lastName ) { peopleService.addPerson( email, firstName, lastName ); return Response.created( uriInfo .getRequestUriBuilder() .path( email ) .build() ).build(); } @Produces ( { MediaType.APPLICATION_JSON } ) @Path ( "/{email}" ) @PUT public Person updatePerson( @PathParam ( "email" ) final String email, @FormParam ( "firstName" ) final String firstName, @FormParam ( "lastName" ) final String lastName ) { final Person person = peopleService.getByEmail( email ); if ( firstName != null ) { person.setFirstName( firstName ); } if ( lastName != null ) { person.setLastName( lastName ); } return person; } @Path ( "/{email}" ) @DELETE public Response deletePerson( @PathParam ( "email" ) final String email ) { peopleService.removePerson( email ); return Response.ok().build(); } public void setPeopleService( final PeopleService peopleService ) { this .peopleService = peopleService; } } |
Как мы видим, здесь ничего не говорится о OSGi . Единственной зависимостью является PeopleService, который каким-то образом должен быть введен в PeopleRestService . Как? Как правило, приложения OSGi используют план в качестве структуры внедрения зависимостей, очень похожий на старый приятель, конфигурацию Spring на основе XML. Он должен быть упакован вместе с приложением в папке OSGI-INF / blueprint . Вот пример проекта для нашего модуля REST , построенного на основе Apache CXF 3.0.1 :
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
|
xsi:schemaLocation=" < cxf:bus id = "bus" > < cxf:features > < cxf:logging /> </ cxf:features > </ cxf:bus > < jaxrs:server address = "/api" id = "api" > < jaxrs:serviceBeans > < ref component-id = "peopleRestService" /> </ jaxrs:serviceBeans > < jaxrs:providers > < bean class = "com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider" /> </ jaxrs:providers > </ jaxrs:server > <!-- Implementation of the rest service --> < bean id = "peopleRestService" class = "com.example.jaxrs.PeopleRestService" > < property name = "peopleService" ref = "peopleService" /> </ bean > < reference id = "peopleService" interface = "com.example.services.PeopleService" /> </ blueprint > |
Очень маленький и простой: в основном, конфигурация просто заявляет, что для работы модуля должна быть предоставлена ссылка на com.example.services.PeopleService (фактически, контейнером OSGi ). Чтобы увидеть, как это произойдет, давайте посмотрим на другой модуль, который предоставляет сервисы. Он содержит только один интерфейс PeopleService :
01
02
03
04
05
06
07
08
09
10
11
12
|
package com.example.services; import java.util.Collection; import com.example.data.model.Person; public interface PeopleService { Collection< Person > getPeople( int page, int pageSize ); Person getByEmail( final String email ); Person addPerson( final String email, final String firstName, final String lastName ); void removePerson( final String email ); } |
А также обеспечивает его реализацию в виде класса PeopleServiceImpl :
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
|
package com.example.services.impl; import java.util.Collection; import org.osgi.service.log.LogService; import com.example.data.PeopleDao; import com.example.data.model.Person; import com.example.services.PeopleService; public class PeopleServiceImpl implements PeopleService { private PeopleDao peopleDao; private LogService logService; @Override public Collection< Person > getPeople( final int page, final int pageSize ) { logService.log( LogService.LOG_INFO, "Getting all people" ); return peopleDao.findAll( page, pageSize ); } @Override public Person getByEmail( final String email ) { logService.log( LogService.LOG_INFO, "Looking for a person with e-mail: " + email ); return peopleDao.find( email ); } @Override public Person addPerson( final String email, final String firstName, final String lastName ) { logService.log( LogService.LOG_INFO, "Adding new person with e-mail: " + email ); return peopleDao.save( new Person( email, firstName, lastName ) ); } @Override public void removePerson( final String email ) { logService.log( LogService.LOG_INFO, "Removing a person with e-mail: " + email ); peopleDao.delete( email ); } public void setPeopleDao( final PeopleDao peopleDao ) { this .peopleDao = peopleDao; } public void setLogService( final LogService logService ) { this .logService = logService; } } |
И на этот раз очень маленькая и чистая реализация с двумя инъекционными зависимостями, org.osgi.service.log.LogService и com.example.data.PeopleDao . Его конфигурация чертежа , расположенная в папке OSGI-INF / blueprint , также выглядит довольно компактно:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
xsi:schemaLocation=" < service ref = "peopleService" interface = "com.example.services.PeopleService" /> < bean id = "peopleService" class = "com.example.services.impl.PeopleServiceImpl" > < property name = "peopleDao" ref = "peopleDao" /> < property name = "logService" ref = "logService" /> </ bean > < reference id = "peopleDao" interface = "com.example.data.PeopleDao" /> < reference id = "logService" interface = "org.osgi.service.log.LogService" /> </ blueprint > |
Ожидается, что ссылки на PeopleDao и LogService будут предоставлены контейнером OSGi во время выполнения. Однако реализация PeopleService предоставляется как сервис, и контейнер OSGi сможет внедрить его в PeopleRestService после активации его пакета.
Последний фрагмент головоломки, модуль доступа к данным, немного сложнее: он содержит конфигурацию постоянства ( META-INF / persistence.xml ) и в основном зависит от возможностей JPA 2.0 контейнера OSGi . Файл persistence.xml довольно прост:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
version = "2.0" > < persistence-unit name = "peopleDb" transaction-type = "JTA" > < jta-data-source > osgi:service/javax.sql.DataSource/(osgi.jndi.service.name=peopleDb) </ jta-data-source > < class >com.example.data.model.Person</ class > < properties > < property name = "openjpa.jdbc.SynchronizeMappings" value = "buildSchema" /> </ properties > </ persistence-unit > </ persistence > |
Как и в сервисном модуле, есть интерфейс PeopleDao :
01
02
03
04
05
06
07
08
09
10
11
12
|
package com.example.data; import java.util.Collection; import com.example.data.model.Person; public interface PeopleDao { Person save( final Person person ); Person find( final String email ); Collection< Person > findAll( final int page, final int pageSize ); void delete( final String email ); } |
При его реализации PeopleDaoImpl :
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
|
package com.example.data.impl; import java.util.Collection; import javax.persistence.EntityManager; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import com.example.data.PeopleDao; import com.example.data.model.Person; public class PeopleDaoImpl implements PeopleDao { private EntityManager entityManager; @Override public Person save( final Person person ) { entityManager.persist( person ); return person; } @Override public Person find( final String email ) { return entityManager.find( Person. class , email ); } public void setEntityManager( final EntityManager entityManager ) { this .entityManager = entityManager; } @Override public Collection< Person > findAll( final int page, final int pageSize ) { final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); final CriteriaQuery< Person > query = cb.createQuery( Person. class ); query.from( Person. class ); return entityManager .createQuery( query ) .setFirstResult(( page - 1 ) * pageSize ) .setMaxResults( pageSize ) .getResultList(); } @Override public void delete( final String email ) { entityManager.remove( find( email ) ); } } |
Обратите внимание, что, хотя мы выполняем манипуляции с данными, здесь не упоминаются транзакции, а также нет явных вызовов API транзакций менеджера сущностей. Мы собираемся использовать декларативный подход к транзакциям, поскольку конфигурация чертежей поддерживает это (расположение не изменяется, папка OSGI-INF / blueprint ):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
xsi:schemaLocation=" < service ref = "peopleDao" interface = "com.example.data.PeopleDao" /> < bean id = "peopleDao" class = "com.example.data.impl.PeopleDaoImpl" > < jpa:context unitname = "peopleDb" property = "entityManager" /> < tx:transaction method = "*" value = "Required" /> </ bean > < bean id = "dataSource" class = "org.hsqldb.jdbc.JDBCDataSource" > < property name = "url" value = "jdbc:hsqldb:mem:peopleDb" /> </ bean > < service ref = "dataSource" interface = "javax.sql.DataSource" > < service-properties > < entry key = "osgi.jndi.service.name" value = "peopleDb" /> </ service-properties > </ service > </ blueprint > |
Следует помнить одну вещь: приложению не нужно создавать диспетчер сущностей JPA 2.1 : среда выполнения OSGi способна сделать это и внедрить его везде, где это требуется, на основе объявлений jpa: context . Следовательно, tx: транзакция указывает среде выполнения обернуть выбранные методы службы внутри транзакции.
Теперь, когда открыт последний сервис PeopleDao , мы готовы к развертыванию наших модулей с Apache Karaf 3.0.1 . Это довольно легко сделать в три этапа:
- запустить контейнер Apache Karaf 3.0.1
1
bin/karaf (or bin\karaf.bat on Windows)
- выполните следующие команды из оболочки Apache Karaf 3.0.1 :
12345678
feature:repo-add cxf
3.0
.
1
feature:install http cxf jpa openjpa transaction jndi jdbc
install -s mvn:org.hsqldb/hsqldb/
2.3
.
2
install -s mvn:com.fasterxml.jackson.core/jackson-core/
2.4
.
0
install -s mvn:com.fasterxml.jackson.core/jackson-annotations/
2.4
.
0
install -s mvn:com.fasterxml.jackson.core/jackson-databind/
2.4
.
0
install -s mvn:com.fasterxml.jackson.jaxrs/jackson-jaxrs-base/
2.4
.
0
install -s mvn:com.fasterxml.jackson.jaxrs/jackson-jaxrs-json-provider/
2.4
.
0
- соберите наши модули и скопируйте их в папку развертывания Apache Karaf 3.0.1 (пока контейнер еще работает):
12
mvn clean
package
cp module*/target/*jar apache-karaf-
3.0
.
1
/deploy/
Когда вы запускаете команду list в оболочке Apache Karaf 3.0.1 , вы должны увидеть список всех активированных пакетов (модулей), подобный этому:
Где module-service , module-jax-rs и module-data соответствуют тем, которые мы разрабатываем. По умолчанию все наши службы Apache CXF 3.0.1 будут доступны по базовому URL http: // : 8181 / cxf / api /. Это легко проверить, выполнив команду cxf: list-endpoints -f в оболочке Apache Karaf 3.0.1 .
Давайте удостоверимся, что наш уровень REST работает должным образом, отправив пару HTTP- запросов. Давайте создадим нового человека:
1
2
3
4
5
6
7
|
curl http: //localhost:8181/cxf/api/people -iX POST -d "firstName=Tom&lastName=Knocker&[email protected]" HTTP/ 1.1 201 Created Content-Length: 0 Date: Sat, 09 Aug 2014 15 : 26 : 17 GMT Location: http: //localhost:8181/cxf/api/people/[email protected] Server: Jetty( 8.1 . 14 .v20131031) |
И убедитесь, что этот человек был успешно создан:
1
2
3
4
5
6
7
8
9
|
curl -i http: //localhost:8181/cxf/api/people HTTP/ 1.1 200 OK Content-Type: application/json Date: Sat, 09 Aug 2014 15 : 28 : 20 GMT Transfer-Encoding: chunked Server: Jetty( 8.1 . 14 .v20131031) |
Было бы неплохо проверить, есть ли в базе данных и человек. С оболочкой Apache Karaf 3.0.1 это очень просто сделать, выполнив всего две команды: jdbc: datasources и jdbc: query peopleDb «выбрать * из людей» .
Потрясающие! Я надеюсь, что это довольно вводное сообщение в блоге откроет еще одну интересную технологию, которую вы можете использовать для разработки надежного, масштабируемого, модульного и управляемого программного обеспечения. Мы не коснулись многих, многих вещей, но это здесь для вас, чтобы открыть. Полный исходный код доступен на GitHub .
Примечание для пользователей Hibernate 4.2.x / 4.3.x : к сожалению, в текущем выпуске Apache Karaf 3.0.1 Hibernate 4.3.x вообще работает должным образом (поскольку JPA 2.1 еще не поддерживается), и, тем не менее, мне удалось При работе с Hibernate 4.2.x контейнер часто отказывался разрешать связанные с JPA зависимости.
Ссылка: | OSGi: портал в архитектуру микро-услуг от нашего партнера по JCG Андрея Редько в блоге Андрея Редько . |