Moment For Technology

How to implement your own API gateway from zero?

Posted on Dec. 1, 2022, 4:55 p.m. by Anay Manda
Category: The back-end Tag: The back-end java api

The preface

You don't even know the signature of the external interface? Learn when you have time.

There are a lot of friends' reactions, the external API related to add, check these work can be unified gateway to deal with.

Speaking of gateways, we are certainly familiar with them. Spring Cloud/Kong /soul is widely used in the market.

Functions of the API gateway

(1) Permission verification on external interfaces

(2) Limit the number and frequency of interface calls

(3) Load balancing, caching, routing, access control, service proxy, monitoring, logging and so on in microservice gateway.

Realize the principle of

In general, requests are directly accessed through the client to the server. 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.

The core processes

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

We let the client call the gateway, and then resolve and forward the message questions in the Filter. After calling the server, the message questions are encapsulated and returned 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
 * @since1.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()));

        // Obtain the corresponding service name based on the URL

        // Do the specific processing logic

        // TODO...

        } else{ filterChain.doFilter(req, servletResponse); }}public void destroy(a) {}}Copy the code

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

The specific implementation

Get appName

Gateways are for all applications within the company and can be distinguished by the unique appName of each service.

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

https://gateway.com/test/version
Copy the code

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

The implementation is also relatively simple:

@Override
public PairString, 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 for requested address {} does not exist., url);
            return Pair.of(null.null);
        }
        String appName = subUrl.substring(0, nextSlash);
        String realUrl = subUrl.substring(nextSlash);
        LOGGER.info(AppName: {}; real request address: {}, url, appName, realUrl);
        return Pair.of(appName, realUrl);
    }
    LOGGER.warn("Request address: {} does not start with /, or is not long enough by 2, ignore it.", url);
    return Pair.of(null.null);
}
Copy the code

Request header

HttpServletRequest to build the corresponding request header information:

/** * Build map information *@paramThe req request *@returnResults *@since1.0.0 * /
private MapString, String buildHeaderMap(final HttpServletRequest req) {
    MapString, String map = new HashMap();
    EnumerationString enumeration = req.getHeaderNames();
    while (enumeration.hasMoreElements()) {
        String name = enumeration.nextElement();
        String value = req.getHeader(name);
        map.put(name, value);
    }
    return map;
}
Copy the code

Service discovery

When appName = test, we can query the IP :port information corresponding to the test application in the configuration center.

@Override
public String buildRequestUrl(PairString, String pair) {
    String appName = pair.getValueOne();
    String appUrl = pair.getValueTwo();
    String ipPort = "127.0.0.1:8081";
    //TODO:Query according to the database configuration
    // Access to different addresses depending on whether HTTPS is enabled
    if (appName.equals("test")) {
        // Load balancing needs to be involved
        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);
}
Copy the code

Here temporarily fixed write dead, finally return the actual server request address.

This can also be combined with specific load balancing/routing policies to make further server selection.

Different Method

HTTP is supported in a variety of ways, but for now we support GET/POST requests.

It essentially calls the server in the form of a build request for a GET/POST request.

This can be implemented in a variety of ways, with the OK-HTTP client as an example.

The interface definition

To facilitate later expansion, all Method calls implement the same interface:

public interface IMethodType {

    /** * processing *@paramContext *@returnResults the * /
    IMethodTypeResult handle(final IMethodTypeContext context);

}
Copy the code

GET

A GET request.

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

    @Override
    public IMethodTypeResult handle(IMethodTypeContext context) {
        String respBody = OkHttpUtil.get(context.url(), context.headerMap());
        returnMethodTypeResult.newInstance().respJson(respBody); }}Copy the code

POST

A 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());

        returnMethodTypeResult.newInstance().respJson(respBody); }}Copy the code

OkHttpUtil implementation

OkHttpUtil is an HTTP invocation utility 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
 * @since1.0.0 * /
public class OkHttpUtil {

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

    /** * get request *@paramUrl address *@returnResults *@since1.0.0 * /
    public static String get(final String url) {
        return get(url, null);
    }

    /** * get request *@paramUrl address *@paramHeaderMap Request header *@returnResults *@since1.0.0 * /
    public static String get(final String url,
                             final MapString, String headerMap) {
        try {
            OkHttpClient client = new OkHttpClient();
            Request.Builder builder = new Request.Builder();
            builder.url(url);

            if(MapUtil.isNotEmpty(headerMap)) {
                for(Map.EntryString, 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 newGatewayServerException(e); }}/** * get request *@paramUrl address *@paramBody Indicates the body of the request@paramHeaderMap Request header *@returnResults *@since1.0.0 * /
    public static String post(final String url,
                              final RequestBody body,
                             final MapString, 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.EntryString, 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 newGatewayServerException(e); }}/** * get request *@paramUrl address *@paramBodyJson request bodyJson *@paramHeaderMap Request header *@returnResults *@since1.0.0 * /
    public static String post(final String url,
                              final String bodyJson,
                              final MapString, String headerMap) {
        RequestBody body = RequestBody.create(JSON, bodyJson);
        returnpost(url, body, headerMap); }}Copy the code

Call result processing

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

The first version of the implementation was pretty rough:

/** * Process the final result *@paramMethodTypeResult results *@paramServletResponse response *@since1.0.0 * /
private void methodTypeResultHandle(final IMethodTypeResult methodTypeResult,
                                    final ServletResponse servletResponse) {
    try {
        final String respBody = methodTypeResult.respJson();
        // Redirection (this scheme should be abandoned for network security reasons, etc.)
        // The request can be redirected or made through the HTTP client.
        // GET/POST
        // Get the character output stream object
        servletResponse.setCharacterEncoding("UTF-8");
        servletResponse.setContentType("text/html; charset=utf-8");
        servletResponse.getWriter().write(respBody);
    } catch (IOException e) {
        throw newGatewayServerException(e); }}Copy the code

Complete implementation

We put the above main processes together, and 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;

/** * Gateway filter **@author binbin.hou
 * @since1.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()));

        // Obtain the corresponding service name based on the URL
        PairString, String pair = requestAppBiz.getRequestPair(req);
        MapString, 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:Support for other methods
            IMethodType methodType = methodHandlerContainer.getMethodType(method);

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

            / / before execution

            / / execution
            IMethodTypeResult methodTypeResult = methodType.handle(typeContext);

            / / after the execution


            // Result processing
            this.methodTypeResultHandle(methodTypeResult, servletResponse);
        } else{ filterChain.doFilter(req, servletResponse); }}public void destroy(a) {}/** * Process the final result *@paramMethodTypeResult results *@paramServletResponse response *@since1.0.0 * /
    private void methodTypeResultHandle(final IMethodTypeResult methodTypeResult,
                                        final ServletResponse servletResponse) {
        try {
            final String respBody = methodTypeResult.respJson();

            // Redirection (this scheme should be abandoned for network security reasons, etc.)
            // The request can be redirected or made through the HTTP client.
            // GET/POST
            // Get the character output stream object
            servletResponse.setCharacterEncoding("UTF-8");
            servletResponse.setContentType("text/html; charset=utf-8");
            servletResponse.getWriter().write(respBody);
        } catch (IOException e) {
            throw newGatewayServerException(e); }}/** * Build map information *@paramThe req request *@returnResults *@since1.0.0 * /
    private MapString, String buildHeaderMap(final HttpServletRequest req) {
        MapString, String map = new HashMap();

        EnumerationString enumeration = req.getHeaderNames();
        while (enumeration.hasMoreElements()) {
            String name = enumeration.nextElement();

            String value = req.getHeader(name);
            map.put(name, value);
        }
        returnmap; }}Copy the code

The gateway validation

The gateway application

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

@SpringBootApplication
@ServletComponentScan
public class Application {

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

Then start the gateway with port 8080

Server application

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

To view the version of the controller implementation:

@RestController
public class MonitorController {

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

request

We access the API gateway directly from the browser:

http://localhost:8080/test/version
Copy the code

Page returns:

1.0 the demoCopy the code

summary

The principle of the API gateway implementation is not difficult, which is to forward requests based on servlets.

While this may seem simple, you can add more powerful features such as traffic limiting, logging, monitoring, and so on.

If you are interested in the API gateway, you may wish to follow the wave, the following content, more exciting.

Note: There is a lot of code involved, which is simplified in this article. If you are interested in all source code, you can pay attention to [old horse xiao Xifeng], the background reply [gateway] can be obtained.

I am an old horse, looking forward to the next reunion with you.

Search
About
mo4tech.com (Moment For Technology) is a global community with thousands techies from across the global hang out!Passionate technologists, be it gadget freaks, tech enthusiasts, coders, technopreneurs, or CIOs, you would find them all here.