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