How to create a springboot state of your own

what is a stater

Stater is a special spring boot project. It implements some common functions, so that you can rely on it and use it directly. It also makes some default conventions in configuration, so you don't need to make complicated configurations.

How the state is injected

Students who have basic knowledge of spring boot know that when the boot project is started, the default is to scan the classes under the package and its descendants to see if it is annotated and whether it is to be included in the spring container for management. Then the package name of the stater must be the same as Your business package name is inconsistent, so how does he realize the above configuration, that is, what is the principle that the spring boot agreement is greater than the configuration?
The secret lies in the spring.factories file. When the spring container starts, it will read the content of the META-INF/spring.factories file, instantiate the classes in it and put it into the container for management. Below is an example

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.soft863.stream.core.eventbus.EventBus,\
  com.soft863.stream.core.eventbus.subscribe.SpringMessageSubscribe,\
  com.soft863.stream.core.eventbus.config.RedisConfig,\
  com.soft863.stream.core.eventbus.subscribe.redis.RedisMessageBroker,\
  com.soft863.stream.core.eventbus.publish.redis.RedisMessagePublisher

As written in the example, even if these classes in the stater are not under the application of the startup project, they can also be scanned for instantiation management.

How to make an agreement

Different branch processing often occurs in some common functions. For example, the database may have differences such as mysql and oracle. For example, if I want to implement an MQ message communication, there may also be unreasonable implementations such as rabbitmq rocketmq. The user's configuration is instantiated differently, or if I use a default function to achieve it, then I need to use a very important annotation @ConditionalOnProperty

To give an example, I want to make a message bus, and I have several options for communication components such as redis stream, rabbitmq rocketmq. Based on lightweight considerations, I want it to be implemented as redis by default, so I can write like this

@ConditionalOnProperty(prefix = "stream.broker", name = "type", havingValue = "redis", matchIfMissing = true)

The meaning of this piece of code is as follows

  • prefix refers to the configuration prefix in the yaml file
  • name refers to the name of the configuration
  • havingValue refers to whether the configuration value of name contains this value
  • matchIfMissing If there is no name configuration, whether this annotation takes effect

To sum up, it is to check whether there is a stream.broker.type parameter in the configuration file. If so, check whether it is redis. If it is redis, instantiate the bean. If not, also instantiate it (of course, this class must be annotated by
@Component ), it is not instantiated only when this parameter exists and is not redis. In this way, the default redis function can be realized.
There are the following annotations with the same function, you can roughly understand the meaning according to the name
insert image description here

demo based on the above theory

Realize function

By default, the event processing message bus of publish-subscribe mode is based on redis stream. During use, you only need to add the @Subscribe annotation to the corresponding method to subscribe to the message, and you don’t need to write other processing code yourself

code directory

insert image description here

core implementation

spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.soft863.stream.core.eventbus.EventBus,\
  com.soft863.stream.core.eventbus.subscribe.SpringMessageSubscribe,\
  com.soft863.stream.core.eventbus.config.RedisConfig,\
  com.soft863.stream.core.eventbus.subscribe.redis.RedisMessageBroker,\
  com.soft863.stream.core.eventbus.publish.redis.RedisMessagePublisher

SpringMessageSubscribe (scan all @Subscribe annotations to generate message subscriptions)

package com.soft863.stream.core.eventbus.subscribe;

import com.soft863.stream.core.eventbus.EventBus;
import com.soft863.stream.core.annotation.Subscribe;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;

@Component
@Slf4j
public class SpringMessageSubscribe implements BeanPostProcessor {
    
    

    private final EventBus eventBus;

    private final MessageAdapterCrater messageAdapterCrater = new DefaultMessageAdapterCrater();

    public SpringMessageSubscribe(EventBus eventBus) {
    
    
        this.eventBus = eventBus;
    }

    /**
     * 为注解添加监听处理
     *
     * @param bean
     * @param beanName
     * @return
     * @throws BeansException
     */
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    
    
        Class<?> type = ClassUtils.getUserClass(bean);

        ReflectionUtils.doWithMethods(type, method -> {
    
    
            // 取得所有订阅方法
            AnnotationAttributes subscribes = AnnotatedElementUtils.getMergedAnnotationAttributes(method, Subscribe.class);
            if (CollectionUtils.isEmpty(subscribes)) {
    
    
                return;
            }
            // 生成订阅Adapter
            MessageAdapter sub = messageAdapterCrater.createMessageAdapter(type, method, subscribes.getString("topic"));
            // 注册订阅Adapter
            eventBus.addAdapter(sub, subscribes.getString("topic"));
        });
        return bean;
    }
}

Redis-based message subscription

package com.soft863.stream.core.eventbus.subscribe.redis;

import com.soft863.stream.core.eventbus.EventBus;
import com.soft863.stream.core.eventbus.subscribe.EventMessageBroker;
import com.soft863.stream.core.eventbus.message.AdapterMessage;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StreamOperations;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.data.redis.stream.StreamListener;
import org.springframework.data.redis.stream.StreamMessageListenerContainer;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.time.Duration;
import java.util.Collections;

/**
 * 消息总线-集群间消息redis实现
 *
 * 实现规则
 *     根据不同的MQ机制实现消息的分组消费,将原始消息转化为AdapterMessage,然后使用继承于EventMessageBroker进行消息多播
 */
@Slf4j
@Component
@ConditionalOnProperty(prefix = "stream.broker", name = "type", havingValue = "redis", matchIfMissing = true)
public class RedisMessageBroker  extends EventMessageBroker implements StreamListener<String, MapRecord<String, String, String>> {
    
    

    @Value("${stream.topic:/stream}")
    String topic;

    @Value("${stream.consumer.id}")
    String consumerId;

    @Value("${stream.timeout:10}")
    Integer timeout;

    @Value("${stream.consumer.group}")
    String groupName = "stream.redis";

    private final RedisTemplate<String, String> streamRedisTemplate;
    private final ApplicationContext applicationContext;
    private final EventBus eventBus;

    public RedisMessageBroker(RedisTemplate<String, String> streamRedisTemplate, ApplicationContext applicationContext, EventBus eventBus) {
    
    
        this.streamRedisTemplate = streamRedisTemplate;
        this.applicationContext = applicationContext;
        this.eventBus = eventBus;
    }


    @SneakyThrows
    @Override
    public void onMessage(MapRecord<String, String, String> message) {
    
    
        log.info("消息内容-->{}", message.getValue());
        StreamOperations<String, String, String> streamOperations = streamRedisTemplate.opsForStream();

        // 服务内消息多播
        AdapterMessage adapterMessage = new AdapterMessage();
        adapterMessage.setTopic(message.getValue().get("topic"));
        adapterMessage.setPayload(message.getValue().get("payload"));
        try {
    
    
            this.multicastEvent(adapterMessage);
        } catch (Exception e) {
    
    
            log.info("消息多播失败:" + e.getLocalizedMessage());
        }
        //消息应答
        streamOperations.acknowledge(topic, groupName, message.getId());
    }


    @Bean
    public StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> emailListenerContainerOptions() {
    
    

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        return StreamMessageListenerContainer.StreamMessageListenerContainerOptions
                .builder()
                //block读取超时时间
                .pollTimeout(Duration.ofSeconds(timeout))
                //count 数量(一次只获取一条消息)
                .batchSize(1)
                //序列化规则
                .serializer(stringRedisSerializer)
                .build();
    }

    /**
     * 开启监听器接收消息
     */
    @Bean
    public StreamMessageListenerContainer<String, MapRecord<String, String, String>> emailListenerContainer(RedisConnectionFactory factory,
                                                                                                            StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> streamMessageListenerContainerOptions) {
    
    
        StreamMessageListenerContainer<String, MapRecord<String, String, String>> listenerContainer = StreamMessageListenerContainer.create(factory,
                streamMessageListenerContainerOptions);

        //如果 流不存在 创建 stream 流
        if (!streamRedisTemplate.hasKey(topic)) {
    
    
            streamRedisTemplate.opsForStream().add(topic, Collections.singletonMap("", ""));
            log.info("初始化集群间通信Topic{} success", topic);
        }

        //创建消费者组
        try {
    
    
            streamRedisTemplate.opsForStream().createGroup(topic, groupName);
        } catch (Exception e) {
    
    
            log.info("消费者组 {} 已存在", groupName);
        }

        //注册消费者 消费者名称,从哪条消息开始消费,消费者类
        // > 表示没消费过的消息
        // $ 表示最新的消息
        listenerContainer.receive(
                Consumer.from(groupName, consumerId),
                StreamOffset.create(topic, ReadOffset.lastConsumed()),
                this
        );

        listenerContainer.start();
        return listenerContainer;
    }

    @Override
    public void multicastEvent(AdapterMessage message) throws IllegalAccessException, InstantiationException, InvocationTargetException, IOException {
    
    
        super.doMulticastEvent(message, eventBus.getSubscribesPool(), applicationContext);
    }
}

Redis-based message publishing

package com.soft863.stream.core.eventbus.publish.redis;

import com.soft863.stream.core.eventbus.message.AdapterMessage;
import com.soft863.stream.core.eventbus.publish.MessagePublisher;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.stream.*;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

/**
 * 消息总线发布Redis实现
 */
@Component
@ConditionalOnProperty(prefix = "stream.broker", name = "type", havingValue = "redis", matchIfMissing = true)
public class RedisMessagePublisher implements MessagePublisher {
    
    

    @Value("${stream.topic:/stream}")
    String topic;

    private final RedisConnectionFactory connectionFactory;

    public RedisMessagePublisher(RedisConnectionFactory connectionFactory) {
    
    
        this.connectionFactory = connectionFactory;
    }

    @Override
    public Boolean publish(AdapterMessage message) {
    
    
        Map value = new HashMap();
        value.put("topic".getBytes(), message.getTopic().getBytes());
        value.put("payload".getBytes(), message.getPayload().getBytes());
        ByteRecord byteRecord = StreamRecords.rawBytes(value).withStreamKey(topic.getBytes());
        // 刚追加记录的记录ID
        RecordId recordId = connectionFactory.getConnection().xAdd(byteRecord);
        return true;
    }
}

Multicast messages for multiple subscribers of the same topic

package com.soft863.stream.core.eventbus.subscribe;

import com.alibaba.fastjson.JSON;
import com.soft863.stream.core.eventbus.message.AdapterMessage;
import com.soft863.stream.core.util.TopicMatcher;
import com.soft863.stream.core.util.TopicUtil;
import org.springframework.context.ApplicationContext;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Map;

public abstract class EventMessageBroker {
    
    

    /**
     * 消息多播
     *
     * 将集群间消息在自己服务内部广播,是的所有订阅消息都可以收到
     *
     * @param message
     * @return
     */
    public abstract void multicastEvent(AdapterMessage message) throws IllegalAccessException, InstantiationException, InvocationTargetException, IOException;

    public void doMulticastEvent(AdapterMessage message, Map<TopicMatcher, Map<String, MessageAdapter>> subscribesPool, ApplicationContext applicationContext) throws InvocationTargetException, IllegalAccessException, InstantiationException, IOException {
    
    
        // 查找相匹配的Adapter
        List<MessageAdapter> adapterList = TopicUtil.getAllMatchedAdapter(message.getTopic(), subscribesPool);

        for (MessageAdapter adapter : adapterList) {
    
    
            if (adapter != null && adapter.isActive()) {
    
    
                // 手动订阅消息
                if (adapter.getCustomer() != null) {
    
    
                    adapter.getCustomer().consume(message);
                } else {
    
    
                    // 取得对象
                    Object instance = applicationContext.getBean(adapter.getClazz());
                    if (instance != null) {
    
    
                        // 将消息转化为所需要的类型
                        if (adapter.getMethod().getParameterCount() > 0) {
    
    
                            Class<?> param = adapter.getMethod().getParameterTypes()[0];
                            adapter.getMethod().invoke(instance, JSON.parseObject(message.getPayload(), param));
                        } else {
    
    
                            adapter.getMethod().invoke(instance);
                        }

                    }
                }
            }

        }

    }
}

Guess you like

Origin blog.csdn.net/baidu_29609961/article/details/130805799