天天看点

SpringCloud:如何使用SpringCloud搭建一个简单的微服务

在构建微服务时,最常用的就是SpringCloud,其中的Netflix-Eureka用的最多,本文主要讲讲如何使用它。

一、配置注册服务器(Registry Server/Eureka Server)

Maven配置:

<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>TestCloud</groupId>
  <artifactId>TestCloud-ServiceRegistry</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>TestCloud-ServiceRegistry</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
  </properties>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.3.RELEASE</version>
    <relativePath />
  </parent>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>Greenwich.SR1</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>
           

application.properties配置:

server.port=8761

eureka.instance.hostname=localhost

# eureka.datacenter=Main

# eureka.environment=prod

eureka.server.enableSelfPreservation=false

eureka.client.register-with-eureka=false

eureka.client.fetch-registry=false

eureka.server.enableSelfPreservation - 当service server心跳超时后,是否还要将service server保存在列表中一段时间。 当网络发生短暂分区时,不会因为大量service server心跳超时,而将service server从列表中删除,从而造成客户端调用大面积失败的情况。

eureka.client.register-with-eureka -  是否将自身也作为service server注册到Eureka Server。

eureka.client.fetch-registry -  是否将自身作为service client从Eureka Server中获取service server列表。

ServiceRegistry代码:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class ServiceRegistryEurekaApp {

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

程序运行后,可以通过访问如下地址访问Eureka Server管理控制台:

http://localhost:8761

当有service server注册上来后,可以通过如下地址查看注册的service server的信息:

http://localhost:8761/eureka/apps

 二、配置服务服务器(Service Server/Eureka Client)

Maven配置:

<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>TestCloud</groupId>
  <artifactId>TestCloud-ServiceServer1</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>TestCloud-ServiceServer1</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.3.RELEASE</version>
    <relativePath />
  </parent>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>Greenwich.SR1</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

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

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>
           

bootstrap.properties配置:

spring.profiles.active=test

spring.application.name=TestServer

# spring.main.allow-bean-definition-overriding=true

# spring.cloud.config.uri=http://localhost:8888

spring.cloud.discovery.enabled=true

spring.cloud.service-registry.auto-registration.enabled=true

spring.cloud.config.uri - 表示以后需要访问的配置服务器的地址。

spring.application.name - 表示此service注册到Registry上的服务名称。

spring.cloud.discovery.enabled - 表示是否打开发现服务,默认值为true。

spring.cloud.service-registry.auto-registration.enabled - 表示是否启动后自动注册,默认值为true。

注册可以使用Netflix-Eureka,也可以使用Zookeeper,以下使用Netflix-Eureka为例进行讲解。

application.properties配置:

server.port=8081

# http://localhost:8081/actuator/refresh

management.endpoints.web.exposure.include=refresh

# management.endpoints.jmx.exposure.exclude=*

# management.context-path=/base

eureka.client.enabled=true

eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/

eureka.client.register-with-eureka=true

eureka.client.fetch-registry=true

# eureka.client.healthcheck.enabled=true

# eureka.instance.statusPageUrlPath=${management.context-path}/info

# eureka.instance.healthCheckUrlPath=${management.context-path}/health

# eureka.instance.instanceId=${spring.application.name}:${random.int}

# hostname为此实例提供给外部调用的域名

eureka.instance.hostname=localhost

eureka.instance.leaseRenewalIntervalInSeconds=10

eureka.instance.leaseExpirationDurationInSeconds=20

eureka.instance.metadataMap.versions=v1

management.endpoints.web.exposure.include - 表示可以放开给外部访问的方法,访问方式:http://localhost:8081/actuator/XXX,其中XXX表示实际调用的方法。比如:management.endpoints.web.exposure.include=refresh表示允许通过访问http://localhost:8081/actuator/refresh,访问结果为重新加载@RefreshScope范围内的配置信息。

eureka.client.enabled - 表示是否启用eureka client

eureka.client.serviceUrl.defaultZone - 表示eureka client可以访问的eureka server的地址

eureka.instance.hostname - 为此实例提供给外部调用的域名。

eureka.instance.leaseRenewalIntervalInSeconds - 心跳间隔。如果连续3次心跳都无回复,表示心跳超时。

eureka.instance.leaseExpirationDurationInSeconds - 心跳超时后,此服务会在eureka server的列表中保持多长时间。

eureka.instance.metadataMap.versions - eureka.instance.metadataMap中可以添加用户自定义的任意属性。

此处分为application.properties和bootstrap.properties两个配置文件,主要是为以后连接Config Server做准备,因为service server启动后必须先访问Config Server获取配置信息,然后才能初始化service server。

Java代码:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication(exclude = { TaskSchedulingAutoConfiguration.class })
public class AppBoot {

  public static void main(String[] args) {
    ConfigurableApplicationContext ctx = SpringApplication.run(AppBoot.class, args);
    ctx.registerShutdownHook();
  }
}
           

 AppBoot负责启动整个应用

import java.util.concurrent.Executor;

import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.serviceregistry.ServiceRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
@EnableDiscoveryClient
@EnableAsync
public class AppConf {

  private ServiceRegistry registry;

  public AppConf(ServiceRegistry registry) {
    this.registry = registry;
  }

  public ServiceRegistry getRegistry() {
    return registry;
  }

  @Bean(name = "taskExecutor")
  public Executor taskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(10);
    executor.setMaxPoolSize(20);
    executor.setQueueCapacity(200);
    executor.setKeepAliveSeconds(60);
    executor.setThreadNamePrefix("taskExec-");
    // executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    return executor;
  }
}
           

AppConf中获取ServiceRegistry,并生成一个线程池供以后使用。

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.serviceregistry.Registration;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/appCtrl")
public class AppController {

  @Autowired
  private Registration registration;

  @Autowired
  private DiscoveryClient discoveryClient;

  @Autowired
  private AppConf configuration;

  @RequestMapping("/register")
  public void register() {
    configuration.getRegistry().register(registration);

    System.out.println("Host:" + registration.getHost());
    System.out.println("Port:" + registration.getPort());
    System.out.println("InstanceId:" + registration.getInstanceId());
    System.out.println("ServiceId:" + registration.getServiceId());
    System.out.println("Uri:" + registration.getUri());
  }

  @RequestMapping("/deregister")
  public void deregister() {
    configuration.getRegistry().deregister(registration);
  }

  @RequestMapping("/service-instances/{applicationName}")
  public List<ServiceInstance> serviceInstancesByApplicationName(@PathVariable String applicationName) {
    return this.discoveryClient.getInstances(applicationName);
  }
}
           

registration本身也是一个service instance,它保存了当前Service Server的相关信息。discoveryClient可以获取Eureka Server上已注册的service instance。AppController可以注册此服务到Eureka Server,或从Eureka Server撤销此服务,也可以获取注册的服务信息。

应用启动后,就可以从Eureka Server的控制台上看到TestServer注册成功了。

关于service server自身的一些信息,可以直接访问http://localhost:8081/actuator中的一些命令获取。

如果不知道/actuator下面有哪些命令,可以通过访问http://localhost:8081/actuator获取所有可用的命令,返回结果为:

{"_links":{

"self":{"href":"http://localhost:8081/actuator","templated":false

"archaius":{"href":"http://localhost:8081/actuator/archaius","templated":false

"auditevents":{"href":"http://localhost:8081/actuator/auditevents","templated":false

"beans":{"href":"http://localhost:8081/actuator/beans","templated":false

"caches-cache":{"href":"http://localhost:8081/actuator/caches/{cache}","templated":true

"caches":{"href":"http://localhost:8081/actuator/caches","templated":false

"health":{"href":"http://localhost:8081/actuator/health","templated":false

"health-component-instance":{"href":"http://localhost:8081/actuator/health/{component}/{instance}","templated":true

"health-component":{"href":"http://localhost:8081/actuator/health/{component}","templated":true

"conditions":{"href":"http://localhost:8081/actuator/conditions","templated":false

"configprops":{"href":"http://localhost:8081/actuator/configprops","templated":false

"env":{"href":"http://localhost:8081/actuator/env","templated":false

"env-toMatch":{"href":"http://localhost:8081/actuator/env/{toMatch}","templated":true

"info":{"href":"http://localhost:8081/actuator/info","templated":false

"loggers":{"href":"http://localhost:8081/actuator/loggers","templated":false

"loggers-name":{"href":"http://localhost:8081/actuator/loggers/{name}","templated":true

"heapdump":{"href":"http://localhost:8081/actuator/heapdump","templated":false

"threaddump":{"href":"http://localhost:8081/actuator/threaddump","templated":false

"metrics":{"href":"http://localhost:8081/actuator/metrics","templated":false

"metrics-requiredMetricName":{"href":"http://localhost:8081/actuator/metrics/{requiredMetricName}","templated":true

"scheduledtasks":{"href":"http://localhost:8081/actuator/scheduledtasks","templated":false

"httptrace":{"href":"http://localhost:8081/actuator/httptrace","templated":false

"mappings":{"href":"http://localhost:8081/actuator/mappings","templated":false

"refresh":{"href":"http://localhost:8081/actuator/refresh","templated":false

"features":{"href":"http://localhost:8081/actuator/features","templated":false

"service-registry":{"href":"http://localhost:8081/actuator/service-registry","templated":false}

}}

 再写一些业务代码,用于提供其它方法供调用:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.jjhgame.test.service.TestService;

@RefreshScope
@RestController
@RequestMapping("/base")
public class MessageRestController {

  private static Logger log = LoggerFactory.getLogger(MessageRestController.class);

  @Autowired
  private TestService testService;

  @Value("${message:Hello default}")
  private String message;

  @RequestMapping("/message")
  String getMessage() {
    String ret = this.message + ":" + System.currentTimeMillis();
    String result = testService.getResult();
    return result;
  }
}
           
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import com.jjhgame.test.web.MessageRestController;

@Service
public class TestService {

  private static Logger log = LoggerFactory.getLogger(MessageRestController.class);

  @Async("taskExecutor")
  public String getResult() {
    String name = "" + Thread.currentThread().getName();

    log.info("==============" + name);

    return name;
  }
}
           

每次调用由@RefreshScope注释的Bean时,Bean都会重新生成。如果启动时连接了Config Server,@RefreshScope范围中的message会在调用http://localhost:8081/actuator/refresh后,重新从Config Server中获取新的值。

三、测试客户端

Maven配置:

<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>TestCloud</groupId>
  <artifactId>TestCloud-Client</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>TestCloud-Client</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.3.RELEASE</version>
    <relativePath />
  </parent>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>Greenwich.SR1</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-config</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-starter-netflix-ribbon</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>

  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>
           

application.properties配置:

spring.application.name=TestClient

server.port=7001

server.serviceName=TestServer

eureka.client.register-with-eureka=false

eureka.client.fetch-registry=true

eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/

TestServer.ribbon.eureka.enabled=true

TestServer.ribbon.ServerListRefreshInterval=5

#TestServer.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.DynamicServerListLoadBalancer

#TestServer.ribbon.NIWSServerListClassName=com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList

eureka.client.register-with-eureka - false 表示不将自身注册成为service server。

eureka.client.fetch-registry - true表示从eureka server上获取服务列表。

eureka.client.serviceUrl.defaultZone - 设置eureka server的访问地址。

TestServer.ribbon.eureka.enabled - 表示是否对TestServer服务开启ribbon组件。

TestServer.ribbon.ServerListRefreshInterval - 表示对TestServer服务开启ribbon组件后,从eureka server上获取最新服务可访问地址的时间间隔。

下面分别以RibbonClient和FeignClient两种形式给出示例代码:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@EnableDiscoveryClient
@SpringBootApplication
@RestController
@RibbonClient(name = "${server.serviceName}", configuration = MyConfiguration.class)
public class ClientApp {

  @Value("${server.serviceName}")
  private String serviceName;

  @LoadBalanced
  @Bean
  RestTemplate restTemplate() {
    return new RestTemplate();
  }

  @Autowired
  RestTemplate restTemplate;

  @RequestMapping("/hi")
  public String hi() {
    String greeting = this.restTemplate.getForObject("http://" + serviceName + "/base/message", String.class);
    return String.format("%s!", greeting);
  }

  public static void main(String[] args) {
    SpringApplication.run(ClientApp.class, args);
  }
}
           
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@EnableDiscoveryClient
@SpringBootApplication
@RestController
@EnableFeignClients
@RibbonClient(name = "${server.serviceName}", configuration = MyConfiguration.class)
public class FeignClientApp {

  @Value("${server.serviceName}")
  private String serviceName;

  @Autowired
  private MessageClient client;

  @RequestMapping("/hihi")
  public String message() {
    return client.message();
  }

  @FeignClient(name = "${server.serviceName}")
  interface MessageClient {
    @RequestMapping(value = "/base/message", method = RequestMethod.GET)
    String message();
  }

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

}
           
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;

import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AvailabilityFilteringRule;
import com.netflix.loadbalancer.IPing;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.PingUrl;

public class MyConfiguration {

  @Autowired
  IClientConfig ribbonClientConfig;

  @Bean
  public IPing ribbonPing(IClientConfig config) {
    return new PingUrl(false, "/base/home");
  }

  @Bean
  public IRule ribbonRule(IClientConfig config) {
    return new AvailabilityFilteringRule();
  }
}
           

通过以下两个地址,就可以测试一下代码运行结果:

http://localhost:7001/hihi

http://localhost:7001/hi

另外,下表为Spring Cloud Netflix提供给Ribbon的默认值:

Bean Type Bean Name Class Name

IClientConfig

ribbonClientConfig

DefaultClientConfigImpl

IRule

ribbonRule

ZoneAvoidanceRule

IPing

ribbonPing

DummyPing

ServerList<Server>

ribbonServerList

ConfigurationBasedServerList

ServerListFilter<Server>

ribbonServerListFilter

ZonePreferenceServerListFilter

ILoadBalancer

ribbonLoadBalancer

ZoneAwareLoadBalancer

ServerListUpdater

ribbonServerListUpdater

PollingServerListUpdater

参考文档

Client Side Load Balancer: Ribbon

Spring Cloud Netflix Eureka - The Hidden Manual

Multi-version Service Discovery using Spring Cloud Netflix Eureka and Ribbon

Spring Cloud Series - Microservices Registration and Discovery using Spring Cloud, Eureka, Ribbon and Feign

Microservice Registration and Discovery with Spring Cloud and Netflix's Eureka

Client Side Load Balancing with Ribbon and Spring Cloud