← Volver al blog
Giancarlos Gaona·Publicado el 17 de marzo de 2026 · Actualizado el 3 de agosto de 2026·7 min de lectura

Service Builder en Liferay DXP 7.4: cuándo y cómo usarlo

LiferayJava

Qué es Service Builder y por qué sigue importando

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.

La estructura de service.xml

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.

Código generado: qué obtienes y qué modificas

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/setters
  • SolicitudPersistenceImpl.java: implementación de queries y finders
  • SolicitudLocalServiceBaseImpl.java: clase base del servicio local con métodos CRUD generados

Clases donde escribes tu lógica:

  • SolicitudLocalServiceImpl.java: aquí va toda tu lógica de negocio para servicios locales
  • SolicitudServiceImpl.java: aquí van los servicios remotos (con verificación de permisos)
  • SolicitudImpl.java: métodos adicionales en el modelo si necesitas lógica calculada

Esta 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.

La separación api/service: por qué importa

En Liferay 7.x, un módulo de Service Builder se divide en dos subproyectos:

  • modulo-api: contiene interfaces, modelos y excepciones. Es lo que otros módulos importan como dependencia.
  • modulo-service: contiene las implementaciones, la persistencia y la lógica de negocio. Nunca se importa directamente.

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.

LocalService vs Service: la diferencia de permisos

Esta distinción confunde a muchos desarrolladores, pero es simple:

  • LocalService: no verifica permisos. Se usa en lógica de backend, tareas programadas, listeners de eventos y cualquier contexto donde los permisos ya fueron verificados o no aplican.
  • Service (Remote Service): verifica permisos antes de ejecutar la acción. Se usa en controladores de portlets, APIs REST expuestas a usuarios finales y cualquier punto de entrada donde un usuario interactúa.
// 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.

Custom Finders y Dynamic Queries

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.

Cuándo elegir Service Builder sobre Objects

Esta es la pregunta clave en Liferay 7.4. Mi criterio después de trabajar con ambas herramientas en proyectos reales:

Usa Objects cuando:

  • La entidad es un CRUD simple sin lógica compleja
  • Los usuarios de negocio necesitan modificar la estructura sin deploy
  • No necesitas joins complejos entre entidades
  • El caso de uso es formularios, listas y vistas básicas

Usa Service Builder cuando:

  • Necesitas validaciones de negocio encadenadas (ej: verificar stock, calcular descuento, actualizar inventario, todo en una transacción)
  • Requieres joins entre múltiples tablas con lógica condicional
  • Integras con sistemas externos vía API dentro del flujo de persistencia
  • La entidad tiene un ciclo de vida complejo con máquina de estados
  • Necesitas control total sobre el SQL generado o queries nativas
  • Mantienes código legacy que ya usa Service Builder y no justifica reescritura

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.

Buenas prácticas en Service Builder

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.

GG
Giancarlos Gaona

Ingeniero de Software y Consultor Senior especializado en Liferay DXP, con más de 8 años de experiencia liderando proyectos enterprise para banca, retail, energía e instituciones públicas.