spring-cloud学习之2.搭建请求网关spring-cloud-getway

一:准备

请求网关,顾名思义,所有请求都有网关统一处理,路由至各个服务,getway是spring最新网关,有取代zuul的趋势,具体请百度。

1.导包

getway包:

<!--gateway 网关依赖,内置webflux 依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

getway熔断:

 <!--        熔断-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

getway自带限流功能,内部使用的是redis

   <!--        redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

eureka实例:

  <!--eureka 客户端 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

2:配置:

server:
  port: 2001
spring:
  application:
    name: dandelion-getway
  cloud:
    gateway:
      discovery:
        locator:
          # 是否和服务注册与发现组件结合,设置为 true 后可以直接使用应用名称调用服务
          enabled: true

      # 路由配置中心
      routes:
        # 服务中心
        - id: dandelion-api #id 以-开头 唯一即可
          uri: lb://dandelion-api  #lb://代表服务内转发,后跟服务名称
          #断言 和过滤差不多意思,其中有配置具体百度
          predicates:
            - Method=GET  #只接受get方法
            - Path=/system/** #只接收/system开头的路径
            # 过滤器配置  getway有两种过滤方式,GatewayFilter和GlobalFilter
            # GatewayFilter : 需要通过spring.cloud.routes.filters 配置在具体路由下,只作用在当前路由上或通过spring.cloud.default-filters配置在全局,作用在所有路由上
            #GlobalFilter : 全局过滤器,不需要在配置文件中配置,作用在所有的路由上。
          filters:
            # 验证码处理
            - ImgCodeFilter
            #设置StripPrefix=1表示从二级url路径转发,即http://localhost:2001/auth/demo将会转发到http://localhost:2002/demo
            - StripPrefix=1
            - name: RequestRateLimiter  #固定名称
              args:
                #配置限流键的解析器
                key-resolver: '#{@ipRequestLimiter}'
                #令牌桶每秒填充速率,1s/1次
                redis-rate-limiter.replenishRate: 1
                # 令牌桶总数量
                redis-rate-limiter.burstCapacity: 1
            # 降级配置
            - name: Hystrix #固定名称
              args:
                name: fallbackcmd
                fallbackUri: 'forward:/fallback'

  redis:
    host: 192.168.211.128
    jedis:
      pool:
        max-wait: 300ms
    timeout: 1 #单位秒

eureka:
  client:
    service-url:
      defaultZone: http://localhost:1001/eureka
  instance:
    instance-id:  ${spring.application.name}:${server.port}
    prefer-ip-address: true     #访问路径可以显示IP地址

hystrix:
  command:
    default:  #default全局有效,service id指定应用有效
      execution:
        timeout:
          enabled: true  #是否启用超时 默认启用
        isolation:
          thread:
            timeoutInMilliseconds: 1000 # 命令执行超时时间,默认1000ms

3.过滤器:

package club.dandelion.cloud.getway.filter;

import club.dandelion.cloud.common.R;
import club.dandelion.cloud.common.cons.Constants;
import club.dandelion.cloud.common.exception.ValidateCodeException;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;

/**
 * 验证码处理
 *
 * @author jiang
 */
@Component
public class ImgCodeFilter extends AbstractGatewayFilterFactory<ImgCodeFilter.Config> {
    private final static String AUTH_URL = "/auth/login";

    @Autowired
    private StringRedisTemplate redisTemplate;

    public ImgCodeFilter() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {

        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            URI uri = request.getURI();
            // 不是登录请求,直接向下执行
            if (!StringUtils.containsIgnoreCase(uri.getPath(), AUTH_URL)) {
                return chain.filter(exchange);
            }
            try {
                String bodyStr = resolveBodyFromRequest(request);
                JSONObject bodyJson = JSONObject.parseObject(bodyStr);
                String code = (String) bodyJson.get("captcha");
                String randomStr = (String) bodyJson.get("randomStr");
                // 校验验证码
                checkCode(code, randomStr);
            } catch (Exception e) {
                e.printStackTrace();
                ServerHttpResponse response = exchange.getResponse();
                response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
                response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
                String msg = JSON.toJSONString(R.error(e.getMessage()));
                DataBuffer bodyDataBuffer = response.bufferFactory().wrap(msg.getBytes());
                return response.writeWith(Mono.just(bodyDataBuffer));
            }
            return chain.filter(exchange);
        };
    }

    /**
     * 获取请求体
     *
     * @param serverHttpRequest
     * @return
     */
    private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest) {
        // 获取请求体
        Flux<DataBuffer> body = serverHttpRequest.getBody();
        AtomicReference<String> bodyRef = new AtomicReference<>();
        body.subscribe(buffer -> {
            CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
            DataBufferUtils.release(buffer);
            bodyRef.set(charBuffer.toString());
        });

        return bodyRef.get();
    }

    /**
     * 检查code
     *
     * @param code
     * @param randomStr
     */
    @SneakyThrows
    private void checkCode(String code, String randomStr) {
        if (StringUtils.isBlank(code)) {
            throw new ValidateCodeException("验证码不能为空");
        }
        if (StringUtils.isBlank(randomStr)) {
            throw new ValidateCodeException("验证码不合法");
        }
        String key = Constants.DEFAULT_CODE_KEY + randomStr;
        String saveCode = redisTemplate.opsForValue().get(key);
        redisTemplate.delete(key);
        if (!code.equalsIgnoreCase(saveCode)) {
            throw new ValidateCodeException("验证码不合法");
        }
    }

    /**
     * 必须要有
     */
    public static class Config {
    }
}
View Code
package club.dandelion.cloud.getway.config;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;

/**
 * getway限流配置
 *
 * @author jiang
 */
@Configuration
public class HttpRequestLimiter {

    @Bean
    public KeyResolver ipRequestLimiter() {
        return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
    }

}
View Code

4.熔断反馈:

import club.dandelion.cloud.common.R;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;

import java.util.Optional;

import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR;

/**
 * 错误信息
 *
 * @author jiang
 */
@Slf4j
@Component
public class HystrixFallbackHandler implements HandlerFunction<ServerResponse> {
    @Override
    public Mono<ServerResponse> handle(ServerRequest serverRequest) {
        Optional<Object> originalUris = serverRequest.attribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
        originalUris.ifPresent(originalUri -> log.error("网关执行请求:{}失败,hystrix服务降级处理", originalUri));
        return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR.value()).contentType(MediaType.APPLICATION_JSON_UTF8)
                .body(BodyInserters.fromValue(JSON.toJSONString(R.error("服务已被降级熔断"))));
    }
}

5:路径路由 相当于controller


import club.dandelion.cloud.getway.handler.HystrixFallbackHandler;
import club.dandelion.cloud.getway.handler.ImgCodeHandler;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;

/**
* 没有controller所以配置路由信息
* <p>
* RouterFunction使用RequestPredicate将传入请求映射到HandlerFunction。
* <p>
* AllArgsConstructor lombok注解,代表有所有的构造参数 所以下面两个属性是根据构造参数注入的
*
* @author jiang
*/
@Configuration
@AllArgsConstructor
public class RouterFunctionConfiguration {

private HystrixFallbackHandler hystrixFallbackHandler;
private ImgCodeHandler imgCodeHandler;

@Bean
public RouterFunction<?> routerFunction() {
return RouterFunctions
.route(RequestPredicates.path("/fallback").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),
hystrixFallbackHandler)
.andRoute(RequestPredicates.GET("/code").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),
imgCodeHandler);
}
}