In your daily business development work, you may have more or less encountered the following scenarios:

  • When a particular event or action occurs, many linkage actions need to be performed, which is too time-consuming to execute serially and too heavy to introduce message-oriented middleware.

  • We want to implement different strategies for different parameters, which is often called the strategy mode, but 10 people may have 10 different writing methods, and it is not so elegant to mix them together.

  • Our system wants to invoke the capabilities provided by other systems, but other systems always occasionally give you a “little surprise”, maybe due to network problems reported timeout exceptions or one of the called distributed application machine suddenly down, we want to introduce retry mechanism elegant and non-invasive.

In fact, the above mentioned several typical business development scenarios Spring provides us with very good feature support, we just need to introduce Spring related dependencies can be easily and quickly used in the business code, instead of introducing too many third-party dependency packages or their own repeated wheel. The following is a detailed introduction.

Gracefully implement the observer pattern using Spring

The observer pattern defines a one-to-many dependency between objects. When the state of an object changes, all objects dependent on it are notified and automatically updated. It mainly solves the problem that the state change of an object notifies other related objects, ensuring ease of use and low coupling. A typical application scenario is: After the user registers, the user needs to send emails and coupons to the user, as shown in the following figure.

After using observer mode

After the UserService completes its user registration logic, it only needs to issue a Single UserRegisterEvent event and does not need to worry about any extension logic. Other services can subscribe to the UserRegisterEvent event themselves, implementing custom extension logic. Spring’s event mechanism consists of three main parts.

  • ApplicationEvent: Implement custom events by inheriting it. In addition, the source attribute can obtain the event source, and the timestamp attribute can obtain the occurrence time.

  • ApplicationEventPublisher: by implementing it, to distribute the change event.

  • ApplicationEventListener: Implements it to listen for events of the specified type and respond to actions. Take a look at some code examples using the above user registration as an example. Start by defining a user registration event, UserRegisterEvent.

    Publicclass UserRegisterEvent extends ApplicationEvent {/** * username */ private String username; public UserRegisterEvent(Object source) { super(source); } public UserRegisterEvent(Object source, String username) { super(source); this.username = username; } public String getUsername() { return username; }}

And then define the user registration service class, realize ApplicationEventPublisherAware interface, so as to inject ApplicationEventPublisher come in. From the code below you can see, the execution of the registration after logic, called the ApplicationEventPublisher publishEvent (ApplicationEvent event) method, has released UserRegisterEvent events.

@Service publicclass UserService implements ApplicationEventPublisherAware { // <1> private Logger logger = LoggerFactory.getLogger(getClass()); private ApplicationEventPublisher applicationEventPublisher; @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } public void register(String username) { // ... Execute loglogger. Info ("[register][execute loglogist for user ({})]", username); / / < 2 >... Release applicationEventPublisher. PublishEvent (new UserRegisterEvent (this username)); }}Copy the code

Create a mailbox Service, implement the ApplicationListener interface, use E generic to set the event of interest, implement onApplicationEvent(E Event) method, for the listening UserRegisterEvent event, Perform custom processing.

@Service publicclass EmailService implements ApplicationListener<UserRegisterEvent> { // <1> private Logger logger = LoggerFactory.getLogger(getClass()); @Override @Async// <3> public void onApplicationEvent(UserRegisterEvent event) { // <2> Logger. info("[onApplicationEvent][send email to user ({})]", event.getUsername()); }}Copy the code

Create a coupon Service that implements the ApplicationListener interface by adding the @EventListener annotation to the method and setting the event to be listened to as UserRegisterEvent. That’s another way to use it.

@Service publicclass CouponService { private Logger logger = LoggerFactory.getLogger(getClass()); @eventListener // <1> public void addCoupon(UserRegisterEvent event) {logger.info("[addCoupon][{}]", event.getUsername()); }}Copy the code

In fact, the Observer mode is different from the publish and subscribe mode. To put it simply, the publish and subscribe mode belongs to the Observer mode in a broad sense. Based on the Subject and Observer of the Observer mode, the intermediary Event Channel is introduced. Further decoupling. As you can see from the graph below, the observer mode is more lightweight and is typically used for stand-alone devices, while the publish-subscribe mode is relatively heavy and is typically used for message notification scenarios in distributed environments.

Use Spring Retry to gracefully introduce retries

Now that Spring Retry is a standalone package (originally part of Spring Batch), here are some important steps for retries using the Spring Retry framework. Step 1: Add the Spring Retry dependency package

<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> < version > 1.1.2. RELEASE < / version > < / dependency >Copy the code

Step 2: Annotate @enableretry on classes that contain main() methods or classes that contain @Configuration. Step 3: Annotate @retryable on methods that you want to retry (which may fail)

@Retryable(maxAttempts=5,backoff = @Backoff(delay = 3000))
public void retrySomething() throws Exception{
    logger.info("printSomething{} is called");
    thrownew SQLException();
}
Copy the code

The retry policy in this case is 5 retries with a delay of 3 seconds. See the detailed usage documentation here. Its main configuration parameters are as follows. The exclude, include, maxAttempts and value attributes are easy to understand, while the backoff attribute is also an annotation containing delay, maxDelay, Multiplier and random attributes.

  • Delay: If this parameter is not set, the default value is 1 second
  • MaxDelay: indicates the maximum retry waiting time
  • Multiplier: The number used to calculate the next delay time (greater than 0 applies)
  • Random: indicates the waiting time of random retry.

The advantages of Spring Retry are obvious. First, it is part of the Spring ecosystem and is not too rigid to use. Second, you just need to annotate the methods that need to be retried and configure the retry policy attribute, without too much intrusive code.

There are two major disadvantages. First, because Spring Retry uses Aspect enhancements, there is an inevitable pitfall over Aspect use — method invocations that will be invalid if the method that is annotated by @retryable is in the same class as the called; Second, Spring’s retry mechanism only supports catching exceptions, not validating judgment retries on return values. Guava Retry is also a good option if you want a more flexible Retry strategy.

Elegant use of Spring features to complete the business policy pattern

The strategy pattern, which I believe everyone should be familiar with, defines a series of algorithms, and encapsulates each algorithm, so that each algorithm can be replaced by each other, so that the algorithm itself and the client using the algorithm are separated from each other and independent.

This applies to scenarios like this: a large function that has many different types of implementations (policy classes), depending on the client. For example, there are many application scenarios, such as ordering preferential strategy and logistics docking strategy.

For example, the service background is as follows: The platform requires authentication based on different services. The authentication logic of each service is different and has its own set of independent judgment logic. Therefore, authentication operations need to be performed based on the passed bizType.

/** * Service permission check processor */ publicInterface PermissionCheckHandler {/** * Check whether it can handle permission check type */ Boolean isMatched(BizType bizType); /** * PermissionCheckResultDTO permissionCheck(Long userId, String bizCode); } The authentication logic for business 1 is assumed to look like this: / * * * cold start permissions check processor * / @ Component publicclass ColdStartPermissionCheckHandlerImpl implements PermissionCheckHandler { @Override public boolean isMatched(BizType bizType) { return BizType.COLD_START.equals(bizType); } @Override public PermissionCheckResultDTO permissionCheck(Long userId, String bizCode) {// business specific authentication logic}} The authentication logic of business 2 is assumed to be like this: / * * * trend business permission check processor * / @ Component publicclass TrendPermissionCheckHandlerImpl implements PermissionCheckHandler { @Override public boolean isMatched(BizType bizType) { return BizType.TREND.equals(bizType); } @override public PermissionCheckResultDTO permissionCheck(Long userId, String bizCode){// Business specific authentication logic}}Copy the code

There may be many other business authentication logics that are not listed here, but the implementation logic should be organized as above. This brings us to the key point, which is to use some of the extension features provided by Spring. Spring provides three main extension points for different Bean life cycle phases:

  • Aware interface
  • BeanPostProcessor
  • InitializingBean and init – method

We mainly use two extension points, Aware interface and InitializingBean, whose main usage is shown in the following code. The key points are the setApplicationContext method that implements the ApplicationContextAware interface and the afterPropertiesSet method of the InitializingBean interface.

The purpose of implementing the ApplicationContextAware interface is to take the resources of the Spring container and make it easy to use its provided getBeansOfType method (which returns the map type, Key corresponds to beanName, value corresponds to bean); The InitializingBean interface is implemented to facilitate custom initialization logic for the Handlers attribute of the Service class.

Obviously, if some other businesses need to develop corresponding authentication logic in the future, we just need to write the corresponding policy class, no need to destroy the logic of the current Service class, which ensures the open and closed principle.

/** * @slf4j@service publicClass PermissionServiceImpl implements PermissionService, ApplicationContextAware, InitializingBean { private ApplicationContext applicationContext; Private List<PermissionCheckHandler> Handlers = new ArrayList<>(); @Override public PermissionCheckResultDTO permissionCheck(ArtemisSellerBizType artemisSellerBizType, Long userId, String bizCode) {// Omit some pre-logic PermissionCheckHandler Handler = getHandler(artemisSellerBizType); return handler.permissionCheck(userId, bizCode); } private PermissionCheckHandler getHandler(ArtemisSellerBizType artemisSellerBizType) { for (PermissionCheckHandler handler : handlers) { if (handler.isMatched(artemisSellerBizType)) { return handler; } } returnnull; } @Override public void afterPropertiesSet() throws Exception { for (PermissionCheckHandler handler : applicationContext.getBeansOfType(PermissionCheckHandler.class) .values()) { handlers.add(handler); log.warn("load permission check handler [{}]", handler.getClass().getName()); } } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; }}Copy the code

Handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler Will there be cases where some handlers are not properly initialized?

The answer is no, the declaration cycle of the Spring Bean guarantees this (provided, of course, that the Handler does not have special initialization logic of its own). After the actual verification, all the handlers will be ready before the Service initialization operation. Interested students can write code verification, you can first log the corresponding hook directly output verification, and then debug the breakpoint in the key point of Spring source code, I believe that there will be a lot of harvest.

Summary & Reflection

Some of the code in the company is old, some of the classes are smelly and long, and there are a lot of bad things about the code, such as duplicate code, long parameter columns, scattershot changes, basic bigotry, and so on. Every day to face these code development, not only wear down our enthusiasm for technology and make people become unmotivated, many students will think — anyway, it has been so, I will do it, I believe many partners have such experience and confusion.

But the only thing you can’t stop is progress, even in the face of the dragon. Of course, when doing requirements, there are many times when you can’t modify the code, it’s too time-consuming, too laborious, too risky. You should at least think about how to design your code to avoid the same situation in the future, so that you don’t make the same mistake next time.

When we actually write code, we need to be careful to explore whether Spring provides us with any existing utility classes and extension points. On the one hand, using these features provided by Spring allows us to build fewer wheels and avoid introducing other heavy libraries. On the other hand, Spring abstracts and encapsulates some classes and specifications provided by libraries such as the JDK, making it easier to use and more suitable for developers.