ApplicationContext 发布事件报错-Caused by-java.lang.IllegalStateException-ApplicationEventMulticaster not initialized - call 'refresh' before multicasting events via the context-Root WebApplicationContext-startup date [Sat Dec 14 15:02:30 CST 2019];root of context hierarchy

2019-12-16   龙德   SpringMVC   ApplicationContext 事件  

报错信息如下:

Caused by: java.lang.IllegalStateException: ApplicationEventMulticaster not initialized - call 'refresh' before multicasting events via the context: Root WebApplicationContext: startup date [Sat Dec 14 15:02:30 CST 2019]; root of context hierarchy
	at org.springframework.context.support.AbstractApplicationContext.getApplicationEventMulticaster(AbstractApplicationContext.java:344)
	at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:331)
	at wang.miansen.roothub.common.dao.jdbc.spring.DataSourceConfiguration.afterPropertiesSet(DataSourceConfiguration.java:163)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1633)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1570)
	... 132 more

抛异常的地方:

可以看到 applicationEventMulticaster 为 null 就抛出了异常。

代码如下

package wang.miansen.roothub.common.dao.jdbc.spring;

public class DataSourceConfiguration implements FactoryBean<DataSource>, ApplicationContextAware, InitializingBean {

	private static final Logger logger = LoggerFactory.getLogger(DataSourceConfiguration.class);

	/**
	 * 数据库基本配置
	 */
	private DataSourceProperties dataSourceProperties;

	/**
	 * 数据源初始化器
	 */
	private DataSourceInitializer dataSourceInitializer;

	/**
	 * 数据源
	 */
	private DataSource dataSource;

	/**
	 * 上下文容器,主要的作用是防止重复初始化数据源。
	 */
	private ApplicationContext applicationContext;

	/**
	 * 创建数据源
	 * @param type
	 * @return
	 */
	@SuppressWarnings("unchecked")
	protected <T> T createDataSource(Class<? extends DataSource> type) {
		return (T) dataSourceProperties.initializeDataSourceBuilder().type(type).build();

	}

    /**
     * 最终注入的数据源
     */
	@Override
	public DataSource getObject() throws Exception {
		return null;
	}

	@Override
	public Class<DataSource> getObjectType() {
		return DataSource.class;
	}

	@Override
	public boolean isSingleton() {
		return true;
	}

	
	public void setDataSourceProperties(DataSourceProperties dataSourceProperties) {
		this.dataSourceProperties = dataSourceProperties;
	}

	public DataSourceInitializer getDataSourceInitializer() {
		if (this.dataSourceInitializer == null) {
			this.dataSourceInitializer = new DataSourceInitializer(this.dataSource, this.dataSourceProperties);
		}
		return this.dataSourceInitializer;
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		// 只存在父容器时才初始化数据源,防止重复初始化。
		if (this.applicationContext.getParent() == null) {
			DataSourceInitializer initializer = getDataSourceInitializer();
			this.applicationContext.publishEvent(new DataSourceSchemaCreatedEvent(initializer));
		}
	}

}

DataSourceConfiguration 类实现了 ApplicationContextAware 和 InitializingBean 接口,本来想在 afterPropertiesSet 方法里发布初始化数据源的事件,结果就报了上面那个错误。

分析一下原因:

在 afterPropertiesSet() 方法中发布事件时,ApplicationContext 的 applicationEventMulticaster 属性为 null,导致抛出异常

跟一下 ApplicationContext 初始化过程看看为什么 applicationEventMulticaster 会是 null。

Spring 中 AbstractApplicationContext 抽象类的 refresh() 方法是用来刷新 Spring 的应用上下文的,所以在 refresh() 方法处打个断点。

afterPropertiesSet() 方法也打上一个断点,然后启动项目。

进入了 refresh() 方法

image

其它的方法先不看,重点是 registerBeanPostProcessors(beanFactory) 方法。

这个方法是用来注册 BeanPostProcessor 的,需要在所有的 application bean 初始化之前调用,拦截 Bean 的创建。

进去看看

调用了一个静态方法,继续往下走。

这里是找出 BeanPostProcessor 接口的所有实现类。就不仔细看了,直接看这里:

可以看到这里调用了 beanFactory.getBean() 方法,触发了 bean 实例的创建。从而触发 setApplicationContext() 和 afterPropertiesSet() 方法。

但是初始化事件广播器的方法 initApplicationEventMulticaster() 是在 registerBeanPostProcessors() 方法之后。

所以导致在 afterPropertiesSet() 方法中发布事件时,ApplicationContext 的 applicationEventMulticaster 属性为 null,导致抛出异常。

解决方法:

既然执行 afterPropertiesSet() 方法时,ApplicationContext 的 applicationEventMulticaster 属性还没准备好,那么可以等 initApplicationEventMulticaster() 方法执行完之后才能发布事件了。

继续往下看,注册监听器的方法是 registerListeners();

那么可以换一种思路,当监听器注册完毕之后在发布事件,因为注册监听器的方法是在初始化事件广播器之后,可以保证在发布事件的时候事件广播器是可用的。

可以这么做,新建一个类,实现 BeanPostProcessor 和 ApplicationContextAware 接口,在 postProcessAfterInitialization() 方法里判断 bean 的类型是否为 DataSourceInitializerListener,如果是就发布事件,具体代码如下:

public class DataSourceInitializedPublisher implements BeanPostProcessor, ApplicationContextAware {

	/**
	 * 上下文容器,可以使用该对象来发布事件。
	 */
	private ApplicationContext applicationContext;

	/**
	 * 数据源
	 */
	private DataSource dataSource;

	/**
	 * 数据库基本配置
	 */
	private DataSourceProperties dataSourceProperties;

	/**
	 * 数据源初始化器
	 */
	private DataSourceInitializer dataSourceInitializer;
	
	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}

	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		// 当事件监听器初始化好之后发布初始化数据源的事件。
		if (bean instanceof DataSourceInitializerListener) {
			// 只存在父容器时才发布,防止重复初始化。
			if (this.applicationContext.getParent() == null) {
				DataSourceInitializer initializer = getDataSourceInitializer();
				this.applicationContext.publishEvent(new DataSourceSchemaCreatedEvent(initializer));
			}
		}
		return bean;
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}

	/**
	 * 实例化 DataSourceInitializer
	 * @return DataSourceInitializer
	 */
	public DataSourceInitializer getDataSourceInitializer() {
		if (this.dataSourceInitializer == null) {
			this.dataSourceInitializer = new DataSourceInitializer(this.dataSource, this.dataSourceProperties);
		}
		return this.dataSourceInitializer;
	}

	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}

	public void setDataSourceProperties(DataSourceProperties dataSourceProperties) {
		this.dataSourceProperties = dataSourceProperties;
	}

}

xml 配置:

<!-- 数据源初始化事件监听器 -->
<bean id="dataSourceInitializerListener" class="wang.miansen.roothub.common.dao.jdbc.spring.DataSourceInitializerListener" />
	
<!-- 发布初始化数据源事件 -->
<bean id="dataSourceInitializedPublisher" class="wang.miansen.roothub.common.dao.jdbc.spring.DataSourceInitializedPublisher" >
		<property name="dataSourceProperties" ref="dataSourceProperties" />
		<property name="dataSource" ref="dataSource" />
</bean>

八、SpringCloud-服务配置中心 Spring-Cloud-Config

2019-10-22   龙德   SpringCloud   SpringCloud Spring-Cloud-Config  

Spring Cloud Config 是用来为分布式系统中的基础设施和微服务应用提供集中化的外部配置支持,它分为服务端与客户端两个部分。其中服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置仓库并为客户端提供获取配置信息、加密 / 解密信息等访问接口;而客户端则是微服务架构中的各个微服务应用或基础设施,它们通过指定的配置中心来管理应用资源与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息。

服务端

新建一个子工程,命名为 spring-cloud-config-server

引入依赖

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

application.properties

# 指定服务名称
spring.application.name=spring-cloud-config-server
# 指定运行端口
server.port=8072

# 配置 Git 仓库地址
spring.cloud.config.server.git.uri=https://github.com/miansen/SpringCloud-Learn
# 配置文件所在的具体目录(也可以用通配符 /**,因为 SpringCloudConfig 只根据配置文件名找配置)
spring.cloud.config.server.git.searchPaths=config
# 配置仓库的分支(默认是master)
spring.cloud.config.label=master
# 配置 Git 仓库的用户名(如果 Git 仓库为公开仓库,可以不填写用户名和密码,如果是私有仓库需要填写)
spring.cloud.config.server.git.username=
# 访问 Git 仓库的用户密码
spring.cloud.config.server.git.password=

在远程 Git 仓库上新建几个配置文件,如图所示:

image

可以看到我新建了4个配置文件

  • application-dev.properties
  • application-dev.yml
  • application-prod.properties
  • application-prod.yml

这4个配置文件的命名都有一定的规律,可以概括为:{应用名} - {环境名} . {格式名}

为什么要这样的命名格式呢,随便命名不行吗?

其实这样命名是跟 http 请求地址和资源文件的映射有关系,SpringCloudConfig 会根据 http 请求地址去查找文件。

查找规则如下:

  • / {应用名} / {环境名} / {分支名}
  • / {应用名} - {环境名} . {格式名}
  • / {分支名} / {应用名} - {环境名} . {格式名}

接着启动工程

按照上面的查找规则,想要获取 application-dev.properties 配置文件,对应的访问地址如下:

  • http://localhost:8072/application/dev/master
  • http://localhost:8072/application-dev.properties
  • http://localhost:8072/master/application-dev.properties

image

可以看到返回了配置文件的内容,证明配置服务端可以从远程 Git 仓库获取到配置信息。

客户端

客户端就是各个微服务应用了,为了演示简单,我这里新建一个子工程,命名为 spring-cloud-config-client

pom.xml 引入下面的依赖

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

注意客户端引入的是 spring-cloud-starter-config

新建 bootstrap.properties 配置文件,boostrap 由父 ApplicationContext 加载,比 applicaton 优先加载

# 指定服务名称
spring.application.name=spring-cloud-config-client
# 指定运行端口
server.port=8073

# 服务配置中心的地址
spring.cloud.config.uri=http://localhost:8072/
# 指定配置文件的分支
spring.cloud.config.label=master
# 指定配置文件的环境
spring.cloud.config.profile=dev

启动类把从服务端获取到的配置输出,以便能直观的看到

@RestController
@SpringBootApplication
public class SpringCloudConfigClientApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringCloudConfigClientApplication.class, args);
	}
	
	@Value("${key}")
    private String key;

    @GetMapping("/key")
    public String getKey() {
        return key;
    }
}

接着启动客户端,访问 http://localhost:8073/key,输出内容如下:

image

以后客户端切换配置时只需要修改 spring.cloud.config.profile 的值即可

七、Spring Cloud-网关 Spring-Cloud-Gateway

2019-10-21   龙德   SpringCloud   Spring-Cloud-Gateway  

简介

Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。

Spring Cloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Netflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。

概念

  • Route(路由):Route 是网关的基础元素,由 ID、目标 URI、断言、过滤器组成。当请求到达网关时,由 Gateway Handler Mapping 通过断言进行路由匹配,当断言为真时,匹配到路由。
  • Predicate(断言):Predicate 是 Java 8 中提供的一个函数。允许开发人员匹配来自 HTTP 的请求,例如请求头或者请求参数。简单来说它就是匹配条件。
  • Filter(过滤器):Filter 是 Gateway 中的过滤器,可以在请求发出前后进行一些业务上的处理。

工作原理

image

当客户端请求到达 Spring Cloud Gateway 后,Gateway Handler Mapping 会将其拦截,根据 predicates 确定请求与哪个路由匹配。如果匹配成功,则会将请求发送至 Gateway web handler。Gateway web handler 处理请求会经过一系列 “pre” 类型的过滤器,然后执行代理请求。执行完之后再经过一系列的 “post” 类型的过滤器,最后返回给客户端。

快速开始

新建一个子工程,命名为 spring-cloud-gateway

引入依赖

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

application.properties 配置路由

spring.application.name=spring-cloud-gateway
server.port=8074

############ 定义了一个 router(注意是数组的形式) ############
# 路由 ID,保持唯一
spring.cloud.gateway.routes[0].id=my-gateway
# 目标服务地址
spring.cloud.gateway.routes[0].uri=http://httpbin.org
# 路由条件
spring.cloud.gateway.routes[0].predicates[0]=Path=/get

上面这段配置的意思是,配置了一个 id 为 my-gateway 的路由规则,当访问地址为 /get 时会自动转发到 http://httpbin.org/get

还可以通过代码的形式配置路由

/**
 * 通过代码的形式配置路由
 * @param builder
 * @return
 */
@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
	return builder.routes()
			.route(
					p -> p.path("/get").uri("http://httpbin.org")
					)
			.build();
}

application.propertise 配置路由和代码配置路由选择其中一个就好了,个人推荐 application.propertise 的形式配置。

启动服务,访问 http://localhost:8074/get

应该会输出以下内容:

{
  "args": {}, 
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", 
    "Accept-Encoding": "gzip, deflate, br", 
    "Accept-Language": "zh-CN,zh;q=0.9", 
    "Cache-Control": "max-age=0", 
    "Cookie": "Webstorm-2f8f75da=e0c5ee46-9276-490c-b32b-d5dc1483ca18; acw_tc=2760828015735472938194099e940a3c3ebc07316bcb1096abc6fefde61bf8; BD_UPN=12314353; H_PS_645EC=8749qnWwXCzugp%2FwPJDVeB7bqBisqx6VKFthj5OZOsWBAz1JPX2YkatsizA; BD_HOME=0", 
    "Forwarded": "proto=http;host=\"localhost:8074\";for=\"0:0:0:0:0:0:0:1:51881\"", 
    "Host": "httpbin.org", 
    "Sec-Fetch-Mode": "navigate", 
    "Sec-Fetch-Site": "none", 
    "Sec-Fetch-User": "?1", 
    "Upgrade-Insecure-Requests": "1", 
    "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36", 
    "X-Forwarded-Host": "localhost:8074"
  }, 
  "origin": "0:0:0:0:0:0:0:1, 183.14.135.71, ::1", 
  "url": "https://localhost:8074/get"
}

整合 Eureka

添加 Eureka Client 的依赖

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

配置基于 Eureka 的路由

spring.application.name=spring-cloud-gateway
server.port=8074

########### 配置注册中心 ###########
# 获取注册实例列表
eureka.client.fetch-registry=true
# 注册到 Eureka 的注册中心
eureka.client.register-with-eureka=true
# 配置注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8070/eureka

############ 定义了一个基于 Eureka 的 router(注意是数组的形式) ############
# 路由 ID,保持唯一
spring.cloud.gateway.routes[0].id=my-gateway
# 目标服务地址
spring.cloud.gateway.routes[0].uri=lb://spring-cloud-provider
# 路由条件
spring.cloud.gateway.routes[0].predicates[0]=Path=/user-service/**

uri 以 lb:// 开头(lb 代表从注册中心获取服务),后面接的就是你需要转发到的服务名称,这个服务名称必须跟 Eureka 中的对应,否则会找不到服务。

spring-cloud-provider 服务提供的接口如下:

@RestController
@RequestMapping("/user-service")
public class UserController {

	@Value("${spring.application.name}")
	private String applicationName;
	
	@Value("${server.port}")
	private String post;
	
	@GetMapping("/users/{name}")
	public String users(@PathVariable("name") String name) {
		return String.format("hello %s,from server %s,post: %s", name, applicationName, post);
	}
}

启动 spring-cloud-eureka-server(注册中心)、spring-cloud-provider 和 spring-cloud-gateway

访问 http://localhost:8074/user-service/users/zhangsan,输出如下:

image

配置默认路由

Spring Cloud Gateway 提供了类似于 Zuul 那种为所有服务转发的功能

配置如下:

spring.application.name=spring-cloud-gateway
server.port=8074

########### 配置注册中心 ###########
# 获取注册实例列表
eureka.client.fetch-registry=true
# 注册到 Eureka 的注册中心
eureka.client.register-with-eureka=true
# 配置注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8070/eureka

# 配置默认路由
spring.cloud.gateway.discovery.locator.enabled=true

开启之后我们需要通过地址去访问服务了,格式如下:

http://网关地址/服务名称(大写)/**

例如:http://localhost:8074/SPRING-CLOUD-PROVIDER/user-service/users/zhangsan

结果如图:

image

服务名称也可以配置成小写的格式,只需要增加一条配置即可:

# 配置服务名称小写
spring.cloud.gateway.discovery.locator.lowerCaseServiceId=true

路由断言工厂

官方提供了很多个常用的路由断言工厂,如图所示:

image

1. Path 路由断言工厂

Path 路由断言工厂接收一个参数,根据 Path 定义好的规则来判断访问的 URI 是否匹配

固定的 Path

# spring.cloud.gateway.routes[0].predicates[0]=Path=/users/zhangsan

带有前缀的 Path

# spring.cloud.gateway.routes[0].predicates[0]=Path=/users/{segment}

使用通配符的 Path

# spring.cloud.gateway.routes[0].predicates[0]=Path=/users/**

2. Query 路由断言工厂

Query 路由断言工厂接收两个参数,一个必需的参数和一个可选的正则表达式

# spring.cloud.gateway.routes[0].predicates[0]=Query=foo, ba.

如果请求包含 foo 查询参数,则此路由将匹配。bar 和 baz 也会匹配,因为第二个参数是正则表达式(注意 ba 后面有个 .)

测试链接:

http://localhost:8074/users/zhangsan?foo=ba

http://localhost:8074/users/zhangsan?foo=bar

http://localhost:8074/users/zhangsan?foo=baz

3. Method 路由断言工厂

Method 路由断言工厂接收一个参数,即要匹配的 HTTP 方法。

# spring.cloud.gateway.routes[0].predicates[0]=Method=GET

4. Header 路由断言工厂

Header 路由断言工厂接收两个参数,分别是请求头名称和正则表达式。

# spring.cloud.gateway.routes[0].predicates[0]=Header=X-Request-Id, \d+

如果请求中带有请求头名为 x-request-id,其值与 \d+ 正则表达式匹配(值为一个或多个数字),则此路由匹配。

具体的可以看一下官方文档,写的很清楚。

https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.0.0.RELEASE/single/spring-cloud-gateway.html

自定义路由断言工厂

自定义路由断言工厂需要继承 AbstractRoutePredicateFactory 类,重写 apply 方法的逻辑。

在 apply 方法中可以通过 exchange.getRequest() 拿到 ServerHttpRequest 对象,从而可以获取到请求的参数、请求方式、请求头等信息。

apply 方法的参数是自定义的配置类,也就是静态内部类 Config,在使用的时候配置参数,就可以在 apply 方法中直接获取使用。

我们自己写一个 Query 路由断言工厂吧,名字就叫 MyQueryRoutePredicateFactory(命名需要以 RoutePredicateFactory 结尾)

代码如下:

```java @Component public class MyQueryRoutePredicateFactory extends AbstractRoutePredicateFactory {

public MyQueryRoutePredicateFactory() {
	super(Config.class);
}

/**
 * 返回有关 args 数量和快捷方式分析顺序的提示。
 * <p>必须要重写这个方法,否则 Config 设置不了参数。
 */
@Override
public List<String> shortcutFieldOrder() {
	return Arrays.asList("param", "regexp");
}

六、SpringCloud-路由器和过滤器 Zuul

2019-10-12   龙德   SpringCloud   SpringCloud Zuul  

路由是微服务架构中的一部分,例如访问 / 映射到首页,访问 /user/** 映射到用户服务,访问 /order/** 映射到订单服务。Zuul 是 Netflix 的基于 JVM 的路由器和服务器端负载均衡器,默认和 Ribbon 结合实现了负载均衡的功能。

路由

新建一个工程,命名为 spring-cloud-zuul-server

pom.xml 引入下面的依赖

<!-- 引入 web 依赖 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 引入 eureka server 依赖 -->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!-- 引入 zuul 依赖 -->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

启动类添加 @EnableZuulProxy 注解

@EnableZuulProxy
@EnableEurekaClient
@SpringBootApplication
public class ZuulServerApplication {

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

}

application.properties

#指定服务名称
spring.application.name=spring-cloud-zuul-server
#指定运行端口
server.port=8070
#获取注册实例列表
eureka.client.fetch-registry=true
#注册到 Eureka 的注册中心
eureka.client.register-with-eureka=true
#配置注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8080/eureka

# 配置 feign 路由
zuul.routes.feign.path=/feign/**
zuul.routes.feign.serviceId=spring-cloud-consumer-feign-hystrix

# 配置 ribbon 路由
zuul.routes.ribbon.path=/ribbon/**
zuul.routes.ribbon.serviceId=spring-cloud-consumer-ribbon-hystrix

这里配置了两个路由,第一个路由的名字是 feign,访问地址是 /feign/**,映射的服务是 spring-cloud-consumer-feign-hystrix,第二个同理。

接着分别启动 spring-cloud-eureka-server、spring-cloud-provider、spring-cloud-consumer-feign-hystrix、spring-cloud-consumer-ribbon-hystrix、spring-cloud-zuul-server 这5个工程。

如图所示:

image

假如没有 Zuul,看看之前我们是怎么访问的。

访问 spring-cloud-consumer-feign-hystrix 服务,则需要输入相应的地址 http://localhost:8076/users/zhangsan

访问 spring-cloud-consumer-ribbon-hystrix 服务,则需要输入相应的地址 http://localhost:8077/users/zhangsan

有多少个服务,就要维护多少个调用方式。

如图所示:

image

现在有了 Zuul,就不需要一个个的去维护了,统一起来全部交给 Zuul 管理。

访问 spring-cloud-consumer-feign-hystrix 服务,只需要输入我们配置的地址 http://localhost:8070/feign/users/zhangsan

访问 spring-cloud-consumer-ribbon-hystrix 服务,只需要输入我们配置的地址 http://localhost:8070/ribbon/users/zhangsan

如图所示:

image

负载均衡

服务往往是集群部署的,如果一个服务有多个实例,Zuul 也可以做到负载均衡。

分别再开启一个 spring-cloud-consumer-feign-hystrix 和 spring-cloud-consumer-ribbon-hystrix 工程

image

如图所示,spring-cloud-consumer-feign-hystrix 服务现在有两个实例,端口是 8075 和 8076

spring-cloud-consumer-ribbon-hystrix 服务也有两个实例,端口是 8077 和 8078

只需要在配置文件里加入以下参数就可以开启 Zuul 的负载均衡功能

# 配置 feign 路由
zuul.routes.feign.path=/feign/**
zuul.routes.feign.serviceId=spring-cloud-consumer-feign-hystrix
spring-cloud-consumer-feign-hystrix.ribbon.listOfServers=http://localhost:8075,http://localhost:8076

# 配置 ribbon 路由
zuul.routes.ribbon.path=/ribbon/**
zuul.routes.ribbon.serviceId=spring-cloud-consumer-ribbon-hystrix
spring-cloud-consumer-ribbon-hystrix.ribbon.listOfServers=http://localhost:8077,http://localhost:8078

过滤

Zuul 不仅是路由器,而且也是过滤器,可以做到安全校验的功能。

继续改造 spring-cloud-zuul-server 工程,新建一个类 MyFilter

@Component
public class MyFilter extends ZuulFilter {

	/**
	 * 是否要过滤,可以写具体的逻辑进行判断。
	 * <p>我这里为 true,永远过滤。
	 */
	@Override
	public boolean shouldFilter() {
		return true;
	}

	/**
	 * 过滤的具体逻辑
	 */
	@Override
	public Object run() throws ZuulException {
		// 共享 RequestContext,上下文对象
		RequestContext ct = RequestContext.getCurrentContext();
		// 我这里只是简单的获取 token,然后判断是否为空
		String token = ct.getRequest().getParameter("token");
		if(token == null || "".equals(token)) {
			// 过滤该请求,不对其进行路由
			ct.setSendZuulResponse(false);
			// 返回错误代码
			ct.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
			// 返回错误信息
			ct.setResponseBody("token must be not null");
		}
		return null;
	}

	/**
	 * 返回一个字符串代表过滤器的类型,在zuul中定义了四种不同生命周期的过滤器类型,具体如下
	 * <p>pre:路由之前
	 * <p>routing:路由之时
	 * <p>post: 路由之后
	 * <p>error:发送错误调用
	 */
	@Override
	public String filterType() {
		return FilterConstants.PRE_TYPE;
	}

	/**
	 * 过滤的顺序, 越小越靠前
	 */
	@Override
	public int filterOrder() {
		return FilterConstants.SERVLET_DETECTION_FILTER_ORDER - 1;
	}

}

访问 http://localhost:8070/ribbon/users/zhangsan,这时 token 为空,则会过滤掉该请求。

image

访问 http://localhost:8070/ribbon/users/zhangsan?token=aaa,则正常

image

源码下载:https://github.com/miansen/SpringCloud-Learn

五、SpringCloud-服务容错 Hystrix(断路器)

2019-09-30   龙德   SpringCloud   SpringCloud Hystrix  

在微服务架构中,服务与服务之间相互调用,互相依赖。如果其中的一个服务发故障,有可能导致整个系统不能用。Hystrix 就是用来预防这种情况的。

在 Ribbon 中使用 Hystrix

spring-cloud-consumer-ribbon 工程复制一份,命名为 spring-cloud-consumer-ribbon-hystrix

引入 Hystrix 依赖

<!-- 引入 Hystrix 依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

application.properties 改一下服务名字就可以了

#指定服务名称
spring.application.name=spring-cloud-consumer-ribbon-hystrix
#指定运行端口
server.port=8077
#获取注册实例列表
eureka.client.fetch-registry=true
#注册到 Eureka 的注册中心
eureka.client.register-with-eureka=true
#配置注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8080/eureka

启动类改成 ConsumerRibbonHystrixApplication,并且加上注解 @EnableCircuitBreaker,开启断路器 Hystrix

为使示例简单,controller 是写在启动类里的。所以我们在启动类里新建一个 getUserFallback() 方法。

@GetMapping("/users/{name}")
@HystrixCommand(fallbackMethod = "getUserFallback")
public String getUser(@PathVariable("name") String name) {
	return restTemplate.getForObject("http://spring-cloud-provider/users/" + name, String.class);
}

public String getUserFallback(String name) {
	return "get " + name +" error";
}

getUser() 方法多了 @HystrixCommand 注解,它的作用是指定 Hystrix 在此方法超时时调用的方法。

可以在配置文件里指定超时时间

#自定义 Hystrix 的超时时间(毫秒)
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000

测试

写好之后测试一下

启动注册中心 spring-cloud-eureka-server 和服务提供者 spring-cloud-provider,紧接着启动现在的服务 spring-cloud-consumber-ribbon-hystrix

启动成功后访问 http://localhost:8077/users/zhangsan

image

可以看到一切正常

接着把服务提供者 spring-cloud-provider 停掉,然后再访问。如果没有引入 Hystrix 的话,我们的访问应该会报错。

但是引入 Hystrix 后,服务就有了容错机制,出现错误后会走到我们指定的方法。

image

在 Feign 中使用 Hystrix

spring-cloud-consumer-feign 工程复制一份,命名为 spring-cloud-consumer-feign-hystrix

Feign 是自带断路器的,不过需要在配置文件中配置打开它

#指定服务名称
spring.application.name=spring-cloud-consumer-feign-hystrix
#指定运行端口
server.port=8076
#获取注册实例列表
eureka.client.fetch-registry=true
#注册到 Eureka 的注册中心
eureka.client.register-with-eureka=true
#配置注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8080/eureka
#开启 Hystrix
feign.hystrix.enabled=true

注解 @FeignClient 加上 fallback 参数,指定由哪个类来处理错误。

@FeignClient(name = "spring-cloud-provider", configuration = FeignContractConfig.class, fallback = UserFeignClientHystrixFallback.class)
public interface UserFeignClient {

UserFeignClientHystrixFallback 类要实现 UserFeignClient 接口,并注入到 IOC 容器中。

@Component
public class UserFeignClientHystrixFallback implements UserFeignClient{

	@Override
	public String getUser(String name) {
		return "get " + name +" error";
	}

}

测试

启动 spring-cloud-consumer-feign-hystrix 工程,访问 http://localhost:8076/users/zhangsan,结果如图:

image

捕获异常

虽然我们已经解决了服务挂掉的问题,但是还不知道服务是咋挂掉的,要是能捕获异常就好了。

这点 Hystrix 也是支持的,看看下面的代码。

注解 @FeignClient 需要改变一下

@FeignClient(name = "spring-cloud-provider", configuration = FeignContractConfig.class, fallbackFactory = UserFeignClientHystrixFallbackFactory.class)

这次 fallbackFactory 属性指定的是 UserFeignClientHystrixFallbackFactory

UserFeignClientHystrixFallbackFactory 类要实现 FallbackFactory 接口并注入 IOC 容器。

@Component
public class UserFeignClientHystrixFallbackFactory implements FallbackFactory<UserFeignClient> {

	private static Logger log = LoggerFactory.getLogger(UserFeignClientHystrixFallbackFactory.class);
	
	@Override
	public UserFeignClient create(Throwable cause) {
		log.info("the server error is: {}", cause.getMessage());
		return new UserFeignClient() {
			@Override
			public String getUser(String name) {
				return "get " + name +" error";
			}
		};
	}
}

create() 这个工厂方法中,它的入参就是服务提供者的异常。

监控与仪表盘

Ribbon 工程的监控

在 spring-cloud-consumer-ribbon-hystrix 工程的启动类里注入一个 bean 来初始化监控的 Servlet

/**
 * 低版本直接启动即可使用 http://ip:port/hystrix.stream 查看监控信息
 * 高版本需要添加本方法方可使用 http://ip:port/{urlMappings} 查看监控信息
 * @return
 */
@Bean
public ServletRegistrationBean getServlet() {
	HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
    ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
    registrationBean.setLoadOnStartup(1);
    registrationBean.addUrlMappings("/actuator/hystrix.stream");
    registrationBean.setName("HystrixMetricsStreamServlet");
    return registrationBean;
}

启动 spring-cloud-consumer-ribbon-hystrix 工程

访问 http://localhost:8077/actuator/hystrix.stream

image

可以看到浏览器一直处于请求的状态,并且页面一直在打印 ping。

因为此时项目中注解了 @HystrixCommand 的方法还没有执行,因此也没有任何的监控数据。

访问 http://localhost:8077/users/zhangsan后,再次访问 http://localhost:8077/actuator/hystrix.stream,就可以看到监控数据了。

image

不过这些数据都是以文件形式展示的,很难一眼看出系统当前的运行状态。

Feign 工程的监控

引入一个新的依赖

<!-- 因为需要监控功能,所以引入 Hystrix 依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

启动类添加注解 @EnableCircuitBreaker,并且同样注入一个 bean 来初始化监控的 Servlet

启动后访问 http://localhost:8076/actuator/hystrix.stream 同样可以监控到数据。

仪表盘

上面的监控得到的数据都是以文件形式展示的,很难一眼看出系统当前的运行状态。

Hystrix 的主要优点之一是它收集关于每个 HystrixCommand 的一套指标,并通过 Hystrix 仪表盘有效的显示每个断路器的运行状况。

所以我们可以通过仪表盘来可视化的监控每个断路器的运行状况。

image

仪表盘长这个样子

新建一个子工程,命名为 spring-cloud-hystrix-dashboard

引入下面的依赖:

<!-- 引入 web 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 引入 eureka server 依赖-->
<dependency>
   	<groupId>org.springframework.cloud</groupId>
   	<artifactId>spring-cloud-starter-netflix-eureka-server</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-hystrix-dashboard</artifactId>
</dependency>

application.properties

#指定服务名称
spring.application.name=spring-cloud-hystrix-dashboard
#指定运行端口
server.port=8072
#获取注册实例列表
eureka.client.fetch-registry=true
#注册到 Eureka 的注册中心
eureka.client.register-with-eureka=true
#配置注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8080/eureka

然后启动类添加注解 @EnableHystrixDashboard,开启仪表盘

访问 http://localhost:8072/hystrix

可以看到这个界面

image

第一个输入框,是要监控的地址,第二个是轮训时间,第三个是仪表盘页面的标题

我们在这3个框里分别输入 http://localhost:8077/actuator/hystrix.stream,2000,RibbonDashboard,然后点击 Monitor Stream,就可以直观的监控到服务运行的状态了。

image

想要监控其他的服务只需要把监控地址换了就行。

Turbine 集群监控

上面的监控只是监控一个单体应用,但在实际应用中,程序往往是集群部署的,所以我们需要对集群进行监控,这时候可以采用 Turbine 进行集群监控。

改造 spring-cloud-hystrix-dashboard 工程

引入新的依赖

<!-- 引入 Turbine 依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-turbine</artifactId>
</dependency>

application.properties 新增几个参数

# 表示要聚合的服务,多个用逗号隔开
turbine.app-config=spring-cloud-consumer-ribbon-hystrix,spring-cloud-consumer-feign-hystrix
# 表示集群的名字为default 
turbine.clusterNameExpression= "default"
# 同一主机上的服务通过host和port的组合来进行区分
turbine.combine-host-port=true

启动类新增注解 @EnableTurbine,开启集群监控面板。

启动注册中心 spring-cloud-eureka-server

启动服务 spring-cloud-consumer-ribbon-hystrix,且这个服务启动两个实例

启动集群监控 spring-cloud-hystrix-dashboard

访问集群监控的地址 http://localhost:8072/hystrix

image

分别输入 http://localhost:8072/turbine.stream,2000,Turbine,点击 Monitor Stream

就可以看到集群监控的页面了

image

源码下载:https://github.com/miansen/SpringCloud-Learn

四、SpringCloud-高可用的服务注册中心

2019-09-27   龙德   SpringCloud   SpringCloud  

前面三篇写了服务注册中心和服务之间的调用方式。到目前为止所有的服务都是注册到一台注册中心,万一这台注册中心挂了,那么所有的服务不是都得挂了。

为了解决这个问题,我们可以搭建一个注册中心集群。

准备环境

集群意味着要有多台服务器,虽然可以通过改 host 文件的形式来搭建集群。不过我并不推荐这种形式,因为实际中不可能去改 host 文件的。所以我准备了3台虚拟机,地址分别是 192.168.8.4、192.168.8.6、192.168.8.8

搭建集群

首先把 spring-cloud-eureka-server 工程复制一份,命名为 spring-cloud-eureka-server-hign,将启动类的名字修改为 EurekaServerHignApplication

(1)新建 application-dev1.properties 配置文件

#指定服务名称
spring.application.name=spring-cloud-eureka-server-hign
#指定运行端口
server.port=8080
#指定主机地址
eureka.instance.hostname=192.168.8.4
#应用程序使用IP地址的方式向Eureka注册
eureka.instance.prefer-ip-address=true
#指定是否要从注册中心获取服务(注册中心不需要开启)
eureka.client.fetch-registry=false
#指定是否要注册到注册中心(注册中心不需要开启)
eureka.client.register-with-eureka=false
#关闭保护模式
eureka.server.enable-self-preservation=false
#配置注册中心地址
eureka.client.service-url.defaultZone=http://192.168.8.6:8081/eureka,http://192.168.8.8:8082/eureka

application-dev1.properties 指定了注册中心的IP和端口 192.168.8.4:8080。它同时也向 192.168.8.6:8081192.168.8.8:8082 这两个注册中心注册。

同理可得

application-dev2.properties 配置文件

#指定服务名称
spring.application.name=spring-cloud-eureka-server-hign
#指定运行端口
server.port=8081
#指定主机地址
eureka.instance.hostname=192.168.8.6
#应用程序使用IP地址的方式向Eureka注册
eureka.instance.prefer-ip-address=true
#指定是否要从注册中心获取服务(注册中心不需要开启)
eureka.client.fetch-registry=false
#指定是否要注册到注册中心(注册中心不需要开启)
eureka.client.register-with-eureka=false
#关闭保护模式
eureka.server.enable-self-preservation=false
#配置注册中心地址
eureka.client.service-url.defaultZone=http://192.168.8.4:8080/eureka,http://192.168.8.8:8082/eureka

application-dev3.properties 配置文件

#指定服务名称
spring.application.name=spring-cloud-eureka-server-hign
#指定运行端口
server.port=8082
#指定主机地址
eureka.instance.hostname=192.168.8.8
#应用程序使用IP地址的方式向Eureka注册
eureka.instance.prefer-ip-address=true
#指定是否要从注册中心获取服务(注册中心不需要开启)
eureka.client.fetch-registry=false
#指定是否要注册到注册中心(注册中心不需要开启)
eureka.client.register-with-eureka=false
#关闭保护模式
eureka.server.enable-self-preservation=false
#配置注册中心地址
eureka.client.service-url.defaultZone=http://192.168.8.4:8080/eureka,http://192.168.8.6:8081/eureka

image

(2)将 spring-cloud-eureka-server-hign 工程打成 jar 包,每台虚拟机上都部署一份

(3)启动程序

第一台虚拟机 192.168.8.4

java -jar spring-cloud-eureka-server-hign-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev1

第二台虚拟机 192.168.8.6

java -jar spring-cloud-eureka-server-hign-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev2

第三台虚拟机 192.168.8.8

java -jar spring-cloud-eureka-server-hign-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev3

(6)分别访问,如图:

image

image

image

使用集群

还记得之前我们是如何注册的吗?没有集群的时候是这样写的

#指定服务名称
spring.application.name=spring-cloud-provider
#指定运行端口
server.port=8078
#获取注册实例列表
eureka.client.fetch-registry=true
#注册到 Eureka 的注册中心
eureka.client.register-with-eureka=true
#配置注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8080/eureka

现在有了集群,就不能这么写,否则集群就没有意义了。

可以这么写:

#指定服务名称
spring.application.name=spring-cloud-provider
#指定运行端口
server.port=8078
#指定主机地址
eureka.instance.hostname=192.168.8.4
#应用程序使用IP地址的方式向Eureka注册
eureka.instance.prefer-ip-address=true
#获取注册实例列表
eureka.client.fetch-registry=true
#注册到 Eureka 的注册中心
eureka.client.register-with-eureka=true
#配置注册中心地址
eureka.client.service-url.defaultZone=http://192.168.8.4:8080/eureka,http://192.168.8.6:8081/eureka,http://192.168.8.8:8082/eureka

同理我们将 spring-cloud-provider 工程复制一份,命名为 spring-cloud-provider-hign,新建 application-dev1.propertiesapplication-dev2.propertiesapplication-dev3.properties 三个配置文件

spring-cloud-provider 工程打成 jar 包,每台虚拟机上都部署一份,然后分别启动工程。

访问如图:

image

源码下载:https://github.com/miansen/SpringCloud-Learn

四、Spring Cloud-接口调用 Feign

2019-09-26   龙德   SpringCloud   Feign  

先看一下 SpringCloud 官网对它的定义

Feign 是一个声明式的 Web 服务客户端。它支持 Feign 本身的注解、JAX-RS 注解以及 SpringMVC 的注解。 SpringCloud 集成 Ribbon 和 Eureka 以在使用 Feign 时提供负载均衡的 http 客户端。

在 Spring Cloud 中使用 Feign

新建一个子工程,命名为 spring-cloud-consumer-feign

引入 Feign 依赖

<!-- 引入 Feign 依赖-->
<dependency>
  	<groupId>org.springframework.cloud</groupId>
  	<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

application.properties

#指定服务名称
spring.application.name=spring-cloud-consumer-feign
#指定运行端口
server.port=8074
#获取注册实例列表
eureka.client.fetch-registry=true
#注册到 Eureka 的注册中心
eureka.client.register-with-eureka=true
#配置注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8080/eureka

启动类添加 @EnableFeignClients 注解,表明这是一个 Feign 客户端。

@EnableFeignClients
@EnableEurekaClient
@RestController
@SpringBootApplication
public class ConsumerFeignApplication {

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

新建一个 UserFeignClient 接口

@FeignClient(name = "spring-cloud-provider")
public interface UserFeignClient {

	@GetMapping("/users/{name}")
	public String getUser(@PathVariable("name") String name);
	
}

注解 @FeignClient(name = “spring-cloud-provider”) 指定了调用哪个服务。

新建一个 UserController 类,并注入 UserFeignClient,就像调用本地方法一样调用远程接口。

@RestController
public class UserController {

	@Autowired
	private UserFeignClient userFeignClient;
	
	@GetMapping("/users/{name}")
	public String getUser(@PathVariable("name") String name) {
		return userFeignClient.getUser(name);
	}
}

启动 spring-cloud-eureka-server、spring-cloud-provider 和 spring-cloud-consumer-feign,访问 http://localhost:8074/users/zhangsan 同样能取得数据。

继承特性

从上面的例子可以看到,UserFeignClient 和 UserController 其实是声明与实现的关系,Feign 是通过接口的形式调用的,利用 Feign 的继承特性,可以把服务的接口单独抽出来,作为公共的依赖,以方便使用。

定义公共的 API 接口

新建一个子工程,命名为 spring-cloud-feign-api

因为要用到 MVC 的注解,所以引入 Feign 依赖,当然引入 web 依赖也可以。

<!-- 引入 Feign 依赖-->
<dependency>
  	<groupId>org.springframework.cloud</groupId>
  	<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

定义一个接口,我这里以 api 开头,以便区分。

public interface UserApiService {
	
	@GetMapping("/api/users/{name}")
	public String getApiUser(@PathVariable("name") String name);
}

在实际项目中,这个工程应该要打成 jar 包的形式供其他工程使用的,这里我就不成 jar 包了,直接引用。

服务提供者实现接口

spring-cloud-provider 工程引入 spring-cloud-feign-api

<dependency>
	<groupId>com.example</groupId>
	<artifactId>spring-cloud-feign-api</artifactId>
	<version>0.0.1-SNAPSHOT</version>
</dependency>

新建一个 Controller,实现 UserApiService 接口

@RestController
public class UserApiController implements UserApiService {

	@Override
	public String getUserApi(String name) {
		return name + ", from user api";
	}

}

不同的是这里不需要在方法上面添加 @GetMapping 注解,这些注解在父接口中都有,不过在 Controller 上还是要添加 @RestController 注解。

服务消费者继承接口

spring-cloud-consumer-feign 同样引入 spring-cloud-feign-api

然后 UserFeignClient 继承 UserApiService

@FeignClient(name = "spring-cloud-provider")
public interface UserFeignClient extends UserApiService {
	
	@GetMapping("/users/{name}")
	public String getUser(@PathVariable("name") String name);
	
}

controller

@RestController
public class UserController {

	@Autowired
	private UserFeignClient userFeignClient;
	
	@GetMapping("/users/{name}")
	public String getUser(@PathVariable("name") String name) {
		return userFeignClient.getUser(name);
	}
	
	@GetMapping("/api/users/{name}")
	public String getApiUser(@PathVariable("name") String name) {
		return userFeignClient.getUserApi(name);
	}
}

访问结果

image

修改 Feign 的默认配置

修改 Feign 的默认配置也存在包扫描的问题,跟修改 Ribbon 的策略一样,我们使用注解的方式忽略扫描的类。

新建一个 config 包,新建类 FeignContract。

@ExcludeFromComponentScan
@Configuration
public class FeignContractConfig {

	@Bean
	public Contract feignContract() {
		return new Contract.Default();
	}
}

在 UserFeignClient 类中的注解 @FeignClient 指定 configuration 参数。

@FeignClient(name = "spring-cloud-provider", configuration = FeignContractConfig.class)

启动类指定包扫描忽略使用 @ExcludeFromComponentScan 注解的类

//忽略使用  @ExcludeFromComponentScan 注解的类
@ComponentScan(excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = ExcludeFromComponentScan.class)})
@EnableFeignClients
@EnableEurekaClient
@RestController
@SpringBootApplication
public class ConsumerFeignApplication {

我们在 FeignContractConfig 类中修改了 Feign 的 Contract ,Contract 是一个契约的概念。 Feign 默认的契约是 SpringMVC,所以我们在 UserFeignClient 类中使用的是 SpringMVC 的注解。现在 Contract.Default() 使用的契约是 Feign 自己的,也就是说我们要把 SpringMVC 的注解修改为 Feign 的注解,否则项目启动不了。

@FeignClient(name = "spring-cloud-provider", configuration = FeignContractConfig.class)
public interface UserFeignClient extends UserApiService {
	
	// SpringMVC 版本
	// @GetMapping("/users/{name}")
	// public String getUser(@PathVariable("name") String name);
	
	// Feign 版本
	@RequestLine("GET /users/{name}")
	public String getUser(@Param ("name") String name);
	
}

源码下载:https://github.com/miansen/SpringCloud-Learn

三、Spring Cloud-负载均衡 Ribbon

2019-09-25   龙德   SpringCloud   Ribbon  

Ribbon 简介

目前主流的负载方案分为以下两种:

  • 服务端负载均衡(Nginx)

服务实例的清单在服务端,服务器进行负载均衡算法分配

  • 客户端负载均衡(Ribbon)

服务实例的清单在客户端,客户端进行负载均衡算法分配,Ribbon 就属于客户端自己做负载。

单独使用 Ribbon

我们使用 Ribbon 来实现一个最简单的负载均衡调用功能,接口就用 spring-cloud-provider 工程提供的 /users/{name} 接口,不过需要改动一下 controller 层

@RestController
public class UserController {

	@Value("${spring.application.name}")
	private String applicationName;
	
	@Value("${server.port}")
	private String post;
	
	@GetMapping("/users/{name}")
	public String users(@PathVariable("name") String name) {
		return String.format("hello %s,from server %s,post: %s", name, applicationName, post);
	}
}

我们把服务名和端口也返回给客户端,目的是能更直观的看到负载均衡的效果。

因为要测试负载均衡,所有需要启动多个服务。这里我启动两个 spring-cloud-provider 服务,端口分别是 8071 和 8072。

接着新建一个 Maven 项目,命名为 ribbon-demo

pom.xml 引入以下依赖

<dependency>
	<groupId>com.netflix.ribbon</groupId>
	<artifactId>ribbon</artifactId>
	<version>2.2.2</version>
</dependency>
<dependency>
	<groupId>com.netflix.ribbon</groupId>
	<artifactId>ribbon-core</artifactId>
	<version>2.2.2</version>
</dependency>
<dependency>
	<groupId>com.netflix.ribbon</groupId>
	<artifactId>ribbon-loadbalancer</artifactId>
	<version>2.2.2</version>
</dependency>

随便新建一个测试类,在 main 方法里调用接口

public static void main(String[] args) {
		// 服务列表
		List<Server> serverList = Lists.newArrayList(new Server("localhost", 8071), new Server("localhost", 8072));

		// 构建负载均衡器的实例
		BaseLoadBalancer loadBalancer = LoadBalancerBuilder.newBuilder().buildFixedServerListLoadBalancer(serverList);
		for (int i = 0; i < 5; i++) {
			// 构建执行负载均衡器命令的实例,用于中转请求到负载均衡器
			LoadBalancerCommand<String> loadBalancerCommand = LoadBalancerCommand.<String>builder()
					.withLoadBalancer(loadBalancer).build();
			Observable<String> observable = loadBalancerCommand.submit(new ServerOperation<String>() {

				@Override
				public Observable<String> call(Server server) {
					try {
						String addr = String.format("http://%s/users/zhangsan", server.getHostPort());
						System.out.println("调用地址:" + addr);
						URL url = new URL(addr);
						HttpURLConnection conn = (HttpURLConnection) url.openConnection();
						conn.setRequestMethod("GET");
						conn.connect();
						InputStream in = conn.getInputStream();
						byte[] data = new byte[in.available()];
						in.read(data);
						return Observable.just(new String(data));
					} catch (Exception e) {
						return Observable.error(e);
					}
				}
			});
			String result = observable.toBlocking().first();
			System.out.println("调用结果:" + result);
		}
	}

控制台输出如下

image

从输出的结果中可以看到,负载均衡起作用了,8071 调用了 2 次,8072 调用了 3 次。

在 Spring Cloud 中使用 Ribbon

Spring Cloud Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,它基于 Netflix Ribbon 实现。通过 Spring Cloud 的封装,可以让我们轻松地将面向服务的 REST 模版请求自动转换成客户端负载均衡的服务调用。 Spring Cloud Ribbon 虽然只是一个工具类框架,它不像服务注册中心、配置中心、API 网关那样需要独立部署,但是它几乎存在于每一个 Spring Cloud 构建的微服务和基础设施中。因为微服务间的调用,API 网关的请求转发等内容,实际上都是通过 Ribbon 来实现的。

在 Spring Cloud 中使用 Ribbon 也挺简单,因为 Spring Cloud 已经帮我们封装好了。甚至都不需要引入依赖,因为 Eureka 中已经引用了 Ribbon。

还记得我们之前在 spring-cloud-consumer 工程里注入了 RestTemplate 吗?

@Configuration
public class BeanConfiguration {

	@Bean
	@LoadBalanced
	public RestTemplate getRestTemplate() {
		return new RestTemplate();
	}
}

其实这就已经实现了负载均衡的效果了,RestTemplate 加了一个 @LoadBalanced 注解之后,就可以结合 Eureka 来动态发现服务并进行负载均衡的调用。

我们再启动服务注册中心 spring-cloud-eureka-server 和服务消费者 spring-cloud-consumer 验证一下。

启动成功后,访问服务消费者 http://localhost:8073/users/zhangsan,每次访问都会轮流的调用不同端口的消费提供者(注意看端口的变化)。

image

自定义负载均衡策略

上面的例子采用的是默认的负载均衡策略,也就是轮训的方式。我们也可以自定义负载均衡的策略。

自定义负载均衡策略有两种方式

  • 第一种方式

通过实现 IRule 接口自定义负载均衡策略,主要的选择服务逻辑在 choose 方法中。因为我们这里只是演示怎么自定义负载策略,所以没写选择的逻辑,而是直接返回服务列表中第一个服务。具体代码如下所示:

public class MyRule implements IRule {

	private ILoadBalancer lb;

	@Override
	public Server choose(Object key) {
		List<Server> servers = lb.getAllServers();
		return servers.get(0);
	}

	@Override
	public void setLoadBalancer(ILoadBalancer lb) {
		this.lb = lb;
	}

	@Override
	public ILoadBalancer getLoadBalancer() {
		return lb;
	}

}

在 Spring Cloud 中,可通过配置的方式使用自定义的负载均衡策略。在 application.propertise 里加入以下配置:

# 自定义的负载策略
spring-cloud-provider.ribbon.NFLoadBalancerRuleClassName=org.spring.cloud.consumer.config.MyRule

注意 spring-cloud-provider 是调用的服务名称。

也可以通过注解的方式使用自定义的负载均衡策略。

启动类上新增 @RibbonClient 注解

@RibbonClient(name = "spring-cloud-provider", configuration = MyRule.class)

这里的 spring-cloud-provider 同样也是调用的服务名称。

  • 第二种方式

通过继承 AbstractLoadBalancerRule 抽象类实现负载均衡的策略,主要的选择服务逻辑也是在 choose 方法中。具体的代码如下所示:

public class MyRule extends AbstractLoadBalancerRule {

	@Override
	public Server choose(Object key) {
		List<Server> servers = getLoadBalancer().getAllServers();
		return servers.get(0);
	}

	@Override
	public void initWithNiwsConfig(IClientConfig clientConfig) {
		// TODO Auto-generated method stub
		
	}
}

同样的,可以通过配置的方式或者是注解的方式使用自定义的负载均衡策略。

重启后再次访问服务消费者 http://localhost:8073/users/zhangsan,这时候端口应该不会发生变化,因为我们每次访问的都是服务列表中的第一个服务。

源码下载:https://github.com/miansen/SpringCloud-Learn

二、Spring Cloud-服务注册中心 Eureka

2019-09-24   龙德   SpringCloud   SpringCloud  

在上一篇 SpringCloud-服务提供者与服务消费者 中,服务提供者和服务消费者其实是两个独立的应用,并且服务调用的地址是硬编码在代码中的。我们需要一个注册中心(Eureka)来调度各个服务,并且监控各个服务的健康状态。

Spring Cloud Eureka 是 Spring Cloud Netflix 微服务套件的一部分,基于 Netflix Eureka 做了二次封装,主要负责实现微服务架构中的服务治理功能。

创建服务注册中心

新建一个子工程 spring-cloud-eureka-server,作为服务注册中心。

pom.xml

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

	<parent>
		<groupId>com.example</groupId>
		<artifactId>spring-cloud-pom</artifactId>
		<version>0.0.1-SNAPSHOT</version>
	</parent>

	<artifactId>spring-cloud-eureka-server</artifactId>
	<name>spring-cloud-eureka-server</name>
	<url>http://maven.apache.org</url>

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

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

application.properties

# 服务名称
spring.application.name=spring-cloud-eureka-server
# 运行端口
server.port=8070
# 是否要从注册中心获取服务(注册中心不需要开启)
eureka.client.fetch-registry=false
# 是否要注册到注册中心(注册中心不需要开启)
eureka.client.register-with-eureka=false
# 关闭保护模式
eureka.server.enable-self-preservation=false

启动类

@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {

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

启动类多了一个 @EnableEurekaServer 注解,表示开启 Eureka Server。

启动工程,访问 http://localhost:8070,便会看到 Eureka 提供的 Web 控制台。

界面如下:

image

因为没有注册服务所以提示 “No instances available”。

创建服务提供者

改造原先的 spring-cloud-provider 工程

pom.xml 添加 Eureka Client 依赖

<!-- 引入 eureka client 依赖-->
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

修改 application.properties 文件,添加以下配置:

# 指定服务名称
spring.application.name=spring-cloud-provider
# 指定运行端口
server.port=8071
# 获取注册实例列表
eureka.client.fetch-registry=true
# 注册到 Eureka 的注册中心
eureka.client.register-with-eureka=true
# 配置注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8070/eureka

启动类

@EnableEurekaClient
@SpringBootApplication
public class ProviderApplication {

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

启动类多了一个 @EnableEurekaClient 注解,表示将此服务注册到 Eureka Server。

接着启动 spring-cloud-provider 工程,刷新 http://localhost:8070,可以看到 spring-cloud-provider 服务注册到 Eureka 了。

image

一个服务可以有多个实例,我们修改 spring-cloud-provider 工程的 application.properties 文件,只需要修改端口号就行了。

比如我修改成 8072,然后再启动。(如果你是用 IDEA,需要点击 Edit Configuration,将默认的 Single instance only (单实例) 的钩去掉才能启动)

启动成功后刷新 http://localhost:8070,可以看到 spring-cloud-provider 服务有两个实例(相当于一个小集群)。

image

创建服务消费者

改造原先的 spring-cloud-consumer 工程

pom.xml 添加 Eureka Client 依赖

<!-- 引入 eureka client 依赖-->
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

修改 application.properties 文件,添加以下配置:

# 指定服务名称
spring.application.name=spring-cloud-consumer
# 指定运行端口
server.port=8073
# 获取注册实例列表
eureka.client.fetch-registry=true
# 注册到 Eureka 的注册中心
eureka.client.register-with-eureka=true
# 配置注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8070/eureka

启动类

@EnableEurekaClient
@SpringBootApplication
public class ConsumerApplication {

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

之前是通过服务接口的具体地址来调用的,既然用了注册中心,那么客户端调用的时候肯定是不需要关心有多少个服务提供接口,下面我们来改造之前的调用代码。

首先改造 RestTemplate 的配置,添加一个 @LoadBalanced 注解,这个注解会自动构造 LoadBalancerClient 接口的实现类并注册到 Spring 容器中,代码如下所示。

@Configuration
public class BeanConfiguration {

	@Bean
	@LoadBalanced
	public RestTemplate getRestTemplate() {
		return new RestTemplate();
	}
}

接下来就是改造调用代码,我们不再直接写固定地址,而是写成服务的名称,这个名称就是我们注册到 Eureka 中的名称,是属性文件中的 spring.application.name,相关代码如下所示。

@RestController
public class UserController {

	@Autowired
	private RestTemplate restTemplate;
	
	@GetMapping("/users/{name}")
	public String getUser(@PathVariable("name") String name) {
		return restTemplate.getForObject("http://spring-cloud-provider/users/" + name, String.class);
	}
}

在 Eureka 中,一个工程的 spring.application.name 的属性值对应着一个服务的 ID,Eureka 可以根据这个 ID 去调度服务,我们无需关心具体的 IP 和端口,只要知道服务提供者的名字就可以调用了。

启动 spring-cloud-consumer 工程,刷新 http://localhost:8070,可以看到服务消费者也注册到 Eureka 了。

image

访问服务消费者 http://localhost:8073/users/zhangsan ,如果返回 “hello zhangsan” 字符串,说明我们调用成功了。

源码下载:https://github.com/miansen/SpringCloud-Learn

一、Spring Cloud-服务提供者与服务消费者

2019-09-20   龙德   SpringCloud   SpringCloud  

本篇以及后面的系列文章都是使用以下环境:

  • JDK 版本:1.8

  • IDA:Eclipse 4.6.0

  • Maven 版本:3.5.0

  • SpringBoot 版本:2.1.8.RELEASE

  • SpringCloud 版本:Greenwich.RELEASE

父工程

创建一个父工程,命名为 spring-cloud-pom, 所有的子工程都继承这个父工程,用以管理子工程的版本依赖。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	
	<!-- Spring Boot -->
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.8.RELEASE</version>
		<relativePath/>
	</parent>
	
	<groupId>com.example</groupId>
	<artifactId>spring-cloud-pom</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>pom</packaging>
	<name>spring-cloud-pom</name>

	<properties>
		<java.version>1.8</java.version>
		<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
	</properties>
	
	<!-- Spring Cloud -->
	<dependencyManagement>
	   <dependencies>
	       <dependency>
	           <groupId>org.springframework.cloud</groupId>
	           <artifactId>spring-cloud-dependencies</artifactId>
	           <version>${spring-cloud.version}</version>
	           <type>pom</type>
	           <scope>import</scope>
	       </dependency>
	   </dependencies>
	</dependencyManagement>

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

父工程只留一个 pom.xml 文件就可以了,其他的文件目录都可以不要。

服务提供者

在父工程下新建一个子工程 spring-cloud-provider,作为服务提供者。

pom.xml

<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <modelVersion>4.0.0</modelVersion>
  
  <parent>
    <groupId>com.example</groupId>
    <artifactId>spring-cloud-pom</artifactId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  
  <artifactId>spring-cloud-provider</artifactId>
  <name>spring-cloud-provider</name>
  <url>http://maven.apache.org</url>
  
  <dependencies>
  	<!-- 引入 web 依赖 -->
  	<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
   </dependency>
  </dependencies>
</project>

application.properties

# 服务名字
spring.application.name=spring-cloud-provider
# 在 8071 端口启动
server.port=8071

controller

创建一个 Controller,提供一个接口给其他服务查询,简单的输出一些信息即可。

@RestController
public class UserController {

	@GetMapping("/users/{name}")
	public String users(@PathVariable("name") String name) {
		return "hello " + name;
	}
}

启动服务,访问 http://localhost:8071/user/zhangsan ,如果能看到我们返回的 “hello zhangsan” 字符串,就证明接口提供成功了。

服务消费者

在父工程下新建一个子工程 spring-cloud-consumer,作为服务消费者。

pom.xml

<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <modelVersion>4.0.0</modelVersion>
  
  <parent>
    <groupId>com.example</groupId>
    <artifactId>spring-cloud-pom</artifactId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  
  <artifactId>spring-cloud-consumer</artifactId>
  <name>spring-cloud-consumer</name>
  <url>http://maven.apache.org</url>
  
  <dependencies>
  	<!-- 引入 web 依赖 -->
  	<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
   </dependency>
  </dependencies>
</project>

application.properties

# 服务名字
spring.application.name=spring-cloud-consumer
# 在 8072 端口启动
server.port=8072

RestTemplate

RestTemplate 是 Spring 提供的用于访问 Rest 服务的客户端,RestTemplate 提供了多种便捷访问远程 Http 服务的方法,能够大大提高客户端的编写效率。我们通过配置 RestTemplate 来调用接口。

@Configuration
public class BeanConfiguration {

	@Bean
	public RestTemplate getRestTemplate() {
		return new RestTemplate();
	}
}

controller

创建 controller,利用 RestTemplate 访问服务提供者的接口。

@RestController
public class UserController {

	@Autowired
	private RestTemplate restTemplate;
	
	@GetMapping("/users/{name}")
	public String getUser(@PathVariable("name") String name) {
		return restTemplate.getForObject("http://localhost:8071/users/" + name, String.class);
	}
}

服务消费者的 getUser() 方法没有自己实现,而是调用服务提供者的接口。

启动服务消费者,访问 http://localhost:8072/users/zhangsan ,如果返回 “hello zhangsan” 字符串就证明调用成功了。

这样一个简单的伪微服务项目的服务提供者和消费者就已经完成了。

之所以说是伪微服务,是因为服务提供者和服务消费者其实是两个独立的应用,它们之间只是简单的通过 http 的方式进行资源访问和操作。而真正微服务架构不仅仅是将单体应用划分为小型的服务单元这么简单,还需要一套的基础组件来管理各个服务,如服务注册、服务发现、配置中心、消息总线、负载均衡、断路器、数据监控等。Spring Cloud 就是这一系列微服务基础组件框架的集合,利用 Spring Boot 的开发便利性,巧妙地简化了微服务系统基础设施的开发。

通俗地讲,Spring Cloud 就是用于构建微服务开发和治理的框架集合(它并不是具体的一个框架,而是框架集合)

Spring Cloud 提供的微服务基础组件有:

  • Eureka:服务注册中心,用于服务管理
  • Ribbon:基于客户端的负载均衡组件
  • Hystrix:容错框架,能够防止服务的雪崩效应
  • Feign:Web 服务客户端,能够简化 HTTP 接口的调用
  • Zuul:API 网关,提供路由转发、请求过滤等功能
  • Config:分布式配置管理
  • Sleuth:服务跟踪
  • Stream:构建消息驱动的微服务应用程序的框架
  • Bus:消息代理的集群消息总线

所以接下来,我们利用 Spring Cloud 提供的组件,一步步的构建出一个真正的微服务系统(Demo)。

源码下载:https://github.com/miansen/SpringCloud-Learn