07 diciembre 2010

Spring BeanPostProcessor, ServletContextAware y ServletContext

En este post muestro cómo definir un componente para configurar un bean de Spring que necesita configuración que no se puede setear rígida en un parámetro de la aplicación. En este caso, necesito saber el path absoluto del directorio de contextos del servlet container (el directorio webapps de Tomcat), y si la aplicación se deploya en distintos sistemas operativos ese path puede cambiar de un sistema operativo al otro.

BeanPostProcessor


Defino una clase que implementa la interface BeanPostProcessor, que provee dos métodos:

Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;

Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;


La clase es DataDistributionServiceBeanPostProcessor:

package ar.com.kamikazesoftware.console.context;

import java.io.File;

import javax.servlet.ServletContext;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.web.context.ServletContextAware;

import ar.com.kamikazesoftware.services.dataDistribution.DataDistributionService;

/**
* {@link BeanPostProcessor} to set the {@link DataDistributionService}'s xmlFilesRootFolder property
* as an absolute path including the servlet container's contexts directory followed by 
* the relative path set by the user in the application's resource bundle parameter called "xmlFilesRootFolder".
* 
* The bean dataDistributionService has injected the xmlFilesRootFolder parameter taking it from the Spring's
* {@link PropertyPlaceholderConfigurer} configured with the application.properties file as the resource bundle:
* 
*  <bean id="dataDistributionService"
*  class="ar.com.kamikazesoftware.services.dataDistribution.local.LocalDataDistributionService">
*  <property name="xmlFilesRootFolder"><value>${xmlFilesRootFolder}</value></property>
*  ...
*
* 
* So before using the original xmlFilesRootFolder, that can't be useful in such environments like Linux
* because the relative path doesn't get set relative to servlet container's contexts directory,
* this BeanPostProcessor sets the xmlFilesRootFolder concatenating before it the absolute servlet container's contexts directory.
* 
* For example, if in application.properties exists this line:
* 
* xmlFilesRootFolder=webapps/ROOT/players/
* 
* And Tomcat is installed on c:/tools/tomcat
* 
* then this BeanPostProcessor sets the DataDistributionService xmlFilesRootFolder property with the value:
* 
* c:/tools/tomcat/webapps/ROOT/players
* 
* @author EMenendez
*
*/
public class DataDistributionServiceBeanPostProcessor implements BeanPostProcessor, ServletContextAware {

private static final Log log = LogFactory.getLog(DataDistributionServiceBeanPostProcessor.class);

private String containerRootDirectoryPath;

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// do nothing before initialization
return bean;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// apply only for DataDistributionService
if (bean instanceof DataDistributionService) {
if (log.isDebugEnabled())
log.debug("Executing postProcessAfterInitialization for bean " + beanName);

DataDistributionService dataDistributionService = (DataDistributionService) bean;

// TODO try use reflection to call getter and setter for XmlFilesRootFolder property, and remove getter and setter from the interface DataDistributionService
String relativeControlFileDirectory = dataDistributionService.getXmlFilesRootFolder();

if (log.isDebugEnabled())
log.debug("DataDistributionService's xmlFilesRootFolder from resource bundle (relative path): " + relativeControlFileDirectory);

String absoluteControlFileDirectory = containerRootDirectoryPath + relativeControlFileDirectory;

dataDistributionService.setXmlFilesRootFolder(absoluteControlFileDirectory);

if (log.isDebugEnabled())
log.debug("DataDistributionService's xmlFilesRootFolder set to absolute path: " + absoluteControlFileDirectory);

return dataDistributionService;
} else {
// Do nothing if the processed bean is not DataDistributionService
return bean;
}
}

@Override
public void setServletContext(ServletContext servletContext) {
// get tomcat/webapps/starmount-console/ dir
String contextRootDirectoryPath = servletContext.getRealPath("/");
File contextRootDirectory = new File(contextRootDirectoryPath);

// get tomcat/webapps/ dir
containerRootDirectoryPath = contextRootDirectory.getParent() + System.getProperty("file.separator");

if (log.isDebugEnabled())
log.debug("Initializing DataDistributionServiceBeanPostProcessor. Set containerRootDirectoryPath to: " + containerRootDirectoryPath);  
}
}


Application Context


Y en applicationContext.xml de Spring están definidos el bean y su post processor:

<bean id="dataDistributionService"
class="ar.com.kamikazesoftware.services.dataDistribution.local.LocalDataDistributionService">
<property name="xmlFilesRootFolder"><value>${xmlFilesRootFolder}</value></property>
<constructor-arg index="0" ref="SceneDAO" />  
</bean> 

<bean class="ar.com.kamikazesoftware.console.context.DataDistributionServiceBeanPostProcessor"/>


ServletContextAware



Como DataDistributionServiceBeanPostProcessor implementa también la interface ServletContextAware, cuando Spring inicializa el post processor ejecuta el método:

public void setServletContext(ServletContext servletContext)


que es el que usamos para obtener el path local donde están guardadas las webapps en el servlet container (Tomcat):

// get tomcat/webapps/myapp/ dir
String contextRootDirectoryPath = servletContext.getRealPath("/");
File contextRootDirectory = new File(contextRootDirectoryPath);
// get tomcat/webapps/ dir
containerRootDirectoryPath = contextRootDirectory.getParent();


De esta forma amigos termina este ejemplo de implementación y uso de BeanPostProcessor, ServletContextAware y ServletContext.

Hasta la próxima!

23 junio 2008

DBCP: Validar las conexiones del pool

MySQL cierra las conexiones que han estado idle por más de un determinado tiempo (default 8 hs.), por lo tanto cuando no se accede a la aplicación por un tiempo, las conexiones de su pool quedan idle y luego MySQL las cierra. Luego, cuando la aplicación intenta usar alguna conexión del pool, tira la exception Broken pipe.

Validación de conexiones


Para evitarlo, se pueden agregar un par de parámetros al data source de la aplicación, que está configurado con DBCP.

validationQuery


Para validar una conexión es necesario setear este parámetro, que tiene que ser una query SQL que devuelva 1 o más filas, que depende del motor de base de datos al que se esté accediendo. En el caso de MySQL una query trivial es SELECT 1 (en Oracle por ej. sería SELECT 1 FROM DUAL).

testOnBorrow


El parámetro testOnBorrow provoca que se verifique que la conexión esté abierta cuando se solicite al pool, antes de usarla. Si la conexión no está abierta el pool la descarta e intenta obtener otra.
La siguiente porción de configuración, es para un data source configurado en Spring. La presento simplemente para ilustrar el seteo de los parámetros de DBCP.

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
 <property name="driverClassName" value="${jdbc.driverClassName}"/>
 <property name="url" value="${jdbc.url}"/>
 <property name="username" value="${jdbc.username}"/>
 <property name="password" value="${jdbc.password}"/>
 <property name="validationQuery">
  <value>SELECT 1</value>
 </property>
 <property name="testOnBorrow">
  <value>true</value>
 </property>
</bean>


testWhileIdle


El parámetro testWhileIdle valida las conexiones cuando éstas se encuentran Idle.

<property name="validationQuery">
 <value>SELECT 1</value>
</property>
<property name="testWhileIdle">
 <value>true</value>
</property>
<property name="timeBetweenEvictionRunsMillis">
 <value>60000</value>
</property>
<property name="numTestsPerEvictionRun">
 <value>3</value>
</property>
<property name="minEvictableIdleTimeMillis">
 <value>7200000</value>
</property>


Links


http://commons.apache.org/dbcp/
http://commons.apache.org/dbcp/configuration.html
post en mailing list de dbpc

03 junio 2008

SQL: información que no está en una relación usando LEFT JOIN

Dada la siguiente estructura, dos tablas de entidad y una de relación (sintaxis de MySQL):

create table alumno (id integer, nombre varchar(255)) primary key (id);
create table materia (id integer, nombre varchar(255)) primary key (id);
create table inscripcion(materia_id integer, alumno_id integer) primary key (materia_id, alumno_id);

Queremos conocer las materias en las cuales no hay inscripto ningún alumno.

Inscribimos a Emiliano en Matemática, a Luis en Lengua, y a Martin en ninguna materia. Por lo tanto nadie está inscripto en Gimnasia

insert into alumno (id, nombre) values (1, 'Emiliano');
insert into alumno (id, nombre) values (2, 'Luis');
insert into alumno (id, nombre) values (3, 'Martin');

insert into materia (id, nombre) values (1, 'Matemática');
insert into materia (id, nombre) values (2, 'Lengua');
insert into materia (id, nombre) values (3, 'Gimnasia');

insert into inscripcion (materia_id, alumno_id) values (1, 1);
insert into inscripcion (materia_id, alumno_id) values (2, 2);

Ejecutamos la query

SELECT m.nombre
FROM materia m
LEFT JOIN inscripcion i ON m.id = i.materia_id
WHERE i.materia_id is null;

y resulta en

+-----------+
| nombre |
+-----------+
| Gimnasia |
+-----------+