Handling Multiple Business Scenarios in an MQ Consumer
At heart, these are several ways to handle too many peer-level business branches in one scenario.
Introduction
Many projects use message queues for asynchronous processing, which necessarily creates a consumer side. A listener may receive several topics or several bizCode values, each requiring different processing logic. The ordinary solution is a series of if-else branches:
1 |
|
This approach violates the open-closed principle: every additional business scenario requires another branch. It is fundamentally a case of too many branches. I came up with several alternatives.
Strategy Pattern
When one consumer listener handles several bizCode values and each value corresponds to a different strategy, we can implement the strategy pattern through a table-driven design. A map routes topics to their handlers.
First, define a strategy interface.
1
2
3
4
5
6
7
8
9
10
11
12
13
14public interface StrategyHandler {
/**
* 监控处理器
* @param bean
*/
void handle(StrategyParam param);
/**
* 支持的业务码
* @return bizCode
*/
BizCode supportBiz();
}Next, define a factory that loads strategies through a map.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class HandlerFactory implements BeanPostProcessor {
private static Map<BizCode, StrategyHandler> HANDLER_CACHE = new HashMap<>(16);
public static StrategyHandler getHandler(BizCode code){
return HANDLER_CACHE.getOrDefault(code, DefaultHandler.INSTANCE);
}
public Object postProcessBeforeInitialization(Object o, String s) throws BeansException {
return o;
}
public Object postProcessAfterInitialization(Object o, String s) throws BeansException {
if(o instanceof StategyHandler){
StategyHandler handler = (StategyHandler)o;
HANDLER_CACHE.put(handler.supportBiz(),handler);
}
return o;
}
}Define the concrete default strategy.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class DefaultHandler implements StategyHandler {
/**
* singleton
*/
public static final DefaultHandler INSTANCE = new DefaultHandler();
public void handle(Param bean) {
//do nothing.
}
public BizCode supportBiz() {
return BizCode.CODE_DEFAULT;
}
}The consumer can now use the factory directly.
1
2
3
4
5
6
7
8
9
10
11
12
public class Subscriber implements MessageListenerConcurrently {
public ConsumeConcurrentlyStatus consumeMessage(final List<MessageExt> msgs,
final ConsumeConcurrentlyContext context) {
// ...
Param param = JSON.parseObject(messageBody, Param.class);
HandlerFactory.getHandler(param.getBizCode()).handle(param);
// ...
}
}
Chain of Responsibility
When one consumer listener handles several bizCode values, we can combine the chain-of-responsibility and template-method patterns to build a general consumer for multiple business scenarios.
Compared with the strategy pattern, this lets users define the degree to which each handler supports different businesses. A handler can support multiple businesses or none, return early, and so forth.
First, define the chain executor. It supports Spring’s
@Orderannotation.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ChainExecutor {
List<ChainHandler> handlerList;
public void init() {
handlerList.sort(AnnotationAwareOrderComparator.INSTANCE);
}
public void process(Chain param) {
// 按照@Order顺序排序
if (CollectionUtils.isEmpty(handlerList)) {
handlerList.forEach(e -> {
if (e.supports(param)) {
e.process(param);
}
});
}
}
}I want each handler to decide whether it terminates the chain, so the parameter includes an
overflag.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class ChainParam {
/**
* 责任链是否终结
*/
boolean over = false;
/**
* Meta传递来的业务bean
*/
Object bean;
/**
* 业务码
*/
BizCode bizCode;
}Next, define the node-handler interface. Common processing is abstracted here using the template-method pattern.
1
2
3
4
5
6
7
8
9
10public interface ChainHandler {
void process(ChainParam param);
boolean supports(ChainParam param);
default boolean supports(BizCode code, ChainParam param) {
return code.equals(param.getBizCode) && !param.isOver();
}
}Finally, define a handler for a particular business.
1
2
3
4
5
6
7
8
9
10
11
12
13
public class SampleHandler implements ChainHandler {
public void process(ChainParam param) {
// do some thing
}
public boolean supports(ChainParam param) {
return supports(BizCode.CODE_A, param);
}
}To use the chain, the consumer only needs to inject
ChainExecutor.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Subscriber implements MessageListenerConcurrently {
private ChainExecutor executor;
public ConsumeConcurrentlyStatus consumeMessage(final List<MessageExt> msgs,
final ConsumeConcurrentlyContext context) {
// ...
ChainParam param = JSON.parseObject(messageBody, ChainParam.class);
executor.process(param);
// ...
}
}
Template Method Pattern
The first two approaches address one consumer processing several businesses. When several consumers process different topics, polymorphism can likewise extract the common parts of those topic consumers. This is where the template-method pattern applies:
1 | public abstract class AbstractSubscriber<Bean> implements MessageListenerConcurrently { |
For each topic, simply add a consumer subclass that implements this abstract class.
Follow-up Thoughts
- The chain-of-responsibility and strategy patterns can also be combined, giving each chain node several corresponding strategy handlers.
- This design makes use of Spring’s container-management capabilities.