当前位置: 首页 > news >正文

SpringCloud + Zookeeper + Feign整合及Feign原理

知其然

SpringCloud + Zookeeper

Spring Cloud 与 Zookeeper的整合只需要添加相关的starter依赖和增加相关注解即可完成。

pom.xml 如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>FeignDemo</artifactId><groupId>com.hui</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>feignHello-service</artifactId><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-zookeeper-discovery</artifactId><!-- 需要特别主题spring cloud 和zookeeper 的版本笔者本地使用了3.4.11的zk,因此在此处排除了默认的zk,单独引入--><exclusions><exclusion><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId><version>3.4.11</version><exclusions><exclusion><groupId>log4j</groupId><artifactId>log4j</artifactId></exclusion><exclusion><groupId>org.slf4j</groupId><artifactId>slf4j-log4j12</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><!--笔者将api与service分为了独立的module,所以这里加入引用api--><dependency><groupId>com.hui</groupId><artifactId>feignHello-api</artifactId><version>1.0-SNAPSHOT</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>

bootstrap.yml如下:

server:port: 8000
spring:application:name: feignHelloServicecloud:zookeeper:connect-string: 192.168.4.192:2181 #zk地址

最后开启服务的注册与发现

@SpringBootApplication
@EnableDiscoveryClient
public class HelloServiceApplication {public static void main(String[] args) {SpringApplication.run(HelloServiceApplication.class, args);}
}

service 和controller实现

@Service
public class HelloService{public HelloResponse sayHello(HelloRequest request) {return new HelloResponse(request.toString());}
}
@Api(value = "Hello 服务", tags = "Hello 服务")
@RestController
public class HelloController implements IHelloServiceClient {@AutowiredHelloService helloService;@Override@ApiOperation(value = "say hello 带默认参数")@GetMapping("/hello")//http://localhost:8000/hello?name=qqqq&age=22public String sayHello(@RequestParam(name = "name", defaultValue = "tony") String name,@RequestParam(name = "age", defaultValue = "18") int age) {HelloRequest request = new HelloRequest(name, age);return helloService.sayHello(request).toString();}@Override@GetMapping("/hello1")@PostMapping(value = "say hello 不带参数")public String sayHello() {HelloRequest request = new HelloRequest("tony", 19);return helloService.sayHello(request).toString();}@Override@PostMapping("/hello2")@ApiOperation(value = "say hello 带请求体")public HelloResponse sayHello(@RequestBody HelloRequest request) {return helloService.sayHello(request);}
}

笔者加入了swagger,如果需要只需加入如下依赖和配置:

    <dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId><version>2.0.7</version></dependency>
@Configuration
@EnableSwagger2WebMvc
public class SwaggerAutoConfiguration {@Beanpublic Docket defaultApi(){Docket docket = new Docket(DocumentationType.SWAGGER_2).apiInfo(new ApiInfoBuilder().title("helloService").description("RpcDemo").version("1.0").build()).select().apis(RequestHandlerSelectors.withClassAnnotation(Api.class)).paths(PathSelectors.any()).build();return docket;}}

至此,spring cloud与zookeeper的整合就完成了,调用结果如下:


helloService.jpg

SpringCloud + Zookeeper + Feign

为了测试与Feign的整合,再构建一个消费者:与上述构建的过程类似。

pom.xml 增加spring-cloud-starter-openfeign依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>FeignDemo</artifactId><groupId>com.hui</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>feignHello-test</artifactId><dependencies><dependency><groupId>com.hui</groupId><artifactId>feignHello-api</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-zookeeper-discovery</artifactId><exclusions><exclusion><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId><version>3.4.11</version><exclusions><exclusion><groupId>log4j</groupId><artifactId>log4j</artifactId></exclusion><exclusion><groupId>org.slf4j</groupId><artifactId>slf4j-log4j12</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId><version>2.2.4.RELEASE</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>

bootstrap.yaml:

server:port: 8001
spring:application:name: feignHelloTestcloud:zookeeper:connect-string: 192.168.4.192:2181main:allow-bean-definition-overriding: true #笔者定义了两个相同FeignClient,所以bean相同

开启服务注册与发现,@EnableFeignClients注解注册FeignClient

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class HelloServiceTestApplication {public static void main(String[] args) {SpringApplication.run(HelloServiceTestApplication.class, args);}
}

@FeignClient注册声明定义FeignClient,笔者以两种方式定义了两个FeignClient:

1.通过请求路径定义FeignClient

@FeignClient(value = "feignHelloService", path = "/hello")
public interface RemoteServiceClient {@RequestMapping("/")String testHello();
}

2.通过生产者(即上述构建的helloService)暴露出来的接口定义FeignClient

@Component
@FeignClient(name = "feignHelloService")
public interface RemoteServiceRpcClient extends IHelloServiceClient {
}

controller 测试:

@Api(value = "Hello 测试 服务", tags = "Hello 测试 服务")
@RestController
public class HelloTestController {@ResourceRemoteServiceClient remoteService;@AutowiredRemoteServiceRpcClient remoteServiceRpcClient;@ApiOperation(value = "远程调用方式测试1")@GetMapping("/remote")public String remoteHello(){return remoteService.testHello();}@ApiOperation(value = "本地调用方式测试")@GetMapping("/local")public String localHello(){return "hello" + UUID.randomUUID().toString();}@ApiOperation(value = "远程调用方式测试2,带请求参数")@PostMapping("/remoteRpc")public String remoteRpcHello(){return remoteServiceRpcClient.sayHello("tony",110);
//        return remoteServiceRpcClient.sayHello();}@ApiOperation(value = "远程调用方式测试3, 带请求体")@PostMapping("/remoteRpc1")public HelloResponse remoteRpcHello1(){return remoteServiceRpcClient.sayHello(new HelloRequest("YYY",122));}}

测试结果如下:


helloServiceTest.jpg

知其所以然

知道了如何将SpringCloud, Zookeeper 和Feign进行整合,我们知道了怎么使用,更重要的是要知道里面的原理,做到知其然更要知其所以然。

通过上述对整合过程的描述中可以发现,@EnableFeignClients和@FeignClient两个注解是将Feign整合进Spring Cloud的重要组成部分,因此,从这两个注解入手来了解Feign。

@EnableFeignClients:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {/*** basePackages属性的别名*/String[] value() default {};/*** 包扫描路径,扫描该路径下被@FeignClient标记的类*/String[] basePackages() default {};/*** 指明包扫描的类*/Class<?>[] basePackageClasses() default {};/*** 对所有feign client定制的配置类*/Class<?>[] defaultConfiguration() default {};/*** 所有被@FeignClient标记的client,如果不为空,就不会基于classpath进行扫描*/Class<?>[] clients() default {};
}

@EnableFeignClients -> FeignClientsRegistrar

@EnableFeignClients注解通过@Import引入了FeignClientsRegistrar进行feign客户端的注册, 同时FeignClientsRegistrar通过实现ImportBeanDefinitionRegistrar来将bean注册spring容器中:

public interface ImportBeanDefinitionRegistrar {/*** 根据使用者的配置类的注解元数据来注册bean的定义* @param importingClassMetadata 配置类的注解元数据* @param registry 当前bean定义的注册器,一般指spring容器*/default void registerBeanDefinitions(AnnotationMetadataimportingClassMetadata, BeanDefinitionRegistry registry) {}
}
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {@Overridepublic void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {//注册默认的配置到spring容器中registerDefaultConfiguration(metadata, registry);//注册发现的feign client到spring容器中registerFeignClients(metadata, registry);}
}

@EnableFeignClients -> FeignClientsRegistrar —> registerDefaultConfiguration

    private void registerDefaultConfiguration(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {//获取@EnableFeignClients注解的属性Map<String, Object> defaultAttrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName(), true);if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {String name;//拼接默认配置名if (metadata.hasEnclosingClass()) {name = "default." + metadata.getEnclosingClassName();}else {name = "default." + metadata.getClassName();}registerClientConfiguration(registry, name,defaultAttrs.get("defaultConfiguration"));}}private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,Object configuration) {//将feign client 配置构建成一个bean注册到spring容器中BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class);builder.addConstructorArgValue(name);builder.addConstructorArgValue(configuration);registry.registerBeanDefinition(name + "." + FeignClientSpecification.class.getSimpleName(),builder.getBeanDefinition());}

@EnableFeignClients -> FeignClientsRegistrar —> registerFeignClients

    public void registerFeignClients(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {//定义一个基于classpath的扫描器,用来获取被@FeignClient注解标注的feign clentClassPathScanningCandidateComponentProvider scanner = getScanner();scanner.setResourceLoader(this.resourceLoader);Set<String> basePackages;//获取@EnableFeignClients注解的属性Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());//@FeignClient注解过滤器AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class);//获取@EnableFeignClient注解的clients属性的值final Class<?>[] clients = attrs == null ? null: (Class<?>[]) attrs.get("clients");if (clients == null || clients.length == 0) {//@EnableFeignClients注解没有配置clients属性的情况//扫描器中加入注解过滤器scanner.addIncludeFilter(annotationTypeFilter);//获取@EnableFeignClients注解中的basePackages属性basePackages = getBasePackages(metadata);}else {//@EnableFeignClients注解配置了clients属性的情况final Set<String> clientClasses = new HashSet<>();basePackages = new HashSet<>();for (Class<?> clazz : clients) {//遍历client,获取器包路径和类名basePackages.add(ClassUtils.getPackageName(clazz));clientClasses.add(clazz.getCanonicalName());}//定义过滤器,只获取在clientClasses集合中的feign clientAbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {@Overrideprotected boolean match(ClassMetadata metadata) {String cleaned = metadata.getClassName().replaceAll("\\$", ".");return clientClasses.contains(cleaned);}};scanner.addIncludeFilter(new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));}//使用扫描器scanner扫描每一个basePackage,获取被@FeignClient标注的客户端for (String basePackage : basePackages) {Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);for (BeanDefinition candidateComponent : candidateComponents) {if (candidateComponent instanceof AnnotatedBeanDefinition) {// verify annotated class is an interfaceAnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();Assert.isTrue(annotationMetadata.isInterface(),"@FeignClient can only be specified on an interface");//获取@FeignClient注解的属性Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());String name = getClientName(attributes);//将针对特定feign client的配置注册到spring容器registerClientConfiguration(registry, name, attributes.get("configuration"));//注册feign client到spring容器       registerFeignClient(registry, annotationMetadata, attributes);}}}}private void registerFeignClient(BeanDefinitionRegistry registry,AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {String className = annotationMetadata.getClassName();//通过FeignClientFactoryBean工厂bean构建BeanDefinitionBuilderBeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);validate(attributes);//@FeignClient注解的属性作为bean的属性definition.addPropertyValue("url", getUrl(attributes));definition.addPropertyValue("path", getPath(attributes));String name = getName(attributes);definition.addPropertyValue("name", name);String contextId = getContextId(attributes);definition.addPropertyValue("contextId", contextId);definition.addPropertyValue("type", className);definition.addPropertyValue("decode404", attributes.get("decode404"));definition.addPropertyValue("fallback", attributes.get("fallback"));definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));//定义根据类型进行自动注入definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);String alias = contextId + "FeignClient";AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);// has a default, won't be nullboolean primary = (Boolean) attributes.get("primary");beanDefinition.setPrimary(primary);String qualifier = getQualifier(attributes);if (StringUtils.hasText(qualifier)) {alias = qualifier;}BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,new String[] { alias });//注册feign client到spring容器BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);}

至此,我们知道了通过@EnableFeignClients和@FeignClient两个注解以及其相关属性,在服务启动时,将每个feign client 以及其对应的配置和每个客户端通用的配置以bean的方式注册完到spring容器中。

FeignClient的自动注入

当使用@Autowired注解自动注入FeignClient时,Spring容器会使用注册FeignClient用到的FeignClientFactoryBean为其生成FeignClient实例。

class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {@Overridepublic Object getObject() throws Exception {return getTarget();}/*** @param <T> the target type of the Feign client* @return a {@link Feign} client created with the specified data and the context* information*/<T> T getTarget() {//从应用上下文中获取FeignClient的上下文FeignContext context = applicationContext.getBean(FeignContext.class);//通过FeignClient的上下文构建feignClient构造器Feign.Builder builder = feign(context);if (!StringUtils.hasText(url)) {//@FeignClient没有配置url的情况,根据name和path属性拼接成urlif (!name.startsWith("http")) {url = "http://" + name;}else {url = name;}url += cleanPath();//没有配置url属性,需要在多个服务节点之间进行负载均衡,生产Feign clientreturn (T) loadBalance(builder, context,new HardCodedTarget<>(type, name, url));}//配置了url属性的情况if (StringUtils.hasText(url) && !url.startsWith("http")) {url = "http://" + url;}String url = this.url + cleanPath();//从上下文获取FeignClien:LoadBalancerFeignClientClient client = getOptional(context, Client.class);if (client != null) {if (client instanceof LoadBalancerFeignClient) {// not load balancing because we have a url,// but ribbon is on the classpath, so unwrapclient = ((LoadBalancerFeignClient) client).getDelegate();}if (client instanceof FeignBlockingLoadBalancerClient) {// not load balancing because we have a url,// but Spring Cloud LoadBalancer is on the classpath, so unwrapclient = ((FeignBlockingLoadBalancerClient) client).getDelegate();}builder.client(client);}//从上下文中获取targeterTargeter targeter = get(context, Targeter.class);//通过targeter、builder生成Feign clientreturn (T) targeter.target(this, builder, context,new HardCodedTarget<>(type, name, url));}protected <T> T loadBalance(Feign.Builder builder, FeignContext context, HardCodedTarget<T> target) {//上下文获取Feign clientClient client = getOptional(context, Client.class);if (client != null) {//将builder与client关联builder.client(client);Targeter targeter = get(context, Targeter.class);//生产Feign clientreturn targeter.target(this, builder, context, target);}throw new IllegalStateException("No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?");}
}

默认使用的targeter是HystrixTargeter,根据builder的类型设置不同的属性,并生产Feign client

class HystrixTargeter implements Targeter {@Overridepublic <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,FeignContext context, Target.HardCodedTarget<T> target) {if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {return feign.target(target);}//....省略return feign.target(target);}
}
public abstract class Feign {public static class Builder {//构建客户端并创建feign client实例public <T> T target(Target<T> target) {return build().newInstance(target);}public Feign build() {Client client = Capability.enrich(this.client, capabilities);Retryer retryer = Capability.enrich(this.retryer, capabilities);List<RequestInterceptor> requestInterceptors = this.requestInterceptors.stream().map(ri -> Capability.enrich(ri, capabilities)).collect(Collectors.toList());Logger logger = Capability.enrich(this.logger, capabilities);Contract contract = Capability.enrich(this.contract, capabilities);Options options = Capability.enrich(this.options, capabilities);Encoder encoder = Capability.enrich(this.encoder, capabilities);Decoder decoder = Capability.enrich(this.decoder, capabilities);InvocationHandlerFactory invocationHandlerFactory =Capability.enrich(this.invocationHandlerFactory, capabilities);QueryMapEncoder queryMapEncoder = Capability.enrich(this.queryMapEncoder, capabilities);SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,logLevel, decode404, closeAfterDecode, propagationPolicy, forceDecoding);ParseHandlersByName handlersByName =new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,errorDecoder, synchronousMethodHandlerFactory);//根据相关配置,构建ReflectiveFeignreturn new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);}}
}
public class ReflectiveFeign extends Feign {private final ParseHandlersByName targetToHandlersByName;private final InvocationHandlerFactory factory;private final QueryMapEncoder queryMapEncoder;ReflectiveFeign(ParseHandlersByName targetToHandlersByName, InvocationHandlerFactory factory,QueryMapEncoder queryMapEncoder) {this.targetToHandlersByName = targetToHandlersByName;this.factory = factory;this.queryMapEncoder = queryMapEncoder;}@Overridepublic <T> T newInstance(Target<T> target) {Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();for (Method method : target.type().getMethods()) {if (method.getDeclaringClass() == Object.class) {continue;} else if (Util.isDefault(method)) {//对于缺省的方法使用DefaultMethodHandlerDefaultMethodHandler handler = new DefaultMethodHandler(method);defaultMethodHandlers.add(handler);methodToHandler.put(method, handler);} else {//对于每个对应服务端的方法,使用nameToHandler获取methodHandlermethodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));}}//通过InvocationHandlerFactory创建FeignInvocationHandler,该handler包含了上面创建的methodTohHandler//构成dispatch,用于对应@FeignClient标注的接口方法,当调用时进行转发处理InvocationHandler handler = factory.create(target, methodToHandler);//为feign客户端实例创建动态代理对象T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),new Class<?>[] {target.type()}, handler);//将缺省的methodHander绑定到动态代理对象上for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {defaultMethodHandler.bindTo(proxy);}return proxy;}
}

总结

从上面的分析可以得出,当服务启动时,通过@EnableFeignClients注解,启动对标注了@FeignClient注解的类进行扫描和注册,通过FeignClientFactoryBean将FeignClient注册到Spring容器中。当使用@Autowired注解进行自动注入时,注册到Spring容器中FeignClient会以动态代理的形式注入,这些动态代理中包含了接口方法的methodHandler用以处理调用转发。

最后编辑于:2025-06-15 09:49:01


喜欢的朋友记得点赞、收藏、关注哦!!!

相关文章:

  • 43-旋转图像
  • 网络安全之任意文件读取利用
  • nt!CcGetDirtyPages函数分析之Scan to the end of the Bcb list--重要
  • 设计模式笔记_创建型_工厂模式
  • C++ vector(2)
  • 【学习笔记】NLP 基础概念
  • 微软因安全漏洞禁用黑暗环境下的Windows Hello面部识别功能
  • langChain4j-流式输出
  • 前端压缩图片的免费软件
  • C# winform教程(二)----ComboBox
  • Pycharm(二十)神经网络入门
  • 【技术】记一次 Docker 中的 ES 数据迁移,使用 Reindex API
  • 运行ollama V0.9.1 异常 GLIBC_2.27 not found
  • 香橙派3B学习笔记12:C语言操作GPIO_<wiringPi.h>_点灯通用输入输出
  • 数字孪生:为UI前端设计带来前所未有的创新体验
  • 《解构线性数据结构的核心骨架:从存储模型到操作范式的深度解析》
  • 深度学习进阶:卷积神经网络(CNN)原理与实战
  • Vue 3 常用响应式数据类型详解:ref、reactive、toRef 和 toRefs
  • 卫星通信链路预算之二:带宽和功带平衡
  • YOLO进化史:从v1到v12的注意力革命 —— 实时检测的“快”与“准”如何兼得?
  • 网站建立与推广/优化方案模板
  • 网站文章排版工具/新闻头条今日要闻10条
  • 佛山企业门户网站建设/seo推广编辑
  • 网络营销方案的制定思路/惠州seo报价
  • 网站建站怎么报价/西部数码域名注册官网
  • 好的买手表网站/合肥seo服务商