Как реализовать собственный API-шлюз с нуля?

Java задняя часть API

Преамбула

Предыдущая статья:Вы даже не знаете сигнатуру внешнего интерфейса? Еще есть время научиться.

Есть много мелких партнеров, которые ответили, что соответствующая подпись и проверка во внешнем API могут быть обработаны шлюзом.

Когда дело доходит до шлюзов, все должны быть с ними знакомы. На рынке широко используются: весеннее облако/конг/соул.

网关

Роль шлюза API

(1) Проверка разрешений во внешнем интерфейсе

(2) Ограничения на количество и частоту интерфейсных вызовов

(3) Балансировка нагрузки, кэширование, маршрутизация, контроль доступа, прокси-сервер службы, мониторинг, ведение журнала и т. д. в шлюзе микрослужб.

Принцип реализации

在这里插入图片描述

В общих запросах доступ к серверу осуществляется напрямую через клиента.Нам нужно реализовать уровень API-шлюза посередине, внешний клиент обращается к шлюзу, а затем шлюз перенаправляет вызов.

основной процесс

Шлюз звучит очень сложно, а основная часть на самом деле основана на сервлете.javax.servlet.Filterреализовать.

Мы позволяем клиенту вызывать шлюз, а затем равномерно анализируем и пересылаем заголовок сообщения в фильтре, вызываем сервер, а затем инкапсулируем его и возвращаем клиенту.

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

/**
 * @author binbin.hou
 * @since 1.0.0
 */
@WebFilter
@Component
public class GatewayFilter implements Filter {

    private static final Logger LOGGER = LoggerFactory.getLogger(GatewayFilter.class);

    public void init(FilterConfig filterConfig) throws ServletException {

    }

    public void doFilter(ServletRequest servletRequest,
                         ServletResponse servletResponse,
                         FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        LOGGER.info("url={}, params={}", req.getRequestURI(), JSON.toJSONString(req.getParameterMap()));

        //根据 URL 获取对应的服务名称

        // 进行具体的处理逻辑

        // TODO...

        } else {
            filterChain.doFilter(req, servletResponse);
        }
    }

    public void destroy() {

    }

}

Далее нам просто нужно сосредоточиться на том, как переопределить метод дофильтра.

Выполнение

получить имя приложения

Шлюз предназначен для всех приложений внутри компании, и мы можем отличить его по уникальному appName каждой службы.

Например, если имя приложения test, то вызывается запрос шлюза:

https://gateway.com/test/version

Для этого запроса соответствующее имя приложения — test, а фактически запрошенный URL — /version.

Конкретная реализация также относительно проста:

@Override
public Pair<String, String> getRequestPair(HttpServletRequest req) {
    final String url = req.getRequestURI();
    if(url.startsWith("/") && url.length() > 1) {
        String subUrl = url.substring(1);
        int nextSlash = subUrl.indexOf("/");
        if(nextSlash < 0) {
            LOGGER.warn("请求地址 {} 对应的 appName 不存在。", url);
            return Pair.of(null, null);
        }
        String appName = subUrl.substring(0, nextSlash);
        String realUrl = subUrl.substring(nextSlash);
        LOGGER.info("请求地址 {} 对应的 appName: {}, 真实请求地址:{}", url, appName, realUrl);
        return Pair.of(appName, realUrl);
    }
    LOGGER.warn("请求地址: {} 不是以 / 开头,或者长度不够 2,直接忽略。", url);
    return Pair.of(null, null);
}

запрашивать информацию заголовка

Соответствующий запрос информации заголовка построен в соответствии с HTTPServletRequest:

/**
 * 构建 map 信息
 * @param req 请求
 * @return 结果
 * @since 1.0.0
 */
private Map<String, String> buildHeaderMap(final HttpServletRequest req) {
    Map<String, String> map = new HashMap<>();
    Enumeration<String> enumeration = req.getHeaderNames();
    while (enumeration.hasMoreElements()) {
        String name = enumeration.nextElement();
        String value = req.getHeader(name);
        map.put(name, value);
    }
    return map;
}

обнаружение службы

Когда мы анализируем запрошенное приложение, appName = test, мы можем запросить соответствующую информацию ip:port в тестовом приложении в центре конфигурации.

@Override
public String buildRequestUrl(Pair<String, String> pair) {
    String appName = pair.getValueOne();
    String appUrl = pair.getValueTwo();
    String ipPort = "127.0.0.1:8081";
    //TODO: 根据数据库配置查询
    // 根据是否启用 HTTPS 访问不同的地址
    if (appName.equals("test")) {
        // 这里需要涉及到负载均衡
        ipPort = "127.0.0.1:8081";
    } else {
        throw new GatewayServerException(GatewayServerRespCode.APP_NAME_NOT_FOUND_IP);
    }
    String format = "http://%s/%s";
    return String.format(format, ipPort, appUrl);
}

Здесь запись временно фиксируется, и, наконец, возвращается адрес запроса фактического сервера.

Здесь вы также можете комбинировать определенные стратегии балансировки нагрузки/маршрутизации для дальнейшего выбора сервера.

Другой метод

Существуют различные способы поддержки HTTP. Мы временно поддерживаем запросы GET/POST.

По сути, это запрос на GET/POST-запросы для построения в виде сервера вызовов.

Здесь реализация может быть самой разнообразной, вот пример реализации клиента ok-http.

Определение интерфейса

Чтобы облегчить последующее расширение, все вызовы метода реализуют один и тот же интерфейс:

public interface IMethodType {

    /**
     * 处理
     * @param context 上下文
     * @return 结果
     */
    IMethodTypeResult handle(final IMethodTypeContext context);

}

GET

ПОЛУЧИТЬ запрос.

@Service
@MethodTypeRoute("GET")
public class GetMethodType implements IMethodType {

    @Override
    public IMethodTypeResult handle(IMethodTypeContext context) {
        String respBody = OkHttpUtil.get(context.url(), context.headerMap());
        return MethodTypeResult.newInstance().respJson(respBody);
    }

}

POST

POST-запрос.

@Service
@MethodTypeRoute("POST")
public class PostMethodType implements IMethodType {

    @Override
    public IMethodTypeResult handle(IMethodTypeContext context) {
        HttpServletRequest req = (HttpServletRequest) context.servletRequest();
        String postJson = HttpUtil.getPostBody(req);
        String respBody = OkHttpUtil.post(context.url(), postJson, context.headerMap());

        return MethodTypeResult.newInstance().respJson(respBody);
    }

}

Реализация OkHttpUtil

OkHttpUtil основан на инструментах http-вызова пакета ok-http.

import com.github.houbb.gateway.server.util.exception.GatewayServerException;
import com.github.houbb.heaven.util.util.MapUtil;
import okhttp3.*;

import java.io.IOException;
import java.util.Map;

/**
 * @author binbin.hou
 * @since 1.0.0
 */
public class OkHttpUtil {

    private static final MediaType JSON
            = MediaType.parse("application/json; charset=utf-8");

    /**
     * get 请求
     * @param url 地址
     * @return 结果
     * @since 1.0.0
     */
    public static String get(final String url) {
        return get(url, null);
    }

    /**
     * get 请求
     * @param url 地址
     * @param headerMap 请求头
     * @return 结果
     * @since 1.0.0
     */
    public static String get(final String url,
                             final Map<String, String> headerMap) {
        try {
            OkHttpClient client = new OkHttpClient();
            Request.Builder builder = new Request.Builder();
            builder.url(url);

            if(MapUtil.isNotEmpty(headerMap)) {
                for(Map.Entry<String, String> entry : headerMap.entrySet()) {
                    builder.header(entry.getKey(), entry.getValue());
                }
            }

            Request request = builder
                    .build();

            Response response = client.newCall(request).execute();
            return response.body().string();
        } catch (IOException e) {
            throw new GatewayServerException(e);
        }
    }

    /**
     * get 请求
     * @param url 地址
     * @param body 请求体
     * @param headerMap 请求头
     * @return 结果
     * @since 1.0.0
     */
    public static String post(final String url,
                              final RequestBody body,
                             final Map<String, String> headerMap) {
        try {
            OkHttpClient client = new OkHttpClient();
            Request.Builder builder = new Request.Builder();
            builder.post(body);
            builder.url(url);

            if(MapUtil.isNotEmpty(headerMap)) {
                for(Map.Entry<String, String> entry : headerMap.entrySet()) {
                    builder.header(entry.getKey(), entry.getValue());
                }
            }

            Request request = builder.build();
            Response response = client.newCall(request).execute();
            return response.body().string();
        } catch (IOException e) {
            throw new GatewayServerException(e);
        }
    }

    /**
     * get 请求
     * @param url 地址
     * @param bodyJson 请求体 JSON
     * @param headerMap 请求头
     * @return 结果
     * @since 1.0.0
     */
    public static String post(final String url,
                              final String bodyJson,
                              final Map<String, String> headerMap) {
        RequestBody body = RequestBody.create(JSON, bodyJson);
        return post(url, body, headerMap);
    }

}

Результаты обработки вызова

После запроса к серверу нам нужно обработать результат.

Реализация первой версии была довольно грубой:

/**
 * 处理最后的结果
 * @param methodTypeResult 结果
 * @param servletResponse 响应
 * @since 1.0.0
 */
private void methodTypeResultHandle(final IMethodTypeResult methodTypeResult,
                                    final ServletResponse servletResponse) {
    try {
        final String respBody = methodTypeResult.respJson();
        // 重定向(因为网络安全等原因,这个方案应该被废弃。)
        // 这里可以重新定向,也可以通过 http client 进行请求。
        // GET/POST
        //获取字符输出流对象
        servletResponse.setCharacterEncoding("UTF-8");
        servletResponse.setContentType("text/html;charset=utf-8");
        servletResponse.getWriter().write(respBody);
    } catch (IOException e) {
        throw new GatewayServerException(e);
    }
}

полная реализация

Мы объединили вышеуказанные основные процессы, и полная реализация выглядит следующим образом:

import com.alibaba.fastjson.JSON;
import com.github.houbb.gateway.server.util.exception.GatewayServerException;
import com.github.houbb.gateway.server.web.biz.IRequestAppBiz;
import com.github.houbb.gateway.server.web.method.IMethodType;
import com.github.houbb.gateway.server.web.method.IMethodTypeContext;
import com.github.houbb.gateway.server.web.method.IMethodTypeResult;
import com.github.houbb.gateway.server.web.method.impl.MethodHandlerContainer;
import com.github.houbb.gateway.server.web.method.impl.MethodTypeContext;
import com.github.houbb.heaven.support.tuple.impl.Pair;
import com.github.houbb.heaven.util.lang.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

/**
 * 网关过滤器
 *
 * @author binbin.hou
 * @since 1.0.0
 */
@WebFilter
@Component
public class GatewayFilter implements Filter {

    private static final Logger LOGGER = LoggerFactory.getLogger(GatewayFilter.class);

    @Autowired
    private IRequestAppBiz requestAppBiz;

    @Autowired
    private MethodHandlerContainer methodHandlerContainer;

    public void init(FilterConfig filterConfig) throws ServletException {

    }

    public void doFilter(ServletRequest servletRequest,
                         ServletResponse servletResponse,
                         FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        LOGGER.info("url={}, params={}", req.getRequestURI(), JSON.toJSONString(req.getParameterMap()));

        //根据 URL 获取对应的服务名称
        Pair<String, String> pair = requestAppBiz.getRequestPair(req);
        Map<String, String> headerMap = buildHeaderMap(req);
        String appName = pair.getValueOne();
        if(StringUtil.isNotEmptyTrim(appName)) {
            String method = req.getMethod();

            String respBody = null;
            String url = requestAppBiz.buildRequestUrl(pair);

            //TODO: 其他方法的支持
            IMethodType methodType = methodHandlerContainer.getMethodType(method);

            IMethodTypeContext typeContext = MethodTypeContext.newInstance()
                    .methodType(method)
                    .url(url)
                    .servletRequest(servletRequest)
                    .servletResponse(servletResponse)
                    .headerMap(headerMap);

            // 执行前

            // 执行
            IMethodTypeResult methodTypeResult = methodType.handle(typeContext);

            // 执行后


            // 结果的处理
            this.methodTypeResultHandle(methodTypeResult, servletResponse);
        } else {
            filterChain.doFilter(req, servletResponse);
        }
    }

    public void destroy() {

    }


    /**
     * 处理最后的结果
     * @param methodTypeResult 结果
     * @param servletResponse 响应
     * @since 1.0.0
     */
    private void methodTypeResultHandle(final IMethodTypeResult methodTypeResult,
                                        final ServletResponse servletResponse) {
        try {
            final String respBody = methodTypeResult.respJson();

            // 重定向(因为网络安全等原因,这个方案应该被废弃。)
            // 这里可以重新定向,也可以通过 http client 进行请求。
            // GET/POST
            //获取字符输出流对象
            servletResponse.setCharacterEncoding("UTF-8");
            servletResponse.setContentType("text/html;charset=utf-8");
            servletResponse.getWriter().write(respBody);
        } catch (IOException e) {
            throw new GatewayServerException(e);
        }
    }

    /**
     * 构建 map 信息
     * @param req 请求
     * @return 结果
     * @since 1.0.0
     */
    private Map<String, String> buildHeaderMap(final HttpServletRequest req) {
        Map<String, String> map = new HashMap<>();

        Enumeration<String> enumeration = req.getHeaderNames();
        while (enumeration.hasMoreElements()) {
            String name = enumeration.nextElement();

            String value = req.getHeader(name);
            map.put(name, value);
        }
        return map;
    }

}

Аутентификация шлюза

Приложение шлюза

После того, как мы добавим перехватчик, определите соответствующее приложение следующим образом:

@SpringBootApplication
@ServletComponentScan
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

Затем запустите шлюз и начните номер порта как 8080.

серверное приложение

Затем запустите службу, соответствующую серверу, номер порта 8081.

Просмотрите реализацию контроллера для номера версии:

@RestController
public class MonitorController {

    @RequestMapping(value = "version", method = RequestMethod.GET)
    public String version() {
        return "1.0-demo";
    }

}

просить

Делаем API прямого доступа к шлюзу в браузере:

http://localhost:8080/test/version

Страница возвращает:

1.0-demo

резюме

Принцип реализации API-шлюза не сложен, он заключается в пересылке запросов на основе сервлетов.

Хотя это выглядит просто, на этой основе могут быть реализованы более мощные функции, такие как ограничение тока, ведение журнала, мониторинг и многое другое.

Если вы заинтересованы в шлюзе API, вы можете обратить внимание на волну, и последующий контент будет более захватывающим.

Примечания: используется много кодов, которые упрощены в тексте. Если вас интересует весь исходный код, вы можете подписаться на [Lao Ma Xiao Xifeng] и ответить [Gateway] в фоновом режиме, чтобы получить его.

Я старая лошадь, и я с нетерпением жду встречи с вами в следующий раз.在这里插入图片描述