How to implement your own API gateway from scratch?

老马啸西风
中文

Preface

Previous article: You don’t even know the external interface signature? You still have to learn when you have time.

A lot of small partners responded that the related endorsements in the external API, and the verification work can be handled by the gateway.

Speaking of gateways, everyone is definitely familiar. The most widely used ones on the market are: spring cloud/kong/soul.

网关

The role of the API gateway

(1) Permission verification in the external interface

(2) Limits on the number of port calls and frequency limits

(3) Load balancing, caching, routing, access control, service proxy, monitoring, log, etc. in the microservice gateway.

Realization principle

在这里插入图片描述

For general requests, the server is directly accessed through the client. We need to implement a layer of api gateway in the middle. The external client accesses the gateway, and then the gateway forwards the call.

Core process

The gateway sounds very complicated, the core part is actually implemented based on javax.servlet.Filter Servlet.

We let the client call the gateway, and then parse and forward the message title in the Filter. After calling the server, we encapsulate and return it to the client.

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() {

    }

}

Next, we only need to focus on how to override the doFilter method.

Implementation

Get appName

The gateway faces all the internal applications of the company, and we can distinguish them by the unique appName of each service.

For example, if the application name is test, the gateway request is called:

https://gateway.com/test/version

For this request, the corresponding appName is test, and the actual requested url is /version.

The specific implementation is also relatively simple:

@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);
}

Request header

Construct the corresponding request header information according to 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;
}

Service discovery

When we parse the requested application, appName = test, we can query the corresponding ip:port information in the test application in the configuration center.

@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);
}

This is temporarily fixed and the request address of the actual server is returned.

Here you can also combine specific load balancing/routing strategies to make further server selections.

Different Method

There are many ways to support HTTP. We temporarily support GET/POST requests.

In essence, it is to call the server in a form of request for GET/POST requests.

The implementation here can be very diverse, here is the implementation of the ok-http client as an example.

Interface definition

In order to facilitate later expansion, all Method calls implement the same interface:

public interface IMethodType {

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

}

GET

GET request.

@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 request.

@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 implementation

OkHttpUtil is an http calling tool class based on ok-http encapsulation.

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);
    }

}

Call result processing

After requesting the server, we need to process the result.

The implementation of the first version is very crude:

/**
 * 处理最后的结果
 * @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);
    }
}

Complete realization

We put the above main process together, the complete implementation is as follows:

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;
    }

}

Gateway verification

Gateway application

After we add the interceptor, define the corresponding Application as follows:

@SpringBootApplication
@ServletComponentScan
public class Application {

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

}

Then start the gateway and start the port number as 8080

Server application

Then start the service corresponding to the server, the port number is 8081.

View the controller implementation of the version number:

@RestController
public class MonitorController {

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

}

request

We directly access the api gateway on the browser:

http://localhost:8080/test/version

Page back:

1.0-demo

summary

The principle of API gateway implementation is not difficult, that is, forwarding requests based on servlets.

Although it looks simple, more powerful features can be implemented on this basis, such as current limiting, logging, monitoring, and so on.

If you are interested in API gateways, you might as well pay attention to a wave, and the follow-up content will be more exciting.

Note: There are many codes involved, and the text is simplified. If you are interested in all the source code, you can follow [Lao Ma Xiao Xifeng] and reply [Gateway] in the background to get it.

I am an old horse, and I look forward to seeing you again next time.
在这里插入图片描述

阅读 1.1k

java 工具
整理 java 开发过程中有用的工具
129 声望
22 粉丝
0 条评论
129 声望
22 粉丝
文章目录
宣传栏