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.
@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.
@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.
@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.
@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.
@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.).
@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.
@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.MyUserProviderSPI Summary
| SPI Interface | Required? | Description |
|---|---|---|
QfUserProvider | ✅ Required | Provides current user information |
QfOrganizationProvider | Conditional | Required when using the organization model |
QfRuntimeInitializationHook | Optional | Hook at initialization time |
QfInitDataContributor | Optional | Insert initial data |
QfDiagnosticListener | Optional | Receive diagnostic events |
QfRuntimeConfigProvider | Optional | Provide dynamic configuration |
QfErrorNotificationHandler | Optional | Handle error notifications |
Next Steps
- Architecture Overview — Q-Framework internal structure
- Annotation Reference — Complete annotation list