spring_cloud config configuration center and automatic hot load configuration using Github

Posted by gibbo1715 on Tue, 04 Jan 2022 15:46:18 +0100

spring_cloud has strong ecological support. Its own distributed configuration center can effectively solve the problem of inconsistent configuration in distributed environment and provide a centralized configuration center. And rely on its spring_bus (rabbitMq provides subscription) and webhook (hook function) of github or gitlab can push the modified configuration to the remote git address, and then push the changes of the configuration center to each cluster server by accessing the endPoints interface address of the configuration server.

Spring Cloud Config is used to provide centralized external configuration support for infrastructure and microservice applications in distributed systems. It is divided into two parts: server and client. The server is also called distributed configuration center. It is an independent micro service application, which is used to connect the configuration warehouse and provide access interfaces for the client to obtain configuration information, encryption / decryption information and so on; The client is each micro service application or infrastructure in the micro service architecture. They manage the application resources and business-related configuration content through the specified configuration center, and obtain and load the configuration information from the configuration center at startup. Spring Cloud Config implements the abstract mapping of environment variables and attribute configurations in the server and client, so it can also be used in applications running in any other language in addition to applications built by spring. Since the configuration center implemented by Spring Cloud Config uses Git to store configuration information by default, the configuration server built by Spring Cloud Config naturally supports version management of micro service application configuration information, and can easily manage and access configuration content through Git client tools. Of course, it also provides support for other storage methods, such as SVN warehouse and localized file system.

Without much to say, let's look at the code:

First, the spring used this time_ The cloud version is Finchley RELEASE. spring_ The boot version is 2.0.3 Release, lower version of spring_cloud does not have the endpoint interface address of actor / bus refresh, so you should pay attention to it when using it

The first is to configure the central server, which requires the following four References:

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>

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

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-bus</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
        </dependency>

The second is the configuration file:

server.port=20000
#git warehouse address of the service
spring.cloud.config.server.git.uri=https://github.com/narutoform/springCloudConfig
#Directory where the configuration file is located
spring.cloud.config.server.git.search-paths=/**
#Branch where the configuration file is located
spring.cloud.config.label=master
#User name of git repository
spring.cloud.config.username=narutoform
#Password of git warehouse
spring.cloud.config.password=*****
spring.application.name=springCloudConfigService
eureka.client.service-url.defaultZone=http://localhost:10000/eureka
eureka.instance.preferIpAddress=true
#rabbitmq
spring.rabbitmq.host=192.168.210.130
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.publisher-confirms=true
management.endpoints.web.exposure.include=bus-refresh

Note that the bus refresh interface should be turned on, and the user name and password are only required to access the items requiring permission, such as gitlab, but not github. In addition, if the configuration of rabbitMq does not need to be configured, the hot update does not need to be written

Startup class:

package cn.chinotan;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.cloud.config.server.EnableConfigServer;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableConfigServer
@EnableEurekaClient
@ServletComponentScan
public class StartConfigServerEureka {

    public static void main(String[] args) {
        SpringApplication.run(StartConfigServerEureka.class, args);
    }

}

You need to register this configuration center with euerka

The next step is the client configuration of the configuration center. This time, two clients are prepared to form a cluster for demonstration

The references required by the client are:

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-bus</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
        </dependency>

The configuration file is: bootstrap yml

#Enable configuration service discovery
spring.cloud.config.discovery.enabled: true
spring.cloud.config.enabled: true
#Configure service instance name
spring.cloud.config.discovery.service-id: springCloudConfigService
#Branch of configuration file
spring.cloud.config.label: master
spring.cloud.config.profile: prod
#Configure Service Center
spring.cloud.config.uri: http://localhost:20000/
eureka.client.service-url.defaultZone: http://localhost:10000/eureka
eureka.instance.preferIpAddress: true
management.endpoints.web.exposure.include: bus-refresh

Note that the configuration center must write to bootstrap YML, because bootstrap YML should precede application YML read

Here is application YML configuration

server.port: 40000
spring.application.name: springCloudConfigClientOne
#rabbitmq
spring.rabbitmq.host: 192.168.210.130
spring.rabbitmq.port: 5672
spring.rabbitmq.username: guest
spring.rabbitmq.password: guest
spring.rabbitmq.publisher-confirms: true

Note that if the client wants hot update, it also needs to introduce spring_ For bus related configuration and rabbitmq related configuration, open the bus refresh interface. The client does not need to input the address of the remote git, but only needs to read from the newly configured server. When connecting, configure the serverId of the erruka of the configuration server. In this paper, it is springCloudConfigService. In addition, label (Branch) and profile (environment) can be specified

After the configuration center server is started, you can start the client to read the configuration obtained by the server

The client starts as follows:

You can see that when the client starts, it will configure the central server to get the configuration from the remote git warehouse

Add the following code to the client to directly read the configuration of the remote configuration center

package cn.chinotan.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RefreshScope
public class ConfigClientController {

    @Value("${key}")
    private String key;

    @GetMapping("/key")
    public String getProfile() {
        return this.key;
    }
}

The structure of the remote configuration center is:

Note that the client needs to add @ RefreshScope to the configuration you want to change before it can hot update the configuration. Otherwise, the subscribed client does not know which configuration to update

In addition, the address accessed by the client can also be accessed directly by get, so as to judge whether the configuration center server starts normally

Through access http://localhost:20000/springCloudConfig/default Just the interface

Prove that the configuration service center can obtain configuration information from the remote program.

The http request address and resource file are mapped as follows:

/{application}/{profile}[/{label}] /{application}-{profile}.yml /{label}/{application}-{profile}.yml /{application}-{profile}.properties /{label}/{application}-{profile}.properties Now let's access the controller written before on the client to get the configuration in the configuration file

It can be seen that the client can get the information in the remote configuration file from the server

In fact, the client will start through spring_ The restTemplate of boot initiates a GET request to GET the information of the server. The source code is as follows:

private Environment getRemoteEnvironment(RestTemplate restTemplate,
			ConfigClientProperties properties, String label, String state) {
		String path = "/{name}/{profile}";
		String name = properties.getName();
		String profile = properties.getProfile();
		String token = properties.getToken();
		int noOfUrls = properties.getUri().length;
		if (noOfUrls > 1) {
			logger.info("Multiple Config Server Urls found listed.");
		}

		Object[] args = new String[] { name, profile };
		if (StringUtils.hasText(label)) {
			if (label.contains("/")) {
				label = label.replace("/", "(_)");
			}
			args = new String[] { name, profile, label };
			path = path + "/{label}";
		}
		ResponseEntity<Environment> response = null;

		for (int i = 0; i < noOfUrls; i++) {
			Credentials credentials = properties.getCredentials(i);
			String uri = credentials.getUri();
			String username = credentials.getUsername();
			String password = credentials.getPassword();

			logger.info("Fetching config from server at : " + uri);

			try {
				HttpHeaders headers = new HttpHeaders();
				addAuthorizationToken(properties, headers, username, password);
				if (StringUtils.hasText(token)) {
					headers.add(TOKEN_HEADER, token);
				}
				if (StringUtils.hasText(state) && properties.isSendState()) {
					headers.add(STATE_HEADER, state);
				}

				final HttpEntity<Void> entity = new HttpEntity<>((Void) null, headers);
				response = restTemplate.exchange(uri + path, HttpMethod.GET, entity,
						Environment.class, args);
			}
			catch (HttpClientErrorException e) {
				if (e.getStatusCode() != HttpStatus.NOT_FOUND) {
					throw e;
				}
			}
			catch (ResourceAccessException e) {
				logger.info("Connect Timeout Exception on Url - " + uri
						+ ". Will be trying the next url if available");
				if (i == noOfUrls - 1)
					throw e;
				else
					continue;
			}

			if (response == null || response.getStatusCode() != HttpStatus.OK) {
				return null;
			}

			Environment result = response.getBody();
			return result;
		}

		return null;
	}

After that, let's try the hot update of the configuration file

When we start the server and client, we will find that rabbitMq has one more switch and several queues, spring_bus pushes the notification of configuration change through this topic switch. The effect is as follows:

After changing the remote configuration file, call the configuration server. http://localhost:20000/actuator/bus -After the refresh interface:

As you can see, the message is delivered and the change results are pushed

Which calls http://localhost:20000/actuator/bus-refresh is because the server exposes this interface at startup

You can see that this is a POST request, and its interface returns nothing after the call, and the lower version of spring_ There is no such interface in cloud

In this way, the hot update configuration file of the client cluster can be realized, but it can also be called manually http://localhost:20000/actuator/bus-refresh interface, is there any way to automatically push to the client after the remote configuration warehouse file is changed? The answer is through GitHub or gitlab's webhook (hook function). Open the GitHub management interface to see the following information. Click add webhook to add a hook function

Because I don't have a public network address, I can only map ports through intranet penetration, using ngrok

So you can pass http://chinogo.free.idcfengye.com This public domain name has accessed my local service

But is that ok? It's still too young

You can see that GitHub will add such a string of payloads to the body by default when making a post request

The function of sending payload has not been canceled, so our spring boot reported 400 error because it failed to deserialize this string of payload normally:

Failed to read HTTP message: org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize instance of `java.lang.String` out of START_ARRAY token; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.lang.String` out of START_ARRAY token

Naturally, I thought of changing the body to be empty to avoid json conversion exceptions. I started to modify the body, so I went to HttpServletRequest to find the setInputStream method. The servlet actually provided us with a wrapper class of HttpServletRequestMapper, We can modify the body content in the request by inheriting this class and overriding the getInputStream method to return the self constructed ServletInputStream. Here, in order to avoid complications, I directly return an empty body. Custom wrapper class

package cn.chinotan.config;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.ByteArrayInputStream;
import java.io.IOException;

/**
 * @program: test
 * @description: Filter webhooks and empty the body
 * @author: xingcheng
 * @create: 2018-10-14 17:56
 **/
public class CustometRequestWrapper extends HttpServletRequestWrapper {

    public CustometRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        byte[] bytes = new byte[0];
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return byteArrayInputStream.read() == -1 ? true:false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }

            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }
        };
    }
}

Implementation filter

package cn.chinotan.config;

import org.springframework.core.annotation.Order;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @program: test
 * @description: filter
 * @author: xingcheng
 * @create: 2018-10-14 17:59
 **/
@WebFilter(filterName = "bodyFilter", urlPatterns = "/*")
@Order(1)
public class MyFilter implements Filter {
    
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;

        String url = new String(httpServletRequest.getRequestURI());

        //Filter only / Actor / bus refresh requests
        if (!url.endsWith("/bus-refresh")) {
            filterChain.doFilter(servletRequest, servletResponse);
            return;
        }

        //Wrap the original request with HttpServletRequest to modify the body content in the post request
        CustometRequestWrapper requestWrapper = new CustometRequestWrapper(httpServletRequest);

        filterChain.doFilter(requestWrapper, servletResponse);
    }

    @Override
    public void destroy() {

    }
}

Don't forget to add this annotation to the startup class:

@ServletComponentScan

In this way, the configuration file can be modified remotely without starting the client for hot loading