Skip to content

SPI Extensions

Q-Framework provides extension points through the SPI (Service Provider Interface) pattern. Behavior can be customized solely through interface implementations, without modifying framework internals.

SPI Design Principles

Q-Framework (defines interfaces)
        ↑ depends on
Application (provides implementations)
  • High-level framework does not depend on low-level adapters (DIP principle)
  • Implementations are discovered automatically at runtime
  • Multiple implementations can be registered for a single interface

QfUserProvider

Provides current request user information. Must be implemented.

java
@Component
public class MyUserProvider implements QfUserProvider {

    @QfAllowedDirectAccess(reason = "SPI implementation — must read user store directly")
    private final UserRepository userRepository;

    @Override
    public QfUser getCurrentUser(HttpServletRequest request) {
        // Works with Spring Security, JWT, Session, or any approach
        String userId = extractUserIdFromRequest(request);

        User user = userRepository.findById(userId)
            .orElseThrow(() -> new UnauthorizedException());

        return QfUser.builder()
            .id(user.getId())
            .name(user.getName())
            .organizationId(user.getOrganizationId())
            .privileges(roleService.getPrivileges(user.getRoles()))
            .locale(user.getPreferredLocale())
            .build();
    }
}

QfOrganizationProvider

Provides organizational hierarchy data. Required when using the organization model.

java
@Component
public class MyOrganizationProvider implements QfOrganizationProvider {

    @QfAllowedDirectAccess(reason = "SPI implementation — must read organization store directly")
    private final OrganizationRepository organizationRepository;

    @Override
    public List<QfOrganization> getOrganizationHierarchy(String rootOrgId) {
        return organizationRepository.findHierarchy(rootOrgId)
            .stream()
            .map(this::toQfOrganization)
            .collect(Collectors.toList());
    }

    @Override
    public String getCurrentUserOrganizationId() {
        return QfContext.currentUser().getOrganizationId();
    }

    private QfOrganization toQfOrganization(OrganizationEntity org) {
        return QfOrganization.builder()
            .id(org.getId())
            .parentId(org.getParentId())
            .name(org.getName())
            .depth(org.getDepth())
            .build();
    }
}

QfRuntimeInitializationHook

Performs additional work at runtime initialization time.

java
@Component
public class MyInitializationHook implements QfRuntimeInitializationHook {

    @Override
    public void onBeforeInitialize(QfRuntimeContext context) {
        // Before initialization: validation, preprocessing, etc.
        log.info("Q-Framework initialization starting");
    }

    @Override
    public void onAfterInitialize(QfRuntimeContext context) {
        // After initialization: cache preloading, default data setup, etc.
        log.info("Registered entities: {}", context.getEntityCount());
        log.info("Registered capabilities: {}", context.getCapabilityCount());
    }
}

QfInitDataContributor

Inserts initial data when the application starts.

java
@Component
public class AdminRoleContributor implements QfInitDataContributor {

    @Override
    public int getOrder() {
        return 100;  // execution order
    }

    @Override
    public void contribute(QfInitDataContext context) {
        // skip if already exists
        if (context.exists("roles", "ROLE_ADMIN")) {
            return;
        }

        // create default admin role
        context.insert("roles", Map.of(
            "id", "ROLE_ADMIN",
            "name", "System Administrator",
            "privileges", context.getAllPrivileges()
        ));
    }
}

QfDiagnosticListener

Receives diagnostic events.

java
@Component
public class MyDiagnosticListener implements QfDiagnosticListener {

    @Override
    public void onEntityRegistered(QfEntityMetadata metadata) {
        log.debug("Entity registered: {}.{}", metadata.getClientApp(), metadata.getName());
    }

    @Override
    public void onValidationError(QfValidationErrorEvent event) {
        // send to monitoring system
        monitoring.track("validation_error", Map.of(
            "entity", event.getEntityName(),
            "field", event.getFieldName()
        ));
    }
}

QfRuntimeConfigProvider

Dynamically provides runtime configuration from external sources (DB, Config Server, etc.).

java
@Component
public class DatabaseConfigProvider implements QfRuntimeConfigProvider {

    @QfAllowedDirectAccess(reason = "SPI implementation — must read config store directly")
    private final ConfigRepository configRepository;

    @Override
    public Map<String, Object> getConfig() {
        // load configuration from DB
        return configRepository.findAll()
            .stream()
            .collect(Collectors.toMap(
                Config::getKey,
                Config::getValue
            ));
    }

    @Override
    public int getOrder() {
        return 200;  // higher priority than application.yml
    }
}

Direct Data Access in SPI Implementations

SPI implementations that inject Spring Data Repositories, EntityManager, JdbcTemplate, or other direct data access types must annotate those fields with @QfAllowedDirectAccess(reason = "...").

This is required when qf.persistence.access.mode is permissive (default) or strict. Without it, the application will fail to start.


SPI Registration

In a Spring Boot environment, simply add @Component and the SPI is automatically registered.

java
@Component  // Q-Framework auto-discovers this
public class MyUserProvider implements QfUserProvider {
    // ...
}

In non-Spring environments, use the ServiceLoader approach:

META-INF/services/net.softminds.qframework.spi.QfUserProvider
→ com.example.myapp.MyUserProvider

SPI Summary

SPI InterfaceRequired?Description
QfUserProvider✅ RequiredProvides current user information
QfOrganizationProviderConditionalRequired when using the organization model
QfRuntimeInitializationHookOptionalHook at initialization time
QfInitDataContributorOptionalInsert initial data
QfDiagnosticListenerOptionalReceive diagnostic events
QfRuntimeConfigProviderOptionalProvide dynamic configuration
QfErrorNotificationHandlerOptionalHandle error notifications

Next Steps

Released under the Apache 2.0 License.