# Spring-Cloud-Study **Repository Path**: lx-jian/spring-cloud-study ## Basic Information - **Project Name**: Spring-Cloud-Study - **Description**: 写法的参考.................... - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-11-24 - **Last Updated**: 2024-12-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 微服务关键功能: ## 创建模块: 选择对应的JDK后,模块自动创建pom ### pom配置: 首先拷贝其他的过来, ***然后去掉不需要的依赖,去掉父类已经有了的依赖*** ## 创建yaml: 示例(***注意注释内容***) ```yaml server: port: 8081 #注意端口不能一样,我们是微服务 spring: application: name: hm-service #这个挺重要的 profiles: active: local datasource: # 在真实项目中,不同的服务用的不同的数据库实例,即服务器都不同 url: jdbc:mysql://${hm.db.host}:3306/hmall?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver username: root password: ${hm.db.pw} mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler global-config: db-config: update-strategy: not_null id-type: auto logging: level: # 日志级别 com.hmall: debug pattern: dateformat: HH:mm:ss:SSS file: path: "logs/${spring.application.name}" # swagger配置 knife4j: enable: true openapi: title: 黑马商城接口文档 description: "黑马商城接口文档" email: zhanghuyi@itcast.cn concat: 虎哥 url: https://www.itcast.cn version: v1.0.0 group: default: group-name: default api-rule: package api-rule-resources: - com.hmall.controller # controller目录 ``` ## 远程调用: ### 注册 **RestTemplate** Bean ```java public class ItemApplication { public static void main(String[] args) { SpringApplication.run(ItemApplication.class, args); } //注册bean,以便在任意地方使用 @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } } ``` ### 构造函数注入 **RestTemplate** : 由于spring不推荐使用Autowaried注入,我们使用构造函数注入,使用下面的方法比较好 @RequiredArgsConstructor注解只给final字段的属性构建 构造函数 ```java @Service @RequiredArgsConstructor public class ItemServiceImpl extends ServiceImpl implements IItemService { private final RestTemplate restTemplate; } ``` ## (重要)远程调用实现: ```Java private void handleCartItems(List vos) { // TODO 1.获取商品id Set itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet()); // 2.查询商品 // List items = itemService.queryItemByIds(itemIds); // 2.1.利用RestTemplate发起http请求,得到http的响应 ResponseEntity> response = restTemplate.exchange( "http://localhost:8081/items?ids={ids}", HttpMethod.GET, null, new ParameterizedTypeReference>() { }, Map.of("ids", CollUtil.join(itemIds, ",")) ); // 2.2.解析响应 if(!response.getStatusCode().is2xxSuccessful()){ // 查询失败,直接结束 return; } List items = response.getBody(); if (CollUtils.isEmpty(items)) { return; } // 3.转为 id 到 item的map Map itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity())); // 4.写入vo for (CartVO v : vos) { ItemDTO item = itemMap.get(v.getItemId()); if (item == null) { continue; } v.setNewPrice(item.getPrice()); v.setStatus(item.getStatus()); v.setStock(item.getStock()); } } ``` 注 : 响应体如果是List泛型,则需要使用ParameterizedTypeReference,如果是一个实体类,那么直接使用:***实体类名.class*** ## 服务器部署nacos: https://nacos.io/zh-cn/ ### nacos控制台: ```http http://Your Ip:8848/nacos/#/login ``` ## 服务注册: 黑马教程3.2部分: ```YAML spring: application: name: item-service # 服务名称 cloud: nacos: server-addr: 192.168.150.101:8848 # nacos地址 ``` Spring可以Copy一个配置好的服务,重复启动两个实例 配置启动项,-Dserver.port=8083,避免冲突 https://b11et3un53m.feishu.cn/wiki/R4Sdwvo8Si4kilkSKfscgQX0niB#PAzNd2yNtoShSbxInaycTQOmnQd ## 服务发现: 常见的负载均衡算法有: - 随机 - 轮询 - IP的hash - 最近最少访问 示例代码: 首先注入,然后使用 **serviceInstance.getUri()** 获取均衡后的url,其他处理类似 ```java //SpringCloud规定的统一接口 private final DiscoveryClient discoveryClient; @Override public void testNacos(List vo/*仅作为演示用*/) { // Nacos中注册过的服务名字(也就是Springboot中配置文件中定义的) List instances = discoveryClient.getInstances("hm-service"); if (CollUtil.isEmpty(instances)) { return; } ServiceInstance serviceInstance = instances.get(RandomUtil.randomInt(instances.size())); //serviceInstance.getHost() 可以取出各种信息 ResponseEntity> responseEntity = restTemplate.exchange( serviceInstance.getUri() + "/items?id={ids}", HttpMethod.GET, null, new ParameterizedTypeReference>() { }, CollUtil.join(vo, ",") ); } ``` **https://b11et3un53m.feishu.cn/wiki/R4Sdwvo8Si4kilkSKfscgQX0niB#EkXBdT9A4ojYTYxJvCXcK0KSn7b** # OpenFeign: ## 引入依赖 在`cart-service`服务的pom.xml中引入`OpenFeign`的依赖和`loadBalancer`依赖: ```XML org.springframework.cloud spring-cloud-starter-openfeign org.springframework.cloud spring-cloud-starter-loadbalancer ``` 在启动类中:**@EnableFeignClients** ## 编写客户端: 不需要具体实现 ```java package com.hmall.item.client; import com.hmall.item.domain.dto.ItemDTO; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import java.util.Collection; import java.util.List; @FeignClient("item-service") public interface ItemClient { @GetMapping("/items") //Collection 因为返回不一定是List,所以直接使用list的父类 List queryItemByIds(@RequestParam("ids") Collection ids); } ``` 这里只需要声明接口,无需实现方法。接口中的几个关键信息: - `@FeignClient("item-service")` :声明服务名称 - `@GetMapping` :声明请求方式 - `@GetMapping("/items")` :声明请求路径 - `@RequestParam("ids") Collection ids` :声明请求参数 - `List` :返回值类型 ## Client使用: 注入: ``` private final ItemClient itemClient; ``` 使用: ```java @Override public void testNacosUp(List vo) { Set itemIds = vo.stream().map(Item::getId).collect(Collectors.toSet()); List items = itemClient.queryItemByIds(itemIds); if (CollUtil.isEmpty(items)) { return; } } ``` # 连接池: ## 依赖: Feign底层发起http请求,依赖于其它的框架。其底层支持的http客户端实现包括: - HttpURLConnection:默认实现,不支持连接池 - Apache HttpClient :支持连接池 - OKHttp:支持连接池 我们使用OKhttp ```XML io.github.openfeign feign-okhttp ``` ## 开启: 在`cart-service`的`application.yml`配置文件中开启Feign的连接池功能: ```YAML feign: okhttp: enabled: true # 开启OKHttp功能 ``` ## 最佳实践: - 思路1:抽取到微服务之外的公共module - 思路2:每个微服务自己抽取一个module https://b11et3un53m.feishu.cn/wiki/R4Sdwvo8Si4kilkSKfscgQX0niB#HuFTdf6QCoSBImxk1gCcfcCTngb 需要引入其他模块的依赖:借鉴黑马的教程-> https://b11et3un53m.feishu.cn/wiki/R4Sdwvo8Si4kilkSKfscgQX0niB#N8jldueiYoCYZCxsLi4cP20bnse # OpenFeign日志: ## 定义日志级别: 在对应的api模块下面新建配置类,代码如下: ```Java package com.hmall.api.config; import feign.Logger; import org.springframework.context.annotation.Bean; public class DefaultFeignConfig { @Bean public Logger.Level feignLogLevel(){ return Logger.Level.FULL; } } ``` ## 配置: 接下来,要让日志级别生效,还需要配置这个类。有两种方式: - **局部**生效:在某个`FeignClient`中配置(即***单个Client类***),只对当前`FeignClient`生效 ```Java @FeignClient(value = "item-service", configuration = DefaultFeignConfig.class) ``` - **全局**生效:在`@EnableFeignClients`中配置(即***启动类***),针对所有`FeignClient`生效。 ```Java @EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class) ``` # 网关路由基础: ***spring-cloud-starter-gateway*** 黑马教程教学:SpringCloudGateway;基于Spring的WebFlux技术,完全支持响应式编程,吞吐能力更强 ## 新的module: https://b11et3un53m.feishu.cn/wiki/UMgpwmmQKisWBIkaABbcwAPonVf#C2ujdKChSoWrZWxhQ9ZcAwtPndf ### 引入依赖: 在`hm-gateway`模块的`pom.xml`文件中引入依赖: ```XML hmall com.heima 1.0.0 4.0.0 hm-gateway 11 11 com.heima hm-common 1.0.0 org.springframework.cloud spring-cloud-starter-gateway com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery org.springframework.cloud spring-cloud-starter-loadbalancer ${project.artifactId} org.springframework.boot spring-boot-maven-plugin ``` ## 新的启动类: ## *配置路由: **重要** ```YAML server: port: 8080 spring: application: name: gateway cloud: nacos: server-addr: 192.168.150.101:8848 gateway: routes: - id: item # 路由规则id,自定义,唯一 uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表 predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务 - Path=/items/**,/search/** # 这里是以请求路径作为判断规则 - id: cart uri: lb://cart-service predicates: - Path=/carts/** - id: user uri: lb://user-service predicates: - Path=/users/**,/addresses/** - id: trade uri: lb://trade-service predicates: - Path=/orders/** - id: pay uri: lb://pay-service predicates: - Path=/pay-orders/** ``` ## 登录校验: 既然网关是所有微服务的入口,一切请求都需要先经过网关。我们完全可以把登录校验的工作放到网关去做,这样之前说的问题就解决了: - 只需要在网关和用户服务保存秘钥 - 只需要在网关开发登录校验功能 ### 网关过滤器: - **`GatewayFilter`**:路由过滤器,作用范围比较灵活,可以是任意指定的路由`Route`. - **`GlobalFilter`**:全局过滤器,作用范围是所有路由,不可配置。 ### 自定义全局过滤器: ```Java @Component public class PrintAnyGlobalFilter implements GlobalFilter, Ordered { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 编写过滤器逻辑 System.out.println("未登录,无法访问"); // 放行 // return chain.filter(exchange); // 拦截 ServerHttpResponse response = exchange.getResponse(); response.setRawStatusCode(401); return response.setComplete(); } @Override public int getOrder() { // 过滤器执行顺序,值越小,优先级越高 return 0; } } ``` #### 登录校验过滤器具体实现: ```Java package com.hmall.gateway.filter; import com.hmall.common.exception.UnauthorizedException; import com.hmall.common.utils.CollUtils; import com.hmall.gateway.config.AuthProperties; import com.hmall.gateway.util.JwtTool; import lombok.RequiredArgsConstructor; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.util.List; @Component @RequiredArgsConstructor @EnableConfigurationProperties(AuthProperties.class) public class AuthGlobalFilter implements GlobalFilter, Ordered { private final JwtTool jwtTool; private final AuthProperties authProperties; private final AntPathMatcher antPathMatcher = new AntPathMatcher(); @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 1.获取Request ServerHttpRequest request = exchange.getRequest(); // 2.判断是否不需要拦截 if(isExclude(request.getPath().toString())){ // 无需拦截,直接放行 return chain.filter(exchange); } // 3.获取请求头中的token String token = null; List headers = request.getHeaders().get("authorization"); if (!CollUtils.isEmpty(headers)) { token = headers.get(0); } // 4.校验并解析token Long userId = null; try { userId = jwtTool.parseToken(token); } catch (UnauthorizedException e) { // 如果无效,拦截 ServerHttpResponse response = exchange.getResponse(); response.setRawStatusCode(401); return response.setComplete(); } // TODO 5.如果有效,传递用户信息 System.out.println("userId = " + userId); // 6.放行 return chain.filter(exchange); } private boolean isExclude(String antPath) { for (String pathPattern : authProperties.getExcludePaths()) { if(antPathMatcher.match(pathPattern, antPath)){ return true; } } return false; } @Override public int getOrder() { return 0; } } ``` ### JWT工具: 具体作用如下: - `AuthProperties`:配置登录校验需要拦截的路径,因为不是所有的路径都需要登录才能访问 - `JwtProperties`:定义与JWT工具有关的属性,比如秘钥文件位置 - `SecurityConfig`:工具的自动装配 - `JwtTool`:JWT工具,其中包含了校验和解析`token`的功能 - `hmall.jks`:秘钥文件 其中`AuthProperties`和`JwtProperties`所需的属性要在`application.yaml`中配置: ```YAML hm: jwt: location: classpath:hmall.jks # 秘钥地址 alias: hmall # 秘钥别名 password: hmall123 # 秘钥文件密码 tokenTTL: 30m # 登录有效期 auth: excludePaths: # 无需登录校验的路径 - /search/** - /users/login - /items/** ``` ## 网关向微服务传递用户信息: 由于网关发送请求到微服务依然采用的是`Http`请求,因此我们可以将用户信息以请求头的方式传递到下游微服务。然后微服务可以从请求头中获取登录用户信息。考虑到微服务内部可能很多地方都需要用到登录用户信息,因此我们可以利用SpringMVC的拦截器来实现登录用户信息获取,并存入ThreadLocal,方便后续使用。 **因此,接下来我们要做的事情有:** - 改造网关过滤器,在获取用户信息后保存到请求头,转发到下游微服务 - 编写微服务拦截器,拦截请求获取用户信息,保存到ThreadLocal后放行 ### 改造网关过滤器 ```java long userId = 1L; String userInfo = Long.toString(userId); ServerWebExchange build = exchange.mutate() .request(b -> b.header("user-info", userInfo)) .build(); return chain.filter(build); ``` ### 微服务器拦截器获取用户: https://b11et3un53m.feishu.cn/wiki/UMgpwmmQKisWBIkaABbcwAPonVf#MRmEd4C5roD6DQxjjD1cG6ConIh 由于每个微服务都有获取登录用户的需求,因此拦截器我们直接写在`hm-common`中,并写好自动装配。这样微服务只需要引入`hm-common`就可以直接具备拦截器功能,无需重复编写。 **UserContext** - ***上下文,缓存*** ```Java package com.hmall.common.interceptor; import cn.hutool.core.util.StrUtil; import com.hmall.common.utils.UserContext; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class UserInfoInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1.获取请求头中的用户信息 String userInfo = request.getHeader("user-info"); // 2.判断是否为空 if (StrUtil.isNotBlank(userInfo)) { // 不为空,保存到ThreadLocal UserContext.setUser(Long.valueOf(userInfo)); } // 3.放行 return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 移除用户 UserContext.removeUser(); } } ``` #### Mvc-config配置: ```Java package com.hmall.common.config; import com.hmall.common.interceptors.UserInfoInterceptor; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @ConditionalOnClass(DispatcherServlet.class) public class MvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new UserInfoInterceptor()); } } ``` 不过,需要注意的是,这个配置类默认是不会生效的,因为它所在的包是`com.hmall.common.config`,与其它微服务的扫描包不一致,无法被扫描到,因此无法生效。 基于SpringBoot的自动装配原理,我们要将其添加到`resources`目录下的`META-INF/spring.factories`文件中: 内容如下: ```Properties org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.hmall.common.config.MyBatisConfig,\ com.hmall.common.config.MvcConfig ``` ## 微服务之间传递用户信息: 微服务之间调用是基于OpenFeign来实现的,并不是我们自己发送的请求。我们如何才能让每一个由OpenFeign发起的请求自动携带登录用户信息呢? 这里要借助Feign中提供的一个拦截器接口:`feign.RequestInterceptor` ```Java public interface RequestInterceptor { void apply(RequestTemplate template); } ``` 我们只需要实现这个接口,然后实现apply方法,利用`RequestTemplate`类来添加请求头,将用户信息保存到请求头中。这样以来,每次OpenFeign发起请求的时候都会调用该方法,传递用户信息。 推荐写在类***DefaultFeignConfig***这个配置需要 局部注册**@FeignClient** 或是**@EnableFeignClients**全局注册 才可以,不用另外创建一个类,在原有的***DefaultFeignConfig***中添加一个Bean: ```Java @Bean public RequestInterceptor userInfoRequestInterceptor(){ return new RequestInterceptor() { @Override public void apply(RequestTemplate template) { // 获取登录用户 Long userId = UserContext.getUser(); if(userId == null) { // 如果为空则直接跳过 return; } // 如果不为空则放入请求头中,传递给下游微服务 template.header("user-info", userId.toString()); } }; } ``` OK了 # 配置管理(后期学): 现在依然还有几个问题需要解决: - 网关路由在配置文件中写死了,如果变更必须重启微服务 - 某些业务配置在配置文件中写死了,每次修改都要重启服务 - 每个微服务都有很多重复的配置,维护成本高 这些问题都可以通过统一的**配置管理器服务**解决。而Nacos不仅仅具备注册中心功能,也具备配置管理的功能: https://b11et3un53m.feishu.cn/wiki/UMgpwmmQKisWBIkaABbcwAPonVf#HeKfdhgSWoX914xH1mGcFCMOnOc 这需要用到nacos的控制台 ```http http://Your Ip:8848/nacos/#/login ``` # 服务保护方案(后期学) 微服务保护的方案有很多,比如: - 请求限流 - 线程隔离 - 服务熔断 https://b11et3un53m.feishu.cn/wiki/QfVrw3sZvihmnPkmALYcUHIDnff#share-M9UNdDopQotFi4xWf3VcDxcsnAZ 通过学习,你将能掌握下面的能力: - 知道雪崩问题产生原因及常见解决方案 - 能使用Sentinel实现服务保护 - 理解分布式事务产生的原因 - 能使用Seata解决分布式事务问题 - 理解AT模式基本原理