前言
最近项目一直在使用Spring Cloud Alibaba+Nacos框架,犹豫项目紧张一直想总结,因为后期监控项目数据的问题,好吧 我承认是懒…所以还是做一个系列分享一下最近在项目上使用这个框架的心得
一、Spring Cloud Alibaba是什么?
简介
从框架名字上看一定会觉得和Spring Cloud又脱不了的干系,其实Spring Cloud Alibaba就是对Spring Cloud实现拓展组件功能,Spring Cloud Alibaba为分布式应用程序开发提供了一站式解决方案。它包含开发分布式应用程序所需的所有组件,使您可以轻松使用Spring Cloud开发应用程序。
使用Spring Cloud Alibaba只需要添加一些注释和少量配置即可将Spring Cloud应用程序连接到Alibaba的分布式解决方案,并使用Alibaba中间件构建分布式应用程序系统,这也是官网对其的解释。
废话不说下面上干货
二、引入相关包
DependencyManagement应用
项目业务模块本身很多,为了解决项目版不统一的问题所以使用了这个标签,必须让所有的子模块使用依赖项的统一版本,保证版本的统一性
在项目顶层的POM文件中,我们会看到dependencyManagement元素。通过它元素来管理jar包的版本,让子项目中引用一个依赖而不用显示的列出版本号。Maven会沿着父子层次向上走,直到找到一个拥有dependencyManagement元素的项目,然后它就会使用在这个dependencyManagement元素中指定的版本号。
DependencyManagement和Dependencies区别
- dependencies: 即使在子项目中不写该依赖项,那么子项目仍然会从父项目中继承该依赖项(全部继承)
- dependencyManagement: 里只是声明依赖,并不实现引入,因此子项目需要显示的声明需要用的依赖。如果不在子项目中声明依赖,是不会从父项目中继承下来的;只有在子项目中写了该依赖项,并且没有指定具体版本,才会从父项目中继承该项,并且version和scope都读取自父pom;另外如果子项目中指定了版本号,那么会使用子项目中指定的jar版本。
<!-- parent-pom.xml -->
<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>
<groupId>com.hs</groupId>
<artifactId>hs-bussiness</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>hs-bussiness</name>
<description>hs-bussiness</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
<nacos.version>0.2.2.RELEASE</nacos.version>
<spring-boot.version>2.0.4.RELEASE</spring-boot.version>
</properties>
<modules>
<module>children-center</module>
</modules>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${nacos.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>
<!-- children-center-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>
<parent>
<groupId>com.hs</groupId>
<artifactId>hs-bussiness</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>com.hs</groupId>
<artifactId>children-center</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>children-center</name>
<description>chunyun-children</description>
<properties>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
好处:统一管理项目的版本号,确保应用的各个项目的依赖和版本一致,才能保证测试的和发布的是相同的成果,因此,在顶层pom中定义共同的依赖关系。同时可以避免在每个使用的子项目中都声明一个版本号,这样想升级或者切换到另一个版本时,只需要在父类容器里更新,不需要任何一个子项目的修改;如果某个子项目需要另外一个版本号时,只需要在dependencies中声明一个版本号即可。子类就会使用子类声明的版本号,不继承于父类版本号。
总结
dependencies中的jar直接加到项目中,管理的是依赖关系(如果有父pom,子pom,则子pom中只能被动接受父类的版本);dependencyManagement主要管理版本,对于子类继承同一个父类是很有用的,集中管理依赖版本不添加依赖关系,对于其中定义的版本,子pom不一定要继承父pom所定义的版本。
三、Spring Cloud Gateway 路由配置
SpringCloud Gateway 特征
SpringCloud官方,对SpringCloud Gateway 特征介绍如下:
- 基于 Spring Framework 5,Project Reactor 和 Spring Boot 2.0
- Hystrix断路器集成
- Spring Cloud DiscoveryClient集成
- 易于编写的 Predicates 和 Filters
- 具备一些网关的高级功能:动态路由、限流、路径重写
从以上的特征来说,和Zuul的特征差别不大。SpringCloud Gateway和Zuul主要的区别,还是在底层的通信框架上。
简单说明一下上文中的三个术语:
Filter(过滤器):
和Zuul的过滤器在概念上类似,可以使用它拦截和修改请求,并且对上游的响应,进行二次处理。过滤器为org.springframework.cloud.gateway.filter.GatewayFilter类的实例。
Route(路由):
网关配置的基本组成模块,和Zuul的路由配置模块类似。一个Route模块由一个 ID,一个目标 URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配,目标URI会被访问。
Predicate(断言):
这是一个 Java 8 的 Predicate,可以使用它来匹配来自 HTTP 请求的任何内容,例如 headers 或参数。断言的输入类型是一个 ServerWebExchange。
<!--gateway依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>2.2.4.RELEASE</version>
</dependency>
<!--hystrix 依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!--路由配置-->
spring:
cloud:
gateway:
discovery:
locator:
enable: true # gateway可以通过开启以下配置来打开根据服务的serviceId来匹配路由,默认是大写
routes:
- id: children-center# 微服务路由规则
uri: lb://children-center # 负载均衡,将请求转发到注册中心注册的hsource-demo服务
# 如果前端请求路径包含 children-center,则应用这条路由规则
#filters: #过滤器
#- RewritePath=/api/(?<segment>.*),/$\{segment} # 将跳转路径中包含的api替换成空
predicates: # 断言
- Path=/children-center/**
filters:# 过滤器配置降级规则
- StripPrefix=1
- name: Hystrix
args:
name: default
fallbackUri: forward:/defaultfallback # 只有该id下的服务会降级
GlobalFilter
GlobalGilter 全局过滤器接口与 GatewayFilter 网关过滤器接口具有相同的方法定义。全局过滤器是一系列特殊的过滤器,会根据条件应用到所有路由中。网关过滤器是更细粒度的过滤器,作用于指定的路由中。
GlobalFilter 有十一个实现类,包括路由转发、负载均衡、ws 路由、netty 路由等全局过滤器。下面我们就分别介绍一下这些全局路由过滤器的实现。以下是过滤校验token示例:
package com.hsource.hsourcegateway.filter;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hsource.common.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import org.apache.commons.lang.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* @Author yang
* @Crreate 2020-10-15 11:54
* @desc
*/
@Component
public class AuthAndLogFilter implements GlobalFilter, Ordered {
static final Logger logger = LogManager.getLogger("request");
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest serverHttpRequest = exchange.getRequest();
ServerHttpResponse serverHttpResponse = exchange.getResponse();
StringBuilder logBuilder = new StringBuilder();
Map<String, String> params = parseRequest(exchange, logBuilder);
String uid = "";
//验证当前请求是否存在白名单
boolean isEcs=isExclusion(serverHttpRequest);
if(!isEcs) {
try {
//获取当前token并解析出当前token中的参数信息
uid=checkToken(params, serverHttpRequest);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
//请求处理
DataBufferFactory bufferFactory = serverHttpResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(serverHttpResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.buffer().map(dataBuffer -> {
//DefaultDataBufferFactory join 乱码的问题解决
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffer);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
String resp = new String(content, StandardCharsets.UTF_8);
logBuilder.append(",resp=").append(resp);
logger.info(logBuilder.toString());
byte[] uppedContent = new String(content, StandardCharsets.UTF_8).getBytes(StandardCharsets.UTF_8);
return bufferFactory.wrap(uppedContent);
}));
}
return super.writeWith(body);
}
};
String uid="";
//解析连接中的参数重新整合
URI uri = exchange.getRequest().getURI();
StringBuilder query = new StringBuilder();
String originalQuery = uri.getRawQuery();
if (StringUtils.isNotBlank(originalQuery)) {
query.append(originalQuery);
if (originalQuery.charAt(originalQuery.length() - 1) != '&') {
query.append('&');
}
}
//将token中的参数加入其中
query.append("testparams");
query.append('=');
query.append(uid);
//重新整理访问的URL
URI newUri = UriComponentsBuilder.fromUri(uri)
.replaceQuery(query.toString())
.build(true)
.toUri();
ServerHttpRequest request = exchange.getRequest().mutate().uri(newUri).build();
return chain.filter(exchange.mutate().request(request).response(decoratedResponse).build());
}
/**
* 将请求参数转为Map<String,String> 集合
**/
private Map<String, String> parseRequest(ServerWebExchange exchange, StringBuilder logBuilder) {
ServerHttpRequest serverHttpRequest = exchange.getRequest();
String method = serverHttpRequest.getMethodValue().toUpperCase();
logBuilder.append(method).append(",").append(serverHttpRequest.getURI());
MultiValueMap<String, String> query = serverHttpRequest.getQueryParams();
Map<String, String> params = new HashMap<>();
query.forEach((k, v) -> {
params.put(k, v.get(0));
});
serverHttpRequest.getHeaders().forEach((k, v) -> {
if(k.equals("token")){
params.put(k,v.get(0));
}
});
if("POST".equals(method)) {
String body = exchange.getAttributeOrDefault("cachedRequestBody", "");
if(StringUtils.isNotBlank(body)) {
logBuilder.append(",body=").append(body);
String[] kvArray = body.split("&");
for (String kv : kvArray) {
if (kv.indexOf("=") >= 0) {
String k = kv.split("=")[0];
String v = kv.split("=")[1];
if(!params.containsKey(k)) {
try {
params.put(k, URLDecoder.decode(v, "UTF-8"));
} catch (UnsupportedEncodingException e) {
}
}
}
}
}
}
return params;
}
/**
*获取token或连接中的token 得到进行token验证
**/
private String checkToken(Map<String, String> params, ServerHttpRequest serverHttpRequest) throws JsonProcessingException {
String uid="";
String token = params.get("token");
if(StringUtils.isBlank(token)) {
return "";
}
ObjectMapper trans=new ObjectMapper();
Claims cls = null;
try {
cls = JwtUtil.parseJWT(token);
} catch (IOException e) {
e.printStackTrace();
return "-1";
}
String json2 = trans.writeValueAsString(cls);
JSONObject jsonObject = (JSONObject) JSON.parseObject(json2);
if(jsonObject==null){
return "";
}
return jsonObject.getString("uid");
}
/**
* @Author yang
* @Crreate 2020-10-15 11:54
* @desc 访问路径白名单
*/
private boolean isExclusion(ServerHttpRequest serverHttpRequest){
String url = serverHttpRequest.getPath().toString();
String[] exclusions = {
"/hs-demo/user/nacos"
};
return Arrays.asList(exclusions).contains(url);
}
}
<!--网关添加降级处理类-->
@RestController
public class FallbackController {
@RequestMapping("/defaultfallback")
public Map<String,Object> defaultfallback(){
System.out.println("降级操作...");
Map<String,Object> map = new HashMap<>();
map.put("code",200);
map.put("msg","服务超时降级");
map.put("data",null);
return map;
}
}
四、Spring Cloud Nacos 注册中心
什么是 Nacos?
服务(Service)是 Nacos 世界的一等公民。Nacos 支持几乎所有主流类型的“服务”的发现、配置和管理:
- Kubernetes Service
- gRPC & Dubbo RPC Service
- Spring Cloud RESTful Service
Nacos 的关键特性包括:
- 服务发现和服务健康监测
Nacos 支持基于 DNS 和基于 RPC 的服务发现。服务提供者使用 原生SDK、OpenAPI、或一个独立的Agent TODO注册 Service 后,服务消费者可以使用DNS TODO 或HTTP&API查找和发现服务。
Nacos 提供对服务的实时的健康检查,阻止向不健康的主机或服务实例发送请求。Nacos 支持传输层 (PING 或 TCP)和应用层 (如 HTTP、MySQL、用户自定义)的健康检查。 对于复杂的云环境和网络拓扑环境中(如 VPC、边缘网络等)服务的健康检查,Nacos 提供了 agent 上报模式和服务端主动检测2种健康检查模式。Nacos 还提供了统一的健康检查仪表盘,帮助您根据健康状态管理服务的可用性及流量。 - 动态配置服务
动态配置服务可以让您以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置。
动态配置消除了配置变更时重新部署应用和服务的需要,让配置管理变得更加高效和敏捷。
配置中心化管理让实现无状态服务变得更简单,让服务按需弹性扩展变得更容易。
Nacos 提供了一个简洁易用的UI (控制台样例 Demo) 帮助您管理所有的服务和应用的配置。Nacos 还提供包括配置版本跟踪、金丝雀发布、一键回滚配置以及客户端配置更新状态跟踪在内的一系列开箱即用的配置管理特性,帮助您更安全地在生产环境中管理配置变更和降低配置变更带来的风险。 - 动态 DNS 服务
动态 DNS 服务支持权重路由,让您更容易地实现中间层负载均衡、更灵活的路由策略、流量控制以及数据中心内网的简单DNS解析服务。动态DNS服务还能让您更容易地实现以 DNS 协议为基础的服务发现,以帮助您消除耦合到厂商私有服务发现 API 上的风险。
Nacos 提供了一些简单的 DNS APIs TODO 帮助您管理服务的关联域名和可用的 IP:PORT 列表. - 服务及其元数据管理
Nacos 能让您从微服务平台建设的视角管理数据中心的所有服务及元数据,包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略、服务的 SLA 以及最首要的 metrics 统计数据。
Nacos地图
<!-- 注册服务 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<!-- 发现服务 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>0.9.0.RELEASE</version>
</dependency>
<!--nacos bootstrap.yml配置-->
spring:
profiles: pro
application:
<!--注册服务名称-->
name: hsource-gateway
cloud:
nacos:
config:
<!--服务地址-->
server-addr: 112.126.102.94:8848
<!--文件扩展形式-->
file-extension: yml
<!--所属分组-->
group: DEFAULT_GROUP
discovery:
<!--发现服务地址-->
server-addr: 112.126.102.94:8848
注册成功后在Nacos->【服务管理】->【服务列表】中可以看到注册的服务
服务的详情可以下线、上线、编辑服务集群等内容