MVC principle of springboot (3)-custom registration

MVC principle of springboot (3)-custom registration

1. Background

At present, in the field of java programming, the spring series are the most popular basic framework dependence besides jdk. Basically all applications use spring as the basic framework for architecture. As large as the first-line manufacturers (Ali, Tencent), as small as personal applications, after the project goes online, there will be some backdoor operations for online running status detection and data correction, etc., and these backdoors are not directly open to users or transparent to users. Then we need to use some methods to protect these backdoor interfaces or shield them by environment. There are generally two commonly used methods:

  • Interface registration and online for permission control: ip dimension, visitor dimension restriction
  • Sub-environment registration: do not register in the online environment, register under the pre-release or gray-scale environment

Of course, based on past experience and risk control safety considerations, the first type is basically not used, so we will mainly talk about the second type today.

2. Principle analysis

From the previous article " Springboot's MVC Principle (2) -Capability Support", we understand the principle of springmvc's support for web capabilities, so briefly review the support of springboot for mvc at startup: RequestMappingHandlerMapping creation: RequestMappingHandlerMapping initialization: Essentially The url and method of the corresponding interface layer in the Controller are encapsulated and injected into the HandlerMapping for DispatcherServlet to use when processing requests.

Back to the main point of our article, if you want to implement interface sub-environment registration, it is necessary to create and initialize RequestMappingHandlerMapping as an entry point. 1. let's take a look at the WebMvcAutoConfiguration class that springboot supports for mvc capabilities:

@Configuration @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }) @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCEServlet + 10) @AutoConfigureAfterAutoConfiguration. class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class}) public class WebMvcAutoConfiguration { //... omitted /** * Configuration equivalent to { @code @EnableWebMvc }. */ @Configuration public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration { private final WebMvcProperties mvcProperties; private final ListableBeanFactory beanFactory; private final WebMvcRegistrations mvcRegistrations; public EnableWebMvcConfiguration ( ObjectProvider<WebMvcProperties> mvcPropertiesProvider, ObjectProvider<WebMvcRegistrations> mvcRegistrationsProvider, ListableBeanFactory beanFactory) { this .mvcProperties = mvcPropertiesProvider.getIfAvailable(); this .mvcRegistrations = mvcRegistrationsProvider.getIfUnique(); this .beanFactory = beanFactory; } @Bean @Primary @Override public RequestMappingHandlerMapping requestMappingHandlerMapping () { //Must BE @Primary Work for MvcUriComponentsBuilder to return Super .requestMappingHandlerMapping (); } @Override protected RequestMappingHandlerMapping createRequestMappingHandlerMapping () { if ( this .mvcRegistrations != null && this .mvcRegistrations.getRequestMappingHandlerMapping() != null ) { return this .mvcRegistrations.getRequestMappingHandlerMapping(); } return super .createRequestMappingHandlerMapping(); } } Copy code

There is an annotation on the WebMvcAutoConfiguration layer configuration:

@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) Copy code

It means that if the WebMvcConfigurationSupport class or its subclasses are not injected into the spring container, the configuration will be injected. WebMvcConfigurationSupport is the main configuration class for mvc capabilities. Generally, @EnableWebMvc is added to the application startup class to enable mvc capabilities, or another advanced implementation is used to directly extend this class and rewrite methods as needed.

A configuration class EnableWebMvcConfiguration is defined in the WebMvcAutoConfiguration class. It can be seen from the class annotation that this configuration class is equivalent to @EnableWebMvc, which is also an implementation of the new version of springboot that enables web capabilities by default. Then EnableWebMvcConfiguration is the core implementation of mvc capability support. Take a look at its inheritance relationship: You can see that EnableWebMvcConfiguration inherits from WebMvcConfigurationSupport, so the @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) mentioned earlier is that if the user customizes WebMvcConfigurationSupport or its subclass implementation, Then give up the default implementation of springboot. As can be seen from the first timing diagram above, the definition of HandlerMapping will eventually use EnableWebMvcConfiguration#createRequestMappingHandlerMapping

@Override protected RequestMappingHandlerMapping createRequestMappingHandlerMapping () { if ( this .mvcRegistrations != null && this .mvcRegistrations.getRequestMappingHandlerMapping() != null ) { return this .mvcRegistrations.getRequestMappingHandlerMapping(); } return super .createRequestMappingHandlerMapping(); } Copy code

The definition of this method is that if the user customizes WebMvcRegistrations and implements the getRequestMappingHandlerMapping method, then use the user-defined HandlerMapping, otherwise use the default RequestMappingHandlerMapping, WebMvcRegistrations:

public interface WebMvcRegistrations { /** * Return the custom { @link RequestMappingHandlerMapping} that should be used and * processed by the MVC configuration. * @return the custom { @link RequestMappingHandlerMapping} instance */ default RequestMappingHandlerMapping getRequestMappingHandlerMapping () { return null ; } /** * Return the custom { @link RequestMappingHandlerAdapter} that should be used and * processed by the MVC configuration. * @return the custom { @link RequestMappingHandlerAdapter} instance */ default RequestMappingHandlerAdapter getRequestMappingHandlerAdapter () { return null ; } /** * Return the custom { @link ExceptionHandlerExceptionResolver} that should be used and * processed by the MVC configuration. * @return the custom { @link ExceptionHandlerExceptionResolver} instance */ default ExceptionHandlerExceptionResolver getExceptionHandlerExceptionResolver () { return null ; } } Copy code

It provides custom extensions for the three basic components of RequestMappingHandlerMapping/RequestMappingHandlerAdapter/ExceptionHandlerExceptionResolver. From here, we can also see the good intentions of spring opening and closing principles.

Through the above description, if we want to customize HandlerMapping or other components, there are two ideas:

  1. Inherit WebMvcConfigurationSupport or its subclasses to implement custom extensions
  2. Customize WebMvcRegistrations for extension

3. interface sub-environment registration

Since the sub-environment registration is mentioned, before implementation, let's briefly talk about the environment-related components EnvironmentAware and Environment. EnvironmentAware is an interface. After the application is started, the environment information is encapsulated into Environment and injected into the implementation class:

public interface EnvironmentAware extends Aware { /** * Set the { @code Environment} that this component runs in. */ void setEnvironment (Environment environment) ; } Copy code

Next, we will start to implement interface sub-environment registration from the code level.

1. Custom HandlerMapping

It can be seen from the inheritance relationship of RequestMappingHandlerMapping that it has provided a more mature implementation of HandlerMapping, then we will follow the thinking of borrowing chicken and laying eggs, and expand on the basis of RequestMappingHandlerMapping.From the initialization sequence diagram of RequestMappingHandlerMapping, we find that the interface is The discovery will eventually be transferred to the getMappingForMethod method of RequestMappingHandlerMapping:

protected RequestMappingInfo getMappingForMethod (Method method, Class<?> handlerType) { RequestMappingInfo info = createRequestMappingInfo(method); if (info != null ) { RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType); if (typeInfo != null ) { info = typeInfo.combine(info); } String prefix = getPathPrefix(handlerType); if (prefix != null ) { info = RequestMappingInfo.paths(prefix).build().combine(info); } } return info; } Copy code

The getMappingForMethod method is transferred to the createRequestMappingInfo method:

private RequestMappingInfo createRequestMappingInfo (AnnotatedElement element) { RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class); RequestCondition<?> condition = (element instanceof Class? getCustomTypeCondition((Class<?>) element): getCustomMethodCondition((Method) element)); return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition): null ); } Copy code

In theory, we inherited RequestMappingHandlerMapping and rewritten createRequestMappingInfo to meet the demands. However, it is a private method and cannot be rewritten, so if you want to extend it, you must rewrite getMappingForMethod and implement createRequestMappingInfo yourself. Custom HandlerMapping implements FilterRequestMappingHandlerMapping:

@Slf4j public class FilterRequestMappingHandlerMapping extends RequestMappingHandlerMapping implements EnvironmentAware { private CurrentEnv currentEnv; @Override public void afterPropertiesSet () { log.info( "FilterRequestMappingHandlerMapping start initializing..." ); super .afterPropertiesSet(); } @Override protected RequestMappingInfo getMappingForMethod (Method method, Class<?> handlerType) { //RequestMappingInfo requestMappingInfo = super.getMappingForMethod(method, handlerType); RequestMappingInfo info = createRequestMappingInfo(method); if (info != null ) { RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType); if (typeInfo != null ) { info = typeInfo.combine(info); } } return info; } private RequestMappingInfo createRequestMappingInfo (AnnotatedElement element) { RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class); if ( null == requestMapping) { return null ; } EnvLimit envLimit = AnnotatedElementUtils.findMergedAnnotation(element, EnvLimit.class); if (isEnvLimited(envLimit)) { log.info( "FilterRequestMappingHandlerMapping.createRequestMappingInfo current env should not registry mapping;env={},url={}" ,currentEnv,requestMapping.value()); return null ; } RequestCondition<?> condition = (element instanceof Class? getCustomTypeCondition((Class<?>) element): getCustomMethodCondition((Method) element)); return createRequestMappingInfo(requestMapping, condition); } @Override public void setEnvironment (Environment environment) { String env = environment.getActiveProfiles()[ 0 ]; log.info( "FilterRequestMappingHandlerMapping.setEnvironment current env is {}" ,env); this .currentEnv = CurrentEnv.of(env.toLowerCase()); } /** * Check whether the current environment needs to register mapping * * @param envLimit * @return */ protected boolean isEnvLimited (EnvLimit envLimit) { if ( null == envLimit) { return false ; } for (CurrentEnv env: envLimit.exclude()) { if (env.equals(currentEnv)) { return true ; } } return false ; } } Copy code

First of all, the environment information will be injected into this class when it is initialized:

/** * Development */ DEV( "dev" , 1 , "development" ), /** * Test */ TEST( "test" , 2 , "TEST" ), /** * Grayscale */ GRAY( "gray" , 3 , "gray" ), /** * produce */ PROD ( "Prod" , . 4 , "production" ) copy the code

Then use the afterPropertiesSet method call chain to finally call createRequestMappingInfo to create the interface mapping information. Here we use the custom annotation EnvLimit to determine whether the current startup environment wants to register the interface:

@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface EnvLimit { /** * Default environmental restrictions * * @return */ CurrentEnv[] value() default {CurrentEnv.DEV,CurrentEnv.TEST,CurrentEnv.GRAY,CurrentEnv.PROD}; CurrentEnv[] exclude() default {CurrentEnv.PROD}; } Copy code

The core is that isEnvLimited compares the EnvLimit limit with the current environment currentEnv to determine whether the current environment is registered for mapping:

protected boolean isEnvLimited (EnvLimit envLimit) { if ( null == envLimit) { return false ; } for (CurrentEnv env: envLimit.exclude()) { if (env.equals(currentEnv)) { return true ; } } return false ; } Copy code

2. Apply custom HandlerMapping

From the second section, we know that there are two ways to use custom web components, here are not long-winded and directly implemented separately:

  • Inherit WebMvcConfigurationSupport or its subclasses

We directly inherit the WebMvcConfigurationSupport class

@Configuration public class CustomWebMvcConfigurationSupport extends WebMvcConfigurationSupport { @Override protected RequestMappingHandlerMapping createRequestMappingHandlerMapping () { return new FilterRequestMappingHandlerMapping(); } } Copy code

We set isLimit to always true and start the application: From the printed log, we found that we have realized the ability to register the interface by environment.

  • Customize WebMvcRegistrations for extension

Customize the WebMvcRegistrations implementation class and inject it into spring:

@Configuration public class WebMvcConfig { @Bean public WebMvcRegistrations webMvcRegistrationsHandlerMapping () { return new WebMvcRegistrations() { @Override public RequestMappingHandlerMapping getRequestMappingHandlerMapping () { return new FilterRequestMappingHandlerMapping(); } }; } } Copy code

Restart the application: The ability to register interfaces in different environments is also realized.

4. summary

In this article, we have implemented the ability to register interfaces in different environments by extending spring components. For the two implementation methods, I personally prefer the second one, customizing the implementation of WebMvcRegistrations, because spring has helped us add a lot of convenience and practicality to the entire WebMvcConfigurationSupport inheritance relationship. If we use the first type, we will either re-start these by ourselves, or discard them directly in most cases. If we use the second type, we will extend our custom capabilities while retaining the default extension features. The shortcomings are clear.