从零搭建微服务项目Base(第7章——微服务网关模块基础实现)
前言:
在前面6章的学习中已经完成了服务间的调用实现,即各微服务通过nacos或eureka服务器完成服务的注册,并从nacos中拉取配置实现热更新。当某个服务接口需要调用其他服务时,通过feign定义接口,并通过注解配置服务名称,在nacos或eureka服务器中找到对应服务端口完成调用。
但实际应用中,用户不可能直接访问这些服务端口,因为每个服务对应一个端口,当服务拆分很多时,会有大量端口,前端开发人员不可能针对每次调用看文档找对应端口,因此引入网关模块,用户只需要访问网关模块端口,网关模块自动转发,且网关模块也能实现服务聚合、负载均衡、用户鉴权等功能。为此,本章实现基础的网关模块,包括网关服务模块创建、路由断言、过滤器配置。
本章代码基于第6章项目,前置源码可在第6章博客下载,博客链接如下:
从零搭建微服务项目(第6章——Feign性能优化以及模块抽取)-CSDN博客https://blog.csdn.net/wlf2030/article/details/145649565简要介绍前置项目流程:order-service以及user-service两服务分别连接order-db以及user-db两数据库,order-db中仅有user-id,user-info存在user-db中,为提供完整order-info,order-service通过nacos发现user-service服务地址并使用Feign调用服务端口拿取user-info结合从order-db中拿取的信息返回给前端。同时项目自定义日志输出。
本项目源码链接如下:
wlf728050719/SpringCloudBase7https://github.com/wlf728050719/SpringCloudBase7以及本专栏会持续更新微服务项目,每一章的项目都会基于前一章项目进行功能的完善,欢迎小伙伴们关注!同时如果只是对单章感兴趣也不用从头看,只需下载前一章项目即可,每一章都会有前置项目准备部分,跟着操作就能实现上一章的最终效果,当然如果是一直跟着做可以直接跳过这一部分。
一、前置项目准备
1.从github下载前一章的项目解压,重命名为Base7打开。
2.重命名模块为Base7.
3.父工程pom.xml中<name>改成Base7。
4.选择环境为dev,并重新加载maven
5.启动nacos(安装和启动见第三章)
6.进入nacos网页 配置管理->配置列表确认有这些yaml文件。
(如果不是一直跟着专栏做自然是没有的,需要看第四章的环境隔离和配置拉取,记得把父工程pom文件中namespace的值与nacos中命名空间生成的保持一致)
7.配置数据源,更换两服务的resources下yml文件的数据库配置,数据库sql见第一章数据库准备部分。
.测试数据库连接 属性->点击数据源->测试连接->输入用户名密码
8.添加运行配置 服务->加号->运行配置类型->spring boot。
启动服务,测试接口。
能够在日志文件中看到最新的日志记录。
二、网关服务模块创建以及配置
1.新建SpringBoot模块,配置如下。
2.不添加任何依赖
3.删除不必要文件和目录,最终结构如下。
4.将gateway模块的pom文件替换为下面内容。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.bit</groupId>
<artifactId>Base7</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gateway</name>
<description>gateway</description>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<!-- nacos客户端依赖包 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
4.修改父文件pom,即将gateway模块声明为父模块的子模块,重新加载maven模块。
5.重新加载maven文件后,查看maven结构是否如下,如果不是见本专栏第0章第三节部分有对应解决方法。
6.在父文件pom中为gateway在各配置环境下设置端口。
7.在gateway的application中排除默认数据源,否则需要在application中配置数据源,后续动态路由时需要使用数据库时再恢复。
8.为gateway在resources目录下创建application.yml配置文件,内容如下:
server:
port: @gateway.port@
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848
discovery:
namespace: @namespace@
gateway:
routes:
- id: user-service
uri:
lb://user-service
predicates:
- Path=/user/**
- id: order-service
uri:
lb://order-service
predicates:
- Path=/order/**
9.启动服务
通过在网关端口输入路径即可通过断言调取对应服务的接口。
三、路由断言
目前是通过application.yml配置路由工厂。但使用动态路由需要重写实现路由工厂类,以及使用统一的格式便于规范数据库中路由信息,即使用args+name,为方便后续章节理解,使用args+name替换gateway的application.yml,内容如下:
server:
port: @gateway.port@
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848
discovery:
namespace: @namespace@
gateway:
routes:
- id: user-service
uri:
lb://user-service
predicates:
- name: Path
args:
_genkey_0: /user/**
- id: order-service
uri:
lb://order-service
predicates:
- name: Path
args:
_genkey_0: /order/**
测试通过
四、路由过滤器
后续鉴权模块需要配合网关的过滤器一起搭配使用才能实现不同角色不同权限访问对应服务/端口,需要手写代码实现 AbstractGatewayFilterFactory,目前先通过yml配置文件为网关添加过滤器方便后续理解。
修改网关模块的yml文件如下:(现在的yml其实就已经很长了,后续必然需要使用代码结合数据库代替配置文件)
server:
port: @gateway.port@
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848
discovery:
namespace: @namespace@
gateway:
routes:
- id: user-service
uri:
lb://user-service
predicates:
- name: Path
args:
_genkey_0: /user/**
filters:
- name: AddRequestHeader
args:
name: source
value: request user from gateway
- id: order-service
uri:
lb://order-service
predicates:
- name: Path
args:
_genkey_0: /order/**
filters:
- name: AddRequestHeader
args:
name: source
value: request order from gateway
为了获取请求头内容,对OrderController和UserController进行修改。直接替换成下面内容即可。
package cn.bit.orderservice.controller;
import cn.bit.common.pojo.vo.OrderInfoVO;
import cn.bit.common.pojo.vo.R;
import cn.bit.orderservice.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/test/{id}")
public String test(@PathVariable Integer id) {
System.out.println(id);
return id.toString();
}
@GetMapping("/info/{id}")
public R getOrderInfoById(@PathVariable Integer id, @RequestHeader(value = "source",required = false) String source) {
log.debug("debug");
log.info("info");
log.warn("warning");
System.out.println(source);
OrderInfoVO orderInfoVO = orderService.getOrderInfoById(id);
if (orderInfoVO == null) {
return R.failed("订单不存在");
}
else
return R.ok(orderInfoVO);
}
}
package cn.bit.userservice.controller;
import cn.bit.common.pojo.dto.UserBaseInfoDTO;
import cn.bit.common.pojo.vo.R;
import cn.bit.common.pojo.vo.UserFavorVO;
import cn.bit.userservice.config.PatternProperties;
import cn.bit.userservice.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private PatternProperties patternProperties;
@GetMapping("/test/{id}")
public String test(@PathVariable Integer id) {
System.out.println(id);
return id.toString()+" "+LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat()));
}
@GetMapping("/favor/{id}")
public R getUserFavorById(@PathVariable Integer id) {
UserFavorVO vo = userService.getUserFavorById(id);
if(vo != null) {
return R.ok(vo);
}
else
return R.failed("用户不存在");
}
@GetMapping("/baseInfo/{id}")
public R getUserBaseInfoById(@PathVariable Integer id, @RequestHeader(value = "source",required = false) String source) {
System.out.println("get request");
System.out.println(source);
UserBaseInfoDTO dto = userService.getUserBaseInfoById(id);
if(dto != null) {
return R.ok(dto);
}
else
return R.failed("用户不存在");
}
}
启动服务
先访问user接口即localhost:1233/user/baseInfo/1
能够验证确实添加了请求头
再访问order接口即localhost:1233/order/info/1
发现order-service获取到请求头,user-service为null,因为网关只对访问order-service的request添加了请求头,order-service之后使用feign访问的user-service,自然没有请求头。
为了实现调用朔源可以修改UserClient的接口方法
以及其调用
再次启动服务调用
五、全局过滤器
之前的过滤器为每个路由的过滤器,而全局过滤器无论使用哪条路由均需要使用。配置如下:
在网关模块创建filter包以及AuthorizeFilter类,内容如下:
package cn.bit.gateway.filter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Order(-1)
@Component
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> queryParams = request.getQueryParams();
String token = queryParams.getFirst("token");
if("admin".equals(token)) {
return chain.filter(exchange);
}
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}
重启后发现只有请求参数包含token字段且值为token才能访问对应服务。
六、过滤器执行顺序
最后:
黑马课程关于网关模块讲解还是比较浅显,和企业实际应用有较大出入,后续会研究动态路由实现以及鉴权模块,这两模块比较复杂所以后续更新会慢些。