20 Spring Framework the Framework Does a Lot of Work but Also Brings Complexity

20 Spring Framework The Framework Does a Lot of Work But Also Brings Complexity #

Today, let’s talk about the complexity that the Spring framework brings to business code, as well as the pitfalls associated with it.

In the previous lesson, we saw the power of using IoC and AOP together through an example of implementing a unified monitoring component. When objects are managed by the Spring container as Beans, we not only have the ability to configure the properties of the Beans through the container, but we can also easily apply AOP to the methods we are interested in.

However, the prerequisite is that the object must be a Bean. You may think that this conclusion is obvious and easy to understand. But just like how we mentioned in the previous lesson that Beans are singletons by default, it is simple to understand but easy to make mistakes in practice. The reasons for this are, on the one hand, that there is a learning curve to understanding the architecture and usage of Spring, and on the other hand, the internal structure of Spring that has accumulated over many years of development is very complex, which is the more important reason.

In my opinion, the complexity of the Spring framework can be mainly seen in three aspects:

First, with the help of IoC and AOP, the Spring framework achieves flexibility in modifying and intercepting Bean definitions and instances, making the actual execution flow of code not linear.

Second, Spring Boot implements automatic configuration based on current dependencies, which saves us the trouble of manual configuration, but also creates some black boxes and increases complexity.

Third, there are multiple versions of Spring Cloud modules, and there is a significant difference between Spring Boot 1.x and 2.x. If you want to do secondary development on Spring Cloud or Spring Boot, the cost of considering compatibility will be high.

Today, we will experience the complexity of Spring through two cases: failing to configure AOP for Spring Cloud Feign component and having the file configuration of a Spring Boot program overwritten. I hope that the content of this lesson will help you confidently find solutions when facing issues that arise from the complexity of the Spring framework.

A Strange Case where Feign AOP Cannot Intercept #

I once encountered a strange case where I wanted to use AOP to handle Feign in order to facilitate unified processing of microservice calls in Spring Cloud. The idea was to use the within pointcut designator to match the implementation of the feign.Client interface for AOP interception.

The code below demonstrates this approach. It uses the @Before annotation to print logs before executing methods. Additionally, it defines a Client class that is marked with the @FeignClient annotation to make it a Feign interface:

// Test Feign
@FeignClient(name = "client")
public interface Client {
    @GetMapping("/feignaop/server")
    String api();
}

// AOP intercepting the implementation of feign.Client
@Aspect
@Slf4j
@Component
public class WrongAspect {

    @Before("within(feign.Client+)")
    public void before(JoinPoint pjp) {
        log.info("within(feign.Client+) pjp {}, args:{}", pjp, pjp.getArgs());
    }
}

// Configuration to scan Feign
@Configuration
@EnableFeignClients(basePackages = "org.geekbang.time.commonmistakes.spring.demo4.feign")
public class Config {

}

After making a service call through Feign, you can see that the logs are printed. The feign.Client interface is indeed intercepted, specifically the execute method:

[15:48:32.850] [http-nio-45678-exec-1] [INFO ] [o.g.t.c.spring.demo4.WrongAspect        :20  ] - within(feign.Client+) pjp execution(Response feign.Client.execute(Request,Options)), args:[GET http://client/feignaop/server HTTP/1.1

Binary data, feign.Request$Options@5c16561a]

Initially, the project used client-side load balancing with Ribbon, and it worked fine. However, the backend services were later load balanced using Nginx for server-side load balancing. As a result, the developers set the URL property in the @FeignClient configuration to directly call the backend service using a fixed URL:

@FeignClient(name = "anotherClient", url = "http://localhost:45678")
public interface ClientWithUrl {

    @GetMapping("/feignaop/server")
    String api();
}

After configuring it this way, the previous AOP aspect unexpectedly stopped working, meaning that the within(feign.Client+) pointcut could no longer intercept the calls to ClientWithUrl.

To reproduce this scenario, I wrote a code snippet that defines two methods for making interface calls using Client and ClientWithUrl:

@Autowired
private Client client;

@Autowired
private ClientWithUrl clientWithUrl;

@GetMapping("client")
public String client() {
    return client.api();
}

@GetMapping("clientWithUrl")
public String clientWithUrl() {
    return clientWithUrl.api();
}

You can see that the AOP aspect logs are printed when calling client, but not when calling clientWithUrl:

[15:50:32.850] [http-nio-45678-exec-1] [INFO ] [o.g.t.c.spring.demo4.WrongAspect        :20  ] - within(feign.Client+) pjp execution(Response feign.Client.execute(Request,Options)), args:[GET http://client/feignaop/server HTTP/1.1
Binary data, feign.Request$Options@5c16561

This is quite puzzling. Does specifying a URL for Feign mean that its implementation is no longer an instance of feign.Client?

To understand the reason, we need to analyze the process of creating a FeignClient, specifically the getTarget method in the FeignClientFactoryBean class. In the source code, there is an if statement on line 4 that executes the loadBalance method when the URL is empty or not configured. This method uses the FeignContext to retrieve an instance of feign.Client from the container:

<T> T getTarget() {

  FeignContext context = this.applicationContext.getBean(FeignContext.class);

  Feign.Builder builder = feign(context);

  if (!StringUtils.hasText(this.url)) {
    ...
    return (T) loadBalance(builder, context,
        new HardCodedTarget<>(this.type, this.name, this.url));
  }
  ...
  String url = this.url + cleanPath();
  Client client = getOptional(context, Client.class);
  if (client != null) {
    if (client instanceof LoadBalancerFeignClient) {
      // not load balancing because we have a url,
      // but ribbon is on the classpath, so unwrap
      client = ((LoadBalancerFeignClient) client).getDelegate();
    }
    builder.client(client);
  }
  ...
}

protected <T> T loadBalance(Feign.Builder builder, FeignContext context, HardCodedTarget<T> target) {

  Client client = getOptional(context, Client.class);
  if (client != null) {
    builder.client(client);
    Targeter targeter = get(context, Targeter.class);
    return targeter.target(this, builder, context, target);
  }
...
}

protected <T> T getOptional(FeignContext context, Class<T> type) {
  return context.getInstance(this.contextId, type);
}

While debugging, you can see that the client is a LoadBalancerFeignClient, which has already been enhanced by a proxy. Clearly, it is a bean:

img

Therefore, for @FeignClient without a specified URL, which corresponds to the LoadBalancerFeignClient, it is possible to intercept it using feign.Client. In line 16 of the source code we posted above, we can see that when the URL is not empty, the client is set to the ‘delegate’ property of LoadBalanceFeignClient. The reason is mentioned in the comment: when there is a URL, client-side load balancing is not needed. However, because Ribbon is in the classpath, the actual Client needs to be extracted from LoadBalanceFeignClient. By debugging, we can see that the client is an ApacheHttpClient:

img

So, where does this ApacheHttpClient come from? Here’s a tip: if you want to know how a class is initialized in the call stack, you can set a breakpoint in the constructor to debug. This way, you can see the entire method call stack in the stack window of the IDE and click on each stack frame to see the entire process.

Using this method, we can see that ApacheHttpClient is instantiated by the HttpClientFeignLoadBalancedConfiguration class:

img

Further examination of the source code of HttpClientFeignLoadBalancedConfiguration reveals that when the LoadBalancerFeignClient bean is instantiated, an ApacheHttpClient is created and set as the delegate:

@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
      SpringClientFactory clientFactory, HttpClient httpClient) {

   ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
   return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
}

public LoadBalancerFeignClient(Client delegate,
      CachingSpringLoadBalancerFactory lbClientFactory,
      SpringClientFactory clientFactory) {

   this.delegate = delegate;
   this.lbClientFactory = lbClientFactory;
   this.clientFactory = clientFactory;
}

Clearly, ApacheHttpClient is created using the ’new’ operator and is not a bean, while LoadBalancerFeignClient is a bean.

With this information, let’s take a look at why ‘within(feign.Client+)’ cannot be intercepted by @FeignClient ClientWithUrl that has the URL set:

The expression declares intercepting the implementation class of feign.Client.

Spring can only intercept beans managed by itself.

Although both LoadBalancerFeignClient and ApacheHttpClient implement the feign.Client interface, the autoconfiguration of HttpClientFeignLoadBalancedConfiguration only defines the former as a bean, while the latter is created using ’new’ and set as the delegate for LoadBalancerFeignClient, thus it is not a bean.

After setting the URL in FeignClient, the client we get is the delegate of LoadBalancerFeignClient, which is not a bean.

Therefore, ‘within(feign.Client+)’ cannot intercept FeignClient with a URL defined.

So, how do we solve this problem? One student suggested modifying the pointcut expression to intercept using the @FeignClient annotation:

@Before("@within(org.springframework.cloud.openfeign.FeignClient)")
public void before(JoinPoint pjp){
    log.info("@within(org.springframework.cloud.openfeign.FeignClient) pjp {}, args:{}", pjp, pjp.getArgs());
}

After modifying it, we can see from the logs that AOP interception is successful:

[15:53:39.093] [http-nio-45678-exec-3] [INFO ] [o.g.t.c.spring.demo4.Wrong2Aspect       :17  ] - @within(org.springframework.cloud.openfeign.FeignClient) pjp execution(String org.geekbang.time.commonmistakes.spring.demo4.feign.ClientWithUrl.api()), args:[]

However, upon closer inspection, we can see that this time the interception is for the API method of the ClientWithUrl interface, not the execute method of the client.Feign interface, which is clearly not what we expected.

The mistake made by this student is not understanding what object should be intercepted. The @FeignClient annotation is marked on the Feign Client interface, so what gets intercepted is the interface defined by Feign, i.e., each actual API interface. On the other hand, intercepting feign.Client interface means intercepting the client implementation class and all the execute methods for all Feign invocations.

Now the question arises: ApacheHttpClient is not a bean and cannot be intercepted, and intercepting the Feign interface itself does not meet the requirements. So what can we do next? After some research, it was found that ApacheHttpClient actually has the opportunity to become an independent bean. By checking the source code of HttpClientFeignConfiguration, it can be seen that when there is no ILoadBalancer type, the automatic configuration will set ApacheHttpClient as a bean.

The reason for doing this is very clear. If we do not want to do client-side load balancing, we should not reference the Ribbon component, naturally there will be no LoadBalancerFeignClient, only ApacheHttpClient:

@Configuration
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
@ConditionalOnMissingBean(CloseableHttpClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
protected static class HttpClientFeignConfiguration {

  @Bean
  @ConditionalOnMissingBean(Client.class)
  public Client feignClient(HttpClient httpClient) {
    return new ApacheHttpClient(httpClient);
  }
}

So, can the problem be solved by commenting out the ribbon module in the pom.xml?

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

However, the problem is not solved, and an error is thrown upon startup:

Caused by: java.lang.IllegalArgumentException: Cannot subclass final class feign.httpclient.ApacheHttpClient
  at org.springframework.cglib.proxy.Enhancer.generateClass(Enhancer.java:657)
  at org.springframework.cglib.core.DefaultGeneratorStrategy.generate(DefaultGeneratorStrategy.java:25)

Here, it involves Spring’s two ways of implementing dynamic proxy:

JDK dynamic proxy, implemented through reflection, only supports proxying classes that implement interfaces;

CGLIB dynamic byte code injection, implemented through inheritance, without this limitation.

By default, Spring Boot 2.x uses CGLIB for dynamic proxying. However, a problem with implementing proxies through inheritance is that final classes cannot be inherited. The ApacheHttpClient class is defined as final, which is why the problem occurs:

public final class ApacheHttpClient implements Client {

To solve this problem, we change the value of the configuration parameter proxy-target-class to false to switch to using JDK dynamic proxying:

spring.aop.proxy-target-class=false

After making these changes, executing the clientWithUrl interface will show that we can now intercept subclasses of feign.Client by using the within(feign.Client+) expression. The following logs show the two interceptions made using @within and within:

[16:29:55.303] [http-nio-45678-exec-1] [INFO ] [o.g.t.c.spring.demo4.Wrong2Aspect       :16  ] - @within(org.springframework.cloud.openfeign.FeignClient) pjp execution(String org.geekbang.time.commonmistakes.spring.demo4.feign.ClientWithUrl.api()), args:[]
[16:29:55.310] [http-nio-45678-exec-1] [INFO ] [o.g.t.c.spring.demo4.WrongAspect        :15  ] - within(feign.Client+) pjp execution(Response feign.Client.execute(Request,Options)), args:[GET http://localhost:45678/feignaop/server HTTP/1.1
Binary data, feign.Request$Options@387550b0]

Now we understand that Spring Cloud uses automatic configuration to configure components based on dependencies, and whether a component becomes a bean determines whether AOP can intercept it. When trying to intercept a Spring Bean through AOP, you should pay attention to it.

With the two cases mentioned in the previous lesson, I have explained the pitfalls related to IoC and AOP. In addition to this, a point that we cannot avoid in business development is Spring’s configuration issues. Let’s take a closer look at that next.

Priority Issues in Spring Program Configuration #

We know that we can configure Spring Boot application parameters through the application.properties configuration file. However, what we may not know is that Spring program configuration has priority, which means that when two different configuration sources contain the same configuration item, one configuration item is likely to be overridden. This is why we may encounter some seemingly strange configuration failures.

Let’s study the configuration sources and their priority issues through a practical case.

For Spring Boot applications, we generally set the management.server.port parameter to expose a separate actuator management port. This is safer and more convenient for monitoring systems to monitor the health of programs.

management.server.port=45679

One day, after the program was redeployed, the monitoring system showed that the program was offline. However, after investigation, it was found that the program was working fine, but the port number of the actuator management port had been changed and was no longer 45679 as defined in the configuration file.

Later, it was discovered that the operations team defined two environment variables MANAGEMENT_SERVER_IP and MANAGEMENT_SERVER_PORT on the server in order to facilitate the monitoring agent to report monitoring data to a unified management service:

MANAGEMENT_SERVER_IP=192.168.0.2
MANAGEMENT_SERVER_PORT=12345

The problem lies here. MANAGEMENT_SERVER_PORT overrides the management.server.port in the configuration file and changes the port number of the application itself. Of course, the monitoring system cannot access the health port of the application through the old management port. As shown in the figure below, the port number of the actuator has become 12345:

img

But the pit doesn’t end there. In order to facilitate user login, developers need to display the default administrator username on the page, so they defined a user.name attribute in the configuration file and set it to defaultadminname:

user.name=defaultadminname

Later, it was found that the username read by the program was not defined in the configuration file at all. What’s going on this time?

With this problem, as well as the problem of environment variables overriding the configuration in the configuration file, let’s write some code to see how many management.server.port and user.name configuration items can be read from Spring.

To query all configurations in Spring, we need to start from the Environment interface. Next, let me explain to you the Property and Profile abstracted by Spring through the Environment:

For Property, various PropertySource classes are abstracted to represent configuration sources. An environment may have multiple configuration sources, each of which contains many configuration items. When querying configuration information, the priority of configuration sources needs to be considered.

Profile defines the concept of scenarios. Usually, we define different environments such as dev, test, stage, and prod as different profiles to logically classify beans according to scenarios. Profiles are also related to configuration files, each environment has its own configuration file, but we will only activate one environment to enable the configuration file for a specific environment.

img

Next, let’s focus on the process of querying Property.

For non-web applications, Spring’s implementation of the Environment interface is the StandardEnvironment class. We use the StandardEnvironment injected by Spring to loop through the PropertySources returned by getPropertySources and query the attribute values of key “user.name” or “management.server.port” in all PropertySources. We then iterate through the getPropertySources method to get all the configuration sources and print them out:

@Autowired
private StandardEnvironment env;

@PostConstruct
public void init() {
    Arrays.asList("user.name", "management.server.port").forEach(key -> {
        env.getPropertySources().forEach(propertySource -> {
            if (propertySource.containsProperty(key)) {
                log.info("{} -> {} Actual value: {}", propertySource, propertySource.getProperty(key), env.getProperty(key));
            }
        });
    });
    System.out.println("Configuration priority:");
    env.getPropertySources().stream().forEach(System.out::println);
}

Let’s study the output log:

2020-01-15 16:08:34.054 INFO 40123 --- [           main] o.g.t.c.s.d.CommonMistakesApplication : ConfigurationPropertySourcesPropertySource {name='configurationProperties'} -> zhuye Actual value: zhuye
2020-01-15 16:08:34.054 INFO 40123 --- [           main] o.g.t.c.s.d.CommonMistakesApplication : PropertiesPropertySource {name='systemProperties'} -> zhuye Actual value: zhuye
2020-01-15 16:08:34.054 INFO 40123 --- [           main] o.g.t.c.s.d.CommonMistakesApplication : OriginTrackedMapPropertySource {name='applicationConfig: [classpath:/application.properties]'} -> defaultadminname Actual value: zhuye
2020-01-15 16:08:34.054 INFO 40123 --- [           main] o.g.t.c.s.d.CommonMistakesApplication : ConfigurationPropertySourcesPropertySource {name='configurationProperties'} -> 12345 Actual value: 12345
2020-01-15 16:08:34.054 INFO 40123 --- [           main] o.g.t.c.s.d.CommonMistakesApplication : OriginAwareSystemEnvironmentPropertySource {name=''} -> 12345 Actual value: 12345
2020-01-15 16:08:34.054 INFO 40123 --- [           main] o.g.t.c.s.d.CommonMistakesApplication : OriginTrackedMapPropertySource {name='applicationConfig: [classpath:/application.properties]'} -> 45679 Actual value: 12345

Configuration priority:
ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
StubPropertySource {name='servletConfigInitParams'}
ServletContextPropertySource {name='servletContextInitParams'}
PropertiesPropertySource {name='systemProperties'}
OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
RandomValuePropertySource {name='random'}
OriginTrackedMapPropertySource {name='applicationConfig: [classpath:/application.properties]'}
MapPropertySource {name='springCloudClientHostInfo'}
MapPropertySource {name='defaultProperties'}

There are three instances of user.name: the first one is configurationProperties with a value of zhuye; the second one is systemProperties, representing system configuration, with a value of zhuye; the third one is applicationConfig, which is our configuration file, with a value of defaultadminname as defined in the configuration file. Similarly, there are three places where management.server.port is defined: the first is configurationProperties, with a value of 12345; the second is systemEnvironment, representing the system environment, with a value of 12345; and the third is applicationConfig, which is our configuration file, with a value defined in the configuration file as 45679.

Lines 7 to 16 of the output show that there are 9 configuration sources in Spring, with ConfigurationPropertySourcesPropertySource, PropertiesPropertySource, OriginAwareSystemEnvironmentPropertySource, and our configuration file being of particular interest.

So, does Spring really query the configuration in this order? And what is configurationProperties at the beginning? To answer these two questions, we need to analyze the source code. Let me explain first that the logic in the following source code analysis is a bit complex, so you can refer to the overall flowchart below to understand it:

img

The StandardEnvironment injected in the Demo extends the AbstractEnvironment class (the purple class in the diagram). Here is the source code of AbstractEnvironment:

public abstract class AbstractEnvironment implements ConfigurableEnvironment {
  private final MutablePropertySources propertySources = new MutablePropertySources();

  private final ConfigurablePropertyResolver propertyResolver =
      new PropertySourcesPropertyResolver(this.propertySources);

  public String getProperty(String key) {
    return this.propertyResolver.getProperty(key);
  }
}

We can see that:

The field propertySources of type MutablePropertySources seems to represent all configuration sources.

The getProperty method queries the configuration through the PropertySourcesPropertyResolver class.

When instantiating the PropertySourcesPropertyResolver, the current MutablePropertySources is passed in.

Next, let’s continue analyzing MutablePropertySources and PropertySourcesPropertyResolver. First, let’s look at the source code of MutablePropertySources (the blue class in the diagram):

public class MutablePropertySources implements PropertySources {

  private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();

  public void addFirst(PropertySource<?> propertySource) {
    removeIfPresent(propertySource);
    this.propertySourceList.add(0, propertySource);
  }

  public void addLast(PropertySource<?> propertySource) {
    removeIfPresent(propertySource);
    this.propertySourceList.add(propertySource);
  }

  public void addBefore(String relativePropertySourceName, PropertySource<?> propertySource) {
    ...
    int index = assertPresentAndGetIndex(relativePropertySourceName);
    addAtIndex(index, propertySource);
  }

  public void addAfter(String relativePropertySourceName, PropertySource<?> propertySource) {
    ...
    int index = assertPresentAndGetIndex(relativePropertySourceName);
    addAtIndex(index + 1, propertySource);
  }

  private void addAtIndex(int index, PropertySource<?> propertySource) {
    removeIfPresent(propertySource);
    this.propertySourceList.add(index, propertySource);
    }
}

We can see that:

The propertySourceList field is used to actually store the PropertySource objects in a CopyOnWriteArrayList.

The class defines methods such as addFirst, addLast, addBefore, addAfter to precisely control the order in which PropertySources are added to the propertySourceList. This also indicates the importance of order.

Let’s continue to look at the source code of PropertySourcesPropertyResolver (the green class in the diagram) and find the actual method for querying the configuration, getProperty.

Here, let’s pay attention to line 9: the propertySources being iterated over is the one passed in via the constructor of PropertySourcesPropertyResolver. Combined with the source code of AbstractEnvironment, we can find that this propertySources is exactly the MutablePropertySources object in AbstractEnvironment. When iterating, if a corresponding Key value is found in the configuration source, that value is used. Therefore, the order of configuration sources in MutablePropertySources is particularly important.

public class PropertySourcesPropertyResolver extends AbstractPropertyResolver {

  private final PropertySources propertySources;

  public PropertySourcesPropertyResolver(@Nullable PropertySources propertySources) {
    this.propertySources = propertySources;
  }

  protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
    if (this.propertySources != null) {
      for (PropertySource<?> propertySource : this.propertySources) {
if (logger.isTraceEnabled()) {
    logger.trace("Searching for key '" + key + "' in PropertySource '" +
        propertySource.getName() + "'");
}
Object value = propertySource.getProperty(key);
if (value != null) {
    if (resolveNestedPlaceholders && value instanceof String) {
        value = resolveNestedPlaceholders((String) value);
    }
    logKeyFound(key, propertySource, value);
    return convertValueIfNecessary(value, targetValueType);
}

Returning to the previous question, when querying all the configuration sources, we noticed that the first one is ConfigurationPropertySourcesPropertySource, what is it?

Actually, it is not an actual configuration source, but plays a proxy role. But through debugging, you will find that the value we obtained is actually provided and returned by it, and there is no looping through the subsequent property sources:

img

By continuing to look at the source code of ConfigurationPropertySourcesPropertySource (the class in the red box), you can find that the getProperty method actually queries the configuration through the findConfigurationProperty method. As shown in the 25th line of the code, this is actually traversing all the configuration sources:

class ConfigurationPropertySourcesPropertySource extends PropertySource<Iterable<ConfigurationPropertySource>>
    implements OriginLookup<String> {

  ConfigurationPropertySourcesPropertySource(String name, Iterable<ConfigurationPropertySource> source) {
    super(name, source);
  }

  @Override
  public Object getProperty(String name) {
    ConfigurationProperty configurationProperty = findConfigurationProperty(name);
    return (configurationProperty != null) ? configurationProperty.getValue() : null;
  }

  private ConfigurationProperty findConfigurationProperty(String name) {
    try {
      return findConfigurationProperty(ConfigurationPropertyName.of(name, true));
    } catch (Exception ex) {
      return null;
    }
  }

  private ConfigurationProperty findConfigurationProperty(ConfigurationPropertyName name) {

    if (name == null) {
      return null;
    }

    for (ConfigurationPropertySource configurationPropertySource : getSource()) {
      ConfigurationProperty configurationProperty = configurationPropertySource.getConfigurationProperty(name);
      if (configurationProperty != null) {
        return configurationProperty;
      }
    }
    return null;
  }
}

Through debugging, it can be found that the list of configuration sources traversed (the result of getSource()) are actually SpringConfigurationPropertySources (the class in yellow), which contains a list of configuration sources we saw before, and the first one is ConfigurationPropertySourcesPropertySource. At this point, our first impression is whether it will cause an infinite loop. How does it exclude itself during traversal?

By observing the configurationProperty, it can be seen that this ConfigurationProperty actually plays a role similar to that of a proxy. The actual configuration is obtained from system properties:

img

Continuing to look at SpringConfigurationPropertySources, it can be found that the iterator it returns is the inner class SourcesIterator. In the fetchNext method, the ConfigurationPropertySourcesPropertySource (line 38 of the source code) is excluded through the isIgnored method:

class SpringConfigurationPropertySources implements Iterable<ConfigurationPropertySource> {
  
  private final Iterable<PropertySource<?>> sources;

  private final Map<PropertySource<?>, ConfigurationPropertySource> cache = new ConcurrentReferenceHashMap<>(16,
      ReferenceType.SOFT);

  SpringConfigurationPropertySources(Iterable<PropertySource<?>> sources) {
    Assert.notNull(sources, "Sources must not be null");
    this.sources = sources;
  }

  @Override
  public Iterator<ConfigurationPropertySource> iterator() {
    return new SourcesIterator(this.sources.iterator(), this::adapt);
  }

  private static class SourcesIterator implements Iterator<ConfigurationPropertySource> {
    @Override
    public boolean hasNext() {
      return fetchNext() != null;
    }

    private ConfigurationPropertySource fetchNext() {
      if (this.next == null) {
        if (this.iterators.isEmpty()) {
          return null;
        }
        if (!this.iterators.peek().hasNext()) {
          this.iterators.pop();
          return fetchNext();
        }
        PropertySource<?> candidate = this.iterators.peek().next();
        if (candidate.getSource() instanceof ConfigurableEnvironment) {
          push((ConfigurableEnvironment) candidate.getSource());
          return fetchNext();
        }
        if (isIgnored(candidate)) {
          return fetchNext();
        }
        this.next = this.adapter.apply(candidate);
      }
      return this.next;
    }

    private void push(ConfigurableEnvironment environment) {
      this.iterators.push(environment.getPropertySources().iterator());
    }

    private boolean isIgnored(PropertySource<?> candidate) {
      return (candidate instanceof StubPropertySource
          || candidate instanceof ConfigurationPropertySourcesPropertySource);
    }
  }
}

We have learned that ConfigurationPropertySourcesPropertySource is the first one among all the configuration sources, which implements the “hijacking” of the traversal logic in PropertySourcesPropertyResolver, and we also know its traversal logic. The last question is, how does it become the first configuration source?

Once again, using the little trick we learned before to look at the instantiation of ConfigurationPropertySourcesPropertySource:

img

It can be seen that the ConfigurationPropertySourcesPropertySource class is instantiated by the attach method of ConfigurationPropertySources. According to the source code, this method actually gets the original MutablePropertySources from the environment, and adds itself as an element:

public final class ConfigurationPropertySources {

  public static void attach(Environment environment) {
    MutablePropertySources sources = ((ConfigurableEnvironment) environment).getPropertySources();
    PropertySource<?> attached = sources.get(ATTACHED_PROPERTY_SOURCE_NAME);
    if (attached == null) {
      sources.addFirst(new ConfigurationPropertySourcesPropertySource(ATTACHED_PROPERTY_SOURCE_NAME,
          new SpringConfigurationPropertySources(sources)));
    }
  }
}

And this attach method is called when the Spring application starts to prepare the environment. The ConfigurationPropertySources.attach method is called in the prepareEnvironment method called in the run method of SpringApplication:

public class SpringApplication {

public ConfigurableApplicationContext run(String... args) {
    ...
    try {
      ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
      ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
      ...
  }

  private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
      ApplicationArguments applicationArguments) {
    ...
    ConfigurationPropertySources.attach(environment);
    ...
    }
}

Now that you have understood how Spring hijacks PropertySourcesPropertyResolver and the reason why configuration sources have priorities, do you completely understand it? If you want to know the priorities of various predefined configuration sources in Spring, you can refer to the official documentation.

Key Review #

Today, I used two practical examples from business development to further study two important aspects of Spring: AOP and configuration priority. By now, you should have a sense of the complexity of implementing Spring.

In the case of using AOP to intercept Feign, we took some detours in implementing the functionality. Spring Cloud utilizes the features of Spring Boot and performs various automatic configurations based on the packages it detects. If we want to extend Spring’s components, we must have a clear understanding of how Spring’s automatic configuration works in order to identify the runtime situation of objects in the Spring container. We cannot assume that all Spring classes we see in the code are automatically Beans.

In the case of configuration priority, when analyzing the priority of configuration sources, it is possible to fall into pitfalls if we believe that seeing PropertySourcesPropertyResolver reveals the truth. We must pay attention to the fact that when analyzing Spring source code, what you see may not necessarily reflect the runtime situation. It is important to use logs or debugging tools to understand the entire process. If you don’t have a debugging tool, you can use Arthas, mentioned in Lesson 11, to analyze the code calling path.

The code used today is stored on GitHub, and you can click on this link to view it.

Reflection and Discussion #

In addition to the four advisors execution, within, @within, and @annotation that we have used in the previous two sessions, Spring AOP also supports five other advisors: this, target, args, @target, and @args. Could you tell us about the functions of these five advisors?

The PropertySources attribute in Spring’s Environment can contain multiple PropertySources, with higher priority given to those placed at the front. Therefore, can we utilize this feature to automatically assign property values in configuration files? For example, we can define %%MYSQL.URL%%, %%MYSQL.USERNAME%%, and %%MYSQL.PASSWORD%%, which respectively represent the database connection string, username, and password. When configuring the data source, we can simply set their values as placeholders. The framework can automatically replace the placeholders with the real database information based on the application name application.name. This way, the production database information does not need to be placed in the configuration file, resulting in better security.

Regarding Spring Core, Spring Boot, and Spring Cloud, have you encountered any other pitfalls? I am Zhu Ye, and I welcome you to leave comments in the comment section to share your thoughts with me. Feel free to share today’s content with your friends or colleagues. Let’s exchange ideas together.