- Eureka单机
Eureka客户端【client】配置
server:
port: 8080
spring:
application:
name: eureka-client-a
#eureka客户端配置
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka #指定服务端
register-with-eureka: true #是否向eureka-server注册
fetch-registry: true #是否从注册中心拉取注册列表【关闭后将无法查找到其他已注册的服务】
registry-fetch-interval-seconds: 10 #为了缓解服务列表脏读问题,配置拉取间隔,默认为30秒[间隔过小会浪费性能]
instance:
hostname: localhost #主机名称
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
prefer-ip-address: true #是否显示IP
lease-renewal-interval-in-seconds: 10 #续约时间间隔
浏览器访问:
Eureka服务端【server】配置【单server版本】
server:
port: 8761
spring:
application:
name: eureka-server
eureka:
server:
eviction-interval-timer-in-ms: 10000 #定期删除操作的间隔
renewal-percent-threshold: 0.85 #当85%的服务没有续约时,eureka进行数据保护而不再删除
instance: #实例配置
# instance-id: localhost:eureka-server:8761 #主机名:应用名:端口
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port} #主机名:应用名:端口
hostname: localhost #主机名或服务IP
prefer-ip-address: true #以IP形式显示具体服务信息
lease-renewal-interval-in-seconds: 5 #服务实例续约的时间间隔[单位秒]eureka给自己续约【要小于上方的定期删除时间间隔】
server日志:【每隔十秒进行一次删除注册】 驱逐任务
2023-03-04 17:09:25.012 INFO 1908 --- [a-EvictionTimer] c.n.e.registry.AbstractInstanceRegistry : Running the evict task with compensationTime 3ms
2023-03-04 17:09:35.019 INFO 1908 --- [a-EvictionTimer] c.n.e.registry.AbstractInstanceRegistry : Running the evict task with compensationTime 6ms
2023-03-04 17:09:45.027 INFO 1908 --- [a-EvictionTimer] c.n.e.registry.AbstractInstanceRegistry : Running the evict task with compensationTime 7ms
Eureka集群
Eureka服务端【server】配置
server:
port: 8761
spring:
application:
name: eureka-server
eureka:
client:
service-url:
defaultZone: http : //peer2:8762/eureka , http : //peer3:8763/eureka 【这个server将自己注册进入集群内其他两个server,搭建成集群】
instance:
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
hostname: peer1 【本server所在电脑IP:peer1】
prefer-ip-address: true
lease-renewal-interval-in-seconds: 5
释:
localhost被改成了peer1,peer2等,这是因为要建立集群需要多个电脑,于是在 hosts 文件内进行了分片配置来模拟
127.0.0.1 peer1
127.0.0.1 peer2
127.0.0.1 peer3
像这样模拟了三台电脑,IP为:peer1,peer2,peer3
浏览器访问:
Eureka客户端【client】配置【集群版本】
server:
port: 8080
spring:
application:
name: eureka-client-a
eureka: #eureka客户端配置
client:
service-url:
defaultZone: http : //peer1:8761/eureka 【该client注册入的server为所在电脑IP为 peer1】
register-with-eureka: true #是否向eureka-server注册
fetch-registry: true #是否从注册中心拉取注册列表【关闭后将无法查找到其他已注册的服务】
registry-fetch-interval-seconds: 10 #为了缓解服务列表脏读问题,配置拉取间隔,默认为30秒[间隔过小会浪费性能]
instance:
hostname: localhost #主机名称
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
prefer-ip-address: true
lease-renewal-interval-in-seconds: 10
释:
将客户端注入IP为peer1的电脑上的server,
因为该server搭建了集群,所以注册进入这个server后,
其他集群内的server【peer2,peer3】会在扩散作用下也获取到这个client的信息
为了防止注册入的这个server故障,可以将这个client注册进入三个server:【不清楚这种情况下置扩散于何地 黑人问号.jpg 】
defaultZone: http : //peer1:8761/eureka,http : //peer2:8762/eureka,http : //peer3:8763/eureka
提及知识点:
分布式数据一致性协议:
协议:raft,Paxos
eureka没有分布式数据一致性机制,节点都是相同的
nacos:raft
Zookeeper:Paxos
Nacos:raft
zk:Paxos
Ribbon
关于Ribbon组件:
Ribbon组件被OpenFeign进行了内嵌,未来不会单独作为组件使用了。
服务消费者
Ribbon依赖:【注:ribbon依赖只需要在服务消费者进行配置】
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
application类:
@SpringBootApplication
@EnableEurekaClient
public class Consumer02Application {
public static void main(String[] args) {
SpringApplication.run(Consumer02Application.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
释:
添加了@LoadBalance后,这个RestTemplate交由ribbon管理,会走ribbon的代码进行负载均衡
controller类:
@RestController
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("testRibbon")
public String testRibbon(String serviceName){
String forObject = restTemplate.getForObject("http://" + serviceName + "/hello", String.class);
return forObject;
}
}
这样配置时,调用要:http://localhost:8082/testRibbon?serviceName=provider
参数serviceName正是由controller内方法的形参接收的
配置了RestTemper的@Bean后,自动注入【@Autoware】的成员变量已经交由ribbon负载均衡管理,必然会执行以下的操作:
问题前导:
【ribbon如何将http://provider/hello成功发送的呢】
【理论格式:http://127.0.0.1:8080/hello】
ribbon执行逻辑:
1 . 拦截这个请求
2 . 截取主机名
3 . 借助eureka根据服务名provider进行服务发现,获取list<>
4 . 负载均衡算法获取服务ip和port端口号
5 . reConstructURL重构
6 . 发起请求
因此,交由ribbon管控的,由@Autoware自动注入的RestTemple不能再正确读取理论格式的url了
这种情况下想要使用 未交由ribbon的RestTemple对象,需要手动new:
RestTemplate restTemplate1 = new RestTemplate();
YML配置:
server:
port: 8082
spring:
application:
name: consumer
eureka:
client:
service-url:
defaultZone: http://192.168.159.130:8761/eureka
instance:
hostname: localhost
prefer-ip-address: true
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
ribbon:
eager-load:
enabled: true
eureka:
enabled: true
http:
client:
enabled: false
okhttp:
enabled: false
释:
1 . eager-load配置:true对应饥饿加载,即服务启动后会立即进行 服务列表拉取 。【默认情况下为false,懒加载,直到被调用才会进行服务列表拉取】
2 . eureka:enable:开启eureka注册中心对ribbon的支持
3 . httpclient:开启httpclient关于http请求发送的服务,替代默认的httpUrlConnection进行http请求发送。
httpclient支持连接池,比不支持连接池的httpUrlConnection效率更高
4 . okhttp:另一款http请求发送工具,优点是轻量级
概:
ribbon的yml相关配置都不是必须的,均可不配,并且仅有服务消费者需要进行配置即可
服务提供者
ribbon的服务提供者不需要进行任何特殊配置
只需要正常注册入配置中心,记好服务的访问路径等信息,在服务提供者内进行正确调用即可
关于启动服务
注意,如果先启动服务消费者,再依次进行服务提供者的话,
服务消费者就可能在 同种的服务提供者 没有全部启动 时就进行了 服务列表拉取 ,
导致尚没有启动完成的服务提供者无法参与到轮询内
直到达到了服务列表拉取的时间间隔,进行了第二次服务列表拉取,轮询才能正常执行
故,为了避免以上问题,需要先将服务提供者全部启动后,再启动服务消费者,这样拉取到的表单就是完整的了
提及知识点:
轮询算法与自旋锁
Feign
服务消费者:
1 . Application类内额外添加了@EnableFeignClient注解
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients //开启feign客户端功能
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
@Bean
public Logger.Level level(){
return Logger.Level.FULL;
}
}
释:
日志级别配置
2 . java目录下添加了feign目录:
@FeignClient(value = "order-service")
public interface UserOrderFeign {
@GetMapping("doOrder")
public String doOrder(); 【方法签名】
}
释:
@FeignClient注解内置需要调用的 服务提供者的服务名
类内添加 需要调用的服务内的方法的 方法签名
方法签名:
即,将服务提供者内controller层内的方法copy一份过来,仅保留方法声明部分,去除方法体
【public是可以省略的,因为@FeignClient内的方法签名默认是public】
3 . pom内添加openFeign依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
4 . yml内添加控制服务间调用细节的一些配置
server:
port: 8081
spring:
application:
name: user-service
eureka:
client:
service-url:
defaultZone: http://192.168.159.130:8761/eureka
ribbon:
ReadTimeout: 3000 #三秒调用超时【三秒内 下一个服务未执行完成 ,则报错】
ConnectTimeout: 3000 #三秒链接超时【三秒内 没有连接到 下一个服务,则报错】
logging:
level:
com.misakimei.feign.UserOrderFeign: debug
释:
feign只是封装了远程调用的功能,所以需要 对其封装的ribbon进行配置 来控制响应时间等参数
logging配合Application类内的日志配置,对指定组件的日志输出进行调控
5 . controller层使用feign来完成可能出现的远程调用操作:
@RestController
public class UserController {
@Autowired
public UserOrderFeign userOrderFeign;
@GetMapping("userDoOrder")
public String userDoOrder(){
System.out.println("userService successful ...");
String s = userOrderFeign.doOrder();
return s;
}
}
释:
定义feign目录下 对应的接口 为成员变量,在方法内直接调用成员变量内对应的 方法签名,远程调用就完成了
6 . 关于有参请求:
@RestController
public class ParamController {
@GetMapping("testUrl/{name}/and/{age}")
public String testUrl(@PathVariable("name")String name,@PathVariable("age")Integer age){ 多个或一个常规参数
System.out.println(name);
System.out.println(age);
return name + age.toString();
}
@GetMapping("oneParam")
public String oneParam(@RequestParam(required = true) String name){ required默认为true,表示请求如果不携带参数将会报错,为false则可以不带参数
System.out.println(name);
return name;
}
@GetMapping("twoParam")
public String twoParam(@RequestParam(required = true)String name,@RequestParam(required = true)Integer age){
System.out.println(name);
System.out.println(age);
return name + age.toString();
}
@PostMapping("oneObj")
public Order oneObj(@RequestBody Order order){ Post请求接一个对象类参数
System.out.println(order);
return order;
}
@PostMapping("oneObjOneParam")
public Order oneObjOneParam(@RequestBody Order order,@RequestParam("name") String name){
System.out.println(order);
System.out.println(name);
return order;
}
Hystrix
服务雪崩:
在微服务状态下的多级调用内,当访问链内存在一个服务模块无法正常工作时,他的上级服务会进行一段时间的 响应等待,
在这个等待的过程中,报错的服务的上级服务会 保留这个等待停止的服务的线程
高并发的情况下,大量这样的保留行为会导致 资源大量被占用 ,随后其上级服务因此而崩溃
多级服务依次崩溃,级联报错,引起的大片服务崩溃的现象就是 服务雪崩
于是,为了应对服务雪崩,出现了hystrix组件
hystrix组件一旦将一个服务判定为故障后,会直接进行报错返回
这样,虽然这个服务仍然处于故障中,但是通过 快速返回错误信息 的方法来避免了 等待故障服务响应 的过程,使得故障不会因此扩散到其他服务,以此避免了服务雪崩的发生
服务消费者:
yml配置:
server:
port: 8081
spring:
application:
name: cuntomer-service
eureka:
client:
service-url:
defaultZone: http://192.168.159.130:8761/eureka
instance:
hostname: localhost
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
feign:
hystrix:
enabled: true
hystrix:
command:
default: #全局配置,可改成服务名
circuitBreaker:
enable: true #熔断器功能【默认为开启】
requestVolumeThreshold: 3 #失败次数【阈值】
sleepWindowInMilliseconds: 20000 #窗口时间
errorThresholdPercentage: 60 #失败率
execution:
isolation:
Strategy: thread #隔离方式
thread:
timeoutInMilliseconds: 3000 #超时时间
fallback:
isolation:
semaphore:
maxConcurrentRequests: 1000
释:
紫色 true:开启hystrix功能
红色:熔断器细节参数配置
窗口时间:
设置窗口 失败阈值 与 失败率计算 的间隔时间
阈值:
设置 失败多少次后进行hystrix失败信息的返回
失败率:
百分比为多少的服务失败后,判定为失败
【阈值和失败率实质上是两种形式的对访问失败这一事件的判定,可以只配一个,也可以同时生效】
隔离方式:
对于 服务消费者 针对多个 服务提供者的隔离方式:【未掌握,可能存在很多认知不足】
线程隔离:对于每一个 消费者 ---> 提供者 的体系,分别设置线程池进行线程管理,
通过规定 线程池的数量上限 来限制 最高并发数
信号量隔离:设置一个所有服务调用共用的 原子计数器 ,每当进行了一次服务调用就会使计数器加一,每次服务结束释放线程就会减一
通过对 计数上限 进行控制来限制 最高并发数
hystrix包配置:
@Component
public class CustomerRentFeignHystrix implements CustomerRentFeign {
@Override
public String rent() {
return "备选方案启动,服务异常 ...";
}
}
释:
feign目录下建立hystrix目录,目录下创造 继承feign接口 的类,重写feign接口内的 方法签名 ,
这个 重写的方法签名 就是 熔断后执行错误反馈 的方法
feign组件配置:
@FeignClient(value = "rent-car-service",fallback = CustomerRentFeignHystrix.class)
public interface CustomerRentFeign {
@GetMapping("rent")
public String rent();
}
释:
fallback:当这个网关内的方法签名执行失败时,会走fallback属性内指向的hystrix类内的失败方案
pom依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
服务提供者:
无特殊配置
模块抽取
对feign和hystrix进行单独抽取为 common-api 类,对domain进行单独抽取为 project-domain 类
使用时,直接将 api 和 domain 以 pom依赖 的形式引入对应的工程,即可使用里面全部的接口和类
这样,在进行controller创建时,就不用依照原本的步骤,先创建controller再在对应 服务消费者的feign内 进行 方法标签 注册了
直接在api的feign内进行对应方法标签的创建,然后让导入了api依赖的服务的controller类实现feign接口,再重写接口内的方法即可
对于hystrix组件的使用如常,还是直接在hystrix类上继承feign,重写方法即可,feign内的fallback标签也如常配置
Zipkin链路追踪
yml配置:
server:
port: 8080
spring:
application:
name: order-service
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
probability: 1 #采集率,1表示百分百采集,默认为0.1,百分之十
rate: 10 #采集时间间隔
eureka:
client:
service-url:
defaultZone: http://192.168.159.130:8761/eureka
instance:
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
hostname: localhost
释:
base-url为zipkin的图形化界面,可以查看每次服务调用发起时不同组件间响应时间等具体信息
注:使用时首先要启动zipkin的jar包来启动服务
pom依赖:【添加在api类内,因为】
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
Admin监控:
创建admin的服务端【server】:
注:需要admin进行服务监控时,需要进行服务端和客户端的配置。理论上需要在每一个服务内配置客户端,让服务端对每一个需要监控的组件进行监管
显然,这样的配置太过于繁琐,于是可以将这个admin的服务端作为eureka的客户端注册到eureka的配置中心内,然后admin就可以直接从服务端内进行客户列表的拉
取,省去了分设客户端的配置。
pom依赖:
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
</dependency>
yml配置:
server:
port: 10086
spring:
application:
name: admin-server
eureka:
client:
service-url:
defaultZone: http://192.168.159.130:8761/eureka
instance:
hostname: localhost
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
management:
endpoints:
web:
exposure:
include: "*" #暴漏所有的监控端点
暴漏需要开启监管的组件:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Gateway网关
gateway:路由转发 和 过滤器链
执行流程:-- filter1 --> filter2 --> interceptor --> controller --> interceptor --> filter2 --> filter1 --
filter与interceptor:
filter过滤器 拦截所有的访问请求,interceptor拦截器 拦截所有对controller发起的请求
Gateway配置:
Gateway作为 独立的模块 进行配置
pom依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
yml实现网关配置:
server:
port: 80 #网关端口
spring:
application:
name: gateway-server #网关服务名
cloud:
gateway:
enabled: true #网关开启【添加了网关依赖后这里默认为开启】
routes:
- id: login-service-route #路由的自定义id,保证唯一即可
uri: http://localhost:8081
predicates:
- Path=/doLogin #访问路径
释:
当出现针对网关端口80进行的,path为/doLogin的访问,就会在gateway的作用下转发到 uri所指的路径去
config实现网关配置:
@Configuration
public class RouteConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder){
return builder.routes()
.route("test-id",r->r.path("/doLogin").uri("http://localhost:8081/")) 1:
.route("test-id",r->r.path("/doLogin").uri("http://localhost:8082/doLogin")) 2: 注:1,2等价
.build();
}
}
释:
config类实现的网关配置与yml不冲突
当gateway的端口为80时,进行如上配置,对 localhost/doLogin 进行访问,就会跳转到http://localhost:8081/doLogin网址下
即:ip+网关端口+path属性 会跳转到对应的 uri+path 所示的路径下
gateway动态路由:
yml配置【网关服务内】:
server:
port: 80
spring:
application:
name: gateway-server
cloud:
gateway:
enabled: true #默认开启
routes:
- id: login-service-route
# uri: http://localhost:8081
# uri: lb://login-service 【一】
predicates:
- Path=/doLogin #指定请求路径
- After=日期 #指定日期后这个请求才允许访问 【三】断言,不符合即404
- Method=GET,POST #指定请求格式 断言 不能 对动态路由生效
- Query=name,url #指定请求必须要携带的参数
- . . .
discovery:
locator:
enabled: true #开启动态路由 开启通过应用名称发现服务的功能 【二】
lower-case-service-id: true #开启服务名称小写
eureka:
client:
service-url:
defaultZone: http://192.168.159.130:8761/eureka
registry-fetch-interval-seconds: 3 #拉取服务列表的间隔
instance:
hostname: localhost
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
释:
配置enabled: true和lower-case-service-id: true后,可以通过 服务名称+path的方式进行访问 ,并且同名服务负载均衡
最终这些 服务名+path 的请求将会发送到uri下所指的路径,使用 lb 则指定转发到的服务名称,所有同名服务负载均衡访问
gateway过滤器:
一般用法:
gatewayFilter:针对某个服务
记录该服务的访问次数
进行限流操作
globalFilter:针对全局
ip校验
token校验
参数校验【防止sql注入】
全局过滤器:
创建filter目录,下设过滤器类:【如下】
@Component 注入容器
@Order(-1) 设置优先级
public class MyGlobalFilter implements GlobalFilter { 继承全局过滤器
/**
* 疑似使用了webFlux的api(
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
/**
* 过滤请求,拿到请求相关参数
*/
ServerHttpRequest request = exchange.getRequest();
对请求的过滤
// 获取路径【http请求端口号后的所有路径,也能将动态路由到的路径拿来】
String path = request.getURI().getPath();
System.out.println(path);
// 获取请求头
HttpHeaders headers = request.getHeaders();
System.out.println(headers);
// 获取方法【请求的形式,如:Get,Post】
String name = request.getMethod().name();
System.out.println(name);
// 获取方法名
String methodName = request.getRemoteAddress().getHostName();
System.out.println(methodName);
// 获取远端地址内的主机ip【】
String hostName = request.getRemoteAddress().getHostName();
System.out.println(hostName);
/**
* 过滤响应,拿到响应相关参数
*/
ServerHttpResponse response = exchange.getResponse();
对响应反馈的过滤【进行了失败相关的反馈】
/**
* 模拟拦截,并发送json错误提示
*/
// 通过json进行前后端间的交互
// 设置响应头里的字符编码,防止中文乱码
response.getHeaders().set("context-type","application/json;charset=utf-8");
// 组装业务返回值
HashMap<Object, Object> map = new HashMap<>();
map.put("code", HttpStatus.UNAUTHORIZED.value()); 传入json值到哈希表【UNAUTHORIZED:错误反馈码】
map.put("msg","未授权");
// 字符转换器
ObjectMapper objectMapper = new ObjectMapper(); hashMap转换成数组
byte[] bytes = new byte[0];
try {
bytes = objectMapper.writeValueAsBytes(map);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
// 通过buffer工厂将字节数组包装成一个数据包
DataBuffer wrap = response.bufferFactory().wrap(bytes); 未知部分【涉及知识盲区】
return response.writeWith(Mono.just(wrap));
// 放行到下一个过滤器
// return chain.filter(exchange);
}
}
对ip权限的判断及反馈模拟:
@Component
@Order(-2)
public class IpCheckFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 拿到请求
ServerHttpRequest request = exchange.getRequest();
// 拿到ip
String ip = request.getHeaders().getHost().getHostString();
// 从数据库表内查询这个ip是否在黑名单
// 此处用true代替了判断
if(true){ 此处进行ip的审核,从数据库获取并比对权限【注:不要在网关内对Mysql进行访问,效率过低】
// 放行
return chain.filter(exchange);
}
// 如判定失败,没有权限,走如下代码
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().set("content-type","application/json;charset=utf-8");
HashMap<String, Object> stringOrderHashMap = new HashMap<>();
stringOrderHashMap.put("code",438); 自定义的错误反馈码
stringOrderHashMap.put("msg","false");
// 转换器
ObjectMapper objectMapper = new ObjectMapper();
byte[] bytes = new byte[0];
try {
// 将哈希表转换成字节型数组
bytes = objectMapper.writeValueAsBytes(stringOrderHashMap);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
// 打包成数据包
DataBuffer wrap = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(wrap));
}
}
token校验:【注意开启redis】
流程:
示意图:
具体配置:
以下程序执行逻辑:
执行设置为网关拦截器白名单的login-service方法,传入用户名和密码,在mysql数据库内验证用户名和密码,
判定无误后login-service生成一个token令牌,将这个令牌存入redis数据库
这时,对网关内白名单之外的服务进行访问,会因没有权限而被token过滤器拦截
当在请求内的Authorization内添加了token令牌后,token过滤器会检测到这个令牌,并对该请求放行
pom依赖:【需要redis的有注册服务【login-service】和网关服务】
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置过滤器:
@Order(2)
@Component
public class TokenCheckFilter implements GlobalFilter { 继承过滤器
// 指定被放行的路径【白名单】
public static final List<String> ALLOW_URL = Arrays.asList("/login-service/doLogin","/myUrl");
创建一个链表【ALLOW_URL】,对允许放行的路径进行设置
// 连接redis
@Autowired
private StringRedisTemplate stringRedisTemplate;
进行使用redis的配置
/**
* 确认token的位置:一般在请求头,一般以Authorization【授权】为key,bearer前缀 + token为value
* 1 . 拿到请求url
* 2 . 判断放行白名单用户
* 3 . 拿到请求头
* 4 . 拿到token
* 5 . 校验
* 6 . 放行/拦截
*
* @param exchange
* @param chain
* @return
*/
@Override 重写过滤器方法
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取请求
ServerHttpRequest request = exchange.getRequest();
// 获取路径
String path = request.getURI().getPath();
// 如果在白名单,放行
if (ALLOW_URL.contains(path)){
return chain.filter(exchange);
}
//检查
// 拿到请求头
HttpHeaders headers = request.getHeaders();
// 拿到token令牌【Authorization可以设置多个】
List<String> authorization = headers.get("Authorization"); post请求内的令牌存放以Authorization为key
// 如果有这个key
(检查集合 是否为空 的小组件 【注意取反】
if (!CollectionUtils.isEmpty(authorization)){
// 获取token
String token = authorization.get(0);
// 判断是否为空
(检查字符串 是否有值 的小组件
if (StringUtils.hasText(token)){
// 将固定前缀【bearer 】替换去掉,获取真实的token 注意,是【bearer空格】
String realToken = token.replaceFirst("bearer ", ""); 前缀替换为空,即删去
// 判断是否为空且redis内是否包含这个token
if (StringUtils.hasText(realToken)&&stringRedisTemplate.hasKey(realToken)){
return chain.filter(exchange); 放行
}
}
}
//能走到这里,就说明没有权限,需要拦截
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().set("content-type","application/json;charset=utf-8");
HashMap<String, Object> map = new HashMap<>(4);
// 返回401
map.put("code", HttpStatus.UNAUTHORIZED.value());
map.put("msg","未授权");
ObjectMapper objectMapper = new ObjectMapper();
byte[] bytes = new byte[0];
try {
bytes = objectMapper.writeValueAsBytes(map);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
DataBuffer wrap = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(wrap));
}
}
login-service的controller配置:
@RestController
public class LoginController {
@Autowired
public StringRedisTemplate redisTemplate;
@GetMapping("doLogin")
public String doLogin(String name, String pwd){
System.out.println(name);
System.out.println(pwd);
// 假设查询了数据库
此处应该对mysql进行查询,查看请求内包含的用户名和密码是否正确
User user = new User(1,name,pwd,18);
// 获取到一个token令牌
String token = UUID.randomUUID().toString();
// 存进redis
数据有效时长【即,超出7200秒后,redis内的这个token就会自动消失】
redisTemplate.opsForValue().set(token,user.toString(), Duration.ofSeconds(7200));
return token; 将自动生成的token令牌返回到页面
}
}
gateway的redis限流:
关于令牌和桶机制:
网关依赖:【修改redis配置】
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
yml配置:
server:
port: 80
spring:
application:
name: gateway-server
redis:
host: localhost
port: 6379
cloud:
gateway:
enabled: true #默认开启
routes:
- id: login-service-route
uri: http://localhost:8081
# uri: lb://login-service
predicates:
- Path=/doLogin
filters:
- name: RequestRateLimiter 在对应的断言内配置过滤器
args:
key-resolver: '#{@ipKeyResolver}' #通过spel表达式获取ioc容器内bean的值,此处取到的是ip过滤器的值
redis-rate-limiter.replenishRate: 1 #生成令牌的速度
redis-rate-limiter.burstCapacity: 3 #桶容量
discovery:
locator:
enabled: true #开启动态路由 开启通过应用名称发现服务的功能
lower-case-service-id: true #开启服务名称小写
eureka:
client:
service-url:
defaultZone: http://192.168.159.130:8761/eureka
registry-fetch-interval-seconds: 3 #拉取服务列表的间隔
instance:
hostname: localhost
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
在config下进行过滤器配置:
@Configuration
public class RequestLimitConfig {
/**
* 针对某一个IP限流
*/
@Bean
@Primary //主要的
public KeyResolver ipKeyResolver(){
return exchange -> Mono.just(exchange.getRequest().getHeaders().getHost().getHostString());
}
/**
* 针对某一个路径限流
*/
@Bean
public KeyResolver apiKeyResolver(){
return exchange -> Mono.just(exchange.getRequest().getPath().value());
}
}
Nacos配置中心
差异:
1 . Nacos的服务中心NacosServer是官方搭建好的,而EurekaServer的是需要自己配置的
2 . Nacos服务端登录默认就有密码设置,EurekaServer需要自己继承SpringSecurity进行安全设置
3 . Nacos隔离的更彻底,
Eureka内两个应用注册了同名方法时,会根据服务名形成一种类似集群的形态,导致对服务的访问在集群内随机挑选一个服务,可能会因此报错
Nacos内有namespace的概念,不同namespace内的服务默认情况下是不可互相访问的,namespace下还有group概念,进行进一步隔离
报错:
1 . Nacos 2.X的客户端不能注册进入Nacos 1.X的客户端
报错:
Request nacos server failed:
namingService subscribe failed, properties:NacosDiscoveryProperties{serverAddr='localhost:8848', endpoint='', namespace='', watchDelay=30000, logName='', service='nacos-client-a', weight=1.0, clusterName='DEFAULT', group='DEFAULT_GROUP', namingLoadCacheAtStart='false', metadata={preserved.register.source=SPRING_CLOUD}, registerEnabled=true, ip='192.168.157.1', networkInterface='', port=-1, secure=false, accessKey='', secretKey='', heartBeatInterval=null, heartBeatTimeout=null, ipDeleteTimeout=null, failFast=true}
Application类:
@SpringBootApplication
@EnableDiscoveryClient 2.X版本这个注解不再必须配置
public class NacosClientBApplication {
public static void main(String[] args) {
SpringApplication.run(NacosClientBApplication.class, args);
}
}
yml配置:
server:
port: 8081
spring:
application:
name: nacos-client-b
cloud:
nacos:
server-addr: localhost:8848 #Nacos服务端
username: nacos 用户名密码
password: nacos
discovery:
namespace: 6b7ad443-78b1-4ce1-b3ff-4f75f3a78960 #注册进的命名空间的id
group: A_GROUP #注册进的组
service: user-service #Nacos列表内的服务名,不进行配置时,默认以applicationName为服务名
Nacos服务发现:
controller:
@RestController
public class TestController {
@Autowired
public DiscoveryClient discoveryClient;
@GetMapping("test")
public String test(){
List<ServiceInstance> instances = discoveryClient.getInstances("user-service"); 发现指定名称的服务【必须在同一个namespace,同一个group】
System.out.println(instances); 对此处添加断点,当运行到此处 size=1 时。说明发现成功, size=2时,发现失败
return "success ...";
}
}
Nacos集成Gateway和OpenFeign
Gateway的yml文件:
server:
port: 80
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848
username: nacos
password: nacos
discovery:
namespace: 6b7ad443-78b1-4ce1-b3ff-4f75f3a78960
group: A_GROUP
gateway:
discovery:
locator:
enabled: true #动态路由开启
lower-case-service-id: true #服务名称转小写
pom依赖:
以下配置替代了parents
<dependencyManagement>
<dependencies>
<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-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<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>
</dependencies>
</dependencyManagement>
注:Nacos来自于alibabaDependence,而Gateway所需的组件来自springclouddependence,所以都需要配置
至于版本匹配问题,查阅github文档
配置完成Gateway后,在服务消费者内添加openfeign组件相关的依赖【注:openFeign组件同样来自 原生spring cloud dependence 内,也需要添加对应的dpendence management】
feign配置与eureka时相同:
@FeignClient(value = "user-service") 注意,此处是注册入Nacos时配置的那个服务名
public interface TestFeign {
@GetMapping("info") 需要进行跨服务调用的方法的方法签名
public String info();
}
接下来,对gateway调用指定服务的指定方法,在动态路由和feign远程调用的作用下,访问就顺利成功了
nacos配置文件中心:
可以在nacos内创建yaml配置文件,供集群等对相同文件进行复用的情况的出现
直接在nacos内 配置详情 处创建,定义文件类型,注意namespace和group,在 配置内容 部分进行配置文件的书写
【注:当想要让配置中心内的配置文件返回修改前的状态 即:回滚,可以点击 更多 ->历史版本 选项,里面存放了三十天之内的全部历史版本,存放在数据库内】
对于想要使用这个文件的服务,在bootstrap.yml内进行如下配置,即可正常使用:
server:
port: 8081
spring:
application:
name: nacos-config-a
cloud:
nacos: 原本application.yml内的配置
config:
server-addr: localhost:8848
username: nacos
password: nacos
prefix: nacos-config-a #读取的配置文件的名称 新增
file-extension: yml #读什么类型的文件
profiles: 这个值将以 - 值 的形式拼接在prefix后面,构成配置文件名
active: dev 没有这个配置时,prefix后的名称就是配置文件的名称
这种情况下,将会读取配置中心内名称为: nacos-config-dev.yml 的文件【这种格式时,文件类型后缀是必须的】
domain读取配置中心的文件进行初始化:
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component 注入ioc容器
@RefreshScope 将这个类加入动态刷新的作用域内【添加这个注解后,对配置中心的文件进行修改后,不再需要刷新对应服务,即可生效】
public class Person {
@Value("${person.name}")
private String name;
@Value("${person.age}")
private Integer age;
@Value("${person.address}")
private String address;
}
Controller读取配置中心的文件:
@RestController
public class TestController {
@Autowired
public Person person;
@GetMapping("info")
public String infoPerson(){
return person.toString(); 注:未知原因,此处不能直接返回person对象,会报错或者重复打印
}
}
同时使用配置中心内多个不同group的配置文件:【同namespace】
yml配置:【方法一】
server:
port: 8082
spring:
application:
name: nacos-config-test
cloud:
nacos:
config:
server-addr: localhost:8848 Nacos服务端IP
username: nacos
password: nacos
namespace: c96607d7-016a-454c-b978-faea64c68ecb namespace的ID
file-extension: yml
extension-configs: #读多个配置文件,需要在同一个命名空间内
- data-id: user-center-dev.yml #文件名
group: A_GROUP #group名 #指定第一个配置文件
refresh: true #支持动态刷新
- data-id: member-center-dev.yml #文件名
group: B_GROUP #group名 #指定第二个配置文件
refresh: false #不支持动态刷新
注:使用这种方式时,不能使用 profiles:active 进行拼接了
yml配置:【方法二】【使用 共享配置文件 的方式】
【一】
server:
port: 8082
spring:
application:
name: nacos-config-test
cloud:
nacos:
config:
server-addr: localhost:8848
username: nacos
password: nacos
namespace: c96607d7-016a-454c-b978-faea64c68ecb
prefix: user-center
file-extension: yml 第一个配置文件【组A_GROUP内】
group: A_GROUP
shared-configs: #共享配置文件
- application-dev.yml #这里填写共享的文件名称【只能在 DEFAULT_GROUP 内】
profiles:
active: dev
【二】
server:
port: 8082
spring:
application:
name: nacos-config-test
cloud:
nacos:
config:
server-addr: localhost:8848
username: nacos
password: nacos
namespace: c96607d7-016a-454c-b978-faea64c68ecb
prefix: user-center
file-extension: yml 第一个配置文件【组A_GROUP内】
group: A_GROUP
shared-configs: #共享配置文件
- data-id: application-dev.yml
group: C_GROUP 这样可以将任意组内的共享配置文件进行配置,并可以进行 动态刷新 的配置
refresh: true
profiles:
active: dev
【这几种方式,都成功读取到了不同 组(group) 的配置文件】
关于使用了配置中心的远端配置文件后,bootstrap.application配置文件和远端配置文件分别要配置什么:
bootstrap.yml:【本地】
1 . 应用名称spring:application:name
2 . nacos的 注册 和 远端配置文件 的 拉取
远端:【将能放在远端的东西都放在远端,这样修改后不需要重启服务】
1 . 端口
2 . 数据源
3 . Redis
4 . Mq
….