← Volver al blog
·7 min de lectura

Service Builder en Liferay DXP 7.4: cuando y como usarlo

LiferayJava

Que es Service Builder y por que sigue importando

Service Builder es la herramienta de generacion de codigo de Liferay que lleva presente en la plataforma desde sus versiones mas tempranas. Su funcion 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 fontaneria de acceso a base de datos.

Con la llegada de Objects en Liferay 7.4, muchos desarrolladores asumen que Service Builder quedo obsoleto. La realidad es diferente. Objects resuelve casos de uso de CRUD simple y formularios de datos con configuracion visual, pero cuando la logica de negocio es compleja, necesitas joins entre tablas, validaciones encadenadas o integracion con sistemas legacy, Service Builder sigue siendo la herramienta correcta.

He trabajado en proyectos enterprise donde se combinan ambos: Objects para entidades simples de configuracion y Service Builder para el nucleo de dominio del negocio. Entender cuando usar cada uno es una habilidad critica para cualquier desarrollador Liferay.

La estructura de service.xml

El corazon de Service Builder es el archivo service.xml. Este archivo define las entidades, sus columnas, relaciones y finders. Veamos la estructura basica:

<?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 proposito claro. El atributo uuid="true" genera un identificador universalmente unico, esencial para exportacion/importacion entre ambientes. Los finders generan metodos de consulta optimizados automaticamente. El bloque order define el ordenamiento por defecto de las consultas.

Un detalle que muchos desarrolladores nuevos pasan por alto: las columnas de auditoria (groupId, companyId, userId, userName, createDate, modifiedDate) no son opcionales en la practica. Liferay las usa internamente para multitenancy, permisos y seguimiento. Si las omites, perderas integracion con funcionalidades core de la plataforma.

Codigo generado: que obtienes y que modificas

Cuando ejecutas buildService (via Gradle o Blade CLI), Service Builder genera una cantidad considerable de clases. La clave es entender cuales puedes modificar y cuales no:

Clases que NO debes tocar (se regeneran):

  • SolicitudModel.java y SolicitudModelImpl.java: el modelo base con getters/setters
  • SolicitudPersistenceImpl.java: implementacion de queries y finders
  • SolicitudLocalServiceBaseImpl.java: clase base del servicio local con metodos CRUD generados

Clases donde escribes tu logica:

  • SolicitudLocalServiceImpl.java: aqui va toda tu logica de negocio para servicios locales
  • SolicitudServiceImpl.java: aqui van los servicios remotos (con verificacion de permisos)
  • SolicitudImpl.java: metodos adicionales en el modelo si necesitas logica calculada

Esta separacion 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 logica en una clase base, la perderas en el siguiente build.

La separacion api/service: por que importa

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

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

Esta separacion sigue el principio de inversion de dependencias. Si tienes un portlet que necesita llamar a SolicitudLocalService, solo depende del modulo api. La implementacion concreta la resuelve el contenedor OSGi en tiempo de ejecucion.

// 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 implementacion del servicio sin recompilar los portlets que lo consumen, siempre que la interfaz no cambie. En proyectos grandes con multiples equipos, esta separacion evita acoplamientos destructivos.

LocalService vs Service: la diferencia de permisos

Esta distincion confunde a muchos desarrolladores, pero es simple:

  • LocalService: no verifica permisos. Se usa en logica 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 accion. Se usa en controladores de portlets, APIs REST expuestas a usuarios finales y cualquier punto de entrada donde un usuario interactua.
// 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
    ModelResourcePermissionHelper.check(
        _solicitudModelResourcePermission,
        getPermissionChecker(), groupId, 0, ActionKeys.ADD_ENTRY);

    // Delegar al LocalService
    return solicitudLocalService.addSolicitud(
        getUserId(), groupId, titulo, descripcion, prioridad);
}

El patron recomendado es que ServiceImpl verifique permisos y delegue a LocalServiceImpl para la logica real. Asi evitas duplicar codigo y mantienes la verificacion de permisos en un solo lugar.

Custom Finders y Dynamic Queries

Los finders declarados en service.xml cubren consultas simples por igualdad. Para queries mas complejas, tienes dos opciones:

Custom SQL con FinderImpl: creas una clase SolicitudFinderImpl en el modulo 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 mas 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 util para busquedas con filtros opcionales donde la query se construye dinamicamente segun los parametros del usuario.

Cuando elegir Service Builder sobre Objects

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

Usa Objects cuando:

  • La entidad es un CRUD simple sin logica 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 basicas

Usa Service Builder cuando:

  • Necesitas validaciones de negocio encadenadas (ej: verificar stock, calcular descuento, actualizar inventario, todo en una transaccion)
  • Requieres joins entre multiples tablas con logica condicional
  • Integras con sistemas externos via API dentro del flujo de persistencia
  • La entidad tiene un ciclo de vida complejo con maquina de estados
  • Necesitas control total sobre el SQL generado o queries nativas
  • Mantienes codigo legacy que ya usa Service Builder y no justifica reescritura

Un patron que funciona bien en proyectos grandes es usar Objects como capa de configuracion y datos de referencia, y Service Builder para las entidades core del dominio donde vive la logica de negocio critica.

Buenas practicas en Service Builder

Despues de anos trabajando con Service Builder en distintas versiones de Liferay, estas son las practicas que mas impacto tienen:

Servicios delgados: los metodos en LocalServiceImpl deben orquestar, no contener cientos de lineas de logica. Extrae logica compleja a clases auxiliares inyectadas via OSGi @Reference.

Evita dependencias circulares: si el modulo A depende del API de B, el modulo B no debe depender del API de A. Esto parece obvio pero ocurre frecuentemente cuando los servicios crecen. La solucion es extraer interfaces comunes a un modulo 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 metodos de LocalServiceImpl son testables con tests de integracion usando @RunWith(Arquillian.class) en Liferay 7.4. Escribe tests para la logica de negocio critica, especialmente validaciones y calculos.

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.