SpringCloud简单工程样例以及一些思考
1. 项目创建
先创建一个项目,删除无用的文件夹(比如src),剩下pom文件。
创建两个Module,一个作为消费者,一个作为生产者。
在父项目中的pom.xml中设置打包类型:
<packaging>pom</packaging>
表示则此模块是一个标准的Maven工程模块,只用于管理子模块和依赖关系,不会生成任何实际的输出物。如果不配置,则打包方式默认为 jar,表示将项目编译后的class文件打成jar包,可以供其他项目使用。
在父项目的pom.xml中配置模块信息,就是我们创建的两个Module:
<modules>
<module>consumer</module>
<module>producer</module>
</modules>
修改两个模块的application.yaml文件,设置其端口号:
server:
# 应用服务 WEB 访问端口
port: 58081
spring:
application:
# 应用名称
name: consumer
server:
# 应用服务 WEB 访问端口
port: 58089
spring:
application:
# 应用名称
name: producer
两个服务都能正常启动:
2. 注册中心 —— nacos
为了能够动态地管理微服务,需要引入注册中心。这里我们使用Nacos作为注册中心,它提供了服务注册与发现、健康检查等功能。
( 注意:由于Nacos是C/S架构,因此需要搭建Nacos的服务端,我们这里在本地搭建起了一个服务端,过程就不再赘述了。)
首先在父工程pom文件中添加依赖:
<dependencyManagement>
<dependencies>
// spring-boot依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
// spring-cloud-alibaba依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
// 在Springboot2.4.x版本之后,需要此依赖用于加载bootstrap.yaml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<version>${bootstap.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
这里spring-cloud-alibaba我们使用的是2021.0.4.0版本,可以查看版本说明。
在子pom文件中引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
// nacos注册中心
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
给微服务中添加bootstrap.yaml文件,该配置文件用于程序引导时执行,设置基本不会变的系统级参数。比如上边application.yaml中的配置,就可以移到bootstrap.yaml中。
之后,可以在bootstrap.yaml中进行nacos的配置:
server:
# 应用服务 WEB 访问端口
port: 58081
spring:
application:
# 应用名称
name: consumer
# Nacos配置
cloud:
nacos:
username: nacos
password: nacos
discovery:
# Nacos 服务器地址
server-addr: 127.0.0.1:8848
# namespace,默认为public
namespace: DEV
# group,默认为DEFAULT_GROUP
group: spring_cloud_demo
server:
# 应用服务 WEB 访问端口
port: 58089
spring:
application:
# 应用名称
name: producer
# Nacos配置
cloud:
nacos:
username: nacos
password: nacos
discovery:
# Nacos 服务器地址
server-addr: 127.0.0.1:8848
# namespace,默认为public
namespace: DEV
# group,默认为DEFAULT_GROUP
group: spring_cloud_demo
登陆Nacos的管理页面,可以在我们设置的namespace下看到注册的服务列表。
3. 配置中心 —— nacos
nacos除了提供注册中心的功能外,还可以作为配置中心,用于统一的配置管理和动态配置项。
先使用普通的属性注入: 创建一个bean,用于测试配置项:
@Data
public class MyDatasource {
private String url;
private String username;
private String password;
private String dbType;
}
创建配置类,其中dbType属性使用@Value注解获取配置值,其它属性使用@ConfigurationProperties注入。
@Configuration
public class DatasourceConfiguration {
@Value("${spring.dbType}")
public String dbType;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public MyDatasource myDatasource() {
MyDatasource datasource = new MyDatasource();
datasource.setDbType(dbType);
return datasource;
}
}
在application.yaml中进行配置:
spring:
datasource:
url: mysql_url
username: mysql
password: 123456
dbType: mysql
创建Controller,提供外部调用的接口:
@Slf4j
@RestController
public class ProducerController {
@Resource
private MyDatasource dataSource;
@RequestMapping("/invokeProducer")
public String invokeProducer() {
log.info("Database type is {}", dataSource.getDbType());
log.info("Url is {}", dataSource.getUrl());
return "succeed";
}
}
先调用一下接口,看看结果:
然后,使用Nacos提供的配置中心功能:
我们在producer的pom中引入配置中心依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
在bootstrap.yaml中,增加配置中心的配置项:
server:
# 应用服务 WEB 访问端口
port: 58089
spring:
application:
# 应用名称
name: producer
# Nacos配置
cloud:
nacos:
username: nacos
password: nacos
discovery:
# Nacos 服务器地址
server-addr: 127.0.0.1:8848
# namespace,默认为public
namespace: DEV
# group,默认为DEFAULT_GROUP
group: spring_cloud_demo
config:
# Nacos 服务器地址
server-addr: 127.0.0.1:8848
# namespace,默认为public
namespace: DEV
# group,默认为DEFAULT_GROUP
group: producer
# 配置文件格式,默认为properties
file-extension: yaml
在nacos管理页面的配置中心,增加如下配置(注意namespace不要弄错):
spring:
datasource:
url: postgresql_url
username: postgresql
password: 123456
dbType: postgresql
点击发布,并调用接口,可以看到控制台有如下打印:
奇怪了,明明已经刷新配置了,为什么dbType的值还是之前的值呢?
这里需要注意,如果使用的是@Value注解,需要使用@RefreshScope标记该bean是可以刷新的,如果使用的是@ConfigurationProperties注解,则不需要使用。
我们增加@RefreshScope注解后,再次尝试:
// 增加@RefreshScope
@RefreshScope
@Configuration
public class DatasourceConfiguration {
@Value("${spring.dbType}")
public String dbType;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public MyDatasource myDatasource() {
MyDatasource datasource = new MyDatasource();
datasource.setDbType(dbType);
return datasource;
}
}
可以看到,配置成功刷新了。
4. 远程调控 —— openFeign
我们在微服务之间进行调用,除了可以使用restTemplate之外,还可以使用openFeign。
创建一个新模块feign-client,并在父工程的pom中增加配置和依赖:
<modules>
<module>consumer</module>
<module>producer</module>
<module>feign-client</module>
</modules>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>${spring-cloud.version}</version>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>feign-client</artifactId>
<version>${revision}</version>
</dependency>
在feign-client的pom中增加依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
创建ProvducerClient接口:
/**
* 声明producer客户端
*/
@FeignClient(name = "producer")
public interface ProducerClient {
@RequestMapping("/invokeProducer")
String invokeProducer();
}
在consumer模块中引入feign-client的依赖,并创建Controller调用producer的接口:
<dependency>
<groupId>com.example</groupId>
<artifactId>feign-client</artifactId>
</dependency>
@Slf4j
@RestController
public class ConsumerController {
@Resource
private ProducerClient producerClient;
@RequestMapping("/invokeConsumer")
public String invokeConsumer() {
log.info("Consumer received request.");
String response = producerClient.invokeProducer();
log.info("Response from producer is {}", response);
return response;
}
}
在consumer启动类上增加配置:
@SpringBootApplication
@EnableFeignClients(basePackages = "com.example.feignclient.client")
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
尝试启动项目,发现如下报错:
Caused by: java.lang.IllegalStateException: No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-loadbalancer?
at org.springframework.cloud.openfeign.FeignClientFactoryBean.loadBalance(FeignClientFactoryBean.java:382) ~[spring-cloud-openfeign-core-3.1.5.jar:3.1.5]
at org.springframework.cloud.openfeign.FeignClientFactoryBean.getTarget(FeignClientFactoryBean.java:427) ~[spring-cloud-openfeign-core-3.1.5.jar:3.1.5]
at org.springframework.cloud.openfeign.FeignClientFactoryBean.getObject(FeignClientFactoryBean.java:402) ~[spring-cloud-openfeign-core-3.1.5.jar:3.1.5]
at org.springframework.cloud.openfeign.FeignClientsRegistrar.lambda$registerFeignClient$0(FeignClientsRegistrar.java:235) ~[spring-cloud-openfeign-core-3.1.5.jar:3.1.5]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1249) ~[spring-beans-5.3.23.jar:5.3.23]
at调控rg.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1191) ~[spring-beans-5.3.23.jar:5.3.23]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) ~[spring-beans-5.3.23.jar:5.3.23]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.23.jar:5.3.23]
... 30 common frames omitted
因为在新版本的openFeign,已经不再使用Ribbon做负载均衡,而是使用loadbalancer了,因此还需要对其进行引入,具体的我们在后边说明。
5. 负载均衡 —— loadbalancer
上边说到在使用feign的时候,需要引入loadbalancer作为负载均衡。
在父工程pom和feign-client的pom中引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
<version>${spring-cloud.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
为了验证负载均衡,我们先修改其controller层,让其返回所在节点的port:
@Slf4j
@RestController
public class ProducerController {
@Resource
private MyDatasource dataSource;
@RequestMapping("/invokeProducer")
public String invokeProducer(HttpServletRequest httpServletRequest) {
log.info("Database type is {}", dataSource.getDbType());
log.info("Url is {}", dataSource.getUrl());
// 返回producer的port
return "Producer port is " + httpServletRequest.getLocalPort();
}
}
然后我们启动两个producer服务,设置其port分别为58088和58089,并多次调用接口:
可以看到,通过负载均衡,consumer以轮询的方式,分别调用了两个producer服务。
6. 网关 —— gateway
上边我们启动了多个producer,那如果我们也启动了多个consumer,那每次调用的时候不是都得换url吗?而且这样岂不是把所有的地址暴露给外部了?
可以使用网关来解决这个问题。
创建一个模块gateway,添加依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>feign-client</artifactId>
</dependency>
添加bootstrap.yaml配置文件:
server:
# 应用服务 WEB 访问端口
port: 58080
spring:
application:
# 应用名称
name: gateway
# Nacos配置
cloud:
nacos:
username: nacos
password: nacos
discovery:
# Nacos 服务器地址
server-addr: 127.0.0.1:8848
# namespace,默认为public
namespace: DEV
# group,默认为DEFAULT_GROUP
group: spring_cloud_demo
config:
# Nacos 服务器地址
server-addr: 127.0.0.1:8848
# namespace,默认为public
namespace: DEV
# group,默认为DEFAULT_GROUP
group: gateway
# 配置文件格式,默认为properties
file-extension: yaml
gateway中创建controller,调用consumer接口:
@RestController
public class GatewayController {
@Resource
private ConsumerClient consumerClient;
@RequestMapping("/invokeConsumer")
public String invokeConsumer() {
return consumerClient.invokeConsumer();
}
}
我们启动2个consumer,端口分别为58081和58082,也启动2个producer,端口分别为58088和58089,调用网关的接口,发现报错:
java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-3
at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:83) ~[reactor-core-3.4.24.jar:3.4.24]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
*__checkpoint ⇢ HTTP GET "/invokeConsumer" [ExceptionHandlingWebHandler]
这是因为gateway是基于WebFlux非阻塞模型的,因此需要使用异步的方式去调用。
修改代码:
@RestController
public class GatewayController {
@Resource
private ConsumerClient consumerClient;
ExecutorService executorService = Executors.newFixedThreadPool(3);
@RequestMapping("/invokeConsumer")
public String invokeConsumer() throws ExecutionException, InterruptedException {
Future<String> response = executorService.submit(() -> consumerClient.invokeConsumer());
return response.get();
}
}
再次调用接口,又出现了新异常:
java.util.concurrent.ExecutionException: feign.codec.DecodeException: No qualifying bean of type 'org.springframework.boot.autoconfigure.http.HttpMessageConverters' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
at java.util.concurrent.FutureTask.report(FutureTask.java:122) ~[na:1.8.0_201]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
*__checkpoint ⇢ HTTP GET "/invokeConsumer" [ExceptionHandlingWebHandler]
因此我们需要创建HttpMessageConverters的Bean注入容器中:
@Configuration
public class GatewayConfiguration {
// HttpMessageConverter负责转换HTTP的请求和响应。
// Spring Cloud Gateway是基于WebFlux的,是ReactiveWeb。所以HttpMessageConverters不会自动注入。
@Bean
@ConditionalOnMissingBean
public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {
return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList()));
}
}
现在再来多次调用接口,打印如下:
可以看到,通过网关,实现了在不修改访问地址的前提下,实现了内部服务的负载均衡访问。
当然,也可以通过配置路由,直接实现请求的转发,而不需要通过controller:
spring:
cloud:
gateway:
routes:
- id: consumer
uri: lb://consumer
predicates:
- Path=/invokeConsumer
这里可以通过匹配/invokeConsumer,路由到consumer服务,并通过lb进行负载均衡。
7. 流量控制器 —— sentinel
虽然上边已经可以完成基本的服务间调用,但是为了防止大流量场景下系统宕机,还需要对流量进行控制,这就可以使用sentinel来实现。
引入依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
在application.yaml中进行配置:
sentinel:
# 服务启动时就直接建立心跳,不需要等到第一次访问接口,才连接sentinel
eager: true
# 控制台地址
transport:
dashboard: localhost:8081
对代码做一些微调,增加一个service层,使用@SentinelResource注解标注了方法,并设置了资源名称:
@Service
public class GatewayService {
@Resource
private ConsumerClient consumerClient;
ExecutorService executorService = Executors.newFixedThreadPool(3);
@SentinelResource(value = "invokeConsumer")
public String invokeConsumer() throws ExecutionException, InterruptedException {
Future<String> response = executorService.submit(() -> consumerClient.invokeConsumer());
return response.get();
}
}
sentinel也需要启动服务端,打开sentinel控制台,可以对流控规则、熔断规则等进行配置:
这里配置了一条流控规则:允许访问该资源的最大QPS为3
使用jmeter调用接口,在一秒内发送了5个请求,可以看到确实只允许通过了3个请求,其余两个被拒绝了
有没有办法在调用失败时,做一些相应的处理呢?
答案是可以,@SentinelResource注解提供了几个属性:
fallbackClass表示发生异常时的回调类,fallback设置回调方法; blockHandlerClass表示进入流控时的回调类,blockHandler设置回调方法。
修改代码:
@Service
public class GatewayService {
@Resource
private ConsumerClient consumerClient;
ExecutorService executorService = Executors.newFixedThreadPool(3);
// fallback为发生异常的回调方法,blockHandler为进入流控时的回调方法
@SentinelResource(value = "invokeConsumer", fallbackClass = FallbackConfigurer.class, fallback = "fallback",
blockHandlerClass = BlockHandlerConfigurer.class, blockHandler = "blockHandler")
public String invokeConsumer() throws ExecutionException, InterruptedException {
Future<String> response = executorService.submit(() -> consumerClient.invokeConsumer());
return response.get();
}
}
其中,@SentinelResource中的属性配置类如下:
/**
* 发生异常时的回调方法
*/
public class FallbackConfigurer {
public static String fallback() {
return "发生了异常.";
}
}
/**
* 进入流控时的回调方法
*/
public class BlockHandlerConfigurer {
public static String blockHandler(BlockException blockException) {
return "发生了流控: " + blockException;
}
}
如果发生了异常,会得到如下响应:
如果进入了流控,会得到如下响应:
8. 一些思考
(1) nacos如何共享配置?
如果多个服务都需要同样的配置,该怎么做呢?
可以使用共享配置来实现。
比如可以在nacos中增加以下配置:
feign:
# Feign整合Sentinel
sentinel:
enabled: true
client:
config:
# 设置通用配置
default:
# client连接超时为100毫秒
# 连接时间不宜过长,防止依赖服务负载过高情况下活跃连接都在长时间尝试建立连接,建议设置比较短以便快速失败
connectTimeout: 100
# client响应超时为1秒
# 单个接口响应时间不宜过长,建议为1秒,超过1秒的一般都需要优化接口,如果无法优化建议走独立配置
readTimeout: 1000
在需要使用该配置的服务的bootstrap.yaml中增加shared-configs配置即可(如果需要额外的扩展配置,也可以使用extension-configs配置):
spring:
# Nacos配置
cloud:
nacos:
config:
# 公共配置文件
shared-configs:
- data-id: feign-config.yaml
group: shared
refresh: true
# 扩展配置文件,优先级大于shared-configs,在其后进行加载
extension-configs:
- data-id: xxx.yaml
group: xxx
refresh: true
更多详情可以参考这篇文章。
(2) nacos的配置如何持久化?
在单机模式下,nacos默认使用一个内嵌式数据库实现配置存储,如果想使用mysql进行配置存储应该如何实现呢?
在nacos-server的conf目录中,有一个nacos-mysql.sql文件,将其导入Mysql中就会创建nacos所需要的一系列表:
在同目录的application.properties文件中,或者在项目中的application.yaml中,填写mysql的连接信息:
可以看到,在config_info表中,就能看到我们在nacos上配置的文件:
(3) 如何使用连接线程池呢?
OpenFeign的底层是通过http/https协议进行通信的,默认使用的是HttpURLConnection,每次请求都会建立、关闭连接,比较浪费性能,可以引入httpclient作为底层的通信框架,它支持连接池。
引入依赖:
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
增加配置:
feign:
httpclient:
# 设置单个HOST最大连接数为50个,可根据高峰期调用频率来调整
maxConnectionsPerRoute: 50
# 设置全局最大连接数为300个连接,可根据具体有多少FeignClient来决定,比如一个HOST最多50个连接,一个有8个HOST,每个HOST调用频率有高有低,可折中取值300
maxConnections: 300
# 设置连接存活时间为900秒,超过该时间后空闲连接会被回收,注意的是如果你通过Java Config覆盖默认ApacheHttpClient,一定要创建定时器来检测无用连接
timeToLive: 900
(4) 负载均衡策略如何修改?
之前我们使用loadbalancer时,没有配置负载均衡策略,默认的是轮询策略,如果想修改为随机策略可以配置一个RandomLoadBalancer。
/**
* 负载均衡策略,默认为round-robin
*/
public class LoadBalancerConfiguration {
@Bean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
// return new RoundRobinLoadBalancer(loadBalancerClientFactory.getLazyProvider(name,
// ServiceInstanceListSupplier.class), name);
return new RandomLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
}
然后使用@LoadBalancerClient进行配置:
/**
* 声明consumer客户端
*/
@FeignClient(name = "consumer")
// 根据name去注册中心找对应的实例地址
@LoadBalancerClient(name = "consumer", configuration = LoadBalancerConfiguration.class)
public interface ConsumerClient {
@RequestMapping("/invokeConsumer")
String invokeConsumer();
}
(5) 如何持久化sentinal配置呢?
每次项目重启后,sentinel中配置的规则都会丢失,可以利用nacos对其进行持久化。
引入依赖:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
在sentinel配置下追加如下配置:
spring:
cloud:
sentinel:
# 服务启动时就直接建立心跳,不需要等到第一次访问接口,才连接sentinel
eager: true
# 控制台地址
transport:
dashboard: localhost:8081
# 持久化
datasource:
ds1: # 随便起名字
nacos:
server-addr: localhost:8848
namespace: DEV
dataId: gateway # 微服务名称
groupId: gateway
data-type: json
rule-type: flow # 流控规则
新增nacos配置:
[
{
"resource": "/invokeConsumer", // 资源名称
"limitApp": "default", // 来源应用
"grade": 1, // 阈值类型:0表示线程数,1表示QPS
"count": 3, // 单机阈值
"strategy": 0, // 流控模式:0表示直接,1表示关联,2表示链路
"controlBehavior": 0, //流控效果:0表示快速失败,1表示warm up,2表示排队等待
"clusterMode": false // 是否集群
}
]
转载自:https://juejin.cn/post/7242131960679202871