面向服务编程
降低耦合
服务粒度比较粗
完全解耦
服务粒度:根据业务,单独把功能抽取成一个独立的服务,对外暴露HTTP接口
电商类
- 分类服务
- 商品服务【增加、删除、修改、简单条件的搜索(对卖家)】
- 搜索服务(对买家)【Elasticsearch】
- 首页服务
- 内容服务
- …
教育类
- www.qfedu.com
是一套工具集,是多个框架的组合
是一种开发设计的思想,是SOA架构的进化后产物
微服务架构风格是将具备独立功能的业务模块拆成一个服务【应用】,对外暴露HTTP接口,提供对其他服务进行远程通信的功能。
一个微服务可以具备自己独立的业务功能和自己独立的数据库及独立的开发语言
一个微服务就是一个SpringBoot项目
微服务架构是一种风格,是一个种思想,是一种理念
SpringCloud是具体的落地实现,是技术栈
是多个框架的组合
实现技术栈
SpringCloud Netflix
Eureka、Ribbon、OpenFeign、Hystrix、Zuul、Config、Bus…
SpringCloud Alibaba
Nacos、Sentinel…
https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E |
---|
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.2.RELEASEversion>
parent>
继承父工程
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
dependencies>
|
@SpringBootApplication
public class DeviceProviderApplication8001 {
public static void main(String[] args) {
SpringApplication.run(DeviceProviderApplication8001.class, args);
}
}
server:
port: 8001
@RestController
@RequestMapping("/device")
public class DeviceController {
@Value("${server.port}")
String port;
@GetMapping("/hello")
public String hello(){
return "hello:" + port;
}
}
继承父工程
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
dependencies>
通过插件生成引导类和配置文件
@SpringBootApplication
public class DeviceConsumerApplication9001 {
public static void main(String[] args) {
SpringApplication.run(DeviceConsumerApplication9001.class, args);
}
}
server:
port: 9001
增加远程调用接口bean【RestTemplate】
RestTemplate |
---|
|
@RestController
@RequestMapping("/device/consumer")
public class DeviceConsumerController {
@Autowired
RestTemplate restTemplate;
@Value("${server.port}")
String port;
@GetMapping("/hello")
public String hello(){
//调用8001的 /device/hello 接口
/**
* 参数一:请求的url地址
* 参数二:目标接口返回值类型
*/
String url = "http://localhost:8001/device/hello";
String result = restTemplate.getForObject(url, String.class);
return port + " --> " + result;
}
}
getForObject方法 |
---|
getForEntity方法 |
---|
请求方式跟Get不一样,其他功能一模一样
服务提供者8002
除Controller接口和端口号不一样,其他都跟8001服务提供者一样
@RestController
@RequestMapping("/device")
public class DeviceController {
@Value("${server.port}")
String port;
@GetMapping("/get")
public String testGetVars(String name, Integer num){
return "get:" + name + "-" + num;
}
}
服务消费者9002
除Controller接口和端口号不一样,其他都跟9001服务提供者一样
/**
* RestTemplate getForEntity
* @param name
* @param num
* @return
*/
@GetMapping("/get/map/entity")
public String getMapEntity(String name, Integer num){
//调用8002的 /device/get 接口,需要传递两个参数
//http://localhost:8002/device/get?name=lucy&num=10
String url = "http://localhost:8002/device/get?name={name}&num={num}";
Map<String, Object> map = new HashMap<>();
map.put("name", name);
map.put("num", num);
/**
* 参数一:请求的url地址
* 参数二:目标接口返回值类型
* 参数三:Map
*/
ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class, map);
System.out.println("---------响应头---------");
HttpHeaders headers = responseEntity.getHeaders();
for (String key : headers.keySet()) {
System.out.println(key + ":" + headers.get(key));
}
System.out.println("----------状态码-----------");
HttpStatus status = responseEntity.getStatusCode();
System.out.println(status.value() + ":" + status.getReasonPhrase());
System.out.println("----------响应体-----------");
//响应体
String result = responseEntity.getBody();
System.out.println(result);
return port + " --> " + result;
}
/**
* RestTemplate Map参数,map中的key由占位符的名称决定
* @param name
* @param num
* @return
*/
@GetMapping("/get/map")
public String getMap(String name, Integer num){
//调用8002的 /device/get 接口,需要传递两个参数
//http://localhost:8002/device/get?name=lucy&num=10
String url = "http://localhost:8002/device/get?name={name}&num={num}";
Map<String, Object> map = new HashMap<>();
map.put("name", name);
map.put("num", num);
/**
* 参数一:请求的url地址
* 参数二:目标接口返回值类型
* 参数三:Map
*/
String result = restTemplate.getForObject(url, String.class, map);
return port + " --> " + result;
}
/**
* RestTemplate 可变参数:参数只需要占位符即可 {1}
* @param name
* @param num
* @return
*/
@GetMapping("/get/vars")
public String getVars(String name, Integer num){
//调用8002的 /device/get 接口,需要传递两个参数
//http://localhost:8002/device/get?name=lucy&num=10
String url = "http://localhost:8002/device/get?name={1}&num={2}";
/**
* 参数一:请求的url地址
* 参数二:目标接口返回值类型
* 参数三:可变参数
*/
String result = restTemplate.getForObject(url, String.class, name, num);
return port + " --> " + result;
}
getForObject【传递Map参数】 |
---|
9002访问8002即可 |
---|
使用Post请求
8002新增接口 |
---|
9002新增接口 |
---|
postman测试 |
---|
DashBoard |
---|
可以实现服务注册与发现的组件
- SpringCloud Netflix:Eureka
- SpringCloud Alibaba:Nacos
- Apache Dubbo:Zookeeper
工作原理 |
---|
3.0 父工程锁定SpringCloud的版本
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>Hoxton.SR9version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
org.springframework.cloud
spring-cloud-starter-netflix-eureka-server
server:
port: 8761
spring:
application:
name: eureka-server
#服务注册中心
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka/
#是否把自己注册到注册中心
register-with-eureka: false
#是否要拉取服务
fetch-registry: false
@EnableEurekaServer |
---|
localhost:8761 |
---|
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
spring:
application:
name: device-provider
#服务注册中心
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
spring:
application:
name: device-consumer
#服务注册中心
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
服务提供者【@EnableDiscoveryClient】 |
---|
localhost:8761 |
---|
服务消费者要使用服务提供者的接口
客户端:服务消费者9001
spring-cloud-starter-netflix-eureka-client 已经包含了ribbon |
---|
@LoadBalanced |
---|
@GetMapping("/hello2")
public String hello2(){
//使用@LoadBalanced后,需要指定服务名称来调用
String url = "http://DEVICE-PROVIDER/device/hello";
String result = restTemplate.getForObject(url, String.class);
return port + " --> " + result;
}
device-provider-8001
,修改相关端口为8003device-provider
有两个实例发现:默认是轮询
http://localhost:9001/device/consumer/hello2
,能够看到在服务提供者的两个实例中轮询在服务消费者的RibbonConfig中增加如下配置
/**
* 指定负载均衡策略为随机
* @return
*/
@Bean
public IRule rule(){
return new RandomRule();
}
注释掉全局的配置,然后在application.yml中增加如下配置
#指定具体服务的负载均衡策略
DEVICE-PROVIDER: # 编写服务名称
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 具体负载均衡使用的类
Eureka的细节与自我保护机制 |
---|
Eureka客户端会定时【每30秒】向注册中心发送心跳包进行续约
Eureka客户端超过指定时间【90秒】都没有向注册中心发送心跳包,那么注册中心就会认为服务已不可用,就会从服务列表中把该服务给剔除
Eureka客户端【消费者】会定时向注册中心更新需要服务列表的信息
配置在EurekaClient
eureka:
client:
serviceUrl:
defaultZone: http://localhost:10086/eureka/
#每隔30秒去注册中心更新服务提供者的列表信息[配置在服务消费者]
registry-fetch-interval-seconds: 30
instance:
#每30秒向注册中心发送心跳包
lease-renewal-interval-in-seconds: 30
#90秒没向注册中心发送心跳包,那么注册中心就会剔除服务
lease-expiration-duration-in-seconds: 90
配置在EurekaServer
eureka:
server:
#关闭自我保护机制
enable-self-preservation: false
如果Eureka宕机了,是否会影响服务之间的调用?
- 对于服务消费者来说,是否经过一次调用了,如果调用过并且成功,会有本地缓存,可以进行远程调用。没有经过调用,那么无法进行远程调用
- 对于服务提供者来说,新服务就无法注册,老服务就无法续约
- 在分布式系统中,CAP是无法都满足的,且P【分区容错性是一定要保证的】
- C:一致性
- A:可用性
- P:分区容错性
- 在保证分区容错性的前提性,是选择保证CP还是AP
- Eukera 保证的AP
- 在无法做到数据一致性的情况下,保证系统可用是比较重要的。因为数据不一致,是可以有补救措施的,相对而言,付出的代价会小一点【张三、李四在同一时间买同一件商品,价格不一样,这时候,一般返补差价就可以解决了】
- Zookeeper 保证的CP
- 在无法做到系统可用的情况下,保证数据一致性是比较重要的。因为系统可用不可用,是可以有补救措施的,相对而言,付出的代价会大很多【张三在早上8点发现淘宝不可用,下午淘宝就上头条新闻了,造成的损失是很大的】
- 需要向对方注册自己的信息
- 允许拉取服务列表
eureka-server-8761
的配置文件【application.yml】#单机版
#eureka:
# client:
# service-url:
# defautZone: http://localhost:8761/eureka/
# fetch-registry: false
# register-with-eureka: false
#集群
eureka:
client:
service-url:
defautZone: http://localhost:8762/eureka/
fetch-registry: true
register-with-eureka: true
复制eureka-server-8761
项目,修改端口为8762
#集群
eureka:
client:
service-url:
defautZone: http://localhost:8761/eureka/
fetch-registry: true
register-with-eureka: true
device-provider-8001
和device-consumer-9001
配置文件eureka:
client:
service-url:
#单机版
#defaultZone: http://localhost:8761/eureka/
#向两个注册中心注册信息
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
测试
- 停止8761的注册中心
- 过一会,大概1分钟左右,刷新8762,可以看到8001、9001服务信息被同步过来了
- 再尝试进行http调用,结果仍旧可以调得通
停止8761的注册中心,等8762同步数据 |
---|
|
Controller调用Service一样
https://spring.io/projects/spring-cloud-openfeign
大家也可以改9001项目,增加openfeign依赖
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
dependencies>
//path : 请求的统一前缀
@FeignClient(name = "device-provider", path = "/device")
public interface DeviceFeign {
//@RequestMapping(value = "/device/hello") //不指定path属性
@RequestMapping(value = "/hello")
String hello();
}
@EnableFeignClients |
---|
消费者控制器接口 |
---|
Feign底层 |
---|
//=================fegin参数传递开始==========
//普通参数
@GetMapping("/testVar")
public String testVar(Integer id){
return "testVar " + port + ":" + id;
}
//路径传参
@GetMapping("/testUrlVar/{name}")
public String testUrlVar(@PathVariable String name){
return "testVar " + port + ":" + name;
}
//多个参数
@GetMapping("/testManyVar")
public String testManyVar(Integer id, String name){
return "testManyVar " + port + ":" + id + " " + name;
}
//实体参数
@PostMapping("/testJson")
public Device testJson(@RequestBody Device device){
return device;
}
//实体参数
@PostMapping("/testJsonCookie")
public String testJsonCookie(@RequestBody Device device, @CookieValue(name = "token", required = false) String token){
System.out.println("device = " + device);
return token;
}
//=================fegin参数传递结束==========
public class CookieInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
String token = "";
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttr = (ServletRequestAttributes) requestAttributes;
HttpServletRequest request = requestAttr.getRequest();
Cookie[] cookies = request.getCookies();
if(!Objects.isNull(cookies)) {
for (Cookie cookie : cookies) {
//客户端传递过来的是login_token
if("login_token".equals(cookie.getName())){
token = "token=" + cookie.getValue();
break;
}
}
}
System.out.println("token-->" + token);
requestTemplate.header(HttpHeaders.COOKIE, token);
}
}
//=================fegin参数传递开始==========
//普通参数
@RequestMapping(value = "/testVar")
String testVar(@RequestParam(name = "id") Integer id);
//路径传参
@RequestMapping(value = "/testUrlVar/{name}")
String testUrlVar(@PathVariable(name = "name") String name);
//多个参数
@RequestMapping(value = "/testManyVar")
String testManyVar(@RequestParam(name = "id") Integer id, @RequestParam(name = "name") String name);
//实体参数
@RequestMapping(value = "/testJson")
Device testJson(@RequestBody Device device);
//实体参数
@RequestMapping(value = "/testJsonCookie")
//public String testJson(@RequestBody Device device, @CookieValue(name = "token") String token);
String testJsonCookie(@RequestBody Device device);
//=================fegin参数传递结束==========
Fegin类上增加配置属性 |
---|
//使用Feign调用,参数详解
//普通参数
@GetMapping("/testVar")
public String testVar(Integer id){
String result = deviceFeign.testVar(id);
return port + "-->" + result;
}
//路径传参
@GetMapping("/testUrlVar/{name}")
public String testUrlVar(@PathVariable String name){
String result = deviceFeign.testUrlVar(name);
return port + "-->" + result;
}
//多个参数
@GetMapping("/testManyVar")
public String testManyVar(Integer id, String name){
String result = deviceFeign.testManyVar(id, name);
return port + "-->" + result;
}
//实体参数
@PostMapping("/testJson")
public String testJson(@RequestBody Device device){
Device device1 = deviceFeign.testJson(device);
return port + "-->" + device1.getDeviceName() + (device1.getPrice() * 100);
}
//实体参数
@PostMapping("/testJsonCookie")
public String testJsonCookie(@RequestBody Device device, @CookieValue(name = "login_token", required = false) String token){
//result : 就是token
String result = deviceFeign.testJsonCookie(device);
return port + "-->" + result;
}
//=================fegin参数传递结束==========
测试Cookie |
---|
触电保护器
】服务雪崩 |
---|
9001应用
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
@EnableHystrix //开启熔断
引导类加注解 |
---|
|
|
|
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
#开启熔断
feign:
hystrix:
enabled: true
#openfeign中如果使用到了拦截器,需要加该配置【信号量管理】
hystrix:
command:
default:
execution:
isolation:
#隔离策略之信号量
strategy: SEMAPHORE
DeviceFeignFallback |
---|
DeviceFeign |
---|
提供者接口 |
---|
消费者设置服务超时时间【全局配置】 |
---|
#openfeign中如果使用到了拦截器,需要加该配置【信号量管理】
hystrix:
command:
default:
execution:
isolation:
#隔离策略之信号量
strategy: SEMAPHORE
#线程隔离
thread:
timeoutInMilliseconds: 2000
#默认超时时间为1S
ribbon:
ReadTimeout: 2000
ConnectionTimeout: 2000
工作原理 |
---|
6.1 配置熔断仪表盘【了解】
org.springframework.cloud
spring-cloud-starter-netflix-hystrix-dashboard
@WebServlet("/hystrix.stream")
public class HystrixDashboardServlet extends HystrixMetricsStreamServlet {
}
@EnableHystrixDashboard
@ServletComponentScan(basePackages = "com.qf.java2107.device.servlets")
http://localhost:9001/hystrix
http://localhost:9001/hystrix |
---|
第一次看到的是一个Loading
监控熔断的界面 |
---|
6.2 测试熔断器效果
static Integer count = 0;
@HystrixCommand(fallbackMethod = "hystrixError", commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled",value = "true"),
//请求总数
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),
//失败比例
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "50"),
//休眠时间
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "5000")
})
@RequestMapping("/test/hystrix")
public String testHystrix(Integer id){
if(id == 1) {
throw new RuntimeException("id不能为1");
}
return "服务正常:" + id;
}
public String hystrixError(Integer id){
count++;
return "熔断成功:" + count;
}
@EnableCircuitBreaker //开启熔断
达到熔断器打开的条件 |
---|
8:00-8:50 晨读 (前一天的知识、面试题)
8:50-9:05 晨考(前一天的知识)
9:05-10:20 第一节课
10:20-10:40 课间休息
10:40-11:30 第二节课
11:30-12:00 整理上午的内容 上午答疑 完成上午的任务
14:00-15:20 第一节课
15:20-15:40 课间休息
15:40-16:30 第二节课
16:30-16:50 课间休息
16:50-18:00 整理整天的内容 下午答疑 完成作业
19:00-21:30 自习 自己思考:瞻前 顾后,需要独立思考的时间,这个时间绝对不能被看视频占用了。
21:30-22:30 小组成员一对一互面 (班长:拍一个小视频10s,发到微信群)
碰到问题怎么办?
在服务的调用过程中,如果都需要通过服务的ip和端口来调用,由于ip地址是变化的,所以调用方需要频繁的调整ip,这样就比较麻烦,通过网关来解决服务转发的功能。除此之外,网关还具有其他的作用:
网关非常重要,在企业中网关是一定要先开发的。网关可以使用以下组件来实现:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-zuulartifactId>
dependency>
spring:
application:
name: zuul-server
server:
port: 8763
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
# zuul路由表
zuul:
routes:
api-ribbon-consumer:
path: /api/ribbon/**
serviceId: DEVICE-CONSUMER
api-feign-consumer:
path: /api/feign/**
serviceId: DEVICE-CONSUMER-OPENFEIGN
@SpringBootApplication
@EnableZuulProxy
public class ZuulServer8763Application {
public static void main(String[] args) {
SpringApplication.run(ZuulServer8763Application.class, args);
}
}
访问服务A:http://localhost:8763/api/ribbon/device/consumer/hello
访问服务B:http://localhost:8763/api/feign/device/consumer/hello
# zuul路由表
zuul:
routes:
api-ribbon-consumer:
path: /ribbon/**
serviceId: DEVICE-CONSUMER
# path中的路径没有被剥离,也就是说path中的路径是在服务中真实存在的。
strip-prefix: false
api-feign-consumer:
path: /feign/**
serviceId: DEVICE-CONSUMER-OPENFEIGN
# 统一前缀
prefix: /api
# strip-prefix: false
# zuul路由表
zuul:
routes:
api-ribbon-consumer:
path: /ribbon/**
serviceId: DEVICE-CONSUMER
# path中的路径没有被剥离,也就是说path中的路径是在服务中真实存在的。
strip-prefix: false
api-feign-consumer:
path: /feign/**
serviceId: DEVICE-CONSUMER-OPENFEIGN
# 统一前缀
prefix: /api
# 保护敏感路径:一旦路径中带有admin,表示敏感路径,不做转发
ignored-patterns: /**/admin/**
spring:
application:
name: zuul-server
server:
port: 8763
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
# zuul路由表
zuul:
routes:
api-ribbon-consumer:
path: /ribbon/**
serviceId: DEVICE-CONSUMER
# path中的路径没有被剥离,也就是说path中的路径是在服务中真实存在的。
strip-prefix: false
api-feign-consumer:
path: /feign/**
serviceId: DEVICE-CONSUMER-OPENFEIGN
# 统一前缀
prefix: /api
# 保护敏感路径:一旦路径中带有admin,表示敏感路径,不做转发
# ignored-patterns: /**/admin/**
# 保护敏感头 把默认的sensitive-headers配置的cookie 删除掉
sensitive-headers:
# strip-prefix: false
package com.qf.zuul.server.fallback;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* @author Thor
* @公众号 Java架构栈
*/
@Component
public class MyZuulFallback implements FallbackProvider {
/**
* 如果要对zuul中配置的所有路由的服务进行错误回调(熔断),return null
* 如果要对指定的服务进行错误回调,return "服务ServiceID"
* @return
*/
@Override
public String getRoute() {
return "DEVICE-CONSUMER";
//return null;
}
/**
* 当服务出现错误后具体要返回的内容,在这个方法里定义
* @param route
* @param cause
* @return
*/
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
return new ClientHttpResponse() {
//响应行:状态码
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
//响应行
@Override
public int getRawStatusCode() throws IOException {
return HttpStatus.OK.value();
}
//响应行
@Override
public String getStatusText() throws IOException {
return HttpStatus.OK.toString();
}
@Override
public void close() {
}
/**
* 响应体:
* 返回一个指定的json字符串
* @return
* @throws IOException
*/
@Override
public InputStream getBody() throws IOException {
//把对象转换成json字符串——jackson
ObjectMapper objectMapper = new ObjectMapper();
//封装一个携带数据的map
Map<String,Object> map = new HashMap<>();
map.put("code",1000);
map.put("message","fall back");
map.put("data",null);
//把map转换成json
String json = objectMapper.writeValueAsString(map);
//获得拥有json字节数据的字节流
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));
return byteArrayInputStream;
}
/**
* 响应头:
* 返回的数据是json
* @return
*/
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
zuul有四种过滤器:
继承ZuulFilter,配置过滤器
package com.qf.zuul.server.filters;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* @author Thor
* @公众号 Java架构栈
*/
@Component
public class MyZuulFilter extends ZuulFilter {
@Autowired
private RedisTemplate redisTemplate;
/**
* 过滤器的类型
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* 相同类型的过滤器的顺序
* 0 表示第一个执行
* @return
*/
@Override
public int filterOrder() {
return 1;
}
/**
* 是否执行该过滤器:过滤器会被调用,但是可以通过这个方法来手动告知是否执行该过滤器。
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 存放的具体的业务逻辑,怎么过滤?
* 做一个计数器限流:
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
//获得一个操作zuul过滤器的上下文对象
RequestContext context = RequestContext.getCurrentContext();
String limitRedisKey = "my:limit";
Long count = redisTemplate.opsForValue().increment(limitRedisKey);
if(count<5){
if(count==1){
redisTemplate.expire(limitRedisKey,60, TimeUnit.SECONDS);
}
//放行
context.setSendZuulResponse(true);
return null;
}
//不放行
context.setSendZuulResponse(false);
context.setResponseStatusCode(200);
context.setResponseBody("request limited!");
return null;
}
}
package com.qf.zuul.server.filters;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.stereotype.Component;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;
/**
* 验证登录的过滤器
* @author Thor
* @公众号 Java架构栈
*/
@Component
public class MyLoginFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
/**
* 验证是否已登陆
* 看cookie中有没有login_token
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
//1.获得Cookie
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest httpServletRequest = context.getRequest();
Cookie[] cookies = httpServletRequest.getCookies();
if(Objects.nonNull(cookies)){
for (Cookie cookie : cookies) {
if("login_token".equals(cookie.getName())){
//表示已登陆: ->sso的验证是否已登陆的接口 返回是否已登陆
context.set("loginToken",cookie.getValue());
//放行?
context.setSendZuulResponse(true);
return null;
}
}
}
//未登陆
//不放行?
context.setSendZuulResponse(false);
context.setResponseStatusCode(200);
context.setResponseBody("no login!");
return null;
}
}
package com.qf.zuul.server.filters;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* @author Thor
* @公众号 Java架构栈
*/
@Component
public class MyZuulFilter extends ZuulFilter {
@Autowired
private RedisTemplate redisTemplate;
/**
* 过滤器的类型
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* 相同类型的过滤器的顺序
* 0 表示第一个执行
* @return
*/
@Override
public int filterOrder() {
return 1;
}
/**
* 是否执行该过滤器:过滤器会被调用,但是可以通过这个方法来手动告知是否执行该过滤器。
* 如果未登陆,第二个过滤器就不执行
* @return
*/
@Override
public boolean shouldFilter() {
RequestContext context = RequestContext.getCurrentContext();
String loginToken = (String) context.get("loginToken");
if(StringUtils.isNotBlank(loginToken)){
//已登陆
return true;
}
//未登陆
return false;
}
/**
* 存放的具体的业务逻辑,怎么过滤?
* 做一个计数器限流:
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
//获得一个操作zuul过滤器的上下文对象
RequestContext context = RequestContext.getCurrentContext();
String limitRedisKey = "my:limit";
Long count = redisTemplate.opsForValue().increment(limitRedisKey);
if(count<5){
if(count==1){
redisTemplate.expire(limitRedisKey,60, TimeUnit.SECONDS);
}
//放行
context.setSendZuulResponse(true);
return null;
}
//不放行
context.setSendZuulResponse(false);
context.setResponseStatusCode(200);
context.setResponseBody("request limited!");
return null;
}
}
服务越来越多,对服务的配置文件也需要统一管理及实现服务配置文件的热更新。
此时,就需要一个配置中心的服务专门来做这个事情。
新建git仓库,往仓库中存放配置文件
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-config-serverartifactId>
dependency>
spring:
application:
name: config-server
cloud:
config:
server:
git:
# git仓库的地址
uri: https://gitee.com/nn214490523/java2107.git
# 具体的文件夹
search-paths: repo
# 默认的分支
default-label: master
# 账号密码
# username: zh
# password: 123456
server:
port: 8888
package com.qf.config.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
@SpringBootApplication
@EnableConfigServer
public class ConfigServer8764Application {
public static void main(String[] args) {
SpringApplication.run(ConfigServer8764Application.class, args);
}
}
在已有的服务中,去连接分布式配置中心服务端来获取配置文件。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-configartifactId>
dependency>
spring:
cloud:
config:
uri: http://localhost:8888
# 要获取的配置文件的名称
name: device-consumer
# 环境
profile: prod
# 分支
label: master
把多个环境的文件准备好
在idea中启动配置中配置profile->dev
打包命令:
mvn clean package -Dmaven.skip.test=true
使用命令来指明环境
java -jar my-consumer-1.0-SNAPSHOT.jar --spring.profiles.active=test
Nacos组件是springcloud组件库中的一个较为推荐的注册中心和配置中心的实现组件。
startup -m standalone
localhost:8848
springboot初始化器连的代理服务器地址:https://start.aliyun.com
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
# 应用名称
spring.application.name=my-provider
# 应用服务 WEB 访问端口
server.port=8001
# Nacos帮助文档: https://nacos.io/zh-cn/docs/concepts.html
# Nacos认证信息
spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discovery.password=nacos
# Nacos 服务发现与注册配置,其中子属性 server-addr 指定 Nacos 服务器主机和端口
spring.cloud.nacos.discovery.server-addr=localhost:8848
# 注册到 nacos 的指定 namespace,默认为 public
spring.cloud.nacos.discovery.namespace=public
package com.qf.my.provider.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Thor
* @公众号 Java架构栈
*/
@RestController
@RequestMapping("/device")
public class MyDeviceController {
@GetMapping("/hello")
public String hello(){
return "hello nacos!";
}
}
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-ribbonartifactId>
<version>2.2.5.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
# 应用名称
spring.application.name=my-consumer
server.port=9001
# Nacos帮助文档: https://nacos.io/zh-cn/docs/concepts.html
# Nacos认证信息
spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discovery.password=nacos
# Nacos 服务发现与注册配置,其中子属性 server-addr 指定 Nacos 服务器主机和端口
spring.cloud.nacos.discovery.server-addr=localhost:8848
# 注册到 nacos 的指定 namespace,默认为 public
spring.cloud.nacos.discovery.namespace=public
package com.qf.my.consumer.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* @author Thor
* @公众号 Java架构栈
*/
@Configuration
public class RestConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
package com.qf.my.consumer.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.xml.ws.FaultAction;
/**
* @author Thor
* @公众号 Java架构栈
*/
@RestController
@RequestMapping("/device")
public class DeviceController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/sayHello")
public String sayHello() {
String url = "http://my-provider/device/hello";
String result = restTemplate.getForObject(url, String.class);
return result;
}
}
服务注册:Nacos Client会通过发送REST请求的方式向Nacos Server注册自己的服务,提供自身的元数据,比如ip地址、端口等信息。Nacos Server接收到注册请求后,就会把这些元数据信息存储在一个双层的内存Map中。
服务心跳:在服务注册后,Nacos Client会维护一个定时心跳来持续通知Nacos Server,说明服务一直处于可用状态,防止被剔除。默认5s发送一次心跳。
服务同步:Nacos Server集群之间会互相同步服务实例,用来保证服务信息的一致性。
服务发现:服务消费者(Nacos Client)在调用服务提供者的服务时,会发送一个REST请求给Nacos Server,获取上面注册的服务清单,并且缓存在Nacos Client本地,同时会在Nacos Client本地开启一个定时任务定时拉取服务端最新的注册表信息更新到本地缓存
服务健康检查:Nacos Server会开启一个定时任务用来检查注册服务实例的健康情况,对于超过15s没有收到客户端心跳的实例会将它的healthy属性置为false(客户端服务发现时不会发现),如果某个实例超过30秒没有收到心跳,直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)
跟Spring Cloud Config相同,Nacos实现的配置中心也需要两个角色:服务端(已经和注册中心整合在一起了)、客户端。
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
# Nacos帮助文档: https://nacos.io/zh-cn/docs/concepts.html
# Nacos认证信息
spring.cloud.nacos.config.username=nacos
spring.cloud.nacos.config.password=nacos
#spring.cloud.nacos.config.contextPath=/nacos
## 设置配置中心服务端地址
spring.cloud.nacos.config.server-addr=localhost:8848
# Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可
#spring.cloud.nacos.config.namespace=
spring.cloud.nacos.config.name=my-config
spring.cloud.nacos.config.file-extension=properties
spring.cloud.nacos.config.group=DEFAULT_GROUP
package com.qf.my.consumer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class MyConsumerApplication {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = SpringApplication.run(MyConsumerApplication.class, args);
//反复的获取配置文件中的最新的user.name
for (;;) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
String userName = applicationContext.getEnvironment().getProperty("user.name");
System.out.println("user name :" +userName);
}
}
}
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`age` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `tb_address`;
CREATE TABLE `tb_address` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`uid` bigint DEFAULT NULL COMMENT '用户id',
`address` varchar(255) DEFAULT NULL COMMENT '收件地址',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;