Story one: peerless beauty, secluded in the empty valley
Beauty classmate Xiao Zhang, in the work encountered trouble. The mood is broken cool broken cool, ineffable.
The background of the story is that due to the change of requirements recently, Xiao Zhang added MQ integration into the project. At the beginning, there was no problem, but then the problems gradually emerged.
I can always consume messages when I Debug locally, but for historical reasons, the company’s projects only differentiate between two environments, test and online. The local boot is the test environment by default, so messages from the test environment are consumed.
The configuration code for MQ is as follows:
@Configuration
public class MqConfig {
@Bean(initMethod = "start", destroyMethod = "shutdown")
public ConsumerBean consumerBean() {/ /... }}Copy the code
To solve Zhang’s problem, there must be a third environment distinction, which is to add a local development environment that determines whether MQ needs to be initialized.
In this case, we can use the Conditional family annotation provided by Spring Boot. The @Conditional annotation determines whether to create a bean into the container based on specific conditions, as shown below:
ConditionalOnProperty to decide whether the MqConfig should be loaded, @conditionalonProperty name is the configuration item name, havingValue is the matching value, That is, MqConfig is initialized only if env=dev exists in the application configuration. The code is as follows:
@Configuration
@ConditionalOnProperty(name = "env", havingValue = "dev")
public class MqConfig {
@Bean(initMethod = "start", destroyMethod = "shutdown")
public ConsumerBean consumerBean() {/ /... }}Copy the code
Dev environment does not need to be loaded. The other thing is that for historical reasons, it’s risky to add an environment, because the content of the corresponding environment is going to have to change, and so on, so let’s just leave the history, the environment unchanged, and see if we can solve this problem from another point.
The problem we are facing now is that we cannot add a new environment and keep the previous test and PROD. You only need to initialize Mq on Test and PROd.
Plan 1: @conditionalonProperty
ConditionalOnProperty (@conditionalonProperty); since this is not possible with the environment, you can add a separate attribute to enable Mq, such as mq.enabled=true and mq.enabled=false.
-dmq. Enabled =true for test and prod startup or in the corresponding configuration file. -dmq. Enabled =false for local development.
While this solves the problem, it is not the best solution because the existing environment and the developer have to add startup parameters locally.
Scheme 2: Inherit SpringBootCondition custom condition
You can use the @Conditional(mqConditional.class) annotation to define a Conditional class in which to determine whether to load the bean.
public class MqConditional extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment environment = context.getEnvironment();
String env = environment.getProperty("env");
if (StringUtils.isBlank(env)) {
return ConditionOutcome.noMatch("no match");
}
if (env.equals("test") || env.equals("prod")) {
return ConditionOutcome.match();
}
return ConditionOutcome.noMatch("no match"); }}Copy the code
Scheme 3: Inherit AnyNestedCondition custom condition
Can use @ Conditional (MqAvailableCondition. Class), a condition for the custom class that can be used in the class of other Conditional annotations to determine, for example using the @ ConditionalOnProperty.
@Order(Ordered.LOWEST_PRECEDENCE)
public class MqAvailableCondition extends AnyNestedCondition {
public MqAvailableCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(name = "env", havingValue = "test")
static class EnvTest {
}
@ConditionalOnProperty(name = "env", havingValue = "prod")
static class EnvProd {
}
}
Copy the code
Scheme 4: @conditionalonexpression
SpEL is supported for judgment, and the bean is loaded if the SpEL expression condition is met. So this is pretty flexible, you can write in all the conditions that need to be satisfied.
@ConditionalOnExpression("#{'test'.equals(environment['env']) || 'prod'.equals(environment['env'])}")
Copy the code
The above expression defines that MqConfig is initialized whenever env is test or prod in the Spring Environment. In this way, old startup commands do not need to be changed, and local development does not need to add parameters, which can be said to be the best solution, because there are fewer changes, less chance of error, easy to use.
Story two: There are beautiful women in the north, unique and independent
Belle Xiao Yang also encountered trouble recently, although it is a girl, but also worked for a few years. Recently, she was promoted by the leadership and asked her to build a set of Spring Cloud framework to share with colleagues.
She had an idea that some of the messages could be passed through Feign or RestTemplate, a natural and friendly way to do that would be to unify them in interceptors.
It would be low-level to write the same code for every service, so she wrote both interceptors in a single module, introduced as a Spring Boot Starter.
Problem a
The first problem we encountered was that the module introduced Feign and Spring-Web dependencies. The general idea was that the user might use Feign to call the interface or RestTemplate to call the interface. If the user does not use Feign, the Starter will rely on Feign if it is introduced.
Set Feign’s Maven dependency optional=true to allow users to import dependencies themselves.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<optional>true</optional>
</dependency>
Copy the code
Question 2
The second problem is the initialization of interceptors. If nothing is done, both interceptors will be initialized. If the user does not rely on Feign, an error will be reported, so we need to handle the initialization of interceptors.
Here is the default configuration:
@Bean
public FeignRequestInterceptor feignRequestInterceptor() {
return new FeignRequestInterceptor();
}
@Bean
public RestTemplateRequestInterceptor restTemplateRequestInterceptor() {
return new RestTemplateRequestInterceptor();
}
Copy the code
Both interceptors are native to the implementation framework, so we can use @conditionalonClass in the outermost layer to determine if the Class reconfiguration exists in the project.
The second layer can be used via @conditionalonproperty, handing control to the user.
@Configuration
@ConditionalOnClass(name = "feign.RequestInterceptor")
protected static class FeignRequestInterceptorConfiguration {
@Bean
@ConditionalOnProperty("feign.requestInterceptor.enabled")
public FeignRequestInterceptor feignRequestInterceptor() {
return new FeignRequestInterceptor();
}
}
@Configuration
@ConditionalOnClass(name = "org.springframework.http.client.ClientHttpRequestInterceptor")
protected static class RestTemplateRequestInterceptorConfiguration {
@Bean
@ConditionalOnProperty("restTemplate.requestInterceptor.enabled")
public RestTemplateRequestInterceptor restTemplateRequestInterceptor() {
returnnew RestTemplateRequestInterceptor(); }}Copy the code
Story 3: Learn by yourself
There is only one way to use ConditionalOnBean and @conditionalonmissingBean in this article. Of course, there are many other ways to use ConditionalOnBean. You can try to understand some functions and situations in which ConditionalOnBean can be used.
Another way to learn is to encourage you to look at the source code of some of the frameworks, especially in the Spring Cloud, where there are a lot of auto-configuration frameworks that use these annotations. I’ll post a few images for you to see.
If you are interested, you can pay attention to my wechat public number, Simtiandi, and read more technical articles for the first time. I also have some open source code on GitHub github.com/yinjihuan