├── .gitignore ├── mini-spring-cloud-examples ├── tutu-server │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ └── application.yml │ │ │ └── java │ │ │ └── com │ │ │ └── github │ │ │ └── cloud │ │ │ └── examples │ │ │ └── TutuServerApplication.java │ └── pom.xml ├── mini-spring-cloud-consumer-examples │ ├── src │ │ ├── main │ │ │ ├── resources │ │ │ │ └── application.yml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── github │ │ │ │ └── cloud │ │ │ │ └── examples │ │ │ │ ├── EchoService.java │ │ │ │ └── ConsumerApplication.java │ │ └── test │ │ │ └── java │ │ │ └── com │ │ │ └── github │ │ │ └── cloud │ │ │ └── examples │ │ │ └── FeignTest.java │ └── pom.xml ├── mini-spring-cloud-provider-example │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ └── application.yml │ │ │ └── java │ │ │ └── com │ │ │ └── github │ │ │ └── cloud │ │ │ └── examples │ │ │ └── ProviderApplication.java │ └── pom.xml └── mini-spring-cloud-api-gateway-example │ ├── src │ └── main │ │ ├── resources │ │ └── application.yml │ │ └── java │ │ └── com │ │ └── github │ │ └── cloud │ │ └── examples │ │ └── ApiGatewayApplication.java │ └── pom.xml ├── assets ├── feign工作流程.png ├── spring-cloud.png ├── zuul-filter.png ├── zuul-servlet.png ├── zuul-framework.png ├── load-balancer-IPing.png ├── load-balancer-IRule.png ├── service-registry-api.png ├── service-registry-maven.png ├── load-balancer-ServerList.png ├── load-balancer-IClientConfig.png ├── load-balancer-ILoadBalancer.png ├── load-balancer-ServerListFilter.png ├── load-balancer-ServerListUpdater.png ├── load-balancer-LoadBalancerInterceptor.png ├── load-balancer-ServiceRequestWrapper.png └── load-balancer-LoadBalancerAutoConfiguration.png ├── mini-spring-cloud-openfeign ├── src │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── spring.factories │ │ └── java │ │ └── com │ │ └── github │ │ └── cloud │ │ └── openfeign │ │ ├── FeignClient.java │ │ ├── EnableFeignClients.java │ │ ├── FeignAutoConfiguration.java │ │ ├── FeignContext.java │ │ ├── FeignClientsConfiguration.java │ │ ├── support │ │ └── SpringMvcContract.java │ │ ├── FeignClientSpecification.java │ │ ├── ribbon │ │ └── LoadBalancerFeignClient.java │ │ ├── FeignClientsRegistrar.java │ │ └── FeignClientFactoryBean.java └── pom.xml ├── mini-spring-cloud-tutu-discovery ├── src │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── spring.factories │ │ └── java │ │ └── com │ │ └── github │ │ └── cloud │ │ └── tutu │ │ ├── discovery │ │ ├── TutuDiscoveryAutoConfiguration.java │ │ └── TutuDiscoveryClient.java │ │ ├── registry │ │ ├── TutuAutoServiceRegistration.java │ │ ├── TutuRegistration.java │ │ ├── TutuServiceRegistryAutoConfiguration.java │ │ └── TutuServiceRegistry.java │ │ ├── TutuDiscoveryProperties.java │ │ └── TutuServiceInstance.java └── pom.xml ├── mini-spring-cloud-load-balancer ├── src │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── spring.factories │ │ └── java │ │ └── com │ │ └── github │ │ └── cloud │ │ └── loadbalancer │ │ └── ribbon │ │ ├── TutuServer.java │ │ ├── config │ │ ├── RibbonTutuAutoConfiguration.java │ │ ├── TutuRibbonClientConfiguration.java │ │ ├── RibbonAutoConfiguration.java │ │ └── RibbonClientConfiguration.java │ │ ├── RibbonClients.java │ │ ├── SpringClientFactory.java │ │ ├── RibbonClientConfigurationRegistrar.java │ │ ├── RibbonClientSpecification.java │ │ ├── TutuServerList.java │ │ └── RibbonLoadBalancerClient.java └── pom.xml ├── mini-spring-cloud-netflix-zuul ├── src │ └── main │ │ └── java │ │ └── com │ │ └── github │ │ └── cloud │ │ └── netflix │ │ └── zuul │ │ ├── filters │ │ ├── RouteLocator.java │ │ ├── support │ │ │ └── FilterConstants.java │ │ ├── Route.java │ │ ├── SimpleRouteLocator.java │ │ ├── ZuulProperties.java │ │ ├── pre │ │ │ └── PreDecorationFilter.java │ │ ├── post │ │ │ └── SendResponseFilter.java │ │ └── route │ │ │ └── RibbonRoutingFilter.java │ │ ├── metrics │ │ ├── EmptyCounterFactory.java │ │ └── EmptyTracerFactory.java │ │ ├── EnableZuulProxy.java │ │ └── ZuulServerAutoConfiguration.java └── pom.xml ├── README.md ├── README_en.md ├── pom.xml ├── changelog_en.md └── changelog.md /.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | target/ 3 | .idea/ 4 | .idea_modules/ 5 | *.iml 6 | *.ipr 7 | *.iws -------------------------------------------------------------------------------- /mini-spring-cloud-examples/tutu-server/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 6688 -------------------------------------------------------------------------------- /assets/feign工作流程.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerekYRC/mini-spring-cloud/HEAD/assets/feign工作流程.png -------------------------------------------------------------------------------- /assets/spring-cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerekYRC/mini-spring-cloud/HEAD/assets/spring-cloud.png -------------------------------------------------------------------------------- /assets/zuul-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerekYRC/mini-spring-cloud/HEAD/assets/zuul-filter.png -------------------------------------------------------------------------------- /assets/zuul-servlet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerekYRC/mini-spring-cloud/HEAD/assets/zuul-servlet.png -------------------------------------------------------------------------------- /assets/zuul-framework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerekYRC/mini-spring-cloud/HEAD/assets/zuul-framework.png -------------------------------------------------------------------------------- /assets/load-balancer-IPing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerekYRC/mini-spring-cloud/HEAD/assets/load-balancer-IPing.png -------------------------------------------------------------------------------- /assets/load-balancer-IRule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerekYRC/mini-spring-cloud/HEAD/assets/load-balancer-IRule.png -------------------------------------------------------------------------------- /assets/service-registry-api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerekYRC/mini-spring-cloud/HEAD/assets/service-registry-api.png -------------------------------------------------------------------------------- /assets/service-registry-maven.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerekYRC/mini-spring-cloud/HEAD/assets/service-registry-maven.png -------------------------------------------------------------------------------- /assets/load-balancer-ServerList.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerekYRC/mini-spring-cloud/HEAD/assets/load-balancer-ServerList.png -------------------------------------------------------------------------------- /assets/load-balancer-IClientConfig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerekYRC/mini-spring-cloud/HEAD/assets/load-balancer-IClientConfig.png -------------------------------------------------------------------------------- /assets/load-balancer-ILoadBalancer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerekYRC/mini-spring-cloud/HEAD/assets/load-balancer-ILoadBalancer.png -------------------------------------------------------------------------------- /assets/load-balancer-ServerListFilter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerekYRC/mini-spring-cloud/HEAD/assets/load-balancer-ServerListFilter.png -------------------------------------------------------------------------------- /assets/load-balancer-ServerListUpdater.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerekYRC/mini-spring-cloud/HEAD/assets/load-balancer-ServerListUpdater.png -------------------------------------------------------------------------------- /assets/load-balancer-LoadBalancerInterceptor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerekYRC/mini-spring-cloud/HEAD/assets/load-balancer-LoadBalancerInterceptor.png -------------------------------------------------------------------------------- /assets/load-balancer-ServiceRequestWrapper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerekYRC/mini-spring-cloud/HEAD/assets/load-balancer-ServiceRequestWrapper.png -------------------------------------------------------------------------------- /assets/load-balancer-LoadBalancerAutoConfiguration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerekYRC/mini-spring-cloud/HEAD/assets/load-balancer-LoadBalancerAutoConfiguration.png -------------------------------------------------------------------------------- /mini-spring-cloud-openfeign/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | com.github.cloud.openfeign.FeignAutoConfiguration -------------------------------------------------------------------------------- /mini-spring-cloud-tutu-discovery/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | com.github.cloud.tutu.registry.TutuServiceRegistryAutoConfiguration,\ 3 | com.github.cloud.tutu.discovery.TutuDiscoveryAutoConfiguration -------------------------------------------------------------------------------- /mini-spring-cloud-examples/mini-spring-cloud-consumer-examples/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: consumer-application 4 | cloud: 5 | tutu: 6 | discovery: 7 | server-addr: localhost:6688 8 | service: ${spring.application.name} -------------------------------------------------------------------------------- /mini-spring-cloud-load-balancer/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | com.github.cloud.loadbalancer.ribbon.config.RibbonAutoConfiguration,\ 3 | com.github.cloud.loadbalancer.ribbon.config.RibbonTutuAutoConfiguration -------------------------------------------------------------------------------- /mini-spring-cloud-examples/mini-spring-cloud-provider-example/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: provider-application 4 | cloud: 5 | tutu: 6 | discovery: 7 | server-addr: localhost:6688 8 | service: ${spring.application.name} 9 | 10 | # 随机端口 11 | server: 12 | port: ${random.int[10000,20000]} -------------------------------------------------------------------------------- /mini-spring-cloud-netflix-zuul/src/main/java/com/github/cloud/netflix/zuul/filters/RouteLocator.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.netflix.zuul.filters; 2 | 3 | /** 4 | * 路由定位器 5 | * 6 | * @author derek(易仁川) 7 | * @date 2022/6/28 8 | */ 9 | public interface RouteLocator { 10 | 11 | /** 12 | * 获取匹配的路由 13 | * 14 | * @param path 15 | * @return 16 | */ 17 | Route getMatchingRoute(String path); 18 | } 19 | -------------------------------------------------------------------------------- /mini-spring-cloud-netflix-zuul/src/main/java/com/github/cloud/netflix/zuul/metrics/EmptyCounterFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.netflix.zuul.metrics; 2 | 3 | import com.netflix.zuul.monitoring.CounterFactory; 4 | 5 | /** 6 | * @author derek(易仁川) 7 | * @date 2022/6/27 8 | */ 9 | public class EmptyCounterFactory extends CounterFactory { 10 | @Override 11 | public void increment(String name) { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/TutuServer.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.loadbalancer.ribbon; 2 | 3 | import com.netflix.loadbalancer.Server; 4 | 5 | /** 6 | * 图图服务实例 7 | * 8 | * @author derek(易仁川) 9 | * @date 2022/3/13 10 | */ 11 | public class TutuServer extends Server { 12 | 13 | public TutuServer(String host, int port) { 14 | super(host, port); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /mini-spring-cloud-examples/mini-spring-cloud-consumer-examples/src/main/java/com/github/cloud/examples/EchoService.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.examples; 2 | 3 | import com.github.cloud.openfeign.FeignClient; 4 | import org.springframework.web.bind.annotation.PostMapping; 5 | 6 | /** 7 | * @author derek(易仁川) 8 | * @date 2022/4/9 9 | */ 10 | @FeignClient("provider-application") 11 | public interface EchoService { 12 | 13 | @PostMapping("echo") 14 | String echo(); 15 | } 16 | -------------------------------------------------------------------------------- /mini-spring-cloud-openfeign/src/main/java/com/github/cloud/openfeign/FeignClient.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.openfeign; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * Feign客户端注解 7 | * 8 | * @author derek(易仁川) 9 | * @date 2022/4/7 10 | */ 11 | @Target(ElementType.TYPE) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Documented 14 | public @interface FeignClient { 15 | 16 | /** 17 | * 服务提供者应用名称 18 | * 19 | * @return 20 | */ 21 | String value(); 22 | } 23 | -------------------------------------------------------------------------------- /mini-spring-cloud-examples/mini-spring-cloud-api-gateway-example/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: api-gateway-application 4 | cloud: 5 | tutu: 6 | discovery: 7 | server-addr: localhost:6688 8 | service: ${spring.application.name} 9 | 10 | server: 11 | port: 8888 12 | 13 | zuul: 14 | servlet-path: /* 15 | routes: 16 | route_provider_application: 17 | path: /provider-application/** 18 | service-id: provider-application -------------------------------------------------------------------------------- /mini-spring-cloud-openfeign/src/main/java/com/github/cloud/openfeign/EnableFeignClients.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.openfeign; 2 | 3 | import org.springframework.context.annotation.Import; 4 | 5 | import java.lang.annotation.*; 6 | 7 | /** 8 | * 启用Feign 9 | * 10 | * @author derek(易仁川) 11 | * @date 2022/4/7 12 | */ 13 | @Retention(RetentionPolicy.RUNTIME) 14 | @Target(ElementType.TYPE) 15 | @Documented 16 | @Import(FeignClientsRegistrar.class) 17 | public @interface EnableFeignClients { 18 | } 19 | -------------------------------------------------------------------------------- /mini-spring-cloud-openfeign/src/main/java/com/github/cloud/openfeign/FeignAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.openfeign; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | /** 7 | * @author derek(易仁川) 8 | * @date 2022/4/9 9 | */ 10 | @Configuration 11 | public class FeignAutoConfiguration { 12 | 13 | @Bean 14 | public FeignContext feignContext() { 15 | return new FeignContext(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/config/RibbonTutuAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.loadbalancer.ribbon.config; 2 | 3 | import com.github.cloud.loadbalancer.ribbon.RibbonClients; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | /** 7 | * @author derek(易仁川) 8 | * @date 2022/3/22 9 | */ 10 | @Configuration 11 | @RibbonClients(defaultConfiguration = TutuRibbonClientConfiguration.class) 12 | public class RibbonTutuAutoConfiguration { 13 | } 14 | -------------------------------------------------------------------------------- /mini-spring-cloud-openfeign/src/main/java/com/github/cloud/openfeign/FeignContext.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.openfeign; 2 | 3 | import org.springframework.cloud.context.named.NamedContextFactory; 4 | 5 | /** 6 | * 为每个feign客户端创建一个应用上下文(ApplicationContext),隔离每个feign客户端的配置 7 | * 8 | * @author derek(易仁川) 9 | * @date 2022/4/9 10 | */ 11 | public class FeignContext extends NamedContextFactory { 12 | 13 | public FeignContext() { 14 | super(FeignClientsConfiguration.class, "feign", "feign.client.name"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /mini-spring-cloud-netflix-zuul/src/main/java/com/github/cloud/netflix/zuul/filters/support/FilterConstants.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.netflix.zuul.filters.support; 2 | 3 | /** 4 | * 过滤器常量类 5 | * 6 | * @author derek(易仁川) 7 | * @date 2022/6/27 8 | */ 9 | public interface FilterConstants { 10 | 11 | String REQUEST_URI_KEY = "requestURI"; 12 | 13 | String SERVICE_ID_KEY = "serviceId"; 14 | 15 | //过滤器类型常量----------------------------------- 16 | String PRE_TYPE = "pre"; 17 | 18 | String ROUTE_TYPE = "route"; 19 | 20 | String POST_TYPE = "post"; 21 | 22 | String ERROR_TYPE = "error"; 23 | } 24 | -------------------------------------------------------------------------------- /mini-spring-cloud-netflix-zuul/src/main/java/com/github/cloud/netflix/zuul/EnableZuulProxy.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.netflix.zuul; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | import org.springframework.context.annotation.Import; 9 | 10 | /** 11 | * 启用zuul网关 12 | * 13 | * @author derek(易仁川) 14 | * @date 2022/6/23 15 | */ 16 | @Target(ElementType.TYPE) 17 | @Retention(RetentionPolicy.RUNTIME) 18 | @Import(ZuulServerAutoConfiguration.class) 19 | public @interface EnableZuulProxy { 20 | } 21 | -------------------------------------------------------------------------------- /mini-spring-cloud-examples/mini-spring-cloud-api-gateway-example/src/main/java/com/github/cloud/examples/ApiGatewayApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.examples; 2 | 3 | import com.github.cloud.netflix.zuul.EnableZuulProxy; 4 | 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | 8 | /** 9 | * @author derek(易仁川) 10 | * @date 2022/6/26 11 | */ 12 | @EnableZuulProxy 13 | @SpringBootApplication 14 | public class ApiGatewayApplication { 15 | 16 | public static void main(String[] args) { 17 | SpringApplication.run(ApiGatewayApplication.class, args); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /mini-spring-cloud-netflix-zuul/src/main/java/com/github/cloud/netflix/zuul/filters/Route.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.netflix.zuul.filters; 2 | 3 | /** 4 | * 路由 5 | * 6 | * @author derek(易仁川) 7 | * @date 2022/6/28 8 | */ 9 | public class Route { 10 | 11 | private String path; 12 | 13 | private String location; 14 | 15 | public Route(String path, String location) { 16 | this.path = path; 17 | this.location = location; 18 | } 19 | 20 | public String getPath() { 21 | return path; 22 | } 23 | 24 | public void setPath(String path) { 25 | this.path = path; 26 | } 27 | 28 | public String getLocation() { 29 | return location; 30 | } 31 | 32 | public void setLocation(String location) { 33 | this.location = location; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /mini-spring-cloud-netflix-zuul/src/main/java/com/github/cloud/netflix/zuul/metrics/EmptyTracerFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.netflix.zuul.metrics; 2 | 3 | import com.netflix.zuul.monitoring.Tracer; 4 | import com.netflix.zuul.monitoring.TracerFactory; 5 | 6 | /** 7 | * @author derek(易仁川) 8 | * @date 2022/6/27 9 | */ 10 | public class EmptyTracerFactory extends TracerFactory { 11 | 12 | private final EmptyTracer emptyTracer = new EmptyTracer(); 13 | 14 | @Override 15 | public Tracer startMicroTracer(String name) { 16 | return emptyTracer; 17 | } 18 | 19 | private static final class EmptyTracer implements Tracer { 20 | 21 | @Override 22 | public void setName(String name) { 23 | } 24 | 25 | @Override 26 | public void stopAndLog() { 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/RibbonClients.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.loadbalancer.ribbon; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.context.annotation.Import; 5 | 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.lang.annotation.Target; 10 | 11 | /** 12 | * @author derek(易仁川) 13 | * @date 2022/3/22 14 | */ 15 | @Configuration(proxyBeanMethods = false) 16 | @Retention(RetentionPolicy.RUNTIME) 17 | @Target({ ElementType.TYPE }) 18 | @Import(RibbonClientConfigurationRegistrar.class) 19 | public @interface RibbonClients { 20 | 21 | Class[] defaultConfiguration() default {}; 22 | 23 | } 24 | 25 | -------------------------------------------------------------------------------- /mini-spring-cloud-tutu-discovery/src/main/java/com/github/cloud/tutu/discovery/TutuDiscoveryAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.tutu.discovery; 2 | 3 | import com.github.cloud.tutu.TutuDiscoveryProperties; 4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | /** 9 | * @author derek(易仁川) 10 | * @date 2022/3/20 11 | */ 12 | @Configuration 13 | public class TutuDiscoveryAutoConfiguration { 14 | 15 | @Bean 16 | @ConditionalOnMissingBean 17 | public TutuDiscoveryProperties tutuDiscoveryProperties() { 18 | return new TutuDiscoveryProperties(); 19 | } 20 | 21 | @Bean 22 | public TutuDiscoveryClient tutuDiscoveryClient(TutuDiscoveryProperties tutuDiscoveryProperties) { 23 | return new TutuDiscoveryClient(tutuDiscoveryProperties); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /mini-spring-cloud-examples/mini-spring-cloud-provider-example/src/main/java/com/github/cloud/examples/ProviderApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.examples; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.web.bind.annotation.PostMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | /** 10 | * @author derek(易仁川) 11 | * @date 2022/3/19 12 | */ 13 | @RestController 14 | @SpringBootApplication 15 | public class ProviderApplication { 16 | 17 | @Value("${server.port}") 18 | private Integer port; 19 | 20 | public static void main(String[] args) { 21 | SpringApplication.run(ProviderApplication.class, args); 22 | } 23 | 24 | @PostMapping("/echo") 25 | public String echo() { 26 | return "Port of the service provider: " + port; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/SpringClientFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.loadbalancer.ribbon; 2 | 3 | import com.github.cloud.loadbalancer.ribbon.config.RibbonClientConfiguration; 4 | import org.springframework.cloud.context.named.NamedContextFactory; 5 | import org.springframework.context.annotation.AnnotationConfigApplicationContext; 6 | 7 | /** 8 | * 为每个负载均衡客户端创建一个应用上下文(ApplicationContext) 9 | * 10 | * @author derek(易仁川) 11 | * @date 2022/3/22 12 | */ 13 | public class SpringClientFactory extends NamedContextFactory { 14 | 15 | private static final String NAMESPACE = "ribbon"; 16 | 17 | public SpringClientFactory() { 18 | super(RibbonClientConfiguration.class, NAMESPACE, "ribbon.client.name"); 19 | } 20 | 21 | @Override 22 | public C getInstance(String name, Class type) { 23 | return super.getInstance(name, type); 24 | } 25 | 26 | @Override 27 | protected AnnotationConfigApplicationContext getContext(String name) { 28 | return super.getContext(name); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/config/TutuRibbonClientConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.loadbalancer.ribbon.config; 2 | 3 | import com.github.cloud.loadbalancer.ribbon.TutuServerList; 4 | import com.github.cloud.tutu.TutuDiscoveryProperties; 5 | import com.netflix.client.config.IClientConfig; 6 | import com.netflix.loadbalancer.ServerList; 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | /** 12 | * 自定义ribbon组件 13 | * 14 | * @author derek(易仁川) 15 | * @date 2022/3/22 16 | */ 17 | @Configuration 18 | public class TutuRibbonClientConfiguration { 19 | 20 | @Bean 21 | @ConditionalOnMissingBean 22 | public ServerList ribbonServerList(IClientConfig config, 23 | TutuDiscoveryProperties discoveryProperties) { 24 | TutuServerList serverList = new TutuServerList(discoveryProperties); 25 | serverList.initWithNiwsConfig(config); 26 | return serverList; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /mini-spring-cloud-netflix-zuul/src/main/java/com/github/cloud/netflix/zuul/filters/SimpleRouteLocator.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.netflix.zuul.filters; 2 | 3 | import org.springframework.util.AntPathMatcher; 4 | import org.springframework.util.PathMatcher; 5 | 6 | import java.util.Map; 7 | 8 | /** 9 | * 路由定位器实现类 10 | * 11 | * @author derek(易仁川) 12 | * @date 2022/6/28 13 | */ 14 | public class SimpleRouteLocator implements RouteLocator { 15 | 16 | private ZuulProperties zuulProperties; 17 | 18 | private PathMatcher pathMatcher = new AntPathMatcher(); 19 | 20 | public SimpleRouteLocator(ZuulProperties zuulProperties) { 21 | this.zuulProperties = zuulProperties; 22 | } 23 | 24 | @Override 25 | public Route getMatchingRoute(String path) { 26 | for (Map.Entry entry : zuulProperties.getRoutes().entrySet()) { 27 | ZuulProperties.ZuulRoute zuulRoute = entry.getValue(); 28 | String pattern = zuulRoute.getPath(); 29 | if (pathMatcher.match(pattern, path)) { 30 | String targetPath = path.substring(pattern.indexOf("*") - 1); 31 | return new Route(targetPath, zuulRoute.getServiceId()); 32 | } 33 | } 34 | 35 | return null; 36 | } 37 | } -------------------------------------------------------------------------------- /mini-spring-cloud-examples/mini-spring-cloud-provider-example/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | mini-spring-cloud 7 | com.github 8 | 1.0.0-SNAPSHOT 9 | ../../pom.xml 10 | 11 | 4.0.0 12 | 13 | mini-spring-cloud-provider-example 14 | 15 | 16 | 17 | org.springframework.boot 18 | spring-boot-starter-web 19 | 20 | 21 | 22 | com.github 23 | mini-spring-cloud-tutu-discovery 24 | 25 | 26 | 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-maven-plugin 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /mini-spring-cloud-netflix-zuul/src/main/java/com/github/cloud/netflix/zuul/filters/ZuulProperties.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.netflix.zuul.filters; 2 | 3 | import java.util.LinkedHashMap; 4 | import java.util.Map; 5 | 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | 8 | /** 9 | * @author derek(易仁川) 10 | * @date 2022/6/23 11 | */ 12 | @ConfigurationProperties("zuul") 13 | public class ZuulProperties { 14 | 15 | private String servletPath = "/zuul/*"; 16 | 17 | private Map routes = new LinkedHashMap<>(); 18 | 19 | public String getServletPath() { 20 | return servletPath; 21 | } 22 | 23 | public void setServletPath(String servletPath) { 24 | this.servletPath = servletPath; 25 | } 26 | 27 | public Map getRoutes() { 28 | return routes; 29 | } 30 | 31 | public void setRoutes(Map routes) { 32 | this.routes = routes; 33 | } 34 | 35 | public static class ZuulRoute { 36 | 37 | private String path; 38 | 39 | private String serviceId; 40 | 41 | public String getPath() { 42 | return path; 43 | } 44 | 45 | public void setPath(String path) { 46 | this.path = path; 47 | } 48 | 49 | public String getServiceId() { 50 | return serviceId; 51 | } 52 | 53 | public void setServiceId(String serviceId) { 54 | this.serviceId = serviceId; 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /mini-spring-cloud-examples/tutu-server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | mini-spring-cloud 7 | com.github 8 | 1.0.0-SNAPSHOT 9 | ../../pom.xml 10 | 11 | 4.0.0 12 | 13 | tutu-server 14 | 15 | 16 | 17 | org.springframework.boot 18 | spring-boot-starter-web 19 | 20 | 21 | 22 | com.alibaba 23 | fastjson 24 | 25 | 26 | 27 | cn.hutool 28 | hutool-all 29 | 30 | 31 | 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-maven-plugin 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /mini-spring-cloud-openfeign/src/main/java/com/github/cloud/openfeign/FeignClientsConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.openfeign; 2 | 3 | import com.github.cloud.openfeign.ribbon.LoadBalancerFeignClient; 4 | import com.github.cloud.openfeign.support.SpringMvcContract; 5 | import feign.Client; 6 | import feign.Contract; 7 | import feign.codec.Decoder; 8 | import feign.codec.Encoder; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 10 | import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | 14 | /** 15 | * 配置feign的核心API 16 | * 17 | * @author derek(易仁川) 18 | * @date 2022/4/9 19 | */ 20 | @Configuration 21 | public class FeignClientsConfiguration { 22 | 23 | @Bean 24 | @ConditionalOnMissingBean 25 | public Encoder encoder() { 26 | return new Encoder.Default(); 27 | } 28 | 29 | @Bean 30 | @ConditionalOnMissingBean 31 | public Decoder decoder() { 32 | return new Decoder.Default(); 33 | } 34 | 35 | @Bean 36 | @ConditionalOnMissingBean 37 | public Contract contract() { 38 | return new SpringMvcContract(); 39 | } 40 | 41 | @Bean 42 | @ConditionalOnMissingBean 43 | public Client client(LoadBalancerClient loadBalancerClient) { 44 | return new LoadBalancerFeignClient(loadBalancerClient, new Client.Default(null, null)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/config/RibbonAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.loadbalancer.ribbon.config; 2 | 3 | import com.github.cloud.loadbalancer.ribbon.RibbonClientSpecification; 4 | import com.github.cloud.loadbalancer.ribbon.RibbonLoadBalancerClient; 5 | import com.github.cloud.loadbalancer.ribbon.SpringClientFactory; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 8 | import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | /** 16 | * @author derek(易仁川) 17 | * @date 2022/3/22 18 | */ 19 | @Configuration 20 | public class RibbonAutoConfiguration { 21 | 22 | @Autowired(required = false) 23 | private List configurations = new ArrayList<>(); 24 | 25 | @Bean 26 | public SpringClientFactory springClientFactory() { 27 | SpringClientFactory factory = new SpringClientFactory(); 28 | factory.setConfigurations(this.configurations); 29 | return factory; 30 | } 31 | 32 | @Bean 33 | @ConditionalOnMissingBean(LoadBalancerClient.class) 34 | public LoadBalancerClient loadBalancerClient() { 35 | return new RibbonLoadBalancerClient(springClientFactory()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /mini-spring-cloud-tutu-discovery/src/main/java/com/github/cloud/tutu/registry/TutuAutoServiceRegistration.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.tutu.registry; 2 | 3 | import org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration; 4 | import org.springframework.cloud.client.serviceregistry.Registration; 5 | import org.springframework.cloud.client.serviceregistry.ServiceRegistry; 6 | 7 | /** 8 | * 服务自动注册 9 | * 10 | * @author derek(易仁川) 11 | * @date 2022/3/19 12 | */ 13 | public class TutuAutoServiceRegistration extends AbstractAutoServiceRegistration { 14 | 15 | private TutuRegistration tutuRegistration; 16 | 17 | protected TutuAutoServiceRegistration(ServiceRegistry serviceRegistry, TutuRegistration tutuRegistration) { 18 | super(serviceRegistry, null); 19 | this.tutuRegistration = tutuRegistration; 20 | } 21 | 22 | @SuppressWarnings("deprecation") 23 | @Override 24 | protected Registration getRegistration() { 25 | if (tutuRegistration.getPort() < 0) { 26 | //设置服务端口 27 | tutuRegistration.setPort(this.getPort().get()); 28 | } 29 | return tutuRegistration; 30 | } 31 | 32 | @Override 33 | protected Object getConfiguration() { 34 | return tutuRegistration.getTutuDiscoveryProperties(); 35 | } 36 | 37 | @Override 38 | protected boolean isEnabled() { 39 | return true; 40 | } 41 | 42 | @Override 43 | protected Registration getManagementRegistration() { 44 | return null; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /mini-spring-cloud-openfeign/src/main/java/com/github/cloud/openfeign/support/SpringMvcContract.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.openfeign.support; 2 | 3 | import feign.Contract; 4 | import feign.MethodMetadata; 5 | import feign.Request; 6 | import org.springframework.web.bind.annotation.PostMapping; 7 | 8 | import java.lang.annotation.Annotation; 9 | import java.lang.reflect.Method; 10 | 11 | /** 12 | * feign支持Spring MVC的注解 13 | * 14 | * @author derek(易仁川) 15 | * @date 2022/4/9 16 | */ 17 | public class SpringMvcContract extends Contract.BaseContract { 18 | 19 | @Override 20 | protected void processAnnotationOnClass(MethodMetadata data, Class clz) { 21 | //TODO 解析接口注解 22 | } 23 | 24 | @Override 25 | protected void processAnnotationOnMethod(MethodMetadata data, Annotation annotation, Method method) { 26 | //解析方法注解 27 | //解析PostMapping注解 28 | if (annotation instanceof PostMapping) { 29 | PostMapping postMapping = (PostMapping) annotation; 30 | data.template().method(Request.HttpMethod.POST); 31 | String path = postMapping.value()[0]; 32 | if (!path.startsWith("/") && !data.template().path().endsWith("/")) { 33 | path = "/" + path; 34 | } 35 | data.template().uri(path, true); 36 | } 37 | 38 | //TODO 解析其他注解 39 | } 40 | 41 | @Override 42 | protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) { 43 | //TODO 解析参数 44 | return true; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /mini-spring-cloud-openfeign/src/main/java/com/github/cloud/openfeign/FeignClientSpecification.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.openfeign; 2 | 3 | import org.springframework.cloud.context.named.NamedContextFactory; 4 | 5 | import java.util.Arrays; 6 | import java.util.Objects; 7 | 8 | /** 9 | * @author derek(易仁川) 10 | * @date 2022/4/9 11 | */ 12 | public class FeignClientSpecification implements NamedContextFactory.Specification { 13 | 14 | private String name; 15 | 16 | private Class[] configuration; 17 | 18 | public FeignClientSpecification(String name, Class[] configuration) { 19 | this.name = name; 20 | this.configuration = configuration; 21 | } 22 | 23 | @Override 24 | public String getName() { 25 | return name; 26 | } 27 | 28 | public void setName(String name) { 29 | this.name = name; 30 | } 31 | 32 | @Override 33 | public Class[] getConfiguration() { 34 | return configuration; 35 | } 36 | 37 | public void setConfiguration(Class[] configuration) { 38 | this.configuration = configuration; 39 | } 40 | 41 | @Override 42 | public boolean equals(Object o) { 43 | if (this == o) return true; 44 | if (o == null || getClass() != o.getClass()) return false; 45 | FeignClientSpecification that = (FeignClientSpecification) o; 46 | return name.equals(that.name) && Arrays.equals(configuration, that.configuration); 47 | } 48 | 49 | @Override 50 | public int hashCode() { 51 | int result = Objects.hash(name); 52 | result = 31 * result + Arrays.hashCode(configuration); 53 | return result; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /mini-spring-cloud-tutu-discovery/src/main/java/com/github/cloud/tutu/registry/TutuRegistration.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.tutu.registry; 2 | 3 | import com.github.cloud.tutu.TutuDiscoveryProperties; 4 | import org.springframework.cloud.client.DefaultServiceInstance; 5 | import org.springframework.cloud.client.serviceregistry.Registration; 6 | 7 | import java.net.URI; 8 | import java.util.Map; 9 | 10 | /** 11 | * tutu服务 12 | * 13 | * @author derek(易仁川) 14 | * @date 2022/3/19 15 | */ 16 | public class TutuRegistration implements Registration { 17 | 18 | private TutuDiscoveryProperties tutuDiscoveryProperties; 19 | 20 | public TutuRegistration(TutuDiscoveryProperties tutuDiscoveryProperties) { 21 | this.tutuDiscoveryProperties = tutuDiscoveryProperties; 22 | } 23 | 24 | @Override 25 | public String getServiceId() { 26 | return tutuDiscoveryProperties.getService(); 27 | } 28 | 29 | @Override 30 | public String getHost() { 31 | return tutuDiscoveryProperties.getIp(); 32 | } 33 | 34 | @Override 35 | public int getPort() { 36 | return tutuDiscoveryProperties.getPort(); 37 | } 38 | 39 | public void setPort(int port) { 40 | this.tutuDiscoveryProperties.setPort(port); 41 | } 42 | 43 | @Override 44 | public boolean isSecure() { 45 | return tutuDiscoveryProperties.isSecure(); 46 | } 47 | 48 | @Override 49 | public URI getUri() { 50 | return DefaultServiceInstance.getUri(this); 51 | } 52 | 53 | @Override 54 | public Map getMetadata() { 55 | return null; 56 | } 57 | 58 | public TutuDiscoveryProperties getTutuDiscoveryProperties() { 59 | return tutuDiscoveryProperties; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/RibbonClientConfigurationRegistrar.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.loadbalancer.ribbon; 2 | 3 | import org.springframework.beans.factory.support.BeanDefinitionBuilder; 4 | import org.springframework.beans.factory.support.BeanDefinitionRegistry; 5 | import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; 6 | import org.springframework.core.type.AnnotationMetadata; 7 | 8 | import java.util.Map; 9 | 10 | /** 11 | * 处理注解RibbonClients的配置类 12 | * 13 | * @author derek(易仁川) 14 | * @date 2022/3/22 15 | */ 16 | public class RibbonClientConfigurationRegistrar implements ImportBeanDefinitionRegistrar { 17 | 18 | @Override 19 | public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { 20 | Map attrs = metadata.getAnnotationAttributes(RibbonClients.class.getName(), true); 21 | 22 | if (attrs != null && attrs.containsKey("defaultConfiguration")) { 23 | String name = "default." + metadata.getClassName(); 24 | registerClientConfiguration(registry, name, attrs.get("defaultConfiguration")); 25 | } 26 | } 27 | 28 | private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, 29 | Object configuration) { 30 | BeanDefinitionBuilder builder = BeanDefinitionBuilder 31 | .genericBeanDefinition(RibbonClientSpecification.class); 32 | builder.addConstructorArgValue(name); 33 | builder.addConstructorArgValue(configuration); 34 | registry.registerBeanDefinition(name + ".RibbonClientSpecification", 35 | builder.getBeanDefinition()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /mini-spring-cloud-tutu-discovery/src/main/java/com/github/cloud/tutu/registry/TutuServiceRegistryAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.tutu.registry; 2 | 3 | import com.github.cloud.tutu.TutuDiscoveryProperties; 4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 6 | import org.springframework.cloud.client.serviceregistry.Registration; 7 | import org.springframework.cloud.client.serviceregistry.ServiceRegistry; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | /** 12 | * 自动配置服务注册相关类 13 | * 14 | * @author derek(易仁川) 15 | * @date 2022/3/19 16 | */ 17 | @Configuration 18 | @ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true) 19 | public class TutuServiceRegistryAutoConfiguration { 20 | 21 | @Bean 22 | @ConditionalOnMissingBean 23 | public TutuDiscoveryProperties tutuProperties() { 24 | return new TutuDiscoveryProperties(); 25 | } 26 | 27 | @Bean 28 | public TutuRegistration tutuRegistration(TutuDiscoveryProperties tutuDiscoveryProperties) { 29 | return new TutuRegistration(tutuDiscoveryProperties); 30 | } 31 | 32 | @Bean 33 | public TutuServiceRegistry tutuServiceRegistry(TutuDiscoveryProperties tutuDiscoveryProperties) { 34 | return new TutuServiceRegistry(tutuDiscoveryProperties); 35 | } 36 | 37 | @Bean 38 | public TutuAutoServiceRegistration tutuAutoServiceRegistration(ServiceRegistry serviceRegistry, TutuRegistration tutuRegistration) { 39 | return new TutuAutoServiceRegistration(serviceRegistry, tutuRegistration); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /mini-spring-cloud-netflix-zuul/src/main/java/com/github/cloud/netflix/zuul/filters/pre/PreDecorationFilter.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.netflix.zuul.filters.pre; 2 | 3 | import com.github.cloud.netflix.zuul.filters.Route; 4 | import com.github.cloud.netflix.zuul.filters.RouteLocator; 5 | import com.netflix.zuul.ZuulFilter; 6 | import com.netflix.zuul.context.RequestContext; 7 | import com.netflix.zuul.exception.ZuulException; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import static com.github.cloud.netflix.zuul.filters.support.FilterConstants.*; 12 | 13 | /** 14 | * pre类型过滤器,根据RouteLocator来进行路由规则的匹配 15 | * 16 | * @author derek(易仁川) 17 | * @date 2022/6/27 18 | */ 19 | public class PreDecorationFilter extends ZuulFilter { 20 | private static Logger logger = LoggerFactory.getLogger(PreDecorationFilter.class); 21 | 22 | private RouteLocator routeLocator; 23 | 24 | public PreDecorationFilter(RouteLocator routeLocator) { 25 | this.routeLocator = routeLocator; 26 | } 27 | 28 | @Override 29 | public String filterType() { 30 | return PRE_TYPE; 31 | } 32 | 33 | @Override 34 | public int filterOrder() { 35 | return 5; 36 | } 37 | 38 | @Override 39 | public boolean shouldFilter() { 40 | return true; 41 | } 42 | 43 | @Override 44 | public Object run() throws ZuulException { 45 | RequestContext requestContext = RequestContext.getCurrentContext(); 46 | String requestURI = requestContext.getRequest().getRequestURI(); 47 | //获取匹配的路由 48 | Route route = routeLocator.getMatchingRoute(requestURI); 49 | if (route != null) { 50 | requestContext.put(REQUEST_URI_KEY, route.getPath()); 51 | requestContext.set(SERVICE_ID_KEY, route.getLocation()); 52 | } else { 53 | logger.error("获取不到匹配的路由, requestURI: {}", requestContext); 54 | } 55 | 56 | return null; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /mini-spring-cloud-examples/mini-spring-cloud-consumer-examples/src/test/java/com/github/cloud/examples/FeignTest.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.examples; 2 | 3 | import feign.Feign; 4 | import feign.RequestLine; 5 | import org.junit.jupiter.api.Test; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.cloud.openfeign.support.SpringMvcContract; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | 13 | /** 14 | * @author derek(易仁川) 15 | * @date 2022/3/27 16 | */ 17 | public class FeignTest { 18 | private static Logger logger = LoggerFactory.getLogger(FeignTest.class); 19 | 20 | interface HelloService { 21 | 22 | @RequestLine("GET /hello") 23 | String hello(); 24 | } 25 | 26 | @Test 27 | public void testOpenFeign() { 28 | HelloService helloService = Feign.builder() 29 | .target(HelloService.class, "http://localhost:8080"); 30 | String response = helloService.hello(); 31 | logger.info("response: {}", response); 32 | boolean succ = response.startsWith("Port of the service provider"); 33 | assertThat(succ).isTrue(); 34 | } 35 | 36 | interface WorldService { 37 | 38 | @GetMapping("/world") 39 | String world(); 40 | } 41 | 42 | @Test 43 | public void testSpringCloudOpenFeign() { 44 | WorldService worldService = Feign.builder() 45 | .contract(new SpringMvcContract()) 46 | .target(WorldService.class, "http://localhost:8080"); 47 | String response = worldService.world(); 48 | logger.info("response: {}", response); 49 | boolean succ = response.startsWith("Port of the service provider"); 50 | assertThat(succ).isTrue(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /mini-spring-cloud-openfeign/src/main/java/com/github/cloud/openfeign/ribbon/LoadBalancerFeignClient.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.openfeign.ribbon; 2 | 3 | import feign.Client; 4 | import feign.Request; 5 | import feign.Response; 6 | import org.springframework.cloud.client.ServiceInstance; 7 | import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; 8 | 9 | import java.io.IOException; 10 | import java.net.URI; 11 | import java.nio.charset.StandardCharsets; 12 | import java.util.HashMap; 13 | 14 | /** 15 | * 具备负载均衡能力的feign client 16 | * 17 | * @author derek(易仁川) 18 | * @date 2022/4/9 19 | */ 20 | public class LoadBalancerFeignClient implements Client { 21 | 22 | private LoadBalancerClient loadBalancerClient; 23 | 24 | private Client delegate; 25 | 26 | public LoadBalancerFeignClient(LoadBalancerClient loadBalancerClient, Client delegate) { 27 | this.loadBalancerClient = loadBalancerClient; 28 | this.delegate = delegate; 29 | } 30 | 31 | @SuppressWarnings("deprecation") 32 | @Override 33 | public Response execute(Request request, Request.Options options) throws IOException { 34 | try { 35 | //客户端负载均衡 36 | URI original = URI.create(request.url()); 37 | String serviceId = original.getHost(); 38 | //选择服务实例 39 | ServiceInstance serviceInstance = loadBalancerClient.choose(serviceId); 40 | //重建请求URI 41 | URI uri = loadBalancerClient.reconstructURI(serviceInstance, original); 42 | 43 | Request newRequest = Request.create(request.httpMethod(), uri.toASCIIString(), new HashMap<>(), 44 | request.body(), StandardCharsets.UTF_8); 45 | return delegate.execute(newRequest, options); 46 | } catch (IOException e) { 47 | throw new RuntimeException(e); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /mini-spring-cloud-tutu-discovery/src/main/java/com/github/cloud/tutu/TutuDiscoveryProperties.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.tutu; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.cloud.commons.util.InetUtils; 6 | import org.springframework.util.StringUtils; 7 | 8 | import javax.annotation.PostConstruct; 9 | 10 | /** 11 | * @author derek(易仁川) 12 | * @date 2022/3/19 13 | */ 14 | @ConfigurationProperties("spring.cloud.tutu.discovery") 15 | public class TutuDiscoveryProperties { 16 | 17 | @Autowired 18 | private InetUtils inetUtils; 19 | 20 | private String serverAddr; 21 | 22 | private String service; 23 | 24 | private String ip; 25 | 26 | private int port = -1; 27 | 28 | private boolean secure = false; 29 | 30 | @PostConstruct 31 | public void init() { 32 | if (!StringUtils.hasLength(ip)) { 33 | //获取服务IP地址 34 | ip = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress(); 35 | } 36 | } 37 | 38 | public String getServerAddr() { 39 | return serverAddr; 40 | } 41 | 42 | public void setServerAddr(String serverAddr) { 43 | this.serverAddr = serverAddr; 44 | } 45 | 46 | public String getService() { 47 | return service; 48 | } 49 | 50 | public void setService(String service) { 51 | this.service = service; 52 | } 53 | 54 | public String getIp() { 55 | return ip; 56 | } 57 | 58 | public void setIp(String ip) { 59 | this.ip = ip; 60 | } 61 | 62 | public int getPort() { 63 | return port; 64 | } 65 | 66 | public void setPort(int port) { 67 | this.port = port; 68 | } 69 | 70 | public boolean isSecure() { 71 | return secure; 72 | } 73 | 74 | public void setSecure(boolean secure) { 75 | this.secure = secure; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /mini-spring-cloud-examples/mini-spring-cloud-api-gateway-example/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | mini-spring-cloud 7 | com.github 8 | 1.0.0-SNAPSHOT 9 | ../../pom.xml 10 | 11 | 4.0.0 12 | 13 | mini-spring-cloud-api-gateway-example 14 | 15 | 16 | 17 | org.springframework.boot 18 | spring-boot-starter-web 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-test 24 | test 25 | 26 | 27 | 28 | com.github 29 | mini-spring-cloud-netflix-zuul 30 | 31 | 32 | 33 | com.github 34 | mini-spring-cloud-load-balancer 35 | 36 | 37 | 38 | com.netflix.archaius 39 | archaius-core 40 | 0.7.6 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-maven-plugin 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /mini-spring-cloud-openfeign/src/main/java/com/github/cloud/openfeign/FeignClientsRegistrar.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.openfeign; 2 | 3 | import cn.hutool.core.util.ClassUtil; 4 | import org.springframework.beans.factory.support.BeanDefinitionRegistry; 5 | import org.springframework.beans.factory.support.GenericBeanDefinition; 6 | import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; 7 | import org.springframework.core.type.AnnotationMetadata; 8 | import org.springframework.util.ClassUtils; 9 | 10 | import java.util.Set; 11 | 12 | /** 13 | * 往bean容器中注册Feign客户端 14 | * 15 | * @author derek(易仁川) 16 | * @date 2022/4/7 17 | */ 18 | public class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar { 19 | 20 | /** 21 | * 往bean容器中注册Feign客户端 22 | * 23 | * @param importingClassMetadata 24 | * @param registry 25 | */ 26 | @Override 27 | public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { 28 | //为FeignClient注解修饰的接口生成代理bean即Feign客户端,并注册到bean容器 29 | String packageName = ClassUtils.getPackageName(importingClassMetadata.getClassName()); 30 | //扫描所有被FeignClient注解修饰的接口 31 | Set> classes = ClassUtil.scanPackageByAnnotation(packageName, FeignClient.class); 32 | for (Class clazz : classes) { 33 | GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); 34 | //使用FeignClientFactoryBean生成Feign客户端 35 | beanDefinition.setBeanClass(FeignClientFactoryBean.class); 36 | String clientName = clazz.getAnnotation(FeignClient.class).value(); 37 | beanDefinition.getPropertyValues().addPropertyValue("contextId", clientName); 38 | beanDefinition.getPropertyValues().addPropertyValue("type", clazz); 39 | 40 | //将Feign客户端注册进bean容器 41 | String beanName = clazz.getName(); 42 | registry.registerBeanDefinition(beanName, beanDefinition); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /mini-spring-cloud-tutu-discovery/src/main/java/com/github/cloud/tutu/TutuServiceInstance.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.tutu; 2 | 3 | import org.springframework.cloud.client.DefaultServiceInstance; 4 | import org.springframework.cloud.client.ServiceInstance; 5 | 6 | import java.net.URI; 7 | import java.util.Map; 8 | 9 | /** 10 | * 服务实例 11 | * 12 | * @author derek(易仁川) 13 | * @date 2022/3/20 14 | */ 15 | public class TutuServiceInstance implements ServiceInstance { 16 | 17 | private String serviceId; 18 | 19 | private String host; 20 | 21 | private int port; 22 | 23 | private boolean secure = false; 24 | 25 | private Map metadata; 26 | 27 | public TutuServiceInstance() { 28 | } 29 | 30 | public TutuServiceInstance(String serviceId, String host, int port) { 31 | this.serviceId = serviceId; 32 | this.host = host; 33 | this.port = port; 34 | } 35 | 36 | @Override 37 | public String getServiceId() { 38 | return serviceId; 39 | } 40 | 41 | @Override 42 | public String getHost() { 43 | return host; 44 | } 45 | 46 | @Override 47 | public int getPort() { 48 | return port; 49 | } 50 | 51 | @Override 52 | public boolean isSecure() { 53 | return secure; 54 | } 55 | 56 | @Override 57 | public URI getUri() { 58 | return DefaultServiceInstance.getUri(this); 59 | } 60 | 61 | @Override 62 | public Map getMetadata() { 63 | return metadata; 64 | } 65 | 66 | public void setServiceId(String serviceId) { 67 | this.serviceId = serviceId; 68 | } 69 | 70 | public void setHost(String host) { 71 | this.host = host; 72 | } 73 | 74 | public void setPort(int port) { 75 | this.port = port; 76 | } 77 | 78 | public void setSecure(boolean secure) { 79 | this.secure = secure; 80 | } 81 | 82 | public void setMetadata(Map metadata) { 83 | this.metadata = metadata; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/RibbonClientSpecification.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.loadbalancer.ribbon; 2 | 3 | import org.springframework.cloud.context.named.NamedContextFactory; 4 | 5 | import java.util.Arrays; 6 | import java.util.Objects; 7 | 8 | /** 9 | * ribbon客户端配置 10 | * 11 | * @author derek(易仁川) 12 | * @date 2022/3/22 13 | */ 14 | public class RibbonClientSpecification implements NamedContextFactory.Specification { 15 | 16 | private String name; 17 | 18 | private Class[] configuration; 19 | 20 | public RibbonClientSpecification() { 21 | } 22 | 23 | public RibbonClientSpecification(String name, Class[] configuration) { 24 | this.name = name; 25 | this.configuration = configuration; 26 | } 27 | 28 | public String getName() { 29 | return name; 30 | } 31 | 32 | public void setName(String name) { 33 | this.name = name; 34 | } 35 | 36 | public Class[] getConfiguration() { 37 | return configuration; 38 | } 39 | 40 | public void setConfiguration(Class[] configuration) { 41 | this.configuration = configuration; 42 | } 43 | 44 | @Override 45 | public boolean equals(Object o) { 46 | if (this == o) { 47 | return true; 48 | } 49 | if (o == null || getClass() != o.getClass()) { 50 | return false; 51 | } 52 | RibbonClientSpecification that = (RibbonClientSpecification) o; 53 | return Arrays.equals(configuration, that.configuration) 54 | && Objects.equals(name, that.name); 55 | } 56 | 57 | @Override 58 | public int hashCode() { 59 | return Objects.hash(configuration, name); 60 | } 61 | 62 | @Override 63 | public String toString() { 64 | return new StringBuilder("RibbonClientSpecification{").append("name='") 65 | .append(name).append("', ").append("configuration=") 66 | .append(Arrays.toString(configuration)).append("}").toString(); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /mini-spring-cloud-netflix-zuul/src/main/java/com/github/cloud/netflix/zuul/filters/post/SendResponseFilter.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.netflix.zuul.filters.post; 2 | 3 | import com.netflix.zuul.ZuulFilter; 4 | import com.netflix.zuul.context.RequestContext; 5 | import com.netflix.zuul.exception.ZuulException; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.util.StreamUtils; 9 | 10 | import javax.servlet.http.HttpServletResponse; 11 | import java.io.InputStream; 12 | import java.io.OutputStream; 13 | 14 | import static com.github.cloud.netflix.zuul.filters.support.FilterConstants.POST_TYPE; 15 | import static org.springframework.util.ReflectionUtils.rethrowRuntimeException; 16 | 17 | /** 18 | * post类型过滤器,向客户端输出响应报文 19 | * 20 | * @author derek(易仁川) 21 | * @date 2022/6/27 22 | */ 23 | public class SendResponseFilter extends ZuulFilter { 24 | private static Logger logger = LoggerFactory.getLogger(SendResponseFilter.class); 25 | 26 | @Override 27 | public String filterType() { 28 | return POST_TYPE; 29 | } 30 | 31 | @Override 32 | public int filterOrder() { 33 | return 1000; 34 | } 35 | 36 | @Override 37 | public boolean shouldFilter() { 38 | return RequestContext.getCurrentContext() 39 | .getResponseDataStream() != null; 40 | } 41 | 42 | @Override 43 | public Object run() throws ZuulException { 44 | //向客户端输出响应报文 45 | RequestContext requestContext = RequestContext.getCurrentContext(); 46 | InputStream inputStream = requestContext.getResponseDataStream(); 47 | try { 48 | HttpServletResponse servletResponse = requestContext.getResponse(); 49 | servletResponse.setCharacterEncoding("UTF-8"); 50 | 51 | OutputStream outStream = servletResponse.getOutputStream(); 52 | StreamUtils.copy(inputStream, outStream); 53 | } catch (Exception e) { 54 | rethrowRuntimeException(e); 55 | } finally { 56 | //关闭输入输出流 57 | if (inputStream != null) { 58 | try { 59 | inputStream.close(); 60 | } catch (Exception e) { 61 | logger.error("关闭输入流失败", e); 62 | } 63 | } 64 | 65 | //Servlet容器会自动关闭输出流 66 | } 67 | return null; 68 | } 69 | } -------------------------------------------------------------------------------- /mini-spring-cloud-examples/mini-spring-cloud-consumer-examples/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | mini-spring-cloud 7 | com.github 8 | 1.0.0-SNAPSHOT 9 | ../../pom.xml 10 | 11 | 4.0.0 12 | 13 | mini-spring-cloud-consumer-examples 14 | 15 | 16 | 17 | org.springframework.boot 18 | spring-boot-starter-web 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-test 24 | test 25 | 26 | 27 | 28 | com.github 29 | mini-spring-cloud-tutu-discovery 30 | 31 | 32 | 33 | com.github 34 | mini-spring-cloud-load-balancer 35 | 36 | 37 | 38 | com.github 39 | mini-spring-cloud-openfeign 40 | 41 | 42 | 43 | org.springframework.cloud 44 | spring-cloud-starter-feign 45 | 1.4.7.RELEASE 46 | test 47 | 48 | 49 | 50 | 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-maven-plugin 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /mini-spring-cloud-openfeign/src/main/java/com/github/cloud/openfeign/FeignClientFactoryBean.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.openfeign; 2 | 3 | import feign.Client; 4 | import feign.Contract; 5 | import feign.Feign; 6 | import feign.Target.HardCodedTarget; 7 | import feign.codec.Decoder; 8 | import feign.codec.Encoder; 9 | import org.springframework.beans.BeansException; 10 | import org.springframework.beans.factory.FactoryBean; 11 | import org.springframework.context.ApplicationContext; 12 | import org.springframework.context.ApplicationContextAware; 13 | 14 | /** 15 | * 生成Feign客户端的FactoryBean 16 | * 17 | * @author derek(易仁川) 18 | * @date 2022/4/7 19 | */ 20 | public class FeignClientFactoryBean implements FactoryBean, ApplicationContextAware { 21 | 22 | private String contextId; 23 | 24 | private Class type; 25 | 26 | private ApplicationContext applicationContext; 27 | 28 | @Override 29 | public Object getObject() throws Exception { 30 | FeignContext feignContext = applicationContext.getBean(FeignContext.class); 31 | Encoder encoder = feignContext.getInstance(contextId, Encoder.class); 32 | Decoder decoder = feignContext.getInstance(contextId, Decoder.class); 33 | Contract contract = feignContext.getInstance(contextId, Contract.class); 34 | Client client = feignContext.getInstance(contextId, Client.class); 35 | 36 | return Feign.builder() 37 | .encoder(encoder) 38 | .decoder(decoder) 39 | .contract(contract) 40 | .client(client) 41 | .target(new HardCodedTarget<>(type, contextId, "http://" + contextId)); 42 | } 43 | 44 | @Override 45 | public Class getObjectType() { 46 | return this.type; 47 | } 48 | 49 | @Override 50 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 51 | this.applicationContext = applicationContext; 52 | } 53 | 54 | public String getContextId() { 55 | return contextId; 56 | } 57 | 58 | public void setContextId(String contextId) { 59 | this.contextId = contextId; 60 | } 61 | 62 | public Class getType() { 63 | return type; 64 | } 65 | 66 | public void setType(Class type) { 67 | this.type = type; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mini-spring-cloud 2 | [![License](https://img.shields.io/badge/license-license-blue)](https://github.com/DerekYRC/mini-spring-cloud) 3 | [![Build Status](https://img.shields.io/badge/build-passing-brightgreen)](https://github.com/DerekYRC/mini-spring-cloud) 4 | [![Stars](https://img.shields.io/github/stars/DerekYRC/mini-spring-cloud)](https://img.shields.io/github/stars/DerekYRC/mini-spring-cloud) 5 | [![Forks](https://img.shields.io/github/forks/DerekYRC/mini-spring-cloud)](https://img.shields.io/github/forks/DerekYRC/mini-spring-cloud) 6 | 7 | **[English](./README_en.md) | 简体中文** 8 | 9 | **姊妹版:** 10 | - [**mini-spring**](https://github.com/DerekYRC/mini-spring) **(简化版的spring框架)** 11 | - [**mini-netty**](https://github.com/DerekYRC/mini-netty) **(简化版的netty框架)** 12 | 13 | ## 关于 14 | **mini-spring-cloud**是简化版的spring-cloud框架,能帮助你快速熟悉spring-cloud源码及掌握其核心原理。在保留spring cloud核心功能的的前提下尽量精简代码,核心功能包括**服务注册**、**服务发现**、**负载均衡**、**集成Feign简化调用**、**流量控制**、**熔断降级**、**API网关**等。 15 | 16 | 希望本项目对你有所帮助,请给个**STAR吧,谢谢!!!** 17 | 18 | ## 功能 19 | * [服务注册](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md#服务注册) 20 | * [服务发现](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md#服务发现) 21 | * [负载均衡](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md#集成ribbon实现客户端负载均衡) 22 | * [集成Feign简化调用方式](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md#集成Feign简化调用方式) 23 | * [API网关](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md#API网关) 24 | * [流量控制](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md#流量控制和熔断降级) 25 | * [熔断降级](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md#流量控制和熔断降级) 26 | 27 | ## 使用方法 28 | 阅读 [changelog.md](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md) 29 | 30 | ## 常见问题 31 | [**点此提问**](https://github.com/DerekYRC/mini-spring-cloud/issues/1) 32 | 33 | ## 贡献 34 | 欢迎Pull Request 35 | 36 | ## 关于我 37 | [**点此了解**](https://github.com/DerekYRC) 38 | 39 | 手机/微信:**15975984828** 邮箱:**15975984828@163.com** 40 | 41 | ## Star History 42 | 43 | [![Star History Chart](https://api.star-history.com/svg?repos=DerekYRC/mini-spring-cloud&type=Date)](https://star-history.com/#DerekYRC/mini-spring-cloud&Date) 44 | 45 | ## 版权说明 46 | 未取得本人书面许可,不得将该项目用于商业用途 47 | 48 | ## 参考 49 | - [《精尽Spring Cloud学习指南》](http://svip.iocoder.cn/Spring-Cloud/tutorials/) 50 | -------------------------------------------------------------------------------- /mini-spring-cloud-tutu-discovery/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | mini-spring-cloud 7 | com.github 8 | 1.0.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | mini-spring-cloud-tutu-discovery 13 | 1.0.0-SNAPSHOT 14 | 15 | 16 | 17 | org.springframework.boot 18 | spring-boot-configuration-processor 19 | true 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot 25 | true 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-autoconfigure 31 | true 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter 37 | true 38 | 39 | 40 | 41 | org.springframework.cloud 42 | spring-cloud-commons 43 | 44 | 45 | 46 | org.springframework.cloud 47 | spring-cloud-context 48 | 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-starter-web 53 | test 54 | 55 | 56 | 57 | cn.hutool 58 | hutool-all 59 | 60 | 61 | 62 | com.alibaba 63 | fastjson 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/TutuServerList.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.loadbalancer.ribbon; 2 | 3 | import cn.hutool.http.HttpUtil; 4 | import com.alibaba.fastjson.JSON; 5 | import com.alibaba.fastjson.JSONObject; 6 | import com.github.cloud.tutu.TutuDiscoveryProperties; 7 | import com.netflix.client.config.IClientConfig; 8 | import com.netflix.loadbalancer.AbstractServerList; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import java.util.HashMap; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.stream.Collectors; 16 | 17 | /** 18 | * 查询图图服务实例列表 19 | * 20 | * @author derek(易仁川) 21 | * @date 2022/3/13 22 | */ 23 | public class TutuServerList extends AbstractServerList { 24 | private static Logger logger = LoggerFactory.getLogger(TutuServerList.class); 25 | 26 | private TutuDiscoveryProperties discoveryProperties; 27 | 28 | private String serviceId; 29 | 30 | public TutuServerList(TutuDiscoveryProperties discoveryProperties) { 31 | this.discoveryProperties = discoveryProperties; 32 | } 33 | 34 | /** 35 | * 查询服务实例列表 36 | * 37 | * @return 38 | */ 39 | @Override 40 | public List getInitialListOfServers() { 41 | return getServer(); 42 | } 43 | 44 | /** 45 | * 查询服务实例列表 46 | * 47 | * @return 48 | */ 49 | @Override 50 | public List getUpdatedListOfServers() { 51 | return getServer(); 52 | } 53 | 54 | private List getServer() { 55 | Map param = new HashMap<>(); 56 | param.put("serviceName", serviceId); 57 | 58 | String response = HttpUtil.get(discoveryProperties.getServerAddr() + "/list", param); 59 | logger.info("query service instance, serviceId: {}, response: {}", serviceId, response); 60 | return JSON.parseArray(response).stream().map(hostInfo -> { 61 | String ip = ((JSONObject) hostInfo).getString("ip"); 62 | Integer port = ((JSONObject) hostInfo).getInteger("port"); 63 | return new TutuServer(ip, port); 64 | }).collect(Collectors.toList()); 65 | } 66 | 67 | public String getServiceId() { 68 | return serviceId; 69 | } 70 | 71 | @Override 72 | public void initWithNiwsConfig(IClientConfig iClientConfig) { 73 | this.serviceId = iClientConfig.getClientName(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /mini-spring-cloud-netflix-zuul/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | mini-spring-cloud 7 | com.github 8 | 1.0.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | mini-spring-cloud-netflix-zuul 13 | 14 | 15 | 16 | com.netflix.zuul 17 | zuul-core 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-configuration-processor 23 | true 24 | 25 | 26 | 27 | org.springframework.boot 28 | spring-boot 29 | true 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-autoconfigure 35 | true 36 | 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter 41 | true 42 | 43 | 44 | 45 | org.springframework.cloud 46 | spring-cloud-commons 47 | 48 | 49 | 50 | org.springframework.cloud 51 | spring-cloud-context 52 | 53 | 54 | 55 | org.springframework.boot 56 | spring-boot-starter-web 57 | 58 | 59 | 60 | cn.hutool 61 | hutool-all 62 | 63 | 64 | 65 | com.alibaba 66 | fastjson 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /mini-spring-cloud-openfeign/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | mini-spring-cloud 7 | com.github 8 | 1.0.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | mini-spring-cloud-openfeign 13 | 14 | 15 | 16 | com.github 17 | mini-spring-cloud-load-balancer 18 | 19 | 20 | 21 | io.github.openfeign 22 | feign-core 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-configuration-processor 28 | true 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot 34 | true 35 | 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-autoconfigure 40 | true 41 | 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-starter 46 | true 47 | 48 | 49 | 50 | org.springframework.cloud 51 | spring-cloud-commons 52 | 53 | 54 | 55 | org.springframework.cloud 56 | spring-cloud-context 57 | 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-starter-web 62 | 63 | 64 | 65 | cn.hutool 66 | hutool-all 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /mini-spring-cloud-tutu-discovery/src/main/java/com/github/cloud/tutu/discovery/TutuDiscoveryClient.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.tutu.discovery; 2 | 3 | import cn.hutool.http.HttpUtil; 4 | import com.alibaba.fastjson.JSON; 5 | import com.alibaba.fastjson.JSONObject; 6 | import com.github.cloud.tutu.TutuDiscoveryProperties; 7 | import com.github.cloud.tutu.TutuServiceInstance; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.cloud.client.ServiceInstance; 11 | import org.springframework.cloud.client.discovery.DiscoveryClient; 12 | 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.stream.Collectors; 17 | 18 | /** 19 | * 服务发现实现类 20 | * 21 | * @author derek(易仁川) 22 | * @date 2022/3/20 23 | */ 24 | public class TutuDiscoveryClient implements DiscoveryClient { 25 | private static final Logger logger = LoggerFactory.getLogger(TutuDiscoveryClient.class); 26 | 27 | private TutuDiscoveryProperties tutuDiscoveryProperties; 28 | 29 | public TutuDiscoveryClient(TutuDiscoveryProperties tutuDiscoveryProperties) { 30 | this.tutuDiscoveryProperties = tutuDiscoveryProperties; 31 | } 32 | 33 | @Override 34 | public List getInstances(String serviceId) { 35 | Map param = new HashMap<>(); 36 | param.put("serviceName", serviceId); 37 | 38 | String response = HttpUtil.get(tutuDiscoveryProperties.getServerAddr() + "/list", param); 39 | logger.info("query service instance, serviceId: {}, response: {}", serviceId, response); 40 | return JSON.parseArray(response).stream().map(hostInfo -> { 41 | TutuServiceInstance serviceInstance = new TutuServiceInstance(); 42 | serviceInstance.setServiceId(serviceId); 43 | String ip = ((JSONObject) hostInfo).getString("ip"); 44 | Integer port = ((JSONObject) hostInfo).getInteger("port"); 45 | serviceInstance.setHost(ip); 46 | serviceInstance.setPort(port); 47 | return serviceInstance; 48 | }).collect(Collectors.toList()); 49 | } 50 | 51 | @Override 52 | public List getServices() { 53 | String response = HttpUtil.post(tutuDiscoveryProperties.getServerAddr() + "/listServiceNames", new HashMap<>()); 54 | logger.info("query service instance list, response: {}", response); 55 | return JSON.parseArray(response, String.class); 56 | } 57 | 58 | @Override 59 | public String description() { 60 | return "Spring Cloud Tutu Discovery Client"; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /mini-spring-cloud-netflix-zuul/src/main/java/com/github/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.netflix.zuul.filters.route; 2 | 3 | import cn.hutool.http.HttpRequest; 4 | import cn.hutool.http.HttpResponse; 5 | import cn.hutool.http.HttpUtil; 6 | import cn.hutool.http.Method; 7 | import com.netflix.zuul.ZuulFilter; 8 | import com.netflix.zuul.context.RequestContext; 9 | import com.netflix.zuul.exception.ZuulException; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.cloud.client.ServiceInstance; 13 | import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; 14 | 15 | import static com.github.cloud.netflix.zuul.filters.support.FilterConstants.*; 16 | import static org.springframework.util.ReflectionUtils.rethrowRuntimeException; 17 | 18 | /** 19 | * route类型过滤器,使用ribbon负载均衡器进行http请求 20 | * 21 | * @author derek(易仁川) 22 | * @date 2022/6/27 23 | */ 24 | public class RibbonRoutingFilter extends ZuulFilter { 25 | private static Logger logger = LoggerFactory.getLogger(RibbonRoutingFilter.class); 26 | 27 | private LoadBalancerClient loadBalancerClient; 28 | 29 | public RibbonRoutingFilter(LoadBalancerClient loadBalancerClient) { 30 | this.loadBalancerClient = loadBalancerClient; 31 | } 32 | 33 | @Override 34 | public String filterType() { 35 | return ROUTE_TYPE; 36 | } 37 | 38 | @Override 39 | public int filterOrder() { 40 | return 10; 41 | } 42 | 43 | @Override 44 | public boolean shouldFilter() { 45 | RequestContext requestContext = RequestContext.getCurrentContext(); 46 | return requestContext.get(SERVICE_ID_KEY) != null; 47 | } 48 | 49 | @Override 50 | public Object run() throws ZuulException { 51 | try { 52 | RequestContext requestContext = RequestContext.getCurrentContext(); 53 | //使用ribbon的负载均衡能力发起远程调用 54 | //TODO 简单实现,熔断降级章节再完善 55 | String serviceId = (String) requestContext.get(SERVICE_ID_KEY); 56 | ServiceInstance serviceInstance = loadBalancerClient.choose(serviceId); 57 | if (serviceInstance == null) { 58 | logger.error("根据serviceId查询不到服务示例,serviceId: {}", serviceId); 59 | return null; 60 | } 61 | 62 | String requestURI = (String) requestContext.get(REQUEST_URI_KEY); 63 | String url = serviceInstance.getUri().toString() + requestURI; 64 | HttpRequest httpRequest = HttpUtil.createRequest(Method.POST, url); 65 | HttpResponse httpResponse = httpRequest.execute(); 66 | 67 | //将响应报文的状态码和内容写进请求上下文中 68 | requestContext.setResponseStatusCode(httpResponse.getStatus()); 69 | requestContext.setResponseDataStream(httpResponse.bodyStream()); 70 | 71 | return httpResponse; 72 | } catch (Exception e) { 73 | rethrowRuntimeException(e); 74 | } 75 | return null; 76 | } 77 | } -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | # mini-spring-cloud 2 | [![License](https://img.shields.io/badge/license-license-blue)](https://github.com/DerekYRC/mini-spring-cloud) 3 | [![Build Status](https://img.shields.io/badge/build-passing-brightgreen)](https://github.com/DerekYRC/mini-spring-cloud) 4 | [![Stars](https://img.shields.io/github/stars/DerekYRC/mini-spring-cloud)](https://img.shields.io/github/stars/DerekYRC/mini-spring-cloud) 5 | [![Forks](https://img.shields.io/github/forks/DerekYRC/mini-spring-cloud)](https://img.shields.io/github/forks/DerekYRC/mini-spring-cloud) 6 | 7 | **English | [简体中文](./README.md)** 8 | 9 | **Sister Projects:** 10 | - [**mini-spring**](https://github.com/DerekYRC/mini-spring) **(simplified Spring framework)** 11 | - [**mini-netty**](https://github.com/DerekYRC/mini-netty) **(simplified Netty framework)** 12 | 13 | ## About 14 | **mini-spring-cloud** is a simplified version of the Spring Cloud framework that helps you quickly familiarize yourself with Spring Cloud source code and master its core principles. While preserving Spring Cloud's core functionality, the code is streamlined as much as possible. Core features include **service registration**, **service discovery**, **load balancing**, **Feign integration for simplified calls**, **traffic control**, **circuit breaker pattern**, **API gateway**, and more. 15 | 16 | Hope this project helps you, please give it a **STAR, thank you!!!** 17 | 18 | ## Features 19 | * [Service Registration](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md#服务注册) 20 | * [Service Discovery](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md#服务发现) 21 | * [Load Balancing](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md#集成ribbon实现客户端负载均衡) 22 | * [Feign Integration for Simplified Calls](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md#集成Feign简化调用方式) 23 | * [API Gateway](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md#API网关) 24 | * [Traffic Control](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md#流量控制和熔断降级) 25 | * [Circuit Breaker](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md#流量控制和熔断降级) 26 | 27 | ## Usage 28 | Read [changelog.md](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog_en.md) 29 | 30 | ## FAQ 31 | [**Ask Questions Here**](https://github.com/DerekYRC/mini-spring-cloud/issues/1) 32 | 33 | ## Contributing 34 | Pull Requests are welcome 35 | 36 | ## About Me 37 | [**Learn More**](https://github.com/DerekYRC) 38 | 39 | Phone/WeChat: **15975984828** Email: **15975984828@163.com** 40 | 41 | ## Star History 42 | 43 | [![Star History Chart](https://api.star-history.com/svg?repos=DerekYRC/mini-spring-cloud&type=Date)](https://star-history.com/#DerekYRC/mini-spring-cloud&Date) 44 | 45 | ## Copyright Notice 46 | This project may not be used for commercial purposes without my written permission. 47 | 48 | ## References 49 | - [《精尽Spring Cloud学习指南》](http://svip.iocoder.cn/Spring-Cloud/tutorials/) 50 | -------------------------------------------------------------------------------- /mini-spring-cloud-load-balancer/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | mini-spring-cloud 7 | com.github 8 | 1.0.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | mini-spring-cloud-load-balancer 13 | 14 | 15 | 16 | com.github 17 | mini-spring-cloud-tutu-discovery 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-configuration-processor 23 | true 24 | 25 | 26 | 27 | org.springframework.boot 28 | spring-boot 29 | true 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-autoconfigure 35 | true 36 | 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter 41 | true 42 | 43 | 44 | 45 | org.springframework.cloud 46 | spring-cloud-commons 47 | 48 | 49 | 50 | org.springframework.cloud 51 | spring-cloud-context 52 | 53 | 54 | 55 | org.springframework.boot 56 | spring-boot-starter-web 57 | 58 | 59 | 60 | cn.hutool 61 | hutool-all 62 | 63 | 64 | 65 | com.alibaba 66 | fastjson 67 | 68 | 69 | 70 | com.netflix.ribbon 71 | ribbon 72 | 73 | 74 | 75 | com.netflix.ribbon 76 | ribbon-loadbalancer 77 | 78 | 79 | 80 | com.netflix.ribbon 81 | ribbon-core 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/config/RibbonClientConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.loadbalancer.ribbon.config; 2 | 3 | import com.netflix.client.config.DefaultClientConfigImpl; 4 | import com.netflix.loadbalancer.*; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import com.netflix.client.config.IClientConfig; 10 | 11 | /** 12 | * 配置ribbon默认组件 13 | * 14 | * @author derek(易仁川) 15 | * @date 2022/3/22 16 | */ 17 | @Configuration 18 | public class RibbonClientConfiguration { 19 | 20 | @Value("${ribbon.client.name}") 21 | private String name; 22 | 23 | @Bean 24 | @ConditionalOnMissingBean 25 | public IClientConfig ribbonClientConfig() { 26 | DefaultClientConfigImpl config = new DefaultClientConfigImpl(); 27 | config.loadProperties(name); 28 | return config; 29 | } 30 | 31 | @Bean 32 | @ConditionalOnMissingBean 33 | public IRule ribbonRule(IClientConfig config) { 34 | ZoneAvoidanceRule rule = new ZoneAvoidanceRule(); 35 | rule.initWithNiwsConfig(config); 36 | return rule; 37 | } 38 | 39 | @Bean 40 | @ConditionalOnMissingBean 41 | public IPing ribbonPing(IClientConfig config) { 42 | return new DummyPing(); 43 | } 44 | 45 | @Bean 46 | @ConditionalOnMissingBean 47 | public ServerList ribbonServerList(IClientConfig config) { 48 | ConfigurationBasedServerList serverList = new ConfigurationBasedServerList(); 49 | serverList.initWithNiwsConfig(config); 50 | return serverList; 51 | } 52 | 53 | @Bean 54 | @ConditionalOnMissingBean 55 | public ServerListUpdater ribbonServerListUpdater(IClientConfig config) { 56 | return new PollingServerListUpdater(config); 57 | } 58 | 59 | @Bean 60 | @ConditionalOnMissingBean 61 | public ServerListFilter ribbonServerListFilter(IClientConfig config) { 62 | ServerListSubsetFilter filter = new ServerListSubsetFilter(); 63 | filter.initWithNiwsConfig(config); 64 | return filter; 65 | } 66 | 67 | @Bean 68 | @ConditionalOnMissingBean 69 | public ILoadBalancer ribbonLoadBalancer(IClientConfig config, 70 | ServerList serverList, ServerListFilter serverListFilter, 71 | IRule rule, IPing ping, ServerListUpdater serverListUpdater) { 72 | return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList, 73 | serverListFilter, serverListUpdater); 74 | } 75 | } 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /mini-spring-cloud-netflix-zuul/src/main/java/com/github/cloud/netflix/zuul/ZuulServerAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.netflix.zuul; 2 | 3 | import java.util.Map; 4 | 5 | import com.github.cloud.netflix.zuul.filters.RouteLocator; 6 | import com.github.cloud.netflix.zuul.filters.SimpleRouteLocator; 7 | import com.github.cloud.netflix.zuul.filters.ZuulProperties; 8 | import com.github.cloud.netflix.zuul.filters.post.SendResponseFilter; 9 | import com.github.cloud.netflix.zuul.filters.pre.PreDecorationFilter; 10 | import com.github.cloud.netflix.zuul.filters.route.RibbonRoutingFilter; 11 | import com.github.cloud.netflix.zuul.metrics.EmptyCounterFactory; 12 | import com.github.cloud.netflix.zuul.metrics.EmptyTracerFactory; 13 | import com.netflix.zuul.ZuulFilter; 14 | import com.netflix.zuul.filters.FilterRegistry; 15 | import com.netflix.zuul.http.ZuulServlet; 16 | import com.netflix.zuul.monitoring.CounterFactory; 17 | import com.netflix.zuul.monitoring.TracerFactory; 18 | 19 | import org.springframework.beans.factory.annotation.Autowired; 20 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 21 | import org.springframework.boot.web.servlet.ServletRegistrationBean; 22 | import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; 23 | import org.springframework.context.annotation.Bean; 24 | import org.springframework.context.annotation.Configuration; 25 | 26 | /** 27 | * zuul API网关自动配置类 28 | * 29 | * @author derek(易仁川) 30 | * @date 2022/6/23 31 | */ 32 | @Configuration 33 | @EnableConfigurationProperties({ZuulProperties.class}) 34 | public class ZuulServerAutoConfiguration { 35 | 36 | @Autowired 37 | protected ZuulProperties zuulProperties; 38 | 39 | /** 40 | * 注册ZuulServlet,用于拦截处理http请求 41 | */ 42 | @Bean 43 | public ServletRegistrationBean zuulServlet() { 44 | return new ServletRegistrationBean<>(new ZuulServlet(), zuulProperties.getServletPath()); 45 | } 46 | 47 | /** 48 | * 路由定位器 49 | */ 50 | @Bean 51 | public RouteLocator simpleRouteLocator() { 52 | return new SimpleRouteLocator(zuulProperties); 53 | } 54 | 55 | /** 56 | * pre类型过滤器,根据RouteLocator来进行路由规则的匹配 57 | */ 58 | @Bean 59 | public ZuulFilter preDecorationFilter(RouteLocator routeLocator) { 60 | return new PreDecorationFilter(routeLocator); 61 | } 62 | 63 | /** 64 | * route类型过滤器,使用ribbon负载均衡器进行http请求 65 | */ 66 | @Bean 67 | ZuulFilter ribbonRoutingFilter(LoadBalancerClient loadBalancerClient) { 68 | return new RibbonRoutingFilter(loadBalancerClient); 69 | } 70 | 71 | /** 72 | * post类型过滤器,向客户端输出响应报文 73 | */ 74 | @Bean 75 | ZuulFilter sendResponseFilter() { 76 | return new SendResponseFilter(); 77 | } 78 | 79 | /** 80 | * 注册过滤器 81 | */ 82 | @Bean 83 | public FilterRegistry filterRegistry(Map filterMap) { 84 | FilterRegistry filterRegistry = FilterRegistry.instance(); 85 | filterMap.forEach((name, filter) -> { 86 | filterRegistry.put(name, filter); 87 | }); 88 | return filterRegistry; 89 | } 90 | 91 | //监控相关类,不必关注------------------------------- 92 | 93 | @Bean 94 | public CounterFactory emptyCounterFactory() { 95 | CounterFactory.initialize(new EmptyCounterFactory()); 96 | return CounterFactory.instance(); 97 | } 98 | 99 | @Bean 100 | public TracerFactory emptyTracerFactory() { 101 | TracerFactory.initialize(new EmptyTracerFactory()); 102 | return TracerFactory.instance(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /mini-spring-cloud-tutu-discovery/src/main/java/com/github/cloud/tutu/registry/TutuServiceRegistry.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.tutu.registry; 2 | 3 | import cn.hutool.http.HttpUtil; 4 | import com.github.cloud.tutu.TutuDiscoveryProperties; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.cloud.client.serviceregistry.Registration; 8 | import org.springframework.cloud.client.serviceregistry.ServiceRegistry; 9 | 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | 13 | /** 14 | * 具体的服务注册实现类 15 | * 16 | * @author derek(易仁川) 17 | * @date 2022/3/19 18 | */ 19 | public class TutuServiceRegistry implements ServiceRegistry { 20 | private static final Logger logger = LoggerFactory.getLogger(TutuServiceRegistry.class); 21 | 22 | private TutuDiscoveryProperties tutuDiscoveryProperties; 23 | 24 | public TutuServiceRegistry(TutuDiscoveryProperties tutuDiscoveryProperties) { 25 | this.tutuDiscoveryProperties = tutuDiscoveryProperties; 26 | } 27 | 28 | /** 29 | * 注册服务实例 30 | * 31 | * @param registration 32 | */ 33 | @Override 34 | public void register(Registration registration) { 35 | Map param = new HashMap<>(); 36 | param.put("serviceName", tutuDiscoveryProperties.getService()); 37 | param.put("ip", tutuDiscoveryProperties.getIp()); 38 | param.put("port", tutuDiscoveryProperties.getPort()); 39 | 40 | String result = HttpUtil.post(tutuDiscoveryProperties.getServerAddr() + "/register", param); 41 | if (Boolean.parseBoolean(result)) { 42 | logger.info("register service successfully, serviceName: {}, ip: {}, port: {}", 43 | tutuDiscoveryProperties.getService(), tutuDiscoveryProperties.getIp(), tutuDiscoveryProperties.getPort()); 44 | } else { 45 | logger.error("register service failed, serviceName: {}, ip: {}, port: {}", 46 | tutuDiscoveryProperties.getService(), tutuDiscoveryProperties.getIp(), tutuDiscoveryProperties.getPort()); 47 | throw new RuntimeException("register service failed, serviceName"); 48 | } 49 | } 50 | 51 | /** 52 | * 注销服务实例 53 | * 54 | * @param registration 55 | */ 56 | @Override 57 | public void deregister(Registration registration) { 58 | Map param = new HashMap<>(); 59 | param.put("serviceName", tutuDiscoveryProperties.getService()); 60 | param.put("ip", tutuDiscoveryProperties.getIp()); 61 | param.put("port", tutuDiscoveryProperties.getPort()); 62 | 63 | String result = HttpUtil.post(tutuDiscoveryProperties.getServerAddr() + "/deregister", param); 64 | if (Boolean.parseBoolean(result)) { 65 | logger.info("de-register service successfully, serviceName: {}, ip: {}, port: {}", 66 | tutuDiscoveryProperties.getService(), tutuDiscoveryProperties.getIp(), tutuDiscoveryProperties.getPort()); 67 | } else { 68 | logger.warn("de-register service failed, serviceName: {}, ip: {}, port: {}", 69 | tutuDiscoveryProperties.getService(), tutuDiscoveryProperties.getIp(), tutuDiscoveryProperties.getPort()); 70 | } 71 | } 72 | 73 | @Override 74 | public void close() { 75 | 76 | } 77 | 78 | @Override 79 | public void setStatus(Registration registration, String status) { 80 | throw new UnsupportedOperationException(); 81 | } 82 | 83 | @Override 84 | public T getStatus(Registration registration) { 85 | return null; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /mini-spring-cloud-examples/mini-spring-cloud-consumer-examples/src/main/java/com/github/cloud/examples/ConsumerApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.examples; 2 | 3 | import com.github.cloud.openfeign.EnableFeignClients; 4 | import com.github.cloud.tutu.discovery.TutuDiscoveryClient; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.cloud.client.ServiceInstance; 9 | import org.springframework.cloud.client.loadbalancer.LoadBalanced; 10 | import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.web.bind.annotation.GetMapping; 14 | import org.springframework.web.bind.annotation.RestController; 15 | import org.springframework.web.client.RestTemplate; 16 | 17 | import java.net.URI; 18 | import java.util.List; 19 | 20 | /** 21 | * @author derek(易仁川) 22 | * @date 2022/3/20 23 | */ 24 | @EnableFeignClients 25 | @SpringBootApplication 26 | public class ConsumerApplication { 27 | 28 | public static void main(String[] args) { 29 | SpringApplication.run(ConsumerApplication.class, args); 30 | } 31 | 32 | @Configuration 33 | static class RestTemplateConfiguration { 34 | 35 | /** 36 | * 赋予负载均衡的能力 37 | * 38 | * @return 39 | */ 40 | @LoadBalanced 41 | @Bean 42 | public RestTemplate restTemplate() { 43 | return new RestTemplate(); 44 | } 45 | } 46 | 47 | @RestController 48 | static class HelloController { 49 | 50 | @Autowired 51 | private TutuDiscoveryClient discoveryClient; 52 | 53 | @Autowired 54 | private LoadBalancerClient loadBalancerClient; 55 | 56 | @Autowired 57 | private RestTemplate loadBalancedRestTemplate; 58 | 59 | @Autowired 60 | private EchoService echoService; 61 | 62 | private RestTemplate restTemplate = new RestTemplate(); 63 | 64 | @GetMapping("/hello") 65 | public String hello() { 66 | List serviceInstances = discoveryClient.getInstances("provider-application"); 67 | if (serviceInstances.size() > 0) { 68 | ServiceInstance serviceInstance = serviceInstances.get(0); 69 | URI uri = serviceInstance.getUri(); 70 | String response = restTemplate.postForObject(uri.toString() + "/echo", null, String.class); 71 | return response; 72 | } 73 | 74 | throw new RuntimeException("No service instance for provider-application found"); 75 | } 76 | 77 | @GetMapping("/world") 78 | public String world() { 79 | ServiceInstance serviceInstance = loadBalancerClient.choose("provider-application"); 80 | if (serviceInstance != null) { 81 | URI uri = serviceInstance.getUri(); 82 | String response = restTemplate.postForObject(uri.toString() + "/echo", null, String.class); 83 | return response; 84 | } 85 | 86 | throw new RuntimeException("No service instance for provider-application found"); 87 | } 88 | 89 | @GetMapping("/foo") 90 | public String foo() { 91 | return loadBalancedRestTemplate.postForObject("http://provider-application/echo", null, String.class); 92 | } 93 | 94 | @GetMapping("/bar") 95 | public String bar() { 96 | return echoService.echo(); 97 | } 98 | } 99 | } 100 | 101 | -------------------------------------------------------------------------------- /mini-spring-cloud-load-balancer/src/main/java/com/github/cloud/loadbalancer/ribbon/RibbonLoadBalancerClient.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.loadbalancer.ribbon; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import com.github.cloud.tutu.TutuServiceInstance; 5 | import com.netflix.loadbalancer.ILoadBalancer; 6 | import com.netflix.loadbalancer.Server; 7 | import org.springframework.cloud.client.ServiceInstance; 8 | import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; 9 | import org.springframework.cloud.client.loadbalancer.LoadBalancerRequest; 10 | import org.springframework.cloud.client.loadbalancer.Request; 11 | 12 | import java.io.IOException; 13 | import java.net.URI; 14 | import java.net.URISyntaxException; 15 | 16 | /** 17 | * ribbon负载均衡客户端 18 | * 19 | * @author derek(易仁川) 20 | * @date 2022/3/22 21 | */ 22 | public class RibbonLoadBalancerClient implements LoadBalancerClient { 23 | 24 | private SpringClientFactory clientFactory; 25 | 26 | public RibbonLoadBalancerClient(SpringClientFactory clientFactory) { 27 | this.clientFactory = clientFactory; 28 | } 29 | 30 | /** 31 | * 选择服务实例 32 | * 33 | * @param serviceId 34 | * @return 35 | */ 36 | @Override 37 | public ServiceInstance choose(String serviceId) { 38 | return choose(serviceId, null); 39 | } 40 | 41 | /** 42 | * 选择服务实例 43 | * 44 | * @param serviceId 45 | * @param request 46 | * @param 47 | * @return 48 | */ 49 | @Override 50 | public ServiceInstance choose(String serviceId, Request request) { 51 | ILoadBalancer loadBalancer = clientFactory.getInstance(serviceId, ILoadBalancer.class); 52 | Server server = loadBalancer.chooseServer("default"); 53 | if (server != null) { 54 | return new TutuServiceInstance(serviceId, server.getHost(), server.getPort()); 55 | } 56 | 57 | return null; 58 | } 59 | 60 | /** 61 | * 重建请求URI,将服务名称替换为服务实例的IP:端口 62 | * 63 | * @param server 64 | * @param original 65 | * @return 66 | */ 67 | @Override 68 | public URI reconstructURI(ServiceInstance server, URI original) { 69 | try { 70 | //将服务名称替换为服务实例的IP:端口,例如http://provider-application/echo被重建为http://192.168.100.1:8888/echo 71 | StringBuilder sb = new StringBuilder(); 72 | sb.append(original.getScheme()).append("://"); 73 | sb.append(server.getHost()); 74 | sb.append(":").append(server.getPort()); 75 | sb.append(original.getRawPath()); 76 | if (StrUtil.isNotEmpty(original.getRawQuery())) { 77 | sb.append("?").append(original.getRawQuery()); 78 | } 79 | URI newURI = new URI(sb.toString()); 80 | return newURI; 81 | } catch (URISyntaxException e) { 82 | throw new RuntimeException(e); 83 | } 84 | } 85 | 86 | /** 87 | * 处理http请求 88 | * 89 | * @param serviceId 90 | * @param request 91 | * @param 92 | * @return 93 | * @throws IOException 94 | */ 95 | @Override 96 | public T execute(String serviceId, LoadBalancerRequest request) throws IOException { 97 | ServiceInstance serviceInstance = choose(serviceId); 98 | return execute(serviceId, serviceInstance, request); 99 | } 100 | 101 | /** 102 | * 处理http请求 103 | * 104 | * @param serviceId 105 | * @param serviceInstance 106 | * @param request 107 | * @param 108 | * @return 109 | * @throws IOException 110 | */ 111 | @Override 112 | public T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest request) throws IOException { 113 | try { 114 | return request.apply(serviceInstance); 115 | } catch (Exception ex) { 116 | throw new RuntimeException(ex); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /mini-spring-cloud-examples/tutu-server/src/main/java/com/github/cloud/examples/TutuServerApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.cloud.examples; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.PostMapping; 10 | import org.springframework.web.bind.annotation.RequestParam; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | import java.util.Collections; 14 | import java.util.Enumeration; 15 | import java.util.HashSet; 16 | import java.util.Set; 17 | import java.util.concurrent.ConcurrentHashMap; 18 | 19 | /** 20 | * @author derek(易仁川) 21 | * @date 2022/3/19 22 | */ 23 | 24 | @RestController 25 | @SpringBootApplication 26 | public class TutuServerApplication { 27 | private static Logger logger = LoggerFactory.getLogger(TutuServerApplication.class); 28 | 29 | private ConcurrentHashMap> serverMap = new ConcurrentHashMap<>(); 30 | 31 | public static void main(String[] args) { 32 | SpringApplication.run(TutuServerApplication.class, args); 33 | } 34 | 35 | /** 36 | * 服务注册 37 | * 38 | * @param serviceName 39 | * @param ip 40 | * @param port 41 | * @return 42 | */ 43 | @PostMapping("register") 44 | public boolean register(@RequestParam("serviceName") String serviceName, @RequestParam("ip") String ip, @RequestParam("port") Integer port) { 45 | logger.info("register service, serviceName: {}, ip: {}, port: {}", serviceName, ip, port); 46 | serverMap.putIfAbsent(serviceName.toLowerCase(), Collections.synchronizedSet(new HashSet<>())); 47 | Server server = new Server(ip, port); 48 | serverMap.get(serviceName).add(server); 49 | return true; 50 | } 51 | 52 | /** 53 | * 服务注销 54 | * 55 | * @param serviceName 56 | * @param ip 57 | * @param port 58 | * @return 59 | */ 60 | @PostMapping("deregister") 61 | public boolean deregister(@RequestParam("serviceName") String serviceName, @RequestParam("ip") String ip, @RequestParam("port") Integer port) { 62 | logger.info("deregister service, serviceName: {}, ip: {}, port: {}", serviceName, ip, port); 63 | Set serverSet = serverMap.get(serviceName.toLowerCase()); 64 | if (serverSet != null) { 65 | Server server = new Server(ip, port); 66 | serverSet.remove(server); 67 | } 68 | return true; 69 | } 70 | 71 | /** 72 | * 根据服务名称查询服务列表 73 | * 74 | * @param serviceName 75 | * @return 76 | */ 77 | @GetMapping("list") 78 | public Set list(@RequestParam("serviceName") String serviceName) { 79 | Set serverSet = serverMap.get(serviceName.toLowerCase()); 80 | logger.info("list service, serviceName: {}, serverSet: {}", serviceName, JSON.toJSONString(serverSet)); 81 | return serverSet != null ? serverSet : Collections.emptySet(); 82 | } 83 | 84 | /** 85 | * 查询所有服务名称列表 86 | * 87 | * @return 88 | */ 89 | @GetMapping("listServiceNames") 90 | public Enumeration listServiceNames() { 91 | return serverMap.keys(); 92 | } 93 | 94 | /** 95 | * 服务 96 | */ 97 | public static class Server { 98 | private String ip; 99 | 100 | private Integer port; 101 | 102 | public Server(String ip, Integer port) { 103 | this.ip = ip; 104 | this.port = port; 105 | } 106 | 107 | public String getIp() { 108 | return ip; 109 | } 110 | 111 | public Integer getPort() { 112 | return port; 113 | } 114 | 115 | @Override 116 | public boolean equals(Object o) { 117 | if (this == o) return true; 118 | if (o == null || getClass() != o.getClass()) return false; 119 | 120 | Server server = (Server) o; 121 | 122 | if (!ip.equals(server.ip)) return false; 123 | return port.equals(server.port); 124 | } 125 | 126 | @Override 127 | public int hashCode() { 128 | int result = ip.hashCode(); 129 | result = 31 * result + port.hashCode(); 130 | return result; 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.cloud 9 | spring-cloud-build 10 | 3.1.1 11 | 12 | 13 | 14 | com.github 15 | mini-spring-cloud 16 | pom 17 | 1.0.0-SNAPSHOT 18 | 19 | 20 | mini-spring-cloud-tutu-discovery 21 | mini-spring-cloud-load-balancer 22 | mini-spring-cloud-openfeign 23 | mini-spring-cloud-netflix-zuul 24 | mini-spring-cloud-examples/tutu-server 25 | mini-spring-cloud-examples/mini-spring-cloud-provider-example 26 | mini-spring-cloud-examples/mini-spring-cloud-consumer-examples 27 | mini-spring-cloud-examples/mini-spring-cloud-api-gateway-example 28 | 29 | 30 | 31 | 2021.0.1 32 | 1.2.79 33 | 5.7.21 34 | 2.3.0 35 | 11.8 36 | 1.3.1 37 | 38 | 39 | 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-dependencies 44 | ${spring-boot.version} 45 | pom 46 | import 47 | 48 | 49 | 50 | org.springframework.cloud 51 | spring-cloud-dependencies 52 | ${spring.cloud.version} 53 | pom 54 | import 55 | 56 | 57 | 58 | com.github 59 | mini-spring-cloud-tutu-discovery 60 | 1.0.0-SNAPSHOT 61 | 62 | 63 | 64 | com.github 65 | mini-spring-cloud-load-balancer 66 | 1.0.0-SNAPSHOT 67 | 68 | 69 | 70 | com.github 71 | mini-spring-cloud-openfeign 72 | 1.0.0-SNAPSHOT 73 | 74 | 75 | 76 | com.github 77 | mini-spring-cloud-netflix-zuul 78 | 1.0.0-SNAPSHOT 79 | 80 | 81 | 82 | com.alibaba 83 | fastjson 84 | ${fastjson.version} 85 | 86 | 87 | 88 | cn.hutool 89 | hutool-all 90 | ${hutool.version} 91 | 92 | 93 | 94 | com.netflix.ribbon 95 | ribbon 96 | ${ribbon.version} 97 | 98 | 99 | 100 | com.netflix.ribbon 101 | ribbon-loadbalancer 102 | ${ribbon.version} 103 | 104 | 105 | 106 | com.netflix.ribbon 107 | ribbon-core 108 | ${ribbon.version} 109 | 110 | 111 | 112 | io.github.openfeign 113 | feign-core 114 | ${feign.version} 115 | 116 | 117 | 118 | com.netflix.zuul 119 | zuul-core 120 | ${zuul.version} 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /changelog_en.md: -------------------------------------------------------------------------------- 1 | # [Preface](#preface) 2 | 3 | One of the purposes of writing this project is to reduce the difficulty of reading the original Spring Cloud source code. After mastering the content explained in this project, reading the original Spring Cloud source code should be much more effective. Therefore, this project's functional implementation logic and principles are consistent with the official version but pursue maximum code simplification. **This project can be understood as a source code guide project**. 4 | 5 | Prerequisites. Reading the source code of Spring, Spring Boot, and Spring Cloud must strictly follow the order: Spring -> Spring Boot -> Spring Cloud. Essential prerequisites for reading Spring Cloud source code: 6 | 7 | - Spring: Recommended is the simplified Spring framework [**mini-spring**](https://github.com/DerekYRC/mini-spring/blob/main/README.md) written by myself. Being familiar with Spring source code makes reading Spring Boot source code very easy. 8 | - Spring Boot: Focus on: 1. Startup process 2. **Auto-configuration principles! Auto-configuration principles!! Auto-configuration principles!!!** Recommended articles: 9 | - [《Spring Boot Essence: Startup Process Source Code Analysis》](https://www.cnblogs.com/java-chen-hao/p/11829344.html) 10 | - [《Spring Boot Auto-Configuration Principles, This Article is Enough!》](https://mp.weixin.qq.com/s/f6oED1hbiWat_0HOwxgfnA) 11 | - Spring Cloud: Learn to use it first, then study the source code. Don't put the cart before the horse. Recommended [《Comprehensive Spring Cloud Learning Guide》](http://svip.iocoder.cn/Spring-Cloud/tutorials/). 12 | 13 | About Spring Cloud. Spring Cloud is a toolkit for building common patterns in distributed systems. Through [**spring-cloud-commons**](https://github.com/spring-cloud/spring-cloud-commons), it defines unified abstract APIs, equivalent to defining a protocol standard. Specific implementations must conform to this protocol standard. Spring Cloud officially integrates third-party components like Eureka, Ribbon, and Hystrix to develop Spring Cloud Netflix. Alibaba combined its own Nacos, Sentinel, and other components to develop Spring Cloud Alibaba. This project develops independently or integrates third-party components based on the spring-cloud-commons protocol standard to provide specific implementations. 14 | 15 | Due to limited technical ability and poor writing skills, everyone can leave comments, ask questions, and make suggestions in this [**issue**](https://github.com/DerekYRC/mini-spring-cloud/issues/1). Pull Requests to improve this project are also welcome. 16 | 17 | # [Service Registration](#service-registration) 18 | > Code branch: service-registry 19 | 20 | For demonstration purposes, a very simple single-machine service registration and discovery center named Tutu is implemented: 21 | 22 | ```java 23 | @RestController 24 | @SpringBootApplication 25 | public class TutuServerApplication { 26 | private static Logger logger = LoggerFactory.getLogger(TutuServerApplication.class); 27 | 28 | private ConcurrentHashMap> serverMap = new ConcurrentHashMap<>(); 29 | 30 | public static void main(String[] args) { 31 | SpringApplication.run(TutuServerApplication.class, args); 32 | } 33 | 34 | /** 35 | * Service registration 36 | */ 37 | @PostMapping("register") 38 | public boolean register(@RequestParam("serviceName") String serviceName, 39 | @RequestParam("ip") String ip, 40 | @RequestParam("port") Integer port) { 41 | logger.info("register service, serviceName: {}, ip: {}, port: {}", serviceName, ip, port); 42 | serverMap.putIfAbsent(serviceName.toLowerCase(), Collections.synchronizedSet(new HashSet<>())); 43 | Server server = new Server(ip, port); 44 | serverMap.get(serviceName).add(server); 45 | return true; 46 | } 47 | 48 | /** 49 | * Service deregistration 50 | */ 51 | @PostMapping("deregister") 52 | public boolean deregister(@RequestParam("serviceName") String serviceName, 53 | @RequestParam("ip") String ip, 54 | @RequestParam("port") Integer port) { 55 | logger.info("deregister service, serviceName: {}, ip: {}, port: {}", serviceName, ip, port); 56 | Set serverSet = serverMap.get(serviceName.toLowerCase()); 57 | if (serverSet != null) { 58 | Server server = new Server(ip, port); 59 | serverSet.remove(server); 60 | } 61 | return true; 62 | } 63 | 64 | /** 65 | * Query service list by service name 66 | */ 67 | @GetMapping("list") 68 | public Set list(@RequestParam("serviceName") String serviceName) { 69 | Set serverSet = serverMap.get(serviceName.toLowerCase()); 70 | logger.info("list service, serviceName: {}, serverSet: {}", serviceName, JSON.toJSONString(serverSet)); 71 | return serverSet != null ? serverSet : Collections.emptySet(); 72 | } 73 | } 74 | ``` 75 | 76 | # [Service Discovery](#service-discovery) 77 | > Code branch: service-discovery 78 | 79 | Implement service discovery capabilities by creating a DiscoveryClient that can retrieve service instances from the Tutu registry. 80 | 81 | Key components: 82 | - ServiceInstance: Represents a service instance with IP and port 83 | - DiscoveryClient: Interface for service discovery operations 84 | - TutuDiscoveryClient: Implementation that communicates with Tutu registry 85 | 86 | # [Client-Side Load Balancing with Ribbon Integration](#client-side-load-balancing) 87 | > Code branch: loadbalancer-ribbon 88 | 89 | Integrate Ribbon for client-side load balancing: 90 | - LoadBalancerClient: Interface for load balancing operations 91 | - RibbonLoadBalancerClient: Implementation using Ribbon algorithms 92 | - Support for multiple load balancing strategies (Round Robin, Random, etc.) 93 | 94 | # [Simplifying Calls with Feign Integration](#feign-integration) 95 | > Code branch: openfeign-integration 96 | 97 | Integrate OpenFeign to simplify service-to-service calls: 98 | - Declarative HTTP client using annotations 99 | - Automatic service discovery integration 100 | - Request/response serialization 101 | - Error handling and fallback mechanisms 102 | 103 | # [API Gateway](#api-gateway) 104 | > Code branch: api-gateway 105 | 106 | Implement a simple API Gateway using Zuul: 107 | - Route configuration and management 108 | - Request routing based on service names 109 | - Load balancing for backend services 110 | - Request/response filtering capabilities 111 | 112 | # [Traffic Control and Circuit Breaker](#traffic-control-and-circuit-breaker) 113 | > Code branch: circuit-breaker 114 | 115 | Implement traffic control and circuit breaker patterns: 116 | - Request rate limiting 117 | - Circuit breaker implementation 118 | - Fallback mechanisms 119 | - Health monitoring and automatic recovery 120 | 121 | --- 122 | 123 | ## Key Features Implemented 124 | 125 | ### Service Registration and Discovery 126 | - Lightweight registry server (Tutu) 127 | - Service registration/deregistration APIs 128 | - Service instance discovery 129 | - Health check mechanisms 130 | 131 | ### Load Balancing 132 | - Client-side load balancing 133 | - Multiple balancing algorithms 134 | - Integration with service discovery 135 | - Failover support 136 | 137 | ### Service Communication 138 | - Declarative HTTP clients (Feign) 139 | - Automatic marshalling/unmarshalling 140 | - Service-to-service authentication 141 | - Request tracing and logging 142 | 143 | ### Resilience Patterns 144 | - Circuit breaker pattern 145 | - Bulkhead pattern 146 | - Timeout handling 147 | - Retry mechanisms 148 | 149 | ### API Management 150 | - Centralized routing 151 | - Request/response transformation 152 | - Rate limiting and throttling 153 | - Security filters 154 | 155 | --- 156 | 157 | *This changelog documents the evolution of mini-spring-cloud from basic service registration to a comprehensive microservices framework, demonstrating core Spring Cloud concepts through practical implementation.* -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # [前言](#前言) 2 | 3 | 写作本项目的目的之一是降低阅读原始Spring Cloud源码的难度。希望掌握本项目讲解的内容之后再阅读原始Spring Cloud的源码能起到事半功倍的效果,所以本项目的功能实现逻辑及原理和官方保持一致但追求代码最大精简化,**本项目可以理解为一个源码导读的项目**。 4 | 5 | 前置知识。阅读Spring、Spring Boot、Spring Cloud三者的源码必须严格按照 Spring -> Spring Boot -> Spring Cloud 的顺序进行,阅读Spring Cloud源码的必备前置知识: 6 | 7 | - Spring,推荐本人写的简化版的Spring框架 [**mini-spring**](https://github.com/DerekYRC/mini-spring/blob/main/README.md) 。熟悉Spring源码,阅读Spring Boot源码会非常轻松。 8 | - Spring Boot,重点掌握:1、启动流程 2、**自动装配的原理! 自动装配的原理!! 自动装配的原理!!!** 推荐文章: 9 | - [《Spring Boot精髓:启动流程源码分析》](https://www.cnblogs.com/java-chen-hao/p/11829344.html) 10 | - [《Spring Boot自动装配原理,这一篇就够了!》](https://mp.weixin.qq.com/s/f6oED1hbiWat_0HOwxgfnA) 11 | - Spring Cloud,先学会使用再研究源码,切勿本末倒置。推荐[《精尽Spring Cloud学习指南》](http://svip.iocoder.cn/Spring-Cloud/tutorials/) 。 12 | 13 | 关于Spring Cloud。Spring Cloud是构建通用模式的分布式系统的工具集,通过[**spring-cloud-commons**](https://github.com/spring-cloud/spring-cloud-commons) 定义了统一的抽象API,相当于定义了一套协议标准,具体的实现需要符合这套协议标准。Spring Cloud官方整合Eureka、Ribbon、Hystrix等第三方组件开发了Spring Cloud Netflix,阿里巴巴结合自身的Nacos、Sentinel等组件开发了Spring Cloud Alibaba。本项目基于spring-cloud-commons的协议标准自主开发或整合第三方组件提供具体的实现。 14 | 15 | 技术能力有限且文采欠佳,大家可以在此[**issue**](https://github.com/DerekYRC/mini-spring-cloud/issues/1) 留言提问和发表建议,也欢迎Pull Request完善此项目。 16 | 17 | # [服务注册](#服务注册) 18 | > 代码分支: service-registry 19 | 20 | 为了演示,写一个非常简单的单机版的服务注册和发现中心,命名图图 21 | ```java 22 | @RestController 23 | @SpringBootApplication 24 | public class TutuServerApplication { 25 | private static Logger logger = LoggerFactory.getLogger(TutuServerApplication.class); 26 | 27 | private ConcurrentHashMap> serverMap = new ConcurrentHashMap<>(); 28 | 29 | public static void main(String[] args) { 30 | SpringApplication.run(TutuServerApplication.class, args); 31 | } 32 | 33 | /** 34 | * 服务注册 35 | * 36 | * @param serviceName 37 | * @param ip 38 | * @param port 39 | * @return 40 | */ 41 | @PostMapping("register") 42 | public boolean register(@RequestParam("serviceName") String serviceName, @RequestParam("ip") String ip, @RequestParam("port") Integer port) { 43 | logger.info("register service, serviceName: {}, ip: {}, port: {}", serviceName, ip, port); 44 | serverMap.putIfAbsent(serviceName.toLowerCase(), Collections.synchronizedSet(new HashSet<>())); 45 | Server server = new Server(ip, port); 46 | serverMap.get(serviceName).add(server); 47 | return true; 48 | } 49 | 50 | /** 51 | * 服务注销 52 | * 53 | * @param serviceName 54 | * @param ip 55 | * @param port 56 | * @return 57 | */ 58 | @PostMapping("deregister") 59 | public boolean deregister(@RequestParam("serviceName") String serviceName, @RequestParam("ip") String ip, @RequestParam("port") Integer port) { 60 | logger.info("deregister service, serviceName: {}, ip: {}, port: {}", serviceName, ip, port); 61 | Set serverSet = serverMap.get(serviceName.toLowerCase()); 62 | if (serverSet != null) { 63 | Server server = new Server(ip, port); 64 | serverSet.remove(server); 65 | } 66 | return true; 67 | } 68 | 69 | /** 70 | * 根据服务名称查询服务列表 71 | * 72 | * @param serviceName 73 | * @return 74 | */ 75 | @GetMapping("list") 76 | public Set list(@RequestParam("serviceName") String serviceName) { 77 | Set serverSet = serverMap.get(serviceName.toLowerCase()); 78 | logger.info("list service, serviceName: {}, serverSet: {}", serviceName, JSON.toJSONString(serverSet)); 79 | return serverSet != null ? serverSet : Collections.emptySet(); 80 | } 81 | 82 | /** 83 | * 查询所有服务名称列表 84 | * 85 | * @return 86 | */ 87 | @GetMapping("listServiceNames") 88 | public Enumeration listServiceNames() { 89 | return serverMap.keys(); 90 | } 91 | 92 | /** 93 | * 服务 94 | */ 95 | public static class Server { 96 | private String ip; 97 | 98 | private Integer port; 99 | 100 | //Construct、Getters、equals、hashCode 101 | } 102 | } 103 | ``` 104 | 配置application.yml: 105 | ```yaml 106 | server: 107 | port: 6688 108 | ``` 109 | 110 | spring-cloud-commons服务注册相关API: 111 | ![](./assets/service-registry-api.png) 112 | - ServiceInstance和Registration,表示系统中服务的实例 113 | - ServiceRegistry,服务注册和注销接口 114 | - AbstractAutoServiceRegistration,自动注册和注销服务。监听WebServerInitializedEvent(Web服务启动完毕事件),WebServerInitializedEvent触发时注册服务实例;@PreDestroy注解修饰的方法注销服务实例。 115 | 116 | ## 服务注册功能实现 117 | 118 | TutuDiscoveryProperties,配置服务注册中心地址: 119 | ```java 120 | @ConfigurationProperties("spring.cloud.tutu.discovery") 121 | public class TutuDiscoveryProperties { 122 | 123 | @Autowired 124 | private InetUtils inetUtils; 125 | 126 | private String serverAddr; 127 | 128 | private String service; 129 | 130 | private String ip; 131 | 132 | private int port = -1; 133 | 134 | private boolean secure = false; 135 | 136 | @PostConstruct 137 | public void init() throws Exception { 138 | if (!StringUtils.hasLength(ip)) { 139 | //获取服务IP地址 140 | ip = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress(); 141 | } 142 | } 143 | 144 | //getters and setters 145 | } 146 | ``` 147 | TutuRegistration,图图服务注册实例: 148 | ```java 149 | public class TutuRegistration implements Registration { 150 | 151 | private TutuDiscoveryProperties tutuDiscoveryProperties; 152 | 153 | public TutuRegistration(TutuDiscoveryProperties tutuDiscoveryProperties) { 154 | this.tutuDiscoveryProperties = tutuDiscoveryProperties; 155 | } 156 | 157 | @Override 158 | public boolean isSecure() { 159 | return tutuDiscoveryProperties.isSecure(); 160 | } 161 | 162 | @Override 163 | public URI getUri() { 164 | return DefaultServiceInstance.getUri(this); 165 | } 166 | 167 | //getters and setters 168 | } 169 | ``` 170 | 注册和注销TutuRegistration的接口TutuServiceRegistry: 171 | ```java 172 | public class TutuServiceRegistry implements ServiceRegistry { 173 | private static final Logger logger = LoggerFactory.getLogger(TutuServiceRegistry.class); 174 | 175 | private TutuDiscoveryProperties tutuDiscoveryProperties; 176 | 177 | public TutuServiceRegistry(TutuDiscoveryProperties tutuDiscoveryProperties) { 178 | this.tutuDiscoveryProperties = tutuDiscoveryProperties; 179 | } 180 | 181 | /** 182 | * 注册服务实例 183 | * 184 | * @param registration 185 | */ 186 | @Override 187 | public void register(Registration registration) { 188 | Map param = new HashMap<>(); 189 | param.put("serviceName", tutuDiscoveryProperties.getService()); 190 | param.put("ip", tutuDiscoveryProperties.getIp()); 191 | param.put("port", tutuDiscoveryProperties.getPort()); 192 | 193 | String result = HttpUtil.post(tutuDiscoveryProperties.getServerAddr() + "/register", param); 194 | if (Boolean.parseBoolean(result)) { 195 | logger.info("register service successfully, serviceName: {}, ip: {}, port: {}", 196 | tutuDiscoveryProperties.getService(), tutuDiscoveryProperties.getIp(), tutuDiscoveryProperties.getPort()); 197 | } else { 198 | logger.error("register service failed, serviceName: {}, ip: {}, port: {}", 199 | tutuDiscoveryProperties.getService(), tutuDiscoveryProperties.getIp(), tutuDiscoveryProperties.getPort()); 200 | throw new RuntimeException("register service failed, serviceName"); 201 | } 202 | } 203 | 204 | /** 205 | * 注销服务实例 206 | * 207 | * @param registration 208 | */ 209 | @Override 210 | public void deregister(Registration registration) { 211 | Map param = new HashMap<>(); 212 | param.put("serviceName", tutuDiscoveryProperties.getService()); 213 | param.put("ip", tutuDiscoveryProperties.getIp()); 214 | param.put("port", tutuDiscoveryProperties.getPort()); 215 | 216 | String result = HttpUtil.post(tutuDiscoveryProperties.getServerAddr() + "/deregister", param); 217 | if (Boolean.parseBoolean(result)) { 218 | logger.info("de-register service successfully, serviceName: {}, ip: {}, port: {}", 219 | tutuDiscoveryProperties.getService(), tutuDiscoveryProperties.getIp(), tutuDiscoveryProperties.getPort()); 220 | } else { 221 | logger.warn("de-register service failed, serviceName: {}, ip: {}, port: {}", 222 | tutuDiscoveryProperties.getService(), tutuDiscoveryProperties.getIp(), tutuDiscoveryProperties.getPort()); 223 | } 224 | } 225 | } 226 | ``` 227 | AbstractAutoServiceRegistration实现类: 228 | ```java 229 | public class TutuAutoServiceRegistration extends AbstractAutoServiceRegistration { 230 | 231 | private TutuRegistration tutuRegistration; 232 | 233 | protected TutuAutoServiceRegistration(ServiceRegistry serviceRegistry, TutuRegistration tutuRegistration) { 234 | super(serviceRegistry, null); 235 | this.tutuRegistration = tutuRegistration; 236 | } 237 | 238 | @Override 239 | protected Registration getRegistration() { 240 | if (tutuRegistration.getPort() < 0) { 241 | //设置服务端口 242 | tutuRegistration.setPort(this.getPort().get()); 243 | } 244 | return tutuRegistration; 245 | } 246 | } 247 | ``` 248 | 自动装配: 249 | TutuServiceRegistryAutoConfiguration: 250 | ```java 251 | /** 252 | * 自动配置服务注册相关类 253 | */ 254 | @Configuration 255 | @ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true) 256 | public class TutuServiceRegistryAutoConfiguration { 257 | 258 | @Bean 259 | @ConditionalOnMissingBean 260 | public TutuDiscoveryProperties tutuProperties() { 261 | return new TutuDiscoveryProperties(); 262 | } 263 | 264 | @Bean 265 | public TutuRegistration tutuRegistration(TutuDiscoveryProperties tutuDiscoveryProperties) { 266 | return new TutuRegistration(tutuDiscoveryProperties); 267 | } 268 | 269 | @Bean 270 | public TutuServiceRegistry tutuServiceRegistry(TutuDiscoveryProperties tutuDiscoveryProperties) { 271 | return new TutuServiceRegistry(tutuDiscoveryProperties); 272 | } 273 | 274 | @Bean 275 | public TutuAutoServiceRegistration tutuAutoServiceRegistration(ServiceRegistry serviceRegistry, TutuRegistration tutuRegistration) { 276 | return new TutuAutoServiceRegistration(serviceRegistry, tutuRegistration); 277 | } 278 | } 279 | ``` 280 | META-INF/spring.factories: 281 | ```yaml 282 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 283 | com.github.cloud.tutu.registry.TutuServiceRegistryAutoConfiguration 284 | ``` 285 | 286 | ## 测试: 287 | 288 | 1、maven install 289 | 290 | ![](./assets/service-registry-maven.png) 291 | 292 | 2、启动服务注册和发现中心TutuServerApplication 293 | 294 | 3、启动服务提供者ProviderApplication,其代码如下: 295 | ```java 296 | @RestController 297 | @SpringBootApplication 298 | public class ProviderApplication { 299 | 300 | @Value("${server.port}") 301 | private Integer port; 302 | 303 | public static void main(String[] args) { 304 | SpringApplication.run(ProviderApplication.class, args); 305 | } 306 | 307 | @PostMapping("/echo") 308 | public String echo() { 309 | return "Port of the service provider: " + port; 310 | } 311 | } 312 | ``` 313 | 配置application.yml: 314 | ```yaml 315 | spring: 316 | application: 317 | name: provider-application 318 | cloud: 319 | tutu: 320 | discovery: 321 | server-addr: localhost:6688 322 | service: ${spring.application.name} 323 | 324 | # 随机端口 325 | server: 326 | port: ${random.int[10000,20000]} 327 | ``` 328 | 329 | 4、浏览器中访问http://localhost:6688/list?serviceName=provider-application 或执行命令 ```curl -X GET 'http://localhost:6688/list?serviceName=provider-application'``` ,响应报文如下,说明服务已经注册到服务注册中心 330 | ```json 331 | [ 332 | { 333 | "ip": "192.168.47.1", 334 | "port": 19588 335 | } 336 | ] 337 | ``` 338 | 339 | # [服务发现](#服务发现) 340 | > 代码分支: service-discovery 341 | 342 | spring-cloud-commons定义的服务发现接口```org.springframework.cloud.client.discovery.DiscoveryClient```: 343 | ```java 344 | public interface DiscoveryClient extends Ordered { 345 | 346 | /** 347 | * Gets all ServiceInstances associated with a particular serviceId. 348 | * @param serviceId The serviceId to query. 349 | * @return A List of ServiceInstance. 350 | */ 351 | List getInstances(String serviceId); 352 | 353 | /** 354 | * @return All known service IDs. 355 | */ 356 | List getServices(); 357 | } 358 | ``` 359 | 360 | 仅需实现DiscoveryClient接口即可,实现类: 361 | ```java 362 | /** 363 | * 服务发现实现类 364 | */ 365 | public class TutuDiscoveryClient implements DiscoveryClient { 366 | private static final Logger logger = LoggerFactory.getLogger(TutuDiscoveryClient.class); 367 | 368 | private TutuDiscoveryProperties tutuDiscoveryProperties; 369 | 370 | public TutuDiscoveryClient(TutuDiscoveryProperties tutuDiscoveryProperties) { 371 | this.tutuDiscoveryProperties = tutuDiscoveryProperties; 372 | } 373 | 374 | @Override 375 | public List getInstances(String serviceId) { 376 | Map param = new HashMap<>(); 377 | param.put("serviceName", serviceId); 378 | 379 | String response = HttpUtil.get(tutuDiscoveryProperties.getServerAddr() + "/list", param); 380 | logger.info("query service instance, serviceId: {}, response: {}", serviceId, response); 381 | return JSON.parseArray(response).stream().map(hostInfo -> { 382 | TutuServiceInstance serviceInstance = new TutuServiceInstance(); 383 | serviceInstance.setServiceId(serviceId); 384 | String ip = ((JSONObject) hostInfo).getString("ip"); 385 | Integer port = ((JSONObject) hostInfo).getInteger("port"); 386 | serviceInstance.setHost(ip); 387 | serviceInstance.setPort(port); 388 | return serviceInstance; 389 | }).collect(Collectors.toList()); 390 | } 391 | 392 | @Override 393 | public List getServices() { 394 | String response = HttpUtil.post(tutuDiscoveryProperties.getServerAddr() + "/listServiceNames", new HashMap<>()); 395 | logger.info("query service instance list, response: {}", response); 396 | return JSON.parseArray(response, String.class); 397 | } 398 | } 399 | ``` 400 | 401 | 自动装配TutuDiscoveryAutoConfiguration: 402 | ```java 403 | @Configuration 404 | public class TutuDiscoveryAutoConfiguration { 405 | 406 | @Bean 407 | @ConditionalOnMissingBean 408 | public TutuDiscoveryProperties tutuDiscoveryProperties() { 409 | return new TutuDiscoveryProperties(); 410 | } 411 | 412 | @Bean 413 | public DiscoveryClient tutuDiscoveryClient(TutuDiscoveryProperties tutuDiscoveryProperties) { 414 | return new TutuDiscoveryClient(tutuDiscoveryProperties); 415 | } 416 | } 417 | ``` 418 | spring.factories: 419 | ```yaml 420 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 421 | com.github.cloud.tutu.registry.TutuServiceRegistryAutoConfiguration,\ 422 | com.github.cloud.tutu.discovery.TutuDiscoveryAutoConfiguration 423 | ``` 424 | 425 | ## 测试: 426 | 427 | 1、maven install,启动服务注册和发现中心TutuServerApplication,启动服务提供者ProviderApplication,启动服务消费者ConsumerApplication(后续测试步骤均同此,不再提及) 428 | 429 | 服务消费者代码如下: 430 | ```java 431 | @SpringBootApplication 432 | public class ConsumerApplication { 433 | 434 | public static void main(String[] args) { 435 | SpringApplication.run(ConsumerApplication.class, args); 436 | } 437 | 438 | @RestController 439 | static class HelloController { 440 | 441 | @Autowired 442 | private DiscoveryClient discoveryClient; 443 | 444 | private RestTemplate restTemplate = new RestTemplate(); 445 | 446 | @GetMapping("/hello") 447 | public String hello() { 448 | List serviceInstances = discoveryClient.getInstances("provider-application"); 449 | if (serviceInstances.size() > 0) { 450 | ServiceInstance serviceInstance = serviceInstances.get(0); 451 | URI uri = serviceInstance.getUri(); 452 | String response = restTemplate.postForObject(uri.toString() + "/echo", null, String.class); 453 | return response; 454 | } 455 | 456 | throw new RuntimeException("No service instance for provider-application found"); 457 | } 458 | } 459 | } 460 | ``` 461 | application.yml: 462 | ```yaml 463 | spring: 464 | application: 465 | name: consumer-application 466 | cloud: 467 | tutu: 468 | discovery: 469 | server-addr: localhost:6688 470 | service: ${spring.application.name} 471 | ``` 472 | 473 | 2、访问http://localhost:8080/hello ,相应报文如下: 474 | ```yaml 475 | Port of the service provider: 19922 476 | ``` 477 | 478 | # [集成ribbon实现客户端负载均衡](#集成ribbon实现客户端负载均衡) 479 | > 代码分支: load-balancer 480 | 481 | ## 关于ribbon 482 | 483 | (翻译自官方文档)ribbon是一个提供如下功能的依赖包: 484 | - 负载均衡 485 | - 容错机制 486 | - 支持多种协议(HTTP, TCP, UDP),支持异步和响应式的调用方式 487 | - 缓存和批处理 488 | 489 | #### ribbon核心API 490 | 491 | 一、IClientConfig接口 492 | 493 | ![](./assets/load-balancer-IClientConfig.png) 494 | 495 | 定义加载和读取ribbon客户端配置的方法,实现类DefaultClientConfigImpl 496 | 497 | 二、IPing接口 498 | 499 | ![](./assets/load-balancer-IPing.png) 500 | 501 | 顾名思义,判断服务是否存活,实现类: 502 | 503 | - NoOpPing,不做检查,认为服务存活 504 | - DummyPing,不做检查,认为服务存活 505 | 506 | 三、ServerList接口 507 | 508 | ![](./assets/load-balancer-ServerList.png) 509 | 510 | 获取服务实例列表的接口,实现类: 511 | 512 | - ConfigurationBasedServerList,基于配置获取服务实例列表 513 | 514 | 四、IRule接口 515 | 516 | ![](./assets/load-balancer-IRule.png) 517 | 518 | 负载均衡规则,实现类: 519 | 520 | - RoundRobinRule,轮询 521 | - RandomRule,随机 522 | - WeightedResponseTimeRule,根据响应时间分配权重,响应时间越短权重越大 523 | - BestAvailableRule,跳过被熔断器标记为"tripped"状态的、并且选择并发请求数最小的服务实例 524 | - ZoneAvoidanceRule,根据所属zone和可用性筛选服务实例,在没有多zone的情况下退化为轮询RoundRobinRule 525 | - AvailabilityFilteringRule,过滤掉一直连接失败或活跃连接数超过配置值的服务实例 526 | - RetryRule,对其他负载均衡规则的包装,在一段时间内失败重试 527 | 528 | 五、ServerListFilter接口 529 | 530 | ![](./assets/load-balancer-ServerListFilter.png) 531 | 532 | 服务实例过滤器 533 | 534 | 六、ServerListUpdater接口 535 | 536 | ![](./assets/load-balancer-ServerListUpdater.png) 537 | 538 | PollingServerListUpdater,起一个周期任务更新服务实例列表 539 | 540 | 七、ILoadBalancer接口 541 | 542 | ![](./assets/load-balancer-ILoadBalancer.png) 543 | 544 | 负载均衡接口,实现类: 545 | 546 | - BaseLoadBalancer,手动设置服务实例,根据负载均衡规则IRule筛选服务实例 547 | - DynamicServerListLoadBalancer,使用ServerListUpdater动态更新服务实例列表 548 | - ZoneAwareLoadBalancer,支持zone 549 | 550 | ## 集成ribbon实现客户端负载均衡(一) 551 | 552 | spring-cloud-commons负载均衡相关API: 553 | 554 | - ServiceInstanceChooser接口,服务实例选择器,根据服务提供者的服务名称选择服务实例 555 | 556 | ```java 557 | /** 558 | * Implemented by classes which use a load balancer to choose a server to send a request to. 559 | */ 560 | public interface ServiceInstanceChooser { 561 | 562 | /** 563 | * Chooses a ServiceInstance from the LoadBalancer for the specified service. 564 | */ 565 | ServiceInstance choose(String serviceId); 566 | 567 | /** 568 | * Chooses a ServiceInstance from the LoadBalancer for the specified service and LoadBalancer request. 569 | */ 570 | ServiceInstance choose(String serviceId, Request request); 571 | } 572 | ``` 573 | 574 | - LoadBalancerClient接口 575 | 576 | ```java 577 | /** 578 | * Represents a client-side load balancer. 579 | */ 580 | public interface LoadBalancerClient extends ServiceInstanceChooser { 581 | 582 | /** 583 | * Executes request using a ServiceInstance from the LoadBalancer for the specified service. 584 | */ 585 | T execute(String serviceId, LoadBalancerRequest request) throws IOException; 586 | 587 | /** 588 | * Executes request using a ServiceInstance from the LoadBalancer for the specified service. 589 | */ 590 | T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest request) throws IOException; 591 | 592 | /** 593 | * Creates a proper URI with a real host and port for systems to utilize. Some systems 594 | * use a URI with the logical service name as the host, such as 595 | * http://myservice/path/to/service. This will replace the service name with the 596 | * host:port from the ServiceInstance. 597 | */ 598 | URI reconstructURI(ServiceInstance instance, URI original); 599 | } 600 | ``` 601 | 602 | 本节只关注ServiceInstanceChooser接口的choose方法,下一节讲解LoadBalancerClient接口的三个方法。 603 | 604 | 负载均衡功能实现: 605 | 606 | RibbonClientConfiguration,配置ribbon核心API默认实现类: 607 | 608 | ```java 609 | /** 610 | * 配置ribbon默认组件 611 | */ 612 | @Configuration 613 | public class RibbonClientConfiguration { 614 | 615 | @Value("${ribbon.client.name}") 616 | private String name; 617 | 618 | @Bean 619 | @ConditionalOnMissingBean 620 | public IClientConfig ribbonClientConfig() { 621 | DefaultClientConfigImpl config = new DefaultClientConfigImpl(); 622 | config.loadProperties(name); 623 | return config; 624 | } 625 | 626 | @Bean 627 | @ConditionalOnMissingBean 628 | public IRule ribbonRule(IClientConfig config) { 629 | ZoneAvoidanceRule rule = new ZoneAvoidanceRule(); 630 | rule.initWithNiwsConfig(config); 631 | return rule; 632 | } 633 | 634 | @Bean 635 | @ConditionalOnMissingBean 636 | public IPing ribbonPing(IClientConfig config) { 637 | return new DummyPing(); 638 | } 639 | 640 | @Bean 641 | @ConditionalOnMissingBean 642 | public ServerList ribbonServerList(IClientConfig config) { 643 | ConfigurationBasedServerList serverList = new ConfigurationBasedServerList(); 644 | serverList.initWithNiwsConfig(config); 645 | return serverList; 646 | } 647 | 648 | @Bean 649 | @ConditionalOnMissingBean 650 | public ServerListUpdater ribbonServerListUpdater(IClientConfig config) { 651 | return new PollingServerListUpdater(config); 652 | } 653 | 654 | @Bean 655 | @ConditionalOnMissingBean 656 | public ServerListFilter ribbonServerListFilter(IClientConfig config) { 657 | ServerListSubsetFilter filter = new ServerListSubsetFilter(); 658 | filter.initWithNiwsConfig(config); 659 | return filter; 660 | } 661 | 662 | @Bean 663 | @ConditionalOnMissingBean 664 | public ILoadBalancer ribbonLoadBalancer(IClientConfig config, 665 | ServerList serverList, ServerListFilter serverListFilter, 666 | IRule rule, IPing ping, ServerListUpdater serverListUpdater) { 667 | return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList, 668 | serverListFilter, serverListUpdater); 669 | } 670 | } 671 | ``` 672 | 673 | 只需实现ribbon核心API中的获取服务实例列表接口ServerList,实现类TutuServerList: 674 | 675 | ```java 676 | /** 677 | * 查询图图服务实例列表 678 | */ 679 | public class TutuServerList extends AbstractServerList { 680 | private static Logger logger = LoggerFactory.getLogger(TutuServerList.class); 681 | 682 | private TutuDiscoveryProperties discoveryProperties; 683 | 684 | private String serviceId; 685 | 686 | public TutuServerList(TutuDiscoveryProperties discoveryProperties) { 687 | this.discoveryProperties = discoveryProperties; 688 | } 689 | 690 | /** 691 | * 查询服务实例列表 692 | * 693 | * @return 694 | */ 695 | @Override 696 | public List getInitialListOfServers() { 697 | return getServer(); 698 | } 699 | 700 | /** 701 | * 查询服务实例列表 702 | * 703 | * @return 704 | */ 705 | @Override 706 | public List getUpdatedListOfServers() { 707 | return getServer(); 708 | } 709 | 710 | private List getServer() { 711 | Map param = new HashMap<>(); 712 | param.put("serviceName", serviceId); 713 | 714 | String response = HttpUtil.get(discoveryProperties.getServerAddr() + "/list", param); 715 | logger.info("query service instance, serviceId: {}, response: {}", serviceId, response); 716 | return JSON.parseArray(response).stream().map(hostInfo -> { 717 | String ip = ((JSONObject) hostInfo).getString("ip"); 718 | Integer port = ((JSONObject) hostInfo).getInteger("port"); 719 | return new TutuServer(ip, port); 720 | }).collect(Collectors.toList()); 721 | } 722 | 723 | public String getServiceId() { 724 | return serviceId; 725 | } 726 | 727 | @Override 728 | public void initWithNiwsConfig(IClientConfig iClientConfig) { 729 | this.serviceId = iClientConfig.getClientName(); 730 | } 731 | } 732 | ``` 733 | 734 | 配置TutuServerList,替换RibbonClientConfiguration中配置的默认实现: 735 | 736 | ```java 737 | @Configuration 738 | @RibbonClients(defaultConfiguration = TutuRibbonClientConfiguration.class) 739 | public class RibbonTutuAutoConfiguration { 740 | } 741 | ``` 742 | 743 | ```java 744 | /** 745 | * 自定义ribbon组件 746 | */ 747 | @Configuration 748 | public class TutuRibbonClientConfiguration { 749 | 750 | @Bean 751 | @ConditionalOnMissingBean 752 | public ServerList ribbonServerList(IClientConfig config, 753 | TutuDiscoveryProperties discoveryProperties) { 754 | TutuServerList serverList = new TutuServerList(discoveryProperties); 755 | serverList.initWithNiwsConfig(config); 756 | return serverList; 757 | } 758 | } 759 | ``` 760 | 761 | 每一个Provider服务集群(应用名称即spring.application.name相同的所有应用服务提供者)对应一套ribbon核心API。**SpringClientFactory继承自NamedContextFactory,为每一套ribbon核心API创建一个子spring应用上下文(ApplicationContext)**,来隔离不同服务的ribbon核心API配置,可以定制化不同服务的负载均衡规则(扩展篇实现)。 762 | 763 | SpringClientFactory的构造函数参数**RibbonClientConfiguration**配置ribbon默认的核心API。 764 | 765 | 修饰RibbonTutuAutoConfiguration配置类的注解RibbonClients引入了**自动配置类RibbonClientConfigurationRegistrar**,将RibbonClients注解指定的defaultConfiguration属性的值即TutuRibbonClientConfiguration配置类**包装为RibbonClientSpecification**。**RibbonClientSpecification作为SpringClientFactory的属性,用来覆盖RibbonClientConfiguration配置类指定的默认的核心API**,比如TutuRibbonClientConfiguration配置类使用TutuServerList替换RibbonClientConfiguration配置类中指定的ConfigurationBasedServerList。 766 | 767 | 可能表述得不清楚,为了充分理解子spring容器的创建逻辑,可以在下面的测试环节debug如下几个方法: 768 | 769 | - RibbonClientConfigurationRegistrar#registerBeanDefinitions 770 | - RibbonAutoConfiguration#springClientFactory 771 | - SpringClientFactory的构造函数和方法 772 | 773 | LoadBalancerClient实现类RibbonLoadBalancerClient: 774 | 775 | ```java 776 | /** 777 | * ribbon负载均衡客户端 778 | */ 779 | public class RibbonLoadBalancerClient implements LoadBalancerClient { 780 | 781 | private SpringClientFactory clientFactory; 782 | 783 | public RibbonLoadBalancerClient(SpringClientFactory clientFactory) { 784 | this.clientFactory = clientFactory; 785 | } 786 | 787 | /** 788 | * 选择服务实例 789 | */ 790 | @Override 791 | public ServiceInstance choose(String serviceId) { 792 | return choose(serviceId, null); 793 | } 794 | 795 | /** 796 | * 选择服务实例 797 | */ 798 | @Override 799 | public ServiceInstance choose(String serviceId, Request request) { 800 | ILoadBalancer loadBalancer = clientFactory.getInstance(serviceId, ILoadBalancer.class); 801 | Server server = loadBalancer.chooseServer("default"); 802 | if (server != null) { 803 | return new TutuServiceInstance(serviceId, server.getHost(), server.getPort()); 804 | } 805 | 806 | return null; 807 | } 808 | } 809 | ``` 810 | 811 | 自动装配spring.factories 812 | 813 | ```yaml 814 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 815 | com.github.cloud.loadbalancer.ribbon.config.RibbonAutoConfiguration,\ 816 | com.github.cloud.loadbalancer.ribbon.config.RibbonTutuAutoConfiguration 817 | ``` 818 | 819 | ## 测试: 820 | 821 | 1、在mini-spring-cloud-provider-example文件夹下执行命令```mvn spring-boot:run```启动多个服务提供者 822 | 823 | 服务消费者代码: 824 | 825 | ```java 826 | @SpringBootApplication 827 | public class ConsumerApplication { 828 | 829 | public static void main(String[] args) { 830 | SpringApplication.run(ConsumerApplication.class, args); 831 | } 832 | 833 | @RestController 834 | static class HelloController { 835 | 836 | private RestTemplate restTemplate = new RestTemplate(); 837 | 838 | @GetMapping("/world") 839 | public String world() { 840 | ServiceInstance serviceInstance = loadBalancerClient.choose("provider-application"); 841 | if (serviceInstance != null) { 842 | URI uri = serviceInstance.getUri(); 843 | String response = restTemplate.postForObject(uri.toString() + "/echo", null, String.class); 844 | return response; 845 | } 846 | 847 | throw new RuntimeException("No service instance for provider-application found"); 848 | } 849 | } 850 | } 851 | ``` 852 | 853 | 2、多次访问```http://localhost:8080/world```, 通过响应报文中的端口可知请求以轮询的方式分配给服务提供者(默认的负载均衡规则ZoneAvoidanceRule在没有多zone的情况下退化为轮询规则) 854 | 855 | ## 集成ribbon实现客户端负载均衡(二) 856 | 857 | 简化调用方式,达到如下的效果,使用服务提供者的名称替换IP和端口 858 | 859 | ```java 860 | restTemplate.postForObject("http://provider-application/echo", null, String.class); 861 | ``` 862 | 863 | 实现LoadBalancerClient的execute方法和reconstructURI方法: 864 | 865 | ```java 866 | public class RibbonLoadBalancerClient implements LoadBalancerClient { 867 | 868 | /** 869 | * 重建请求URI,将服务名称替换为服务实例的IP:端口 870 | */ 871 | @Override 872 | public URI reconstructURI(ServiceInstance server, URI original) { 873 | try { 874 | //将服务名称替换为服务实例的IP:端口,例如http://provider-application/echo被重建为http://192.168.100.1:8888/echo 875 | StringBuilder sb = new StringBuilder(); 876 | sb.append(original.getScheme()).append("://"); 877 | sb.append(server.getHost()); 878 | sb.append(":").append(server.getPort()); 879 | sb.append(original.getRawPath()); 880 | if (StrUtil.isNotEmpty(original.getRawQuery())) { 881 | sb.append("?").append(original.getRawQuery()); 882 | } 883 | URI newURI = new URI(sb.toString()); 884 | return newURI; 885 | } catch (URISyntaxException e) { 886 | throw new RuntimeException(e); 887 | } 888 | } 889 | 890 | /** 891 | * 处理http请求 892 | */ 893 | @Override 894 | public T execute(String serviceId, LoadBalancerRequest request) throws IOException { 895 | ServiceInstance serviceInstance = choose(serviceId); 896 | return execute(serviceId, serviceInstance, request); 897 | } 898 | 899 | /** 900 | * 处理http请求 901 | * 902 | */ 903 | @Override 904 | public T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest request) throws IOException { 905 | try { 906 | return request.apply(serviceInstance); 907 | } catch (Exception ex) { 908 | throw new RuntimeException(ex); 909 | } 910 | } 911 | } 912 | ``` 913 | 914 | - reconstructURI方法,重建请求URI,将服务名称替换为服务实例的IP:端口,例如```http://provider-application/echo``` 被重建为```http://192.168.100.1:8888/echo``` 915 | - execute方法,处理http请求 916 | 917 | 有了RibbonLoadBalancerClient的reconstructURI和execute方法,将所有http请求委托给RibbonLoadBalancerClient即可。其实spring-cloud-commons已经帮我们配置好拦截RestTemplate的http请求委托给RibbonLoadBalancerClient的拦截器LoadBalancerInterceptor,配置类(有删减)如下: 918 | 919 | ![](./assets/load-balancer-LoadBalancerAutoConfiguration.png) 920 | 921 | LoadBalancerAutoConfiguration配置类**为每一个被LoadBalanced注解修饰的RestTemplate增加LoadBalancerInterceptor拦截器。** 922 | 923 | ![](./assets/load-balancer-LoadBalancerInterceptor.png) 924 | 925 | LoadBalancerInterceptor将http请求委托给LoadBalancerClient执行,其中requestFactory.createRequest使用ServiceRequestWrapper包装原始的http请求 926 | 927 | ![](./assets/load-balancer-ServiceRequestWrapper.png) 928 | 929 | ServiceRequestWrapper调用LoadBalancerClient#reconstructURI方法重建请求URI,将服务名称替换为服务实例的IP:端口 930 | 931 | ## 测试: 932 | 933 | 服务消费者代码如下: 934 | 935 | ```java 936 | @SpringBootApplication 937 | public class ConsumerApplication { 938 | 939 | public static void main(String[] args) { 940 | SpringApplication.run(ConsumerApplication.class, args); 941 | } 942 | 943 | @Configuration 944 | static class RestTemplateConfiguration { 945 | 946 | /** 947 | * 赋予负载均衡的能力 948 | * 949 | * @return 950 | */ 951 | @LoadBalanced 952 | @Bean 953 | public RestTemplate restTemplate() { 954 | return new RestTemplate(); 955 | } 956 | } 957 | 958 | @RestController 959 | static class HelloController { 960 | 961 | @Autowired 962 | private RestTemplate loadBalancedRestTemplate; 963 | 964 | @GetMapping("/foo") 965 | public String foo() { 966 | return loadBalancedRestTemplate.postForObject("http://provider-application/echo", null, String.class); 967 | } 968 | } 969 | } 970 | ``` 971 | 972 | 访问```http://localhost:8080/foo``` 973 | 974 | # [集成Feign简化调用方式](#集成Feign简化调用方式) 975 | > 代码分支: open-feign 976 | 977 | ## 关于feign 978 | [Open Feign](https://github.com/OpenFeign/feign) 是一个简化http调用方式的Java客户端。使用示例: 979 | 980 | ```java 981 | interface HelloService { 982 | 983 | @RequestLine("GET /hello") 984 | String hello(); 985 | } 986 | 987 | @Test 988 | public void testOpenFeign() { 989 | HelloService helloService = Feign.builder() 990 | .target(HelloService.class, "http://localhost:8080"); 991 | String response = helloService.hello(); 992 | } 993 | ``` 994 | 995 | Spring Cloud基于Open Feign开发了[Spring Cloud OpenFeign](https://github.com/spring-cloud/spring-cloud-openfeign) ,得以支持Spring Mvc的注解(通过实现了feign的Contract接口的实现类SpringMvcContract),使用示例: 996 | 997 | ```java 998 | interface WorldService { 999 | 1000 | @GetMapping("/world") 1001 | String world(); 1002 | } 1003 | 1004 | @Test 1005 | public void testSpringCloudOpenFeign() { 1006 | WorldService worldService = Feign.builder() 1007 | .contract(new SpringMvcContract()) 1008 | .target(WorldService.class, "http://localhost:8080"); 1009 | String response = worldService.world(); 1010 | } 1011 | ``` 1012 | 1013 | 可以dubug上面两个示例,代码放在测试类FeignTest中,重点关注Contract接口对注解的解析 1014 | 1015 | #### Open Feign工作流程 1016 | 1017 | ![](./assets/feign工作流程.png) 1018 | 1019 | #### Open Feign核心API 1020 | 1021 | 一、Contract接口 1022 | 1023 | 负责解析Feign客户端接口的类注解、方法注解和参数。 1024 | 1025 | 实现类```feign.Contract.Default```支持Open Feign的注解,比如上面第一个示例中的RequestLine注解。 1026 | 1027 | Spring Cloud OpenFeign开发的实现类```SpringMvcContract```支持Spring MVC的注解,如GetMapping、PostMapping、RequestMapping。 1028 | 1029 | 二、Encoder接口 1030 | 1031 | 编码器,将请求对象编码为请求体 1032 | 1033 | 三、Decoder接口 1034 | 1035 | 解码器,将响应体解码为对象 1036 | 1037 | 四、RequestInterceptor拦截器接口 1038 | 1039 | 对请求进行拦截处理 1040 | 1041 | 五、Client接口 1042 | 1043 | 提交http请求的接口 1044 | 1045 | ## 功能实现 1046 | 1047 | **@EnableFeignClients注解**开启集成Feign客户端,该注解Import配置类FeignClientsRegistrar: 1048 | 1049 | ```java 1050 | /** 1051 | * 启用Feign 1052 | */ 1053 | @Retention(RetentionPolicy.RUNTIME) 1054 | @Target(ElementType.TYPE) 1055 | @Documented 1056 | @Import(FeignClientsRegistrar.class) 1057 | public @interface EnableFeignClients { 1058 | } 1059 | ``` 1060 | 1061 | 配置类FeignClientsRegistrar扫描每个被FeignClient注解修饰的接口,基于JDK动态代理生成对象,注册到bean容器: 1062 | 1063 | ```java 1064 | /** 1065 | * 往bean容器中注册Feign客户端 1066 | */ 1067 | public class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar { 1068 | 1069 | /** 1070 | * 往bean容器中注册Feign客户端 1071 | */ 1072 | @Override 1073 | public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { 1074 | //为FeignClient注解修饰的接口生成代理bean即Feign客户端,并注册到bean容器 1075 | String packageName = ClassUtils.getPackageName(importingClassMetadata.getClassName()); 1076 | //扫描所有被FeignClient注解修饰的接口 1077 | Set> classes = ClassUtil.scanPackageByAnnotation(packageName, FeignClient.class); 1078 | for (Class clazz : classes) { 1079 | GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); 1080 | //使用FeignClientFactoryBean生成Feign客户端 1081 | beanDefinition.setBeanClass(FeignClientFactoryBean.class); 1082 | String clientName = clazz.getAnnotation(FeignClient.class).value(); 1083 | beanDefinition.getPropertyValues().addPropertyValue("contextId", clientName); 1084 | beanDefinition.getPropertyValues().addPropertyValue("type", clazz); 1085 | 1086 | //将Feign客户端注册进bean容器 1087 | String beanName = clazz.getName(); 1088 | registry.registerBeanDefinition(beanName, beanDefinition); 1089 | } 1090 | } 1091 | } 1092 | ``` 1093 | 1094 | 注意BeanDefinition指定的beanClass为FeignClientFactoryBean,它是FactoryBean的实现类,bean容器取其getObject方法返回值作为bean: 1095 | 1096 | ```java 1097 | /** 1098 | * 生成Feign客户端的FactoryBean 1099 | */ 1100 | public class FeignClientFactoryBean implements FactoryBean, ApplicationContextAware { 1101 | 1102 | private String contextId; 1103 | 1104 | private Class type; 1105 | 1106 | private ApplicationContext applicationContext; 1107 | 1108 | @Override 1109 | public Object getObject() throws Exception { 1110 | FeignContext feignContext = applicationContext.getBean(FeignContext.class); 1111 | Encoder encoder = feignContext.getInstance(contextId, Encoder.class); 1112 | Decoder decoder = feignContext.getInstance(contextId, Decoder.class); 1113 | Contract contract = feignContext.getInstance(contextId, Contract.class); 1114 | Client client = feignContext.getInstance(contextId, Client.class); 1115 | 1116 | return Feign.builder() 1117 | .encoder(encoder) 1118 | .decoder(decoder) 1119 | .contract(contract) 1120 | .client(client) 1121 | .target(new HardCodedTarget<>(type, contextId, "http://" + contextId)); 1122 | } 1123 | 1124 | //other methods 1125 | } 1126 | ``` 1127 | 1128 | 跟ribbon一样,每一个Provider服务集群(应用名称即spring.application.name相同的所有应用服务提供者)对应一套feign核心API。**FeignContext继承自NamedContextFactory,为每一套feign核心API创建一个子spring应用上下文(ApplicationContext)**,来隔离不同服务的feign核心API配置(扩展篇实现)。 1129 | 1130 | FeignContext: 1131 | 1132 | ```java 1133 | /** 1134 | * 为每个feign客户端创建一个应用上下文(ApplicationContext),隔离每个feign客户端的配置 1135 | */ 1136 | public class FeignContext extends NamedContextFactory { 1137 | 1138 | public FeignContext() { 1139 | super(FeignClientsConfiguration.class, "feign", "feign.client.name"); 1140 | } 1141 | } 1142 | ``` 1143 | 1144 | FeignClientsConfiguration配置类配置feign的核心API 1145 | 1146 | ```java 1147 | /** 1148 | * 配置feign的核心API 1149 | */ 1150 | @Configuration 1151 | public class FeignClientsConfiguration { 1152 | 1153 | @Bean 1154 | @ConditionalOnMissingBean 1155 | public Encoder encoder() { 1156 | return new Encoder.Default(); 1157 | } 1158 | 1159 | @Bean 1160 | @ConditionalOnMissingBean 1161 | public Decoder decoder() { 1162 | return new Decoder.Default(); 1163 | } 1164 | 1165 | @Bean 1166 | @ConditionalOnMissingBean 1167 | public Contract contract() { 1168 | return new SpringMvcContract(); 1169 | } 1170 | 1171 | @Bean 1172 | @ConditionalOnMissingBean 1173 | public Client client(LoadBalancerClient loadBalancerClient) { 1174 | return new LoadBalancerFeignClient(loadBalancerClient, new Client.Default(null, null)); 1175 | } 1176 | } 1177 | ``` 1178 | 1179 | SpringMvcContract简单实现支持Spring MVC的PostMapping注解: 1180 | 1181 | ```java 1182 | /** 1183 | * feign支持Spring MVC的注解 1184 | */ 1185 | public class SpringMvcContract extends Contract.BaseContract { 1186 | 1187 | @Override 1188 | protected void processAnnotationOnClass(MethodMetadata data, Class clz) { 1189 | //TODO 解析接口注解 1190 | } 1191 | 1192 | @Override 1193 | protected void processAnnotationOnMethod(MethodMetadata data, Annotation annotation, Method method) { 1194 | //解析方法注解 1195 | //解析PostMapping注解 1196 | if (annotation instanceof PostMapping) { 1197 | PostMapping postMapping = (PostMapping) annotation; 1198 | data.template().method(Request.HttpMethod.POST); 1199 | String path = postMapping.value()[0]; 1200 | if (!path.startsWith("/") && !data.template().path().endsWith("/")) { 1201 | path = "/" + path; 1202 | } 1203 | data.template().uri(path, true); 1204 | } 1205 | 1206 | //TODO 解析其他注解 1207 | } 1208 | 1209 | @Override 1210 | protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) { 1211 | //TODO 解析参数 1212 | return true; 1213 | } 1214 | } 1215 | ``` 1216 | 1217 | LoadBalancerFeignClient组合ribbon的客户端负载均衡能力选择服务示例,然后发送http请求: 1218 | 1219 | ```java 1220 | /** 1221 | * 具备负载均衡能力的feign client 1222 | */ 1223 | public class LoadBalancerFeignClient implements Client { 1224 | 1225 | private LoadBalancerClient loadBalancerClient; 1226 | 1227 | private Client delegate; 1228 | 1229 | public LoadBalancerFeignClient(LoadBalancerClient loadBalancerClient, Client delegate) { 1230 | this.loadBalancerClient = loadBalancerClient; 1231 | this.delegate = delegate; 1232 | } 1233 | 1234 | @SuppressWarnings("deprecation") 1235 | @Override 1236 | public Response execute(Request request, Request.Options options) throws IOException { 1237 | try { 1238 | //客户端负载均衡 1239 | URI original = URI.create(request.url()); 1240 | String serviceId = original.getHost(); 1241 | //选择服务实例 1242 | ServiceInstance serviceInstance = loadBalancerClient.choose(serviceId); 1243 | //重建请求URI 1244 | URI uri = loadBalancerClient.reconstructURI(serviceInstance, original); 1245 | 1246 | Request newRequest = Request.create(request.httpMethod(), uri.toASCIIString(), new HashMap<>(), 1247 | request.body(), StandardCharsets.UTF_8); 1248 | return delegate.execute(newRequest, options); 1249 | } catch (IOException e) { 1250 | throw new RuntimeException(e); 1251 | } 1252 | } 1253 | } 1254 | ``` 1255 | 1256 | 自动装配: 1257 | 1258 | ```java 1259 | @Configuration 1260 | public class FeignAutoConfiguration { 1261 | 1262 | @Bean 1263 | public FeignContext feignContext() { 1264 | return new FeignContext(); 1265 | } 1266 | } 1267 | ``` 1268 | 1269 | spring.factories: 1270 | 1271 | ```yaml 1272 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 1273 | com.github.cloud.openfeign.FeignAutoConfiguration 1274 | ``` 1275 | 1276 | ## 测试: 1277 | 1278 | 消费者代码,使用@EnableFeignClients注解启用Feign: 1279 | 1280 | ```java 1281 | @EnableFeignClients 1282 | @SpringBootApplication 1283 | public class ConsumerApplication { 1284 | 1285 | public static void main(String[] args) { 1286 | SpringApplication.run(ConsumerApplication.class, args); 1287 | } 1288 | 1289 | @RestController 1290 | static class HelloController { 1291 | 1292 | @Autowired 1293 | private EchoService echoService; 1294 | 1295 | @GetMapping("/bar") 1296 | public String bar() { 1297 | return echoService.echo(); 1298 | } 1299 | } 1300 | } 1301 | ``` 1302 | 1303 | Feign客户端: 1304 | 1305 | ```java 1306 | @FeignClient("provider-application") 1307 | public interface EchoService { 1308 | 1309 | @PostMapping("echo") 1310 | String echo(); 1311 | } 1312 | ``` 1313 | 1314 | 访问```http://localhost:8080/bar``` 1315 | 1316 | # [API网关](#API网关) 1317 | > 代码分支: api-gateway-netflix-zuul 1318 | 1319 | ## 关于Netflix Zuul 1320 | 1321 | Netflix Zuul是一个提供动态路由、监控、弹性容量、安全等功能的基于第七层网络协议的应用程序网关。 1322 | 1323 | #### Zuul核心框架和执行流程 1324 | 1325 | ![](./assets/zuul-framework.png) 1326 | 1327 | **ZuulServlet负责拦截http请求,然后将http请求交给由ZuulFilter组成的过滤器链处理,ZuulFilter加载模块负责加载ZuulFilter。** 1328 | 1329 | **可见ZuulFilter过滤器是zuul框架中的核心**,API网关的鉴权、限流、权限、熔断、协议转换、错误码统一、缓存、日志、监控、告警等等功能可以实现ZuulFilter过滤器来实现。 1330 | 1331 | #### ZuulFilter过滤器类型及执行顺序 1332 | 1333 | ZuulFilter过滤器分为四种类型: 1334 | 1335 | - pre类型:调用远程服务之前执行 1336 | - route:路由、发起远程调用 1337 | - post:向客户端输出响应报文 1338 | - error:处理过滤器链执行过程中出现的错误 1339 | 1340 | ZuulServlet.service方法: 1341 | 1342 | ![](./assets/zuul-servlet.png) 1343 | 1344 | 从ZuulServlet.service方法中能看出四种类型的过滤器的执行顺序如下图所示: 1345 | 1346 | ![](./assets/zuul-filter.png) 1347 | 1348 | ## 功能实现 1349 | 1350 | EnableZuulProxy注解启用API网关功能 1351 | 1352 | ```java 1353 | @Target(ElementType.TYPE) 1354 | @Retention(RetentionPolicy.RUNTIME) 1355 | @Import(ZuulServerAutoConfiguration.class) 1356 | public @interface EnableZuulProxy { 1357 | } 1358 | ``` 1359 | 1360 | EnableZuulProxy注解引入配置类ZuulServerAutoConfiguration,该配置类配置了ZuulServlet、过滤器加载模块的FilterRegistry、实现的三个ZuulFilter以及PreDecorationFilter过滤器需要使用的路由定位器RouteLocator。 1361 | 1362 | ```java 1363 | 1364 | @Configuration 1365 | @EnableConfigurationProperties({ZuulProperties.class}) 1366 | public class ZuulServerAutoConfiguration { 1367 | 1368 | @Autowired 1369 | protected ZuulProperties zuulProperties; 1370 | 1371 | /** 1372 | * 注册ZuulServlet,用于拦截处理http请求 1373 | */ 1374 | @Bean 1375 | public ServletRegistrationBean zuulServlet() { 1376 | return new ServletRegistrationBean<>(new ZuulServlet(), zuulProperties.getServletPath()); 1377 | } 1378 | 1379 | /** 1380 | * 路由定位器 1381 | */ 1382 | @Bean 1383 | public RouteLocator simpleRouteLocator() { 1384 | return new SimpleRouteLocator(zuulProperties); 1385 | } 1386 | 1387 | /** 1388 | * pre类型过滤器,根据RouteLocator来进行路由规则的匹配 1389 | */ 1390 | @Bean 1391 | public ZuulFilter preDecorationFilter(RouteLocator routeLocator) { 1392 | return new PreDecorationFilter(routeLocator); 1393 | } 1394 | 1395 | /** 1396 | * route类型过滤器,使用ribbon负载均衡器进行http请求 1397 | */ 1398 | @Bean 1399 | ZuulFilter ribbonRoutingFilter(LoadBalancerClient loadBalancerClient) { 1400 | return new RibbonRoutingFilter(loadBalancerClient); 1401 | } 1402 | 1403 | /** 1404 | * post类型过滤器,向客户端输出响应报文 1405 | */ 1406 | @Bean 1407 | ZuulFilter sendResponseFilter() { 1408 | return new SendResponseFilter(); 1409 | } 1410 | 1411 | /** 1412 | * 注册过滤器 1413 | */ 1414 | @Bean 1415 | public FilterRegistry filterRegistry(Map filterMap) { 1416 | FilterRegistry filterRegistry = FilterRegistry.instance(); 1417 | filterMap.forEach((name, filter) -> { 1418 | filterRegistry.put(name, filter); 1419 | }); 1420 | return filterRegistry; 1421 | } 1422 | } 1423 | ``` 1424 | 1425 | 只针对正常流程实现了以下三个过滤器,想了解更多过滤器可以参考这篇文章: [**Spring Cloud 源码分析(四)Zuul:核心过滤器**](https://blog.didispace.com/spring-cloud-source-zuul/) 1426 | 1427 | - **pre类型过滤器PreDecorationFilter**,使用路由定位器RouteLocator根据请求路径匹配路由,将路由信息放进请求上下文RequestContext中 1428 | 1429 | ```java 1430 | /** 1431 | * pre类型过滤器,根据RouteLocator来进行路由规则的匹配 1432 | */ 1433 | public class PreDecorationFilter extends ZuulFilter { 1434 | private static Logger logger = LoggerFactory.getLogger(PreDecorationFilter.class); 1435 | 1436 | private RouteLocator routeLocator; 1437 | 1438 | public PreDecorationFilter(RouteLocator routeLocator) { 1439 | this.routeLocator = routeLocator; 1440 | } 1441 | 1442 | @Override 1443 | public String filterType() { 1444 | return PRE_TYPE; 1445 | } 1446 | 1447 | @Override 1448 | public int filterOrder() { 1449 | return 5; 1450 | } 1451 | 1452 | @Override 1453 | public boolean shouldFilter() { 1454 | return true; 1455 | } 1456 | 1457 | @Override 1458 | public Object run() throws ZuulException { 1459 | RequestContext requestContext = RequestContext.getCurrentContext(); 1460 | String requestURI = requestContext.getRequest().getRequestURI(); 1461 | //获取匹配的路由 1462 | Route route = routeLocator.getMatchingRoute(requestURI); 1463 | if (route != null) { 1464 | requestContext.put(REQUEST_URI_KEY, route.getPath()); 1465 | requestContext.set(SERVICE_ID_KEY, route.getLocation()); 1466 | } else { 1467 | logger.error("获取不到匹配的路由, requestURI: {}", requestContext); 1468 | } 1469 | 1470 | return null; 1471 | } 1472 | } 1473 | ``` 1474 | 1475 | 路由定位器: 1476 | 1477 | ```java 1478 | /** 1479 | * 路由定位器 1480 | */ 1481 | public interface RouteLocator { 1482 | 1483 | /** 1484 | * 获取匹配的路由 1485 | * 1486 | * @param path 1487 | * @return 1488 | */ 1489 | Route getMatchingRoute(String path); 1490 | } 1491 | ``` 1492 | 1493 | ```java 1494 | /** 1495 | * 路由定位器实现类 1496 | */ 1497 | public class SimpleRouteLocator implements RouteLocator { 1498 | 1499 | private ZuulProperties zuulProperties; 1500 | 1501 | private PathMatcher pathMatcher = new AntPathMatcher(); 1502 | 1503 | public SimpleRouteLocator(ZuulProperties zuulProperties) { 1504 | this.zuulProperties = zuulProperties; 1505 | } 1506 | 1507 | @Override 1508 | public Route getMatchingRoute(String path) { 1509 | for (Map.Entry entry : zuulProperties.getRoutes().entrySet()) { 1510 | ZuulProperties.ZuulRoute zuulRoute = entry.getValue(); 1511 | String pattern = zuulRoute.getPath(); 1512 | if (pathMatcher.match(pattern, path)) { 1513 | String targetPath = path.substring(pattern.indexOf("*") - 1); 1514 | return new Route(targetPath, zuulRoute.getServiceId()); 1515 | } 1516 | } 1517 | 1518 | return null; 1519 | } 1520 | } 1521 | ``` 1522 | 1523 | - **route类型过滤器RibbonRoutingFilter**,根据PreDecorationFilter过滤器匹配的路由信息发起远程调用,将调用结果放进请求上下文RequestContext 1524 | 1525 | ```java 1526 | /** 1527 | * route类型过滤器,使用ribbon负载均衡器进行http请求 1528 | */ 1529 | public class RibbonRoutingFilter extends ZuulFilter { 1530 | private static Logger logger = LoggerFactory.getLogger(RibbonRoutingFilter.class); 1531 | 1532 | private LoadBalancerClient loadBalancerClient; 1533 | 1534 | public RibbonRoutingFilter(LoadBalancerClient loadBalancerClient) { 1535 | this.loadBalancerClient = loadBalancerClient; 1536 | } 1537 | 1538 | @Override 1539 | public String filterType() { 1540 | return ROUTE_TYPE; 1541 | } 1542 | 1543 | @Override 1544 | public int filterOrder() { 1545 | return 10; 1546 | } 1547 | 1548 | @Override 1549 | public boolean shouldFilter() { 1550 | RequestContext requestContext = RequestContext.getCurrentContext(); 1551 | return requestContext.get(SERVICE_ID_KEY) != null; 1552 | } 1553 | 1554 | @Override 1555 | public Object run() throws ZuulException { 1556 | try { 1557 | RequestContext requestContext = RequestContext.getCurrentContext(); 1558 | //使用ribbon的负载均衡能力发起远程调用 1559 | //TODO 简单实现,熔断降级章节再完善 1560 | String serviceId = (String) requestContext.get(SERVICE_ID_KEY); 1561 | ServiceInstance serviceInstance = loadBalancerClient.choose(serviceId); 1562 | if (serviceInstance == null) { 1563 | logger.error("根据serviceId查询不到服务示例,serviceId: {}", serviceId); 1564 | return null; 1565 | } 1566 | 1567 | String requestURI = (String) requestContext.get(REQUEST_URI_KEY); 1568 | String url = serviceInstance.getUri().toString() + requestURI; 1569 | HttpRequest httpRequest = HttpUtil.createRequest(Method.POST, url); 1570 | HttpResponse httpResponse = httpRequest.execute(); 1571 | 1572 | //将响应报文的状态码和内容写进请求上下文中 1573 | requestContext.setResponseStatusCode(httpResponse.getStatus()); 1574 | requestContext.setResponseDataStream(httpResponse.bodyStream()); 1575 | 1576 | return httpResponse; 1577 | } catch (Exception e) { 1578 | rethrowRuntimeException(e); 1579 | } 1580 | return null; 1581 | } 1582 | } 1583 | ``` 1584 | 1585 | - **post类型过滤器SendResponseFilter**,将RibbonRoutingFilter过滤器发起远程调用的结果作为响应报文输出给客户端 1586 | 1587 | ```java 1588 | /** 1589 | * post类型过滤器,向客户端输出响应报文 1590 | */ 1591 | public class SendResponseFilter extends ZuulFilter { 1592 | private static Logger logger = LoggerFactory.getLogger(SendResponseFilter.class); 1593 | 1594 | @Override 1595 | public String filterType() { 1596 | return POST_TYPE; 1597 | } 1598 | 1599 | @Override 1600 | public int filterOrder() { 1601 | return 1000; 1602 | } 1603 | 1604 | @Override 1605 | public boolean shouldFilter() { 1606 | return RequestContext.getCurrentContext() 1607 | .getResponseDataStream() != null; 1608 | } 1609 | 1610 | @Override 1611 | public Object run() throws ZuulException { 1612 | //向客户端输出响应报文 1613 | RequestContext requestContext = RequestContext.getCurrentContext(); 1614 | InputStream inputStream = requestContext.getResponseDataStream(); 1615 | try { 1616 | HttpServletResponse servletResponse = requestContext.getResponse(); 1617 | servletResponse.setCharacterEncoding("UTF-8"); 1618 | 1619 | OutputStream outStream = servletResponse.getOutputStream(); 1620 | StreamUtils.copy(inputStream, outStream); 1621 | } catch (Exception e) { 1622 | rethrowRuntimeException(e); 1623 | } finally { 1624 | //关闭输入输出流 1625 | if (inputStream != null) { 1626 | try { 1627 | inputStream.close(); 1628 | } catch (Exception e) { 1629 | logger.error("关闭输入流失败", e); 1630 | } 1631 | } 1632 | 1633 | //Servlet容器会自动关闭输出流 1634 | } 1635 | return null; 1636 | } 1637 | } 1638 | ``` 1639 | 1640 | ## 测试: 1641 | 1642 | 启动API网关ApiGatewayApplication 1643 | 1644 | API网关代码: 1645 | 1646 | ```java 1647 | 1648 | @EnableZuulProxy 1649 | @SpringBootApplication 1650 | public class ApiGatewayApplication { 1651 | 1652 | public static void main(String[] args) { 1653 | SpringApplication.run(ApiGatewayApplication.class, args); 1654 | } 1655 | } 1656 | ``` 1657 | 1658 | 配置application.yml: 1659 | ```yaml 1660 | spring: 1661 | application: 1662 | name: api-gateway-application 1663 | cloud: 1664 | tutu: 1665 | discovery: 1666 | server-addr: localhost:6688 1667 | service: ${spring.application.name} 1668 | 1669 | server: 1670 | port: 8888 1671 | 1672 | zuul: 1673 | servlet-path: /* 1674 | routes: 1675 | route_provider_application: 1676 | path: /provider-application/** 1677 | service-id: provider-application 1678 | ``` 1679 | 1680 | 访问```http://localhost:8888/provider-application/echo``` 1681 | 1682 | # [流量控制和熔断降级](#流量控制和熔断降级) 1683 | 1684 | TODO 待研究完Sentinel再写本章节,估计得隔一段时间~~~ 1685 | 1686 | 更新 1687 | 1688 | Sentinel源码不难,感兴趣的小伙伴阅读sentinel-core模块即可,推荐几篇文章: 1689 | 1690 | - Sentinel入门查看官方文档 [《Sentinel官方文档》](https://sentinelguard.io/zh-cn/docs/introduction.html) ,如果想阅读源码需要重点理解《基本原理》和《Sentinel核心类解析》章节: 1691 | - [《Sentinel基本原理》](https://sentinelguard.io/zh-cn/docs/basic-implementation.html) 1692 | - [《Sentinel核心类解析》](https://github.com/alibaba/Sentinel/wiki/Sentinel-%E6%A0%B8%E5%BF%83%E7%B1%BB%E8%A7%A3%E6%9E%90) 1693 | - 下面四篇文章来自于github用户all4you的项目[sentinel-tutorial](https://github.com/all4you/sentinel-tutorial) ,作者写得非常用心和精彩,给作者点个star吧 1694 | - [《Sentinel原理:全解析》](https://github.com/all4you/sentinel-tutorial/blob/master/sentinel-principle/sentinel-overall-introduce/sentinel-overall-introduce.md) 1695 | - [《Sentinel原理:重要概念》](https://github.com/all4you/sentinel-tutorial/blob/master/sentinel-principle/sentinel-concept-of-entities/sentinel-concept-of-entities.md) 1696 | - [《Sentinel原理:调用链》](https://github.com/all4you/sentinel-tutorial/blob/master/sentinel-principle/sentinel-slot-chain/sentinel-slot-chain.md) 1697 | - [《Sentinel原理:滑动窗口》](https://github.com/all4you/sentinel-tutorial/blob/master/sentinel-principle/sentinel-slide-window/sentinel-slide-window.md) 1698 | - [《Sentinel原理:扩展点》](https://github.com/all4you/sentinel-tutorial/blob/master/sentinel-principle/sentinel-extension-points/sentinel-extension-points.md) 1699 | 1700 | Sentinel整合到Spring Cloud的原理也很简单,具体查看Spring Web拦截器```SentinelWebInterceptor``` 和```SentinelWebTotalInterceptor```,还有支持```SentinelResource```注解的```SentinelResourceAspect```切面类,我就不整合到该项目了。 1701 | 1702 | 非常感谢阅读到此的小伙伴,希望小伙伴们在此[**issue**](https://github.com/DerekYRC/mini-spring-cloud/issues/1) 留言发表评论,也可以写下希望我写的下一个项目,Netty/RocketMQ等等。 1703 | 1704 | 撒花~~~ 1705 | --------------------------------------------------------------------------------