The story of Luo Meiqi and Chun Porter

Time:2021-12-2

The story of Luo Meiqi and Chun Porter

Author Liao Tian
Source|Alibaba cloud official account

Reading Guide:After more than six months of incubation, rocketmq spring officially graduated as a subproject of Apache rocketmq and released its first release version 2.0.1. This project encapsulates the rocketmq client in the way of spring boot, which allows users to send and consume messages through simple annotation and standard spring messaging API.

At the project release stage, we were honored to invite the original staff of the spring community to review our code. Through several rounds of in-depth communication on slack, we felt the spring team’s standards for the quality of open source code and the requirements for the details of the springboot project. This article is a summary of the experience and skills in the process of review and code improvement. I hope students engaged in spring boot development can help. We organize this process into the stories of Luo Meiqi, a contributor of rocketmq community, and spring boot of spring community.

The beginning of the story

The beginning of the story is like this. Luo Meiqi Meimei has a set of rocketmq client code, which is responsible for sending messages and consuming messages. I heard the name of chunporter’s little brother early. Through spring boot, you can make your client call very simple. You can start it by using an independent application only with some simple annotations and code, eliminating complex code writing and parameter configuration.

Smart, she implemented a rocketmq spring client by referring to the spring of message components already implemented in the industry:

  • A message sending client is required. It is an automatically created spring bean, and the relevant properties can be automatically set according to the configuration of the configuration file. It is named rocketmqtemplate. At the same time, it encapsulates various synchronous and asynchronous methods for sending messages.
@Resourceprivate RocketMQTemplate rocketMQTemplate;
...
SendResult sendResult = rocketMQTemplate.syncSend(xxxTopic, "Hello, World!");
  • A message receiving client is required. It is a listener that can be applied for callback to callback the consumption message to the user for related processing.
@[email protected](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);
   }
}

Specifically: the consumer client listener needs to be marked with a custom annotation @ rocketmqmessagelistener. This annotation has two functions:

  • Define the configuration parameters of message consumption (such as topic of consumption, sequential consumption, consumption group, etc.).
  • You can enable spring boot to discover and initialize all listeners marked with this annotation during startup. See the listenercontainerconfiguration class and its interface method aftersingletonsinstantiated() that implements smartinitializingsingleton for details.

Through research, it is found that the core implementation of spring boot is automatic configuration, which needs to be divided into three parts:

  • Autoconfiguration class, marked by @ configuration, is used to create springbeans required by rocketmq clients, such as rocketmqtemplate mentioned above and containers that can handle consumption callback listeners. Each listener corresponds to a container springbean to start mqpushconsumer, and push the monitored consumption messages to the listener for callback in the future. Refer to rocketmqautoconfiguration.java   (editor’s note: This is the final released class. There is no trace of review).
  • The configuration class defined above is not “automatically” configured and needs to be declared by meta-inf / spring.factories. You can refer to spring.factories. The advantage of using this meta configuration is that the upper level user does not need to care about the details and switches of the automatic configuration class. As long as there is this meta-inf file and configuration class in the classpath, it can be automatically configured.
  • In addition, the configuration class defined above also defines the @ enableconfiguraitonproperties annotation to introduce the configurationproperties class. Its function is to define automatically configured properties. Refer to rocketmqproperties.java. Upper level users can configure relevant property files according to the properties defined in this class (i.e. meta-inf / application.properties or meta-inf / application. Yaml).

Story development

Luo Meiqi and Meimei developed and completed the rocketmq springboot package according to this idea, formed a starter and gave it to the community partners for trial use. Nice~ everyone gave good feedback after use. But I still want to ask my professional brother chunporter for his opinions.

Chun Porter’s little brother reviewed Luo Meiqi’s code quite responsibly. First, he threw out two links:

Then he explained:

“Spring boot contains two concepts – auto configuration and starter POMS, which are interrelated but not simply bound together:

  • Auto configuration is responsible for responding to the current state of the application and configuring the appropriate spring beans. It is placed in the user’s classpath, and related functions can be provided in combination with other dependencies in the classpath.
  • Starter POM is responsible for organizing auto configuration and some additional dependencies to provide out of the box functions. It is usually a maven project, which is only a POM file and does not need to contain any additional classes or resources.

In other words, starter POM is responsible for configuring the full amount of classpath, while auto configuration is responsible for the specific response (Implementation); The former is total solution, and the latter can be used on demand.

Your current system is a single module, mixing auto configuration and starter POM together, which is not conducive to future expansion and separate use of modules. “

Luo Meiqi learned that differentiation is really important for future project maintenance, so she modularized the code:

|— rocketmq-spring-boot-parent   Parent POM
|— rocketmq-spring-boot               Auto configuraiton module
|— rocketmq-spring-stater            Starter module (actually contains only one POM. XML file)
|— rocketmq-spring-samples          Sample calling starter

“Well, this module structure is much clearer,” nodded chunporter. “But the usage of some tags in this autoconfiguration file is incorrect. Please note it for you. In addition, considering that spring boot 1. X will no longer be supported officially by August 2020, it is recommended to directly support spring boot 2. X.”

@Configuration
@EnableConfigurationProperties(RocketMQProperties.class)
@ConditionalOnClass(MQClientAPIImpl.class)
@Order ~ ~ Chun Porter: it's unreasonable to use order in this class. It's not recommended. You can control the construction order of runtime beans in other ways
@Slf4j
public class RocketMQAutoConfiguration {
   @Bean
   @Conditionalonclass (defaultmqproducer. Class) ~ ~ chunbaud: it is unscientific to directly use the class for attributes. You need to use (name = "full name of class") so that cnfe will not be thrown when the class is not in the classpath
   @ConditionalOnMissingBean(DefaultMQProducer.class)
   @Conditionalonproperty (prefix = "spring. Rocketmq", value = {"nameserver", "producer. Group"}) ~ ~ springport: nameserver property name should be written as name server [1]
   @Order (1) ~ ~ Chun Porter: delete public defaultmqproducer mqproducer (rocketmqproperties rocketmqproperties){
       ...
   }
   @Bean
   @ConditionalOnClass(ObjectMapper.class)
   @Conditionalonmissingbean (name = "rocketmvmessageobjectmapper") ~ ~ Chun Porter: binding to a specific instance name is not recommended. The design intention is to use an existing objectmapper in the system. If not, instantiate one here and change it to
    @ConditionalOnMissingBean(ObjectMapper.class)
   public ObjectMapper rocketMQMessageObjectMapper() {
       return new ObjectMapper();
   }
   @Bean(destroyMethod = "destroy")
   @ConditionalOnBean(DefaultMQProducer.class)
   @Conditionalonmissingbean (name = "rocketmqtemplate") ~ ~ springport: same as above
   @Order (2) ~ ~ Chun Porter: delete it 
   public RocketMQTemplate rocketMQTemplate(DefaultMQProducer mqProducer,
       @Autowired (required = false) ~ ~ chunporter: delete
       @Qualifier ("rocketmvmessageobjectmapper") ~ ~ Chun Porter: delete it and do not bind it to a specific instance              
          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_infrastruture) ~ ~ Chun Porter: This bean (rocketmqtransactionannotationprocessor) is recommended to be declared as static, because this rocketmqtransactionannotationprocessor implements the beanpostprocessor interface, and the methods in the interface can directly use this static instance when calling (when creating a bean related to a transaction), Instead of waiting until other beans of this configuration class are built [2]
   public RocketMQTransactionAnnotationProcessor transactionAnnotationProcessor(     
   TransactionHandlerRegistry transactionHandlerRegistry) {
     return new RocketMQTransactionAnnotationProcessor(transactionHandlerRegistry);
  }
   @Configuration ~ ~ springport: this embedded configuration class is complex. It is recommended to form a top-level class independently and use it
   @Import is introduced in the main configuration class 
   @ConditionalOnClass(DefaultMQPushConsumer.class)
   @EnableConfigurationProperties(RocketMQProperties.class)
   @Conditionalonproperty (prefix = "spring. Rocketmq", value = "nameserver") ~ ~ springport: name server
   public static class ListenerContainerConfiguration implements ApplicationContextAware, InitializingBean {
      ...
      @Resource ~ ~ chunbaud: delete this annotation. The field injection method is not recommended. It is recommended to initialize member variables by using setters or constructing parameters
      private StandardEnvironment environment;
       @Autowired (required = false) ~ ~ chunporter: this annotation is unnecessary
       public ListenerContainerConfiguration(
           @Qualifier ("rocketmqmessageobjectmapper") objectmapper (objectmapper) {~ ~ Chun Porter: @ qualifier doesn't need
           this.objectMapper = objectMapper;
       }

Note [1]: when declaring attributes, do not use hump naming method, but use – horizontal line separation, so as to support the relaxed rules of attribute names.

Note [2]: the function of the beanpostprocessor interface is: if you need to add some own logical processing before and after the spring container completes bean instantiation, configuration and other initialization, you can define the implementation of one or more beanpostprocessor interfaces and then register them in the container. Why is it recommended to declare it static

If they don’t we basically register the post-processor at the same “time” as all the other beans in that class and the contract of BPP is that it must be registered very early on. This may not make a difference for this particular class but flagging  it as static as the side effect to make clear your BPP implementation is not supposed to drag other beans via dependency injection.

Autoconfiguration is really learned. Luo Meiqi quickly adjusted the code and looked much cleaner. However, chunporter made two suggestions:

@Configuration
public class ListenerContainerConfiguration implements ApplicationContextAware, SmartInitializingSingleton {
    private ObjectMapper objectMapper = new ObjectMapper(); ~~ Chun Porter: in terms of performance, do not initialize this member variable. Since this member is set in the constructor / setter method, do not initialize here, especially when its construction cost is very high.
   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);   ~~ Chun Porter: the following method of manually registering beans is provided in spring 4. X. you can consider using the method of genericapplicationcontext.registerbean provided in spring 5.0 to call new through the supplier to construct bean instances [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);   ~~ Chun Porter: your startup method here is called through afterpropertieset(), which is not recommended. You should implement smartlifecycle to define startup and shutdown methods, so that they can be started automatically when ApplicationContext is refreshed; It also avoids the risk of stuck caused by low-level resource problems during context initialization
   if (!container.isStarted()) {
       try {
           container.start();
       } catch (Exception e) {
         log.error("started container failed. {}", container, e);           throw new RuntimeException(e);
       }
   }
   ...
 }
}

Note [3]: use genericapplicationcontext.registerbean.

public final < T > void registerBean(
 Class< T > beanClass, Supplier< T > supplier, BeanDefinitionCustomizer… ustomizers)

“And, and”, after Luo Meiqi adopted chunporter’s opinions and adjusted the code greatly, chunporter’s brother put forward several unique requirements for spring boot:

  • Use spring’s assert. In the traditional java code, we use assert to make assertions. Assertions in spring boot need to use its own assert class, as shown in the following example:
import org.springframework.util.Assert;
...
Assert.hasText(nameServer, "[rocketmq.name-server] must not be null");
  • The auto configuration unit test uses the applicationcontextrunner provided by spring 2.0:
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);               });
   }
  • Add spring boot configuration processor annotation processor in pom.xml file of auto configuration module, so that it can generate auxiliary metadata file and speed up startup time.

See here for details:
https://docs.spring.io/spring…

Finally, Chun Porter professionally provided Luo Meiqi and Meimei with the following two opinions:

1. General specifications, good code should be easy to read and easy to maintain

1) Annotation and naming conventions

Our common code comments are divided into multiple lines (//) and single line (/ /…). For member variables, methods or code logic that need to be described, multiple lines of comments should be provided; Some simple code logic comments can also use single line comments. The general requirements for annotation areBegin with a capital letter and end with a period; For single line notes, it is also requiredStart with capital letters *; And single line comments at the end of the line are not recommended.

When naming variables and methods, try to use words accurately, and try not to use abbreviations, such as sendmsgtimeout. It is recommended to write sendmessagetimeout; The package name supports. It is recommended to change it to support.

2) Need to use   Lombok

The advantage of using Lombok is that the code is more concise. You only need to use some comments to omit many methods such as constructor, setter and getter; However, there is also a disadvantage that developers need to configure Lombok plug-in in their ide environment to support this function. Therefore, the recommended method of spring community is not to use Lombok, so that new users can directly view and maintain the code without relying on ide settings.

3) Control of package name

If there is no class in a package directory, it is recommended to remove the package directory. For example: org.apache.rocketmq.spring.starter has no specific class definition under the spring directory, so this layer of directory should be removed (editor’s note: we finally changed the package to org.apache.rocketmq.spring, and moved the directory and classes under the starter one layer up). We put all enum classes under the package org.apache.rocketmq.spring.enums. The package name is not standardized. We need to adjust the enum class to a specific package and remove the enums package; Class hiding. For some classes, it is only used by other classes in the package without revealing the specific use details to the end user. It is recommended to use the package private constraint, such as the transactionhandler class.

4) Static import is not recommended, although it has the advantage of less code and the disadvantage of damaging the readability and maintainability of the program.

2. Efficiency, in-depth code details

  • Static + final method: the static method of a class should not be combined with final unless the class itself is final and declares a private constructor. If the two are combined, it is assumed that this subclass can no longer define the method, which will cause trouble for future extensions and subclass calls.
  • For beans declared in the configuration file, try to use the constructor or setter method to set member variables instead of @ autowarned, @ resource and other methods.
  • Do not initialize useless member variables.
  • If a method is not called anywhere, it should be deleted; If an interface method is not required, do not implement the interface class.

Note [4]: the following screenshot is a code example with fieldinjection converted to constructor settings.

The story of Luo Meiqi and Chun Porter

Convert to:

The story of Luo Meiqi and Chun Porter

The end of the story

Luo Meiqi adjusted the code according to the above requirements, greatly improved the code quality, and summarized the key points of spring boot development:

  • Before writing, refer to the mature spring boot implementation code.
  • Pay attention to the division of modules and distinguish between autoconfiguration and starter.
  • When writing autoconfiguration bean, pay attention to the use of @ conditional annotation; Try to use constructor or setter methods to set variables and avoid field injection; Multiple configuration beans can be associated with @ import; Use the autoconfiguration test class provided by spring 2.0.
  • Pay attention to some details: static and beanpostprocessor; Use of lifecycle; Unnecessary initialization of member properties, etc.

Through this review, we learned some constraints required by spring boot and auto configuration, submitted the final code with confidence, and invited partners in rocketmq community to use rocketmq spring function together. Readers can check the final repaired code in the reference code library, Also hope to have more valuable feedback and strengthen, come on!

Postscript

Open source software is not only an easy-to-use product, but also the code quality and style will affect the majority of developers. Luo Meiqi, an active community contributor, is also working with partners in rocketmq community to continuously improve the spring code, and invited spring community in springport to give publicity and introduction. Next, promote rocketmq spring starter to spring initializr, Users can directly use rocketmq spring on the start.spring.io website like other starters (such as Tomcat starter).

Nail search Group No.: 21982288, you can join the official nail group of Apache rocketmq Chinese developers!

The story of Luo Meiqi and Chun Porter

Log in to start.aliyun.com on the PC side to know the mobile hand lab and immersively experience the online interactive tutorial