This guide is for business developers writing Hook scripts on the iComposer platform. It covers the concepts, authoring workflow, and verification methods for the three types of Hooks.
1. Overview: What are Hooks?
iComposer Hooks are a hot-deployable Groovy script extension mechanism that lets tenants inject long-lived, runtime-resident custom logic without restarting the service:
- Listen to messages (iComposer Listener)
- Participate in distributed transactions (TCC)
- React to cluster-wide cache changes (Clear Cache Listener)
Unlike regular Groovy APIs / ServiceFunctions (request-driven and executed only when invoked), a Hook continuously listens for events after registration and is triggered automatically when an event arrives.
Key characteristics
- Script-based: Write Groovy method bodies in the UI editor; saving compiles immediately.
- Hot reload: After a new version is published, no restart is needed — the platform automatically unloads the old script and loads the new one via cache events.
- Tenant-isolated: Each Hook belongs to a tenant/module and is isolated by tenant context at registration time.
- Auto-recovery on startup: When the runtime service restarts, it automatically pulls all active Hooks from the database and re-registers them.
2. The three Hook types at a glance
| Type | Category ID | Handler interface | When triggered | Typical scenario |
|---|---|---|---|---|
| iComposer Listener | 1 | ListenerHandler | MQ message arrival (Kafka / RabbitMQ) | Asynchronously consume order / notification queues |
| TCC | 2 | TccHandler | Seata global transaction prepare / commit / rollback | Cross-service distributed transactions: reserve / confirm / roll back resources |
| Clear Cache Listener | 3 | ClearCacheHandler | Cluster-wide cache change event | Clear local caches when upstream configuration changes |
3. Authoring and deploying Hooks in the UI
All three Hook types share the same authoring and deployment flow — learn it once.
3.1 Entry point and steps
- Open the iComposer designer and go to the Hooks management page.
- Create a new hook and choose a category:
1→ iComposer Listener2→ TCC3→ Clear Cache Listener
- After choosing a category, the editor automatically applies the corresponding code template as a starting point.
- Write/modify the Groovy method body in the “user code area”.
- Save and set the status to ACTIVE; the platform immediately triggers compilation.
3.2 Built-in capabilities available in the script
BaseMonitorGroovySupport provides the following out-of-the-box methods for every Hook:
| Purpose | Call |
|---|---|
| Get a Spring Bean | getBean("beanName") or getBean(SomeType.class) |
| Logging | info(...) / warn(...) / error(...) / debug(...) |
In addition, utility classes under com.insuremo.icomposer.utils (such as IComposerHooksUtils, IComposerEnv) can be imported and used directly in scripts. See each type’s section below for details.
4. Type 1: iComposer Listener tutorial
4.1 Responsibility
Listen continuously (Kafka / RabbitMQ) and hand each message off to a ServiceFunction for processing.
4.2 Interface and methods
| Method | When called | What you do |
|---|---|---|
void init() | On application startup / after hot-reload registration | Read MQ config, create the Listener container, register it with the Registry |
void destroy() | On unregister / shutdown / hot-reload unload | Release custom resources |
You may also define extra methods (e.g.
send(data)as a companion producer); the platform will not interfere.
4.3 When triggered
Consumption starts automatically after the Spring container starts; each time a message arrives your message callback is triggered.
4.4 Three-step authoring
① Read config IComposerEnv.getParameter("mq.<business>.xxx")
② Build container Build a ConnectionFactory / ConsumerFactory + Listener container;
in the message callback, use IComposerHooksUtils.executeMethod(functionName, methodName, body)
to hand the message off to a ServiceFunction
③ Register IComposerHooksUtils.registerResource(this, [container: container])Key points:
- Lifecycle ordering: Before calling your
destroy(), the platform first callsstop()on anycontainerregistered viaregisterResource(waiting for in-flight messages to finish processing). Thereforedestroy()only needs to release custom resources outside the container (e.g. a self-createdCachingConnectionFactory). registerResource(this, [container: container])automatically extracts and manages thecontainer(a SpringLifecycle); the resource ID is your class name with theGry_Monitor_prefix removed.- For
executeMethod(functionName, methodName, args...), whenmethodNameisnullit defaults toexecute. - Always
try/catchinside the message callback and log exceptions witherror(...)to avoid silently swallowing them. - Multiple MQ connections: each MonitorTask script uses its own
mq.<business>.*config-center parameters, independent of one another.
4.5 Full example: Kafka Listener
import org.springframework.kafka.listener.ConcurrentMessageListenerContainer
import org.springframework.kafka.listener.ContainerProperties
import org.springframework.kafka.core.DefaultKafkaConsumerFactory
import com.insuremo.icomposer.utils.IComposerHooksUtils
import com.insuremo.icomposer.utils.IComposerEnv
private ConcurrentMessageListenerContainer container
void init() {
// 1. Read Kafka connection params from the config center (do not hardcode)
def bootstrapServers = IComposerEnv.getParameter("mq.claim.bootstrapServers")
def topic = IComposerEnv.getParameter("mq.claim.topic")
def groupId = IComposerEnv.getParameter("mq.claim.groupId")
def consumers = IComposerEnv.getParameter("mq.claim.consumers", "3") as int
// 2. Build the ConsumerFactory + Listener container
def props = new HashMap<String, Object>()
props.put("bootstrap.servers", bootstrapServers)
props.put("group.id", groupId)
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
props.put("auto.offset.reset", "latest")
def consumerFactory = new DefaultKafkaConsumerFactory<String, String>(props)
def containerProps = new ContainerProperties(topic)
containerProps.setGroupId(groupId)
container = new ConcurrentMessageListenerContainer(consumerFactory, containerProps)
container.getContainerProperties().setMessageListener({ record ->
try {
// Hand the message off to a ServiceFunction for processing
IComposerHooksUtils.executeMethod("ClaimHandlerFunction", "execute", record.value())
} catch (Exception e) {
error("Failed to process message: ${e.message}"); e.printStackTrace()
}
})
container.setConcurrency(consumers)
// 3. Register with the Registry (container is managed automatically)
IComposerHooksUtils.registerResource(this, [container: container])
info("Kafka ClaimListenerHandler started")
}
void destroy() {
// The framework already called lifecycle.stop() to wait for in-flight messages;
// release only custom resources here
info("Kafka ClaimListenerHandler destroyed")
}Configure in the config center:
mq.claim.bootstrapServers,mq.claim.topic,mq.claim.groupId,mq.claim.consumers.
4.6 Variant: RabbitMQ Listener
Replace the Kafka parts of the example above with RabbitMQ; the structure is identical:
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer
import com.insuremo.icomposer.utils.IComposerHooksUtils
import com.insuremo.icomposer.utils.IComposerEnv
private CachingConnectionFactory connectionFactory
private SimpleMessageListenerContainer container
void init() {
def host = IComposerEnv.getParameter("mq.order.host")
def port = IComposerEnv.getParameter("mq.order.port", "5672") as int
def username = IComposerEnv.getParameter("mq.order.username")
def password = IComposerEnv.getParameter("mq.order.password")
def virtualHost = IComposerEnv.getParameter("mq.order.virtualHost", "/")
def queueName = IComposerEnv.getParameter("mq.order.queue")
def consumers = IComposerEnv.getParameter("mq.order.consumers", "3") as int
connectionFactory = new CachingConnectionFactory(host, port)
connectionFactory.setUsername(username)
connectionFactory.setPassword(password)
connectionFactory.setVirtualHost(virtualHost)
container = new SimpleMessageListenerContainer()
container.setConnectionFactory(connectionFactory)
container.setQueueNames(queueName)
container.setConcurrentConsumers(consumers)
container.setMessageListener({ message ->
try {
def body = new String(message.getBody(), "UTF-8")
IComposerHooksUtils.executeMethod("OrderHandlerFunction", "execute", body)
} catch (Exception e) {
error("Failed to process message: ${e.message}"); e.printStackTrace()
}
})
IComposerHooksUtils.registerResource(this, [container: container])
info("RabbitMQ OrderListenerHandler started")
}
void destroy() {
if (connectionFactory != null) {
connectionFactory.destroy()
}
info("RabbitMQ OrderListenerHandler destroyed")
}Configure in the config center:
mq.order.host,mq.order.port,mq.order.username,mq.order.password,mq.order.virtualHost,mq.order.queue,mq.order.consumers.
4.7 How to verify
- After saving the script (ACTIVE), make sure the application has started or finished hot-reloading.
- Send a test message to the target queue / topic.
- Check the application logs: you should see the
startedlog line, plus the ServiceFunction’s execution log when a message arrives.
5. Type 2: TCC distributed transaction tutorial
5.1 Responsibility
Act as a branch participant in a Seata TCC (Try-Confirm-Cancel) distributed transaction, reserving resources in the Try phase and letting the transaction coordinator decide the final commit or rollback.
5.2 Interface and methods
| Method | Phase | Return | Description |
|---|---|---|---|
Boolean prepare(IComposerTccActionContext ctx) | Try | Boolean | Reserve resources (freeze an amount, lock stock, etc.) |
Boolean commit(IComposerTccActionContext ctx) | Confirm | Boolean | Confirm resources (deduct, confirm payment, etc.) |
Boolean rollback(IComposerTccActionContext ctx) | Cancel | Boolean | Release the reserved resources |
The return type is
Boolean(the wrapper type), not the primitiveboolean. Returningtruemeans success; throwing an exception or returningfalsemeans failure.
5.3 When triggered
- prepare: Triggered explicitly by business code calling
IComposerHooksUtils.prepareTcc("actionName", params). - commit / rollback: Coordinated and called by the Seata TC (transaction coordinator) when the global transaction ends; not called directly by business code.
5.4 Key API: IComposerTccActionContext
| Method | Return | Description |
|---|---|---|
getParams() | String | The business-parameter JSON passed in during prepare |
getXid() | String | Global transaction ID |
getActionName() | String | Name of the current TCC action |
5.5 Authoring tips
- Resources reserved in prepare must be confirmed in commit and released in rollback — the three must agree.
- commit / rollback must be idempotent: Seata may retry these two methods due to timeouts or other reasons.
getParams()returns a JSON string; parse it yourself as needed.
5.6 Full example: freeze / confirm / unfreeze an amount
import com.insuremo.icomposer.hooks.tcc.IComposerTccActionContext
import com.insuremo.icomposer.utils.IComposerJsonUtils
// Try: freeze the amount
Boolean prepare(IComposerTccActionContext ctx) {
def params = new IComposerJsonUtils().parseText(ctx.getParams())
def xid = ctx.getXid()
info("TCC prepare: xid=${xid}, orderId=${params.orderId}, amount=${params.amount}")
// Business logic: validate and freeze the amount
// orderService.freezeAmount(params.orderId as String, params.amount as BigDecimal)
return true
}
// Confirm: confirm the deduction
Boolean commit(IComposerTccActionContext ctx) {
def xid = ctx.getXid()
info("TCC commit: xid=${xid}, action=${ctx.getActionName()}")
// Business logic: confirm-deduct the amount frozen in prepare (must be idempotent)
// (prepare usually records the freeze info keyed by xid; confirm accordingly)
return true
}
// Cancel: unfreeze and release
Boolean rollback(IComposerTccActionContext ctx) {
def xid = ctx.getXid()
info("TCC rollback: xid=${xid}, action=${ctx.getActionName()}")
// Business logic: release the amount frozen in prepare (must be idempotent)
// (prepare usually records the freeze info keyed by xid; release accordingly)
return true
}How business code triggers prepare (written in another api/ServiceFunction):
import com.insuremo.icomposer.utils.IComposerHooksUtils
def params = [orderId: "ORD-001", amount: 100]
boolean ok = IComposerHooksUtils.prepareTcc("FreezeAmountAction", params)
// ok == true means prepare succeeded; commit / rollback are then coordinated by Seata TC when the global transaction ends5.7 How to verify
- Issue a business request that calls
prepareTcc("FreezeAmountAction", ...). - Success path: all branches’ prepare succeed → observe the
prepareandcommitlogs. - Failure path: deliberately make one branch’s prepare fail or throw → observe the
prepareandrollbacklogs.
6. Type 3: ClearCache cache-event tutorial
6.1 Responsibility
Listen for the platform’s cluster-wide cache change events. When another node publishes a configuration / product / rule change (triggering cluster-wide cache sync), this node clears or refreshes its locally derived caches accordingly.
6.2 Interface and methods
Implement com.insuremo.unicorn.platform.groovy.monitor.cache.clear.ClearCacheHandler:
void onCacheChanged(IComposerCacheChangeEvent event,
String bucketName,
String cacheName,
Object cacheKey)| Parameter | Meaning |
|---|---|
event | Cache change event; carries tenant, change type, etc. |
bucketName | Cache bucket name |
cacheName | Cache name |
cacheKey | The changed cache key; null means clear-all |
6.3 When triggered
After the iComposer service receives a platform cache-sync event, it dispatches to all registered ClearCacheHandlers. Triggered on every cache change.
6.4 Key API: IComposerCacheChangeEvent
| Method | Description |
|---|---|
getType() | Change type (e.g. CLEAR for clear-all / UPDATE for a single entry) |
getTenantCode() | Tenant code |
6.5 Authoring tips
onCacheChangedreceives all cache change events; filter bybucketName/cacheNamefor the ones you care about.cacheKey == nullmeans clear-all; in that case you should refresh the entire local cache.- A single handler throwing an exception does not affect other handlers (the dispatcher calls them one by one and isolates failures).
6.6 Full example: clear locally derived caches by cache name
import com.insuremo.icomposer.hooks.cache.clear.IComposerCacheChangeEvent
void onCacheChanged(IComposerCacheChangeEvent event,
String bucketName,
String cacheName,
Object cacheKey) {
info("Cache changed: bucket=${bucketName}, cache=${cacheName}, key=${cacheKey}, type=${event.getType()}, tenant=${event.getTenantCode()}")
if (cacheKey == null) {
// Clear-all
// productLocalCache.invalidateAll()
return
}
// Branch by cache name
if (cacheName.contains("ProductCache")) {
// productLocalCache.invalidate(cacheKey)
} else if (cacheName.contains("RuleCache")) {
// ruleEngine.reload(cacheKey)
}
}7. FAQ & troubleshooting
Q1: Saved but not taking effect?
Check whether the status is ACTIVE; check whether compilation reported errors (the return info when saving); confirm the runtime has hot-reloaded (use the status API in 8.2 to see whether the resource is registered). If necessary, bump the version number and save again to trigger cache invalidation.
Q2: Should I write a full class, or just the method body?
Write only a method-body fragment (imports + methods). The platform’s MonitorTaskBeanGenerator automatically wraps it in a class Gry_Monitor_<name> extends BaseMonitorGroovySupport implements <corresponding Handler> shell. Adding an outer class yourself will instead cause a conflict.
Q3: How do I get a Spring Bean / log in a script?
Use getBean("xxx") or getBean(SomeType.class) to get a Bean; use info(...) / warn(...) / error(...) / debug(...) for logging — these are provided by BaseMonitorGroovySupport.
Q4: What if one Hook needs to connect to multiple MQs?
Give each MonitorTask script its own mq.<business>.* config-center parameters; each creates its own connection inside the script, independently.
Q5: TCC commit / rollback not being called?
Check: ① did prepare return true; ② is Seata TC running normally; ③ confirm that commit/rollback is coordinated by TC, not called directly by business code.
Q6: What happens if Listener consumption throws an exception?
try/catch inside the message callback and log it with error(...). Before destroy(), the framework first calls lifecycle.stop() to wait for in-flight messages to finish — it will not abort abruptly.
Q7: Too many events in onCacheChanged?
This is normal — it receives every cache change. Filter by bucketName / cacheName inside the method; cacheKey == null means clear-all.
Appendix: Cheat sheet for the three Hook types
| Dimension | iComposer Listener | TCC | Clear Cache Listener |
|---|---|---|---|
| Category ID | 1 | 2 | 3 |
| Handler interface | ListenerHandler | TccHandler | ClearCacheHandler |
| Key methods | init() / destroy() | prepare / commit / rollback(ctx) returning Boolean | onCacheChanged(event, bucketName, cacheName, cacheKey) |
| Triggered by | Spring MQ container (message arrival) | Business calls IComposerHooksUtils.prepareTcc + Seata TC coordination | IComposerCacheChangeListener dispatch |
| Register / trigger API | IComposerHooksUtils.registerResource(...) | IComposerHooksUtils.prepareTcc(...) | Auto-dispatched by the platform; no manual registration |