從零搭建Spring Cloud Gateway閘道器(三)——報文結構轉換

李鋒鏑發表於2020-07-13

背景

作為閘道器,有些時候可能報文的結構並不符合前端或者某些服務的需求,或者因為某些原因,其他服務修改報文結構特別麻煩、或者需要修改的地方特別多,這個時候就需要走閘道器單獨轉換一次。

實現

話不多說,直接上程式碼。

首先,我們定義好配置:

package com.lifengdi.gateway.properties.entity;

import lombok.Data;
import org.springframework.util.CollectionUtils;

import java.util.*;

/**
 * 需要轉換報文結構的URL地址配置類
 *
 * @author: Li Fengdi
 * @date: 2020-7-11 16:57:07
 */
@Data
public class MessageTransformUrl {

    // 介面地址,多個地址使用英文逗號分隔
    private String[] paths;

    /**
     * <p>格式</p>
     * <p>新欄位:老欄位</p>
     * <p>若新老欄位一致,可以只配置新欄位</p>
     */
    private List<String> fields;

    /**
     * <p>返回體型別,預設為json </p>
     * <p>可配置的型別參見{@link com.lifengdi.gateway.enums.TransformContentTypeEnum}</p>
     * <p>如需自定義配置,可以繼承{@link com.lifengdi.gateway.transform.AbstractMessageTransform}類,
     * 或者實現{@link com.lifengdi.gateway.transform.IMessageTransform}介面類,重寫transform方法</p>
      */
    private String contentType;

    private Set<String> pathList;

    public Set<String> getPathList() {
        if (CollectionUtils.isEmpty(pathList) && Objects.nonNull(paths)) {
            setPathList(new HashSet<>(Arrays.asList(paths)));
        }
        return pathList;
    }
}
package com.lifengdi.gateway.properties;

import com.lifengdi.gateway.properties.entity.MessageTransformUrl;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * 報文結構轉換引數配置
 * @author: Li Fengdi
 * @date: 2020-7-11 16:55:53
 */
@Component
@ConfigurationProperties(prefix = "trans")
@Data
public class MessageTransformProperties {

    private List<MessageTransformUrl> urlList;

}

在yaml檔案中的配置如下:

# 報文轉換配置
trans:
  url-list:
    - paths: /jar/api/cockpit
      content-type: application/json
      fields:
        # 新欄位:老欄位,若新老欄位一致,可以只配置新欄位
        - code:rs
        - msg:rsdesp
        - data:resultMessage
    - paths: /war/api/delivertool
      fields:
        - code:rs
        - msg:rsdesp
        - data:resultMessage

這裡呢,大家也可以根據需要,放入資料庫或者其他可以動態修改的地方,這裡只是圖方便,所以直接放在yaml檔案中。

其次我們定義一個報文轉換介面類,方便後續的擴充套件。這個介面很簡單,只有一個transform()方法,主要功能就是轉換報文結構。

package com.lifengdi.gateway.transform;

import com.lifengdi.gateway.properties.entity.MessageTransformUrl;

/**
 * 報文結構轉換介面
 *
 * @author: Li Fengdi
 * @date: 2020-7-11 16:57:07
 */
public interface IMessageTransform {

    /**
     * 轉換報文結構
     *
     * @param originalContent 需要轉換的原始內容
     * @param transformUrl    MessageTransformUrl
     * @return 轉換後的結構
     */
    String transform(String originalContent, MessageTransformUrl transformUrl);
}

然後我們再增加一個抽象類,這個類主要提供一個解耦的作用,也是為了方便後續進行擴充套件。

package com.lifengdi.gateway.transform;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import javax.annotation.Resource;

/**
 * 報文轉換抽象類
 *
 * @author: Li Fengdi
 * @date: 2020-7-11 16:57:07
 */
public abstract class AbstractMessageTransform implements IMessageTransform {
    @Resource
    protected ObjectMapper objectMapper;

    /**
     * ResponseResult轉JSON
     *
     * @param object 需要轉換為json的物件
     * @return JSON字串
     */
    public String toJsonString(Object object) throws JsonProcessingException {
        return objectMapper.writeValueAsString(object);
    }

}

這個類非常簡單,只有一個toJsonString()方法,主要作用就是將物件轉換成json字串。

接著我們繼續來寫一個實現類,主要功能就是實現JSON型別的報文的結構轉換,如果需要其他型別的報文的同學,可以自定義開發。

package com.lifengdi.gateway.transform.impl;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.lifengdi.gateway.properties.entity.MessageTransformUrl;
import com.lifengdi.gateway.transform.AbstractMessageTransform;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * application/json型別轉換實現類
 * @author: Li Fengdi
 * @date: 2020-7-11 16:57:07
 */
@Service
@Slf4j
public class JsonMessageTransformImpl extends AbstractMessageTransform {

    @Override
    public String transform(String originalContent, MessageTransformUrl transformUrl) {

        if (StringUtils.isBlank(originalContent)) {
            return originalContent;
        }

        try {
            // 原始報文轉換為JsonNode
            JsonNode jsonNode = objectMapper.readTree(originalContent);

            List<String> fields = transformUrl.getFields();

            // 建立新的JSON物件
            ObjectNode rootNode = objectMapper.createObjectNode();
            fields.forEach(field -> {
                String[] fieldArray = field.split(":");
                String newFiled = fieldArray[0];
                String oldField = fieldArray.length > 1 ? fieldArray[1] : newFiled;
                if (jsonNode.has(oldField)) {
                    rootNode.set(newFiled, jsonNode.get(oldField));
                }
            });

            return toJsonString(rootNode);
        } catch (JsonProcessingException e) {
            log.error("application/json型別轉換異常,originalContent:{},transformUrl:{}", originalContent, transformUrl);
            return originalContent;
        }
    }
}

這個類繼承了AbstractMessageTransform這個類,重寫了transform()方法,使用objectMapperJsonNodeObjectNode來實現對JSON的解析、轉換等工作。

接下來我們定義一個列舉類,方便我們去匹配對應的報文轉換實現類。

package com.lifengdi.gateway.enums;

import lombok.Getter;
import org.apache.commons.lang.StringUtils;
import org.springframework.lang.Nullable;

/**
 * 報文結構轉換轉換型別列舉類
 *
 * @author: Li Fengdi
 * @date: 2020-7-11 16:57:07
 */
@Getter
public enum TransformContentTypeEnum {

    DEFAULT(null, "jsonMessageTransformImpl")
    , APPLICATION_JSON("application/json", "jsonMessageTransformImpl")
    ;
    /**
     * 內容型別
     */
    private String contentType;

    /**
     * 報文轉換結構實現類
     */
    private String transImpl;

    TransformContentTypeEnum(String contentType, String transImpl) {
        this.contentType = contentType;
        this.transImpl = transImpl;
    }

    /**
     * 根據contentType獲取對應列舉
     * <p>
     * 如果contentType為空則返回預設列舉
     * </p>
     *
     * @param contentType contentType
     * @return TransformContentTypeEnum
     */
    public static TransformContentTypeEnum getWithDefault(@Nullable String contentType) {
        if (StringUtils.isNotBlank(contentType)) {
            for (TransformContentTypeEnum transformContentTypeEnum : values()) {
                if (contentType.equals(transformContentTypeEnum.contentType)) {
                    return transformContentTypeEnum;
                }
            }
        }
        return DEFAULT;
    }
}

這個類也很簡單,定義列舉,然後一個靜態方法,靜態方法的作用是根據響應頭中的contentType來獲取對應的報文結構轉換實現類,如果獲取不到,則會返回一個預設的實現類,我這裡定義的預設的實現類就是我們上邊寫的JsonMessageTransformImpl類。

最後呢,我們定義一個工廠類,供我們的Filter呼叫。

package com.lifengdi.gateway.transform;

import com.lifengdi.gateway.enums.TransformContentTypeEnum;
import com.lifengdi.gateway.properties.MessageTransformProperties;
import com.lifengdi.gateway.properties.entity.MessageTransformUrl;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

/**
 * 報文結構轉換工廠類
 *
 * @author: Li Fengdi
 * @date: 2020-7-11 16:57:07
 */
@Component
public class MessageTransformFactory {

    @Resource
    private Map<String, AbstractMessageTransform> messageTransformMap;

    @Resource
    private MessageTransformProperties messageTransformProperties;

    /**
     * 根據contentType獲取對應的內容轉換實現類
     *
     * @param contentType 內容型別
     * @return 內容轉換實現類
     */
    private AbstractMessageTransform getMessageTransform(String contentType) {
        return messageTransformMap.get(TransformContentTypeEnum.getWithDefault(contentType).getTransImpl());
    }

    /**
     * 報文轉換
     *
     * @param originalContent 原始內容
     * @param transformUrl    url
     * @return 轉換後的訊息
     */
    private String messageTransform(String originalContent, MessageTransformUrl transformUrl) {
        String contentType = transformUrl.getContentType();
        AbstractMessageTransform messageTransform = getMessageTransform(contentType);

        return messageTransform.transform(originalContent, transformUrl);
    }

    /**
     * 判斷是否是需要轉換報文結構的介面,如果是則轉換,否則返回原值
     *
     * @param path            介面路徑
     * @param originalContent 原始內容
     * @return 轉換後的內容
     */
    public String compareAndTransform(String path, String originalContent) {
        if (StringUtils.isBlank(originalContent)) {
            return null;
        }
        List<MessageTransformUrl> urlList = messageTransformProperties.getUrlList();
        if (CollectionUtils.isEmpty(urlList)) {
            return originalContent;
        }
        return urlList .stream()
                .filter(transformUrl -> transformUrl.getPathList().contains(path))
                .findFirst()
                .map(url -> messageTransform(originalContent, url))
                .orElse(originalContent);
    }

    /**
     * 判斷是否是需要轉換報文結構的介面,如果是則轉換,否則返回原值
     *
     * @param path              介面路徑
     * @param originalContent   原始內容
     * @param originalByteArray 二進位制原始內容
     * @param charset           charset
     * @param newResponseBody   新報文內容
     * @return 響應體陣列陣列
     */
    public byte[] compareAndTransform(String path, String originalContent, byte[] originalByteArray, Charset charset,
                                      AtomicReference<String> newResponseBody) {
        if (StringUtils.isBlank(originalContent)) {
            return null;
        }
        List<MessageTransformUrl> urlList = messageTransformProperties.getUrlList();
        if (CollectionUtils.isEmpty(urlList)) {
            return originalByteArray;
        }
        return urlList.stream()
                .filter(transformUrl -> transformUrl.getPathList().contains(path))
                .findFirst()
                .map(url -> {
                    String messageTransform = messageTransform(originalContent, url);
                    if (originalContent.equals(messageTransform)) {
                        return originalByteArray;
                    }
                    newResponseBody.set(messageTransform);
                    return messageTransform.getBytes(charset);
                })
                .orElse(originalByteArray);
    }
}

這個工廠對外提供的方法只有compareAndTransform()兩個方法,主要功能就是判斷是否是需要轉換報文結構的介面,如果是則轉換,否則返回原值。

接下來就是在我們的Filter呼叫即可。呼叫程式碼如下:

content = messageTransformFactory.compareAndTransform(path, responseBody, content, charset, newResponseBody);

Git地址:https://github.com/lifengdi/spring-cloud-gateway-demo

最後

上面的只是簡單的示例,很多情況都沒有考慮進去,大家借鑑即可。

原文地址:https://www.lifengdi.com/archives/article/2006

相關文章