Migración de EJB3 a JPA y Spring.

1
14257

Migración de EJB3 a JPA y Spring.

0. Índice de contenidos.

1. Introducción

Más que un tutorial, el presente se trata de una guía paso a paso de cómo realizar una migración de una aplicación montada con EJB3 y JSF al soporte que proporciona Hibernate para JPA y a Spring, con el mismo soporte de JSF.

Lo primero sería explicar el por qué de la migración, ¡estan locos estos romanos!, pues no, podríamos más bien decir que se nos rompió el amor de tanto usarlo,
por lo pesado que se hace el día a día de esta tecnología, en concreto la empaquetación y despliegue del contenedor de EJBs, en un servidor de aplicaciones, con las implicaciones que conlleva en el desarrollo.

Inmersos, como estamos, en el uso de metodologías ágiles se nos hacía muy duro que luego el desarrollo no fuese, ni mucho menos, tan ágil.

La mayoría de las veces no necesitas hacer uso de todo el soporte que te ofrece un servidor de aplicaciones, te basta un contenedor más ligero.
En nuestro caso, el desarrollo de la capa de negocio y persistencia con EJB3, al principio supuso una revolución, por la sencillez respecto a EJB2.1, pero no es más cierto que la mayoría de las veces no requerimos hacer uso de la
transaccionalidad distribuida que te proporciona un CMT, no accedemos a colas de mensajería, los EJB Timers pueden implementarse con otros apis y el soporte a la persistencia no difiere al que te pueden proporcionar otras soluciones como Hibernate, que de hecho se basa en éste.

Si lo anterior no convence, vamos a apoyar nuestra decisión en el uso de Spring, puesto que nos da soporte para casi toda esa funcionalidad y, lo más importante, no nos cierra las puertas a que si, en un futuro, necesitamos correr bajo un servidor de aplicaciones, lo hagamos; y, si necesitamos hacer uso de la transaccionalidad distribuida de aquél, sea cuestión de cambiar la configuración de la unidad de persistencia.

El objetivo: poder desplegar una aplicación que hasta ahora sólo podía correr en un servidor de aplicaciones como Jboss Server, en un contenedor de servlets como Apache Tomcat.

Los requisitos: que todo siga funcionando como hasta ahora y que el cambio sea transparente al usuario final. Para ello es imprescindible tener una buena batería de tests, que pasen antes y después de la migración.

2. Entorno.

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil Asus G1 (Core 2 Duo a 2.1 GHz, 2048 MB RAM, 120 GB HD).
  • Sistema operativo: Windows Vista Ultimate.
  • Eclipse Galileo
  • EJB 3.0
  • JSF 1.2, Facelets, Tomahawk 1.1.9
  • Hibernate 3.4.0.GA
  • Spring 2.5.6

3. Descripción del proyecto.

La aplicación está montada con maven y se distribuye en los siguientes módulos:

  • autentia-parent: el padre del resto de módulos,
  • autentia-core: módulo que contiene la lógica de negocio, basada en EJBs de servicio, y la capa de persistencia, basada en EJBs de entidad. Contiene una serie de EJBs Timers para servicios temporizados.
  • autentia-core-test: módulo que contiene tests de integración de la capa de negocio, que corren bajo embedded jboss; incluye tests de persistencia que se ejecutan contra una hsqldb.
  • autentia-web: módulo que contiene la capa de presentación, con JSF, Apache MyFaces y Apache Tomahawk, que se comunica con la capa de negocio vía jndi y usa la inyección de dependencias propia de JSF (no hay inyección de dependencias de los EJBs).
  • autentia-ear: módulo que contiene una dependencia con los módulos core y web, y el tipo de empaquetación para la distribución de la aplicación en ese formato.

4. Primeros pasos.

  • Trabajando con Eclipse bajo Windows lo primero siempre debe ser la modificación del encoding del workspace a UTF-8.
  • Sobre un workspace vació descargamos del proyecto autentia-parent y no realizamos importaciones de los módulos que contiene para que no interfiera la compilación de los mismos en nuestras tareas, lo haremos de manera incremental, de abajo a arriba.

5. Modificaciones en el proyecto autentia-parent.

  • Realizaremos las siguientes modificaciones en el pom.xml del proyecto parent
    • eliminamos el módulo autentia-ear,
    • subimos el nivel de compilación a la 1.6,
    • eliminamos las dependencias de los artefactos propios de Jboss (autentia-dependenciesJBoss), si los hubiese,
    • añadimos la dependencia a la librería de Spring para que sea común a todos los módulos del proyecto,
    • 
          		org.springframework
          		spring
          		${spring.version}
          	
          
      
    • añadimos una propiedad para que la versión que usamos de Spring se defina en un único punto de los pom.xml,
    •  
          		2.5.6
        	 
      
  • borramos del módulo autentia-ear, tras comprobar que las dependencias de su pom.xml solo incluyen los proyectos autentia-core y autentia-web.

6. Modificaciones en el proyecto autentia-core.

  • importamos el proyecto autentia-core en el workspace como un proyecto de maven,
  • comprobamos en las propiedades del proyecto que se esté compilando con la versión 1.6 y que en la opción de project facets la versión de java sea la 6.0.,
  • si fuese necesario, modificamos el output folder de la compilación de build/classes a target/classes.
  • puede existir un target runtime a una versión 4.x de Jboss, accediendo a las propiedades del proyecto y en la opción targered runtimes lo deseleccionamos,
    • Modificaciones en el fichero persistence xml:
    • añadimos la declaración de las clases de entidad
    • com.autentia.training.entity.AbstractContact
        		    com.autentia.training.entity.Address
        		    ...
        		    com.autentia.training.entity.billing.Invoice
        		    ...
            
      
    • eliminamos el provider de EJB de la definición de la unidad de persistencia,
    • org.hibernate.ejb.HibernatePersistence
      
    • modificamos el atributo transaction-type de «JTA» a «RESOURCE_LOCAL»,
    • modificamos el nombre de la unidad de persistencia a autentia-pu,
    • añadimos al fichero de Tomcat conf/context.xml la definición de una fuente de datos:
    • 
          
      
    • modificamos el nombre del datasource de «java:/autentiaDS» a «java:comp/env/jdbc/autentiaDataSource».
  • Modificaciones en el pom.xml:
    • modificamos el tipo de empaquetación de ejb a jar,
    • eliminamos el plugin maven-ejb-plugin que genera un módulo de ejb 3.0.,
    • añadimos la dependencia de hibernate-entitymanager
    • 
      		      org.hibernate
      		      hibernate-entitymanager
      		      3.4.0.GA
      		    
      
  • Modificación de las anotaciones
    • de @Stateless a @Service,
    • de @EJB a @Resource,
    • el Dao lo anotamos con @Repository(«dao»)
    • eliminamos la anotación @Local
    • añadimos @Transactional(readOnly=true) en la interfaz del Dao, asignando @Transactional(readOnly=false) en los métodos save, delete y merge.
    • la clase de utilidades com.autentia.util.ejb.EjbUtil permite hacer un lookup de un EJB por jndi, la eliminamos y sustituimos la búsqueda por una inyección vía @Resource,
    • añadimos el soporte para la configuración del IoC de Spring, con el fichero applicationContext-core.xml en autentia-core\src\main\resources\ y el siguiente contenido:
    • 	
            	
            	
            	
            		
            	
            	
            		
                 
            
      
  • migración de los servicios de temporizador
    • eliminamos los métodos startUpTimer y shutDownTimer y la inyección de @Resource TimerService timerService;
    • la anotación @Timeout ya no es necesaria, y el método execute ya no requiere un Timer como parámetro,
    • añadimos el soporte para Quartz al pom.xml
    • 
      		    quartz
      		    quartz
      		    1.5.2
      		 
      
    • añadimos a la configuración del applicationContext-core.xml la configuración necesaria para los servicios. Ahí va un ejemplo:
    • 
        		
        	
        		
        	
        		
        		
        		
        	
        	
        		  
        		
        	
        	
        	
        		
        			
        				
        				
        			
        		
        	 
        
      

      Para más info este echad un ojo a este tutorial de planificador de tareas con Spring.

      Las propiedades asignadas con ${…} se obtienen de un fichero de properties que se configura con un PropertyPlaceholderConfigurer

      
        	
        		
        			true
        		
        		
        			
        				classpath:application.properties
        			
        		
        	
      	
      

En este punto no deberíamos tener errores de compilación en el proyecto autentia-core.

7. Modificaciones en el proyecto autentia-core-tests.

  • importamos el proyecto autentia-core-tests como un proyecto de maven,
  • comprobamos en las propiedades del proyecto que se esté compilando con la versión 1.6.,
  • modificamos el pom.xml para:
    • eliminar las dependencias al embedded jbos, en nuestro caso se arrastra de la dependencia com.autentia.common.autentia-test,
    • añadir la dependencia a la librería de tests de Spring
    •   
                org.springframework
                spring-test
                ${spring.version}
                test
              
          
      
    • y la dependencia a hsqldb
    •   
                hsqldb
                hsqldb
                1.8.0.7
                test
              
        
      
  • eliminamos la clase de utilidades com.autentia.test.ejb.Util y modificamos todas las referenicas que se realizaban al método lookupLocalBean por la declaración de la interfaz a nivel de clase anotado con un @Resource, los atributos no pueden ser static,
  • eliminamos de la carpeta de test/resource todo lo referente al embedded jboss,
  • sustituimos la configuración de log4j de xml por un fichero de properties,
  • eliminamos el fichero test/resources/jndi.properties,
  • añadimos a la cabecera de los tests (antes de la declaración de clases) las siguientes anotaciones:
  •   @RunWith(SpringJUnit4ClassRunner.class)
            @ContextConfiguration(locations = { "classpath:applicationContext-core-test.xml" })
    
  • y en aquellos tests que necesiten transaccionalidad, además, las siguientes:
  •        @TransactionConfiguration(transactionManager="transactionManager", defaultRollback=false)
            @Transactional
    

    con ello ya no es necesario realizar un lookup de la transacción.

    para poder usar la anotación de transaccionalidad tenemos que añadir la dependencia a la librería cglib puesto que el test
    no implementa una interfaz.

    
        		cglib
        		cglib-nodep
        		2.2
        		test
        	
      
    
  • eliminamos la clase EmbeddedJBossUtil y todos los métodos anotados con @BeforeClass y @AfterClass de los tests.
  • modificamos el nombre del fichero persistence-test.xml para llamarlo persistence.xml y ubicarlo en una carpeta META-INF a nivel de test/resources.
    Añadimos la declaración de las clases de entidad en el contenido del fichero y la configuración para que levante una base de datos hsqldb.
    El nombre de la unidad de persistencia será autentia-test-pu.
  • 
        
        	
        
        		com.autentia.training.entity.AbstractContact
            ...
        		
        			
        			
        			
        			
        			
        			
        			
        		
        	 
    
    
  • añadimos un applicationContext-core-test.xml, en el directorio test/resources con:
    • la configuración de anotaciones de Spring para que escanee a partir de com.autentia.training,
    • la configuración de transaccionalidad,
    • la declaración del bean del entityManager al que se le pasa el nombre de la unidad de persistencia definida en el persistence.xml,
    • la declaración del transactionManager al que se le pasa el entityManager.
     
            
                        
            	
            
            	
            
            	
            
            	
            		
            	
            
            	
            		
            	   
            	
            
    
    

En este punto los tests deberían pasar correctamente.

8. Modificaciones en el proyecto autentia-web.

  • importación del proyecto autentia-web como un proyecto de maven,
  • comprobamos en las propiedades del proyecto que se esté compilando con la versión 1.6 y que en la opción de project facets la versión de java sea la 6.0.,
  • si procede, modificamos el output folder de la compilación a target/classes,
  • puede existir un target runtime a una versión 4.x de Jboss, accediendo a las propiedades del proyecto y en la opción targered runtimes lo deseleccionamos,
  • se produce una inconsistencia en la jerarquía de clases de la clase de un tag propio de JSF (HtmlVacationCalendarTag). Añadimos las siguientes dependencias al pom.xml
  • 
      			javax.servlet
      			servlet-api
      			2.5
      		
      		
      			javax.servlet.jsp
      			jsp-api
      			2.1
      			provided
      		
      		
      			javax.servlet
      			jstl
      			1.1.2
      		
      		
      			taglibs
      			standard
      			1.1.2
      		
      		
      			javax.el
      			el-api
      			1.0
      			provided
      		
      
    
  • en el mismo pom.xml, modificamos el scope del modulo autentia-core puesto que ahora no lo provee el ear, eliminando <scope>provided</scope>.
  • realizamos las siguientes modificaciones realtivas a los controladores:
    • eliminamos el fichero faces-beans.xml comprobando que la declaración de los beans sigan la CoC de nombrar al bean como a la clase con la primera letra en minusculas.
      Modificamos el parámetro de contexto javax.faces.CONFIG_FILES del web.xml para que no haga referencia a dicho fichero,
    • añadimos a los controladores la anotación @Controller y @Scope(«session») y si hay algún caso en el que no se siga la CoC
      añadimos a la declaración el nombre @Controller(«invoiceCtrl»),
    • eliminamos la importación de com.autentia.util.ejb.EjbUtil, puesto que ya no se obtienen los servicios vía jndi. Sustituimos el lookupLocalBean,
      eliminando el constructor de la clase, por un @Resource en la declaración de la variable de clase que hace referencia a la interfaz,
    • en las clases que no están manejadas por Spring, nos apoyamos en una clase de utilidades que implementa un método getBean,
    • el método getBean de la clase de utilidades JSFUtil ahora invoca al método análogo de la clase SpringUtil.
    • en aquellos controladores que necesitan una inicialización de objetos, lo implementamos, en vez de en el constructor, en un método anotado con @PostConstruct,
  • modificaciones en el descriptor de despliegue web.xml:
    • eliminamos la clase TransactionFilter que implementa el patrón OpenSessionInView y su referencia en el web.xml sustituyéndola por un filtro propio de Spring para el entityManager: org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter
    • eliminamos el parámetro de contexto de jboss que prioriza la implementación de Myfaces para Jboss.
    •   	
            	
            		
            			If true, jboss disables the use of his own implementacion of jsf and permits to the war to be deployed to use his
            			own jsf implementation
            		
            		org.jboss.jbossfaces.WAR_BUNDLES_JSF_IMPL
            		true
            	
            
      
    • Si tuviésemos alguna referencia al datasource en el web.xml (un servlet de informes, e.ej.) modificaremos la referencia:
    •   java:/autentiaDS
      		  
      		          a
      		  
      		          java:comp/env/jdbc/autentiaDataSource
            
      
  • añadimos en el fichero de configuración faces-config.xml la integración con el contexto de IoC de Spring, de este modo todos los controladores son manejados por Spring,
    quién resolverá la inyección de dependencias:
  •           org.springframework.web.jsf.el.SpringBeanFacesELResolver
          
    
  • en la opción de project facets del proyecto, subimos el nivel de soporte de servlet api de 2.4 a 2.5. Usando JSF 1.2 se requiere el soporte de 2.5. Desde la ventana de propiedades del proyecto no lo
    permite añadir así que editamos directamente el fichero org.eclipse.wst.common.project.facet.core.xml para modificarlo. Tiramos y arrancamos Eclipse entre medias.
  • en las propiedades del proyecto, tenemos que añadir el proyecto autentia-core y las dependencias de maven dentro de la opción Java EE Module Dependencies.
  • al desplegar en el servidor no copia las librerías al directorio lib del directorio de despliegue. Añadimos la dependencia a «Web App Libraries» y eliminamos la específica de Tomcat.
  • se produce un error de invalid classpath con la librería commons-io, añadimos la siguiente exclusion a tomahawk
  •    
              
                commons-io
                commons-io
              
            
          
    
  • eliminamos los siguientes ficheros del workspace:
    • jndi.properties
    • login.config.xml
    • sql/jboss/autentia-ds.xml
    • jboss-web.xml
  • añadimos un applicationContext.xml propio para la parte web, de momento vacío,
  • añadimos al web.xml la configuración del listener de Spring,
  •    
                Startup Spring context
                org.springframework.web.context.ContextLoaderListener
              
        
              
                contextConfigLocation
                
                    classpath:applicationContext-core.xml,
                    /WEB-INF/applicationContext.xml
                
              
          
    

En este punto la aplicación web se despliega y muestra la página de login pero aún no hemos configurado la autenticación.

9. De JAAS a Spring Security.

  • eliminamos la configuración de autenticación y autorización del web.xml,
  • añadimos un applicationContext-security.xml en el que definiremos la configuración de seguridad, todo lo que en el web xml es un:
  • /pages/news/edit.jsf
    

    asociado a una en el applicationContext-security es:

    
    			 
    

    cuidado con el siguiente log de warning, puesto que a nivel de web.xml se pueden duplicar, pero en el applicationContext-security no.

    WARN HttpSecurityBeanDefinitionParser - Duplicate URL defined: [/pages/job/edit.jsf]. The original attribute values will be overwritten
    
  • para securizar una url para todos los roles:
  •    
    
  • generamos una clase de servicio que implementa la interfaz UserDetailsService que contiene la lógica de obtención del usuario y sus roles:
  •    package com.autentia.training.security;
          
          import java.util.ArrayList;
          import java.util.HashMap;
          import java.util.List;
          import java.util.Map;
          
          import javax.annotation.Resource;
          
          import org.apache.commons.logging.Log;
          import org.apache.commons.logging.LogFactory;
          import org.springframework.dao.DataAccessException;
          import org.springframework.security.GrantedAuthority;
          import org.springframework.security.GrantedAuthorityImpl;
          import org.springframework.security.userdetails.UserDetails;
          import org.springframework.security.userdetails.UserDetailsService;
          import org.springframework.security.userdetails.UsernameNotFoundException;
          import org.springframework.stereotype.Service;
          
          import com.autentia.training.entity.Role;
          import com.autentia.training.entity.User;
          import com.autentia.training.service.IDao;
          
          @Service
          public class EntityManagerUserDetailsService implements UserDetailsService {
          
          	private static final Log log = LogFactory.getLog(EntityManagerUserDetailsService.class);
          	
          	@Resource
          	IDao dao;
          	
          	public static final String DEF_USERS_BY_USERNAME_QUERY = "from User where login=:username";
          	public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY =
          		"select r from User u, Role r where (u.role.id=r.id or u.roleBilling.id=r.id or u.roleQA.id=r.id) and login=:username";
          
          	private String authoritiesByUsernameQuery;
          	private String usersByUsernameQuery;
          
          	public EntityManagerUserDetailsService() {
          		usersByUsernameQuery = DEF_USERS_BY_USERNAME_QUERY;
          		authoritiesByUsernameQuery = DEF_AUTHORITIES_BY_USERNAME_QUERY;
          	}
          
          	protected void addCustomAuthorities(String username,
          			List authorities) {
          	}
          
          	public String getAuthoritiesByUsernameQuery() {
          		return authoritiesByUsernameQuery;
          	}
          
          	public String getUsersByUsernameQuery() {
          		return usersByUsernameQuery;
          	}
          
          	public UserDetails loadUserByUsername(String username)
          			throws UsernameNotFoundException, DataAccessException {
          
          		log.trace("trying to get a user entity for the username='"+username+"'.");
          		
          		Map params = new HashMap();
          		params.put("username", username);
          		List users = dao.findAllByQuery(
          				usersByUsernameQuery, params);
          
          		log.trace("'"+users.size()+"'users found.");
          		if (users.size() == 0) {
          			throw new UsernameNotFoundException("User not found");
          		}
          
          		User user = users.get(0);
          
          		List roles = dao.findAllByQuery(authoritiesByUsernameQuery,
          				params);
          
          		log.trace("'"+roles.size()+"' roles found associated.");
          		if (roles.size() == 0) {
          			throw new UsernameNotFoundException("User has no GrantedAuthority");
          		}
          		
          		List dbAuths = new ArrayList();
          		for (Role role : roles) {
          			dbAuths.add(new GrantedAuthorityImpl(role.getName()));
          		}
          
          		addCustomAuthorities(username, dbAuths);
          
          		GrantedAuthority[] arrayAuths = (GrantedAuthority[]) dbAuths
          				.toArray(new GrantedAuthority[dbAuths.size()]);
          
          		user.setAuthorities(arrayAuths);
          
          		return user;
          	}
          
          	public void setAuthoritiesByUsernameQuery(String queryString) {
          		authoritiesByUsernameQuery = queryString;
          	}
          
          	public void setUsersByUsernameQuery(String usersByUsernameQueryString) {
          		this.usersByUsernameQuery = usersByUsernameQueryString;
          	}
          
          }
    
        
    
  • la clase User debe implementar la interfaz org.springframework.security.userdetails.UserDetails que obliga a implementar una serie de método con información sobre el estado del usuario para la autenticación, que podemos o no usar: isEnabled, isAccountNonLocked, isAccountNonExpired.
    Lo más importante es que almacene la información sobre sus roles para que todo siga funcionando como hasta ahora
  • 	/**
          	 * @see UserDetails#getAuthorities()
          	 */
          	public GrantedAuthority[] getAuthorities() {
          		return authorities;
          	}
          
          	/**
          	 * @see UserDetails#setAuthorities()
          	 */
          	public void setAuthorities(GrantedAuthority[] authorities) {
          		this.authorities = authorities;
          	}
        
    
  • la configuración de seguridad queda como sigue:
  • 
                   
                  
                  ...
              
        		
            
    
          	
          	  
          	  
          	
          	
          	
          				
          		
          				
          		
          
          		
          
          		
          		
          						
          				
          					/notAuthorized.jsf?login_error=1
          				
          				
          					/notAuthorized.jsf?login_error=2
          				
          			
          		
          	
          
          	
          	
          
          	
          		
          			
          				
          				
          			
          		
          	
          
          	
          		
          	
          
          	
        
    

    Con ello, no se requiere modificar el nombre de los perfiles, para que mantengan el patrón «ROLE_».

  • modificamos el action del formulario de autenticación de j_security_check a j_spring_security_check,
  • modificamos el action de logout para que contenga algo como lo que sigue:
  •  
            
          
        
    
  • añadimos las dependencias necesarias de Spring Security al pom.xml del proyecto autentia-parent,
  • 
      			org.springframework.security
      			spring-security-core
      			2.0.4
      			
      				
      					org.springframework
      					spring-aop
      				
      				
      					org.springframework
      					spring-beans
      				
      				
      					org.springframework
      					spring-context
      				
      				
      					org.springframework
      					spring-core
      				
      				
      					org.springframework
      					spring-support
      				
      			
      		
      		
      			org.springframework.security
      			spring-security-core-tiger
      			2.0.4
      		
      		
      		
      			org.aspectj
      			aspectjrt
      			1.6.1
      		
      		
      			org.aspectj
      			aspectjweaver
      			1.6.1
      		
        
    
  • introducimos en el web.xml el filtro de seguridad
  •   
        		springSecurityFilterChain
        		org.springframework.web.filter.DelegatingFilterProxy
        	
        	
        		springSecurityFilterChain
        		/*
        	
        
    
  • tenemos una clase de utilidades para dar soporte al cambio de conrtraseña, lo modificamos para que haga referencia a una clase de utilidades de Spring en vez de a una de Jboss
  •  final String passwordHash = new Md5PasswordEncoder().encodePassword(password, null);
        
    

En este punto la aplicación web, además de desplegarse, permite autenticarse y la autorización funciona como antes.

Si tenemos tests funcionales, deberían pasar con la misma efectividad que antes.

10. Conclusiones.

Por el número de pasos a realizar, se entiende la recomendación de no llevar a cabo la migración sin una buena batería de tests.

Ahora desplegamos en 18 seg. y la empaquetación la lleva a cabo el IDE (Eclipse), modificamos el fuente de una jspx y lo recarga en caliente. Parece una exageración,
pero antes, entre que compilaba, empaquetaba y se desplegaba en Jboss, ya se te había olvidado por qué querías desplegar y qué ibas a probar, hemos ganado bastante en salud mental.

El coste de la migración es perfectamente asumible, para que tengáis una referencia: una aplicación con 80 controladores, 20 servicios y 50 entidades se ha migrado en 4 jornadas
por una sola persona, incluido el coste de investigar la mejor manera de llevarlo a cabo, la documentación de la migración (este tutorial) y las pruebas.
Sí es cierto que nosotros tenemos las aplicaciones muy normalizadas y nuestros fuentes son todos muy estandar, con lo que podemos hacer modificaciones masivas en el código sin miedo.
Esta es la segunda migración que «sufre» a aplicación, la primera fue a facelets.

Si os animáis, contadnos qué tal os ha ido! y si no, pensad que siempre lo podemos hacer nosotros!!!, trabajamos en esto.

Un saludo.

Jose

mailto:jmsanchez@autentia.com

1 COMENTARIO

  1. Buenísimo, muchísimas gracias José Manuel, sólo me queda la pregunta de si actualmente en el año 2017, siempre es 100% posible hacer la migración de EJB hacia Spring, o pueden existir limitantes. Gracias.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad