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:

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:

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!

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).