The author source | | liao days alibaba cloud native public number

** RocketMQ-Spring has been incubated for over six months, and has graduated as a subproject of Apache RocketMQ with its first Release 2.0.1. This project encapsulates the RocketMQ client using Spring Boot, allowing users to send and consume messages using simple annotations and standard Spring Messaging API code.

During the project release phase, 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 requirements for details of the SpringBoot project. This article is a summary of the experience and skills in the process of Review and code improvement, hoping to be helpful to students engaged in Spring Boot development. We parsed this process into the story of RocketMQ community contributor Maggie Luo and Spring community’s SpringBoot.

The beginning of the story

The story begins with Mei Qi Luo having a client code for RocketMQ, which sends and consumes messages. Early heard of the name of Spring Port little brother, through Spring Boot can make their own client call very simple, only use some simple annotations and code can use the independent application way to start, eliminating the complex code writing and parameter configuration.

She cleverly implemented a RocketMQ Spring client by referencing the industry’s already implemented Spring messaging component:

  • You need a message sending client that is an automatically created Spring Bean with properties that can be set automatically according to the configuration file, named RocketMQTemplate, and that encapsulates the various synchronous and asynchronous methods for sending messages.
@Resourceprivate RocketMQTemplate rocketMQTemplate; . SendResult sendResult = rocketMQTemplate.syncSend(xxxTopic, "Hello, World!" );Copy the code
  • A receiving client of the message is required, which is a Listener that can be applied with callbacks to call back the consuming message to the user for relevant processing.
@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); }}Copy the code

Note that the consumer client Listener needs to be annotated with a custom annotation @rocketmqMessagelistener. This annotation does two things:

  • Define the configuration parameters for message consumption (e.g., topic consumed, whether consumed sequentially, consuming group, etc.).
  • You can have spring-Boot discover all listeners marked with this annotation during startup and initialize them. See ListenerContainerConfiguration class and interface method of realizing SmartInitializingSingleton afterSingletonsInstantiated ().

According to the research, the most core implementation of Spring-Boot is auto Configuration, which is divided into three parts:

  • The AutoConfiguration class, annotated by @Configuration, is used to create the SpringBean required by the RocketMQ client, RocketMQTemplate, as mentioned above, and a container capable of handling the consumer callback Listener, each Listener corresponds to a container SpringBean that launches MQPushConsumer, In the future, it pushes the monitored consumption message to the Listener for callback. Refer to RocketMQAutoConfiguration. Java (editor’s note: this is the final release of class, there is no evidence of review).
  • The Configuration class defined above is not “automatically” configured and needs to be declared by meta-INF/spring.Factories. The advantage of spring.factories using this META Configuration is that the upper level user doesn’t need to worry about the details and switches of the auto-configuration class, as long as the meta-INF file and the Configuration class are present in the classpath.
  • In addition, the Configuration class definition above, also defines @ EnableConfiguraitonProperties annotations to introduce ConfigurationProperties class, its role is to define the automatic Configuration properties, Reference RocketMQProperties. Java, upper users can according to the definition in the class configuration is related to the properties of the properties file (namely the meta-inf/application. The properties or the meta-inf/application. The yaml).

Development of the story

According to this idea, Luo Meiqi developed RocketMQ SpringBoot package and gave it to the community as a starter for trial. Nice ~ feedback is good after everyone uses it. But still want to consult professional spring port little brother, see his opinion.

Chunport dutifully reviewed Luo’s code, first Posting two links:

  • Github.com/spring-proj…
  • Docs. Spring. IO/spring – the boot…

Then he explained:

“There are two concepts in Spring Boot – auto-Configuration and starter-poms, which are related to each other 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 combined with other dependencies in the CLASSPATH to provide related functionality.
  • The starter-POm is responsible for organizing auto-configuration with additional dependencies to provide out-of-the-box functionality. It is usually a Maven project that contains just a POM file. You don’t need to include any additional classes or resources.

In other words, the starter-POM is responsible for configuring the full CLASSPATH, while the Auto-Configuration is responsible for the specific response (implementation); The former is total-solution and the latter can be used on demand.

You now have a single module with auto-configuration and starter-POm mixed together, which is not conducive to future extensions and module isolation.”

Realizing that differentiation is really important for future project maintenance, Luo modularized the code:

| rocketmq – spring – the boot – parent father POM | rocketmq – spring – the boot auto – configuraiton module | rocketmq – spring – stater The starter module (actually contains only a pom. XML file) | rocketmq – spring – call the starter sample samples samples

“Good, this module structure is much clearer,” springport nodded. “But some of the labels in this AutoConfiguration file are not used correctly. Considering that Spring Boot 1.X will no longer be officially supported by Spring in August 2020, we recommend implementing direct support for Spring Boot 2.X.”

@Configuration @EnableConfigurationProperties(RocketMQProperties.class) @ConditionalOnClass(MQClientAPIImpl.class) @order ~~ Spring port: The use of Order in this class is not recommended. Can through other ways to control the runtime is Bean build order @ Slf4j public class RocketMQAutoConfiguration {@ Bean ConditionalOnClass(defaultmqproducer.class) ~~ Spring Potter: It is not scientific to use the class directly for attributes. Instead, use the (name=" class full name ") approach so that when the class is not in the classpath, CNFE @conditionalonmissingBean (defaultmqproducer.class) @conditionalonProperty (prefix = "spring. Rocketmq ", Value = {"nameServer", "producer.group"}) ~~ nameServer: nameServer [1] @order (1) ~~ Public DefaultMQProducer mqProducer(RocketMQProperties RocketMQProperties) {... } @ Bean @ ConditionalOnClass (ObjectMapper. Class) @ ConditionalOnMissingBean (name = "rocketMQMessageObjectMapper") ~ ~ spring potter: Binding to a specific instance name is not recommended. The design intention is to use ObjectMapper that already exists in the system. If not, instantiate one here. Need to change to @ 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) ~~ Delete the @ the Qualifier (" rocketMQMessageObjectMapper ") ~ ~ spring potter: RocketMQTemplate RocketMQTemplate = new 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) ~ ~ spring potter: The bean (RocketMQTransactionAnnotationProcessor) advice statement into the static, Because this RocketMQTransactionAnnotationProcessor implements the BeanPostProcessor interface, the interface method when invoked in () when creating Transaction related Bean can directly use the static instance, But don't wait for the other Bean Configuration class build good [2] public RocketMQTransactionAnnotationProcessor transactionAnnotationProcessor ( TransactionHandlerRegistry transactionHandlerRegistry) { return new RocketMQTransactionAnnotationProcessor(transactionHandlerRegistry); } @configuration ~~ Spring port: This embedded Configuration class is quite complex, and it is recommended to make a separate top-level class. In the main Configuration class and use the @ Import @ ConditionalOnClass (DefaultMQPushConsumer. Class) @EnableConfigurationProperties(RocketMQProperties.class) @ConditionalOnProperty(prefix = "spring.rocketmq", Value = "nameServer") ~~ name-server public static class ListenerContainerConfiguration implements ApplicationContextAware, InitializingBean { ... @resource ~~ Springport: Delete the annotation. The field injection method is not recommended. We recommend using setters or constructing parameters to initialize member variables. @autoWired (Required = false) ~~ Spring Port: This annotation is not to need to public ListenerContainerConfiguration (@ the Qualifier (" rocketMQMessageObjectMapper ") ObjectMapper ObjectMapper) { @qualifier does not need this. ObjectMapper = objectMapper; }Copy the code

Note [1] : Do not use hump nomenclature when declaring attributes, use – line separation to support relaxed rules for attribute names.

Note [2] : The BeanPostProcessor interface is used to: If you need to add some of your own logic before and after the Spring container completes Bean instantiation, configuration, and other initialization, you can define one or more implementations of the BeanPostProcessor interface and register them with the container. Static static static spring port

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 very knowledgeable, Luo Meiqi quickly adjusted the code, look a lot cleaner. Still, Springport offers two suggestions:

@Configuration public class ListenerContainerConfiguration implements ApplicationContextAware, SmartInitializingSingleton { private ObjectMapper objectMapper = new ObjectMapper(); ~~ Springport: For performance reasons, do not initialize this member variable. Since this member is set in the constructor /setter method, do not initialize it here, especially if it is expensive to construct. 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); ~~ Spring Potter The following method of manually registering beans is provided in Spring 4.x, Can provide GenericApplicationContext. Consider using Spring5.0 registerBean method, through: supplier call new to construct the Bean instance [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); ~~ Springport: Your startup method is called by afterPropertiesSet(), which is not recommended. You should implement SmartLifecycle to define start and stop methods so that they start automatically when ApplicationContext refreshes; It also avoids the danger of being stuck during context initialization due to underlying resource problems if (! container.isStarted()) { try { container.start(); } catch (Exception e) { log.error("started container failed. {}", container, e); throw new RuntimeException(e); }}... }}Copy the code

Note [3] : using GenericApplicationContext registerBean way.

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

“There is, there is”, after Luo Meiqi adopted Spring Port’s advice to adjust the code, Spring port brother also put forward a few unique requirements of Spring Boot:

  • In traditional Java code, we use Assert to Assert. In Spring Boot, Assert uses 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");Copy the code
  • The Auto Configuration unit test uses the ApplicationContextRunner provided with 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); }); }Copy the code
  • In the pum. XML file of the auto-Configuration module, add the spring-boot-configuration-processor annotation processor, so that it can generate auxiliary metadata files to speed up the startup time.

For details, see here: docs.spring. IO /spring-boot…

In the end, Spring Port also provided the following two aspects of advice to Luo meiqi:

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

1) Comments and naming conventions

Our usual code comments are divided into multiple lines (/**… */) and single line (//…) For member variables that need to be explained, methods or code logic should provide multi-line comments; Some simple code logic comments can also use single-line comments. The general rule for comments is to start with a capital letter and end with a period. For single-line comments, capital letters are also required; Single-line comments at the end of lines are not recommended.

When naming variables and methods, try to use accurate words and do not use abbreviations, such as sendMsgTimeout. SendMessageTimeout is recommended. The package name is supports. You are advised to change it to support.

2) Whether Lombok is needed

The benefit of Lombok is that your code is much cleaner, and you can omit constructor, setter, and getter methods with just a few comments. The downside, however, is that developers need to configure Lombok plug-ins in their IDE to support this functionality, so the Spring community recommends not using Lombok so that new users can view and maintain code directly, independent of IDE Settings.

3) Control of package names

If there is no class in a package directory, you are advised to remove the package directory. For example: org. Apache. Rocketmq. Spring. The starter in the spring directory no specific class definition, then you should get rid of this directory (editor’s note: We finally change the package to org. Apache. Rocketmq. Spring, will be the starter of the directory and classes up a layer). We put all Enum in package org. Apache. Rocketmq. Spring, enums, named after the package is not standard, need to adjust an Enum to a specific package, remove enums package; Class hiding. For classes that are only used by other classes in the package without exposing the details of use to the end user, the package Private constraint is recommended, such as the TransactionHandler class.

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

2. Efficiency, diving into the details of the code

  • Static + final method: A static method of a class should not incorporate final unless the class itself is final and declares a private constructor (cTOR), otherwise the subclass is no longer hiding to define the method, making future extensions and subclass calls problematic.
  • Use constructors or setters to set member variables for beans declared in configuration files rather than @Autowared, @resource, etc.
  • Do not initialize unnecessary member variables.
  • If a method is not called anywhere, it should be deleted; If an interface method doesn’t need it, don’t implement it.

Note [4] : The screenshot below is an example of code with FieldInjection converted into constructor Settings.

Converted to:

The end of the story

Luo meiqi adjusted the code according to the above requirements, which greatly improved the quality of the code, and summarized the key points of Spring Boot development:

  • Before writing reference mature Spring Boot implementation code.
  • Pay attention to module division, distinguish between autoConfiguration and starter.
  • Note the use of the @Conditional annotation when writing autoConfiguration beans; Use constructors or setter methods to set variables. Avoid Field Injection. Multiple Configuration beans can be associated using @import; Use the AutoConfigruation test class provided with Spring 2.0.
  • Note some details: static vs. BeanPostProcessor; Lifecycle; Initialization of unnecessary member attributes, etc.

I have learned some constraints required by Spring-boot and auto-configuration through this Review, and I am confident to submit the final code. The RocketMQ community is once again invited to use rocketMQ-Spring. You can check out the reference code base for the final fix, and hope for more feedback and enhancements.

Afterword.

Open source software not only provides a good product to use, but also affects the quality and style of the code. Active community contributor Meiqi Luo has been working with the RocketMQ community to improve the Spring code, and invited the Spring community in Spring Port to give presentations. The next step is to push Rocketmq-spring-starter into Spring Initializr so that users can use the rocketmq-Spring-starter directly on start.spring. IO like other starter (e.g. Tomcat starter uses RocketMq-Spring.

To join Apache RocketMQ China developer’s official Spike search group number: 21982288!

Log in to start.aliyun.com for immersive online interactive tutorials.