欢迎光临
我们一直在努力

java微服务

1. 服务拆分方式

  • 纵向拆分:按照业务模块拆分
  • 横向拆分:抽取公共服务,提高复用性

2. 两种工程结构

2.1 独立 Project

每个服务为一个独立的 Project。

2.2 Maven 聚合

每个服务为一个 Module。

3. 远程调用

Spring 提供了一个 RestTemplate,可以方便地进行 HTTP 请求的发送。

  • 先将RestTemplate注册为一个Bean:

@Configuration
public class RemoteCallConfig {

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

  • RestTemplate的exchange方法:

public <T> ResponseEntity<T> exchange(
String url, // 请求地址
HttpMethod method, // 请求方法 (GET/POST/PUT/DELETE 等)
HttpEntity<?> requestEntity,// 请求实体(含 Headers + Body)
Class<T> responseType, // 响应要转换的目标类型
Object… uriVariables // URL 中的路径参数(可变参数)
)

发送请求时,注入restTemplate然后调用exchange方法设置好参数,然后就可以得到response

4. 服务治理

但是用 RestTemplate 时,一个服务可能有多个实例,或者一个服务挂了需要切换到其他实例,这个时候如果用 RestTemplate 远程调用,就得改 RestTemplate 调用的 IP 和端口,会很麻烦,这就涉及到服务治理问题。

这时候就需要一个注册中心,服务将信息注册到注册中心,然后调用者直接在注册中心查询自己需要的服务,然后调用。

4.1 Nacos 注册中心

Nacos 本身也有一些数据需要被存储,所以得配置数据库。nacos.env 是 Nacos 的配置文件,在里面进行数据源的部署。

4.2 服务注册

  • 引入 Nacos Discovery 依赖
  • spring-cloud-starter-alibaba-nacos-discovery

  • 配置 Nacos 地址
  • spring:
    application:
    name: 服务名称
    cloud:
    nacos:
    server-addr: nacos地址

    4.3 服务发现

    另外,服务发现需要用到一个工具 DiscoveryClient,Spring Cloud 已经帮我们自动装配,我们可以直接注入使用。

    private final DiscoveryClient discoveryClient;

    private void handleCartItems(List<CartVO> vos) {
    // 1. 根据服务名称,拉取服务的实例列表
    List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
    // 2. 负载均衡,挑选一个实例
    ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()));
    // 3. 获取实例的IP和端口
    URI uri = instance.getUri();
    // … 略
    }

    然后取出 URI 就可以调用了(URI 包含 IP 和端口)。

    5. OpenFeign

    5.1 快速入门

    OpenFeign 是一个声明式的 HTTP 客户端,基于常见注解实现 HTTP 请求。

  • 引入依赖,包括 OpenFeign 和负载均衡组件
  • spring-cloud-starter-openfeign
    spring-cloud-starter-loadbalancer

  • 通过 @EnableFeignClients 注解启动 OpenFeign 功能
  • 编写 FeignClient
  • @FeignClient("item-service")
    public interface ItemClient {

    @GetMapping("/items")
    List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
    }

    这里只需要声明接口,无需实现方法。接口中的几个关键信息:

    • @FeignClient("item-service"):声明服务名称
    • @GetMapping:声明请求方式
    • @GetMapping("/items"):声明请求路径
    • @RequestParam("ids") Collection<Long> ids:声明请求参数
    • List<ItemDTO>:返回值类型

    有了上述信息,OpenFeign 就可以利用动态代理帮我们实现这个方法,并且向 http://item-service/items 发送一个 GET 请求,携带 ids 为请求参数,并自动将返回值处理为 List<ItemDTO>。我们只需要直接调用这个方法,即可实现远程调用了。

    5.2 使用 FeignClient

    注入接口,然后调用接口方法即可。

    private ItemClient itemClient;
    List<ItemDTO> items = itemClient.queryItemByIds(List.of(1, 2, 3));

    5.3 连接池

    Feign 底层发起 HTTP 请求,依赖于其它的框架。其底层支持的 HTTP 客户端实现包括:

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

    因此我们通常会使用带有连接池的客户端来代替默认的 HttpURLConnection。比如,我们使用 OKHttp。

  • 引入依赖
  • feign-okhttp

  • 开启连接池功能
  • feign:
    okhttp:
    enabled: true

    重启服务,连接池就生效了。

    5.4 优化方案

    当一个服务的实体类和业务一直被其他服务重复调用时,其他服务就要重复地写接口,非常麻烦。

    两种方案:

  • 将这个服务进行拆分,将它的这些实体类、Client 接口、业务拆分为模块放到该服务的模块下,到时候谁需要就在 pom 中引入即可
  • 将这些通用的实体类、Client 统一写到一个模块,到时候谁需要谁就引入
  • 怎么选择呢?

    前面说到微服务架构有两种:

    • 一种是每个服务是一个独立的 Project,这种就可以使用方案一
    • 第二种就是 Maven 聚合结构,使用方案二

    注意引入模块后,一些不在启动类包下的 Client 无法被扫描到,也就无法加入 IOC 容器。解决办法很简单,在启动类上添加声明即可,两种方式:

    • 方式1:声明扫描包

    @EnableFeignClients("要扫描的client包路径")

    • 方式2:声明要用的 FeignClient

    @EnableFeignClients(clients = {client类})

    5.5 日志

    OpenFeign 只会在 FeignClient 所在包的日志级别为 DEBUG 时,才会输出日志。而且其日志级别有 4 级:

    • NONE:不记录任何日志信息,这是默认值
    • BASIC:仅记录请求的方法,URL 以及响应状态码和执行时间
    • HEADERS:在 BASIC 的基础上,额外记录了请求和响应的头信息
    • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据

    Feign 默认的日志级别就是 NONE,所以默认我们看不到请求日志。

    定义日志级别

    在模块下新建一个配置类,定义 Feign 的日志级别:

    import feign.Logger;
    import org.springframework.context.annotation.Bean;

    public class DefaultFeignConfig {
    @Bean
    public Logger.Level feignLogLevel() {
    return Logger.Level.FULL;
    }
    }

    配置

    接下来,要让日志级别生效,还需要配置这个类。有两种方式:

    • 局部生效:在某个 FeignClient 中配置,只对当前 FeignClient 生效

    @FeignClient(value = "", configuration = DefaultFeignConfig.class)

    • 全局生效:在 @EnableFeignClients 中配置,针对所有 FeignClient 生效

    @EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)

    6. 网关

    网络的关口,负责请求的路由、转发、身份校验。

    6.1 Spring Cloud Gateway

    快速入门

  • 引入依赖
  • spring-cloud-starter-gateway

  • 编写启动类
  • 配置路由规则
  • spring:
    application:
    name: gateway
    cloud:
    nacos:
    server-addr:
    gateway:
    routes:
    id: item # 路由规则id,自定义,唯一
    uri: lb://服务名 # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
    predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
    Path= # 这里是以请求路径作为判断规则

    6.2 路由属性

    • id:路由唯一标识
    • uri:路由目标地址
    • predicates:路由断言,判断请求是否符合当前路由
    • filters:路由过滤器,对请求或响应做特殊处理

    6.3 断言类型

    Spring Cloud Gateway 中支持的断言类型有很多:

    名称说明示例
    After 是某个时间点后的请求 – After=2037-01-20T17:42:47.789-07:00[America/Denver]
    Before 是某个时间点之前的请求 – Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]
    Between 是某两个时间点之间的请求 – Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver]
    Cookie 请求必须包含某些 Cookie – Cookie=chocolate, ch.p
    Header 请求必须包含某些 Header – Header=X-Request-Id, \\d+
    Host 请求必须是访问某个 Host(域名) – Host=**.somehost.org,**.anotherhost.org
    Method 请求方式必须是指定方式 – Method=GET,POST
    Path 请求路径必须符合指定规则 – Path=/red/{segment},/blue/**
    Query 请求参数必须包含指定参数 – Query=name, Jack 或者 – Query=name
    RemoteAddr 请求者的 IP 必须是指定范围 – RemoteAddr=192.168.1.1/24
    Weight 权重处理

    6.4 网关工作流程

  • 客户端请求进入网关后由 HandlerMapping 对请求做判断,找到与当前请求匹配的路由规则(Route),然后将请求交给 WebHandler 去处理。
  • WebHandler 则会加载当前路由下需要执行的过滤器链(Filter chain),然后按照顺序逐一执行过滤器(后面称为 Filter)。
  • Filter 分为两部分,是因为 Filter 内部的逻辑分为 pre 和 post 两部分,分别会在请求路由到微服务之前和之后被执行。
  • 只有所有 Filter 的 pre 逻辑都依次顺序执行通过后,请求才会被路由到微服务。
  • 微服务返回结果后,再倒序执行 Filter 的 post 逻辑。
  • 最终把响应结果返回。
  • 最终请求转发是有一个名为 NettyRoutingFilter 的过滤器来执行的,而且这个过滤器是整个过滤器链中顺序最靠后的一个。如果我们能够定义一个过滤器,在其中实现登录校验逻辑,并且将过滤器执行顺序定义到 NettyRoutingFilter 之前,这就符合我们的需求了!

    6.5 自定义过滤器

    网关过滤器链中的过滤器有两种:

    • GatewayFilter:路由过滤器,作用范围比较灵活,可以是任意指定的路由 Route
    • GlobalFilter:全局过滤器,作用范围是所有路由,不可配置

    @Component
    public class PrintAnyGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> 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;
    }
    }

    7. 微服务获取用户

    现在,网关已经可以完成登录校验并获取登录用户身份信息。但是当网关将请求转发到微服务时,微服务又该如何获取用户身份呢?

    由于网关发送请求到微服务依然采用的是 HTTP 请求,因此我们可以将用户信息以请求头的方式传递到下游微服务。然后微服务可以从请求头中获取登录用户信息。考虑到微服务内部可能很多地方都需要用到登录用户信息,因此我们可以利用 Spring MVC 的拦截器来实现登录用户信息获取,并存入 ThreadLocal,方便后续使用。

    因此,接下来我们要做两件事:

    • 改造网关过滤器,在获取用户信息后保存到请求头,转发到下游微服务
    • 编写微服务拦截器,拦截请求获取用户信息,保存到 ThreadLocal 后放行

    使用 exchange 的 mutate 方法可以改变下游的请求信息,然后就可以在这加入请求头,将用户信息加入请求头。

    String userInfo = userId.toString();
    ServerWebExchange ex = exchange.mutate()
    .request(builder -> builder.header("user-info", userInfo))
    .build();

    // 放行
    return chain.filter(ex);

    编写拦截器后注意两件事:

  • 其他微服务扫描不到拦截器的配置类,为了让配置生效要将配置类放入 spring.factories
  • Spring Cloud Gateway 不引入 Spring MVC,所以 Gateway 服务不能扫描该配置,解决办法很简单:Spring MVC 有通用的类文件 DispatcherServlet,而 Gateway 没有,就可以在配置类上加入条件注解 @ConditionalOnClass(DispatcherServlet.class),这样 Gateway 就不会加载该配置了
  • 8. OpenFeign 传递用户

    前端发起的请求都会经过网关再到微服务,由于我们之前编写的过滤器和拦截器功能,微服务可以轻松获取登录用户信息。

    但有些业务是比较复杂的,请求到达微服务后还需要调用其它多个微服务。

    由于微服务获取用户信息是通过拦截器在请求头中读取,因此要想实现微服务之间的用户信息传递,就必须在微服务发起调用时把用户信息存入请求头。

    微服务之间调用是基于 OpenFeign 来实现的,并不是我们自己发送的请求。我们如何才能让每一个由 OpenFeign 发起的请求自动携带登录用户信息呢?

    这里要借助 Feign 中提供的一个拦截器接口:feign.RequestInterceptor

    我们只需要实现这个接口,然后实现 apply 方法,利用 RequestTemplate 类来添加请求头,将用户信息保存到请求头中。这样一来,每次 OpenFeign 发起请求的时候都会调用该方法,传递用户信息。

    9. 配置管理

    微服务重复配置过多,维护成本高,引入配置管理服务。

    配置更改后服务要重启。

    配置管理能实现快速更改配置,同时实现配置热更新。

    9.1 配置共享

    我们可以把微服务共享的配置抽取到 Nacos 中统一管理,这样就不需要每个微服务都重复配置了。分为两步:

    • 在 Nacos 中添加共享配置。因为正常流程下,Nacos 共享配置在 application.yaml 之前加载,这就会导致 application 配置里 Nacos 地址无法被读取,也就无法加载 Nacos 中的配置
    • 微服务拉取配置(bootstrap.yaml)。bootstrap.yaml 的加载比 application.yaml 要早

    步骤:

  • 引入依赖
  • spring-cloud-starter-alibaba-nacos-config
    spring-cloud-starter-bootstrap

  • 新建 bootstrap.yaml 文件
  • spring:
    application:
    name: # 服务名称
    profiles:
    active: dev
    cloud:
    nacos:
    server-addr: # nacos地址
    config:
    file-extension: yaml # 文件后缀名
    shared-configs: # 共享配置
    dataId: sharedjdbc.yaml # 共享mybatis配置
    dataId: sharedlog.yaml # 共享日志配置
    dataId: sharedswagger.yaml # 共享日志配置

    9.2 配置热更新

    修改配置后微服务无需重启就能使配置生效。

  • Nacos 中要有一个与微服务名有关的配置。dataId 格式:[服务名]-[spring.active.profile].[后缀名]
  • 微服务读取配置,如让一个 Properties 类加注解 @ConfigurationProperties(prefix = "")
  • 9.3 动态路由

    网关的路由配置全部是在项目启动时由 org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator 在项目启动的时候加载,并且一经加载就会缓存到内存中的路由表内(一个 Map),不会改变。也不会监听路由变更,所以,我们无法利用配置热更新来实现路由更新。

    步骤:

  • 引入依赖
  • spring-cloud-starter-alibaba-nacos-config
    spring-cloud-starter-bootstrap

  • 编写 bootstrap.yaml 文件,修改 Gateway 的 resources 目录下的 application.yml,把之前的路由移除

  • 定义配置监听器:

  • @Component
    @RequiredArgsConstructor
    public class DynamicRouteLoader {
    private final RouteDefinitionWriter writer;
    private final NacosConfigManager nacosConfigManager;
    // 路由配置文件的id和分组
    private final String dataId = "gateway-routes.json";
    private final String group = "DEFAULT_GROUP";
    // 保存更新过的路由id
    private final Set<String> routeIds = new HashSet<>();

    @PostConstruct
    public void initRouteConfigListener() throws NacosException {
    // 1.注册监听器并首次拉取配置
    String configInfo = nacosConfigManager.getConfigService()
    .getConfigAndSignListener(dataId, group, 5000, new Listener() {
    @Override
    public Executor getExecutor() {
    return null;
    }

    @Override
    public void receiveConfigInfo(String configInfo) {
    updateConfigInfo(configInfo);
    }
    });
    // 2.首次启动时,更新一次配置
    updateConfigInfo(configInfo);
    }

    private void updateConfigInfo(String configInfo) {
    log.debug("监听到路由配置变更,{}", configInfo);
    // 1.反序列化
    List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);
    // 2.更新前先清空旧路由
    // 2.1.清除旧路由
    for (String routeId : routeIds) {
    writer.delete(Mono.just(routeId)).subscribe();
    }
    routeIds.clear();
    // 2.2.判断是否有新的路由要更新
    if (CollUtils.isEmpty(routeDefinitions)) {
    // 无新路由配置,直接结束
    return;
    }
    // 3.更新路由
    routeDefinitions.forEach(routeDefinition -> {
    // 3.1.更新路由
    writer.save(Mono.just(routeDefinition)).subscribe();
    // 3.2.记录路由id,方便将来删除
    routeIds.add(routeDefinition.getId());
    });
    }
    }

    接下来,我们直接在 Nacos 控制台添加路由配置,路由文件名为 gateway-routes.json,类型为 JSON:

    {
    "id": "",
    "predicates": [{
    "name": "",
    "args": {}
    }],
    "filters": [],
    "uri": ""
    }

    和之前 YAML 文件差不多,只是格式变化了,因为 JSON 格式更容易读取为 String,也就是上面要得到的 configInfo。

    赞(0)
    未经允许不得转载:171主机测评 » java微服务
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址