Una de las múltiples actividades que realizamos como consultores en Java es realizar revisiones de aplicaciones con diversos tipos de problemas. Este 2015 no fue la excepción y en términos generales los equipos de desarrollo cometen -de manera consistente- el mismo tipo de errores en diferentes clientes.

Es por ello que en esta primer parte detallamos algunos de los casos más representativos y la manera más adecuada para atenuarlos o corregirlos.

##1. JPA y su mal uso

Este caso es muy típico; los programadores realizan un desarrollo en Java EE 5 o 6 y consideran que emplean JPA (Java Persistence API) de manera adecuada y de acuerdo a la especificación. Pero a la luz de una revisión es claro que “usan” JPA de una manera totalmente incorrecta.

Un ejemplo es aquel en donde definen un persistence.xml; pero todo el acceso a la base de datos lo realizan por medio de:

  • Obtener una conexión del pool de conexiones y realizar consultas usando JDBC de manera directa.

  • Inyectan el EntityManager; pero no definen ninguna entidad y la totalidad de las operaciones contra la base de datos son por medio de Native Queries.

Cualquier de estas opciones constituye una mala práctica en mayor o menor medida.

En el primer caso; al hacer uso de comandos clásicos de JDBC literalmente no emplean nada del rico API que provee JPA. Constituye no solo un desconocimiento de pilares fundamentales de Java EE, sino que además genera un esfuerzo en tiempo de desarrollo que era enteramente innecesario y que implica mucho código de carpintería para poder alcanzar un objetivo.

En el segundo caso; hemos podido observar proyectos en donde la totalidad del acceso a datos se hizo por medio de Native Queries. Esto desvirtua multiples funcionalidades que brinda JPA y en general es debido a que los desarrolladores conocían como programar por medio de JDBC y fuerzan ese conocimiento sobre el API de JPA. Siendo esto evidentemente erróneo. El uso de native queries debe ser una decisión tomada con prudencia y atendiendo un caso muy específico en donde se demande tal situación. Y evidentemente, la razón que motiva esa decisión debe estar indicada como comentario en el código fuente del aplicativo.

Una variante de este caso; es cuando un porcentaje muy elevado de las consultas son por medio de native queries. Eso inmediatamente es motivo de llamada de atención hacia el grupo de desarrollo.

##2. Inyección de SQL

Este escenario es tradicionalmente una derivación del uso de native queries o JDBC directo. En este caso; el equipo de desarrollo escribe sentencias como:

StringBuilder queryStr = new StringBuilder();
queryStr.append("select * from Tabla ");
queryStr.append("where id = ");
queryStr.append(parametro);
Query query = em.createNativeQuery(queryStr.toString());

Como se observa; el programador tan solo hace una concatenación del parámetro que se recibe en un formulario -por ejemplo- y simplemente lo agrega a un String que forma el comando de SQL que se envía a la base de datos. Sin ningún tipo de validación.

El problema de ello es que esta 100% expuesto a una vulnerabilidad del tipo SQL Injection. Para un atacante, sería algo tan simple como enviar de parámetros algo como 1 or 0=0 para que ese comando retornara valores. Y si ese código se emplea para validar un usuario entonces es trivial lograr acceso a un sistema. Eso sin tomar en cuenta que sería posible enviar sentencias de SQL completas, imaginar un caso en donde se envie un drop o delete no sería mayor inconveniente.

Esta situación es perfectamente evitable si se emplea JPA y los parámetros se definen como tales. O incluso; si en el peor caso se escribe un query nativo y se emplea el método setParameter para cada uno de ellos. Esa simple medida evita vulnerabilidades como esta.

Si en su código encuentran casos como este, deben considerar una falla crítica que demanda una corrección inmediata.

##3. Sesión

Una de las razones por las que más nos contactan es para afinar un servidor de aplicaciones pues una o más aplicaciones se comportan mal. Sin excepción, en todos los casos que hemos atendido el problema no es el servidor de aplicaciones sino el aplicativo mismo que ha sido mal programado. Hemos tenido casos en donde clientes le asignan hasta 30 GB al Heap del JVM para minimizar el problema de caídas por Out of Memory.

Usualmente este problema se debe a que los programadores definen todos o casi todos los Managed Beans de JSF de tipo Session. Las razones detrás de ello son múltiples; pero entre las explicaciones más inusuales que he recibido tenemos:

  • Es que solo así me funcionaba la pantalla (clásico cuando quieren usar AJAX y no lo hacen de manera adecuada)

  • Es que no sabía cómo hacer para pasar un valor entre una página y otra.

Situaciones que típicamente se dan por desconocimiento del ciclo de vida de JSF y de las implicaciones que tiene definir un Managed Bean de Session. Si a eso sumamos que en muchos casos guardan decenas de variables en session, en muy poco tiempo y con muy pocos usuarios una aplicación genera errores de Out of Memory.

Sin duda el aplicativo sirve en el ambiente del desarrollador, pero tan pronto como entra en producción y se le agrega una carga moderada el sistema colapsa. El mayor problema de esto es que si no es detectado a tiempo (durante la fase de desarrollo) el esfuerzo de corrección es usualmente enorme debido a que todo el sistema fue programado de esa manera.

##4. Garbage Collector

Una de las peores cosas que puede verse en un código es tener invocaciones manuales al garbage collector. Hemos podido ver código en donde se tiene invocaciones al System.gc() en un listener de JSF. Por qué considero que es una de las peores prácticas?

  • Es un estupendo indicador de que el código es fundamentalmente de mala calidad. Cualquier código que dependa de el para su correctitud esta mal.
  • Es una curita al código. Generalmente señala un serio problema de programación que puede ir relacionado a un exceso de elementos en sesión o una mala codificación de algunos métodos.
  • No hay garantía de que la invocación al GC sea realizada, perfectamente el JVM puede ignorarla.
  • El programador no puede saber que clase de GC esta corriendo en el servidor de aplicaciones. Algunos de ellos no “paran el mundo” pero no todos son así y no siempre es así.

##5. Excepciones

Uno de los mayores retos que se tienen al analizar aplicaciones en ambientes de producción es la ausencia de mensajes en los logs. Y en muchisimas ocasiones es debido a que los programadores silencian las excepciones. por ejemplo:

try {
 // algún codigo....
} catch (Exception ex) {
// No se hace nada
}

El problema es que no hay manera alguna de determinar que se dio una excepción y por ello no sirve algún elemento del aplicativo. Esto es una pesadilla para el recurso a carga de la atención de incidentes; pues hasta que no se llega a estudiar en detalle el código y ver ese manejo de la excepción no hay manera de poder hacer un diagnóstico oportuno. Eventualmente la solución es agregar un Logger y de esa manera poder revisar que causa el problema.

##6. Herencia

Este caso es aquel en donde el grupo de desarrollo ha utilizado una jerarquía de herencia con muchos niveles de profundidad y en donde cada uno de los niveles no aporta ninguna lógica o esta es mínima. El principal problema de este accionar es la enorme dificultad de mantener el código a lo largo del tiempo. En mi experiencia, la jerarquía de herencia de un sistema debe ser simple y elegante. En ocasiones resulta interesante definir interfaces para teóricamente poder tener múltiples implementaciones. En la práctica generalmente solo tiene una implementación.

Autor

Gerardo Arroyo

Chief Technology Officer.

  • Ingeniero en Software. ITCR.
  • Master en Computación en Informática. UCR.
  • Oracle Certified Expert, JEE 6 Web Services Developer.
  • Oracle Certified Professional, Java SE 7 Programmer.
  • Oracle WebLogic Server 12c Certified Implementation Specialist.

Flecha Roja Technologies, 2016.