本文是个人在学习书籍《Go语言高并发与微服务实战》第8章内容过程中,而动手做的小实验,涉及spring cloud config和yaml相关知识,仅供参考。

准备spring boot环境

对java熟悉的童鞋可以直接使用IDEA,安装插件即可直接使用。
本人使用Mac机器,在这里尝试使用CLI模式搭建环境(PS:因对java不专,最后发现此步骤非必须,但还是保留搭建记录吧)。

# brew tap spring-io/tap
# brew install spring-boot

试一下helloworld运行,新建并编辑文件app.groovy:

@RestController
class ThisWillActuallyRun {

    @RequestMapping("/")
    String home() {
        "Hello World!"
    }

}

运行效果:

# spring run app.groovy
Resolving dependencies..............................................................................

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.5.2)

2021-07-19 14:58:03.533  INFO 40088 --- [       runner-0] o.s.boot.SpringApplication               : Starting application using Java 1.8.0_281 on lijingzhaodeMacBook-Pro.local with PID 40088 (started by lijingzhao in /Users/lijingzhao/LocalProject/spring/config-demo)
2021-07-19 14:58:03.548  INFO 40088 --- [       runner-0] o.s.boot.SpringApplication               : No active profile set, falling back to default profiles: default
2021-07-19 14:58:05.353  INFO 40088 --- [       runner-0] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2021-07-19 14:58:05.375  INFO 40088 --- [       runner-0] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2021-07-19 14:58:05.375  INFO 40088 --- [       runner-0] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.48]
2021-07-19 14:58:05.435  INFO 40088 --- [       runner-0] org.apache.catalina.loader.WebappLoader  : Unknown class loader [org.springframework.boot.cli.compiler.ExtendedGroovyClassLoader$DefaultScopeParentClassLoader@63cb21ef] of class [class org.springframework.boot.cli.compiler.ExtendedGroovyClassLoader$DefaultScopeParentClassLoader]
2021-07-19 14:58:05.478  INFO 40088 --- [       runner-0] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2021-07-19 14:58:05.478  INFO 40088 --- [       runner-0] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1599 ms
2021-07-19 14:58:06.727  INFO 40088 --- [       runner-0] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2021-07-19 14:58:06.748  INFO 40088 --- [       runner-0] o.s.boot.SpringApplication               : Started application in 4.243 seconds (JVM running for 132.146)
2021-07-19 14:59:21.243  INFO 40088 --- [nio-8080-exec-3] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2021-07-19 14:59:21.244  INFO 40088 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2021-07-19 14:59:21.249  INFO 40088 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet        : Completed initialization in 4 ms

打开 http://localhost:8080/ 即可看到效果

部署配置中心server端

使用IDEA Initializr可生成依赖Config Server的项目。
也可以使用网页工具 https://start.spring.io/ 帮你生成项目代码,本文使用项目名为config-server。

生成代码后,修改 src/main/java/com/example/configserver/ConfigServerApplication.java 文件,增加 @EnableConfigServer ,整体代码如下:

package com.example.configserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {

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

}

新增配置文件 src/main/resources/application.yml 内容如下:

server:
  port: 8888
spring:
  application:
    name: config-server
  profiles:
    active: git #设置使用本地配置(默认是git,可以设置:subversion(SVN),native(本地))
  cloud:
    config:
      server:
        #如下是GIT配置
        git:
          uri: https://github.com/longjoy/config-repo.git 
          search-paths: ${APP_ENV:dev}
          username: 
          password: 
          default-label: master

使用marven工具编译打包成jar,java指令运行:

# ./mvnw clean package
# java -jar target/config-server-0.0.1-SNAPSHOT.jar

运行成功后,可使用curl指令 curl http://localhost:8888/master/client-demo-dev.yaml 查看结果

编写client端代码

resume.go

package main

import (
	"fmt"
	"github.com/longjoy/micro-go-book/ch8-config/conf"
	"github.com/spf13/viper"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/resumes", func(w http.ResponseWriter, req *http.Request) {
		//q := events.goreq.URL.Query().Get("q")
		_, _ = fmt.Fprintf(w, "个人信息:\n")
		_, _ = fmt.Fprintf(w, "姓名:%s,\n性别:%s,\n年龄 %d!", viper.GetString("resume.name"), conf.Resume.Sex, conf.Resume.Age) //这个写入到w的是输出到客户端的
	})
	log.Fatal(http.ListenAndServe(":8081", nil))
}

conf/config.go

package conf

import (
	"fmt"
	"log"
	"net/http"

	"github.com/spf13/viper"
)

const (
	kAppName       = "APP_NAME"
	kConfigServer  = "CONFIG_SERVER"
	kConfigLabel   = "CONFIG_LABEL"
	kConfigProfile = "CONFIG_PROFILE"
	kConfigType    = "CONFIG_TYPE"
)

var (
	Resume ResumeConfig
)

type ResumeConfig struct {
	Name string
	Age  int
	Sex  string
}

func init() {
	viper.AutomaticEnv()
	initDefault()

	if err := loadRemoteConfig(); err != nil {
		log.Fatal("Fail to load config", err)
	}

	if err := sub("resume", &Resume); err != nil {
		log.Fatal("Fail to parse config", err)
	}
}

func initDefault() {
	viper.SetDefault(kAppName, "client-demo")
	viper.SetDefault(kConfigServer, "http://localhost:8888")
	viper.SetDefault(kConfigLabel, "master")
	viper.SetDefault(kConfigProfile, "dev")
	viper.SetDefault(kConfigType, "yaml")
}

func loadRemoteConfig() (err error) {
	confAddr := fmt.Sprintf("%v/%v/%v-%v.%v",
		viper.Get(kConfigServer), viper.Get(kConfigLabel),
		viper.Get(kAppName), viper.Get(kConfigProfile),
		viper.Get(kConfigType))
	resp, err := http.Get(confAddr)
	if err != nil {
		return
	}
	defer resp.Body.Close()

	viper.SetConfigType(viper.GetString(kConfigType))
	if err = viper.ReadConfig(resp.Body); err != nil {
		return
	}
	log.Println("Load config from: ", confAddr)
	return
}

func sub(key string, value interface{}) error {
	log.Printf("配置文件的前缀为:%v", key)
	sub := viper.Sub(key)
	sub.AutomaticEnv()
	sub.SetEnvPrefix(key)
	return sub.Unmarshal(value)
}

编译运行

# go run resume.go
2021/07/19 18:10:38 Load config from:  http://localhost:8888/master/client-demo-dev.yaml
2021/07/19 18:10:38 配置文件的前缀为:resume

浏览器访问 http://127.0.0.1:8081/resumes ,或使用curl指令curl http://127.0.0.1:8081/resumes 可以查看client程序获取到的配置内容。

总结

  • spring cloud config是一个比较容易上手的配置中心,通过它我们可以快速了解配置中心的功能、原理及其使用。
  • yaml是其中一种配置文件格式,而viper是一个比较方便的配置解析工具,支持的格式也比较多,可以灵活使用。
  • 关于热更新问题,书中提到的方案是amqp,其实用的是消息队列通知,本文则不做演示;因为配置改动频率不高,且引入mq感觉不太划算,而且还有别的方式可以实现。

关于我

name: yison.li
blog: http://yyeer.com
github: https://github.com/yisonli