Spring Cloud Gateway网关实现短网址生成、解析、转发

Spring Cloud Gateway网关实现短网址生成、解析、转发

1、概述

在一些生成二维码等场景中,需要使用短网址,本文讲解如何在Spring Cloud Gateway中实现短网址生成、解析、转发,其中使用到了Redis作为存储

2、基础实现

1、定义长网址转短网址的算法API(本算法逻辑来源于网络)

public class ShortUrl {

  private static final List<Character> BASE64_CHARS = Arrays.asList('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
      'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
      'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3',
      '4', '5', '6', '7', '8', '9', '+', '_', '&', '=', '?');

  public static final int SHORT_URL_LEN = 6;

  private final byte[] hash;

  public ShortUrl() {
    this(random());
  }

  public ShortUrl(byte[] hash) {
    this.hash = Arrays.copyOf(hash, hash.length);
  }

  public ShortUrl(String hash) {
    this(hash(hash));
  }

  private static char byte2char(byte b) {
    return BASE64_CHARS.get(b);
  }

  private static byte char2byte(char charAt) {
    return (byte) BASE64_CHARS.indexOf(charAt);
  }

  private static byte[] hash(String hash) {
    final byte[] res = new byte[SHORT_URL_LEN];
    for (int i = 0; i < SHORT_URL_LEN; i++) {
      res[i] = char2byte(hash.charAt(i));
    }
    return res;
  }

  /**
   * Random short url.
   *
   * @return
   */
  private static byte[] random() {
    final Random rand = ThreadLocalRandom.current();
    return new byte[] {
        // 1st random char
        (byte) rand.nextInt(BASE64_CHARS.size()),
        // 2nd random char
        (byte) rand.nextInt(BASE64_CHARS.size()),
        // 3rd random char
        (byte) rand.nextInt(BASE64_CHARS.size()),
        // 4th random char
        (byte) rand.nextInt(BASE64_CHARS.size()),
        // 5th random char
        (byte) rand.nextInt(BASE64_CHARS.size()),
        // 6th random char
        (byte) rand.nextInt(BASE64_CHARS.size()) };
  }

  @Override
  public boolean equals(Object o) {
    if (o == this) {
      return true;
    }
    if (o != null && o.getClass() == this.getClass()) {
      return Arrays.equals(((ShortUrl) o).hash, this.hash);
    }
    return false;
  }

  /*
   * hashCode() implementation.
   *
   * A ShortUrl instance needs 36bits, hashCode() only uses 32b.
   *
   * (non-Javadoc)
   *
   * @see java.lang.Object#hashCode()
   */
  @Override
  public int hashCode() {
    int hashCode = this.hash[0] & 3;
    hashCode = hashCode * 64 + this.hash[1];
    hashCode = hashCode * 64 + this.hash[2];
    hashCode = hashCode * 64 + this.hash[3];
    hashCode = hashCode * 64 + this.hash[4];
    hashCode = hashCode * 64 + this.hash[5];
    return hashCode;
  }

  /*
   * Human readable form.
   *
   * (non-Javadoc)
   *
   * @see java.lang.Object#toString()
   */
  @Override
  public String toString() {
    final StringBuilder sb = new StringBuilder();
    int idx = 0;
    sb.append(byte2char(this.hash[idx++]));
    sb.append(byte2char(this.hash[idx++]));
    sb.append(byte2char(this.hash[idx++]));
    sb.append(byte2char(this.hash[idx++]));
    sb.append(byte2char(this.hash[idx++]));
    sb.append(byte2char(this.hash[idx]));
    return sb.toString();
  }
}

2、创建抽象类AbstractDwzService和子类LocalDwzService

创建属性类:DwzProperties

@ConfigurationProperties(prefix = "dwz")
@RefreshScope
public class DwzProperties {
    /**
     * 短网址类型,支持:local
     */
    private String type;

    /**
     * 短网址失效时间
     */
    private Duration ttl = Duration.ofDays(365);

    /**
     * host
     */
    private String host;
}

创建抽象类:AbstractDwzService

public abstract class AbstractDwzService {
    @Autowired
    protected StringRedisTemplate redisTemplate;
    protected static final String code2UrlKeyPrefix = "_gateway:short-code-url:";

    /**
     * 长网址转换成短码
     *
     * @param longUrl
     * @return 短网址
     * 调用接口错误时,返回长网址
     */
    abstract String getShortCode(String longUrl);

    /**
     * 根据短网址编码获取长网址
     *
     * @param shortCode 短网址编码
     * @return 长网址
     */
    protected String getLongUrl(String shortCode) {
        String codeUrlKey = code2UrlKeyPrefix + shortCode;
        return redisTemplate.opsForValue().get(codeUrlKey);
    }
}

创建实现类LocalDwzService ,通过@ConditionalOnProperty很容易切换到一些互联网第三方的如百度短网址等

@ConditionalOnProperty(name = "dwz.type", havingValue = "local", matchIfMissing = true)
@Service
@RequiredArgsConstructor
public class LocalDwzService extends AbstractDwzService {
    private final DwzProperties properties;
    //第三个参数设为true时,在被使用时会把被使用的数据放到结尾。
    private final Map<String, String> lruMap = new LinkedHashMap<String, String>(1024,0.75F ,true){
        @Override
        protected boolean removeEldestEntry(Map.Entry<String, String> var){
            // 当容量大于指定值时候删除头部元素
            return this.size() > 102400;
        }
    };

    @Override
    public  String getShortCode(String longUrl) {
        String md5Hex = DigestUtils.md5Hex(longUrl);
        return lruMap.computeIfAbsent(md5Hex, key -> {
            ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
            String shortCode = new ShortUrl(md5Hex).toString();
            String codeUrlKey = code2UrlKeyPrefix + shortCode;
            valueOperations.set(codeUrlKey, longUrl, properties.getTtl());
            return shortCode;
        });
    }
}

3、路由处理HandlerFunction

DwzDecodeHandler(短网址转长网址)
DwzEncodeHandler(长网址转短网址)
DwzRedirectHandler(短网址重定向长网址)

@Component
public class DwzDecodeHandler implements HandlerFunction<ServerResponse> {
    @Autowired
    private AbstractDwzService dwzService;

    @Override
    public @NotNull Mono<ServerResponse> handle(@NotNull ServerRequest request) {
        ServerHttpRequest httpRequest = request.exchange().getRequest();
        String code = httpRequest.getQueryParams().getFirst("code");
        try {
            String longUrl = dwzService.getLongUrl(code);
            return ServerResponse.status(HttpStatus.OK)
                    .header("Long-Url", longUrl)
                    .body(BodyInserters.fromValue(Collections.singletonMap("longUrl", longUrl)));
        } catch (Exception e) {
            return Mono.error(e);
        }
    }
}


@Slf4j
@Component
public class DwzEncodeHandler implements HandlerFunction<ServerResponse> {
    @Autowired
    private AbstractDwzService dwzService;
    @Autowired
    private DwzProperties properties;

    @Override
    public @NotNull Mono<ServerResponse> handle(@NotNull ServerRequest request) {
        ServerHttpRequest httpRequest = request.exchange().getRequest();
        String longUrl = httpRequest.getQueryParams().getFirst("url");
        try {
            String shortCode = dwzService.getShortCode(longUrl);
            String shortRedirectUrl = getHost(httpRequest) + "/=" + shortCode;
            Map<String, String> body = new HashMap<>(2);
            body.put("code", shortCode);
            body.put("redirectUrl", shortRedirectUrl);
            log.info("生成短网址成功,短码:{} ,短网址:{},原网址:{}", shortCode, shortRedirectUrl, longUrl);
            return ServerResponse.status(HttpStatus.OK)
                    .header(HttpHeaders.LOCATION, shortRedirectUrl)
                    .body(BodyInserters.fromValue(body));
        } catch (Exception e) {
            return Mono.error(e);
        }
    }

    private String getHost(ServerHttpRequest httpRequest) {
        String host = properties.getHost();
        if (StringUtils.isNotBlank(host)) {
            return host;
        }
        URI uri = httpRequest.getURI();
        return StrUtil.format("{}://{}:{}", uri.getScheme(), uri.getHost(), uri.getPort());
    }
}


@Component
public class DwzRedirectHandler implements HandlerFunction<ServerResponse> {
    @Autowired
    private AbstractDwzService dwzService;

    @Override
    public @NotNull Mono<ServerResponse> handle(@NotNull ServerRequest request) {
        ServerWebExchange exchange = request.exchange();
        ServerHttpRequest httpRequest = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        String path = httpRequest.getPath().value();
        String code = path.substring(path.lastIndexOf('/') + 2);
        String longUrl = dwzService.getLongUrl(code);
        if (StringUtils.isBlank(longUrl)) {
            return ServerResponse.status(HttpStatus.NOT_FOUND).build();
        }
        String url = httpRequest.getURI().toString();
        String paramDelimiter = "?";
        int paraIndex = url.lastIndexOf(paramDelimiter);
        if (paraIndex >= 0 && !url.endsWith(paramDelimiter)) {
            if (longUrl.contains(paramDelimiter)) {
                longUrl = longUrl + "&" + url.substring(paraIndex + 1);
            } else {
                longUrl = longUrl + paramDelimiter + url.substring(paraIndex + 1);
            }
        }

        response.getHeaders().set(HttpHeaders.LOCATION, longUrl);
        //303状态码表示由于请求对应的资源存在着另一个URI,应使用GET方法定向获取请求的资源
        return ServerResponse.status(HttpStatus.SEE_OTHER)
                .header(HttpHeaders.LOCATION, longUrl)
                .header(HttpHeaders.CONTENT_TYPE, "text/plain;charset=UTF-8")
                .build();
    }
}


4、配置路由

@Configuration
public class RouterFunctionConfiguration {
    @Autowired
    private DwzEncodeHandler dwzHandler;
    @Autowired
    private DwzDecodeHandler decodeHandler;
    @Autowired
    private DwzRedirectHandler redirectHandler;

    @Bean
    public RouterFunction routerFunction() {
        return RouterFunctions
                .route(RequestPredicates.GET("/url/short"), dwzHandler)
                .andRoute(RequestPredicates.GET("/url/long"), decodeHandler)
                .andRoute(RequestPredicates.GET("/=*"), redirectHandler);
    }
}

然后在application.yml中增加配置即可

dwz:
  type: local
  ttl: 365D
  host: http://localhost:8080

5、测试

长网址转短网址,GET /url/short?url={url},其中,如果url中携带参数需要encode,示例
以淘宝为例:生成淘宝的短网址并且携带参数,长网址:http://www.taobao.com?a=001,URL Encode长网址结果:http%3a%2f%2fwww.taobao.com%3fa%3d001

调用
POST http://localhost:8080/url/short?url=http%3a%2f%2fwww.taobao.com%3fa%3d001

结果

{
    "code": "89be85",
    "redirectUrl": "http://localhost:8080/=89be85"
}

访问redirectUrl,即可重定向到淘宝页面。还可以在短网址中携带额外参数,如:http://localhost:8080/=89be85?from=csdn

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值