> 文章列表 > SpringCloud学习-实用篇02

SpringCloud学习-实用篇02

SpringCloud学习-实用篇02

以下内容的代码可见:SpringCloud_learn/day02


1.Nacos配置管理

之前提到的Nacos是作为注册中心,除此之外它还有配置管理功能


统一配置管理

假设有多个微服务之间有关联,此时修改了某个微服务的配置后其他相关的微服务也需要重启,十分麻烦,此时就需要Nacos进行统一的配置管理,并且实现配置更改热更新(修改配置后服务不需要重启即可生效),具体步骤如下:

在这里插入图片描述

  • Nacos中添加配置信息:

在这里插入图片描述

  • 填写配置信息:图中填写的配置是有热更新需求的配置(比如控制启用哪个服务的配置或者模板类型的配置就可写在这)

在这里插入图片描述

将部分配置放到Nacos后,此时微服务就要获取到这些配置,首先需要注意以下两点:

  • 获取步骤是先读取到Nacos中配置的内容,再将其和本地配置文件的内容进行合并
  • 假设将Nacos地址放在本地配置文件中,就无法先读取到Nacos的配置文件,所以将Nacos地址放在优先级较高的配置文件bootstrap.yml

在这里插入图片描述

Nacos和本地获取配置具体操作步骤如下:

  • 引入Nacos的配置管理客户端依赖:
<!--nacos配置管理依赖-->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
  • userservice中的resource目录添加一个bootstrap.yml文件(一般将与Nacos相关的配置放在这个文件中,同时将application.yml中重复的配置删除):
# ${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}作为文件ID去Nacos读取配置
spring:application:name: userservice  # 服务名称profiles:active: dev  # 环境cloud:nacos:server-addr: localhost:8848  # nacos地址config:file-extension: yaml  # 文件后缀名
  • 测试是否读到Naocs配置文件:输入http://localhost:8081/user/now看是否输出相应格式的日期
@Value("${pattern.dateformat}")
private String dateformat;@GetMapping("now")
public String now(){return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}

tips:

  • 如果无法一直无法读取到相应日期可以重新install该模块

配置自动刷新

Nacos中的配置文件变更后,微服务无需重启就可感知。有以下两种方式:

  • @Value注入的变量所在类上添加注解@RefreshScope
@Slf4j
@RestController
@RequestMapping("/user")
@RefreshScope
public class UserController {@Value("${pattern.dateformat}")private String dateformat;...
}
  • 使用@ConfigurationProperties注解(推荐使用)

    • 首先在user-service服务中添加一个类config/PatternProperties用于读取patterrn.dateformat属性:
    @Data
    @Component
    @ConfigurationProperties(prefix = "pattern")
    public class PatternProperties {private String dateformat;
    }
    
    • UserController中使用该类代替@Value:之前的@Value@RefreshScope就不需要了
    @GetMapping("now")
    public String now() {return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat()));
    }
    

tips:

  • 建议将一些关键参数和需要运行时调整的参数放到nacos配置中心,一般都是自定义配置

多环境配置共享

微服务启动时会从nacos读取多个配置文件,无论profile如何变化,[spring.application.name].yaml一定会加载,因此多环境共享配置可写入该文件

  • [spring.application.name]-[spring.profiles.active].yaml,如userservice-dev.yaml
  • [spring.application.name].yaml,如userservice.yaml

假设在能读取到的配置文件中存在多个系统的属性,具体该读取哪个配置文件?

  • 服务名-profile.yaml >服务名称.yaml > 本地配置(前两个就是Nacos中的配置)

tips:

  • 每次更换测试环境或开发环境都要在配置文件中改写十分麻烦,可以在配置中直接更改再启动服务

在这里插入图片描述


搭建Nacos集群

之前Nacos全是单节点,而在生产环境中一定要部署为集群状态。部署的集群结构如下:

  • 包含3个Nacos节点(因为还是在一台机器上实践,所以将它们端口配置为不一样即可)
  • 使用Nginx作为负载均衡器代理3个Nacos
  • Nacos的配置从数据库(这里还是以单点数据库为例)中读取

在这里插入图片描述

搭建集群步骤如下:

  • 初始化数据库:SQL代码见SQL.txt(之后在Nacos进行的配置都会写入该数据库)

  • 下载并配置Nacos

    • 进入Nacosconf目录,修改配置文件cluster.conf.example,重命名为cluster.conf
    • cluster.conf文件中添加以下内容:
    127.0.0.1:8845
    127.0.0.1.8846
    127.0.0.1.8847
    
    • 修改同一个目录下的application.properties文件,添加数据库配置:
    # 把注释符号去掉并修改相应信息即可
    spring.datasource.platform=mysql
    db.num=1
    db.url.0=jdbc:mysql://127.0.0.1:3306/xxx?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
    db.user.0=xxx
    db.password.0=xxx
    
  • 启动Nacos

    • Nacos文件夹复制三份并分别命名(nacos1、nacos2、nacos3
    • 分别修改三个文件夹中的conf/application.properties的端口号
    server.port=8845
    server.port=8846
    server.port=8847
    
    • 分别启动三个Nacos节点:startup.cmd(以集群启动不再加参数)
  • 配置Nginx反向代理:

    • 修改conf/nginx.conf文件:注意是添加到http{xxx}范围内
    upstream nacos-cluster {server 127.0.0.1:8845;server 127.0.0.1:8846;server 127.0.0.1:8847;
    }server {listen       80;server_name  localhost;location /nacos {proxy_pass http://nacos-cluster;}
    }
    
    • 启动Nginx(start nginx.exe),在浏览器访问http://localhost/nacos即可(停止Nginx的命令为nginx.exe -s stop)
  • 修改服务中的application.yml文件:

spring:cloud:nacos:server-addr: localhost:80 # Nacos地址(即在Nginx中配置的代理地址)

2.http客户端Feign


Feign替代RestTemplate

以前在order-service中利用RestTemplate发起远程调用的代码如下,主要存在两个问题:

String url = "http://userservice/user/" + order.getUserId();
User user = restTemplate.getForObject(url, User.class);
  • 代码可读性差,编程体验不统一
  • 参数复杂,URL难以维护

Feign是一个声明式的http客户端,它可以优雅的实现http请求的发送,解决上述问题。使用Feign的步骤如下:

  • 引入依赖:
<!-- Feign客户端依赖-->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  • order-service的启动类添加注解开启Feign的功能:
@MapperScan("cn.itcast.order.mapper")
@EnableFeignClients
@SpringBootApplication
public class OrderApplication {...
}
  • 编写Feign客户端:主要是基于SpringMVC的注解来声明远程调用的信息
@FeignClient("userservice")
public interface UserClient {// 下面代码和RestTemplate代码中其实都是一一对应的@GetMapping("/user/{id}")User findById(@PathVariable("id") Long id);
}
  • Feign客户端代替RestTemplate
// 使用Feign实现远程调用
@Autowired
private UserClient userClient;public Order queryOrderById(Long orderId) {// 1.查询订单Order order = orderMapper.findById(orderId);// 2.查询用户User user = userClient.findById(order.getUserId());// 3.封装user信息order.setUser(user);  // 4.返回return order;
}

tips:

  • Feign的依赖包已经集成了ribbon,所以实现了负载均衡
  • 注意order-service服务的端口不要和nginx使用的8080端口冲突,同时把server-addr改为localhost:80

自定义Feign的配置

Feign可以使用自定义配置来覆盖默认配置,可修改的配置如下:

在这里插入图片描述

一般要配置的就是日志级别,配置Feign日志有两种方式:

  • 配置文件方式:可全局生效或局部生效

    feign:client:config:userservice:  # 写服务名称就是针对某个微服务的局部配置# default: # 写"default"就是全局配置loggerLevel: FULL  # 日志级别 
    
  • Java代码方式

    • 先声明一个Bean:该类不需要加注解标记为配置类
    public class DefaultFeignConfiguration {@Beanpublic Logger.Level logLevel(){return Logger.Level.BASIC;}
    }
    
    • 如果需要全局配置,则把它放到@EnableFeignClients这个注解中:
    @MapperScan("cn.itcast.order.mapper")
    @EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
    @SpringBootApplication
    public class OrderApplication {...
    }
    
    • 如果需要局部配置,则把它放到@FeignClient这个注解中:
    @FeignClient(value = "userservice", configuration = DefaultFeignConfiguration.class)
    public interface UserClient {@GetMapping("/user/{id}")User findById(@PathVariable("id") Long id);
    }
    

Feign的性能优化

Feign底层的客户端实现:

  • URLConnection:默认实现,不支持连接池
  • Apache HttpClient:支持连接池
  • OKHttp:支持连接池

因此优化Feign的性能主要包括:

  • 使用连接池代替默认的URLConnection,具体步骤如下:

    • 引入依赖:
    <!-- Feign的httpClient依赖 -->
    <dependency><groupId>io.github.openfeign</groupId><artifactId>feign-httpclient</artifactId>
    </dependency>
    
    • 配置连接池:
    feign:client:config:default:loggerLevel: BASIC  # 日志级别,BASIC即基本的请求和响应信息httpclient:enabled: true  # 开启feign对HttpClient的支持max-connections: 200  # 最大连接数max-connections-per-route: 50  # 每个路径的最大连接数
    
  • 日志级别最好用basicnone


Feign的最佳实践

Feign的客户端与Controller代码相似,可以对重复的代码进行简化。具体采用以下两种方式:

  • 给消费者(即order-service)的FeignClient和提供者(即user-service)的Controller定义统一的父接口作为标准:

    • 步骤:

      • 定义一个API接口,利用定义方法并基于SpringMVC注解做声明
      • Feign客户端和Controller都集成改接口

      在这里插入图片描述

    • 优缺点:

      • 简单且实现了代码共享
      • 服务提供方、服务消费方紧耦合
      • 参数列表中的注解映射并不会继承,因此Controller中必须再次声明方法、参数列表、注解
  • FeignClient抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用

    在这里插入图片描述

    • 步骤:

      • 创建名为feign-api模块

      • feign-api中然后引入feignstarter依赖

      • order-service中的UserClientUserDefaultFeignConfiguration复制到feign-api(复制后将order-service相关代码删除)

      • order-servicepom文件中中引入feign-api的依赖:

      <!-- 引入feign-api -->
      <dependency><groupId>cn.itcast.demo</groupId><artifactId>feign-api</artifactId><version>1.0</version>
      </dependency>
      
    • 注意点:现在UserClientcn.itcast.feign.clients包下,而order-service@EnableFeignClients注解是在cn.itcast.order包下,导致order-service启动类扫描不到UserClient,注入UserClient失败。解决方法如下:

      • 指定Feign应该扫描的包:会将该包下的所有clients全部扫描
      @EnableFeignClients(basePackages = "cn.itcast.feign.clients", defaultConfiguration = DefaultFeignConfiguration.class) @SpringBootApplication
      public class OrderApplication {...
      }
      
      • 指定FeignClient字节码:推荐使用
      @EnableFeignClients(clients = UserClient.class, defaultConfiguration = DefaultFeignConfiguration.class)  @SpringBootApplication
      public class OrderApplication {...
      }
      

3.统一网关Gateway

它旨在为微服务架构提供一种简单有效的统一的API路由管理方式


为什么需要网关

Gateway网关是所有微服务的统一入口。其核心功能特性如下:

  • 身份认证和权限校验:需要校验用户是是否有请求资格,没有则进行拦截
  • 服务路由、负载均衡:网关不处理业务,而是根据某种规则把请求转发到某个微服务,该过程即路由。路由的目标服务有多个时需要做负载均衡
  • 请求限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大

在这里插入图片描述

SpringCloud中网关的实现包括两种:

  • SpringCloudGateway:基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能
  • zuul:基于Servlet的实现,属于阻塞式编程

搭建网关服务

具体步骤如下:

  • 创建新模块(如果没有使用模板创建则记得手动创建启动类),并引入SpringCloudGateway的依赖和nacos的服务发现依赖(网关也是一个服务):
<!-- 网关 -->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- nacos服务发现依赖 -->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
  • 编写基础配置和路由规则:
server:port: 10010  # 网关端口
spring:application:name: gateway  # 服务名称cloud:nacos:#      server-addr: localhost:8848 # nacos地址(未配置集群和Nginx前)server-addr: localhost:80 # nacos地址(即在Nginx中配置的代理地址)gateway:routes: # 网关路由配置- id: user-service  # 路由id,自定义,唯一即可# uri: http://127.0.0.1:8081 # 可使用固定地址作为路由的目标地址uri: lb://userservice # 可使用服务名称作为路由的目标地址,其中lb即负载均衡predicates: # 路由断言,即判断请求是否符合路由规则的条件- Path=/user/**  # 按照路径匹配,只要以/user/开头就符合要求
  • 启动网关:之后访问http://localhost:10010/user/1时,因为符合/user/**规则,所以请求转发到http://userservice/user/1

网关路由的流程图如下:

在这里插入图片描述

网关路由可以配置的内容如下:

  • 路由id:路由的唯一标示
  • 路由目标uri:路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡
  • 路由断言predicates:判断路由的规则
  • 路由过滤器filters:对请求或响应做处理

断言工厂

在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件。例如Path=/user/**是按照路径匹配,该规则由PathRoutePredicateFactory类处理。以下是SpringCloudGateway的断言工厂:

在这里插入图片描述


过滤器工厂

GatewayFilter是网关中提供的一种过滤器,可对进入网关的请求和微服务返回的响应做处理:

在这里插入图片描述

Spring提供了多种不同的路由过滤器工厂:

在这里插入图片描述

当前过滤器写在某个具体路由下只对访问具体服务的请求有效,要对所有的路由都生效可将过滤器工厂写到default下:

spring:application:name: gatewaycloud:gateway:routes: - id: user-service uri: lb://userservicepredicates: - Path=/user/**  - id: order-serviceuri: lb://orderservicepredicates:- Path=/order/**# 给所有进入userservice的请求添加请求头filters: # 针对某个服务请求的过滤器- AddRequestHeader=test filter1! # 添加请求头# 给所有服务的请求添加请求头default-filters: # 默认过滤器,会对所有的路由请求都生效- AddRequestHeader=test filter2! 

全局过滤器

之前介绍的每种过滤器的作用都是固定的,如果想在拦截请求后做自己的业务逻辑则没法实现(即GatewayFilter通过配置定义,处理逻辑固定),此时需要全局过滤器GlobalFilter

假设现在定义全局过滤器拦截请求,判断请求的参数是否满足下面条件(如果同时满足则放行,否则拦截):

  • 参数中是否有authorization
  • authorization参数值是否为admin

具体实现步骤如下:

  • 自定义类实现GlobalFilter接口
  • 添加@Order注解
  • 编写处理逻辑
// GlobalFilter接口的定义如下
public interface GlobalFilter {/***  处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理** @param exchange 请求上下文,里面可以获取Request、Response等信息* @param chain 用来把请求委托给下一个过滤器* @return {@code Mono<Void>} 返回标示当前过滤器业务结束*/Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}// 自定义类
@Order(-1)  // 参数值越小,优先级越大
@Component
public class AuthorizeFilter implements GlobalFilter {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 1.获取请求参数MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();// 2.获取authorization参数String auth = params.getFirst("authorization");// 3.校验if ("admin".equals(auth)) {// 符合要求则放行return chain.filter(exchange);}// 4.如果不符合要求则拦截// 4.1.禁止访问,设置状态码exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);// 4.2.结束处理return exchange.getResponse().setComplete();}
}

过滤器执行顺序

请求路由后,会将当前路由过滤器和DefaultFilterGlobalFilter合并到一个过滤器链中,排序后依次执行每个过滤器。执行顺序如下:

  • 每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前
  • GlobalFilter通过实现Ordered接口或添加@Order注解来指定order值,由我们自己指定
  • 路由过滤器和defaultFilterorderSpring指定,默认是按照声明顺序从1递增(虽然两种过滤器声明在一个配置文件中,但分开计数)
  • 当过滤器的order值一样时按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行

跨域问题处理

跨域:域名不一致就是跨域

  • 域名不同: www.taobao.com和www.taobao.org、www.jd.com和miaosha.jd.com
  • 域名相同,端口不同:localhost:8080localhost:8081

跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,即ajax请求被浏览器拦截
解决方案:一般使用CORS(浏览器询问服务器能否允许某个请求跨域),网关处理跨域采用的同样是CORS方案,进行以下配置即可(可以通过resources/index.html进行跨域测试)

spring:cloud:gateway:globalcors: # 全局的跨域处理add-to-simple-url-handler-mapping: true  # 解决options请求被拦截间题corsConfigurations:'[/**]':al1owed0rigins: #允许哪些网站的跨域请求- "http://localhost:8090"allowedMethods: #允许的跨域ajax的请求方式- "GET"- "POST"- "DELETE"- "PUT"- "OPTIONS"allowedHeaders: "*"  # 允许在请求中携带的头信息allowCredentials: true  # 是否允许携带cookiemaxAge: 360000  # 这次跨域检测的有效期

tips:

  • 对于服务器端的两个模块间的请求不属于浏览器的请求,并且不是ajax请求,所以不会存在跨域问题
  • 浏览器询问服务器某个请求能否跨域属于options请求,所以需要解决询问请求被拦截的问题
  • 在跨域检测的有效期内每次请求都需要去询问下服务器,不跨域就放行。过了有效期后所有请求就直接放行,提升性能

参考

黑马程序员SpringCloud框架P24-P41