1. 程式人生 > >從零搭建Spring Cloud Gateway閘道器(三)——報文結構轉換

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

## 背景 作為閘道器,有些時候可能報文的結構並不符合前端或者某些服務的需求,或者因為某些原因,其他服務修改報文結構特別麻煩、或者需要修改的地方特別多,這個時候就需要走閘道器單獨轉換一次。 ## 實現 話不多說,直接上程式碼。 首先,我們定義好配置: ```java 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; /** *

格式

*

新欄位:老欄位

*

若新老欄位一致,可以只配置新欄位

*/ private List fields; /** *

返回體型別,預設為json

*

可配置的型別參見{@link com.lifengdi.gateway.enums.TransformContentTypeEnum}

*

如需自定義配置,可以繼承{@link com.lifengdi.gateway.transform.AbstractMessageTransform}類, * 或者實現{@link com.lifengdi.gateway.transform.IMessageTransform}介面類,重寫transform方法

*/ private String contentType; private Set pathList; public Set getPathList() { if (CollectionUtils.isEmpty(pathList) && Objects.nonNull(paths)) { setPathList(new HashSet<>(Arrays.asList(paths))); } return pathList; } } ``` ```java 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 urlList; } ``` 在yaml檔案中的配置如下: ```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()`方法,主要功能就是轉換報文結構。 ```java 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); } ``` 然後我們再增加一個抽象類,這個類主要提供一個解耦的作用,也是為了方便後續進行擴充套件。 ```java 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型別的報文的結構轉換,如果需要其他型別的報文的同學,可以自定義開發。 ```java 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 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()`方法,使用`objectMapper`、`JsonNode`、`ObjectNode`來實現對JSON的解析、轉換等工作。 接下來我們定義一個列舉類,方便我們去匹配對應的報文轉換實現類。 ```java 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獲取對應列舉 *

* 如果contentType為空則返回預設列舉 *

* * @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呼叫。 ```java 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 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 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 newResponseBody) { if (StringUtils.isBlank(originalContent)) { return null; } List 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呼叫即可。呼叫程式碼如下: ```java content = messageTransformFactory.compareAndTransform(path, responseBody, content, charset, newResponseBody); ``` Git地址:[https://github.com/lifengdi/spring-cloud-gateway-demo](https://github.com/lifengdi/spring-cloud-gateway-demo) ## 最後 上面的只是簡單的示例,很多情況都沒有考慮進去,大家借鑑即可。 原文地址:[https://www.lifengdi.com/archives/article/2006](https://www.lifengdi.com/archives/artic