# SpringCloud Netfliex **Repository Path**: jiang-qikun/spring-cloud-netfliex ## Basic Information - **Project Name**: SpringCloud Netfliex - **Description**: No description available - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-03-11 - **Last Updated**: 2022-03-13 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 1:注册中心eureka ``` # 官网地址 https://github.com/Netflix/eureka/wiki ``` ### 单节点搭建 1:引入 pom 依赖 ```xml org.springframework.cloud spring-cloud-starter-netflix-eureka-server ``` 2:启动类上添加注解 ``` @EnableEurekaServer ``` 3:配置配置文件 ```yaml server: port: 7900 spring: application: name: eureka-server eureka: client: #是否将自己注册到Eureka Server,默认为true,由于当前就是server,故而设置成false,表明该服务不会向eureka注册自己的信息 register-with-eureka: false #是否从eureka server获取注册信息,由于单节点,不需要同步其他节点数据,用false fetch-registry: true #设置服务注册中心的URL,用于client和server端交流 service-url: defaultZone: http://localhost:7900/eureka/ server: # 自我保护看自己情况 enable-self-preservation: true # 续约阈值,和自我保护相关 renewal-percent-threshold: 0.85 # server剔除过期服务的时间间隔 eviction-interval-timer-in-ms: 1000 # 是否开启readOnly读缓存 use-read-only-response-cache: true # 关闭 readOnly response-cache-update-interval-ms: 1000 ``` ### euraka 集群 无法达到强数据一致性,为了保证可用性 有两种方式: 方式一:eureka 中的节点互为客户端,互相拉取数据。 ps:注册的时候向一个注册就行,一般搭建这个 方式二:不互相交流,服务独立开,服务注册的时候需要向所有的服务器注册 ps: 很清晰,简单。但是在拉取服务列表的时候需要拉取所有 eureka 节点的,请求变多了。 spring 官网有很好的例子,可以参考: ```shell https://docs.spring.io/spring-cloud-netflix/docs/current/reference/html/#registering-with-eureka ``` #### 两节点 ```yaml --- spring: profiles: peer1 eureka: instance: hostname: peer1 client: serviceUrl: defaultZone: https://peer2/eureka/ --- spring: profiles: peer2 eureka: instance: hostname: peer2 client: serviceUrl: defaultZone: https://peer1/eureka/ ``` #### 三节点 ```yaml eureka: client: serviceUrl: defaultZone: https://peer1/eureka/,http://peer2/eureka/,http://peer3/eureka/ --- spring: profiles: peer1 eureka: instance: hostname: peer1 --- spring: profiles: peer2 eureka: instance: hostname: peer2 --- spring: profiles: peer3 eureka: instance: hostname: peer3 ``` ## 2:上报节点信息 Actuator 微服务只要给 Eureka 发送心跳包,Eureka 就认为服务是可用的,但是服务如果内部出现了异常,其实已经不可用了,但是Eureka 不会让服务下线 可以通过节点信息去手动控制上下线 (适用于服务可用,但是业务不可用) 1:eureka client 端引入 maven 依赖,eureka server 端默认带了这个依赖,不需要引入 ```xml org.springframework.boot spring-boot-starter-actuator ``` 2: 服务的配置文件需要进行配置 ```yaml eureka: client: service-url: defaultZone: http://localhost:7900/eureka/ # 客户端配置健康检查,将自己真正的服务状态传给 eureka server healthcheck: enabled: true management: ## 可以手动将服务的状态置为down endpoint: shutdown: enabled: true ## 开启所有端点 endpoints: web: exposure: include: '*' ``` 3:编写手动下线的处理类 ```java @Service public class HealthStatusService implements HealthIndicator { private Boolean status = true; public void setStatus(Boolean status) { this.status = status; } @Override public Health health() { if (status) { return new Health.Builder().up().build(); } return new Health.Builder().down().build(); } public String getStatus() { return this.status.toString(); } } ``` 4:在controller 类里面提供调用接口 ``` /** * 根据服务状态:控制服务上下线 * localhost:端口/health?status=true 上线 * localhost:端口/health?status=false 下线 */ @GetMapping("/health") public String health(@RequestParam("status") Boolean status) { healthStatusSrv.setStatus(status); return healthStatusSrv.getStatus(); } ``` 5:浏览器调用接口手动将服务下线 ``` # 上线 localhost:端口/health?status=true # 下线 localhost:端口/health?status=false # 上上线完成后,可以查看节点的健康状态;也可以去 Eureka 端查看服务状态 localhost:端口/actuator/health ``` ## 3:登录验证 Security (非重点) 1:Eureka server 端加上 maven 依赖,不让随意访问 ```xml org.springframework.boot spring-boot-starter-security ``` 2:需要在 配置文件配置账号密码 ```yaml spring: security: user: name: admin password: admin ``` 3:在访问 Eureka 的服务端的时候,localhost:7900 的时候,出现一个登录界面,输入账号密码即可。 4:注意(重要):如果server 端开启了登录验证,client 注册的时候也是需要在配置文件配置账号密码的,否则注册不上去 ```yaml # 需要在注册的时候加上账号密码,这个是简单的使用,复杂的需要重写方法 eureka: client: #是否将自己注册到Eureka Server,默认为true,由于当前就是server,故而设置成false,表明该服务不会向eureka注册自己的信息 register-with-eureka: false #是否从eureka server获取注册信息,由于单节点,不需要同步其他节点数据,用false fetch-registry: true #设置服务注册中心的URL,用于client和server端交流 service-url: defaultZone: http://admin:admin@localhost:7900/eureka/ ``` 如果 client 端启动报错,注册失败 ``` Root name 'timestamp' does not match expected ('instance') for type [simple ``` 是默认开启了防止跨域攻击,需要在 server 端增加配置 ```java @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter{ @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable(); super.configure(http); } } ``` ## 4:服务提供者 1:引入 maven 依赖 ``` org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.boot spring-boot-starter-web ``` 2: 配置文件配置,为了测试负载均衡,配置了两个服务 ```yaml spring: application: name: service-provider eureka: client: service-url: defaultZone: http://localhost:7900/eureka/ --- spring: config: activate: on-profile: provider1 server: #服务端口 port: 8001 --- spring: config: activate: on-profile: provider2 server: #服务端口 port: 8002 ``` 3:controller 类提供服务 ```java @RestController public class ProviderController { @Value("${server.port}") private String port; @GetMapping("/send-message") public String sendMessage(){ return "port: "+port; } @GetMapping("test-hystrix") public String testHystrix(){ int i=1/0; return "port: "+port; } } ``` ## 5:服务调用方 RestTempte && Feign && Ribbon 1:添加 maven 依赖 ``` org.springframework.cloud spring-cloud-starter-openfeign org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.boot spring-boot-starter-web ``` 2:启动类添加注解(不适用 Feign 可以省掉这一步) ``` @EnableFeignClients ``` 3:写配置类,注入 RestTemplate ,并加上 ribbon (不使用 RestTemplate,可以省掉这一步 ) ```java @Component public class Configuration { /** * 创建 RestTemplate 并使用 Ribbon 负载均衡 */ @Bean @LoadBalanced public RestTemplate getRestTemplate(){ return new RestTemplate(); } } ``` 4: 编写 Feign 调用接口(不使用 Feign,可以省掉这一步) ```java @FeignClient(name = "service-provider") public interface ProviderFeign { @GetMapping(value = "/send-message") String sendMessage(); @GetMapping(value="/test-hystrix") String testHystrix(); } ``` 5:编写 Controller 类,调用服务方 ```java @RestController @Log4j2(topic = "ConsumerController") public class ConsumerController { @Autowired private RestTemplate restTemplate; @Autowired private ProviderFeign providerFeign; /** * RestTemplate 调用 */ @GetMapping("/rest-message") public String getMessageByRestTemplate(){ String serviceName ="service-provider"; String url ="http://"+ serviceName +"/send-message"; String result = restTemplate.getForObject(url, String.class); log.debug("使用 RestTemplate + ribbon 掉用服务的结果是:{}",result); return result; } /** * Feign 调用 */ @GetMapping("/feign-message") public String getMessageByFeign(){ String result = providerFeign.sendMessage(); log.debug("feign 掉用服务的结果是:{}",result); return result; } } ``` 5:调用方可以设置超时重试(与版本有关,有的不一定好用,建议使用限流组件) ```yaml ribbon: #连接超时 ConnectTimeout: 1000 #业务超时,3秒没有结果,会再次尝试 ReadTimeout: 3000 #同一台实例最大重试次数,不包括首次调用 MaxAutoRetries: 1 #重试负载均衡其他的实例最大重试次数,不包括首次调用 MaxAutoRetriesNextServer: 1 #是否所有操作都重试 OkToRetryOnAllOperations: false ``` ## 6:服务熔断 Hystrix 1: 调用方加上 maven 依赖,本案例加在 api-passener ```xml org.springframework.cloud spring-cloud-starter-netflix-hystrix ``` 2:启动类上加注解 ``` @EnableCircuitBreaker ``` ### 4.1:RestTemplate 集成 Hystrix 在 Controller 类中的调用方法上写上 @HystrixCommand(fallbackMethod="sendFail"),并写一个 sendFail 方法 我这个案例是在 ConsumerController 新写一个方法 ```j /** * RestTemplate 调用 Hystrix+ */ @GetMapping("/rest-message") @HystrixCommand(fallbackMethod = "sendFail") public String getMessage2ByRestTemplate(){ String serviceName ="xxx"; String url ="http://"+ serviceName +"/send-message"; String result = restTemplate.getForObject(url, String.class); log.debug("使用 RestTemplate + ribbon 掉用服务的结果是:{}",result); return result; } /** * 降级方法:此方法的 请求参数和 返回参数 要和原方法一致。 */ private String sendFail(){ return "调用服务失败,熔断"; } ``` ### 4.2:Feign 集成 Hystrix 默认情况下hystrix使用线程池控制请求隔离,针对不同的服务使用不同的线程池。并发数量超过线程池数量,不再调用服务。 1:feign 自带 Hystrix,但是默认没有打开,需要在配置文件配置 ```yaml feign: circuitbreaker: enabled: true ``` 2: 修改 Feign 调用接口,注意后面的快速失败类 ``` @FeignClient(name = "service-provider",fallbackFactory = ProviderFallBackFactory.class) ``` 3:新建快速失败类,实现 Feign 接口 ```java @Component public class ProviderFallBackFactory implements FallbackFactory { /** * 服务提供方 catch 住异常,返回错误码,这边拿到错误码,去做返回 */ @Override public ProviderFeign create(Throwable cause) { return new ProviderFeign() { @Override public String sendMessage() { return "调用 sendMessage 方法失败"; } @Override public String testHystrix() { return "调用 testHystrix 方法失败"; } }; } } ``` 4: 是在 ConsumerController 新写一个方法 ``` /** * Feign 测试 Hystrix */ @GetMapping("/feign-Hystrix") public String getMessage2ByFeign(){ String result = providerFeign.testHystrix(); log.debug("feign 掉用服务的结果是:{}",result); return result; } ``` 5:可以配置信号量隔离与线程隔离(面试多问) 线程池隔离技术,是用 Hystrix 自己的线程去执行调用;针对不同的服务使用不同的线程池。并发数量超过线程池数量,不再调用服务。 信号量隔离技术,是直接让 tomcat 线程去调用依赖服务。信号量隔离,只是一道关卡,信号量有多少,就允许多少个 tomcat 线程通过它,然后去执行。 信号量隔离主要维护的是Tomcat的线程,不需要内部线程池,更加轻量级。可以做限流 ```yaml hystrix: command: default: execution: isolation: #隔离策略,默认是Thread, 可选可选THREAD|SEMAPHORE strategy: SEMAPHORE # thread: # #命令执行超时时间,默认1000ms # timeoutInMilliseconds: 1000 # #发生超时是是否中断,默认true # interruptOnTimeout: true # SEMAPHORE: # #最大并发请求数,默认10,该参数当使用ExecutionIsolationStrategy.SEMAPHORE策略时才有效。如果达到最大并发请求数,请求会被拒绝。 # maxConcurrentRequests: 10 # #执行是否启用超时,默认启用true # timeout: true ``` ### 4.3:开启 Hystrix dashboard ,需要引入 actuator 1:引入依赖 ``` org.springframework.cloud spring-cloud-starter-netflix-hystrix-dashboard org.springframework.boot spring-boot-starter-actuator ``` 2: 启动类加上注解 ``` @EnableHystrixDashboard ``` 3:访问浏览器 ``` # 健康上报,查看前需要调用一下接口访问,因为这个是懒加载 http://localhost:端口/actuator/hystrix.stream http://localhost:端口/hystrix ``` 注意:如果访问 dashboard 不显示,在配置文件添加下配置 ```yaml # 解决 dashboard 不显示的问题 hystrix: dashboard: proxy-stream-allow-list: "*" ``` ## 7:网关 zuul(不太好用) 1: 新建一个服务,加上 maven 依赖 ``` org.springframework.cloud spring-cloud-starter-netflix-zuul 2.2.5.RELEASE org.springframework.cloud spring-cloud-starter-netflix-eureka-client ``` 2:配置文件进行配置 ```yaml spring: application: name: cloud-zuul eureka: client: service-url: defaultZone: http://localhost:7900/eureka/ server: port: 8004 #开启路由断点 management: ## 可以手动将服务的状态置为down endpoint: routes: enabled: true health: enabled: true show-details: always ## 开启所有端点 endpoints: web: exposure: include: '*' ``` 3:启动类上加上注解 ``` @EnableZuulProxy ``` 4:注意:版本问题:高版本的cloud版本不支持zuul了 ``` # 使用以下版本 SpringBoot 2.3.2.RELEASE SpringCloud Hoxton.SR8 SpringCloudAlibaba 2.2.3.RELEASE ``` ### 经典问题1:token 不向后传 前端调用网关,网关不把前端传来的 token 透传给后面的服务 目的:把功能提到网关做,避免重复工作,符合阿里的最小知道原则.但是如果你想做一个单独的服务来完成这个工作,那么token 需要后传 ```yaml # 解决办法。配置文件添加配置。以下配置为空表示:所有请求头都透传到后面微服务 zuul: sensitive-headers: ``` ### 经典问题2:服务改造,url 转变成 另外的 url 前端请求的 url 进行转化成其他的 url 步骤:开发之前,想清楚步骤 1:准备 router 过滤器 2:接收请求过来的 url 3:将 url 和要转发的目的地url 映射 4:网关本来就有转发的功能。设置 RequestContext 中的 serviceId,url ``` ```java @Component public class RibbonFilter extends ZuulFilter { /** * 过滤器类型 */ @Override public String filterType() { return FilterConstants.ROUTE_TYPE; } /** * 优先级,数字越小,越先执行 */ @Override public int filterOrder() { return 0; } /** * 是否执行此过滤器 */ @Override public boolean shouldFilter() { return true; } /** * 过滤器的业务逻辑 */ @Override public Object run() throws ZuulException { RequestContext ctx = RequestContext.getCurrentContext(); String url = ctx.getRequest().getRequestURI(); Map forwardMap = getForwardMap(url); if(forwardMap != null){ String fowardUrl = forwardMap.get(url); String serviceId = getServiceId(fowardUrl); // ps:如果用的到不是 eureka,不需要设置 serviceId String requestUrl = getRequestUrl(fowardUrl,serviceId); //1.设置目标service的Controller的路径 ctx.put(FilterConstants.REQUEST_URI_KEY,requestUrl); //2.设置目标service的serviceId ctx.put(FilterConstants.SERVICE_ID_KEY,serviceId); } return null; } private String getServiceId(String url){ if(url.startsWith("/")){ String temp = url.substring(1); return temp.split("/")[0]; }else{ return null; } } private String getRequestUrl(String url,String serviceId){ return url.substring(serviceId.length() +1); } private Map getForwardMap(String originalUrl){ //todo:这里是返回一个map,传入一个originUrl,返回一个要转发的url return null; } } ``` ### 经典问题3:动态路由 比如 根据不同的用户调用不同的服务,入口都是网关,但是不同用户可能根据地区不同调用所在地区服务,不同地区服务业务有差别 解决方法: 1:网关灰度的思路,但是要求服务名称一致,如果是不同的服务,不可取 2:直接连接服务地址 ```yaml zuul: routes: # zuul 请求路径 xxxx: /zuul-api-driver/** # 将请求路径和 服务地址关联 custom-zuul-name: path: /zuul-api-driver/** url: http://localhost:8080/ ``` ``` # 与上面filter 类似 RequestContext ctx = RequestContext.getCurrentContext(); // 中间省略 根据用户选择服务 ... // 匹配服务 ctx.setRouteHost(new URI("http://localhost:8003/test/ses-s").toURL()); ``` ### 网关高可用 可以通过 nginx 对 多个网关服务做负载,为了防止 nginx 的单点问题,可以对nginx 做一个主备或者做集群 nginx 做集群,前面可以 通过 keepalived 来转发请求,提供统一的访问地址。 ### 网关的其他问题 #### 网关过滤器为什么也有访问顺序? 要考虑到过滤器的执行顺序对计算资源的节省,要把小的逻辑放在前面,比如 ip 黑名单,用户黑白名单放在前面,鉴权放在后面。 当系统面临大流量的时候,不至于措手不及 #### 网关的接口容错 ps:网关调用后面服务的接口出错了,网关应该怎么办? ```java @Component public class ZuulFailback implements FallbackProvider { /** * 我这里是对所有的服务,可以修改成对应的服务名称 */ @Override public String getRoute() { return "*"; } @Override public ClientHttpResponse fallbackResponse(String route, Throwable cause) { return new ClientHttpResponse() { @Override public HttpStatus getStatusCode() throws IOException { return HttpStatus.BAD_REQUEST; } @Override public int getRawStatusCode() throws IOException { return HttpStatus.BAD_REQUEST.value(); } @Override public String getStatusText() throws IOException { return "message"; } @Override public void close() { } @Override public InputStream getBody() throws IOException { // 最好和通用的返回值结构一样 return new ByteArrayInputStream(JSONObject.toString("fail","-1").getBytes()); } @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return headers; } }; } } ``` #### 网关的多过滤器如何控制开关 生产中有个小技巧,可以将过滤器开关存储在数据库中,修改数据库的值即可控制过滤器的开关 #### 查看网关中有多少个过滤器 需要在网关中配置 Actuator ,访问 http://网关ip:端口/actuator/filters 即可查看 ps:建议每个微服务都配置 Actuator ,方便查看节点信息 #### ip限制,ip调用次数限制,设备号过滤 使用 pre 过滤器类型,对某个服务的访问进行限制,不让频繁访问,使用 redis 的过期时间或者访问次数 ```java public class IPFilter extends ZuulFilter { @Override public String filterType() { return FilterConstants.PRE_TYPE; } @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); // 1:获取ip 地址 // 2:访问 redis ,拿到访问次数,做对比 // 3:如果达到了访问次数,设置不向后面的过滤器转发,返回 null;否则继续执行 // ps:后面的过滤器可以根据 RequestContext.getCurrentContext().sendZuulResponse(); // 不向后面的 router 过滤器转发,但是还是会把所有的 pre 过滤器执行一遍 context.setSendZuulResponse(false); return null; } } ``` #### 网关限流 限流包括:网关的限流,每个服务的限流 ```java @Component public class LimitFilter extends ZuulFilter { @Override public String filterType() { return FilterConstants.PRE_TYPE; } @Override public int filterOrder() { // -10 是最小的了 return -10; } @Override public boolean shouldFilter() { return true; } // 每秒向令牌桶中放入两个令牌,设置每秒的请求数 public static final RateLimiter RATE_LIMITER=RateLimiter.create(50); @Override public Object run() throws ZuulException { RequestContext context = RequestContext.getCurrentContext(); if(RATE_LIMITER.tryAcquire()){ System.out.println("通过"); return null; }else { System.out.println("被限流"); // 为了控制不向下面所有的过滤器走了,设置条件 ,其他过滤器 (Boolean) RequestContext.getCurrentContext().get("limit"); context.set("limit",false); // 只能控制不向 router 过滤器走了 context.setSendZuulResponse(false); context.setResponseStatusCode(HttpStatus.TOO_MANY_REQUESTS.value()); } return null; } } ``` ```java // 其他的过滤器,这个不重要 @Component public class LimitFilter2 extends ZuulFilter { @Override public String filterType() { return FilterConstants.PRE_TYPE; } @Override public int filterOrder() { return -9; } @Override public boolean shouldFilter() { Object limit = RequestContext.getCurrentContext().get("limit"); return Objects.isNull(limit)? true:(Boolean)limit; } @Override public Object run() throws ZuulException { System.out.println("限流之后的过滤器"); return null; } } ``` #### 服务限流 ```java @Component public class LimitFilter implements Filter { // 2=每秒2个;0.1 = 10秒1个 private static final RateLimiter RATE_LIMITER = RateLimiter.create(1); @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { // 限流的业务逻辑 if (RATE_LIMITER.tryAcquire()){ filterChain.doFilter(servletRequest,servletResponse); }else { servletResponse.setCharacterEncoding("utf-8"); servletResponse.setContentType("text/html; charset=utf-8"); PrintWriter pw = servletResponse.getWriter(); pw.write("限流了"); pw.close(); } } @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void destroy() { } } ``` ## 8:链路跟踪 Sleuth(不太好用) 1:每个要跟踪的服务都加上 maven 依赖 ```xml org.springframework.cloud spring-cloud-starter-sleuth ``` 2:启动服务,查看控制台日志即可 ## 9:链路跟踪 zipkin(不太好用) 1:每个要跟踪的服务都加上 maven 依赖 ```xml org.springframework.cloud spring-cloud-starter-zipkin 2.2.8.RELEASE ``` 2:每个要跟踪的服务配置文件添加配置,每个服务都向 zipkin 上报信息 ```yaml spring: #zipkin zipkin: base-url: http://localhost:9411/ #采样比例1 sleuth: sampler: rate: 1 ``` 3:下载 zipkin 到下面的地址去下载 ``` https://repo1.maven.org/maven2/io/zipkin/zipkin-server/ ``` 4:启动 zipkin ``` java -jar zipkin.jar ``` 5:查看前端页面,http://localhost:9411/ ,需要先访问接口,在前端界面输入服务名称 ## 10:健康检查 admin-服务 down 了,发送钉钉消息 1:新建一个服务,添加 maven 依赖 ``` de.codecentric spring-boot-admin-starter-server de.codecentric spring-boot-admin-server-ui 2.2.1 com.alibaba fastjson 1.2.79 ``` 2:启动类加上注解 ``` @EnableAdminServer ``` 3:配置文件 ```yaml server: port: 8005 spring: application: name: admin-server ``` 4:其他服务加上 maven 依赖 ``` de.codecentric spring-boot-admin-starter-client 2.6.2 org.springframework.boot spring-boot-starter-actuator ``` 5:其他服务配置文件添加配置 ```yaml spring: # 需要向这个地址上报服务信息 boot: admin: client: url: http://localhost:8005 management: ## 可以手动将服务的状态置为down endpoint: routes: enabled: true health: enabled: true show-details: always ## 开启所有端点 endpoints: web: exposure: include: '*' ``` 6:服务上下线:发送钉钉消息 新建一个消息类 ```java @Data public class Message { private String msgtype; private String text; } ``` 新建一个钉钉工具类 ```java public class DingDingMessageUtil { //注意:打开钉钉一个群聊,打开群设置,打开智能群助手,添加机器人 安全设置:选择 自定义关键词 确定后,保存 token public static String access_token = "f54f611b869ad8e82ec8250f85f5e4bb6a06146c4dc7046d4ae9217fec95ef17"; public static void sendTextMessage(String msg) { try { Message message = new Message(); message.setMsgtype("text"); message.setText(msg); URL url = new URL("https://oapi.dingtalk.com/robot/send?access_token=" + access_token); // 建立 http 连接 HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setDoOutput(true); conn.setDoInput(true); conn.setUseCaches(false); conn.setRequestMethod("POST"); conn.setRequestProperty("Charset", "UTF-8"); conn.setRequestProperty("Content-Type", "application/Json; charset=UTF-8"); conn.connect(); OutputStream out = conn.getOutputStream(); String textMessage = JSONObject.toJSONString(message); byte[] data = textMessage.getBytes(); out.write(data); out.flush(); out.close(); InputStream in = conn.getInputStream(); byte[] data1 = new byte[in.available()]; in.read(data1); System.out.println(new String(data1)); } catch (Exception e) { e.printStackTrace(); } } } ``` 新建一个过滤器类,继承 AbstractStatusChangeNotifier 抽象类 ```java public class DingDingNotifier extends AbstractStatusChangeNotifier { public DingDingNotifier(InstanceRepository repository) { super(repository); } @Override protected Mono doNotify(InstanceEvent event, Instance instance) { String serviceName = instance.getRegistration().getName(); String serviceUrl = instance.getRegistration().getServiceUrl(); String status = instance.getStatusInfo().getStatus(); Map details = instance.getStatusInfo().getDetails(); StringBuilder str = new StringBuilder(); str.append("服务预警 : 【" + serviceName + "】"); str.append("【服务地址】" + serviceUrl); str.append("【状态】" + status); str.append("【详情】" + JSONObject.toJSONString(details)); return Mono.fromRunnable(() -> { DingDingMessageUtil.sendTextMessage(str.toString()); }); } } ``` 在启动类里面注入过滤器类 ``` @Bean public DingDingNotifier dingDingNotifier(InstanceRepository repository) { return new DingDingNotifier(repository); } ``` 7:启动 admin-server 服务,可以到前端 ui 界面查看 ,并手动关掉服务,查看钉钉信息 ## 11:邮件发送 mail -本例配合 admin 使用 服务 down 了,发送钉钉消息 1:需要在 admin-server 中引入 maven 依赖 ```xml org.springframework.boot spring-boot-starter-mail ``` 2:配置文件添加配置,授权码需要到邮箱里面去获取 ```yaml spring: application: name: admin-server security: user: name: root password: root # 邮件设置 mail: host: smtp.qq.com username: 1677081700 # 授权码:从 qq 邮箱->账户->STMP 开启->需要发短信,会得到一串密钥,填写在这里 password: wcyyhhrdveuybcgd properties: mail: smpt: auth: true starttls: enable: true required: true boot: admin: notify: mail: to: 1677081700@qq.com from: 1677081700@qq.com ``` 3:手动关掉服务,查看邮箱信息 ## 12:配置中心 config 动态配置 1:新建一个微服务,添加 maven 依赖 ``` org.springframework.cloud spring-cloud-config-server org.springframework.cloud spring-cloud-starter-netflix-eureka-client ``` 2:配置配置文件 ```yaml server: port: 8006 spring: application: name: config-server # 配置 git 仓库的路径和分支 cloud: config: server: git: uri: https://gitee.com/jiang-qikun/spring-cloud-config-center.git label: master eureka: client: service-url: defaultZone: http://localhost:7900/eureka/ ``` 3:启动类添加注解 ``` @EnableConfigServer ``` 4:在 git 仓库新建仓库 https://gitee.com/jiang-qikun/spring-cloud-config-center.git, 添加一个配置文件,命名规则是 服务名-环境名称.yml ps:环境名称: dev/test/pre/ft/uat 等 5:启动服务,去读取 git 仓库的文件,浏览器访问 http://localhost:8006/master/service-provider-dev.yml 6: 我们这个例子使用的是 service-provider 服务,因此需要现在 service-provider 的 maven 依赖里面添加 ``` org.springframework.cloud spring-cloud-config-client org.springframework.cloud spring-cloud-starter-bootstrap 3.1.1 ``` 7:修改 service-provider 服务的配置文件名称,变成 bootstrap.yml,并添加内容 ```yaml spring: cloud: config: # 直接URL方式查找 或者配置中心 两种方式二选一 # uri: http://localhost:8006/ discovery: enabled: true service-id: config-server profile: dev label: master ``` 8:手动刷新-针对单服务:编写 Controller ,能够访问 git 仓库的配置, 类上添加 @RefreshScope 注解 ```java @RestController @RefreshScope public class ConfigController { @Value("${myConfig}") private String myConfig; @GetMapping("/test-config") public String testConfig(){ return "myConfig: "+ myConfig; } } ``` 9:先访问一下接口,然后修改 git 仓库中的配置内容,向浏览器发送刷新请求 http://localhost:8001/actuator/refresh, 刷新的很慢,基本是把所有的配置都重启了。刷新完成后,再次访问接口,查看内容是否发生了变化 ps: 只能一个一个服务的刷新,有点麻烦,可以通过下面的消息总线 bus 来刷新 ## 13:消息总线 bus 所有服务都接入 bus ,与eureka 不同的是,这个是主动推消息的。 底层是一个消息中间件,需要一个 mq 和 bus. SpringCloud 里面的 bus 要求 mq 支持 amqp 协议,可以使用 kafka, RabbitMQ, 另外的 mq 也可以接进来,但是比较麻烦,这两个可以无缝集成。kafka 面向大的数据中转开发,RabbitMQ 面向应用级开发,我们这里使用 RabbitMQ。 这个组件有很多功能,下面简单使用一个功能 ### 自动刷新:通过消息总线,配合 config 动态刷新 1:erlang安装,下载地址: http://www.erlang.org/downloads ,下载完成后,需要像jdk 一样配置环境变量。 配置完成后,在命令行里面输入 erl , 能显示版本说明成功 2: RabbitMQ 安装,下载地址: http://www.rabbitmq.com/install-windows.html 安装完成后,在 sbin 窗口执行以下命令 ```shell # 开启RabbitMQ节点 rabbitmqctl start_app # 开启RabbitMQ管理模块的插件,并配置到RabbitMQ节点上 rabbitmq-plugins enable rabbitmq_management ``` 3:启动 RabbitMQ ,上面步骤完成后,可以在菜单栏-所有程序 里面点开 RabbitMQ Server,双击 RabbitMQ Server -start 启动即可。 启动完成后,可以通过浏览器访问 http://localhost:15672 ,查看管理界面,用户名密码均为guest ps:遇到报错可以参考这篇博客 : https://blog.csdn.net/lezeqe/article/details/106098082 4:所有服务 配置文件添加对 mq 的配置,除了 eureka,admin,zuul也可以不配置,只配置自己想动态改变的, 我这里只在 config-server,service-provider 中添加了 ```yaml spring: # 消息总线配置,注意端口是 5672 rabbitmq: host: localhost port: 5672 username: guest password: guest ``` 5:只在 config-server,service-provider 服务中添加 maven 依赖 ```xml org.springframework.cloud spring-cloud-starter-bus-amqp ``` 6:前面没有在 config-server 添加 actuator 的配置,这里需要在 maven 添加依赖 ```xml org.springframework.boot spring-boot-starter-actuator ``` 在配置文件添加配置 ```yaml management: ## 可以手动将服务的状态置为down endpoint: routes: enabled: true health: enabled: true show-details: always ## 开启所有端点 endpoints: web: exposure: include: '*' ``` 7:启动所有服务,修改 git 仓库中的内容,可以调用配置中心的 http://localhost:8006/actuator/busrefresh ,post 请求,还是慢 ps: 有的版本是 http://localhost:8006/actuator/bus-refresh ,这个可以通过 http://localhost:8006/actuator 查看 这样几乎所有的服务都跟着更新了