Implementando o Pattern Singleton na Plataforma Java EE

Veja neste artigo o que é o padrão de projeto Singleton e como podemos implementá-lo na plataforma Java EE 7

O padrão de projeto Singleton é um padrão bastante utilizado, simples de ser construído, mas que merece muita atenção, sendo inclusive considerado por alguns como um anti-pattern. No entanto, diversos frameworks empresariais, como o Spring e a plataforma Java SE, utilizam bastante o padrão Singleton. Além disso, a plataforma Java EE 7 já oferece formas elegantes e simples para implementarmos o Singleton.

Os padrões de projetos são essenciais para o sucesso de qualquer projeto de software. Neste artigo veremos um dos padrões de projetos mais utilizados nas mais diversas aplicações e frameworks: o padrão Singleton. Veremos o que é e como ele é comumente implementado como um POJO e como podemos implementá-lo na plataforma Java. Também veremos algumas armadilhas que devemos evitar quando estamos implementando o Singleton.

Padrão Singleton

O padrão de projeto Singleton está descrito no livro do GoF, sendo um padrão criacional, que tem como objetivo criar apenas uma única instância do seu próprio tipo.

A criação de apenas uma única instância de uma classe é útil quando necessitamos de um acesso global. No entanto, isso pode introduzir alguns problemas de "race conditions" se o Singleton é utilizado em um ambiente multithread, já que é um problema em que vários processos utilizam o mesmo recurso ao mesmo tempo de maneira aparentemente exclusiva.

Como será visto mais adiante, devido o suporte que a plataforma Java EE oferece os desenvolvedores não precisam mais implementar o Singleton programaticamente.

O Singleton muitas vezes é utilizado em combinação com padrão Factory. Normalmente utiliza-se o padrão Singleton quando é necessário acessar informações que são compartilhadas por todo o domínio da aplicação como, por exemplo, as informações de configuração, quando é preciso carregar e colocar em cache recursos considerados caros permitindo assim o acesso compartilhado global e com isso melhorar o desempenho, quando é necessário criar uma instância de uma aplicação de log, para gerenciar objetos dentro de uma classe que implementa o padrão Factory, para criar um objeto Façade porque normalmente precisamos apenas de um, entre outras situações que necessitam de um acesso compartilhado.

Porém, o padrão Singleton também tem desvantagens, como nas situações em que os desenvolvedores criam diversos Singletons desnecessariamente para fazer cache de recursos, não permitindo assim que o Garbage Collector recupere os objetos e recursos de memória disponíveis. Por isso que a utilização em demasia do padrão Singleton pode prejudicar a performance e causar problemas de memória nas aplicações.

Implementando um Singleton em Código Puro (POJO)

Uma forma de garantirmos que o nosso Singleton ofereça apenas uma instância é controlar a criação do objeto. Assim, podemos fazer isso tornando o construtor invisível utilizando o modificador de visibilidade private, conforme o código da Listagem 1.

Listagem 1. Criando a classe Singleton com o construtor privado.

package br.com.devmedia.exemplosingleton; public class MeuSingletonExemplo { private MeuSingletonExemplo() { // Mais código aqui. } }

Após isso, precisamos de um método que criará a instância ou retornará essa instância se a instância já tiver sido criada. Como uma instância do "MeuSingletonExemplo" ainda não existe, devemos marcar a criação do como estática para permitir acesso através do nome da classe. Por padrão, utilizamos o método getInstance(), assim acessaríamos a instância criada através da chamada "MeuSingletonExemplo.getInstance()". Segue na Listagem 2 a implementação para o método getInstance().

Listagem 2. Permitindo a instanciação do Singleton através do método estático getInstance.

package br.com.devmedia.exemplosingleton; public class MeuSingletonExemplo { private static MeuSingletonExemplo instance; private MeuSingletonExemplo() { } public static MeuSingletonExemplo getInstance() { if (instance==null){ instance = new MeuSingletonExemplo(); } return instance; } }

Podemos verificar na linha "if (instance==null)" que foi realizado um teste antes da criação do Singleton e, caso não exista nenhum, criamos este Singleton. Caso contrário, retornamos a instância que foi criada em uma chamada anterior para o método getInstance(). Cada chamada subsequente retorna o objeto MySingleton criado anteriormente.

Apesar do código acima funcionar, ainda temos outro problema. Poderíamos ter um problema com race conditions, onde mais que uma instância do Singleton pode ser criada em um ambiente multithread. Para corrigir o problema de race condition devemos adquirir um lock e não liberar até que a instância seja retornada. Segue na Listagem 3 um código que mostra como podemos implementar da forma correta o Singleton.

Listagem 3. Sincronizando o Singleton para garantir segurança contra threads concorrentes.

package br.com.devmedia.exemplosingleton; public class MeuSingletonExemplo { private static MeuSingletonExemplo instance; private MeuSingletonExemplo() {} public static synchronized MeuSingletonExemplo getInstance() { if (instance==null) { instance=new MeuSingletonExemplo (); } return instance; } }

Outra possibilidade é criar uma instância do Singleton no momento em que a classe é carregada, conforme podemos verificar na implementação da Listagem 4.

Listagem 4. Criando um objeto Singleton no momento do carregamento da classe.

package br.com.devmedia.exemplosingleton; public class MeuSingletonExemplo { private final static MeuSingletonExemplo instance = new MeuSingletonExemplo(); private MeuSingletonExemplo() {} public static MeuSingletonExemplo getInstance() { return instance; } }

Com isso não precisamos sincronizar a criação da instância Singleton e temos a criação de um objeto Singleton apenas uma vez após a JVM carregar as classes. Isso ocorre porque membros static e também blocos estáticos são executados quando a classe é carregada. Também podemos utilizar bloco estático, porém devemos atentar que esta é uma inicialização mais tardia invocada antes que o construtor seja chamado. Segue na Listagem 5 um exemplo.

Listagem 5. Criando um objeto Singleton dentro de um bloco estático.

package com.devchronicles.singleton; public class MeuSingletonExemplo { private static MeuSingletonExemplo instance=null; static { instance = new MeuSingletonExemplo(); } private MeuSingletonExemplo() {} public static MeuSingletonExemplo getInstance() { return instance; } }

Ainda existe outra forma de criar Singletons, através de uma dupla checagem de bloqueio, sendo inclusive considerado um dos métodos mais seguros porque ele checa a instância do Singleton antes de bloquear a classe e novamente antes de criar o objeto. Segue na Listagem 6 um exemplo.

Listagem 6. Criando um objeto Singleton com dupla checagem de bloqueio.

package br.com.devmedia.exemplosingleton; public class MeuSingletonExemplo { private volatile MeuSingletonExemplo instance; private MeuSingletonExemplo () {} public MeuSingletonExemplo getInstance() { if (instance == null) { synchronized (MeuSingletonExemplo.class) { if (instance == null) { instance = new MeuSingletonExemplo(); } } } return instance; } }

Podemos verificar que temos duas checagens, primeiramente em "if (instance == null)" e depois em "synchronized (MeuSingletonExemplo.class)" uma nova checagem.

Mesmo assim essa abordagem também não é totalmente segura, pois a API Java Reflection permite que os desenvolvedores alterem o acesso do modificador do construtor para público, o que poderia permitir ao Singleton novas criações.

No entanto, existe uma abordagem totalmente segura que é através do uso do tipo Enum, que foi introduzido no Java 5. Os tipos Enum são Singletons por natureza, por isso a JVM consegue gerenciar grande parte do trabalho necessário para a criação de um Singleton. Segue na Listagem 7 uma implementação em Java utilizando Singleton com Enum.

Listagem 7. Criando um objeto Singleton através de um Enum.

package com.devchronicles.singleton; public enum MeuSingletonEnumExemplo { INSTANCE; public void metodoExemplo(){ } }

Dessa forma, podemos obter uma instância do objeto criado conforme o código a seguir:

MeuSingletonEnumExemplo singleton = MeuSingletonEnumExemplo.INSTANCE;

Após isso, podemos chamar qualquer método com o código a seguir:

singleton. metodoExemplo();

Implementando um Singleton na plataforma Java EE

O padrão de projeto Singleton também pode ser aplicado na plataforma Java EE, inclusive de uma forma bastante elegante utilizando Singleton Beans. Os Singletons também possuem suporte das anotações, o que facilita a sua implementação na plataforma Java EE. Assim, basta inserir a anotação Singleton, conforme mostra o exemplo da Listagem 8.

Listagem 8. Inserindo a anotação Singleton na plataforma Java EE.

package br.com.devmedia.exemplosingleton; import java.util.HashMap; import java.util.Map; import javax.annotation.PostConstruct; import javax.ejb.Singleton; import java.util.logging.Logger; @Singleton public class CacheSingletonBeanExemplo { private Map<Integer, String> meuCache; @PostConstruct public void start() { Logger.getLogger("MeuLoggerGlobal").info("Iniciou!"); meuCache = new HashMap<Integer, String>(); } public void addUsuario(Integer id, String nome){ meuCache.put(id, name); } public String getNome(Integer id){ return meuCache.get(id); } }

Com essa simples anotação, sem precisar de qualquer configuração em arquivos XML, já é possível tornar o bean um Singleton. Dessa forma, a anotação @Singleton marca a classe como um Singleton EJB e o container gerencia a criação e o uso das instâncias únicas.

Normalmente uma instância de um Singleton é inicializada somente quando ela é necessária. No entanto, algumas vezes é preciso que o Singleton esteja disponível logo na inicialização da aplicação principalmente quando a criação de uma instância é “cara” ou é garantido que precisaremos do bean no início da aplicação.

Para isso deve-se usar a anotação @Startup na classe conforme mostra o exemplo da Listagem 9.

Listagem 9. Inserindo a anotação @Startup para criar o Singleon na inicialização da plataforma Java EE.

package br.com.devmedia.exemplosingleton; import java.util.HashMap; import java.util.Map; import javax.annotation.PostConstruct; import javax.ejb.Singleton; import javax.ejb.Startup; import java.util.logging.Logger; @Startup @Singleton public class CacheSingletonBeanExemplo { private Map<Integer, String> meuCache; @PostConstruct public void start() { Logger.getLogger("MeuLoggerGlobal").info("Iniciou!"); meuCache = new HashMap<Integer, String>(); } public void addUsuario(Integer id, String nome){ meuCache.put(id, name); } public String getNome(Integer id){ return meuCache.get(id); } }

Outra situação que pode ser útil quando se cria os Singleton na inicialização é determinar a ordem que eles serão inicializados. Isso porque quando é necessário garantir que um recurso depende de outro para ser executado, assim o outro recurso deveria inicializar primeiro. Para isso, pode ser utilizada a anotação @DependsOn passando o nome da classe que o bean depende. Segue na Listagem 10 um exemplo.

Listagem 10. Especificando a ordem de inicialização através da anotação @DependsOn.

package br.com.devmedia.exemplosingleton; import java.util.HashMap; import java.util.Map; import javax.annotation.PostConstruct; import javax.ejb.Singleton; import javax.ejb.Startup; import javax.ejb.DependsOn; import javax.ejb.EJB; @Startup @DependsOn("MeuLoggingBeanExemplo") @Singleton public class CacheSingletonBeanExemplo { private Map<Integer, String> meuCache; @EJB MeuLoggingBeanExemplo logging; @PostConstruct public void start(){ logging.logInfo("Iniciou!"); meuCache = new HashMap<Integer, String>(); } public void addUsuario(Integer id, String nome){ meuCache.put(id, nome); } public String getNome(Integer id){ return meuCache.get(id); } }

Segue na Listagem 11 o bean Singleton referenciado no exemplo anterior como uma dependência.

Listagem 11. Classe dependente da anterior para execução.

package br.com.devmedia.exemplosingleton; import javax.annotation.PostConstruct; import javax.ejb.Singleton; import javax.ejb.Startup; import java.util.logging.Logger; @Startup @Singleton public class MeuLoggingBeanExemplo { private Logger logger; @PostConstruct public void start(){ logger = Logger.getLogger("MeuLoggerGlobal"); logger.info("Inicializado Primeiro!!!"); } public void logInfo(String mensagem){ logger.info(mensagem); } }

Os métodos anotados com @PostConstruct são invocados ou o bean é novamente construído depois que todas injeções de dependência foram realizadas e antes que o primeiro método de negócio seja invocado.

O exemplo anterior executa quando o servidor de aplicação é iniciado. CacheSingletonBeanExemplo aguarda para ser executado, pois ele depende da inicialização de MeuLoggingBeanExemplo. A saída do logger será:

> Inicializado Primeiro!!! > Iniciou!

Também poderíamos definir que o Singleton deveria inicializar após a inicialização de uma sequência de outros beans. Assim, poderiam ser especificados múltiplos beans em @DependsOn. O bean da Listagem 12 depende de MeuLoggingBeanExemplo e MeuInitializationBeanExemplo.

Listagem 12. Especificando múltiplas dependências de beans.

@Startup @DependsOn({"MeuLoggingBeanExemplo","MeuInitializationBeanExemplo"}) @Singleton public class CacheSingletonBeanExemplo { // Mais códigos aqui. }

A ordem em que MeuLoggingBeanExemplo e MeuInitializationBeanExemplo são inicializados depende das suas anotações @DependsOn. Se nenhum bean depende explicitamente um do outro, os beans são inicializados pelo container em uma ordem específica.

Apesar de não ser preciso se preocupar com a inicialização do Singleton, deve-se cuidar com a concorrência, pois ainda estamos dentro de um ambiente de concorrência. Assim, o Java EE resolve esses problemas também através de anotações.

O Java EE oferece dois tipos de gerenciamento de concorrência: concorrência gerenciada pelo container (containermanaged concurrency) e concorrência gerenciada por bean (beanmanaged concurrency). Na concorrência gerenciada pelo container é o próprio container o responsável por gerenciar qualquer coisa relacionada ao acesso de leitura e escrita, já na concorrência gerenciada por bean o desenvolvedor deve gerenciar a concorrência usando métodos em Java como synchronization. A anotação ConcurrencyManagementType.BEAN habilita a concorrência gerenciada por bean, sendo que o Java EE utiliza por padrão a concorrência gerenciada pelo container. Para habilitar explicitamente a concorrência gerenciada por container podemos utilizar a anotação ConcurrencyManagementType.CONTAINER, como mostra o exemplo da Listagem 13.

Listagem 13. Habilitando explicitamente a concorrência gerenciada por container.

@Startup @DependsOn("MeuLoggingBeanExemplo") @ConcurrencyManagement(ConcurrencyManagementType.CONTAINER) @Singleton public class CacheSingletonBeanExemplo { // Mais códigos aqui. }

Na Listagem 14 temos um exemplo utilizando a anotação @Lock para controlar o acesso concorrente.

Listagem 14. Gerenciando concorrência utilizando @Lock.

package br.com.devmedia.exemplosingleton; import java.util.HashMap; import java.util.Map; import javax.annotation.PostConstruct; import javax.ejb.ConcurrencyManagement; import javax.ejb.ConcurrencyManagementType; import javax.ejb.DependsOn; import javax.ejb.EJB; import javax.ejb.Lock; import javax.ejb.LockType; import javax.ejb.Singleton; import javax.ejb.Startup; @Startup @DependsOn("MeuLoggingBeanExemplo") @ConcurrencyManagement(ConcurrencyManagementType.CONTAINER) @Singleton public class CacheSingletonBeanExemplo { private Map<Integer, String> meuCache; @EJB MeuLoggingBeanExemplo logging; @PostConstruct public void start(){ logging.logInfo("Iniciou!"); meuCache = new HashMap<Integer, String>(); } @Lock(LockType.WRITE) public void addUsuario(Integer id, String nome){ meuCache.put(id, nome); } @Lock(LockType.READ) public String getNome(Integer id){ return meuCache.get(id); } }

Temos dois tipos de bloqueios para controle de acesso aos métodos de negócio do bean: @Lock(LockType.WRITE) que bloqueia o bean para outros clientes enquanto o método está sendo invocado, e @Lock(LockType.READ) que permite acesso concorrente ao método e não bloqueia o bean para outros clientes.

Métodos que alteram dados são normalmente anotados com acesso WRITE para prevenir o acesso à informação assim como atualizações. No exemplo anterior o método addUser() está anotado com tipo WRITE de modo que se houver uma chamada ao método getName(), essa deverá aguardar o método addUser() retornar antes que este possa completar a sua chamada.

Podemos ter um lançamento de exceção do tipo ConcurrentAccessTimeoutException pelo container caso o método addUser() não complete em um período específico de timeout. O período do timeout pode ser configurado através de uma anotação conforme pode ser visto no exemplo abaixo. Podemos configurar o valor na anotação LockType em nível de classe. Isso é aplicado a todos os métodos de negócio que não configuram explicitamente o seu próprio LockType. Como o LockType padrão é WRITE normalmente é suficiente configurarmos apenas os métodos que requerem acesso concorrente. Veja a Listagem 15.

Listagem 15. Definindo um timeout específico para acesso concorrente.

package br.com.devmedia.exemplosingleton; import java.util.logging.Logger; import javax.annotation.PostConstruct; import javax.ejb.Singleton; import javax.ejb.Startup; import javax.ejb.DependsOn; import javax.ejb.ConcurrencyManagement; import javax.ejb.ConcurrencyManagementType; import javax.ejb.AccessTimeout; import java.util.Map; import javax.ejb.EJB; import java.util.HashMap; import javax.ejb.Lock; import javax.ejb.LockType; import java.util.concurrent.TimeUnit; @Startup @DependsOn("MeuLoggingBeanExemplo") @ConcurrencyManagement(ConcurrencyManagementType.CONTAINER) @Singleton @AccessTimeout(value=120000) public class CacheSingletonBeanExemplo { private Map<Integer, String> meuCache; @EJB MeuLoggingBeanExemplo logging; @PostConstruct public void start(){ logging.logInfo("Iniciou!"); meuCache = new HashMap<Integer, String>(); } @AccessTimeout(value=30, unit=TimeUnit.SECONDS) @Lock(LockType.WRITE) public void addUsuario(Integer id, String nome){ meuCache.put(id, nome); } @Lock(LockType.READ) public String getNome(Integer id){ return meuCache.get(id); } }

A anotação @AccessTimeout pode possuir diferentes constantes para TimeUnit, como NANOSECONDS, MICROSECONDS, MILLISECONDS, e SECONDS.

Se nenhum valor para TimeUnit é dado, o valor é interpretado como milissegundos por padrão. Podemos também colocar esta anotação em nível de classe e aplicar isto para todos os métodos.

Espero q tenham gostado do artigo. Até a próxima.

Bibliografia

[1] Erich Gamma, Ricard Helm, Ralph Johnson, John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994).

[2] Java EE 6 and the Ewoks: http://www.devchronicles.com/2011/11/javaee6-and-ewoks.html.

[3] Eric Freeman, Elisabeth Robson, Bert Bates, Kathy Sierra. Head First Design Patterns. O'Reilly Media, 2004.

Artigos relacionados