Service Builder es la herramienta de generación de código de Liferay que lleva presente en la plataforma desde sus versiones más tempranas. Su función principal es generar la capa de persistencia completa a partir de un archivo XML declarativo: modelos, servicios locales, servicios remotos, finders personalizados y toda la fontanería de acceso a base de datos.
Con la llegada de Objects en Liferay 7.4, muchos desarrolladores asumen que Service Builder quedó obsoleto. La realidad es diferente. Objects resuelve casos de uso de CRUD simple y formularios de datos con configuración visual, pero cuando la lógica de negocio es compleja, necesitas joins entre tablas, validaciones encadenadas o integración con sistemas legacy, Service Builder sigue siendo la herramienta correcta.
He trabajado en proyectos enterprise donde se combinan ambos: Objects para entidades simples de configuración y Service Builder para el núcleo de dominio del negocio. Entender cuándo usar cada uno es una habilidad crítica para cualquier desarrollador Liferay.
El corazón de Service Builder es el archivo service.xml. Este archivo define las entidades, sus columnas, relaciones y finders. Veamos la estructura básica:
<?xml version="1.0"?>
<!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 7.4.0//EN"
"http://www.liferay.com/dtd/liferay-service-builder_7_4_0.dtd">
<service-builder package-path="com.ejemplo.proyecto">
<namespace>Proyecto</namespace>
<entity name="Solicitud" local-service="true" remote-service="true"
uuid="true" uuid-accessor="true">
<!-- Campos de auditoria (Liferay los gestiona automaticamente) -->
<column name="solicitudId" type="long" primary="true" />
<column name="groupId" type="long" />
<column name="companyId" type="long" />
<column name="userId" type="long" />
<column name="userName" type="String" />
<column name="createDate" type="Date" />
<column name="modifiedDate" type="Date" />
<!-- Campos de negocio -->
<column name="titulo" type="String" />
<column name="descripcion" type="String" />
<column name="estado" type="int" />
<column name="prioridad" type="int" />
<column name="fechaLimite" type="Date" />
<!-- Finders personalizados -->
<finder name="Estado" return-type="Collection">
<finder-column name="estado" />
</finder>
<finder name="G_E" return-type="Collection">
<finder-column name="groupId" />
<finder-column name="estado" />
</finder>
<!-- Orden por defecto -->
<order by="desc">
<order-column name="createDate" />
</order>
</entity>
</service-builder>
Cada elemento tiene un propósito claro. El atributo uuid="true" genera un identificador universalmente único, esencial para exportación/importación entre ambientes. Los finders generan métodos de consulta optimizados automáticamente. El bloque order define el ordenamiento por defecto de las consultas.
Un detalle que muchos desarrolladores nuevos pasan por alto: las columnas de auditoría (groupId, companyId, userId, userName, createDate, modifiedDate) no son opcionales en la práctica. Liferay las usa internamente para multitenancy, permisos y seguimiento. Si las omites, perderás integración con funcionalidades core de la plataforma.
Cuando ejecutas buildService (vía Gradle o Blade CLI), Service Builder genera una cantidad considerable de clases. La clave es entender cuáles puedes modificar y cuáles no:
Clases que NO debes tocar (se regeneran):
SolicitudModel.java y SolicitudModelImpl.java: el modelo base con getters/settersSolicitudPersistenceImpl.java: implementación de queries y findersSolicitudLocalServiceBaseImpl.java: clase base del servicio local con métodos CRUD generadosClases donde escribes tu lógica:
SolicitudLocalServiceImpl.java: aquí va toda tu lógica de negocio para servicios localesSolicitudServiceImpl.java: aquí van los servicios remotos (con verificación de permisos)SolicitudImpl.java: métodos adicionales en el modelo si necesitas lógica calculadaEsta separación es fundamental. Service Builder regenera las clases base cada vez que ejecutas el build, pero respeta tus implementaciones en las clases *Impl. Si por error escribes lógica en una clase base, la perderás en el siguiente build.
En Liferay 7.x, un módulo de Service Builder se divide en dos subproyectos:
Esta separación sigue el principio de inversión de dependencias. Si tienes un portlet que necesita llamar a SolicitudLocalService, solo depende del módulo api. La implementación concreta la resuelve el contenedor OSGi en tiempo de ejecución.
// build.gradle del portlet que consume el servicio
dependencies {
compileOnly project(":modules:solicitud:solicitud-api")
// NUNCA: compileOnly project(":modules:solicitud:solicitud-service")
}
Esto tiene implicaciones reales: puedes actualizar la implementación del servicio sin recompilar los portlets que lo consumen, siempre que la interfaz no cambie. En proyectos grandes con múltiples equipos, esta separación evita acoplamientos destructivos.
Esta distinción confunde a muchos desarrolladores, pero es simple:
// En SolicitudLocalServiceImpl.java - SIN verificacion de permisos
public Solicitud addSolicitud(long userId, long groupId,
String titulo, String descripcion, int prioridad) {
long solicitudId = counterLocalService.increment();
Solicitud solicitud = solicitudPersistence.create(solicitudId);
solicitud.setUserId(userId);
solicitud.setGroupId(groupId);
solicitud.setTitulo(titulo);
solicitud.setDescripcion(descripcion);
solicitud.setPrioridad(prioridad);
solicitud.setEstado(WorkflowConstants.STATUS_DRAFT);
solicitud.setCreateDate(new Date());
solicitud.setModifiedDate(new Date());
return solicitudPersistence.update(solicitud);
}
// En SolicitudServiceImpl.java - CON verificacion de permisos
public Solicitud addSolicitud(long groupId, String titulo,
String descripcion, int prioridad) throws PortalException {
// Verificar que el usuario tiene permiso para crear solicitudes
ModelResourcePermissionUtil.check(
_solicitudModelResourcePermission,
getPermissionChecker(), groupId, 0, ActionKeys.ADD_ENTRY);
// Delegar al LocalService
return solicitudLocalService.addSolicitud(
getUserId(), groupId, titulo, descripcion, prioridad);
}
El patrón recomendado es que ServiceImpl verifique permisos y delegue a LocalServiceImpl para la lógica real. Así evitas duplicar código y mantienes la verificación de permisos en un solo lugar.
Los finders declarados en service.xml cubren consultas simples por igualdad. Para queries más complejas, tienes dos opciones:
Custom SQL con FinderImpl: creas una clase SolicitudFinderImpl en el módulo service y escribes SQL nativo en archivos XML ubicados en src/main/resources/META-INF/custom-sql/.
-- custom-sql/default.xml
<custom-sql>
<sql id="com.ejemplo.proyecto.service.persistence.SolicitudFinder.findByEstadoYPrioridad">
SELECT * FROM Proyecto_Solicitud
WHERE estado = ? AND prioridad >= ?
AND groupId = ?
ORDER BY createDate DESC
</sql>
</custom-sql>
Dynamic Query: usa la API de Hibernate Criteria envuelta por Liferay. Es más flexible pero menos performante para queries complejas.
DynamicQuery dynamicQuery = DynamicQueryFactoryUtil.forClass(
Solicitud.class, classLoader);
dynamicQuery.add(RestrictionsFactoryUtil.eq("estado", estado));
dynamicQuery.add(RestrictionsFactoryUtil.ge("prioridad", minPrioridad));
dynamicQuery.addOrder(OrderFactoryUtil.desc("createDate"));
List<Solicitud> resultados = solicitudLocalService.dynamicQuery(dynamicQuery);
En mi experiencia, Custom SQL es preferible cuando la query es conocida y estable. Dynamic Query es útil para búsquedas con filtros opcionales donde la query se construye dinámicamente según los parámetros del usuario.
Esta es la pregunta clave en Liferay 7.4. Mi criterio después de trabajar con ambas herramientas en proyectos reales:
Usa Objects cuando:
Usa Service Builder cuando:
Un patrón que funciona bien en proyectos grandes es usar Objects como capa de configuración y datos de referencia, y Service Builder para las entidades core del dominio donde vive la lógica de negocio crítica.
Después de años trabajando con Service Builder en distintas versiones de Liferay, estas son las prácticas que más impacto tienen:
Servicios delgados: los métodos en LocalServiceImpl deben orquestar, no contener cientos de líneas de lógica. Extrae lógica compleja a clases auxiliares inyectadas vía OSGi @Reference.
Evita dependencias circulares: si el módulo A depende del API de B, el módulo B no debe depender del API de A. Esto parece obvio pero ocurre frecuentemente cuando los servicios crecen. La solución es extraer interfaces comunes a un módulo compartido o usar eventos.
Versionado de service.xml: cada cambio en service.xml que modifica columnas existentes requiere un upgrade step. No cambies tipos de columna directamente; crea una nueva columna, migra datos y elimina la vieja en un upgrade posterior.
Testing: los métodos de LocalServiceImpl son testables con tests de integración usando @RunWith(Arquillian.class) en Liferay 7.4. Escribe tests para la lógica de negocio crítica, especialmente validaciones y cálculos.
Service Builder no es glamuroso ni moderno, pero es una herramienta probada que resuelve problemas reales de persistencia empresarial. Conocerlo en profundidad sigue siendo una ventaja competitiva para cualquier desarrollador Liferay.