mirror of
https://gitee.com/mirrors/Spring-Cloud-Alibaba.git
synced 2021-06-26 13:25:11 +08:00
Temporary commit
This commit is contained in:
parent
53b677f3b7
commit
7aa17d3760
@ -37,14 +37,14 @@ import javax.ws.rs.Path;
|
||||
@Configuration
|
||||
public class DubboRestAutoConfiguration {
|
||||
|
||||
/**
|
||||
* A Feign Contract bean for JAX-RS if available
|
||||
*/
|
||||
@ConditionalOnClass(Path.class)
|
||||
@Bean
|
||||
public Contract jaxrs2Contract() {
|
||||
return new JAXRS2Contract();
|
||||
}
|
||||
// /**
|
||||
// * A Feign Contract bean for JAX-RS if available
|
||||
// */
|
||||
// @ConditionalOnClass(Path.class)
|
||||
// @Bean
|
||||
// public Contract jaxrs2Contract() {
|
||||
// return new JAXRS2Contract();
|
||||
// }
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ -52,14 +52,14 @@ public class DubboRestAutoConfiguration {
|
||||
return new ObjectMapper();
|
||||
}
|
||||
|
||||
/**
|
||||
* A Feign Contract bean for Spring MVC if available
|
||||
*/
|
||||
@ConditionalOnClass(RequestMapping.class)
|
||||
@Bean
|
||||
public Contract springMvcContract() {
|
||||
return new SpringMvcContract();
|
||||
}
|
||||
// /**
|
||||
// * A Feign Contract bean for Spring MVC if available
|
||||
// */
|
||||
// @ConditionalOnClass(RequestMapping.class)
|
||||
// @Bean
|
||||
// public Contract springMvcContract() {
|
||||
// return new SpringMvcContract();
|
||||
// }
|
||||
|
||||
@Bean
|
||||
public RestMetadataResolver metadataJsonResolver(ObjectMapper objectMapper) {
|
||||
|
@ -16,10 +16,23 @@
|
||||
*/
|
||||
package org.springframework.cloud.alibaba.dubbo.autoconfigure;
|
||||
|
||||
import com.alibaba.dubbo.config.ApplicationConfig;
|
||||
import com.alibaba.dubbo.config.RegistryConfig;
|
||||
import com.alibaba.dubbo.config.spring.ReferenceBean;
|
||||
import feign.Client;
|
||||
import feign.Request;
|
||||
import feign.Response;
|
||||
import org.codehaus.jackson.map.ObjectMapper;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.ListableBeanFactory;
|
||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.cloud.alibaba.dubbo.rest.feign.RestMetadataResolver;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.discovery.DiscoveryClient;
|
||||
import org.springframework.cloud.context.named.NamedContextFactory;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
@ -27,8 +40,18 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* The Auto-Configuration class for Dubbo REST Discovery
|
||||
@ -41,28 +64,148 @@ import java.util.Map;
|
||||
DubboRestMetadataRegistrationAutoConfiguration.class})
|
||||
public class DubboRestDiscoveryAutoConfiguration {
|
||||
|
||||
|
||||
@Autowired
|
||||
private DiscoveryClient discoveryClient;
|
||||
|
||||
// 1. Get all service names from Spring beans that was annotated by @FeignClient
|
||||
// 2. Get all service instances by echo specified service name
|
||||
// 3. Get Rest metadata from service instance
|
||||
// 4. Resolve REST metadata from the @FeignClient instance
|
||||
@Autowired
|
||||
private RestMetadataResolver restMetadataResolver;
|
||||
|
||||
@Autowired(required = false)
|
||||
private ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* Feign Request -> Dubbo ReferenceBean
|
||||
*/
|
||||
private Map<String, ReferenceBean> referenceBeanCache = new HashMap<>();
|
||||
|
||||
@Autowired
|
||||
private ApplicationConfig applicationConfig;
|
||||
|
||||
@Value("${spring.cloud.nacos.discovery.server-addr}")
|
||||
private String nacosServerAddress;
|
||||
|
||||
|
||||
private volatile boolean initialized = false;
|
||||
|
||||
@Autowired
|
||||
private ListableBeanFactory beanFactory;
|
||||
|
||||
@Scheduled(initialDelay = 10 * 1000, fixedRate = 5000)
|
||||
public void init() {
|
||||
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, NamedContextFactory.Specification> specifications =
|
||||
beanFactory.getBeansOfType(NamedContextFactory.Specification.class);
|
||||
ServiceAnnotationBeanPostProcessor
|
||||
// 1. Get all service names from Spring beans that was annotated by @FeignClient
|
||||
List<String> serviceNames = new LinkedList<>();
|
||||
|
||||
specifications.forEach((beanName, specification) -> {
|
||||
String serviceName = beanName.substring(0, beanName.indexOf("."));
|
||||
serviceNames.add(serviceName);
|
||||
|
||||
// 2. Get all service instances by echo specified service name
|
||||
List<ServiceInstance> serviceInstances = discoveryClient.getInstances(serviceName);
|
||||
if (!serviceInstances.isEmpty()) {
|
||||
ServiceInstance serviceInstance = serviceInstances.get(0);
|
||||
// 3. Get Rest metadata from service instance
|
||||
Map<String, String> metadata = serviceInstance.getMetadata();
|
||||
// 4. Resolve REST metadata from the @FeignClient instance
|
||||
String restMetadataJson = metadata.get("restMetadata");
|
||||
/**
|
||||
* {
|
||||
* "providers:org.springframework.cloud.alibaba.dubbo.service.EchoService:1.0.0": [
|
||||
* "{\"method\":\"POST\",\"url\":\"/plus?a={a}&b={b}\",\"headers\":{}}",
|
||||
* "{\"method\":\"GET\",\"url\":\"/echo?message={message}\",\"headers\":{}}"
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
try {
|
||||
Map<String, List<String>> restMetadata = objectMapper.readValue(restMetadataJson, Map.class);
|
||||
|
||||
restMetadata.forEach((dubboServiceName, restJsons) -> {
|
||||
restJsons.stream().map(restMetadataResolver::resolveRequest).forEach(request -> {
|
||||
referenceBeanCache.put(request.toString(), buildReferenceBean(dubboServiceName));
|
||||
});
|
||||
});
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
//
|
||||
}
|
||||
});
|
||||
|
||||
initialized = true;
|
||||
|
||||
}
|
||||
|
||||
private ReferenceBean buildReferenceBean(String dubboServiceName) {
|
||||
ReferenceBean referenceBean = new ReferenceBean();
|
||||
applicationConfig.setName("service-consumer");
|
||||
referenceBean.setApplication(applicationConfig);
|
||||
RegistryConfig registryConfig = new RegistryConfig();
|
||||
// requires dubbo-registry-nacos
|
||||
registryConfig.setAddress("nacos://" + nacosServerAddress);
|
||||
referenceBean.setRegistry(registryConfig);
|
||||
String[] parts = StringUtils.delimitedListToStringArray(dubboServiceName, ":");
|
||||
referenceBean.setInterface(parts[1]);
|
||||
referenceBean.setVersion(parts[2]);
|
||||
referenceBean.setGroup(parts.length > 3 ? parts[3] : null);
|
||||
referenceBean.get();
|
||||
return referenceBean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SmartInitializingSingleton onBeansInitialized(ListableBeanFactory beanFactory) {
|
||||
return () -> {
|
||||
Map<String, Object> feignClientBeans = beanFactory.getBeansWithAnnotation(FeignClient.class);
|
||||
feignClientBeans.forEach((beanName, bean) -> {
|
||||
if (bean instanceof NamedContextFactory.Specification) {
|
||||
NamedContextFactory.Specification specification = (NamedContextFactory.Specification) bean;
|
||||
String serviceName = specification.getName();
|
||||
public BeanPostProcessor wrapClientBeanPostProcessor() {
|
||||
return new BeanPostProcessor() {
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (bean instanceof Client) {
|
||||
Client client = (Client) bean;
|
||||
// wrapper
|
||||
return new DubboFeignClientProxy(client);
|
||||
}
|
||||
});
|
||||
return bean;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class DubboFeignClientProxy implements Client {
|
||||
|
||||
private final Client delegate;
|
||||
|
||||
DubboFeignClientProxy(Client delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response execute(Request request, Request.Options options) throws IOException {
|
||||
|
||||
ReferenceBean referenceBean = referenceBeanCache.get(request.toString());
|
||||
|
||||
if (referenceBean != null) {
|
||||
Object dubboClient = referenceBean.get();
|
||||
Method method = null;
|
||||
Object[] params = null;
|
||||
|
||||
try {
|
||||
Object result = method.invoke(dubboClient, params);
|
||||
// wrapper as a Response
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
} catch (InvocationTargetException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return delegate.execute(request, options);
|
||||
}
|
||||
}
|
||||
|
||||
@EventListener(ContextRefreshedEvent.class)
|
||||
public void onContextRefreshed(ContextRefreshedEvent event) {
|
||||
|
||||
|
@ -23,17 +23,23 @@ import com.alibaba.dubbo.config.spring.context.event.ServiceBeanExportedEvent;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import feign.Contract;
|
||||
import feign.jaxrs2.JAXRS2Contract;
|
||||
import org.springframework.beans.factory.BeanClassLoaderAware;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.cloud.alibaba.dubbo.rest.feign.RestMetadataResolver;
|
||||
import org.springframework.cloud.client.discovery.event.InstancePreRegisteredEvent;
|
||||
import org.springframework.cloud.client.serviceregistry.Registration;
|
||||
import org.springframework.cloud.openfeign.support.SpringMvcContract;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@ -51,7 +57,7 @@ import static org.springframework.cloud.alibaba.dubbo.registry.SpringCloudRegist
|
||||
@Configuration
|
||||
@AutoConfigureAfter(value = {
|
||||
DubboRestAutoConfiguration.class, DubboServiceRegistrationAutoConfiguration.class})
|
||||
public class DubboRestMetadataRegistrationAutoConfiguration {
|
||||
public class DubboRestMetadataRegistrationAutoConfiguration implements BeanClassLoaderAware {
|
||||
|
||||
/**
|
||||
* A Map to store REST metadata temporary, its' key is the special service name for a Dubbo service,
|
||||
@ -60,17 +66,38 @@ public class DubboRestMetadataRegistrationAutoConfiguration {
|
||||
private final Map<String, Set<String>> restMetadata = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* Autowire Feign Contract Beans
|
||||
* Feign Contracts
|
||||
*/
|
||||
@Autowired(required = false)
|
||||
private Collection<Contract> contracts = Collections.emptyList();
|
||||
|
||||
private ClassLoader classLoader;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
private RestMetadataResolver restMetadataResolver;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
contracts = initFeignContracts();
|
||||
}
|
||||
|
||||
private Collection<Contract> initFeignContracts() {
|
||||
Collection<Contract> contracts = new LinkedList<>();
|
||||
|
||||
if (ClassUtils.isPresent("javax.ws.rs.Path", classLoader)) {
|
||||
contracts.add(new JAXRS2Contract());
|
||||
}
|
||||
|
||||
if (ClassUtils.isPresent("org.springframework.web.bind.annotation.RequestMapping", classLoader)) {
|
||||
contracts.add(new SpringMvcContract());
|
||||
}
|
||||
|
||||
return contracts;
|
||||
}
|
||||
|
||||
|
||||
@EventListener(ServiceBeanExportedEvent.class)
|
||||
public void recordRestMetadata(ServiceBeanExportedEvent event) {
|
||||
ServiceBean serviceBean = event.getServiceBean();
|
||||
@ -105,4 +132,9 @@ public class DubboRestMetadataRegistrationAutoConfiguration {
|
||||
String restMetadataJson = objectMapper.writeValueAsString(restMetadata);
|
||||
serviceInstanceMetadata.put("restMetadata", restMetadataJson);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanClassLoader(ClassLoader classLoader) {
|
||||
this.classLoader = classLoader;
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.springframework.cloud.alibaba.dubbo.autoconfigure.DubboRestAutoConfiguration,\
|
||||
org.springframework.cloud.alibaba.dubbo.autoconfigure.DubboServiceRegistrationAutoConfiguration
|
||||
org.springframework.cloud.alibaba.dubbo.autoconfigure.DubboServiceRegistrationAutoConfiguration,\
|
||||
org.springframework.cloud.alibaba.dubbo.autoconfigure.DubboRestMetadataRegistrationAutoConfiguration,\
|
||||
org.springframework.cloud.alibaba.dubbo.autoconfigure.DubboRestDiscoveryAutoConfiguration
|
||||
|
@ -29,8 +29,16 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.alibaba.dubbo.service.EchoService;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -39,18 +47,37 @@ import java.util.List;
|
||||
*/
|
||||
@EnableDiscoveryClient
|
||||
@EnableAutoConfiguration
|
||||
@EnableFeignClients
|
||||
@EnableScheduling
|
||||
@RestController
|
||||
public class DubboSpringCloudBootstrap {
|
||||
|
||||
@Reference(version = "1.0.0")
|
||||
private EchoService echoService;
|
||||
|
||||
@Reference(version = "1.0.0")
|
||||
private EchoService echoServiceForRest;
|
||||
@Autowired
|
||||
@Lazy
|
||||
private FeignEchoService feignEchoService;
|
||||
|
||||
@GetMapping(value = "/call/echo")
|
||||
public String echo(@RequestParam("message") String message) {
|
||||
return feignEchoService.echo(message);
|
||||
}
|
||||
|
||||
@FeignClient("spring-cloud-alibaba-dubbo")
|
||||
public interface FeignEchoService {
|
||||
|
||||
@GetMapping(value = "/echo")
|
||||
String echo(@RequestParam("message") String message);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ApplicationRunner applicationRunner() {
|
||||
return arguments -> {
|
||||
// Dubbo Service call
|
||||
System.out.println(echoService.echo("mercyblitz"));
|
||||
// Spring Cloud Open Feign REST Call
|
||||
System.out.println(feignEchoService.echo("mercyblitz"));
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -43,13 +43,14 @@ import javax.ws.rs.QueryParam;
|
||||
public class DefaultEchoService implements EchoService {
|
||||
|
||||
@Override
|
||||
@GetMapping(value = "/echo",
|
||||
consumes = MediaType.APPLICATION_JSON_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||
@GetMapping(value = "/echo"
|
||||
// consumes = MediaType.APPLICATION_JSON_VALUE,
|
||||
// produces = MediaType.APPLICATION_JSON_UTF8_VALUE
|
||||
)
|
||||
@Path("/echo")
|
||||
@GET
|
||||
@Consumes("application/json")
|
||||
@Produces("application/json;charset=UTF-8")
|
||||
// @Consumes("application/json")
|
||||
// @Produces("application/json;charset=UTF-8")
|
||||
public String echo(@RequestParam @QueryParam("message") String message) {
|
||||
return RpcContext.getContext().getUrl() + " [echo] : " + message;
|
||||
}
|
||||
|
@ -10,4 +10,4 @@ dubbo:
|
||||
port: 9090
|
||||
server: netty
|
||||
registry:
|
||||
address: spring-cloud://dummy
|
||||
address: spring-cloud://nacos
|
@ -5,7 +5,6 @@ spring:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: 127.0.0.1:8848
|
||||
port: 12345
|
||||
eureka:
|
||||
client:
|
||||
enabled: false
|
||||
|
@ -16,15 +16,31 @@
|
||||
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Dubbo Nacos registry dependency -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>dubbo-registry-nacos</artifactId>
|
||||
<version>0.0.2</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-alibaba-dubbo</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
@ -40,10 +56,10 @@
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
|
||||
</dependency>
|
||||
<!--<dependency>-->
|
||||
<!--<groupId>org.springframework.cloud</groupId>-->
|
||||
<!--<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>-->
|
||||
<!--</dependency>-->
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
@ -0,0 +1,42 @@
|
||||
package org.springframework.cloud.alibaba.cloud.examples.demos;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@EnableAutoConfiguration // 激活自动装配
|
||||
@EnableDiscoveryClient // 激活服务注册和发现
|
||||
@EnableFeignClients // 激活 @FeignClients注册
|
||||
public class SpringCloudRestClientBootstrap {
|
||||
|
||||
@FeignClient("spring-cloud-alibaba-dubbo")
|
||||
public interface FeignEchoService {
|
||||
|
||||
@GetMapping(value = "/echo")
|
||||
String echo(@RequestParam("message") String message);
|
||||
}
|
||||
|
||||
|
||||
@RestController
|
||||
public static class EchoServiceController {
|
||||
|
||||
@Autowired
|
||||
private FeignEchoService feignEchoService;
|
||||
|
||||
@GetMapping("/call/echo")
|
||||
public String echo(@RequestParam("message") String message) {
|
||||
return feignEchoService.echo(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SpringCloudRestClientBootstrap.class, args);
|
||||
}
|
||||
}
|
@ -3,10 +3,5 @@ server.port=18083
|
||||
management.endpoints.web.exposure.include=*
|
||||
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
|
||||
|
||||
feign.sentinel.enabled=true
|
||||
dubbo.registry.address= spring-cloud://nacos
|
||||
|
||||
spring.cloud.sentinel.transport.dashboard=localhost:8080
|
||||
spring.cloud.sentinel.eager=true
|
||||
|
||||
spring.cloud.sentinel.datasource.ds1.file.file=classpath: flowrule.json
|
||||
spring.cloud.sentinel.datasource.ds1.file.data-type=json
|
Loading…
x
Reference in New Issue
Block a user