故事的开始
-
需要一个消息的发送客户端,它是一个自动创建的 Spring Bean,并且相关属性要能够根据配置文件的配置自动设置, 命名它为:RocketMQTemplate, 同时让它封装发送消息的各种同步和异步的方法。
@Resourceprivate RocketMQTemplate rocketMQTemplate;
...
SendResult sendResult = rocketMQTemplate.syncSend(xxxTopic, "Hello, World!");
-
需要消息的接收客户端,它是一个能够被应用回调的 Listener, 来将消费消息回调给用户进行相关的处理。
@Service@RocketMQMessageListener(topic = "xxx", consumerGroup = "xxx_consumer")
public class StringConsumer implements RocketMQListener<String> {
@Override public void onMessage(String message) {
System.out.printf("------- StringConsumer received: %s \n", message);
}
}
-
定义消息消费的配置参数(如: 消费的 topic, 是否顺序消费,消费组等)。
-
可以让 spring-boot 在启动过程中发现标注了这个注解的所有 Listener, 并进行初始化,详见 ListenerContainerConfiguration 类及其实现 SmartInitializingSingleton 的接口方法 afterSingletonsInstantiated()。
-
AutoConfiguration 类,它由 @Configuration 标注,用来创建 RocketMQ 客户端所需要的 SpringBean,如上面所提到的 RocketMQTemplate 和能够处理消费回调 Listener 的容器,每个 Listener 对应一个容器 SpringBean 来启动 MQPushConsumer,并将来将监听到的消费消息并推送给 Listener 进行回调。可参考 RocketMQAutoConfiguration.java (编者注: 这个是最终发布的类,没有 review 的痕迹啦)。
-
上面定义的 Configuration 类,它本身并不会“自动”配置,需要由 META-INF/spring.factories 来声明,可参考 spring.factories 使用这个 META 配置的好处是上层用户不需要关心自动配置类的细节和开关,只要 classpath 中有这个 META-INF 文件和 Configuration 类,即可自动配置。
-
另外,上面定义的 Configuration 类,还定义了 @EnableConfiguraitonProperties 注解来引入 ConfigurationProperties 类,它的作用是定义自动配置的属性,可参考 RocketMQProperties.java,上层用户可以根据这个类里定义的属性来配置相关的属性文件(即 META-INF/application.properties 或 META-INF/application.yaml)。
故事的发展
-
https://github.com/spring-projects/spring-boot/wiki/Building-On-Spring-Boot
-
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-auto-configuration.html
-
auto-configuration 负责响应应用程序的当前状态并配置适当的 Spring Bean。它放在用户的 CLASSPATH 中结合在 CLASSPATH 中的其它依赖就可以提供相关的功能。
-
Starter-POM 负责把 auto-configuration 和一些附加的依赖组织在一起,提供开箱即用的功能,它通常是一个 maven project,里面只是一个 POM 文件,不需要包含任何附加的 classes 或 resources。
@Configuration
@EnableConfigurationProperties(RocketMQProperties.class)
@ConditionalOnClass(MQClientAPIImpl.class)
@Order ~~春波特: 这个类里使用Order很不合理呵,不建议使用,完全可以通过其他方式控制runtime是Bean的构建顺序
@Slf4j
public class RocketMQAutoConfiguration {
@Bean
@ConditionalOnClass(DefaultMQProducer.class) ~~春波特: 属性直接使用类是不科学的,需要用(name="类全名") 方式,这样在类不在classpath时,不会抛出CNFE
@ConditionalOnMissingBean(DefaultMQProducer.class)
@ConditionalOnProperty(prefix = "spring.rocketmq", value = {"nameServer", "producer.group"}) ~~春波特: nameServer属性名要写成name-server [1]
@Order(1) ~~春波特: 删掉呵 public DefaultMQProducer mqProducer(RocketMQProperties rocketMQProperties) {
...
}
@Bean
@ConditionalOnClass(ObjectMapper.class)
@ConditionalOnMissingBean(name = "rocketMQMessageObjectMapper") ~~春波特: 不建议与具体的实例名绑定,设计的意图是使用系统中已经存在的ObjectMapper, 如果没有,则在这里实例化一个,需要改成
@ConditionalOnMissingBean(ObjectMapper.class)
public ObjectMapper rocketMQMessageObjectMapper() {
return new ObjectMapper();
}
@Bean(destroyMethod = "destroy")
@ConditionalOnBean(DefaultMQProducer.class)
@ConditionalOnMissingBean(name = "rocketMQTemplate") ~~春波特: 与上面一样
@Order(2) ~~春波特: 删掉呵
public RocketMQTemplate rocketMQTemplate(DefaultMQProducer mqProducer,
@Autowired(required = false) ~~春波特: 删掉
@Qualifier("rocketMQMessageObjectMapper") ~~春波特: 删掉,不要与具体实例绑定
ObjectMapper objectMapper) {
RocketMQTemplate rocketMQTemplate = new RocketMQTemplate();
rocketMQTemplate.setProducer(mqProducer);
if (Objects.nonNull(objectMapper)) {
rocketMQTemplate.setObjectMapper(objectMapper);
}
return rocketMQTemplate;
}
@Bean(name = RocketMQConfigUtils.ROCKETMQ_TRANSACTION_ANNOTATION_PROCESSOR_BEAN_NAME)
@ConditionalOnBean(TransactionHandlerRegistry.class)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) ~~春波特: 这个bean(RocketMQTransactionAnnotationProcessor)建议声明成static的,因为这个RocketMQTransactionAnnotationProcessor实现了BeanPostProcessor接口,接口里方法在调用的时候(创建Transaction相关的Bean的时候)可以直接使用这个static实例,而不要等到这个Configuration类的其他的Bean都构建好 [2]
public RocketMQTransactionAnnotationProcessor transactionAnnotationProcessor(
TransactionHandlerRegistry transactionHandlerRegistry) {
return new RocketMQTransactionAnnotationProcessor(transactionHandlerRegistry);
}
@Configuration ~~春波特: 这个内嵌的Configuration类比较复杂,建议独立成一个顶级类,并且使用
@Import在主Configuration类中引入
@ConditionalOnClass(DefaultMQPushConsumer.class)
@EnableConfigurationProperties(RocketMQProperties.class)
@ConditionalOnProperty(prefix = "spring.rocketmq", value = "nameServer") ~~春波特: name-server
public static class ListenerContainerConfiguration implements ApplicationContextAware, InitializingBean {
...
@Resource ~~春波特: 删掉这个annotation, 这个field injection的方式不推荐,建议使用setter或者构造参数的方式初始化成员变量
private StandardEnvironment environment;
@Autowired(required = false) ~~春波特: 这个注解是不需要的
public ListenerContainerConfiguration(
@Qualifier("rocketMQMessageObjectMapper") ObjectMapper objectMapper) { ~~春波特: @Qualifier 不需要
this.objectMapper = objectMapper;
}
@Configuration
public class ListenerContainerConfiguration implements ApplicationContextAware, SmartInitializingSingleton {
private ObjectMapper objectMapper = new ObjectMapper(); ~~春波特: 性能上考虑,不要初始化这个成员变量,既然这个成员是在构造/setter方法里设置的,就不要在这里初始化,尤其是当它的构造成本很高的时候。
private void registerContainer(String beanName, Object bean) { Class<?> clazz = AopUtils.getTargetClass(bean);
if(!RocketMQListener.class.isAssignableFrom(bean.getClass())){
throw new IllegalStateException(clazz + " is not instance of " + RocketMQListener.class.getName());
}
RocketMQListener rocketMQListener = (RocketMQListener) bean; RocketMQMessageListener annotation = clazz.getAnnotation(RocketMQMessageListener.class);
validate(annotation); ~~春波特: 下面的这种手工注册Bean的方式是Spring 4.x里提供能,可以考虑使用Spring5.0 里提供的 GenericApplicationContext.registerBean的方法,通过supplier调用new来构造Bean实例 [3]
BeanDefinitionBuilder beanBuilder = BeanDefinitionBuilder.rootBeanDefinition(DefaultRocketMQListenerContainer.class);
beanBuilder.addPropertyValue(PROP_NAMESERVER, rocketMQProperties.getNameServer());
...
beanBuilder.setDestroyMethodName(METHOD_DESTROY);
String containerBeanName = String.format("%s_%s", DefaultRocketMQListenerContainer.class.getName(), counter.incrementAndGet());
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getBeanFactory();
beanFactory.registerBeanDefinition(containerBeanName, beanBuilder.getBeanDefinition());
DefaultRocketMQListenerContainer container = beanFactory.getBean(containerBeanName, DefaultRocketMQListenerContainer.class); ~~春波特: 你这里的启动方法是通过 afterPropertiesSet() 调用的,这个是不建议的,应该实现SmartLifecycle来定义启停方法,这样在ApplicationContext刷新时能够自动启动;并且避免了context初始化时由于底层资源问题导致的挂住(stuck)的危险
if (!container.isStarted()) {
try {
container.start();
} catch (Exception e) {
log.error("started container failed. {}", container, e); throw new RuntimeException(e);
}
}
...
}
}
-
使用 Spring 的 Assert 在传统的 Java 代码中我们使用 assert 进行断言,Spring Boot 中断言需要使用它自有的 Assert 类,如下示例:
import org.springframework.util.Assert;
...
Assert.hasText(nameServer, "[rocketmq.name-server] must not be null")
-
Auto Configuration 单元测试使用 Spring 2.0 提供的 ApplicationContextRunner:
public class RocketMQAutoConfigurationTest {
private ApplicationContextRunner runner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(RocketMQAutoConfiguration.class));
@Test(expected = NoSuchBeanDefinitionException.class) public void testRocketMQAutoConfigurationNotCreatedByDefault() {
runner.run(context -> context.getBean(RocketMQAutoConfiguration.class)); }
@Test
public void testDefaultMQProducerWithRelaxPropertyName() {
runner.withPropertyValues("rocketmq.name-server=127.0.0.1:9876", "rocketmq.producer.group=spring_rocketmq").
run((context) -> {
assertThat(context).hasSingleBean(DefaultMQProducer.class); assertThat(context).hasSingleBean(RocketMQProperties.class); });
}
-
在 auto-configuration 模块的 pom.xml 文件里,加入 spring-boot-configuration-processor 注解处理器,这样它能够生成辅助元数据文件,加快启动时间。
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-auto-configuration.html#boot-features-custom-starter-module-autoconfigure
1. 通用的规范,好的代码要易读易于维护
1)注释与命名规范
2)是否需要使用 Lombok
3)对于包名(package)的控制
4)不建议使用 Static Import, 虽然使用它的好处是更少的代码,坏处是破坏程序的可读性和易维护性。
2. 效率,深入代码的细节
-
static + final method:一个类的 static 方法不要结合 final,除非这个类本身是 final 并且声明 private 构造(ctor),如果两者结合以为这子类不能再(hiding)定义该方法,给将来的扩展和子类调用带来麻烦。
-
在配置文件声明的 Bean 尽量使用构造函数或者 Setter 方法设置成员变量,而不要使用@Autowared,@Resource等方式注入。
-
不要额外初始化无用的成员变量。
-
如果一个方法没有任何地方调用,就应该删除;如果一个接口方法不需要,就不要实现这个接口类。
故事的结局
-
编写前参考成熟的 spring boot 实现代码。
-
要注意模块的划分,区分 autoconfiguration 和 starter。
-
在编写 autoconfiguration Bean 的时候,注意 @Conditional 注解的使用;尽量使用构造器或者 setter 方法来设置变量,避免使用 Field Injection 方式;多个 Configuration Bean 可以使用 @Import 关联;使用 Spring 2.0 提供的 AutoConfigruation 测试类。
-
注意一些细节:static 与 BeanPostProcessor;Lifecycle 的使用;不必要的成员属性的初始化等。
后记
一个一线小兵的成事方法

