Minhas boas práticas – Java
1. Pattern.compile() em vez de String.replaceAll() dentro de loops
public String replaceAll(String regex, String replacement) {
return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}
O Pattern sempre precisa ser compilado primeiro. Se o mesmo Pattern roda muitas vezes com strings curtas, a performance fica bem melhor quando você reutiliza o Pattern
já compilado.
2. StringBuilder em vez de + dentro de loops
Dentro de um loop, new StringBuilder() tem performance melhor do que +.
3. Usar equals() e hashCode() corretamente
Às vezes todos os campos entram no equals() ou no hashCode(). Isso é útil para verificar se uma entidade foi alterada, mas está errado. O objetivo do
equals() não é verificar se uma entidade foi modificada, e sim determinar se, mesmo que a instância do objeto não seja a mesma, ainda se trata da mesma entidade.
Ou seja: use apenas a chave primária!
Isso é necessário para qualquer tipo de collection. Ao chamar .contains() em uma List, o Java usa o método equals. HashMaps também precisam de hashCode().
Os dois devem ser sobrescritos juntos.
Uma armadilha comum são entidades ainda não salvas, sem ID. Cuidado aqui: persista cedo para evitar problemas.
Veja também:
O contrato desses métodos precisa ser sempre respeitado:
4. Armadilhas com DateTime em ambientes com sistemas operacionais diferentes
Alguns sistemas operacionais têm um relógio interno com precisão de nanossegundos; outros, de milissegundos. Por isso, ZonedDateTime.now() não funciona igual nas duas plataformas e quebra o
equals(). Em vez disso, ajuste SEMPRE a precisão!
/**
* truncating the precision, because it relies on the internal clock of linux/macOs/windows.
* windows and older macOS versions use only milliseconds, whereas linux and new macOS versions use microsecond precision.
*/
public static ZonedDateTime getNow() {
return ZonedDateTime.now().truncatedTo(DateTimeUtil.PRECISION);
}
As anotações JPA a seguir também dependem de ZonedDateTime.now() e nunca devem ser usadas sem esse ajuste:
@CreationTimestamp@UpdateTimestamp@PastOrPresent
5. Não declarar tipos de dados complexos inline
Sempre crie classes próprias para tipos de dados complexos, em vez de aninhar Pair, Lists etc.
Ruim:
// unlesbar
public Triple<List<Pair<List<List<ChatMessage>>, ZonedDateTime>>, Language, ChatRoom> getChatMessagesGroupedByDay();
Melhor:
public class FTOChatMessageCollection {
private List<FTOChatMessageCollectionPerDay> messagesPerDay;
private Language language;
private ChatRoom chatRoom;
// constructor, getter & setter ...
}
public class FTOChatMessageCollectionPerDay {
private List<FTOChatMessage> messages;
private ZonedDateTime day;
// constructor, getter & setter ...
}
Vantagens:
- Nomes melhores do que
getFirst()egetSecond() - O uso fica claramente definido pelos getters e setters (a classe é imutável? Qual lista pode ser ampliada? Como o objeto deve ser instanciado?) → o encapsulamento documenta sozinho como o código deve ser usado
- Legibilidade muito melhor
Exceções:
Para relações simples, Pair é ótimo! Por exemplo:
Pair<User, Training> pair = this.addCurrentUserToTraining(training);
6. Cache – do jeito certo (guia de configuração mínima)
O cache (pronúncia /kæʃ/, como “cash”) é, no fundo, apenas um Map que guarda entradas. Coisas mais complexas, como invalidação baseada em tempo, ficaram de fora.
Nem sempre é preciso usar uma biblioteca quando um simples Map resolve! Muitos desenvolvedores não entendem isso e, por medo, usam bibliotecas por princípio. O aspecto mais importante do cache é invalidar no momento certo.
Armadilha nº 1: vazamentos de memória
Vazamentos de memória acontecem com muita facilidade quando se usa cache. Basta esquecer de sobrescrever direito equals() e hashCode() e pronto: você tem um HashMap
vazando memória. Um método que é sempre seguro é criar as chaves do Map manualmente. O ideal é incluir apenas o ID da entidade; caso contrário, o Map já vaza memória quando você altera
um único campo. E isso faz sentido: só porque o primeiro nome de um usuário mudou, ele não deixa de ser o mesmo usuário. Veja também: 3. Usar equals() e hashCode()
corretamente
Armadilha nº 2: thread safety
Idealmente, o cache usa um ConcurrentHashMap, que oferece operações atômicas e thread-safe. O Map só é modificado por uma única chamada – computeIfAbsent() –, e é assim que se garante a thread safety:
private ConcurrentMap<String, T> cache = new ConcurrentHashMap<>();
protected T getOrCompute(final String key, final Supplier<T> computeFunction) {
// thread safe, because `computeIfAbsent` is atomic, so no need for `synchronized` here.
// if you extend this to more than 1 map operation, you should synchronize it
return this.getCacheMap().computeIfAbsent(key, k -> computeFunction.get());
}
Se o código fosse mais longo e houvesse várias chamadas, o método inteiro teria de ser synchronized.
7. Serialização – ainda é necessária?
Minha pergunta: Existe uma regra do Sonar chamada Fields in a “Serializable” class should either be transient or serializable (campos de uma classe “Serializable” devem ser transient ou serializáveis). Ela aparece quando você adiciona um campo não serializável à sua classe. A regra diz:
Por exemplo, sob carga, a maioria dos frameworks de aplicação J2EE descarrega objetos em disco, e um objeto supostamente Serializable com membros de dados não transient e não serializáveis pode causar travamentos do programa e abrir as portas para atacantes.
Isso me parece desatualizado. Em Effective Java 3rd Edition consta que a serialização não é relevante no desenvolvimento de software novo. Além disso, remover a serialização do Java é uma “meta de longo prazo” da Oracle. O próprio arquiteto-chefe do Java, Mark Reinhold, declarou:
“A serialização foi um erro terrível em 1997.”
A pergunta sobre se o Spring Framework pode descarregar objetos em disco já foi feita aqui:
Então eu pergunto:
Algum servidor de aplicação ou framework moderno, ou a própria implementação J2EE (em especial componentes Java que interagem com CDI, EJB, JPA), descarrega objetos em disco sob carga pesada?
Ou essa afirmação está desatualizada?
Resposta:
TL;DR Se você não está implementando uma aplicação web stateful e distribuível, não ativa a persistência de sessão e usa apenas caches em memória, talvez consiga viver sem precisar de Serializable em lugar nenhum.
Explicação:
Para a replicação de sessões, a serialização do Java continua obrigatória na especificação Servlet mais recente: https://jakarta.ee/specifications/servlet/5.0/jakarta-servlet-spec-5.0.html#distributed-environments
Um caso de uso comum: você tem vários servidores atrás de um load balancer, faz login em um deles e recebe um cookie JSESSIONID. Se o load balancer mandar você para outro
servidor na requisição seguinte, seus atributos de login e outros atributos com escopo de sessão são serializados e replicados no novo servidor, para que o seu JSESSIONID continue válido lá.
Outro caso em que você precisa de serialização para os atributos com escopo de sessão é quando o servidor de aplicação decide gravar as sessões em disco. No Tomcat 8, por exemplo, isso pode ser configurado para entrar em ação quando há sessões demais para manter em memória, ou quando o servidor é reiniciado e você quer manter as sessões ativas. Veja https://tomcat.apache.org/tomcat-9.0-doc/config/manager.html
Todo servidor de aplicação compatível com Servlet 5.0 precisa, por padrão, suportar objetos Serializable em sessões HTTP para esses casos de uso.
Em alguns servidores, isso pode ser ajustado ou substituído usando, por exemplo, serialização Jackson-JSON ou algo semelhante. Veja, por exemplo, https://stackoverflow.com/questions/46459707/how-to-use-jackson-instead-of-jdkserializationredisserializer-in-spring
Outro caso de uso comum é o cache. Ele serve para manter objetos acessíveis o mais rápido possível, de preferência em memória, mas eventualmente transferidos de forma temporária para o disco ou para um servidor de cache externo.
Existe uma API padrão para isso (JSR-107) que optou explicitamente por não exigir objetos Serializable, mas a maioria das implementações ainda usa, por padrão, a serialização do Java para essa transferência.
Também dá para ajustar essas implementações para suportar outros mecanismos de serialização, por exemplo no ehcache: https://www.ehcache.org/documentation/3.8/serializers-copiers.html
8. Hibernate: estratégia de fetching
Considerando o artigo EAGER fetching is a code smell, escrito por um sabichão extremamente carismático que é contribuidor do
Hibernate, é preferível usar LAZY como FetchType. Mesmo que alguns tipos de dados não façam sentido sem uma chave estrangeira, esses tipos são, sim, consultados
isoladamente (getCount(), data tables etc.).
A seguir, todos os cenários, ordenados pelo grau de satisfação com a solução, começando pelo menos satisfatório:
1. Pior caso: LazyInitializationException
Nem preciso dizer nada. Todos nós conhecemos essa dor.
2. Correção rápida
public void updateChatRoom(ChatMessage message) {
// reload here or LazyInitializationException would be thrown
message = this.chatMessageService.reload(message); // not optimal: second roundtrip to the database - could easily take 100ms!
// we missed the chance to unproxy without a performance hit.
ChatRoom chatRoom = message.getChatRoom().getUpdatedAt(DateTimeUtil.getNow());
}
3. Fazer o unproxy diretamente no service
public ChatMessage getChatMessage(final Long id, boolean withRoom) {
// assume this is the same service + the same transaction.
// so we are in the same persistence context + everything will be performed together at the transaction flush.
// perfect: no additional roundtrip like in example 2.!
ChatMessage message = this.chatMessageService.getById(id);
if (withRoom) {
ChatRoom chatRoom = this.chatMessageService.unproxy(message.getChatRoom());
}
// we should not call message.setChatRoom() -> this getter should not even exist as Hibernate would
// map this relation for us! setting it again would perform an update on our foreign key!!
// so we have no option to set the ChatRoom back to the message and have to pass it everywhere in a Tuple or something similar
return ???;
}
4. Fazer o fetch diretamente no service (faça assim!)
public ChatMessage getChatMessage(final Long id, boolean withRoom) {
// all aforementioned problems solved
return this.executeSingleResultQuery((query, cb, root) -> {
if (withRoom) {
root.fetch(ChatRoomEntity_.partition).fetch(ChatPartitionEntity_.chatModule);
}
return cb.equal(root.get(ChatRoomEntity_.id), id);
});
}
9. Hibernate Search
O Hibernate Search se integra perfeitamente ao Hibernate JPA. Ele percebe quando entidades mudam e é fácil de configurar:
@Entity
@Indexed(interceptor = SearchableChatMessageInterceptor.class) // hier können Einträge von der Indexierung ausgeschlossen werden
@FullTextFilterDef(name = "SearchableChatMessageFilter", impl = SearchableChatMessageFilter.class) // hier wird der Key festgelegt
public class ChatMessageEntity implements Searchable { // das Searchable dient als Marker-Interface
@Column
@Field // dieses Feld wird indexiert
private String message;
@ManyToOne(targetEntity = ChatRoomEntity.class, fetch = FetchType.LAZY, optional = false)
@IndexedEmbedded(includeEmbeddedObjectId = true, targetElement = ChatRoomEntity.class) // indexiert den Fremschlüssel mit.
// Über dieses Entity wird auch die Message gesucht,
// wie in @FullTextFilterDef -> SearchableChatMessageFilter definiert
private ChatRoom chatRoom;
// ...
}
Localmente, a busca fica desativada:
// DefaultInstanceInitializer.java
// we don't need the global search everywhere, givin an option to override this setting here
protected boolean isBuildSearchIndexes() {
return !Constants.Mode.isDebug();
}
Busca aproximada & comportamento da busca
if ((needle.split(" ").length > 1 || needle.contains("\"")) && !searchExact) {
/*
* Simple Query String. Provides basic functionality like:
*
* boolean (AND using “+”, OR using “|”, NOT using “-“)
* prefix (prefix*)
* phrase (“some phrase”)
* precedence (using parentheses)
* fuzzy (fuzy~2)
* near operator for phrase queries (“some phrase”~3)
*/
query = qb.simpleQueryString()
.onField(field)
.andFields(moreFields)
.withAndAsDefaultOperator()
.matching(needle)
.createQuery();
} // ...
Lucene
Depurando o Lucene com o Luke
Nossa versão do Hibernate Search precisa ser depurada com o Luke versão 6.6.0. Só funciona com essa versão!
- Download: https://github.com/DmitryKey/luke/releases
- Com ele, dá para abrir os índices da pasta
target/. - É um bom jeito de ver se as entidades estão sendo indexadas corretamente.
- Também é possível iniciar uma busca a partir do próprio Luke.
ElasticSearch
O Hibernate Search também suporta ElasticSearch (que passa a ser usado no lugar do Lucene e oferece uma busca aproximada melhor / uma experiência mais parecida com a do Google).