Back-End

18 fev, 2016

Aquisição de serviços OSGi em Java 8

Publicidade

Ao realizar algumas limpezas de código em Eclipse, Lars fez a observação “Isso parece desnecessariamente difícil – por que não existe uma API simples para isso?”.

O código em questão foi a aquisição de uma instância de DebugOptions que é usada literalmente durante todo tempo de execução do Eclipse para determinar se uma opção está presente para permitir a depuração. Na verdade, o uso de DebugOptions em si não era exatamente o problema (embora o getBooleanOption seja incapaz de determinar se é ou não um valor booleano que está presente ou é false – mas isso é outro bug). O problema é que olhar para um serviço OSGi é mais que um single-line, e como tal, é o tipo de coisa que provavelmente causa mais dor do que realmente precisa.

Há três maneiras de obter serviços em OSGi, na ordem inversa da facilidade de uso:

  • Adquira BundleContext (como por via BundleActivator ou por meio de um manipulador como FrameworkUtil e, em seguida, usar getService() diretamente.
  • Use um ServiceTracker para manter um cache dos serviços disponíveis para retorno rápido.
  • Use serviços declarativos para instanciar o componente e ter os serviços injetados diretamente.

O problema com o uso de serviços declarativos é que você deu não só a aquisição do serviço, mas também a vida útil do componente. Ele também impede o uso de métodos ou integração com outras APIs estáticas que esperam para gerenciar a criação do objeto (ou, na verdade, usam a criação do objeto com constructors ou padrões de construtor).

Os outros dois fazem uso de uma quantidade não-trivial de código para executar, tudo para satisfazer um pedido do tipo “Por favor me dê uma instância’.

Felizmente, o Java 8 fornece uma maneira simples de abstrair isso; utilizando o Supplier de interface. Um supplier é algo que, quando perguntado, retorna uma instância de um determinado pedido. Também é usado em um número de diferentes coleções para adiar a aquisição do objeto até que seja necessário. Isso se encaixa com o que estamos tentando fazer – obter uma instância de um serviço. Então, como isso pode se parecer em OSGi?

public class OSGiTracker<T> implements AutoClosable, Supplier<T> {
  private final ServiceTracker<T,T> serviceTracker;
  private boolean closed = true;
  private OSGiTracker(Class<T> target, Class<?> source) {
    if (target == null) {
      throw new IllegalArgumentException("Target cannot be null");
    }
    if (source == null) {
      throw new IllegalArgumentException("Source cannot be null");
    }
    Bundle bundle = FrameworkUtil.getBundle(source);
    BundleContext context = bundle == null ? null : bundle.getBundleContext();
    if (context == null) {
      throw new IllegalArgumentException(
       "Unable to acquire bundle context for " + source.getCanonicalName());
    }
    this.serviceTracker = new ServiceTracker<T,T>(context,target,null);
  }
  public static <T> OSGiTracker<T> supply(Class<T> target, Class<?> source) {
    return new OSGiTracker<>(target, source);
  }
  @Override
  public T get() {
    if(closed) {
      serviceTracker.open();
      closed = false;
    }
    return serviceTracker.getService();
  }
  protected void finalize() throws Throwable {
    close();
    super.finalize();
  }
  public void close() throws Exception {
    if(serviceTracker != null && !closed) {
      serviceTracker.close();
    }
  }
}

Isso proporciona um Supplier de um dado serviço, e os dois únicos parâmetros necessários são o alvo genérico necessário (por exemplo, DebugOptions) e a classe de chamada (de modo que o BundleContext possa ser resolvido). A API não fica muito mais simples do que isso. Assim é como ela se parece quando podemos usá-la:

private final Supplier<DebugOptions> options =
 OSGiTracker.supply(DebugOptions.class, getClass());

private final boolean DEBUG = options.get().getBooleanOption(...)

Não é preciso se preocupar com outras dependências da classe OSGiTracker, e a API do lado do cliente é trivial.

No entanto, na maioria dos casos não há necessidade de manter um ServiceTracker esperando, em uma vez que você esteja usando-o em uma base one-shot. Se o serviço não estiver lá, você quer usar um padrão, sem alterar qualquer outra coisa. Como resultado, há uma implementação alternativa que pode ser utilizada:

public class OSGiSupplier<T> implements Supplier<T> {
  private final BundleContext context;
  private final ServiceReference<T> serviceReference;
  private OSGiSupplier(Class<T> target, Class<?> source) {
    if (target == null) {
      throw new IllegalArgumentException("Target cannot be null");
    }
    if (source == null) {
      throw new IllegalArgumentException("Source cannot be null");
    }
    Bundle bundle = FrameworkUtil.getBundle(source);
    BundleContext context = bundle == null ? null : bundle.getBundleContext();
    if (context == null) {
      throw new IllegalArgumentException(
       "Unable to acquire bundle context for " + source.getCanonicalName());
    }
    this.context = context;
    this.serviceReference = context.getServiceReference(target);
  }
  public static <T> OSGiSupplier<T> supply(Class<T> target, Class<?> source) {
    return new OSGiSupplier<>(target, source);
  }
  @Override
  public T get() {
    try {
      T service = context.getService(serviceReference);
      if (service != null) {
        context.ungetService(serviceReference);
      }
      return service;
    } catch (Throwable t) {
      return null;
    }
  }
}

A API para usar é quase idêntica:

private final Supplier<DebugOptions> options =
 OSGiSupplier.supply(DebugOptions.class, getClass());

private final boolean DEBUG = options.get().getBooleanOption(...)

Vê a diferença? É o tipo de supplier que estamos usando. Caso contrário, o tipo de campo e seu caso de uso são idênticos. Isso torna mais fácil alternar entre os dois; na verdade, nós poderíamos quebrar isso em outro supplier se quiséssemos que isso fosse o padrão para tudo que fosse false ou null.

public class DebugOptionsWrapper implements Supplier<DebugOptions> {
  private final Supplier<DebugOptions> delegate;
  public DebugOptionsWrapper(Supplier<DebugOptions> delegate) {
    this.delegate = delegate;
  }
  @Override
  public DebugOptions get() {
    DebugOptions options = delegate.get();
    if(options == null) {
      return new DebugOptions() { ... } // Empty implementation
    } else {
      return options;
    }
  }
}

Isso poderia ser empacotado para evitar que qualquer NullPointerException seja lançada se o serviço não estiver presente:

private final Supplier<DebugOptions> options =
 new DebugOptionsWrapper(OSGiSupplier.supply(DebugOptions.class, getClass()));

A mudança para Java 8 facilita esses tipos de melhorias, e deve ser parte de Eclipse. A pergunta é: para onde se deve ir depois disso? Alguns pacotes compartilhados fariam sentido, mas isso pertence a org.eclipse.core.runtime ou org.eclipse.equinox.util. Será que ainda faz sentido adicionar isso ao Eclipse?

***

Alex Blewitt faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://alblue.bandlem.com/2015/09/osgi-services-in-java-8.html