Статьи

OSGi: шлюз в архитектуру микро-сервисов

Термины «модульность» и «архитектура микросервисов» в наши дни появляются довольно часто в контексте построения масштабируемых, надежных распределенных систем. Сама платформа 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
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"        
    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
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"        
    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 :
    1
    2
    3
    4
    5
    6
    7
    8
    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 (пока контейнер еще работает):
    1
    2
    mvn clean package
    cp module*/target/*jar apache-karaf-3.0.1/deploy/

Когда вы запускаете команду list в оболочке Apache Karaf 3.0.1 , вы должны увидеть список всех активированных пакетов (модулей), подобный этому:

апач-karaf-лист
Где 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 .

апач-karaf-CxF-список-конечные точки

Давайте удостоверимся, что наш уровень 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)
 
[{"email":"[email protected]","firstName":"Tom","lastName":"Knocker"}]

Было бы неплохо проверить, есть ли в базе данных и человек. С оболочкой Apache Karaf 3.0.1 это очень просто сделать, выполнив всего две команды: jdbc: datasources и jdbc: query peopleDb «выбрать * из людей» .

апач-karaf-лист-источники данных

Потрясающие! Я надеюсь, что это довольно вводное сообщение в блоге откроет еще одну интересную технологию, которую вы можете использовать для разработки надежного, масштабируемого, модульного и управляемого программного обеспечения. Мы не коснулись многих, многих вещей, но это здесь для вас, чтобы открыть. Полный исходный код доступен на 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 Андрея Редько в блоге Андрея Редько .