Pruebas y más pruebas con JRuby. Hoy ha tocado darle vueltas al misteriosos funcionamiento del classpath cuando se ejecuta JRuby.
Imaginemos el siguiente escenario, creamos una nueva aplicación rails con JRuby y accedemos a la consola con un $jruby script/console.
Una vez lanzado la consola y cargado en entorno, preguntamos a jruby cual es el classpath de la aplicación.
Para ello podemos consultar el valor de $CLASSPATH para obtener una lista las rutas en el classpath de jruby:
>> $CLASSPATH
=> ["file:/usr/local/jruby/jruby-1.1b1/lib/ruby/gems/1.8/gems/jruby-openssl-0.0.4/lib/jopenssl.jar", "file:/usr/local/jruby/jruby-1.1b1/lib/ruby/gems/1.8/gems/ActiveRecord-JDBC-0.5/lib/jdbc_adapter_internal.jar"]
Alternativamente podemos preguntarle a Java que nos diga el valor del classpath usando la clase System:
>> java.lang.System.get_properties.get_property "java.class.path"
=> "/usr/local/jruby/jruby-1.1b1/lib/bsf.jar:/usr/local/jruby/jruby-1.1b1/lib/jruby.jar:/usr/local/jruby/jruby-1.1b1/lib/mysql-connector-java-5.1.5-bin.jar"
Hasta aquí nada raro, JRuby incluye en el classpath por defecto las rutas del classpath del entorno desde donde se ejecutando el proceso java que lanza el interprete JRuby y los ficheros localizados en el directorio $JRUBY_HOME/lib.
De este modo usando la función include_class podremos incluir cualquier clase java que pertenezca a las clases estandar del JDK o que se encuentren localizada en cualquiera de las rutas anteriores.
Supongamos que ahora queremos utilizar en nuestra aplicacion una serie de bibliotecas java para su uso desde rails. Una posible solución es crear un directorio como RAILS_ROOT/lib/classes, copiar ahí todos los jars con las clases que queramos utilizar y hacer desde environment.rb algo como:
if RUBY_PLATFORM =~ /java/
if File.exist?(File.join(RAILS_ROOT,'lib','classes'))
Dir.open(File.join(RAILS_ROOT,'lib','classes')).each do |filename|
if filename =~ /.jar|.zip/
$CLASSPATH << File.expand_path(File.join(RAILS_ROOT,'lib','classes',filename))
end
end
end
end
Si ahora abrimos la consola y preguntamos por el valor de $CLASSPATH, obtendremos el comportamiento deseado:
>> $CLASSPATH
=> ["file:/usr/local/jruby/jruby-1.1b1/lib/ruby/gems/1.8/gems/jruby-openssl-0.0.4/lib/jopenssl.jar", "file:/usr/local/jruby/jruby-1.1b1/lib/ruby/gems/1.8/gems/ActiveRecord-JDBC-0.5/lib/jdbc_adapter_internal.jar", "file:/Users/antoniogarrotehernandez/Desarrollo/Caja4JRuby/lib/classes/antlr-2.7.6.jar", "file:/Users/antoniogarrotehernandez/Desarrollo/Caja4JRuby/lib/classes/asm.jar", "file:/Users/antoniogarrotehernandez/Desarrollo/Caja4JRuby/lib/classes/Caja4HibernateModel.jar", "file:/Users/antoniogarrotehernandez/Desarrollo/Caja4JRuby/lib/classes/cglib-2.1.3.jar", "file:/Users/antoniogarrotehernandez/Desarrollo/Caja4JRuby/lib/classes/commons-collections-2.1.1.jar", "file:/Users/antoniogarrotehernandez/Desarrollo/Caja4JRuby/lib/classes/commons-logging-1.0.4.jar", "file:/Users/antoniogarrotehernandez/Desarrollo/Caja4JRuby/lib/classes/dom4j-1.6.1.jar", "file:/Users/antoniogarrotehernandez/Desarrollo/Caja4JRuby/lib/classes/ehcache-1.2.3.jar", "file:/Users/antoniogarrotehernandez/Desarrollo/Caja4JRuby/lib/classes/ejb3-persistence.jar", "file:/Users/antoniogarrotehernandez/Desarrollo/Caja4JRuby/lib/classes/hibernate-annotations.jar", "file:/Users/antoniogarrotehernandez/Desarrollo/Caja4JRuby/lib/classes/hibernate-commons-annotations.jar", "file:/Users/antoniogarrotehernandez/Desarrollo/Caja4JRuby/lib/classes/hibernate-entitymanager.jar", "file:/Users/antoniogarrotehernandez/Desarrollo/Caja4JRuby/lib/classes/hibernate-validator.jar", "file:/Users/antoniogarrotehernandez/Desarrollo/Caja4JRuby/lib/classes/hibernate3.jar", "file:/Users/antoniogarrotehernandez/Desarrollo/Caja4JRuby/lib/classes/javassist.jar", "file:/Users/antoniogarrotehernandez/Desarrollo/Caja4JRuby/lib/classes/jboss-archive-browsing.jar", "file:/Users/antoniogarrotehernandez/Desarrollo/Caja4JRuby/lib/classes/jta.jar"]
Y por supuesto podremos incluir las clases de cualquiera de esos jars.
Por ejemplo un POJO correspondiente a un modelo Hibernate:
>> include_class "model.Post"
=> ["model.Post"]
Parece, que tenemos todo lo necesario para utilizar cualquier clase java que queramos. Intentemos obtener una entidad persistente de ese modelo hibernate usando las interfaces JPA de la especificación EJB3.0:
>> javax.persistence.Persistence.create_entity_manager_factory "Caja4HibernateModelPU"
NativeException: javax.persistence.PersistenceException: No Persistence provider for EntityManager named Caja4HibernateModelPU
...
Algo está fallando.
El problema es que con la técnica anterior hemos modificado el classpath de JRuby, pero no el classpath de la JVM que ejecuta JRuby. Esto provoca que cuando las clases de la implementación JPA de hibernate buscan el fichero persistence.xml con la configuración de la unidad de persistencia, no la encuentran en el classpath, a pesar de aparecer listada en el valor de $CLASSPATH.
Podemos comprobar esto preguntandole a la clase System por el classpath:
>> java.lang.System.get_properties.get_property "java.class.path"
=> "/usr/local/jruby/jruby-1.1b1/lib/bsf.jar:/usr/local/jruby/jruby-1.1b1/lib/jruby.jar:/usr/local/jruby/jruby-1.1b1/lib/mysql-connector-java-5.1.5-bin.jar"
>>
Ni rastro de los jars.
Las posibles soluciones a esto por ahora pasan por:
- copiar todos los jars $JRUBY_HOME/lib
- modificar el script jruby para que incluya las rutas
- fijar la variable CLASSPATH en la shell que lanza el script server
La solución por la que he optado es crear una tarea Rake que utilice una modificación del script anterior para que genere un script que fije el classpath y para modificar el classpath con un $source script/setup_classpath:
desc 'setup classpath script for the application'
task :setup_classpath_script => :environment do
actual = ""
jars = Dir.entries(File.join(RAILS_ROOT,"lib","classes")).map{ |f|
if(f =~ /.jar|.zip/)
puts "Added: #{File.expand_path(f,File.join(RAILS_ROOT,"lib","classes"))}"
File.expand_path(f,File.join(RAILS_ROOT,"lib","classes"))
else
nil
end
}.compact
if(ENV['CLASSPATH']!=nil && ENV['CLASSPATH']!="" && ENV['CLASSPATH']!=" " && jars.length>0)
actual = ENV['CLASSPATH']+":"
else
actual = ENV['CLASSPATH']
end
to_append = ""
if(jars.length>0)
to_append = jars.join(":")
end
File.open(File.join(RAILS_ROOT,"script","setup_classpath"),"w") do |f|
f << "export CLASSPATH=#{actual+to_append}"
end
`chmod uga+x #{File.expand_path(File.join(RAILS_ROOT,'script','setup_classpath'))}`
puts "You can setup the classpath executing \n$source script/setup_classpath"
end
De todas formas, esto no pasa de ser un parche temporal. (jruby 1.1b1) Además curiosamente, con esta técnica, la clase System devuelve el classpath correcto pero si preguntamos a $CLASSPATH ninguno de estos jars aparecerán listados. A pesar de que podremos crear las clases sin ningún problema:
>> javax.persistence.Persistence.create_entity_manager_factory "Caja4HibernateModelPU"
18-dic-2007 23:20:45 org.hibernate.cfg.annotations.Version
INFO: Hibernate Annotations 3.3.0.GA
18-dic-2007 23:20:45 org.hibernate.cfg.Environment
INFO: Hibernate 3.2.5
18-dic-2007 23:20:45 org.hibernate.cfg.Environment
INFO: hibernate.properties not found
18-dic-2007 23:20:45 org.hibernate.cfg.Environment buildBytecodeProvider
INFO: Bytecode provider name : cglib
18-dic-2007 23:20:45 org.hibernate.cfg.Environment
INFO: using JDK 1.4 java.sql.Timestamp handling
18-dic-2007 23:20:46 org.hibernate.ejb.Version
INFO: Hibernate EntityManager 3.3.1.GA
18-dic-2007 23:20:46 org.hibernate.cfg.AnnotationBinder bindClass
INFO: Binding entity from annotated class: model.Post
18-dic-2007 23:20:46 org.hibernate.cfg.annotations.QueryBinder bindQuery
INFO: Binding Named query: Post.findById => SELECT p FROM Post p WHERE p.id = :id
18-dic-2007 23:20:46 org.hibernate.cfg.annotations.QueryBinder bindQuery
INFO: Binding Named query: Post.findByTitle => SELECT p FROM Post p WHERE p.title = :title
18-dic-2007 23:20:46 org.hibernate.cfg.annotations.QueryBinder bindQuery
INFO: Binding Named query: Post.findByCreatedAt => SELECT p FROM Post p WHERE p.createdAt = :createdAt
18-dic-2007 23:20:46 org.hibernate.cfg.annotations.EntityBinder bindTable
INFO: Bind entity model.Post on table posts
18-dic-2007 23:20:47 org.hibernate.validator.Version
INFO: Hibernate Validator 3.0.0.GA
18-dic-2007 23:20:47 org.hibernate.connection.DriverManagerConnectionProvider configure
INFO: Using Hibernate built-in connection pool (not for production use!)
18-dic-2007 23:20:47 org.hibernate.connection.DriverManagerConnectionProvider configure
INFO: Hibernate connection pool size: 20
18-dic-2007 23:20:47 org.hibernate.connection.DriverManagerConnectionProvider configure
INFO: autocommit mode: true
18-dic-2007 23:20:47 org.hibernate.connection.DriverManagerConnectionProvider configure
INFO: using driver: com.mysql.jdbc.Driver at URL: jdbc:mysql://localhost:3306/caja4
18-dic-2007 23:20:47 org.hibernate.connection.DriverManagerConnectionProvider configure
INFO: connection properties: {user=root, password=****, autocommit=true, release_mode=auto}
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory buildSettings
INFO: RDBMS: MySQL, version: 5.0.45
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory buildSettings
INFO: JDBC driver: MySQL-AB JDBC Driver, version: mysql-connector-java-5.1.5 ( Revision: ${svn.Revision} )
18-dic-2007 23:20:48 org.hibernate.dialect.Dialect
INFO: Using dialect: org.hibernate.dialect.MySQLDialect
18-dic-2007 23:20:48 org.hibernate.transaction.TransactionFactoryFactory buildTransactionFactory
INFO: Transaction strategy: org.hibernate.transaction.JDBCTransactionFactory
18-dic-2007 23:20:48 org.hibernate.transaction.TransactionManagerLookupFactory getTransactionManagerLookup
INFO: No TransactionManagerLookup configured (in JTA environment, use of read-write or transactional second-level cache is not recommended)
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory buildSettings
INFO: Automatic flush during beforeCompletion(): disabled
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory buildSettings
INFO: Automatic session close at end of transaction: disabled
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory buildSettings
INFO: JDBC batch size: 15
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory buildSettings
INFO: JDBC batch updates for versioned data: disabled
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory buildSettings
INFO: Scrollable result sets: enabled
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory buildSettings
INFO: JDBC3 getGeneratedKeys(): enabled
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory buildSettings
INFO: Connection release mode: auto
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory buildSettings
INFO: Maximum outer join fetch depth: 2
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory buildSettings
INFO: Default batch fetch size: 1
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory buildSettings
INFO: Generate SQL with comments: disabled
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory buildSettings
INFO: Order SQL updates by primary key: disabled
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory buildSettings
INFO: Order SQL inserts for batching: disabled
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory createQueryTranslatorFactory
INFO: Query translator: org.hibernate.hql.ast.ASTQueryTranslatorFactory
18-dic-2007 23:20:48 org.hibernate.hql.ast.ASTQueryTranslatorFactory
INFO: Using ASTQueryTranslatorFactory
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory buildSettings
INFO: Query language substitutions: {}
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory buildSettings
INFO: JPA-QL strict compliance: enabled
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory buildSettings
INFO: Second-level cache: enabled
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory buildSettings
INFO: Query cache: disabled
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory createCacheProvider
INFO: Cache provider: org.hibernate.cache.NoCacheProvider
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory buildSettings
INFO: Optimize cache for minimal puts: disabled
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory buildSettings
INFO: Structured second-level cache entries: disabled
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory buildSettings
INFO: Statistics: disabled
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory buildSettings
INFO: Deleted entity synthetic identifier rollback: disabled
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory buildSettings
INFO: Default entity-mode: pojo
18-dic-2007 23:20:48 org.hibernate.cfg.SettingsFactory buildSettings
INFO: Named query checking : enabled
18-dic-2007 23:20:48 org.hibernate.impl.SessionFactoryImpl
INFO: building session factory
18-dic-2007 23:20:48 org.hibernate.impl.SessionFactoryObjectFactory addInstance
INFO: Not binding factory to JNDI, no JNDI name configured
18-dic-2007 23:20:48 org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: Running hbm2ddl schema update
18-dic-2007 23:20:48 org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: fetching database metadata
18-dic-2007 23:20:48 org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: updating schema
18-dic-2007 23:20:49 org.hibernate.tool.hbm2ddl.TableMetadata
INFO: table found: caja4.posts
18-dic-2007 23:20:49 org.hibernate.tool.hbm2ddl.TableMetadata
INFO: columns: [updated_at, title, created_at, body, id]
18-dic-2007 23:20:49 org.hibernate.tool.hbm2ddl.TableMetadata
INFO: foreign keys: []
18-dic-2007 23:20:49 org.hibernate.tool.hbm2ddl.TableMetadata
INFO: indexes: [primary]
18-dic-2007 23:20:49 org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: schema update complete
=> #