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

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/netyeaxi/article/details/89045357

在构建微服务时,最常用的就是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>
  </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。

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.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);
  }

  @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);
  }
}

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;
  }
}

如果启动时连接了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

猜你喜欢

转载自blog.csdn.net/netyeaxi/article/details/89045357