Introdução

Às vezes, principalmente em grandes projetos, há uma necessidade de controlar a serialização de uma resposta de serviço - especialmente para filtrar campos de um objeto em resposta. Para resolver este tipo de problema, temos várias soluções. Vamos dar uma olhada.

Problema

Por exemplo, temos um objeto chamado “User” que contém campos como: id, email, fullName, password, secretKey. Temos uma tarefa para retornar esse objeto para um destinatário com campos diferentes, dependendo da permissão na sessão. Além disso, temos dois tipos de permissões: admin e user:

  1. Quando o destinatário tem uma função “admin”, devemos retornar o objeto completo com os campos: id, email, fullName, password, secretKey.
  2. Quando um destinatário tem uma função “user”, devemos retornar um objeto parcialmente com os campos: email, fullName.

Soluções Padrão

  1. Herdar duas classes diferentes com um conjunto diferente de campos da classe User e chamá-las como: UserAdmin, UserSimple.
  2. Redefinir campos da classe User em resposta ao método, dependendo da função.
  3. Em seguida, retorne o destinatário para uma sequência serializada com campos removidos, dependendo da função, em vez da classe User.

Talvez esta solução pareça simples e justificada, mas não é a melhor. Em algumas situações, aumentará o número de classes semelhantes ou levará à imprevisibilidade da execução do serviço. Mas em todas as soluções, precisamos criar código adicional.

Solução JFilter

Essa abordagem oferece outra solução. Podemos deixar a resposta do método do serviço inalterada. Mas precisamos de alguma forma informar ao conversor de mensagens do Spring. Para que ele saiba como serializar um objeto e filtro/excluir campos, o módulo JFilter foi projetado para resolver esse tipo de problema.

Vamos ver o diagrama.

Como você pode ver, o módulo JFilter é incorporado entre os métodos Spring Web Service e Send response. A descrição das partes internas do módulo inclui:

  1. FilterAdvice - é um componente ControllerAdvice que manipula todas as respostas do Spring Web Service.
  2. FilterProvider - é um componente que tenta encontrar um filtro adequado.
  3. FilterConverter - é uma classe que tenta fornecer um serializador JSON ou XML adequado.
  4. Após a serialização, a resposta será gravada no corpo HttpOutputMessage

Usando

Antes de discutir os tipos de filtro, devemos saber como usá-los em seu projeto. Então, primeiro de tudo, precisamos importar o módulo:

<dependency>
    <groupId>com.github.rkonovalov</groupId>
    <artifactId>json-ignore</artifactId>
    <version>1.0.8</version>
</dependency>

Se você estiver usando outras ferramentas de automação, você pode encontrar uma declaração seguindo este endereço.

O próximo passo é habilitar o filtro:

@ComponentScan({"com.jfilter.components"})
@EnableJsonFilter

Essa anotação deve ser declarada no componente de configuração do Spring Web Service. O código a seguir mostra como você pode declará-lo:

@Configuration
@EnableWebMvc
@ComponentScan({"com.jfilter.components"})
@EnableJsonFilter
public class AppConfig extends WebMvcConfigurerAdapter {

}

Depois desses dois passos simples, podemos começar a explorar os tipos de filtro. Mas antes disso, também precisamos definir um componente de Spring Rest de amostra com o método de resposta:

@RestController
public class SessionService {
    @Autowired
    private UserController userController;   
    @RequestMapping(value = "/users/signIn",
            params = {"email", "password"}, method = RequestMethod.POST,
            consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE},
            produces = {MediaType.APPLICATION_JSON_VALUE})            
    public User signIn(@RequestParam("email") String email, @RequestParam("password") String password) {
        return userController.signInUser(email, password);
    }
}

Este método retorna a instância do objeto User. Gerando o JSON que será enviado ao destinatário.

{
  "id": 10,
  "email": "janedoe@gmail.com", 
  "fullName": "Jane Doe",
  "password": "12345",
  "secretKey": "54321",
  "address": {
    "id": 15,
    "apartmentNumber": 22,
    "street": {
      "id": 155,
      "streetName": "Bourbon Street",
      "streetNumber": 15
    }
  }
}

É isso aí. Agora, podemos declarar as anotações de filtro.

Filtros

Os filtros analisam principalmente o Spring Service Response por uma anotação de filtro e, se a anotação estiver configurada, ela tentará gerar uma lista de campos filtráveis/de exclusão do objeto de resposta. Vamos olhá-los um pouco mais de perto.

Filtro de campo

Esta é uma anotação de filtro simples na qual você pode definir campos filtráveis de um objeto.

Exemplo de anotação

@FieldFilterSetting(className = User.class, fields = {"id", "password", "secretKey"})

Exempo de código

@FieldFilterSetting(className = User.class, fields = {"id", "password", "secretKey"})
    @RequestMapping(value = "/users/signIn",
            params = {"email", "password"}, method = RequestMethod.POST,
            consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE},
            produces = {MediaType.APPLICATION_JSON_VALUE})            
    public User signIn(@RequestParam("email") String email, @RequestParam("password") String password) {
        return userController.signInUser(email, password);
    }

Resultado

{ 
  "email": "janedoe@gmail.com", 
  "fullName": "Jane Doe",
  "address": {
    "id": 15,
    "apartmentNumber": 22,
    "street": {
      "id": 155,
      "streetName": "Bourbon Street",
      "streetNumber": 15
    }
  }
}

Como você pode ver, os campos id, password e secretKey foram filtrados com sucesso da resposta.

Se você quiser filtrar campos de uma classe complexa que contenha subclasses com os mesmos nomes de campos, será possível filtrá-los sem qualificações para um determinado nome de classe.

Exemplo de anotação

@FieldFilterSetting(fields = {"id", "password", "secretKey"})

Resultado

{ 
  "email": "janedoe@gmail.com", 
  "fullName": "Jane Doe",
  "address": {
    "apartmentNumber": 22,
    "street": {
      "streetName": "Bourbon Street",
      "streetNumber": 15
    }
  }
}

Como você pode ver, todos os campos id de todos os sub-objetos foram filtrados. Além disso, você pode filtrar campos de sub-objetos separadamente.

Exemplo de anotação

@FieldFilterSetting(className = User.class, fields = {"id", "password", "secretKey"})
@FieldFilterSetting(className = Address.class, fields = {"apartmentNumber"})
@FieldFilterSetting(className = Street.class, fields = {"streetNumber"})

Resultado

{
  "email": "janedoe@gmail.com", 
  "fullName": "Jane Doe",
  "address": {
    "id": 15,
    "street": {
      "id": 155,
      "streetName": "Bourbon Street"
    }
  }
}

Estratégia de filtro por sessão

Às vezes, você precisa filtrar os campos dependendo da função do destinatário (atributo na Sessão HTTP). Por exemplo, você tem duas funções de destinatário: ADMIN e USER

Exemplo de anotação com perfil USER

@SessionStrategy(attributeName = "ROLE", attributeValue = "USER", ignoreFields = {
            @FieldFilterSetting(className = User.class, fields = {"id", "password"})
    })

Exemplo de anotação com perfil ADMIN

@SessionStrategy(attributeName = "ROLE", attributeValue = "ADMIN", ignoreFields = {
                @FieldFilterSetting(className = User.class, fields = {"id"})
    })

Então, se a sessão tiver um atributo ROLE com o valor>

  1. USER - campos id, password será removido do resultado.
  2. ADMIN - o campo id será removido do resultado.

Assim, este filtro oferece flexibilidade e possibilidade de filtragem seletiva.

Filtro XML Schema-Based

Se você precisar configurar a filtragem de campo em um arquivo, esse filtro poderá fornecer esse recurso.

Exemplo de anotação

@FileFilterSetting(fileName = "filter_configuration.xml")

Arquivo XML de configuração

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE config PUBLIC
        "-//json/json-ignore mapping DTD 1.0//EN"
        "https://rkonovalov.github.io/json-ignore-schema-1.0.dtd">
<config>
    <controller class-name="com.example.SessionService">
        <strategy attribute-name="ROLE" attribute-value="USER">
            <filter class="com.example.User">
                <field name="password"/>
            </filter>
        </strategy>
    </controller>
</config>

Descrição das tags do XML

Nota: os arquivos de configuração podem ser alterados após o início do aplicativo e o controlador irá recarregar corretamente todos os arquivos alterados. Não se preocupe!

Filtragem no Spring Controller

Se você não quiser adicionar anotações de filtro a cada Service Response, poderá adicionar uma anotação em todo o Service Controller.

Exemplo de anotação

@FileFilterSetting(fileName = "filter_configuration.xml")
@RestController
public class SessionService {
    @Autowired
    private UserController userController;  
    @RequestMapping(value = "/users/signIn",
            params = {"email", "password"}, method = RequestMethod.POST,
            consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE},
            produces = {MediaType.APPLICATION_JSON_VALUE})            
    public User signIn(@RequestParam("email") String email, @RequestParam("password") String password) {
        return userController.signInUser(email, password);
    }
}

Você pode declarar todos os filtros necessários.

Filtro dinâmico

Usando esse tipo de filtro, você pode criar seu próprio filtro com um comportamento personalizado. Todos os filtros dinâmicos devem implementar o DynamicFilterEvent.class, e o filtro deve ser anotado pela anotaçãoDynamicFilterComponent.

Exemplo de filtro personalizado dinâmico

@DynamicFilterComponent
    public class DemoIdFilter implements DynamicFilterEvent {
        @Override
        public FilterFields onGetFilterFields(MethodParameter methodParameter, RequestSession request) {
            if(request.getSession().getAttribute("SOME_VALUE") != null) {
                return new FilterFields(User.class, Arrays.asList("id", "password", "email"));
            } else
                return new FilterFields();
        }
    }

In this example, we created the DemoIdFilter that attempts to find the attribute SOME_VALUE in an Http Session. If the attribute exists, the event onGetFilterFields returns a configured FilterFields. As you can see, FilterFields contains a filter for the User.class. If the Response Body of the Spring Service contains an object instance of the User.class, it will be filtered and the fields id, password, email will be removed from the response. Neste exemplo, criamos o DemoIdFilter que tenta encontrar o atributoSOME_VALUE em uma sessão Http. Se o atributo existir, o evento onGetFilterFields retornará um FilterFields configurado. Como você pode ver, FilterFields contém um filtro para o User.class. Se a resposta do Spring Service contiver uma instância de objeto do User.class, ele será filtrado e os campos id, password, email serão removidos da resposta.

Exemplo de anotação

@DynamicFilter(DemoIdFilter.class)
    @RequestMapping(value = "/users/signIn",
            params = {"email", "password"}, method = RequestMethod.POST,
            consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE},
            produces = {MediaType.APPLICATION_JSON_VALUE})            
    public User signIn(@RequestParam("email") String email, @RequestParam("password") String password) {
        return userController.signInUser(email, password);
    }

Com esse filtro, você obtém uma grande personalização do Service Response.

Verifique estes endereços para aprender mais:

Conclusão

No mundo dos big data e dos sistemas complexos, às vezes precisamos controlar e alterar os dados com flexibilidade. Talvez esta solução não seja a melhor, mas espero que você economize um tempo extra. Você pode usá-lo em seus projetos sem a “dança da chuva” e ainda conseguir o resultado desejado.