Статьи

Apache CXF 3.0: JAX-RS 2.0 и Bean Validation 1.1 наконец-то вместе

Предстоящий выпуск 3.0 (в настоящее время находится на втором этапе 2 этапа) великолепной платформы Apache CXF предоставляет много интересных и полезных функций, приближаясь к полноправной поддержке JAX-RS 2.0 . Одна из этих функций, долгожданная многими из нас, – это поддержка Bean Validation 1.1 : простая и лаконичная модель для добавления возможностей проверки на уровень сервисов REST .

В этом посте мы рассмотрим, как настроить Bean Validation 1.1 в ваших проектах Apache CXF, и обсудим некоторые интересные варианты использования. Чтобы держать этот пост достаточно кратким и сфокусированным, мы не будем обсуждать саму Bean Validation 1.1 , а сконцентрируемся больше на интеграции с ресурсами JAX-RS 2.0 (некоторые из основ валидации bean мы уже рассматривали в более ранних статьях ).

На данный момент Hibernate Validator является де-факто эталонной реализацией спецификации Bean Validation 1.1 , последней версией которой является 5.1.0.Final, и в этом качестве он будет поставщиком проверки по нашему выбору (проект Apache BVal на данный момент поддерживает только Bean Validation 1.0 ). Стоит отметить, что Apache CXF не зависит от реализации и будет одинаково хорошо работать с Hibernate Validator или Apache BVal после его выпуска.

Мы собираемся создать очень простое приложение для управления людьми. Наша модель состоит из одного класса с именем Person .

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
package com.example.model;
 
import javax.validation.constraints.NotNull;
 
import org.hibernate.validator.constraints.Email;
 
public class Person {
    @NotNull @Email private String email;
    @NotNull private String firstName;
    @NotNull private String lastName;
 
    public Person() {
    }
 
    public Person( final String email ) {
        this.email = email;
    }
 
    public String getEmail() {
        return email;
    }
 
    public void setEmail( final String email ) {
        this.email = email;
    }
 
    public String getFirstName() {
        return firstName;
    }
 
    public String getLastName() {
        return lastName;
    }
 
    public void setFirstName( final String firstName ) {
        this.firstName = firstName;
    }
 
    public void setLastName( final String lastName ) {
        this.lastName = lastName;
    }       
}

Из приведенного выше фрагмента видно, что класс Person налагает несколько ограничений на свои свойства: все они не должны быть нулевыми . Кроме того, свойство электронной почты должно содержать действительный адрес электронной почты (который будет проверен специфичным для Hibernate Validator ограничением @Email ). Довольно просто

Теперь давайте взглянем на ресурсы JAX-RS 2.0 с ограничениями проверки. Скелет класса PeopleRestService связан с URL-путем / people и показан ниже.

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
package com.example.rs;
 
import java.util.Collection;
 
import javax.inject.Inject;
import javax.validation.Valid;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
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.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 org.hibernate.validator.constraints.Length;
 
import com.example.model.Person;
import com.example.services.PeopleService;
 
@Path( "/people" )
public class PeopleRestService {
    @Inject private PeopleService peopleService;
 
    // REST methods here
}

Это должно выглядеть очень знакомо, ничего нового. Первый метод, который мы собираемся добавить и украсить с помощью проверочных ограничений, – это getPerson , который будет искать человека по его адресу электронной почты.

1
2
3
4
5
6
7
@Produces( { MediaType.APPLICATION_JSON } )
@Path( "/{email}" )
@GET
public @Valid Person getPerson(
        @Length( min = 5, max = 255 ) @PathParam( "email" ) final String email ) {
    return peopleService.getByEmail( email );
}

Есть несколько отличий от традиционного объявления метода JAX-RS 2.0 . Во-первых, нам бы хотелось, чтобы адрес электронной почты (параметр пути электронной почты ) был длиной не менее 5 символов (но не более 255 символов), что определяется аннотацией @Length (min = 5, max = 255) . Во-вторых, мы бы хотели, чтобы этот метод возвращал только правильного человека, поэтому мы пометили возвращаемое значение метода аннотацией @Valid . Эффект @Valid очень интересен: рассматриваемый экземпляр лица будет проверен на предмет всех ограничений проверки, объявленных его классом ( Person ).

В настоящее время Bean Validation 1.1 по умолчанию не активен в ваших проектах Apache CXF, поэтому, если вы запустите свое приложение и вызовете эту конечную точку REST , все ограничения проверки будут просто проигнорированы. Хорошей новостью является то, что активировать Bean Validation 1.1 очень легко, поскольку для ее обычной конфигурации требуется добавить только три компонента (пожалуйста, ознакомьтесь с этой документацией по функциям для получения более подробной информации и расширенной конфигурации):

  • In -inteceptor JAXRSBeanValidationInInterceptor: выполняет проверку входных параметров методов ресурса JAX-RS 2.0
  • Out -inteceptor JAXRSBeanValidationOutInterceptor: выполняет проверку возвращаемых значений методов ресурса JAX-RS 2.0
  • Сопоставитель исключений ValidationExceptionMapper : сопоставляет нарушения проверки с кодами состояния HTTP . Согласно спецификации все нарушения проверки входных параметров приводят к ошибке 400 Bad Request . Соответственно, все нарушения проверки возвращаемых значений приводят к ошибке 500 Internal Server Error . На данный момент ValidationExceptionMapper не включает в ответ дополнительную информацию (так как это может нарушить протокол приложения), но ее можно легко расширить, чтобы предоставить более подробную информацию об ошибках проверки.

Класс AppConfig демонстрирует один из способов соединения всех необходимых компонентов вместе с помощью RuntimeDelegate и JAXRSServerFactoryBean (также поддерживается конфигурация на основе XML).

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
package com.example.config;
 
import java.util.Arrays;
 
import javax.ws.rs.ext.RuntimeDelegate;
 
import org.apache.cxf.bus.spring.SpringBus;
import org.apache.cxf.endpoint.Server;
import org.apache.cxf.interceptor.Interceptor;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.apache.cxf.jaxrs.validation.JAXRSBeanValidationInInterceptor;
import org.apache.cxf.jaxrs.validation.JAXRSBeanValidationOutInterceptor;
import org.apache.cxf.jaxrs.validation.ValidationExceptionMapper;
import org.apache.cxf.message.Message;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
 
import com.example.rs.JaxRsApiApplication;
import com.example.rs.PeopleRestService;
import com.example.services.PeopleService;
import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
 
@Configuration
public class AppConfig {   
    @Bean( destroyMethod = "shutdown" )
    public SpringBus cxf() {
        return new SpringBus();
    }
 
    @Bean @DependsOn( "cxf" )
    public Server jaxRsServer() {
        final JAXRSServerFactoryBean factory =
            RuntimeDelegate.getInstance().createEndpoint(
                jaxRsApiApplication(),
                JAXRSServerFactoryBean.class
            );
        factory.setServiceBeans( Arrays.< Object >asList( peopleRestService() ) );
        factory.setAddress( factory.getAddress() );
        factory.setInInterceptors(
            Arrays.< Interceptor< ? extends Message > >asList(
                new JAXRSBeanValidationInInterceptor()
            )
        );
        factory.setOutInterceptors(
            Arrays.< Interceptor< ? extends Message > >asList(
                new JAXRSBeanValidationOutInterceptor()
            )
        );
        factory.setProviders(
            Arrays.asList(
                new ValidationExceptionMapper(),
                new JacksonJsonProvider()
            )
        );
 
        return factory.create();
    }
 
    @Bean
    public JaxRsApiApplication jaxRsApiApplication() {
        return new JaxRsApiApplication();
    }
 
    @Bean
    public PeopleRestService peopleRestService() {
        return new PeopleRestService();
    }
 
    @Bean
    public PeopleService peopleService() {
        return new PeopleService();
    }
}

Все входящие / исходящие перехватчики и преобразователь исключений вводятся. Отлично, давайте построим проект и запустим сервер, чтобы проверить, что Bean Validation 1.1 активен и работает как положено.

1
2
mvn clean package
java -jar target/jaxrs-2.0-validation-0.0.1-SNAPSHOT.jar

Теперь, если мы выдадим REST- запрос с коротким (или недействительным) адресом электронной почты a @ b , сервер должен вернуть 400 Bad Request . Давайте подтвердим это.

1
2
3
4
5
6
> curl http://localhost:8080/rest/api/people/a@b -i
 
HTTP/1.1 400 Bad Request
Date: Wed, 26 Mar 2014 00:11:59 GMT
Content-Length: 0
Server: Jetty(9.1.z-SNAPSHOT)

Превосходно! Чтобы быть полностью уверенным, мы можем проверить вывод консоли сервера и найти там исключение проверки типа ConstraintViolationException и его трассировки стека. Кроме того, в последней строке приводятся сведения о том, что пошло не так: PeopleRestService.getPerson.arg0: длина должна быть от 5 до 255 (обратите внимание, поскольку имена аргументов в настоящее время недоступны в JVM после компиляции, они заменяются местозаполнителями, такими как arg0 , arg1 …)

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
WARNING: Interceptor for {http://rs.example.com/}PeopleRestService has thrown exception, unwinding now
javax.validation.ConstraintViolationException
        at org.apache.cxf.validation.BeanValidationProvider.validateParameters(BeanValidationProvider.java:119)
        at org.apache.cxf.validation.BeanValidationInInterceptor.handleValidation(BeanValidationInInterceptor.java:59)
        at org.apache.cxf.validation.AbstractValidationInterceptor.handleMessage(AbstractValidationInterceptor.java:73)
        at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:307)
        at org.apache.cxf.transport.ChainInitiationObserver.onMessage(ChainInitiationObserver.java:121)
        at org.apache.cxf.transport.http.AbstractHTTPDestination.invoke(AbstractHTTPDestination.java:240)
        at org.apache.cxf.transport.servlet.ServletController.invokeDestination(ServletController.java:223)
        at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:197)
        at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:149)
        at org.apache.cxf.transport.servlet.CXFNonSpringServlet.invoke(CXFNonSpringServlet.java:167)
        at org.apache.cxf.transport.servlet.AbstractHTTPServlet.handleRequest(AbstractHTTPServlet.java:286)
        at org.apache.cxf.transport.servlet.AbstractHTTPServlet.doGet(AbstractHTTPServlet.java:211)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:687)
        at org.apache.cxf.transport.servlet.AbstractHTTPServlet.service(AbstractHTTPServlet.java:262)
        at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:711)
        at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:552)
        at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1112)
        at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:479)
        at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1046)
        at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
        at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:97)
        at org.eclipse.jetty.server.Server.handle(Server.java:462)
        at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:281)
        at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:232)
        at org.eclipse.jetty.io.AbstractConnection$1.run(AbstractConnection.java:505)
        at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:607)
        at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:536)
        at java.lang.Thread.run(Unknown Source)
 
Mar 25, 2014 8:11:59 PM org.apache.cxf.jaxrs.validation.ValidationExceptionMapper toResponse
WARNING: PeopleRestService.getPerson.arg0: length must be between 5 and 255

Далее мы добавим еще два метода REST для демонстрации коллекций и проверки ответа в действии.

1
2
3
4
5
6
@Produces( { MediaType.APPLICATION_JSON } )
@GET
public @Valid Collection< Person > getPeople(
        @Min( 1 ) @QueryParam( "count" ) @DefaultValue( "1" ) final int count ) {
    return peopleService.getPeople( count );
}

Аннотация @Valid для коллекции объектов гарантирует, что каждый отдельный объект в коллекции является действительным. Параметр count также должен иметь минимальное значение 1 аннотацией @Min (1) ( @DefaultValue учитывается, если параметр запроса не указан). Давайте специально добавим человека без указания имени и фамилии, чтобы результирующая коллекция содержала хотя бы один экземпляр лица, который не должен проходить процесс проверки.

1
> curl http://localhost:8080/rest/api/people -X POST -id "email=a@b3.com"

При этом вызов метода getPeople REST должен вернуть 500 Internal Server Error . Давайте проверим, что это так.

1
2
3
4
5
6
> curl -i http://localhost:8080/rest/api/people?count=10
 
HTTP/1.1 500 Server Error
Date: Wed, 26 Mar 2014 01:28:58 GMT
Content-Length: 0
Server: Jetty(9.1.z-SNAPSHOT)

Глядя на вывод консоли сервера, подсказка, что не так, тут же.

1
2
3
4
Mar 25, 2014 9:28:58 PM org.apache.cxf.jaxrs.validation.ValidationExceptionMapper toResponse
WARNING: PeopleRestService.getPeople.[0].firstName: may not be null
Mar 25, 2014 9:28:58 PM org.apache.cxf.jaxrs.validation.ValidationExceptionMapper toResponse
WARNING: PeopleRestService.getPeople.[0].lastName: may not be null

И, наконец, еще один пример, на этот раз с универсальным объектом Response .

01
02
03
04
05
06
07
08
09
10
11
@Valid
@Produces( { MediaType.APPLICATION_JSON  } )
@POST
public Response addPerson( @Context final UriInfo uriInfo,
        @NotNull @Length( min = 5, max = 255 ) @FormParam( "email" ) final String email,
        @FormParam( "firstName" ) final String firstName,
        @FormParam( "lastName" ) final String lastName ) {       
    final Person person = peopleService.addPerson( email, firstName, lastName );
    return Response.created( uriInfo.getRequestUriBuilder().path( email ).build() )
        .entity( person ).build();
}

Последний пример немного сложен: класс Response является частью API JAX-RS 2.0 и не имеет определенных ограничений проверки. Таким образом, наложение каких-либо правил проверки на экземпляр этого класса не вызовет никаких нарушений. Но Apache CXF старается изо всех сил и выполняет простой, но полезный трюк: вместо экземпляра Response будет проверяться сущность ответа. Мы можем легко проверить это, пытаясь создать человека без указания имени и фамилии: ожидаемый результат должен быть 500 Internal Server Error .

1
2
3
4
5
6
> curl http://localhost:8080/rest/api/people -X POST -id "email=a@b3.com"
 
HTTP/1.1 500 Server Error
Date: Wed, 26 Mar 2014 01:13:06 GMT
Content-Length: 0
Server: Jetty(9.1.z-SNAPSHOT)

И вывод консоли сервера более подробный:

1
2
3
4
Mar 25, 2014 9:13:06 PM org.apache.cxf.jaxrs.validation.ValidationExceptionMapper toResponse
WARNING: PeopleRestService.addPerson.<return value>.firstName: may not be null
Mar 25, 2014 9:13:06 PM org.apache.cxf.jaxrs.validation.ValidationExceptionMapper toResponse
WARNING: PeopleRestService.addPerson.<return value>.lastName: may not be null

Ницца! В этом сообщении мы только что коснулись темы о том, как Bean Validation 1.1 может улучшить ваши проекты Apache CXF , предоставляя такую ​​богатую и расширяемую поддержку декларативной проверки. Обязательно попробуйте!

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