O Spring MVC é um framework web amplamente utilizado em diversos times da Meituan Dianping. No desenvolvimento de projetos baseados em Spring MVC, anotações são utliizadas em praticamente todos os módulos, incluindo anotações fornecidas pelo Java, como @Override e @Deprecated, além de anotações do próprio Spring, como @Controller, @Service e @Autowired. Também podem existir anotações personalizadas. As anotações servem não apenas para marcar e documentar o código, mas também para simplificar a configuração, acelerando assim o processo de desenvolvimento.
Anotações são como açúcar sintático que às vezes uso sem muitos cuidados, até que me deparei com um problema de NullPointerException causado por uma anotação @Service. Foi então que percebi os riscos de usar anotações de forma inadequada.
Causa do Incidente
Fui solicitado a encapsular um serviço HTTP upstream para uso interno do sistema. Após realizar o encapsulamento e testar localmente, deployei para o ambiente de teste. Surpreendentemente, recebi um erro NullPointerException:
ERROR [qtp384587033-86] 2015-12-21 16:29:00.905 com.meituan.trip.mobile.hermes.common.utils.HttpClientUtils.doRequest(HttpClientUtils.java:359) HttpClientUtils.doRequest invoke get error, url:nullmt/api/test/v1/query?id=123456
org.apache.http.client.ClientProtocolException
at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:186) ~[httpclient-4.3.5.jar:4.3.5]
...
Caused by: org.apache.http.ProtocolException: Target host is not specified
...
A partir da pilha de erros, era evidente que o problema estava relacionado ao URL não estar formatado corretamente (sem http://). No entanto, eu tinha configurado o URL via XML e ele funcionava nos testes locais. Apenas ao adicionar a anotação @Service à classe, o problema se reproduzia.
Análise do Problema
Com a anotação @Service, observei no log de inicialização que o bean foi substituído:
INFO [main] 2015-12-21 16:28:47.078 org.springframework.beans.factory.support.DefaultListableBeanFactory.registerBeanDefinition(DefaultListableBeanFactory.java:665) Substituindo definição de bean 'queryPartnerImpl': substituindo [Generic bean: class [com.meituan.trip.mobile.hermes.sal.meilv.impl.QueryPartnerImpl]; escopo=singleton; abstrato=falso; lazyInit=falso; autowireMode=0; dependencyCheck=0; autowireCandidate=true; principal=falso; factoryBeanName=nulo; factoryMethodName=nulo; initMethodName=nulo; destroyMethodName=nulo; definido em arquivo [/Users/hanzhankang/hermes/hermes-sal/target/classes/com/meituan/trip/mobile/hermes/sal/meilv/impl/QueryPartnerImpl.class]] por [Generic bean: class [com.meituan.trip.mobile.hermes.sal.meilv.impl.QueryPartnerImpl]; escopo=; abstrato=falso; lazyInit=falso; autowireMode=0; dependencyCheck=0; autowireCandidate=true; principal=falso; factoryBeanName=nulo; factoryMethodName=nulo; initMethodName=nulo; destroyMethodName=nulo; definido em recurso de classe path [sal/service-outer.xml]]
A substituição ocorreu porque havia dois beans com o mesmo nome no mesmo WebApplicationContext. O bean finalmente carregado era aquele definido no arquivo XML, com todas as propriedades configuradas corretamente.
Diagnóstico e Resolução
Usando Jmap para verificar o número de instâncias de QueryPartnerImpl no sistema:
$ jmap -histo:live 20881 | grep QueryPartnerImpl
1354: 2 80 com.meituan.trip.mobile.hermes.sal.meilv.impl.QueryPartnerImpl
Havia duas instâncias! Isso era incomum, já que o Spring cria beans singleton por padrão. Para investigar, realizei um dump do heap usando Jmap e analisei com o MAT (Memory Analyzer Tool).
Através do MAT, descobri que uma das instâncias tinha suas propriedades injetadas corretamente, enquanto a outra não tinha nenhum valor definido. Isso explicava o NullPointerException.
A investigação mostrou que:
- Dois contextos diferentes estavam criando beans com o mesmo nome.
- ContextLoaderListener criava um contexto raiz.
- DispatcherServlet criava seu próprio contexto filho.
- Anotações de escaneamento estavam ativadas em ambos os contextos.
A solução foi configurar o escaneamento de anotações de forma mais granular, usando include-filter e exclude-filter para evitar conflitos:
<context:component-scan base-package="com.meituan.trip.mobile.hermes">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/>
</context:component-scan>
Conclusão
- Anotações devem ser usadas com cautela e consistência.
- O escaneamento de anotações deve ser bem configurado para evitar conflitos entre contextos.
- Testes unitários podem não capturar problemas relacionados a múltiplos contextos.
- Seguir as melhores práticas do framework pode prevenir muitos problemas.