Статьи

Помимо спецификации JAX-RS: поисковое расширение Apache CXF

В сегодняшнем посте мы рассмотрим не только спецификацию JAX-RS 2.0, но и полезные расширения, которые Apache CXF , одна из популярных реализаций JAX-RS 2.0 , предлагает разработчикам сервисов и API-интерфейсов REST . В частности, мы поговорим о расширении поиска с использованием подмножества фильтров запросов OData 2.0 .

В двух словах, расширение поиска просто отображает какое-то выражение фильтра на набор совпадающих типизированных объектов (экземпляров классов Java). Фильтры запросов OData 2.0 могут быть очень сложными, однако на данный момент Apache CXF поддерживает только их подмножество:

оператор Описание пример
уравнение равных городской экв ‘Редмонд’
Небраска Не равный город н ‘Лондон’
GT Лучше чем цена GT 20
GE Больше или равно цена ge 10
л Меньше, чем цена 20 лт
ле Меньше или равно цена ле 100
и Логический и цена ле 200 и цена гт 3,5
или же Логический или цена ле 3,5 или цена гт 200

По сути, для настройки и активации расширения поиска для ваших служб JAX-RS достаточно определить два свойства: search.query.parameter.name и search.parser , а также один дополнительный поставщик SearchContextProvider :

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
@Configuration
public class AppConfig {   
    @Bean( destroyMethod = "shutdown" )
    public SpringBus cxf() {
        return new SpringBus();
    }
     
    @Bean @DependsOn( "cxf" )
    public Server jaxRsServer() {
        final Map< String, Object > properties = new HashMap< String, Object >();       
        properties.put( "search.query.parameter.name", "$filter" );
        properties.put( "search.parser", new ODataParser< Person >( Person.class ) );
 
        final JAXRSServerFactoryBean factory =
            RuntimeDelegate.getInstance().createEndpoint(
                jaxRsApiApplication(),
                JAXRSServerFactoryBean.class
            );
        factory.setProvider( new SearchContextProvider() );
        factory.setProvider( new JacksonJsonProvider() );
        factory.setServiceBeans( Arrays.< Object >asList( peopleRestService() ) );
        factory.setAddress( factory.getAddress() );     
        factory.setProperties( properties );
 
        return factory.create();
    }
     
    @Bean
    public JaxRsApiApplication jaxRsApiApplication() {
        return new JaxRsApiApplication();
    }
     
    @Bean
    public PeopleRestService peopleRestService() {
        return new PeopleRestService();
    }  
}

Search.query.parameter.name определяет, каким будет имя параметра строки запроса, используемого в качестве фильтра (мы устанавливаем его равным $ filter ), тогда как search.parser определяет анализатор, который будет использоваться для анализа выражения фильтра (мы устанавливаем это быть ODataParser параметризованным с классом Person ). ODataParser построен на основе превосходного проекта Apache Olingo, который в настоящее время реализует протокол OData 2.0 (поддержка OData 4.0 уже на подходе).

Как только конфигурация выполнена, любая служба JAX-RS 2.0 может воспользоваться возможностями поиска, введя контекстный параметр SearchContext . Давайте посмотрим на это в действии, определив службу REST для управления людьми, представленными следующим классом Person :

1
2
3
4
5
6
7
public class Person {
    private String firstName;
    private String lastName;
    private int age;
 
    // Setters and getters here
}

PeopleRestService просто позволит создавать новых людей с помощью HTTP POST и выполнять поиск с использованием HTTP GET , перечисленных в / search endpoint:

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
package com.example.rs;
 
import java.util.ArrayList;
import java.util.Collection;
 
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
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 org.apache.cxf.jaxrs.ext.search.SearchCondition;
import org.apache.cxf.jaxrs.ext.search.SearchContext;
 
import com.example.model.Person;
 
@Path( "/people" )
public class PeopleRestService {
    private final Collection< Person > people = new ArrayList<>();
        
    @Produces( { MediaType.APPLICATION_JSON  } )
    @POST
    public Response addPerson( @Context final UriInfo uriInfo,
            @FormParam( "firstName" ) final String firstName,
            @FormParam( "lastName" ) final String lastName,
            @FormParam( "age" ) final int age ) {     
         
        final Person person = new Person( firstName, lastName, age );
        people.add( person );
         
        return Response
            .created( uriInfo.getRequestUriBuilder().path( "/search" )
            .queryParam( "$filter=firstName eq '{firstName}' and lastName eq '{lastName}' and age eq {age}" )
            .build( firstName, lastName, age ) )
            .entity( person ).build();
    }
     
    @GET
    @Path("/search")
    @Produces( { MediaType.APPLICATION_JSON  } )
    public Collection< Person > findPeople( @Context SearchContext searchContext ) {       
        final SearchCondition< Person > filter = searchContext.getCondition( Person.class );
        return filter.findAll( people );
    }
}

Метод findPeople — это тот, который мы ищем. Благодаря тому, что Apache CXF делает все возможное, метод выглядит очень простым: SearchContext внедряется, а выражение фильтра автоматически выбирается из параметра строки запроса $ filter . Последняя часть заключается в применении фильтра к данным, который в нашем случае является просто набором людей . Очень чистый и понятный.

Давайте построим проект и запустим его:

1
2
mvn clean package
java -jar target/cxf-search-extension-0.0.1-SNAPSHOT.jar

Используя удивительный инструмент curl , давайте выполним пару запросов HTTP POST, чтобы сгенерировать некоторые данные для выполнения запросов фильтра:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
> curl http://localhost:8080/rest/api/people -X POST -d "firstName=Tom&lastName=Knocker&age=16"
{
    "firstName": "Tom",
    "lastName": "Knocker",
    "age": 16
}
 
> curl http://localhost:8080/rest/api/people -X POST -d "firstName=Bob&lastName=Bobber&age=23"
{
    "firstName": "Bob",
    "lastName": "Bobber",
    "age": 23
}
 
> curl http://localhost:8080/rest/api/people -X POST -d "firstName=Tim&lastName=Smith&age=50"
{
    "firstName": "Tim",
    "lastName": "Smith",
    "age": 50
}

Имея образцы данных, давайте продолжим и предложим пару различных критериев поиска, достаточно сложных, чтобы продемонстрировать мощь фильтров запросов OData 2.0 :

    • найти всех людей, чье имя Боб ($ filter = «firstName eq ‘Bob’» )
1
2
3
4
5
6
7
8
9
> curl -G -X GET http://localhost:8080/rest/api/people/search --data-urlencode
  $filter="firstName eq 'Bob'"
[
    {
        "firstName": "Bob",
        "lastName": "Bobber",
        "age": 23
    }
]
    • найти всех людей, чья фамилия Боббер или фамилия Смит, а firstName не Боб ($ filter = «lastName eq« Bobber »или (lastName eq« Smith »и firstName ne« Bob »)» )
01
02
03
04
05
06
07
08
09
10
11
12
13
14
> curl -G -X GET http://localhost:8080/rest/api/people/search --data-urlencode
  $filter="lastName eq 'Bobber' or (lastName eq 'Smith' and firstName ne 'Bob')"
[
    {
        "firstName": "Bob",
        "lastName": "Bobber",
        "age": 23
    },
    {   
        "firstName": "Tim",
        "lastName": "Smith",
        "age": 50
    }
]
    • найти всех людей, чье имя начинается с буквы T, и которые старше 16 лет ($ filter = «firstName eq ‘T *’ и age ge 16» )
01
02
03
04
05
06
07
08
09
10
11
12
13
14
> curl -G -X GET http://localhost:8080/rest/api/people/search --data-urlencode
  $filter="firstName eq 'T*' and age ge 16"
[
    {
        "firstName": "Tom",
        "lastName": "Knocker",
        "age": 16
    },
    {
        "firstName": "Tim",
        "lastName": "Smith",
        "age": 50
    }
]

Примечание : если вы запускаете эти команды в Linux-подобной среде, вам может потребоваться экранировать знак $ с помощью \ $ , например:
curl -X GET -G http: // localhost: 8080 / rest / api / people / search –data-urlencode \ $ filter = ”firstName eq ‘Bob’”

На данный момент Apache CXF предлагает только базовую поддержку фильтров запросов OData 2.0 со многими мощными выражениями, оставленными в стороне. Тем не менее, существует обязательство продвигать его вперед, как только сообщество проявит достаточный интерес к использованию этой функции.

Стоит отметить, что фильтры запросов OData 2.0 — не единственная доступная опция. Расширение поиска также поддерживает FIQL (язык запросов элементов фида ), и эта замечательная статья от одного из основных разработчиков Apache CXF является прекрасным введением в него.

Я думаю, что эта весьма полезная функция Apache CXF может сэкономить много вашего времени и усилий, предоставляя простые (и не такие простые) возможности поиска для ваших служб JAX-RS 2.0 . Пожалуйста, попробуйте, если он соответствует потребностям вашего приложения.

  • Полный исходный код проекта доступен на Github .