Suporte do Apache CXF para serviços da Web RESTful

Suporte do Apache CXF para serviços da Web RESTful

1. Visão geral

Este tutorial apresentaApache CXF como uma estrutura compatível com o padrão JAX-RS, que define o suporte do ecossistema Java para o padrão de arquitetura REpresentational State Transfer (REST).

Especificamente, descreve passo a passo como construir e publicar um serviço da Web RESTful e como gravar testes de unidade para verificar um serviço.

Este é o terceiro de uma série no Apache CXF; the first one enfoca o uso de CXF como uma implementação JAX-WS totalmente compatível. Osecond article fornece um guia sobre como usar o CXF com Spring.

2. Dependências do Maven

A primeira dependência necessária éorg.apache.cxf:cxf-rt-frontend-jaxrs. This artifact provides JAX-RS APIs as well as a CXF implementation:


    org.apache.cxf
    cxf-rt-frontend-jaxrs
    3.1.7

Neste tutorial, usamos o CXF para criar um endpointServer para publicar um serviço da web em vez de usar um contêiner de servlet. Portanto, a seguinte dependência precisa ser incluída no arquivo Maven POM:


    org.apache.cxf
    cxf-rt-transports-http-jetty
    3.1.7

Finalmente, vamos adicionar a biblioteca HttpClient para facilitar os testes de unidade:


    org.apache.httpcomponents
    httpclient
    4.5.2

Here você pode encontrar a versão mais recente da dependênciacxf-rt-frontend-jaxrs. Você também pode querer consultarthis link para as versões mais recentes dos artefatosorg.apache.cxf:cxf-rt-transports-http-jetty. Finalmente, a versão mais recente dehttpclient pode ser encontradahere.

3. Classes de recursos e mapeamento de solicitações

Vamos começar a implementar um exemplo simples; vamos configurar nossa API REST com dois recursosCourseeStudent.

Começaremos de forma simples e avançaremos para um exemplo mais complexo conforme avançamos.

3.1. Os recursos

Aqui está a definição da classe de recursoStudent:

@XmlRootElement(name = "Student")
public class Student {
    private int id;
    private String name;

    // standard getters and setters
    // standard equals and hashCode implementations

}

Observe que estamos usando a anotação@XmlRootElement para dizer ao JAXB que as instâncias desta classe devem ser empacotadas para XML.

A seguir, vem a definição da classe de recursoCourse:

@XmlRootElement(name = "Course")
public class Course {
    private int id;
    private String name;
    private List students = new ArrayList<>();

    private Student findById(int id) {
        for (Student student : students) {
            if (student.getId() == id) {
                return student;
            }
        }
        return null;
    }
    // standard getters and setters
    // standard equals and hasCode implementations

}

Finalmente, vamos implementarCourseRepository - que é o recurso raiz e serve como ponto de entrada para recursos de serviço da web:

@Path("course")
@Produces("text/xml")
public class CourseRepository {
    private Map courses = new HashMap<>();

    // request handling methods

    private Course findById(int id) {
        for (Map.Entry course : courses.entrySet()) {
            if (course.getKey() == id) {
                return course.getValue();
            }
        }
        return null;
    }
}

Observe o mapeamento com a anotação@Path. OCourseRepository é o recurso raiz aqui, então ele é mapeado para lidar com todos os URLS começando comcourse.

O valor da anotação@Produces é usado para dizer ao servidor para converter objetos retornados de métodos dentro desta classe em documentos XML antes de enviá-los aos clientes. Estamos usando JAXB aqui como o padrão, uma vez que nenhum outro mecanismo de ligação é especificado.

3.2. Configuração Simples de Dados

Como este é um exemplo de implementação simples, estamos usando dados na memória em vez de uma solução persistente completa.

Com isso em mente, vamos implementar alguma lógica de configuração simples para preencher alguns dados no sistema:

{
    Student student1 = new Student();
    Student student2 = new Student();
    student1.setId(1);
    student1.setName("Student A");
    student2.setId(2);
    student2.setName("Student B");

    List course1Students = new ArrayList<>();
    course1Students.add(student1);
    course1Students.add(student2);

    Course course1 = new Course();
    Course course2 = new Course();
    course1.setId(1);
    course1.setName("REST with Spring");
    course1.setStudents(course1Students);
    course2.setId(2);
    course2.setName("Learn Spring Security");

    courses.put(1, course1);
    courses.put(2, course2);
}

Os métodos desta classe que cuidam de solicitações HTTP são abordados na próxima subseção.

3.3. A API - Métodos de mapeamento de solicitação

Agora, vamos para a implementação da API REST real.

Vamos começar a adicionar operações de API - usando a anotação@Path - direto nos POJOs de recursos.

É importante entender que é uma diferença significativa da abordagem em um projeto Spring típico - onde as operações da API seriam definidas em um controlador, não no próprio POJO.

Vamos começar com métodos de mapeamento definidos dentro da classeCourse:

@GET
@Path("{studentId}")
public Student getStudent(@PathParam("studentId")int studentId) {
    return findById(studentId);
}

Simplificando, o método é invocado ao lidar com solicitaçõesGET, denotadas pela anotação@GET.

Observe a sintaxe simples de mapeamento do parâmetro de caminhostudentId da solicitação HTTP.

Estamos simplesmente usando o método auxiliarfindById para retornar a instânciaStudent correspondente.

O método a seguir lida comPOST solicitações, indicadas pela anotação@POST, adicionando o objetoStudent recebido à listastudents:

@POST
@Path("")
public Response createStudent(Student student) {
    for (Student element : students) {
        if (element.getId() == student.getId() {
            return Response.status(Response.Status.CONFLICT).build();
        }
    }
    students.add(student);
    return Response.ok(student).build();
}

Isso retorna uma resposta200 OK se a operação de criação foi bem-sucedida, ou409 Conflict se um objeto com oid enviado já existe.

Observe também que podemos pular a anotação@Path, pois seu valor é uma String vazia.

O último método cuida das solicitações deDELETE. Ele remove um elemento da listastudents cujoid é o parâmetro do caminho recebido e retorna uma resposta com o statusOK (200). Caso não haja elementos associados aoid especificado, o que implica que não há nada a ser removido, este método retorna uma resposta com o statusNot Found (404):

@DELETE
@Path("{studentId}")
public Response deleteStudent(@PathParam("studentId") int studentId) {
    Student student = findById(studentId);
    if (student == null) {
        return Response.status(Response.Status.NOT_FOUND).build();
    }
    students.remove(student);
    return Response.ok().build();
}

Vamos prosseguir para solicitar métodos de mapeamento da classeCourseRepository.

O métodogetCourse a seguir retorna um objetoCourse que é o valor de uma entrada no mapacourses cuja chave é o parâmetro de caminhocourseId recebido de umGET solicitação. Internamente, o método despacha parâmetros de caminho para o método auxiliarfindById para fazer seu trabalho.

@GET
@Path("courses/{courseId}")
public Course getCourse(@PathParam("courseId") int courseId) {
    return findById(courseId);
}

O método a seguir atualiza uma entrada existente do mapacourses, onde o corpo da solicitaçãoPUT recebida é o valor da entrada e o parâmetrocourseId é a chave associada:

@PUT
@Path("courses/{courseId}")
public Response updateCourse(@PathParam("courseId") int courseId, Course course) {
    Course existingCourse = findById(courseId);
    if (existingCourse == null) {
        return Response.status(Response.Status.NOT_FOUND).build();
    }
    if (existingCourse.equals(course)) {
        return Response.notModified().build();
    }
    courses.put(courseId, course);
    return Response.ok().build();
}

Este métodoupdateCourse retorna uma resposta com o statusOK (200) se a atualização for bem-sucedida, não muda nada e retorna uma respostaNot Modified (304) se os objetos existentes e carregados têm o mesmos valores de campo. No caso de uma instânciaCourse com oid fornecido não ser encontrada no mapacourses, o método retorna uma resposta com o statusNot Found (404).

O terceiro método dessa classe de recurso raiz não manipula diretamente nenhuma solicitação HTTP. Em vez disso, ele delega solicitações para a classeCourse, onde as solicitações são tratadas por métodos de correspondência:

@Path("courses/{courseId}/students")
public Course pathToStudent(@PathParam("courseId") int courseId) {
    return findById(courseId);
}

Mostramos métodos dentro da classeCourse que processam solicitações delegadas logo antes.

4. Server Endpoint

Esta seção se concentra na construção de um servidor CXF, usado para publicar o serviço da web RESTful cujos recursos estão representados na seção anterior. A primeira etapa é instanciar um objetoJAXRSServerFactoryBean e definir a classe de recurso raiz:

JAXRSServerFactoryBean factoryBean = new JAXRSServerFactoryBean();
factoryBean.setResourceClasses(CourseRepository.class);

Um provedor de recursos precisa ser configurado no bean de fábrica para gerenciar o ciclo de vida da classe de recurso raiz. Usamos o provedor de recursos singleton padrão que retorna a mesma instância de recurso para cada solicitação:

factoryBean.setResourceProvider(
  new SingletonResourceProvider(new CourseRepository()));

Também definimos um endereço para indicar o URL em que o serviço da web é publicado:

factoryBean.setAddress("http://localhost:8080/");

Agora ofactoryBean pode ser usado para criar um novoserver que começará a escutar as conexões de entrada:

Server server = factoryBean.create();

Todo o código acima nesta seção deve ser agrupado no métodomain:

public class RestfulServer {
    public static void main(String args[]) throws Exception {
        // code snippets shown above
    }
}

A invocação deste métodomain é apresentada na seção 6.

5. Casos de teste

Esta seção descreve os casos de teste usados ​​para validar o serviço da web que criamos anteriormente. Esses testes validam os estados dos recursos do serviço após responder às solicitações HTTP dos quatro métodos mais comumente usados, a saberGET,POST,PUT eDELETE.

5.1. Preparação

Primeiro, dois campos estáticos são declarados na classe de teste, denominadosRestfulTest:

private static String BASE_URL = "http://localhost:8080/example/courses/";
private static CloseableHttpClient client;

Antes de executar os testes, criamos um objetoclient, que é usado para se comunicar com o servidor e destruí-lo posteriormente:

@BeforeClass
public static void createClient() {
    client = HttpClients.createDefault();
}

@AfterClass
public static void closeClient() throws IOException {
    client.close();
}

A instânciaclient agora está pronta para ser usada por casos de teste.

5.2. GET solicitações

Na classe de teste, definimos dois métodos para enviar solicitaçõesGET para o servidor que executa o serviço da web.

O primeiro método é obter uma instânciaCourse dado seuid no recurso:

private Course getCourse(int courseOrder) throws IOException {
    URL url = new URL(BASE_URL + courseOrder);
    InputStream input = url.openStream();
    Course course
      = JAXB.unmarshal(new InputStreamReader(input), Course.class);
    return course;
}

A segunda é obter uma instânciaStudent, dado oids do curso e do aluno no recurso:

private Student getStudent(int courseOrder, int studentOrder)
  throws IOException {
    URL url = new URL(BASE_URL + courseOrder + "/students/" + studentOrder);
    InputStream input = url.openStream();
    Student student
      = JAXB.unmarshal(new InputStreamReader(input), Student.class);
    return student;
}

Esses métodos enviam solicitações HTTPGET para o recurso de serviço e, em seguida, desempacotam respostas XML para instâncias das classes correspondentes. Ambos são usados ​​para verificar os estados dos recursos do serviço após a execução das solicitaçõesPOST,PUT eDELETE.

5.3. POST solicitações

Esta subseção apresenta dois casos de teste para solicitaçõesPOST, ilustrando as operações do serviço da web quando a instânciaStudent carregada leva a um conflito e quando é criada com sucesso.

No primeiro teste, usamos um objetoStudent desempacotado do arquivoconflict_student.xml, localizado no classpath com o seguinte conteúdo:


    2
    Student B

É assim que esse conteúdo é convertido em um corpo de solicitaçãoPOST:

HttpPost httpPost = new HttpPost(BASE_URL + "1/students");
InputStream resourceStream = this.getClass().getClassLoader()
  .getResourceAsStream("conflict_student.xml");
httpPost.setEntity(new InputStreamEntity(resourceStream));

O cabeçalhoContent-Type é definido para informar ao servidor que o tipo de conteúdo da solicitação é XML:

httpPost.setHeader("Content-Type", "text/xml");

Como o objetoStudent carregado já existe na primeira instânciaCourse, esperamos que a criação falhe e uma resposta com o statusConflict (409) seja retornada. O seguinte trecho de código verifica a expectativa:

HttpResponse response = client.execute(httpPost);
assertEquals(409, response.getStatusLine().getStatusCode());

No próximo teste, extraímos o corpo de uma solicitação HTTP de um arquivo denominadocreated_student.xml, também no classpath. Aqui está o conteúdo do arquivo:


    3
    Student C

Semelhante ao caso de teste anterior, criamos e executamos uma solicitação e, em seguida, verificamos se uma nova instância foi criada com sucesso:

HttpPost httpPost = new HttpPost(BASE_URL + "2/students");
InputStream resourceStream = this.getClass().getClassLoader()
  .getResourceAsStream("created_student.xml");
httpPost.setEntity(new InputStreamEntity(resourceStream));
httpPost.setHeader("Content-Type", "text/xml");

HttpResponse response = client.execute(httpPost);
assertEquals(200, response.getStatusLine().getStatusCode());

Podemos confirmar novos estados do recurso de serviço da web:

Student student = getStudent(2, 3);
assertEquals(3, student.getId());
assertEquals("Student C", student.getName());

Esta é a aparência da resposta XML a uma solicitação para o novo objetoStudent:



    3
    Student C

5.4. PUT solicitações

Vamos começar com uma solicitação de atualização inválida, onde o objetoCourse sendo atualizado não existe. Aqui está o conteúdo da instância usada para substituir um objetoCourse não existente no recurso de serviço da web:


    3
    Apache CXF Support for RESTful

Esse conteúdo é armazenado em um arquivo chamadonon_existent_course.xml no caminho de classe. Ele é extraído e usado para preencher o corpo de uma solicitaçãoPUT pelo código abaixo:

HttpPut httpPut = new HttpPut(BASE_URL + "3");
InputStream resourceStream = this.getClass().getClassLoader()
  .getResourceAsStream("non_existent_course.xml");
httpPut.setEntity(new InputStreamEntity(resourceStream));

O cabeçalhoContent-Type é definido para informar ao servidor que o tipo de conteúdo da solicitação é XML:

httpPut.setHeader("Content-Type", "text/xml");

Como enviamos intencionalmente uma solicitação inválida para atualizar um objeto inexistente, espera-se que uma respostaNot Found (404) seja recebida. A resposta é validada:

HttpResponse response = client.execute(httpPut);
assertEquals(404, response.getStatusLine().getStatusCode());

No segundo caso de teste para solicitaçõesPUT, enviamos um objetoCourse com os mesmos valores de campo. Visto que nada é alterado neste caso, esperamos que uma resposta com o statusNot Modified (304) seja retornada. Todo o processo é ilustrado:

HttpPut httpPut = new HttpPut(BASE_URL + "1");
InputStream resourceStream = this.getClass().getClassLoader()
  .getResourceAsStream("unchanged_course.xml");
httpPut.setEntity(new InputStreamEntity(resourceStream));
httpPut.setHeader("Content-Type", "text/xml");

HttpResponse response = client.execute(httpPut);
assertEquals(304, response.getStatusLine().getStatusCode());

Ondeunchanged_course.xml é o arquivo no caminho de classe que mantém as informações usadas para atualizar. Aqui está o seu conteúdo:


    1
    REST with Spring

Na última demonstração das solicitaçõesPUT, executamos uma atualização válida. A seguir está o conteúdo do arquivochanged_course.xml cujo conteúdo é usado para atualizar uma instânciaCourse no recurso de serviço da web:


    2
    Apache CXF Support for RESTful

É assim que a solicitação é criada e executada:

HttpPut httpPut = new HttpPut(BASE_URL + "2");
InputStream resourceStream = this.getClass().getClassLoader()
  .getResourceAsStream("changed_course.xml");
httpPut.setEntity(new InputStreamEntity(resourceStream));
httpPut.setHeader("Content-Type", "text/xml");

Vamos validar uma solicitaçãoPUT para o servidor e validar um upload bem-sucedido:

HttpResponse response = client.execute(httpPut);
assertEquals(200, response.getStatusLine().getStatusCode());

Vamos verificar os novos estados do recurso de serviço da web:

Course course = getCourse(2);
assertEquals(2, course.getId());
assertEquals("Apache CXF Support for RESTful", course.getName());

O seguinte snippet de código mostra o conteúdo da resposta XML quando uma solicitação GET para o objetoCourse carregado anteriormente é enviada:



    2
    Apache CXF Support for RESTful

5.5. DELETE solicitações

Primeiro, vamos tentar excluir uma instânciaStudent inexistente. A operação deve falhar e uma resposta correspondente com o statusNot Found (404) é esperada:

HttpDelete httpDelete = new HttpDelete(BASE_URL + "1/students/3");
HttpResponse response = client.execute(httpDelete);
assertEquals(404, response.getStatusLine().getStatusCode());

No segundo caso de teste paraDELETE solicitações, criamos, executamos e verificamos uma solicitação:

HttpDelete httpDelete = new HttpDelete(BASE_URL + "1/students/1");
HttpResponse response = client.execute(httpDelete);
assertEquals(200, response.getStatusLine().getStatusCode());

Verificamos novos estados do recurso de serviço da web com o seguinte snippet de código:

Course course = getCourse(1);
assertEquals(1, course.getStudents().size());
assertEquals(2, course.getStudents().get(0).getId());
assertEquals("Student B", course.getStudents().get(0).getName());

A seguir, listamos a resposta XML que é recebida após uma solicitação para o primeiro objetoCourse no recurso de serviço da web:



    1
    REST with Spring
    
        2
        Student B
    

É claro que o primeiroStudent foi removido com sucesso.

6. Execução de Teste

A seção 4 descreveu como criar e destruir uma instânciaServer no métodomain da classeRestfulServer.

A última etapa para colocar o servidor em funcionamento é invocar esse métodomain. Para conseguir isso, o plug-in Exec Maven é incluído e configurado no arquivo Maven POM:


    org.codehaus.mojo
    exec-maven-plugin
    1.5.0
    
        
          com.example.cxf.jaxrs.implementation.RestfulServer
        
    

A última versão deste plugin pode ser encontrada emthis link.

No processo de compilar e empacotar o artefato ilustrado neste tutorial, o plugin Maven Surefire executa automaticamente todos os testes incluídos em classes com nomes que começam ou terminam comTest. Se for esse o caso, o plug-in deve ser configurado para excluir esses testes:


    maven-surefire-plugin
    2.19.1
    
    
        **/ServiceTest
    
    

Com a configuração acima,ServiceTest é excluído, pois é o nome da classe de teste. Você pode escolher qualquer nome para essa classe, desde que os testes contidos nela não sejam executados pelo plug-in Maven Surefire antes que o servidor esteja pronto para as conexões.

Para a última versão do plugin Maven Surefire, por favor, verifiquehere.

Agora você pode executar a metaexec:java para iniciar o servidor de serviço da Web RESTful e, em seguida, executar os testes acima usando um IDE. Da mesma forma, você pode iniciar o teste executando o comandomvn -Dtest=ServiceTest test in a terminal.

7. Conclusão

Este tutorial ilustrou o uso do Apache CXF como uma implementação JAX-RS. Ele demonstrou como a estrutura poderia ser usada para definir recursos para um serviço da Web RESTful e criar um servidor para publicar o serviço.

A implementação de todos esses exemplos e trechos de código pode ser encontrada emthe GitHub project.