feat: support TagUtils, ContextToHeaderInterceptor in TSF(2024). (#1754)

pull/1756/head
shedfreewu 2 weeks ago committed by GitHub
parent 1c01fccb62
commit 75f72ab023
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -5,4 +5,5 @@
- [refactor:optimize auto configuration.](https://github.com/Tencent/spring-cloud-tencent/pull/1739)
- [refactor:optimize config locate.](https://github.com/Tencent/spring-cloud-tencent/pull/1741)
- [feat:support async metadata transfer.](https://github.com/Tencent/spring-cloud-tencent/pull/1743)
- [fix:replace with string inside @ConditionalOnClass.](https://github.com/Tencent/spring-cloud-tencent/pull/1750)
- [fix:replace with string inside @ConditionalOnClass.](https://github.com/Tencent/spring-cloud-tencent/pull/1750)
- [feat: support TagUtils, ContextToHeaderInterceptor in TSF.](https://github.com/Tencent/spring-cloud-tencent/pull/1754)

@ -17,6 +17,8 @@
package com.tencent.cloud.metadata.config;
import java.util.List;
import com.tencent.cloud.common.async.PolarisAsyncProperties;
import com.tencent.cloud.common.constant.OrderConstant;
import com.tencent.cloud.metadata.core.DecodeTransferMetadataReactiveFilter;
@ -26,12 +28,14 @@ import com.tencent.cloud.metadata.core.EncodeTransferMedataRestTemplateEnhancedP
import com.tencent.cloud.metadata.core.EncodeTransferMedataScgEnhancedPlugin;
import com.tencent.cloud.metadata.core.EncodeTransferMedataWebClientEnhancedPlugin;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.tsf.core.filter.ContextToHeaderInterceptor;
import static jakarta.servlet.DispatcherType.ASYNC;
import static jakarta.servlet.DispatcherType.ERROR;
@ -92,8 +96,9 @@ public class MetadataTransferAutoConfiguration {
protected static class MetadataTransferScgFilterConfig {
@Bean
public EncodeTransferMedataScgEnhancedPlugin encodeTransferMedataScgEnhancedPlugin() {
return new EncodeTransferMedataScgEnhancedPlugin();
public EncodeTransferMedataScgEnhancedPlugin encodeTransferMedataScgEnhancedPlugin(
@Autowired(required = false) List<ContextToHeaderInterceptor> contextToHeaderInterceptorList) {
return new EncodeTransferMedataScgEnhancedPlugin(contextToHeaderInterceptorList);
}
}
@ -106,8 +111,9 @@ public class MetadataTransferAutoConfiguration {
protected static class MetadataTransferFeignInterceptorConfig {
@Bean
public EncodeTransferMedataFeignEnhancedPlugin encodeTransferMedataFeignEnhancedPlugin() {
return new EncodeTransferMedataFeignEnhancedPlugin();
public EncodeTransferMedataFeignEnhancedPlugin encodeTransferMedataFeignEnhancedPlugin(
@Autowired(required = false) List<ContextToHeaderInterceptor> contextToHeaderInterceptorList) {
return new EncodeTransferMedataFeignEnhancedPlugin(contextToHeaderInterceptorList);
}
}
@ -120,8 +126,9 @@ public class MetadataTransferAutoConfiguration {
protected static class MetadataTransferRestTemplateConfig {
@Bean
public EncodeTransferMedataRestTemplateEnhancedPlugin encodeTransferMedataRestTemplateEnhancedPlugin() {
return new EncodeTransferMedataRestTemplateEnhancedPlugin();
public EncodeTransferMedataRestTemplateEnhancedPlugin encodeTransferMedataRestTemplateEnhancedPlugin(
@Autowired(required = false) List<ContextToHeaderInterceptor> contextToHeaderInterceptorList) {
return new EncodeTransferMedataRestTemplateEnhancedPlugin(contextToHeaderInterceptorList);
}
}
@ -134,8 +141,9 @@ public class MetadataTransferAutoConfiguration {
protected static class MetadataTransferWebClientConfig {
@Bean
public EncodeTransferMedataWebClientEnhancedPlugin encodeTransferMedataWebClientEnhancedPlugin() {
return new EncodeTransferMedataWebClientEnhancedPlugin();
public EncodeTransferMedataWebClientEnhancedPlugin encodeTransferMedataWebClientEnhancedPlugin(
@Autowired(required = false) List<ContextToHeaderInterceptor> contextToHeaderInterceptorList) {
return new EncodeTransferMedataWebClientEnhancedPlugin(contextToHeaderInterceptorList);
}
}
}

@ -19,8 +19,11 @@ package com.tencent.cloud.metadata.core;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.metadata.MetadataContextHolder;
@ -38,6 +41,7 @@ import com.tencent.polaris.metadata.core.MetadataType;
import feign.Request;
import shade.polaris.com.google.common.collect.ImmutableMap;
import org.springframework.tsf.core.filter.ContextToHeaderInterceptor;
import org.springframework.util.CollectionUtils;
import static com.tencent.cloud.common.constant.MetadataConstant.HeaderName.APPLICATION_METADATA;
@ -50,6 +54,13 @@ import static com.tencent.cloud.common.constant.MetadataConstant.HeaderName.CUST
* @author Shedfree Wu
*/
public class EncodeTransferMedataFeignEnhancedPlugin implements EnhancedPlugin {
private List<ContextToHeaderInterceptor> contextToHeaderInterceptorList;
public EncodeTransferMedataFeignEnhancedPlugin(List<ContextToHeaderInterceptor> contextToHeaderInterceptorList) {
this.contextToHeaderInterceptorList = Optional.ofNullable(contextToHeaderInterceptorList).orElse(Collections.EMPTY_LIST);
}
@Override
public EnhancedPluginType getType() {
return EnhancedPluginType.Client.PRE;
@ -61,6 +72,7 @@ public class EncodeTransferMedataFeignEnhancedPlugin implements EnhancedPlugin {
return;
}
Request request = (Request) context.getOriginRequest();
contextToHeaderInterceptorList.forEach(ContextToHeaderInterceptor::beforeContextToHeader);
// get metadata of current thread
MetadataContext metadataContext = MetadataContextHolder.get();
@ -89,6 +101,8 @@ public class EncodeTransferMedataFeignEnhancedPlugin implements EnhancedPlugin {
// set headers that need to be transmitted from the upstream
this.buildTransmittedHeader(request, transHeaders);
contextToHeaderInterceptorList.forEach(ContextToHeaderInterceptor::afterContextToHeader);
}
private void buildTransmittedHeader(Request request, Map<String, String> transHeaders) {

@ -17,7 +17,10 @@
package com.tencent.cloud.metadata.core;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.metadata.MetadataContextHolder;
@ -34,6 +37,7 @@ import com.tencent.polaris.metadata.core.MetadataType;
import shade.polaris.com.google.common.collect.ImmutableMap;
import org.springframework.http.HttpRequest;
import org.springframework.tsf.core.filter.ContextToHeaderInterceptor;
import org.springframework.util.CollectionUtils;
import static com.tencent.cloud.common.constant.MetadataConstant.HeaderName.APPLICATION_METADATA;
@ -46,6 +50,13 @@ import static com.tencent.cloud.common.constant.MetadataConstant.HeaderName.CUST
* @author Shedfree Wu
*/
public class EncodeTransferMedataRestTemplateEnhancedPlugin implements EnhancedPlugin {
private List<ContextToHeaderInterceptor> contextToHeaderInterceptorList;
public EncodeTransferMedataRestTemplateEnhancedPlugin(List<ContextToHeaderInterceptor> contextToHeaderInterceptorList) {
this.contextToHeaderInterceptorList = Optional.ofNullable(contextToHeaderInterceptorList).orElse(Collections.EMPTY_LIST);
}
@Override
public EnhancedPluginType getType() {
return EnhancedPluginType.Client.BEFORE_CALLING;
@ -58,6 +69,8 @@ public class EncodeTransferMedataRestTemplateEnhancedPlugin implements EnhancedP
}
HttpRequest httpRequest = (HttpRequest) context.getOriginRequest();
contextToHeaderInterceptorList.forEach(ContextToHeaderInterceptor::beforeContextToHeader);
// get metadata of current thread
MetadataContext metadataContext = MetadataContextHolder.get();
Map<String, String> customMetadata = metadataContext.getCustomMetadata();
@ -86,6 +99,8 @@ public class EncodeTransferMedataRestTemplateEnhancedPlugin implements EnhancedP
// set headers that need to be transmitted from the upstream
this.buildTransmittedHeader(httpRequest, transHeaders);
contextToHeaderInterceptorList.forEach(ContextToHeaderInterceptor::afterContextToHeader);
}
private void buildTransmittedHeader(HttpRequest request, Map<String, String> transHeaders) {

@ -17,7 +17,10 @@
package com.tencent.cloud.metadata.core;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.tencent.cloud.common.constant.MetadataConstant;
import com.tencent.cloud.common.metadata.MetadataContext;
@ -35,6 +38,7 @@ import com.tencent.polaris.metadata.core.MetadataType;
import shade.polaris.com.google.common.collect.ImmutableMap;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.tsf.core.filter.ContextToHeaderInterceptor;
import org.springframework.util.CollectionUtils;
import org.springframework.web.server.ServerWebExchange;
@ -48,6 +52,13 @@ import static com.tencent.cloud.common.constant.MetadataConstant.HeaderName.CUST
* @author Shedfree Wu
*/
public class EncodeTransferMedataScgEnhancedPlugin implements EnhancedPlugin {
private List<ContextToHeaderInterceptor> contextToHeaderInterceptorList;
public EncodeTransferMedataScgEnhancedPlugin(List<ContextToHeaderInterceptor> contextToHeaderInterceptorList) {
this.contextToHeaderInterceptorList = Optional.ofNullable(contextToHeaderInterceptorList).orElse(Collections.EMPTY_LIST);
}
@Override
public EnhancedPluginType getType() {
return EnhancedPluginType.Client.PRE;
@ -69,6 +80,8 @@ public class EncodeTransferMedataScgEnhancedPlugin implements EnhancedPlugin {
metadataContext = MetadataContextHolder.get();
}
contextToHeaderInterceptorList.forEach(ContextToHeaderInterceptor::beforeContextToHeader);
Map<String, String> customMetadata = metadataContext.getCustomMetadata();
Map<String, String> disposableMetadata = metadataContext.getDisposableMetadata();
Map<String, String> applicationMetadata = metadataContext.getApplicationMetadata();
@ -86,6 +99,8 @@ public class EncodeTransferMedataScgEnhancedPlugin implements EnhancedPlugin {
this.buildMetadataHeader(builder, applicationMetadata, APPLICATION_METADATA);
TransHeadersTransfer.transfer(exchange.getRequest());
contextToHeaderInterceptorList.forEach(ContextToHeaderInterceptor::afterContextToHeader);
context.setOriginRequest(exchange.mutate().request(builder.build()).build());
}

@ -17,7 +17,10 @@
package com.tencent.cloud.metadata.core;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.metadata.MetadataContextHolder;
@ -33,6 +36,7 @@ import com.tencent.polaris.metadata.core.MessageMetadataContainer;
import com.tencent.polaris.metadata.core.MetadataType;
import shade.polaris.com.google.common.collect.ImmutableMap;
import org.springframework.tsf.core.filter.ContextToHeaderInterceptor;
import org.springframework.util.CollectionUtils;
import org.springframework.web.reactive.function.client.ClientRequest;
@ -46,6 +50,13 @@ import static com.tencent.cloud.common.constant.MetadataConstant.HeaderName.CUST
* @author Shedfree Wu
*/
public class EncodeTransferMedataWebClientEnhancedPlugin implements EnhancedPlugin {
private List<ContextToHeaderInterceptor> contextToHeaderInterceptorList;
public EncodeTransferMedataWebClientEnhancedPlugin(List<ContextToHeaderInterceptor> contextToHeaderInterceptorList) {
this.contextToHeaderInterceptorList = Optional.ofNullable(contextToHeaderInterceptorList).orElse(Collections.EMPTY_LIST);
}
@Override
public EnhancedPluginType getType() {
return EnhancedPluginType.Client.PRE;
@ -56,6 +67,8 @@ public class EncodeTransferMedataWebClientEnhancedPlugin implements EnhancedPlug
if (!(context.getOriginRequest() instanceof ClientRequest)) {
return;
}
contextToHeaderInterceptorList.forEach(ContextToHeaderInterceptor::beforeContextToHeader);
ClientRequest clientRequest = (ClientRequest) context.getOriginRequest();
MetadataContext metadataContext = MetadataContextHolder.get();
@ -77,6 +90,9 @@ public class EncodeTransferMedataWebClientEnhancedPlugin implements EnhancedPlug
this.buildMetadataHeader(requestBuilder, disposableMetadata, CUSTOM_DISPOSABLE_METADATA);
this.buildMetadataHeader(requestBuilder, applicationMetadata, APPLICATION_METADATA);
this.buildTransmittedHeader(requestBuilder, transHeaders);
contextToHeaderInterceptorList.forEach(ContextToHeaderInterceptor::beforeContextToHeader);
context.setOriginRequest(requestBuilder.build());
}

@ -19,6 +19,7 @@ package com.tencent.cloud.metadata.core;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import com.tencent.cloud.common.constant.MetadataConstant;
@ -114,7 +115,7 @@ public class EncodeTransferMedataScgFilterTest {
MockServerHttpRequest mockServerHttpRequest = MockServerHttpRequest.get("/test").build();
EncodeTransferMedataScgEnhancedPlugin plugin = new EncodeTransferMedataScgEnhancedPlugin();
EncodeTransferMedataScgEnhancedPlugin plugin = new EncodeTransferMedataScgEnhancedPlugin(new ArrayList<>());
plugin.getOrder();
EnhancedGatewayGlobalFilter filter = new EnhancedGatewayGlobalFilter(new DefaultEnhancedPluginRunner(Arrays.asList(plugin), registration, null));
filter.getOrder();

@ -0,0 +1,30 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package org.springframework.tsf.core.filter;
/**
* Compatible with old versions TSF SDK.
*
* @author Shedfree Wu
*/
public interface ContextToHeaderInterceptor {
void beforeContextToHeader();
void afterContextToHeader();
}

@ -0,0 +1,64 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package org.springframework.tsf.core.util;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.tencent.cloud.common.util.JacksonUtils;
import com.tencent.cloud.common.util.TsfTagUtils;
import org.springframework.tsf.core.entity.Metadata;
import org.springframework.tsf.core.entity.Tag;
/**
* Compatible with old versions TSF SDK.
*
* @author Shedfree Wu
*/
public final class TagUtils {
private TagUtils() {
}
public static <T> String serializeToJson(T object) {
return JacksonUtils.serialize2Json(object);
}
public static List<Tag> deserializeTagList(String buffer) {
return TsfTagUtils.deserializeTagList(buffer);
}
public static Metadata deserializeMetadata(String buffer) {
return TsfTagUtils.deserializeMetadata(buffer);
}
public static List<Tag> getTagListFromTagMap(Map<String, Tag> tagMap) {
return new ArrayList<>(tagMap.values());
}
public static Map<String, Tag> buildTagMapFromTagList(List<Tag> tagList) {
Map<String, Tag> result = new HashMap<>();
for (Tag tag : tagList) {
result.put(tag.getKey(), tag);
}
return result;
}
}

@ -0,0 +1,92 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package org.springframework.tsf.core.util;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.tencent.cloud.common.util.JacksonUtils;
import com.tencent.cloud.common.util.TsfTagUtils;
import com.tencent.cloud.common.util.UrlUtils;
import org.junit.jupiter.api.Test;
import org.springframework.tsf.core.entity.Metadata;
import org.springframework.tsf.core.entity.Tag;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Test for {@link TagUtils}.
*/
public class TagUtilsTest {
@Test
public void testSerializeDeserialize() {
Tag tag = new Tag("key", "value");
String json = TagUtils.serializeToJson(Collections.singletonList(tag));
List<Tag> tagList = TagUtils.deserializeTagList(json);
assertThat(tagList).isEqualTo(Collections.singletonList(tag));
}
@Test
public void testBuildTagMapFromTagList() {
Tag tag = new Tag("key", "value");
List<Tag> tagList = Collections.singletonList(tag);
Map<String, Tag> tagMap = TagUtils.buildTagMapFromTagList(tagList);
List<Tag> tagList2 = TagUtils.getTagListFromTagMap(tagMap);
assertThat(tagList2).isEqualTo(tagList);
}
@Test
public void testDeserializeMetadata() {
String encodedInput = "%7B%22ai%22%3A%22applicationId%22%2C%22av%22%3A%22applicationVersion%22%2C%22sn%22%3A%22serviceName%22%2C%22ii%22%3A%22instanceId%22%2C%22gi%22%3A%22groupId%22%2C%22li%22%3A%22127.0.0.1%22%2C%22lis%22%3A%22127.0.0.1%22%2C%22ni%22%3A%22namespaceId%22%2C%22pi%22%3Atrue%7D";
Metadata expectedMetadata = new Metadata();
expectedMetadata.setApplicationId("applicationId");
expectedMetadata.setApplicationVersion("applicationVersion");
expectedMetadata.setServiceName("serviceName");
expectedMetadata.setInstanceId("instanceId");
expectedMetadata.setGroupId("groupId");
expectedMetadata.setLocalIp("127.0.0.1");
expectedMetadata.setLocalIps("127.0.0.1");
expectedMetadata.setNamespaceId("namespaceId");
expectedMetadata.setPreferIpv6(true);
String json = JacksonUtils.serialize2Json(expectedMetadata);
String buffer = UrlUtils.encode(json);
System.out.println(buffer);
Metadata result = TagUtils.deserializeMetadata(encodedInput);
assertThat(result).isNotNull();
assertThat(result.toString()).isEqualTo(expectedMetadata.toString());
assertThat(result.getApplicationId()).isEqualTo(expectedMetadata.getApplicationId());
assertThat(result.getApplicationVersion()).isEqualTo(expectedMetadata.getApplicationVersion());
assertThat(result.getServiceName()).isEqualTo(expectedMetadata.getServiceName());
assertThat(result.getInstanceId()).isEqualTo(expectedMetadata.getInstanceId());
assertThat(result.getGroupId()).isEqualTo(expectedMetadata.getGroupId());
assertThat(result.getLocalIp()).isEqualTo(expectedMetadata.getLocalIp());
assertThat(result.getLocalIps()).isEqualTo(expectedMetadata.getLocalIps());
assertThat(result.getNamespaceId()).isEqualTo(expectedMetadata.getNamespaceId());
assertThat(result.isPreferIpv6()).isEqualTo(expectedMetadata.isPreferIpv6());
assertThat(TsfTagUtils.deserializeMetadata(null)).isNull();
assertThat(TsfTagUtils.deserializeMetadata("")).isNull();
}
}

@ -0,0 +1,40 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package com.tencent.cloud.tsf.demo.consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.tsf.core.filter.ContextToHeaderInterceptor;
@Component
public class TestContextToHeaderInterceptor implements ContextToHeaderInterceptor {
private static final Logger LOG = LoggerFactory.getLogger(TestContextToHeaderInterceptor.class);
@Override
public void beforeContextToHeader() {
LOG.info("beforeContextToHeader");
}
@Override
public void afterContextToHeader() {
LOG.info("afterContextToHeader");
}
}

@ -20,8 +20,10 @@ import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.tencent.cloud.tsf.demo.provider.config.ProviderNameConfig;
@ -33,6 +35,9 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.tsf.core.entity.Tag;
import org.springframework.tsf.core.util.TagUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@ -194,4 +199,14 @@ public class ProviderController {
LOG.info("provider-demo -- response info: [" + resultMap + "]");
return resultMap;
}
@GetMapping("/tagUtils")
public String tagUtils(@RequestParam("key") String key, @RequestParam("value") String value) {
Tag tag = new Tag(key, value);
String json = TagUtils.serializeToJson(Collections.singletonList(tag));
List<Tag> tagList = TagUtils.deserializeTagList(json);
return "serializeToJson: " + json + ", deserializeTagList: " + tagList;
}
}

Loading…
Cancel
Save